compound-agent 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3348,7 +3348,7 @@ Before capturing, verify the lesson is:
3348
3348
  The JSONL file requires proper ID generation, schema validation, and SQLite sync.
3349
3349
  Use CLI (\`npx ca learn\`) \u2014 never manual edits.
3350
3350
 
3351
- See [documentation](https://github.com/Nathandela/learning_agent) for more details.
3351
+ See [documentation](https://github.com/Nathandela/compound-agent) for more details.
3352
3352
  ${AGENTS_SECTION_END_MARKER}
3353
3353
  `;
3354
3354
  var LEGACY_ROOT_SLASH_COMMANDS = [
@@ -3368,7 +3368,7 @@ var PLUGIN_MANIFEST = {
3368
3368
  name: "Nathan Delacr\xE9taz",
3369
3369
  url: "https://github.com/Nathandela"
3370
3370
  },
3371
- repository: "https://github.com/Nathandela/learning_agent",
3371
+ repository: "https://github.com/Nathandela/compound-agent",
3372
3372
  license: "MIT",
3373
3373
  hooks: {
3374
3374
  SessionStart: [
@@ -3737,674 +3737,6 @@ function formatError(command, code, message, remediation) {
3737
3737
  return `ERROR [${command}] ${code}: ${message} \u2014 ${remediation}`;
3738
3738
  }
3739
3739
 
3740
- // src/setup/hooks-user-prompt.ts
3741
- var CORRECTION_PATTERNS = [
3742
- /\bactually\b/i,
3743
- /\bno[,.]?\s/i,
3744
- /\bwrong\b/i,
3745
- /\bthat'?s not right\b/i,
3746
- /\bthat'?s incorrect\b/i,
3747
- /\buse .+ instead\b/i,
3748
- /\bi told you\b/i,
3749
- /\bi already said\b/i,
3750
- /\bnot like that\b/i,
3751
- /\byou forgot\b/i,
3752
- /\byou missed\b/i,
3753
- /\bstop\s*(,\s*)?(doing|using|that)\b/i,
3754
- /\bwait\s*(,\s*)?(that|no|wrong)\b/i
3755
- ];
3756
- var HIGH_CONFIDENCE_PLANNING = [
3757
- /\bdecide\b/i,
3758
- /\bchoose\b/i,
3759
- /\bpick\b/i,
3760
- /\bwhich approach\b/i,
3761
- /\bwhat do you think\b/i,
3762
- /\bshould we\b/i,
3763
- /\bwould you\b/i,
3764
- /\bhow should\b/i,
3765
- /\bwhat'?s the best\b/i,
3766
- /\badd feature\b/i,
3767
- /\bset up\b/i
3768
- ];
3769
- var LOW_CONFIDENCE_PLANNING = [
3770
- /\bimplement\b/i,
3771
- /\bbuild\b/i,
3772
- /\bcreate\b/i,
3773
- /\brefactor\b/i,
3774
- /\bfix\b/i,
3775
- /\bwrite\b/i,
3776
- /\bdevelop\b/i
3777
- ];
3778
- var CORRECTION_REMINDER = "Remember: You have memory tools available - `npx ca learn` to save insights, `npx ca search` to find past solutions.";
3779
- var PLANNING_REMINDER = "If you're uncertain or hesitant, remember your memory tools: `npx ca search` may have relevant context from past sessions.";
3780
- function detectCorrection(prompt) {
3781
- return CORRECTION_PATTERNS.some((pattern) => pattern.test(prompt));
3782
- }
3783
- function detectPlanning(prompt) {
3784
- if (HIGH_CONFIDENCE_PLANNING.some((pattern) => pattern.test(prompt))) {
3785
- return true;
3786
- }
3787
- const lowMatches = LOW_CONFIDENCE_PLANNING.filter((pattern) => pattern.test(prompt));
3788
- return lowMatches.length >= 2;
3789
- }
3790
- function processUserPrompt(prompt) {
3791
- if (detectCorrection(prompt)) {
3792
- return {
3793
- hookSpecificOutput: {
3794
- hookEventName: "UserPromptSubmit",
3795
- additionalContext: CORRECTION_REMINDER
3796
- }
3797
- };
3798
- }
3799
- if (detectPlanning(prompt)) {
3800
- return {
3801
- hookSpecificOutput: {
3802
- hookEventName: "UserPromptSubmit",
3803
- additionalContext: PLANNING_REMINDER
3804
- }
3805
- };
3806
- }
3807
- return {};
3808
- }
3809
- var SAME_TARGET_THRESHOLD = 2;
3810
- var TOTAL_FAILURE_THRESHOLD = 3;
3811
- var STATE_FILE_NAME = ".ca-failure-state.json";
3812
- var STATE_MAX_AGE_MS = 60 * 60 * 1e3;
3813
- var failureCount = 0;
3814
- var lastFailedTarget = null;
3815
- var sameTargetCount = 0;
3816
- function defaultState() {
3817
- return { count: 0, lastTarget: null, sameTargetCount: 0, timestamp: Date.now() };
3818
- }
3819
- function readFailureState(stateDir) {
3820
- try {
3821
- const filePath = join(stateDir, STATE_FILE_NAME);
3822
- if (!existsSync(filePath)) return defaultState();
3823
- const raw = readFileSync(filePath, "utf-8");
3824
- const parsed = JSON.parse(raw);
3825
- if (Date.now() - parsed.timestamp > STATE_MAX_AGE_MS) return defaultState();
3826
- return parsed;
3827
- } catch {
3828
- return defaultState();
3829
- }
3830
- }
3831
- function writeFailureState(stateDir, state) {
3832
- try {
3833
- const filePath = join(stateDir, STATE_FILE_NAME);
3834
- writeFileSync(filePath, JSON.stringify(state), "utf-8");
3835
- } catch {
3836
- }
3837
- }
3838
- function deleteStateFile(stateDir) {
3839
- try {
3840
- const filePath = join(stateDir, STATE_FILE_NAME);
3841
- if (existsSync(filePath)) unlinkSync(filePath);
3842
- } catch {
3843
- }
3844
- }
3845
- var FAILURE_TIP = "Tip: Multiple failures detected. `npx ca search` may have solutions for similar issues.";
3846
- function resetFailureState(stateDir) {
3847
- failureCount = 0;
3848
- lastFailedTarget = null;
3849
- sameTargetCount = 0;
3850
- if (stateDir) deleteStateFile(stateDir);
3851
- }
3852
- function getFailureTarget(toolName, toolInput) {
3853
- if (toolName === "Bash" && typeof toolInput.command === "string") {
3854
- const trimmed = toolInput.command.trim();
3855
- const firstSpace = trimmed.indexOf(" ");
3856
- return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
3857
- }
3858
- if ((toolName === "Edit" || toolName === "Write") && typeof toolInput.file_path === "string") {
3859
- return toolInput.file_path;
3860
- }
3861
- return null;
3862
- }
3863
- function processToolFailure(toolName, toolInput, stateDir) {
3864
- if (stateDir) {
3865
- const persisted = readFailureState(stateDir);
3866
- failureCount = persisted.count;
3867
- lastFailedTarget = persisted.lastTarget;
3868
- sameTargetCount = persisted.sameTargetCount;
3869
- }
3870
- failureCount++;
3871
- const target = getFailureTarget(toolName, toolInput);
3872
- if (target !== null && target === lastFailedTarget) {
3873
- sameTargetCount++;
3874
- } else {
3875
- sameTargetCount = 1;
3876
- lastFailedTarget = target;
3877
- }
3878
- const shouldShowTip = sameTargetCount >= SAME_TARGET_THRESHOLD || failureCount >= TOTAL_FAILURE_THRESHOLD;
3879
- if (shouldShowTip) {
3880
- resetFailureState(stateDir);
3881
- return {
3882
- hookSpecificOutput: {
3883
- hookEventName: "PostToolUseFailure",
3884
- additionalContext: FAILURE_TIP
3885
- }
3886
- };
3887
- }
3888
- if (stateDir) {
3889
- writeFailureState(stateDir, {
3890
- count: failureCount,
3891
- lastTarget: lastFailedTarget,
3892
- sameTargetCount,
3893
- timestamp: Date.now()
3894
- });
3895
- }
3896
- return {};
3897
- }
3898
- function processToolSuccess(stateDir) {
3899
- resetFailureState(stateDir);
3900
- }
3901
- var STATE_DIR = ".claude";
3902
- var STATE_FILE = ".ca-phase-state.json";
3903
- var PHASE_STATE_MAX_AGE_MS = 72 * 60 * 60 * 1e3;
3904
- var PHASES = ["brainstorm", "plan", "work", "review", "compound"];
3905
- var GATES = ["post-plan", "gate-3", "gate-4", "final"];
3906
- var PHASE_INDEX = {
3907
- brainstorm: 1,
3908
- plan: 2,
3909
- work: 3,
3910
- review: 4,
3911
- compound: 5
3912
- };
3913
- function getStatePath(repoRoot) {
3914
- return join(repoRoot, STATE_DIR, STATE_FILE);
3915
- }
3916
- function isPhaseName(value) {
3917
- return typeof value === "string" && PHASES.includes(value);
3918
- }
3919
- function isGateName(value) {
3920
- return typeof value === "string" && GATES.includes(value);
3921
- }
3922
- function isIsoDate(value) {
3923
- if (typeof value !== "string") return false;
3924
- return !Number.isNaN(Date.parse(value));
3925
- }
3926
- function isStringArray(value) {
3927
- return Array.isArray(value) && value.every((item) => typeof item === "string");
3928
- }
3929
- function validatePhaseState(raw) {
3930
- if (typeof raw !== "object" || raw === null) return false;
3931
- const state = raw;
3932
- 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);
3933
- }
3934
- function expectedGateForPhase(phaseIndex) {
3935
- if (phaseIndex === 2) return "post-plan";
3936
- if (phaseIndex === 3) return "gate-3";
3937
- if (phaseIndex === 4) return "gate-4";
3938
- if (phaseIndex === 5) return "final";
3939
- return null;
3940
- }
3941
- function initPhaseState(repoRoot, epicId) {
3942
- const dir = join(repoRoot, STATE_DIR);
3943
- mkdirSync(dir, { recursive: true });
3944
- const state = {
3945
- lfg_active: true,
3946
- epic_id: epicId,
3947
- current_phase: "brainstorm",
3948
- phase_index: PHASE_INDEX.brainstorm,
3949
- skills_read: [],
3950
- gates_passed: [],
3951
- started_at: (/* @__PURE__ */ new Date()).toISOString()
3952
- };
3953
- writeFileSync(getStatePath(repoRoot), JSON.stringify(state, null, 2), "utf-8");
3954
- return state;
3955
- }
3956
- function getPhaseState(repoRoot) {
3957
- try {
3958
- const path2 = getStatePath(repoRoot);
3959
- if (!existsSync(path2)) return null;
3960
- const raw = readFileSync(path2, "utf-8");
3961
- const parsed = JSON.parse(raw);
3962
- if (!validatePhaseState(parsed)) return null;
3963
- const age = Date.now() - new Date(parsed.started_at).getTime();
3964
- if (age > PHASE_STATE_MAX_AGE_MS) {
3965
- cleanPhaseState(repoRoot);
3966
- return null;
3967
- }
3968
- return parsed;
3969
- } catch {
3970
- return null;
3971
- }
3972
- }
3973
- function updatePhaseState(repoRoot, partial) {
3974
- const current = getPhaseState(repoRoot);
3975
- if (current === null) return null;
3976
- const updated = {
3977
- ...current,
3978
- ...partial
3979
- };
3980
- if (!validatePhaseState(updated)) return null;
3981
- writeFileSync(getStatePath(repoRoot), JSON.stringify(updated, null, 2), "utf-8");
3982
- return updated;
3983
- }
3984
- function startPhase(repoRoot, phase) {
3985
- return updatePhaseState(repoRoot, {
3986
- current_phase: phase,
3987
- phase_index: PHASE_INDEX[phase]
3988
- });
3989
- }
3990
- function cleanPhaseState(repoRoot) {
3991
- try {
3992
- const path2 = getStatePath(repoRoot);
3993
- if (existsSync(path2)) unlinkSync(path2);
3994
- } catch {
3995
- }
3996
- }
3997
- function recordGatePassed(repoRoot, gate) {
3998
- const current = getPhaseState(repoRoot);
3999
- if (current === null) return null;
4000
- const gatesPassed = current.gates_passed.includes(gate) ? current.gates_passed : [...current.gates_passed, gate];
4001
- const updated = { ...current, gates_passed: gatesPassed };
4002
- if (gate === "final") {
4003
- cleanPhaseState(repoRoot);
4004
- return updated;
4005
- }
4006
- writeFileSync(getStatePath(repoRoot), JSON.stringify(updated, null, 2), "utf-8");
4007
- return updated;
4008
- }
4009
- function printStatusHuman(state) {
4010
- if (state === null) {
4011
- console.log("No active LFG session.");
4012
- return;
4013
- }
4014
- console.log("Active LFG Session");
4015
- console.log(` Epic: ${state.epic_id}`);
4016
- console.log(` Phase: ${state.current_phase} (${state.phase_index}/5)`);
4017
- console.log(` Skills read: ${state.skills_read.length === 0 ? "(none)" : state.skills_read.join(", ")}`);
4018
- console.log(` Gates passed: ${state.gates_passed.length === 0 ? "(none)" : state.gates_passed.join(", ")}`);
4019
- console.log(` Started: ${state.started_at}`);
4020
- }
4021
- function registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot) {
4022
- phaseCheck.command("init <epic-id>").description("Initialize phase state for an epic").action((epicId) => {
4023
- if (!EPIC_ID_PATTERN.test(epicId)) {
4024
- console.error(`Invalid epic ID: "${epicId}"`);
4025
- process.exitCode = 1;
4026
- return;
4027
- }
4028
- if (getDryRun()) {
4029
- console.log(`[dry-run] Would initialize phase state for epic ${epicId} in ${repoRoot()}`);
4030
- return;
4031
- }
4032
- initPhaseState(repoRoot(), epicId);
4033
- console.log(`Phase state initialized for ${epicId}. Current phase: brainstorm (1/5).`);
4034
- });
4035
- phaseCheck.command("start <phase>").description("Start or resume a phase").action((phase) => {
4036
- if (!isPhaseName(phase)) {
4037
- console.error(`Invalid phase: "${phase}". Valid phases: ${PHASES.join(", ")}`);
4038
- process.exitCode = 1;
4039
- return;
4040
- }
4041
- if (getDryRun()) {
4042
- console.log(`[dry-run] Would start phase ${phase}`);
4043
- return;
4044
- }
4045
- const state = startPhase(repoRoot(), phase);
4046
- if (state === null) {
4047
- console.error("No active phase state. Run: ca phase-check init <epic-id>");
4048
- process.exitCode = 1;
4049
- return;
4050
- }
4051
- console.log(`Phase updated: ${state.current_phase} (${state.phase_index}/5).`);
4052
- });
4053
- phaseCheck.command("gate <gate-name>").description("Record a phase gate as passed").action((gateName) => {
4054
- if (!isGateName(gateName)) {
4055
- console.error(`Invalid gate: "${gateName}". Valid gates: ${GATES.join(", ")}`);
4056
- process.exitCode = 1;
4057
- return;
4058
- }
4059
- if (getDryRun()) {
4060
- console.log(`[dry-run] Would record gate ${gateName}`);
4061
- return;
4062
- }
4063
- const state = recordGatePassed(repoRoot(), gateName);
4064
- if (state === null) {
4065
- console.error("No active phase state. Run: ca phase-check init <epic-id>");
4066
- process.exitCode = 1;
4067
- return;
4068
- }
4069
- if (gateName === "final") {
4070
- console.log("Final gate recorded. Phase state cleaned.");
4071
- return;
4072
- }
4073
- console.log(`Gate recorded: ${gateName}.`);
4074
- });
4075
- phaseCheck.command("status").description("Show current phase state").option("--json", "Output raw JSON").action((options) => {
4076
- const state = getPhaseState(repoRoot());
4077
- if (options.json) {
4078
- console.log(JSON.stringify(state ?? { lfg_active: false }));
4079
- return;
4080
- }
4081
- printStatusHuman(state);
4082
- });
4083
- phaseCheck.command("clean").description("Remove phase state file").action(() => {
4084
- if (getDryRun()) {
4085
- console.log("[dry-run] Would delete phase state file");
4086
- return;
4087
- }
4088
- cleanPhaseState(repoRoot());
4089
- console.log("Phase state cleaned.");
4090
- });
4091
- }
4092
- function registerPhaseCheckCommand(program2) {
4093
- const phaseCheck = program2.command("phase-check").description("Manage LFG phase state").option("--dry-run", "Show what would be done without making changes");
4094
- const getDryRun = () => phaseCheck.opts().dryRun ?? false;
4095
- const repoRoot = () => getRepoRoot();
4096
- registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot);
4097
- program2.command("phase-clean").description("Remove phase state file (alias for `phase-check clean`)").action(() => {
4098
- cleanPhaseState(repoRoot());
4099
- console.log("Phase state cleaned.");
4100
- });
4101
- }
4102
-
4103
- // src/setup/hooks-phase-guard.ts
4104
- function processPhaseGuard(repoRoot, toolName, _toolInput) {
4105
- try {
4106
- if (toolName !== "Edit" && toolName !== "Write") return {};
4107
- const state = getPhaseState(repoRoot);
4108
- if (state === null || !state.lfg_active) return {};
4109
- const expectedSkillPath = `.claude/skills/compound/${state.current_phase}/SKILL.md`;
4110
- const skillRead = state.skills_read.includes(expectedSkillPath);
4111
- if (!skillRead) {
4112
- return {
4113
- hookSpecificOutput: {
4114
- hookEventName: "PreToolUse",
4115
- 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.`
4116
- }
4117
- };
4118
- }
4119
- return {};
4120
- } catch {
4121
- return {};
4122
- }
4123
- }
4124
-
4125
- // src/setup/hooks-read-tracker.ts
4126
- var SKILL_PATH_PATTERN = /(?:^|\/)\.claude\/skills\/compound\/([^/]+)\/SKILL\.md$/;
4127
- function normalizePath(path2) {
4128
- return path2.replaceAll("\\", "/");
4129
- }
4130
- function toCanonicalSkillPath(filePath) {
4131
- const normalized = normalizePath(filePath);
4132
- const match = SKILL_PATH_PATTERN.exec(normalized);
4133
- if (!match?.[1]) return null;
4134
- return `.claude/skills/compound/${match[1]}/SKILL.md`;
4135
- }
4136
- function processReadTracker(repoRoot, toolName, toolInput) {
4137
- try {
4138
- if (toolName !== "Read") return {};
4139
- const state = getPhaseState(repoRoot);
4140
- if (state === null || !state.lfg_active) return {};
4141
- const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : null;
4142
- if (filePath === null) return {};
4143
- const canonicalPath = toCanonicalSkillPath(filePath);
4144
- if (canonicalPath === null) return {};
4145
- if (!state.skills_read.includes(canonicalPath)) {
4146
- updatePhaseState(repoRoot, {
4147
- skills_read: [...state.skills_read, canonicalPath]
4148
- });
4149
- }
4150
- return {};
4151
- } catch {
4152
- return {};
4153
- }
4154
- }
4155
-
4156
- // src/setup/hooks-stop-audit.ts
4157
- function hasTransitionEvidence(state) {
4158
- if (state.phase_index === 5) return true;
4159
- const nextPhase = PHASES[state.phase_index];
4160
- if (nextPhase === void 0) return false;
4161
- const nextSkillPath = `.claude/skills/compound/${nextPhase}/SKILL.md`;
4162
- return state.skills_read.includes(nextSkillPath);
4163
- }
4164
- function processStopAudit(repoRoot, stopHookActive = false) {
4165
- try {
4166
- if (stopHookActive) return {};
4167
- const state = getPhaseState(repoRoot);
4168
- if (state === null || !state.lfg_active) return {};
4169
- const expectedGate = expectedGateForPhase(state.phase_index);
4170
- if (expectedGate === null) return {};
4171
- if (state.gates_passed.includes(expectedGate)) return {};
4172
- if (!hasTransitionEvidence(state)) return {};
4173
- return {
4174
- continue: false,
4175
- stopReason: `PHASE GATE NOT VERIFIED: ${state.current_phase} requires gate '${expectedGate}'. Run: npx ca phase-check gate ${expectedGate}`
4176
- };
4177
- } catch {
4178
- return {};
4179
- }
4180
- }
4181
-
4182
- // src/setup/hooks.ts
4183
- var HOOK_FILE_MODE = 493;
4184
- function hasCompoundAgentHook(content) {
4185
- return content.includes(HOOK_MARKER);
4186
- }
4187
- async function getGitHooksDir(repoRoot) {
4188
- const gitPath = join(repoRoot, ".git");
4189
- if (!existsSync(gitPath)) {
4190
- return null;
4191
- }
4192
- let gitDir = gitPath;
4193
- if (lstatSync(gitPath).isFile()) {
4194
- const content = readFileSync(gitPath, "utf-8").trim();
4195
- const match = /^gitdir:\s*(.+)$/.exec(content);
4196
- if (!match?.[1]) return null;
4197
- gitDir = resolve(repoRoot, match[1]);
4198
- }
4199
- const configPath2 = join(gitDir, "config");
4200
- if (existsSync(configPath2)) {
4201
- const config = await readFile(configPath2, "utf-8");
4202
- const match = /hooksPath\s*=\s*(.+)$/m.exec(config);
4203
- if (match?.[1]) {
4204
- const hooksPath = match[1].trim();
4205
- return hooksPath.startsWith("/") ? hooksPath : join(repoRoot, hooksPath);
4206
- }
4207
- }
4208
- const defaultHooksDir = join(gitDir, "hooks");
4209
- return existsSync(defaultHooksDir) ? defaultHooksDir : null;
4210
- }
4211
- function findFirstTopLevelExitLine(lines) {
4212
- let insideFunction = 0;
4213
- let heredocDelimiter = null;
4214
- for (let i = 0; i < lines.length; i++) {
4215
- const line = lines[i] ?? "";
4216
- const trimmed = line.trim();
4217
- if (heredocDelimiter !== null) {
4218
- if (trimmed === heredocDelimiter) {
4219
- heredocDelimiter = null;
4220
- }
4221
- continue;
4222
- }
4223
- const heredocMatch = /<<-?\s*['"]?(\w+)['"]?/.exec(line);
4224
- if (heredocMatch?.[1]) {
4225
- heredocDelimiter = heredocMatch[1];
4226
- continue;
4227
- }
4228
- for (const char of line) {
4229
- if (char === "{") insideFunction++;
4230
- if (char === "}") insideFunction = Math.max(0, insideFunction - 1);
4231
- }
4232
- if (insideFunction > 0) {
4233
- continue;
4234
- }
4235
- if (/^\s*exit\s+(\d+|\$\w+|\$\?)\s*$/.test(trimmed)) {
4236
- return i;
4237
- }
4238
- }
4239
- return -1;
4240
- }
4241
- async function installPreCommitHook(repoRoot) {
4242
- const gitHooksDir = await getGitHooksDir(repoRoot);
4243
- if (!gitHooksDir) {
4244
- return { status: "not_git_repo" };
4245
- }
4246
- await mkdir(gitHooksDir, { recursive: true });
4247
- const hookPath = join(gitHooksDir, "pre-commit");
4248
- if (existsSync(hookPath)) {
4249
- const content = await readFile(hookPath, "utf-8");
4250
- if (hasCompoundAgentHook(content)) {
4251
- return { status: "already_installed" };
4252
- }
4253
- const lines = content.split("\n");
4254
- const exitLineIndex = findFirstTopLevelExitLine(lines);
4255
- let newContent;
4256
- if (exitLineIndex === -1) {
4257
- newContent = content.trimEnd() + "\n" + COMPOUND_AGENT_HOOK_BLOCK;
4258
- } else {
4259
- const before = lines.slice(0, exitLineIndex);
4260
- const after = lines.slice(exitLineIndex);
4261
- newContent = before.join("\n") + COMPOUND_AGENT_HOOK_BLOCK + after.join("\n");
4262
- }
4263
- await writeFile(hookPath, newContent, "utf-8");
4264
- chmodSync(hookPath, HOOK_FILE_MODE);
4265
- return { status: "appended" };
4266
- }
4267
- await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
4268
- chmodSync(hookPath, HOOK_FILE_MODE);
4269
- return { status: "installed" };
4270
- }
4271
- async function installPostCommitHook(repoRoot) {
4272
- const gitHooksDir = await getGitHooksDir(repoRoot);
4273
- if (!gitHooksDir) {
4274
- return { status: "not_git_repo" };
4275
- }
4276
- await mkdir(gitHooksDir, { recursive: true });
4277
- const hookPath = join(gitHooksDir, "post-commit");
4278
- if (existsSync(hookPath)) {
4279
- const content = await readFile(hookPath, "utf-8");
4280
- if (content.includes(POST_COMMIT_HOOK_MARKER)) {
4281
- return { status: "already_installed" };
4282
- }
4283
- const lines = content.split("\n");
4284
- const exitLineIndex = findFirstTopLevelExitLine(lines);
4285
- let newContent;
4286
- if (exitLineIndex === -1) {
4287
- newContent = content.trimEnd() + "\n" + COMPOUND_AGENT_POST_COMMIT_BLOCK;
4288
- } else {
4289
- const before = lines.slice(0, exitLineIndex);
4290
- const after = lines.slice(exitLineIndex);
4291
- newContent = before.join("\n") + COMPOUND_AGENT_POST_COMMIT_BLOCK + after.join("\n");
4292
- }
4293
- await writeFile(hookPath, newContent, "utf-8");
4294
- chmodSync(hookPath, HOOK_FILE_MODE);
4295
- return { status: "appended" };
4296
- }
4297
- await writeFile(hookPath, POST_COMMIT_HOOK_TEMPLATE, "utf-8");
4298
- chmodSync(hookPath, HOOK_FILE_MODE);
4299
- return { status: "installed" };
4300
- }
4301
- async function readStdin() {
4302
- const chunks = [];
4303
- for await (const chunk of process.stdin) {
4304
- chunks.push(chunk);
4305
- }
4306
- return Buffer.concat(chunks).toString("utf-8");
4307
- }
4308
- async function runUserPromptHook() {
4309
- try {
4310
- const input = await readStdin();
4311
- const data = JSON.parse(input);
4312
- if (!data.prompt) {
4313
- console.log(JSON.stringify({}));
4314
- return;
4315
- }
4316
- const result = processUserPrompt(data.prompt);
4317
- console.log(JSON.stringify(result));
4318
- } catch {
4319
- console.log(JSON.stringify({}));
4320
- }
4321
- }
4322
- async function runPostToolFailureHook() {
4323
- try {
4324
- const input = await readStdin();
4325
- const data = JSON.parse(input);
4326
- if (!data.tool_name) {
4327
- console.log(JSON.stringify({}));
4328
- return;
4329
- }
4330
- const stateDir = join(getRepoRoot(), ".claude");
4331
- const result = processToolFailure(data.tool_name, data.tool_input ?? {}, stateDir);
4332
- console.log(JSON.stringify(result));
4333
- } catch {
4334
- console.log(JSON.stringify({}));
4335
- }
4336
- }
4337
- async function runPostToolSuccessHook() {
4338
- try {
4339
- await readStdin();
4340
- const stateDir = join(getRepoRoot(), ".claude");
4341
- processToolSuccess(stateDir);
4342
- console.log(JSON.stringify({}));
4343
- } catch {
4344
- console.log(JSON.stringify({}));
4345
- }
4346
- }
4347
- async function runToolHook(processor) {
4348
- try {
4349
- const input = await readStdin();
4350
- const data = JSON.parse(input);
4351
- if (!data.tool_name) {
4352
- console.log(JSON.stringify({}));
4353
- return;
4354
- }
4355
- console.log(JSON.stringify(processor(getRepoRoot(), data.tool_name, data.tool_input ?? {})));
4356
- } catch {
4357
- console.log(JSON.stringify({}));
4358
- }
4359
- }
4360
- async function runStopAuditHook() {
4361
- try {
4362
- const input = await readStdin();
4363
- const data = JSON.parse(input);
4364
- console.log(JSON.stringify(processStopAudit(getRepoRoot(), data.stop_hook_active ?? false)));
4365
- } catch {
4366
- console.log(JSON.stringify({}));
4367
- }
4368
- }
4369
- function registerHooksCommand(program2) {
4370
- const hooksCommand = program2.command("hooks").description("Git hooks management");
4371
- hooksCommand.command("run <hook>").description("Run a hook script (called by git/Claude hooks)").option("--json", "Output as JSON").action(async (hook, options) => {
4372
- if (hook === "pre-commit") {
4373
- if (options.json) {
4374
- console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
4375
- } else {
4376
- console.log(PRE_COMMIT_MESSAGE);
4377
- }
4378
- } else if (hook === "user-prompt") {
4379
- await runUserPromptHook();
4380
- } else if (hook === "post-tool-failure") {
4381
- await runPostToolFailureHook();
4382
- } else if (hook === "post-tool-success") {
4383
- await runPostToolSuccessHook();
4384
- } else if (hook === "phase-guard") {
4385
- await runToolHook(processPhaseGuard);
4386
- } else if (hook === "post-read" || hook === "read-tracker") {
4387
- await runToolHook(processReadTracker);
4388
- } else if (hook === "phase-audit" || hook === "stop-audit") {
4389
- await runStopAuditHook();
4390
- } else {
4391
- if (options.json) {
4392
- console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
4393
- } else {
4394
- console.error(
4395
- formatError(
4396
- "hooks",
4397
- "UNKNOWN_HOOK",
4398
- `Unknown hook: ${hook}`,
4399
- "Valid hooks: pre-commit, user-prompt, post-tool-failure, post-tool-success, post-read (or read-tracker), phase-guard, phase-audit (or stop-audit)"
4400
- )
4401
- );
4402
- }
4403
- process.exitCode = 1;
4404
- }
4405
- });
4406
- }
4407
-
4408
3740
  // src/setup/templates/agents-external.ts
4409
3741
  var EXTERNAL_AGENT_TEMPLATES = {
4410
3742
  "external-reviewer-gemini.md": `---
@@ -6850,80 +6182,907 @@ name: Test Cleaner
6850
6182
  description: Multi-phase test suite optimization with adversarial review
6851
6183
  ---
6852
6184
 
6853
- # Test Cleaner Skill
6854
-
6855
- ## Overview
6856
- Analyze, optimize, and clean a project's test suite through a multi-phase workflow with adversarial review. Produces machine-readable output and feeds findings into compound-agent memory.
6857
-
6858
- ## Methodology
6859
-
6860
- ### Phase 1: Analysis
6861
- Spawn multiple analysis subagents in parallel:
6862
- - **Cargo-cult detector**: Find fake tests, mocked business logic, trivial assertions
6863
- - **Redundancy analyzer**: Identify overlapping/duplicate test coverage
6864
- - **Independence checker**: Verify tests don't depend on execution order or shared state
6865
- - **Invariant tracer**: Map which invariants each test verifies (Lamport framework)
6866
- - **Coverage analyzer**: Identify untested code paths and modules
6867
-
6868
- ### Phase 2: Planning
6869
- Synthesize analysis results into a refined optimization plan:
6870
- - Categorize findings by severity (P1/P2/P3)
6871
- - Propose specific changes for each finding
6872
- - Estimate impact on test suite speed and coverage
6873
- - Iterate with subagents until the plan is comprehensive
6874
-
6875
- ### Phase 3: Adversarial Review (CRITICAL QUALITY GATE)
6876
- **This is THE KEY PHASE -- the most important phase in the entire workflow. NEVER skip, NEVER rush, NEVER settle for "good enough."**
6877
-
6878
- Expose the plan to two neutral reviewer subagents:
6879
- - **Reviewer A** (Opus): Independent critique of the optimization plan
6880
- - **Reviewer B** (Sonnet): Independent critique from a different perspective
6881
-
6882
- Both reviewers challenge assumptions, identify risks, and suggest improvements.
6883
-
6884
- **Mandatory iteration loop**: After each reviewer pass, if ANY issues, concerns, or suggestions remain from EITHER reviewer, revise the plan and re-submit to BOTH reviewers. Repeat until BOTH reviewers explicitly approve with ZERO reservations. Do not proceed to Phase 4 until unanimous, unconditional approval is reached.
6885
-
6886
- This is the critical quality gate. Loop as many times as needed. The test suite must be bulletproof before execution begins.
6887
-
6888
- ### Phase 4: Execution
6889
- Apply the agreed changes:
6890
- - Machine-readable output format: \`ERROR [file:line] type: description\`
6891
- - Include \`REMEDIATION\` suggestions and \`SEE\` references
6892
- - Use \`pnpm test:segment\`, \`pnpm test:random\`, \`pnpm test:critical\` for targeted validation
6185
+ # Test Cleaner Skill
6186
+
6187
+ ## Overview
6188
+ Analyze, optimize, and clean a project's test suite through a multi-phase workflow with adversarial review. Produces machine-readable output and feeds findings into compound-agent memory.
6189
+
6190
+ ## Methodology
6191
+
6192
+ ### Phase 1: Analysis
6193
+ Spawn multiple analysis subagents in parallel:
6194
+ - **Cargo-cult detector**: Find fake tests, mocked business logic, trivial assertions
6195
+ - **Redundancy analyzer**: Identify overlapping/duplicate test coverage
6196
+ - **Independence checker**: Verify tests don't depend on execution order or shared state
6197
+ - **Invariant tracer**: Map which invariants each test verifies (Lamport framework)
6198
+ - **Coverage analyzer**: Identify untested code paths and modules
6199
+
6200
+ ### Phase 2: Planning
6201
+ Synthesize analysis results into a refined optimization plan:
6202
+ - Categorize findings by severity (P1/P2/P3)
6203
+ - Propose specific changes for each finding
6204
+ - Estimate impact on test suite speed and coverage
6205
+ - Iterate with subagents until the plan is comprehensive
6206
+
6207
+ ### Phase 3: Adversarial Review (CRITICAL QUALITY GATE)
6208
+ **This is THE KEY PHASE -- the most important phase in the entire workflow. NEVER skip, NEVER rush, NEVER settle for "good enough."**
6209
+
6210
+ Expose the plan to two neutral reviewer subagents:
6211
+ - **Reviewer A** (Opus): Independent critique of the optimization plan
6212
+ - **Reviewer B** (Sonnet): Independent critique from a different perspective
6213
+
6214
+ Both reviewers challenge assumptions, identify risks, and suggest improvements.
6215
+
6216
+ **Mandatory iteration loop**: After each reviewer pass, if ANY issues, concerns, or suggestions remain from EITHER reviewer, revise the plan and re-submit to BOTH reviewers. Repeat until BOTH reviewers explicitly approve with ZERO reservations. Do not proceed to Phase 4 until unanimous, unconditional approval is reached.
6217
+
6218
+ This is the critical quality gate. Loop as many times as needed. The test suite must be bulletproof before execution begins.
6219
+
6220
+ ### Phase 4: Execution
6221
+ Apply the agreed changes:
6222
+ - Machine-readable output format: \`ERROR [file:line] type: description\`
6223
+ - Include \`REMEDIATION\` suggestions and \`SEE\` references
6224
+ - Use \`pnpm test:segment\`, \`pnpm test:random\`, \`pnpm test:critical\` for targeted validation
6225
+
6226
+ ### Phase 5: Verification
6227
+ - Run full test suite after changes
6228
+ - Compare before/after metrics (count, duration, coverage)
6229
+ - Feed findings into compound-agent memory via \`npx ca learn\`
6230
+
6231
+ ## Test Scripts Integration
6232
+ - \`pnpm test:segment <module>\` -- Test specific module in isolation
6233
+ - \`pnpm test:random <pct>\` -- Deterministic random subset (seeded per-agent)
6234
+ - \`pnpm test:critical\` -- P1/critical tests only (fast CI feedback)
6235
+
6236
+ ## Memory Integration
6237
+ - Run \`npx ca search "test optimization"\` before starting
6238
+ - After completion, capture findings via \`npx ca learn\`
6239
+ - Feed patterns into CCT system for future sessions
6240
+
6241
+ ## Common Pitfalls
6242
+ - Deleting tests without verifying coverage is maintained elsewhere
6243
+ - Optimizing for speed at the cost of correctness
6244
+ - Settling for partial approval or cutting the Phase 3 review loop short before BOTH reviewers approve with zero reservations
6245
+ - Making changes without machine-readable output
6246
+ - Not feeding results back into compound-agent memory
6247
+
6248
+ ## Quality Criteria
6249
+ - All 5 phases completed (analysis, planning, review, execution, verification)
6250
+ - Both adversarial reviewers approved with zero reservations after iterative refinement
6251
+ - Machine-readable output format used throughout
6252
+ - Full test suite passes after changes
6253
+ - Coverage not degraded
6254
+ - Findings captured in compound-agent memory
6255
+ `
6256
+ };
6257
+
6258
+ // src/setup/gemini.ts
6259
+ var HOOKS = {
6260
+ "ca-prime.sh": `#!/usr/bin/env bash
6261
+ input=$(cat)
6262
+ echo "$input" | npx ca prime > /dev/null 2>&1
6263
+ echo '{"decision": "allow"}'
6264
+ `,
6265
+ "ca-user-prompt.sh": `#!/usr/bin/env bash
6266
+ input=$(cat)
6267
+ echo "$input" | npx ca hooks run user-prompt > /dev/null 2>&1
6268
+ echo '{"decision": "allow"}'
6269
+ `,
6270
+ "ca-post-tool.sh": `#!/usr/bin/env bash
6271
+ input=$(cat)
6272
+ echo "$input" | npx ca hooks run post-tool-success > /dev/null 2>&1
6273
+ echo '{"decision": "allow"}'
6274
+ `,
6275
+ "ca-phase-guard.sh": `#!/usr/bin/env bash
6276
+ input=$(cat)
6277
+ echo "$input" | npx ca hooks run phase-guard > /dev/null 2>&1
6278
+ rc=$?
6279
+ if [ $rc -ne 0 ]; then
6280
+ echo '{"decision": "deny", "reason": "Phase guard: read the phase skill before editing"}'
6281
+ exit 0
6282
+ fi
6283
+ echo '{"decision": "allow"}'
6284
+ `
6285
+ };
6286
+ var SETTINGS_JSON = {
6287
+ hooks: {
6288
+ SessionStart: [
6289
+ {
6290
+ matcher: ".*",
6291
+ hooks: [{ name: "ca-prime", type: "command", command: "bash .gemini/hooks/ca-prime.sh" }]
6292
+ }
6293
+ ],
6294
+ BeforeAgent: [
6295
+ {
6296
+ matcher: ".*",
6297
+ hooks: [{ name: "ca-user-prompt", type: "command", command: "bash .gemini/hooks/ca-user-prompt.sh" }]
6298
+ }
6299
+ ],
6300
+ BeforeTool: [
6301
+ {
6302
+ matcher: "replace|write_file|create_file",
6303
+ hooks: [{ name: "ca-phase-guard", type: "command", command: "bash .gemini/hooks/ca-phase-guard.sh" }]
6304
+ }
6305
+ ],
6306
+ AfterTool: [
6307
+ {
6308
+ matcher: "run_shell_command|replace|write_file|create_file",
6309
+ hooks: [{ name: "ca-post-tool", type: "command", command: "bash .gemini/hooks/ca-post-tool.sh" }]
6310
+ }
6311
+ ]
6312
+ }
6313
+ };
6314
+ function parseDescription(content, fallback) {
6315
+ const raw = content.match(/description:\s*(.*)/)?.[1] ?? fallback;
6316
+ return raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
6317
+ }
6318
+ async function writeSettings(geminiDir) {
6319
+ const settingsPath = join(geminiDir, "settings.json");
6320
+ let settings = SETTINGS_JSON;
6321
+ if (existsSync(settingsPath)) {
6322
+ try {
6323
+ const existing = JSON.parse(await readFile(settingsPath, "utf8"));
6324
+ settings = {
6325
+ ...existing,
6326
+ hooks: {
6327
+ ...existing.hooks,
6328
+ ...SETTINGS_JSON.hooks
6329
+ }
6330
+ };
6331
+ } catch {
6332
+ }
6333
+ }
6334
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6335
+ }
6336
+ async function writeTomlCommands(geminiDir) {
6337
+ for (const [filename, content] of Object.entries(WORKFLOW_COMMANDS)) {
6338
+ const cmdName = filename.replace(".md", "");
6339
+ const description = parseDescription(content, `Compound ${cmdName} command`);
6340
+ const toml = `description = "${description}"
6341
+ prompt = """
6342
+ @{.claude/commands/compound/${filename}}
6343
+
6344
+ {{args}}
6345
+ """
6346
+ `;
6347
+ await writeFile(join(geminiDir, "commands", "compound", `${cmdName}.toml`), toml, "utf8");
6348
+ }
6349
+ }
6350
+ async function writeSkills(geminiDir) {
6351
+ for (const [phase, content] of Object.entries(PHASE_SKILLS)) {
6352
+ const skillDir = join(geminiDir, "skills", `compound-${phase}`);
6353
+ await mkdir(skillDir, { recursive: true });
6354
+ const description = parseDescription(content, `Compound ${phase} skill`);
6355
+ await writeFile(join(skillDir, "SKILL.md"), `---
6356
+ name: compound-${phase}
6357
+ description: ${description}
6358
+ ---
6359
+
6360
+ ${content}
6361
+ `, "utf8");
6362
+ }
6363
+ for (const [name, content] of Object.entries(AGENT_ROLE_SKILLS)) {
6364
+ const skillDir = join(geminiDir, "skills", `compound-agent-${name}`);
6365
+ await mkdir(skillDir, { recursive: true });
6366
+ const description = parseDescription(content, `Compound agent ${name} skill`);
6367
+ await writeFile(join(skillDir, "SKILL.md"), `---
6368
+ name: compound-agent-${name}
6369
+ description: ${description}
6370
+ ---
6893
6371
 
6894
- ### Phase 5: Verification
6895
- - Run full test suite after changes
6896
- - Compare before/after metrics (count, duration, coverage)
6897
- - Feed findings into compound-agent memory via \`npx ca learn\`
6372
+ ${content}
6373
+ `, "utf8");
6374
+ }
6375
+ }
6376
+ async function installGeminiAdapter(options) {
6377
+ const repoRoot = getRepoRoot();
6378
+ const geminiDir = join(repoRoot, ".gemini");
6379
+ if (options.dryRun) {
6380
+ if (options.json) {
6381
+ console.log(JSON.stringify({ dryRun: true, wouldInstall: true, location: geminiDir }));
6382
+ } else {
6383
+ console.log(`Would install gemini hooks and commands to ${geminiDir}`);
6384
+ }
6385
+ return;
6386
+ }
6387
+ await mkdir(join(geminiDir, "hooks"), { recursive: true });
6388
+ await mkdir(join(geminiDir, "commands", "compound"), { recursive: true });
6389
+ for (const [filename, content] of Object.entries(HOOKS)) {
6390
+ await writeFile(join(geminiDir, "hooks", filename), content, { mode: 493 });
6391
+ }
6392
+ await writeSettings(geminiDir);
6393
+ await writeTomlCommands(geminiDir);
6394
+ await writeSkills(geminiDir);
6395
+ if (options.json) {
6396
+ console.log(JSON.stringify({ installed: true, location: geminiDir, action: "created" }));
6397
+ } else {
6398
+ out.success("Gemini CLI compatibility hooks installed");
6399
+ console.log(` Location: ${geminiDir}`);
6400
+ console.log(" Hooks: SessionStart, BeforeAgent, BeforeTool, AfterTool");
6401
+ console.log(" Commands: /compound:* aliases generated");
6402
+ }
6403
+ }
6404
+ function registerGeminiSubcommand(setupCommand) {
6405
+ setupCommand.command("gemini").description("Install Gemini CLI compatibility hooks (Adapter Pattern)").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
6406
+ try {
6407
+ await installGeminiAdapter(options);
6408
+ } catch (err) {
6409
+ if (options.json) {
6410
+ console.log(JSON.stringify({ error: String(err) }));
6411
+ } else {
6412
+ console.error(formatError("setup", "GEMINI_INSTALL_ERROR", String(err), "Check .gemini/ directory permissions"));
6413
+ }
6414
+ process.exitCode = 1;
6415
+ }
6416
+ });
6417
+ }
6898
6418
 
6899
- ## Test Scripts Integration
6900
- - \`pnpm test:segment <module>\` -- Test specific module in isolation
6901
- - \`pnpm test:random <pct>\` -- Deterministic random subset (seeded per-agent)
6902
- - \`pnpm test:critical\` -- P1/critical tests only (fast CI feedback)
6419
+ // src/setup/hooks-user-prompt.ts
6420
+ var CORRECTION_PATTERNS = [
6421
+ /\bactually\b/i,
6422
+ /\bno[,.]?\s/i,
6423
+ /\bwrong\b/i,
6424
+ /\bthat'?s not right\b/i,
6425
+ /\bthat'?s incorrect\b/i,
6426
+ /\buse .+ instead\b/i,
6427
+ /\bi told you\b/i,
6428
+ /\bi already said\b/i,
6429
+ /\bnot like that\b/i,
6430
+ /\byou forgot\b/i,
6431
+ /\byou missed\b/i,
6432
+ /\bstop\s*(,\s*)?(doing|using|that)\b/i,
6433
+ /\bwait\s*(,\s*)?(that|no|wrong)\b/i
6434
+ ];
6435
+ var HIGH_CONFIDENCE_PLANNING = [
6436
+ /\bdecide\b/i,
6437
+ /\bchoose\b/i,
6438
+ /\bpick\b/i,
6439
+ /\bwhich approach\b/i,
6440
+ /\bwhat do you think\b/i,
6441
+ /\bshould we\b/i,
6442
+ /\bwould you\b/i,
6443
+ /\bhow should\b/i,
6444
+ /\bwhat'?s the best\b/i,
6445
+ /\badd feature\b/i,
6446
+ /\bset up\b/i
6447
+ ];
6448
+ var LOW_CONFIDENCE_PLANNING = [
6449
+ /\bimplement\b/i,
6450
+ /\bbuild\b/i,
6451
+ /\bcreate\b/i,
6452
+ /\brefactor\b/i,
6453
+ /\bfix\b/i,
6454
+ /\bwrite\b/i,
6455
+ /\bdevelop\b/i
6456
+ ];
6457
+ var CORRECTION_REMINDER = "Remember: You have memory tools available - `npx ca learn` to save insights, `npx ca search` to find past solutions.";
6458
+ var PLANNING_REMINDER = "If you're uncertain or hesitant, remember your memory tools: `npx ca search` may have relevant context from past sessions.";
6459
+ function detectCorrection(prompt) {
6460
+ return CORRECTION_PATTERNS.some((pattern) => pattern.test(prompt));
6461
+ }
6462
+ function detectPlanning(prompt) {
6463
+ if (HIGH_CONFIDENCE_PLANNING.some((pattern) => pattern.test(prompt))) {
6464
+ return true;
6465
+ }
6466
+ const lowMatches = LOW_CONFIDENCE_PLANNING.filter((pattern) => pattern.test(prompt));
6467
+ return lowMatches.length >= 2;
6468
+ }
6469
+ function processUserPrompt(prompt) {
6470
+ if (detectCorrection(prompt)) {
6471
+ return {
6472
+ hookSpecificOutput: {
6473
+ hookEventName: "UserPromptSubmit",
6474
+ additionalContext: CORRECTION_REMINDER
6475
+ }
6476
+ };
6477
+ }
6478
+ if (detectPlanning(prompt)) {
6479
+ return {
6480
+ hookSpecificOutput: {
6481
+ hookEventName: "UserPromptSubmit",
6482
+ additionalContext: PLANNING_REMINDER
6483
+ }
6484
+ };
6485
+ }
6486
+ return {};
6487
+ }
6488
+ var SAME_TARGET_THRESHOLD = 2;
6489
+ var TOTAL_FAILURE_THRESHOLD = 3;
6490
+ var STATE_FILE_NAME = ".ca-failure-state.json";
6491
+ var STATE_MAX_AGE_MS = 60 * 60 * 1e3;
6492
+ var failureCount = 0;
6493
+ var lastFailedTarget = null;
6494
+ var sameTargetCount = 0;
6495
+ function defaultState() {
6496
+ return { count: 0, lastTarget: null, sameTargetCount: 0, timestamp: Date.now() };
6497
+ }
6498
+ function readFailureState(stateDir) {
6499
+ try {
6500
+ const filePath = join(stateDir, STATE_FILE_NAME);
6501
+ if (!existsSync(filePath)) return defaultState();
6502
+ const raw = readFileSync(filePath, "utf-8");
6503
+ const parsed = JSON.parse(raw);
6504
+ if (Date.now() - parsed.timestamp > STATE_MAX_AGE_MS) return defaultState();
6505
+ return parsed;
6506
+ } catch {
6507
+ return defaultState();
6508
+ }
6509
+ }
6510
+ function writeFailureState(stateDir, state) {
6511
+ try {
6512
+ const filePath = join(stateDir, STATE_FILE_NAME);
6513
+ writeFileSync(filePath, JSON.stringify(state), "utf-8");
6514
+ } catch {
6515
+ }
6516
+ }
6517
+ function deleteStateFile(stateDir) {
6518
+ try {
6519
+ const filePath = join(stateDir, STATE_FILE_NAME);
6520
+ if (existsSync(filePath)) unlinkSync(filePath);
6521
+ } catch {
6522
+ }
6523
+ }
6524
+ var FAILURE_TIP = "Tip: Multiple failures detected. `npx ca search` may have solutions for similar issues.";
6525
+ function resetFailureState(stateDir) {
6526
+ failureCount = 0;
6527
+ lastFailedTarget = null;
6528
+ sameTargetCount = 0;
6529
+ if (stateDir) deleteStateFile(stateDir);
6530
+ }
6531
+ function getFailureTarget(toolName, toolInput) {
6532
+ if (toolName === "Bash" && typeof toolInput.command === "string") {
6533
+ const trimmed = toolInput.command.trim();
6534
+ const firstSpace = trimmed.indexOf(" ");
6535
+ return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
6536
+ }
6537
+ if ((toolName === "Edit" || toolName === "Write") && typeof toolInput.file_path === "string") {
6538
+ return toolInput.file_path;
6539
+ }
6540
+ return null;
6541
+ }
6542
+ function processToolFailure(toolName, toolInput, stateDir) {
6543
+ if (stateDir) {
6544
+ const persisted = readFailureState(stateDir);
6545
+ failureCount = persisted.count;
6546
+ lastFailedTarget = persisted.lastTarget;
6547
+ sameTargetCount = persisted.sameTargetCount;
6548
+ }
6549
+ failureCount++;
6550
+ const target = getFailureTarget(toolName, toolInput);
6551
+ if (target !== null && target === lastFailedTarget) {
6552
+ sameTargetCount++;
6553
+ } else {
6554
+ sameTargetCount = 1;
6555
+ lastFailedTarget = target;
6556
+ }
6557
+ const shouldShowTip = sameTargetCount >= SAME_TARGET_THRESHOLD || failureCount >= TOTAL_FAILURE_THRESHOLD;
6558
+ if (shouldShowTip) {
6559
+ resetFailureState(stateDir);
6560
+ return {
6561
+ hookSpecificOutput: {
6562
+ hookEventName: "PostToolUseFailure",
6563
+ additionalContext: FAILURE_TIP
6564
+ }
6565
+ };
6566
+ }
6567
+ if (stateDir) {
6568
+ writeFailureState(stateDir, {
6569
+ count: failureCount,
6570
+ lastTarget: lastFailedTarget,
6571
+ sameTargetCount,
6572
+ timestamp: Date.now()
6573
+ });
6574
+ }
6575
+ return {};
6576
+ }
6577
+ function processToolSuccess(stateDir) {
6578
+ resetFailureState(stateDir);
6579
+ }
6580
+ var STATE_DIR = ".claude";
6581
+ var STATE_FILE = ".ca-phase-state.json";
6582
+ var PHASE_STATE_MAX_AGE_MS = 72 * 60 * 60 * 1e3;
6583
+ var PHASES = ["brainstorm", "plan", "work", "review", "compound"];
6584
+ var GATES = ["post-plan", "gate-3", "gate-4", "final"];
6585
+ var PHASE_INDEX = {
6586
+ brainstorm: 1,
6587
+ plan: 2,
6588
+ work: 3,
6589
+ review: 4,
6590
+ compound: 5
6591
+ };
6592
+ function getStatePath(repoRoot) {
6593
+ return join(repoRoot, STATE_DIR, STATE_FILE);
6594
+ }
6595
+ function isPhaseName(value) {
6596
+ return typeof value === "string" && PHASES.includes(value);
6597
+ }
6598
+ function isGateName(value) {
6599
+ return typeof value === "string" && GATES.includes(value);
6600
+ }
6601
+ function isIsoDate(value) {
6602
+ if (typeof value !== "string") return false;
6603
+ return !Number.isNaN(Date.parse(value));
6604
+ }
6605
+ function isStringArray(value) {
6606
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
6607
+ }
6608
+ function validatePhaseState(raw) {
6609
+ if (typeof raw !== "object" || raw === null) return false;
6610
+ const state = raw;
6611
+ 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);
6612
+ }
6613
+ function expectedGateForPhase(phaseIndex) {
6614
+ if (phaseIndex === 2) return "post-plan";
6615
+ if (phaseIndex === 3) return "gate-3";
6616
+ if (phaseIndex === 4) return "gate-4";
6617
+ if (phaseIndex === 5) return "final";
6618
+ return null;
6619
+ }
6620
+ function initPhaseState(repoRoot, epicId) {
6621
+ const dir = join(repoRoot, STATE_DIR);
6622
+ mkdirSync(dir, { recursive: true });
6623
+ const state = {
6624
+ lfg_active: true,
6625
+ epic_id: epicId,
6626
+ current_phase: "brainstorm",
6627
+ phase_index: PHASE_INDEX.brainstorm,
6628
+ skills_read: [],
6629
+ gates_passed: [],
6630
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
6631
+ };
6632
+ writeFileSync(getStatePath(repoRoot), JSON.stringify(state, null, 2), "utf-8");
6633
+ return state;
6634
+ }
6635
+ function getPhaseState(repoRoot) {
6636
+ try {
6637
+ const path2 = getStatePath(repoRoot);
6638
+ if (!existsSync(path2)) return null;
6639
+ const raw = readFileSync(path2, "utf-8");
6640
+ const parsed = JSON.parse(raw);
6641
+ if (!validatePhaseState(parsed)) return null;
6642
+ const age = Date.now() - new Date(parsed.started_at).getTime();
6643
+ if (age > PHASE_STATE_MAX_AGE_MS) {
6644
+ cleanPhaseState(repoRoot);
6645
+ return null;
6646
+ }
6647
+ return parsed;
6648
+ } catch {
6649
+ return null;
6650
+ }
6651
+ }
6652
+ function updatePhaseState(repoRoot, partial) {
6653
+ const current = getPhaseState(repoRoot);
6654
+ if (current === null) return null;
6655
+ const updated = {
6656
+ ...current,
6657
+ ...partial
6658
+ };
6659
+ if (!validatePhaseState(updated)) return null;
6660
+ writeFileSync(getStatePath(repoRoot), JSON.stringify(updated, null, 2), "utf-8");
6661
+ return updated;
6662
+ }
6663
+ function startPhase(repoRoot, phase) {
6664
+ return updatePhaseState(repoRoot, {
6665
+ current_phase: phase,
6666
+ phase_index: PHASE_INDEX[phase]
6667
+ });
6668
+ }
6669
+ function cleanPhaseState(repoRoot) {
6670
+ try {
6671
+ const path2 = getStatePath(repoRoot);
6672
+ if (existsSync(path2)) unlinkSync(path2);
6673
+ } catch {
6674
+ }
6675
+ }
6676
+ function recordGatePassed(repoRoot, gate) {
6677
+ const current = getPhaseState(repoRoot);
6678
+ if (current === null) return null;
6679
+ const gatesPassed = current.gates_passed.includes(gate) ? current.gates_passed : [...current.gates_passed, gate];
6680
+ const updated = { ...current, gates_passed: gatesPassed };
6681
+ if (gate === "final") {
6682
+ cleanPhaseState(repoRoot);
6683
+ return updated;
6684
+ }
6685
+ writeFileSync(getStatePath(repoRoot), JSON.stringify(updated, null, 2), "utf-8");
6686
+ return updated;
6687
+ }
6688
+ function printStatusHuman(state) {
6689
+ if (state === null) {
6690
+ console.log("No active LFG session.");
6691
+ return;
6692
+ }
6693
+ console.log("Active LFG Session");
6694
+ console.log(` Epic: ${state.epic_id}`);
6695
+ console.log(` Phase: ${state.current_phase} (${state.phase_index}/5)`);
6696
+ console.log(` Skills read: ${state.skills_read.length === 0 ? "(none)" : state.skills_read.join(", ")}`);
6697
+ console.log(` Gates passed: ${state.gates_passed.length === 0 ? "(none)" : state.gates_passed.join(", ")}`);
6698
+ console.log(` Started: ${state.started_at}`);
6699
+ }
6700
+ function registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot) {
6701
+ phaseCheck.command("init <epic-id>").description("Initialize phase state for an epic").action((epicId) => {
6702
+ if (!EPIC_ID_PATTERN.test(epicId)) {
6703
+ console.error(`Invalid epic ID: "${epicId}"`);
6704
+ process.exitCode = 1;
6705
+ return;
6706
+ }
6707
+ if (getDryRun()) {
6708
+ console.log(`[dry-run] Would initialize phase state for epic ${epicId} in ${repoRoot()}`);
6709
+ return;
6710
+ }
6711
+ initPhaseState(repoRoot(), epicId);
6712
+ console.log(`Phase state initialized for ${epicId}. Current phase: brainstorm (1/5).`);
6713
+ });
6714
+ phaseCheck.command("start <phase>").description("Start or resume a phase").action((phase) => {
6715
+ if (!isPhaseName(phase)) {
6716
+ console.error(`Invalid phase: "${phase}". Valid phases: ${PHASES.join(", ")}`);
6717
+ process.exitCode = 1;
6718
+ return;
6719
+ }
6720
+ if (getDryRun()) {
6721
+ console.log(`[dry-run] Would start phase ${phase}`);
6722
+ return;
6723
+ }
6724
+ const state = startPhase(repoRoot(), phase);
6725
+ if (state === null) {
6726
+ console.error("No active phase state. Run: ca phase-check init <epic-id>");
6727
+ process.exitCode = 1;
6728
+ return;
6729
+ }
6730
+ console.log(`Phase updated: ${state.current_phase} (${state.phase_index}/5).`);
6731
+ });
6732
+ phaseCheck.command("gate <gate-name>").description("Record a phase gate as passed").action((gateName) => {
6733
+ if (!isGateName(gateName)) {
6734
+ console.error(`Invalid gate: "${gateName}". Valid gates: ${GATES.join(", ")}`);
6735
+ process.exitCode = 1;
6736
+ return;
6737
+ }
6738
+ if (getDryRun()) {
6739
+ console.log(`[dry-run] Would record gate ${gateName}`);
6740
+ return;
6741
+ }
6742
+ const state = recordGatePassed(repoRoot(), gateName);
6743
+ if (state === null) {
6744
+ console.error("No active phase state. Run: ca phase-check init <epic-id>");
6745
+ process.exitCode = 1;
6746
+ return;
6747
+ }
6748
+ if (gateName === "final") {
6749
+ console.log("Final gate recorded. Phase state cleaned.");
6750
+ return;
6751
+ }
6752
+ console.log(`Gate recorded: ${gateName}.`);
6753
+ });
6754
+ phaseCheck.command("status").description("Show current phase state").option("--json", "Output raw JSON").action((options) => {
6755
+ const state = getPhaseState(repoRoot());
6756
+ if (options.json) {
6757
+ console.log(JSON.stringify(state ?? { lfg_active: false }));
6758
+ return;
6759
+ }
6760
+ printStatusHuman(state);
6761
+ });
6762
+ phaseCheck.command("clean").description("Remove phase state file").action(() => {
6763
+ if (getDryRun()) {
6764
+ console.log("[dry-run] Would delete phase state file");
6765
+ return;
6766
+ }
6767
+ cleanPhaseState(repoRoot());
6768
+ console.log("Phase state cleaned.");
6769
+ });
6770
+ }
6771
+ function registerPhaseCheckCommand(program2) {
6772
+ const phaseCheck = program2.command("phase-check").description("Manage LFG phase state").option("--dry-run", "Show what would be done without making changes");
6773
+ const getDryRun = () => phaseCheck.opts().dryRun ?? false;
6774
+ const repoRoot = () => getRepoRoot();
6775
+ registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot);
6776
+ program2.command("phase-clean").description("Remove phase state file (alias for `phase-check clean`)").action(() => {
6777
+ cleanPhaseState(repoRoot());
6778
+ console.log("Phase state cleaned.");
6779
+ });
6780
+ }
6903
6781
 
6904
- ## Memory Integration
6905
- - Run \`npx ca search "test optimization"\` before starting
6906
- - After completion, capture findings via \`npx ca learn\`
6907
- - Feed patterns into CCT system for future sessions
6782
+ // src/setup/hooks-phase-guard.ts
6783
+ function processPhaseGuard(repoRoot, toolName, _toolInput) {
6784
+ try {
6785
+ if (toolName !== "Edit" && toolName !== "Write") return {};
6786
+ const state = getPhaseState(repoRoot);
6787
+ if (state === null || !state.lfg_active) return {};
6788
+ const expectedSkillPath = `.claude/skills/compound/${state.current_phase}/SKILL.md`;
6789
+ const skillRead = state.skills_read.includes(expectedSkillPath);
6790
+ if (!skillRead) {
6791
+ return {
6792
+ hookSpecificOutput: {
6793
+ hookEventName: "PreToolUse",
6794
+ 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.`
6795
+ }
6796
+ };
6797
+ }
6798
+ return {};
6799
+ } catch {
6800
+ return {};
6801
+ }
6802
+ }
6908
6803
 
6909
- ## Common Pitfalls
6910
- - Deleting tests without verifying coverage is maintained elsewhere
6911
- - Optimizing for speed at the cost of correctness
6912
- - Settling for partial approval or cutting the Phase 3 review loop short before BOTH reviewers approve with zero reservations
6913
- - Making changes without machine-readable output
6914
- - Not feeding results back into compound-agent memory
6804
+ // src/setup/hooks-read-tracker.ts
6805
+ var SKILL_PATH_PATTERN = /(?:^|\/)\.claude\/skills\/compound\/([^/]+)\/SKILL\.md$/;
6806
+ function normalizePath(path2) {
6807
+ return path2.replaceAll("\\", "/");
6808
+ }
6809
+ function toCanonicalSkillPath(filePath) {
6810
+ const normalized = normalizePath(filePath);
6811
+ const match = SKILL_PATH_PATTERN.exec(normalized);
6812
+ if (!match?.[1]) return null;
6813
+ return `.claude/skills/compound/${match[1]}/SKILL.md`;
6814
+ }
6815
+ function processReadTracker(repoRoot, toolName, toolInput) {
6816
+ try {
6817
+ if (toolName !== "Read") return {};
6818
+ const state = getPhaseState(repoRoot);
6819
+ if (state === null || !state.lfg_active) return {};
6820
+ const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : null;
6821
+ if (filePath === null) return {};
6822
+ const canonicalPath = toCanonicalSkillPath(filePath);
6823
+ if (canonicalPath === null) return {};
6824
+ if (!state.skills_read.includes(canonicalPath)) {
6825
+ updatePhaseState(repoRoot, {
6826
+ skills_read: [...state.skills_read, canonicalPath]
6827
+ });
6828
+ }
6829
+ return {};
6830
+ } catch {
6831
+ return {};
6832
+ }
6833
+ }
6915
6834
 
6916
- ## Quality Criteria
6917
- - All 5 phases completed (analysis, planning, review, execution, verification)
6918
- - Both adversarial reviewers approved with zero reservations after iterative refinement
6919
- - Machine-readable output format used throughout
6920
- - Full test suite passes after changes
6921
- - Coverage not degraded
6922
- - Findings captured in compound-agent memory
6923
- `
6924
- };
6835
+ // src/setup/hooks-stop-audit.ts
6836
+ function hasTransitionEvidence(state) {
6837
+ if (state.phase_index === 5) return true;
6838
+ const nextPhase = PHASES[state.phase_index];
6839
+ if (nextPhase === void 0) return false;
6840
+ const nextSkillPath = `.claude/skills/compound/${nextPhase}/SKILL.md`;
6841
+ return state.skills_read.includes(nextSkillPath);
6842
+ }
6843
+ function processStopAudit(repoRoot, stopHookActive = false) {
6844
+ try {
6845
+ if (stopHookActive) return {};
6846
+ const state = getPhaseState(repoRoot);
6847
+ if (state === null || !state.lfg_active) return {};
6848
+ const expectedGate = expectedGateForPhase(state.phase_index);
6849
+ if (expectedGate === null) return {};
6850
+ if (state.gates_passed.includes(expectedGate)) return {};
6851
+ if (!hasTransitionEvidence(state)) return {};
6852
+ return {
6853
+ continue: false,
6854
+ stopReason: `PHASE GATE NOT VERIFIED: ${state.current_phase} requires gate '${expectedGate}'. Run: npx ca phase-check gate ${expectedGate}`
6855
+ };
6856
+ } catch {
6857
+ return {};
6858
+ }
6859
+ }
6925
6860
 
6926
- // src/setup/primitives.ts
6861
+ // src/setup/hooks.ts
6862
+ var HOOK_FILE_MODE = 493;
6863
+ function hasCompoundAgentHook(content) {
6864
+ return content.includes(HOOK_MARKER);
6865
+ }
6866
+ async function getGitHooksDir(repoRoot) {
6867
+ const gitPath = join(repoRoot, ".git");
6868
+ if (!existsSync(gitPath)) {
6869
+ return null;
6870
+ }
6871
+ let gitDir = gitPath;
6872
+ if (lstatSync(gitPath).isFile()) {
6873
+ const content = readFileSync(gitPath, "utf-8").trim();
6874
+ const match = /^gitdir:\s*(.+)$/.exec(content);
6875
+ if (!match?.[1]) return null;
6876
+ gitDir = resolve(repoRoot, match[1]);
6877
+ }
6878
+ const configPath2 = join(gitDir, "config");
6879
+ if (existsSync(configPath2)) {
6880
+ const config = await readFile(configPath2, "utf-8");
6881
+ const match = /hooksPath\s*=\s*(.+)$/m.exec(config);
6882
+ if (match?.[1]) {
6883
+ const hooksPath = match[1].trim();
6884
+ return hooksPath.startsWith("/") ? hooksPath : join(repoRoot, hooksPath);
6885
+ }
6886
+ }
6887
+ const defaultHooksDir = join(gitDir, "hooks");
6888
+ return existsSync(defaultHooksDir) ? defaultHooksDir : null;
6889
+ }
6890
+ function findFirstTopLevelExitLine(lines) {
6891
+ let insideFunction = 0;
6892
+ let heredocDelimiter = null;
6893
+ for (let i = 0; i < lines.length; i++) {
6894
+ const line = lines[i] ?? "";
6895
+ const trimmed = line.trim();
6896
+ if (heredocDelimiter !== null) {
6897
+ if (trimmed === heredocDelimiter) {
6898
+ heredocDelimiter = null;
6899
+ }
6900
+ continue;
6901
+ }
6902
+ const heredocMatch = /<<-?\s*['"]?(\w+)['"]?/.exec(line);
6903
+ if (heredocMatch?.[1]) {
6904
+ heredocDelimiter = heredocMatch[1];
6905
+ continue;
6906
+ }
6907
+ for (const char of line) {
6908
+ if (char === "{") insideFunction++;
6909
+ if (char === "}") insideFunction = Math.max(0, insideFunction - 1);
6910
+ }
6911
+ if (insideFunction > 0) {
6912
+ continue;
6913
+ }
6914
+ if (/^\s*exit\s+(\d+|\$\w+|\$\?)\s*$/.test(trimmed)) {
6915
+ return i;
6916
+ }
6917
+ }
6918
+ return -1;
6919
+ }
6920
+ async function installPreCommitHook(repoRoot) {
6921
+ const gitHooksDir = await getGitHooksDir(repoRoot);
6922
+ if (!gitHooksDir) {
6923
+ return { status: "not_git_repo" };
6924
+ }
6925
+ await mkdir(gitHooksDir, { recursive: true });
6926
+ const hookPath = join(gitHooksDir, "pre-commit");
6927
+ if (existsSync(hookPath)) {
6928
+ const content = await readFile(hookPath, "utf-8");
6929
+ if (hasCompoundAgentHook(content)) {
6930
+ return { status: "already_installed" };
6931
+ }
6932
+ const lines = content.split("\n");
6933
+ const exitLineIndex = findFirstTopLevelExitLine(lines);
6934
+ let newContent;
6935
+ if (exitLineIndex === -1) {
6936
+ newContent = content.trimEnd() + "\n" + COMPOUND_AGENT_HOOK_BLOCK;
6937
+ } else {
6938
+ const before = lines.slice(0, exitLineIndex);
6939
+ const after = lines.slice(exitLineIndex);
6940
+ newContent = before.join("\n") + COMPOUND_AGENT_HOOK_BLOCK + after.join("\n");
6941
+ }
6942
+ await writeFile(hookPath, newContent, "utf-8");
6943
+ chmodSync(hookPath, HOOK_FILE_MODE);
6944
+ return { status: "appended" };
6945
+ }
6946
+ await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
6947
+ chmodSync(hookPath, HOOK_FILE_MODE);
6948
+ return { status: "installed" };
6949
+ }
6950
+ async function installPostCommitHook(repoRoot) {
6951
+ const gitHooksDir = await getGitHooksDir(repoRoot);
6952
+ if (!gitHooksDir) {
6953
+ return { status: "not_git_repo" };
6954
+ }
6955
+ await mkdir(gitHooksDir, { recursive: true });
6956
+ const hookPath = join(gitHooksDir, "post-commit");
6957
+ if (existsSync(hookPath)) {
6958
+ const content = await readFile(hookPath, "utf-8");
6959
+ if (content.includes(POST_COMMIT_HOOK_MARKER)) {
6960
+ return { status: "already_installed" };
6961
+ }
6962
+ const lines = content.split("\n");
6963
+ const exitLineIndex = findFirstTopLevelExitLine(lines);
6964
+ let newContent;
6965
+ if (exitLineIndex === -1) {
6966
+ newContent = content.trimEnd() + "\n" + COMPOUND_AGENT_POST_COMMIT_BLOCK;
6967
+ } else {
6968
+ const before = lines.slice(0, exitLineIndex);
6969
+ const after = lines.slice(exitLineIndex);
6970
+ newContent = before.join("\n") + COMPOUND_AGENT_POST_COMMIT_BLOCK + after.join("\n");
6971
+ }
6972
+ await writeFile(hookPath, newContent, "utf-8");
6973
+ chmodSync(hookPath, HOOK_FILE_MODE);
6974
+ return { status: "appended" };
6975
+ }
6976
+ await writeFile(hookPath, POST_COMMIT_HOOK_TEMPLATE, "utf-8");
6977
+ chmodSync(hookPath, HOOK_FILE_MODE);
6978
+ return { status: "installed" };
6979
+ }
6980
+ async function readStdin() {
6981
+ const chunks = [];
6982
+ for await (const chunk of process.stdin) {
6983
+ chunks.push(chunk);
6984
+ }
6985
+ return Buffer.concat(chunks).toString("utf-8");
6986
+ }
6987
+ async function runUserPromptHook() {
6988
+ try {
6989
+ const input = await readStdin();
6990
+ const data = JSON.parse(input);
6991
+ if (!data.prompt) {
6992
+ console.log(JSON.stringify({}));
6993
+ return;
6994
+ }
6995
+ const result = processUserPrompt(data.prompt);
6996
+ console.log(JSON.stringify(result));
6997
+ } catch {
6998
+ console.log(JSON.stringify({}));
6999
+ }
7000
+ }
7001
+ async function runPostToolFailureHook() {
7002
+ try {
7003
+ const input = await readStdin();
7004
+ const data = JSON.parse(input);
7005
+ if (!data.tool_name) {
7006
+ console.log(JSON.stringify({}));
7007
+ return;
7008
+ }
7009
+ const stateDir = join(getRepoRoot(), ".claude");
7010
+ const result = processToolFailure(data.tool_name, data.tool_input ?? {}, stateDir);
7011
+ console.log(JSON.stringify(result));
7012
+ } catch {
7013
+ console.log(JSON.stringify({}));
7014
+ }
7015
+ }
7016
+ async function runPostToolSuccessHook() {
7017
+ try {
7018
+ await readStdin();
7019
+ const stateDir = join(getRepoRoot(), ".claude");
7020
+ processToolSuccess(stateDir);
7021
+ console.log(JSON.stringify({}));
7022
+ } catch {
7023
+ console.log(JSON.stringify({}));
7024
+ }
7025
+ }
7026
+ async function runToolHook(processor) {
7027
+ try {
7028
+ const input = await readStdin();
7029
+ const data = JSON.parse(input);
7030
+ if (!data.tool_name) {
7031
+ console.log(JSON.stringify({}));
7032
+ return;
7033
+ }
7034
+ console.log(JSON.stringify(processor(getRepoRoot(), data.tool_name, data.tool_input ?? {})));
7035
+ } catch {
7036
+ console.log(JSON.stringify({}));
7037
+ }
7038
+ }
7039
+ async function runStopAuditHook() {
7040
+ try {
7041
+ const input = await readStdin();
7042
+ const data = JSON.parse(input);
7043
+ console.log(JSON.stringify(processStopAudit(getRepoRoot(), data.stop_hook_active ?? false)));
7044
+ } catch {
7045
+ console.log(JSON.stringify({}));
7046
+ }
7047
+ }
7048
+ function registerHooksCommand(program2) {
7049
+ const hooksCommand = program2.command("hooks").description("Git hooks management");
7050
+ hooksCommand.command("run <hook>").description("Run a hook script (called by git/Claude hooks)").option("--json", "Output as JSON").action(async (hook, options) => {
7051
+ if (hook === "pre-commit") {
7052
+ if (options.json) {
7053
+ console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
7054
+ } else {
7055
+ console.log(PRE_COMMIT_MESSAGE);
7056
+ }
7057
+ } else if (hook === "user-prompt") {
7058
+ await runUserPromptHook();
7059
+ } else if (hook === "post-tool-failure") {
7060
+ await runPostToolFailureHook();
7061
+ } else if (hook === "post-tool-success") {
7062
+ await runPostToolSuccessHook();
7063
+ } else if (hook === "phase-guard") {
7064
+ await runToolHook(processPhaseGuard);
7065
+ } else if (hook === "post-read" || hook === "read-tracker") {
7066
+ await runToolHook(processReadTracker);
7067
+ } else if (hook === "phase-audit" || hook === "stop-audit") {
7068
+ await runStopAuditHook();
7069
+ } else {
7070
+ if (options.json) {
7071
+ console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
7072
+ } else {
7073
+ console.error(
7074
+ formatError(
7075
+ "hooks",
7076
+ "UNKNOWN_HOOK",
7077
+ `Unknown hook: ${hook}`,
7078
+ "Valid hooks: pre-commit, user-prompt, post-tool-failure, post-tool-success, post-read (or read-tracker), phase-guard, phase-audit (or stop-audit)"
7079
+ )
7080
+ );
7081
+ }
7082
+ process.exitCode = 1;
7083
+ }
7084
+ });
7085
+ }
6927
7086
  var GENERATED_MARKER = "<!-- generated by compound-agent -->\n";
6928
7087
  function hasCompoundAgentSection(content) {
6929
7088
  return content.includes(COMPOUND_AGENT_SECTION_HEADER);
@@ -7410,6 +7569,7 @@ async function runSetup(options) {
7410
7569
  postCommitHook = (await installPostCommitHook(repoRoot)).status;
7411
7570
  }
7412
7571
  const { hooks } = await configureClaudeSettings();
7572
+ await installGeminiAdapter({ dryRun: false, json: true });
7413
7573
  const gitignore = await ensureGitignore(repoRoot);
7414
7574
  let modelStatus = "skipped";
7415
7575
  if (!options.skipModel) {
@@ -7499,6 +7659,7 @@ async function runUpdate(repoRoot, dryRun) {
7499
7659
  let configUpdated = false;
7500
7660
  if (!dryRun) {
7501
7661
  const { hooks } = await configureClaudeSettings();
7662
+ await installGeminiAdapter({ dryRun: false, json: true });
7502
7663
  configUpdated = hooks;
7503
7664
  }
7504
7665
  const gitignore = dryRun ? { added: [] } : await ensureGitignore(repoRoot);
@@ -9030,7 +9191,27 @@ function registerVerifyGatesCommand(program2) {
9030
9191
  }
9031
9192
 
9032
9193
  // src/changelog-data.ts
9033
- var CHANGELOG_RECENT = `## [1.4.4] - 2026-02-23
9194
+ var CHANGELOG_RECENT = `## [1.5.0] - 2026-02-24
9195
+
9196
+ ### Added
9197
+
9198
+ - **Gemini CLI compatibility adapter**: \`ca setup gemini\` scaffolds \`.gemini/\` directory with hook scripts, TOML slash commands, and inlined skills -- bridging compound-agent to work with Google's Gemini CLI via the Adapter Pattern
9199
+ - **Gemini hooks**: Maps SessionStart, BeforeAgent, BeforeTool, AfterTool to compound-agent's existing hook pipeline (\`ca prime\`, \`ca hooks run user-prompt\`, \`ca hooks run phase-guard\`, \`ca hooks run post-tool-success\`)
9200
+ - **Gemini TOML commands**: Auto-generates \`.gemini/commands/compound/*.toml\` using \`@{path}\` file injection to maintain a single source of truth with Claude commands
9201
+ - **Gemini skills proxying**: Inlines phase and agent role skill content into \`.gemini/skills/\` with YAML frontmatter
9202
+ - **23 integration tests** for the Gemini adapter covering hooks, settings.json, TOML commands, skills, and dry-run mode
9203
+
9204
+ ### Fixed
9205
+
9206
+ - **Gemini hook stderr leak**: Corrected \`2>&1 > /dev/null\` (leaks stderr to stdout, corrupting JSON) to \`> /dev/null 2>&1\`
9207
+ - **Gemini TOML file injection syntax**: Changed \`@path\` to \`@{path}\` (Gemini CLI requires curly braces)
9208
+ - **Gemini skill file injection**: Skills now inline content instead of using \`@{path}\` which only works in TOML prompt fields, not SKILL.md
9209
+ - **Gemini phase guard always allowing**: Hook now checks \`ca hooks run phase-guard\` exit code and returns structured \`{"decision": "deny"}\` on failure (exit 0, not exit 2, so Gemini parses the reason from stdout)
9210
+ - **Gemini BeforeTool matcher incomplete**: Added \`create_file\` to BeforeTool and AfterTool matchers alongside \`replace\` and \`write_file\`
9211
+ - **TOML description escaping**: \`parseDescription\` now escapes \`\\\` and \`"\` to prevent malformed TOML output
9212
+ - **Flaky embedding test**: Added 15s timeout to \`isModelUsable\` test
9213
+
9214
+ ## [1.4.4] - 2026-02-23
9034
9215
 
9035
9216
  ### Added
9036
9217
 
@@ -9076,15 +9257,7 @@ var CHANGELOG_RECENT = `## [1.4.4] - 2026-02-23
9076
9257
 
9077
9258
  - **SQLite health check in \`ca doctor\`**: New check reports \`[FAIL]\` with fix hint when \`better-sqlite3\` cannot load
9078
9259
  - **SQLite status in \`ca setup --status\`**: Shows "OK" or "not available" alongside other status checks
9079
- - **\`resetSqliteAvailability()\` export**: Allows re-probing SQLite after native module rebuild
9080
-
9081
- ## [1.4.2] - 2026-02-23
9082
-
9083
- ### Fixed
9084
-
9085
- - **Banner audio crash on headless Linux**: Async \`ENOENT\` error from missing \`aplay\` no longer crashes \`ca setup --update\`
9086
- - **PowerShell path injection on Windows**: Temp paths containing apostrophes no longer break or inject commands in \`banner-audio.ts\`
9087
- - **Banner audio test coverage**: Rewrote tests with proper mock isolation (\`vi.spyOn\` + file-scope \`vi.mock\`), covering async ENOENT, sync throw, stop() idempotency, and normal exit cleanup`;
9260
+ - **\`resetSqliteAvailability()\` export**: Allows re-probing SQLite after native module rebuild`;
9088
9261
 
9089
9262
  // src/commands/about.ts
9090
9263
  function registerAboutCommand(program2) {
@@ -9759,21 +9932,27 @@ fi
9759
9932
 
9760
9933
  # parse_json() - extract a value from JSON stdin
9761
9934
  # Uses jq (primary) with python3 fallback
9762
- # Usage: echo '{"status":"open"}' | parse_json '.status'
9935
+ # Auto-unwraps single-element arrays (bd show --json returns [...])
9936
+ # Usage: echo '[{"status":"open"}]' | parse_json '.status'
9763
9937
  parse_json() {
9764
9938
  local filter="$1"
9765
9939
  if [ "$HAS_JQ" = true ]; then
9766
- jq -r "$filter"
9940
+ jq -r "if type == \\"array\\" then .[0] else . end | $filter"
9767
9941
  else
9768
9942
  python3 -c "
9769
9943
  import sys, json
9770
9944
  data = json.load(sys.stdin)
9945
+ if isinstance(data, list):
9946
+ data = data[0] if data else {}
9771
9947
  f = '$filter'.strip('.')
9772
9948
  parts = [p for p in f.split('.') if p]
9773
9949
  v = data
9774
- for p in parts:
9775
- v = v[p]
9776
- print(v)
9950
+ try:
9951
+ for p in parts:
9952
+ v = v[p]
9953
+ except (KeyError, IndexError, TypeError):
9954
+ v = None
9955
+ print('' if v is None else v)
9777
9956
  "
9778
9957
  fi
9779
9958
  }
@@ -10299,6 +10478,7 @@ function registerSetupCommands(program2) {
10299
10478
  const setupCommand = program2.command("setup");
10300
10479
  registerSetupAllCommand(setupCommand);
10301
10480
  registerClaudeSubcommand(setupCommand);
10481
+ registerGeminiSubcommand(setupCommand);
10302
10482
  registerDownloadModelCommand(program2);
10303
10483
  }
10304
10484
  function registerManagementCommands(program2) {