@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
@@ -64,10 +64,20 @@ const STACK = 'ios-swift';
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
 
@@ -65,3 +65,7 @@ Document PASS/FAIL in `validation-report.md`.
65
65
 
66
66
  13. **Product-specs assets (FAIL):** package must ship `presets/_shared/assets/docs-specs/INDEX.md`, `_templates/page.md`, `_templates/feature.md`. Cursor+Claude twins for `product-specs` and `product-specs-authoring` must embed matching shared-core markers; both stems **requestable** (`alwaysApply: false` / Claude `paths: docs/specs/**`) — never session-start.
67
67
  14. **Optional consumer check (WARN default):** `scripts/check-product-specs.sh` + schema stub may exist for opt-in consumer CI; **FAIL** only with `--strict` or AC that requires named specs — do not force consumer CI.
68
+
69
+ ## Receipt binding
70
+
71
+ > **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 -->
@@ -49,3 +49,7 @@ Write `.cursor/team/tasks/<slug>/review.md` with verdict APPROVE | REQUEST_CHANG
49
49
  - **Owned outputs:** `review.md`; upsert only the manifest entry assigned by handoff (`artifactOutputId`; fallback `review`) and preserve all foreign entries.
50
50
  - **Terminal receipt:** cover applicable AC/task IDs, list authoritative evidence paths, and emit `completed` or `changes_requested`. Bind it to the supplied `attemptId` when present.
51
51
  - **Shared state:** never create or mutate `status.json` or `metrics.json`; the orchestration runtime/hook owns lifecycle, gates, retries, attempts, and timestamps.
52
+
53
+ ## Receipt binding
54
+
55
+ > **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 -->
@@ -42,3 +42,7 @@ If a Sentry/Datadog (or similar) MCP is ready and the task is a production error
42
42
 
43
43
  - When writing or changing code, load / follow rule `anti-sycophancy-discipline`.
44
44
  - When acting on review feedback (`changes_requested` / human comments): verify each item against the codebase before implementing; no performative agreement.
45
+
46
+ ## Receipt binding
47
+
48
+ > **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 -->
@@ -11,6 +11,7 @@ Implement Java features following hexagonal boundaries.
11
11
  1. Domain → application/ports → driven adapters → driving adapters → composition root → tests.
12
12
  2. Read `feature-delivery-workflow.mdc`, `architecture-boundaries.mdc`, layer globs.
13
13
  3. After `*.java` edits: invoke `post-change-test.mdc` (+ `java-tooling.mdc`).
14
+ 4. **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, 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.
14
15
 
15
16
  ## Do / Don't
16
17
 
@@ -89,3 +90,7 @@ Load requestable rule `mcp-usage` when verifying third-party library APIs (Conte
89
90
  - **Owned outputs:** `implementation.md` plus changed implementation/test paths; upsert only the manifest entry assigned by handoff (`artifactOutputId`; fallback `implementation`) and preserve all foreign entries.
90
91
  - **Terminal receipt:** cover applicable AC/task IDs, list authoritative evidence paths, and emit `completed` or `blocked`. Bind it to the supplied `attemptId` when present.
91
92
  - **Shared state:** never create or mutate `status.json` or `metrics.json`; the orchestration runtime/hook owns lifecycle, gates, retries, attempts, and timestamps.
93
+
94
+ ## Receipt binding
95
+
96
+ > **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 -->
@@ -22,3 +22,7 @@ Coordinate with existing plans under `.cursor/team/tasks/<slug>/`.
22
22
  ## Design guidance
23
23
 
24
24
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
25
+
26
+ ## Receipt binding
27
+
28
+ > **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 -->
@@ -74,6 +74,10 @@ Keep top-level `profile` and emit this top-level object in every new `pipeline.j
74
74
  - Use concrete flags: `public-contract`, `cross-layer`, `security-sensitive`, `migration`, `destructive`, `concurrency-state`, `external-dependency`, `unclear-acceptance-criteria`, `broad-test-surface`, `behavior-regression`.
75
75
  - `routingReasons` must be non-empty and explain profile, gates, specialists, skips, or checkpoints; never write “standard by default.”
76
76
  - `estimatedWorkPackages` is an integer ≥1; `openDecisionCount` is an integer ≥0. Do not lower complexity because implementation is familiar.
77
+ - Add **api-contract-reviewer** for new/changed backend contracts — before developer.
78
+ - Add **accessibility-reviewer** / **security-reviewer** after build-verifier (parallel when both apply).
79
+ - Add **tech-writer** when docs/changelog requested.
80
+
77
81
 
78
82
  ## Model tiers (`steps[].model`)
79
83
 
@@ -65,10 +65,20 @@ const STACK = 'java';
65
65
  const PLATFORM = 'cursor';
66
66
 
67
67
  function waitForMetricsLock() {
68
+ // OD-1: real advisory lock via create-exclusive (fs.openSync(_, 'wx')).
69
+ // The previous impl spun on Atomics.wait against an unrelated SharedArrayBuffer
70
+ // and never blocked on the lock file, so two concurrent subagentStop events
71
+ // could both acquire the lock and corrupt metrics.json. Retry EEXIST with a
72
+ // 10 ms sleep up to 200 attempts (2 s ceiling). On any other error, throw.
68
73
  for (let attempt = 0; attempt < 200; attempt += 1) {
69
- try { fs.mkdirSync(METRICS_LOCK); return; } catch (error) {
70
- if (error && error.code !== 'EEXIST') throw error;
71
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
74
+ try {
75
+ const fd = fs.openSync(METRICS_LOCK, 'wx');
76
+ try { fs.closeSync(fd); } catch {}
77
+ return;
78
+ } catch (error) {
79
+ if (!error || error.code !== 'EEXIST') throw error;
80
+ const until = Date.now() + 10;
81
+ while (Date.now() < until) { /* 10 ms busy-wait */ }
72
82
  }
73
83
  }
74
84
  throw new Error('metrics lock timeout');
@@ -77,7 +87,7 @@ let taskLockHeld = false;
77
87
  function releaseTaskLock() {
78
88
  if (!taskLockHeld) return;
79
89
  taskLockHeld = false;
80
- try { fs.rmdirSync(METRICS_LOCK); } catch {}
90
+ try { fs.unlinkSync(METRICS_LOCK); } catch {}
81
91
  }
82
92
  waitForMetricsLock();
83
93
  taskLockHeld = true;
@@ -105,7 +115,7 @@ function mutateMetrics(mutator) {
105
115
  fs.writeFileSync(temporary, JSON.stringify(metrics, null, 2) + '\n');
106
116
  fs.renameSync(temporary, METRICS_PATH);
107
117
  } finally {
108
- if (acquiredHere) { try { fs.rmdirSync(METRICS_LOCK); } catch {} }
118
+ if (acquiredHere) { try { fs.unlinkSync(METRICS_LOCK); } catch {} }
109
119
  }
110
120
  }
111
121
  function appendMetric(event) { mutateMetrics((metrics) => metrics.events.push({ eventId: `${metrics.task.runId}:${Date.now()}:${Math.random().toString(16).slice(2)}`, ...event })); }
@@ -480,7 +490,19 @@ function legacyFlow() {
480
490
 
481
491
 
482
492
  if (hookInput.action === 'end_human_gate') {
483
- status.awaitingHumanGate = false;
493
+ status.awaitingHumanGate = false;
494
+ // A2 (retro §8): pin a single runId per pipeline so subsequent events don't
495
+ // fragment the metrics ledger across multiple runId sequences. Prefer
496
+ // metrics.json.task.runId when metrics already exist, then status.metricsRunId,
497
+ // then mint a fresh seed only on first ever emit.
498
+ try {
499
+ if (fs.existsSync(METRICS_PATH)) {
500
+ const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
501
+ if (existing && existing.task && typeof existing.task.runId === 'string') {
502
+ status.metricsRunId = existing.task.runId;
503
+ }
504
+ }
505
+ } catch {}
484
506
  if (status.state === 'awaiting_approval') {
485
507
  status.state = 'in_progress';
486
508
  status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
@@ -519,11 +541,29 @@ if (hookInput.action === 'end_human_gate') {
519
541
  }
520
542
  }
521
543
  }
522
- writeStatus({
523
- awaitingHumanGate: false,
524
- state: status.state,
525
- phase: status.phase,
526
- });
544
+ // A2 (retro §8): if gate closes the final human gate of the pipeline
545
+ // (no resume happens, or current step's gate was the last), surface
546
+ // state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
547
+ // the post-gate idle state as "still in progress".
548
+ {
549
+ let gateWasTerminal = false;
550
+ if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
551
+ try {
552
+ const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
553
+ const steps = pipe.steps || [];
554
+ const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
555
+ const cur = steps[idx];
556
+ const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
557
+ const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
558
+ if (cur && hasGate(cur)) gateWasTerminal = true;
559
+ } catch {}
560
+ }
561
+ writeStatus({
562
+ awaitingHumanGate: false,
563
+ state: gateWasTerminal ? 'awaiting_approval' : status.state,
564
+ phase: status.phase,
565
+ });
566
+ }
527
567
  process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
528
568
  process.exit(0);
529
569
  }
@@ -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
 
@@ -59,3 +59,7 @@ Document PASS/FAIL in `validation-report.md`. Soft leakage output:
59
59
 
60
60
  13. **Product-specs assets (FAIL):** package must ship `presets/_shared/assets/docs-specs/INDEX.md`, `_templates/page.md`, `_templates/feature.md`. Cursor+Claude twins for `product-specs` and `product-specs-authoring` must embed matching shared-core markers; both stems **requestable** (`alwaysApply: false` / Claude `paths: docs/specs/**`) — never session-start.
61
61
  14. **Optional consumer check (WARN default):** `scripts/check-product-specs.sh` + schema stub may exist for opt-in consumer CI; **FAIL** only with `--strict` or AC that requires named specs — do not force consumer CI.
62
+
63
+ ## Receipt binding
64
+
65
+ > **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 -->
@@ -19,6 +19,7 @@ Implement MCP server features following **mcp-ts** boundaries.
19
19
 
20
20
  1. Read `feature-delivery-workflow.mdc`, `mcp-server-boundaries.mdc`, `reference-features.mdc`.
21
21
  2. After `*.ts` edits: invoke `post-change-test.mdc` (+ `mcp-ts-tooling.mdc`).
22
+ 3. **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, 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.
22
23
 
23
24
  ## Do / Don't
24
25
 
@@ -88,3 +89,7 @@ Load requestable rule `mcp-usage` when verifying third-party library APIs (Conte
88
89
  - **Owned outputs:** `implementation.md` plus changed implementation/test paths; upsert only the manifest entry assigned by handoff (`artifactOutputId`; fallback `implementation`) and preserve all foreign entries.
89
90
  - **Terminal receipt:** cover applicable AC/task IDs, list authoritative evidence paths, and emit `completed` or `blocked`. Bind it to the supplied `attemptId` when present.
90
91
  - **Shared state:** never create or mutate `status.json` or `metrics.json`; the orchestration runtime/hook owns lifecycle, gates, retries, attempts, and timestamps.
92
+
93
+ ## Receipt binding
94
+
95
+ > **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 -->
@@ -19,7 +19,7 @@ You are a task router for a **TypeScript MCP server** agent team. You **do not**
19
19
  |--------|---------|---------------|
20
20
  | `feature` | "add tool", "new", "implement", schema/handler | task-analyst → feature-developer → build-verifier → **security-reviewer** when [sensitive](#security-reviewer-routing); else document skip |
21
21
  | `bugfix` | "bug", "fix", "doesn't work", "crashes" | task-analyst → feature-developer → build-verifier → **security-reviewer** when [sensitive](#security-reviewer-routing); else document skip |
22
- | `review-only` | "review", "review MR", "check diff" | `security-reviewer` for threat/capability/auth/secrets diffs; **plus** parent applies skill `code-review` (write `review.md`) — no `code-reviewer` agent in Phase 1. Pure schema/style-only diffs: skip security step, parent skill only (document in `skipped`) |
22
+ | `review-only` | "review", "review MR", "check diff" | `security-reviewer` for threat/capability/auth/secrets diffs; **plus** parent applies skill `code-review` (write `review.md`) — no dedicated agent in Phase 1. Pure schema/style-only diffs: skip security step, parent skill only (document in `skipped`) |
23
23
  | `spike` | "investigate", "spike", proof of concept | task-analyst → solution-architect |
24
24
  | `refactor` | "refactor", "no behavior change" | task-analyst → feature-developer → build-verifier → **security-reviewer** when [sensitive](#security-reviewer-routing); else document skip |
25
25
  | `retro` | "retro", "postmortem" | (orchestrator → `/technical-retro`) |
@@ -66,10 +66,20 @@ const STACK = 'mcp-ts';
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 })); }
@@ -425,7 +435,19 @@ function legacyFlow() {
425
435
 
426
436
 
427
437
  if (hookInput.action === 'end_human_gate') {
428
- status.awaitingHumanGate = false;
438
+ status.awaitingHumanGate = false;
439
+ // A2 (retro §8): pin a single runId per pipeline so subsequent events don't
440
+ // fragment the metrics ledger across multiple runId sequences. Prefer
441
+ // metrics.json.task.runId when metrics already exist, then status.metricsRunId,
442
+ // then mint a fresh seed only on first ever emit.
443
+ try {
444
+ if (fs.existsSync(METRICS_PATH)) {
445
+ const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
446
+ if (existing && existing.task && typeof existing.task.runId === 'string') {
447
+ status.metricsRunId = existing.task.runId;
448
+ }
449
+ }
450
+ } catch {}
429
451
  if (status.state === 'awaiting_approval') {
430
452
  status.state = 'in_progress';
431
453
  status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
@@ -464,11 +486,29 @@ if (hookInput.action === 'end_human_gate') {
464
486
  }
465
487
  }
466
488
  }
467
- writeStatus({
468
- awaitingHumanGate: false,
469
- state: status.state,
470
- phase: status.phase,
471
- });
489
+ // A2 (retro §8): if gate closes the final human gate of the pipeline
490
+ // (no resume happens, or current step's gate was the last), surface
491
+ // state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
492
+ // the post-gate idle state as "still in progress".
493
+ {
494
+ let gateWasTerminal = false;
495
+ if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
496
+ try {
497
+ const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
498
+ const steps = pipe.steps || [];
499
+ const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
500
+ const cur = steps[idx];
501
+ const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
502
+ const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
503
+ if (cur && hasGate(cur)) gateWasTerminal = true;
504
+ } catch {}
505
+ }
506
+ writeStatus({
507
+ awaitingHumanGate: false,
508
+ state: gateWasTerminal ? 'awaiting_approval' : status.state,
509
+ phase: status.phase,
510
+ });
511
+ }
472
512
  process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
473
513
  process.exit(0);
474
514
  }
@@ -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
 
@@ -107,6 +107,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **FAI
107
107
 
108
108
  Do not fix code — report only. Do not advance to code-reviewer until PASS.
109
109
 
110
+ **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 -->
111
+
110
112
  ## Artifact contract
111
113
 
112
114
  - **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.
@@ -54,6 +54,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **REQ
54
54
 
55
55
  Do not mix this output with retrospective facilitation — keep review and retro separate.
56
56
 
57
+ **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 -->
58
+
57
59
  ## Design guidance
58
60
 
59
61
  - 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 Next.js monorepo with strict la
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 -->
@@ -16,7 +16,7 @@ You generate unit tests from project test plans.
16
16
  ## Rules
17
17
 
18
18
  1. Follow **`unit-testing` skill** and **`tests-unit.mdc`**.
19
- 2. Write specs colocated with source: `*.spec.ts` / `*.spec.tsx` next to the module under test.
19
+ 2. Write specs under the mirrored path `app/src/<rel>` `app/__tests__/unit/<rel>`; do not colocate them under `app/src/**`.
20
20
  3. Use `@testing-library/react` and `userEvent` for components; project test-store/mocks for store/API.
21
21
  4. `describe` / `it` titles in **Russian**, each sentence starting with a capital letter.
22
22
  5. Do not weaken or skip scenarios from the plan without documenting a blocker in the handoff.
@@ -19,7 +19,7 @@ Use the **`unit-testing` skill** and **`tests-unit.mdc`**.
19
19
 
20
20
  1. Map acceptance criteria to test scenarios (mappers, store logic, client behavior, components).
21
21
  2. Identify gaps vs existing specs — do not duplicate covered cases.
22
- 3. Specify file paths for planned specs (colocated with source per project convention).
22
+ 3. Specify mirrored file paths for planned specs: `app/src/<rel>` `app/__tests__/unit/<rel>`.
23
23
  4. List behavior branches: happy path, edge cases, error paths.
24
24
 
25
25
  ## Output
@@ -64,10 +64,20 @@ const STACK = 'next';
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
  }
@@ -39,7 +39,7 @@ impact: HIGH
39
39
 
40
40
  ## Agent requirement
41
41
 
42
- - When changing the shared client implementation — **update or add behavior tests** next to the client module (`tests-unit.mdc`).
42
+ - When changing the shared client implementation — **update or add behavior tests** under the mirrored `app/__tests__/unit/lib/clients/**` path (`tests-unit.mdc`).
43
43
  - Do not introduce a **second full HTTP stack** without an explicit task and alignment with repo architecture.
44
44
 
45
45
  ## Incorrect