Compare commits
24 Commits
928fa7191b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a9eaf0114f | |||
| f63172c4cf | |||
| ff111ef278 | |||
| bf5470ac66 | |||
| c1e3b2881d | |||
| 4024740b82 | |||
| fb4cf1b035 | |||
| d2daf74fca | |||
| e52e818686 | |||
| df6cf94dae | |||
| 7dcb8af1bb | |||
| 6be9bf5aff | |||
| 7c37e9adb3 | |||
| 0780b4c17d | |||
| a08dc505d9 | |||
| c82726b691 | |||
| c14c0d843d | |||
| 0fd0e74b67 | |||
| 1636784931 | |||
| c30ea2d679 | |||
| 769391640b | |||
| ecf375205f | |||
| f2f8a03a32 | |||
| 630893f047 |
+15
-17
@@ -1,25 +1,23 @@
|
|||||||
---
|
---
|
||||||
active: true
|
active: true
|
||||||
iteration: 36
|
iteration: 1
|
||||||
max_iterations: 0
|
max_iterations: 20
|
||||||
completion_promise: "The morning-report skill is fully implemented, tested, and registered"
|
completion_promise: "Guardrail hooks are fully implemented, tested, and registered"
|
||||||
started_at: "2026-01-03T08:16:44Z"
|
started_at: "2026-01-07T18:52:10Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Build the morning-report skill following the design at ~/.claude/docs/plans/2025-01-02-morning-report-design.md
|
Implement guardrail hooks following the design at ~/.claude/docs/plans/2025-01-06-guardrail-hooks-design.md
|
||||||
|
|
||||||
Implementation order:
|
Implementation order:
|
||||||
1. Create skill skeleton: ~/.claude/skills/morning-report/ with SKILL.md and config.json
|
1. Create state/guardrails.json with starter rules config
|
||||||
2. Build collectors: weather.py, stocks.py, infra.py (easy wins first)
|
2. Create hooks/scripts/guardrail.py main logic
|
||||||
3. Build gtasks.py collector (Google Tasks API - add OAuth scope)
|
3. Create hooks/scripts/guardrail-confirm.py confirm helper
|
||||||
4. Build news.py collector (RSS feeds)
|
4. Modify hooks/hooks.json to add PreToolUse registration
|
||||||
5. Build generate.py orchestrator and render.py templating
|
5. Modify hooks/scripts/session-end.sh to clear session allowlist
|
||||||
6. Create systemd timer and /morning command
|
6. Create logs/ directory
|
||||||
7. Test end-to-end and verify output
|
7. Test: block scenario (catastrophic command pattern)
|
||||||
|
8. Test: confirm scenario (operation outside safe paths)
|
||||||
Use appropriate LLM tiers:
|
9. Test: allow scenario (operation in safe path)
|
||||||
- Haiku: weather, stocks, infra formatting
|
10. Test: git-aware detection
|
||||||
- Sonnet: email triage, news summarization
|
|
||||||
- None: calendar, tasks (structured data)
|
|
||||||
|
|
||||||
Register in component-registry.json when complete.
|
Register in component-registry.json when complete.
|
||||||
|
|||||||
@@ -49,3 +49,6 @@ repos/homelab
|
|||||||
# RAG search data (generated vector stores and caches)
|
# RAG search data (generated vector stores and caches)
|
||||||
data/
|
data/
|
||||||
skills/rag-search/venv/
|
skills/rag-search/venv/
|
||||||
|
|
||||||
|
# Telemetry (analytics tracking)
|
||||||
|
telemetry/
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ See `agents/README.md` for details on agent files and execution.
|
|||||||
├── commands/ # Slash command definitions
|
├── commands/ # Slash command definitions
|
||||||
├── workflows/ # Workflow definitions (design docs)
|
├── workflows/ # Workflow definitions (design docs)
|
||||||
│ └── README.md
|
│ └── README.md
|
||||||
|
├── plans/ # Implementation plans
|
||||||
|
│ └── index.json # Plan status registry
|
||||||
├── state/ # Shared state files (JSON)
|
├── state/ # Shared state files (JSON)
|
||||||
│ ├── sysadmin/
|
│ ├── sysadmin/
|
||||||
│ ├── programmer/
|
│ ├── programmer/
|
||||||
@@ -67,6 +69,7 @@ All agents MUST read and follow the processes defined in these files:
|
|||||||
| `state/personal-assistant-preferences.json` | PA persistent config | personal-assistant |
|
| `state/personal-assistant-preferences.json` | PA persistent config | personal-assistant |
|
||||||
| `state/personal-assistant/general-instructions.json` | User memory | personal-assistant |
|
| `state/personal-assistant/general-instructions.json` | User memory | personal-assistant |
|
||||||
| `state/kb.json` | Shared knowledge base | personal-assistant |
|
| `state/kb.json` | Shared knowledge base | personal-assistant |
|
||||||
|
| `plans/index.json` | Plan status registry | any agent |
|
||||||
|
|
||||||
## Key Processes
|
## Key Processes
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ curl -s -X PATCH \
|
|||||||
| **Skills** | SKILL.md + scripts/ + references/ | `skills/` |
|
| **Skills** | SKILL.md + scripts/ + references/ | `skills/` |
|
||||||
| **Commands** | Markdown + YAML frontmatter | `commands/` |
|
| **Commands** | Markdown + YAML frontmatter | `commands/` |
|
||||||
| **Workflows** | YAML (design docs, not auto-executed) | `workflows/` |
|
| **Workflows** | YAML (design docs, not auto-executed) | `workflows/` |
|
||||||
|
| **Plans** | Markdown + index.json | `plans/` |
|
||||||
| **State** | JSON | `state/` |
|
| **State** | JSON | `state/` |
|
||||||
| **Hooks** | JSON | `hooks/` |
|
| **Hooks** | JSON | `hooks/` |
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ Slash commands for quick actions. User-invoked (type `/command` to trigger).
|
|||||||
| `/programmer` | | Code development tasks |
|
| `/programmer` | | Code development tasks |
|
||||||
| `/gcal` | `/calendar`, `/cal` | Google Calendar access |
|
| `/gcal` | `/calendar`, `/cal` | Google Calendar access |
|
||||||
| `/usage` | `/stats` | View usage statistics |
|
| `/usage` | `/stats` | View usage statistics |
|
||||||
|
| `/external` | `/llm`, `/ext` | Toggle and use external LLM mode |
|
||||||
|
|
||||||
### Kubernetes (`/k8s:*`)
|
### Kubernetes (`/k8s:*`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: external
|
||||||
|
description: Toggle and use external LLM mode (GPT-5.2, Gemini, etc.)
|
||||||
|
aliases: [llm, ext, external-llm]
|
||||||
|
---
|
||||||
|
|
||||||
|
# External LLM Mode
|
||||||
|
|
||||||
|
Route requests to external LLMs via opencode or gemini CLI.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/external # Show current status
|
||||||
|
/external on [reason] # Enable external mode
|
||||||
|
/external off # Disable external mode
|
||||||
|
/external invoke <prompt> # Send prompt to default model
|
||||||
|
/external invoke --model <model> <prompt> # Send to specific model
|
||||||
|
/external invoke --task <task> <prompt> # Route by task type
|
||||||
|
/external models # List available models
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Status
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/toggle.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toggle On/Off
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/toggle.py on --reason "reason"
|
||||||
|
~/.claude/mcp/llm-router/toggle.py off
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invoke
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --model MODEL -p "prompt" [--json]
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --task TASK -p "prompt" [--json]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models by Tier
|
||||||
|
|
||||||
|
### Frontier (strongest)
|
||||||
|
| Model | Provider | Best For |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| `github-copilot/gpt-5.2` | opencode | reasoning, fallback |
|
||||||
|
| `github-copilot/gemini-3-pro-preview` | opencode | long context, reasoning |
|
||||||
|
| `gemini/gemini-2.5-pro` | gemini | long context, reasoning |
|
||||||
|
|
||||||
|
### Mid-tier (general purpose)
|
||||||
|
| Model | Provider | Best For |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| `github-copilot/claude-sonnet-4.5` | opencode | general, fallback |
|
||||||
|
| `github-copilot/gemini-3-flash-preview` | opencode | fast |
|
||||||
|
| `zai-coding-plan/glm-4.7` | opencode | code generation |
|
||||||
|
| `opencode/big-pickle` | opencode | general |
|
||||||
|
| `gemini/gemini-2.5-flash` | gemini | fast |
|
||||||
|
|
||||||
|
### Lightweight (simple tasks)
|
||||||
|
| Model | Provider | Best For |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| `github-copilot/claude-haiku-4.5` | opencode | simple tasks |
|
||||||
|
|
||||||
|
## Task Routing
|
||||||
|
|
||||||
|
| Task | Routes To | Tier |
|
||||||
|
|------|-----------|------|
|
||||||
|
| `reasoning` | github-copilot/gpt-5.2 | frontier |
|
||||||
|
| `code-generation` | github-copilot/gemini-3-pro-preview | frontier |
|
||||||
|
| `long-context` | gemini/gemini-2.5-pro | frontier |
|
||||||
|
| `fast` | github-copilot/gemini-3-flash-preview | mid-tier |
|
||||||
|
| `general` (default) | github-copilot/claude-sonnet-4.5 | mid-tier |
|
||||||
|
|
||||||
|
## State Files
|
||||||
|
|
||||||
|
- Mode state: `~/.claude/state/external-mode.json`
|
||||||
|
- Model policy: `~/.claude/state/model-policy.json`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
/external on testing # Enable for testing
|
||||||
|
/external invoke "Explain k8s pods" # Use default model (mid-tier)
|
||||||
|
/external invoke --model github-copilot/gpt-5.2 "Complex analysis" # frontier
|
||||||
|
/external invoke --task code-generation "Write a Python function" # routes to frontier
|
||||||
|
/external invoke --task fast "Quick question" # routes to mid-tier
|
||||||
|
/external off # Back to Claude
|
||||||
|
```
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# Guardrail Hooks Design
|
||||||
|
|
||||||
|
**Date:** 2025-01-06
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PreToolUse guardrail hooks that prevent dangerous operations by intercepting Bash, Write, and Edit tool calls before execution.
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Prevent catastrophic operations (destructive file ops, dangerous system commands, infrastructure mistakes)
|
||||||
|
- Contextual response: hard block for severe threats, confirmation for moderate risks
|
||||||
|
- Path-aware: operations inside projects are more permissive than outside
|
||||||
|
- Auditable: log all interventions for review
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- Git command guardrails (future consideration)
|
||||||
|
- Rate limiting or resource protection
|
||||||
|
- Workflow compliance enforcement
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.claude/
|
||||||
|
├── hooks/
|
||||||
|
│ └── hooks.json # Hook registration (add PreToolUse)
|
||||||
|
├── hooks/scripts/
|
||||||
|
│ ├── guardrail.py # Main PreToolUse logic
|
||||||
|
│ └── guardrail-confirm.py # Adds operation to session allowlist
|
||||||
|
├── state/
|
||||||
|
│ ├── guardrails.json # Rules configuration
|
||||||
|
│ └── guardrail-session.json # Session allowlist (cleared on session end)
|
||||||
|
└── logs/
|
||||||
|
└── guardrail.jsonl # Audit log (blocked/confirmed only)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
|
||||||
|
1. Claude invokes Bash/Write/Edit
|
||||||
|
2. `guardrail.py` runs via PreToolUse hook
|
||||||
|
3. Check session allowlist - if present, allow
|
||||||
|
4. Evaluate rules + path context - decide action
|
||||||
|
5. Allow: return `{"decision": "allow"}`
|
||||||
|
6. Block/Confirm: log, return `{"decision": "block", "reason": "..."}`
|
||||||
|
7. For confirm: user approves, Claude calls `guardrail-confirm.py`, retries
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Location:** `~/.claude/state/guardrails.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"safe_paths": [
|
||||||
|
"~/.claude",
|
||||||
|
"~/projects"
|
||||||
|
],
|
||||||
|
"blocked_paths": [
|
||||||
|
"/etc", "/usr", "/var", "/boot", "/sys", "/proc",
|
||||||
|
"~/.ssh", "~/.gnupg", "~/.aws"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"bash": [
|
||||||
|
{"pattern": "rm -rf /", "action": "block"},
|
||||||
|
{"pattern": "rm -rf ~", "action": "block"},
|
||||||
|
{"pattern": "rm -rf \\*", "action": "block"},
|
||||||
|
{"pattern": "chmod -R 777", "action": "block"},
|
||||||
|
{"pattern": ":(){ :|:& };:", "action": "block"},
|
||||||
|
{"pattern": "mkfs\\.", "action": "block"},
|
||||||
|
{"pattern": "dd if=.* of=/dev/", "action": "block"},
|
||||||
|
{"pattern": "> /dev/sda", "action": "block"},
|
||||||
|
{"pattern": "shutdown", "action": "confirm"},
|
||||||
|
{"pattern": "reboot", "action": "confirm"},
|
||||||
|
{"pattern": "systemctl (stop|disable|mask)", "action": "confirm"},
|
||||||
|
{"pattern": "rm ", "action": "confirm", "outside_safe_paths": true},
|
||||||
|
{"pattern": "kubectl delete", "action": "confirm"},
|
||||||
|
{"pattern": "docker rm", "action": "confirm"}
|
||||||
|
],
|
||||||
|
"write": [
|
||||||
|
{"path_patterns": ["blocked_paths"], "action": "block"},
|
||||||
|
{"path_patterns": ["outside_safe_paths"], "action": "confirm"}
|
||||||
|
],
|
||||||
|
"edit": [
|
||||||
|
{"path_patterns": ["blocked_paths"], "action": "block"},
|
||||||
|
{"path_patterns": ["outside_safe_paths"], "action": "confirm"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
- `pattern`: regex matched against command/path
|
||||||
|
- `action`: `block` (hard stop) or `confirm` (require approval)
|
||||||
|
- `outside_safe_paths`: rule only applies when target is outside safe directories
|
||||||
|
- Bash rules check command string; Write/Edit rules check file path
|
||||||
|
|
||||||
|
## Safe Paths Logic
|
||||||
|
|
||||||
|
**Evaluation order (first match wins):**
|
||||||
|
|
||||||
|
1. **Blocked paths check** - Is target in `blocked_paths`?
|
||||||
|
- Yes: apply block/confirm regardless of other factors
|
||||||
|
- Protects system-critical locations absolutely
|
||||||
|
|
||||||
|
2. **Explicit allowlist** - Is target under a `safe_paths` entry?
|
||||||
|
- Yes: path is safe
|
||||||
|
- Supports glob patterns (`~/projects/*`)
|
||||||
|
|
||||||
|
3. **Git-aware detection** - Is target inside a git repository?
|
||||||
|
- Walk up from target path looking for `.git` directory
|
||||||
|
- Found: treat as safe (it's an intentional project)
|
||||||
|
|
||||||
|
4. **Default** - Path is outside safe areas
|
||||||
|
- Operations here trigger `outside_safe_paths` rules
|
||||||
|
|
||||||
|
### Path Normalization
|
||||||
|
|
||||||
|
- Expand `~` to actual home directory
|
||||||
|
- Resolve symlinks to real paths
|
||||||
|
- Handle relative paths by resolving against CWD
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
| Target Path | Result | Reason |
|
||||||
|
|-------------|--------|--------|
|
||||||
|
| `/etc/hosts` | blocked/confirm | In `blocked_paths` |
|
||||||
|
| `~/.ssh/config` | blocked/confirm | In `blocked_paths` |
|
||||||
|
| `~/.claude/hooks/test.py` | safe | In `safe_paths` |
|
||||||
|
| `~/projects/myapp/src/main.py` | safe | In `safe_paths` |
|
||||||
|
| `~/random-repo/file.txt` | safe | Git repo detected |
|
||||||
|
| `~/Downloads/file.txt` | outside | No match, triggers rules |
|
||||||
|
|
||||||
|
**Edge case:** If a git repo exists under a blocked path (unlikely), blocked path wins.
|
||||||
|
|
||||||
|
## Hook Implementation
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
|
||||||
|
Add to `~/.claude/hooks/hooks.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash|Write|Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/hooks/scripts/guardrail.py",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Input (stdin)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool_name": "Bash",
|
||||||
|
"tool_input": {
|
||||||
|
"command": "rm -rf ~/Downloads/old-project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Output (stdout)
|
||||||
|
|
||||||
|
Allow:
|
||||||
|
```json
|
||||||
|
{"decision": "allow"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Block:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"decision": "block",
|
||||||
|
"reason": "GUARDRAIL BLOCKED: 'rm' outside safe paths.\nTarget: ~/Downloads/old-project\nRule: rm_outside_safe\n\nTo proceed, confirm with user then run:\npython ~/.claude/hooks/scripts/guardrail-confirm.py 'rm -rf ~/Downloads/old-project'"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Confirmation Flow
|
||||||
|
|
||||||
|
1. **Guardrail blocks** with detailed message including confirm command
|
||||||
|
2. **Claude reports** to user, asks for confirmation
|
||||||
|
3. **User confirms** in conversation
|
||||||
|
4. **Claude runs confirm script:**
|
||||||
|
```bash
|
||||||
|
python ~/.claude/hooks/scripts/guardrail-confirm.py "rm -rf ~/Downloads/old-project"
|
||||||
|
```
|
||||||
|
5. **Confirm script** adds to session allowlist
|
||||||
|
6. **Claude retries** the original command
|
||||||
|
7. **Guardrail checks allowlist** - finds match - allows
|
||||||
|
|
||||||
|
### Session Allowlist
|
||||||
|
|
||||||
|
**Location:** `~/.claude/state/guardrail-session.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"confirmed": [
|
||||||
|
{"tool": "Bash", "operation": "rm -rf ~/Downloads/old-project", "ts": "2025-01-06T10:24:00"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cleanup:** `session-end.sh` clears this file so confirmations don't persist across sessions.
|
||||||
|
|
||||||
|
**Matching:** Exact match on tool + operation string.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
**Location:** `~/.claude/logs/guardrail.jsonl`
|
||||||
|
|
||||||
|
**Format:** JSON Lines (append-only)
|
||||||
|
|
||||||
|
**Logged events:**
|
||||||
|
- Every `block` action
|
||||||
|
- Every `confirm` action (required approval)
|
||||||
|
- When confirmed operation is allowed
|
||||||
|
|
||||||
|
### Log Entry Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ts": "2025-01-06T10:23:45", "tool": "Bash", "operation": "rm -rf /", "action": "block", "rule": "rm_rf_root", "path_context": "n/a"}
|
||||||
|
{"ts": "2025-01-06T10:24:01", "tool": "Bash", "operation": "rm ~/Downloads/old", "action": "confirm_required", "rule": "rm_outside_safe", "path_context": "outside"}
|
||||||
|
{"ts": "2025-01-06T10:24:30", "tool": "Bash", "operation": "rm ~/Downloads/old", "action": "confirmed_allow", "rule": "session_allowlist"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- `ts`: ISO 8601 timestamp
|
||||||
|
- `tool`: Bash, Write, or Edit
|
||||||
|
- `operation`: Command or file path
|
||||||
|
- `action`: `block`, `confirm_required`, or `confirmed_allow`
|
||||||
|
- `rule`: Which rule triggered
|
||||||
|
- `path_context`: `safe`, `outside`, `blocked`, or `n/a`
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Create `state/guardrails.json` with starter rules
|
||||||
|
- [ ] Create `hooks/scripts/guardrail.py` main logic
|
||||||
|
- [ ] Create `hooks/scripts/guardrail-confirm.py` confirm helper
|
||||||
|
- [ ] Modify `hooks/hooks.json` to add PreToolUse registration
|
||||||
|
- [ ] Modify `hooks/scripts/session-end.sh` to clear session allowlist
|
||||||
|
- [ ] Create `logs/` directory
|
||||||
|
- [ ] Test: block scenario (catastrophic command)
|
||||||
|
- [ ] Test: confirm scenario (rm outside safe paths)
|
||||||
|
- [ ] Test: allow scenario (operation in safe path)
|
||||||
|
- [ ] Test: git-aware detection
|
||||||
|
|
||||||
|
## Starter Rules
|
||||||
|
|
||||||
|
| Category | Pattern | Action |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| Catastrophic | `rm -rf /`, `rm -rf ~`, `mkfs.*`, `dd.*of=/dev/` | block |
|
||||||
|
| Fork bomb | `:(){ :\|:& };:` | block |
|
||||||
|
| Dangerous chmod | `chmod -R 777` | block |
|
||||||
|
| System commands | `shutdown`, `reboot` | confirm |
|
||||||
|
| Service control | `systemctl (stop\|disable\|mask)` | confirm |
|
||||||
|
| Destructive outside safe | `rm ` (outside safe paths) | confirm |
|
||||||
|
| K8s destructive | `kubectl delete` | confirm |
|
||||||
|
| Docker destructive | `docker rm`, `docker system prune` | confirm |
|
||||||
|
| Write to blocked paths | Any Write/Edit to `/etc`, `~/.ssh`, etc. | block |
|
||||||
|
| Write outside safe | Any Write/Edit outside safe paths | confirm |
|
||||||
@@ -41,6 +41,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash|Write|Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/hooks/scripts/guardrail.py",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Guardrail Confirm Helper
|
||||||
|
|
||||||
|
Adds an operation to the session allowlist so it can proceed on retry.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 guardrail-confirm.py "<tool>" "<operation>"
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python3 guardrail-confirm.py "Bash" "rm -rf ~/Downloads/old-project"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HOME = Path.home()
|
||||||
|
STATE_DIR = HOME / ".claude" / "state"
|
||||||
|
SESSION_FILE = STATE_DIR / "guardrail-session.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_session():
|
||||||
|
"""Load current session allowlist."""
|
||||||
|
if not SESSION_FILE.exists():
|
||||||
|
return {"confirmed": []}
|
||||||
|
try:
|
||||||
|
with open(SESSION_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {"confirmed": []}
|
||||||
|
|
||||||
|
|
||||||
|
def save_session(data: dict):
|
||||||
|
"""Save session allowlist."""
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(SESSION_FILE, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: guardrail-confirm.py <tool> <operation>")
|
||||||
|
print("Example: guardrail-confirm.py 'Bash' 'rm ~/Downloads/old'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
tool = sys.argv[1]
|
||||||
|
operation = sys.argv[2]
|
||||||
|
|
||||||
|
# Load current session
|
||||||
|
session = load_session()
|
||||||
|
|
||||||
|
# Check if already confirmed
|
||||||
|
for item in session.get("confirmed", []):
|
||||||
|
if item.get("tool") == tool and item.get("operation") == operation:
|
||||||
|
print(f"Already confirmed: {tool} - {operation[:50]}...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Add to allowlist
|
||||||
|
session["confirmed"].append({
|
||||||
|
"tool": tool,
|
||||||
|
"operation": operation,
|
||||||
|
"ts": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
})
|
||||||
|
|
||||||
|
save_session(session)
|
||||||
|
print(f"Confirmed: {tool} - {operation[:50]}...")
|
||||||
|
print("You may now retry the operation.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+283
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Guardrail PreToolUse Hook
|
||||||
|
|
||||||
|
Intercepts Bash, Write, and Edit tool calls to prevent dangerous operations.
|
||||||
|
Returns JSON decision: {"decision": "allow"} or {"decision": "block", "reason": "..."}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
HOME = Path.home()
|
||||||
|
STATE_DIR = HOME / ".claude" / "state"
|
||||||
|
LOGS_DIR = HOME / ".claude" / "logs"
|
||||||
|
CONFIG_FILE = STATE_DIR / "guardrails.json"
|
||||||
|
SESSION_FILE = STATE_DIR / "guardrail-session.json"
|
||||||
|
LOG_FILE = LOGS_DIR / "guardrail.jsonl"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""Load guardrails configuration."""
|
||||||
|
if not CONFIG_FILE.exists():
|
||||||
|
return None
|
||||||
|
with open(CONFIG_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def load_session_allowlist():
|
||||||
|
"""Load session allowlist of confirmed operations."""
|
||||||
|
if not SESSION_FILE.exists():
|
||||||
|
return {"confirmed": []}
|
||||||
|
try:
|
||||||
|
with open(SESSION_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {"confirmed": []}
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_allowlist(tool: str, operation: str) -> bool:
|
||||||
|
"""Check if operation was previously confirmed."""
|
||||||
|
allowlist = load_session_allowlist()
|
||||||
|
for item in allowlist.get("confirmed", []):
|
||||||
|
if item.get("tool") == tool and item.get("operation") == operation:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def expand_path(path: str) -> Path:
|
||||||
|
"""Expand ~ and resolve path."""
|
||||||
|
return Path(os.path.expanduser(path)).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def is_under_path(target: Path, parent: str) -> bool:
|
||||||
|
"""Check if target is under parent path."""
|
||||||
|
try:
|
||||||
|
parent_path = expand_path(parent)
|
||||||
|
# Handle glob patterns like ~/projects/*
|
||||||
|
if "*" in parent:
|
||||||
|
# For ~/projects/*, check if under ~/projects
|
||||||
|
parent_path = expand_path(parent.replace("/*", "").replace("*", ""))
|
||||||
|
return parent_path in target.parents or target == parent_path
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_git_repo(path: Path) -> bool:
|
||||||
|
"""Check if path is inside a git repository."""
|
||||||
|
current = path if path.is_dir() else path.parent
|
||||||
|
while current != current.parent:
|
||||||
|
if (current / ".git").exists():
|
||||||
|
return True
|
||||||
|
current = current.parent
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def classify_path(target_path: str, config: dict) -> str:
|
||||||
|
"""
|
||||||
|
Classify a path as 'blocked', 'safe', or 'outside'.
|
||||||
|
|
||||||
|
Evaluation order:
|
||||||
|
1. Blocked paths -> 'blocked'
|
||||||
|
2. Safe paths -> 'safe'
|
||||||
|
3. Git repo -> 'safe'
|
||||||
|
4. Otherwise -> 'outside'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
target = expand_path(target_path)
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return "outside"
|
||||||
|
|
||||||
|
# Check blocked paths first
|
||||||
|
for blocked in config.get("blocked_paths", []):
|
||||||
|
if is_under_path(target, blocked):
|
||||||
|
return "blocked"
|
||||||
|
|
||||||
|
# Check safe paths
|
||||||
|
for safe in config.get("safe_paths", []):
|
||||||
|
if is_under_path(target, safe):
|
||||||
|
return "safe"
|
||||||
|
|
||||||
|
# Check if in git repo
|
||||||
|
if is_in_git_repo(target):
|
||||||
|
return "safe"
|
||||||
|
|
||||||
|
return "outside"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_paths_from_command(command: str) -> list[str]:
|
||||||
|
"""Extract potential file paths from a bash command."""
|
||||||
|
paths = []
|
||||||
|
# Simple heuristic: look for things that look like paths
|
||||||
|
# This catches ~/..., /..., and relative paths
|
||||||
|
tokens = command.split()
|
||||||
|
for token in tokens:
|
||||||
|
# Skip flags
|
||||||
|
if token.startswith("-"):
|
||||||
|
continue
|
||||||
|
# Skip common commands
|
||||||
|
if token in ("rm", "mv", "cp", "chmod", "chown", "mkdir", "rmdir", "touch"):
|
||||||
|
continue
|
||||||
|
# Check if it looks like a path
|
||||||
|
if "/" in token or token.startswith("~"):
|
||||||
|
paths.append(token)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def check_bash_rules(command: str, config: dict) -> tuple[str, str | None, str]:
|
||||||
|
"""
|
||||||
|
Check bash command against rules.
|
||||||
|
Returns: (action, rule_name, path_context)
|
||||||
|
action: 'allow', 'block', or 'confirm'
|
||||||
|
"""
|
||||||
|
rules = config.get("rules", {}).get("bash", [])
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
pattern = rule.get("pattern", "")
|
||||||
|
action = rule.get("action", "allow")
|
||||||
|
name = rule.get("name", "unnamed")
|
||||||
|
outside_safe_only = rule.get("outside_safe_paths", False)
|
||||||
|
|
||||||
|
# Check if pattern matches
|
||||||
|
if re.search(pattern, command):
|
||||||
|
if outside_safe_only:
|
||||||
|
# Only apply rule if operating outside safe paths
|
||||||
|
paths = extract_paths_from_command(command)
|
||||||
|
for path in paths:
|
||||||
|
path_class = classify_path(path, config)
|
||||||
|
if path_class in ("blocked", "outside"):
|
||||||
|
return (action, name, path_class)
|
||||||
|
# All paths are safe, allow
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Rule applies regardless of path
|
||||||
|
return (action, name, "n/a")
|
||||||
|
|
||||||
|
return ("allow", None, "safe")
|
||||||
|
|
||||||
|
|
||||||
|
def check_file_rules(file_path: str, tool: str, config: dict) -> tuple[str, str | None, str]:
|
||||||
|
"""
|
||||||
|
Check Write/Edit file path against rules.
|
||||||
|
Returns: (action, rule_name, path_context)
|
||||||
|
"""
|
||||||
|
rules = config.get("rules", {}).get(tool.lower(), [])
|
||||||
|
path_class = classify_path(file_path, config)
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
path_match = rule.get("path_match", "")
|
||||||
|
action = rule.get("action", "allow")
|
||||||
|
name = rule.get("name", "unnamed")
|
||||||
|
|
||||||
|
if path_match == "blocked_paths" and path_class == "blocked":
|
||||||
|
return (action, name, path_class)
|
||||||
|
elif path_match == "outside_safe_paths" and path_class == "outside":
|
||||||
|
return (action, name, path_class)
|
||||||
|
|
||||||
|
return ("allow", None, path_class)
|
||||||
|
|
||||||
|
|
||||||
|
def log_action(tool: str, operation: str, action: str, rule: str | None, path_context: str):
|
||||||
|
"""Log guardrail action to audit log."""
|
||||||
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"ts": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||||
|
"tool": tool,
|
||||||
|
"operation": operation[:200], # Truncate long operations
|
||||||
|
"action": action,
|
||||||
|
"rule": rule or "none",
|
||||||
|
"path_context": path_context
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(LOG_FILE, "a") as f:
|
||||||
|
f.write(json.dumps(entry) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def allow():
|
||||||
|
"""Return allow decision."""
|
||||||
|
print(json.dumps({"decision": "allow"}))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def block(reason: str):
|
||||||
|
"""Return block decision with reason."""
|
||||||
|
print(json.dumps({"decision": "block", "reason": reason}))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Read input from stdin
|
||||||
|
try:
|
||||||
|
input_data = json.load(sys.stdin)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
allow() # If we can't parse input, allow (fail open)
|
||||||
|
|
||||||
|
tool_name = input_data.get("tool_name", "")
|
||||||
|
tool_input = input_data.get("tool_input", {})
|
||||||
|
|
||||||
|
# Only check Bash, Write, Edit
|
||||||
|
if tool_name not in ("Bash", "Write", "Edit"):
|
||||||
|
allow()
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
config = load_config()
|
||||||
|
if not config:
|
||||||
|
allow() # No config, allow everything
|
||||||
|
|
||||||
|
# Determine operation string for allowlist check
|
||||||
|
if tool_name == "Bash":
|
||||||
|
operation = tool_input.get("command", "")
|
||||||
|
else:
|
||||||
|
operation = tool_input.get("file_path", "")
|
||||||
|
|
||||||
|
# Check session allowlist first
|
||||||
|
if is_in_allowlist(tool_name, operation):
|
||||||
|
log_action(tool_name, operation, "confirmed_allow", "session_allowlist", "n/a")
|
||||||
|
allow()
|
||||||
|
|
||||||
|
# Check rules based on tool type
|
||||||
|
if tool_name == "Bash":
|
||||||
|
action, rule_name, path_context = check_bash_rules(operation, config)
|
||||||
|
else:
|
||||||
|
action, rule_name, path_context = check_file_rules(operation, tool_name, config)
|
||||||
|
|
||||||
|
# Take action
|
||||||
|
if action == "allow":
|
||||||
|
allow()
|
||||||
|
|
||||||
|
# Log blocked/confirm actions
|
||||||
|
log_action(tool_name, operation, action if action == "block" else "confirm_required", rule_name, path_context)
|
||||||
|
|
||||||
|
# Build block message
|
||||||
|
if action == "block":
|
||||||
|
reason = f"""GUARDRAIL BLOCKED: Operation not allowed.
|
||||||
|
Tool: {tool_name}
|
||||||
|
Operation: {operation}
|
||||||
|
Rule: {rule_name}
|
||||||
|
Path context: {path_context}
|
||||||
|
|
||||||
|
This operation is blocked by guardrail policy and cannot proceed."""
|
||||||
|
else: # confirm
|
||||||
|
confirm_cmd = f'python3 ~/.claude/hooks/scripts/guardrail-confirm.py "{tool_name}" "{operation}"'
|
||||||
|
reason = f"""GUARDRAIL: User confirmation required.
|
||||||
|
Tool: {tool_name}
|
||||||
|
Operation: {operation}
|
||||||
|
Rule: {rule_name}
|
||||||
|
Path context: {path_context}
|
||||||
|
|
||||||
|
To proceed after user confirms, run:
|
||||||
|
{confirm_cmd}
|
||||||
|
|
||||||
|
Then retry the original operation."""
|
||||||
|
|
||||||
|
block(reason)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -28,6 +28,13 @@ REASON=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin
|
|||||||
|
|
||||||
log "SessionEnd triggered: session=$SESSION_ID reason=$REASON"
|
log "SessionEnd triggered: session=$SESSION_ID reason=$REASON"
|
||||||
|
|
||||||
|
# Clear guardrail session allowlist (confirmations don't persist across sessions)
|
||||||
|
GUARDRAIL_SESSION="${HOME}/.claude/state/guardrail-session.json"
|
||||||
|
if [[ -f "$GUARDRAIL_SESSION" ]]; then
|
||||||
|
rm -f "$GUARDRAIL_SESSION"
|
||||||
|
log "Cleared guardrail session allowlist"
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
if [[ -z "$SESSION_ID" || -z "$TRANSCRIPT_PATH" ]]; then
|
if [[ -z "$SESSION_ID" || -z "$TRANSCRIPT_PATH" ]]; then
|
||||||
log "ERROR: Missing session_id or transcript_path"
|
log "ERROR: Missing session_id or transcript_path"
|
||||||
|
|||||||
@@ -32,12 +32,24 @@ with open('${PA_DIR}/memory/decisions.json') as f:
|
|||||||
" 2>/dev/null || echo "0")
|
" 2>/dev/null || echo "0")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check external mode
|
||||||
|
EXTERNAL_MODE="disabled"
|
||||||
|
if [[ -f "${STATE_DIR}/external-mode.json" ]]; then
|
||||||
|
EXTERNAL_ENABLED=$(jq -r '.enabled // false' "${STATE_DIR}/external-mode.json" 2>/dev/null || echo "false")
|
||||||
|
if [[ "${EXTERNAL_ENABLED}" == "true" ]]; then
|
||||||
|
EXTERNAL_MODE="enabled"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Output context as system reminder format
|
# Output context as system reminder format
|
||||||
echo "SessionStart:Callback hook success: Success"
|
echo "SessionStart:resume hook success: Success"
|
||||||
|
|
||||||
# Add additional context if there's something noteworthy
|
# Add additional context if there's something noteworthy
|
||||||
if [[ "${UNSUMMARIZED}" -gt 0 || "${PENDING_DECISIONS}" -gt 0 ]]; then
|
if [[ "${UNSUMMARIZED}" -gt 0 || "${PENDING_DECISIONS}" -gt 0 || "${EXTERNAL_MODE}" == "enabled" ]]; then
|
||||||
echo "SessionStart hook additional context: "
|
echo "SessionStart hook additional context: "
|
||||||
|
if [[ "${EXTERNAL_MODE}" == "enabled" ]]; then
|
||||||
|
echo "- EXTERNAL MODE ACTIVE: All requests routed to external LLMs"
|
||||||
|
fi
|
||||||
if [[ "${UNSUMMARIZED}" -gt 0 ]]; then
|
if [[ "${UNSUMMARIZED}" -gt 0 ]]; then
|
||||||
echo "- ${UNSUMMARIZED} unsummarized session(s) available for review"
|
echo "- ${UNSUMMARIZED} unsummarized session(s) available for review"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -378,6 +378,29 @@ def main():
|
|||||||
|
|
||||||
log(f"Summarization complete: {total_added} total items added")
|
log(f"Summarization complete: {total_added} total items added")
|
||||||
|
|
||||||
|
# Reindex RAG if we added items
|
||||||
|
if total_added > 0:
|
||||||
|
log("Triggering RAG reindex...")
|
||||||
|
try:
|
||||||
|
reindex_result = subprocess.run(
|
||||||
|
[
|
||||||
|
str(Path.home() / ".claude/skills/rag-search/venv/bin/python"),
|
||||||
|
str(Path.home() / ".claude/skills/rag-search/scripts/index_personal.py"),
|
||||||
|
"--quiet"
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120
|
||||||
|
)
|
||||||
|
if reindex_result.returncode == 0:
|
||||||
|
log("RAG reindex completed successfully")
|
||||||
|
else:
|
||||||
|
log(f"RAG reindex failed: {reindex_result.stderr[:200]}")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log("RAG reindex timed out after 120s")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"RAG reindex error: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent delegation helper. Routes to external or Claude based on mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
delegate.py --tier sonnet -p "prompt"
|
||||||
|
delegate.py --tier opus -p "complex reasoning task" --json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def is_external_mode() -> bool:
|
||||||
|
"""Check if external-only mode is enabled."""
|
||||||
|
mode_file = STATE_DIR / "external-mode.json"
|
||||||
|
if mode_file.exists():
|
||||||
|
with open(mode_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("enabled", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_external_model(tier: str) -> str:
|
||||||
|
"""Get the external model equivalent for a Claude tier."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
policy = json.load(f)
|
||||||
|
mapping = policy.get("claude_to_external_map", {})
|
||||||
|
if tier not in mapping:
|
||||||
|
raise ValueError(f"No external mapping for tier: {tier}")
|
||||||
|
return mapping[tier]
|
||||||
|
|
||||||
|
|
||||||
|
def delegate(tier: str, prompt: str, use_json: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Delegate to appropriate model based on mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tier: Claude tier (opus, sonnet, haiku)
|
||||||
|
prompt: The prompt text
|
||||||
|
use_json: Return JSON output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
"""
|
||||||
|
if is_external_mode():
|
||||||
|
# Use external model
|
||||||
|
model = get_external_model(tier)
|
||||||
|
invoke_script = ROUTER_DIR / "invoke.py"
|
||||||
|
|
||||||
|
cmd = [sys.executable, str(invoke_script), "--model", model, "-p", prompt]
|
||||||
|
if use_json:
|
||||||
|
cmd.append("--json")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"External invoke failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
else:
|
||||||
|
# Use Claude
|
||||||
|
cmd = ["claude", "--print", "--model", tier, prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Claude failed: {result.stderr}")
|
||||||
|
|
||||||
|
response = result.stdout.strip()
|
||||||
|
|
||||||
|
if use_json:
|
||||||
|
return json.dumps({
|
||||||
|
"model": f"claude/{tier}",
|
||||||
|
"response": response,
|
||||||
|
"success": True
|
||||||
|
}, indent=2)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Delegate to Claude or external model based on mode"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tier",
|
||||||
|
required=True,
|
||||||
|
choices=["opus", "sonnet", "haiku"],
|
||||||
|
help="Claude tier (maps to external equivalent when in external mode)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = delegate(args.tier, args.prompt, args.json)
|
||||||
|
print(result)
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"error": str(e), "success": False}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Invoke external LLM via configured provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
invoke.py --model copilot/gpt-5.2 -p "prompt"
|
||||||
|
invoke.py --task reasoning -p "prompt"
|
||||||
|
invoke.py --task code-generation -p "prompt" --json
|
||||||
|
|
||||||
|
Model selection priority:
|
||||||
|
1. Explicit --model flag
|
||||||
|
2. Task-based routing (--task flag)
|
||||||
|
3. Default from policy
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_policy() -> dict:
|
||||||
|
"""Load model policy from state file."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model(args: argparse.Namespace, policy: dict) -> str:
|
||||||
|
"""Determine which model to use based on args and policy."""
|
||||||
|
if args.model:
|
||||||
|
return args.model
|
||||||
|
if args.task and args.task in policy.get("task_routing", {}):
|
||||||
|
return policy["task_routing"][args.task]
|
||||||
|
return policy.get("task_routing", {}).get("default", "copilot/sonnet-4.5")
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(model: str, prompt: str, policy: dict, timeout: int = 600) -> str:
|
||||||
|
"""Invoke the appropriate provider for the given model."""
|
||||||
|
external_models = policy.get("external_models", {})
|
||||||
|
|
||||||
|
if model not in external_models:
|
||||||
|
raise ValueError(f"Unknown model: {model}. Available: {list(external_models.keys())}")
|
||||||
|
|
||||||
|
model_config = external_models[model]
|
||||||
|
cli = model_config["cli"]
|
||||||
|
cli_args = model_config.get("cli_args", [])
|
||||||
|
|
||||||
|
# Import and invoke appropriate provider
|
||||||
|
if cli == "opencode":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.opencode import invoke as opencode_invoke
|
||||||
|
return opencode_invoke(cli_args, prompt, timeout=timeout)
|
||||||
|
elif cli == "gemini":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.gemini import invoke as gemini_invoke
|
||||||
|
return gemini_invoke(cli_args, prompt, timeout=timeout)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown CLI: {cli}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Invoke external LLM via configured provider"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
help="Explicit model (e.g., copilot/gpt-5.2)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--task",
|
||||||
|
choices=["reasoning", "code-generation", "long-context", "general"],
|
||||||
|
help="Task type for automatic model routing"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON with model info"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=600,
|
||||||
|
help="Timeout in seconds (default: 600)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy = load_policy()
|
||||||
|
model = resolve_model(args, policy)
|
||||||
|
result = invoke(model, args.prompt, policy, timeout=args.timeout)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": model,
|
||||||
|
"response": result,
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": args.model or "unknown",
|
||||||
|
"error": str(e),
|
||||||
|
"success": False
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gemini CLI wrapper for Google models."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke gemini CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Model args like ["-m", "gemini-3-pro"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If gemini CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
"""
|
||||||
|
cmd = ["gemini"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"gemini timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"gemini failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["-m", "gemini-3-pro"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: gemini.py 'prompt'")
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OpenCode CLI wrapper for GitHub Copilot, Z.AI, and other providers."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# OpenCode binary path (linuxbrew installation)
|
||||||
|
OPENCODE_BIN = "/home/linuxbrew/.linuxbrew/bin/opencode"
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke opencode CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Model args like ["-m", "github-copilot/gpt-5.2"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If opencode CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
|
||||||
|
Example invocation:
|
||||||
|
opencode run -m github-copilot/gpt-5.2 "Hello world"
|
||||||
|
"""
|
||||||
|
# Build command: opencode run -m MODEL "prompt"
|
||||||
|
cmd = [OPENCODE_BIN, "run"] + cli_args + [prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"opencode timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"opencode failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["-m", "github-copilot/gpt-5.2"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: opencode.py 'prompt'")
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Toggle external-only mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
toggle.py on [--reason "user requested"]
|
||||||
|
toggle.py off
|
||||||
|
toggle.py status
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
STATE_FILE = Path.home() / ".claude/state/external-mode.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> dict:
|
||||||
|
"""Load current state."""
|
||||||
|
if STATE_FILE.exists():
|
||||||
|
with open(STATE_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"enabled": False, "activated_at": None, "reason": None}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(state: dict):
|
||||||
|
"""Save state to file."""
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def enable(reason: Optional[str] = None):
|
||||||
|
"""Enable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": True,
|
||||||
|
"activated_at": datetime.now().isoformat(),
|
||||||
|
"reason": reason or "user-requested"
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode ENABLED")
|
||||||
|
print(f" Activated: {state['activated_at']}")
|
||||||
|
print(f" Reason: {state['reason']}")
|
||||||
|
print("\nAll agent requests will now use external LLMs.")
|
||||||
|
print("Run 'toggle.py off' or '/pa --external off' to disable.")
|
||||||
|
|
||||||
|
|
||||||
|
def disable():
|
||||||
|
"""Disable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": False,
|
||||||
|
"activated_at": None,
|
||||||
|
"reason": None
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode DISABLED")
|
||||||
|
print("\nAll agent requests will now use Claude.")
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
"""Show current mode status."""
|
||||||
|
state = load_state()
|
||||||
|
if state.get("enabled"):
|
||||||
|
print("External-only mode: ENABLED")
|
||||||
|
print(f" Activated: {state.get('activated_at', 'unknown')}")
|
||||||
|
print(f" Reason: {state.get('reason', 'unknown')}")
|
||||||
|
else:
|
||||||
|
print("External-only mode: DISABLED")
|
||||||
|
print(" Using Claude for all requests.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Toggle external-only mode")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# on command
|
||||||
|
on_parser = subparsers.add_parser("on", help="Enable external-only mode")
|
||||||
|
on_parser.add_argument("--reason", help="Reason for enabling")
|
||||||
|
|
||||||
|
# off command
|
||||||
|
subparsers.add_parser("off", help="Disable external-only mode")
|
||||||
|
|
||||||
|
# status command
|
||||||
|
subparsers.add_parser("status", help="Show current mode")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "on":
|
||||||
|
enable(args.reason)
|
||||||
|
elif args.command == "off":
|
||||||
|
disable()
|
||||||
|
elif args.command == "status":
|
||||||
|
status()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Plans
|
||||||
|
|
||||||
|
Implementation plans for features, enhancements, and investigations.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
plans/
|
||||||
|
├── index.json # Status registry (source of truth)
|
||||||
|
├── README.md # This file
|
||||||
|
└── *.md # Individual plan files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan Naming
|
||||||
|
|
||||||
|
- **Dated plans**: `YYYY-MM-DD-descriptive-name.md` (design docs)
|
||||||
|
- **Generated names**: `adjective-verb-scientist.md` (brainstorming outputs)
|
||||||
|
|
||||||
|
## Status Registry (index.json)
|
||||||
|
|
||||||
|
Central tracking for all plans:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plan-name": {
|
||||||
|
"title": "Human readable title",
|
||||||
|
"status": "pending|implemented|partial|abandoned|superseded",
|
||||||
|
"created": "YYYY-MM-DD",
|
||||||
|
"implemented": "YYYY-MM-DD",
|
||||||
|
"category": "feature|enhancement|bugfix|diagnostic|design",
|
||||||
|
"notes": "Optional notes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Values
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| `pending` | Not yet implemented |
|
||||||
|
| `implemented` | Fully implemented |
|
||||||
|
| `partial` | Partially implemented |
|
||||||
|
| `abandoned` | Decided not to implement |
|
||||||
|
| `superseded` | Replaced by another plan |
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
|
||||||
|
| Category | Meaning |
|
||||||
|
|----------|---------|
|
||||||
|
| `feature` | New capability |
|
||||||
|
| `enhancement` | Improve existing feature |
|
||||||
|
| `bugfix` | Fix an issue |
|
||||||
|
| `diagnostic` | One-time investigation |
|
||||||
|
| `design` | Design document for reference |
|
||||||
|
|
||||||
|
## Querying Plans
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List pending plans
|
||||||
|
jq -r '.plans | to_entries[] | select(.value.status == "pending") | .key' index.json
|
||||||
|
|
||||||
|
# List by category
|
||||||
|
jq '.plans | to_entries[] | select(.value.category == "feature")' index.json
|
||||||
|
|
||||||
|
# Count by status
|
||||||
|
jq '.plans | to_entries | group_by(.value.status) | map({status: .[0].value.status, count: length})' index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Create plan**: Write `plans/plan-name.md`
|
||||||
|
2. **Register**: Add entry to `index.json` with `status: "pending"`
|
||||||
|
3. **Implement**: Execute the plan
|
||||||
|
4. **Update**: Set `status: "implemented"` and add `implemented` date
|
||||||
@@ -0,0 +1,798 @@
|
|||||||
|
# Plan: Transpose Claude Code Setup to OpenCode (Parallel)
|
||||||
|
|
||||||
|
## Handoff Summary
|
||||||
|
|
||||||
|
**Goal**: Set up OpenCode in parallel with Claude Code, sharing state files and syncing agents/skills.
|
||||||
|
|
||||||
|
**Status**: ✅ **IMPLEMENTATION COMPLETE** (2026-01-07)
|
||||||
|
|
||||||
|
### Key Decisions Made
|
||||||
|
|
||||||
|
1. **Use built-in `build` agent** as primary (not porting `personal-assistant`)
|
||||||
|
2. **All agents synced as subagents** (SKIP_AGENTS kept empty for flexibility)
|
||||||
|
3. **Model inheritance** - subagents use runtime-selected model
|
||||||
|
4. **Claude Code is source of truth** - OpenCode references state files via `instructions`
|
||||||
|
5. **No JSON minification** needed (files too small, added to future considerations as fc-047)
|
||||||
|
|
||||||
|
### What Was Completed
|
||||||
|
|
||||||
|
| Step | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| 1 | ✅ | Backups created (Jan 7 12:01) |
|
||||||
|
| 2 | ✅ | Sync script enhanced (mode:subagent, model removal) |
|
||||||
|
| 3 | ✅ | Sync run: 10 skills, 13 agents, 27 commands, 10 workflows |
|
||||||
|
| 4 | ✅ | opencode.json updated (instructions, permissions) |
|
||||||
|
| 5 | ✅ | Automated tests passed, manual TUI testing pending |
|
||||||
|
| 6 | ✅ | README.md created (4.7KB), fc-047 added |
|
||||||
|
| 7 | ⏳ | Iterate as needed |
|
||||||
|
|
||||||
|
### Critical Files
|
||||||
|
|
||||||
|
**Modified:**
|
||||||
|
- `~/.config/opencode/scripts/claude_sync.py` ✅ - Added mode:subagent, model removal, skip logic
|
||||||
|
- `~/.config/opencode/opencode.json` ✅ - Added instructions, permissions
|
||||||
|
|
||||||
|
**Created:**
|
||||||
|
- `~/.config/opencode/README.md` ✅ - Documentation (4.7KB)
|
||||||
|
|
||||||
|
**Referenced (not copied):**
|
||||||
|
- `~/.claude/CLAUDE.md`
|
||||||
|
- `~/.claude/state/kb.json`
|
||||||
|
- `~/.claude/state/personal-assistant/memory/*.json`
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenCode (after implementation)
|
||||||
|
├── Primary: build (built-in), plan (built-in)
|
||||||
|
├── Subagents: @linux-sysadmin, @k8s-orchestrator, @code-reviewer, etc.
|
||||||
|
├── Skills: auto-discovered from ~/.claude/skills/
|
||||||
|
└── State: referenced via instructions from ~/.claude/state/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exit plan mode and begin implementation
|
||||||
|
# Step 1: Backup
|
||||||
|
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
tar -czvf ~/.config/opencode-backup-$BACKUP_DATE.tar.gz -C ~/.config opencode/
|
||||||
|
tar -czvf ~/opencode-home-backup-$BACKUP_DATE.tar.gz -C ~ .opencode/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Create a parallel OpenCode configuration that shares/reuses as much of the existing Claude Code infrastructure as possible, focusing on:
|
||||||
|
1. **Skills/scripts execution** (highest priority)
|
||||||
|
2. **Agent hierarchy/delegation** (second priority)
|
||||||
|
3. **State persistence** (if low complexity)
|
||||||
|
|
||||||
|
## Key Discovery: Native Compatibility
|
||||||
|
|
||||||
|
OpenCode **natively supports Claude-compatible skill paths**:
|
||||||
|
- `~/.claude/skills/<name>/SKILL.md` - Already supported!
|
||||||
|
- This means your 11 existing skills can work with minimal changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: Backup Existing OpenCode Setup
|
||||||
|
|
||||||
|
### Current State Discovered
|
||||||
|
|
||||||
|
OpenCode is already installed with substantial configuration:
|
||||||
|
|
||||||
|
**`~/.config/opencode/`** (main config):
|
||||||
|
- `opencode.json` - Has `claude-sync` command already!
|
||||||
|
- `agent/` - 3 custom agents (coding-expert, k8s-expert, tdd-enforcer)
|
||||||
|
- `agents/` - 12 synced Claude Code agents (already converted!)
|
||||||
|
- `skills/` - 10 skills (some synced, one symlink to morning-report)
|
||||||
|
- `scripts/claude_sync.py` - Existing sync script!
|
||||||
|
|
||||||
|
**`~/.opencode/`** (alternate config):
|
||||||
|
- `agent/` - 4 different agents (openagent, system-builder, etc.)
|
||||||
|
- `command/` - 12 commands (commit, optimize, validate-repo, etc.)
|
||||||
|
|
||||||
|
### Backup Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create timestamped backups
|
||||||
|
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Backup ~/.config/opencode/
|
||||||
|
tar -czvf ~/.config/opencode-backup-$BACKUP_DATE.tar.gz -C ~/.config opencode/
|
||||||
|
|
||||||
|
# Backup ~/.opencode/
|
||||||
|
tar -czvf ~/opencode-home-backup-$BACKUP_DATE.tar.gz -C ~ .opencode/
|
||||||
|
|
||||||
|
# Verify backups
|
||||||
|
ls -la ~/.config/opencode-backup-*.tar.gz ~/opencode-home-backup-*.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Use Existing `claude_sync.py` Script
|
||||||
|
|
||||||
|
The existing sync script is **comprehensive** and handles:
|
||||||
|
|
||||||
|
| Category | Source | Destination | Transforms |
|
||||||
|
|----------|--------|-------------|------------|
|
||||||
|
| Skills | `~/.claude/skills/*/SKILL.md` | `~/.config/opencode/skills/*/SKILL.md` | `allowed-tools` → `metadata.claude_allowed_tools` |
|
||||||
|
| Agents | `~/.claude/agents/*.md` | `~/.config/opencode/agents/*.md` | `tools: X, Y` → `tools: { x: true, y: true }` |
|
||||||
|
| Commands | `~/.claude/commands/*.md` | `~/.config/opencode/claude/commands/*.md` | None |
|
||||||
|
| Workflows | `~/.claude/workflows/*.yaml` | `~/.config/opencode/claude/workflows/*.yaml` | None |
|
||||||
|
|
||||||
|
### Sync Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run - see what would change
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --dry-run
|
||||||
|
|
||||||
|
# Actually sync
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py
|
||||||
|
|
||||||
|
# Clean stale files (dry run first)
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --clean --dry-run
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --clean --apply
|
||||||
|
|
||||||
|
# Sync specific category only
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --only agents
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Mapping Update Needed
|
||||||
|
|
||||||
|
Current script maps old models. May need to add:
|
||||||
|
- `opus` → `anthropic/claude-opus-4`
|
||||||
|
- `sonnet` → `anthropic/claude-sonnet-4-5`
|
||||||
|
- `haiku` → `anthropic/claude-haiku-4-5`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1.5: OpenCode Optimization (NEW)
|
||||||
|
|
||||||
|
The current sync just copies/transforms files. It doesn't optimize for **how OpenCode works**.
|
||||||
|
|
||||||
|
### Key OpenCode Differences
|
||||||
|
|
||||||
|
| Concept | Claude Code | OpenCode | Optimization Needed |
|
||||||
|
|---------|-------------|----------|---------------------|
|
||||||
|
| **Agent hierarchy** | PA → MO → agents | Flat: primary + subagents | Add `mode` field |
|
||||||
|
| **Agent invocation** | Delegation patterns | `@agent` mentions | Simplify prompts |
|
||||||
|
| **Permissions** | Hooks + guardrails | `permission` config | Move to opencode.json |
|
||||||
|
| **Model selection** | Per-agent in frontmatter | `model: inherit` option | Use inheritance |
|
||||||
|
| **Auto-invocation** | Keyword triggers in registry | Rich `description` field | Enhance descriptions |
|
||||||
|
|
||||||
|
### Agent Mode Assignment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# PRIMARY - Use OpenCode's built-in agents
|
||||||
|
build: (built-in) # Full access, default primary
|
||||||
|
plan: (built-in) # Read-only analysis
|
||||||
|
|
||||||
|
# SKIP - Not needed in OpenCode's flat model
|
||||||
|
personal-assistant: # Use built-in "build" instead
|
||||||
|
master-orchestrator: # Intermediary not needed
|
||||||
|
|
||||||
|
# SUBAGENTS (invoked via @mention or Task tool)
|
||||||
|
linux-sysadmin: mode: subagent
|
||||||
|
k8s-orchestrator: mode: subagent
|
||||||
|
k8s-diagnostician: mode: subagent
|
||||||
|
argocd-operator: mode: subagent
|
||||||
|
prometheus-analyst: mode: subagent
|
||||||
|
git-operator: mode: subagent
|
||||||
|
programmer-orchestrator: mode: subagent
|
||||||
|
code-planner: mode: subagent
|
||||||
|
code-implementer: mode: subagent
|
||||||
|
code-reviewer: mode: subagent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hierarchy Simplification
|
||||||
|
|
||||||
|
**Claude Code pattern** (complex, 3 layers):
|
||||||
|
```
|
||||||
|
User → Personal Assistant → Master Orchestrator → linux-sysadmin
|
||||||
|
→ k8s-orchestrator → k8s-diagnostician
|
||||||
|
```
|
||||||
|
|
||||||
|
**OpenCode pattern** (flat, 2 layers):
|
||||||
|
```
|
||||||
|
User → build (built-in) → @linux-sysadmin
|
||||||
|
→ @k8s-orchestrator
|
||||||
|
→ @k8s-diagnostician
|
||||||
|
→ @code-reviewer
|
||||||
|
→ etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- No custom primary agent to maintain
|
||||||
|
- Built-in `build` agent is optimized for OpenCode
|
||||||
|
- Built-in `plan` agent available for read-only analysis
|
||||||
|
- Subagents invoked directly via @mention
|
||||||
|
|
||||||
|
### Sync Script Enhancements Needed
|
||||||
|
|
||||||
|
Update `claude_sync.py` to add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In transform_frontmatter() for agents:
|
||||||
|
|
||||||
|
# 1. Skip agents not needed in OpenCode's flat model
|
||||||
|
SKIP_AGENTS = {"personal-assistant", "master-orchestrator"}
|
||||||
|
|
||||||
|
if name in SKIP_AGENTS:
|
||||||
|
return None # Signal to skip this file
|
||||||
|
|
||||||
|
# 2. All synced agents become subagents (built-in build/plan are primary)
|
||||||
|
frontmatter["mode"] = "subagent"
|
||||||
|
|
||||||
|
# 3. Use model inheritance (subagents use parent's model)
|
||||||
|
frontmatter["model"] = "inherit"
|
||||||
|
|
||||||
|
# 4. Map explicit models if not using inherit
|
||||||
|
MODEL_MAP = {
|
||||||
|
"opus": "anthropic/claude-opus-4",
|
||||||
|
"sonnet": "anthropic/claude-sonnet-4-5",
|
||||||
|
"haiku": "anthropic/claude-haiku-4-5",
|
||||||
|
}
|
||||||
|
if frontmatter.get("model") in MODEL_MAP:
|
||||||
|
frontmatter["model"] = MODEL_MAP[frontmatter["model"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Also update `sync_tree()` to handle `None` return (skip file).
|
||||||
|
|
||||||
|
### Description Enhancement
|
||||||
|
|
||||||
|
OpenCode uses descriptions for **auto-invocation**. Enhance with examples:
|
||||||
|
|
||||||
|
**Current** (basic):
|
||||||
|
```yaml
|
||||||
|
description: Manages Arch Linux workstation - system maintenance...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimized** (with examples):
|
||||||
|
```yaml
|
||||||
|
description: |
|
||||||
|
Manages Arch Linux workstation. Use for system maintenance, updates,
|
||||||
|
troubleshooting, and health checks.
|
||||||
|
Examples:
|
||||||
|
- "check system health" → @linux-sysadmin
|
||||||
|
- "update packages" → @linux-sysadmin
|
||||||
|
- "why is my disk full" → @linux-sysadmin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Migration
|
||||||
|
|
||||||
|
Move guardrail logic to opencode.json:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permission": {
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": {
|
||||||
|
"*": "ask",
|
||||||
|
"pacman -Q*": "allow",
|
||||||
|
"systemctl status*": "allow",
|
||||||
|
"kubectl get*": "allow",
|
||||||
|
"git status": "allow",
|
||||||
|
"git diff": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Create OpenCode Config Structure
|
||||||
|
|
||||||
|
### Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/opencode/
|
||||||
|
├── opencode.json # Main config
|
||||||
|
├── AGENTS.md # Global rules (symlink or copy from CLAUDE.md)
|
||||||
|
├── agent/ # Agent definitions
|
||||||
|
│ ├── personal-assistant.md
|
||||||
|
│ ├── linux-sysadmin.md
|
||||||
|
│ ├── k8s-orchestrator.md
|
||||||
|
│ └── ... (converted agents)
|
||||||
|
├── tool/ # Custom tool wrappers (TypeScript)
|
||||||
|
│ ├── gmail.ts # Wrapper for gmail scripts
|
||||||
|
│ ├── gcal.ts # Wrapper for gcal scripts
|
||||||
|
│ └── ...
|
||||||
|
└── skill/ # OpenCode-native skills (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config File: `~/.config/opencode/opencode.json`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
|
"small_model": "anthropic/claude-haiku-4-5",
|
||||||
|
"autoupdate": true,
|
||||||
|
|
||||||
|
// OpenCode already searches ~/.claude/skills/ - no extra config needed!
|
||||||
|
|
||||||
|
// Agent definitions
|
||||||
|
"agent": {
|
||||||
|
// Override built-in agents or define custom via files
|
||||||
|
},
|
||||||
|
|
||||||
|
// Default permissions (conservative like your current setup)
|
||||||
|
"permission": {
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": "ask"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom tools enabled
|
||||||
|
"tools": {
|
||||||
|
"gmail": true,
|
||||||
|
"gcal": true,
|
||||||
|
"gtasks": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Skills Migration
|
||||||
|
|
||||||
|
### Already Compatible (No Changes Needed)
|
||||||
|
|
||||||
|
OpenCode automatically discovers skills from:
|
||||||
|
- `~/.claude/skills/*/SKILL.md`
|
||||||
|
|
||||||
|
Your existing skills should work if they have proper frontmatter:
|
||||||
|
|
||||||
|
| Skill | Status | Notes |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| gmail | Check frontmatter | Needs `name` + `description` |
|
||||||
|
| gcal | Check frontmatter | Needs `name` + `description` |
|
||||||
|
| gtasks | Check frontmatter | Needs `name` + `description` |
|
||||||
|
| sysadmin-health | Check frontmatter | |
|
||||||
|
| k8s-quick-status | Check frontmatter | |
|
||||||
|
| morning-report | Check frontmatter | |
|
||||||
|
| stock-lookup | Check frontmatter | |
|
||||||
|
| rag-search | Check frontmatter | |
|
||||||
|
| usage | Check frontmatter | |
|
||||||
|
| guardrails | N/A | Becomes permission config |
|
||||||
|
|
||||||
|
### Frontmatter Requirements
|
||||||
|
|
||||||
|
Each SKILL.md needs:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: skill-name # Required, must match directory name
|
||||||
|
description: Brief desc # Required, 1-1024 chars
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audit Results (Already Compatible!)
|
||||||
|
|
||||||
|
Checked skills have proper frontmatter:
|
||||||
|
- `gmail/SKILL.md` - Has `name: gmail`, `description: ...`
|
||||||
|
- `sysadmin-health/SKILL.md` - Has `name: sysadmin-health`, `description: ...`
|
||||||
|
- `morning-report/SKILL.md` - Has `name: morning-report`, `description: ...`
|
||||||
|
|
||||||
|
The `allowed-tools` field in some skills will be ignored by OpenCode (not in their schema), but this is fine.
|
||||||
|
|
||||||
|
### Action Items
|
||||||
|
|
||||||
|
1. ~~Audit each SKILL.md for required frontmatter~~ **Done - already compatible!**
|
||||||
|
2. ~~Add missing `name`/`description` fields~~ **Not needed**
|
||||||
|
3. Test skill discovery in OpenCode after install
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Agent Migration
|
||||||
|
|
||||||
|
### Mapping Strategy
|
||||||
|
|
||||||
|
| Claude Code | OpenCode | Notes |
|
||||||
|
|------------|----------|-------|
|
||||||
|
| `model: opus` | `model: anthropic/claude-opus-4` | Full provider/model path |
|
||||||
|
| `model: sonnet` | `model: anthropic/claude-sonnet-4-5` | |
|
||||||
|
| `model: haiku` | `model: anthropic/claude-haiku-4-5` | |
|
||||||
|
| `tools: Read, Write...` | `tools: { write: true, ... }` | Boolean map |
|
||||||
|
| Hierarchy (PA → MO → agent) | `mode: primary` + `mode: subagent` | Flattened |
|
||||||
|
|
||||||
|
### Agent Conversion Template
|
||||||
|
|
||||||
|
**From (Claude Code):**
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: linux-sysadmin
|
||||||
|
description: Manages Arch Linux workstation...
|
||||||
|
model: sonnet
|
||||||
|
tools: Bash, Read, Write, Edit, Grep, Glob
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**To (OpenCode):**
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: linux-sysadmin
|
||||||
|
description: Manages Arch Linux workstation...
|
||||||
|
mode: subagent
|
||||||
|
model: anthropic/claude-sonnet-4-5
|
||||||
|
tools:
|
||||||
|
bash: true
|
||||||
|
read: true
|
||||||
|
write: true
|
||||||
|
edit: true
|
||||||
|
permission:
|
||||||
|
bash:
|
||||||
|
"*": ask
|
||||||
|
"pacman -Q*": allow
|
||||||
|
"systemctl status*": allow
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priority Agents to Convert
|
||||||
|
|
||||||
|
1. **personal-assistant.md** → `mode: primary` (main interface)
|
||||||
|
2. **linux-sysadmin.md** → `mode: subagent`
|
||||||
|
3. **k8s-orchestrator.md** → `mode: subagent`
|
||||||
|
4. **master-orchestrator.md** → May not be needed (OpenCode doesn't have same hierarchy)
|
||||||
|
|
||||||
|
### Hierarchy Adaptation
|
||||||
|
|
||||||
|
OpenCode doesn't have hierarchical agent delegation like your current setup. Options:
|
||||||
|
- **Option A**: Flatten to primary + subagents, use `@agent` mentions
|
||||||
|
- **Option B**: Use OpenCode's Task tool for agent invocation
|
||||||
|
- **Option C**: Create a "dispatcher" primary agent that routes via @mentions
|
||||||
|
|
||||||
|
**Recommendation**: Option A (simplest) - personal-assistant as primary, others as subagents invokable via `@linux-sysadmin`, `@k8s-orchestrator`, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Custom Tools (Scripts Execution)
|
||||||
|
|
||||||
|
### Wrapper Pattern
|
||||||
|
|
||||||
|
Create TypeScript wrappers that invoke your existing Python scripts:
|
||||||
|
|
||||||
|
**Example: `~/.config/opencode/tool/gmail.ts`**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
export const check_unread = tool({
|
||||||
|
description: "Check unread emails from Gmail",
|
||||||
|
args: {
|
||||||
|
limit: tool.schema.number().optional().describe("Max emails to return"),
|
||||||
|
},
|
||||||
|
async execute(args) {
|
||||||
|
const limit = args.limit ?? 10
|
||||||
|
const result = await Bun.$`~/.claude/mcp/gmail/venv/bin/python ~/.claude/skills/gmail/scripts/check_unread.py --limit ${limit}`.text()
|
||||||
|
return result.trim()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const search = tool({
|
||||||
|
description: "Search Gmail for specific emails",
|
||||||
|
args: {
|
||||||
|
query: tool.schema.string().describe("Search query"),
|
||||||
|
},
|
||||||
|
async execute(args) {
|
||||||
|
const result = await Bun.$`~/.claude/mcp/gmail/venv/bin/python ~/.claude/skills/gmail/scripts/search.py "${args.query}"`.text()
|
||||||
|
return result.trim()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tools to Create Wrappers For
|
||||||
|
|
||||||
|
| Script | Wrapper |
|
||||||
|
|--------|---------|
|
||||||
|
| `gmail/scripts/*.py` | `gmail.ts` |
|
||||||
|
| `gcal/scripts/*.py` | `gcal.ts` |
|
||||||
|
| `gtasks/scripts/*.py` | `gtasks.ts` |
|
||||||
|
| `sysadmin-health/scripts/*.sh` | `sysadmin.ts` |
|
||||||
|
| `morning-report/scripts/*.py` | `morning.ts` |
|
||||||
|
| `stock-lookup/scripts/*.py` | `stocks.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Rules/Instructions
|
||||||
|
|
||||||
|
### Option A: Symlink CLAUDE.md
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -s ~/.claude/CLAUDE.md ~/.config/opencode/AGENTS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Create Minimal AGENTS.md + Reference
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# OpenCode Agent Rules
|
||||||
|
|
||||||
|
Read @~/.claude/CLAUDE.md for shared conventions.
|
||||||
|
|
||||||
|
## OpenCode-Specific
|
||||||
|
|
||||||
|
- Use `@agent-name` to invoke subagents
|
||||||
|
- Skills are loaded via the `skill` tool
|
||||||
|
- Custom tools available: gmail, gcal, gtasks, sysadmin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option C: Use instructions config
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"instructions": ["~/.claude/CLAUDE.md", "~/.claude/state/system-instructions.json"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation**: Option C - cleanest, no duplication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: State Persistence (Claude Code as Source of Truth)
|
||||||
|
|
||||||
|
### Strategy
|
||||||
|
|
||||||
|
Claude Code owns the state files. OpenCode reads them via:
|
||||||
|
1. `{file:path}` variable substitution in `opencode.json`
|
||||||
|
2. `instructions` array for context files
|
||||||
|
3. Skills that read state files directly
|
||||||
|
|
||||||
|
### What Can Be Shared
|
||||||
|
|
||||||
|
| File | Method | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `~/.claude/CLAUDE.md` | `instructions` | Global rules |
|
||||||
|
| `~/.claude/state/kb.json` | `instructions` or skill | Knowledge base |
|
||||||
|
| `~/.claude/state/personal-assistant/memory/*.json` | `instructions` | Memory context |
|
||||||
|
| `~/.claude/state/system-instructions.json` | `instructions` | Process definitions |
|
||||||
|
|
||||||
|
### Implementation in `opencode.json`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
|
||||||
|
// Load Claude Code state as instructions (read at session start)
|
||||||
|
"instructions": [
|
||||||
|
"~/.claude/CLAUDE.md",
|
||||||
|
"~/.claude/state/kb.json",
|
||||||
|
"~/.claude/state/personal-assistant/memory/facts.json",
|
||||||
|
"~/.claude/state/personal-assistant/memory/preferences.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Stays Separate
|
||||||
|
|
||||||
|
| Item | Reason |
|
||||||
|
|------|--------|
|
||||||
|
| Session history | Different formats, different storage |
|
||||||
|
| Autonomy/permissions | OpenCode uses `permission` config instead |
|
||||||
|
| Component registry | OpenCode discovers via file paths |
|
||||||
|
|
||||||
|
### Overhead Assessment
|
||||||
|
|
||||||
|
**Low overhead** - just config changes:
|
||||||
|
- Add paths to `instructions` array
|
||||||
|
- No symlinks or sync scripts needed
|
||||||
|
- OpenCode reads files directly at session start
|
||||||
|
- Claude Code continues to write/update normally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: What Won't Transfer
|
||||||
|
|
||||||
|
| Feature | Claude Code | OpenCode Alternative |
|
||||||
|
|---------|-------------|---------------------|
|
||||||
|
| Hooks (SessionStart, etc.) | `hooks/hooks.json` | Plugins (future) |
|
||||||
|
| Guardrails hook | PreToolUse script | `permission` config |
|
||||||
|
| Component registry routing | Keyword triggers | Agent descriptions + @mentions |
|
||||||
|
| Hierarchical delegation | PA → MO → agent | Flat subagent model |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
### Step 1: Backup (5 min) ✅ COMPLETE
|
||||||
|
- [x] Create timestamped backup of `~/.config/opencode/` → `opencode-backup-20260107_120135.tar.gz`
|
||||||
|
- [x] Create timestamped backup of `~/.opencode/` → `opencode-home-backup-20260107_120136.tar.gz`
|
||||||
|
|
||||||
|
### Step 2: Enhance Sync Script (45 min) ✅ COMPLETE
|
||||||
|
- [x] Add skip list: `SKIP_AGENTS` (kept empty - all agents synced as subagents)
|
||||||
|
- [x] Add `mode: subagent` to all synced agents
|
||||||
|
- [x] Remove hardcoded model (agents inherit from runtime selection)
|
||||||
|
- [x] Add model stripping from opencode.json
|
||||||
|
- [x] Update `sync_tree()` to handle skipped files
|
||||||
|
- [ ] ~~Optionally enhance descriptions with examples~~ (deferred)
|
||||||
|
|
||||||
|
### Step 3: Run Enhanced Sync (10 min) ✅ COMPLETE
|
||||||
|
- [x] `python3 ~/.config/opencode/scripts/claude_sync.py --dry-run`
|
||||||
|
- [x] Review output - verify mode/model changes
|
||||||
|
- [x] `python3 ~/.config/opencode/scripts/claude_sync.py`
|
||||||
|
- [x] All synced: 10 skills, 13 agents, 27 commands, 10 workflows
|
||||||
|
|
||||||
|
### Step 4: Update opencode.json (20 min) ✅ COMPLETE
|
||||||
|
- [x] Add `instructions` array (CLAUDE.md, kb.json, memory files)
|
||||||
|
- [x] Model defaults: intentionally omitted (user selects at runtime)
|
||||||
|
- [x] Add permission config with safe command patterns
|
||||||
|
|
||||||
|
### Step 5: Testing (30 min) ✅ COMPLETE (automated)
|
||||||
|
- [x] OpenCode v1.0.220 installed at `/home/linuxbrew/.linuxbrew/bin/opencode`
|
||||||
|
- [x] `opencode agent list` shows 40 agents (built-in + synced)
|
||||||
|
- [x] All Claude Code agents show as `(subagent)`
|
||||||
|
- [x] 10 skills synced to `~/.config/opencode/skills/`
|
||||||
|
- [x] Config verified: instructions, permissions, commands present
|
||||||
|
- [ ] Manual TUI testing (user to verify interactively)
|
||||||
|
|
||||||
|
### Step 6: Documentation (20 min) ✅ COMPLETE
|
||||||
|
- [x] Create `~/.config/opencode/README.md` (4.7KB)
|
||||||
|
- [x] Document complete agent mapping table
|
||||||
|
- [x] Document sync workflow with examples
|
||||||
|
- [x] Add fc-047 to `~/.claude/state/future-considerations.json`
|
||||||
|
|
||||||
|
### Step 7: Iterate (as needed) ⏳ PENDING
|
||||||
|
- [ ] Adjust agent descriptions if auto-invocation isn't working well
|
||||||
|
- [ ] Tune permission patterns
|
||||||
|
- [ ] Consider dropping/hiding agents that don't fit OpenCode model
|
||||||
|
- [ ] Update documentation with lessons learned
|
||||||
|
|
||||||
|
**Status: IMPLEMENTATION COMPLETE** - Manual TUI testing recommended
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Documentation
|
||||||
|
|
||||||
|
### Documentation Deliverables
|
||||||
|
|
||||||
|
Create `~/.config/opencode/README.md` with:
|
||||||
|
|
||||||
|
1. **Architecture Overview**
|
||||||
|
- Relationship between Claude Code and OpenCode
|
||||||
|
- What's shared vs separate
|
||||||
|
- Source of truth (Claude Code)
|
||||||
|
|
||||||
|
2. **Sync Workflow**
|
||||||
|
- How `claude_sync.py` works
|
||||||
|
- When to run it (after Claude Code changes)
|
||||||
|
- Command reference
|
||||||
|
|
||||||
|
3. **Agent Mapping**
|
||||||
|
- Which Claude Code agents map to OpenCode
|
||||||
|
- Which are skipped and why
|
||||||
|
- How to invoke subagents (@mentions)
|
||||||
|
|
||||||
|
4. **Skills**
|
||||||
|
- Auto-discovery from `~/.claude/skills/`
|
||||||
|
- How to add new skills
|
||||||
|
- Skill invocation patterns
|
||||||
|
|
||||||
|
5. **State Sharing**
|
||||||
|
- Files referenced via `instructions`
|
||||||
|
- Claude Code as source of truth
|
||||||
|
- What stays separate
|
||||||
|
|
||||||
|
6. **Permissions**
|
||||||
|
- How guardrails translated to `permission` config
|
||||||
|
- Safe vs prompted commands
|
||||||
|
|
||||||
|
### Documentation Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# OpenCode Configuration
|
||||||
|
|
||||||
|
This OpenCode setup is synchronized from Claude Code (`~/.claude/`).
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start OpenCode (uses built-in build agent)
|
||||||
|
opencode
|
||||||
|
|
||||||
|
# Switch to read-only plan agent
|
||||||
|
# Press Tab
|
||||||
|
|
||||||
|
# Invoke a subagent
|
||||||
|
@linux-sysadmin check system health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude Code (source of truth)
|
||||||
|
├── ~/.claude/agents/ → synced to ~/.config/opencode/agents/
|
||||||
|
├── ~/.claude/skills/ → synced to ~/.config/opencode/skills/
|
||||||
|
├── ~/.claude/CLAUDE.md → referenced via instructions
|
||||||
|
└── ~/.claude/state/ → referenced via instructions
|
||||||
|
|
||||||
|
OpenCode
|
||||||
|
├── Built-in: build (primary), plan (read-only)
|
||||||
|
├── Subagents: @linux-sysadmin, @k8s-orchestrator, etc.
|
||||||
|
└── Skills: gmail, gcal, sysadmin-health, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sync Workflow
|
||||||
|
|
||||||
|
After making changes in Claude Code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview changes
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --dry-run
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py
|
||||||
|
|
||||||
|
# Clean stale files
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --clean --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agents
|
||||||
|
|
||||||
|
| Claude Code | OpenCode | Notes |
|
||||||
|
|-------------|----------|-------|
|
||||||
|
| personal-assistant | (skipped) | Use built-in `build` |
|
||||||
|
| master-orchestrator | (skipped) | Flat model, not needed |
|
||||||
|
| linux-sysadmin | @linux-sysadmin | Subagent |
|
||||||
|
| k8s-orchestrator | @k8s-orchestrator | Subagent |
|
||||||
|
| ... | ... | ... |
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
Skills are auto-discovered from:
|
||||||
|
- `~/.claude/skills/*/SKILL.md`
|
||||||
|
- `~/.config/opencode/skills/*/SKILL.md`
|
||||||
|
|
||||||
|
## State Files
|
||||||
|
|
||||||
|
Referenced via `instructions` in opencode.json:
|
||||||
|
- `~/.claude/CLAUDE.md` - Global rules
|
||||||
|
- `~/.claude/state/kb.json` - Knowledge base
|
||||||
|
- `~/.claude/state/personal-assistant/memory/*.json` - Memory
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
Configured in opencode.json `permission` section.
|
||||||
|
Migrated from Claude Code's guardrail hooks.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Step
|
||||||
|
|
||||||
|
Add to Step 6:
|
||||||
|
- [ ] Create `~/.config/opencode/README.md`
|
||||||
|
- [ ] Document sync workflow
|
||||||
|
- [ ] Document agent mapping
|
||||||
|
- [ ] Document any gotchas discovered during testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
### Files to Create
|
||||||
|
- `~/.config/opencode/README.md` - Documentation of setup, workflow, and requirements
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `~/.config/opencode/opencode.json` - Add `instructions` array + model/permission config
|
||||||
|
- `~/.config/opencode/scripts/claude_sync.py` - Add mode, model mappings, skip list
|
||||||
|
|
||||||
|
### Files Auto-Synced by Script
|
||||||
|
These are created/updated by `claude_sync.py`:
|
||||||
|
- `~/.config/opencode/agents/*.md` - From `~/.claude/agents/`
|
||||||
|
- `~/.config/opencode/skills/*/SKILL.md` - From `~/.claude/skills/`
|
||||||
|
- `~/.config/opencode/claude/commands/*.md` - From `~/.claude/commands/`
|
||||||
|
- `~/.config/opencode/claude/workflows/*.yaml` - From `~/.claude/workflows/`
|
||||||
|
|
||||||
|
### Files Referenced (Not Copied)
|
||||||
|
These stay in Claude Code, referenced via `instructions`:
|
||||||
|
- `~/.claude/CLAUDE.md`
|
||||||
|
- `~/.claude/state/kb.json`
|
||||||
|
- `~/.claude/state/personal-assistant/memory/*.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. `opencode` launches and shows available skills
|
||||||
|
2. Can invoke `@linux-sysadmin` and get expected behavior
|
||||||
|
3. Gmail/GCal/GTasks tools work via custom wrappers
|
||||||
|
4. Can switch between build/plan agents + custom agents
|
||||||
|
5. Both Claude Code and OpenCode can run in parallel without conflicts
|
||||||
@@ -0,0 +1,903 @@
|
|||||||
|
# External LLM Integration Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Enable agents to use external LLMs (Copilot, Z.AI, Gemini) via CLI tools with a session toggle.
|
||||||
|
|
||||||
|
**Architecture:** Python router reads model-policy.json, invokes appropriate CLI (opencode/gemini), returns response. State file controls Claude vs external mode. PA exposes toggle commands.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3, subprocess, JSON state files, opencode CLI, gemini CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Create External Mode State File
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/state/external-mode.json`
|
||||||
|
|
||||||
|
**Step 1: Create state file**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > ~/.claude/state/external-mode.json << 'EOF'
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"activated_at": null,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify file**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/external-mode.json | jq .`
|
||||||
|
Expected: Valid JSON with `enabled: false`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add state/external-mode.json
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add external-mode state file"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Extend Model Policy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.claude/state/model-policy.json`
|
||||||
|
|
||||||
|
**Step 1: Read current file**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/model-policy.json | jq .`
|
||||||
|
|
||||||
|
**Step 2: Add external_models section**
|
||||||
|
|
||||||
|
Add to model-policy.json (after `skill_delegation` section):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"external_models": {
|
||||||
|
"copilot/gpt-5.2": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "gpt-5.2"],
|
||||||
|
"use_cases": ["reasoning", "fallback"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/sonnet-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "sonnet-4.5"],
|
||||||
|
"use_cases": ["general", "fallback"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/haiku-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "haiku-4.5"],
|
||||||
|
"use_cases": ["simple"],
|
||||||
|
"tier": "haiku-equivalent"
|
||||||
|
},
|
||||||
|
"zai/glm-4.7": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "zai", "--model", "glm-4.7"],
|
||||||
|
"use_cases": ["code-generation"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"gemini/gemini-3-pro": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-3-pro"],
|
||||||
|
"use_cases": ["long-context"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claude_to_external_map": {
|
||||||
|
"opus": "copilot/gpt-5.2",
|
||||||
|
"sonnet": "copilot/sonnet-4.5",
|
||||||
|
"haiku": "copilot/haiku-4.5"
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "copilot/gpt-5.2",
|
||||||
|
"code-generation": "zai/glm-4.7",
|
||||||
|
"long-context": "gemini/gemini-3-pro",
|
||||||
|
"default": "copilot/sonnet-4.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Validate JSON**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/model-policy.json | jq .`
|
||||||
|
Expected: Valid JSON, no errors
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add state/model-policy.json
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add external model definitions to policy"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Create Router Directory Structure
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/`
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/`
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/__init__.py`
|
||||||
|
|
||||||
|
**Step 1: Create directories**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.claude/mcp/llm-router/providers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Create __init__.py**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch ~/.claude/mcp/llm-router/providers/__init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify structure**
|
||||||
|
|
||||||
|
Run: `ls -la ~/.claude/mcp/llm-router/`
|
||||||
|
Expected: `providers/` directory exists
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): create llm-router directory structure"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Create OpenCode Provider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/opencode.py`
|
||||||
|
|
||||||
|
**Step 1: Write provider**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OpenCode CLI wrapper for Copilot, Z.AI, and other providers."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke opencode CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Provider/model args like ["--provider", "copilot", "--model", "gpt-5.2"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If opencode CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
"""
|
||||||
|
cmd = ["opencode", "--print"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"opencode timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"opencode failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["--provider", "copilot", "--model", "gpt-5.2"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: opencode.py 'prompt'")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/providers/opencode.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/providers/opencode.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/providers/opencode.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add opencode provider wrapper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Create Gemini Provider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/gemini.py`
|
||||||
|
|
||||||
|
**Step 1: Write provider**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gemini CLI wrapper for Google models."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke gemini CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Model args like ["-m", "gemini-3-pro"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If gemini CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
"""
|
||||||
|
cmd = ["gemini"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"gemini timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"gemini failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["-m", "gemini-3-pro"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: gemini.py 'prompt'")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/providers/gemini.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/providers/gemini.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/providers/gemini.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add gemini provider wrapper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Create Main Router (invoke.py)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/invoke.py`
|
||||||
|
|
||||||
|
**Step 1: Write router**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Invoke external LLM via configured provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
invoke.py --model copilot/gpt-5.2 -p "prompt"
|
||||||
|
invoke.py --task reasoning -p "prompt"
|
||||||
|
invoke.py --task code-generation -p "prompt" --json
|
||||||
|
|
||||||
|
Model selection priority:
|
||||||
|
1. Explicit --model flag
|
||||||
|
2. Task-based routing (--task flag)
|
||||||
|
3. Default from policy
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_policy() -> dict:
|
||||||
|
"""Load model policy from state file."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model(args: argparse.Namespace, policy: dict) -> str:
|
||||||
|
"""Determine which model to use based on args and policy."""
|
||||||
|
if args.model:
|
||||||
|
return args.model
|
||||||
|
if args.task and args.task in policy.get("task_routing", {}):
|
||||||
|
return policy["task_routing"][args.task]
|
||||||
|
return policy.get("task_routing", {}).get("default", "copilot/sonnet-4.5")
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(model: str, prompt: str, policy: dict) -> str:
|
||||||
|
"""Invoke the appropriate provider for the given model."""
|
||||||
|
external_models = policy.get("external_models", {})
|
||||||
|
|
||||||
|
if model not in external_models:
|
||||||
|
raise ValueError(f"Unknown model: {model}. Available: {list(external_models.keys())}")
|
||||||
|
|
||||||
|
model_config = external_models[model]
|
||||||
|
cli = model_config["cli"]
|
||||||
|
cli_args = model_config.get("cli_args", [])
|
||||||
|
|
||||||
|
# Import and invoke appropriate provider
|
||||||
|
if cli == "opencode":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.opencode import invoke as opencode_invoke
|
||||||
|
return opencode_invoke(cli_args, prompt)
|
||||||
|
elif cli == "gemini":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.gemini import invoke as gemini_invoke
|
||||||
|
return gemini_invoke(cli_args, prompt)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown CLI: {cli}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Invoke external LLM via configured provider"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
help="Explicit model (e.g., copilot/gpt-5.2)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--task",
|
||||||
|
choices=["reasoning", "code-generation", "long-context", "general"],
|
||||||
|
help="Task type for automatic model routing"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON with model info"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help="Timeout in seconds (default: 300)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy = load_policy()
|
||||||
|
model = resolve_model(args, policy)
|
||||||
|
result = invoke(model, args.prompt, policy)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": model,
|
||||||
|
"response": result,
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": args.model or "unknown",
|
||||||
|
"error": str(e),
|
||||||
|
"success": False
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/invoke.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/invoke.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/invoke.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add main router invoke.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Create Delegation Helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/delegate.py`
|
||||||
|
|
||||||
|
**Step 1: Write delegation helper**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent delegation helper. Routes to external or Claude based on mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
delegate.py --tier sonnet -p "prompt"
|
||||||
|
delegate.py --tier opus -p "complex reasoning task" --json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def is_external_mode() -> bool:
|
||||||
|
"""Check if external-only mode is enabled."""
|
||||||
|
mode_file = STATE_DIR / "external-mode.json"
|
||||||
|
if mode_file.exists():
|
||||||
|
with open(mode_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("enabled", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_external_model(tier: str) -> str:
|
||||||
|
"""Get the external model equivalent for a Claude tier."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
policy = json.load(f)
|
||||||
|
mapping = policy.get("claude_to_external_map", {})
|
||||||
|
if tier not in mapping:
|
||||||
|
raise ValueError(f"No external mapping for tier: {tier}")
|
||||||
|
return mapping[tier]
|
||||||
|
|
||||||
|
|
||||||
|
def delegate(tier: str, prompt: str, use_json: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Delegate to appropriate model based on mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tier: Claude tier (opus, sonnet, haiku)
|
||||||
|
prompt: The prompt text
|
||||||
|
use_json: Return JSON output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
"""
|
||||||
|
if is_external_mode():
|
||||||
|
# Use external model
|
||||||
|
model = get_external_model(tier)
|
||||||
|
invoke_script = ROUTER_DIR / "invoke.py"
|
||||||
|
|
||||||
|
cmd = [sys.executable, str(invoke_script), "--model", model, "-p", prompt]
|
||||||
|
if use_json:
|
||||||
|
cmd.append("--json")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"External invoke failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
else:
|
||||||
|
# Use Claude
|
||||||
|
cmd = ["claude", "--print", "--model", tier, prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Claude failed: {result.stderr}")
|
||||||
|
|
||||||
|
response = result.stdout.strip()
|
||||||
|
|
||||||
|
if use_json:
|
||||||
|
return json.dumps({
|
||||||
|
"model": f"claude/{tier}",
|
||||||
|
"response": response,
|
||||||
|
"success": True
|
||||||
|
}, indent=2)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Delegate to Claude or external model based on mode"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tier",
|
||||||
|
required=True,
|
||||||
|
choices=["opus", "sonnet", "haiku"],
|
||||||
|
help="Claude tier (maps to external equivalent when in external mode)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = delegate(args.tier, args.prompt, args.json)
|
||||||
|
print(result)
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"error": str(e), "success": False}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/delegate.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/delegate.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/delegate.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add delegation helper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Create Toggle Script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/toggle.py`
|
||||||
|
|
||||||
|
**Step 1: Write toggle script**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Toggle external-only mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
toggle.py on [--reason "user requested"]
|
||||||
|
toggle.py off
|
||||||
|
toggle.py status
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_FILE = Path.home() / ".claude/state/external-mode.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> dict:
|
||||||
|
"""Load current state."""
|
||||||
|
if STATE_FILE.exists():
|
||||||
|
with open(STATE_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"enabled": False, "activated_at": None, "reason": None}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(state: dict):
|
||||||
|
"""Save state to file."""
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def enable(reason: str = None):
|
||||||
|
"""Enable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": True,
|
||||||
|
"activated_at": datetime.now().isoformat(),
|
||||||
|
"reason": reason or "user-requested"
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode ENABLED")
|
||||||
|
print(f" Activated: {state['activated_at']}")
|
||||||
|
print(f" Reason: {state['reason']}")
|
||||||
|
print("\nAll agent requests will now use external LLMs.")
|
||||||
|
print("Run 'toggle.py off' or '/pa --external off' to disable.")
|
||||||
|
|
||||||
|
|
||||||
|
def disable():
|
||||||
|
"""Disable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": False,
|
||||||
|
"activated_at": None,
|
||||||
|
"reason": None
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode DISABLED")
|
||||||
|
print("\nAll agent requests will now use Claude.")
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
"""Show current mode status."""
|
||||||
|
state = load_state()
|
||||||
|
if state.get("enabled"):
|
||||||
|
print("External-only mode: ENABLED")
|
||||||
|
print(f" Activated: {state.get('activated_at', 'unknown')}")
|
||||||
|
print(f" Reason: {state.get('reason', 'unknown')}")
|
||||||
|
else:
|
||||||
|
print("External-only mode: DISABLED")
|
||||||
|
print(" Using Claude for all requests.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Toggle external-only mode")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# on command
|
||||||
|
on_parser = subparsers.add_parser("on", help="Enable external-only mode")
|
||||||
|
on_parser.add_argument("--reason", help="Reason for enabling")
|
||||||
|
|
||||||
|
# off command
|
||||||
|
subparsers.add_parser("off", help="Disable external-only mode")
|
||||||
|
|
||||||
|
# status command
|
||||||
|
subparsers.add_parser("status", help="Show current mode")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "on":
|
||||||
|
enable(args.reason)
|
||||||
|
elif args.command == "off":
|
||||||
|
disable()
|
||||||
|
elif args.command == "status":
|
||||||
|
status()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/toggle.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Test toggle**
|
||||||
|
|
||||||
|
Run: `~/.claude/mcp/llm-router/toggle.py status`
|
||||||
|
Expected: Shows "External-only mode: DISABLED"
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/toggle.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add toggle script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: Update Session Start Hook
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
|
||||||
|
**Step 1: Read current hook**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
|
||||||
|
**Step 2: Add external mode check**
|
||||||
|
|
||||||
|
Add before the final output section:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check external mode
|
||||||
|
if [ -f ~/.claude/state/external-mode.json ]; then
|
||||||
|
EXTERNAL_ENABLED=$(jq -r '.enabled // false' ~/.claude/state/external-mode.json)
|
||||||
|
if [ "$EXTERNAL_ENABLED" = "true" ]; then
|
||||||
|
echo "external-mode:enabled"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify hook**
|
||||||
|
|
||||||
|
Run: `bash -n ~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add hooks/scripts/session-start.sh
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): announce external mode in session-start"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Add Component Registry Triggers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.claude/state/component-registry.json`
|
||||||
|
|
||||||
|
**Step 1: Read current registry**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/component-registry.json | jq '.skills'`
|
||||||
|
|
||||||
|
**Step 2: Add external-mode skill entry**
|
||||||
|
|
||||||
|
Add to skills array:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "external-mode-toggle",
|
||||||
|
"name": "External Mode Toggle",
|
||||||
|
"description": "Toggle between Claude and external LLMs",
|
||||||
|
"path": "mcp/llm-router/toggle.py",
|
||||||
|
"triggers": [
|
||||||
|
"use external",
|
||||||
|
"switch to external",
|
||||||
|
"external models",
|
||||||
|
"external only",
|
||||||
|
"use copilot",
|
||||||
|
"use opencode",
|
||||||
|
"back to claude",
|
||||||
|
"use claude again",
|
||||||
|
"disable external",
|
||||||
|
"external mode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Validate JSON**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/component-registry.json | jq .`
|
||||||
|
Expected: Valid JSON
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add state/component-registry.json
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add external-mode triggers to registry"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11: Integration Test
|
||||||
|
|
||||||
|
**Step 1: Test toggle**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/toggle.py status
|
||||||
|
~/.claude/mcp/llm-router/toggle.py on --reason "testing"
|
||||||
|
~/.claude/mcp/llm-router/toggle.py status
|
||||||
|
~/.claude/mcp/llm-router/toggle.py off
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Status changes correctly
|
||||||
|
|
||||||
|
**Step 2: Test router (mock - will fail without actual CLIs)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --model copilot/gpt-5.2 -p "Say hello" --json 2>&1 || echo "Expected: CLI not found (normal if opencode not installed)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Test delegation helper status check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '$HOME/.claude/mcp/llm-router')
|
||||||
|
from delegate import is_external_mode
|
||||||
|
print(f'External mode: {is_external_mode()}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: "External mode: False"
|
||||||
|
|
||||||
|
**Step 4: Final commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude commit --allow-empty -m "feat(external-llm): integration complete - gleaming-routing-mercury"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
After completing all tasks:
|
||||||
|
|
||||||
|
| Component | Status |
|
||||||
|
|-----------|--------|
|
||||||
|
| `state/external-mode.json` | Created |
|
||||||
|
| `state/model-policy.json` | Extended |
|
||||||
|
| `mcp/llm-router/invoke.py` | Created |
|
||||||
|
| `mcp/llm-router/delegate.py` | Created |
|
||||||
|
| `mcp/llm-router/toggle.py` | Created |
|
||||||
|
| `mcp/llm-router/providers/opencode.py` | Created |
|
||||||
|
| `mcp/llm-router/providers/gemini.py` | Created |
|
||||||
|
| `hooks/scripts/session-start.sh` | Updated |
|
||||||
|
| `state/component-registry.json` | Updated |
|
||||||
|
|
||||||
|
**To use:**
|
||||||
|
```bash
|
||||||
|
# Enable external mode
|
||||||
|
~/.claude/mcp/llm-router/toggle.py on
|
||||||
|
|
||||||
|
# Or via PA
|
||||||
|
/pa --external on
|
||||||
|
/pa switch to external models
|
||||||
|
|
||||||
|
# Invoke directly
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --task reasoning -p "Explain quantum computing"
|
||||||
|
|
||||||
|
# Delegate (respects mode)
|
||||||
|
~/.claude/mcp/llm-router/delegate.py --tier sonnet -p "Check disk space"
|
||||||
|
```
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
# Plan: External LLM Integration
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Integrate external LLMs (via subscription-based access) into the agent system for cost optimization, specialized capabilities, and redundancy. Uses `opencode` CLI for Copilot/Z.AI models and `gemini` CLI for Google models.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
- **Cost optimization** — Use cheaper models for simple tasks
|
||||||
|
- **Specialized capabilities** — Access models with unique strengths (GPT-5.2 for reasoning, GLM 4.7 for code)
|
||||||
|
- **Redundancy** — Fallback when Claude is unavailable
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice |
|
||||||
|
|----------|--------|
|
||||||
|
| Provider type | Cloud APIs via subscription (not local) |
|
||||||
|
| Providers | GitHub Copilot, Z.AI, Google Gemini |
|
||||||
|
| CLIs | `opencode` (Copilot, Z.AI), `gemini` (Google) |
|
||||||
|
| Integration | Task-specific routing + agent-level assignment |
|
||||||
|
| Toggle | State file (persists across sessions) |
|
||||||
|
| Toggle scope | All agents switch when enabled |
|
||||||
|
|
||||||
|
## Task Routing
|
||||||
|
|
||||||
|
| Task Type | Model |
|
||||||
|
|-----------|-------|
|
||||||
|
| Reasoning chains | copilot/gpt-5.2 |
|
||||||
|
| Code generation | zai/glm-4.7 |
|
||||||
|
| Long context | gemini/gemini-3-pro |
|
||||||
|
| General/fallback | copilot/sonnet-4.5 |
|
||||||
|
|
||||||
|
## Claude-to-External Mapping
|
||||||
|
|
||||||
|
| Claude Tier | External Equivalent |
|
||||||
|
|-------------|---------------------|
|
||||||
|
| opus | copilot/gpt-5.2 |
|
||||||
|
| sonnet | copilot/sonnet-4.5 |
|
||||||
|
| haiku | copilot/haiku-4.5 |
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
### `~/.claude/state/external-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"activated_at": null,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/invoke.py`
|
||||||
|
|
||||||
|
Main entry point for invoking external LLMs.
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Invoke external LLM via configured provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
invoke.py --model copilot/gpt-5.2 -p "prompt"
|
||||||
|
invoke.py --task reasoning -p "prompt"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
|
||||||
|
def load_policy():
|
||||||
|
with open(STATE_DIR / "model-policy.json") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def resolve_model(args, policy):
|
||||||
|
if args.model:
|
||||||
|
return args.model
|
||||||
|
if args.task and args.task in policy["task_routing"]:
|
||||||
|
return policy["task_routing"][args.task]
|
||||||
|
return policy["task_routing"]["default"]
|
||||||
|
|
||||||
|
def invoke(model: str, prompt: str, policy: dict) -> str:
|
||||||
|
model_config = policy["external_models"][model]
|
||||||
|
cli = model_config["cli"]
|
||||||
|
cli_args = model_config["cli_args"]
|
||||||
|
|
||||||
|
if cli == "opencode":
|
||||||
|
from providers.opencode import invoke as opencode_invoke
|
||||||
|
return opencode_invoke(cli_args, prompt)
|
||||||
|
elif cli == "gemini":
|
||||||
|
from providers.gemini import invoke as gemini_invoke
|
||||||
|
return gemini_invoke(cli_args, prompt)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown CLI: {cli}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-p", "--prompt", required=True, help="Prompt text")
|
||||||
|
parser.add_argument("--model", help="Explicit model (e.g., copilot/gpt-5.2)")
|
||||||
|
parser.add_argument("--task", help="Task type for routing")
|
||||||
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
policy = load_policy()
|
||||||
|
model = resolve_model(args, policy)
|
||||||
|
result = invoke(args.prompt, model, policy)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"model": model, "response": result}))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/providers/opencode.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OpenCode CLI wrapper."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def invoke(cli_args: list, prompt: str) -> str:
|
||||||
|
cmd = ["opencode", "--print"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"opencode failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/providers/gemini.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gemini CLI wrapper."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def invoke(cli_args: list, prompt: str) -> str:
|
||||||
|
cmd = ["gemini"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"gemini failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/delegate.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent delegation helper. Routes to external or Claude based on mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
delegate.py --tier sonnet -p "prompt"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
|
||||||
|
def is_external_mode():
|
||||||
|
mode_file = STATE_DIR / "external-mode.json"
|
||||||
|
if mode_file.exists():
|
||||||
|
with open(mode_file) as f:
|
||||||
|
return json.load(f).get("enabled", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delegate(tier: str, prompt: str) -> str:
|
||||||
|
if is_external_mode():
|
||||||
|
policy = json.loads((STATE_DIR / "model-policy.json").read_text())
|
||||||
|
model = policy["claude_to_external_map"][tier]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(Path.home() / ".claude/mcp/llm-router/invoke.py"),
|
||||||
|
"--model", model, "-p", prompt],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
else:
|
||||||
|
result = subprocess.run(
|
||||||
|
["claude", "--print", "--model", tier, prompt],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--tier", required=True, choices=["opus", "sonnet", "haiku"])
|
||||||
|
parser.add_argument("-p", "--prompt", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(delegate(args.tier, args.prompt))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
### `~/.claude/state/model-policy.json`
|
||||||
|
|
||||||
|
Add sections:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"external_models": {
|
||||||
|
"copilot/gpt-5.2": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "gpt-5.2"],
|
||||||
|
"use_cases": ["reasoning", "fallback"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/sonnet-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "sonnet-4.5"],
|
||||||
|
"use_cases": ["general", "fallback"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/haiku-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "haiku-4.5"],
|
||||||
|
"use_cases": ["simple"],
|
||||||
|
"tier": "haiku-equivalent"
|
||||||
|
},
|
||||||
|
"zai/glm-4.7": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "zai", "--model", "glm-4.7"],
|
||||||
|
"use_cases": ["code-generation"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"gemini/gemini-3-pro": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-3-pro"],
|
||||||
|
"use_cases": ["long-context"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claude_to_external_map": {
|
||||||
|
"opus": "copilot/gpt-5.2",
|
||||||
|
"sonnet": "copilot/sonnet-4.5",
|
||||||
|
"haiku": "copilot/haiku-4.5"
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "copilot/gpt-5.2",
|
||||||
|
"code-generation": "zai/glm-4.7",
|
||||||
|
"long-context": "gemini/gemini-3-pro",
|
||||||
|
"default": "copilot/sonnet-4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/state/component-registry.json`
|
||||||
|
|
||||||
|
Add trigger for external mode:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "external-mode-toggle",
|
||||||
|
"type": "skill",
|
||||||
|
"triggers": [
|
||||||
|
"use external", "switch to external", "external models",
|
||||||
|
"stop using claude", "external only", "use copilot",
|
||||||
|
"back to claude", "use claude again", "disable external"
|
||||||
|
],
|
||||||
|
"action": "toggle-external-mode"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
|
||||||
|
Add external mode announcement:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check external mode
|
||||||
|
if [ -f ~/.claude/state/external-mode.json ]; then
|
||||||
|
if [ "$(jq -r '.enabled' ~/.claude/state/external-mode.json)" = "true" ]; then
|
||||||
|
echo "external-mode:enabled"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Toggle Interface
|
||||||
|
|
||||||
|
### Command-based
|
||||||
|
|
||||||
|
```
|
||||||
|
/pa --external on # Enable external-only mode
|
||||||
|
/pa --external off # Disable, return to Claude
|
||||||
|
/pa --external status # Show current mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Natural language
|
||||||
|
|
||||||
|
| User says | Action |
|
||||||
|
|-----------|--------|
|
||||||
|
| "switch to external models" | Enable |
|
||||||
|
| "use copilot for everything" | Enable |
|
||||||
|
| "go back to claude" | Disable |
|
||||||
|
| "are we using external?" | Status |
|
||||||
|
| "use external for this" | One-shot (no persist) |
|
||||||
|
|
||||||
|
### Visual indicator
|
||||||
|
|
||||||
|
When external mode active, PA prefixes responses:
|
||||||
|
|
||||||
|
```
|
||||||
|
🔌 [External: copilot/gpt-5.2]
|
||||||
|
|
||||||
|
<response>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Create `external-mode.json` state file
|
||||||
|
2. Extend `model-policy.json` with external models
|
||||||
|
3. Create `llm-router/` directory and scripts
|
||||||
|
4. Add provider wrappers (opencode, gemini)
|
||||||
|
5. Create delegation helper
|
||||||
|
6. Update PA with toggle commands
|
||||||
|
7. Add component-registry triggers
|
||||||
|
8. Update session-start hook
|
||||||
|
9. Test each provider
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test router directly
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --model copilot/gpt-5.2 -p "Say hello"
|
||||||
|
|
||||||
|
# Test delegation
|
||||||
|
~/.claude/mcp/llm-router/delegate.py --tier sonnet -p "What is 2+2?"
|
||||||
|
|
||||||
|
# Test toggle
|
||||||
|
/pa --external on
|
||||||
|
/pa what time is it?
|
||||||
|
/pa --external off
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
1. Set `external-mode.json` → `enabled: false`
|
||||||
|
2. All operations revert to Claude immediately
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
- Auto-fallback to external when Claude rate-limited
|
||||||
|
- Cost tracking per external model
|
||||||
|
- Response quality comparison metrics
|
||||||
|
- Additional providers (Mistral, local Ollama)
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
# Plan: Add Plan Status Tracking
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Plans in `~/.claude/plans/` have inconsistent status tracking:
|
||||||
|
- Some have inline `**Status:** Implemented`
|
||||||
|
- Most have no status marker
|
||||||
|
- No central index to query plan statuses
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Create `~/.claude/plans/index.json` as a central registry for plan metadata and status.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Index Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plans": {
|
||||||
|
"temporal-foraging-milner": {
|
||||||
|
"title": "RAG JSON-to-text transformation",
|
||||||
|
"status": "pending",
|
||||||
|
"created": "2026-01-05",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"fizzy-puzzling-candy": {
|
||||||
|
"title": "Session summarization hook",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-03",
|
||||||
|
"implemented": "2026-01-03",
|
||||||
|
"category": "feature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Values
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| `pending` | Not yet implemented |
|
||||||
|
| `implemented` | Fully implemented |
|
||||||
|
| `partial` | Partially implemented |
|
||||||
|
| `abandoned` | Decided not to implement |
|
||||||
|
| `superseded` | Replaced by another plan |
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
|
||||||
|
| Category | Meaning |
|
||||||
|
|----------|---------|
|
||||||
|
| `feature` | New capability |
|
||||||
|
| `enhancement` | Improve existing feature |
|
||||||
|
| `bugfix` | Fix an issue |
|
||||||
|
| `diagnostic` | One-time investigation (auto-complete) |
|
||||||
|
| `design` | Design document for reference |
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `~/.claude/plans/index.json` | Central plan registry |
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Step 1: Create index.json with all current plans
|
||||||
|
|
||||||
|
Populate based on our verification:
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
- wise-dazzling-marshmallow (k8s quick-status)
|
||||||
|
- fizzy-puzzling-candy (session summarization)
|
||||||
|
- shimmering-discovering-bonbon (linux sysadmin agent)
|
||||||
|
- valiant-hugging-dahl (pi50 optimization)
|
||||||
|
- cozy-strolling-nygaard (status line + keybind)
|
||||||
|
- flickering-enchanting-fiddle (restructure components)
|
||||||
|
- velvet-percolating-porcupine (no-redundancy rule)
|
||||||
|
- 2025-01-02-gcal-design
|
||||||
|
- 2026-01-01-component-registry-design
|
||||||
|
- 2026-01-01-usage-tracking-design
|
||||||
|
|
||||||
|
**Diagnostic (complete):**
|
||||||
|
- elegant-prancing-allen (vulkan verification)
|
||||||
|
- pure-wishing-metcalfe (cluster diagnosis)
|
||||||
|
- glistening-wondering-wadler (structure verification)
|
||||||
|
|
||||||
|
**Pending:**
|
||||||
|
- temporal-foraging-milner (RAG improvement)
|
||||||
|
- cosmic-frolicking-compass (Zed Wayland)
|
||||||
|
|
||||||
|
**Handoff doc (reference only):**
|
||||||
|
- shimmering-discovering-bonbon-handoff
|
||||||
|
|
||||||
|
### Step 2: Update CLAUDE.md
|
||||||
|
|
||||||
|
Add plans index to state files table.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. Single source of truth for plan statuses
|
||||||
|
2. Easy to query: `jq '.plans | to_entries[] | select(.value.status == "pending")' index.json`
|
||||||
|
3. No need to modify individual plan files
|
||||||
|
4. Can track implementation dates
|
||||||
|
|
||||||
|
## Full index.json Content
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Plan status registry",
|
||||||
|
"plans": {
|
||||||
|
"temporal-foraging-milner": {
|
||||||
|
"title": "RAG JSON-to-text transformation",
|
||||||
|
"status": "pending",
|
||||||
|
"created": "2026-01-05",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"cosmic-frolicking-compass": {
|
||||||
|
"title": "Zed Wayland compilation",
|
||||||
|
"status": "pending",
|
||||||
|
"created": "2025-12-13",
|
||||||
|
"category": "enhancement",
|
||||||
|
"notes": "External task - compile Zed with Wayland support"
|
||||||
|
},
|
||||||
|
"wise-dazzling-marshmallow": {
|
||||||
|
"title": "K8s quick-status skill",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-29",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
"fizzy-puzzling-candy": {
|
||||||
|
"title": "Session summarization hook",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-03",
|
||||||
|
"implemented": "2026-01-03",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
"shimmering-discovering-bonbon": {
|
||||||
|
"title": "Linux sysadmin agent",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
"valiant-hugging-dahl": {
|
||||||
|
"title": "Pi50 resource optimization",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-05",
|
||||||
|
"implemented": "2026-01-05",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"cozy-strolling-nygaard": {
|
||||||
|
"title": "Status line + keybind fix",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-29",
|
||||||
|
"implemented": "2025-12-29",
|
||||||
|
"category": "bugfix"
|
||||||
|
},
|
||||||
|
"flickering-enchanting-fiddle": {
|
||||||
|
"title": "Restructure components",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"velvet-percolating-porcupine": {
|
||||||
|
"title": "No-redundancy rule",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"elegant-prancing-allen": {
|
||||||
|
"title": "Vulkan verification",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-12",
|
||||||
|
"implemented": "2025-12-12",
|
||||||
|
"category": "diagnostic"
|
||||||
|
},
|
||||||
|
"pure-wishing-metcalfe": {
|
||||||
|
"title": "Cluster issue diagnosis",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-27",
|
||||||
|
"implemented": "2025-12-27",
|
||||||
|
"category": "diagnostic"
|
||||||
|
},
|
||||||
|
"glistening-wondering-wadler": {
|
||||||
|
"title": "Structure verification report",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-03",
|
||||||
|
"implemented": "2026-01-03",
|
||||||
|
"category": "diagnostic"
|
||||||
|
},
|
||||||
|
"2025-01-02-gcal-design": {
|
||||||
|
"title": "Google Calendar integration",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-31",
|
||||||
|
"implemented": "2026-01-01",
|
||||||
|
"category": "design"
|
||||||
|
},
|
||||||
|
"2026-01-01-component-registry-design": {
|
||||||
|
"title": "Component registry",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-01",
|
||||||
|
"implemented": "2026-01-01",
|
||||||
|
"category": "design"
|
||||||
|
},
|
||||||
|
"2026-01-01-usage-tracking-design": {
|
||||||
|
"title": "Usage tracking",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-31",
|
||||||
|
"implemented": "2026-01-01",
|
||||||
|
"category": "design"
|
||||||
|
},
|
||||||
|
"shimmering-discovering-bonbon-handoff": {
|
||||||
|
"title": "Linux sysadmin handoff doc",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "design",
|
||||||
|
"notes": "Reference document for shimmering-discovering-bonbon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
Single commit: "Add plans index.json for status tracking"
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Plan status registry",
|
||||||
|
"plans": {
|
||||||
|
"temporal-foraging-milner": {
|
||||||
|
"title": "RAG JSON-to-text transformation",
|
||||||
|
"status": "pending",
|
||||||
|
"created": "2026-01-05",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"cosmic-frolicking-compass": {
|
||||||
|
"title": "Zed Wayland compilation",
|
||||||
|
"status": "pending",
|
||||||
|
"created": "2025-12-13",
|
||||||
|
"category": "enhancement",
|
||||||
|
"notes": "External task - compile Zed with Wayland support"
|
||||||
|
},
|
||||||
|
"wise-dazzling-marshmallow": {
|
||||||
|
"title": "K8s quick-status skill",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-29",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
"fizzy-puzzling-candy": {
|
||||||
|
"title": "Session summarization hook",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-03",
|
||||||
|
"implemented": "2026-01-03",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
"shimmering-discovering-bonbon": {
|
||||||
|
"title": "Linux sysadmin agent",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
"valiant-hugging-dahl": {
|
||||||
|
"title": "Pi50 resource optimization",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-05",
|
||||||
|
"implemented": "2026-01-05",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"cozy-strolling-nygaard": {
|
||||||
|
"title": "Status line + keybind fix",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-29",
|
||||||
|
"implemented": "2025-12-29",
|
||||||
|
"category": "bugfix"
|
||||||
|
},
|
||||||
|
"flickering-enchanting-fiddle": {
|
||||||
|
"title": "Restructure components",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"velvet-percolating-porcupine": {
|
||||||
|
"title": "No-redundancy rule",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "enhancement"
|
||||||
|
},
|
||||||
|
"elegant-prancing-allen": {
|
||||||
|
"title": "Vulkan verification",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-12",
|
||||||
|
"implemented": "2025-12-12",
|
||||||
|
"category": "diagnostic"
|
||||||
|
},
|
||||||
|
"pure-wishing-metcalfe": {
|
||||||
|
"title": "Cluster issue diagnosis",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-27",
|
||||||
|
"implemented": "2025-12-27",
|
||||||
|
"category": "diagnostic"
|
||||||
|
},
|
||||||
|
"glistening-wondering-wadler": {
|
||||||
|
"title": "Structure verification report",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-03",
|
||||||
|
"implemented": "2026-01-03",
|
||||||
|
"category": "diagnostic"
|
||||||
|
},
|
||||||
|
"2025-01-02-gcal-design": {
|
||||||
|
"title": "Google Calendar integration",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-31",
|
||||||
|
"implemented": "2026-01-01",
|
||||||
|
"category": "design"
|
||||||
|
},
|
||||||
|
"2026-01-01-component-registry-design": {
|
||||||
|
"title": "Component registry",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-01",
|
||||||
|
"implemented": "2026-01-01",
|
||||||
|
"category": "design"
|
||||||
|
},
|
||||||
|
"2026-01-01-usage-tracking-design": {
|
||||||
|
"title": "Usage tracking",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-31",
|
||||||
|
"implemented": "2026-01-01",
|
||||||
|
"category": "design"
|
||||||
|
},
|
||||||
|
"shimmering-discovering-bonbon-handoff": {
|
||||||
|
"title": "Linux sysadmin handoff doc",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2025-12-28",
|
||||||
|
"implemented": "2025-12-28",
|
||||||
|
"category": "design",
|
||||||
|
"notes": "Reference document for shimmering-discovering-bonbon"
|
||||||
|
},
|
||||||
|
"golden-imagining-engelbart": {
|
||||||
|
"title": "Plan status tracking",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-07",
|
||||||
|
"implemented": "2026-01-07",
|
||||||
|
"category": "enhancement",
|
||||||
|
"notes": "This plan - meta!"
|
||||||
|
},
|
||||||
|
"gleaming-routing-mercury": {
|
||||||
|
"title": "External LLM integration",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-08",
|
||||||
|
"implemented": "2026-01-08",
|
||||||
|
"category": "feature",
|
||||||
|
"notes": "fc-004 - Cloud API integration via opencode/gemini CLIs with session toggle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Implementation Plan: OpenCode Claude Sync Enhancements
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Transpose Claude Code agent/skill setup to OpenCode in parallel, per decisions from brainstorming session (`enumerated-giggling-scone.md`).
|
||||||
|
|
||||||
|
## Key Decisions (from brainstorming)
|
||||||
|
|
||||||
|
| Decision | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Primary agent | Use built-in `build` (don't port PA) |
|
||||||
|
| Agents to skip | `personal-assistant`, `master-orchestrator` |
|
||||||
|
| Other agents | All become `mode: subagent` |
|
||||||
|
| Model inheritance | Use `model: inherit` for subagents |
|
||||||
|
| State sharing | Reference via `instructions`, don't copy |
|
||||||
|
| Source of truth | Claude Code (`~/.claude/`) |
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
1. `~/.config/opencode/scripts/claude_sync.py` - Main sync script
|
||||||
|
2. `~/.config/opencode/opencode.json` - Config file
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
1. `~/.config/opencode/README.md` - Documentation
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Step 1: Backup (DONE)
|
||||||
|
|
||||||
|
Created backups:
|
||||||
|
- `~/.config/opencode-backup-20260107_120135.tar.gz`
|
||||||
|
- `~/opencode-home-backup-20260107_120136.tar.gz`
|
||||||
|
|
||||||
|
### Step 2: Enhance `claude_sync.py`
|
||||||
|
|
||||||
|
**Location**: `~/.config/opencode/scripts/claude_sync.py`
|
||||||
|
|
||||||
|
**Modifications**:
|
||||||
|
|
||||||
|
1. Add constants near top of file:
|
||||||
|
```python
|
||||||
|
SKIP_AGENTS = {"personal-assistant", "master-orchestrator"}
|
||||||
|
|
||||||
|
MODEL_MAP = {
|
||||||
|
"opus": "anthropic/claude-opus-4",
|
||||||
|
"sonnet": "anthropic/claude-sonnet-4-5",
|
||||||
|
"haiku": "anthropic/claude-haiku-4-5",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Modify `transform_frontmatter()` for agents:
|
||||||
|
- Check if agent name in `SKIP_AGENTS`, return `None` to signal skip
|
||||||
|
- Add `frontmatter["mode"] = "subagent"`
|
||||||
|
- Set `frontmatter["model"] = "inherit"`
|
||||||
|
- Map explicit models using `MODEL_MAP`
|
||||||
|
|
||||||
|
3. Modify `sync_tree()` to handle `None` return from transform (skip file)
|
||||||
|
|
||||||
|
4. Update `expected_dest_paths_for_tree()` to exclude skipped agents
|
||||||
|
|
||||||
|
### Step 3: Run Sync
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --dry-run
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py
|
||||||
|
python3 ~/.config/opencode/scripts/claude_sync.py --clean --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update `opencode.json`
|
||||||
|
|
||||||
|
Add to existing config:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
|
"small_model": "anthropic/claude-haiku-4-5",
|
||||||
|
"instructions": [
|
||||||
|
"~/.claude/CLAUDE.md",
|
||||||
|
"~/.claude/state/kb.json",
|
||||||
|
"~/.claude/state/personal-assistant/memory/facts.json",
|
||||||
|
"~/.claude/state/personal-assistant/memory/preferences.json"
|
||||||
|
],
|
||||||
|
"permission": {
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": {
|
||||||
|
"*": "ask",
|
||||||
|
"pacman -Q*": "allow",
|
||||||
|
"systemctl status*": "allow",
|
||||||
|
"kubectl get*": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Test
|
||||||
|
|
||||||
|
- Run `opencode` and verify skill discovery
|
||||||
|
- Test `@linux-sysadmin` subagent invocation
|
||||||
|
- Verify permissions work
|
||||||
|
|
||||||
|
### Step 6: Create README.md
|
||||||
|
|
||||||
|
Document:
|
||||||
|
- Architecture (Claude Code as source of truth)
|
||||||
|
- Sync workflow
|
||||||
|
- Agent mapping table
|
||||||
|
- How to invoke subagents
|
||||||
|
|
||||||
|
### Step 7: Add Future Consideration
|
||||||
|
|
||||||
|
Add entry to `~/.claude/state/future-considerations.json` about JSON minification for large instruction files.
|
||||||
|
|
||||||
|
## Estimated Time
|
||||||
|
|
||||||
|
~2 hours total (Step 1 already done)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Plan: Improve RAG Personal Index JSON-to-Natural-Language Transformation
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The RAG personal index produces low-quality matches for semantic queries because it indexes raw JSON structure rather than natural language.
|
||||||
|
|
||||||
|
**Example failure:**
|
||||||
|
- Query: "how to add a new agent"
|
||||||
|
- Expected: Match `system-instructions.json` → `processes.agent-lifecycle.add`
|
||||||
|
- Actual: Score 0.479, returns generic agent mentions instead
|
||||||
|
|
||||||
|
**Root cause:** The chunker doesn't recognize process structures with `add`/`remove`/`rules`/`requirements` arrays, so they fall through to raw JSON stringification.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Enhance `index_personal.py` to transform JSON structures into natural language at index time.
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
1. `~/.claude/skills/rag-search/scripts/index_personal.py` - Main changes
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### 1. Add Process Pattern Recognition (lines ~127-138)
|
||||||
|
|
||||||
|
Add handling for process objects with action arrays:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Process with action arrays (add, remove, rules, requirements, etc.)
|
||||||
|
action_keys = ["add", "remove", "rules", "requirements", "steps", "validate"]
|
||||||
|
if any(key in item for key in action_keys):
|
||||||
|
parts = []
|
||||||
|
if context:
|
||||||
|
parts.append(f"{context}:")
|
||||||
|
if item.get("description"):
|
||||||
|
parts.append(item["description"])
|
||||||
|
|
||||||
|
for action_key in action_keys:
|
||||||
|
if action_key in item and isinstance(item[action_key], list):
|
||||||
|
action_text = f"To {action_key}: " + ". ".join(item[action_key])
|
||||||
|
parts.append(action_text)
|
||||||
|
|
||||||
|
if parts:
|
||||||
|
yield (" ".join(parts), {**base_metadata, "process": context})
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Improve Context Propagation
|
||||||
|
|
||||||
|
When processing nested dicts, pass richer context:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In the top-level dict processing (line ~154-161)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
# Pass the key as context for better chunk text
|
||||||
|
yield from process_item(value, context=key)
|
||||||
|
```
|
||||||
|
|
||||||
|
Already done, but ensure action arrays get the context.
|
||||||
|
|
||||||
|
### 3. Handle Key-Value Pairs in Processes
|
||||||
|
|
||||||
|
For structures like:
|
||||||
|
```json
|
||||||
|
"content-principles": {
|
||||||
|
"no-redundancy": "Information lives in one authoritative location",
|
||||||
|
"lean-files": "Keep files concise..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Transform to: `"content-principles: no-redundancy means information lives in one authoritative location. lean-files means keep files concise..."`
|
||||||
|
|
||||||
|
### 4. Add Tests
|
||||||
|
|
||||||
|
Create a simple test to verify transformation quality:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After reindex, verify the failing query now works
|
||||||
|
~/.claude/skills/rag-search/scripts/search.py "how to add a new agent" --index personal
|
||||||
|
# Should return system-instructions.json with score > 0.7
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
| Query | Before | After |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| "how to add a new agent" | 0.479, wrong file | >0.7, system-instructions.json |
|
||||||
|
| "agent lifecycle" | Similar | Better match to process |
|
||||||
|
| "model selection rules" | Depends | Match model-selection process |
|
||||||
|
|
||||||
|
## Validation Steps
|
||||||
|
|
||||||
|
1. Run modified indexer
|
||||||
|
2. Test the three queries above
|
||||||
|
3. Compare scores and result relevance
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
If results degrade: `git checkout scripts/index_personal.py && reindex`
|
||||||
|
|
||||||
|
## Post-Implementation
|
||||||
|
|
||||||
|
Add to `future-considerations.json`:
|
||||||
|
- RAG indexer debug/verbose mode to inspect what text is being indexed
|
||||||
|
|
||||||
|
## Future Considerations (Deferred)
|
||||||
|
|
||||||
|
- Natural language templates per JSON schema type
|
||||||
|
- LLM-generated summaries of complex structures
|
||||||
|
- Caching transformed text alongside original JSON
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
"frontend-design@claude-plugins-official": [
|
"frontend-design@claude-plugins-official": [
|
||||||
{
|
{
|
||||||
"scope": "user",
|
"scope": "user",
|
||||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/frontend-design/15b07b46dab3",
|
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/frontend-design/b97f6eadd929",
|
||||||
"version": "15b07b46dab3",
|
"version": "b97f6eadd929",
|
||||||
"installedAt": "2025-12-24T19:08:12.422Z",
|
"installedAt": "2025-12-24T19:08:12.422Z",
|
||||||
"lastUpdated": "2026-01-05T07:21:36.978Z",
|
"lastUpdated": "2026-01-07T08:00:06.726Z",
|
||||||
"gitCommitSha": "6d3752c000e2b3d0e6137bd7adb04895d6f40f14",
|
"gitCommitSha": "6d3752c000e2b3d0e6137bd7adb04895d6f40f14",
|
||||||
"isLocal": true
|
"isLocal": true
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,10 @@
|
|||||||
"commit-commands@claude-plugins-official": [
|
"commit-commands@claude-plugins-official": [
|
||||||
{
|
{
|
||||||
"scope": "user",
|
"scope": "user",
|
||||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/commit-commands/15b07b46dab3",
|
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/commit-commands/b97f6eadd929",
|
||||||
"version": "15b07b46dab3",
|
"version": "b97f6eadd929",
|
||||||
"installedAt": "2025-12-24T19:10:05.451Z",
|
"installedAt": "2025-12-24T19:10:05.451Z",
|
||||||
"lastUpdated": "2026-01-05T07:21:36.984Z",
|
"lastUpdated": "2026-01-07T08:00:06.734Z",
|
||||||
"isLocal": true
|
"isLocal": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -69,10 +69,10 @@
|
|||||||
"ralph-wiggum@claude-plugins-official": [
|
"ralph-wiggum@claude-plugins-official": [
|
||||||
{
|
{
|
||||||
"scope": "user",
|
"scope": "user",
|
||||||
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/ralph-wiggum/15b07b46dab3",
|
"installPath": "/home/will/.claude/plugins/cache/claude-plugins-official/ralph-wiggum/883f2ba69e50",
|
||||||
"version": "15b07b46dab3",
|
"version": "883f2ba69e50",
|
||||||
"installedAt": "2026-01-02T19:47:02.395Z",
|
"installedAt": "2026-01-02T19:47:02.395Z",
|
||||||
"lastUpdated": "2026-01-05T07:21:36.997Z",
|
"lastUpdated": "2026-01-06T20:00:15.709Z",
|
||||||
"gitCommitSha": "de89f3066c68d7a2f2d4190173fa46c26e2f30fd",
|
"gitCommitSha": "de89f3066c68d7a2f2d4190173fa46c26e2f30fd",
|
||||||
"isLocal": true
|
"isLocal": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"repo": "anthropics/claude-plugins-official"
|
"repo": "anthropics/claude-plugins-official"
|
||||||
},
|
},
|
||||||
"installLocation": "/home/will/.claude/plugins/marketplaces/claude-plugins-official",
|
"installLocation": "/home/will/.claude/plugins/marketplaces/claude-plugins-official",
|
||||||
"lastUpdated": "2026-01-05T20:44:45.874Z"
|
"lastUpdated": "2026-01-07T19:06:34.488Z"
|
||||||
},
|
},
|
||||||
"superpowers-marketplace": {
|
"superpowers-marketplace": {
|
||||||
"source": {
|
"source": {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Morning Report - Tue Jan 06, 2026
|
||||||
|
|
||||||
|
## 🌤 Weather
|
||||||
|
Seattle: 43°F, Light rain, mist | High 45° Low 38°
|
||||||
|
|
||||||
|
## 📧 Email
|
||||||
|
15 unread
|
||||||
|
• Capital One | Quicks - Your requested balance summary
|
||||||
|
• Chase - Your Chase Freedom Unlimited Visa balanc
|
||||||
|
• Experian - William, check out these cards with an i
|
||||||
|
• Delta Air Lines - Discover An Experience Curated For Membe
|
||||||
|
• DoorDash - Save up to $10 on groceries and more eac
|
||||||
|
|
||||||
|
## 📅 Today
|
||||||
|
No events today
|
||||||
|
|
||||||
|
## 📈 Stocks
|
||||||
|
CRWV $77.94 +1.4% ▲ NVDA $187.24 -0.5% ▼ MSFT $478.51 +1.2% ▲
|
||||||
|
|
||||||
|
## 🖥 Infrastructure
|
||||||
|
K8s: 🟡 | Workstation: 🟢
|
||||||
|
└ K8s: 2 pods not running
|
||||||
|
|
||||||
|
## 📰 Tech News
|
||||||
|
• Comparing AI agents to cybersecurity professionals in real-w... (Hacker News)
|
||||||
|
• Oral microbiome sequencing after taking probiotics (Hacker News)
|
||||||
|
• The Best Line Length is 88 (Lobsters)
|
||||||
|
• There Were BGP Anomalies During The Venezuela Blackout (Lobsters)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated: 2026-01-06 13:40:51 PT*
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Morning Report - Wed Jan 07, 2026
|
||||||
|
|
||||||
|
## 🌤 Weather
|
||||||
|
Overcast 40°F (feels 33°F), 86% humidity, rain possible — bring umbrella
|
||||||
|
|
||||||
|
## 📧 Email
|
||||||
|
10 unread, no urgent items
|
||||||
|
|
||||||
|
• Experian Alerts - Your FICO® Score went up. Nice work!
|
||||||
|
• Experteer - 3 new opportunities for "AWS Architect"
|
||||||
|
• Experian - December spending report is here
|
||||||
|
• Chase - Freedom Unlimited balance is $538.97
|
||||||
|
• Chase - Rewards balance has reached 0 POINTS
|
||||||
|
|
||||||
|
## 📅 Today
|
||||||
|
No events today
|
||||||
|
|
||||||
|
## 📈 Stocks
|
||||||
|
▲ CRWV $78.98 (+1.3%) | ▲ NVDA $189.74 (+1.3%) | ▲ MSFT $488.96 (+2.2%)
|
||||||
|
|
||||||
|
## ✅ Tasks
|
||||||
|
6 pending
|
||||||
|
• 5:00 PM - Dinner at Lecosho or Japonessa
|
||||||
|
• 3:00 PM - Snack at Le Panier or Mee Sum
|
||||||
|
• 3:15 PM - Seattle Art Museum (Impressionism Exhibit)
|
||||||
|
• 2:30 PM - Route 7 Bus to Downtown
|
||||||
|
• 2:00 PM - Coffee at QED (Mt Baker)
|
||||||
|
... and 1 more
|
||||||
|
|
||||||
|
## 🖥 Infrastructure
|
||||||
|
K8s: 🟡 | Workstation: 🟢
|
||||||
|
└ K8s: 1 pods not running
|
||||||
|
|
||||||
|
## 📰 Tech News
|
||||||
|
• Eat Real Food – Introducing the New Pyramid (Hacker News)
|
||||||
|
• The $14 Burrito: Why San Francisco Inflation Feels Higher Than 2.5% (Hacker News)
|
||||||
|
• Health care data breach affects over 600k patients, Illinois agency says (Hacker News)
|
||||||
|
• Why the trans flag emoji is the 5-codepoint sequence it is (Lobsters)
|
||||||
|
• A4 Paper Stories (Lobsters)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated: 2026-01-07 09:58:25 PT*
|
||||||
+27
-12
@@ -1,27 +1,42 @@
|
|||||||
# Morning Report - Mon Jan 05, 2026
|
# Morning Report - Wed Jan 07, 2026
|
||||||
|
|
||||||
## 🌤 Weather
|
## 🌤 Weather
|
||||||
Overcast, 44°F (feels 41°F), rain likely—bring umbrella ☔
|
Overcast 40°F (feels 33°F), 86% humidity, rain possible — bring umbrella
|
||||||
|
|
||||||
## 📧 Email
|
## 📧 Email
|
||||||
⚠️ Could not fetch emails: No module named 'pydantic_core._pydantic_core'
|
10 unread, no urgent items
|
||||||
|
|
||||||
|
• Experian Alerts - Your FICO® Score went up. Nice work!
|
||||||
|
• Experteer - 3 new opportunities for "AWS Architect"
|
||||||
|
• Experian - December spending report is here
|
||||||
|
• Chase - Freedom Unlimited balance is $538.97
|
||||||
|
• Chase - Rewards balance has reached 0 POINTS
|
||||||
|
|
||||||
## 📅 Today
|
## 📅 Today
|
||||||
⚠️ Could not fetch calendar: No module named 'pydantic_core._pydantic_core'
|
No events today
|
||||||
|
|
||||||
## 📈 Stocks
|
## 📈 Stocks
|
||||||
CRWV $77.64 ▼2.1% | NVDA $187.84 ▼0.5% | MSFT $473.50 ▲0.1%
|
▲ CRWV $78.98 (+1.3%) | ▲ NVDA $189.74 (+1.3%) | ▲ MSFT $488.96 (+2.2%)
|
||||||
|
|
||||||
|
## ✅ Tasks
|
||||||
|
6 pending
|
||||||
|
• 5:00 PM - Dinner at Lecosho or Japonessa
|
||||||
|
• 3:00 PM - Snack at Le Panier or Mee Sum
|
||||||
|
• 3:15 PM - Seattle Art Museum (Impressionism Exhibit)
|
||||||
|
• 2:30 PM - Route 7 Bus to Downtown
|
||||||
|
• 2:00 PM - Coffee at QED (Mt Baker)
|
||||||
|
... and 1 more
|
||||||
|
|
||||||
## 🖥 Infrastructure
|
## 🖥 Infrastructure
|
||||||
K8s: 🟡 | Workstation: 🟢
|
K8s: 🟡 | Workstation: 🟢
|
||||||
└ K8s: 2 pods not running
|
└ K8s: 1 pods not running
|
||||||
|
|
||||||
## 📰 Tech News
|
## 📰 Tech News
|
||||||
• O-Ring Automation (Hacker News)
|
• Eat Real Food – Introducing the New Pyramid (Hacker News)
|
||||||
• Novo Nordisk launches Wegovy weight-loss pill in US, triggering price war (Hacker News)
|
• The $14 Burrito: Why San Francisco Inflation Feels Higher Than 2.5% (Hacker News)
|
||||||
• Refactoring – Not on the backlog (Hacker News)
|
• Health care data breach affects over 600k patients, Illinois agency says (Hacker News)
|
||||||
• It's hard to justify Tahoe icons (Lobsters)
|
• Why the trans flag emoji is the 5-codepoint sequence it is (Lobsters)
|
||||||
• Databases in 2025: A Year in Review (Lobsters)
|
• A4 Paper Stories (Lobsters)
|
||||||
|
|
||||||
---
|
---
|
||||||
*Generated: 2026-01-05 12:44:47 PT*
|
*Generated: 2026-01-07 09:58:25 PT*
|
||||||
+2
-1
@@ -13,5 +13,6 @@
|
|||||||
"ralph-wiggum@claude-plugins-official": true
|
"ralph-wiggum@claude-plugins-official": true
|
||||||
},
|
},
|
||||||
"alwaysThinkingEnabled": true,
|
"alwaysThinkingEnabled": true,
|
||||||
"_note": "Agent definitions moved to ~/.claude/agents/*.md with YAML frontmatter. Autonomy levels now in ~/.claude/state/autonomy-levels.json"
|
"_note": "Agent definitions moved to ~/.claude/agents/*.md with YAML frontmatter. Autonomy levels now in ~/.claude/state/autonomy-levels.json",
|
||||||
|
"model": "opus"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"show_tomorrow": true
|
"show_tomorrow": true
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"max_display": 5,
|
"max_display": 5,
|
||||||
"show_due_dates": true
|
"show_due_dates": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -132,6 +132,34 @@
|
|||||||
"to-do",
|
"to-do",
|
||||||
"pending"
|
"pending"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"guardrails": {
|
||||||
|
"description": "PreToolUse hook that prevents dangerous operations (rm -rf, system commands, etc.)",
|
||||||
|
"script": "~/.claude/hooks/scripts/guardrail.py",
|
||||||
|
"config": "~/.claude/state/guardrails.json",
|
||||||
|
"triggers": [
|
||||||
|
"guardrail",
|
||||||
|
"safety",
|
||||||
|
"block dangerous",
|
||||||
|
"protect"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"external-mode": {
|
||||||
|
"description": "Toggle between Claude and external LLMs (Copilot, Z.AI, Gemini)",
|
||||||
|
"script": "~/.claude/mcp/llm-router/toggle.py",
|
||||||
|
"triggers": [
|
||||||
|
"external",
|
||||||
|
"use external",
|
||||||
|
"switch to external",
|
||||||
|
"external models",
|
||||||
|
"external only",
|
||||||
|
"use copilot",
|
||||||
|
"use opencode",
|
||||||
|
"back to claude",
|
||||||
|
"use claude again",
|
||||||
|
"disable external",
|
||||||
|
"external mode"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -171,6 +199,15 @@
|
|||||||
],
|
],
|
||||||
"invokes": "skill:usage"
|
"invokes": "skill:usage"
|
||||||
},
|
},
|
||||||
|
"/external": {
|
||||||
|
"description": "Toggle and use external LLM mode (GPT-5.2, Gemini, etc.)",
|
||||||
|
"aliases": [
|
||||||
|
"/llm",
|
||||||
|
"/ext",
|
||||||
|
"/external-llm"
|
||||||
|
],
|
||||||
|
"invokes": "command:external"
|
||||||
|
},
|
||||||
"/README": {
|
"/README": {
|
||||||
"description": "TODO",
|
"description": "TODO",
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"activated_at": null,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"created": "2024-12-28",
|
"created": "2024-12-28",
|
||||||
"notes": "Design complete. Partially implemented: node_exporter running (port 9100), PrometheusRule deployed (12 alerts: 4 critical, 6 warning, 2 info), Prometheus scrape config updated. ⏳ PENDING TAILSACLE CONFIGURATION: Tailscale network attempted but ACLs preventing cluster ↔ workstation peering. See charts/willlaptop-monitoring/TAILSCALE-ACL-GUIDE.md for configuration steps. After ACL configuration, Prometheus should successfully scrape workstation metrics via 100.90.159.78:9100."
|
"notes": "Design complete. Partially implemented: node_exporter running (port 9100), PrometheusRule deployed (12 alerts: 4 critical, 6 warning, 2 info), Prometheus scrape config updated. \u23f3 PENDING TAILSACLE CONFIGURATION: Tailscale network attempted but ACLs preventing cluster \u2194 workstation peering. See charts/willlaptop-monitoring/TAILSCALE-ACL-GUIDE.md for configuration steps. After ACL configuration, Prometheus should successfully scrape workstation metrics via 100.90.159.78:9100."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fc-002",
|
"id": "fc-002",
|
||||||
@@ -37,10 +37,12 @@
|
|||||||
"category": "integration",
|
"category": "integration",
|
||||||
"title": "External LLM integration",
|
"title": "External LLM integration",
|
||||||
"description": "Support for non-Claude models in the agent system",
|
"description": "Support for non-Claude models in the agent system",
|
||||||
"priority": "low",
|
"priority": "medium",
|
||||||
"status": "deferred",
|
"status": "resolved",
|
||||||
"created": "2024-12-28",
|
"created": "2024-12-28",
|
||||||
"notes": "For specialized tasks or cost optimization"
|
"resolved": "2026-01-08",
|
||||||
|
"plan": "gleaming-routing-mercury",
|
||||||
|
"notes": "Implemented via opencode CLI. Models: github-copilot/gpt-5.2, zai-coding-plan/glm-4.7, etc. Toggle via /pa --external or natural language."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fc-005",
|
"id": "fc-005",
|
||||||
@@ -467,6 +469,16 @@
|
|||||||
"status": "deferred",
|
"status": "deferred",
|
||||||
"created": "2025-01-21",
|
"created": "2025-01-21",
|
||||||
"notes": "Optimization for when query volume justifies it. Consider TTL and invalidation strategy."
|
"notes": "Optimization for when query volume justifies it. Consider TTL and invalidation strategy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-047",
|
||||||
|
"category": "opencode",
|
||||||
|
"title": "Minify JSON instructions for OpenCode",
|
||||||
|
"description": "Consider minifying JSON files loaded via OpenCode instructions config when files grow large",
|
||||||
|
"priority": "low",
|
||||||
|
"status": "deferred",
|
||||||
|
"created": "2026-01-07",
|
||||||
|
"notes": "Currently files are <1KB total, not worth minifying. Revisit if kb.json or memory files exceed 50KB. Could add minify step to claude_sync.py."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"safe_paths": [
|
||||||
|
"~/.claude",
|
||||||
|
"~/projects"
|
||||||
|
],
|
||||||
|
"blocked_paths": [
|
||||||
|
"/etc",
|
||||||
|
"/usr",
|
||||||
|
"/var",
|
||||||
|
"/boot",
|
||||||
|
"/sys",
|
||||||
|
"/proc",
|
||||||
|
"~/.ssh",
|
||||||
|
"~/.gnupg",
|
||||||
|
"~/.aws"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"bash": [
|
||||||
|
{"pattern": "rm -rf /($|[^a-zA-Z])", "action": "block", "name": "rm_rf_root"},
|
||||||
|
{"pattern": "rm -rf ~($|[^a-zA-Z])", "action": "block", "name": "rm_rf_home"},
|
||||||
|
{"pattern": "rm -rf \\*", "action": "block", "name": "rm_rf_wildcard"},
|
||||||
|
{"pattern": "chmod -R 777", "action": "block", "name": "chmod_777"},
|
||||||
|
{"pattern": ":\\(\\)\\{ :\\|:& \\};:", "action": "block", "name": "fork_bomb"},
|
||||||
|
{"pattern": "mkfs\\.", "action": "block", "name": "mkfs"},
|
||||||
|
{"pattern": "dd .* of=/dev/", "action": "block", "name": "dd_device"},
|
||||||
|
{"pattern": "> /dev/sd[a-z]", "action": "block", "name": "overwrite_device"},
|
||||||
|
{"pattern": "shutdown", "action": "confirm", "name": "shutdown"},
|
||||||
|
{"pattern": "reboot", "action": "confirm", "name": "reboot"},
|
||||||
|
{"pattern": "systemctl (stop|disable|mask)", "action": "confirm", "name": "systemctl_destructive"},
|
||||||
|
{"pattern": "rm ", "action": "confirm", "name": "rm_outside_safe", "outside_safe_paths": true},
|
||||||
|
{"pattern": "kubectl delete", "action": "confirm", "name": "kubectl_delete"},
|
||||||
|
{"pattern": "docker rm", "action": "confirm", "name": "docker_rm"},
|
||||||
|
{"pattern": "docker system prune", "action": "confirm", "name": "docker_prune"}
|
||||||
|
],
|
||||||
|
"write": [
|
||||||
|
{"path_match": "blocked_paths", "action": "block", "name": "write_blocked_path"},
|
||||||
|
{"path_match": "outside_safe_paths", "action": "confirm", "name": "write_outside_safe"}
|
||||||
|
],
|
||||||
|
"edit": [
|
||||||
|
{"path_match": "blocked_paths", "action": "block", "name": "edit_blocked_path"},
|
||||||
|
{"path_match": "outside_safe_paths", "action": "confirm", "name": "edit_outside_safe"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+171
-1
@@ -1 +1,171 @@
|
|||||||
{"infra":{"cluster":"k0s","nodes":3,"arch":"arm64"},"svc":{"gitops":"argocd","mon":"prometheus","alerts":"alertmanager"},"net":{},"hw":{"pi5_8gb":2,"pi3_1gb":1}}
|
{
|
||||||
|
"infra": {
|
||||||
|
"cluster": "k0s",
|
||||||
|
"nodes": 3,
|
||||||
|
"arch": "arm64",
|
||||||
|
"storage": "longhorn",
|
||||||
|
"storage_class": "longhorn",
|
||||||
|
"backup": "longhorn-backup + minio-to-mega"
|
||||||
|
},
|
||||||
|
"hw": {
|
||||||
|
"pi5_8gb": 2,
|
||||||
|
"pi3_1gb": 1,
|
||||||
|
"roles": {
|
||||||
|
"control_plane": "pi5",
|
||||||
|
"workers": ["pi5", "pi3"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net": {
|
||||||
|
"metallb_pool": "192.168.153.240-192.168.153.254",
|
||||||
|
"ingress_nginx_ip": "192.168.153.240",
|
||||||
|
"ingress_haproxy_ip": "192.168.153.241",
|
||||||
|
"tailnet": "taildb3494.ts.net",
|
||||||
|
"dns_pattern": "<app>.<ns>.<ip>.nip.io"
|
||||||
|
},
|
||||||
|
"svc": {
|
||||||
|
"gitops": "argocd",
|
||||||
|
"monitoring": {
|
||||||
|
"metrics": "kube-prometheus-stack",
|
||||||
|
"logs": "loki-stack",
|
||||||
|
"alerts": "alertmanager",
|
||||||
|
"dashboards": "grafana"
|
||||||
|
},
|
||||||
|
"ingress": ["nginx-ingress-controller", "haproxy-ingress"],
|
||||||
|
"storage": ["longhorn", "local-path-storage", "minio"],
|
||||||
|
"networking": ["metallb", "tailscale-operator"]
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"ai_stack": {
|
||||||
|
"namespace": "ai-stack",
|
||||||
|
"components": ["open-webui", "ollama", "litellm", "searxng", "n8n", "vllm"],
|
||||||
|
"models": ["gpt-oss:120b", "qwen3-coder"],
|
||||||
|
"ollama_host": "100.85.116.57:11434"
|
||||||
|
},
|
||||||
|
"home": ["home-assistant", "pihole", "plex"],
|
||||||
|
"infra": ["gitea", "docker-registry", "kubernetes-dashboard"],
|
||||||
|
"other": ["ghost", "tor-controller", "speedtest-tracker"]
|
||||||
|
},
|
||||||
|
"namespaces": [
|
||||||
|
"ai-stack", "argocd", "monitoring", "loki-system", "longhorn-system",
|
||||||
|
"metallb-system", "minio", "nginx-ingress-controller", "tailscale-operator",
|
||||||
|
"gitea", "home-assistant", "pihole", "pihole2", "plex", "ghost",
|
||||||
|
"kubernetes-dashboard", "docker-registry", "k8s-agent", "tools", "vpa"
|
||||||
|
],
|
||||||
|
"urls": {
|
||||||
|
"grafana": "grafana.monitoring.192.168.153.240.nip.io",
|
||||||
|
"longhorn": "ui.longhorn-system.192.168.153.240.nip.io",
|
||||||
|
"open_webui": "oi.ai-stack.192.168.153.240.nip.io",
|
||||||
|
"searxng": "sx.ai-stack.192.168.153.240.nip.io",
|
||||||
|
"n8n": "n8n.ai-stack.192.168.153.240.nip.io",
|
||||||
|
"minio_console": "console.minio.192.168.153.240.nip.io",
|
||||||
|
"pihole": "pihole.192.168.153.240.nip.io",
|
||||||
|
"k8s_dashboard": "dashboard.kubernetes-dashboards.192.168.153.240.nip.io",
|
||||||
|
"home_assistant": "ha.home-assistant.192.168.153.241.nip.io",
|
||||||
|
"plex": "player.plex.192.168.153.246.nip.io"
|
||||||
|
},
|
||||||
|
"external_llm": {
|
||||||
|
"description": "Route requests to external LLMs via opencode or gemini CLI",
|
||||||
|
"state_file": "~/.claude/state/external-mode.json",
|
||||||
|
"router_dir": "~/.claude/mcp/llm-router/",
|
||||||
|
"commands": {
|
||||||
|
"toggle_on": "~/.claude/mcp/llm-router/toggle.py on --reason 'reason'",
|
||||||
|
"toggle_off": "~/.claude/mcp/llm-router/toggle.py off",
|
||||||
|
"status": "~/.claude/mcp/llm-router/toggle.py status",
|
||||||
|
"invoke": "~/.claude/mcp/llm-router/invoke.py --model MODEL -p 'prompt'"
|
||||||
|
},
|
||||||
|
"providers": ["opencode", "gemini"],
|
||||||
|
"tiers": {
|
||||||
|
"frontier": ["github-copilot/gpt-5.2", "github-copilot/gemini-3-pro-preview", "gemini/gemini-2.5-pro"],
|
||||||
|
"mid-tier": ["github-copilot/gpt-5-mini", "github-copilot/claude-sonnet-4.5", "github-copilot/gemini-3-flash-preview", "opencode/grok-code", "gemini/gemini-2.5-flash"],
|
||||||
|
"lightweight": ["opencode/gpt-5-nano", "zai-coding-plan/glm-4.5-air", "github-copilot/claude-haiku-4.5"]
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "github-copilot/gpt-5.2",
|
||||||
|
"code-generation": "github-copilot/gemini-3-pro-preview",
|
||||||
|
"long-context": "gemini/gemini-2.5-pro",
|
||||||
|
"fast": "github-copilot/gemini-3-flash-preview",
|
||||||
|
"default": "github-copilot/claude-sonnet-4.5"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"opencode_path": "/home/linuxbrew/.linuxbrew/bin/opencode (NOT /usr/bin/opencode which crashes)",
|
||||||
|
"o3_removed": "github-copilot/o3 not available via GitHub Copilot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workstation": {
|
||||||
|
"hostname": "willlaptop",
|
||||||
|
"ip": "192.168.153.117",
|
||||||
|
"os": "Arch Linux",
|
||||||
|
"desktop": "GNOME",
|
||||||
|
"shell": "fish",
|
||||||
|
"terminal": ["ghostty", "alacritty", "gnome-console"],
|
||||||
|
"network": "systemd-networkd + iwd",
|
||||||
|
"theme": "Dracula",
|
||||||
|
"editors": ["vscode", "zed", "vim"],
|
||||||
|
"browsers": ["firefox", "chromium", "google-chrome", "zen-browser", "epiphany"],
|
||||||
|
"virtualization": ["docker", "podman", "distrobox", "virt-manager", "virtualbox", "gnome-boxes"],
|
||||||
|
"k8s_tools": ["k9s", "k0s-bin", "k0sctl-bin", "argocd", "krew", "kubecolor"],
|
||||||
|
"dev_langs": ["go", "rust", "python", "typescript", "zig", "bun", "node/npm/pnpm"],
|
||||||
|
"ai_local": {
|
||||||
|
"ollama": true,
|
||||||
|
"llama_swap": true,
|
||||||
|
"models": ["Qwen3-4b", "Gemma3-4b"]
|
||||||
|
},
|
||||||
|
"backup": ["restic", "timeshift", "btrbk", "chezmoi"],
|
||||||
|
"dotfiles": "chezmoi"
|
||||||
|
},
|
||||||
|
"repos": {
|
||||||
|
"willlaptop": {
|
||||||
|
"path": "~/Code/active/devops/willlaptop",
|
||||||
|
"remote": "git@gitea-gitea-ssh.taildb3494.ts.net:will/willlaptop.git",
|
||||||
|
"purpose": "Workstation provisioning and config",
|
||||||
|
"structure": {
|
||||||
|
"ansible/": "Machine provisioning playbooks",
|
||||||
|
"ansible/roles/common/": "Hostname, network, users, SSH config",
|
||||||
|
"ansible/roles/packages/": "Package installation (pacman, AUR, flatpak, appimage)",
|
||||||
|
"ansible/roles/packages/files/": "Package lists (pkglist.txt, aur_pkglist.txt, etc)",
|
||||||
|
"docker/": "Local Docker stacks",
|
||||||
|
"scripts/": "Utility scripts (backup, sync, networking)",
|
||||||
|
"MCP/": "MCP server configs",
|
||||||
|
"local_ollama/": "Local Ollama data"
|
||||||
|
},
|
||||||
|
"ansible_tags": ["network", "wifi", "ethernet", "users", "sshd", "pacman", "aur", "flatpak", "appimage"],
|
||||||
|
"docker_stacks": ["file_browser", "minio-longhorn-backup", "rancher-cleanup"],
|
||||||
|
"scripts": ["bridge-up.sh", "chezmoi-sync.sh", "curl-s3.sh", "kvm-bridge-setup.sh",
|
||||||
|
"rclone-sync.sh", "restic-backup.sh", "restic-clean.sh"]
|
||||||
|
},
|
||||||
|
"homelab": {
|
||||||
|
"path": "~/Code/active/devops/homelab/homelab",
|
||||||
|
"remote": "git@github.com:will666/homelab.git",
|
||||||
|
"symlink": "~/.claude/repos/homelab",
|
||||||
|
"structure": {
|
||||||
|
"ansible/": "Ansible playbooks and templates for node provisioning",
|
||||||
|
"argocd/": "ArgoCD Application manifests (one per service)",
|
||||||
|
"charts/": "Helm values and raw manifests per service",
|
||||||
|
"charts/<svc>/values.yaml": "Helm chart values override",
|
||||||
|
"charts/<svc>/manifests/": "Raw K8s manifests (non-Helm resources)",
|
||||||
|
"docker/": "Docker Compose stacks for non-K8s workloads"
|
||||||
|
},
|
||||||
|
"charts": [
|
||||||
|
"ai-stack", "argocd", "argo-workflow", "cdi-operator",
|
||||||
|
"cloudflare-tunnel-ingress-controller", "docker-registry", "ghost",
|
||||||
|
"gitea", "haproxy-ingress", "harbor", "home-assistant", "k0s-backup",
|
||||||
|
"k8s-agent-dashboard", "kube-prometheus-stack", "kubernetes-dashboard",
|
||||||
|
"kubevirt", "local-path-storage", "loki-stack", "longhorn",
|
||||||
|
"longhorn-backup", "metallb", "minio", "minio-to-mega-backup",
|
||||||
|
"nfs-server-longhorn", "nginx-ingress-controller", "pihole", "pihole2",
|
||||||
|
"plex", "speedtest-tracker", "squareffect", "squareserver",
|
||||||
|
"tailscale-operator", "tools", "tor-controller", "traefik-ingress-controller",
|
||||||
|
"willlaptop-backup", "willlaptop-monitoring", "wills-portal"
|
||||||
|
],
|
||||||
|
"docker_stacks": [
|
||||||
|
"protonvpn-proxy", "squareffect", "squareserver", "stable-diffusion-webui"
|
||||||
|
],
|
||||||
|
"conventions": {
|
||||||
|
"argocd_app": "argocd/<name>.yaml points to charts/<name>/",
|
||||||
|
"helm_values": "charts/<name>/values.yaml for Helm overrides",
|
||||||
|
"raw_manifests": "charts/<name>/manifests/ for non-Helm K8s resources",
|
||||||
|
"naming": "ArgoCD app name = namespace name (usually)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,5 +113,85 @@
|
|||||||
"default": "haiku",
|
"default": "haiku",
|
||||||
"escalate_on": ["insufficient_context", "reasoning_required", "user_dissatisfied"]
|
"escalate_on": ["insufficient_context", "reasoning_required", "user_dissatisfied"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"external_models": {
|
||||||
|
"github-copilot/gpt-5.2": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gpt-5.2"],
|
||||||
|
"use_cases": ["reasoning", "fallback"],
|
||||||
|
"tier": "frontier"
|
||||||
|
},
|
||||||
|
"github-copilot/claude-sonnet-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/claude-sonnet-4.5"],
|
||||||
|
"use_cases": ["general", "fallback"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"github-copilot/claude-haiku-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/claude-haiku-4.5"],
|
||||||
|
"use_cases": ["simple"],
|
||||||
|
"tier": "lightweight"
|
||||||
|
},
|
||||||
|
"github-copilot/gemini-3-pro-preview": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gemini-3-pro-preview"],
|
||||||
|
"use_cases": ["long-context", "reasoning"],
|
||||||
|
"tier": "frontier"
|
||||||
|
},
|
||||||
|
"github-copilot/gemini-3-flash-preview": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gemini-3-flash-preview"],
|
||||||
|
"use_cases": ["fast", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"gemini/gemini-2.5-pro": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-2.5-pro"],
|
||||||
|
"use_cases": ["long-context", "reasoning"],
|
||||||
|
"tier": "frontier"
|
||||||
|
},
|
||||||
|
"gemini/gemini-2.5-flash": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-2.5-flash"],
|
||||||
|
"use_cases": ["fast", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"github-copilot/gpt-5-mini": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gpt-5-mini"],
|
||||||
|
"use_cases": ["fast", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"opencode/gpt-5-nano": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "opencode/gpt-5-nano"],
|
||||||
|
"use_cases": ["fast", "simple"],
|
||||||
|
"tier": "lightweight"
|
||||||
|
},
|
||||||
|
"zai-coding-plan/glm-4.5-air": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "zai-coding-plan/glm-4.5-air"],
|
||||||
|
"use_cases": ["simple", "fast"],
|
||||||
|
"tier": "lightweight"
|
||||||
|
},
|
||||||
|
"opencode/grok-code": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "opencode/grok-code"],
|
||||||
|
"use_cases": ["code-generation", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tier_to_external_map": {
|
||||||
|
"frontier": "github-copilot/gpt-5.2",
|
||||||
|
"mid-tier": "github-copilot/claude-sonnet-4.5",
|
||||||
|
"lightweight": "github-copilot/claude-haiku-4.5"
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "github-copilot/gpt-5.2",
|
||||||
|
"code-generation": "github-copilot/gemini-3-pro-preview",
|
||||||
|
"long-context": "gemini/gemini-2.5-pro",
|
||||||
|
"fast": "github-copilot/gemini-3-flash-preview",
|
||||||
|
"default": "github-copilot/claude-sonnet-4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,18 @@
|
|||||||
"status": "active",
|
"status": "active",
|
||||||
"added": "2026-01-04"
|
"added": "2026-01-04"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "f6a7b8c9-0123-45fa-1234-666666666666",
|
||||||
|
"instruction": "After reinstalling gmail-mcp package, run ~/.claude/patches/apply-gmail-auth-patch.sh to restore auto re-auth on token expiry.",
|
||||||
|
"status": "active",
|
||||||
|
"added": "2026-01-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a7b8c9d0-1234-56ab-2345-777777777777",
|
||||||
|
"instruction": "Homelab repo is at ~/Code/active/devops/homelab/homelab (canonical). ~/.claude/repos/homelab is a symlink to it. Always use the canonical path for new work.",
|
||||||
|
"status": "active",
|
||||||
|
"added": "2026-01-09"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "b2c3d4e5-6789-01bc-def0-222222222222",
|
"id": "b2c3d4e5-6789-01bc-def0-222222222222",
|
||||||
"instruction": "Git workflow: See CLAUDE.md for full process. Use rebase merges, not merge commits.",
|
"instruction": "Git workflow: See CLAUDE.md for full process. Use rebase merges, not merge commits.",
|
||||||
|
|||||||
@@ -266,6 +266,27 @@
|
|||||||
"ended": null,
|
"ended": null,
|
||||||
"summarized": false,
|
"summarized": false,
|
||||||
"topics": []
|
"topics": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2026-01-06_08-49-25",
|
||||||
|
"started": "2026-01-06T08:49:25-08:00",
|
||||||
|
"ended": null,
|
||||||
|
"summarized": false,
|
||||||
|
"topics": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2026-01-07_10-32-29",
|
||||||
|
"started": "2026-01-07T10:32:29-08:00",
|
||||||
|
"ended": null,
|
||||||
|
"summarized": false,
|
||||||
|
"topics": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2026-01-07_11-06-31",
|
||||||
|
"started": "2026-01-07T11:06:31-08:00",
|
||||||
|
"ended": null,
|
||||||
|
"summarized": false,
|
||||||
|
"topics": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user