From da4db99c94cf272d3341910bc8c8a26d2e6e6960 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 11 Mar 2026 01:51:41 -0700 Subject: [PATCH] fix: repair opencode config and project metadata --- .opencode/MIGRATION.md | 4 +- .opencode/README.md | 2 +- .opencode/opencode.json | 80 +++++++++--------- skills/continuous-learning-v2/SKILL.md | 1 + .../scripts/detect-project.sh | 39 +++++++-- tests/hooks/hooks.test.js | 65 ++++++++++++++- tests/opencode-config.test.js | 81 +++++++++++++++++++ 7 files changed, 223 insertions(+), 49 deletions(-) create mode 100644 tests/opencode-config.test.js diff --git a/.opencode/MIGRATION.md b/.opencode/MIGRATION.md index afb2507b..f533a67d 100644 --- a/.opencode/MIGRATION.md +++ b/.opencode/MIGRATION.md @@ -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" ] diff --git a/.opencode/README.md b/.opencode/README.md index e8737e81..a93cfa98 100644 --- a/.opencode/README.md +++ b/.opencode/README.md @@ -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" diff --git a/.opencode/opencode.json b/.opencode/opencode.json index 476aadce..1038140f 100644 --- a/.opencode/opencode.json +++ b/.opencode/opencode.json @@ -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": { diff --git a/skills/continuous-learning-v2/SKILL.md b/skills/continuous-learning-v2/SKILL.md index 0256e1cb..59be7e1b 100644 --- a/skills/continuous-learning-v2/SKILL.md +++ b/skills/continuous-learning-v2/SKILL.md @@ -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/ diff --git a/skills/continuous-learning-v2/scripts/detect-project.sh b/skills/continuous-learning-v2/scripts/detect-project.sh index a44a0264..6f88deb0 100755 --- a/skills/continuous-learning-v2/scripts/detect-project.sh +++ b/skills/continuous-learning-v2/scripts/detect-project.sh @@ -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 } diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index d0583138..540d5084 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -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(); diff --git a/tests/opencode-config.test.js b/tests/opencode-config.test.js new file mode 100644 index 00000000..690db04d --- /dev/null +++ b/tests/opencode-config.test.js @@ -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);