@bonesofspring/ai-rules 0.2.21 → 0.2.22

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.
Files changed (176) hide show
  1. package/package.json +1 -1
  2. package/presets/_shared/core/agent-team/agent-artifact-contracts.md +2 -0
  3. package/presets/_shared/core/meta/preset-twin-sync.md +1 -1
  4. package/presets/claude/android-kotlin/agents/build-verifier.md +2 -0
  5. package/presets/claude/android-kotlin/agents/code-reviewer.md +2 -0
  6. package/presets/claude/android-kotlin/agents/debugger.md +1 -0
  7. package/presets/claude/android-kotlin/agents/feature-developer.md +3 -0
  8. package/presets/claude/android-kotlin/agents/qa-tester.md +2 -0
  9. package/presets/claude/android-kotlin/agents/task-router.md +4 -0
  10. package/presets/claude/android-kotlin/hooks/chain-team-phases.sh +51 -11
  11. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +1 -1
  12. package/presets/claude/go/agents/build-verifier.md +4 -0
  13. package/presets/claude/go/agents/code-reviewer.md +4 -0
  14. package/presets/claude/go/agents/debugger.md +4 -0
  15. package/presets/claude/go/agents/feature-developer.md +5 -0
  16. package/presets/claude/go/agents/qa-tester.md +4 -0
  17. package/presets/claude/go/agents/task-router.md +4 -0
  18. package/presets/claude/go/hooks/chain-team-phases.sh +51 -11
  19. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +1 -1
  20. package/presets/claude/ios-swift/agents/build-verifier.md +2 -0
  21. package/presets/claude/ios-swift/agents/code-reviewer.md +2 -0
  22. package/presets/claude/ios-swift/agents/debugger.md +1 -0
  23. package/presets/claude/ios-swift/agents/feature-developer.md +3 -0
  24. package/presets/claude/ios-swift/agents/qa-tester.md +2 -0
  25. package/presets/claude/ios-swift/agents/task-router.md +4 -0
  26. package/presets/claude/ios-swift/hooks/chain-team-phases.sh +51 -11
  27. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +1 -1
  28. package/presets/claude/java/agents/build-verifier.md +4 -0
  29. package/presets/claude/java/agents/code-reviewer.md +4 -0
  30. package/presets/claude/java/agents/debugger.md +4 -0
  31. package/presets/claude/java/agents/feature-developer.md +5 -0
  32. package/presets/claude/java/agents/qa-tester.md +4 -0
  33. package/presets/claude/java/agents/task-router.md +4 -0
  34. package/presets/claude/java/hooks/chain-team-phases.sh +51 -11
  35. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +1 -1
  36. package/presets/claude/mcp-ts/agents/build-verifier.md +4 -0
  37. package/presets/claude/mcp-ts/agents/feature-developer.md +5 -0
  38. package/presets/claude/mcp-ts/agents/task-router.md +1 -1
  39. package/presets/claude/mcp-ts/hooks/chain-team-phases.sh +51 -11
  40. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +1 -1
  41. package/presets/claude/next/agents/build-verifier.md +2 -0
  42. package/presets/claude/next/agents/code-reviewer.md +2 -0
  43. package/presets/claude/next/agents/debugger.md +4 -0
  44. package/presets/claude/next/agents/feature-developer.md +3 -0
  45. package/presets/claude/next/agents/qa-tester.md +4 -0
  46. package/presets/claude/next/agents/unit-test-generator.md +1 -1
  47. package/presets/claude/next/agents/unit-test-planner.md +1 -1
  48. package/presets/claude/next/hooks/chain-team-phases.sh +51 -11
  49. package/presets/claude/next/rules/api-and-data/http-client.md +1 -1
  50. package/presets/claude/next/rules/architecture/reference-features.md +1 -1
  51. package/presets/claude/next/rules/testing/README.md +1 -1
  52. package/presets/claude/next/rules/testing/tests-e2e-structure.md +2 -0
  53. package/presets/claude/next/rules/testing/tests-unit.md +3 -5
  54. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +1 -1
  55. package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +1 -1
  56. package/presets/claude/next/skills/playwright-e2e/SKILL.md +5 -3
  57. package/presets/claude/next/skills/unit-testing/SKILL.md +6 -4
  58. package/presets/claude/nuxt/agents/build-verifier.md +2 -0
  59. package/presets/claude/nuxt/agents/code-reviewer.md +2 -0
  60. package/presets/claude/nuxt/agents/debugger.md +4 -0
  61. package/presets/claude/nuxt/agents/feature-developer.md +3 -0
  62. package/presets/claude/nuxt/agents/qa-tester.md +4 -0
  63. package/presets/claude/nuxt/hooks/chain-team-phases.sh +51 -11
  64. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +1 -1
  65. package/presets/claude/php-hexagonal/agents/build-verifier.md +4 -0
  66. package/presets/claude/php-hexagonal/agents/code-reviewer.md +4 -0
  67. package/presets/claude/php-hexagonal/agents/debugger.md +4 -0
  68. package/presets/claude/php-hexagonal/agents/feature-developer.md +5 -0
  69. package/presets/claude/php-hexagonal/agents/qa-tester.md +4 -0
  70. package/presets/claude/php-hexagonal/agents/task-router.md +4 -0
  71. package/presets/claude/php-hexagonal/hooks/chain-team-phases.sh +51 -11
  72. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +1 -1
  73. package/presets/claude/php-laravel/agents/build-verifier.md +4 -0
  74. package/presets/claude/php-laravel/agents/code-reviewer.md +4 -0
  75. package/presets/claude/php-laravel/agents/debugger.md +4 -0
  76. package/presets/claude/php-laravel/agents/feature-developer.md +5 -0
  77. package/presets/claude/php-laravel/agents/qa-tester.md +4 -0
  78. package/presets/claude/php-laravel/agents/task-router.md +1 -0
  79. package/presets/claude/php-laravel/hooks/chain-team-phases.sh +51 -11
  80. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +1 -1
  81. package/presets/claude/svelte/agents/build-verifier.md +2 -0
  82. package/presets/claude/svelte/agents/code-reviewer.md +2 -0
  83. package/presets/claude/svelte/agents/debugger.md +4 -0
  84. package/presets/claude/svelte/agents/feature-developer.md +3 -0
  85. package/presets/claude/svelte/agents/qa-tester.md +4 -0
  86. package/presets/claude/svelte/hooks/chain-team-phases.sh +51 -11
  87. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +1 -1
  88. package/presets/cursor/android-kotlin/agents/build-verifier.md +2 -0
  89. package/presets/cursor/android-kotlin/agents/code-reviewer.md +2 -0
  90. package/presets/cursor/android-kotlin/agents/debugger.md +1 -0
  91. package/presets/cursor/android-kotlin/agents/feature-developer.md +3 -0
  92. package/presets/cursor/android-kotlin/agents/qa-tester.md +2 -0
  93. package/presets/cursor/android-kotlin/agents/task-router.md +4 -0
  94. package/presets/cursor/android-kotlin/hooks/chain-team-phases.sh +51 -11
  95. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +1 -1
  96. package/presets/cursor/go/agents/build-verifier.md +4 -0
  97. package/presets/cursor/go/agents/code-reviewer.md +4 -0
  98. package/presets/cursor/go/agents/debugger.md +4 -0
  99. package/presets/cursor/go/agents/feature-developer.md +5 -0
  100. package/presets/cursor/go/agents/qa-tester.md +4 -0
  101. package/presets/cursor/go/agents/task-router.md +4 -0
  102. package/presets/cursor/go/hooks/chain-team-phases.sh +51 -11
  103. package/presets/cursor/go/rules/preset-twin-sync.mdc +1 -1
  104. package/presets/cursor/ios-swift/agents/build-verifier.md +2 -0
  105. package/presets/cursor/ios-swift/agents/code-reviewer.md +2 -0
  106. package/presets/cursor/ios-swift/agents/debugger.md +1 -0
  107. package/presets/cursor/ios-swift/agents/feature-developer.md +3 -0
  108. package/presets/cursor/ios-swift/agents/qa-tester.md +2 -0
  109. package/presets/cursor/ios-swift/agents/task-router.md +4 -0
  110. package/presets/cursor/ios-swift/hooks/chain-team-phases.sh +51 -11
  111. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +1 -1
  112. package/presets/cursor/java/agents/build-verifier.md +4 -0
  113. package/presets/cursor/java/agents/code-reviewer.md +4 -0
  114. package/presets/cursor/java/agents/debugger.md +4 -0
  115. package/presets/cursor/java/agents/feature-developer.md +5 -0
  116. package/presets/cursor/java/agents/qa-tester.md +4 -0
  117. package/presets/cursor/java/agents/task-router.md +4 -0
  118. package/presets/cursor/java/hooks/chain-team-phases.sh +51 -11
  119. package/presets/cursor/java/rules/preset-twin-sync.mdc +1 -1
  120. package/presets/cursor/mcp-ts/agents/build-verifier.md +4 -0
  121. package/presets/cursor/mcp-ts/agents/feature-developer.md +5 -0
  122. package/presets/cursor/mcp-ts/agents/task-router.md +1 -1
  123. package/presets/cursor/mcp-ts/hooks/chain-team-phases.sh +51 -11
  124. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +1 -1
  125. package/presets/cursor/next/agents/build-verifier.md +2 -0
  126. package/presets/cursor/next/agents/code-reviewer.md +2 -0
  127. package/presets/cursor/next/agents/debugger.md +4 -0
  128. package/presets/cursor/next/agents/feature-developer.md +3 -0
  129. package/presets/cursor/next/agents/qa-tester.md +4 -0
  130. package/presets/cursor/next/agents/unit-test-generator.md +1 -1
  131. package/presets/cursor/next/agents/unit-test-planner.md +1 -1
  132. package/presets/cursor/next/hooks/chain-team-phases.sh +51 -11
  133. package/presets/cursor/next/rules/http-client.mdc +1 -1
  134. package/presets/cursor/next/rules/preset-twin-sync.mdc +1 -1
  135. package/presets/cursor/next/rules/react-ui.mdc +1 -1
  136. package/presets/cursor/next/rules/reference-features.mdc +1 -1
  137. package/presets/cursor/next/rules/tests-e2e-structure.mdc +2 -0
  138. package/presets/cursor/next/rules/tests-unit.mdc +3 -5
  139. package/presets/cursor/next/skills/playwright-e2e/SKILL.md +5 -3
  140. package/presets/cursor/next/skills/unit-testing/SKILL.md +6 -4
  141. package/presets/cursor/nuxt/agents/build-verifier.md +2 -0
  142. package/presets/cursor/nuxt/agents/code-reviewer.md +2 -0
  143. package/presets/cursor/nuxt/agents/debugger.md +4 -0
  144. package/presets/cursor/nuxt/agents/feature-developer.md +3 -0
  145. package/presets/cursor/nuxt/agents/qa-tester.md +4 -0
  146. package/presets/cursor/nuxt/hooks/chain-team-phases.sh +51 -11
  147. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +1 -1
  148. package/presets/cursor/php-hexagonal/agents/build-verifier.md +4 -0
  149. package/presets/cursor/php-hexagonal/agents/code-reviewer.md +4 -0
  150. package/presets/cursor/php-hexagonal/agents/debugger.md +4 -0
  151. package/presets/cursor/php-hexagonal/agents/feature-developer.md +5 -0
  152. package/presets/cursor/php-hexagonal/agents/qa-tester.md +4 -0
  153. package/presets/cursor/php-hexagonal/agents/task-router.md +4 -0
  154. package/presets/cursor/php-hexagonal/hooks/chain-team-phases.sh +51 -11
  155. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +1 -1
  156. package/presets/cursor/php-laravel/agents/build-verifier.md +4 -0
  157. package/presets/cursor/php-laravel/agents/code-reviewer.md +4 -0
  158. package/presets/cursor/php-laravel/agents/debugger.md +4 -0
  159. package/presets/cursor/php-laravel/agents/feature-developer.md +5 -0
  160. package/presets/cursor/php-laravel/agents/qa-tester.md +4 -0
  161. package/presets/cursor/php-laravel/agents/task-router.md +1 -0
  162. package/presets/cursor/php-laravel/hooks/chain-team-phases.sh +51 -11
  163. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +1 -1
  164. package/presets/cursor/svelte/agents/build-verifier.md +2 -0
  165. package/presets/cursor/svelte/agents/code-reviewer.md +2 -0
  166. package/presets/cursor/svelte/agents/debugger.md +4 -0
  167. package/presets/cursor/svelte/agents/feature-developer.md +3 -0
  168. package/presets/cursor/svelte/agents/qa-tester.md +4 -0
  169. package/presets/cursor/svelte/hooks/chain-team-phases.sh +51 -11
  170. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +1 -1
  171. package/scripts/check-preset-structure.sh +10 -0
  172. package/scripts/check-preset-token-budget.sh +100 -0
  173. package/scripts/check-task-router-intents.sh +141 -0
  174. package/scripts/test-agent-task-metrics-hooks.mjs +99 -1
  175. package/scripts/test-chain-team-phases-coverage.mjs +100 -1
  176. package/scripts/test-task-router-intents-fixtures.mjs +109 -0
@@ -66,10 +66,20 @@ const STACK = 'php-laravel';
66
66
  const PLATFORM = 'cursor';
67
67
 
68
68
  function waitForMetricsLock() {
69
+ // OD-1: real advisory lock via create-exclusive (fs.openSync(_, 'wx')).
70
+ // The previous impl spun on Atomics.wait against an unrelated SharedArrayBuffer
71
+ // and never blocked on the lock file, so two concurrent subagentStop events
72
+ // could both acquire the lock and corrupt metrics.json. Retry EEXIST with a
73
+ // 10 ms sleep up to 200 attempts (2 s ceiling). On any other error, throw.
69
74
  for (let attempt = 0; attempt < 200; attempt += 1) {
70
- try { fs.mkdirSync(METRICS_LOCK); return; } catch (error) {
71
- if (error && error.code !== 'EEXIST') throw error;
72
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
75
+ try {
76
+ const fd = fs.openSync(METRICS_LOCK, 'wx');
77
+ try { fs.closeSync(fd); } catch {}
78
+ return;
79
+ } catch (error) {
80
+ if (!error || error.code !== 'EEXIST') throw error;
81
+ const until = Date.now() + 10;
82
+ while (Date.now() < until) { /* 10 ms busy-wait */ }
73
83
  }
74
84
  }
75
85
  throw new Error('metrics lock timeout');
@@ -78,7 +88,7 @@ let taskLockHeld = false;
78
88
  function releaseTaskLock() {
79
89
  if (!taskLockHeld) return;
80
90
  taskLockHeld = false;
81
- try { fs.rmdirSync(METRICS_LOCK); } catch {}
91
+ try { fs.unlinkSync(METRICS_LOCK); } catch {}
82
92
  }
83
93
  waitForMetricsLock();
84
94
  taskLockHeld = true;
@@ -106,7 +116,7 @@ function mutateMetrics(mutator) {
106
116
  fs.writeFileSync(temporary, JSON.stringify(metrics, null, 2) + '\n');
107
117
  fs.renameSync(temporary, METRICS_PATH);
108
118
  } finally {
109
- if (acquiredHere) { try { fs.rmdirSync(METRICS_LOCK); } catch {} }
119
+ if (acquiredHere) { try { fs.unlinkSync(METRICS_LOCK); } catch {} }
110
120
  }
111
121
  }
112
122
  function appendMetric(event) { mutateMetrics((metrics) => metrics.events.push({ eventId: `${metrics.task.runId}:${Date.now()}:${Math.random().toString(16).slice(2)}`, ...event })); }
@@ -481,7 +491,19 @@ function legacyFlow() {
481
491
 
482
492
 
483
493
  if (hookInput.action === 'end_human_gate') {
484
- status.awaitingHumanGate = false;
494
+ status.awaitingHumanGate = false;
495
+ // A2 (retro §8): pin a single runId per pipeline so subsequent events don't
496
+ // fragment the metrics ledger across multiple runId sequences. Prefer
497
+ // metrics.json.task.runId when metrics already exist, then status.metricsRunId,
498
+ // then mint a fresh seed only on first ever emit.
499
+ try {
500
+ if (fs.existsSync(METRICS_PATH)) {
501
+ const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
502
+ if (existing && existing.task && typeof existing.task.runId === 'string') {
503
+ status.metricsRunId = existing.task.runId;
504
+ }
505
+ }
506
+ } catch {}
485
507
  if (status.state === 'awaiting_approval') {
486
508
  status.state = 'in_progress';
487
509
  status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
@@ -520,11 +542,29 @@ if (hookInput.action === 'end_human_gate') {
520
542
  }
521
543
  }
522
544
  }
523
- writeStatus({
524
- awaitingHumanGate: false,
525
- state: status.state,
526
- phase: status.phase,
527
- });
545
+ // A2 (retro §8): if gate closes the final human gate of the pipeline
546
+ // (no resume happens, or current step's gate was the last), surface
547
+ // state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
548
+ // the post-gate idle state as "still in progress".
549
+ {
550
+ let gateWasTerminal = false;
551
+ if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
552
+ try {
553
+ const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
554
+ const steps = pipe.steps || [];
555
+ const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
556
+ const cur = steps[idx];
557
+ const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
558
+ const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
559
+ if (cur && hasGate(cur)) gateWasTerminal = true;
560
+ } catch {}
561
+ }
562
+ writeStatus({
563
+ awaitingHumanGate: false,
564
+ state: gateWasTerminal ? 'awaiting_approval' : status.state,
565
+ phase: status.phase,
566
+ });
567
+ }
528
568
  process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
529
569
  process.exit(0);
530
570
  }
@@ -40,7 +40,7 @@ Cursor `.mdc` — **SoT per stack**. Claude topic `.md` — derived twin.
40
40
  - **Domain** rules: ≥15 body lines (FAIL if thin without documented alias reason).
41
41
  - **Thin aliases** (`agent-team-intake`, `technical-retro`): may be shorter (soft) if they only point to a command/skill.
42
42
  - **Slim-companion rules** (UI-edit bundle, slim app-cores, slim tooling): may be shorter than 15 lines **if and only if** the body is a pointer into shared-core / stack-prose (no standalone prose). Examples live in stack `rules/README.md` (e.g. `cursor/next/rules/README.md` "UI-edit bundle" intent).
43
- - **Mechanical enforcement is pending.** Today build-verifier excludes only `agent-team-intake` / `technical-retro` / `preset-layering`; slim-companion rules currently pass under the same human-judgement path as thin aliases. Follow-up: add `allow_slim_stems` to `packages/ai-rules/scripts/check-preset-token-budget.sh` analogous to `trio_stems_allowlist`.
43
+ - Mechanical enforcement lives in `packages/ai-rules/scripts/check-preset-token-budget.sh` see `SLIM_COMPANION_ALLOWLIST` (the 53 stems accepted as slim pointers) and `MIN_SLIM_BODY_LINES` (5-line floor). Slim-companion rules with a body of 6–14 lines whose stem is **not** in the allowlist are reported as slim violations by the script.
44
44
 
45
45
  ## Severity (build-verifier)
46
46
 
@@ -97,6 +97,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **FAI
97
97
 
98
98
  Do not fix code — report only. Do not advance to code-reviewer until PASS.
99
99
 
100
+ **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
101
+
100
102
  ## Artifact contract
101
103
 
102
104
  - **Authoritative inputs:** read `.cursor/team/tasks/<slug>/artifact-manifest.json` first; consume handoff `artifactInputIds` when supplied, otherwise consume the manifest entries for brief/decomposition, implementation evidence, pipeline scope, and changed files.
@@ -55,6 +55,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **REQ
55
55
 
56
56
  Do not mix this output with retrospective facilitation — keep review and retro separate.
57
57
 
58
+ **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
59
+
58
60
  ## Design guidance
59
61
 
60
62
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
@@ -81,3 +81,7 @@ Do not perform formal code review or write e2e plans — those are separate agen
81
81
 
82
82
  - When writing or changing code, load / follow rule `anti-sycophancy-discipline`.
83
83
  - When acting on review feedback (`changes_requested` / human comments): verify each item against the codebase before implementing; no performative agreement.
84
+
85
+ ## Receipt binding
86
+
87
+ > **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
@@ -13,6 +13,7 @@ You are a senior frontend developer working in a SvelteKit monorepo with strict
13
13
  2. Read `status.json` — proceed if `in_progress`, `approved`, or `retryAfterFix`.
14
14
  3. Read `pipeline.json` for step context and scope.
15
15
  4. Follow the **`feature-delivery` skill** (layer order, reference features, validation handoff).
16
+ 5. **Verify carry-over at HEAD before applying** (refactor intent only). When the brief comes from a prior review's carry-over findings list, for each item run `git show HEAD:<path>` or `grep` to confirm the finding still applies at the current HEAD; if the file already contains the fix (carry-over was closed in a prior uncommitted batch), mark it as **NO-OP at HEAD** in `implementation.md` and skip the edit; if the file is unchanged, apply the fix and cite file:line. Target ≤2 NO-OP items per refactor pipeline.
16
17
 
17
18
  ## Task execution rules
18
19
 
@@ -40,6 +41,8 @@ Emit the owned implementation receipt with outcome `completed`. Handoff: tasks d
40
41
 
41
42
  Do not perform formal code review — that is the code-reviewer subagent's job.
42
43
 
44
+ **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
45
+
43
46
  ## Design guidance
44
47
 
45
48
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
@@ -53,3 +53,7 @@ Provide summary:
53
53
  ## Design guidance
54
54
 
55
55
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
56
+
57
+ ## Receipt binding
58
+
59
+ > **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
@@ -64,10 +64,20 @@ const STACK = 'svelte';
64
64
  const PLATFORM = 'cursor';
65
65
 
66
66
  function waitForMetricsLock() {
67
+ // OD-1: real advisory lock via create-exclusive (fs.openSync(_, 'wx')).
68
+ // The previous impl spun on Atomics.wait against an unrelated SharedArrayBuffer
69
+ // and never blocked on the lock file, so two concurrent subagentStop events
70
+ // could both acquire the lock and corrupt metrics.json. Retry EEXIST with a
71
+ // 10 ms sleep up to 200 attempts (2 s ceiling). On any other error, throw.
67
72
  for (let attempt = 0; attempt < 200; attempt += 1) {
68
- try { fs.mkdirSync(METRICS_LOCK); return; } catch (error) {
69
- if (error && error.code !== 'EEXIST') throw error;
70
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
73
+ try {
74
+ const fd = fs.openSync(METRICS_LOCK, 'wx');
75
+ try { fs.closeSync(fd); } catch {}
76
+ return;
77
+ } catch (error) {
78
+ if (!error || error.code !== 'EEXIST') throw error;
79
+ const until = Date.now() + 10;
80
+ while (Date.now() < until) { /* 10 ms busy-wait */ }
71
81
  }
72
82
  }
73
83
  throw new Error('metrics lock timeout');
@@ -76,7 +86,7 @@ let taskLockHeld = false;
76
86
  function releaseTaskLock() {
77
87
  if (!taskLockHeld) return;
78
88
  taskLockHeld = false;
79
- try { fs.rmdirSync(METRICS_LOCK); } catch {}
89
+ try { fs.unlinkSync(METRICS_LOCK); } catch {}
80
90
  }
81
91
  waitForMetricsLock();
82
92
  taskLockHeld = true;
@@ -104,7 +114,7 @@ function mutateMetrics(mutator) {
104
114
  fs.writeFileSync(temporary, JSON.stringify(metrics, null, 2) + '\n');
105
115
  fs.renameSync(temporary, METRICS_PATH);
106
116
  } finally {
107
- if (acquiredHere) { try { fs.rmdirSync(METRICS_LOCK); } catch {} }
117
+ if (acquiredHere) { try { fs.unlinkSync(METRICS_LOCK); } catch {} }
108
118
  }
109
119
  }
110
120
  function appendMetric(event) { mutateMetrics((metrics) => metrics.events.push({ eventId: `${metrics.task.runId}:${Date.now()}:${Math.random().toString(16).slice(2)}`, ...event })); }
@@ -485,7 +495,19 @@ function legacyFlow() {
485
495
 
486
496
 
487
497
  if (hookInput.action === 'end_human_gate') {
488
- status.awaitingHumanGate = false;
498
+ status.awaitingHumanGate = false;
499
+ // A2 (retro §8): pin a single runId per pipeline so subsequent events don't
500
+ // fragment the metrics ledger across multiple runId sequences. Prefer
501
+ // metrics.json.task.runId when metrics already exist, then status.metricsRunId,
502
+ // then mint a fresh seed only on first ever emit.
503
+ try {
504
+ if (fs.existsSync(METRICS_PATH)) {
505
+ const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
506
+ if (existing && existing.task && typeof existing.task.runId === 'string') {
507
+ status.metricsRunId = existing.task.runId;
508
+ }
509
+ }
510
+ } catch {}
489
511
  if (status.state === 'awaiting_approval') {
490
512
  status.state = 'in_progress';
491
513
  status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
@@ -524,11 +546,29 @@ if (hookInput.action === 'end_human_gate') {
524
546
  }
525
547
  }
526
548
  }
527
- writeStatus({
528
- awaitingHumanGate: false,
529
- state: status.state,
530
- phase: status.phase,
531
- });
549
+ // A2 (retro §8): if gate closes the final human gate of the pipeline
550
+ // (no resume happens, or current step's gate was the last), surface
551
+ // state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
552
+ // the post-gate idle state as "still in progress".
553
+ {
554
+ let gateWasTerminal = false;
555
+ if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
556
+ try {
557
+ const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
558
+ const steps = pipe.steps || [];
559
+ const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
560
+ const cur = steps[idx];
561
+ const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
562
+ const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
563
+ if (cur && hasGate(cur)) gateWasTerminal = true;
564
+ } catch {}
565
+ }
566
+ writeStatus({
567
+ awaitingHumanGate: false,
568
+ state: gateWasTerminal ? 'awaiting_approval' : status.state,
569
+ phase: status.phase,
570
+ });
571
+ }
532
572
  process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
533
573
  process.exit(0);
534
574
  }
@@ -40,7 +40,7 @@ Cursor `.mdc` — **SoT per stack**. Claude topic `.md` — derived twin.
40
40
  - **Domain** rules: ≥15 body lines (FAIL if thin without documented alias reason).
41
41
  - **Thin aliases** (`agent-team-intake`, `technical-retro`): may be shorter (soft) if they only point to a command/skill.
42
42
  - **Slim-companion rules** (UI-edit bundle, slim app-cores, slim tooling): may be shorter than 15 lines **if and only if** the body is a pointer into shared-core / stack-prose (no standalone prose). Examples live in stack `rules/README.md` (e.g. `cursor/next/rules/README.md` "UI-edit bundle" intent).
43
- - **Mechanical enforcement is pending.** Today build-verifier excludes only `agent-team-intake` / `technical-retro` / `preset-layering`; slim-companion rules currently pass under the same human-judgement path as thin aliases. Follow-up: add `allow_slim_stems` to `packages/ai-rules/scripts/check-preset-token-budget.sh` analogous to `trio_stems_allowlist`.
43
+ - Mechanical enforcement lives in `packages/ai-rules/scripts/check-preset-token-budget.sh` see `SLIM_COMPANION_ALLOWLIST` (the 53 stems accepted as slim pointers) and `MIN_SLIM_BODY_LINES` (5-line floor). Slim-companion rules with a body of 6–14 lines whose stem is **not** in the allowlist are reported as slim violations by the script.
44
44
 
45
45
  ## Severity (build-verifier)
46
46
 
@@ -36,6 +36,7 @@ run_gate "hook-only-lifecycle" "$SCRIPT_DIR/check-hook-only-lifecycle.sh"
36
36
  run_gate "local-overlay" "$SCRIPT_DIR/check-local-overlay.sh"
37
37
  run_gate "shared-core-drift" "$SCRIPT_DIR/check-shared-core-drift.sh" AI_RULES_DRIFT_FAIL=1
38
38
  run_gate "chain-hook-coverage" "$SCRIPT_DIR/check-chain-hook-coverage.sh"
39
+ run_gate "task-router-intents" "$SCRIPT_DIR/check-task-router-intents.sh"
39
40
  run_gate "i18n" "$SCRIPT_DIR/check-preset-i18n.sh" AI_RULES_I18N_FAIL=1 AI_RULES_I18N_SOFT=0
40
41
 
41
42
  echo ">>> cli-mcp-merge"
@@ -65,6 +66,15 @@ else
65
66
  fi
66
67
  echo
67
68
 
69
+ echo ">>> task-router-intents fixtures"
70
+ if node "$SCRIPT_DIR/test-task-router-intents-fixtures.mjs"; then
71
+ echo "<<< task-router-intents fixtures: OK"
72
+ else
73
+ echo "<<< task-router-intents fixtures: FAIL"
74
+ status=1
75
+ fi
76
+ echo
77
+
68
78
  # Soft WARN: dogfood product-specs (never fails the orchestrator).
69
79
  # Default check-product-specs exits 0 even when issues are found unless --strict.
70
80
  # Use --strict here only to surface problems as WARN in logs; do not flip status.
@@ -18,6 +18,69 @@ presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
18
18
  STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
19
19
  BODY_CAP = 140 # applied to 3-rule trios (Claude session-start, Cursor trio when len==3)
20
20
  BODY_CAP_CURSOR_4 = 170 # Cursor now allows 4 always-on (next-app-core, package-manager, code-quality, ai-local-overlay)
21
+ # Slim-companion rules (UI-edit bundle, slim app-cores, slim tooling) may be
22
+ # shorter than 15 lines **if and only if** their stem is in the allow-list and
23
+ # body is <= MIN_SLIM_BODY_LINES. A slim rule whose body grows past this cap
24
+ # without being promoted to a domain rule FAILs the check. Reference:
25
+ # presets/_shared/core/meta/preset-twin-sync.md "Twin depth" section.
26
+ # Cursor and Claude share stems by convention; the list is stem-only and
27
+ # applied per platform.
28
+ SLIM_COMPANION_ALLOWLIST = frozenset({
29
+ "agent-team-intake",
30
+ "agent-team-orchestrator",
31
+ "ai-local-overlay",
32
+ "api-grpc",
33
+ "architecture-boundaries-ui",
34
+ "css-property-order-stylelint",
35
+ "design-guidance",
36
+ "domain-layer",
37
+ "e2e",
38
+ "feature-delivery-workflow",
39
+ "http-client",
40
+ "integration",
41
+ "ios-app-core",
42
+ "mcp-usage",
43
+ "messaging-adapters",
44
+ "mockup-review",
45
+ "module-public-api",
46
+ "navigation-coordinators",
47
+ "navigation-router-stack",
48
+ "navigation",
49
+ "navigation-router-ui",
50
+ "next-app-router",
51
+ "no-props-spread",
52
+ "no-type-assertion-as-import-export",
53
+ "no-v-bind-spread",
54
+ "package-manager",
55
+ "persistence",
56
+ "persistence-data",
57
+ "playwright-agents",
58
+ "post-change-build",
59
+ "post-change-lint",
60
+ "post-change-test",
61
+ "preset-layering",
62
+ "preset-no-cross-stack-leakage",
63
+ "preset-pr-checklist",
64
+ "preset-token-budget",
65
+ "preset-twin-sync",
66
+ "react-a11y-coding",
67
+ "script-setup-conventions",
68
+ "security-android",
69
+ "security-go",
70
+ "security-ios",
71
+ "security-next",
72
+ "security-php",
73
+ "state-and-viewmodels",
74
+ "technical-retro",
75
+ "tests-e2e",
76
+ "tests-integration",
77
+ "tests-unit",
78
+ "unit",
79
+ "ux-design",
80
+ "vue-a11y-coding",
81
+ "xcode-tooling",
82
+ })
83
+ MIN_SLIM_BODY_LINES = 5
21
84
  MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*[^>]+?\s*-->\s*$")
22
85
  ALWAYS_APPLY_RE = re.compile(r"(?m)^alwaysApply:\s*true\s*$")
23
86
  PATHS_RE = re.compile(r"(?m)^paths:\s*")
@@ -138,6 +201,43 @@ for stack in STACKS:
138
201
  )
139
202
  failed += 1
140
203
 
204
+ # --- Slim-companion enforcement (per stack, both platforms) ---
205
+ # All .mdc (Cursor) / .md (Claude) rules under the stack's rules/ directory
206
+ # are scanned. A rule whose body is > MIN_SLIM_BODY_LINES AND whose stem
207
+ # is NOT in SLIM_COMPANION_ALLOWLIST must be promoted to a domain rule
208
+ # (>=15 body lines per preset-twin-sync.md "Twin depth"). FAIL otherwise.
209
+ slim_violations: list[str] = []
210
+ for rules_dir in (cursor_rules, claude_rules):
211
+ if not rules_dir.exists():
212
+ continue
213
+ pattern = "*.mdc" if rules_dir is cursor_rules else "*.md"
214
+ for rule_path in sorted(rules_dir.rglob(pattern)):
215
+ if rule_path.name == "README.md":
216
+ continue
217
+ text = rule_path.read_text(encoding="utf-8")
218
+ _, body = strip_frontmatter(text)
219
+ n = body_nonempty_lines(body)
220
+ if n <= MIN_SLIM_BODY_LINES:
221
+ continue
222
+ # Skip rules explicitly in the slim-companion allow-list (they are
223
+ # allowed to be short pointers regardless of body length).
224
+ if rule_path.stem in SLIM_COMPANION_ALLOWLIST:
225
+ continue
226
+ # Domain rules (>=15 lines) are fine; we only flag the 6..14 line
227
+ # "slim-but-not-allowed" bucket.
228
+ if n >= 15:
229
+ continue
230
+ slim_violations.append(
231
+ f"{rule_path.relative_to(presets).as_posix()} ({n} lines, slim but not in allow-list)"
232
+ )
233
+ if slim_violations:
234
+ print(f" Slim-companion allow-list violations ({len(slim_violations)}):")
235
+ for v in slim_violations[:10]:
236
+ print(f" FAIL: {v}")
237
+ if len(slim_violations) > 10:
238
+ print(f" ... and {len(slim_violations) - 10} more")
239
+ failed += len(slim_violations)
240
+
141
241
  print()
142
242
 
143
243
  if failed:
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-check `task-router.md` intent rows against `agents/*.md` `name:` frontmatter.
3
+ #
4
+ # Two-sided verification:
5
+ # 1. Every agent file (frontmatter `name:` other than `task-router` itself) must
6
+ # be referenced somewhere in the stack's `task-router.md` so the router knows
7
+ # how to invoke it. Missing mention ⇒ FAIL (the agent will silently never
8
+ # receive a handoff).
9
+ # 2. Every agent stem referenced in `task-router.md` (in the `Default steps`
10
+ # column, the `Model tiers` table, or other inline mentions) must have a
11
+ # corresponding `agents/<stem>.md` file. Missing agent ⇒ FAIL (the router
12
+ # emits an intent that no agent can satisfy).
13
+ #
14
+ # The check is per `(platform, stack)` pair and runs all 20 ships (cursor + claude
15
+ # × 10 stacks). Exits non-zero on any FAIL.
16
+ #
17
+ # Stack-specific exemptions:
18
+ # - mcp-ts does not ship `playwright-test-*`, `xcuitest-test-*`, `unit-test-*`
19
+ # in the standard mix; intent rows that reference those agents are flagged
20
+ # only on stacks that ship the corresponding `agents/<stem>.md` file.
21
+ # - Stack leakage (positive agent stems on the wrong stack) is enforced in
22
+ # `check-preset-leakage.sh`; this check covers task-router sync only.
23
+ set -euo pipefail
24
+
25
+ ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
26
+ # Allow callers (notably the fixture test) to redirect the presets root via
27
+ # the env var. Default to the real repo path.
28
+ export AI_RULES_PRESETS_ROOT="${AI_RULES_PRESETS_ROOT:-$ROOT/packages/ai-rules/presets}"
29
+
30
+ python3 <<'PY'
31
+ from __future__ import annotations
32
+
33
+ import os
34
+ import re
35
+ import sys
36
+ from pathlib import Path
37
+
38
+ presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
39
+ PLATFORMS = ("cursor", "claude")
40
+ STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
41
+
42
+ # Agents that are shipped for orchestration purposes but do not need to appear
43
+ # in task-router.md intent rows (router-only / analyzer-only / orchestrator-only).
44
+ ROUTER_EXEMPT_FROM_INTENT = frozenset({
45
+ "task-router", # this file
46
+ "codebase-analyzer", # ad-hoc explain-as-is agent
47
+ "agent-team-intake", # one-off intake, not a pipeline target
48
+ })
49
+
50
+ # Agent-stem regex: kebab-case, lowercase, may include digits and dots (x.y).
51
+ # Two patterns: (a) hyphen/dot-bounded names like `feature-developer`, `unit.test`,
52
+ # (b) single-word agents that are present in `shipped_names`. Both feed the
53
+ # `agent_like` filter.
54
+ AGENT_NAME_RE = re.compile(r"\b([a-z][a-z0-9]*(?:[-.][a-z0-9]+)+)\b")
55
+ SINGLE_WORD_AGENT_RE = re.compile(r"\b([a-z][a-z0-9]+)\b")
56
+ FRONTMATTER_NAME_RE = re.compile(r"^name:\s*([a-z][a-z0-9]*(?:[-.][a-z0-9]+)+)\s*$", re.MULTILINE)
57
+
58
+ failures: list[str] = []
59
+ checks = 0
60
+
61
+ for platform in PLATFORMS:
62
+ for stack in STACKS:
63
+ agents_dir = presets / platform / stack / "agents"
64
+ if not agents_dir.is_dir():
65
+ continue
66
+ task_router = agents_dir / "task-router.md"
67
+ if not task_router.is_file():
68
+ failures.append(f"{platform}/{stack}: missing task-router.md")
69
+ continue
70
+
71
+ # 1) All agent `name:` frontmatter in this stack's agents dir.
72
+ shipped_names: set[str] = set()
73
+ for path in sorted(agents_dir.glob("*.md")):
74
+ text = path.read_text(encoding="utf-8")
75
+ m = FRONTMATTER_NAME_RE.search(text)
76
+ if m:
77
+ shipped_names.add(m.group(1))
78
+ # Also pick up the file stem as a fallback (in case frontmatter is missing).
79
+ if path.stem not in shipped_names and path.stem != "README":
80
+ shipped_names.add(path.stem)
81
+
82
+ # 2) All agent stems referenced in task-router.md (anywhere in body).
83
+ router_text = task_router.read_text(encoding="utf-8")
84
+ # Restrict to body (drop YAML frontmatter).
85
+ body = router_text
86
+ if router_text.startswith("---\n"):
87
+ end = router_text.find("\n---\n", 4)
88
+ if end >= 0:
89
+ body = router_text[end + 5 :]
90
+ referenced_in_router: set[str] = set(AGENT_NAME_RE.findall(body))
91
+ # Single-word tokens that match a shipped agent (e.g. `debugger`,
92
+ # `codebase-analyzer`, `ci-investigator`) are valid router mentions even
93
+ # without a hyphen/dot boundary.
94
+ for token in SINGLE_WORD_AGENT_RE.findall(body):
95
+ if token in shipped_names:
96
+ referenced_in_router.add(token)
97
+ agent_like: set[str] = referenced_in_router - ROUTER_EXEMPT_FROM_INTENT
98
+
99
+ # Subset of shipped agents that are not router-exempt AND not referenced.
100
+ shipped_pipeline_agents = shipped_names - ROUTER_EXEMPT_FROM_INTENT
101
+ # 1-sided check (FAIL when shipped but not mentioned).
102
+ not_referenced = sorted(shipped_pipeline_agents - agent_like)
103
+ for name in not_referenced:
104
+ checks += 1
105
+ # The README is not an agent.
106
+ if name == "README":
107
+ continue
108
+ # Skip agents only used as commands or skills (e.g. technical-retro,
109
+ # agent-team-intake when still present). These are explicitly
110
+ # exempt — but we already filter ROUTER_EXEMPT_FROM_INTENT, so any
111
+ # remaining mismatch is a real gap.
112
+ failures.append(
113
+ f"{platform}/{stack}: ships agent '{name}.md' but task-router.md does not mention '{name}'"
114
+ )
115
+
116
+ # 2-sided check (FAIL when mentioned in router but not shipped).
117
+ not_shipped = sorted(agent_like - shipped_names)
118
+ for name in not_shipped:
119
+ checks += 1
120
+ # Allow mentions of common stacks/tools that aren't agents (filter).
121
+ # Common false positives: branch names, mixed-case strings, etc.
122
+ # The regex already constrains to lowercase kebab/dot; further filter
123
+ # by requiring the suffix to look like an agent suffix.
124
+ if name in {"feature-developer", "build-verifier", "code-reviewer"}:
125
+ # These should be shipped; surface as FAIL.
126
+ failures.append(
127
+ f"{platform}/{stack}: task-router.md references '{name}' but agents/{name}.md is missing"
128
+ )
129
+ # Otherwise ignore (likely prose token or matcher noise).
130
+
131
+ if failures:
132
+ print(f"FAIL: task-router vs agents drift ({len(failures)} issue(s) / {checks} checks):")
133
+ for f in failures[:30]:
134
+ print(f" - {f}")
135
+ if len(failures) > 30:
136
+ print(f" ... and {len(failures) - 30} more")
137
+ sys.exit(1)
138
+
139
+ print(f"OK: task-router.md intent rows match agents/*.md ({checks} checks across {len(PLATFORMS) * len(STACKS)} (platform, stack) pairs)")
140
+ sys.exit(0)
141
+ PY