compound-agent 1.6.0 → 1.6.1
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 +26 -0
- package/README.md +5 -5
- package/dist/cli.js +1050 -822
- package/dist/cli.js.map +1 -1
- package/dist/index.js +21 -10
- package/dist/index.js.map +1 -1
- package/docs/research/spec_design/decision_theory_specifications_and_multi_criteria_tradeoffs.md +0 -0
- package/docs/research/spec_design/design_by_contract.md +251 -0
- package/docs/research/spec_design/domain_driven_design_strategic_modeling.md +183 -0
- package/docs/research/spec_design/formal_specification_methods.md +161 -0
- package/docs/research/spec_design/logic_and_proof_theory_under_the_curry_howard_correspondence.md +250 -0
- package/docs/research/spec_design/natural_language_formal_semantics_abuguity_in_specifications.md +259 -0
- package/docs/research/spec_design/requirements_engineering.md +234 -0
- package/docs/research/spec_design/systems_engineering_specifications_emergent_behavior_interface_contracts.md +149 -0
- package/docs/research/spec_design/what_is_this_about.md +305 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createRequire } from 'module';
|
|
|
11
11
|
import { execSync, execFileSync, spawn } from 'child_process';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { Command } from 'commander';
|
|
14
|
-
import
|
|
14
|
+
import chalk5 from 'chalk';
|
|
15
15
|
import { createInterface } from 'readline';
|
|
16
16
|
|
|
17
17
|
var __defProp = Object.defineProperty;
|
|
@@ -2193,7 +2193,10 @@ function acquireEmbedLock(repoRoot) {
|
|
|
2193
2193
|
if (err.code !== "EEXIST") throw err;
|
|
2194
2194
|
const existing = readLock(file);
|
|
2195
2195
|
if (existing && isProcessAlive(existing.pid)) {
|
|
2196
|
-
|
|
2196
|
+
const lockAge = Date.now() - new Date(existing.startedAt).getTime();
|
|
2197
|
+
if (lockAge < LOCK_MAX_AGE_MS) {
|
|
2198
|
+
return { acquired: false, holder: existing.pid };
|
|
2199
|
+
}
|
|
2197
2200
|
}
|
|
2198
2201
|
try {
|
|
2199
2202
|
unlinkSync(file);
|
|
@@ -2221,8 +2224,10 @@ function releaseLock(file) {
|
|
|
2221
2224
|
} catch {
|
|
2222
2225
|
}
|
|
2223
2226
|
}
|
|
2227
|
+
var LOCK_MAX_AGE_MS;
|
|
2224
2228
|
var init_embed_lock = __esm({
|
|
2225
2229
|
"src/memory/knowledge/embed-lock.ts"() {
|
|
2230
|
+
LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
2226
2231
|
}
|
|
2227
2232
|
});
|
|
2228
2233
|
function statusPath(repoRoot) {
|
|
@@ -4395,9 +4400,24 @@ async function ensureGitignore(repoRoot) {
|
|
|
4395
4400
|
if (missing.length === 0) {
|
|
4396
4401
|
return { added: [] };
|
|
4397
4402
|
}
|
|
4398
|
-
|
|
4399
|
-
const
|
|
4400
|
-
|
|
4403
|
+
let newContent;
|
|
4404
|
+
const sectionIdx = lines.findIndex((l) => l.trim() === SECTION_COMMENT);
|
|
4405
|
+
if (sectionIdx >= 0) {
|
|
4406
|
+
let insertAfter = sectionIdx;
|
|
4407
|
+
for (let i = sectionIdx + 1; i < lines.length; i++) {
|
|
4408
|
+
const line = lines[i];
|
|
4409
|
+
if (line === void 0) break;
|
|
4410
|
+
const trimmed = line.trim();
|
|
4411
|
+
if (trimmed === "" || trimmed.startsWith("#")) break;
|
|
4412
|
+
insertAfter = i;
|
|
4413
|
+
}
|
|
4414
|
+
lines.splice(insertAfter + 1, 0, ...missing);
|
|
4415
|
+
newContent = lines.join("\n");
|
|
4416
|
+
} else {
|
|
4417
|
+
const section = [SECTION_COMMENT, ...missing].join("\n");
|
|
4418
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? "\n\n" : content.length > 0 ? "\n" : "";
|
|
4419
|
+
newContent = content + separator + section + "\n";
|
|
4420
|
+
}
|
|
4401
4421
|
await writeFile(gitignorePath, newContent, "utf-8");
|
|
4402
4422
|
return { added: missing };
|
|
4403
4423
|
}
|
|
@@ -4881,7 +4901,7 @@ Analyze the repository to understand its structure, coding conventions, tech sta
|
|
|
4881
4901
|
Return findings directly to the caller for synthesis into the plan.
|
|
4882
4902
|
|
|
4883
4903
|
## Deployment
|
|
4884
|
-
Subagent spawned via the Task tool during the **plan** and **
|
|
4904
|
+
Subagent spawned via the Task tool during the **plan** and **spec-dev** phases. Return findings directly to the caller.
|
|
4885
4905
|
|
|
4886
4906
|
## Output Format
|
|
4887
4907
|
Return a structured summary:
|
|
@@ -4912,7 +4932,7 @@ Search compound-agent memory to find relevant lessons, patterns, and decisions f
|
|
|
4912
4932
|
Return findings directly to the caller for synthesis into the plan.
|
|
4913
4933
|
|
|
4914
4934
|
## Deployment
|
|
4915
|
-
Subagent spawned via the Task tool during the **plan** and **
|
|
4935
|
+
Subagent spawned via the Task tool during the **plan** and **spec-dev** phases. Return findings directly to the caller.
|
|
4916
4936
|
|
|
4917
4937
|
## Output Format
|
|
4918
4938
|
Return a list of relevant memory items:
|
|
@@ -5557,16 +5577,16 @@ var AGENT_ROLE_SKILLS = {
|
|
|
5557
5577
|
|
|
5558
5578
|
// src/setup/templates/commands.ts
|
|
5559
5579
|
var WORKFLOW_COMMANDS = {
|
|
5560
|
-
"
|
|
5561
|
-
name: compound:
|
|
5562
|
-
description:
|
|
5563
|
-
argument-hint: "<goal or
|
|
5580
|
+
"spec-dev.md": `---
|
|
5581
|
+
name: compound:spec-dev
|
|
5582
|
+
description: Develop precise specifications through Socratic dialogue, EARS notation, and Mermaid diagrams
|
|
5583
|
+
argument-hint: "<goal or feature to specify>"
|
|
5564
5584
|
---
|
|
5565
5585
|
$ARGUMENTS
|
|
5566
5586
|
|
|
5567
|
-
#
|
|
5587
|
+
# Spec Dev
|
|
5568
5588
|
|
|
5569
|
-
**MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/
|
|
5589
|
+
**MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/spec-dev/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full workflow you must follow.
|
|
5570
5590
|
`,
|
|
5571
5591
|
"plan.md": `---
|
|
5572
5592
|
name: compound:plan
|
|
@@ -5612,17 +5632,17 @@ $ARGUMENTS
|
|
|
5612
5632
|
|
|
5613
5633
|
**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.
|
|
5614
5634
|
`,
|
|
5615
|
-
"
|
|
5616
|
-
name: compound:
|
|
5635
|
+
"cook-it.md": `---
|
|
5636
|
+
name: compound:cook-it
|
|
5617
5637
|
description: Full workflow cycle chaining all five phases
|
|
5618
5638
|
argument-hint: "<goal>"
|
|
5619
5639
|
disable-model-invocation: true
|
|
5620
5640
|
---
|
|
5621
5641
|
$ARGUMENTS
|
|
5622
5642
|
|
|
5623
|
-
#
|
|
5643
|
+
# Cook It
|
|
5624
5644
|
|
|
5625
|
-
**MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/
|
|
5645
|
+
**MANDATORY FIRST STEP -- NON-NEGOTIABLE**: Use the Read tool to open and read \`.claude/skills/compound/cook-it/SKILL.md\` NOW. Do NOT proceed until you have read the complete skill file. It contains the full orchestration workflow you must follow.
|
|
5626
5646
|
`,
|
|
5627
5647
|
"research.md": `---
|
|
5628
5648
|
name: compound:research
|
|
@@ -5779,7 +5799,7 @@ npx ca doctor
|
|
|
5779
5799
|
settings.json # Claude Code hooks
|
|
5780
5800
|
plugin.json # Plugin manifest
|
|
5781
5801
|
agents/compound/ # Subagent definitions
|
|
5782
|
-
commands/compound/ # Slash commands (
|
|
5802
|
+
commands/compound/ # Slash commands (spec-dev, plan, work, review, compound, cook-it)
|
|
5783
5803
|
skills/compound/ # Phase skills + agent role skills
|
|
5784
5804
|
lessons/
|
|
5785
5805
|
index.jsonl # Memory items (git-tracked source of truth)
|
|
@@ -5800,14 +5820,14 @@ docs/compound/
|
|
|
5800
5820
|
| Search docs knowledge | \`npx ca knowledge "query"\` |
|
|
5801
5821
|
| Check plan against memory | \`npx ca check-plan --plan "description"\` |
|
|
5802
5822
|
| View stats | \`npx ca stats\` |
|
|
5803
|
-
| Run full workflow | \`/compound:
|
|
5823
|
+
| Run full workflow | \`/compound:cook-it <epic-id>\` |
|
|
5804
5824
|
| Health check | \`npx ca doctor\` |
|
|
5805
5825
|
|
|
5806
5826
|
---
|
|
5807
5827
|
|
|
5808
5828
|
## Further reading
|
|
5809
5829
|
|
|
5810
|
-
- [WORKFLOW.md](WORKFLOW.md) -- The 5-phase development workflow and
|
|
5830
|
+
- [WORKFLOW.md](WORKFLOW.md) -- The 5-phase development workflow and cook-it orchestrator
|
|
5811
5831
|
- [CLI_REFERENCE.md](CLI_REFERENCE.md) -- Complete CLI command reference
|
|
5812
5832
|
- [SKILLS.md](SKILLS.md) -- Phase skills and agent role skills
|
|
5813
5833
|
- [INTEGRATION.md](INTEGRATION.md) -- Memory system, hooks, beads, and agent guidance
|
|
@@ -5815,29 +5835,29 @@ docs/compound/
|
|
|
5815
5835
|
"WORKFLOW.md": `---
|
|
5816
5836
|
version: "{{VERSION}}"
|
|
5817
5837
|
last-updated: "{{DATE}}"
|
|
5818
|
-
summary: "The 5-phase compound-agent workflow and
|
|
5838
|
+
summary: "The 5-phase compound-agent workflow and cook-it orchestrator"
|
|
5819
5839
|
---
|
|
5820
5840
|
|
|
5821
5841
|
# Workflow
|
|
5822
5842
|
|
|
5823
|
-
Every feature or epic follows five phases. The \`/compound:
|
|
5843
|
+
Every feature or epic follows five phases. The \`/compound:cook-it\` skill chains them with enforcement gates.
|
|
5824
5844
|
|
|
5825
5845
|
---
|
|
5826
5846
|
|
|
5827
|
-
## Phase 1:
|
|
5847
|
+
## Phase 1: Spec Dev
|
|
5828
5848
|
|
|
5829
|
-
|
|
5849
|
+
Develop precise specifications through Socratic dialogue, EARS notation, and Mermaid diagrams.
|
|
5830
5850
|
|
|
5831
|
-
- Ask "why" before "how"
|
|
5832
|
-
- Search memory for
|
|
5833
|
-
-
|
|
5851
|
+
- Ask "why" before "how" -- understand the real need
|
|
5852
|
+
- Search memory for past features, constraints, decisions
|
|
5853
|
+
- Use EARS notation for clear, testable requirements
|
|
5834
5854
|
- Create a beads epic: \`bd create --title="..." --type=epic\`
|
|
5835
5855
|
|
|
5836
5856
|
## Phase 2: Plan
|
|
5837
5857
|
|
|
5838
5858
|
Decompose work into small, testable tasks with dependencies.
|
|
5839
5859
|
|
|
5840
|
-
- Review
|
|
5860
|
+
- Review spec-dev output
|
|
5841
5861
|
- Create beads tasks: \`bd create --title="..." --type=task\`
|
|
5842
5862
|
- Create Review and Compound blocking tasks (these survive compaction)
|
|
5843
5863
|
|
|
@@ -5870,20 +5890,20 @@ Extract and store lessons learned. This is what makes the system compound.
|
|
|
5870
5890
|
|
|
5871
5891
|
---
|
|
5872
5892
|
|
|
5873
|
-
##
|
|
5893
|
+
## Cook-it orchestrator
|
|
5874
5894
|
|
|
5875
|
-
\`/compound:
|
|
5895
|
+
\`/compound:cook-it\` chains all 5 phases with enforcement gates.
|
|
5876
5896
|
|
|
5877
5897
|
### Invocation
|
|
5878
5898
|
|
|
5879
5899
|
\`\`\`
|
|
5880
|
-
/compound:
|
|
5881
|
-
/compound:
|
|
5900
|
+
/compound:cook-it <epic-id>
|
|
5901
|
+
/compound:cook-it <epic-id> from plan
|
|
5882
5902
|
\`\`\`
|
|
5883
5903
|
|
|
5884
5904
|
### Phase execution protocol
|
|
5885
5905
|
|
|
5886
|
-
For each phase,
|
|
5906
|
+
For each phase, cook-it:
|
|
5887
5907
|
|
|
5888
5908
|
1. Announces progress: \`[Phase N/5] PHASE_NAME\`
|
|
5889
5909
|
2. Initializes state: \`npx ca phase-check start <phase>\`
|
|
@@ -5902,18 +5922,18 @@ For each phase, LFG:
|
|
|
5902
5922
|
| Gate 4 | After Review | \`/implementation-reviewer\` returned APPROVED |
|
|
5903
5923
|
| Final | After Compound | \`npx ca verify-gates <epic-id>\` passes, \`pnpm test\` and \`pnpm lint\` pass |
|
|
5904
5924
|
|
|
5905
|
-
If any gate fails,
|
|
5925
|
+
If any gate fails, cook-it stops. You must fix the issue before proceeding.
|
|
5906
5926
|
|
|
5907
5927
|
### Resumption
|
|
5908
5928
|
|
|
5909
|
-
If interrupted,
|
|
5929
|
+
If interrupted, cook-it can resume:
|
|
5910
5930
|
|
|
5911
5931
|
1. Run \`bd show <epic-id>\` and read the notes for phase state
|
|
5912
5932
|
2. Re-invoke with \`from <phase>\` to skip completed phases
|
|
5913
5933
|
|
|
5914
5934
|
### Phase state tracking
|
|
5915
5935
|
|
|
5916
|
-
|
|
5936
|
+
Cook-it persists state in \`.claude/.ca-phase-state.json\`. Useful commands:
|
|
5917
5937
|
|
|
5918
5938
|
\`\`\`bash
|
|
5919
5939
|
npx ca phase-check status # See current phase state
|
|
@@ -5922,7 +5942,7 @@ npx ca phase-check clean # Reset phase state (escape hatch)
|
|
|
5922
5942
|
|
|
5923
5943
|
### Session close
|
|
5924
5944
|
|
|
5925
|
-
Before saying "done",
|
|
5945
|
+
Before saying "done", cook-it runs this inviolable checklist:
|
|
5926
5946
|
|
|
5927
5947
|
\`\`\`bash
|
|
5928
5948
|
git status
|
|
@@ -6084,21 +6104,21 @@ Skills are instructions that Claude reads before executing each phase. They live
|
|
|
6084
6104
|
|
|
6085
6105
|
## Phase skills
|
|
6086
6106
|
|
|
6087
|
-
### \`/compound:
|
|
6107
|
+
### \`/compound:spec-dev\`
|
|
6088
6108
|
|
|
6089
|
-
**Purpose**:
|
|
6109
|
+
**Purpose**: Develop precise specifications through Socratic dialogue, EARS notation, and Mermaid diagrams.
|
|
6090
6110
|
|
|
6091
6111
|
**When invoked**: At the start of a new feature or epic, before any planning.
|
|
6092
6112
|
|
|
6093
|
-
**What it does**: Spawns research subagents,
|
|
6113
|
+
**What it does**: Guides the user through 4 phases (Explore, Understand, Specify, Hand off) to produce a rigorous spec. Spawns research subagents, uses Mermaid diagrams as thinking tools, detects NL ambiguity, writes EARS-notation requirements, and stores the consolidated spec in the beads epic description.
|
|
6094
6114
|
|
|
6095
6115
|
### \`/compound:plan\`
|
|
6096
6116
|
|
|
6097
6117
|
**Purpose**: Decompose work into small testable tasks with dependencies.
|
|
6098
6118
|
|
|
6099
|
-
**When invoked**: After
|
|
6119
|
+
**When invoked**: After spec-dev, before any implementation.
|
|
6100
6120
|
|
|
6101
|
-
**What it does**: Reviews
|
|
6121
|
+
**What it does**: Reviews spec-dev output, spawns analysts, decomposes into tasks with acceptance criteria, creates beads issues, and creates Review + Compound blocking tasks.
|
|
6102
6122
|
|
|
6103
6123
|
### \`/compound:work\`
|
|
6104
6124
|
|
|
@@ -6124,7 +6144,7 @@ Skills are instructions that Claude reads before executing each phase. They live
|
|
|
6124
6144
|
|
|
6125
6145
|
**What it does**: Spawns an analysis pipeline (context-analyzer, lesson-extractor, pattern-matcher, solution-writer, compounding), applies quality filters, classifies items by type and severity, stores via \`npx ca learn\`, runs \`npx ca verify-gates\`.
|
|
6126
6146
|
|
|
6127
|
-
### \`/compound:
|
|
6147
|
+
### \`/compound:cook-it\`
|
|
6128
6148
|
|
|
6129
6149
|
**Purpose**: Full-cycle orchestrator chaining all five phases.
|
|
6130
6150
|
|
|
@@ -6147,12 +6167,12 @@ Skills are instructions that Claude reads before executing each phase. They live
|
|
|
6147
6167
|
Skills are invoked as Claude Code slash commands:
|
|
6148
6168
|
|
|
6149
6169
|
\`\`\`
|
|
6150
|
-
/compound:
|
|
6170
|
+
/compound:spec-dev # Start spec-dev phase
|
|
6151
6171
|
/compound:plan # Start plan phase
|
|
6152
6172
|
/compound:work # Start work phase
|
|
6153
6173
|
/compound:review # Start review phase
|
|
6154
6174
|
/compound:compound # Start compound phase
|
|
6155
|
-
/compound:
|
|
6175
|
+
/compound:cook-it <epic-id> # Run all phases end-to-end
|
|
6156
6176
|
/compound:research # Spawn research subagent
|
|
6157
6177
|
/compound:test-clean # Clean test artifacts
|
|
6158
6178
|
/compound:get-a-phd <focus> # Deep research for agent knowledge
|
|
@@ -6311,61 +6331,91 @@ Work is not complete until \`git push\` succeeds.
|
|
|
6311
6331
|
|
|
6312
6332
|
// src/setup/templates/skills.ts
|
|
6313
6333
|
var PHASE_SKILLS = {
|
|
6314
|
-
|
|
6315
|
-
name:
|
|
6316
|
-
description:
|
|
6334
|
+
"spec-dev": `---
|
|
6335
|
+
name: Spec Dev
|
|
6336
|
+
description: Develop precise specifications through Socratic dialogue, EARS notation, and Mermaid diagrams
|
|
6317
6337
|
---
|
|
6318
6338
|
|
|
6319
|
-
#
|
|
6339
|
+
# Spec Dev Skill
|
|
6320
6340
|
|
|
6321
6341
|
## Overview
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6342
|
+
Develop unambiguous, testable specifications before implementation. Structured 4-phase process producing EARS-notation requirements, architecture diagrams, and a beads epic.
|
|
6343
|
+
|
|
6344
|
+
Scale formality to risk: skip for trivial (<1h), lightweight (EARS + epic) for small, full 4-phase for medium+. Use \`AskUserQuestion\` early to gauge scope.
|
|
6345
|
+
|
|
6346
|
+
## Methodology: 4-Phase Spec Development
|
|
6347
|
+
|
|
6348
|
+
### Phase 1: Explore
|
|
6349
|
+
**Goal**: Map the problem domain before narrowing.
|
|
6350
|
+
1. Ask "why" before "how" -- understand the real need
|
|
6351
|
+
2. Search memory: \`npx ca search\` for past features, constraints, decisions
|
|
6352
|
+
3. Search knowledge: \`npx ca knowledge "relevant terms"\`
|
|
6353
|
+
4. Spawn subagents for research (\`.claude/agents/compound/repo-analyst.md\`, \`memory-analyst.md\`, or \`subagent_type: Explore\`)
|
|
6354
|
+
5. For deep domain knowledge, consider \`/get-a-phd\`
|
|
6355
|
+
6. Build a discovery mindmap (Mermaid \`mindmap\`) -- makes implicit assumptions visible
|
|
6356
|
+
7. Use \`AskUserQuestion\` to clarify scope and preferences
|
|
6357
|
+
|
|
6358
|
+
**Iteration trigger**: If research reveals the problem is fundamentally different, restart Explore.
|
|
6359
|
+
|
|
6360
|
+
### Phase 2: Understand
|
|
6361
|
+
**Goal**: Crystallize requirements through Socratic dialogue.
|
|
6362
|
+
1. For each capability, ask: triggers? edge cases? constraints? acceptance criteria?
|
|
6363
|
+
2. Use Mermaid diagrams (\`sequenceDiagram\`, \`stateDiagram-v2\`) to expose hidden structure
|
|
6364
|
+
3. Detect ambiguities: vague adjectives, unclear pronouns, passive voice, compound requirements. See \`references/spec-guide.md\` for full checklist
|
|
6365
|
+
4. Build a domain glossary for ambiguous terms
|
|
6366
|
+
5. Use \`AskUserQuestion\` to resolve each ambiguity
|
|
6367
|
+
|
|
6368
|
+
**Iteration trigger**: If specifying reveals missing knowledge, loop back to Explore.
|
|
6369
|
+
|
|
6370
|
+
### Phase 3: Specify
|
|
6371
|
+
**Goal**: Produce formal, testable requirements.
|
|
6372
|
+
1. Write each requirement using **EARS notation**:
|
|
6373
|
+
- Ubiquitous: \`The system shall <action>.\`
|
|
6374
|
+
- Event-driven: \`When <trigger>, the system shall <action>.\`
|
|
6375
|
+
- State-driven: \`While <state>, the system shall <action>.\`
|
|
6376
|
+
- Unwanted behavior: \`If <condition>, then the system shall <action>.\`
|
|
6377
|
+
- Optional: \`Where <feature>, the system shall <action>.\`
|
|
6378
|
+
- Combined ordering: \`Where > While > When > If/then > shall\`
|
|
6379
|
+
2. Verify each requirement: no vague adjectives, edge cases covered, quantities specified, testable
|
|
6380
|
+
3. Document trade-offs when requirements conflict (see \`references/spec-guide.md\`)
|
|
6381
|
+
4. Produce architecture diagrams (\`erDiagram\`, \`C4Context\`, \`flowchart\`)
|
|
6382
|
+
5. Create ADRs in \`docs/decisions/\` for significant decisions
|
|
6383
|
+
|
|
6384
|
+
**Iteration trigger**: If contradictions or gaps emerge, loop back to Understand.
|
|
6385
|
+
|
|
6386
|
+
### Phase 4: Hand off
|
|
6387
|
+
1. Store spec in beads epic description (\`bd update <epic> --description="..."\`) -- single source of truth
|
|
6388
|
+
2. Create beads epic if needed (\`bd create\`)
|
|
6389
|
+
3. Flag open questions for plan phase
|
|
6390
|
+
4. Capture lessons: \`npx ca learn\`
|
|
6338
6391
|
|
|
6339
6392
|
## Memory Integration
|
|
6340
|
-
-
|
|
6341
|
-
-
|
|
6342
|
-
-
|
|
6393
|
+
- \`npx ca search\` before generating approaches
|
|
6394
|
+
- \`npx ca knowledge\` for indexed project docs
|
|
6395
|
+
- \`npx ca learn\` after corrections or discoveries
|
|
6343
6396
|
|
|
6344
|
-
##
|
|
6345
|
-
|
|
6346
|
-
- Review existing ADRs in \`docs/decisions/\` -- prior decisions may constrain the brainstorm
|
|
6347
|
-
- Auto-create ADR for each significant decision made during convergence
|
|
6397
|
+
## Reference Material
|
|
6398
|
+
Read \`.claude/skills/compound/spec-dev/references/spec-guide.md\` on demand for EARS patterns, Mermaid templates, ambiguity checklists, and trade-off frameworks.
|
|
6348
6399
|
|
|
6349
6400
|
## Common Pitfalls
|
|
6350
|
-
- Jumping to
|
|
6351
|
-
-
|
|
6352
|
-
-
|
|
6353
|
-
- Not
|
|
6354
|
-
- Over-
|
|
6355
|
-
-
|
|
6356
|
-
- Not
|
|
6357
|
-
-
|
|
6401
|
+
- Jumping to solutions before exploring the problem
|
|
6402
|
+
- Skipping diagrams -- they reveal hidden assumptions
|
|
6403
|
+
- Vague requirements without EARS patterns
|
|
6404
|
+
- Not searching memory for past patterns and pitfalls
|
|
6405
|
+
- Over-specifying trivial tasks
|
|
6406
|
+
- Ignoring iteration signals when gaps emerge
|
|
6407
|
+
- Not creating the beads epic
|
|
6408
|
+
- Specifying implementation instead of requirements
|
|
6358
6409
|
|
|
6359
6410
|
## Quality Criteria
|
|
6360
|
-
-
|
|
6361
|
-
-
|
|
6362
|
-
-
|
|
6363
|
-
-
|
|
6364
|
-
-
|
|
6365
|
-
-
|
|
6366
|
-
-
|
|
6367
|
-
-
|
|
6368
|
-
- A beads epic was created from conclusions via \`bd create\`
|
|
6411
|
+
- [ ] Requirements use EARS notation
|
|
6412
|
+
- [ ] Ambiguities detected and resolved via dialogue
|
|
6413
|
+
- [ ] Mermaid diagrams used as thinking tools
|
|
6414
|
+
- [ ] Memory searched (\`npx ca search\`)
|
|
6415
|
+
- [ ] Trade-offs documented with rationale
|
|
6416
|
+
- [ ] User engaged via \`AskUserQuestion\` at decisions
|
|
6417
|
+
- [ ] Spec stored in beads epic description
|
|
6418
|
+
- [ ] ADRs created for significant decisions
|
|
6369
6419
|
`,
|
|
6370
6420
|
plan: `---
|
|
6371
6421
|
name: Plan
|
|
@@ -6378,7 +6428,7 @@ description: Decompose work into small testable tasks with clear dependencies
|
|
|
6378
6428
|
Create a concrete implementation plan by decomposing work into small, testable tasks with dependencies and acceptance criteria.
|
|
6379
6429
|
|
|
6380
6430
|
## Methodology
|
|
6381
|
-
1.
|
|
6431
|
+
1. Read the spec from the epic description (\`bd show <epic>\`) for EARS requirements, decisions, and open questions
|
|
6382
6432
|
2. Search memory with \`npx ca search\` and docs with \`npx ca knowledge "relevant topic"\` for architectural patterns and past mistakes
|
|
6383
6433
|
3. Spawn **subagents** via Task tool in parallel for research (lightweight, no inter-agent coordination):
|
|
6384
6434
|
- Available agents: \`.claude/agents/compound/repo-analyst.md\`, \`memory-analyst.md\`
|
|
@@ -6389,9 +6439,10 @@ Create a concrete implementation plan by decomposing work into small, testable t
|
|
|
6389
6439
|
6. Use \`AskUserQuestion\` to resolve ambiguities, conflicting constraints, or priority trade-offs before decomposing
|
|
6390
6440
|
7. Decompose into tasks small enough to verify individually
|
|
6391
6441
|
8. Define acceptance criteria for each task
|
|
6392
|
-
9.
|
|
6393
|
-
10.
|
|
6394
|
-
11. Create
|
|
6442
|
+
9. Ensure each task traces back to a spec requirement for traceability
|
|
6443
|
+
10. Map dependencies between tasks
|
|
6444
|
+
11. Create beads issues: \`bd create --title="..." --type=task\`
|
|
6445
|
+
12. Create review and compound blocking tasks (\`bd create\` + \`bd dep add\`) that depend on work tasks \u2014 these survive compaction and surface via \`bd ready\` after work completes
|
|
6395
6446
|
|
|
6396
6447
|
## Memory Integration
|
|
6397
6448
|
- Run \`npx ca search\` and \`npx ca knowledge "relevant topic"\` for patterns related to the feature area
|
|
@@ -6420,6 +6471,7 @@ Create a concrete implementation plan by decomposing work into small, testable t
|
|
|
6420
6471
|
- Existing docs and ADRs were checked for constraints
|
|
6421
6472
|
- Ambiguities resolved via \`AskUserQuestion\` before decomposing
|
|
6422
6473
|
- Complexity estimates are realistic (no "should be quick")
|
|
6474
|
+
- Each task traces back to a spec requirement
|
|
6423
6475
|
|
|
6424
6476
|
## POST-PLAN VERIFICATION -- MANDATORY
|
|
6425
6477
|
After creating all tasks, verify review and compound tasks exist:
|
|
@@ -6439,18 +6491,20 @@ Execute implementation through an AgentTeam using adaptive TDD. The lead coordin
|
|
|
6439
6491
|
## Methodology
|
|
6440
6492
|
1. Pick tasks from \`bd ready\` or \`$ARGUMENTS\`
|
|
6441
6493
|
2. Mark tasks in progress: \`bd update <id> --status=in_progress\`
|
|
6442
|
-
3.
|
|
6443
|
-
4.
|
|
6444
|
-
5.
|
|
6494
|
+
3. Read the epic description (\`bd show <epic>\`) for spec context -- EARS requirements guide what "done" looks like
|
|
6495
|
+
4. Run \`npx ca search\` per agent/subtask for targeted context. Display results.
|
|
6496
|
+
5. Assess parallelization: identify independent tasks that can be worked simultaneously
|
|
6497
|
+
6. Deploy an **AgentTeam** (TeamCreate + Task with \`team_name\`) with MULTIPLE test-writers and implementers:
|
|
6445
6498
|
- Role skills: \`.claude/skills/compound/agents/{test-writer,implementer}/SKILL.md\`
|
|
6446
6499
|
- Scale teammate count to independent tasks; pairs coordinate via SendMessage on shared interfaces
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6500
|
+
7. Agents communicate via SendMessage when working on overlapping areas.
|
|
6501
|
+
8. Lead coordinates: review agent outputs, resolve conflicts, verify tests pass. Do not write code directly.
|
|
6502
|
+
9. If implementation diverges from spec requirements, stop and discuss with user via AskUserQuestion before proceeding.
|
|
6503
|
+
10. If blocked, use AskUserQuestion to get user direction.
|
|
6504
|
+
11. Shut down the team when done: send shutdown_request to all teammates.
|
|
6505
|
+
12. Commit incrementally as tests pass.
|
|
6506
|
+
13. Run full test suite for regressions.
|
|
6507
|
+
14. Close tasks: \`bd close <id>\`
|
|
6454
6508
|
|
|
6455
6509
|
## Memory Integration
|
|
6456
6510
|
- Run \`npx ca search\` per delegated subtask with the subtask's specific description
|
|
@@ -6477,6 +6531,12 @@ for complex changes. For all changes, \`/implementation-reviewer\` is the minimu
|
|
|
6477
6531
|
- **Subagent spawning within teammates**: each teammate should spawn opus subagents for independent subtasks (e.g., a test-writer spawning subagents to write tests for multiple modules in parallel)
|
|
6478
6532
|
- **Coordinate on shared interfaces**: teammates working on overlapping APIs must communicate via SendMessage before implementing
|
|
6479
6533
|
|
|
6534
|
+
## Literature
|
|
6535
|
+
- Consult \`docs/compound/research/tdd/\` for TDD methodology, test-first development evidence, and best practices
|
|
6536
|
+
- Consult \`docs/compound/research/property-testing/\` for property-based testing theory and invariant design
|
|
6537
|
+
- Run \`npx ca knowledge "TDD test-first"\` for indexed knowledge on testing methodology
|
|
6538
|
+
- Run \`npx ca search "testing"\` for lessons from past TDD cycles
|
|
6539
|
+
|
|
6480
6540
|
## Common Pitfalls
|
|
6481
6541
|
- Lead writing code instead of delegating to agents
|
|
6482
6542
|
- Not injecting memory context into agent prompts
|
|
@@ -6490,6 +6550,7 @@ for complex changes. For all changes, \`/implementation-reviewer\` is the minimu
|
|
|
6490
6550
|
- Incremental commits made as tests pass
|
|
6491
6551
|
- All tests pass after refactoring
|
|
6492
6552
|
- Task lifecycle tracked via beads (\`bd\`)
|
|
6553
|
+
- Implementation aligns with spec requirements from epic
|
|
6493
6554
|
|
|
6494
6555
|
## PHASE GATE 3 -- MANDATORY
|
|
6495
6556
|
Before starting Review, verify ALL work tasks are closed:
|
|
@@ -6509,23 +6570,25 @@ Perform thorough code review by spawning specialized reviewers in parallel, cons
|
|
|
6509
6570
|
|
|
6510
6571
|
## Methodology
|
|
6511
6572
|
1. Run quality gates first: \`pnpm test && pnpm lint\`
|
|
6512
|
-
2.
|
|
6513
|
-
3.
|
|
6573
|
+
2. Read the epic description (\`bd show <epic>\`) for EARS requirements -- reviewers verify each requirement is met
|
|
6574
|
+
3. Search memory with \`npx ca search\` for known patterns and recurring issues
|
|
6575
|
+
4. Select reviewer tier based on diff size:
|
|
6514
6576
|
- **Small** (<100 lines): 4 core -- security, test-coverage, simplicity, cct-reviewer
|
|
6515
6577
|
- **Medium** (100-500): add architecture, performance, edge-case (7 total)
|
|
6516
6578
|
- **Large** (500+): all 11 reviewers including docs, consistency, error-handling, pattern-matcher
|
|
6517
|
-
|
|
6579
|
+
5. Spawn reviewers in an **AgentTeam** (TeamCreate + Task with \`team_name\`):
|
|
6518
6580
|
- Role skills: \`.claude/skills/compound/agents/{security-reviewer,architecture-reviewer,performance-reviewer,test-coverage-reviewer,simplicity-reviewer}/SKILL.md\`
|
|
6519
6581
|
- Security specialist skills (on-demand, spawned by security-reviewer): \`.claude/skills/compound/agents/{security-injection,security-secrets,security-auth,security-data,security-deps}/SKILL.md\`
|
|
6520
6582
|
- For large diffs (500+), deploy MULTIPLE instances; split files across instances, coordinate via SendMessage
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6583
|
+
6. Reviewers communicate findings to each other via \`SendMessage\`
|
|
6584
|
+
7. Collect, consolidate, and deduplicate all findings
|
|
6585
|
+
8. Classify by severity: P0 (blocks merge), P1 (critical/blocking), P2 (important), P3 (minor)
|
|
6586
|
+
9. Use \`AskUserQuestion\` when severity is ambiguous or fix has multiple valid options
|
|
6587
|
+
10. Create beads issues for P1 findings: \`bd create --title="P1: ..."\`
|
|
6588
|
+
11. Verify spec alignment: flag unmet EARS requirements as P1, flag requirements met but missing from acceptance criteria as gaps
|
|
6589
|
+
12. Fix all P1 findings before proceeding
|
|
6590
|
+
13. Run \`/implementation-reviewer\` as mandatory gate
|
|
6591
|
+
14. Capture novel findings with \`npx ca learn\`; pattern-matcher auto-reinforces recurring issues
|
|
6529
6592
|
|
|
6530
6593
|
## Memory Integration
|
|
6531
6594
|
- Run \`npx ca search\` before review for known recurring issues
|
|
@@ -6537,6 +6600,11 @@ Perform thorough code review by spawning specialized reviewers in parallel, cons
|
|
|
6537
6600
|
- **docs-reviewer** checks code/docs alignment and ADR compliance
|
|
6538
6601
|
- Flags undocumented public APIs and ADR violations
|
|
6539
6602
|
|
|
6603
|
+
## Literature
|
|
6604
|
+
- Consult \`docs/compound/research/code-review/\` for systematic review methodology, severity taxonomies, and evidence-based review practices
|
|
6605
|
+
- Run \`npx ca knowledge "code review methodology"\` for indexed knowledge on review techniques
|
|
6606
|
+
- Run \`npx ca search "review"\` for lessons from past review cycles
|
|
6607
|
+
|
|
6540
6608
|
## Common Pitfalls
|
|
6541
6609
|
- Ignoring reviewer feedback because "it works"
|
|
6542
6610
|
- Not running all 11 reviewer perspectives (skipping dimensions)
|
|
@@ -6556,6 +6624,7 @@ Perform thorough code review by spawning specialized reviewers in parallel, cons
|
|
|
6556
6624
|
- security-reviewer P0 findings: none (blocks merge)
|
|
6557
6625
|
- security-reviewer P1 findings: all acknowledged or resolved
|
|
6558
6626
|
- All P1 findings fixed before \`/implementation-reviewer\` approval
|
|
6627
|
+
- All spec requirements verified against implementation
|
|
6559
6628
|
- \`/implementation-reviewer\` approved as mandatory gate
|
|
6560
6629
|
|
|
6561
6630
|
## PHASE GATE 4 -- MANDATORY
|
|
@@ -6580,26 +6649,32 @@ Lessons go to \`.claude/lessons/index.jsonl\` through the CLI. MEMORY.md is a di
|
|
|
6580
6649
|
|
|
6581
6650
|
## Methodology
|
|
6582
6651
|
1. Review what happened during this cycle (git diff, test results, plan context)
|
|
6583
|
-
2.
|
|
6652
|
+
2. Detect spec drift: compare final implementation against original EARS requirements in the epic description (\`bd show <epic>\`). Note any divergences -- what changed, why, was it justified. If drift reveals a spec was wrong or incomplete, flag that for lesson extraction.
|
|
6653
|
+
3. Spawn the analysis pipeline in an **AgentTeam** (TeamCreate + Task with \`team_name\`):
|
|
6584
6654
|
- Role skills: \`.claude/skills/compound/agents/{context-analyzer,lesson-extractor,pattern-matcher,solution-writer,compounding}/SKILL.md\`
|
|
6585
6655
|
- For large diffs, deploy MULTIPLE context-analyzers and lesson-extractors
|
|
6586
6656
|
- Pipeline: context-analyzers -> lesson-extractors -> pattern-matcher + solution-writer -> compounding
|
|
6587
6657
|
- Agents coordinate via SendMessage throughout the pipeline
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6658
|
+
4. Agents pass results through the pipeline via \`SendMessage\`. The lead coordinates: context-analyzer and lesson-extractor feed pattern-matcher and solution-writer, which feed compounding.
|
|
6659
|
+
5. Apply quality filters: novelty check (>0.98 cosine similarity = skip), specificity check
|
|
6660
|
+
6. Classify each item by type: lesson, solution, pattern, or preference
|
|
6661
|
+
7. Classify severity: high (data loss/security/contradictions), medium (workflow/patterns), low (style/optimizations)
|
|
6662
|
+
8. Store via \`npx ca learn\` with supersedes/related links where applicable.
|
|
6593
6663
|
At minimum, capture 1 lesson per significant decision made during this cycle
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6664
|
+
9. Delegate to the \`compounding\` subagent to run synthesis: cluster accumulated lessons by similarity and write CCT patterns to \`.claude/lessons/cct-patterns.jsonl\`
|
|
6665
|
+
10. Update outdated docs and deprecate superseded ADRs (set status to \`deprecated\`)
|
|
6666
|
+
11. Use \`AskUserQuestion\` to confirm high-severity items with the user before storing; medium/low items are auto-stored
|
|
6597
6667
|
|
|
6598
6668
|
## Docs Integration
|
|
6599
6669
|
- docs-reviewer checks if \`docs/\` content is outdated after the cycle
|
|
6600
6670
|
- Check \`docs/decisions/\` for ADRs contradicted by the work done
|
|
6601
6671
|
- Set ADR status to \`deprecated\` if a decision was reversed, referencing the new ADR
|
|
6602
6672
|
|
|
6673
|
+
## Literature
|
|
6674
|
+
- Consult \`docs/compound/research/learning-systems/\` for knowledge compounding theory, spaced repetition, and lesson extraction methodology
|
|
6675
|
+
- Run \`npx ca knowledge "knowledge compounding"\` for indexed knowledge on learning systems
|
|
6676
|
+
- Run \`npx ca search "compound"\` for lessons from past compounding cycles
|
|
6677
|
+
|
|
6603
6678
|
## Common Pitfalls
|
|
6604
6679
|
- Not spawning the analysis team (analyzing solo misses cross-cutting patterns)
|
|
6605
6680
|
- Capturing without checking for duplicates via \`npx ca search\`
|
|
@@ -6619,6 +6694,7 @@ Lessons go to \`.claude/lessons/index.jsonl\` through the CLI. MEMORY.md is a di
|
|
|
6619
6694
|
- User confirmed high-severity items
|
|
6620
6695
|
- Beads checked for related issues (\`bd\`)
|
|
6621
6696
|
- Each item gives clear, concrete guidance for future sessions
|
|
6697
|
+
- Spec drift analyzed and captured
|
|
6622
6698
|
|
|
6623
6699
|
## FINAL GATE -- EPIC CLOSURE
|
|
6624
6700
|
Before closing the epic:
|
|
@@ -6657,7 +6733,7 @@ Conduct deep research on a topic and produce a structured survey document follow
|
|
|
6657
6733
|
- References (full citations)
|
|
6658
6734
|
- Practitioner Resources (annotated tools/repos)
|
|
6659
6735
|
6. Store output at \`docs/compound/research/<topic-slug>.md\` (kebab-case filename)
|
|
6660
|
-
7. Report key findings back for upstream skill (
|
|
6736
|
+
7. Report key findings back for upstream skill (spec-dev/plan) to act on
|
|
6661
6737
|
|
|
6662
6738
|
## Memory Integration
|
|
6663
6739
|
- Run \`npx ca search\` with topic keywords before starting research
|
|
@@ -6734,19 +6810,30 @@ Annotated tools, repos, articles grouped by category.
|
|
|
6734
6810
|
- Practitioner resources annotated
|
|
6735
6811
|
- No recommendations -- landscape presentation only
|
|
6736
6812
|
`,
|
|
6737
|
-
|
|
6738
|
-
name:
|
|
6813
|
+
"cook-it": `---
|
|
6814
|
+
name: Cook It
|
|
6739
6815
|
description: Full-cycle orchestrator chaining all five phases with gates and controls
|
|
6740
6816
|
---
|
|
6741
6817
|
|
|
6742
|
-
#
|
|
6818
|
+
# Cook It Skill
|
|
6743
6819
|
|
|
6744
6820
|
## Overview
|
|
6745
|
-
|
|
6821
|
+
|
|
6822
|
+
\`\`\`
|
|
6823
|
+
|
|
6824
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
6825
|
+
\u2502\u2592\u2592\u2592\u2592\u2592\u2502
|
|
6826
|
+
\u2590\u259B\u2588\u2588\u2588\u259C\u258C o
|
|
6827
|
+
\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598|
|
|
6828
|
+
\u2598\u2598 \u259D\u259D
|
|
6829
|
+
Let's cook!
|
|
6830
|
+
\`\`\`
|
|
6831
|
+
|
|
6832
|
+
Chain all 5 phases end-to-end: Spec Dev, Plan, Work, Review, Compound. This skill governs the orchestration -- phase sequencing, gates, progress tracking, and error recovery.
|
|
6746
6833
|
|
|
6747
6834
|
## CRITICAL RULE -- READ BEFORE EXECUTE
|
|
6748
6835
|
Before starting EACH phase, you MUST use the Read tool to open its skill file:
|
|
6749
|
-
- .claude/skills/compound/
|
|
6836
|
+
- .claude/skills/compound/spec-dev/SKILL.md
|
|
6750
6837
|
- .claude/skills/compound/plan/SKILL.md
|
|
6751
6838
|
- .claude/skills/compound/work/SKILL.md
|
|
6752
6839
|
- .claude/skills/compound/review/SKILL.md
|
|
@@ -6780,7 +6867,7 @@ If a gate fails, DO NOT proceed. Fix the issue first.
|
|
|
6780
6867
|
- **Progress**: Always announce current phase number before starting.
|
|
6781
6868
|
|
|
6782
6869
|
## Stop Conditions
|
|
6783
|
-
-
|
|
6870
|
+
- Spec dev reveals goal is unclear -- stop, ask user
|
|
6784
6871
|
- Tests produce unresolvable failures -- stop, report
|
|
6785
6872
|
- Review finds critical security issues -- stop, report
|
|
6786
6873
|
|
|
@@ -6881,6 +6968,103 @@ Apply the agreed changes:
|
|
|
6881
6968
|
- Findings captured in compound-agent memory
|
|
6882
6969
|
`
|
|
6883
6970
|
};
|
|
6971
|
+
var PHASE_SKILL_REFERENCES = {
|
|
6972
|
+
"spec-dev/references/spec-guide.md": `# Spec Dev Quick Reference
|
|
6973
|
+
|
|
6974
|
+
## EARS Notation Patterns
|
|
6975
|
+
|
|
6976
|
+
EARS (Easy Approach to Requirements Syntax) provides five sentence templates:
|
|
6977
|
+
|
|
6978
|
+
| Pattern | Template | Example |
|
|
6979
|
+
|---------|----------|---------|
|
|
6980
|
+
| **Ubiquitous** | The system shall \`<action>\`. | The system shall validate all inputs. |
|
|
6981
|
+
| **Event-driven** | When \`<trigger>\`, the system shall \`<action>\`. | When the user submits the form, the system shall validate all fields. |
|
|
6982
|
+
| **State-driven** | While \`<state>\`, the system shall \`<action>\`. | While the system is in maintenance mode, the system shall reject new connections. |
|
|
6983
|
+
| **Unwanted behavior** | If \`<condition>\`, then the system shall \`<action>\`. | If the database connection fails, then the system shall retry with exponential backoff. |
|
|
6984
|
+
| **Optional** | Where \`<feature>\`, the system shall \`<action>\`. | Where SSO is enabled, the system shall redirect to the identity provider. |
|
|
6985
|
+
|
|
6986
|
+
**Combined ordering**: Where > While > When > If/then > shall
|
|
6987
|
+
|
|
6988
|
+
### Quality Checks for Each Requirement
|
|
6989
|
+
- [ ] Uses one of the five EARS patterns
|
|
6990
|
+
- [ ] No vague adjectives (fast, efficient, user-friendly, easy)
|
|
6991
|
+
- [ ] Quantities specified where applicable (timeouts, limits, thresholds)
|
|
6992
|
+
- [ ] Edge cases addressed (empty input, max values, concurrent access)
|
|
6993
|
+
- [ ] Testable \u2014 can write a pass/fail test against it
|
|
6994
|
+
- [ ] Single responsibility \u2014 one requirement per sentence
|
|
6995
|
+
|
|
6996
|
+
---
|
|
6997
|
+
|
|
6998
|
+
## Mermaid Diagram Selection Guide
|
|
6999
|
+
|
|
7000
|
+
| Diagram Type | Use When | Syntax |
|
|
7001
|
+
|-------------|----------|--------|
|
|
7002
|
+
| \`mindmap\` | Exploring problem domain, brainstorming | Phase 1 (Explore) |
|
|
7003
|
+
| \`sequenceDiagram\` | Showing interactions between components | Phase 2 (Understand) |
|
|
7004
|
+
| \`stateDiagram-v2\` | Modeling lifecycle, state transitions | Phase 2 (Understand) |
|
|
7005
|
+
| \`flowchart\` | Showing decision logic, data flow | Phase 3 (Specify) |
|
|
7006
|
+
| \`erDiagram\` | Defining data models, relationships | Phase 3 (Specify) |
|
|
7007
|
+
| \`C4Context\` | System-level architecture boundaries | Phase 3 (Specify) |
|
|
7008
|
+
|
|
7009
|
+
### When to Use Diagrams
|
|
7010
|
+
- **Always** use a mindmap in Explore to surface hidden assumptions
|
|
7011
|
+
- **Use sequence diagrams** when 2+ components interact
|
|
7012
|
+
- **Use state diagrams** when an entity has a lifecycle
|
|
7013
|
+
- **Skip diagrams** only for truly trivial features (<1h of work)
|
|
7014
|
+
|
|
7015
|
+
---
|
|
7016
|
+
|
|
7017
|
+
## NL Ambiguity Detection Checklist
|
|
7018
|
+
|
|
7019
|
+
Watch for these ambiguity patterns in requirements:
|
|
7020
|
+
|
|
7021
|
+
| Pattern | Example | Fix |
|
|
7022
|
+
|---------|---------|-----|
|
|
7023
|
+
| **Vague adjectives** | "fast response" | Specify: "response within 200ms" |
|
|
7024
|
+
| **Unclear pronouns** | "it should update" | Name the subject: "the cache should update" |
|
|
7025
|
+
| **Passive voice** | "data is validated" | Active: "the API validates data" |
|
|
7026
|
+
| **Compound requirements** | "shall validate and log and notify" | Split into 3 separate requirements |
|
|
7027
|
+
| **Unbounded lists** | "supports CSV, JSON, etc." | Enumerate all formats explicitly |
|
|
7028
|
+
| **Missing quantities** | "handles large files" | Specify: "handles files up to 500MB" |
|
|
7029
|
+
| **Implicit assumptions** | "users can access" | Specify: "authenticated users with role X can access" |
|
|
7030
|
+
| **Temporal ambiguity** | "after processing" | Specify: "within 5s of processing completion" |
|
|
7031
|
+
|
|
7032
|
+
---
|
|
7033
|
+
|
|
7034
|
+
## Trade-off Documentation Framework
|
|
7035
|
+
|
|
7036
|
+
When requirements conflict, document the trade-off:
|
|
7037
|
+
|
|
7038
|
+
### Template
|
|
7039
|
+
\`\`\`
|
|
7040
|
+
### Trade-off: [Short Title]
|
|
7041
|
+
|
|
7042
|
+
**Tension**: [Requirement A] conflicts with [Requirement B].
|
|
7043
|
+
|
|
7044
|
+
**Options**:
|
|
7045
|
+
1. [Option 1]: [Description]. Pro: [benefit]. Con: [cost].
|
|
7046
|
+
2. [Option 2]: [Description]. Pro: [benefit]. Con: [cost].
|
|
7047
|
+
|
|
7048
|
+
**Decision**: [Chosen option] because [rationale].
|
|
7049
|
+
|
|
7050
|
+
**Consequence**: [What this means for implementation].
|
|
7051
|
+
\`\`\`
|
|
7052
|
+
|
|
7053
|
+
### Common Trade-off Dimensions
|
|
7054
|
+
- **Performance vs. Safety**: Validation adds latency
|
|
7055
|
+
- **Flexibility vs. Simplicity**: Configuration adds complexity
|
|
7056
|
+
- **Consistency vs. Availability**: Strict consistency limits throughput
|
|
7057
|
+
- **Security vs. Usability**: Auth steps add friction
|
|
7058
|
+
- **Completeness vs. Time-to-market**: More features delay delivery
|
|
7059
|
+
|
|
7060
|
+
### Decision Criteria
|
|
7061
|
+
When evaluating trade-offs, consider:
|
|
7062
|
+
1. **Reversibility**: Can we change this later? Prefer reversible decisions.
|
|
7063
|
+
2. **Blast radius**: How many components does this affect?
|
|
7064
|
+
3. **Evidence**: What data supports each option?
|
|
7065
|
+
4. **Alignment**: Which option best serves the stated goal?
|
|
7066
|
+
`
|
|
7067
|
+
};
|
|
6884
7068
|
|
|
6885
7069
|
// src/setup/gemini.ts
|
|
6886
7070
|
var HOOKS = {
|
|
@@ -6942,6 +7126,9 @@ function parseDescription(content, fallback) {
|
|
|
6942
7126
|
const raw = content.match(/description:\s*(.*)/)?.[1] ?? fallback;
|
|
6943
7127
|
return raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
6944
7128
|
}
|
|
7129
|
+
function stripFrontmatter(content) {
|
|
7130
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, "");
|
|
7131
|
+
}
|
|
6945
7132
|
async function writeSettings(geminiDir) {
|
|
6946
7133
|
const settingsPath = join(geminiDir, "settings.json");
|
|
6947
7134
|
let settings = SETTINGS_JSON;
|
|
@@ -6979,24 +7166,26 @@ async function writeSkills(geminiDir) {
|
|
|
6979
7166
|
const skillDir = join(geminiDir, "skills", `compound-${phase}`);
|
|
6980
7167
|
await mkdir(skillDir, { recursive: true });
|
|
6981
7168
|
const description = parseDescription(content, `Compound ${phase} skill`);
|
|
7169
|
+
const body = stripFrontmatter(content);
|
|
6982
7170
|
await writeFile(join(skillDir, "SKILL.md"), `---
|
|
6983
7171
|
name: compound-${phase}
|
|
6984
7172
|
description: ${description}
|
|
6985
7173
|
---
|
|
6986
7174
|
|
|
6987
|
-
${
|
|
7175
|
+
${body}
|
|
6988
7176
|
`, "utf8");
|
|
6989
7177
|
}
|
|
6990
7178
|
for (const [name, content] of Object.entries(AGENT_ROLE_SKILLS)) {
|
|
6991
7179
|
const skillDir = join(geminiDir, "skills", `compound-agent-${name}`);
|
|
6992
7180
|
await mkdir(skillDir, { recursive: true });
|
|
6993
7181
|
const description = parseDescription(content, `Compound agent ${name} skill`);
|
|
7182
|
+
const body = stripFrontmatter(content);
|
|
6994
7183
|
await writeFile(join(skillDir, "SKILL.md"), `---
|
|
6995
7184
|
name: compound-agent-${name}
|
|
6996
7185
|
description: ${description}
|
|
6997
7186
|
---
|
|
6998
7187
|
|
|
6999
|
-
${
|
|
7188
|
+
${body}
|
|
7000
7189
|
`, "utf8");
|
|
7001
7190
|
}
|
|
7002
7191
|
}
|
|
@@ -7207,10 +7396,10 @@ function processToolSuccess(stateDir) {
|
|
|
7207
7396
|
var STATE_DIR = ".claude";
|
|
7208
7397
|
var STATE_FILE = ".ca-phase-state.json";
|
|
7209
7398
|
var PHASE_STATE_MAX_AGE_MS = 72 * 60 * 60 * 1e3;
|
|
7210
|
-
var PHASES = ["
|
|
7399
|
+
var PHASES = ["spec-dev", "plan", "work", "review", "compound"];
|
|
7211
7400
|
var GATES = ["post-plan", "gate-3", "gate-4", "final"];
|
|
7212
7401
|
var PHASE_INDEX = {
|
|
7213
|
-
|
|
7402
|
+
"spec-dev": 1,
|
|
7214
7403
|
plan: 2,
|
|
7215
7404
|
work: 3,
|
|
7216
7405
|
review: 4,
|
|
@@ -7232,10 +7421,17 @@ function isIsoDate(value) {
|
|
|
7232
7421
|
function isStringArray(value) {
|
|
7233
7422
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
7234
7423
|
}
|
|
7424
|
+
function migrateLegacyFields(raw) {
|
|
7425
|
+
if (raw.cookit_active === void 0 && typeof raw.lfg_active === "boolean") {
|
|
7426
|
+
raw.cookit_active = raw.lfg_active;
|
|
7427
|
+
delete raw.lfg_active;
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7235
7430
|
function validatePhaseState(raw) {
|
|
7236
7431
|
if (typeof raw !== "object" || raw === null) return false;
|
|
7237
7432
|
const state = raw;
|
|
7238
|
-
|
|
7433
|
+
migrateLegacyFields(state);
|
|
7434
|
+
return typeof state.cookit_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);
|
|
7239
7435
|
}
|
|
7240
7436
|
function expectedGateForPhase(phaseIndex) {
|
|
7241
7437
|
if (phaseIndex === 2) return "post-plan";
|
|
@@ -7248,10 +7444,10 @@ function initPhaseState(repoRoot, epicId) {
|
|
|
7248
7444
|
const dir = join(repoRoot, STATE_DIR);
|
|
7249
7445
|
mkdirSync(dir, { recursive: true });
|
|
7250
7446
|
const state = {
|
|
7251
|
-
|
|
7447
|
+
cookit_active: true,
|
|
7252
7448
|
epic_id: epicId,
|
|
7253
|
-
current_phase: "
|
|
7254
|
-
phase_index: PHASE_INDEX
|
|
7449
|
+
current_phase: "spec-dev",
|
|
7450
|
+
phase_index: PHASE_INDEX["spec-dev"],
|
|
7255
7451
|
skills_read: [],
|
|
7256
7452
|
gates_passed: [],
|
|
7257
7453
|
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -7314,10 +7510,10 @@ function recordGatePassed(repoRoot, gate) {
|
|
|
7314
7510
|
}
|
|
7315
7511
|
function printStatusHuman(state) {
|
|
7316
7512
|
if (state === null) {
|
|
7317
|
-
console.log("No active
|
|
7513
|
+
console.log("No active cook-it session.");
|
|
7318
7514
|
return;
|
|
7319
7515
|
}
|
|
7320
|
-
console.log("Active
|
|
7516
|
+
console.log("Active cook-it Session");
|
|
7321
7517
|
console.log(` Epic: ${state.epic_id}`);
|
|
7322
7518
|
console.log(` Phase: ${state.current_phase} (${state.phase_index}/5)`);
|
|
7323
7519
|
console.log(` Skills read: ${state.skills_read.length === 0 ? "(none)" : state.skills_read.join(", ")}`);
|
|
@@ -7336,7 +7532,7 @@ function registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot) {
|
|
|
7336
7532
|
return;
|
|
7337
7533
|
}
|
|
7338
7534
|
initPhaseState(repoRoot(), epicId);
|
|
7339
|
-
console.log(`Phase state initialized for ${epicId}. Current phase:
|
|
7535
|
+
console.log(`Phase state initialized for ${epicId}. Current phase: spec-dev (1/5).`);
|
|
7340
7536
|
});
|
|
7341
7537
|
phaseCheck.command("start <phase>").description("Start or resume a phase").action((phase) => {
|
|
7342
7538
|
if (!isPhaseName(phase)) {
|
|
@@ -7381,7 +7577,7 @@ function registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot) {
|
|
|
7381
7577
|
phaseCheck.command("status").description("Show current phase state").option("--json", "Output raw JSON").action((options) => {
|
|
7382
7578
|
const state = getPhaseState(repoRoot());
|
|
7383
7579
|
if (options.json) {
|
|
7384
|
-
console.log(JSON.stringify(state ?? {
|
|
7580
|
+
console.log(JSON.stringify(state ?? { cookit_active: false }));
|
|
7385
7581
|
return;
|
|
7386
7582
|
}
|
|
7387
7583
|
printStatusHuman(state);
|
|
@@ -7396,7 +7592,7 @@ function registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot) {
|
|
|
7396
7592
|
});
|
|
7397
7593
|
}
|
|
7398
7594
|
function registerPhaseCheckCommand(program2) {
|
|
7399
|
-
const phaseCheck = program2.command("phase-check").description("Manage
|
|
7595
|
+
const phaseCheck = program2.command("phase-check").description("Manage cook-it phase state").option("--dry-run", "Show what would be done without making changes");
|
|
7400
7596
|
const getDryRun = () => phaseCheck.opts().dryRun ?? false;
|
|
7401
7597
|
const repoRoot = () => getRepoRoot();
|
|
7402
7598
|
registerPhaseSubcommands(phaseCheck, getDryRun, repoRoot);
|
|
@@ -7411,14 +7607,14 @@ function processPhaseGuard(repoRoot, toolName, _toolInput) {
|
|
|
7411
7607
|
try {
|
|
7412
7608
|
if (toolName !== "Edit" && toolName !== "Write") return {};
|
|
7413
7609
|
const state = getPhaseState(repoRoot);
|
|
7414
|
-
if (state === null || !state.
|
|
7610
|
+
if (state === null || !state.cookit_active) return {};
|
|
7415
7611
|
const expectedSkillPath = `.claude/skills/compound/${state.current_phase}/SKILL.md`;
|
|
7416
7612
|
const skillRead = state.skills_read.includes(expectedSkillPath);
|
|
7417
7613
|
if (!skillRead) {
|
|
7418
7614
|
return {
|
|
7419
7615
|
hookSpecificOutput: {
|
|
7420
7616
|
hookEventName: "PreToolUse",
|
|
7421
|
-
additionalContext: `PHASE GUARD WARNING: You are in
|
|
7617
|
+
additionalContext: `PHASE GUARD WARNING: You are in cook-it phase ${state.phase_index}/5 (${state.current_phase}) but have NOT read the skill file yet. Read ${expectedSkillPath} before continuing.`
|
|
7422
7618
|
}
|
|
7423
7619
|
};
|
|
7424
7620
|
}
|
|
@@ -7443,7 +7639,7 @@ function processReadTracker(repoRoot, toolName, toolInput) {
|
|
|
7443
7639
|
try {
|
|
7444
7640
|
if (toolName !== "Read") return {};
|
|
7445
7641
|
const state = getPhaseState(repoRoot);
|
|
7446
|
-
if (state === null || !state.
|
|
7642
|
+
if (state === null || !state.cookit_active) return {};
|
|
7447
7643
|
const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : null;
|
|
7448
7644
|
if (filePath === null) return {};
|
|
7449
7645
|
const canonicalPath = toCanonicalSkillPath(filePath);
|
|
@@ -7471,7 +7667,7 @@ function processStopAudit(repoRoot, stopHookActive = false) {
|
|
|
7471
7667
|
try {
|
|
7472
7668
|
if (stopHookActive) return {};
|
|
7473
7669
|
const state = getPhaseState(repoRoot);
|
|
7474
|
-
if (state === null || !state.
|
|
7670
|
+
if (state === null || !state.cookit_active) return {};
|
|
7475
7671
|
const expectedGate = expectedGateForPhase(state.phase_index);
|
|
7476
7672
|
if (expectedGate === null) return {};
|
|
7477
7673
|
if (state.gates_passed.includes(expectedGate)) return {};
|
|
@@ -7486,6 +7682,12 @@ function processStopAudit(repoRoot, stopHookActive = false) {
|
|
|
7486
7682
|
}
|
|
7487
7683
|
|
|
7488
7684
|
// src/setup/hooks.ts
|
|
7685
|
+
function logHookError(hookName, err) {
|
|
7686
|
+
if (process.env.CA_DEBUG) {
|
|
7687
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7688
|
+
console.error(`[CA_DEBUG] Hook ${hookName} error: ${msg}`);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7489
7691
|
var HOOK_FILE_MODE = 493;
|
|
7490
7692
|
function hasCompoundAgentHook(content) {
|
|
7491
7693
|
return content.includes(HOOK_MARKER);
|
|
@@ -7621,7 +7823,8 @@ async function runUserPromptHook() {
|
|
|
7621
7823
|
}
|
|
7622
7824
|
const result = processUserPrompt(data.prompt);
|
|
7623
7825
|
console.log(JSON.stringify(result));
|
|
7624
|
-
} catch {
|
|
7826
|
+
} catch (err) {
|
|
7827
|
+
logHookError("user-prompt", err);
|
|
7625
7828
|
console.log(JSON.stringify({}));
|
|
7626
7829
|
}
|
|
7627
7830
|
}
|
|
@@ -7636,7 +7839,8 @@ async function runPostToolFailureHook() {
|
|
|
7636
7839
|
const stateDir = join(getRepoRoot(), ".claude");
|
|
7637
7840
|
const result = processToolFailure(data.tool_name, data.tool_input ?? {}, stateDir);
|
|
7638
7841
|
console.log(JSON.stringify(result));
|
|
7639
|
-
} catch {
|
|
7842
|
+
} catch (err) {
|
|
7843
|
+
logHookError("post-tool-failure", err);
|
|
7640
7844
|
console.log(JSON.stringify({}));
|
|
7641
7845
|
}
|
|
7642
7846
|
}
|
|
@@ -7646,7 +7850,8 @@ async function runPostToolSuccessHook() {
|
|
|
7646
7850
|
const stateDir = join(getRepoRoot(), ".claude");
|
|
7647
7851
|
processToolSuccess(stateDir);
|
|
7648
7852
|
console.log(JSON.stringify({}));
|
|
7649
|
-
} catch {
|
|
7853
|
+
} catch (err) {
|
|
7854
|
+
logHookError("post-tool-success", err);
|
|
7650
7855
|
console.log(JSON.stringify({}));
|
|
7651
7856
|
}
|
|
7652
7857
|
}
|
|
@@ -7659,7 +7864,8 @@ async function runToolHook(processor) {
|
|
|
7659
7864
|
return;
|
|
7660
7865
|
}
|
|
7661
7866
|
console.log(JSON.stringify(processor(getRepoRoot(), data.tool_name, data.tool_input ?? {})));
|
|
7662
|
-
} catch {
|
|
7867
|
+
} catch (err) {
|
|
7868
|
+
logHookError("tool-hook", err);
|
|
7663
7869
|
console.log(JSON.stringify({}));
|
|
7664
7870
|
}
|
|
7665
7871
|
}
|
|
@@ -7668,7 +7874,8 @@ async function runStopAuditHook() {
|
|
|
7668
7874
|
const input = await readStdin();
|
|
7669
7875
|
const data = JSON.parse(input);
|
|
7670
7876
|
console.log(JSON.stringify(processStopAudit(getRepoRoot(), data.stop_hook_active ?? false)));
|
|
7671
|
-
} catch {
|
|
7877
|
+
} catch (err) {
|
|
7878
|
+
logHookError("stop-audit", err);
|
|
7672
7879
|
console.log(JSON.stringify({}));
|
|
7673
7880
|
}
|
|
7674
7881
|
}
|
|
@@ -7796,6 +8003,14 @@ async function installPhaseSkills(repoRoot) {
|
|
|
7796
8003
|
created = true;
|
|
7797
8004
|
}
|
|
7798
8005
|
}
|
|
8006
|
+
for (const [relPath, content] of Object.entries(PHASE_SKILL_REFERENCES)) {
|
|
8007
|
+
const filePath = join(repoRoot, ".claude", "skills", "compound", relPath);
|
|
8008
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
8009
|
+
if (!existsSync(filePath)) {
|
|
8010
|
+
await writeFile(filePath, content, "utf-8");
|
|
8011
|
+
created = true;
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
7799
8014
|
return created;
|
|
7800
8015
|
}
|
|
7801
8016
|
async function installAgentRoleSkills(repoRoot) {
|
|
@@ -8054,7 +8269,7 @@ async function runUninstall(repoRoot, dryRun) {
|
|
|
8054
8269
|
return actions;
|
|
8055
8270
|
}
|
|
8056
8271
|
var GENERATED_HEADER = "<!-- generated by compound-agent -->\n";
|
|
8057
|
-
var DEPRECATED_COMMANDS = ["search.md", "list.md", "show.md", "stats.md", "wrong.md", "learn.md"];
|
|
8272
|
+
var DEPRECATED_COMMANDS = ["search.md", "list.md", "show.md", "stats.md", "wrong.md", "learn.md", "brainstorm.md", "lfg.md"];
|
|
8058
8273
|
function detectExistingInstall(repoRoot) {
|
|
8059
8274
|
return existsSync(join(repoRoot, ".claude", "lessons", "index.jsonl"));
|
|
8060
8275
|
}
|
|
@@ -8129,7 +8344,7 @@ async function runUpgrade(repoRoot, dryRun = false) {
|
|
|
8129
8344
|
const docVersionUpdated = await upgradeDocVersion(repoRoot, VERSION, dryRun);
|
|
8130
8345
|
const parts = [];
|
|
8131
8346
|
if (removedCommands.length > 0) {
|
|
8132
|
-
parts.push(`Removed ${removedCommands.length} deprecated command(s)`);
|
|
8347
|
+
parts.push(`Removed ${removedCommands.length} deprecated command(s): ${removedCommands.join(", ")}`);
|
|
8133
8348
|
}
|
|
8134
8349
|
if (strippedHeaders > 0) {
|
|
8135
8350
|
parts.push(`Stripped headers from ${strippedHeaders} file(s)`);
|
|
@@ -8814,10 +9029,10 @@ init_storage();
|
|
|
8814
9029
|
// src/commands/shared.ts
|
|
8815
9030
|
init_utils();
|
|
8816
9031
|
var out = {
|
|
8817
|
-
success: (msg) => console.log(
|
|
8818
|
-
error: (msg) => console.error(
|
|
8819
|
-
info: (msg) => console.log(
|
|
8820
|
-
warn: (msg) => console.log(
|
|
9032
|
+
success: (msg) => console.log(chalk5.green("[ok]"), msg),
|
|
9033
|
+
error: (msg) => console.error(chalk5.red("[error]"), msg),
|
|
9034
|
+
info: (msg) => console.log(chalk5.blue("[info]"), msg),
|
|
9035
|
+
warn: (msg) => console.log(chalk5.yellow("[warn]"), msg)
|
|
8821
9036
|
};
|
|
8822
9037
|
function getGlobalOpts(cmd) {
|
|
8823
9038
|
const opts = cmd.optsWithGlobals();
|
|
@@ -9470,15 +9685,15 @@ function formatLessonForPrime(lesson) {
|
|
|
9470
9685
|
return `- **${lesson.insight}**${tags}
|
|
9471
9686
|
Learned: ${date} via ${source}`;
|
|
9472
9687
|
}
|
|
9473
|
-
function
|
|
9688
|
+
function formatActiveCookitSection(repoRoot) {
|
|
9474
9689
|
const state = getPhaseState(repoRoot);
|
|
9475
|
-
if (state === null || !state.
|
|
9690
|
+
if (state === null || !state.cookit_active) return null;
|
|
9476
9691
|
const skillsRead = state.skills_read.length === 0 ? "(none)" : state.skills_read.join(", ");
|
|
9477
9692
|
const gatesPassed = state.gates_passed.length === 0 ? "(none)" : state.gates_passed.join(", ");
|
|
9478
9693
|
return `
|
|
9479
9694
|
---
|
|
9480
9695
|
|
|
9481
|
-
# ACTIVE
|
|
9696
|
+
# ACTIVE COOK-IT SESSION
|
|
9482
9697
|
|
|
9483
9698
|
Epic: ${state.epic_id}
|
|
9484
9699
|
Phase: ${state.current_phase} (${state.phase_index}/5)
|
|
@@ -9510,9 +9725,9 @@ Critical lessons from past corrections:
|
|
|
9510
9725
|
${formattedLessons}
|
|
9511
9726
|
`;
|
|
9512
9727
|
}
|
|
9513
|
-
const
|
|
9514
|
-
if (
|
|
9515
|
-
output +=
|
|
9728
|
+
const cookitSection = formatActiveCookitSection(root);
|
|
9729
|
+
if (cookitSection !== null) {
|
|
9730
|
+
output += cookitSection;
|
|
9516
9731
|
}
|
|
9517
9732
|
return output;
|
|
9518
9733
|
}
|
|
@@ -9553,13 +9768,13 @@ function registerAuditCommands(program2) {
|
|
|
9553
9768
|
const line = formatFinding(finding);
|
|
9554
9769
|
switch (finding.severity) {
|
|
9555
9770
|
case "error":
|
|
9556
|
-
console.log(
|
|
9771
|
+
console.log(chalk5.red(line));
|
|
9557
9772
|
break;
|
|
9558
9773
|
case "warning":
|
|
9559
|
-
console.log(
|
|
9774
|
+
console.log(chalk5.yellow(line));
|
|
9560
9775
|
break;
|
|
9561
9776
|
default:
|
|
9562
|
-
console.log(
|
|
9777
|
+
console.log(chalk5.blue(line));
|
|
9563
9778
|
break;
|
|
9564
9779
|
}
|
|
9565
9780
|
}
|
|
@@ -9701,13 +9916,13 @@ function registerRulesCommands(program2) {
|
|
|
9701
9916
|
const line = formatViolation(result.rule, violation);
|
|
9702
9917
|
switch (result.rule.severity) {
|
|
9703
9918
|
case "error":
|
|
9704
|
-
console.log(
|
|
9919
|
+
console.log(chalk5.red(line));
|
|
9705
9920
|
break;
|
|
9706
9921
|
case "warning":
|
|
9707
|
-
console.log(
|
|
9922
|
+
console.log(chalk5.yellow(line));
|
|
9708
9923
|
break;
|
|
9709
9924
|
default:
|
|
9710
|
-
console.log(
|
|
9925
|
+
console.log(chalk5.blue(line));
|
|
9711
9926
|
break;
|
|
9712
9927
|
}
|
|
9713
9928
|
}
|
|
@@ -9873,7 +10088,7 @@ async function runVerifyGates(epicId, options = {}) {
|
|
|
9873
10088
|
const allPassed = checks.every((check) => check.status === "pass");
|
|
9874
10089
|
if (allPassed) {
|
|
9875
10090
|
const state = getPhaseState(repoRoot);
|
|
9876
|
-
if (state !== null && state.
|
|
10091
|
+
if (state !== null && state.cookit_active && state.gates_passed.includes("final")) {
|
|
9877
10092
|
cleanPhaseState(repoRoot);
|
|
9878
10093
|
}
|
|
9879
10094
|
}
|
|
@@ -9914,7 +10129,33 @@ function registerVerifyGatesCommand(program2) {
|
|
|
9914
10129
|
}
|
|
9915
10130
|
|
|
9916
10131
|
// src/changelog-data.ts
|
|
9917
|
-
var CHANGELOG_RECENT = `## [1.6.
|
|
10132
|
+
var CHANGELOG_RECENT = `## [1.6.1] - 2026-03-05
|
|
10133
|
+
|
|
10134
|
+
### Changed
|
|
10135
|
+
|
|
10136
|
+
- **Renamed brainstorm phase to spec-dev**: The \`/compound:brainstorm\` slash command is now \`/compound:spec-dev\`. The phase focuses on structured specification development using EARS notation, Mermaid diagrams, and Socratic dialogue rather than open-ended brainstorming. Old \`brainstorm.md\` command files are auto-cleaned during upgrade.
|
|
10137
|
+
- **Integration test stability**: Reduced integration test parallelism (\`maxForks: 1\`) and increased timeouts to 60s to eliminate non-deterministic ETIMEDOUT failures under load.
|
|
10138
|
+
|
|
10139
|
+
### Added
|
|
10140
|
+
|
|
10141
|
+
- **Spec reference file**: \`.claude/skills/compound/spec-dev/references/spec-guide.md\` provides quick-reference material for EARS patterns, Mermaid diagram selection, NL ambiguity detection, and trade-off documentation frameworks. Installed automatically during \`ca setup\`.
|
|
10142
|
+
- **Hook error visibility**: Hook runners now log errors to stderr when \`CA_DEBUG\` environment variable is set, instead of silently swallowing all failures.
|
|
10143
|
+
- **check-plan stdin safety**: \`ca check-plan\` now enforces a 30-second timeout and 1MB size limit when reading from stdin, preventing hangs in CI/CD environments.
|
|
10144
|
+
- **Embed lock expiry**: Embedding lock files now expire after 1 hour as a safety valve against zombie processes holding locks indefinitely.
|
|
10145
|
+
- **Phase-state backward compatibility**: Legacy \`lfg_active\` field in phase state files is automatically migrated to \`cookit_active\` on read.
|
|
10146
|
+
- **clean-lessons scope messaging**: \`ca clean-lessons\` now reports when non-lesson items are excluded from analysis.
|
|
10147
|
+
|
|
10148
|
+
### Fixed
|
|
10149
|
+
|
|
10150
|
+
- **Missing spec-guide.md**: The reference file was declared in skill templates and CHANGELOG but never generated during setup. Now installed alongside phase skills.
|
|
10151
|
+
- **Upgrade cleanup for lfg.md**: Added \`lfg.md\` to deprecated commands list so \`ca setup --update\` removes stale lfg command files from upgraded repos.
|
|
10152
|
+
- **Docs template terminology**: WORKFLOW.md template now uses "Spec Dev" instead of "Brainstorm" for Phase 1.
|
|
10153
|
+
- **Test file naming**: Renamed \`brainstorm-phase.test.ts\` to \`spec-dev-phase.test.ts\` to match the refactored phase name.
|
|
10154
|
+
- **Library bundle cleanup**: Moved CLI-only re-exports (\`registerWatchCommand\`, \`registerLoopCommands\`) out of the library barrel to eliminate unused import warnings in \`dist/index.js\`.
|
|
10155
|
+
- **plan.test.ts embedding guard**: Added \`skipIf(skipEmbedding)\` to unguarded test that calls \`retrieveForPlan\` without mocking.
|
|
10156
|
+
- **Agent template test count**: Updated setup.test.ts to expect 9 agent templates (was 8), matching the actual AGENT_TEMPLATES count after \`lessons-reviewer.md\` was added.
|
|
10157
|
+
|
|
10158
|
+
## [1.6.0] - 2026-03-02
|
|
9918
10159
|
|
|
9919
10160
|
### Added
|
|
9920
10161
|
|
|
@@ -9991,42 +10232,7 @@ var CHANGELOG_RECENT = `## [1.6.0] - 2026-03-02
|
|
|
9991
10232
|
- **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)
|
|
9992
10233
|
- **Gemini BeforeTool matcher incomplete**: Added \`create_file\` to BeforeTool and AfterTool matchers alongside \`replace\` and \`write_file\`
|
|
9993
10234
|
- **TOML description escaping**: \`parseDescription\` now escapes \`\\\` and \`"\` to prevent malformed TOML output
|
|
9994
|
-
- **Flaky embedding test**: Added 15s timeout to \`isModelUsable\` test
|
|
9995
|
-
|
|
9996
|
-
## [1.4.4] - 2026-02-23
|
|
9997
|
-
|
|
9998
|
-
### Added
|
|
9999
|
-
|
|
10000
|
-
- **Security arc with P0-P3 severity model**: Security-reviewer promoted from generic OWASP checker to mandatory core-4 reviewer with P0 (blocks merge), P1 (requires ack), P2 (should fix), P3 (nice to have) classification
|
|
10001
|
-
- **5 on-demand security specialist skills**: \`/security-injection\`, \`/security-secrets\`, \`/security-auth\`, \`/security-data\`, \`/security-deps\` -- spawned by security-reviewer via SendMessage within the review AgentTeam for deep trace analysis
|
|
10002
|
-
- **6 security reference docs** (\`docs/research/security/\`): overview, injection-patterns, secrets-checklist, auth-patterns, data-exposure, dependency-security -- distilled from the secure-coding-failure PhD survey into actionable agent guides
|
|
10003
|
-
- **Native addon build injection** (\`scripts/postinstall.mjs\`): Postinstall script auto-patches consumer \`package.json\` with \`pnpm.onlyBuiltDependencies\` config for \`better-sqlite3\` and \`node-llama-cpp\`. Handles indent preservation, BOM stripping, atomic writes
|
|
10004
|
-
- **CLI preflight diagnostics** (\`src/cli-preflight.ts\`): Catches native module load failures before commands run, prints PM-specific fix instructions (pnpm: 3 options; npm/yarn: rebuild + build tool hints)
|
|
10005
|
-
- **\`ca doctor\` pnpm check**: Verifies \`onlyBuiltDependencies\` is configured correctly for pnpm projects, recognizes wildcard \`["*"]\` as valid
|
|
10006
|
-
- **Escalation-wiring tests**: 7 new tests verifying security-reviewer mentions all 5 specialists, each specialist declares "Spawned by security-reviewer", P0 documented as merge-blocking, each specialist has \`npx ca knowledge\` and references correct research doc
|
|
10007
|
-
- **better-sqlite3 injection patterns**: Added project-specific \`db.exec()\` vs \`db.prepare().run()\` examples to \`injection-patterns.md\`
|
|
10008
|
-
|
|
10009
|
-
### Fixed
|
|
10010
|
-
|
|
10011
|
-
- **Noisy \`node-llama-cpp\` warnings on headless Linux**: Vulkan binary fallback and \`special_eos_id\` tokenizer warnings no longer print during \`ca search\` / \`ca knowledge\` -- GPU auto-detection preserved via \`progressLogs: false\` + \`logLevel: error\`
|
|
10012
|
-
- **Resource leak in \`isModelUsable()\`**: \`Llama\` and \`LlamaModel\` instances are now properly disposed after the preflight usability check
|
|
10013
|
-
- **Wildcard \`onlyBuiltDependencies\`**: Doctor and postinstall now recognize \`["*"]\` as fully configured (no false positive)
|
|
10014
|
-
- **Infinity loop marker injection**: \`--model\` validated against shell metacharacters; grep patterns anchored (\`^EPIC_COMPLETE\`, \`^EPIC_FAILED\`) to prevent false-positive matches from prompt echo in logs
|
|
10015
|
-
- **Template-to-deployed SKILL.md drift**: Backported all deployed specialist improvements (output fields, collaboration notes, \`npx ca knowledge\` lines) into source templates so \`ca setup --update\` no longer regresses
|
|
10016
|
-
- **SSRF citations**: 3 OWASP references in \`secure-coding-failure.md\` corrected from A01 (Broken Access Control) to A10 (SSRF)
|
|
10017
|
-
- **Stale verification docs**: Exit criteria updated from 6 to 8 categories (added Security Clear + Workflow Gates); closed-loop review process updated with security check in Stage 4 flowchart
|
|
10018
|
-
- **Broken dual-path reference** in \`subagent-pipeline.md\`: Now documents both \`docs/research/security/\` (source repo) and \`docs/compound/research/security/\` (consumer repos)
|
|
10019
|
-
- **Incomplete OWASP mapping** in \`overview.md\`: Completed from 5/10 to 10/10 (added A04, A05, A07, A08, A09)
|
|
10020
|
-
|
|
10021
|
-
### Changed
|
|
10022
|
-
|
|
10023
|
-
- **\`getLlama()\` initialization hardened**: Both call sites (\`nomic.ts\`, \`model.ts\`) now pass \`build: 'never'\` to prevent silent compilation from source on exotic platforms; set \`NODE_LLAMA_CPP_DEBUG=true\` to re-enable verbose output
|
|
10024
|
-
- **Review skill wired to security arc**: P0 added to severity overview, security specialist skills listed as on-demand members, quality criteria include P0/P1 checks
|
|
10025
|
-
- **WORKFLOW template**: Severity classification updated from P1/P2/P3 to P0-P3 with "Fix all P0/P1 findings"
|
|
10026
|
-
- **Zero-findings instruction**: All 6 security templates (reviewer + 5 specialists) now include "return CLEAR" instruction when no findings detected
|
|
10027
|
-
- **Scope-limiting instruction**: \`security-injection\` prioritizes files with interpreter sinks over pure data/config for large diffs (500+ lines)
|
|
10028
|
-
- **Non-web context**: \`security-auth\` includes step for CLI/API-only projects without web routes
|
|
10029
|
-
- **Graceful audit skip**: \`security-deps\` handles missing \`pnpm audit\` / \`pip-audit\` gracefully instead of failing`;
|
|
10235
|
+
- **Flaky embedding test**: Added 15s timeout to \`isModelUsable\` test`;
|
|
10030
10236
|
|
|
10031
10237
|
// src/commands/about.ts
|
|
10032
10238
|
function registerAboutCommand(program2) {
|
|
@@ -10214,6 +10420,9 @@ async function cleanLessonsAction() {
|
|
|
10214
10420
|
await syncIfNeeded(repoRoot);
|
|
10215
10421
|
const { items } = await readMemoryItems(repoRoot);
|
|
10216
10422
|
const activeItems = items.filter((item) => !item.invalidatedAt && item.type === "lesson");
|
|
10423
|
+
if (items.length > activeItems.length) {
|
|
10424
|
+
console.log(`Analyzing ${activeItems.length} lesson-type items only (${items.length - activeItems.length} non-lesson items excluded).`);
|
|
10425
|
+
}
|
|
10217
10426
|
const pairs = await findDuplicatePairs(repoRoot, activeItems);
|
|
10218
10427
|
if (pairs.length === 0) {
|
|
10219
10428
|
console.log("No similar lessons found. Your lesson database is clean.");
|
|
@@ -10508,137 +10717,452 @@ function registerCaptureCommands(program2) {
|
|
|
10508
10717
|
await handleCapture(this, options);
|
|
10509
10718
|
});
|
|
10510
10719
|
}
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
function
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
# LOOP_DRY_RUN=1 ./infinity-loop.sh # Preview without executing
|
|
10522
|
-
|
|
10523
|
-
set -euo pipefail
|
|
10524
|
-
|
|
10525
|
-
# Config
|
|
10526
|
-
MAX_RETRIES=${maxRetries}
|
|
10527
|
-
MODEL="${model}"
|
|
10528
|
-
EPIC_IDS="${epicIds}"
|
|
10529
|
-
LOG_DIR="agent_logs"
|
|
10530
|
-
|
|
10531
|
-
# Helpers
|
|
10532
|
-
timestamp() { date '+%Y-%m-%d_%H-%M-%S'; }
|
|
10533
|
-
log() { echo "[$(timestamp)] $*"; }
|
|
10534
|
-
die() { log "FATAL: $*"; exit 1; }
|
|
10535
|
-
|
|
10536
|
-
command -v claude >/dev/null || die "claude CLI required"
|
|
10537
|
-
command -v bd >/dev/null || die "bd (beads) CLI required"
|
|
10538
|
-
|
|
10539
|
-
# Detect JSON parser: prefer jq, fall back to python3
|
|
10540
|
-
HAS_JQ=false
|
|
10541
|
-
command -v jq >/dev/null 2>&1 && HAS_JQ=true
|
|
10542
|
-
if [ "$HAS_JQ" = false ]; then
|
|
10543
|
-
command -v python3 >/dev/null 2>&1 || die "jq or python3 required for JSON parsing"
|
|
10544
|
-
fi
|
|
10545
|
-
|
|
10546
|
-
# parse_json() - extract a value from JSON stdin
|
|
10547
|
-
# Uses jq (primary) with python3 fallback
|
|
10548
|
-
# Auto-unwraps single-element arrays (bd show --json returns [...])
|
|
10549
|
-
# Usage: echo '[{"status":"open"}]' | parse_json '.status'
|
|
10550
|
-
parse_json() {
|
|
10551
|
-
local filter="$1"
|
|
10552
|
-
if [ "$HAS_JQ" = true ]; then
|
|
10553
|
-
jq -r "if type == \\"array\\" then .[0] else . end | $filter"
|
|
10554
|
-
else
|
|
10555
|
-
python3 -c "
|
|
10556
|
-
import sys, json
|
|
10557
|
-
data = json.load(sys.stdin)
|
|
10558
|
-
if isinstance(data, list):
|
|
10559
|
-
data = data[0] if data else {}
|
|
10560
|
-
f = '$filter'.strip('.')
|
|
10561
|
-
parts = [p for p in f.split('.') if p]
|
|
10562
|
-
v = data
|
|
10563
|
-
try:
|
|
10564
|
-
for p in parts:
|
|
10565
|
-
v = v[p]
|
|
10566
|
-
except (KeyError, IndexError, TypeError):
|
|
10567
|
-
v = None
|
|
10568
|
-
print('' if v is None else v)
|
|
10569
|
-
"
|
|
10570
|
-
fi
|
|
10720
|
+
init_storage();
|
|
10721
|
+
init_search2();
|
|
10722
|
+
function parseLimitOrNull(rawLimit, optionName, commandName) {
|
|
10723
|
+
try {
|
|
10724
|
+
return parseLimit(rawLimit, optionName);
|
|
10725
|
+
} catch (err) {
|
|
10726
|
+
const message = err instanceof Error ? err.message : `Invalid ${optionName}`;
|
|
10727
|
+
console.error(formatError(commandName, "INVALID_LIMIT", message, `Use --${optionName} with a positive integer`));
|
|
10728
|
+
return null;
|
|
10729
|
+
}
|
|
10571
10730
|
}
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10731
|
+
var MAX_STDIN_BYTES = 1048576;
|
|
10732
|
+
var STDIN_TIMEOUT_MS = 3e4;
|
|
10733
|
+
async function readPlanFromStdin() {
|
|
10734
|
+
const { stdin } = await import('process');
|
|
10735
|
+
if (!stdin.isTTY) {
|
|
10736
|
+
const chunks = [];
|
|
10737
|
+
let totalBytes = 0;
|
|
10738
|
+
const timeout = new Promise(
|
|
10739
|
+
(_, reject) => setTimeout(() => reject(new Error("stdin read timed out after 30s")), STDIN_TIMEOUT_MS)
|
|
10740
|
+
);
|
|
10741
|
+
const read = (async () => {
|
|
10742
|
+
for await (const chunk of stdin) {
|
|
10743
|
+
const buf = chunk;
|
|
10744
|
+
totalBytes += buf.length;
|
|
10745
|
+
if (totalBytes > MAX_STDIN_BYTES) {
|
|
10746
|
+
throw new Error(`stdin exceeds ${MAX_STDIN_BYTES} byte limit`);
|
|
10747
|
+
}
|
|
10748
|
+
chunks.push(buf);
|
|
10749
|
+
}
|
|
10750
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
10751
|
+
})();
|
|
10752
|
+
try {
|
|
10753
|
+
return await Promise.race([read, timeout]);
|
|
10754
|
+
} catch (err) {
|
|
10755
|
+
console.error(`Warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
10756
|
+
return void 0;
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
return void 0;
|
|
10575
10760
|
}
|
|
10576
|
-
function
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
fi
|
|
10589
|
-
done
|
|
10590
|
-
return 1
|
|
10591
|
-
else
|
|
10592
|
-
# Dynamic: get next ready epic from dependency graph, filtering processed
|
|
10593
|
-
local epic_id
|
|
10594
|
-
if [ "$HAS_JQ" = true ]; then
|
|
10595
|
-
epic_id=$(bd list --type=epic --ready --json --limit=10 2>/dev/null | jq -r '.[].id' 2>/dev/null | while read -r id; do
|
|
10596
|
-
case " $PROCESSED " in *" $id "*) continue ;; esac
|
|
10597
|
-
echo "$id"
|
|
10598
|
-
break
|
|
10599
|
-
done)
|
|
10600
|
-
else
|
|
10601
|
-
epic_id=$(bd list --type=epic --ready --json --limit=10 2>/dev/null | python3 -c "
|
|
10602
|
-
import sys, json
|
|
10603
|
-
processed = set('$PROCESSED'.split())
|
|
10604
|
-
items = json.load(sys.stdin)
|
|
10605
|
-
for item in items:
|
|
10606
|
-
if item['id'] not in processed:
|
|
10607
|
-
print(item['id'])
|
|
10608
|
-
break" 2>/dev/null || echo "")
|
|
10609
|
-
fi
|
|
10610
|
-
if [ -z "$epic_id" ]; then
|
|
10611
|
-
return 1
|
|
10612
|
-
fi
|
|
10613
|
-
echo "$epic_id"
|
|
10614
|
-
return 0
|
|
10615
|
-
fi
|
|
10761
|
+
function outputCheckPlanJson(lessons) {
|
|
10762
|
+
const jsonOutput = {
|
|
10763
|
+
lessons: lessons.map((l) => ({
|
|
10764
|
+
id: l.lesson.id,
|
|
10765
|
+
insight: l.lesson.insight,
|
|
10766
|
+
rankScore: l.finalScore ?? l.score,
|
|
10767
|
+
// Use finalScore if available, fallback to raw score
|
|
10768
|
+
source: l.lesson.source
|
|
10769
|
+
})),
|
|
10770
|
+
count: lessons.length
|
|
10771
|
+
};
|
|
10772
|
+
console.log(JSON.stringify(jsonOutput));
|
|
10616
10773
|
}
|
|
10617
|
-
|
|
10774
|
+
function outputCheckPlanHuman(lessons, quiet) {
|
|
10775
|
+
console.log("## Lessons Check\n");
|
|
10776
|
+
console.log("Relevant to your plan:\n");
|
|
10777
|
+
lessons.forEach((item, i) => {
|
|
10778
|
+
const num = i + 1;
|
|
10779
|
+
console.log(`${num}. ${chalk5.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
|
|
10780
|
+
console.log(` - Source: ${item.lesson.source}`);
|
|
10781
|
+
console.log();
|
|
10782
|
+
});
|
|
10783
|
+
if (!quiet) {
|
|
10784
|
+
console.log("---");
|
|
10785
|
+
console.log("Consider these lessons while implementing.");
|
|
10786
|
+
}
|
|
10618
10787
|
}
|
|
10619
|
-
function
|
|
10620
|
-
return
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10788
|
+
function formatSource2(source) {
|
|
10789
|
+
return source.replace(/_/g, " ");
|
|
10790
|
+
}
|
|
10791
|
+
function outputSessionLessonsHuman(lessons, quiet) {
|
|
10792
|
+
console.log("## Lessons from Past Sessions\n");
|
|
10793
|
+
console.log("These lessons were captured from previous corrections and should inform your work:\n");
|
|
10794
|
+
lessons.forEach((lesson, i) => {
|
|
10795
|
+
const num = i + 1;
|
|
10796
|
+
const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
|
|
10797
|
+
const tagsDisplay = lesson.tags.length > 0 ? ` (${lesson.tags.join(", ")})` : "";
|
|
10798
|
+
console.log(`${num}. **${lesson.insight}**${tagsDisplay}`);
|
|
10799
|
+
console.log(` Learned: ${date} via ${formatSource2(lesson.source)}`);
|
|
10800
|
+
console.log();
|
|
10801
|
+
});
|
|
10802
|
+
if (!quiet) {
|
|
10803
|
+
console.log("Consider these lessons when planning and implementing tasks.");
|
|
10804
|
+
}
|
|
10805
|
+
}
|
|
10806
|
+
async function searchAction(cmd, query, options) {
|
|
10807
|
+
const repoRoot = getRepoRoot();
|
|
10808
|
+
const limit = parseLimitOrNull(options.limit, "limit", "search");
|
|
10809
|
+
if (limit === null) {
|
|
10810
|
+
process.exitCode = 1;
|
|
10811
|
+
return;
|
|
10812
|
+
}
|
|
10813
|
+
const { verbose, quiet } = getGlobalOpts(cmd);
|
|
10814
|
+
await syncIfNeeded(repoRoot);
|
|
10815
|
+
let results;
|
|
10816
|
+
if (isModelAvailable()) {
|
|
10817
|
+
try {
|
|
10818
|
+
const candidateLimit = limit * CANDIDATE_MULTIPLIER;
|
|
10819
|
+
const [vectorResults, keywordResults] = await Promise.all([
|
|
10820
|
+
searchVector(repoRoot, query, { limit: candidateLimit }),
|
|
10821
|
+
searchKeywordScored(repoRoot, query, candidateLimit)
|
|
10822
|
+
]);
|
|
10823
|
+
const merged = mergeHybridResults(vectorResults, keywordResults, { minScore: MIN_HYBRID_SCORE });
|
|
10824
|
+
const ranked = rankLessons(merged);
|
|
10825
|
+
results = ranked.slice(0, limit).map((r) => r.lesson);
|
|
10826
|
+
} catch {
|
|
10827
|
+
results = await searchKeyword(repoRoot, query, limit);
|
|
10828
|
+
}
|
|
10829
|
+
} else {
|
|
10830
|
+
results = await searchKeyword(repoRoot, query, limit);
|
|
10831
|
+
}
|
|
10832
|
+
if (results.length > 0) {
|
|
10833
|
+
incrementRetrievalCount(repoRoot, results.map((lesson) => lesson.id));
|
|
10834
|
+
}
|
|
10835
|
+
if (results.length === 0) {
|
|
10836
|
+
console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
|
|
10837
|
+
return;
|
|
10838
|
+
}
|
|
10839
|
+
if (!quiet) {
|
|
10840
|
+
out.info(`Found ${results.length} lesson(s):
|
|
10841
|
+
`);
|
|
10842
|
+
}
|
|
10843
|
+
for (const lesson of results) {
|
|
10844
|
+
console.log(`[${chalk5.cyan(lesson.id)}] ${lesson.insight}`);
|
|
10845
|
+
console.log(` Trigger: ${lesson.trigger}`);
|
|
10846
|
+
if (verbose && lesson.context) {
|
|
10847
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
10848
|
+
console.log(` Created: ${lesson.created}`);
|
|
10849
|
+
}
|
|
10850
|
+
if (lesson.tags.length > 0) {
|
|
10851
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
10852
|
+
}
|
|
10853
|
+
console.log();
|
|
10854
|
+
}
|
|
10855
|
+
}
|
|
10856
|
+
async function listAction(cmd, options) {
|
|
10857
|
+
const repoRoot = getRepoRoot();
|
|
10858
|
+
const limit = parseLimitOrNull(options.limit, "limit", "list");
|
|
10859
|
+
if (limit === null) {
|
|
10860
|
+
process.exitCode = 1;
|
|
10861
|
+
return;
|
|
10862
|
+
}
|
|
10863
|
+
const { verbose, quiet } = getGlobalOpts(cmd);
|
|
10864
|
+
const { items, skippedCount } = await readMemoryItems(repoRoot);
|
|
10865
|
+
const filteredItems = options.invalidated ? items.filter((i) => i.invalidatedAt) : items;
|
|
10866
|
+
if (filteredItems.length === 0) {
|
|
10867
|
+
if (options.invalidated) {
|
|
10868
|
+
console.log("No invalidated lessons found.");
|
|
10869
|
+
} else {
|
|
10870
|
+
console.log('No lessons found. Get started with: learn "Your first lesson"');
|
|
10871
|
+
}
|
|
10872
|
+
if (skippedCount > 0) {
|
|
10873
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
10874
|
+
}
|
|
10875
|
+
return;
|
|
10876
|
+
}
|
|
10877
|
+
const toShow = filteredItems.slice(0, limit);
|
|
10878
|
+
if (!quiet) {
|
|
10879
|
+
const label = options.invalidated ? "invalidated lesson(s)" : "item(s)";
|
|
10880
|
+
out.info(`Showing ${toShow.length} of ${filteredItems.length} ${label}:
|
|
10881
|
+
`);
|
|
10882
|
+
}
|
|
10883
|
+
for (const item of toShow) {
|
|
10884
|
+
const invalidMarker = item.invalidatedAt ? chalk5.red("[INVALID] ") : "";
|
|
10885
|
+
console.log(`[${chalk5.cyan(item.id)}] ${invalidMarker}${item.insight}`);
|
|
10886
|
+
if (verbose) {
|
|
10887
|
+
console.log(` Type: ${item.type} | Source: ${item.source}`);
|
|
10888
|
+
console.log(` Created: ${item.created}`);
|
|
10889
|
+
if (item.context) {
|
|
10890
|
+
console.log(` Context: ${item.context.tool} - ${item.context.intent}`);
|
|
10891
|
+
}
|
|
10892
|
+
if (item.invalidatedAt) {
|
|
10893
|
+
console.log(` Invalidated: ${item.invalidatedAt}`);
|
|
10894
|
+
if (item.invalidationReason) {
|
|
10895
|
+
console.log(` Reason: ${item.invalidationReason}`);
|
|
10896
|
+
}
|
|
10897
|
+
}
|
|
10898
|
+
} else {
|
|
10899
|
+
console.log(` Type: ${item.type} | Source: ${item.source}`);
|
|
10900
|
+
}
|
|
10901
|
+
if (item.tags.length > 0) {
|
|
10902
|
+
console.log(` Tags: ${item.tags.join(", ")}`);
|
|
10903
|
+
}
|
|
10904
|
+
console.log();
|
|
10905
|
+
}
|
|
10906
|
+
if (skippedCount > 0) {
|
|
10907
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
10908
|
+
}
|
|
10909
|
+
}
|
|
10910
|
+
async function loadSessionAction(cmd, options) {
|
|
10911
|
+
const repoRoot = getRepoRoot();
|
|
10912
|
+
const { quiet } = getGlobalOpts(cmd);
|
|
10913
|
+
const lessons = await loadSessionLessons(repoRoot);
|
|
10914
|
+
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
10915
|
+
const totalCount = allLessons.length;
|
|
10916
|
+
if (options.json) {
|
|
10917
|
+
console.log(JSON.stringify({ lessons, count: lessons.length, totalCount }));
|
|
10918
|
+
return;
|
|
10919
|
+
}
|
|
10920
|
+
if (lessons.length === 0) {
|
|
10921
|
+
console.log("No high-severity lessons found.");
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10924
|
+
outputSessionLessonsHuman(lessons, quiet);
|
|
10925
|
+
if (totalCount > LESSON_COUNT_WARNING_THRESHOLD) {
|
|
10926
|
+
console.log("");
|
|
10927
|
+
out.info(`${totalCount} lessons in index. Consider \`ca compact\` to reduce context pollution.`);
|
|
10928
|
+
}
|
|
10929
|
+
const oldLessons = lessons.filter((l) => getLessonAgeDays(l) > AGE_FLAG_THRESHOLD_DAYS);
|
|
10930
|
+
if (oldLessons.length > 0) {
|
|
10931
|
+
console.log("");
|
|
10932
|
+
out.warn(`${oldLessons.length} lesson(s) are over ${AGE_FLAG_THRESHOLD_DAYS} days old. Review for continued validity.`);
|
|
10933
|
+
}
|
|
10934
|
+
}
|
|
10935
|
+
async function checkPlanAction(cmd, options) {
|
|
10936
|
+
const repoRoot = getRepoRoot();
|
|
10937
|
+
const limit = parseLimitOrNull(options.limit, "limit", "check-plan");
|
|
10938
|
+
if (limit === null) {
|
|
10939
|
+
process.exitCode = 1;
|
|
10940
|
+
return;
|
|
10941
|
+
}
|
|
10942
|
+
const { quiet } = getGlobalOpts(cmd);
|
|
10943
|
+
const planText = options.plan ?? await readPlanFromStdin();
|
|
10944
|
+
if (!planText) {
|
|
10945
|
+
console.error(formatError("check-plan", "NO_PLAN", "No plan provided", "Use --plan <text> or pipe text to stdin"));
|
|
10946
|
+
process.exitCode = 1;
|
|
10947
|
+
return;
|
|
10948
|
+
}
|
|
10949
|
+
await syncIfNeeded(repoRoot);
|
|
10950
|
+
if (!isModelAvailable()) {
|
|
10951
|
+
if (options.json) {
|
|
10952
|
+
console.log(JSON.stringify({
|
|
10953
|
+
lessons: [],
|
|
10954
|
+
count: 0,
|
|
10955
|
+
error: "Embedding model not found",
|
|
10956
|
+
action: "Run: npx ca download-model"
|
|
10957
|
+
}));
|
|
10958
|
+
} else {
|
|
10959
|
+
console.error(formatError("check-plan", "MODEL_UNAVAILABLE", "Embedding model not found", "Run: npx ca download-model"));
|
|
10960
|
+
}
|
|
10961
|
+
process.exitCode = 1;
|
|
10962
|
+
return;
|
|
10963
|
+
}
|
|
10964
|
+
try {
|
|
10965
|
+
const result = await retrieveForPlan(repoRoot, planText, limit);
|
|
10966
|
+
if (options.json) {
|
|
10967
|
+
outputCheckPlanJson(result.lessons);
|
|
10968
|
+
return;
|
|
10969
|
+
}
|
|
10970
|
+
if (result.lessons.length === 0) {
|
|
10971
|
+
console.log("No relevant lessons found for this plan.");
|
|
10972
|
+
return;
|
|
10973
|
+
}
|
|
10974
|
+
outputCheckPlanHuman(result.lessons, quiet);
|
|
10975
|
+
} catch (err) {
|
|
10976
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
10977
|
+
if (options.json) {
|
|
10978
|
+
console.log(JSON.stringify({
|
|
10979
|
+
lessons: [],
|
|
10980
|
+
count: 0,
|
|
10981
|
+
error: message
|
|
10982
|
+
}));
|
|
10983
|
+
} else {
|
|
10984
|
+
console.error(formatError("check-plan", "PLAN_CHECK_FAILED", message, "Check model installation and try again"));
|
|
10985
|
+
}
|
|
10986
|
+
process.exitCode = 1;
|
|
10987
|
+
}
|
|
10988
|
+
}
|
|
10989
|
+
function registerRetrievalCommands(program2) {
|
|
10990
|
+
program2.command("search <query>").description("Search lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
|
|
10991
|
+
await searchAction(this, query, options);
|
|
10992
|
+
});
|
|
10993
|
+
program2.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).option("--invalidated", "Show only invalidated lessons").action(async function(options) {
|
|
10994
|
+
await listAction(this, options);
|
|
10995
|
+
});
|
|
10996
|
+
program2.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
10997
|
+
await loadSessionAction(this, options);
|
|
10998
|
+
});
|
|
10999
|
+
program2.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
|
|
11000
|
+
await checkPlanAction(this, options);
|
|
11001
|
+
});
|
|
11002
|
+
}
|
|
11003
|
+
|
|
11004
|
+
// src/commands/index.ts
|
|
11005
|
+
function registerSetupCommands(program2) {
|
|
11006
|
+
registerInitCommand(program2);
|
|
11007
|
+
registerHooksCommand(program2);
|
|
11008
|
+
const setupCommand = program2.command("setup");
|
|
11009
|
+
registerSetupAllCommand(setupCommand);
|
|
11010
|
+
registerClaudeSubcommand(setupCommand);
|
|
11011
|
+
registerGeminiSubcommand(setupCommand);
|
|
11012
|
+
registerDownloadModelCommand(program2);
|
|
11013
|
+
}
|
|
11014
|
+
function registerManagementCommands(program2) {
|
|
11015
|
+
registerInvalidationCommands(program2);
|
|
11016
|
+
registerMaintenanceCommands(program2);
|
|
11017
|
+
registerIOCommands(program2);
|
|
11018
|
+
registerPrimeCommand(program2);
|
|
11019
|
+
registerCrudCommands(program2);
|
|
11020
|
+
registerAuditCommands(program2);
|
|
11021
|
+
registerDoctorCommand(program2);
|
|
11022
|
+
registerReviewerCommand(program2);
|
|
11023
|
+
registerRulesCommands(program2);
|
|
11024
|
+
registerTestSummaryCommand(program2);
|
|
11025
|
+
registerVerifyGatesCommand(program2);
|
|
11026
|
+
registerAboutCommand(program2);
|
|
11027
|
+
registerKnowledgeCommand(program2);
|
|
11028
|
+
registerKnowledgeIndexCommand(program2);
|
|
11029
|
+
registerCleanLessonsCommand(program2);
|
|
11030
|
+
program2.command("worktree").description("(removed) Use Claude Code native worktree support").action(() => {
|
|
11031
|
+
console.error("ca worktree has been removed. Use Claude Code's native EnterWorktree support instead.");
|
|
11032
|
+
process.exitCode = 1;
|
|
11033
|
+
});
|
|
11034
|
+
}
|
|
11035
|
+
var LOOP_EPIC_ID_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
11036
|
+
var MODEL_PATTERN = /^[a-zA-Z0-9_.:/-]+$/;
|
|
11037
|
+
function buildScriptHeader(timestamp, maxRetries, model, epicIds) {
|
|
11038
|
+
return `#!/usr/bin/env bash
|
|
11039
|
+
# Infinity Loop - Generated by: ca loop
|
|
11040
|
+
# Date: ${timestamp}
|
|
11041
|
+
# Autonomously processes beads epics via Claude Code sessions.
|
|
11042
|
+
#
|
|
11043
|
+
# Usage:
|
|
11044
|
+
# ./infinity-loop.sh
|
|
11045
|
+
# LOOP_DRY_RUN=1 ./infinity-loop.sh # Preview without executing
|
|
11046
|
+
|
|
11047
|
+
set -euo pipefail
|
|
11048
|
+
|
|
11049
|
+
# Config
|
|
11050
|
+
MAX_RETRIES=${maxRetries}
|
|
11051
|
+
MODEL="${model}"
|
|
11052
|
+
EPIC_IDS="${epicIds}"
|
|
11053
|
+
LOG_DIR="agent_logs"
|
|
11054
|
+
|
|
11055
|
+
# Helpers
|
|
11056
|
+
timestamp() { date '+%Y-%m-%d_%H-%M-%S'; }
|
|
11057
|
+
log() { echo "[$(timestamp)] $*"; }
|
|
11058
|
+
die() { log "FATAL: $*"; exit 1; }
|
|
11059
|
+
|
|
11060
|
+
command -v claude >/dev/null || die "claude CLI required"
|
|
11061
|
+
command -v bd >/dev/null || die "bd (beads) CLI required"
|
|
11062
|
+
|
|
11063
|
+
# Detect JSON parser: prefer jq, fall back to python3
|
|
11064
|
+
HAS_JQ=false
|
|
11065
|
+
command -v jq >/dev/null 2>&1 && HAS_JQ=true
|
|
11066
|
+
if [ "$HAS_JQ" = false ]; then
|
|
11067
|
+
command -v python3 >/dev/null 2>&1 || die "jq or python3 required for JSON parsing"
|
|
11068
|
+
fi
|
|
11069
|
+
|
|
11070
|
+
# parse_json() - extract a value from JSON stdin
|
|
11071
|
+
# Uses jq (primary) with python3 fallback
|
|
11072
|
+
# Auto-unwraps single-element arrays (bd show --json returns [...])
|
|
11073
|
+
# Usage: echo '[{"status":"open"}]' | parse_json '.status'
|
|
11074
|
+
parse_json() {
|
|
11075
|
+
local filter="$1"
|
|
11076
|
+
if [ "$HAS_JQ" = true ]; then
|
|
11077
|
+
jq -r "if type == \\"array\\" then .[0] else . end | $filter"
|
|
11078
|
+
else
|
|
11079
|
+
python3 -c "
|
|
11080
|
+
import sys, json
|
|
11081
|
+
data = json.load(sys.stdin)
|
|
11082
|
+
if isinstance(data, list):
|
|
11083
|
+
data = data[0] if data else {}
|
|
11084
|
+
f = '$filter'.strip('.')
|
|
11085
|
+
parts = [p for p in f.split('.') if p]
|
|
11086
|
+
v = data
|
|
11087
|
+
try:
|
|
11088
|
+
for p in parts:
|
|
11089
|
+
v = v[p]
|
|
11090
|
+
except (KeyError, IndexError, TypeError):
|
|
11091
|
+
v = None
|
|
11092
|
+
print('' if v is None else v)
|
|
11093
|
+
"
|
|
11094
|
+
fi
|
|
11095
|
+
}
|
|
11096
|
+
|
|
11097
|
+
mkdir -p "$LOG_DIR"
|
|
11098
|
+
` + buildEpicSelector() + buildPromptFunction();
|
|
11099
|
+
}
|
|
11100
|
+
function buildEpicSelector() {
|
|
11101
|
+
return `
|
|
11102
|
+
get_next_epic() {
|
|
11103
|
+
if [ -n "$EPIC_IDS" ]; then
|
|
11104
|
+
# From explicit list, find first still-open epic not yet processed
|
|
11105
|
+
for epic_id in $EPIC_IDS; do
|
|
11106
|
+
case " $PROCESSED " in *" $epic_id "*) continue ;; esac
|
|
11107
|
+
local status
|
|
11108
|
+
status=$(bd show "$epic_id" --json 2>/dev/null | parse_json '.status' 2>/dev/null || echo "")
|
|
11109
|
+
if [ "$status" = "open" ]; then
|
|
11110
|
+
echo "$epic_id"
|
|
11111
|
+
return 0
|
|
11112
|
+
fi
|
|
11113
|
+
done
|
|
11114
|
+
return 1
|
|
11115
|
+
else
|
|
11116
|
+
# Dynamic: get next ready epic from dependency graph, filtering processed
|
|
11117
|
+
local epic_id
|
|
11118
|
+
if [ "$HAS_JQ" = true ]; then
|
|
11119
|
+
epic_id=$(bd list --type=epic --ready --json --limit=10 2>/dev/null | jq -r '.[].id' 2>/dev/null | while read -r id; do
|
|
11120
|
+
case " $PROCESSED " in *" $id "*) continue ;; esac
|
|
11121
|
+
echo "$id"
|
|
11122
|
+
break
|
|
11123
|
+
done)
|
|
11124
|
+
else
|
|
11125
|
+
epic_id=$(bd list --type=epic --ready --json --limit=10 2>/dev/null | python3 -c "
|
|
11126
|
+
import sys, json
|
|
11127
|
+
processed = set('$PROCESSED'.split())
|
|
11128
|
+
items = json.load(sys.stdin)
|
|
11129
|
+
for item in items:
|
|
11130
|
+
if item['id'] not in processed:
|
|
11131
|
+
print(item['id'])
|
|
11132
|
+
break" 2>/dev/null || echo "")
|
|
11133
|
+
fi
|
|
11134
|
+
if [ -z "$epic_id" ]; then
|
|
11135
|
+
return 1
|
|
11136
|
+
fi
|
|
11137
|
+
echo "$epic_id"
|
|
11138
|
+
return 0
|
|
11139
|
+
fi
|
|
11140
|
+
}
|
|
11141
|
+
`;
|
|
11142
|
+
}
|
|
11143
|
+
function buildPromptFunction() {
|
|
11144
|
+
return `
|
|
11145
|
+
build_prompt() {
|
|
11146
|
+
local epic_id="$1"
|
|
11147
|
+
cat <<'PROMPT_HEADER'
|
|
11148
|
+
You are running in an autonomous infinity loop. Your task is to fully implement a beads epic.
|
|
11149
|
+
|
|
11150
|
+
## Step 1: Load context
|
|
11151
|
+
Run these commands to prime your session:
|
|
11152
|
+
PROMPT_HEADER
|
|
11153
|
+
cat <<PROMPT_BODY
|
|
11154
|
+
\\\`\\\`\\\`bash
|
|
11155
|
+
npx ca load-session
|
|
11156
|
+
bd show $epic_id
|
|
10633
11157
|
\\\`\\\`\\\`
|
|
10634
11158
|
|
|
10635
11159
|
Read the epic details carefully. Understand scope, acceptance criteria, and sub-tasks.
|
|
10636
11160
|
|
|
10637
11161
|
## Step 2: Execute the workflow
|
|
10638
11162
|
Run the full compound workflow for this epic, starting from the plan phase
|
|
10639
|
-
(
|
|
11163
|
+
(spec-dev is already done -- the epic exists):
|
|
10640
11164
|
|
|
10641
|
-
/compound:
|
|
11165
|
+
/compound:cook-it from plan -- Epic: $epic_id
|
|
10642
11166
|
|
|
10643
11167
|
Work through all phases: plan, work, review, compound.
|
|
10644
11168
|
|
|
@@ -10788,540 +11312,244 @@ while true; do
|
|
|
10788
11312
|
else
|
|
10789
11313
|
FAILED=$((FAILED + 1))
|
|
10790
11314
|
log "Epic $EPIC_ID failed after $((MAX_RETRIES + 1)) attempts. Stopping loop."
|
|
10791
|
-
PROCESSED="$PROCESSED $EPIC_ID"
|
|
10792
|
-
break
|
|
10793
|
-
fi
|
|
10794
|
-
|
|
10795
|
-
PROCESSED="$PROCESSED $EPIC_ID"
|
|
10796
|
-
done
|
|
10797
|
-
|
|
10798
|
-
log "Loop finished. Completed: $COMPLETED, Failed: $FAILED, Skipped: $SKIPPED"
|
|
10799
|
-
[ $FAILED -eq 0 ] && exit 0 || exit 1`;
|
|
10800
|
-
}
|
|
10801
|
-
function validateOptions(options) {
|
|
10802
|
-
if (!Number.isInteger(options.maxRetries) || options.maxRetries < 0) {
|
|
10803
|
-
throw new Error(`Invalid maxRetries: must be a non-negative integer, got ${options.maxRetries}`);
|
|
10804
|
-
}
|
|
10805
|
-
if (!MODEL_PATTERN.test(options.model)) {
|
|
10806
|
-
throw new Error(`Invalid model "${options.model}": must match ${MODEL_PATTERN}`);
|
|
10807
|
-
}
|
|
10808
|
-
if (options.epics) {
|
|
10809
|
-
for (const id of options.epics) {
|
|
10810
|
-
if (!LOOP_EPIC_ID_PATTERN.test(id)) {
|
|
10811
|
-
throw new Error(`Invalid epic ID "${id}": must match ${LOOP_EPIC_ID_PATTERN}`);
|
|
10812
|
-
}
|
|
10813
|
-
}
|
|
10814
|
-
}
|
|
10815
|
-
}
|
|
10816
|
-
function generateLoopScript(options) {
|
|
10817
|
-
validateOptions(options);
|
|
10818
|
-
const epicIds = options.epics?.join(" ") ?? "";
|
|
10819
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10820
|
-
return buildScriptHeader(timestamp, options.maxRetries, options.model, epicIds) + buildStreamExtractor() + buildMainLoop();
|
|
10821
|
-
}
|
|
10822
|
-
async function handleLoop(cmd, options) {
|
|
10823
|
-
const outputPath = resolve(options.output ?? "./infinity-loop.sh");
|
|
10824
|
-
if (existsSync(outputPath) && !options.force) {
|
|
10825
|
-
out.error(`File already exists: ${outputPath}`);
|
|
10826
|
-
out.info("Use --force to overwrite");
|
|
10827
|
-
process.exitCode = 1;
|
|
10828
|
-
return;
|
|
10829
|
-
}
|
|
10830
|
-
const maxRetries = Number(options.maxRetries ?? 1);
|
|
10831
|
-
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
10832
|
-
out.error(`Invalid --max-retries: must be a non-negative integer, got "${options.maxRetries}"`);
|
|
10833
|
-
process.exitCode = 1;
|
|
10834
|
-
return;
|
|
10835
|
-
}
|
|
10836
|
-
let script;
|
|
10837
|
-
try {
|
|
10838
|
-
script = generateLoopScript({
|
|
10839
|
-
epics: options.epics,
|
|
10840
|
-
maxRetries,
|
|
10841
|
-
model: options.model ?? "claude-opus-4-6"
|
|
10842
|
-
});
|
|
10843
|
-
} catch (err) {
|
|
10844
|
-
out.error(err.message);
|
|
10845
|
-
process.exitCode = 1;
|
|
10846
|
-
return;
|
|
10847
|
-
}
|
|
10848
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
10849
|
-
await writeFile(outputPath, script, "utf-8");
|
|
10850
|
-
await chmod(outputPath, 493);
|
|
10851
|
-
out.success(`Generated infinity loop script: ${outputPath}`);
|
|
10852
|
-
out.info("Run it with: " + outputPath);
|
|
10853
|
-
out.info("Preview with: LOOP_DRY_RUN=1 " + outputPath);
|
|
10854
|
-
}
|
|
10855
|
-
function registerLoopCommands(program2) {
|
|
10856
|
-
program2.command("loop").description("Generate infinity loop script for epic tasks").option("--epics <ids...>", "Specific epic IDs to process").option("-o, --output <path>", "Output script path", "./infinity-loop.sh").option("--max-retries <n>", "Max retries per epic on failure", "1").option("--model <model>", "Claude model to use", "claude-opus-4-6").option("--force", "Overwrite existing script").action(async function(options) {
|
|
10857
|
-
await handleLoop(this, options);
|
|
10858
|
-
});
|
|
10859
|
-
}
|
|
10860
|
-
var EPIC_ID_PATTERN2 = /^[a-zA-Z0-9_.-]+$/;
|
|
10861
|
-
function formatTime(timestamp) {
|
|
10862
|
-
if (!timestamp) {
|
|
10863
|
-
const now = /* @__PURE__ */ new Date();
|
|
10864
|
-
return now.toTimeString().slice(0, 8);
|
|
10865
|
-
}
|
|
10866
|
-
try {
|
|
10867
|
-
return new Date(timestamp).toTimeString().slice(0, 8);
|
|
10868
|
-
} catch {
|
|
10869
|
-
return (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
|
|
10870
|
-
}
|
|
10871
|
-
}
|
|
10872
|
-
function formatNumber(n) {
|
|
10873
|
-
return n.toLocaleString();
|
|
10874
|
-
}
|
|
10875
|
-
function formatStreamEvent(event) {
|
|
10876
|
-
const time = chalk4.dim(formatTime(event.timestamp));
|
|
10877
|
-
switch (event.type) {
|
|
10878
|
-
case "content_block_start": {
|
|
10879
|
-
if (event.content_block?.type === "tool_use") {
|
|
10880
|
-
const name = event.content_block.name ?? "unknown";
|
|
10881
|
-
return `${time} ${chalk4.cyan("TOOL")} ${name}`;
|
|
10882
|
-
}
|
|
10883
|
-
if (event.content_block?.type === "thinking") {
|
|
10884
|
-
return `${time} ${chalk4.magenta("THINK")} thinking...`;
|
|
10885
|
-
}
|
|
10886
|
-
return null;
|
|
10887
|
-
}
|
|
10888
|
-
case "content_block_delta": {
|
|
10889
|
-
if (event.delta?.type === "text_delta") {
|
|
10890
|
-
const text = event.delta.text ?? "";
|
|
10891
|
-
const truncated = text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
10892
|
-
return `${time} ${chalk4.blue("TEXT")} ${truncated.replace(/\n/g, " ")}`;
|
|
10893
|
-
}
|
|
10894
|
-
return null;
|
|
10895
|
-
}
|
|
10896
|
-
case "message_delta": {
|
|
10897
|
-
const usage = event.usage;
|
|
10898
|
-
if (usage?.output_tokens) {
|
|
10899
|
-
return `${time} ${chalk4.dim("TOKENS")} ${formatNumber(usage.output_tokens)} out (final)`;
|
|
10900
|
-
}
|
|
10901
|
-
return null;
|
|
10902
|
-
}
|
|
10903
|
-
case "message_start": {
|
|
10904
|
-
if (event.message?.usage) {
|
|
10905
|
-
const { input_tokens, output_tokens } = event.message.usage;
|
|
10906
|
-
const inTok = input_tokens ? formatNumber(input_tokens) : "?";
|
|
10907
|
-
const outTok = output_tokens ? formatNumber(output_tokens) : "?";
|
|
10908
|
-
return `${time} ${chalk4.dim("TOKENS")} ${inTok} in / ${outTok} out`;
|
|
10909
|
-
}
|
|
10910
|
-
return null;
|
|
10911
|
-
}
|
|
10912
|
-
case "result": {
|
|
10913
|
-
const text = typeof event.result === "string" ? event.result : "";
|
|
10914
|
-
const markers = ["EPIC_COMPLETE", "EPIC_FAILED", "HUMAN_REQUIRED"];
|
|
10915
|
-
const found = markers.find((m) => text.includes(m));
|
|
10916
|
-
if (found) {
|
|
10917
|
-
const markerLine = text.split("\n").find((l) => l.includes(found)) ?? found;
|
|
10918
|
-
const display = markerLine.length > 120 ? markerLine.slice(0, 117) + "..." : markerLine;
|
|
10919
|
-
return `${time} ${chalk4.yellow.bold("MARKER")} ${display}`;
|
|
10920
|
-
}
|
|
10921
|
-
return null;
|
|
10922
|
-
}
|
|
10923
|
-
default:
|
|
10924
|
-
return null;
|
|
10925
|
-
}
|
|
10926
|
-
}
|
|
10927
|
-
function findLatestTraceFile(logDir) {
|
|
10928
|
-
if (!existsSync(logDir)) return null;
|
|
10929
|
-
const latestPath = join(logDir, ".latest");
|
|
10930
|
-
if (existsSync(latestPath)) {
|
|
10931
|
-
try {
|
|
10932
|
-
const target = readlinkSync(latestPath);
|
|
10933
|
-
const resolved = resolve(logDir, target);
|
|
10934
|
-
if (existsSync(resolved)) return resolved;
|
|
10935
|
-
} catch {
|
|
10936
|
-
}
|
|
10937
|
-
}
|
|
10938
|
-
try {
|
|
10939
|
-
const files = readdirSync(logDir).filter((f) => f.startsWith("trace_") && f.endsWith(".jsonl")).sort().reverse();
|
|
10940
|
-
const first = files[0];
|
|
10941
|
-
if (first) return join(logDir, first);
|
|
10942
|
-
} catch {
|
|
10943
|
-
}
|
|
10944
|
-
return null;
|
|
10945
|
-
}
|
|
10946
|
-
function processLine(line) {
|
|
10947
|
-
const trimmed = line.trim();
|
|
10948
|
-
if (!trimmed) return;
|
|
10949
|
-
try {
|
|
10950
|
-
const event = JSON.parse(trimmed);
|
|
10951
|
-
const formatted = formatStreamEvent(event);
|
|
10952
|
-
if (formatted) {
|
|
10953
|
-
console.log(formatted);
|
|
10954
|
-
}
|
|
10955
|
-
} catch {
|
|
10956
|
-
}
|
|
11315
|
+
PROCESSED="$PROCESSED $EPIC_ID"
|
|
11316
|
+
break
|
|
11317
|
+
fi
|
|
11318
|
+
|
|
11319
|
+
PROCESSED="$PROCESSED $EPIC_ID"
|
|
11320
|
+
done
|
|
11321
|
+
|
|
11322
|
+
log "Loop finished. Completed: $COMPLETED, Failed: $FAILED, Skipped: $SKIPPED"
|
|
11323
|
+
[ $FAILED -eq 0 ] && exit 0 || exit 1`;
|
|
10957
11324
|
}
|
|
10958
|
-
|
|
10959
|
-
if (
|
|
10960
|
-
|
|
10961
|
-
const rl2 = createInterface({ input: child.stdout });
|
|
10962
|
-
rl2.on("line", processLine);
|
|
10963
|
-
const cleanup2 = () => {
|
|
10964
|
-
child.kill("SIGTERM");
|
|
10965
|
-
};
|
|
10966
|
-
process.on("SIGINT", cleanup2);
|
|
10967
|
-
process.on("SIGTERM", cleanup2);
|
|
10968
|
-
return new Promise((done) => {
|
|
10969
|
-
child.on("close", () => {
|
|
10970
|
-
process.off("SIGINT", cleanup2);
|
|
10971
|
-
process.off("SIGTERM", cleanup2);
|
|
10972
|
-
done();
|
|
10973
|
-
});
|
|
10974
|
-
});
|
|
10975
|
-
}
|
|
10976
|
-
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
10977
|
-
const rl = createInterface({ input: stream });
|
|
10978
|
-
try {
|
|
10979
|
-
for await (const line of rl) {
|
|
10980
|
-
processLine(line);
|
|
10981
|
-
}
|
|
10982
|
-
} finally {
|
|
10983
|
-
rl.close();
|
|
10984
|
-
stream.destroy();
|
|
11325
|
+
function validateOptions(options) {
|
|
11326
|
+
if (!Number.isInteger(options.maxRetries) || options.maxRetries < 0) {
|
|
11327
|
+
throw new Error(`Invalid maxRetries: must be a non-negative integer, got ${options.maxRetries}`);
|
|
10985
11328
|
}
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
let logDir;
|
|
10989
|
-
try {
|
|
10990
|
-
logDir = join(getRepoRoot(), "agent_logs");
|
|
10991
|
-
} catch {
|
|
10992
|
-
logDir = resolve("agent_logs");
|
|
11329
|
+
if (!MODEL_PATTERN.test(options.model)) {
|
|
11330
|
+
throw new Error(`Invalid model "${options.model}": must match ${MODEL_PATTERN}`);
|
|
10993
11331
|
}
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
out.error(`Invalid epic ID: ${options.epic}`);
|
|
10999
|
-
process.exitCode = 1;
|
|
11000
|
-
return;
|
|
11001
|
-
}
|
|
11002
|
-
if (existsSync(logDir)) {
|
|
11003
|
-
try {
|
|
11004
|
-
const files = readdirSync(logDir).filter((f) => f.startsWith(`trace_${options.epic}`) && f.endsWith(".jsonl")).sort().reverse();
|
|
11005
|
-
const first = files[0];
|
|
11006
|
-
if (first) traceFile = join(logDir, first);
|
|
11007
|
-
} catch {
|
|
11332
|
+
if (options.epics) {
|
|
11333
|
+
for (const id of options.epics) {
|
|
11334
|
+
if (!LOOP_EPIC_ID_PATTERN.test(id)) {
|
|
11335
|
+
throw new Error(`Invalid epic ID "${id}": must match ${LOOP_EPIC_ID_PATTERN}`);
|
|
11008
11336
|
}
|
|
11009
11337
|
}
|
|
11010
|
-
if (!traceFile) {
|
|
11011
|
-
out.error(`No trace file found for epic: ${options.epic}`);
|
|
11012
|
-
process.exitCode = 1;
|
|
11013
|
-
return;
|
|
11014
|
-
}
|
|
11015
|
-
} else {
|
|
11016
|
-
traceFile = findLatestTraceFile(logDir);
|
|
11017
|
-
if (!traceFile) {
|
|
11018
|
-
out.info("No active trace found. Run `ca loop` to generate a loop script first.");
|
|
11019
|
-
process.exitCode = 0;
|
|
11020
|
-
return;
|
|
11021
|
-
}
|
|
11022
|
-
}
|
|
11023
|
-
out.info(`Watching: ${traceFile}`);
|
|
11024
|
-
await tailFile(traceFile, follow);
|
|
11025
|
-
}
|
|
11026
|
-
function registerWatchCommand(program2) {
|
|
11027
|
-
program2.command("watch").description("Tail and pretty-print live trace from infinity loop sessions").option("--epic <id>", "Watch a specific epic trace").option("--no-follow", "Print existing trace and exit (no live tail)").action(async function(options) {
|
|
11028
|
-
await handleWatch(this, options);
|
|
11029
|
-
});
|
|
11030
|
-
}
|
|
11031
|
-
init_storage();
|
|
11032
|
-
init_search2();
|
|
11033
|
-
function parseLimitOrNull(rawLimit, optionName, commandName) {
|
|
11034
|
-
try {
|
|
11035
|
-
return parseLimit(rawLimit, optionName);
|
|
11036
|
-
} catch (err) {
|
|
11037
|
-
const message = err instanceof Error ? err.message : `Invalid ${optionName}`;
|
|
11038
|
-
console.error(formatError(commandName, "INVALID_LIMIT", message, `Use --${optionName} with a positive integer`));
|
|
11039
|
-
return null;
|
|
11040
|
-
}
|
|
11041
|
-
}
|
|
11042
|
-
async function readPlanFromStdin() {
|
|
11043
|
-
const { stdin } = await import('process');
|
|
11044
|
-
if (!stdin.isTTY) {
|
|
11045
|
-
const chunks = [];
|
|
11046
|
-
for await (const chunk of stdin) {
|
|
11047
|
-
chunks.push(chunk);
|
|
11048
|
-
}
|
|
11049
|
-
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
11050
|
-
}
|
|
11051
|
-
return void 0;
|
|
11052
|
-
}
|
|
11053
|
-
function outputCheckPlanJson(lessons) {
|
|
11054
|
-
const jsonOutput = {
|
|
11055
|
-
lessons: lessons.map((l) => ({
|
|
11056
|
-
id: l.lesson.id,
|
|
11057
|
-
insight: l.lesson.insight,
|
|
11058
|
-
rankScore: l.finalScore ?? l.score,
|
|
11059
|
-
// Use finalScore if available, fallback to raw score
|
|
11060
|
-
source: l.lesson.source
|
|
11061
|
-
})),
|
|
11062
|
-
count: lessons.length
|
|
11063
|
-
};
|
|
11064
|
-
console.log(JSON.stringify(jsonOutput));
|
|
11065
|
-
}
|
|
11066
|
-
function outputCheckPlanHuman(lessons, quiet) {
|
|
11067
|
-
console.log("## Lessons Check\n");
|
|
11068
|
-
console.log("Relevant to your plan:\n");
|
|
11069
|
-
lessons.forEach((item, i) => {
|
|
11070
|
-
const num = i + 1;
|
|
11071
|
-
console.log(`${num}. ${chalk4.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
|
|
11072
|
-
console.log(` - Source: ${item.lesson.source}`);
|
|
11073
|
-
console.log();
|
|
11074
|
-
});
|
|
11075
|
-
if (!quiet) {
|
|
11076
|
-
console.log("---");
|
|
11077
|
-
console.log("Consider these lessons while implementing.");
|
|
11078
11338
|
}
|
|
11079
11339
|
}
|
|
11080
|
-
function
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
console.log("These lessons were captured from previous corrections and should inform your work:\n");
|
|
11086
|
-
lessons.forEach((lesson, i) => {
|
|
11087
|
-
const num = i + 1;
|
|
11088
|
-
const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
|
|
11089
|
-
const tagsDisplay = lesson.tags.length > 0 ? ` (${lesson.tags.join(", ")})` : "";
|
|
11090
|
-
console.log(`${num}. **${lesson.insight}**${tagsDisplay}`);
|
|
11091
|
-
console.log(` Learned: ${date} via ${formatSource2(lesson.source)}`);
|
|
11092
|
-
console.log();
|
|
11093
|
-
});
|
|
11094
|
-
if (!quiet) {
|
|
11095
|
-
console.log("Consider these lessons when planning and implementing tasks.");
|
|
11096
|
-
}
|
|
11340
|
+
function generateLoopScript(options) {
|
|
11341
|
+
validateOptions(options);
|
|
11342
|
+
const epicIds = options.epics?.join(" ") ?? "";
|
|
11343
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11344
|
+
return buildScriptHeader(timestamp, options.maxRetries, options.model, epicIds) + buildStreamExtractor() + buildMainLoop();
|
|
11097
11345
|
}
|
|
11098
|
-
async function
|
|
11099
|
-
const
|
|
11100
|
-
|
|
11101
|
-
|
|
11346
|
+
async function handleLoop(cmd, options) {
|
|
11347
|
+
const outputPath = resolve(options.output ?? "./infinity-loop.sh");
|
|
11348
|
+
if (existsSync(outputPath) && !options.force) {
|
|
11349
|
+
out.error(`File already exists: ${outputPath}`);
|
|
11350
|
+
out.info("Use --force to overwrite");
|
|
11102
11351
|
process.exitCode = 1;
|
|
11103
11352
|
return;
|
|
11104
11353
|
}
|
|
11105
|
-
const
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
const candidateLimit = limit * CANDIDATE_MULTIPLIER;
|
|
11111
|
-
const [vectorResults, keywordResults] = await Promise.all([
|
|
11112
|
-
searchVector(repoRoot, query, { limit: candidateLimit }),
|
|
11113
|
-
searchKeywordScored(repoRoot, query, candidateLimit)
|
|
11114
|
-
]);
|
|
11115
|
-
const merged = mergeHybridResults(vectorResults, keywordResults, { minScore: MIN_HYBRID_SCORE });
|
|
11116
|
-
const ranked = rankLessons(merged);
|
|
11117
|
-
results = ranked.slice(0, limit).map((r) => r.lesson);
|
|
11118
|
-
} catch {
|
|
11119
|
-
results = await searchKeyword(repoRoot, query, limit);
|
|
11120
|
-
}
|
|
11121
|
-
} else {
|
|
11122
|
-
results = await searchKeyword(repoRoot, query, limit);
|
|
11123
|
-
}
|
|
11124
|
-
if (results.length > 0) {
|
|
11125
|
-
incrementRetrievalCount(repoRoot, results.map((lesson) => lesson.id));
|
|
11354
|
+
const maxRetries = Number(options.maxRetries ?? 1);
|
|
11355
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
11356
|
+
out.error(`Invalid --max-retries: must be a non-negative integer, got "${options.maxRetries}"`);
|
|
11357
|
+
process.exitCode = 1;
|
|
11358
|
+
return;
|
|
11126
11359
|
}
|
|
11127
|
-
|
|
11128
|
-
|
|
11360
|
+
let script;
|
|
11361
|
+
try {
|
|
11362
|
+
script = generateLoopScript({
|
|
11363
|
+
epics: options.epics,
|
|
11364
|
+
maxRetries,
|
|
11365
|
+
model: options.model ?? "claude-opus-4-6"
|
|
11366
|
+
});
|
|
11367
|
+
} catch (err) {
|
|
11368
|
+
out.error(err.message);
|
|
11369
|
+
process.exitCode = 1;
|
|
11129
11370
|
return;
|
|
11130
11371
|
}
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11372
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
11373
|
+
await writeFile(outputPath, script, "utf-8");
|
|
11374
|
+
await chmod(outputPath, 493);
|
|
11375
|
+
out.success(`Generated infinity loop script: ${outputPath}`);
|
|
11376
|
+
out.info("Run it with: " + outputPath);
|
|
11377
|
+
out.info("Preview with: LOOP_DRY_RUN=1 " + outputPath);
|
|
11378
|
+
}
|
|
11379
|
+
function registerLoopCommands(program2) {
|
|
11380
|
+
program2.command("loop").description("Generate infinity loop script for epic tasks").option("--epics <ids...>", "Specific epic IDs to process").option("-o, --output <path>", "Output script path", "./infinity-loop.sh").option("--max-retries <n>", "Max retries per epic on failure", "1").option("--model <model>", "Claude model to use", "claude-opus-4-6").option("--force", "Overwrite existing script").action(async function(options) {
|
|
11381
|
+
await handleLoop(this, options);
|
|
11382
|
+
});
|
|
11383
|
+
}
|
|
11384
|
+
var EPIC_ID_PATTERN2 = /^[a-zA-Z0-9_.-]+$/;
|
|
11385
|
+
function formatTime(timestamp) {
|
|
11386
|
+
if (!timestamp) {
|
|
11387
|
+
const now = /* @__PURE__ */ new Date();
|
|
11388
|
+
return now.toTimeString().slice(0, 8);
|
|
11134
11389
|
}
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
11140
|
-
console.log(` Created: ${lesson.created}`);
|
|
11141
|
-
}
|
|
11142
|
-
if (lesson.tags.length > 0) {
|
|
11143
|
-
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
11144
|
-
}
|
|
11145
|
-
console.log();
|
|
11390
|
+
try {
|
|
11391
|
+
return new Date(timestamp).toTimeString().slice(0, 8);
|
|
11392
|
+
} catch {
|
|
11393
|
+
return (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
|
|
11146
11394
|
}
|
|
11147
11395
|
}
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11396
|
+
function formatNumber(n) {
|
|
11397
|
+
return n.toLocaleString();
|
|
11398
|
+
}
|
|
11399
|
+
function formatStreamEvent(event) {
|
|
11400
|
+
const time = chalk5.dim(formatTime(event.timestamp));
|
|
11401
|
+
switch (event.type) {
|
|
11402
|
+
case "content_block_start": {
|
|
11403
|
+
if (event.content_block?.type === "tool_use") {
|
|
11404
|
+
const name = event.content_block.name ?? "unknown";
|
|
11405
|
+
return `${time} ${chalk5.cyan("TOOL")} ${name}`;
|
|
11406
|
+
}
|
|
11407
|
+
if (event.content_block?.type === "thinking") {
|
|
11408
|
+
return `${time} ${chalk5.magenta("THINK")} thinking...`;
|
|
11409
|
+
}
|
|
11410
|
+
return null;
|
|
11163
11411
|
}
|
|
11164
|
-
|
|
11165
|
-
|
|
11412
|
+
case "content_block_delta": {
|
|
11413
|
+
if (event.delta?.type === "text_delta") {
|
|
11414
|
+
const text = event.delta.text ?? "";
|
|
11415
|
+
const truncated = text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
11416
|
+
return `${time} ${chalk5.blue("TEXT")} ${truncated.replace(/\n/g, " ")}`;
|
|
11417
|
+
}
|
|
11418
|
+
return null;
|
|
11166
11419
|
}
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
const label = options.invalidated ? "invalidated lesson(s)" : "item(s)";
|
|
11172
|
-
out.info(`Showing ${toShow.length} of ${filteredItems.length} ${label}:
|
|
11173
|
-
`);
|
|
11174
|
-
}
|
|
11175
|
-
for (const item of toShow) {
|
|
11176
|
-
const invalidMarker = item.invalidatedAt ? chalk4.red("[INVALID] ") : "";
|
|
11177
|
-
console.log(`[${chalk4.cyan(item.id)}] ${invalidMarker}${item.insight}`);
|
|
11178
|
-
if (verbose) {
|
|
11179
|
-
console.log(` Type: ${item.type} | Source: ${item.source}`);
|
|
11180
|
-
console.log(` Created: ${item.created}`);
|
|
11181
|
-
if (item.context) {
|
|
11182
|
-
console.log(` Context: ${item.context.tool} - ${item.context.intent}`);
|
|
11420
|
+
case "message_delta": {
|
|
11421
|
+
const usage = event.usage;
|
|
11422
|
+
if (usage?.output_tokens) {
|
|
11423
|
+
return `${time} ${chalk5.dim("TOKENS")} ${formatNumber(usage.output_tokens)} out (final)`;
|
|
11183
11424
|
}
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
}
|
|
11425
|
+
return null;
|
|
11426
|
+
}
|
|
11427
|
+
case "message_start": {
|
|
11428
|
+
if (event.message?.usage) {
|
|
11429
|
+
const { input_tokens, output_tokens } = event.message.usage;
|
|
11430
|
+
const inTok = input_tokens ? formatNumber(input_tokens) : "?";
|
|
11431
|
+
const outTok = output_tokens ? formatNumber(output_tokens) : "?";
|
|
11432
|
+
return `${time} ${chalk5.dim("TOKENS")} ${inTok} in / ${outTok} out`;
|
|
11189
11433
|
}
|
|
11190
|
-
|
|
11191
|
-
console.log(` Type: ${item.type} | Source: ${item.source}`);
|
|
11434
|
+
return null;
|
|
11192
11435
|
}
|
|
11193
|
-
|
|
11194
|
-
|
|
11436
|
+
case "result": {
|
|
11437
|
+
const text = typeof event.result === "string" ? event.result : "";
|
|
11438
|
+
const markers = ["EPIC_COMPLETE", "EPIC_FAILED", "HUMAN_REQUIRED"];
|
|
11439
|
+
const found = markers.find((m) => text.includes(m));
|
|
11440
|
+
if (found) {
|
|
11441
|
+
const markerLine = text.split("\n").find((l) => l.includes(found)) ?? found;
|
|
11442
|
+
const display = markerLine.length > 120 ? markerLine.slice(0, 117) + "..." : markerLine;
|
|
11443
|
+
return `${time} ${chalk5.yellow.bold("MARKER")} ${display}`;
|
|
11444
|
+
}
|
|
11445
|
+
return null;
|
|
11195
11446
|
}
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
if (skippedCount > 0) {
|
|
11199
|
-
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
11447
|
+
default:
|
|
11448
|
+
return null;
|
|
11200
11449
|
}
|
|
11201
11450
|
}
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
const
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
if (lessons.length === 0) {
|
|
11213
|
-
console.log("No high-severity lessons found.");
|
|
11214
|
-
return;
|
|
11215
|
-
}
|
|
11216
|
-
outputSessionLessonsHuman(lessons, quiet);
|
|
11217
|
-
if (totalCount > LESSON_COUNT_WARNING_THRESHOLD) {
|
|
11218
|
-
console.log("");
|
|
11219
|
-
out.info(`${totalCount} lessons in index. Consider \`ca compact\` to reduce context pollution.`);
|
|
11451
|
+
function findLatestTraceFile(logDir) {
|
|
11452
|
+
if (!existsSync(logDir)) return null;
|
|
11453
|
+
const latestPath = join(logDir, ".latest");
|
|
11454
|
+
if (existsSync(latestPath)) {
|
|
11455
|
+
try {
|
|
11456
|
+
const target = readlinkSync(latestPath);
|
|
11457
|
+
const resolved = resolve(logDir, target);
|
|
11458
|
+
if (existsSync(resolved)) return resolved;
|
|
11459
|
+
} catch {
|
|
11460
|
+
}
|
|
11220
11461
|
}
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11462
|
+
try {
|
|
11463
|
+
const files = readdirSync(logDir).filter((f) => f.startsWith("trace_") && f.endsWith(".jsonl")).sort().reverse();
|
|
11464
|
+
const first = files[0];
|
|
11465
|
+
if (first) return join(logDir, first);
|
|
11466
|
+
} catch {
|
|
11225
11467
|
}
|
|
11468
|
+
return null;
|
|
11226
11469
|
}
|
|
11227
|
-
|
|
11228
|
-
const
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11470
|
+
function processLine(line) {
|
|
11471
|
+
const trimmed = line.trim();
|
|
11472
|
+
if (!trimmed) return;
|
|
11473
|
+
try {
|
|
11474
|
+
const event = JSON.parse(trimmed);
|
|
11475
|
+
const formatted = formatStreamEvent(event);
|
|
11476
|
+
if (formatted) {
|
|
11477
|
+
console.log(formatted);
|
|
11478
|
+
}
|
|
11479
|
+
} catch {
|
|
11233
11480
|
}
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
if (
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11481
|
+
}
|
|
11482
|
+
async function tailFile(filePath, follow) {
|
|
11483
|
+
if (follow) {
|
|
11484
|
+
const child = spawn("tail", ["-f", "-n", "+1", filePath], { stdio: ["ignore", "pipe", "ignore"] });
|
|
11485
|
+
const rl2 = createInterface({ input: child.stdout });
|
|
11486
|
+
rl2.on("line", processLine);
|
|
11487
|
+
const cleanup2 = () => {
|
|
11488
|
+
child.kill("SIGTERM");
|
|
11489
|
+
};
|
|
11490
|
+
process.on("SIGINT", cleanup2);
|
|
11491
|
+
process.on("SIGTERM", cleanup2);
|
|
11492
|
+
return new Promise((done) => {
|
|
11493
|
+
child.on("close", () => {
|
|
11494
|
+
process.off("SIGINT", cleanup2);
|
|
11495
|
+
process.off("SIGTERM", cleanup2);
|
|
11496
|
+
done();
|
|
11497
|
+
});
|
|
11498
|
+
});
|
|
11240
11499
|
}
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
count: 0,
|
|
11247
|
-
error: "Embedding model not found",
|
|
11248
|
-
action: "Run: npx ca download-model"
|
|
11249
|
-
}));
|
|
11250
|
-
} else {
|
|
11251
|
-
console.error(formatError("check-plan", "MODEL_UNAVAILABLE", "Embedding model not found", "Run: npx ca download-model"));
|
|
11500
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
11501
|
+
const rl = createInterface({ input: stream });
|
|
11502
|
+
try {
|
|
11503
|
+
for await (const line of rl) {
|
|
11504
|
+
processLine(line);
|
|
11252
11505
|
}
|
|
11253
|
-
|
|
11254
|
-
|
|
11506
|
+
} finally {
|
|
11507
|
+
rl.close();
|
|
11508
|
+
stream.destroy();
|
|
11255
11509
|
}
|
|
11510
|
+
}
|
|
11511
|
+
async function handleWatch(cmd, options) {
|
|
11512
|
+
let logDir;
|
|
11256
11513
|
try {
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11514
|
+
logDir = join(getRepoRoot(), "agent_logs");
|
|
11515
|
+
} catch {
|
|
11516
|
+
logDir = resolve("agent_logs");
|
|
11517
|
+
}
|
|
11518
|
+
const follow = options.follow !== false;
|
|
11519
|
+
let traceFile = null;
|
|
11520
|
+
if (options.epic) {
|
|
11521
|
+
if (!EPIC_ID_PATTERN2.test(options.epic)) {
|
|
11522
|
+
out.error(`Invalid epic ID: ${options.epic}`);
|
|
11523
|
+
process.exitCode = 1;
|
|
11260
11524
|
return;
|
|
11261
11525
|
}
|
|
11262
|
-
if (
|
|
11263
|
-
|
|
11526
|
+
if (existsSync(logDir)) {
|
|
11527
|
+
try {
|
|
11528
|
+
const files = readdirSync(logDir).filter((f) => f.startsWith(`trace_${options.epic}`) && f.endsWith(".jsonl")).sort().reverse();
|
|
11529
|
+
const first = files[0];
|
|
11530
|
+
if (first) traceFile = join(logDir, first);
|
|
11531
|
+
} catch {
|
|
11532
|
+
}
|
|
11533
|
+
}
|
|
11534
|
+
if (!traceFile) {
|
|
11535
|
+
out.error(`No trace file found for epic: ${options.epic}`);
|
|
11536
|
+
process.exitCode = 1;
|
|
11264
11537
|
return;
|
|
11265
11538
|
}
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
count: 0,
|
|
11273
|
-
error: message
|
|
11274
|
-
}));
|
|
11275
|
-
} else {
|
|
11276
|
-
console.error(formatError("check-plan", "PLAN_CHECK_FAILED", message, "Check model installation and try again"));
|
|
11539
|
+
} else {
|
|
11540
|
+
traceFile = findLatestTraceFile(logDir);
|
|
11541
|
+
if (!traceFile) {
|
|
11542
|
+
out.info("No active trace found. Run `ca loop` to generate a loop script first.");
|
|
11543
|
+
process.exitCode = 0;
|
|
11544
|
+
return;
|
|
11277
11545
|
}
|
|
11278
|
-
process.exitCode = 1;
|
|
11279
11546
|
}
|
|
11547
|
+
out.info(`Watching: ${traceFile}`);
|
|
11548
|
+
await tailFile(traceFile, follow);
|
|
11280
11549
|
}
|
|
11281
|
-
function
|
|
11282
|
-
program2.command("
|
|
11283
|
-
await
|
|
11284
|
-
});
|
|
11285
|
-
program2.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).option("--invalidated", "Show only invalidated lessons").action(async function(options) {
|
|
11286
|
-
await listAction(this, options);
|
|
11287
|
-
});
|
|
11288
|
-
program2.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
11289
|
-
await loadSessionAction(this, options);
|
|
11290
|
-
});
|
|
11291
|
-
program2.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
|
|
11292
|
-
await checkPlanAction(this, options);
|
|
11293
|
-
});
|
|
11294
|
-
}
|
|
11295
|
-
|
|
11296
|
-
// src/commands/index.ts
|
|
11297
|
-
function registerSetupCommands(program2) {
|
|
11298
|
-
registerInitCommand(program2);
|
|
11299
|
-
registerHooksCommand(program2);
|
|
11300
|
-
const setupCommand = program2.command("setup");
|
|
11301
|
-
registerSetupAllCommand(setupCommand);
|
|
11302
|
-
registerClaudeSubcommand(setupCommand);
|
|
11303
|
-
registerGeminiSubcommand(setupCommand);
|
|
11304
|
-
registerDownloadModelCommand(program2);
|
|
11305
|
-
}
|
|
11306
|
-
function registerManagementCommands(program2) {
|
|
11307
|
-
registerInvalidationCommands(program2);
|
|
11308
|
-
registerMaintenanceCommands(program2);
|
|
11309
|
-
registerIOCommands(program2);
|
|
11310
|
-
registerPrimeCommand(program2);
|
|
11311
|
-
registerCrudCommands(program2);
|
|
11312
|
-
registerAuditCommands(program2);
|
|
11313
|
-
registerDoctorCommand(program2);
|
|
11314
|
-
registerReviewerCommand(program2);
|
|
11315
|
-
registerRulesCommands(program2);
|
|
11316
|
-
registerTestSummaryCommand(program2);
|
|
11317
|
-
registerVerifyGatesCommand(program2);
|
|
11318
|
-
registerAboutCommand(program2);
|
|
11319
|
-
registerKnowledgeCommand(program2);
|
|
11320
|
-
registerKnowledgeIndexCommand(program2);
|
|
11321
|
-
registerCleanLessonsCommand(program2);
|
|
11322
|
-
program2.command("worktree").description("(removed) Use Claude Code native worktree support").action(() => {
|
|
11323
|
-
console.error("ca worktree has been removed. Use Claude Code's native EnterWorktree support instead.");
|
|
11324
|
-
process.exitCode = 1;
|
|
11550
|
+
function registerWatchCommand(program2) {
|
|
11551
|
+
program2.command("watch").description("Tail and pretty-print live trace from infinity loop sessions").option("--epic <id>", "Watch a specific epic trace").option("--no-follow", "Print existing trace and exit (no live tail)").action(async function(options) {
|
|
11552
|
+
await handleWatch(this, options);
|
|
11325
11553
|
});
|
|
11326
11554
|
}
|
|
11327
11555
|
|