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
@@ -67,7 +67,6 @@ const FRAMEWORK_RUNTIME_DEPS = [
67
67
  'js-yaml',
68
68
  'minimatch',
69
69
  'picomatch',
70
- 'string-argv',
71
70
  'typhonjs-escomplex',
72
71
  ];
73
72
 
@@ -112,17 +112,6 @@ export class ITicketingProvider {
112
112
  throw new Error('Not implemented: getTicketDependencies');
113
113
  }
114
114
 
115
- /**
116
- * Fetch recent comments across the repository.
117
- * Useful for auditing and visualization of agent telemetry.
118
- *
119
- * @param {number} [limit=100]
120
- * @returns {Promise<object[]>}
121
- */
122
- async getRecentComments(_limit = 100) {
123
- throw new Error('Not implemented: getRecentComments');
124
- }
125
-
126
115
  /**
127
116
  * Fetch all comments for a specific ticket.
128
117
  *
@@ -137,42 +126,6 @@ export class ITicketingProvider {
137
126
  // Write Operations
138
127
  // ---------------------------------------------------------------------------
139
128
 
140
- /**
141
- * Create a child ticket within an Epic's structural hierarchy.
142
- *
143
- * @param {number} parentId - GitHub Issue number of the immediate structural parent (e.g. Epic or Story).
144
- * @param {{
145
- * epicId: number,
146
- * title: string,
147
- * body: string,
148
- * labels: string[],
149
- * dependencies: number[]
150
- * }} ticketData - The ticket content and metadata.
151
- * @returns {Promise<{ id: number, url: string }>}
152
- */
153
-
154
- /**
155
- * Link an existing issue as a sub-issue of a parent.
156
- *
157
- * @param {number} parentId - GitHub Issue number of the parent.
158
- * @param {number} childId - GitHub internal database ID of the sub-issue.
159
- * @returns {Promise<void>}
160
- */
161
- async addSubIssue(_parentId, _childId) {
162
- throw new Error('Not implemented: addSubIssue');
163
- }
164
-
165
- /**
166
- * Remove a sub-issue link from a parent.
167
- *
168
- * @param {number} parentId - GitHub Issue number of the parent.
169
- * @param {number} childId - GitHub internal database ID of the sub-issue.
170
- * @returns {Promise<void>}
171
- */
172
- async removeSubIssue(_parentId, _childId) {
173
- throw new Error('Not implemented: removeSubIssue');
174
- }
175
-
176
129
  /**
177
130
  * Mutate labels, body (tasklist checkboxes), and assignees on a ticket.
178
131
  *
@@ -213,17 +166,6 @@ export class ITicketingProvider {
213
166
  throw new Error('Not implemented: deleteComment');
214
167
  }
215
168
 
216
- /**
217
- * Open a Pull Request linking the specified ticket.
218
- *
219
- * @param {string} branchName - The source branch for the PR.
220
- * @param {number} ticketId - GitHub Issue number to link.
221
- * @returns {Promise<{ number: number, url: string, htmlUrl: string }>}
222
- */
223
- async createPullRequest(_branchName, _ticketId) {
224
- throw new Error('Not implemented: createPullRequest');
225
- }
226
-
227
169
  // ---------------------------------------------------------------------------
228
170
  // Setup Operations (used by bootstrap)
229
171
  // ---------------------------------------------------------------------------
@@ -16,25 +16,25 @@
16
16
  * @module lib/audit-baselines/staleness
17
17
  */
18
18
 
19
- import { execFileSync } from 'node:child_process';
19
+ import { execFileCapture } from '../child-exec.js';
20
20
  import { KIND_SPECS } from './kinds.js';
21
21
  import { ageInDays } from './read.js';
22
22
 
23
23
  /**
24
24
  * Run a git command under `cwd`, returning trimmed stdout, or `null` on any
25
- * failure or empty result.
25
+ * failure or empty result. Buffer and shell policy come from the shared
26
+ * child-process surface ([`child-exec.js`](../child-exec.js)).
26
27
  *
27
28
  * @param {string[]} args
28
29
  * @param {{ cwd: string, run?: Function }} io
29
30
  * @returns {string | null}
30
31
  */
31
- function git(args, { cwd, run = execFileSync }) {
32
+ function git(args, { cwd, run }) {
32
33
  try {
33
34
  const stdout = String(
34
- run('git', args, {
35
+ execFileCapture('git', args, {
36
+ run,
35
37
  cwd,
36
- encoding: 'utf8',
37
- maxBuffer: 16 * 1024 * 1024,
38
38
  stdio: ['ignore', 'pipe', 'ignore'],
39
39
  }),
40
40
  ).trim();
@@ -16,7 +16,7 @@
16
16
  * @module lib/audit-baselines/trend
17
17
  */
18
18
 
19
- import { execFileSync } from 'node:child_process';
19
+ import { execFileCapture } from '../child-exec.js';
20
20
  import { trendRollupOf } from './kinds.js';
21
21
 
22
22
  /**
@@ -25,13 +25,13 @@ import { trendRollupOf } from './kinds.js';
25
25
  * @param {{ cwd: string, relPath: string, limit: number, run?: Function }} args
26
26
  * @returns {Array<{ sha: string, committedAt: string }>}
27
27
  */
28
- function listCommits({ cwd, relPath, limit, run = execFileSync }) {
28
+ function listCommits({ cwd, relPath, limit, run }) {
29
29
  let stdout;
30
30
  try {
31
- stdout = run(
31
+ stdout = execFileCapture(
32
32
  'git',
33
33
  ['log', `-n${limit}`, '--format=%H %cI', '--', relPath],
34
- { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
34
+ { run, cwd, stdio: ['ignore', 'pipe', 'ignore'] },
35
35
  );
36
36
  } catch {
37
37
  return [];
@@ -52,13 +52,12 @@ function listCommits({ cwd, relPath, limit, run = execFileSync }) {
52
52
  * @param {{ cwd: string, kind: string, sha: string, relPath: string, run?: Function }} args
53
53
  * @returns {object | null}
54
54
  */
55
- function rollupAt({ cwd, kind, sha, relPath, run = execFileSync }) {
55
+ function rollupAt({ cwd, kind, sha, relPath, run }) {
56
56
  let stdout;
57
57
  try {
58
- stdout = run('git', ['show', `${sha}:${relPath}`], {
58
+ stdout = execFileCapture('git', ['show', `${sha}:${relPath}`], {
59
+ run,
59
60
  cwd,
60
- encoding: 'utf8',
61
- maxBuffer: 64 * 1024 * 1024,
62
61
  stdio: ['ignore', 'pipe', 'ignore'],
63
62
  });
64
63
  } catch {
@@ -16,9 +16,9 @@
16
16
  * @module lib/audit-baselines/weights
17
17
  */
18
18
 
19
- import { execFileSync } from 'node:child_process';
20
19
  import fs from 'node:fs';
21
20
  import path from 'node:path';
21
+ import { execFileCapture } from '../child-exec.js';
22
22
  import { computeInDegree, resolveRepoGraph } from '../import-graph.js';
23
23
 
24
24
  /** Neutral multiplier every degraded weight collapses to. */
@@ -47,10 +47,10 @@ function saturate(count, half) {
47
47
  * `degraded` is true when git could not answer at all — not a git work
48
48
  * tree, no commits yet, or the binary is unavailable.
49
49
  */
50
- export function readChurn({ cwd, windowDays = 180, run = execFileSync }) {
50
+ export function readChurn({ cwd, windowDays = 180, run }) {
51
51
  let stdout;
52
52
  try {
53
- stdout = run(
53
+ stdout = execFileCapture(
54
54
  'git',
55
55
  [
56
56
  'log',
@@ -60,9 +60,8 @@ export function readChurn({ cwd, windowDays = 180, run = execFileSync }) {
60
60
  '--no-renames',
61
61
  ],
62
62
  {
63
+ run,
63
64
  cwd,
64
- encoding: 'utf8',
65
- maxBuffer: 64 * 1024 * 1024,
66
65
  // git narrates "not a git repository" on stderr; the degradation is
67
66
  // reported in the envelope, not shouted at the operator.
68
67
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -41,7 +41,7 @@ import path from 'node:path';
41
41
  import { AUDIT_LENSES } from '../audit-to-stories/audit-lenses.js';
42
42
  import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
43
43
  import { Logger } from '../Logger.js';
44
- import { estimateTokens } from '../orchestration/context-envelope.js';
44
+ import { estimateTokens } from '../orchestration/spec-spill.js';
45
45
  import {
46
46
  changeSetLacksSiblingTest,
47
47
  matchesAnyFilePattern,
@@ -22,6 +22,7 @@
22
22
  * structured contract.
23
23
  */
24
24
 
25
+ import path from 'node:path';
25
26
  import { AGENT_LABELS, RISK_LABELS, TYPE_LABELS } from '../label-constants.js';
26
27
  import { serialize } from '../story-body/story-body.js';
27
28
  import { definesAuditLabel } from './audit-label-taxonomy.js';
@@ -116,10 +117,7 @@ function acceptanceCriteriaFromGroup(group) {
116
117
 
117
118
  /**
118
119
  * Resolve the `edges[]` sequencing anchored on this group. Each edge whose
119
- * `fromGroupKey` matches this group's key contributes its `toGroupKey`. Group
120
- * keys are the only stable identifier available at emit time — issues are not
121
- * numbered yet — so the relationship is preserved as machine-readable keys the
122
- * operator can resolve.
120
+ * `fromGroupKey` matches this group's key contributes its `toGroupKey`.
123
121
  *
124
122
  * @param {object} group
125
123
  * @param {Array<{ fromGroupKey: string, toGroupKey: string }>} edges
@@ -135,22 +133,38 @@ function sequencingDepsForGroup(group, edges) {
135
133
  }
136
134
 
137
135
  /**
138
- * Render the carried-through `edges[]` sequencing as a dedicated extended
139
- * markdown block. The canonical `depends_on[]` footer only round-trips `#N`
140
- * issue refs (`blocked by #123`), which do not exist before the issues are
141
- * opened; rendering the group-key sequencing as its own informational section
142
- * keeps the signal in the body (not discarded — Story #4270) and survives
143
- * `parse()` / `serialize()` round-tripping (it is preamble/extended content,
144
- * not a structured section). Returns the empty string when there is no
145
- * sequencing to surface.
136
+ * Resolve this group's sequencing to canonical `#N` issue refs, or `[]` when
137
+ * the caller has no issue numbers yet (Story #5044).
146
138
  *
147
- * @param {string[]} deps
148
- * @returns {string}
139
+ * Group keys are the only identifier that exists at *emit* time — the issues
140
+ * are not numbered — which is why this used to render as a prose
141
+ * `## Sequencing` block that nothing could act on. Standalone audit Stories
142
+ * therefore hardcoded `depends_on: []`, and their only actual serializer was
143
+ * an accident: their shared provenance footers collided under the delivery
144
+ * footprint guard. Narrowing that scrape removes the accident, so the ordering
145
+ * has to become real in the same change.
146
+ *
147
+ * The resolution is the two-pass shape `plan-persist` already uses: create every
148
+ * issue first, then re-render each body with the now-known numbers and mirror
149
+ * the same edges as native `blocked_by` relations. An edge whose target was not
150
+ * created (deduped against an existing Issue, suppressed by the ledger) simply
151
+ * drops — a `blocked by #undefined` would be worse than an absent edge.
152
+ *
153
+ * A **plain object**, deliberately, not a `Map`: the same map is handed to
154
+ * `applyBlockedByDependencies`, which indexes it with property access, so a
155
+ * `Map` there would silently resolve every lookup to `undefined`, skip every
156
+ * edge, and report success having written nothing. One shape, both halves.
157
+ *
158
+ * @param {string[]} deps Group keys this group depends on.
159
+ * @param {Record<string, number>|null} issueByGroupKey
160
+ * @returns {string[]} `#N` refs, in `deps` order.
149
161
  */
150
- function sequencingSection(deps) {
151
- if (deps.length === 0) return '';
152
- const lines = deps.map((k) => `- depends on group \`${k}\``);
153
- return ['## Sequencing', '', lines.join('\n'), ''].join('\n');
162
+ function dependencyRefs(deps, issueByGroupKey) {
163
+ if (!issueByGroupKey) return [];
164
+ return deps
165
+ .map((key) => issueByGroupKey[key])
166
+ .filter((n) => Number.isInteger(n) && n > 0)
167
+ .map((n) => `#${n}`);
154
168
  }
155
169
 
156
170
  function agentPromptsSection(group) {
@@ -162,6 +176,21 @@ function agentPromptsSection(group) {
162
176
  return blocks.join('\n\n') || '_(no copy-pasteable prompts captured)_';
163
177
  }
164
178
 
179
+ /**
180
+ * Link each source audit report **once**.
181
+ *
182
+ * This used to render `- [\`path\`](path)` — the same
183
+ * `temp/audits/audit-<lens>-results.md` in the link text and again in the URL,
184
+ * byte-identical across every Story of a same-lens sweep. That doubled a token
185
+ * the delivery footprint guard scraped as edit intent, so a lens's whole cohort
186
+ * serialized on a report none of them would ever write to (Story #5044). The
187
+ * guard now ignores markdown-link URLs and temp-root paths, but rendering the
188
+ * path twice was never useful to a reader either: the file name is the label,
189
+ * the path is the target.
190
+ *
191
+ * @param {object} group
192
+ * @returns {string}
193
+ */
165
194
  function contextLinksFromGroup(group) {
166
195
  const reports = uniq(
167
196
  (group.findings ?? [])
@@ -169,7 +198,11 @@ function contextLinksFromGroup(group) {
169
198
  .filter((s) => typeof s === 'string'),
170
199
  );
171
200
  if (reports.length === 0) return '_(no source audit reports captured)_';
172
- return reports.map((r) => `- [\`${r}\`](${r})`).join('\n');
201
+ // `path.basename` rather than `split('/')`: on win32 it splits on both
202
+ // separators, so an absolute Windows path yields the file name instead of
203
+ // the whole path — which would render the path twice in one link and
204
+ // re-create the very duplication this function exists to remove.
205
+ return reports.map((r) => `- [${path.basename(r)}](${r})`).join('\n');
173
206
  }
174
207
 
175
208
  function labelsForGroup(group) {
@@ -226,20 +259,27 @@ function assertLabelsInTaxonomy(labels) {
226
259
  * — the dependency `edges[]` emitted by `groupFindings`. Edges anchored on
227
260
  * this group are carried through to `depends_on[]`; omit when no sequencing
228
261
  * is known.
229
- * @returns {{ title: string, body: string, labels: string[] }}
262
+ * @param {Record<string, number>|null} [params.issueByGroupKey]
263
+ * — group key → opened issue number. Supplied on the **second** pass, once
264
+ * the issues exist, so this group's edges render as canonical
265
+ * `blocked by #N` footers (Story #5044). Omit on the first pass.
266
+ * @returns {{ title: string, body: string, labels: string[], groupKey: string, dependsOn: string[] }}
267
+ * `groupKey` and `dependsOn` are the caller's handle on the second pass:
268
+ * they name this Story and the groups it must follow, so the caller can map
269
+ * both onto issue numbers without re-deriving the grouping.
230
270
  */
231
- export function buildStoryBody({ group, edges = [] }) {
271
+ export function buildStoryBody({ group, edges = [], issueByGroupKey = null }) {
232
272
  if (!group || !Array.isArray(group.findings)) {
233
273
  throw new Error('buildStoryBody: group with findings[] is required');
234
274
  }
235
275
  const title = group.title;
276
+ const dependsOn = sequencingDepsForGroup(group, edges);
236
277
 
237
278
  // Build the canonical StoryBody object from the audit group data. The
238
279
  // acceptance + verify arrays are populated so the body clears the
239
- // inline-contract bar; changes[] carries the file footprint. The edges[]
240
- // sequencing is carried through as an extended `## Sequencing` block (see
241
- // sequencingSection) group keys are not `#N` refs, so they cannot ride the
242
- // canonical depends_on footer.
280
+ // inline-contract bar; changes[] carries the file footprint. `depends_on`
281
+ // is empty on the first pass (the blockers have no issue numbers yet) and
282
+ // carries real `#N` refs on the second see dependencyRefs.
243
283
  const storyBody = {
244
284
  goal: goalFromGroup(group),
245
285
  changes: changesFromGroup(group),
@@ -248,21 +288,21 @@ export function buildStoryBody({ group, edges = [] }) {
248
288
  references: [],
249
289
  wide: null,
250
290
  reason_to_exist: null,
251
- depends_on: [],
252
- estimated_test_files: null,
291
+ depends_on: dependencyRefs(dependsOn, issueByGroupKey),
253
292
  };
254
293
 
255
- // Serialize via the canonical serializer (no footer depends_on is empty).
256
- const canonicalSections = serialize(storyBody);
257
- const sequencing = sequencingSection(sequencingDepsForGroup(group, edges));
294
+ // The `---` / `blocked by #N` footer is the canonical serializer's own, so
295
+ // the body round-trips through `parse()` and `/deliver`'s resolver reads the
296
+ // ordering from the same place it reads every other Story's.
297
+ const canonicalSections = serialize(storyBody, {
298
+ includeFooter: storyBody.depends_on.length > 0,
299
+ });
258
300
 
259
- // Append audit-specific extended sections (sequencing, agent prompts,
260
- // context links, fingerprint footer) that are not part of the canonical
261
- // shape.
301
+ // Append audit-specific extended sections (agent prompts, context links,
302
+ // provenance footers) that are not part of the canonical shape.
262
303
  const body = [
263
304
  canonicalSections,
264
305
  '',
265
- ...(sequencing ? [sequencing] : []),
266
306
  '## Agent Prompts',
267
307
  '',
268
308
  agentPromptsSection(group),
@@ -277,5 +317,11 @@ export function buildStoryBody({ group, edges = [] }) {
277
317
  renderSemanticKeyFooter(group.findings),
278
318
  ].join('\n');
279
319
 
280
- return { title, body, labels: labelsForGroup(group) };
320
+ return {
321
+ title,
322
+ body,
323
+ labels: labelsForGroup(group),
324
+ groupKey: group.groupKey,
325
+ dependsOn,
326
+ };
281
327
  }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * lib/audit-to-stories/wire-dependencies.js — turn a standalone audit cohort's
3
+ * detected group edges into declared ordering, once the issues exist.
4
+ *
5
+ * `groupFindings` detects `edges[]` between finding groups, but at emit time
6
+ * the groups have no issue numbers, so the ordering could only be rendered as
7
+ * prose. Every standalone audit Story therefore shipped with `depends_on: []`,
8
+ * and what actually kept a cohort from being co-dispatched onto colliding
9
+ * branches was an **accident**: siblings shared the sweep-wide audit provenance
10
+ * footers `plan-persist` stamps, and the delivery footprint guard scraped
11
+ * path-shaped tokens out of them. Story #5044 narrows that scrape, which is why
12
+ * this module lands with it — removing the accidental serializer without giving
13
+ * the cohort a real one would leave it less ordered than before.
14
+ *
15
+ * The shape is `plan-persist`'s two-pass crossing (`plan-persist/story-ops.js`),
16
+ * because it is the same problem: **create every issue first, then mirror the
17
+ * edges**. Both halves are written:
18
+ *
19
+ * 1. The **body footer** (`---` / `blocked by #N`) — canonical, parsed by
20
+ * `/deliver`'s resolver, and the fallback when the dependencies API is
21
+ * unavailable.
22
+ * 2. The **native `blocked_by` relation** — visible in the GitHub UI,
23
+ * readable without parsing markdown, and settable by an operator later.
24
+ *
25
+ * Native mirroring is **non-fatal by design**, matching `plan-persist`: the
26
+ * footer has already been written by the time it runs, so a dependencies API
27
+ * that says no costs visibility, not ordering.
28
+ *
29
+ * @module lib/audit-to-stories/wire-dependencies
30
+ */
31
+
32
+ import { applyBlockedByDependencies } from '../../providers/github/blocked-by-add.js';
33
+ import { Logger } from '../Logger.js';
34
+ import { buildStoryBody } from './build-story-body.js';
35
+
36
+ /**
37
+ * Re-render each created Story's body with its blockers resolved to `#N`, and
38
+ * mirror the same edges as native `blocked_by` relations.
39
+ *
40
+ * Groups whose issue was not created — deduped against an existing Issue,
41
+ * suppressed by the ledger, or simply not in `issueByGroupKey` — are skipped
42
+ * rather than guessed at, and an edge pointing at one drops with them
43
+ * (`dependencyRefs` filters it). A `blocked by #undefined` would gate a Story
44
+ * on nothing forever, which is strictly worse than the un-ordered cohort this
45
+ * replaces.
46
+ *
47
+ * @param {object} args
48
+ * @param {Array<object>} args.groups The `create`-eligible groups, in
49
+ * the order their issues were opened.
50
+ * @param {Array<{ fromGroupKey: string, toGroupKey: string }>} [args.edges]
51
+ * @param {Record<string, number>} args.issueByGroupKey Group key → issue number.
52
+ * @param {(issueNumber: number, body: string) => Promise<unknown>} args.updateBody
53
+ * Persist a re-rendered body. Injected so the caller owns the provider call.
54
+ * @param {object|null} [args.provider] Provider for native edge mirroring.
55
+ * Omit (or pass one without the dependency ports) to write footers only.
56
+ * @returns {Promise<{
57
+ * storiesWired: number,
58
+ * bodiesUpdated: number,
59
+ * edgesDeclared: number,
60
+ * native: { edgesAdded: number, edgesSkipped: number, edgesFailed: number }|null
61
+ * }>}
62
+ */
63
+ export async function wireAuditStoryEdges({
64
+ groups,
65
+ edges = [],
66
+ issueByGroupKey,
67
+ updateBody,
68
+ provider = null,
69
+ }) {
70
+ const wired = collectWiredStories({ groups, edges, issueByGroupKey });
71
+ if (wired.length === 0) {
72
+ return {
73
+ storiesWired: 0,
74
+ bodiesUpdated: 0,
75
+ edgesDeclared: 0,
76
+ native: null,
77
+ };
78
+ }
79
+
80
+ let bodiesUpdated = 0;
81
+ for (const story of wired) {
82
+ await updateBody(story.issueNumber, story.body);
83
+ bodiesUpdated++;
84
+ }
85
+
86
+ return {
87
+ storiesWired: wired.length,
88
+ bodiesUpdated,
89
+ edgesDeclared: wired.reduce((n, s) => n + s.blockerKeys.length, 0),
90
+ native: await mirrorNativeEdges({ provider, wired, issueByGroupKey }),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Re-render every group that both has an issue **and** has at least one blocker
96
+ * whose issue also exists.
97
+ *
98
+ * A group with no resolvable blocker is deliberately left alone rather than
99
+ * rewritten to an identical body: an issue-body update is a mutation and a
100
+ * notification, and doing it for a no-op edit is noise on every Story of every
101
+ * sweep.
102
+ *
103
+ * @param {object} args
104
+ * @returns {Array<{ groupKey: string, issueNumber: number, body: string, blockerKeys: string[] }>}
105
+ */
106
+ function collectWiredStories({ groups, edges, issueByGroupKey }) {
107
+ const wired = [];
108
+ for (const group of groups ?? []) {
109
+ const issueNumber = issueByGroupKey?.[group?.groupKey];
110
+ if (!Number.isInteger(issueNumber)) continue;
111
+ const rendered = buildStoryBody({ group, edges, issueByGroupKey });
112
+ const blockerKeys = rendered.dependsOn.filter((key) =>
113
+ Number.isInteger(issueByGroupKey[key]),
114
+ );
115
+ if (blockerKeys.length === 0) continue;
116
+ wired.push({
117
+ groupKey: rendered.groupKey,
118
+ issueNumber,
119
+ body: rendered.body,
120
+ blockerKeys,
121
+ });
122
+ }
123
+ return wired;
124
+ }
125
+
126
+ /**
127
+ * Mirror the declared edges as native GitHub `blocked_by` relations.
128
+ *
129
+ * Two shape hazards this crossing inherits from `plan-persist`, both silent if
130
+ * missed: `applyBlockedByDependencies` indexes `slugToIssueNumber` with plain
131
+ * property access (so it must be a plain object, never a `Map` — a `Map` yields
132
+ * `undefined` for every lookup, skips every edge, and reports success having
133
+ * written nothing), and it reads `dependsOn`, not `depends_on`.
134
+ *
135
+ * @param {object} args
136
+ * @returns {Promise<{ edgesAdded: number, edgesSkipped: number, edgesFailed: number }|null>}
137
+ * `null` when there is no interface to mirror through.
138
+ */
139
+ async function mirrorNativeEdges({ provider, wired, issueByGroupKey }) {
140
+ if (
141
+ typeof provider?.getDependencyWriteContext !== 'function' ||
142
+ typeof provider?.getTicket !== 'function'
143
+ ) {
144
+ Logger.warn(
145
+ '[audit-to-stories] provider exposes no getDependencyWriteContext/getTicket — ' +
146
+ 'skipping native blocked_by edges. Ordering survives in the ' +
147
+ '`blocked by #N` body footers just written.',
148
+ );
149
+ return null;
150
+ }
151
+ try {
152
+ const { gh, owner, repo } = provider.getDependencyWriteContext();
153
+ const summary = await applyBlockedByDependencies({
154
+ // The group key IS the slug here — it is the stable identifier both
155
+ // sides of an edge are keyed by — so `issueByGroupKey` is already the
156
+ // slug→number map the helper wants.
157
+ stories: wired.map((s) => ({
158
+ slug: s.groupKey,
159
+ dependsOn: s.blockerKeys,
160
+ })),
161
+ slugToIssueNumber: issueByGroupKey,
162
+ getTicket: (issueNumber) => provider.getTicket(issueNumber),
163
+ owner,
164
+ repo,
165
+ gh,
166
+ });
167
+ if (summary.edgesFailed > 0) {
168
+ Logger.warn(
169
+ `[audit-to-stories] ${summary.edgesFailed} native blocked_by edge(s) could ` +
170
+ 'not be written. Ordering survives in the `blocked by #N` body footers.',
171
+ );
172
+ }
173
+ return {
174
+ edgesAdded: summary.edgesAdded,
175
+ edgesSkipped: summary.edgesSkipped,
176
+ edgesFailed: summary.edgesFailed,
177
+ };
178
+ } catch (err) {
179
+ Logger.warn(
180
+ `[audit-to-stories] native blocked_by mirroring failed (${err.message}) — ` +
181
+ 'ordering survives in the `blocked by #N` body footers.',
182
+ );
183
+ return null;
184
+ }
185
+ }