fix: repair opencode config and project metadata

This commit is contained in:
Affaan Mustafa
2026-03-11 01:51:41 -07:00
committed by Affaan Mustafa
parent dba4c462c4
commit da4db99c94
7 changed files with 223 additions and 49 deletions

View File

@@ -148,7 +148,7 @@ You are an expert planning specialist...
"description": "Expert planning specialist...",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/planner.txt}",
"prompt": "{file:prompts/agents/planner.txt}",
"tools": { "read": true, "bash": true }
}
}
@@ -213,7 +213,7 @@ Create a detailed implementation plan for: $ARGUMENTS
```json
{
"instructions": [
".opencode/instructions/INSTRUCTIONS.md",
"instructions/INSTRUCTIONS.md",
"rules/common/security.md",
"rules/common/coding-style.md"
]

View File

@@ -189,7 +189,7 @@ Full configuration in `opencode.json`:
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-5",
"small_model": "anthropic/claude-haiku-4-5",
"plugin": ["./.opencode/plugins"],
"plugin": ["./plugins"],
"instructions": [
"skills/tdd-workflow/SKILL.md",
"skills/security-review/SKILL.md"

View File

@@ -6,7 +6,7 @@
"instructions": [
"AGENTS.md",
"CONTRIBUTING.md",
".opencode/instructions/INSTRUCTIONS.md",
"instructions/INSTRUCTIONS.md",
"skills/tdd-workflow/SKILL.md",
"skills/security-review/SKILL.md",
"skills/coding-standards/SKILL.md",
@@ -20,7 +20,7 @@
"skills/eval-harness/SKILL.md"
],
"plugin": [
"./.opencode/plugins"
"./plugins"
],
"agent": {
"build": {
@@ -38,7 +38,7 @@
"description": "Expert planning specialist for complex features and refactoring. Use for implementation planning, architectural changes, or complex refactoring.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/planner.txt}",
"prompt": "{file:prompts/agents/planner.txt}",
"tools": {
"read": true,
"bash": true,
@@ -50,7 +50,7 @@
"description": "Software architecture specialist for system design, scalability, and technical decision-making.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/architect.txt}",
"prompt": "{file:prompts/agents/architect.txt}",
"tools": {
"read": true,
"bash": true,
@@ -62,7 +62,7 @@
"description": "Expert code review specialist. Reviews code for quality, security, and maintainability. Use immediately after writing or modifying code.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/code-reviewer.txt}",
"prompt": "{file:prompts/agents/code-reviewer.txt}",
"tools": {
"read": true,
"bash": true,
@@ -74,7 +74,7 @@
"description": "Security vulnerability detection and remediation specialist. Use after writing code that handles user input, authentication, API endpoints, or sensitive data.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/security-reviewer.txt}",
"prompt": "{file:prompts/agents/security-reviewer.txt}",
"tools": {
"read": true,
"bash": true,
@@ -86,7 +86,7 @@
"description": "Test-Driven Development specialist enforcing write-tests-first methodology. Use when writing new features, fixing bugs, or refactoring code. Ensures 80%+ test coverage.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/tdd-guide.txt}",
"prompt": "{file:prompts/agents/tdd-guide.txt}",
"tools": {
"read": true,
"write": true,
@@ -98,7 +98,7 @@
"description": "Build and TypeScript error resolution specialist. Use when build fails or type errors occur. Fixes build/type errors only with minimal diffs.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/build-error-resolver.txt}",
"prompt": "{file:prompts/agents/build-error-resolver.txt}",
"tools": {
"read": true,
"write": true,
@@ -110,7 +110,7 @@
"description": "End-to-end testing specialist using Playwright. Generates, maintains, and runs E2E tests for critical user flows.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/e2e-runner.txt}",
"prompt": "{file:prompts/agents/e2e-runner.txt}",
"tools": {
"read": true,
"write": true,
@@ -122,7 +122,7 @@
"description": "Documentation and codemap specialist. Use for updating codemaps and documentation.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/doc-updater.txt}",
"prompt": "{file:prompts/agents/doc-updater.txt}",
"tools": {
"read": true,
"write": true,
@@ -134,7 +134,7 @@
"description": "Dead code cleanup and consolidation specialist. Use for removing unused code, duplicates, and refactoring.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/refactor-cleaner.txt}",
"prompt": "{file:prompts/agents/refactor-cleaner.txt}",
"tools": {
"read": true,
"write": true,
@@ -146,7 +146,7 @@
"description": "Expert Go code reviewer specializing in idiomatic Go, concurrency patterns, error handling, and performance.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/go-reviewer.txt}",
"prompt": "{file:prompts/agents/go-reviewer.txt}",
"tools": {
"read": true,
"bash": true,
@@ -158,7 +158,7 @@
"description": "Go build, vet, and compilation error resolution specialist. Fixes Go build errors with minimal changes.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/go-build-resolver.txt}",
"prompt": "{file:prompts/agents/go-build-resolver.txt}",
"tools": {
"read": true,
"write": true,
@@ -170,7 +170,7 @@
"description": "PostgreSQL database specialist for query optimization, schema design, security, and performance. Incorporates Supabase best practices.",
"mode": "subagent",
"model": "anthropic/claude-opus-4-5",
"prompt": "{file:.opencode/prompts/agents/database-reviewer.txt}",
"prompt": "{file:prompts/agents/database-reviewer.txt}",
"tools": {
"read": true,
"write": true,
@@ -182,135 +182,135 @@
"command": {
"plan": {
"description": "Create a detailed implementation plan for complex features",
"template": "{file:.opencode/commands/plan.md}\n\n$ARGUMENTS",
"template": "{file:commands/plan.md}\n\n$ARGUMENTS",
"agent": "planner",
"subtask": true
},
"tdd": {
"description": "Enforce TDD workflow with 80%+ test coverage",
"template": "{file:.opencode/commands/tdd.md}\n\n$ARGUMENTS",
"template": "{file:commands/tdd.md}\n\n$ARGUMENTS",
"agent": "tdd-guide",
"subtask": true
},
"code-review": {
"description": "Review code for quality, security, and maintainability",
"template": "{file:.opencode/commands/code-review.md}\n\n$ARGUMENTS",
"template": "{file:commands/code-review.md}\n\n$ARGUMENTS",
"agent": "code-reviewer",
"subtask": true
},
"security": {
"description": "Run comprehensive security review",
"template": "{file:.opencode/commands/security.md}\n\n$ARGUMENTS",
"template": "{file:commands/security.md}\n\n$ARGUMENTS",
"agent": "security-reviewer",
"subtask": true
},
"build-fix": {
"description": "Fix build and TypeScript errors with minimal changes",
"template": "{file:.opencode/commands/build-fix.md}\n\n$ARGUMENTS",
"template": "{file:commands/build-fix.md}\n\n$ARGUMENTS",
"agent": "build-error-resolver",
"subtask": true
},
"e2e": {
"description": "Generate and run E2E tests with Playwright",
"template": "{file:.opencode/commands/e2e.md}\n\n$ARGUMENTS",
"template": "{file:commands/e2e.md}\n\n$ARGUMENTS",
"agent": "e2e-runner",
"subtask": true
},
"refactor-clean": {
"description": "Remove dead code and consolidate duplicates",
"template": "{file:.opencode/commands/refactor-clean.md}\n\n$ARGUMENTS",
"template": "{file:commands/refactor-clean.md}\n\n$ARGUMENTS",
"agent": "refactor-cleaner",
"subtask": true
},
"orchestrate": {
"description": "Orchestrate multiple agents for complex tasks",
"template": "{file:.opencode/commands/orchestrate.md}\n\n$ARGUMENTS",
"template": "{file:commands/orchestrate.md}\n\n$ARGUMENTS",
"agent": "planner",
"subtask": true
},
"learn": {
"description": "Extract patterns and learnings from session",
"template": "{file:.opencode/commands/learn.md}\n\n$ARGUMENTS"
"template": "{file:commands/learn.md}\n\n$ARGUMENTS"
},
"checkpoint": {
"description": "Save verification state and progress",
"template": "{file:.opencode/commands/checkpoint.md}\n\n$ARGUMENTS"
"template": "{file:commands/checkpoint.md}\n\n$ARGUMENTS"
},
"verify": {
"description": "Run verification loop",
"template": "{file:.opencode/commands/verify.md}\n\n$ARGUMENTS"
"template": "{file:commands/verify.md}\n\n$ARGUMENTS"
},
"eval": {
"description": "Run evaluation against criteria",
"template": "{file:.opencode/commands/eval.md}\n\n$ARGUMENTS"
"template": "{file:commands/eval.md}\n\n$ARGUMENTS"
},
"update-docs": {
"description": "Update documentation",
"template": "{file:.opencode/commands/update-docs.md}\n\n$ARGUMENTS",
"template": "{file:commands/update-docs.md}\n\n$ARGUMENTS",
"agent": "doc-updater",
"subtask": true
},
"update-codemaps": {
"description": "Update codemaps",
"template": "{file:.opencode/commands/update-codemaps.md}\n\n$ARGUMENTS",
"template": "{file:commands/update-codemaps.md}\n\n$ARGUMENTS",
"agent": "doc-updater",
"subtask": true
},
"test-coverage": {
"description": "Analyze test coverage",
"template": "{file:.opencode/commands/test-coverage.md}\n\n$ARGUMENTS",
"template": "{file:commands/test-coverage.md}\n\n$ARGUMENTS",
"agent": "tdd-guide",
"subtask": true
},
"setup-pm": {
"description": "Configure package manager",
"template": "{file:.opencode/commands/setup-pm.md}\n\n$ARGUMENTS"
"template": "{file:commands/setup-pm.md}\n\n$ARGUMENTS"
},
"go-review": {
"description": "Go code review",
"template": "{file:.opencode/commands/go-review.md}\n\n$ARGUMENTS",
"template": "{file:commands/go-review.md}\n\n$ARGUMENTS",
"agent": "go-reviewer",
"subtask": true
},
"go-test": {
"description": "Go TDD workflow",
"template": "{file:.opencode/commands/go-test.md}\n\n$ARGUMENTS",
"template": "{file:commands/go-test.md}\n\n$ARGUMENTS",
"agent": "tdd-guide",
"subtask": true
},
"go-build": {
"description": "Fix Go build errors",
"template": "{file:.opencode/commands/go-build.md}\n\n$ARGUMENTS",
"template": "{file:commands/go-build.md}\n\n$ARGUMENTS",
"agent": "go-build-resolver",
"subtask": true
},
"skill-create": {
"description": "Generate skills from git history",
"template": "{file:.opencode/commands/skill-create.md}\n\n$ARGUMENTS"
"template": "{file:commands/skill-create.md}\n\n$ARGUMENTS"
},
"instinct-status": {
"description": "View learned instincts",
"template": "{file:.opencode/commands/instinct-status.md}\n\n$ARGUMENTS"
"template": "{file:commands/instinct-status.md}\n\n$ARGUMENTS"
},
"instinct-import": {
"description": "Import instincts",
"template": "{file:.opencode/commands/instinct-import.md}\n\n$ARGUMENTS"
"template": "{file:commands/instinct-import.md}\n\n$ARGUMENTS"
},
"instinct-export": {
"description": "Export instincts",
"template": "{file:.opencode/commands/instinct-export.md}\n\n$ARGUMENTS"
"template": "{file:commands/instinct-export.md}\n\n$ARGUMENTS"
},
"evolve": {
"description": "Cluster instincts into skills",
"template": "{file:.opencode/commands/evolve.md}\n\n$ARGUMENTS"
"template": "{file:commands/evolve.md}\n\n$ARGUMENTS"
},
"promote": {
"description": "Promote project instincts to global scope",
"template": "{file:.opencode/commands/promote.md}\n\n$ARGUMENTS"
"template": "{file:commands/promote.md}\n\n$ARGUMENTS"
},
"projects": {
"description": "List known projects and instinct stats",
"template": "{file:.opencode/commands/projects.md}\n\n$ARGUMENTS"
"template": "{file:commands/projects.md}\n\n$ARGUMENTS"
}
},
"permission": {

View File

@@ -258,6 +258,7 @@ Other behavior (observation capture, instinct thresholds, project scoping, promo
| +-- commands/ # Global generated commands
+-- projects/
+-- a1b2c3d4e5f6/ # Project hash (from git remote URL)
| +-- project.json # Per-project metadata mirror (id/name/root/remote)
| +-- observations.jsonl
| +-- observations.archive/
| +-- instincts/

View File

@@ -142,6 +142,7 @@ _clv2_update_project_registry() {
local pname="$2"
local proot="$3"
local premote="$4"
local pdir="$_CLV2_PROJECT_DIR"
mkdir -p "$(dirname "$_CLV2_REGISTRY_FILE")"
@@ -155,27 +156,55 @@ _clv2_update_project_registry() {
_CLV2_REG_PNAME="$pname" \
_CLV2_REG_PROOT="$proot" \
_CLV2_REG_PREMOTE="$premote" \
_CLV2_REG_PDIR="$pdir" \
_CLV2_REG_FILE="$_CLV2_REGISTRY_FILE" \
"$_CLV2_PYTHON_CMD" -c '
import json, os
import json, os, tempfile
from datetime import datetime, timezone
registry_path = os.environ["_CLV2_REG_FILE"]
project_dir = os.environ["_CLV2_REG_PDIR"]
project_file = os.path.join(project_dir, "project.json")
os.makedirs(project_dir, exist_ok=True)
def atomic_write_json(path, payload):
fd, tmp_path = tempfile.mkstemp(
prefix=f".{os.path.basename(path)}.tmp.",
dir=os.path.dirname(path),
text=True,
)
try:
with os.fdopen(fd, "w") as f:
json.dump(payload, f, indent=2)
f.write("\n")
os.replace(tmp_path, path)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
try:
with open(registry_path) as f:
registry = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
registry = {}
registry[os.environ["_CLV2_REG_PID"]] = {
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
entry = registry.get(os.environ["_CLV2_REG_PID"], {})
metadata = {
"id": os.environ["_CLV2_REG_PID"],
"name": os.environ["_CLV2_REG_PNAME"],
"root": os.environ["_CLV2_REG_PROOT"],
"remote": os.environ["_CLV2_REG_PREMOTE"],
"last_seen": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
"created_at": entry.get("created_at", now),
"last_seen": now,
}
with open(registry_path, "w") as f:
json.dump(registry, f, indent=2)
registry[os.environ["_CLV2_REG_PID"]] = metadata
atomic_write_json(project_file, metadata)
atomic_write_json(registry_path, registry)
' 2>/dev/null || true
}

View File

@@ -8,7 +8,7 @@ const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawn } = require('child_process');
const { spawn, spawnSync } = require('child_process');
// Test helper
function test(name, fn) {
@@ -2148,6 +2148,69 @@ async function runTests() {
passed++;
else failed++;
if (
await asyncTest('detect-project writes project metadata to the registry and project directory', async () => {
const testRoot = createTestDir();
const homeDir = path.join(testRoot, 'home');
const repoDir = path.join(testRoot, 'repo');
const detectProjectPath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'scripts', 'detect-project.sh');
try {
fs.mkdirSync(homeDir, { recursive: true });
fs.mkdirSync(repoDir, { recursive: true });
spawnSync('git', ['init'], { cwd: repoDir, stdio: 'ignore' });
spawnSync('git', ['remote', 'add', 'origin', 'https://github.com/example/ecc-test.git'], { cwd: repoDir, stdio: 'ignore' });
const shellCommand = [
`cd "${repoDir}"`,
`source "${detectProjectPath}" >/dev/null 2>&1`,
'printf "%s\\n" "$PROJECT_ID"',
'printf "%s\\n" "$PROJECT_DIR"'
].join('; ');
const proc = spawn('bash', ['-lc', shellCommand], {
env: { ...process.env, HOME: homeDir },
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', data => (stdout += data));
proc.stderr.on('data', data => (stderr += data));
const code = await new Promise((resolve, reject) => {
proc.on('close', resolve);
proc.on('error', reject);
});
assert.strictEqual(code, 0, `detect-project should source cleanly, stderr: ${stderr}`);
const [projectId, projectDir] = stdout.trim().split(/\r?\n/);
const registryPath = path.join(homeDir, '.claude', 'homunculus', 'projects.json');
const projectMetadataPath = path.join(projectDir, 'project.json');
assert.ok(projectId, 'detect-project should emit a project id');
assert.ok(fs.existsSync(registryPath), 'projects.json should be created');
assert.ok(fs.existsSync(projectMetadataPath), 'project.json should be written in the project directory');
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
const metadata = JSON.parse(fs.readFileSync(projectMetadataPath, 'utf8'));
assert.ok(registry[projectId], 'registry should contain the detected project');
assert.strictEqual(metadata.id, projectId, 'project.json should include the detected id');
assert.strictEqual(metadata.name, path.basename(repoDir), 'project.json should include the repo name');
assert.strictEqual(fs.realpathSync(metadata.root), fs.realpathSync(repoDir), 'project.json should include the repo root');
assert.strictEqual(metadata.remote, 'https://github.com/example/ecc-test.git', 'project.json should include the sanitized remote');
assert.ok(metadata.created_at, 'project.json should include created_at');
assert.ok(metadata.last_seen, 'project.json should include last_seen');
} finally {
cleanupTestDir(testRoot);
}
})
)
passed++;
else failed++;
if (await asyncTest('observe.sh falls back to legacy output fields when tool_response is null', async () => {
const homeDir = createTestDir();
const projectDir = createTestDir();

View File

@@ -0,0 +1,81 @@
/**
* Tests for .opencode/opencode.json local file references.
*
* Run with: node tests/opencode-config.test.js
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const repoRoot = path.join(__dirname, '..');
const opencodeDir = path.join(repoRoot, '.opencode');
const configPath = path.join(opencodeDir, 'opencode.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
let passed = 0;
let failed = 0;
if (
test('plugin paths do not duplicate the .opencode directory', () => {
const plugins = config.plugin || [];
for (const pluginPath of plugins) {
assert.ok(!pluginPath.includes('.opencode/'), `Plugin path should be config-relative, got: ${pluginPath}`);
assert.ok(fs.existsSync(path.resolve(opencodeDir, pluginPath)), `Plugin path should resolve from .opencode/: ${pluginPath}`);
}
})
)
passed++;
else failed++;
if (
test('file references are config-relative and resolve to existing files', () => {
const refs = [];
function walk(value) {
if (typeof value === 'string') {
const matches = value.matchAll(/\{file:([^}]+)\}/g);
for (const match of matches) {
refs.push(match[1]);
}
return;
}
if (Array.isArray(value)) {
value.forEach(walk);
return;
}
if (value && typeof value === 'object') {
Object.values(value).forEach(walk);
}
}
walk(config);
assert.ok(refs.length > 0, 'Expected to find file references in opencode.json');
for (const ref of refs) {
assert.ok(!ref.startsWith('.opencode/'), `File ref should not duplicate .opencode/: ${ref}`);
assert.ok(fs.existsSync(path.resolve(opencodeDir, ref)), `File ref should resolve from .opencode/: ${ref}`);
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);