mandrel 2.31.0 → 2.33.0

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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -54,7 +54,6 @@ import {
54
54
  import { getStoryBranch, gitSpawn, gitSync } from './lib/git-utils.js';
55
55
  import { Logger } from './lib/Logger.js';
56
56
  import { TYPE_LABELS } from './lib/label-constants.js';
57
- import { setActiveStoryEnv } from './lib/observability/active-story-env.js';
58
57
  import { emitTerseResult } from './lib/observability/terse-result.js';
59
58
  import {
60
59
  executeFastForward,
@@ -492,10 +491,9 @@ export function seedStoryBranch({ cwd, storyBranch, baseBranch, progress }) {
492
491
  }
493
492
 
494
493
  /**
495
- * Provision a worktree (or check out the branch in single-tree mode), then
496
- * record the active-story environment markers. Returns the resolved
497
- * `workCwd`, `worktreeCreated`, and `installStatus`. Exported for testing
498
- * (owns the worktree/single-tree routing + setActiveStoryEnv call).
494
+ * Provision a worktree (or check out the branch in single-tree mode).
495
+ * Returns the resolved `workCwd`, `worktreeCreated`, and `installStatus`.
496
+ * Exported for testing (owns the worktree/single-tree routing).
499
497
  *
500
498
  * @param {object} opts
501
499
  * @param {object} opts.runtime
@@ -541,23 +539,6 @@ export async function provisionWorktree({
541
539
  gitSync(cwd, 'checkout', storyBranch);
542
540
  }
543
541
 
544
- try {
545
- // v2 Stories are standalone — no parent Epic. The helper omits
546
- // CC_EPIC_ID from env + file; the trace hook keys its standalone-trace
547
- // branch on CC_EPIC_ID being absent.
548
- setActiveStoryEnv({
549
- storyId,
550
- workCwd,
551
- logger: {
552
- warn: (m) => progress('ENV', `⚠️ ${m}`),
553
- },
554
- });
555
- } catch (err) {
556
- Logger.error(
557
- `[single-story-init] ⚠️ Failed to set active-Story env: ${err?.message ?? err}`,
558
- );
559
- }
560
-
561
542
  return { workCwd, worktreeCreated, installStatus };
562
543
  }
563
544
 
@@ -572,13 +553,11 @@ export async function runSingleStoryInit({
572
553
  injectedConfig,
573
554
  injectedSweep,
574
555
  // Story #3483: lets tests drive the lease preflight deterministically.
575
- // `injectedAcquireLease` swaps the guard; `leaseNow` injects the clock the
576
- // fail-closed liveness check evaluates against (audit #3513). `steal`
577
- // forcibly transfers a foreign claim — the standalone path has no Epic
578
- // heartbeat ledger, so a foreign assignee blocks unless stolen.
556
+ // `injectedAcquireLease` swaps the guard. `steal` forcibly transfers a
557
+ // foreign claim the lease fails closed (audit #3513), so a foreign
558
+ // assignee blocks unless stolen.
579
559
  injectedAcquireLease,
580
560
  steal = false,
581
- leaseNow,
582
561
  injectedVerifyRemote,
583
562
  // Story #4620: swap the git-touching provisioning steps so the
584
563
  // early-flip-then-rollback ordering is unit-testable without a real worktree.
@@ -652,13 +631,12 @@ export async function runSingleStoryInit({
652
631
  `Standalone Story: "${story.title}" → branch ${storyBranch} from ${baseBranch}.`,
653
632
  );
654
633
 
655
- // Story #3483 — lease preflight. Take an exclusive, time-bounded claim on
656
- // the Story ticket before any git mutation so two concurrent standalone
657
- // runs cannot both drive the same Story. The standalone path has no Epic
658
- // heartbeat ledger, so the guard fails closed (audit #3513): a foreign
659
- // assignee is treated as a live claim and aborts init (naming the current
660
- // owner) unless --steal forcibly transfers it. Unclaimed / self-held claims
661
- // proceed. Skipped under --dry-run (no assignee mutation).
634
+ // Story #3483 — lease preflight. Take an exclusive claim on the Story
635
+ // ticket before any git mutation so two concurrent standalone runs cannot
636
+ // both drive the same Story. The guard fails closed (audit #3513): a
637
+ // foreign assignee aborts init (naming the current owner) unless --steal
638
+ // forcibly transfers it. Unclaimed / self-held claims proceed. Skipped
639
+ // under --dry-run (no assignee mutation).
662
640
  let workCwd = cwd;
663
641
  let worktreeCreated = false;
664
642
  let installStatus = { status: 'skipped', reason: 'dry-run' };
@@ -670,7 +648,6 @@ export async function runSingleStoryInit({
670
648
  storyId,
671
649
  config,
672
650
  steal: stealRequested,
673
- now: leaseNow,
674
651
  });
675
652
  progress(
676
653
  'LEASE',
@@ -55,7 +55,8 @@
55
55
  * inFlight: number,
56
56
  * cycleError: string | null,
57
57
  * wedged: { reason, stories: [{ id, unmetBlockers }] } | null,
58
- * inFlightReservation: { available, withheld: [{ id, blockedBy, reason }], note }
58
+ * inFlightReservation: { available, withheld: [{ id, blockedBy, reason, source, paths }], note },
59
+ * footprintGuard: { mode, withheld: [{ id, blockedBy, scope, source, paths }], advisory, note }
59
60
  * }
60
61
  *
61
62
  * `inFlightReservation` reports the cross-beat half of the co-dispatch guard
@@ -70,6 +71,17 @@
70
71
  * `available: false` rather than an empty — and therefore indistinguishable —
71
72
  * result.
72
73
  *
74
+ * `footprintGuard` reports the **beat-local** half, which until Story #5044 was
75
+ * reported nowhere: a same-beat overlap skip was a bare `continue` inside
76
+ * `planReadySet`, so the Story vanished from `ready[]` with no field anywhere
77
+ * naming the collision. Every entry in either report now also carries the
78
+ * colliding `paths` and a `source` tag — `declared-overlap` when both Stories'
79
+ * `changes[]` named the path (intended serialization: two Stories really do
80
+ * rewrite the same generated baseline) versus `scraped-overlap` when only the
81
+ * text evidence produced it. `mode` names the `footprintGuard` config value;
82
+ * under `advisory` the collisions are detected and listed in `advisory[]` but
83
+ * dispatch follows the declared `depends_on` edges alone.
84
+ *
73
85
  * Probe mode adds fields the caller can no longer compute for itself:
74
86
  * `done: number[]` (the resolved done set, in-set ∪ satisfied foreign
75
87
  * blockers), `epilogueDue: boolean` (true exactly when every listed Story
@@ -111,19 +123,24 @@ import { readFileSync } from 'node:fs';
111
123
  import { parseArgs } from 'node:util';
112
124
 
113
125
  import { runAsCli } from './lib/cli-utils.js';
114
- import { getRunners, resolveConfig } from './lib/config-resolver.js';
126
+ import { getPaths, getRunners, resolveConfig } from './lib/config-resolver.js';
115
127
  import { detectCycle } from './lib/Graph.js';
116
128
  import { Logger } from './lib/Logger.js';
117
129
  import { AGENT_LABELS } from './lib/label-constants.js';
118
130
  import { parseIds } from './lib/orchestration/resolve-stories.js';
119
131
  import { buildStoryAdjacency } from './lib/story-adjacency.js';
120
132
  import { expandIdList } from './lib/util/parse-id-list.js';
133
+ import { OVERLAP_SOURCES } from './lib/wave-runner/footprint.js';
121
134
  import {
122
135
  createProbeContext,
123
136
  probeLiveState,
124
137
  validateProbeFlags,
125
138
  } from './lib/wave-runner/live-probe.js';
126
- import { planReadySet } from './lib/wave-runner/ready-set.js';
139
+ import {
140
+ GUARD_MODES,
141
+ planReadySet,
142
+ WITHHOLD_SCOPES,
143
+ } from './lib/wave-runner/ready-set.js';
127
144
 
128
145
  /**
129
146
  * Exit code for a wedged run — deliberately distinct from the cycle exit (2)
@@ -214,7 +231,29 @@ Output envelope:
214
231
  "wedged": null,
215
232
  "inFlightReservation": {
216
233
  "available": true,
217
- "withheld": [{ "id": 4951, "blockedBy": 4949 }],
234
+ "withheld": [
235
+ {
236
+ "id": 4951,
237
+ "blockedBy": 4949,
238
+ "reason": "in-flight-earlier-beat",
239
+ "source": "declared-overlap",
240
+ "paths": ["lib/shared.js"]
241
+ }
242
+ ],
243
+ "note": "..."
244
+ },
245
+ "footprintGuard": {
246
+ "mode": "enforce",
247
+ "withheld": [
248
+ {
249
+ "id": 4952,
250
+ "blockedBy": 4951,
251
+ "scope": "beat",
252
+ "source": "scraped-overlap",
253
+ "paths": ["lib/other.js"]
254
+ }
255
+ ],
256
+ "advisory": [],
218
257
  "note": "..."
219
258
  }
220
259
  }
@@ -225,6 +264,15 @@ blocking id — so an unfilled slot is explained rather than mysterious. It need
225
264
  the in-flight Stories' footprints, which only --probe-live has: under --dag the
226
265
  report is { available: false } and selection de-conflicts within the beat only.
227
266
 
267
+ footprintGuard names each Story withheld from THIS beat by a peer already
268
+ admitted on it — the half that used to be an unreported skip — and every
269
+ entry in either report carries the colliding paths plus a source tag
270
+ (declared-overlap when both changes[] declarations named the path, else
271
+ scraped-overlap from the text evidence). Its "mode" echoes
272
+ delivery.deliverRunner.footprintGuard: under "advisory" the collisions are
273
+ detected and listed in "advisory" but never withhold, and dispatch follows the
274
+ declared depends_on edges alone.
275
+
228
276
  Exit codes:
229
277
  0 - Success, ready set emitted
230
278
  1 - Invalid input (missing/malformed DAG, invalid --concurrency/--in-flight/--done)
@@ -258,6 +306,7 @@ function inputErrorResult(message, concurrencyCap = null, inFlightValue = 0) {
258
306
  cycleError: null,
259
307
  wedged: null,
260
308
  inFlightReservation: null,
309
+ footprintGuard: null,
261
310
  inputError: message,
262
311
  },
263
312
  exitCode: 1,
@@ -299,10 +348,18 @@ const RESERVATION_REASONS = Object.freeze({
299
348
  * and no later beat of this run will clear it. `foreignHeldIds` splits the two
300
349
  * so each carries its own reason (Story #4960).
301
350
  *
351
+ * Each entry also carries the **colliding paths** and an
352
+ * `OVERLAP_SOURCES` tag (Story #5044). A withhold that names no path is one an
353
+ * operator cannot act on, and `declared-overlap` vs `scraped-overlap` is the
354
+ * difference between "these two Stories both declared this generated baseline,
355
+ * serializing them is the point" and "one Story's body happened to mention a
356
+ * path the other declared" — the same unfilled slot for two very different
357
+ * reasons.
358
+ *
302
359
  * @param {object[]|null|undefined} inFlightRecords
303
- * @param {Array<{id: number, blockedBy: number}>} withheld
360
+ * @param {Array<{id: number, blockedBy: number, source?: string, paths?: string[]}>} withheld
304
361
  * @param {Iterable<number>} [foreignHeldIds] Ids held by a foreign lease.
305
- * @returns {{ available: boolean, withheld: Array<{id: number, blockedBy: number, reason: string}>, note: string|null }}
362
+ * @returns {{ available: boolean, withheld: Array<{id: number, blockedBy: number, reason: string, source: string, paths: string[]}>, note: string|null }}
306
363
  */
307
364
  export function buildReservationReport(
308
365
  inFlightRecords,
@@ -326,10 +383,13 @@ export function buildReservationReport(
326
383
  }
327
384
  const foreign = new Set(foreignHeldIds);
328
385
  const classified = withheld.map((w) => ({
329
- ...w,
386
+ id: w.id,
387
+ blockedBy: w.blockedBy,
330
388
  reason: foreign.has(w.blockedBy)
331
389
  ? RESERVATION_REASONS.FOREIGN_LEASE
332
390
  : RESERVATION_REASONS.EARLIER_BEAT,
391
+ source: w.source ?? OVERLAP_SOURCES.DECLARED,
392
+ paths: w.paths ?? [],
333
393
  }));
334
394
  return {
335
395
  available: true,
@@ -338,6 +398,87 @@ export function buildReservationReport(
338
398
  };
339
399
  }
340
400
 
401
+ /**
402
+ * Report the **beat-local** half of the footprint guard, plus the guard mode
403
+ * itself (Story #5044).
404
+ *
405
+ * Until now a same-beat overlap skip was an anonymous `continue` inside
406
+ * `planReadySet`: the Story simply did not appear in `ready[]` and no field
407
+ * anywhere said why. That is the same unexplained-unfilled-slot failure the
408
+ * cross-beat `inFlightReservation` report was built to remove, left standing on
409
+ * the other half of the guard — and it is how a plan whose siblings collided
410
+ * only on machine-generated footer text ran fully serial without leaving a
411
+ * trace to notice.
412
+ *
413
+ * `withheld` and `advisory` are disjoint by construction: under `enforce` every
414
+ * detection withheld, under `advisory` none did. Reporting them as separate
415
+ * lists rather than one flagged list means a consumer counting withheld
416
+ * dispatches never has to inspect a boolean to get the count right.
417
+ *
418
+ * @param {Array<object>} footprintWithholds The kernel's complete ledger.
419
+ * @param {'enforce'|'advisory'} mode
420
+ * @returns {{ mode: string, withheld: object[], advisory: object[], note: string|null }}
421
+ */
422
+ export function buildFootprintGuardReport(footprintWithholds, mode) {
423
+ const ledger = Array.isArray(footprintWithholds) ? footprintWithholds : [];
424
+ const project = ({ id, blockedBy, scope, source, paths }) => ({
425
+ id,
426
+ blockedBy,
427
+ scope,
428
+ source,
429
+ paths,
430
+ });
431
+ const beat = ledger
432
+ .filter((w) => w.scope === WITHHOLD_SCOPES.BEAT && w.enforced)
433
+ .map(project);
434
+ const advisory = ledger.filter((w) => !w.enforced).map(project);
435
+ return {
436
+ mode,
437
+ withheld: beat,
438
+ advisory,
439
+ note: footprintGuardNote(beat, advisory, mode),
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Render the operator-facing note for the beat-local guard, naming the
445
+ * colliding path(s) for every entry. `null` when the guard neither withheld nor
446
+ * waved anything through.
447
+ *
448
+ * @param {object[]} beat
449
+ * @param {object[]} advisory
450
+ * @param {string} mode
451
+ * @returns {string|null}
452
+ */
453
+ function footprintGuardNote(beat, advisory, mode) {
454
+ const detail = (entries) =>
455
+ entries
456
+ .map(
457
+ (w) =>
458
+ `#${w.id} ← #${w.blockedBy} on ${w.paths.join(', ')} (${w.source})`,
459
+ )
460
+ .join('; ');
461
+ if (beat.length > 0) {
462
+ return (
463
+ `${beat.length} Story(ies) withheld from THIS beat because their file ` +
464
+ `footprint overlaps a peer already admitted on it — ${detail(beat)}. ` +
465
+ `Each is still eligible and re-admits on a later beat once its peer ` +
466
+ `lands. A ${OVERLAP_SOURCES.SCRAPED} source means the collision came ` +
467
+ `from path evidence in the Story text rather than from either ` +
468
+ `changes[] declaration.`
469
+ );
470
+ }
471
+ if (advisory.length > 0) {
472
+ return (
473
+ `footprintGuard is '${mode}': ${advisory.length} footprint collision(s) ` +
474
+ `were detected and NOT enforced — ${detail(advisory)}. Dispatch followed ` +
475
+ `the declared depends_on edges alone. Set ` +
476
+ `delivery.deliverRunner.footprintGuard: 'enforce' to serialize these.`
477
+ );
478
+ }
479
+ return null;
480
+ }
481
+
341
482
  /**
342
483
  * Render the operator-facing reservation note, one sentence per reason class
343
484
  * present. Neither class is a failure or a wedge, but they clear by different
@@ -600,6 +741,29 @@ export function resolveCapPrecedence({ cwd, config, override } = {}) {
600
741
  };
601
742
  }
602
743
 
744
+ /**
745
+ * Resolve the two footprint-guard inputs from the same config seam the cap
746
+ * comes from (Story #5044).
747
+ *
748
+ * `tempRoot` is threaded rather than hardcoded because the evidence scrape must
749
+ * exclude the project's *configured* scratch root: a consumer that sets
750
+ * `project.paths.tempRoot: '.scratch'` would otherwise have every sibling
751
+ * citing a report under it collide, which is the exact defect this Story
752
+ * removes for the default `temp/`.
753
+ *
754
+ * @param {object} [opts]
755
+ * @param {string} [opts.cwd] Repo root for config resolution.
756
+ * @param {object} [opts.config] Pre-resolved config (test injection).
757
+ * @returns {{ footprintGuard: 'enforce'|'advisory', tempRoot: string }}
758
+ */
759
+ export function resolveFootprintGuardSettings({ cwd, config } = {}) {
760
+ const resolved = config ?? resolveConfig({ cwd });
761
+ return {
762
+ footprintGuard: getRunners(resolved).deliverRunner.footprintGuard,
763
+ tempRoot: getPaths(resolved).tempRoot,
764
+ };
765
+ }
766
+
603
767
  /**
604
768
  * Build the per-beat ready-set envelope from a validated DAG.
605
769
  *
@@ -648,6 +812,8 @@ export function buildReadySetEnvelope(
648
812
  inFlight = 0,
649
813
  inFlightRecords = null,
650
814
  foreignHeldIds = [],
815
+ footprintGuard = GUARD_MODES.ENFORCE,
816
+ tempRoot,
651
817
  },
652
818
  ) {
653
819
  const totalStories = nodes.length;
@@ -668,6 +834,11 @@ export function buildReadySetEnvelope(
668
834
  // which Stories a reservation withheld (Story #4950). Never omitted on a
669
835
  // resolved beat: an absent report reads exactly like an empty one.
670
836
  inFlightReservation: buildReservationReport(inFlightRecords, []),
837
+ // The beat-local half of the same guard, plus the mode it ran in (Story
838
+ // #5044). Also never omitted: a same-beat skip used to be reported nowhere
839
+ // at all, which is precisely how an evidence-widening artifact could
840
+ // serialize a whole run unnoticed.
841
+ footprintGuard: buildFootprintGuardReport([], footprintGuard),
671
842
  };
672
843
 
673
844
  if (totalStories === 0) {
@@ -714,7 +885,7 @@ export function buildReadySetEnvelope(
714
885
  return rec;
715
886
  });
716
887
 
717
- const { selected, withheldByInFlight } = planReadySet({
888
+ const { selected, footprintWithholds, guardMode } = planReadySet({
718
889
  stories: records,
719
890
  doneIds,
720
891
  inFlight,
@@ -723,13 +894,21 @@ export function buildReadySetEnvelope(
723
894
  // contract (an array) while `base.inFlightReservation` reports that the
724
895
  // reservation itself was unavailable rather than merely empty.
725
896
  inFlightRecords: inFlightRecords ?? [],
897
+ footprintGuard,
898
+ tempRoot,
726
899
  });
727
900
  const ready = selected.map((rec) => rec.id);
901
+ // The cross-beat report reads the enforced in-flight slice of the kernel's
902
+ // ledger rather than the legacy `withheldByInFlight` list, so it carries the
903
+ // colliding paths and the overlap source through to the operator.
728
904
  const reservation = buildReservationReport(
729
905
  inFlightRecords,
730
- withheldByInFlight,
906
+ footprintWithholds.filter(
907
+ (w) => w.scope === WITHHOLD_SCOPES.IN_FLIGHT && w.enforced,
908
+ ),
731
909
  foreignHeldIds,
732
910
  );
911
+ const guardReport = buildFootprintGuardReport(footprintWithholds, guardMode);
733
912
 
734
913
  // Wedge detection (Story #4540). `ready: []` is normal while work is in
735
914
  // flight — the loop is simply waiting. But ready-empty AND nothing in
@@ -750,6 +929,7 @@ export function buildReadySetEnvelope(
750
929
  ready,
751
930
  wedged: wedge,
752
931
  inFlightReservation: reservation,
932
+ footprintGuard: guardReport,
753
933
  },
754
934
  exitCode: WEDGED_EXIT_CODE,
755
935
  };
@@ -761,6 +941,7 @@ export function buildReadySetEnvelope(
761
941
  ready,
762
942
  wedged: null,
763
943
  inFlightReservation: reservation,
944
+ footprintGuard: guardReport,
764
945
  },
765
946
  exitCode: 0,
766
947
  };
@@ -896,6 +1077,7 @@ export function runStoriesWaveTick({
896
1077
  capPrecedence,
897
1078
  doneIds,
898
1079
  inFlight: inFlightValue,
1080
+ ...resolveFootprintGuardSettings({ cwd, config }),
899
1081
  });
900
1082
  }
901
1083
 
@@ -1001,6 +1183,7 @@ export async function runProbedStoriesWaveTick({
1001
1183
  // ...and the only mode that can tell a foreign lease-holder apart from
1002
1184
  // this run's own earlier-beat dispatch (Story #4960).
1003
1185
  foreignHeldIds: foreignHeld.map((h) => h.id),
1186
+ ...resolveFootprintGuardSettings({ cwd, config }),
1004
1187
  });
1005
1188
 
1006
1189
  const done = [...doneIds].sort((a, b) => a - b);