compound-agent 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { getLlama, resolveModelFile } from 'node-llama-cpp';
4
- import { mkdirSync, writeFileSync, statSync, existsSync, readFileSync, unlinkSync, chmodSync, readdirSync } from 'fs';
4
+ import { mkdirSync, writeFileSync, statSync, existsSync, unlinkSync, readFileSync, chmodSync, readdirSync } from 'fs';
5
5
  import { homedir } from 'os';
6
6
  import { join, dirname, resolve, relative } from 'path';
7
7
  import * as fs from 'fs/promises';
@@ -1733,7 +1733,13 @@ var CLAUDE_HOOK_MARKERS = [
1733
1733
  "compound-agent load-session",
1734
1734
  "ca hooks run user-prompt",
1735
1735
  "ca hooks run post-tool-failure",
1736
- "ca hooks run post-tool-success"
1736
+ "ca hooks run post-tool-success",
1737
+ "ca hooks run phase-guard",
1738
+ "ca hooks run read-tracker",
1739
+ "ca hooks run stop-audit",
1740
+ // v1.2.9 canonical names
1741
+ "ca hooks run post-read",
1742
+ "ca hooks run phase-audit"
1737
1743
  ];
1738
1744
  var CLAUDE_HOOK_CONFIG = {
1739
1745
  matcher: "",
@@ -1780,11 +1786,32 @@ var CLAUDE_POST_TOOL_SUCCESS_HOOK_CONFIG = {
1780
1786
  }
1781
1787
  ]
1782
1788
  };
1783
- var MCP_SERVER_CONFIG = {
1784
- "compound-agent": {
1785
- command: "npx",
1786
- args: ["compound-agent-mcp"]
1787
- }
1789
+ var CLAUDE_PHASE_GUARD_HOOK_CONFIG = {
1790
+ matcher: "Edit|Write",
1791
+ hooks: [
1792
+ {
1793
+ type: "command",
1794
+ command: "npx ca hooks run phase-guard 2>/dev/null || true"
1795
+ }
1796
+ ]
1797
+ };
1798
+ var CLAUDE_POST_READ_HOOK_CONFIG = {
1799
+ matcher: "Read",
1800
+ hooks: [
1801
+ {
1802
+ type: "command",
1803
+ command: "npx ca hooks run post-read 2>/dev/null || true"
1804
+ }
1805
+ ]
1806
+ };
1807
+ var CLAUDE_PHASE_AUDIT_HOOK_CONFIG = {
1808
+ matcher: "",
1809
+ hooks: [
1810
+ {
1811
+ type: "command",
1812
+ command: "npx ca hooks run phase-audit 2>/dev/null || true"
1813
+ }
1814
+ ]
1788
1815
  };
1789
1816
  var COMPOUND_AGENT_SECTION_HEADER = "## Compound Agent Integration";
1790
1817
  var CLAUDE_REF_START_MARKER = "<!-- compound-agent:claude-ref:start -->";
@@ -1900,6 +1927,22 @@ var PLUGIN_MANIFEST = {
1900
1927
  {
1901
1928
  matcher: "Bash|Edit|Write",
1902
1929
  hooks: [{ type: "command", command: "npx ca hooks run post-tool-success 2>/dev/null || true" }]
1930
+ },
1931
+ {
1932
+ matcher: "Read",
1933
+ hooks: [{ type: "command", command: "npx ca hooks run post-read 2>/dev/null || true" }]
1934
+ }
1935
+ ],
1936
+ PreToolUse: [
1937
+ {
1938
+ matcher: "Edit|Write",
1939
+ hooks: [{ type: "command", command: "npx ca hooks run phase-guard 2>/dev/null || true" }]
1940
+ }
1941
+ ],
1942
+ Stop: [
1943
+ {
1944
+ matcher: "",
1945
+ hooks: [{ type: "command", command: "npx ca hooks run phase-audit 2>/dev/null || true" }]
1903
1946
  }
1904
1947
  ]
1905
1948
  // Note: PreCommit is handled by git hooks, not Claude Code hooks
@@ -1924,7 +1967,7 @@ async function readClaudeSettings(settingsPath) {
1924
1967
  function hasClaudeHook(settings) {
1925
1968
  const hooks = settings.hooks;
1926
1969
  if (!hooks) return false;
1927
- const hookTypes = ["SessionStart", "PreCompact", "UserPromptSubmit", "PostToolUseFailure", "PostToolUse"];
1970
+ const hookTypes = ["SessionStart", "PreCompact", "UserPromptSubmit", "PostToolUseFailure", "PostToolUse", "PreToolUse", "Stop"];
1928
1971
  return hookTypes.some((hookType) => {
1929
1972
  const hookArray = hooks[hookType];
1930
1973
  if (!hookArray) return false;
@@ -1936,15 +1979,10 @@ function hasClaudeHook(settings) {
1936
1979
  });
1937
1980
  });
1938
1981
  }
1939
- function addCompoundAgentHook(settings) {
1940
- if (!settings.hooks) {
1941
- settings.hooks = {};
1942
- }
1982
+ function hasAllCompoundAgentHooks(settings) {
1943
1983
  const hooks = settings.hooks;
1944
- if (!hooks.SessionStart) {
1945
- hooks.SessionStart = [];
1946
- }
1947
- hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
1984
+ if (!hooks) return false;
1985
+ return hasHookTypeAny(hooks.SessionStart ?? [], ["ca prime"]) && hasHookTypeAny(hooks.PreCompact ?? [], ["ca prime"]) && hasHookTypeAny(hooks.UserPromptSubmit ?? [], ["ca hooks run user-prompt"]) && hasHookTypeAny(hooks.PostToolUseFailure ?? [], ["ca hooks run post-tool-failure"]) && hasHookTypeAny(hooks.PostToolUse ?? [], ["ca hooks run post-tool-success"]) && hasHookTypeAny(hooks.PostToolUse ?? [], ["ca hooks run post-read", "ca hooks run read-tracker"]) && hasHookTypeAny(hooks.PreToolUse ?? [], ["ca hooks run phase-guard"]) && hasHookTypeAny(hooks.Stop ?? [], ["ca hooks run phase-audit", "ca hooks run stop-audit"]);
1948
1986
  }
1949
1987
  function addAllCompoundAgentHooks(settings) {
1950
1988
  if (!settings.hooks) {
@@ -1954,92 +1992,60 @@ function addAllCompoundAgentHooks(settings) {
1954
1992
  if (!hooks.SessionStart) {
1955
1993
  hooks.SessionStart = [];
1956
1994
  }
1957
- if (!hasHookType(hooks.SessionStart, "ca prime")) {
1995
+ if (!hasHookTypeAny(hooks.SessionStart, ["ca prime"])) {
1958
1996
  hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
1959
1997
  }
1960
1998
  if (!hooks.PreCompact) {
1961
1999
  hooks.PreCompact = [];
1962
2000
  }
1963
- if (!hasHookType(hooks.PreCompact, "ca prime")) {
2001
+ if (!hasHookTypeAny(hooks.PreCompact, ["ca prime"])) {
1964
2002
  hooks.PreCompact.push(CLAUDE_PRECOMPACT_HOOK_CONFIG);
1965
2003
  }
1966
2004
  if (!hooks.UserPromptSubmit) {
1967
2005
  hooks.UserPromptSubmit = [];
1968
2006
  }
1969
- if (!hasHookType(hooks.UserPromptSubmit, "ca hooks run user-prompt")) {
2007
+ if (!hasHookTypeAny(hooks.UserPromptSubmit, ["ca hooks run user-prompt"])) {
1970
2008
  hooks.UserPromptSubmit.push(CLAUDE_USER_PROMPT_HOOK_CONFIG);
1971
2009
  }
1972
2010
  if (!hooks.PostToolUseFailure) {
1973
2011
  hooks.PostToolUseFailure = [];
1974
2012
  }
1975
- if (!hasHookType(hooks.PostToolUseFailure, "ca hooks run post-tool-failure")) {
2013
+ if (!hasHookTypeAny(hooks.PostToolUseFailure, ["ca hooks run post-tool-failure"])) {
1976
2014
  hooks.PostToolUseFailure.push(CLAUDE_POST_TOOL_FAILURE_HOOK_CONFIG);
1977
2015
  }
1978
2016
  if (!hooks.PostToolUse) {
1979
2017
  hooks.PostToolUse = [];
1980
2018
  }
1981
- if (!hasHookType(hooks.PostToolUse, "ca hooks run post-tool-success")) {
2019
+ if (!hasHookTypeAny(hooks.PostToolUse, ["ca hooks run post-tool-success"])) {
1982
2020
  hooks.PostToolUse.push(CLAUDE_POST_TOOL_SUCCESS_HOOK_CONFIG);
1983
2021
  }
1984
- }
1985
- function hasHookType(hookArray, marker) {
1986
- return hookArray.some((entry) => {
1987
- const hookEntry = entry;
1988
- return hookEntry.hooks?.some((h) => h.command?.includes(marker));
1989
- });
1990
- }
1991
- function getMcpJsonPath(repoRoot) {
1992
- const root = repoRoot ?? getRepoRoot();
1993
- return join(root, ".mcp.json");
1994
- }
1995
- async function readMcpJson(mcpPath) {
1996
- if (!existsSync(mcpPath)) {
1997
- return {};
2022
+ if (!hasHookTypeAny(hooks.PostToolUse, ["ca hooks run post-read", "ca hooks run read-tracker"])) {
2023
+ hooks.PostToolUse.push(CLAUDE_POST_READ_HOOK_CONFIG);
1998
2024
  }
1999
- const content = await readFile(mcpPath, "utf-8");
2000
- return JSON.parse(content);
2001
- }
2002
- async function writeMcpJson(mcpPath, config) {
2003
- const tempPath = mcpPath + ".tmp";
2004
- await writeFile(tempPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2005
- await rename(tempPath, mcpPath);
2006
- }
2007
- async function addMcpServerToMcpJson(repoRoot) {
2008
- const mcpPath = getMcpJsonPath(repoRoot);
2009
- const config = await readMcpJson(mcpPath);
2010
- if (!config.mcpServers) {
2011
- config.mcpServers = {};
2025
+ if (!hooks.PreToolUse) {
2026
+ hooks.PreToolUse = [];
2012
2027
  }
2013
- const mcpServers = config.mcpServers;
2014
- if (mcpServers["compound-agent"]) {
2015
- return false;
2028
+ if (!hasHookTypeAny(hooks.PreToolUse, ["ca hooks run phase-guard"])) {
2029
+ hooks.PreToolUse.push(CLAUDE_PHASE_GUARD_HOOK_CONFIG);
2016
2030
  }
2017
- Object.assign(mcpServers, MCP_SERVER_CONFIG);
2018
- await writeMcpJson(mcpPath, config);
2019
- return true;
2020
- }
2021
- async function hasMcpServerInMcpJson(repoRoot) {
2022
- const mcpPath = getMcpJsonPath(repoRoot);
2023
- const config = await readMcpJson(mcpPath);
2024
- const mcpServers = config.mcpServers;
2025
- return !!mcpServers?.["compound-agent"];
2026
- }
2027
- async function removeMcpServerFromMcpJson(repoRoot) {
2028
- const mcpPath = getMcpJsonPath(repoRoot);
2029
- const config = await readMcpJson(mcpPath);
2030
- const mcpServers = config.mcpServers;
2031
- if (!mcpServers?.["compound-agent"]) {
2032
- return false;
2031
+ if (!hooks.Stop) {
2032
+ hooks.Stop = [];
2033
2033
  }
2034
- delete mcpServers["compound-agent"];
2035
- await writeMcpJson(mcpPath, config);
2036
- return true;
2034
+ if (!hasHookTypeAny(hooks.Stop, ["ca hooks run phase-audit", "ca hooks run stop-audit"])) {
2035
+ hooks.Stop.push(CLAUDE_PHASE_AUDIT_HOOK_CONFIG);
2036
+ }
2037
+ }
2038
+ function hasHookTypeAny(hookArray, markers) {
2039
+ return hookArray.some((entry) => {
2040
+ const hookEntry = entry;
2041
+ return hookEntry.hooks?.some((h) => markers.some((marker) => h.command?.includes(marker)));
2042
+ });
2037
2043
  }
2038
2044
  function removeCompoundAgentHook(settings) {
2039
2045
  const hooks = settings.hooks;
2040
2046
  if (!hooks) return false;
2041
2047
  let anyRemoved = false;
2042
- const hookTypes = ["SessionStart", "PreCompact", "UserPromptSubmit", "PostToolUseFailure", "PostToolUse"];
2048
+ const hookTypes = ["SessionStart", "PreCompact", "UserPromptSubmit", "PostToolUseFailure", "PostToolUse", "PreToolUse", "Stop"];
2043
2049
  for (const hookType of hookTypes) {
2044
2050
  if (!hooks[hookType]) continue;
2045
2051
  const originalLength = hooks[hookType].length;
@@ -2070,11 +2076,11 @@ async function installClaudeHooksForInit(repoRoot) {
2070
2076
  } catch {
2071
2077
  return { installed: false, action: "error", error: "Failed to parse settings.json" };
2072
2078
  }
2073
- if (hasClaudeHook(settings)) {
2079
+ if (hasAllCompoundAgentHooks(settings)) {
2074
2080
  return { installed: true, action: "already_installed" };
2075
2081
  }
2076
2082
  try {
2077
- addCompoundAgentHook(settings);
2083
+ addAllCompoundAgentHooks(settings);
2078
2084
  await writeClaudeSettings(settingsPath, settings);
2079
2085
  return { installed: true, action: "installed" };
2080
2086
  } catch (err) {
@@ -2124,120 +2130,743 @@ async function removeClaudeMdReference(repoRoot) {
2124
2130
  return true;
2125
2131
  }
2126
2132
 
2127
- // src/setup/templates/agents-external.ts
2128
- var EXTERNAL_AGENT_TEMPLATES = {
2129
- "external-reviewer-gemini.md": `---
2130
- name: External Reviewer (Gemini)
2131
- description: Cross-model review using Gemini CLI in headless mode
2132
- model: sonnet
2133
- ---
2134
-
2135
- # External Reviewer \u2014 Gemini
2136
-
2137
- ## Role
2138
- Run a cross-model code review by invoking the Gemini CLI in headless mode. Provides an independent perspective from a different LLM to catch issues Claude may miss.
2139
-
2140
- ## Prerequisites
2141
- - Gemini CLI installed (\`npm i -g @google/gemini-cli\`)
2142
- - Authenticated (\`gemini auth login\`)
2143
-
2144
- ## Instructions
2145
- 1. **Check availability** \u2014 run \`command -v gemini\` via Bash. If not found, report "Gemini CLI not installed \u2014 skipping external review" and stop.
2146
- 2. **Gather context**:
2147
- - Get the beads issue being worked on: \`bd list --status=in_progress\` then \`bd show <id>\` to get the issue title and description.
2148
- - Get the diff: \`git diff HEAD~1\` (or the appropriate range for this session's changes).
2149
- 3. **Build the review prompt** combining beads context + diff:
2150
- \`\`\`
2151
- ISSUE: <title>
2152
- DESCRIPTION: <description>
2153
- DIFF:
2154
- <git diff output>
2155
-
2156
- Review these changes for:
2157
- 1. Correctness bugs and logic errors
2158
- 2. Security vulnerabilities
2159
- 3. Missed edge cases
2160
- 4. Code quality issues
2161
- Output a numbered list of findings. Be concise and actionable. Skip praise.
2162
- \`\`\`
2163
- 4. **Call Gemini headless**:
2164
- \`\`\`bash
2165
- echo "<prompt>" | gemini -p "Review the following code changes" --output-format json
2166
- \`\`\`
2167
- 5. **Parse the response** \u2014 extract the \`.response\` field from the JSON output.
2168
- 6. **Present findings** to the user as a numbered list with severity tags (P1/P2/P3).
2169
- 7. **If Gemini returns an error** (auth failure, rate limit, timeout), report the error and skip gracefully. Never block the pipeline on external reviewer failure.
2170
-
2171
- ## Output Format
2172
- \`\`\`
2173
- ## Gemini External Review
2174
-
2175
- **Status**: Completed | Skipped (reason)
2176
- **Findings**: N items
2177
-
2178
- 1. [P2] <finding description> \u2014 <file:line>
2179
- 2. [P3] <finding description> \u2014 <file:line>
2180
- ...
2181
- \`\`\`
2182
-
2183
- ## Important
2184
- - This is **advisory, not blocking**. Findings inform but do not gate the pipeline.
2185
- - Do NOT retry more than once on failure.
2186
- - Do NOT feed the entire codebase \u2014 only the diff and issue context.
2187
- `,
2188
- "external-reviewer-codex.md": `---
2189
- name: External Reviewer (Codex)
2190
- description: Cross-model review using OpenAI Codex CLI in headless mode
2191
- model: sonnet
2192
- ---
2193
-
2194
- # External Reviewer \u2014 Codex
2195
-
2196
- ## Role
2197
- Run a cross-model code review by invoking the OpenAI Codex CLI in headless exec mode. Provides an independent perspective from OpenAI's reasoning models to catch issues Claude may miss.
2198
-
2199
- ## Prerequisites
2200
- - Codex CLI installed (\`npm i -g @openai/codex\`)
2201
- - Authenticated (\`codex login --api-key\`)
2202
-
2203
- ## Instructions
2204
- 1. **Check availability** \u2014 run \`command -v codex\` via Bash. If not found, report "Codex CLI not installed \u2014 skipping external review" and stop.
2205
- 2. **Gather context**:
2206
- - Get the beads issue being worked on: \`bd list --status=in_progress\` then \`bd show <id>\` to get the issue title and description.
2207
- - Get the diff: \`git diff HEAD~1\` (or the appropriate range for this session's changes).
2208
- 3. **Build the review prompt** combining beads context + diff:
2209
- \`\`\`
2210
- ISSUE: <title>
2211
- DESCRIPTION: <description>
2212
- DIFF:
2213
- <git diff output>
2214
-
2215
- Review these changes for:
2216
- 1. Correctness bugs and logic errors
2217
- 2. Security vulnerabilities
2218
- 3. Missed edge cases
2219
- 4. Code quality issues
2220
- Output a numbered list of findings. Be concise and actionable. Skip praise.
2221
- \`\`\`
2222
- 4. **Call Codex headless**:
2223
- \`\`\`bash
2224
- echo "<prompt>" | codex exec --quiet "Review the following code changes for bugs, security issues, and missed edge cases"
2225
- \`\`\`
2226
- 5. **Parse the response** \u2014 Codex exec prints the final answer to stdout.
2227
- 6. **Present findings** to the user as a numbered list with severity tags (P1/P2/P3).
2228
- 7. **If Codex returns an error** (auth failure, rate limit, timeout), report the error and skip gracefully. Never block the pipeline on external reviewer failure.
2133
+ // src/cli-error-format.ts
2134
+ function formatError(command, code, message, remediation) {
2135
+ return `ERROR [${command}] ${code}: ${message} \u2014 ${remediation}`;
2136
+ }
2137
+ var STATE_DIR = ".claude";
2138
+ var STATE_FILE = ".ca-phase-state.json";
2139
+ var EPIC_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
2140
+ var PHASES = ["brainstorm", "plan", "work", "review", "compound"];
2141
+ var GATES = ["post-plan", "gate-3", "gate-4", "final"];
2142
+ var PHASE_INDEX = {
2143
+ brainstorm: 1,
2144
+ plan: 2,
2145
+ work: 3,
2146
+ review: 4,
2147
+ compound: 5
2148
+ };
2149
+ function getStatePath(repoRoot) {
2150
+ return join(repoRoot, STATE_DIR, STATE_FILE);
2151
+ }
2152
+ function isPhaseName(value) {
2153
+ return typeof value === "string" && PHASES.includes(value);
2154
+ }
2155
+ function isGateName(value) {
2156
+ return typeof value === "string" && GATES.includes(value);
2157
+ }
2158
+ function isIsoDate(value) {
2159
+ if (typeof value !== "string") return false;
2160
+ return !Number.isNaN(Date.parse(value));
2161
+ }
2162
+ function isStringArray(value) {
2163
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2164
+ }
2165
+ function validatePhaseState(raw) {
2166
+ if (typeof raw !== "object" || raw === null) return false;
2167
+ const state = raw;
2168
+ return typeof state.lfg_active === "boolean" && typeof state.epic_id === "string" && isPhaseName(state.current_phase) && typeof state.phase_index === "number" && state.phase_index >= 1 && state.phase_index <= 5 && isStringArray(state.skills_read) && Array.isArray(state.gates_passed) && state.gates_passed.every((gate) => isGateName(gate)) && isIsoDate(state.started_at);
2169
+ }
2170
+ function expectedGateForPhase(phaseIndex) {
2171
+ if (phaseIndex === 2) return "post-plan";
2172
+ if (phaseIndex === 3) return "gate-3";
2173
+ if (phaseIndex === 4) return "gate-4";
2174
+ if (phaseIndex === 5) return "final";
2175
+ return null;
2176
+ }
2177
+ function initPhaseState(repoRoot, epicId) {
2178
+ const dir = join(repoRoot, STATE_DIR);
2179
+ mkdirSync(dir, { recursive: true });
2180
+ const state = {
2181
+ lfg_active: true,
2182
+ epic_id: epicId,
2183
+ current_phase: "brainstorm",
2184
+ phase_index: PHASE_INDEX.brainstorm,
2185
+ skills_read: [],
2186
+ gates_passed: [],
2187
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
2188
+ };
2189
+ writeFileSync(getStatePath(repoRoot), JSON.stringify(state, null, 2), "utf-8");
2190
+ return state;
2191
+ }
2192
+ function getPhaseState(repoRoot) {
2193
+ try {
2194
+ const path = getStatePath(repoRoot);
2195
+ if (!existsSync(path)) return null;
2196
+ const raw = readFileSync(path, "utf-8");
2197
+ const parsed = JSON.parse(raw);
2198
+ return validatePhaseState(parsed) ? parsed : null;
2199
+ } catch {
2200
+ return null;
2201
+ }
2202
+ }
2203
+ function updatePhaseState(repoRoot, partial) {
2204
+ const current = getPhaseState(repoRoot);
2205
+ if (current === null) return null;
2206
+ const updated = {
2207
+ ...current,
2208
+ ...partial
2209
+ };
2210
+ if (!validatePhaseState(updated)) return null;
2211
+ writeFileSync(getStatePath(repoRoot), JSON.stringify(updated, null, 2), "utf-8");
2212
+ return updated;
2213
+ }
2214
+ function startPhase(repoRoot, phase) {
2215
+ return updatePhaseState(repoRoot, {
2216
+ current_phase: phase,
2217
+ phase_index: PHASE_INDEX[phase]
2218
+ });
2219
+ }
2220
+ function cleanPhaseState(repoRoot) {
2221
+ try {
2222
+ const path = getStatePath(repoRoot);
2223
+ if (existsSync(path)) unlinkSync(path);
2224
+ } catch {
2225
+ }
2226
+ }
2227
+ function recordGatePassed(repoRoot, gate) {
2228
+ const current = getPhaseState(repoRoot);
2229
+ if (current === null) return null;
2230
+ const gatesPassed = current.gates_passed.includes(gate) ? current.gates_passed : [...current.gates_passed, gate];
2231
+ const updated = { ...current, gates_passed: gatesPassed };
2232
+ if (gate === "final") {
2233
+ cleanPhaseState(repoRoot);
2234
+ return updated;
2235
+ }
2236
+ writeFileSync(getStatePath(repoRoot), JSON.stringify(updated, null, 2), "utf-8");
2237
+ return updated;
2238
+ }
2239
+ function printStatusHuman(state) {
2240
+ if (state === null) {
2241
+ console.log("No active LFG session.");
2242
+ return;
2243
+ }
2244
+ console.log("Active LFG Session");
2245
+ console.log(` Epic: ${state.epic_id}`);
2246
+ console.log(` Phase: ${state.current_phase} (${state.phase_index}/5)`);
2247
+ console.log(` Skills read: ${state.skills_read.length === 0 ? "(none)" : state.skills_read.join(", ")}`);
2248
+ console.log(` Gates passed: ${state.gates_passed.length === 0 ? "(none)" : state.gates_passed.join(", ")}`);
2249
+ console.log(` Started: ${state.started_at}`);
2250
+ }
2251
+ function registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot) {
2252
+ phaseCheck.command("init <epic-id>").description("Initialize phase state for an epic").action((epicId) => {
2253
+ if (!EPIC_ID_PATTERN.test(epicId)) {
2254
+ console.error(`Invalid epic ID: "${epicId}"`);
2255
+ process.exit(1);
2256
+ }
2257
+ if (getDryRun()) {
2258
+ console.log(`[dry-run] Would initialize phase state for epic ${epicId} in ${repoRoot()}`);
2259
+ return;
2260
+ }
2261
+ initPhaseState(repoRoot(), epicId);
2262
+ console.log(`Phase state initialized for ${epicId}. Current phase: brainstorm (1/5).`);
2263
+ });
2264
+ phaseCheck.command("start <phase>").description("Start or resume a phase").action((phase) => {
2265
+ if (!isPhaseName(phase)) {
2266
+ console.error(`Invalid phase: "${phase}". Valid phases: ${PHASES.join(", ")}`);
2267
+ process.exit(1);
2268
+ }
2269
+ if (getDryRun()) {
2270
+ console.log(`[dry-run] Would start phase ${phase}`);
2271
+ return;
2272
+ }
2273
+ const state = startPhase(repoRoot(), phase);
2274
+ if (state === null) {
2275
+ console.error("No active phase state. Run: ca phase-check init <epic-id>");
2276
+ process.exit(1);
2277
+ }
2278
+ console.log(`Phase updated: ${state.current_phase} (${state.phase_index}/5).`);
2279
+ });
2280
+ phaseCheck.command("gate <gate-name>").description("Record a phase gate as passed").action((gateName) => {
2281
+ if (!isGateName(gateName)) {
2282
+ console.error(`Invalid gate: "${gateName}". Valid gates: ${GATES.join(", ")}`);
2283
+ process.exit(1);
2284
+ }
2285
+ if (getDryRun()) {
2286
+ console.log(`[dry-run] Would record gate ${gateName}`);
2287
+ return;
2288
+ }
2289
+ const state = recordGatePassed(repoRoot(), gateName);
2290
+ if (state === null) {
2291
+ console.error("No active phase state. Run: ca phase-check init <epic-id>");
2292
+ process.exit(1);
2293
+ }
2294
+ if (gateName === "final") {
2295
+ console.log("Final gate recorded. Phase state cleaned.");
2296
+ return;
2297
+ }
2298
+ console.log(`Gate recorded: ${gateName}.`);
2299
+ });
2300
+ phaseCheck.command("status").description("Show current phase state").option("--json", "Output raw JSON").action((options) => {
2301
+ const state = getPhaseState(repoRoot());
2302
+ if (options.json) {
2303
+ console.log(JSON.stringify(state ?? { lfg_active: false }));
2304
+ return;
2305
+ }
2306
+ printStatusHuman(state);
2307
+ });
2308
+ phaseCheck.command("clean").description("Remove phase state file").action(() => {
2309
+ if (getDryRun()) {
2310
+ console.log("[dry-run] Would delete phase state file");
2311
+ return;
2312
+ }
2313
+ cleanPhaseState(repoRoot());
2314
+ console.log("Phase state cleaned.");
2315
+ });
2316
+ }
2317
+ function registerPhaseCheckCommand(program2) {
2318
+ const phaseCheck = program2.command("phase-check").description("Manage LFG phase state").option("--dry-run", "Show what would be done without making changes");
2319
+ const getDryRun = () => phaseCheck.opts().dryRun ?? false;
2320
+ const repoRoot = () => process.cwd();
2321
+ registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot);
2322
+ program2.command("phase-clean").description("Remove phase state file (alias for `phase-check clean`)").action(() => {
2323
+ cleanPhaseState(repoRoot());
2324
+ console.log("Phase state cleaned.");
2325
+ });
2326
+ }
2229
2327
 
2230
- ## Output Format
2231
- \`\`\`
2232
- ## Codex External Review
2328
+ // src/setup/hooks-phase-guard.ts
2329
+ function processPhaseGuard(repoRoot, toolName, _toolInput) {
2330
+ try {
2331
+ if (toolName !== "Edit" && toolName !== "Write") return {};
2332
+ const state = getPhaseState(repoRoot);
2333
+ if (state === null || !state.lfg_active) return {};
2334
+ const expectedSkillPath = `.claude/skills/compound/${state.current_phase}/SKILL.md`;
2335
+ const skillRead = state.skills_read.includes(expectedSkillPath);
2336
+ if (!skillRead) {
2337
+ return {
2338
+ hookSpecificOutput: {
2339
+ hookEventName: "PreToolUse",
2340
+ additionalContext: `PHASE GUARD WARNING: You are in LFG phase ${state.phase_index}/5 (${state.current_phase}) but have NOT read the skill file yet. Read ${expectedSkillPath} before continuing.`
2341
+ }
2342
+ };
2343
+ }
2344
+ return {};
2345
+ } catch {
2346
+ return {};
2347
+ }
2348
+ }
2233
2349
 
2234
- **Status**: Completed | Skipped (reason)
2235
- **Findings**: N items
2350
+ // src/setup/hooks-read-tracker.ts
2351
+ var SKILL_PATH_PATTERN = /(?:^|\/)\.claude\/skills\/compound\/([^/]+)\/SKILL\.md$/;
2352
+ function normalizePath(path) {
2353
+ return path.replaceAll("\\", "/");
2354
+ }
2355
+ function toCanonicalSkillPath(filePath) {
2356
+ const normalized = normalizePath(filePath);
2357
+ const match = SKILL_PATH_PATTERN.exec(normalized);
2358
+ if (!match?.[1]) return null;
2359
+ return `.claude/skills/compound/${match[1]}/SKILL.md`;
2360
+ }
2361
+ function processReadTracker(repoRoot, toolName, toolInput) {
2362
+ try {
2363
+ if (toolName !== "Read") return {};
2364
+ const state = getPhaseState(repoRoot);
2365
+ if (state === null || !state.lfg_active) return {};
2366
+ const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : null;
2367
+ if (filePath === null) return {};
2368
+ const canonicalPath = toCanonicalSkillPath(filePath);
2369
+ if (canonicalPath === null) return {};
2370
+ if (!state.skills_read.includes(canonicalPath)) {
2371
+ updatePhaseState(repoRoot, {
2372
+ skills_read: [...state.skills_read, canonicalPath]
2373
+ });
2374
+ }
2375
+ return {};
2376
+ } catch {
2377
+ return {};
2378
+ }
2379
+ }
2236
2380
 
2237
- 1. [P2] <finding description> \u2014 <file:line>
2238
- 2. [P3] <finding description> \u2014 <file:line>
2239
- ...
2240
- \`\`\`
2381
+ // src/setup/hooks-stop-audit.ts
2382
+ function hasTransitionEvidence(state) {
2383
+ if (state.phase_index === 5) return true;
2384
+ const nextPhase = PHASES[state.phase_index];
2385
+ if (nextPhase === void 0) return false;
2386
+ const nextSkillPath = `.claude/skills/compound/${nextPhase}/SKILL.md`;
2387
+ return state.skills_read.includes(nextSkillPath);
2388
+ }
2389
+ function processStopAudit(repoRoot, stopHookActive = false) {
2390
+ try {
2391
+ if (stopHookActive) return {};
2392
+ const state = getPhaseState(repoRoot);
2393
+ if (state === null || !state.lfg_active) return {};
2394
+ const expectedGate = expectedGateForPhase(state.phase_index);
2395
+ if (expectedGate === null) return {};
2396
+ if (state.gates_passed.includes(expectedGate)) return {};
2397
+ if (!hasTransitionEvidence(state)) return {};
2398
+ return {
2399
+ continue: false,
2400
+ stopReason: `PHASE GATE NOT VERIFIED: ${state.current_phase} requires gate '${expectedGate}'. Run: npx ca phase-check gate ${expectedGate}`
2401
+ };
2402
+ } catch {
2403
+ return {};
2404
+ }
2405
+ }
2406
+
2407
+ // src/setup/hooks.ts
2408
+ var HOOK_FILE_MODE = 493;
2409
+ var CORRECTION_PATTERNS = [
2410
+ /\bactually\b/i,
2411
+ /\bno[,.]?\s/i,
2412
+ /\bwrong\b/i,
2413
+ /\bthat'?s not right\b/i,
2414
+ /\bthat'?s incorrect\b/i,
2415
+ /\buse .+ instead\b/i,
2416
+ /\bi told you\b/i,
2417
+ /\bi already said\b/i,
2418
+ /\bnot like that\b/i,
2419
+ /\byou forgot\b/i,
2420
+ /\byou missed\b/i,
2421
+ /\bstop\s*(,\s*)?(doing|using|that)\b/i,
2422
+ /\bwait\s*(,\s*)?(that|no|wrong)\b/i
2423
+ ];
2424
+ var HIGH_CONFIDENCE_PLANNING = [
2425
+ /\bdecide\b/i,
2426
+ /\bchoose\b/i,
2427
+ /\bpick\b/i,
2428
+ /\bwhich approach\b/i,
2429
+ /\bwhat do you think\b/i,
2430
+ /\bshould we\b/i,
2431
+ /\bwould you\b/i,
2432
+ /\bhow should\b/i,
2433
+ /\bwhat'?s the best\b/i,
2434
+ /\badd feature\b/i,
2435
+ /\bset up\b/i
2436
+ ];
2437
+ var LOW_CONFIDENCE_PLANNING = [
2438
+ /\bimplement\b/i,
2439
+ /\bbuild\b/i,
2440
+ /\bcreate\b/i,
2441
+ /\brefactor\b/i,
2442
+ /\bfix\b/i,
2443
+ /\bwrite\b/i,
2444
+ /\bdevelop\b/i
2445
+ ];
2446
+ var CORRECTION_REMINDER = "Remember: You have memory tools available - `npx ca learn` to save insights, `npx ca search` to find past solutions.";
2447
+ var PLANNING_REMINDER = "If you're uncertain or hesitant, remember your memory tools: `npx ca search` may have relevant context from past sessions.";
2448
+ function detectCorrection(prompt) {
2449
+ return CORRECTION_PATTERNS.some((pattern) => pattern.test(prompt));
2450
+ }
2451
+ function detectPlanning(prompt) {
2452
+ if (HIGH_CONFIDENCE_PLANNING.some((pattern) => pattern.test(prompt))) {
2453
+ return true;
2454
+ }
2455
+ const lowMatches = LOW_CONFIDENCE_PLANNING.filter((pattern) => pattern.test(prompt));
2456
+ return lowMatches.length >= 2;
2457
+ }
2458
+ function processUserPrompt(prompt) {
2459
+ if (detectCorrection(prompt)) {
2460
+ return {
2461
+ hookSpecificOutput: {
2462
+ hookEventName: "UserPromptSubmit",
2463
+ additionalContext: CORRECTION_REMINDER
2464
+ }
2465
+ };
2466
+ }
2467
+ if (detectPlanning(prompt)) {
2468
+ return {
2469
+ hookSpecificOutput: {
2470
+ hookEventName: "UserPromptSubmit",
2471
+ additionalContext: PLANNING_REMINDER
2472
+ }
2473
+ };
2474
+ }
2475
+ return {};
2476
+ }
2477
+ var SAME_TARGET_THRESHOLD = 2;
2478
+ var TOTAL_FAILURE_THRESHOLD = 3;
2479
+ var STATE_FILE_NAME = ".ca-failure-state.json";
2480
+ var STATE_MAX_AGE_MS = 60 * 60 * 1e3;
2481
+ var failureCount = 0;
2482
+ var lastFailedTarget = null;
2483
+ var sameTargetCount = 0;
2484
+ function defaultState() {
2485
+ return { count: 0, lastTarget: null, sameTargetCount: 0, timestamp: Date.now() };
2486
+ }
2487
+ function readFailureState(stateDir) {
2488
+ try {
2489
+ const filePath = join(stateDir, STATE_FILE_NAME);
2490
+ if (!existsSync(filePath)) return defaultState();
2491
+ const raw = readFileSync(filePath, "utf-8");
2492
+ const parsed = JSON.parse(raw);
2493
+ if (Date.now() - parsed.timestamp > STATE_MAX_AGE_MS) return defaultState();
2494
+ return parsed;
2495
+ } catch {
2496
+ return defaultState();
2497
+ }
2498
+ }
2499
+ function writeFailureState(stateDir, state) {
2500
+ try {
2501
+ const filePath = join(stateDir, STATE_FILE_NAME);
2502
+ writeFileSync(filePath, JSON.stringify(state), "utf-8");
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ function deleteStateFile(stateDir) {
2507
+ try {
2508
+ const filePath = join(stateDir, STATE_FILE_NAME);
2509
+ if (existsSync(filePath)) unlinkSync(filePath);
2510
+ } catch {
2511
+ }
2512
+ }
2513
+ var FAILURE_TIP = "Tip: Multiple failures detected. `npx ca search` may have solutions for similar issues.";
2514
+ function resetFailureState(stateDir) {
2515
+ failureCount = 0;
2516
+ lastFailedTarget = null;
2517
+ sameTargetCount = 0;
2518
+ if (stateDir) deleteStateFile(stateDir);
2519
+ }
2520
+ function getFailureTarget(toolName, toolInput) {
2521
+ if (toolName === "Bash" && typeof toolInput.command === "string") {
2522
+ const trimmed = toolInput.command.trim();
2523
+ const firstSpace = trimmed.indexOf(" ");
2524
+ return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
2525
+ }
2526
+ if ((toolName === "Edit" || toolName === "Write") && typeof toolInput.file_path === "string") {
2527
+ return toolInput.file_path;
2528
+ }
2529
+ return null;
2530
+ }
2531
+ function processToolFailure(toolName, toolInput, stateDir) {
2532
+ if (stateDir) {
2533
+ const persisted = readFailureState(stateDir);
2534
+ failureCount = persisted.count;
2535
+ lastFailedTarget = persisted.lastTarget;
2536
+ sameTargetCount = persisted.sameTargetCount;
2537
+ }
2538
+ failureCount++;
2539
+ const target = getFailureTarget(toolName, toolInput);
2540
+ if (target !== null && target === lastFailedTarget) {
2541
+ sameTargetCount++;
2542
+ } else {
2543
+ sameTargetCount = 1;
2544
+ lastFailedTarget = target;
2545
+ }
2546
+ const shouldShowTip = sameTargetCount >= SAME_TARGET_THRESHOLD || failureCount >= TOTAL_FAILURE_THRESHOLD;
2547
+ if (shouldShowTip) {
2548
+ resetFailureState(stateDir);
2549
+ return {
2550
+ hookSpecificOutput: {
2551
+ hookEventName: "PostToolUseFailure",
2552
+ additionalContext: FAILURE_TIP
2553
+ }
2554
+ };
2555
+ }
2556
+ if (stateDir) {
2557
+ writeFailureState(stateDir, {
2558
+ count: failureCount,
2559
+ lastTarget: lastFailedTarget,
2560
+ sameTargetCount,
2561
+ timestamp: Date.now()
2562
+ });
2563
+ }
2564
+ return {};
2565
+ }
2566
+ function processToolSuccess(stateDir) {
2567
+ resetFailureState(stateDir);
2568
+ }
2569
+ function hasCompoundAgentHook(content) {
2570
+ return content.includes(HOOK_MARKER);
2571
+ }
2572
+ async function getGitHooksDir(repoRoot) {
2573
+ const gitDir = join(repoRoot, ".git");
2574
+ if (!existsSync(gitDir)) {
2575
+ return null;
2576
+ }
2577
+ const configPath2 = join(gitDir, "config");
2578
+ if (existsSync(configPath2)) {
2579
+ const config = await readFile(configPath2, "utf-8");
2580
+ const match = /hooksPath\s*=\s*(.+)$/m.exec(config);
2581
+ if (match?.[1]) {
2582
+ const hooksPath = match[1].trim();
2583
+ return hooksPath.startsWith("/") ? hooksPath : join(repoRoot, hooksPath);
2584
+ }
2585
+ }
2586
+ const defaultHooksDir = join(gitDir, "hooks");
2587
+ return existsSync(defaultHooksDir) ? defaultHooksDir : null;
2588
+ }
2589
+ function findFirstTopLevelExitLine(lines) {
2590
+ let insideFunction = 0;
2591
+ let heredocDelimiter = null;
2592
+ for (let i = 0; i < lines.length; i++) {
2593
+ const line = lines[i] ?? "";
2594
+ const trimmed = line.trim();
2595
+ if (heredocDelimiter !== null) {
2596
+ if (trimmed === heredocDelimiter) {
2597
+ heredocDelimiter = null;
2598
+ }
2599
+ continue;
2600
+ }
2601
+ const heredocMatch = /<<-?\s*['"]?(\w+)['"]?/.exec(line);
2602
+ if (heredocMatch?.[1]) {
2603
+ heredocDelimiter = heredocMatch[1];
2604
+ continue;
2605
+ }
2606
+ for (const char of line) {
2607
+ if (char === "{") insideFunction++;
2608
+ if (char === "}") insideFunction = Math.max(0, insideFunction - 1);
2609
+ }
2610
+ if (insideFunction > 0) {
2611
+ continue;
2612
+ }
2613
+ if (/^\s*exit\s+(\d+|\$\w+|\$\?)\s*$/.test(trimmed)) {
2614
+ return i;
2615
+ }
2616
+ }
2617
+ return -1;
2618
+ }
2619
+ async function installPreCommitHook(repoRoot) {
2620
+ const gitHooksDir = await getGitHooksDir(repoRoot);
2621
+ if (!gitHooksDir) {
2622
+ return { status: "not_git_repo" };
2623
+ }
2624
+ await mkdir(gitHooksDir, { recursive: true });
2625
+ const hookPath = join(gitHooksDir, "pre-commit");
2626
+ if (existsSync(hookPath)) {
2627
+ const content = await readFile(hookPath, "utf-8");
2628
+ if (hasCompoundAgentHook(content)) {
2629
+ return { status: "already_installed" };
2630
+ }
2631
+ const lines = content.split("\n");
2632
+ const exitLineIndex = findFirstTopLevelExitLine(lines);
2633
+ let newContent;
2634
+ if (exitLineIndex === -1) {
2635
+ newContent = content.trimEnd() + "\n" + COMPOUND_AGENT_HOOK_BLOCK;
2636
+ } else {
2637
+ const before = lines.slice(0, exitLineIndex);
2638
+ const after = lines.slice(exitLineIndex);
2639
+ newContent = before.join("\n") + COMPOUND_AGENT_HOOK_BLOCK + after.join("\n");
2640
+ }
2641
+ await writeFile(hookPath, newContent, "utf-8");
2642
+ chmodSync(hookPath, HOOK_FILE_MODE);
2643
+ return { status: "appended" };
2644
+ }
2645
+ await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
2646
+ chmodSync(hookPath, HOOK_FILE_MODE);
2647
+ return { status: "installed" };
2648
+ }
2649
+ async function readStdin() {
2650
+ const chunks = [];
2651
+ for await (const chunk of process.stdin) {
2652
+ chunks.push(chunk);
2653
+ }
2654
+ return Buffer.concat(chunks).toString("utf-8");
2655
+ }
2656
+ async function runUserPromptHook() {
2657
+ try {
2658
+ const input = await readStdin();
2659
+ const data = JSON.parse(input);
2660
+ if (!data.prompt) {
2661
+ console.log(JSON.stringify({}));
2662
+ return;
2663
+ }
2664
+ const result = processUserPrompt(data.prompt);
2665
+ console.log(JSON.stringify(result));
2666
+ } catch {
2667
+ console.log(JSON.stringify({}));
2668
+ }
2669
+ }
2670
+ async function runPostToolFailureHook() {
2671
+ try {
2672
+ const input = await readStdin();
2673
+ const data = JSON.parse(input);
2674
+ if (!data.tool_name) {
2675
+ console.log(JSON.stringify({}));
2676
+ return;
2677
+ }
2678
+ const stateDir = join(process.cwd(), ".claude");
2679
+ const result = processToolFailure(data.tool_name, data.tool_input ?? {}, stateDir);
2680
+ console.log(JSON.stringify(result));
2681
+ } catch {
2682
+ console.log(JSON.stringify({}));
2683
+ }
2684
+ }
2685
+ async function runPostToolSuccessHook() {
2686
+ try {
2687
+ await readStdin();
2688
+ const stateDir = join(process.cwd(), ".claude");
2689
+ processToolSuccess(stateDir);
2690
+ console.log(JSON.stringify({}));
2691
+ } catch {
2692
+ console.log(JSON.stringify({}));
2693
+ }
2694
+ }
2695
+ async function runToolHook(processor) {
2696
+ try {
2697
+ const input = await readStdin();
2698
+ const data = JSON.parse(input);
2699
+ if (!data.tool_name) {
2700
+ console.log(JSON.stringify({}));
2701
+ return;
2702
+ }
2703
+ console.log(JSON.stringify(processor(process.cwd(), data.tool_name, data.tool_input ?? {})));
2704
+ } catch {
2705
+ console.log(JSON.stringify({}));
2706
+ }
2707
+ }
2708
+ async function runStopAuditHook() {
2709
+ try {
2710
+ const input = await readStdin();
2711
+ const data = JSON.parse(input);
2712
+ console.log(JSON.stringify(processStopAudit(process.cwd(), data.stop_hook_active ?? false)));
2713
+ } catch {
2714
+ console.log(JSON.stringify({}));
2715
+ }
2716
+ }
2717
+ function registerHooksCommand(program2) {
2718
+ const hooksCommand = program2.command("hooks").description("Git hooks management");
2719
+ hooksCommand.command("run <hook>").description("Run a hook script (called by git/Claude hooks)").option("--json", "Output as JSON").action(async (hook, options) => {
2720
+ if (hook === "pre-commit") {
2721
+ if (options.json) {
2722
+ console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
2723
+ } else {
2724
+ console.log(PRE_COMMIT_MESSAGE);
2725
+ }
2726
+ } else if (hook === "user-prompt") {
2727
+ await runUserPromptHook();
2728
+ } else if (hook === "post-tool-failure") {
2729
+ await runPostToolFailureHook();
2730
+ } else if (hook === "post-tool-success") {
2731
+ await runPostToolSuccessHook();
2732
+ } else if (hook === "phase-guard") {
2733
+ await runToolHook(processPhaseGuard);
2734
+ } else if (hook === "post-read" || hook === "read-tracker") {
2735
+ await runToolHook(processReadTracker);
2736
+ } else if (hook === "phase-audit" || hook === "stop-audit") {
2737
+ await runStopAuditHook();
2738
+ } else {
2739
+ if (options.json) {
2740
+ console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
2741
+ } else {
2742
+ console.error(
2743
+ formatError(
2744
+ "hooks",
2745
+ "UNKNOWN_HOOK",
2746
+ `Unknown hook: ${hook}`,
2747
+ "Valid hooks: pre-commit, user-prompt, post-tool-failure, post-tool-success, post-read (or read-tracker), phase-guard, phase-audit (or stop-audit)"
2748
+ )
2749
+ );
2750
+ }
2751
+ process.exit(1);
2752
+ }
2753
+ });
2754
+ }
2755
+
2756
+ // src/setup/templates/agents-external.ts
2757
+ var EXTERNAL_AGENT_TEMPLATES = {
2758
+ "external-reviewer-gemini.md": `---
2759
+ name: External Reviewer (Gemini)
2760
+ description: Cross-model review using Gemini CLI in headless mode
2761
+ model: sonnet
2762
+ ---
2763
+
2764
+ # External Reviewer \u2014 Gemini
2765
+
2766
+ ## Role
2767
+ Run a cross-model code review by invoking the Gemini CLI in headless mode. Provides an independent perspective from a different LLM to catch issues Claude may miss.
2768
+
2769
+ ## Prerequisites
2770
+ - Gemini CLI installed (\`npm i -g @google/gemini-cli\`)
2771
+ - Authenticated (\`gemini auth login\`)
2772
+
2773
+ ## Instructions
2774
+ 1. **Check availability** \u2014 run \`command -v gemini\` via Bash. If not found, report "Gemini CLI not installed \u2014 skipping external review" and stop.
2775
+ 2. **Gather context**:
2776
+ - Get the beads issue being worked on: \`bd list --status=in_progress\` then \`bd show <id>\` to get the issue title and description.
2777
+ - Get the diff: \`git diff HEAD~1\` (or the appropriate range for this session's changes).
2778
+ 3. **Build the review prompt** combining beads context + diff:
2779
+ \`\`\`
2780
+ ISSUE: <title>
2781
+ DESCRIPTION: <description>
2782
+ DIFF:
2783
+ <git diff output>
2784
+
2785
+ Review these changes for:
2786
+ 1. Correctness bugs and logic errors
2787
+ 2. Security vulnerabilities
2788
+ 3. Missed edge cases
2789
+ 4. Code quality issues
2790
+ Output a numbered list of findings. Be concise and actionable. Skip praise.
2791
+ \`\`\`
2792
+ 4. **Call Gemini headless**:
2793
+ \`\`\`bash
2794
+ echo "<prompt>" | gemini -p "Review the following code changes" --output-format json
2795
+ \`\`\`
2796
+ 5. **Parse the response** \u2014 extract the \`.response\` field from the JSON output.
2797
+ 6. **Present findings** to the user as a numbered list with severity tags (P1/P2/P3).
2798
+ 7. **If Gemini returns an error** (auth failure, rate limit, timeout), report the error and skip gracefully. Never block the pipeline on external reviewer failure.
2799
+
2800
+ ## Output Format
2801
+ \`\`\`
2802
+ ## Gemini External Review
2803
+
2804
+ **Status**: Completed | Skipped (reason)
2805
+ **Findings**: N items
2806
+
2807
+ 1. [P2] <finding description> \u2014 <file:line>
2808
+ 2. [P3] <finding description> \u2014 <file:line>
2809
+ ...
2810
+ \`\`\`
2811
+
2812
+ ## Important
2813
+ - This is **advisory, not blocking**. Findings inform but do not gate the pipeline.
2814
+ - Do NOT retry more than once on failure.
2815
+ - Do NOT feed the entire codebase \u2014 only the diff and issue context.
2816
+ `,
2817
+ "external-reviewer-codex.md": `---
2818
+ name: External Reviewer (Codex)
2819
+ description: Cross-model review using OpenAI Codex CLI in headless mode
2820
+ model: sonnet
2821
+ ---
2822
+
2823
+ # External Reviewer \u2014 Codex
2824
+
2825
+ ## Role
2826
+ Run a cross-model code review by invoking the OpenAI Codex CLI in headless exec mode. Provides an independent perspective from OpenAI's reasoning models to catch issues Claude may miss.
2827
+
2828
+ ## Prerequisites
2829
+ - Codex CLI installed (\`npm i -g @openai/codex\`)
2830
+ - Authenticated (\`codex login --api-key\`)
2831
+
2832
+ ## Instructions
2833
+ 1. **Check availability** \u2014 run \`command -v codex\` via Bash. If not found, report "Codex CLI not installed \u2014 skipping external review" and stop.
2834
+ 2. **Gather context**:
2835
+ - Get the beads issue being worked on: \`bd list --status=in_progress\` then \`bd show <id>\` to get the issue title and description.
2836
+ - Get the diff: \`git diff HEAD~1\` (or the appropriate range for this session's changes).
2837
+ 3. **Build the review prompt** combining beads context + diff:
2838
+ \`\`\`
2839
+ ISSUE: <title>
2840
+ DESCRIPTION: <description>
2841
+ DIFF:
2842
+ <git diff output>
2843
+
2844
+ Review these changes for:
2845
+ 1. Correctness bugs and logic errors
2846
+ 2. Security vulnerabilities
2847
+ 3. Missed edge cases
2848
+ 4. Code quality issues
2849
+ Output a numbered list of findings. Be concise and actionable. Skip praise.
2850
+ \`\`\`
2851
+ 4. **Call Codex headless**:
2852
+ \`\`\`bash
2853
+ echo "<prompt>" | codex exec --quiet "Review the following code changes for bugs, security issues, and missed edge cases"
2854
+ \`\`\`
2855
+ 5. **Parse the response** \u2014 Codex exec prints the final answer to stdout.
2856
+ 6. **Present findings** to the user as a numbered list with severity tags (P1/P2/P3).
2857
+ 7. **If Codex returns an error** (auth failure, rate limit, timeout), report the error and skip gracefully. Never block the pipeline on external reviewer failure.
2858
+
2859
+ ## Output Format
2860
+ \`\`\`
2861
+ ## Codex External Review
2862
+
2863
+ **Status**: Completed | Skipped (reason)
2864
+ **Findings**: N items
2865
+
2866
+ 1. [P2] <finding description> \u2014 <file:line>
2867
+ 2. [P3] <finding description> \u2014 <file:line>
2868
+ ...
2869
+ \`\`\`
2241
2870
 
2242
2871
  ## Important
2243
2872
  - This is **advisory, not blocking**. Findings inform but do not gate the pipeline.
@@ -2926,13 +3555,7 @@ $ARGUMENTS
2926
3555
 
2927
3556
  # Brainstorm
2928
3557
 
2929
- Run the **brainstorm** phase. Follow the brainstorm skill for full instructions.
2930
-
2931
- Key steps:
2932
- - Search memory and explore docs for prior context
2933
- - Clarify scope and constraints via AskUserQuestion
2934
- - Propose 2-3 approaches with tradeoffs
2935
- - Create a beads epic from conclusions
3558
+ **MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/brainstorm/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full workflow you must follow.
2936
3559
  `,
2937
3560
  "plan.md": `---
2938
3561
  name: compound:plan
@@ -2943,13 +3566,7 @@ $ARGUMENTS
2943
3566
 
2944
3567
  # Plan
2945
3568
 
2946
- Run the **plan** phase. Follow the plan skill for full instructions.
2947
-
2948
- Key steps:
2949
- - Spawn subagents to research constraints and patterns
2950
- - Decompose into tasks with acceptance criteria
2951
- - Create review and compound blocking tasks
2952
- - Verify POST-PLAN gates
3569
+ **MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/plan/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full workflow you must follow.
2953
3570
  `,
2954
3571
  "work.md": `---
2955
3572
  name: compound:work
@@ -2960,13 +3577,7 @@ $ARGUMENTS
2960
3577
 
2961
3578
  # Work
2962
3579
 
2963
- Run the **work** phase. Follow the work skill for full instructions.
2964
-
2965
- Key steps:
2966
- - Deploy AgentTeam with test-writers and implementers
2967
- - Lead coordinates, delegates, does not code directly
2968
- - Commit incrementally as tests pass
2969
- - Run verification gates before closing tasks
3580
+ **MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/work/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full workflow you must follow.
2970
3581
  `,
2971
3582
  "review.md": `---
2972
3583
  name: compound:review
@@ -2977,13 +3588,7 @@ $ARGUMENTS
2977
3588
 
2978
3589
  # Review
2979
3590
 
2980
- Run the **review** phase. Follow the review skill for full instructions.
2981
-
2982
- Key steps:
2983
- - Run quality gates, then spawn reviewers in parallel
2984
- - Classify findings as P1/P2/P3
2985
- - Fix all P1 findings before proceeding
2986
- - Submit to /implementation-reviewer as mandatory gate
3591
+ **MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/review/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full workflow you must follow.
2987
3592
  `,
2988
3593
  "compound.md": `---
2989
3594
  name: compound:compound
@@ -2994,13 +3599,7 @@ $ARGUMENTS
2994
3599
 
2995
3600
  # Compound
2996
3601
 
2997
- Run the **compound** phase. Follow the compound skill for full instructions.
2998
-
2999
- Key steps:
3000
- - Spawn analysis agents in an AgentTeam
3001
- - Apply quality filters, then store via npx ca learn
3002
- - Delegate CCT synthesis to compounding agent
3003
- - Verify FINAL GATE before closing epic
3602
+ **MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/compound/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full workflow you must follow.
3004
3603
  `,
3005
3604
  "lfg.md": `---
3006
3605
  name: compound:lfg
@@ -3012,13 +3611,7 @@ $ARGUMENTS
3012
3611
 
3013
3612
  # LFG
3014
3613
 
3015
- Run all 5 phases. Follow each phase skill for full instructions.
3016
-
3017
- - Brainstorm: explore and define the problem
3018
- - Plan: decompose into tasks with dependencies
3019
- - Work: delegate to AgentTeam for TDD
3020
- - Review: multi-agent review with severity classification
3021
- - Compound: capture lessons via npx ca learn
3614
+ **MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/lfg/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full orchestration workflow you must follow.
3022
3615
  `,
3023
3616
  // =========================================================================
3024
3617
  // Utility commands (CLI wrappers)
@@ -3426,6 +4019,76 @@ Before closing the epic:
3426
4019
  - Run \`pnpm test\` and \`pnpm lint\` -- must pass
3427
4020
  If verify-gates fails, the missing phase was SKIPPED. Go back and complete it.
3428
4021
  CRITICAL: 3/5 phases is NOT success. All 5 phases are required.
4022
+ `,
4023
+ lfg: `---
4024
+ name: LFG
4025
+ description: Full-cycle orchestrator chaining all five phases with gates and controls
4026
+ ---
4027
+
4028
+ # LFG Skill
4029
+
4030
+ ## Overview
4031
+ Chain all 5 phases end-to-end: Brainstorm, Plan, Work, Review, Compound. This skill governs the orchestration -- phase sequencing, gates, progress tracking, and error recovery.
4032
+
4033
+ ## CRITICAL RULE -- READ BEFORE EXECUTE
4034
+ Before starting EACH phase, you MUST use the Read tool to open its skill file:
4035
+ - .claude/skills/compound/brainstorm/SKILL.md
4036
+ - .claude/skills/compound/plan/SKILL.md
4037
+ - .claude/skills/compound/work/SKILL.md
4038
+ - .claude/skills/compound/review/SKILL.md
4039
+ - .claude/skills/compound/compound/SKILL.md
4040
+
4041
+ Do NOT proceed from memory. Read the skill, then follow it exactly.
4042
+
4043
+ ## Phase Execution Protocol
4044
+ 0. Initialize state: \`npx ca phase-check init <epic-id>\`
4045
+ For each phase:
4046
+ 1. Announce: "[Phase N/5] PHASE_NAME"
4047
+ 2. Start state: \`npx ca phase-check start <phase>\`
4048
+ 3. Read the phase skill file (see above)
4049
+ 4. Run \`npx ca search\` with the current goal -- display results before proceeding
4050
+ 5. Execute the phase following the skill instructions
4051
+ 6. Update epic state: \`bd update <epic-id> --notes="Phase: NAME COMPLETE | Next: NEXT"\`
4052
+ 7. Verify phase gate before proceeding to the next phase
4053
+
4054
+ ## Phase Gates (MANDATORY)
4055
+ - **After Plan**: Run \`bd list --status=open\` and verify Review + Compound tasks exist, then run \`npx ca phase-check gate post-plan\`
4056
+ - **After Work (GATE 3)**: \`bd list --status=in_progress\` must be empty. Then run \`npx ca phase-check gate gate-3\`
4057
+ - **After Review (GATE 4)**: /implementation-reviewer must have returned APPROVED. Then run \`npx ca phase-check gate gate-4\`
4058
+ - **After Compound (FINAL GATE)**: Run \`npx ca verify-gates <epic-id>\` (must PASS), \`pnpm test\`, and \`pnpm lint\`, then run \`npx ca phase-check gate final\` (auto-cleans phase state)
4059
+
4060
+ If a gate fails, DO NOT proceed. Fix the issue first.
4061
+
4062
+ ## Phase Control
4063
+ - **Skip phases**: Parse arguments for "from PHASE" (e.g., "from plan"). Skip earlier phases.
4064
+ - **Resume**: After interruption, run \`bd show <epic-id>\` and read notes for phase state. Resume from that phase.
4065
+ - **Retry**: If a phase fails, report and ask user to retry, skip, or abort via AskUserQuestion.
4066
+ - **Progress**: Always announce current phase number before starting.
4067
+
4068
+ ## Stop Conditions
4069
+ - Brainstorm reveals goal is unclear -- stop, ask user
4070
+ - Tests produce unresolvable failures -- stop, report
4071
+ - Review finds critical security issues -- stop, report
4072
+
4073
+ ## Common Pitfalls
4074
+ - Skipping the Read step for a phase skill (NON-NEGOTIABLE)
4075
+ - Not running phase gates between phases
4076
+ - Not announcing progress ("[Phase N/5]")
4077
+ - Proceeding after a failed gate
4078
+ - Not updating epic notes with phase state (loses resume ability)
4079
+ - Batching all commits to the end instead of committing incrementally
4080
+
4081
+ ## Quality Criteria
4082
+ - All 5 phases were executed (3/5 is NOT success)
4083
+ - Each phase skill was Read before execution
4084
+ - Phase gates verified between each transition
4085
+ - Epic notes updated after each phase
4086
+ - Memory searched at the start of each phase
4087
+ - \`npx ca verify-gates\` passed at the end
4088
+
4089
+ ## SESSION CLOSE -- INVIOLABLE
4090
+ Before saying "done": git status, git add, bd sync, git commit, bd sync, git push.
4091
+ If phase state gets stuck, use the escape hatch: \`npx ca phase-check clean\` (or \`npx ca phase-clean\`).
3429
4092
  `
3430
4093
  };
3431
4094
 
@@ -3541,7 +4204,7 @@ async function ensureLessonsDirectory(repoRoot) {
3541
4204
  }
3542
4205
  return lessonsDir;
3543
4206
  }
3544
- async function configureClaudeSettings(repoRoot) {
4207
+ async function configureClaudeSettings() {
3545
4208
  const settingsPath = getClaudeSettingsPath(false);
3546
4209
  let settings;
3547
4210
  try {
@@ -3549,14 +4212,11 @@ async function configureClaudeSettings(repoRoot) {
3549
4212
  } catch {
3550
4213
  settings = {};
3551
4214
  }
3552
- const hadHooks = hasClaudeHook(settings);
4215
+ const hadHooks = hasAllCompoundAgentHooks(settings);
3553
4216
  addAllCompoundAgentHooks(settings);
3554
4217
  await writeClaudeSettings(settingsPath, settings);
3555
- const hadMcp = await hasMcpServerInMcpJson(repoRoot);
3556
- const mcpAdded = await addMcpServerToMcpJson(repoRoot);
3557
4218
  return {
3558
- hooks: !hadHooks,
3559
- mcpServer: mcpAdded && !hadMcp
4219
+ hooks: !hadHooks
3560
4220
  };
3561
4221
  }
3562
4222
  async function runSetup(options) {
@@ -3569,7 +4229,11 @@ async function runSetup(options) {
3569
4229
  await installWorkflowCommands(repoRoot);
3570
4230
  await installPhaseSkills(repoRoot);
3571
4231
  await installAgentRoleSkills(repoRoot);
3572
- const { hooks, mcpServer } = await configureClaudeSettings(repoRoot);
4232
+ let gitHooks = "skipped";
4233
+ if (!options.skipHooks) {
4234
+ gitHooks = (await installPreCommitHook(repoRoot)).status;
4235
+ }
4236
+ const { hooks } = await configureClaudeSettings();
3573
4237
  let modelStatus = "skipped";
3574
4238
  if (!options.skipModel) {
3575
4239
  try {
@@ -3588,7 +4252,7 @@ async function runSetup(options) {
3588
4252
  lessonsDir,
3589
4253
  agentsMd: agentsMdUpdated,
3590
4254
  hooks,
3591
- mcpServer,
4255
+ gitHooks,
3592
4256
  model: modelStatus
3593
4257
  };
3594
4258
  }
@@ -3632,10 +4296,6 @@ async function runUninstall(repoRoot, dryRun) {
3632
4296
  }
3633
4297
  } catch {
3634
4298
  }
3635
- if (await hasMcpServerInMcpJson(repoRoot)) {
3636
- if (!dryRun) await removeMcpServerFromMcpJson(repoRoot);
3637
- actions.push("Removed compound-agent from .mcp.json");
3638
- }
3639
4299
  if (!dryRun) {
3640
4300
  const removed = await removeAgentsSection(repoRoot);
3641
4301
  if (removed) actions.push("Removed compound-agent section from AGENTS.md");
@@ -3709,8 +4369,8 @@ async function runUpdate(repoRoot, dryRun) {
3709
4369
  }
3710
4370
  let configUpdated = false;
3711
4371
  if (!dryRun) {
3712
- const { hooks, mcpServer } = await configureClaudeSettings(repoRoot);
3713
- configUpdated = hooks || mcpServer;
4372
+ const { hooks } = await configureClaudeSettings();
4373
+ configUpdated = hooks;
3714
4374
  }
3715
4375
  return { updated, added, skipped, configUpdated };
3716
4376
  }
@@ -3728,557 +4388,291 @@ async function runStatus(repoRoot) {
3728
4388
  let hooksInstalled = false;
3729
4389
  try {
3730
4390
  const settings = await readClaudeSettings(settingsPath);
3731
- hooksInstalled = hasClaudeHook(settings);
4391
+ hooksInstalled = hasAllCompoundAgentHooks(settings);
3732
4392
  } catch {
3733
4393
  }
3734
4394
  console.log(` Hooks: ${hooksInstalled ? "installed" : "not installed"}`);
3735
- const mcpInstalled = await hasMcpServerInMcpJson(repoRoot);
3736
- console.log(` MCP server: ${mcpInstalled ? "installed" : "not installed"}`);
3737
- }
3738
- function registerSetupAllCommand(setupCommand) {
3739
- setupCommand.description("One-shot setup: init + hooks + MCP server + model");
3740
- setupCommand.command("all", { isDefault: true }).description("Run full setup (default)").option("--skip-model", "Skip embedding model download").option("--uninstall", "Remove all generated files and configuration").option("--update", "Regenerate files (preserves user customizations)").option("--status", "Show installation status").option("--dry-run", "Show what would change without changing").action(async (options) => {
3741
- const repoRoot = getRepoRoot();
3742
- const dryRun = options.dryRun ?? false;
3743
- if (options.uninstall) {
3744
- const prefix = dryRun ? "[dry-run] Would have: " : "";
3745
- const actions = await runUninstall(repoRoot, dryRun);
3746
- if (actions.length === 0) {
3747
- console.log("Nothing to uninstall.");
3748
- } else {
3749
- for (const action of actions) {
3750
- console.log(` ${prefix}${action}`);
3751
- }
3752
- out.success(dryRun ? "Dry run complete (no changes made)" : "Uninstall complete");
3753
- }
3754
- return;
3755
- }
3756
- if (options.update) {
3757
- const result2 = await runUpdate(repoRoot, dryRun);
3758
- const prefix = dryRun ? "[dry-run] " : "";
3759
- if (result2.updated === 0 && result2.added === 0) {
3760
- console.log(`${prefix}All generated files are up to date.`);
3761
- } else {
3762
- if (result2.updated > 0) console.log(` ${prefix}Updated: ${result2.updated} file(s)`);
3763
- if (result2.added > 0) console.log(` ${prefix}Added: ${result2.added} file(s)`);
3764
- }
3765
- if (result2.skipped > 0) console.log(` Skipped: ${result2.skipped} user-customized file(s)`);
3766
- if (result2.configUpdated) console.log(` ${prefix}Config: hooks/MCP updated`);
3767
- return;
3768
- }
3769
- if (options.status) {
3770
- await runStatus(repoRoot);
3771
- return;
3772
- }
3773
- const result = await runSetup({ skipModel: options.skipModel });
3774
- out.success("Compound agent setup complete");
3775
- console.log(` Lessons directory: ${result.lessonsDir}`);
3776
- console.log(` AGENTS.md: ${result.agentsMd ? "Updated" : "Already configured"}`);
3777
- console.log(` Claude hooks: ${result.hooks ? "Installed" : "Already configured"}`);
3778
- console.log(` MCP server: ${result.mcpServer ? "Registered in .mcp.json" : "Already configured"}`);
3779
- switch (result.model) {
3780
- case "skipped":
3781
- console.log(" Model: Skipped (--skip-model)");
3782
- break;
3783
- case "downloaded":
3784
- console.log(" Model: Downloaded");
3785
- break;
3786
- case "already_exists":
3787
- console.log(" Model: Already exists");
3788
- break;
3789
- case "failed":
3790
- console.log(" Model: Download failed (run `ca download-model` manually)");
3791
- break;
3792
- }
3793
- console.log("");
3794
- console.log("Next steps:");
3795
- console.log(" 1. Restart Claude Code to load hooks");
3796
- console.log(" 2. Use `npx ca search` and `npx ca learn` commands");
3797
- });
3798
- }
3799
-
3800
- // src/cli-error-format.ts
3801
- function formatError(command, code, message, remediation) {
3802
- return `ERROR [${command}] ${code}: ${message} \u2014 ${remediation}`;
3803
- }
3804
-
3805
- // src/setup/claude.ts
3806
- async function handleStatus(alreadyInstalled, displayPath, settingsPath, options) {
3807
- const repoRoot = getRepoRoot();
3808
- const learnMdPath = join(repoRoot, ".claude", "commands", "learn.md");
3809
- const searchMdPath = join(repoRoot, ".claude", "commands", "search.md");
3810
- const mcpPath = getMcpJsonPath(repoRoot);
3811
- const learnExists = existsSync(learnMdPath);
3812
- const searchExists = existsSync(searchMdPath);
3813
- const mcpExists = existsSync(mcpPath);
3814
- const mcpInstalled = mcpExists && await hasMcpServerInMcpJson(repoRoot);
3815
- let status;
3816
- if (alreadyInstalled && mcpInstalled && learnExists && searchExists) {
3817
- status = "connected";
3818
- } else if (alreadyInstalled || mcpInstalled || learnExists || searchExists) {
3819
- status = "partial";
3820
- } else {
3821
- status = "disconnected";
3822
- }
3823
- const result = {
3824
- settingsFile: displayPath,
3825
- mcpFile: ".mcp.json",
3826
- exists: existsSync(settingsPath),
3827
- validJson: true,
3828
- hookInstalled: alreadyInstalled,
3829
- mcpInstalled,
3830
- slashCommands: { learn: learnExists, search: searchExists },
3831
- status
3832
- };
3833
- if (options.json) {
3834
- console.log(JSON.stringify(result, null, 2));
3835
- return;
3836
- }
3837
- console.log("Claude Code Integration Status");
3838
- console.log("\u2500".repeat(40));
3839
- console.log("");
3840
- console.log(`Hooks file: ${displayPath}`);
3841
- console.log(` ${result.exists ? "[ok]" : "[missing]"} File exists`);
3842
- console.log(` ${result.validJson ? "[ok]" : "[error]"} Valid JSON`);
3843
- console.log(` ${result.hookInstalled ? "[ok]" : "[warn]"} SessionStart hook installed`);
3844
- console.log("");
3845
- console.log("MCP config: .mcp.json");
3846
- console.log(` ${mcpExists ? "[ok]" : "[missing]"} File exists`);
3847
- console.log(` ${mcpInstalled ? "[ok]" : "[warn]"} compound-agent MCP server`);
3848
- console.log("");
3849
- console.log("Slash commands:");
3850
- console.log(` ${learnExists ? "[ok]" : "[warn]"} /learn command`);
3851
- console.log(` ${searchExists ? "[ok]" : "[warn]"} /search command`);
3852
- console.log("");
3853
- if (status === "connected") {
3854
- out.success("All checks passed. Integration is connected.");
3855
- } else if (status === "partial") {
3856
- out.warn("Partial setup detected.");
3857
- console.log("");
3858
- console.log("Run 'npx ca setup' to complete setup.");
3859
- } else {
3860
- out.error("Not connected.");
3861
- console.log("");
3862
- console.log("Run 'npx ca setup' to set up Compound Agent.");
3863
- }
3864
- }
3865
- async function handleUninstall(settings, settingsPath, alreadyInstalled, displayPath, options) {
3866
- const repoRoot = getRepoRoot();
3867
- if (options.dryRun) {
3868
- if (options.json) {
3869
- console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
3870
- } else {
3871
- if (alreadyInstalled) {
3872
- console.log(`Would remove compound-agent hooks from ${displayPath}`);
3873
- } else {
3874
- console.log("No compound-agent hooks to remove");
3875
- }
3876
- }
3877
- return;
3878
- }
3879
- const removedHook = removeCompoundAgentHook(settings);
3880
- if (removedHook) {
3881
- await writeClaudeSettings(settingsPath, settings);
3882
- }
3883
- const removedMcp = await removeMcpServerFromMcpJson(repoRoot);
3884
- const removedAgents = await removeAgentsSection(repoRoot);
3885
- const removedClaudeMd = await removeClaudeMdReference(repoRoot);
3886
- const anyRemoved = removedHook || removedMcp || removedAgents || removedClaudeMd;
3887
- if (anyRemoved) {
3888
- if (options.json) {
3889
- console.log(JSON.stringify({
3890
- installed: false,
3891
- location: displayPath,
3892
- action: "removed",
3893
- mcpRemoved: removedMcp,
3894
- agentsMdRemoved: removedAgents,
3895
- claudeMdRemoved: removedClaudeMd
3896
- }));
3897
- } else {
3898
- out.success("Compound agent removed");
3899
- if (removedHook) console.log(` Hooks: ${displayPath}`);
3900
- if (removedMcp) console.log(" MCP: .mcp.json");
3901
- if (removedAgents) console.log(" AGENTS.md: Compound Agent section removed");
3902
- if (removedClaudeMd) console.log(" CLAUDE.md: Compound Agent reference removed");
3903
- }
3904
- } else {
3905
- if (options.json) {
3906
- console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
3907
- } else {
3908
- out.info("No compound agent hooks to remove");
3909
- if (options.global) {
3910
- console.log(" Hint: Try without --global to check project settings.");
3911
- } else {
3912
- console.log(" Hint: Try with --global flag to check global settings.");
3913
- }
3914
- }
3915
- }
3916
4395
  }
3917
- async function handleInstall(settings, settingsPath, alreadyInstalled, displayPath, options) {
3918
- if (options.dryRun) {
3919
- if (options.json) {
3920
- console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
3921
- } else {
3922
- if (alreadyInstalled) {
3923
- console.log("Compound agent hooks already installed");
3924
- } else {
3925
- console.log(`Would install compound-agent hooks to ${displayPath}`);
3926
- }
3927
- }
4396
+ function printSetupGitHooksStatus(gitHooks) {
4397
+ if (gitHooks === "skipped") {
4398
+ console.log(" Git hooks: Skipped (--skip-hooks)");
3928
4399
  return;
3929
4400
  }
3930
- if (alreadyInstalled) {
3931
- if (options.json) {
3932
- console.log(JSON.stringify({
3933
- installed: true,
3934
- location: displayPath,
3935
- hooks: ["SessionStart"],
3936
- action: "unchanged"
3937
- }));
3938
- } else {
3939
- out.info("Compound agent hooks already installed");
3940
- console.log(` Location: ${displayPath}`);
3941
- }
4401
+ if (gitHooks === "not_git_repo") {
4402
+ console.log(" Git hooks: Skipped (not a git repository)");
3942
4403
  return;
3943
4404
  }
3944
- const fileExists = existsSync(settingsPath);
3945
- addCompoundAgentHook(settings);
3946
- await writeClaudeSettings(settingsPath, settings);
3947
- if (options.json) {
3948
- console.log(JSON.stringify({
3949
- installed: true,
3950
- location: displayPath,
3951
- hooks: ["SessionStart"],
3952
- action: fileExists ? "updated" : "created"
3953
- }));
3954
- } else {
3955
- out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
3956
- console.log(` Location: ${displayPath}`);
3957
- console.log(" Hook: SessionStart (startup|resume|compact)");
3958
- console.log("");
3959
- console.log("Lessons will be loaded automatically at session start.");
3960
- if (!options.global) {
3961
- console.log("");
3962
- console.log("Note: Project hooks override global hooks.");
3963
- }
4405
+ if (gitHooks === "installed") {
4406
+ console.log(" Git hooks: Installed");
4407
+ return;
3964
4408
  }
4409
+ if (gitHooks === "appended") {
4410
+ console.log(" Git hooks: Appended to existing pre-commit hook");
4411
+ return;
4412
+ }
4413
+ console.log(" Git hooks: Already configured");
3965
4414
  }
3966
- function registerClaudeSubcommand(setupCommand) {
3967
- setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove compound-agent hooks").option("--status", "Check status of Claude Code integration").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
3968
- const settingsPath = getClaudeSettingsPath(options.global ?? false);
3969
- const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
3970
- let settings;
3971
- try {
3972
- settings = await readClaudeSettings(settingsPath);
3973
- } catch {
3974
- if (options.json) {
3975
- console.log(JSON.stringify({ error: "Failed to parse settings file" }));
4415
+ function registerSetupAllCommand(setupCommand) {
4416
+ setupCommand.description("One-shot setup: init + hooks + model");
4417
+ setupCommand.command("all", { isDefault: true }).description("Run full setup (default)").option("--skip-model", "Skip embedding model download").option("--skip-hooks", "Skip git hooks installation").option("--uninstall", "Remove all generated files and configuration").option("--update", "Regenerate files (preserves user customizations)").option("--status", "Show installation status").option("--dry-run", "Show what would change without changing").action(async (options) => {
4418
+ const repoRoot = getRepoRoot();
4419
+ const dryRun = options.dryRun ?? false;
4420
+ if (options.uninstall) {
4421
+ const prefix = dryRun ? "[dry-run] Would have: " : "";
4422
+ const actions = await runUninstall(repoRoot, dryRun);
4423
+ if (actions.length === 0) {
4424
+ console.log("Nothing to uninstall.");
3976
4425
  } else {
3977
- console.error(formatError("setup", "PARSE_ERROR", "Failed to parse settings file", "Check if JSON is valid"));
4426
+ for (const action of actions) {
4427
+ console.log(` ${prefix}${action}`);
4428
+ }
4429
+ out.success(dryRun ? "Dry run complete (no changes made)" : "Uninstall complete");
3978
4430
  }
3979
- process.exit(1);
3980
- }
3981
- const alreadyInstalled = hasClaudeHook(settings);
3982
- if (options.status) {
3983
- await handleStatus(alreadyInstalled, displayPath, settingsPath, options);
3984
- } else if (options.uninstall) {
3985
- await handleUninstall(settings, settingsPath, alreadyInstalled, displayPath, options);
3986
- } else {
3987
- await handleInstall(settings, settingsPath, alreadyInstalled, displayPath, options);
4431
+ return;
3988
4432
  }
3989
- });
3990
- }
3991
- function registerDownloadModelCommand(program2) {
3992
- program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
3993
- const alreadyExisted = isModelAvailable();
3994
- if (alreadyExisted) {
3995
- const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
3996
- const size2 = statSync(modelPath2).size;
3997
- if (options.json) {
3998
- console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
4433
+ if (options.update) {
4434
+ const result2 = await runUpdate(repoRoot, dryRun);
4435
+ const prefix = dryRun ? "[dry-run] " : "";
4436
+ if (result2.updated === 0 && result2.added === 0) {
4437
+ console.log(`${prefix}All generated files are up to date.`);
3999
4438
  } else {
4000
- console.log("Model already exists.");
4001
- console.log(`Path: ${modelPath2}`);
4002
- console.log(`Size: ${formatBytes(size2)}`);
4439
+ if (result2.updated > 0) console.log(` ${prefix}Updated: ${result2.updated} file(s)`);
4440
+ if (result2.added > 0) console.log(` ${prefix}Added: ${result2.added} file(s)`);
4003
4441
  }
4442
+ if (result2.skipped > 0) console.log(` Skipped: ${result2.skipped} user-customized file(s)`);
4443
+ if (result2.configUpdated) console.log(` ${prefix}Config: hooks updated`);
4004
4444
  return;
4005
4445
  }
4006
- if (!options.json) {
4007
- console.log("Downloading embedding model...");
4446
+ if (options.status) {
4447
+ await runStatus(repoRoot);
4448
+ return;
4008
4449
  }
4009
- const modelPath = await resolveModel({ cli: !options.json });
4010
- const size = statSync(modelPath).size;
4011
- if (options.json) {
4012
- console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
4013
- } else {
4014
- console.log(`
4015
- Model downloaded successfully!`);
4016
- console.log(`Path: ${modelPath}`);
4017
- console.log(`Size: ${formatBytes(size)}`);
4450
+ const result = await runSetup({ skipModel: options.skipModel, skipHooks: options.skipHooks });
4451
+ out.success("Compound agent setup complete");
4452
+ console.log(` Lessons directory: ${result.lessonsDir}`);
4453
+ console.log(` AGENTS.md: ${result.agentsMd ? "Updated" : "Already configured"}`);
4454
+ console.log(` Claude hooks: ${result.hooks ? "Installed" : "Already configured"}`);
4455
+ printSetupGitHooksStatus(result.gitHooks);
4456
+ switch (result.model) {
4457
+ case "skipped":
4458
+ console.log(" Model: Skipped (--skip-model)");
4459
+ break;
4460
+ case "downloaded":
4461
+ console.log(" Model: Downloaded");
4462
+ break;
4463
+ case "already_exists":
4464
+ console.log(" Model: Already exists");
4465
+ break;
4466
+ case "failed":
4467
+ console.log(" Model: Download failed (run `ca download-model` manually)");
4468
+ break;
4018
4469
  }
4470
+ console.log("");
4471
+ console.log("Next steps:");
4472
+ console.log(" 1. Restart Claude Code to load hooks");
4473
+ console.log(" 2. Use `npx ca search` and `npx ca learn` commands");
4019
4474
  });
4020
4475
  }
4021
- var HOOK_FILE_MODE = 493;
4022
- var CORRECTION_PATTERNS = [
4023
- /\bactually\b/i,
4024
- /\bno[,.]?\s/i,
4025
- /\bwrong\b/i,
4026
- /\bthat'?s not right\b/i,
4027
- /\bthat'?s incorrect\b/i,
4028
- /\buse .+ instead\b/i,
4029
- /\bi told you\b/i,
4030
- /\bi already said\b/i,
4031
- /\bnot like that\b/i,
4032
- /\byou forgot\b/i,
4033
- /\byou missed\b/i,
4034
- /\bstop\s*(,\s*)?(doing|using|that)\b/i,
4035
- /\bwait\s*(,\s*)?(that|no|wrong)\b/i
4036
- ];
4037
- var HIGH_CONFIDENCE_PLANNING = [
4038
- /\bdecide\b/i,
4039
- /\bchoose\b/i,
4040
- /\bpick\b/i,
4041
- /\bwhich approach\b/i,
4042
- /\bwhat do you think\b/i,
4043
- /\bshould we\b/i,
4044
- /\bwould you\b/i,
4045
- /\bhow should\b/i,
4046
- /\bwhat'?s the best\b/i,
4047
- /\badd feature\b/i,
4048
- /\bset up\b/i
4049
- ];
4050
- var LOW_CONFIDENCE_PLANNING = [
4051
- /\bimplement\b/i,
4052
- /\bbuild\b/i,
4053
- /\bcreate\b/i,
4054
- /\brefactor\b/i,
4055
- /\bfix\b/i,
4056
- /\bwrite\b/i,
4057
- /\bdevelop\b/i
4058
- ];
4059
- var CORRECTION_REMINDER = "Remember: You have memory tools available - `npx ca learn` to save insights, `npx ca search` to find past solutions.";
4060
- var PLANNING_REMINDER = "If you're uncertain or hesitant, remember your memory tools: `npx ca search` may have relevant context from past sessions.";
4061
- function detectCorrection(prompt) {
4062
- return CORRECTION_PATTERNS.some((pattern) => pattern.test(prompt));
4063
- }
4064
- function detectPlanning(prompt) {
4065
- if (HIGH_CONFIDENCE_PLANNING.some((pattern) => pattern.test(prompt))) {
4066
- return true;
4067
- }
4068
- const lowMatches = LOW_CONFIDENCE_PLANNING.filter((pattern) => pattern.test(prompt));
4069
- return lowMatches.length >= 2;
4070
- }
4071
- function processUserPrompt(prompt) {
4072
- if (detectCorrection(prompt)) {
4073
- return {
4074
- hookSpecificOutput: {
4075
- hookEventName: "UserPromptSubmit",
4076
- additionalContext: CORRECTION_REMINDER
4077
- }
4078
- };
4079
- }
4080
- if (detectPlanning(prompt)) {
4081
- return {
4082
- hookSpecificOutput: {
4083
- hookEventName: "UserPromptSubmit",
4084
- additionalContext: PLANNING_REMINDER
4085
- }
4086
- };
4087
- }
4088
- return {};
4089
- }
4090
- var SAME_TARGET_THRESHOLD = 2;
4091
- var TOTAL_FAILURE_THRESHOLD = 3;
4092
- var failureCount = 0;
4093
- var lastFailedTarget = null;
4094
- var sameTargetCount = 0;
4095
- var FAILURE_TIP = "Tip: Multiple failures detected. `npx ca search` may have solutions for similar issues.";
4096
- function resetFailureState() {
4097
- failureCount = 0;
4098
- lastFailedTarget = null;
4099
- sameTargetCount = 0;
4100
- }
4101
- function getFailureTarget(toolName, toolInput) {
4102
- if (toolName === "Bash" && typeof toolInput.command === "string") {
4103
- const trimmed = toolInput.command.trim();
4104
- const firstSpace = trimmed.indexOf(" ");
4105
- return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
4106
- }
4107
- if ((toolName === "Edit" || toolName === "Write") && typeof toolInput.file_path === "string") {
4108
- return toolInput.file_path;
4109
- }
4110
- return null;
4111
- }
4112
- function processToolFailure(toolName, toolInput) {
4113
- failureCount++;
4114
- const target = getFailureTarget(toolName, toolInput);
4115
- if (target !== null && target === lastFailedTarget) {
4116
- sameTargetCount++;
4476
+ async function handleStatus(alreadyInstalled, displayPath, settingsPath, options) {
4477
+ const repoRoot = getRepoRoot();
4478
+ const learnMdPath = join(repoRoot, ".claude", "commands", "learn.md");
4479
+ const searchMdPath = join(repoRoot, ".claude", "commands", "search.md");
4480
+ const learnExists = existsSync(learnMdPath);
4481
+ const searchExists = existsSync(searchMdPath);
4482
+ let status;
4483
+ if (alreadyInstalled && learnExists && searchExists) {
4484
+ status = "connected";
4485
+ } else if (alreadyInstalled || learnExists || searchExists) {
4486
+ status = "partial";
4117
4487
  } else {
4118
- sameTargetCount = 1;
4119
- lastFailedTarget = target;
4120
- }
4121
- const shouldShowTip = sameTargetCount >= SAME_TARGET_THRESHOLD || failureCount >= TOTAL_FAILURE_THRESHOLD;
4122
- if (shouldShowTip) {
4123
- resetFailureState();
4124
- return {
4125
- hookSpecificOutput: {
4126
- hookEventName: "PostToolUseFailure",
4127
- additionalContext: FAILURE_TIP
4128
- }
4129
- };
4488
+ status = "disconnected";
4130
4489
  }
4131
- return {};
4132
- }
4133
- function processToolSuccess() {
4134
- resetFailureState();
4135
- }
4136
- function hasCompoundAgentHook(content) {
4137
- return content.includes(HOOK_MARKER);
4138
- }
4139
- async function getGitHooksDir(repoRoot) {
4140
- const gitDir = join(repoRoot, ".git");
4141
- if (!existsSync(gitDir)) {
4142
- return null;
4490
+ const result = {
4491
+ settingsFile: displayPath,
4492
+ exists: existsSync(settingsPath),
4493
+ validJson: true,
4494
+ hookInstalled: alreadyInstalled,
4495
+ slashCommands: { learn: learnExists, search: searchExists },
4496
+ status
4497
+ };
4498
+ if (options.json) {
4499
+ console.log(JSON.stringify(result, null, 2));
4500
+ return;
4143
4501
  }
4144
- const configPath2 = join(gitDir, "config");
4145
- if (existsSync(configPath2)) {
4146
- const config = await readFile(configPath2, "utf-8");
4147
- const match = /hooksPath\s*=\s*(.+)$/m.exec(config);
4148
- if (match?.[1]) {
4149
- const hooksPath = match[1].trim();
4150
- return hooksPath.startsWith("/") ? hooksPath : join(repoRoot, hooksPath);
4151
- }
4502
+ console.log("Claude Code Integration Status");
4503
+ console.log("\u2500".repeat(40));
4504
+ console.log("");
4505
+ console.log(`Hooks file: ${displayPath}`);
4506
+ console.log(` ${result.exists ? "[ok]" : "[missing]"} File exists`);
4507
+ console.log(` ${result.validJson ? "[ok]" : "[error]"} Valid JSON`);
4508
+ console.log(` ${result.hookInstalled ? "[ok]" : "[warn]"} Compound Agent hooks installed`);
4509
+ console.log("");
4510
+ console.log("Slash commands:");
4511
+ console.log(` ${learnExists ? "[ok]" : "[warn]"} /learn command`);
4512
+ console.log(` ${searchExists ? "[ok]" : "[warn]"} /search command`);
4513
+ console.log("");
4514
+ if (status === "connected") {
4515
+ out.success("All checks passed. Integration is connected.");
4516
+ } else if (status === "partial") {
4517
+ out.warn("Partial setup detected.");
4518
+ console.log("");
4519
+ console.log("Run 'npx ca setup' to complete setup.");
4520
+ } else {
4521
+ out.error("Not connected.");
4522
+ console.log("");
4523
+ console.log("Run 'npx ca setup' to set up Compound Agent.");
4152
4524
  }
4153
- const defaultHooksDir = join(gitDir, "hooks");
4154
- return existsSync(defaultHooksDir) ? defaultHooksDir : null;
4155
4525
  }
4156
- function findFirstTopLevelExitLine(lines) {
4157
- let insideFunction = 0;
4158
- let heredocDelimiter = null;
4159
- for (let i = 0; i < lines.length; i++) {
4160
- const line = lines[i] ?? "";
4161
- const trimmed = line.trim();
4162
- if (heredocDelimiter !== null) {
4163
- if (trimmed === heredocDelimiter) {
4164
- heredocDelimiter = null;
4526
+ async function handleUninstall(settings, settingsPath, alreadyInstalled, displayPath, options) {
4527
+ const repoRoot = getRepoRoot();
4528
+ if (options.dryRun) {
4529
+ if (options.json) {
4530
+ console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
4531
+ } else {
4532
+ if (alreadyInstalled) {
4533
+ console.log(`Would remove compound-agent hooks from ${displayPath}`);
4534
+ } else {
4535
+ console.log("No compound-agent hooks to remove");
4165
4536
  }
4166
- continue;
4167
- }
4168
- const heredocMatch = /<<-?\s*['"]?(\w+)['"]?/.exec(line);
4169
- if (heredocMatch?.[1]) {
4170
- heredocDelimiter = heredocMatch[1];
4171
- continue;
4172
- }
4173
- for (const char of line) {
4174
- if (char === "{") insideFunction++;
4175
- if (char === "}") insideFunction = Math.max(0, insideFunction - 1);
4176
- }
4177
- if (insideFunction > 0) {
4178
- continue;
4179
- }
4180
- if (/^\s*exit\s+(\d+|\$\w+|\$\?)\s*$/.test(trimmed)) {
4181
- return i;
4182
4537
  }
4538
+ return;
4183
4539
  }
4184
- return -1;
4185
- }
4186
- async function installPreCommitHook(repoRoot) {
4187
- const gitHooksDir = await getGitHooksDir(repoRoot);
4188
- if (!gitHooksDir) {
4189
- return { status: "not_git_repo" };
4540
+ const removedHook = removeCompoundAgentHook(settings);
4541
+ if (removedHook) {
4542
+ await writeClaudeSettings(settingsPath, settings);
4190
4543
  }
4191
- await mkdir(gitHooksDir, { recursive: true });
4192
- const hookPath = join(gitHooksDir, "pre-commit");
4193
- if (existsSync(hookPath)) {
4194
- const content = await readFile(hookPath, "utf-8");
4195
- if (hasCompoundAgentHook(content)) {
4196
- return { status: "already_installed" };
4544
+ const removedAgents = await removeAgentsSection(repoRoot);
4545
+ const removedClaudeMd = await removeClaudeMdReference(repoRoot);
4546
+ const anyRemoved = removedHook || removedAgents || removedClaudeMd;
4547
+ if (anyRemoved) {
4548
+ if (options.json) {
4549
+ console.log(JSON.stringify({
4550
+ installed: false,
4551
+ location: displayPath,
4552
+ action: "removed",
4553
+ agentsMdRemoved: removedAgents,
4554
+ claudeMdRemoved: removedClaudeMd
4555
+ }));
4556
+ } else {
4557
+ out.success("Compound agent removed");
4558
+ if (removedHook) console.log(` Hooks: ${displayPath}`);
4559
+ if (removedAgents) console.log(" AGENTS.md: Compound Agent section removed");
4560
+ if (removedClaudeMd) console.log(" CLAUDE.md: Compound Agent reference removed");
4197
4561
  }
4198
- const lines = content.split("\n");
4199
- const exitLineIndex = findFirstTopLevelExitLine(lines);
4200
- let newContent;
4201
- if (exitLineIndex === -1) {
4202
- newContent = content.trimEnd() + "\n" + COMPOUND_AGENT_HOOK_BLOCK;
4562
+ } else {
4563
+ if (options.json) {
4564
+ console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
4203
4565
  } else {
4204
- const before = lines.slice(0, exitLineIndex);
4205
- const after = lines.slice(exitLineIndex);
4206
- newContent = before.join("\n") + COMPOUND_AGENT_HOOK_BLOCK + after.join("\n");
4566
+ out.info("No compound agent hooks to remove");
4567
+ if (options.global) {
4568
+ console.log(" Hint: Try without --global to check project settings.");
4569
+ } else {
4570
+ console.log(" Hint: Try with --global flag to check global settings.");
4571
+ }
4207
4572
  }
4208
- await writeFile(hookPath, newContent, "utf-8");
4209
- chmodSync(hookPath, HOOK_FILE_MODE);
4210
- return { status: "appended" };
4211
- }
4212
- await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
4213
- chmodSync(hookPath, HOOK_FILE_MODE);
4214
- return { status: "installed" };
4215
- }
4216
- async function readStdin() {
4217
- const chunks = [];
4218
- for await (const chunk of process.stdin) {
4219
- chunks.push(chunk);
4220
4573
  }
4221
- return Buffer.concat(chunks).toString("utf-8");
4222
4574
  }
4223
- async function runUserPromptHook() {
4224
- try {
4225
- const input = await readStdin();
4226
- const data = JSON.parse(input);
4227
- if (!data.prompt) {
4228
- console.log(JSON.stringify({}));
4229
- return;
4575
+ async function handleInstall(settings, settingsPath, alreadyInstalled, displayPath, options) {
4576
+ if (options.dryRun) {
4577
+ if (options.json) {
4578
+ console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
4579
+ } else {
4580
+ if (alreadyInstalled) {
4581
+ console.log("Compound agent hooks already installed");
4582
+ } else {
4583
+ console.log(`Would install compound-agent hooks to ${displayPath}`);
4584
+ }
4230
4585
  }
4231
- const result = processUserPrompt(data.prompt);
4232
- console.log(JSON.stringify(result));
4233
- } catch {
4234
- console.log(JSON.stringify({}));
4586
+ return;
4235
4587
  }
4236
- }
4237
- async function runPostToolFailureHook() {
4238
- try {
4239
- const input = await readStdin();
4240
- const data = JSON.parse(input);
4241
- if (!data.tool_name) {
4242
- console.log(JSON.stringify({}));
4243
- return;
4588
+ if (alreadyInstalled) {
4589
+ if (options.json) {
4590
+ console.log(JSON.stringify({
4591
+ installed: true,
4592
+ location: displayPath,
4593
+ hooks: ["SessionStart", "PreCompact", "UserPromptSubmit", "PostToolUseFailure", "PostToolUse", "PreToolUse", "Stop"],
4594
+ action: "unchanged"
4595
+ }));
4596
+ } else {
4597
+ out.info("Compound agent hooks already installed");
4598
+ console.log(` Location: ${displayPath}`);
4244
4599
  }
4245
- const result = processToolFailure(data.tool_name, data.tool_input ?? {});
4246
- console.log(JSON.stringify(result));
4247
- } catch {
4248
- console.log(JSON.stringify({}));
4600
+ return;
4249
4601
  }
4250
- }
4251
- async function runPostToolSuccessHook() {
4252
- try {
4253
- await readStdin();
4254
- processToolSuccess();
4255
- console.log(JSON.stringify({}));
4256
- } catch {
4257
- console.log(JSON.stringify({}));
4602
+ const fileExists = existsSync(settingsPath);
4603
+ addAllCompoundAgentHooks(settings);
4604
+ await writeClaudeSettings(settingsPath, settings);
4605
+ if (options.json) {
4606
+ console.log(JSON.stringify({
4607
+ installed: true,
4608
+ location: displayPath,
4609
+ hooks: ["SessionStart", "PreCompact", "UserPromptSubmit", "PostToolUseFailure", "PostToolUse", "PreToolUse", "Stop"],
4610
+ action: fileExists ? "updated" : "created"
4611
+ }));
4612
+ } else {
4613
+ out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
4614
+ console.log(` Location: ${displayPath}`);
4615
+ console.log(" Hooks: SessionStart, PreCompact, UserPromptSubmit, PostToolUseFailure, PostToolUse, PreToolUse, Stop");
4616
+ console.log("");
4617
+ console.log("Lessons will be loaded automatically at session start.");
4618
+ if (!options.global) {
4619
+ console.log("");
4620
+ console.log("Note: Project hooks override global hooks.");
4621
+ }
4258
4622
  }
4259
4623
  }
4260
- function registerHooksCommand(program2) {
4261
- const hooksCommand = program2.command("hooks").description("Git hooks management");
4262
- hooksCommand.command("run <hook>").description("Run a hook script (called by git/Claude hooks)").option("--json", "Output as JSON").action(async (hook, options) => {
4263
- if (hook === "pre-commit") {
4624
+ function registerClaudeSubcommand(setupCommand) {
4625
+ setupCommand.command("claude").description("Install Claude Code hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove compound-agent hooks").option("--status", "Check status of Claude Code integration").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
4626
+ const settingsPath = getClaudeSettingsPath(options.global ?? false);
4627
+ const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
4628
+ let settings;
4629
+ try {
4630
+ settings = await readClaudeSettings(settingsPath);
4631
+ } catch {
4264
4632
  if (options.json) {
4265
- console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
4633
+ console.log(JSON.stringify({ error: "Failed to parse settings file" }));
4266
4634
  } else {
4267
- console.log(PRE_COMMIT_MESSAGE);
4635
+ console.error(formatError("setup", "PARSE_ERROR", "Failed to parse settings file", "Check if JSON is valid"));
4268
4636
  }
4269
- } else if (hook === "user-prompt") {
4270
- await runUserPromptHook();
4271
- } else if (hook === "post-tool-failure") {
4272
- await runPostToolFailureHook();
4273
- } else if (hook === "post-tool-success") {
4274
- await runPostToolSuccessHook();
4637
+ process.exit(1);
4638
+ }
4639
+ const alreadyInstalled = hasAllCompoundAgentHooks(settings);
4640
+ if (options.status) {
4641
+ await handleStatus(alreadyInstalled, displayPath, settingsPath, options);
4642
+ } else if (options.uninstall) {
4643
+ await handleUninstall(settings, settingsPath, alreadyInstalled, displayPath, options);
4275
4644
  } else {
4645
+ await handleInstall(settings, settingsPath, alreadyInstalled, displayPath, options);
4646
+ }
4647
+ });
4648
+ }
4649
+ function registerDownloadModelCommand(program2) {
4650
+ program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
4651
+ const alreadyExisted = isModelAvailable();
4652
+ if (alreadyExisted) {
4653
+ const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
4654
+ const size2 = statSync(modelPath2).size;
4276
4655
  if (options.json) {
4277
- console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
4656
+ console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
4278
4657
  } else {
4279
- console.error(formatError("hooks", "UNKNOWN_HOOK", `Unknown hook: ${hook}`, "Valid hooks: pre-session, post-tool-failure, post-tool-success"));
4658
+ console.log("Model already exists.");
4659
+ console.log(`Path: ${modelPath2}`);
4660
+ console.log(`Size: ${formatBytes(size2)}`);
4280
4661
  }
4281
- process.exit(1);
4662
+ return;
4663
+ }
4664
+ if (!options.json) {
4665
+ console.log("Downloading embedding model...");
4666
+ }
4667
+ const modelPath = await resolveModel({ cli: !options.json });
4668
+ const size = statSync(modelPath).size;
4669
+ if (options.json) {
4670
+ console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
4671
+ } else {
4672
+ console.log(`
4673
+ Model downloaded successfully!`);
4674
+ console.log(`Path: ${modelPath}`);
4675
+ console.log(`Size: ${formatBytes(size)}`);
4282
4676
  }
4283
4677
  });
4284
4678
  }
@@ -4604,12 +4998,10 @@ async function runDoctor(repoRoot) {
4604
4998
  let hooksOk = false;
4605
4999
  try {
4606
5000
  const settings = await readClaudeSettings(settingsPath);
4607
- hooksOk = hasClaudeHook(settings);
5001
+ hooksOk = hasAllCompoundAgentHooks(settings);
4608
5002
  } catch {
4609
5003
  }
4610
5004
  checks.push(hooksOk ? { name: "Claude hooks", status: "pass" } : { name: "Claude hooks", status: "fail", fix: "Run: npx ca setup" });
4611
- const mcpOk = await hasMcpServerInMcpJson(repoRoot);
4612
- checks.push(mcpOk ? { name: "MCP server", status: "pass" } : { name: "MCP server", status: "fail", fix: "Run: npx ca setup" });
4613
5005
  let modelOk = false;
4614
5006
  try {
4615
5007
  modelOk = isModelAvailable();
@@ -4947,6 +5339,26 @@ function formatLessonForPrime(lesson) {
4947
5339
  return `- **${lesson.insight}**${tags}
4948
5340
  Learned: ${date} via ${source}`;
4949
5341
  }
5342
+ function formatActiveLfgSection(repoRoot) {
5343
+ const state = getPhaseState(repoRoot);
5344
+ if (state === null || !state.lfg_active) return null;
5345
+ const skillsRead = state.skills_read.length === 0 ? "(none)" : state.skills_read.join(", ");
5346
+ const gatesPassed = state.gates_passed.length === 0 ? "(none)" : state.gates_passed.join(", ");
5347
+ return `
5348
+ ---
5349
+
5350
+ # ACTIVE LFG SESSION
5351
+
5352
+ Epic: ${state.epic_id}
5353
+ Phase: ${state.current_phase} (${state.phase_index}/5)
5354
+ Skills read: ${skillsRead}
5355
+ Gates passed: ${gatesPassed}
5356
+ Started: ${state.started_at}
5357
+
5358
+ Resume from phase ${state.current_phase}. Run: \`npx ca phase-check start ${state.current_phase}\`
5359
+ Read the skill file first: \`.claude/skills/compound/${state.current_phase}/SKILL.md\`
5360
+ `;
5361
+ }
4950
5362
  async function getPrimeContext(repoRoot) {
4951
5363
  const root = getRepoRoot();
4952
5364
  try {
@@ -4967,6 +5379,10 @@ Critical lessons from past corrections:
4967
5379
  ${formattedLessons}
4968
5380
  `;
4969
5381
  }
5382
+ const lfgSection = formatActiveLfgSection(root);
5383
+ if (lfgSection !== null) {
5384
+ output += lfgSection;
5385
+ }
4970
5386
  return output;
4971
5387
  }
4972
5388
  function registerPrimeCommand(program2) {
@@ -5271,7 +5687,7 @@ function registerTestSummaryCommand(program2) {
5271
5687
  process.exit(exitCode);
5272
5688
  });
5273
5689
  }
5274
- var EPIC_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
5690
+ var EPIC_ID_PATTERN2 = /^[a-zA-Z0-9_-]+$/;
5275
5691
  function parseDepsJson(raw) {
5276
5692
  const data = JSON.parse(raw);
5277
5693
  const issue = Array.isArray(data) ? data[0] : data;
@@ -5314,10 +5730,11 @@ function checkGate(deps, prefix, gateName) {
5314
5730
  }
5315
5731
  return { name: gateName, status: "pass" };
5316
5732
  }
5317
- async function runVerifyGates(epicId) {
5318
- if (!EPIC_ID_PATTERN.test(epicId)) {
5733
+ async function runVerifyGates(epicId, options = {}) {
5734
+ if (!EPIC_ID_PATTERN2.test(epicId)) {
5319
5735
  throw new Error(`Invalid epic ID: "${epicId}" (must be alphanumeric with hyphens/underscores)`);
5320
5736
  }
5737
+ const repoRoot = options.repoRoot ?? getRepoRoot();
5321
5738
  const raw = execFileSync("bd", ["show", epicId, "--json"], { encoding: "utf-8" });
5322
5739
  let deps;
5323
5740
  try {
@@ -5326,10 +5743,18 @@ async function runVerifyGates(epicId) {
5326
5743
  const textRaw = execFileSync("bd", ["show", epicId], { encoding: "utf-8" });
5327
5744
  deps = parseDepsText(textRaw);
5328
5745
  }
5329
- return [
5746
+ const checks = [
5330
5747
  checkGate(deps, "Review:", "Review task"),
5331
5748
  checkGate(deps, "Compound:", "Compound task")
5332
5749
  ];
5750
+ const allPassed = checks.every((check) => check.status === "pass");
5751
+ if (allPassed) {
5752
+ const state = getPhaseState(repoRoot);
5753
+ if (state !== null && state.lfg_active && state.gates_passed.includes("final")) {
5754
+ cleanPhaseState(repoRoot);
5755
+ }
5756
+ }
5757
+ return checks;
5333
5758
  }
5334
5759
  var STATUS_LABEL = {
5335
5760
  pass: "PASS",
@@ -5338,7 +5763,7 @@ var STATUS_LABEL = {
5338
5763
  function registerVerifyGatesCommand(program2) {
5339
5764
  program2.command("verify-gates <epic-id>").description("Verify workflow gates are satisfied before epic closure").action(async (epicId) => {
5340
5765
  try {
5341
- const checks = await runVerifyGates(epicId);
5766
+ const checks = await runVerifyGates(epicId, { repoRoot: getRepoRoot() });
5342
5767
  console.log(`Gate checks for epic ${epicId}:
5343
5768
  `);
5344
5769
  for (const check of checks) {
@@ -5397,7 +5822,7 @@ function outputCapturePreview(lesson) {
5397
5822
  console.log(` Insight: ${lesson.insight}`);
5398
5823
  console.log(` Type: ${lesson.type}`);
5399
5824
  console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
5400
- console.log("\nTo save: run with --yes flag, or use memory_capture MCP tool");
5825
+ console.log("\nTo save: run with --yes flag");
5401
5826
  }
5402
5827
  function createLessonFromInputFile(result, confirmed) {
5403
5828
  return {
@@ -5605,7 +6030,7 @@ function registerCaptureCommands(program2) {
5605
6030
  await handleCapture(this, options);
5606
6031
  });
5607
6032
  }
5608
- var EPIC_ID_PATTERN2 = /^[a-zA-Z0-9_.-]+$/;
6033
+ var EPIC_ID_PATTERN3 = /^[a-zA-Z0-9_.-]+$/;
5609
6034
  function buildScriptHeader(timestamp, maxRetries, model, epicIds) {
5610
6035
  return `#!/usr/bin/env bash
5611
6036
  # Infinity Loop - Generated by: ca loop
@@ -5818,8 +6243,8 @@ function validateOptions(options) {
5818
6243
  }
5819
6244
  if (options.epics) {
5820
6245
  for (const id of options.epics) {
5821
- if (!EPIC_ID_PATTERN2.test(id)) {
5822
- throw new Error(`Invalid epic ID "${id}": must match ${EPIC_ID_PATTERN2}`);
6246
+ if (!EPIC_ID_PATTERN3.test(id)) {
6247
+ throw new Error(`Invalid epic ID "${id}": must match ${EPIC_ID_PATTERN3}`);
5823
6248
  }
5824
6249
  }
5825
6250
  }
@@ -6155,6 +6580,7 @@ registerManagementCommands(program);
6155
6580
  registerSetupCommands(program);
6156
6581
  registerCompoundCommands(program);
6157
6582
  registerLoopCommands(program);
6583
+ registerPhaseCheckCommand(program);
6158
6584
  program.parse();
6159
6585
  //# sourceMappingURL=cli.js.map
6160
6586
  //# sourceMappingURL=cli.js.map