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/CHANGELOG.md +24 -1
- package/README.md +7 -16
- package/dist/cli.js +1181 -755
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2600 -4
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/dist/mcp.d.ts +0 -77
- package/dist/mcp.js +0 -1125
- package/dist/mcp.js.map +0 -1
- package/dist/types--TsW4ZqX.d.ts +0 -2601
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,
|
|
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
|
|
1784
|
-
"
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
|
1940
|
-
if (!settings.hooks) {
|
|
1941
|
-
settings.hooks = {};
|
|
1942
|
-
}
|
|
1982
|
+
function hasAllCompoundAgentHooks(settings) {
|
|
1943
1983
|
const hooks = settings.hooks;
|
|
1944
|
-
if (!hooks
|
|
1945
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
2000
|
-
|
|
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
|
-
|
|
2014
|
-
|
|
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
|
-
|
|
2018
|
-
|
|
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
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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 (
|
|
2079
|
+
if (hasAllCompoundAgentHooks(settings)) {
|
|
2074
2080
|
return { installed: true, action: "already_installed" };
|
|
2075
2081
|
}
|
|
2076
2082
|
try {
|
|
2077
|
-
|
|
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/
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
-
|
|
2235
|
-
|
|
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
|
-
|
|
2238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3713
|
-
configUpdated = hooks
|
|
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 =
|
|
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
|
-
|
|
3918
|
-
if (
|
|
3919
|
-
|
|
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 (
|
|
3931
|
-
|
|
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
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
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
|
|
3967
|
-
setupCommand.
|
|
3968
|
-
|
|
3969
|
-
const
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
if (
|
|
3975
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
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(
|
|
4001
|
-
console.log(`
|
|
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 (
|
|
4007
|
-
|
|
4446
|
+
if (options.status) {
|
|
4447
|
+
await runStatus(repoRoot);
|
|
4448
|
+
return;
|
|
4008
4449
|
}
|
|
4009
|
-
const
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
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
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
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
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
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
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
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
|
-
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
if (
|
|
4196
|
-
|
|
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
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
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
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
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
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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
|
-
|
|
4232
|
-
console.log(JSON.stringify(result));
|
|
4233
|
-
} catch {
|
|
4234
|
-
console.log(JSON.stringify({}));
|
|
4586
|
+
return;
|
|
4235
4587
|
}
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
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
|
-
|
|
4246
|
-
console.log(JSON.stringify(result));
|
|
4247
|
-
} catch {
|
|
4248
|
-
console.log(JSON.stringify({}));
|
|
4600
|
+
return;
|
|
4249
4601
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
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({
|
|
4633
|
+
console.log(JSON.stringify({ error: "Failed to parse settings file" }));
|
|
4266
4634
|
} else {
|
|
4267
|
-
console.
|
|
4635
|
+
console.error(formatError("setup", "PARSE_ERROR", "Failed to parse settings file", "Check if JSON is valid"));
|
|
4268
4636
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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({
|
|
4656
|
+
console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
|
|
4278
4657
|
} else {
|
|
4279
|
-
console.
|
|
4658
|
+
console.log("Model already exists.");
|
|
4659
|
+
console.log(`Path: ${modelPath2}`);
|
|
4660
|
+
console.log(`Size: ${formatBytes(size2)}`);
|
|
4280
4661
|
}
|
|
4281
|
-
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
5822
|
-
throw new Error(`Invalid epic ID "${id}": must match ${
|
|
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
|