@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonesofspring/ai-rules",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "description": "Presets of Cursor and Claude rules/commands for Revy Ross personal use",
5
5
  "license": "MIT",
6
6
  "author": "Revy Ross",
@@ -78,6 +78,8 @@ A producer finishes its step by upserting only its owned artifact entry and term
78
78
 
79
79
  `blocked` is terminal for the producer attempt but non-advancing for the pipeline: the hook persists `status.state` / `phase` as `blocked` and emits no next-agent handoff. A receipt must either carry the active `attemptId` or belong to the step's assigned output entry. Its terminal timestamp must be at or after the active attempt's `startedAt`; otherwise the hook keeps the attempt active in `awaiting_artifact_receipt` and does not advance or retry.
80
80
 
81
+ > **Receipt binding (owner prompts).** Each owning-agent prompt (`feature-developer`, `build-verifier`, `code-reviewer`, `debugger`, `qa-tester` across `cursor/` and `claude/` platforms × 10 stacks) carries the same one-paragraph callout under `## On completion` (or appended as `## Receipt binding` when no `On completion` section exists). When writing the terminal receipt, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>`. 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.
82
+
81
83
  ## Ownership and writes
82
84
 
83
85
  - Agents own their output artifacts and their manifest entries/receipts. They merge by stable `id` and preserve all foreign entries.
@@ -29,7 +29,7 @@ Cursor `.mdc` — **SoT per stack**. Claude topic `.md` — derived twin.
29
29
  - **Domain** rules: ≥15 body lines (FAIL if thin without documented alias reason).
30
30
  - **Thin aliases** (`agent-team-intake`, `technical-retro`): may be shorter (soft) if they only point to a command/skill.
31
31
  - **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).
32
- - **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`.
32
+ - 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.
33
33
 
34
34
  ## Severity (build-verifier)
35
35
 
@@ -71,6 +71,8 @@ Write `.claude/team/tasks/<slug>/validation-report.md` with verdict, commands, f
71
71
 
72
72
  Write the owned validation receipt (`completed` or `validation_failed`). Do not fix code — report only.
73
73
 
74
+ **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 -->
75
+
74
76
  ## Artifact contract
75
77
 
76
78
  - **Authoritative inputs:** read `.claude/team/tasks/<slug>/artifact-manifest.json` first; consume handoff `artifactInputIds` when supplied.
@@ -66,6 +66,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **REQ
66
66
 
67
67
  Do not mix this output with retrospective facilitation — keep review and retro separate.
68
68
 
69
+ **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 -->
70
+
69
71
  ## Design guidance
70
72
 
71
73
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
@@ -57,6 +57,7 @@ fixApplied: false
57
57
 
58
58
  Write the owned terminal receipt described under **Artifact contract**.
59
59
 
60
+ **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 -->
60
61
 
61
62
  ## 3-fix architecture gate
62
63
 
@@ -12,6 +12,7 @@ You are a senior Android developer (Jetpack Compose-first, Gradle modules as in
12
12
  1. Read `.claude/team/tasks/<slug>/brief.md`, `decomposition.md`, prior artifacts.
13
13
  2. `status.json` must allow work (`in_progress` / `approved` / `retryAfterFix`).
14
14
  3. Follow skill **`feature-delivery`** + `architecture/feature-delivery.md` + `architecture/reference-features.md`.
15
+ 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 (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.
15
16
 
16
17
  ## Rules
17
18
 
@@ -47,6 +48,8 @@ When copying from `next` into `android-kotlin` (or another stack):
47
48
 
48
49
  Emit the owned implementation receipt with outcome `completed`. Handoff by layer + gaps for verifier/reviewer. No formal code review.
49
50
 
51
+ **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 -->
52
+
50
53
  ## Design guidance
51
54
 
52
55
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
@@ -47,6 +47,8 @@ When the task is mostly UI e2e, prefer `instrumentation-test-planner`, `instrume
47
47
 
48
48
  Write the owned terminal receipt described under **Artifact contract**.
49
49
 
50
+ **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 -->
51
+
50
52
  ## Artifact contract
51
53
 
52
54
  - **Authoritative inputs:** read `.claude/team/tasks/<slug>/artifact-manifest.json` first; consume handoff `artifactInputIds` when supplied, otherwise consume the manifest entries for `brief.md`, `decomposition.md`, and other authoritative prior artifacts for this step.
@@ -82,6 +82,10 @@ Keep top-level `profile` and emit this top-level object in every new `pipeline.j
82
82
  - Use concrete flags: `public-contract`, `cross-layer`, `security-sensitive`, `migration`, `destructive`, `concurrency-state`, `external-dependency`, `unclear-acceptance-criteria`, `broad-test-surface`, `behavior-regression`.
83
83
  - `routingReasons` must be non-empty and explain profile, gates, specialists, skips, or checkpoints; never write “standard by default.”
84
84
  - `estimatedWorkPackages` is an integer ≥1; `openDecisionCount` is an integer ≥0. Do not lower complexity because implementation is familiar.
85
+ - Add **api-contract-reviewer** for new/changed backend contracts — before developer.
86
+ - Add **accessibility-reviewer** / **security-reviewer** after build-verifier (parallel when both apply).
87
+ - Add **tech-writer** when docs/changelog requested.
88
+
85
89
 
86
90
  ## Model tiers (`steps[].model`)
87
91
 
@@ -64,10 +64,20 @@ const STACK = 'android-kotlin';
64
64
  const PLATFORM = 'claude';
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
 
@@ -55,3 +55,7 @@ Document PASS/FAIL in `validation-report.md`.
55
55
 
56
56
  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.
57
57
  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.
58
+
59
+ ## Receipt binding
60
+
61
+ > **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 `.claude/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 Go 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 `*.go` edits: invoke `post-change-test.mdc` (+ `go-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 `.claude/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 = 'go';
65
65
  const PLATFORM = 'claude';
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
 
@@ -89,6 +89,8 @@ PASS | FAIL
89
89
 
90
90
  Write the owned validation receipt (`completed` or `validation_failed`). Do not fix code — report only; the runtime owns `status.json`.
91
91
 
92
+ **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 -->
93
+
92
94
  ## Artifact contract
93
95
 
94
96
  - **Authoritative inputs:** read `.claude/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.
@@ -66,6 +66,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **REQ
66
66
 
67
67
  Do not mix this output with retrospective facilitation — keep review and retro separate.
68
68
 
69
+ **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 -->
70
+
69
71
  ## Design guidance
70
72
 
71
73
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
@@ -62,6 +62,7 @@ Handoff summary: root cause, recommended fix, files touched (if any), `fixApplie
62
62
 
63
63
  Do not perform formal code review or write XCUITest plans — those are separate agents.
64
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 -->
65
66
 
66
67
  ## 3-fix architecture gate
67
68
 
@@ -12,6 +12,7 @@ You are a senior iOS developer (SwiftUI-first, SPM as in repo) with strict layer
12
12
  1. Read `.claude/team/tasks/<slug>/brief.md`, `decomposition.md`, prior artifacts.
13
13
  2. `status.json` must allow work (`in_progress` / `approved` / `retryAfterFix`).
14
14
  3. Follow skill **`feature-delivery`** + `architecture/feature-delivery.md` + `architecture/reference-features.md`.
15
+ 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 (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.
15
16
 
16
17
  ## Rules
17
18
 
@@ -47,6 +48,8 @@ When copying from `next` into `ios-swift` (or another stack):
47
48
 
48
49
  Emit the owned implementation receipt with outcome `completed`. Handoff by layer + gaps for verifier/reviewer. No formal code review.
49
50
 
51
+ **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 -->
52
+
50
53
  ## Design guidance
51
54
 
52
55
  - Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
@@ -55,6 +55,8 @@ Provide summary:
55
55
  - Gaps vs acceptance criteria
56
56
  - Suggest `/technical-retro` with the slug when done
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.
@@ -82,6 +82,10 @@ Keep top-level `profile` and emit this top-level object in every new `pipeline.j
82
82
  - Use concrete flags: `public-contract`, `cross-layer`, `security-sensitive`, `migration`, `destructive`, `concurrency-state`, `external-dependency`, `unclear-acceptance-criteria`, `broad-test-surface`, `behavior-regression`.
83
83
  - `routingReasons` must be non-empty and explain profile, gates, specialists, skips, or checkpoints; never write “standard by default.”
84
84
  - `estimatedWorkPackages` is an integer ≥1; `openDecisionCount` is an integer ≥0. Do not lower complexity because implementation is familiar.
85
+ - Add **api-contract-reviewer** for new/changed backend contracts — before developer.
86
+ - Add **accessibility-reviewer** / **security-reviewer** after build-verifier (parallel when both apply).
87
+ - Add **tech-writer** when docs/changelog requested.
88
+
85
89
 
86
90
  ## Model tiers (`steps[].model`)
87
91
 
@@ -64,10 +64,20 @@ const STACK = 'ios-swift';
64
64
  const PLATFORM = 'claude';
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 `.claude/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 -->