Compare commits

...

24 Commits

Author SHA1 Message Date
OpenCode Test a9eaf0114f fix(external-llm): align tier defaults with industry benchmarks
- frontier: gpt-5.2 (GPT)
- mid-tier: claude-sonnet-4.5 (Claude)
- lightweight: claude-haiku-4.5 (Claude)

Prioritizes correctness over speed, aligned with MMLU/GPQA/Arena rankings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:42:38 -08:00
OpenCode Test f63172c4cf feat(external-llm): standardize tiers and optimize model selection
- Rename tiers: opus/sonnet/haiku → frontier/mid-tier/lightweight
- Align with industry benchmarks (MMLU, GPQA, Chatbot Arena)
- Add /external command for LLM mode control
- Fix invoke.py timeout passthrough (now 600s default)

Tier changes:
- Promote gemini-2.5-pro to frontier (benchmark-validated)
- Demote glm-4.7 to mid-tier then removed (unreliable)
- Promote gemini-2.5-flash to mid-tier

New models added:
- gpt-5-mini, gpt-5-nano (GPT family coverage)
- grok-code (Grok/X family)
- glm-4.5-air (lightweight GLM)

Removed (redundant/unreliable):
- o3 (not available)
- glm-4.7 (timeouts)
- gpt-4o, big-pickle, glm-4.5-flash (redundant)

Final: 11 models across 3 tiers, 4 model families

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 03:30:51 -08:00
OpenCode Test ff111ef278 fix(external-llm): correct o3 and glm-4.7 tiers
- github-copilot/o3: opus -> sonnet-equivalent
- zai-coding-plan/glm-4.7: sonnet -> opus-equivalent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 08:13:20 -08:00
OpenCode Test bf5470ac66 fix(external-llm): correct gemini CLI model tiers
- gemini/gemini-2.5-pro: opus -> sonnet-equivalent
- gemini/gemini-2.5-flash: sonnet -> haiku-equivalent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 00:02:35 -08:00
OpenCode Test c1e3b2881d feat(external-llm): add gemini-3-flash-preview via OpenCode
Gemini 3 models via github-copilot provider (OpenCode):
- github-copilot/gemini-3-pro-preview (opus-tier)
- github-copilot/gemini-3-flash-preview (sonnet-tier)

Native Gemini CLI models unchanged:
- gemini/gemini-2.5-pro
- gemini/gemini-2.5-flash

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 23:52:32 -08:00
OpenCode Test 4024740b82 fix(external-llm): remove non-existent gemini-3 models
Removed gemini/gemini-3-pro and gemini/gemini-3-flash from native
gemini CLI - these models return 404 Not Found.

Remaining gemini models (via native CLI):
- gemini/gemini-2.5-pro
- gemini/gemini-2.5-flash

Note: github-copilot/gemini-3-pro-preview (via opencode) kept for now.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:49:47 -08:00
OpenCode Test fb4cf1b035 fix(external-llm): correct opencode CLI syntax and gemini routing
- OpenCode: use `opencode run -m MODEL "prompt"` syntax
- OpenCode: set correct binary path (/home/linuxbrew/.linuxbrew/bin/opencode)
- Gemini: route long-context to gemini-2.5-pro (gemini-3 not available yet)

Tested working:
- opencode/big-pickle
- github-copilot/claude-sonnet-4.5
- zai-coding-plan/glm-4.7
- gemini/gemini-2.5-pro
- gemini/gemini-2.5-flash

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:46:38 -08:00
OpenCode Test d2daf74fca feat(external-llm): add native gemini CLI models
Added gemini CLI models:
- gemini/gemini-3-pro (long-context, reasoning)
- gemini/gemini-3-flash (fast, general)
- gemini/gemini-2.5-pro (long-context, reasoning)
- gemini/gemini-2.5-flash (fast, general)

Updated long-context routing to use native gemini CLI (gemini-3-pro)
instead of opencode/github-copilot path.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:37:45 -08:00
OpenCode Test e52e818686 chore: mark fc-004 external LLM integration as resolved
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:34:56 -08:00
OpenCode Test df6cf94dae feat(external-llm): add external LLM integration (fc-004)
Implements external LLM routing via opencode CLI for:
- GitHub Copilot (gpt-5.2, claude-sonnet-4.5, claude-haiku-4.5, o3, gemini-3-pro)
- Z.AI (glm-4.7 for code generation)
- OpenCode native (big-pickle)

Components:
- mcp/llm-router/invoke.py: Main router with task-based model selection
- mcp/llm-router/delegate.py: Agent delegation helper (respects external mode)
- mcp/llm-router/toggle.py: Enable/disable external-only mode
- mcp/llm-router/providers/: CLI wrappers for opencode and gemini

Features:
- Persistent toggle via state/external-mode.json
- Task routing: reasoning -> gpt-5.2, code-gen -> glm-4.7, long-context -> gemini
- Claude tier mapping: opus -> gpt-5.2, sonnet -> claude-sonnet-4.5, haiku -> claude-haiku-4.5
- Session-start hook announces when external mode is active
- Natural language toggle support via component registry

Plan: gleaming-routing-mercury

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:34:35 -08:00
OpenCode Test 7dcb8af1bb Set default model to opus in settings.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 13:49:30 -08:00
OpenCode Test 6be9bf5aff Mark OpenCode transposition plan as complete
- Implementation complete (2026-01-07)
- All steps verified: backup, sync script, config, testing, docs
- 40 agents discovered (built-in + synced)
- 10 skills, 27 commands, 10 workflows synced
- Manual TUI testing pending

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 13:45:59 -08:00
OpenCode Test 7c37e9adb3 Add OpenCode sync enhancement plan and future consideration
- Add fc-047: Consider JSON minification for OpenCode instructions
- Add brainstorming plan for OpenCode Claude sync enhancements
- Add implementation status plan

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 12:15:33 -08:00
OpenCode Test 0780b4c17d Document plans index in CLAUDE.md and plans/README.md
- Add plans/ to directory structure
- Add plans/index.json to shared state files table
- Add Plans row to component formats table
- Create plans/README.md with schema and query examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:28:56 -08:00
OpenCode Test a08dc505d9 Add plans index.json for status tracking
Central registry tracking all 17 plans with status, category, and dates.
Enables easy querying of pending vs implemented plans.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:27:18 -08:00
OpenCode Test c82726b691 Add RAG JSON-to-text transformation plan
Design for improving semantic search quality by transforming JSON
structures into natural language at index time.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:11:34 -08:00
OpenCode Test c14c0d843d Update ralph-loop to guardrail hooks task
Switch from completed morning-report to guardrail hooks implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:11:27 -08:00
OpenCode Test 0fd0e74b67 Track new PA session entries
Add session records for Jan 06 and Jan 07 sessions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:11:21 -08:00
OpenCode Test 1636784931 Enable tasks section in morning report, add daily archives
- Enable tasks collector in morning-report config
- Update morning.md with Jan 07 report
- Archive Jan 06 and Jan 07 reports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:11:15 -08:00
OpenCode Test c30ea2d679 Update plugin metadata from auto-updates
Plugins refreshed their cache versions and timestamps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:11:08 -08:00
OpenCode Test 769391640b Add telemetry directory to gitignore
Analytics tracking data should not be version controlled.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:10:58 -08:00
OpenCode Test ecf375205f Implement guardrail hooks for dangerous operation prevention
- Add PreToolUse hook intercepting Bash, Write, Edit
- Block catastrophic commands (rm -rf /, mkfs, etc.)
- Require confirmation for operations outside safe paths
- Git-aware: operations in git repos are allowed
- Session allowlist for user-confirmed operations
- Audit logging to logs/guardrail.jsonl
- Clear session allowlist on SessionEnd

Config: state/guardrails.json
Scripts: hooks/scripts/guardrail.py, guardrail-confirm.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 10:57:53 -08:00
OpenCode Test f2f8a03a32 Add guardrail hooks design document
PreToolUse hooks to prevent dangerous operations:
- Intercepts Bash, Write, Edit before execution
- Contextual response (block vs confirm)
- Path-aware with git repo detection
- Session allowlist for user confirmations
- Audit logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 10:50:03 -08:00
OpenCode Test 630893f047 Add conditional RAG reindex after session summarization
When summarize-transcript.py extracts items to memory files, it now
triggers index_personal.py to update the RAG search index. Only runs
when items were actually added (total_added > 0) to avoid unnecessary
reindexing on trivial sessions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 10:32:04 -08:00
41 changed files with 4488 additions and 48 deletions
+15 -17
View File
@@ -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.
+3
View File
@@ -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/
+4
View File
@@ -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/` |
+1
View File
@@ -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:*`)
+89
View File
@@ -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 |
+12
View File
@@ -41,6 +41,18 @@
} }
] ]
} }
],
"PreToolUse": [
{
"matcher": "Bash|Write|Edit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/scripts/guardrail.py",
"timeout": 5
}
]
}
] ]
} }
} }
+73
View File
@@ -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()
+283
View File
@@ -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()
+7
View File
@@ -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"
+14 -2
View File
@@ -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
+23
View File
@@ -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()
+125
View File
@@ -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()
+127
View File
@@ -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()
+49
View File
@@ -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'")
+56
View File
@@ -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'")
+98
View File
@@ -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()
+74
View File
@@ -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
+798
View File
@@ -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
+903
View File
@@ -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"
```
+375
View File
@@ -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)
+230
View File
@@ -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"
+134
View File
@@ -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"
}
}
}
+115
View File
@@ -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)
+110
View File
@@ -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
+9 -9
View File
@@ -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
} }
+1 -1
View File
@@ -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": {
+31
View File
@@ -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*
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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"
} }
+1 -1
View File
@@ -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
}, },
+37
View File
@@ -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": [],
+5
View File
@@ -0,0 +1,5 @@
{
"enabled": false,
"activated_at": null,
"reason": null
}
+16 -4
View File
@@ -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."
} }
] ]
} }
+45
View File
@@ -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
View File
@@ -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)"
}
}
}
}
+80
View File
@@ -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": []
} }
] ]
} }