mustard-claude 3.1.1 → 3.1.3

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/bin/mustard.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "mustard-claude",
3
- "version": "3.1.1",
4
- "description": "Framework-agnostic CLI for Claude Code project setup",
5
- "type": "module",
6
- "bin": {
7
- "mustard": "./bin/mustard.js"
8
- },
9
- "main": "dist/cli.js",
10
- "types": "dist/cli.d.ts",
11
- "files": [
12
- "bin/",
13
- "dist/",
14
- "templates/"
15
- ],
16
- "keywords": [
17
- "claude",
18
- "claude-code",
19
- "ai",
20
- "cli",
21
- "scaffold"
22
- ],
23
- "author": "rubensrpj",
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/rubensrpj/mustard.git"
28
- },
29
- "homepage": "https://github.com/rubensrpj/mustard#readme",
30
- "bugs": {
31
- "url": "https://github.com/rubensrpj/mustard/issues"
32
- },
33
- "publishConfig": {
34
- "access": "public"
35
- },
36
- "dependencies": {
37
- "chalk": "^5.3.0",
38
- "commander": "^12.1.0",
39
- "inquirer": "^9.2.15",
40
- "ora": "^8.0.1"
41
- },
42
- "engines": {
43
- "node": ">=18.0.0"
44
- },
45
- "devDependencies": {
46
- "@types/inquirer": "^9.0.9",
47
- "@types/node": "^25.2.1",
48
- "rimraf": "^6.1.2",
49
- "typescript": "^5.9.3"
50
- },
51
- "scripts": {
52
- "start": "node bin/mustard.js",
53
- "build": "tsc",
54
- "clean": "rimraf dist",
55
- "typecheck": "tsc --noEmit",
56
- "test": "node --test",
57
- "release": "npm version patch && npm run build && npm publish"
58
- }
59
- }
1
+ {
2
+ "name": "mustard-claude",
3
+ "version": "3.1.3",
4
+ "description": "Framework-agnostic CLI for Claude Code project setup",
5
+ "type": "module",
6
+ "bin": {
7
+ "mustard": "./bin/mustard.js"
8
+ },
9
+ "main": "dist/cli.js",
10
+ "types": "dist/cli.d.ts",
11
+ "files": [
12
+ "bin/",
13
+ "dist/",
14
+ "templates/"
15
+ ],
16
+ "scripts": {
17
+ "start": "node bin/mustard.js",
18
+ "build": "tsc",
19
+ "clean": "rimraf dist",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "node --test",
22
+ "release": "npm version patch && npm run build && npm publish"
23
+ },
24
+ "keywords": [
25
+ "claude",
26
+ "claude-code",
27
+ "ai",
28
+ "cli",
29
+ "scaffold"
30
+ ],
31
+ "author": "rubensrpj",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/rubensrpj/mustard.git"
36
+ },
37
+ "homepage": "https://github.com/rubensrpj/mustard#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/rubensrpj/mustard/issues"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "chalk": "^5.3.0",
46
+ "commander": "^12.1.0",
47
+ "inquirer": "^9.2.15",
48
+ "ora": "^8.0.1"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/inquirer": "^9.0.9",
55
+ "@types/node": "^25.2.1",
56
+ "rimraf": "^6.1.2",
57
+ "typescript": "^5.9.3"
58
+ }
59
+ }
@@ -31,7 +31,7 @@ Guards always loaded via `{subproject}/CLAUDE.md`.
31
31
 
32
32
  ## Stack
33
33
 
34
- Node.js (>=18), CommonJS, no external dependencies. 8 lifecycle hooks, 3 sync scripts, 14 slash commands, 6 foundation skills.
34
+ Node.js (>=18), CommonJS, no external dependencies. 16 lifecycle hooks, 10 scripts, 16 slash commands, 6 foundation skills.
35
35
 
36
36
  ## Commands
37
37
 
@@ -55,6 +55,7 @@ node scripts/sync-registry.js --force
55
55
  - PreToolUse hooks use `permissionDecision` response format
56
56
  - PostToolUse hooks use `decision` response format
57
57
  - Every new hook must be registered in `settings.json` with a timeout
58
+ - Task dispatch failures (API overload) are logged to `pipeline-state.lastDispatchFailure`; `/resume` auto-recovers within 10 min
58
59
  - Generated files must start with `<!-- mustard:generated -->` header
59
60
  - Skills must have YAML frontmatter BEFORE the `<!-- mustard:generated -->` line
60
61
 
@@ -155,7 +155,7 @@ When user chooses "Approve and implement now":
155
155
  6. Dispatch agents (wave rules: DB+Backend parallel, Frontend after Backend UNLESS spec marks task as `(parallel-safe)` — see `pipeline-config.md` Parallel Rules). Agent prompt includes `{recommended_skills}` as skill hints — agents read SKILL.md of relevant skills before implementing
156
156
  7. Wave transitions between waves (from `pipeline-config.md`)
157
157
  8. On return: validate (build/type-check), update spec `[ ]` → `[x]` (line-by-line edits, NEVER copy entire spec blocks as old_string)
158
- 8b. **Agent Memory:** After agents return and spec is updated, write agent memory: `echo '{"agent_type":"{type}","wave":{N},"pipeline":"{spec-name}","summary":"{what agent did}","details":{...}}' | node .claude/scripts/memory-write.js` — one per agent. Skip if single-wave pipeline (no downstream agents to benefit).
158
+ 8b. **Agent Memory:** After agents return and spec is updated, write agent memory: `node .claude/scripts/memory-write.js --json '{"agent_type":"{type}","wave":{N},"pipeline":"{spec-name}","summary":"{what agent did}","details":{...}}'` — one per agent. Skip if single-wave pipeline (no downstream agents to benefit).
159
159
 
160
160
  #### Escalation Status Handling
161
161
 
@@ -12,6 +12,25 @@ Resumes an interrupted pipeline. The main context BECOMES the Pipeline Runner
12
12
 
13
13
  ## Action
14
14
 
15
+ ### Step 0: Dispatch Failure Pre-Check
16
+
17
+ Before the normal detect-and-confirm flow, scan the newest pipeline state for a recent dispatch failure flagged by `subagent-tracker` (PostToolUse on Task).
18
+
19
+ 1. Glob `.claude/.pipeline-states/*.json` (exclude `*.metrics.json`) and pick the file with the newest mtime.
20
+ 2. Read it and inspect the `lastDispatchFailure` field.
21
+ 3. If present:
22
+ - Compute `ageMs = Date.now() - new Date(lastDispatchFailure.at).getTime()`.
23
+ - **If ageMs <= 10 * 60 * 1000** (≤10 min, fresh):
24
+ 1. Inform the user: `Detected failed dispatch ({agentType}) due to {reason} at {at}. Re-dispatching with same prompt.`
25
+ 2. Re-invoke the Task tool with:
26
+ - `subagent_type`: `lastDispatchFailure.agentType` (fallback: `general-purpose`)
27
+ - `description`: `lastDispatchFailure.description`
28
+ - `prompt`: `lastDispatchFailure.prompt`
29
+ 3. After the re-dispatch returns, clear the flag: remove `lastDispatchFailure` from the state object and rewrite the pipeline-state JSON.
30
+ 4. Fall through to Step 1 (normal resume flow continues from the updated state).
31
+ - **If ageMs > 10 * 60 * 1000** (stale): silently remove `lastDispatchFailure` from the state and rewrite the file, then continue to Step 1.
32
+ 4. If `lastDispatchFailure` is absent, skip Step 0 entirely and proceed to Step 1.
33
+
15
34
  ### Step 1: Detect & Confirm
16
35
 
17
36
  1. Glob `.claude/spec/active/*/spec.md` — if 0 specs → inform user and stop
@@ -103,7 +122,7 @@ Run `node .claude/scripts/diff-context.js` to capture the current git state. Inc
103
122
 
104
123
  17b. **Agent Memory:** After each wave completes and spec checkboxes are updated, write agent memories for downstream waves:
105
124
  ```bash
106
- echo '{"agent_type":"{agent_type}","wave":{N},"pipeline":"{spec-name}","summary":"{1-line summary of what agent did}","details":{"files_modified":[...],"decisions":[...]}}' | node .claude/scripts/memory-write.js
125
+ node .claude/scripts/memory-write.js --json '{"agent_type":"{agent_type}","wave":{N},"pipeline":"{spec-name}","summary":"{1-line summary of what agent did}","details":{"files_modified":[...],"decisions":[...]}}'
107
126
  ```
108
127
  One call per agent in the completed wave. Summary ≤300 chars (key facts: files created, patterns used, endpoints added). Skip if no downstream waves remain.
109
128
 
@@ -167,7 +186,7 @@ When a pipeline is paused (user leaves session or requests pause):
167
186
  - Set `nextAction` to the specific next step (ONE sentence)
168
187
  2. Write agent memory for carry-over:
169
188
  ```bash
170
- echo '{"agent_type":"orchestrator","wave":0,"pipeline":"{spec-name}","summary":"Paused at {phase}. Next: {nextAction}"}' | node .claude/scripts/memory-write.js
189
+ node .claude/scripts/memory-write.js --json '{"agent_type":"orchestrator","wave":0,"pipeline":"{spec-name}","summary":"Paused at {phase}. Next: {nextAction}"}'
171
190
  ```
172
191
  3. Confirm to user: "Pipeline paused. Next action saved: {nextAction}"
173
192
 
@@ -2,16 +2,21 @@
2
2
 
3
3
  Orchestrator fills `{placeholders}` before dispatch. Agent receives the rendered version.
4
4
 
5
- ---
5
+ Single unified template for all dispatches:
6
+ - When `.claude/agents/{subproject}-impl.md` **exists**: orchestrator leaves `{role_block}` empty (role/boundary/validate/return already defined in the custom agent).
7
+ - When it **does NOT exist**: orchestrator fills `{role_block}` with `ROLE: {role} — {boundary}` / `Validate: {validate_command}` / `Return: {return_sections}`.
8
+
9
+ `{context_extras}` is optional (e.g. extra line to read `notes.md`); leave empty when unused.
6
10
 
7
- ## Compact Template (custom agent — role already defined in agent)
11
+ ---
8
12
 
9
- Use when `.claude/agents/{subproject}-impl.md` exists. Role, boundary, return format, and validation are already in the agent definition — prompt only needs references + entity + task.
13
+ ## Dispatch Template
10
14
 
11
15
  ```
12
16
  ## CONTEXT
13
- 1. Read `{subproject}/CLAUDE.md` — guards, stack, key paths
17
+ 1. Read `{subproject}/CLAUDE.md` — guards, stack, paths
14
18
  2. Read `{subproject}/.claude/commands/guards.md` — mandatory rules
19
+ {context_extras}
15
20
 
16
21
  ## REFERENCE
17
22
  {reference_files}
@@ -20,67 +25,26 @@ Use when `.claude/agents/{subproject}-impl.md` exists. Role, boundary, return fo
20
25
  {entity_info}
21
26
 
22
27
  ## SKILLS
23
- Your available skills are listed in the system. Before implementing, check if any skill matches your task — read its SKILL.md for patterns and examples.
24
- Key skills for this task: {recommended_skills}
25
- If a skill has `references/` files, read them only when you need concrete code examples.
28
+ Available skills listed in system. Read SKILL.md only if task matches. Key: {recommended_skills}
29
+ Load references/ only for concrete examples.
26
30
 
27
31
  ## WEB VALIDATION
28
- When in doubt about API usage, library version, or implementation pattern: search the web for the latest documentation before implementing. Only proceed when 100% confident.
32
+ In doubt about API/version/pattern search web for latest docs before implementing.
33
+
34
+ ## ROLE
35
+ {role_block}
29
36
 
30
37
  ## EFFICIENCY
31
- - Absolute paths NEVER cd
32
- - Chain kill+build in single Bash
33
- - Max 3 build attempts STOP + report
38
+ - Absolute paths, no cd
39
+ - Read each file once
40
+ - Max 3 build attempts, then STOP + report
34
41
 
35
42
  {retry_context}
36
43
 
37
44
  ## TASK
38
45
  {task_steps}
39
- ```
40
-
41
- ---
42
-
43
- ## Full Template (fallback — general-purpose agent, no custom agent)
44
-
45
- Use when `.claude/agents/{subproject}-impl.md` does NOT exist.
46
-
47
- ```
48
- ## STEP 0: READ CONTEXT
49
- 1. `{subproject}/CLAUDE.md` — guards, stack, key paths
50
- 2. `{subproject}/.claude/commands/guards.md` — mandatory rules
51
- 3. `{subproject}/.claude/commands/notes.md` — project-specific notes
52
-
53
- ## REFERENCE MODULE
54
- {reference_files}
55
46
 
56
- ## GUARDS (verify in return)
57
- {guards_summary}
58
-
59
- ## ENTITY REGISTRY
60
- {entity_info}
61
-
62
- ## SKILLS
63
- Your available skills are listed in the system. Before implementing, check if any skill matches your task — read its SKILL.md for patterns and examples.
64
- Key skills for this task: {recommended_skills}
65
- If a skill has `references/` files, read them only when you need concrete code examples.
66
-
67
- ## WEB VALIDATION
68
- When in doubt about API usage, library version, or implementation pattern: search the web for the latest documentation before implementing. Only proceed when 100% confident.
69
-
70
- ## ROLE: {role} — {boundary}
71
- Validate: {validate_command}
72
- Return: {return_sections}
73
-
74
- ## EFFICIENCY RULES
75
- - Shell state does NOT persist between Bash calls — ALWAYS use absolute paths, NEVER cd
76
- - Build: {build_command}
77
- - Read each file ONCE — trust your edit
78
- - Max 3 build attempts/step. After 3rd: STOP and report error.
79
-
80
- {retry_context}
81
-
82
- ## TASK — Execute in order
83
- {task_steps}
47
+ Guards carregados via CLAUDE.md acima — respeite sem exceção.
84
48
  ```
85
49
 
86
50
  ---
@@ -265,6 +265,25 @@ describe("memory-write.js", () => {
265
265
  });
266
266
  }
267
267
 
268
+ function runScriptArg(inputObj, opts = {}) {
269
+ return new Promise((resolve, reject) => {
270
+ const cwd = opts.cwd || PROJECT_DIR;
271
+ const child = spawn(
272
+ process.execPath,
273
+ [path.join(SCRIPTS_DIR, "memory-write.js"), "--json", JSON.stringify(inputObj)],
274
+ { cwd, stdio: ["ignore", "pipe", "pipe"] }
275
+ );
276
+ let stdout = "";
277
+ let stderr = "";
278
+ child.stdout.on("data", (d) => (stdout += d));
279
+ child.stderr.on("data", (d) => (stderr += d));
280
+ child.on("error", reject);
281
+ child.on("close", (code) => {
282
+ resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() });
283
+ });
284
+ });
285
+ }
286
+
268
287
  it("should create memory entry and index", async () => {
269
288
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem-test-"));
270
289
  const memDir = path.join(tmpDir, ".claude", ".agent-memory");
@@ -317,6 +336,32 @@ describe("memory-write.js", () => {
317
336
  const result = await runScript("not valid json");
318
337
  assert.equal(result.code, 0, "Should exit 0 even on bad input");
319
338
  });
339
+
340
+ it("should accept input via --json arg (Windows-friendly mode)", async () => {
341
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem-test-arg-"));
342
+ const memDir = path.join(tmpDir, ".claude", ".agent-memory");
343
+ try {
344
+ const result = await runScriptArg({
345
+ cwd: tmpDir,
346
+ agent_type: "arg-impl",
347
+ wave: 2,
348
+ pipeline: "arg-pipeline",
349
+ summary: "Wrote via --json arg mode.",
350
+ details: { mode: "arg" },
351
+ });
352
+ assert.equal(result.code, 0, `Exit code should be 0, stderr: ${result.stderr}`);
353
+ assert.ok(fs.existsSync(memDir), "Memory dir should exist");
354
+ const indexPath = path.join(memDir, "_index.json");
355
+ assert.ok(fs.existsSync(indexPath), "Index file should exist");
356
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
357
+ assert.equal(index.length, 1, "Index should have 1 entry");
358
+ assert.equal(index[0].agent_type, "arg-impl");
359
+ assert.equal(index[0].wave, 2);
360
+ assert.ok(index[0].summary.includes("arg mode"), "Summary should round-trip");
361
+ } finally {
362
+ fs.rmSync(tmpDir, { recursive: true, force: true });
363
+ }
364
+ });
320
365
  });
321
366
 
322
367
  // ─── subagent-tracker.js (memory injection) ─────────────────────────────────
@@ -384,3 +429,168 @@ describe("subagent-tracker.js memory injection", () => {
384
429
  }
385
430
  });
386
431
  });
432
+
433
+ // ─── metrics-tracker.js (sidecar + no-recursion) ────────────────────────────
434
+
435
+ describe("metrics-tracker.js", () => {
436
+ const hook = "metrics-tracker.js";
437
+
438
+ function setupPipelineState(tmpDir) {
439
+ const statesDir = path.join(tmpDir, ".claude", ".pipeline-states");
440
+ fs.mkdirSync(statesDir, { recursive: true });
441
+ const pipelinePath = path.join(statesDir, "test-pipeline.json");
442
+ fs.writeFileSync(pipelinePath, JSON.stringify({
443
+ v: 1,
444
+ name: "test-pipeline",
445
+ phase: "EXECUTE",
446
+ phaseName: "EXECUTE",
447
+ status: "approved",
448
+ startedAt: "2026-04-05T10:00:00.000Z",
449
+ }), "utf8");
450
+ return { statesDir, pipelinePath };
451
+ }
452
+
453
+ it("should write metrics to sidecar and leave pipeline-state untouched", async () => {
454
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "metrics-test-"));
455
+ const { statesDir, pipelinePath } = setupPipelineState(tmpDir);
456
+ const sidecarPath = path.join(statesDir, "test-pipeline.metrics.json");
457
+ try {
458
+ const mtimeBefore = fs.statSync(pipelinePath).mtimeMs;
459
+ // Wait a beat so any write would produce a different mtime
460
+ await new Promise((r) => setTimeout(r, 50));
461
+
462
+ const result = await runHook(hook, {
463
+ tool_name: "Edit",
464
+ tool_input: { file_path: path.join(tmpDir, "src/foo.ts") },
465
+ cwd: tmpDir,
466
+ }, { cwd: tmpDir, projectDir: tmpDir });
467
+
468
+ assert.equal(result.code, 0);
469
+ const mtimeAfter = fs.statSync(pipelinePath).mtimeMs;
470
+ assert.equal(mtimeAfter, mtimeBefore, "pipeline-state.json must NOT be modified");
471
+ assert.ok(fs.existsSync(sidecarPath), "sidecar must be created");
472
+ const sidecar = JSON.parse(fs.readFileSync(sidecarPath, "utf8"));
473
+ assert.equal(sidecar.metrics.apiCalls, 1);
474
+ assert.equal(sidecar.metrics.toolBreakdown.Edit, 1);
475
+ assert.equal(sidecar.previousPhase, "EXECUTE");
476
+ assert.equal(sidecar.metrics.startedAt, "2026-04-05T10:00:00.000Z", "startedAt inherited from pipeline-state");
477
+ } finally {
478
+ fs.rmSync(tmpDir, { recursive: true, force: true });
479
+ }
480
+ });
481
+
482
+ it("should not create recursive .metrics.metrics.json sidecars across multiple calls", async () => {
483
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "metrics-recursion-"));
484
+ const { statesDir } = setupPipelineState(tmpDir);
485
+ try {
486
+ // Fire 5 PostToolUse events in sequence
487
+ for (let i = 0; i < 5; i++) {
488
+ const r = await runHook(hook, {
489
+ tool_name: "Write",
490
+ tool_input: { file_path: path.join(tmpDir, `src/file${i}.ts`) },
491
+ cwd: tmpDir,
492
+ }, { cwd: tmpDir, projectDir: tmpDir });
493
+ assert.equal(r.code, 0);
494
+ }
495
+
496
+ const files = fs.readdirSync(statesDir).sort();
497
+ assert.deepEqual(
498
+ files,
499
+ ["test-pipeline.json", "test-pipeline.metrics.json"],
500
+ `Only 2 files expected, got: ${files.join(", ")}`
501
+ );
502
+
503
+ const sidecar = JSON.parse(
504
+ fs.readFileSync(path.join(statesDir, "test-pipeline.metrics.json"), "utf8")
505
+ );
506
+ assert.equal(sidecar.metrics.apiCalls, 5, "All 5 calls must aggregate into the same sidecar");
507
+ assert.equal(sidecar.metrics.toolBreakdown.Write, 5);
508
+ } finally {
509
+ fs.rmSync(tmpDir, { recursive: true, force: true });
510
+ }
511
+ });
512
+ });
513
+
514
+ // ─── subagent-tracker.js (overload detection) ───────────────────────────────
515
+
516
+ describe("subagent-tracker.js overload detection", () => {
517
+ const hook = "subagent-tracker.js";
518
+
519
+ function setupPipelineState(tmpDir) {
520
+ const statesDir = path.join(tmpDir, ".claude", ".pipeline-states");
521
+ fs.mkdirSync(statesDir, { recursive: true });
522
+ const pipelinePath = path.join(statesDir, "p.json");
523
+ fs.writeFileSync(pipelinePath, JSON.stringify({
524
+ v: 1,
525
+ phase: "EXECUTE",
526
+ startedAt: "2026-04-05T10:00:00.000Z",
527
+ }), "utf8");
528
+ fs.mkdirSync(path.join(tmpDir, ".claude", ".agent-state"), { recursive: true });
529
+ return pipelinePath;
530
+ }
531
+
532
+ async function dispatchTaskResult(tmpDir, toolResponse) {
533
+ return runHook(hook, {
534
+ hook_event_name: "PostToolUse",
535
+ tool_name: "Task",
536
+ tool_input: {
537
+ subagent_type: "general-purpose",
538
+ description: "test dispatch",
539
+ prompt: "Do something",
540
+ },
541
+ tool_response: toolResponse,
542
+ cwd: tmpDir,
543
+ }, { cwd: tmpDir, projectDir: tmpDir });
544
+ }
545
+
546
+ it("should flag lastDispatchFailure on real overload (is_error=true + 529)", async () => {
547
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "overload-real-"));
548
+ const pipelinePath = setupPipelineState(tmpDir);
549
+ try {
550
+ const r = await dispatchTaskResult(tmpDir, {
551
+ is_error: true,
552
+ content: "Error: 529 overloaded",
553
+ });
554
+ assert.equal(r.code, 0);
555
+ const state = JSON.parse(fs.readFileSync(pipelinePath, "utf8"));
556
+ assert.ok(state.lastDispatchFailure, "flag must be set");
557
+ assert.equal(state.lastDispatchFailure.reason, "api_overload");
558
+ assert.equal(state.lastDispatchFailure.agentType, "general-purpose");
559
+ assert.equal(state.lastDispatchFailure.description, "test dispatch");
560
+ } finally {
561
+ fs.rmSync(tmpDir, { recursive: true, force: true });
562
+ }
563
+ });
564
+
565
+ it("should NOT flag on happy-path agent that merely documents rate limiting", async () => {
566
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "overload-docs-"));
567
+ const pipelinePath = setupPipelineState(tmpDir);
568
+ try {
569
+ const r = await dispatchTaskResult(tmpDir, {
570
+ is_error: false,
571
+ content: "Documented rate limiting, 429 and 529 handling, api error recovery.",
572
+ });
573
+ assert.equal(r.code, 0);
574
+ const state = JSON.parse(fs.readFileSync(pipelinePath, "utf8"));
575
+ assert.equal(state.lastDispatchFailure, undefined, "flag must NOT be set (false positive guard)");
576
+ } finally {
577
+ fs.rmSync(tmpDir, { recursive: true, force: true });
578
+ }
579
+ });
580
+
581
+ it("should NOT flag on unrelated error (is_error=true without overload keywords)", async () => {
582
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "overload-unrelated-"));
583
+ const pipelinePath = setupPipelineState(tmpDir);
584
+ try {
585
+ const r = await dispatchTaskResult(tmpDir, {
586
+ is_error: true,
587
+ content: "SyntaxError in src/foo.ts line 42",
588
+ });
589
+ assert.equal(r.code, 0);
590
+ const state = JSON.parse(fs.readFileSync(pipelinePath, "utf8"));
591
+ assert.equal(state.lastDispatchFailure, undefined, "unrelated failure must not be flagged as overload");
592
+ } finally {
593
+ fs.rmSync(tmpDir, { recursive: true, force: true });
594
+ }
595
+ });
596
+ });
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * AUTO-FORMAT: PostToolUse hook for Write|Edit
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * SAFETY: PreToolUse guard for dangerous Bash commands
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * ENFORCEMENT: Entity Registry validation
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * SAFETY: PreToolUse guard for sensitive file access
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * GUARD-VERIFY: PostToolUse hook for Write|Edit
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * METRICS-TRACKER: PostToolUse hook that tracks pipeline metrics
4
5
  *
@@ -31,7 +32,7 @@ process.stdin.on('end', () => {
31
32
  const statesDir = path.join(cwd, '.claude', '.pipeline-states');
32
33
  if (!fs.existsSync(statesDir)) { process.exit(0); }
33
34
 
34
- const files = fs.readdirSync(statesDir).filter(f => f.endsWith('.json'));
35
+ const files = fs.readdirSync(statesDir).filter(f => f.endsWith('.json') && !f.endsWith('.metrics.json'));
35
36
  if (files.length === 0) { process.exit(0); }
36
37
 
37
38
  // Update the most recently modified pipeline state
@@ -50,34 +51,64 @@ process.stdin.on('end', () => {
50
51
 
51
52
  if (!newest) { process.exit(0); }
52
53
 
53
- const state = JSON.parse(fs.readFileSync(newest, 'utf8'));
54
-
55
- // Initialize metrics if not present
56
- if (!state.metrics) {
57
- state.metrics = {
54
+ // Read pipeline-state.json READ-ONLY (to derive currentPhase, status, startedAt).
55
+ // Never write to it — metrics live in a sidecar to avoid "file modified since
56
+ // read" races with Edit/Write on the pipeline-state file.
57
+ let pipelineState = {};
58
+ try {
59
+ pipelineState = JSON.parse(fs.readFileSync(newest, 'utf8'));
60
+ } catch {}
61
+
62
+ const sidecarPath = newest.replace(/\.json$/, '.metrics.json');
63
+ let sidecar;
64
+ if (fs.existsSync(sidecarPath)) {
65
+ try {
66
+ sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
67
+ } catch {
68
+ sidecar = null;
69
+ }
70
+ }
71
+ if (!sidecar || typeof sidecar !== 'object') {
72
+ sidecar = {
73
+ v: 1,
74
+ metrics: {
75
+ apiCalls: 0,
76
+ toolBreakdown: {},
77
+ retries: 0,
78
+ startedAt: pipelineState.startedAt || new Date().toISOString(),
79
+ },
80
+ previousPhase: '',
81
+ };
82
+ }
83
+ if (!sidecar.metrics) {
84
+ sidecar.metrics = {
58
85
  apiCalls: 0,
59
86
  toolBreakdown: {},
60
87
  retries: 0,
61
- startedAt: state.startedAt || new Date().toISOString(),
88
+ startedAt: pipelineState.startedAt || new Date().toISOString(),
62
89
  };
63
90
  }
91
+ if (!sidecar.metrics.toolBreakdown) sidecar.metrics.toolBreakdown = {};
92
+
93
+ // Alias for minimal churn below — all mutations go to the sidecar.
94
+ const state = sidecar;
64
95
 
65
96
  // ── wave_reentry: track EXECUTE → PLAN transitions ──────────────────────
66
97
  // previousPhase is updated on every write so we can detect phase changes.
67
- const currentPhase = state.phaseName || state.phase || '';
68
- const previousPhase = state.previousPhase || '';
98
+ const currentPhase = pipelineState.phaseName || pipelineState.phase || '';
99
+ const previousPhase = sidecar.previousPhase || '';
69
100
  if (currentPhase === 'PLAN' && previousPhase === 'EXECUTE') {
70
101
  state.metrics.wave_reentry = (state.metrics.wave_reentry || 0) + 1;
71
102
  }
72
103
  // Always update previousPhase to the current phase so the NEXT write can
73
104
  // detect a transition.
74
- state.previousPhase = currentPhase;
105
+ sidecar.previousPhase = currentPhase;
75
106
 
76
107
  // ── gate_saves: spec edits in PLAN phase after first /approve ────────────
77
- // Proxy for "first approve recorded": state.status === 'approved' (set by
78
- // /approve command). A spec file is any .md in .claude/spec/ or matching
79
- // *spec*.md anywhere in the pipeline-states dir.
80
- if ((toolName === 'Edit' || toolName === 'Write') && currentPhase === 'PLAN' && state.status === 'approved') {
108
+ // Proxy for "first approve recorded": pipelineState.status === 'approved'
109
+ // (set by /approve command). A spec file is any .md in .claude/spec/ or
110
+ // matching *spec*.md anywhere in the pipeline-states dir.
111
+ if ((toolName === 'Edit' || toolName === 'Write') && currentPhase === 'PLAN' && pipelineState.status === 'approved') {
81
112
  const toolFilePath = (data.tool_input || {}).file_path || (data.tool_input || {}).path || '';
82
113
  const isSpecFile =
83
114
  /[/\\]\.claude[/\\]spec[/\\]/.test(toolFilePath) ||
@@ -151,7 +182,8 @@ process.stdin.on('end', () => {
151
182
 
152
183
  state.metrics.updatedAt = new Date().toISOString();
153
184
 
154
- fs.writeFileSync(newest, JSON.stringify(state, null, 2), 'utf8');
185
+ // Write ONLY the sidecar — never touch pipeline-state.json from this hook.
186
+ fs.writeFileSync(sidecarPath, JSON.stringify(sidecar, null, 2), 'utf8');
155
187
 
156
188
  process.exit(0);
157
189
  } catch (err) {
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * PRE-COMPACT: Preserve context before conversation compaction
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * RTK REWRITE: PreToolUse hook that rewrites Bash commands through RTK
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * SESSION-CLEANUP: Clean stale state files on session end
4
5
  *
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * SESSION-MEMORY: Injects persistent memory into session context
4
5
  * @version 1.0.0
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
  /**
3
4
  * SUBAGENT TRACKER: Tracks active subagents for statusline display
4
5
  *
5
- * Handles 4 events:
6
- * - PreToolUse(Task): queues description + type before agent starts
7
- * - SubagentStart: writes agent state file (consumes from queue)
8
- * - SubagentStop: removes agent state file + prunes stale queue
9
- * - SessionStart: cleans up stale state from previous sessions
6
+ * Handles 5 events:
7
+ * - PreToolUse(Task): queues description + type before agent starts
8
+ * - PostToolUse(Task): detects API overload / dispatch failures and flags pipeline state
9
+ * - SubagentStart: writes agent state file (consumes from queue)
10
+ * - SubagentStop: removes agent state file + prunes stale queue
11
+ * - SessionStart: cleans up stale state from previous sessions
10
12
  *
11
13
  * State dir: .claude/.agent-state/{agent_id}.json
12
14
  * Queue: .claude/.agent-state/_queue.json
@@ -42,6 +44,8 @@ process.stdin.on('end', () => {
42
44
 
43
45
  if (event === 'PreToolUse' && data.tool_name === 'Task') {
44
46
  handlePreToolUse(data, stateDir);
47
+ } else if (event === 'PostToolUse' && data.tool_name === 'Task') {
48
+ handlePostToolUse(data, stateDir);
45
49
  } else if (event === 'SubagentStart') {
46
50
  handleStart(data, stateDir);
47
51
  } else if (event === 'SubagentStop') {
@@ -177,6 +181,65 @@ function parseRecommendedSkills(prompt) {
177
181
  return skills;
178
182
  }
179
183
 
184
+ /**
185
+ * PostToolUse(Task): Detect API overload / dispatch failures in tool_response
186
+ * and flag the active pipeline state with `lastDispatchFailure` so /resume can
187
+ * auto-recover.
188
+ *
189
+ * We write to pipeline-state ONLY when a failure is detected — happy-path
190
+ * dispatches never touch the state file from here.
191
+ */
192
+ function handlePostToolUse(data, stateDir) {
193
+ try {
194
+ if (isSelfDelegation(data)) { return; }
195
+
196
+ const toolResponse = data.tool_response || {};
197
+ const responseText = JSON.stringify(toolResponse).toLowerCase();
198
+ // Detect overload conservatively: require is_error=true (Claude Code sets
199
+ // this on Task tool failures) AND at least one overload keyword. This
200
+ // avoids false positives on agents that merely *document* rate limiting
201
+ // or error handling in their returned content.
202
+ const isOverload =
203
+ toolResponse.is_error === true &&
204
+ /overload|rate.?limit|\b429\b|\b529\b|throttl|too many requests/.test(responseText);
205
+
206
+ if (!isOverload) return;
207
+
208
+ const projectDir = path.resolve(stateDir, '..', '..');
209
+ const statesDir = path.join(projectDir, '.claude', '.pipeline-states');
210
+ if (!fs.existsSync(statesDir)) return;
211
+
212
+ const files = fs.readdirSync(statesDir)
213
+ .filter(f => f.endsWith('.json') && !f.endsWith('.metrics.json'));
214
+ if (files.length === 0) return;
215
+
216
+ let newest = null;
217
+ let newestMtime = 0;
218
+ for (const f of files) {
219
+ try {
220
+ const fp = path.join(statesDir, f);
221
+ const stat = fs.statSync(fp);
222
+ if (stat.mtimeMs > newestMtime) {
223
+ newestMtime = stat.mtimeMs;
224
+ newest = fp;
225
+ }
226
+ } catch {}
227
+ }
228
+ if (!newest) return;
229
+
230
+ const toolInput = data.tool_input || {};
231
+ const state = JSON.parse(fs.readFileSync(newest, 'utf8'));
232
+ state.lastDispatchFailure = {
233
+ at: new Date().toISOString(),
234
+ reason: 'api_overload',
235
+ agentType: toolInput.subagent_type || 'unknown',
236
+ description: toolInput.description || '',
237
+ prompt: (toolInput.prompt || '').slice(0, 2000),
238
+ };
239
+ fs.writeFileSync(newest, JSON.stringify(state, null, 2), 'utf8');
240
+ } catch {} // fail-open: failure detection is advisory
241
+ }
242
+
180
243
  function handleStart(data, stateDir) {
181
244
  const agentId = data.agent_id || `unknown-${Date.now()}`;
182
245
  const agentType = data.agent_type || 'unknown';
@@ -3,10 +3,14 @@
3
3
  /**
4
4
  * memory-write.js
5
5
  *
6
- * Receives a JSON memory entry from stdin and persists it to
6
+ * Receives a JSON memory entry and persists it to
7
7
  * {projectDir}/.claude/.agent-memory/.
8
8
  *
9
- * Input schema (stdin):
9
+ * Input (two modes):
10
+ * 1. --json '<JSON>' CLI arg (Windows-friendly, avoids shell echo pipe issues)
11
+ * 2. stdin piped JSON (POSIX)
12
+ *
13
+ * Input schema:
10
14
  * {
11
15
  * "agent_type": "templates-impl",
12
16
  * "wave": 1,
@@ -110,17 +114,24 @@ function resolveSessionPrefix(projectDir) {
110
114
  // ---------------------------------------------------------------------------
111
115
 
112
116
  async function main() {
113
- // Collect stdin.
114
117
  let raw = "";
115
- for await (const chunk of process.stdin) {
116
- raw += chunk;
118
+
119
+ // --json arg mode (Windows-friendly: avoids shell echo pipe issues)
120
+ const jsonArgIdx = process.argv.indexOf("--json");
121
+ if (jsonArgIdx !== -1 && process.argv[jsonArgIdx + 1]) {
122
+ raw = process.argv[jsonArgIdx + 1];
123
+ } else {
124
+ // stdin fallback (POSIX)
125
+ for await (const chunk of process.stdin) {
126
+ raw += chunk;
127
+ }
117
128
  }
118
129
 
119
130
  let input;
120
131
  try {
121
132
  input = JSON.parse(raw);
122
133
  } catch (err) {
123
- process.stderr.write(`[memory-write] Failed to parse stdin JSON: ${err.message}\n`);
134
+ process.stderr.write(`[memory-write] Failed to parse input JSON: ${err.message}\n`);
124
135
  process.exit(0);
125
136
  }
126
137
 
@@ -146,6 +146,11 @@
146
146
  "type": "command",
147
147
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-knowledge-inc.js",
148
148
  "timeout": 5
149
+ },
150
+ {
151
+ "type": "command",
152
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/subagent-tracker.js",
153
+ "timeout": 3
149
154
  }
150
155
  ]
151
156
  }