mandrel 2.55.0 → 2.57.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 (131) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +4 -30
  4. package/.agents/docs/configuration.md +11 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/rules/ci-remediation.md +39 -21
  9. package/.agents/schemas/agentrc.schema.json +28 -185
  10. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  11. package/.agents/scripts/acceptance-eval.js +107 -17
  12. package/.agents/scripts/audit-to-stories.js +222 -75
  13. package/.agents/scripts/ceremony-derive.js +191 -0
  14. package/.agents/scripts/check-context-budget.js +28 -33
  15. package/.agents/scripts/check-cyclomatic.js +4 -3
  16. package/.agents/scripts/deliver-light.js +31 -94
  17. package/.agents/scripts/file-ci-gap.js +306 -0
  18. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  19. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
  20. package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
  21. package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
  22. package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
  23. package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
  24. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
  25. package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
  26. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
  27. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  28. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  29. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  30. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  31. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  32. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  33. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  34. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  35. package/.agents/scripts/lib/config/explain.js +0 -19
  36. package/.agents/scripts/lib/config/limits.js +18 -78
  37. package/.agents/scripts/lib/config/quality.js +6 -3
  38. package/.agents/scripts/lib/config/runners.js +3 -2
  39. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  40. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  41. package/.agents/scripts/lib/config-settings-schema.js +49 -143
  42. package/.agents/scripts/lib/crap-engine.js +35 -4
  43. package/.agents/scripts/lib/crap-utils.js +17 -1
  44. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  45. package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
  46. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
  47. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
  48. package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
  49. package/.agents/scripts/lib/findings/route-finding.js +38 -0
  50. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  51. package/.agents/scripts/lib/github/framework-repo.js +148 -2
  52. package/.agents/scripts/lib/label-constants.js +6 -1
  53. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  54. package/.agents/scripts/lib/observability/source-classifier.js +2 -0
  55. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  56. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  57. package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
  58. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
  59. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  60. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  61. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  62. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  63. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  64. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  65. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  66. package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
  67. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  68. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  69. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +133 -299
  70. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  71. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  72. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  73. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  74. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  75. package/.agents/scripts/lib/orchestration/run-epilogue.js +4 -4
  76. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  77. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  78. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  79. package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
  80. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  81. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  82. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  83. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  84. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  85. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  86. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  87. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  88. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  89. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  90. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  91. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  92. package/.agents/scripts/lib/test-run-credit.js +266 -0
  93. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  94. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  95. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  96. package/.agents/scripts/plan-context.js +7 -9
  97. package/.agents/scripts/plan-critics.js +28 -54
  98. package/.agents/scripts/plan-persist.js +25 -68
  99. package/.agents/scripts/pr-watch-with-update.js +3 -2
  100. package/.agents/scripts/quality-preview.js +51 -0
  101. package/.agents/scripts/run-tests.js +12 -0
  102. package/.agents/scripts/stories-wave-tick.js +23 -45
  103. package/.agents/scripts/test-isolate.js +13 -180
  104. package/.agents/scripts/update-coverage-baseline.js +25 -70
  105. package/.agents/scripts/update-crap-baseline.js +19 -123
  106. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  107. package/.agents/workflows/audit-clean-code.md +4 -3
  108. package/.agents/workflows/audit-to-stories.md +63 -27
  109. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  110. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  111. package/.agents/workflows/helpers/code-review.md +2 -3
  112. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  113. package/.agents/workflows/helpers/deliver-light.md +40 -105
  114. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  115. package/.agents/workflows/helpers/deliver-story-reference.md +56 -62
  116. package/.agents/workflows/helpers/deliver-story.md +9 -13
  117. package/.agents/workflows/helpers/plan-reference.md +132 -196
  118. package/.agents/workflows/mandrel-plan.md +28 -41
  119. package/.agents/workflows/memory-consolidate.md +9 -13
  120. package/docs/CHANGELOG.md +33 -0
  121. package/lib/migrations/index.js +4 -0
  122. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  123. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  124. package/package.json +1 -1
  125. package/.agents/scripts/lib/framework-version.js +0 -39
  126. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  127. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  128. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  129. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  130. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  131. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -1,124 +1,50 @@
1
1
  /**
2
- * plan-critic-conditions.js — size/heuristic-conditional dispatch decisions for
3
- * the /mandrel-plan author-step critics (Epic #4474 PR6, design §4).
2
+ * plan-critic-conditions.js — the pre-mortem critic's dispatch decision for
3
+ * the `/mandrel-plan` critic CLI (Epic #4474 PR6, design §4; narrowed to one
4
+ * arm by Story #5312).
4
5
  *
5
- * The collapsed plan flow keeps the consolidation and pre-mortem critics as
6
- * fresh-context sub-agent dispatches, but makes each dispatch
7
- * **conditional** instead of unconditional — the dominant plan cost is
8
- * turns × standing context, and an unconditional critic pays a full
9
- * sub-agent spawn even when it provably has nothing to find. This module
10
- * computes those decisions deterministically so the workflow never judges
11
- * its own dispatch conditions:
6
+ * The critic is a fresh-context sub-agent dispatch, and the dispatch is
7
+ * **conditional**: the dominant plan cost is turns × standing context, and an
8
+ * unconditional critic pays a full sub-agent spawn even when it provably has
9
+ * nothing to find. This module computes the decision deterministically so
10
+ * the workflow never judges its own dispatch condition.
12
11
  *
13
- * - **Consolidation**: dispatch only when the existing
14
- * `evaluateConsolidationPrecondition` gate says `dispatch: true` AND
15
- * (the draft has more than `CONSOLIDATION_STORY_THRESHOLD` stories OR
16
- * the precondition confirmed a divergence from the Delivery Slicing
17
- * table). A fail-open precondition (missing/unparseable table) on a
18
- * small draft is NOT a confirmed divergence — it skips, because a
19
- * ≤-threshold draft is small enough for gate #2's single-view review
20
- * to catch a distorted shape without a dedicated sub-agent.
21
- * - **Pre-mortem**: dispatch when the ticket count is at least half
22
- * of `maxTickets`, OR any configured `planning.riskHeuristics` phrase
23
- * matches the plan text (case-insensitive substring), OR the
24
- * **external-dependency probe** (Story #4700) finds an out-of-repo marker
25
- * in the plan text. Story #4542 removed the authored-risk-verdict condition
26
- * along with the verdict itself; every surviving condition reads the plan's
27
- * own observable text and shape rather than a self-assessment.
28
- *
29
- * The external-dependency probe (Story #4700) is what gives the default N=1
30
- * path a cheap viability check: on that path the size condition is unreachable
31
- * (`count*2 >= maxTickets` never holds at one ticket) and a repo whose resolved
32
- * `planning.riskHeuristics` is empty has no phrase to match, so a plan-time
33
- * discoverable blocker — a scoped package the plan names that no manifest
34
- * declares, a cross-repo reference, an external service prerequisite — reached
12
+ * One trigger survives: the **external-dependency probe** (Story #4700),
13
+ * which finds an out-of-repo marker in the plan text — a scoped package the
14
+ * plan names that no manifest declares, a cross-repo reference, an external
15
+ * service prerequisite — so a plan-time discoverable blocker does not reach
35
16
  * delivery unquestioned (the swarm-os #757 shape). The probe is deliberately
36
- * **conservative**: it matches only explicit markers (npm scoped-package specs,
37
- * `github.com/<owner>/<repo>` URLs, prerequisite-keyword-anchored endpoints),
38
- * never NLP guesswork, so a plan with no such marker dispatches exactly as it
39
- * did before.
17
+ * **conservative**: it matches only explicit markers (npm scoped-package
18
+ * specs, `github.com/<owner>/<repo>` URLs, prerequisite-keyword-anchored
19
+ * endpoints), never NLP guesswork.
20
+ *
21
+ * Story #5312 deleted the two triggers that sat beside it — the ticket count
22
+ * reaching half a `maxTickets` budget, and a `planning.riskHeuristics` phrase
23
+ * matching the plan text — with the constants they read. The count trigger
24
+ * was unreachable at the default N=1; the phrase list was empty in every
25
+ * consumer that resolved it. The consolidation critic went with them: its
26
+ * one deterministic input was a `## Delivery Slicing` table no Story carries.
40
27
  *
41
- * Under-firing risk (design PR6 note): the persist validators are
42
- * unchanged hard gates and G2's cohort re-measures plan quality; every
43
- * skip decision this module produces is logged to the plan-metrics ledger
44
- * (`appendCriticSkip`) by the caller so under-firing is auditable.
28
+ * Under-firing risk (design PR6 note): the persist validators are unchanged
29
+ * hard gates; every skip decision this module produces is logged to the
30
+ * plan-metrics ledger (`appendCriticSkip`) by the caller so under-firing is
31
+ * auditable.
45
32
  *
46
33
  * Pure, synchronous, no I/O. The single caller is `plan-critics-evaluate.js`,
47
- * driven by the `plan-critics.js` CLI that `/mandrel-plan` runs between Author and
48
- * Persist (Story #4592); the CLI owns reading the authored artifacts and the
49
- * resolved config.
50
- */
51
-
52
- import { evaluateConsolidationPrecondition } from './consolidation-precondition.js';
53
-
54
- /**
55
- * Draft-story count above which the consolidation critic fires even
56
- * without a confirmed slicing divergence (#4474 PR6: "> 5 stories").
34
+ * driven by the `plan-critics.js` CLI the operator runs between Author and
35
+ * Persist when they want the critic; the CLI owns reading the authored
36
+ * artifacts and the resolved config.
57
37
  */
58
- export const CONSOLIDATION_STORY_THRESHOLD = 5;
59
38
 
60
39
  /**
61
40
  * @typedef {Object} CriticDispatchDecision
62
- * @property {'consolidation'|'pre-mortem'} critic
41
+ * @property {'pre-mortem'} critic
63
42
  * @property {boolean} dispatch
64
43
  * @property {string[]} reasons Why the critic fires — or why it is safe to
65
44
  * skip. Never empty: a skip's reasons are the audit trail the
66
45
  * plan-metrics ledger records.
67
46
  */
68
47
 
69
- /**
70
- * Decide the consolidation dispatch: precondition AND size/divergence.
71
- *
72
- * @param {object} input
73
- * @param {object[]} input.draftStories - The draft `tickets.json` array
74
- * (raw Story objects with top-level `slug` / `depends_on` / `body`).
75
- * @param {string} input.specText - The text carrying the `## Delivery
76
- * Slicing` table. At author time this is the authored `techspec.md`
77
- * content (the Epic body carries the same folded section post-persist).
78
- * @returns {CriticDispatchDecision}
79
- */
80
- export function evaluateConsolidationDispatch({ draftStories, specText }) {
81
- const precondition = evaluateConsolidationPrecondition({
82
- draftStories,
83
- epicBody: specText,
84
- });
85
-
86
- if (!precondition.dispatch) {
87
- return {
88
- critic: 'consolidation',
89
- dispatch: false,
90
- reasons: precondition.reasons,
91
- };
92
- }
93
-
94
- const storyCount = draftStories.length;
95
- const oversized = storyCount > CONSOLIDATION_STORY_THRESHOLD;
96
- const diverges = precondition.cause === 'divergence';
97
-
98
- if (!oversized && !diverges) {
99
- return {
100
- critic: 'consolidation',
101
- dispatch: false,
102
- reasons: [
103
- `Draft has ${storyCount} story(ies) (≤ ${CONSOLIDATION_STORY_THRESHOLD}) and no confirmed Delivery Slicing divergence — gate #2's single-view review covers a draft this small.`,
104
- ...precondition.reasons,
105
- ],
106
- };
107
- }
108
-
109
- const reasons = [];
110
- if (diverges) reasons.push(...precondition.reasons);
111
- if (oversized) {
112
- reasons.push(
113
- `Draft has ${storyCount} stories (> ${CONSOLIDATION_STORY_THRESHOLD}) — large enough that a distorted shape can hide from the gate #2 single view.`,
114
- );
115
- }
116
- if (!diverges && precondition.cause === 'fail-open') {
117
- reasons.push(...precondition.reasons);
118
- }
119
- return { critic: 'consolidation', dispatch: true, reasons };
120
- }
121
-
122
48
  /**
123
49
  * Explicit npm scoped-package marker: `@scope/name`. Requires the leading `@`
124
50
  * and an interior `/`, so bare GitHub handles (`@dsj1984`) and the
@@ -215,8 +141,8 @@ function matchExternalServicePrereqs(planText) {
215
141
  /**
216
142
  * The external-dependency probe (Story #4700): a conservative, marker-only
217
143
  * scan of the draft plan text for artifacts outside the current repo that the
218
- * plan depends on. A match is the pre-mortem's third dispatch condition; a
219
- * no-match plan behaves exactly as it did before this probe existed.
144
+ * plan depends on. A match is the pre-mortem's dispatch condition; a no-match
145
+ * plan skips the critic.
220
146
  *
221
147
  * @param {object} input
222
148
  * @param {string} [input.planText] - Concatenated plan text (tech spec +
@@ -262,19 +188,12 @@ export function evaluateExternalDependencyProbe({
262
188
  }
263
189
 
264
190
  /**
265
- * Decide the pre-mortem dispatch: size ≥ ½ budget, a risk-heuristic phrase
266
- * match, or an external-dependency probe match (Story #4700).
191
+ * Decide the pre-mortem dispatch: an external-dependency probe match
192
+ * (Story #4700) — the one deterministic trigger left after Story #5312.
267
193
  *
268
194
  * @param {object} input
269
- * @param {number} input.ticketCount - Draft ticket count (0 in the
270
- * single-delivery shape — no tickets exist).
271
- * @param {number} input.maxTickets - The reviewability budget
272
- * (`getLimits(config).maxTickets`).
273
- * @param {string[]} [input.riskHeuristics] - `planning.riskHeuristics`
274
- * phrases from the resolved config.
275
- * @param {string} [input.planText] - Concatenated plan text the heuristics and
276
- * the external-dependency probe match against (tech spec + serialized
277
- * tickets).
195
+ * @param {string} [input.planText] - Concatenated plan text the probe
196
+ * matches against (tech spec + serialized tickets).
278
197
  * @param {string[]} [input.knownPackages] - Package specifiers the repo's own
279
198
  * manifests declare (own name + dependency maps + workspace package names),
280
199
  * passed to the external-dependency probe.
@@ -284,58 +203,28 @@ export function evaluateExternalDependencyProbe({
284
203
  * @returns {CriticDispatchDecision}
285
204
  */
286
205
  export function evaluatePremortemDispatch({
287
- ticketCount,
288
- maxTickets,
289
- riskHeuristics = [],
290
206
  planText = '',
291
207
  knownPackages = [],
292
208
  ownerRepo = null,
293
209
  }) {
294
- if (!Number.isInteger(maxTickets) || maxTickets <= 0) {
295
- throw new TypeError(
296
- 'evaluatePremortemDispatch: maxTickets must be a positive integer',
297
- );
298
- }
299
- const reasons = [];
300
-
301
- const count = Number.isInteger(ticketCount) ? ticketCount : 0;
302
- if (count * 2 >= maxTickets) {
303
- reasons.push(
304
- `Ticket count ${count} is at least half the reviewability budget (maxTickets ${maxTickets}).`,
305
- );
306
- }
307
-
308
- const haystack = String(planText).toLowerCase();
309
- const matched = riskHeuristics.filter(
310
- (phrase) =>
311
- typeof phrase === 'string' &&
312
- phrase.trim().length > 0 &&
313
- haystack.includes(phrase.trim().toLowerCase()),
314
- );
315
- if (matched.length > 0) {
316
- reasons.push(
317
- `planning.riskHeuristics match(es) in the plan text: ${matched.map((p) => `"${p.trim()}"`).join(', ')}.`,
318
- );
319
- }
320
-
321
210
  const externalDeps = evaluateExternalDependencyProbe({
322
211
  planText,
323
212
  knownPackages,
324
213
  ownerRepo,
325
214
  });
326
215
  if (externalDeps.matched) {
327
- reasons.push(...externalDeps.reasons);
328
- }
329
-
330
- if (reasons.length > 0) {
331
- return { critic: 'pre-mortem', dispatch: true, reasons };
216
+ return {
217
+ critic: 'pre-mortem',
218
+ dispatch: true,
219
+ reasons: externalDeps.reasons,
220
+ };
332
221
  }
333
222
 
334
223
  return {
335
224
  critic: 'pre-mortem',
336
225
  dispatch: false,
337
226
  reasons: [
338
- `Ticket count ${count} is under half the budget (maxTickets ${maxTickets}), no planning.riskHeuristics phrase matches the plan text, and the external-dependency probe found no out-of-repo markers.`,
227
+ 'The external-dependency probe found no out-of-repo markers in the plan text.',
339
228
  ],
340
229
  };
341
230
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * plan-critics-evaluate.js — shared critic-dispatch evaluation for the
3
- * collapsed /mandrel-plan flow (#4496 fix 6).
3
+ * collapsed /mandrel-plan flow (#4496 fix 6; pre-mortem only since Story #5312).
4
4
  *
5
- * One consumer: the `plan-critics.js` CLI, which `/mandrel-plan` runs between its
6
- * Author and Persist steps. The CLI loads the draft artifacts, calls this
7
- * module, prints the verdict as JSON, and records every skip on the
8
- * plan-metrics ledger; the workflow dispatches a fresh-context critic
9
- * sub-agent on a `dispatch: true` verdict and folds the findings into a
10
- * re-author round before persist.
5
+ * One consumer: the `plan-critics.js` CLI, which the operator runs between
6
+ * Author and Persist when they want the pre-mortem. The CLI loads the draft
7
+ * artifacts, calls this module, prints the verdict as JSON, and records a
8
+ * skip on the plan-metrics ledger; the workflow dispatches a fresh-context
9
+ * critic sub-agent on a `dispatch: true` verdict and folds the findings into
10
+ * a re-author round before persist.
11
11
  *
12
12
  * Story #4592 moved that evaluation here from `run-plan-persist.js`, which
13
13
  * ran it after authoring was finished and immediately before
@@ -21,26 +21,7 @@
21
21
  * @module lib/orchestration/plan-critics-evaluate
22
22
  */
23
23
 
24
- import { getLimits } from '../config-resolver.js';
25
- import {
26
- evaluateConsolidationDispatch,
27
- evaluatePremortemDispatch,
28
- } from './plan-critic-conditions.js';
29
- import { evaluateTextHygiene } from './plan-text-hygiene.js';
30
-
31
- /**
32
- * Resolve the planning risk heuristics list from the canonical config
33
- * block (same resolution `plan-context.js` and the decompose context use).
34
- *
35
- * @param {object} config
36
- * @returns {string[]}
37
- */
38
- function resolveRiskHeuristics(config = {}) {
39
- if (Array.isArray(config.planning?.riskHeuristics)) {
40
- return config.planning.riskHeuristics;
41
- }
42
- return [];
43
- }
24
+ import { evaluatePremortemDispatch } from './plan-critic-conditions.js';
44
25
 
45
26
  /**
46
27
  * Resolve the `{ owner, repo }` the external-dependency probe's cross-repo arm
@@ -57,24 +38,11 @@ function resolveOwnerRepo(config = {}) {
57
38
  }
58
39
 
59
40
  /**
60
- * Evaluate the consolidation + pre-mortem critic dispatch conditions over
61
- * the authored planning artifacts (#4474 PR6 conditions, unchanged):
62
- *
63
- * - Consolidation: skipped outright when `tickets` is null/absent (the
64
- * single-delivery shape authors no draft tickets); otherwise the
65
- * deterministic precondition + size/divergence conditions.
66
- * - Pre-mortem: ticket count at least half `maxTickets`, OR any
67
- * `planning.riskHeuristics` phrase matching the plan text, OR the
68
- * external-dependency probe (Story #4700) matching an out-of-repo marker
69
- * — a scoped package absent from `knownPackages`, a cross-repo
70
- * `github.com/<owner>/<repo>` reference, or a named external service
71
- * prerequisite. Story #4542 retired its authored-risk-level condition with
72
- * the verdict itself.
73
- * - Text hygiene (Story #4599, advisory-only): deterministic body lints
74
- * (dangling-citation / open-question / slicing-mass) over the draft
75
- * stories. It has no `dispatch` semantics and spawns nothing — its
76
- * `findings[]` are re-author-round input, and the consolidation /
77
- * premortem dispatch verdicts are untouched by it.
41
+ * Evaluate the pre-mortem critic's dispatch condition over the authored
42
+ * planning artifacts: the external-dependency probe (Story #4700) matching an
43
+ * out-of-repo marker — a scoped package absent from `knownPackages`, a
44
+ * cross-repo `github.com/<owner>/<repo>` reference, or a named external
45
+ * service prerequisite.
78
46
  *
79
47
  * @param {{
80
48
  * techSpecContent: string,
@@ -88,9 +56,7 @@ function resolveOwnerRepo(config = {}) {
88
56
  * that gathers them; this module stays pure. Empty when unresolved, which
89
57
  * only widens what the probe treats as external.
90
58
  * @returns {{
91
- * consolidation: { critic: string, dispatch: boolean, reasons: string[] },
92
59
  * premortem: { critic: string, dispatch: boolean, reasons: string[] },
93
- * textHygiene: { critic: string, findings: Array<object> },
94
60
  * }}
95
61
  */
96
62
  export function evaluatePlanCritics({
@@ -100,24 +66,7 @@ export function evaluatePlanCritics({
100
66
  knownPackages = [],
101
67
  }) {
102
68
  const ticketList = Array.isArray(tickets) ? tickets : null;
103
- const consolidation =
104
- ticketList === null
105
- ? {
106
- critic: 'consolidation',
107
- dispatch: false,
108
- reasons: [
109
- 'single-delivery shape — no draft tickets exist to consolidate.',
110
- ],
111
- }
112
- : evaluateConsolidationDispatch({
113
- draftStories: ticketList,
114
- specText: techSpecContent,
115
- });
116
-
117
69
  const premortem = evaluatePremortemDispatch({
118
- ticketCount: ticketList?.length ?? 0,
119
- maxTickets: getLimits(config).maxTickets,
120
- riskHeuristics: resolveRiskHeuristics(config),
121
70
  planText: [
122
71
  techSpecContent ?? '',
123
72
  ticketList ? JSON.stringify(ticketList) : '',
@@ -126,10 +75,5 @@ export function evaluatePlanCritics({
126
75
  ownerRepo: resolveOwnerRepo(config),
127
76
  });
128
77
 
129
- const textHygiene = {
130
- critic: 'text-hygiene',
131
- findings: evaluateTextHygiene({ draftStories: ticketList }).findings,
132
- };
133
-
134
- return { consolidation, premortem, textHygiene };
78
+ return { premortem };
135
79
  }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * lib/orchestration/plan-persist/audit-provenance.js — what an audit-seeded
3
+ * plan leaves behind for the next sweep.
4
+ *
5
+ * The chained `/mandrel-plan` path files Stories from an audit seed, and until
6
+ * Story #5307 those Stories were invisible to the sweep that proposed them:
7
+ * they carried no `audit::*` label, so they were absent from the label-listed
8
+ * corpus dedup indexes, and nothing recorded them in the cross-run ledger. The
9
+ * provenance footers `carryProvenanceFooters` stamps could not cover for
10
+ * either — with an index in play the exact lookup is answered from that pool
11
+ * and never reaches the provider.
12
+ *
13
+ * Both halves ride the create loop in `story-ops.js` because that is the only
14
+ * seam on this path that cannot be forgotten: there is no second required pass
15
+ * here the way `--wire-edges` is one for the standalone filer.
16
+ */
17
+
18
+ import {
19
+ DEFAULT_LEDGER_PATH,
20
+ readLedger,
21
+ recordFiledIdentities,
22
+ writeLedger,
23
+ } from '../../findings/audit-ledger.js';
24
+ import {
25
+ parseAuditLabelFooter,
26
+ parseFingerprintFooter,
27
+ parseSemanticKeyFooter,
28
+ } from '../../findings/route-finding.js';
29
+ import { Logger } from '../../Logger.js';
30
+
31
+ /**
32
+ * A fresh accumulator for one persist run's audit filings.
33
+ *
34
+ * The run collects into this and flushes once, so a plan that files N Stories
35
+ * leaves one reviewable ledger diff rather than N.
36
+ *
37
+ * @param {string} [ledgerPath]
38
+ * @returns {{ path: string, ledger: object|null, recorded: number, ambiguous: number }}
39
+ */
40
+ function newAuditLedgerRecord(ledgerPath) {
41
+ return {
42
+ path: ledgerPath ?? DEFAULT_LEDGER_PATH,
43
+ ledger: null,
44
+ recorded: 0,
45
+ ambiguous: 0,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Merge the seed's `audit::*` labels into an audit-seeded Story's labels.
51
+ *
52
+ * Taken from the **seed union**, not from the attributed provenance source: a
53
+ * label only scopes which issues the next sweep's corpus fetch returns, and
54
+ * matching inside that corpus is by fingerprint and semantic key. A superset
55
+ * corpus can therefore only ever find more, never less — so erring wide is the
56
+ * safe direction, and it keeps lens-to-label knowledge in the audit filer that
57
+ * owns it rather than re-deriving a dimension here (the junk-derivation trap
58
+ * Story #4195 closed).
59
+ *
60
+ * A non-audit seed carries no such footer, so this is a no-op there.
61
+ *
62
+ * @param {Array<object>} stories — the assembled Stories.
63
+ * @param {string} [provenanceSource] — the seed this plan was authored from.
64
+ * @returns {Array<object>}
65
+ */
66
+ export function withAuditLabels(stories, provenanceSource) {
67
+ const fromSeed = parseAuditLabelFooter(provenanceSource ?? '');
68
+ if (fromSeed.length === 0) return stories;
69
+ return stories.map((story) => ({
70
+ ...story,
71
+ labels: [...new Set([...story.labels, ...fromSeed])],
72
+ }));
73
+ }
74
+
75
+ /**
76
+ * Record one just-created Story in the cross-run audit ledger.
77
+ *
78
+ * The identities are read back off the body this run assembled, so what is
79
+ * recorded is exactly what was stamped — attribution already applied. A Story
80
+ * from a non-audit plan carries no footers and records nothing, which is why
81
+ * a `--seed` or `--tickets` run never touches the ledger file at all.
82
+ *
83
+ * **The union fallback is deliberately NOT recorded.** When the seed carried
84
+ * footers but the plan attributed none per-Story, every sibling carries every
85
+ * fingerprint; binding a finding to one of them would be a coin flip, and a
86
+ * wrong binding is worse than none — the finding would be suppressed against an
87
+ * Issue that never tracked it, or resurrected when an unrelated Story closed.
88
+ * The audit path stamps `provenance` mechanically, so this is the exception.
89
+ *
90
+ * @param {{ story: object, id: number, ledgerRecord: object, attributed: boolean }} args
91
+ */
92
+ function recordAuditFiling({ story, id, ledgerRecord, attributed }) {
93
+ const fingerprints = parseFingerprintFooter(story.body);
94
+ if (fingerprints.length === 0) return;
95
+ if (!attributed) {
96
+ ledgerRecord.ambiguous += fingerprints.length;
97
+ return;
98
+ }
99
+ const semanticKeys = parseSemanticKeyFooter(story.body);
100
+ const identities = fingerprints.map((fingerprint, i) => ({
101
+ fingerprint,
102
+ semanticKey: semanticKeys[i] ?? '',
103
+ title: story.title,
104
+ }));
105
+ const { ledger, recorded } = recordFiledIdentities({
106
+ ledger: ledgerRecord.ledger ?? readLedger(ledgerRecord.path),
107
+ identities,
108
+ issue: { number: id },
109
+ });
110
+ ledgerRecord.ledger = ledger;
111
+ ledgerRecord.recorded += recorded;
112
+ }
113
+
114
+ /**
115
+ * Persist the run's audit filings, once, after every Story exists.
116
+ *
117
+ * Writing per-Story would rewrite a committed baseline N times for one plan;
118
+ * writing once keeps the diff to a single reviewable change. A run that
119
+ * recorded nothing writes nothing.
120
+ *
121
+ * @param {object} ledgerRecord
122
+ * @param {{ warn: Function }} logger
123
+ */
124
+ function flushAuditLedger(ledgerRecord, logger) {
125
+ if (ledgerRecord.ambiguous > 0) {
126
+ logger.warn(
127
+ `[plan-persist] audit ledger: ${ledgerRecord.ambiguous} identity(ies) not recorded — ` +
128
+ 'the seed carried provenance footers but no Story attributed them, so ownership is ' +
129
+ 'ambiguous. Author per-Story `provenance` to record them.',
130
+ );
131
+ }
132
+ if (!ledgerRecord.ledger || ledgerRecord.recorded === 0) return;
133
+ writeLedger(ledgerRecord.path, ledgerRecord.ledger);
134
+ logger.warn(
135
+ `[plan-persist] audit ledger: recorded ${ledgerRecord.recorded} filed finding(s) ` +
136
+ `to ${ledgerRecord.path}.`,
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Record everything one persist run filed, once, after every Story exists.
142
+ *
143
+ * Driven from the persist orchestrator rather than from inside the create loop:
144
+ * the loop's job is to create issues, and this is the only other place that
145
+ * always runs after it. Either seam is unforgettable — there is no second
146
+ * required pass on this path the way `--wire-edges` is one for the standalone
147
+ * filer — and keeping the side effect at the orchestration layer leaves the
148
+ * create loop doing one thing.
149
+ *
150
+ * One write per plan, not per Story: the ledger is committed state, and N
151
+ * rewrites of it for one plan is a diff nobody can review. A run that recorded
152
+ * nothing — a non-audit plan, or one whose identities were all union-carried —
153
+ * writes nothing at all.
154
+ *
155
+ * @param {object} params
156
+ * @param {Array<object>} params.stories — the assembled Stories.
157
+ * @param {Array<{ slug: string, id: number }>} params.created — persist receipts.
158
+ * @param {Array<object>} params.tickets — the raw authored tickets.
159
+ * @param {string} [params.ledgerPath]
160
+ * @param {{ warn: Function }} [params.logger]
161
+ * @param {boolean} [params.dryRun] — a dry run records nothing at all.
162
+ * @returns {{ recorded: number, ambiguous: number }}
163
+ */
164
+ export function recordAuditFilings({
165
+ stories,
166
+ created,
167
+ tickets,
168
+ ledgerPath,
169
+ logger,
170
+ dryRun = false,
171
+ }) {
172
+ // A dry run created no issue to record against, and must not touch committed
173
+ // state. Owned here rather than at the call site so the contract is testable.
174
+ if (dryRun) return { recorded: 0, ambiguous: 0 };
175
+ const record = newAuditLedgerRecord(ledgerPath);
176
+ const idBySlug = new Map((created ?? []).map((c) => [c.slug, c.id]));
177
+ // Attribution is a property of what the plan AUTHORED, so it is read off the
178
+ // raw tickets rather than threaded through assembly: a Story that declared no
179
+ // `provenance` inherited the seed union, where ownership is a coin flip.
180
+ const attributedSlugs = new Set(
181
+ (tickets ?? [])
182
+ .filter((t) => t?.provenance !== undefined && t?.provenance !== null)
183
+ .map((t) => t?.slug),
184
+ );
185
+ for (const story of stories ?? []) {
186
+ const id = idBySlug.get(story.slug);
187
+ if (typeof id !== 'number') continue;
188
+ recordAuditFiling({
189
+ story,
190
+ id,
191
+ ledgerRecord: record,
192
+ attributed: attributedSlugs.has(story.slug),
193
+ });
194
+ }
195
+ flushAuditLedger(record, logger ?? Logger);
196
+ return { recorded: record.recorded, ambiguous: record.ambiguous };
197
+ }