mandrel 2.56.0 → 2.58.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 (114) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -33
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -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/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/evidence-gate.js +17 -1
  16. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  17. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  18. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  19. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  20. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  21. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  22. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  23. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  24. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  25. package/.agents/scripts/lib/config/explain.js +0 -19
  26. package/.agents/scripts/lib/config/limits.js +18 -78
  27. package/.agents/scripts/lib/config/quality.js +6 -3
  28. package/.agents/scripts/lib/config/runners.js +3 -2
  29. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  30. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  31. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  32. package/.agents/scripts/lib/crap-engine.js +35 -4
  33. package/.agents/scripts/lib/crap-utils.js +17 -1
  34. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  35. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  36. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  37. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  38. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  39. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  40. package/.agents/scripts/lib/orchestration/code-review.js +7 -3
  41. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  42. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  43. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  44. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  45. package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
  46. package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
  47. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  48. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  49. package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
  51. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
  52. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
  53. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  54. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  55. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  56. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
  57. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  58. package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
  59. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  60. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
  61. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  62. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
  63. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  64. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  65. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  66. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  67. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  68. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  69. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  70. package/.agents/scripts/lib/story-body/story-body.js +54 -240
  71. package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
  72. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  73. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  74. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  75. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  76. package/.agents/scripts/lib/test-run-credit.js +277 -0
  77. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  78. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  79. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  80. package/.agents/scripts/plan-context.js +7 -9
  81. package/.agents/scripts/plan-critics.js +28 -54
  82. package/.agents/scripts/plan-persist.js +25 -68
  83. package/.agents/scripts/quality-preview.js +51 -0
  84. package/.agents/scripts/run-tests.js +12 -0
  85. package/.agents/scripts/stories-wave-tick.js +23 -45
  86. package/.agents/scripts/test-isolate.js +13 -180
  87. package/.agents/scripts/update-coverage-baseline.js +25 -70
  88. package/.agents/scripts/update-crap-baseline.js +19 -123
  89. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  90. package/.agents/workflows/audit-clean-code.md +4 -3
  91. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  92. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  93. package/.agents/workflows/helpers/code-review.md +2 -3
  94. package/.agents/workflows/helpers/deliver-digest.md +46 -55
  95. package/.agents/workflows/helpers/deliver-light.md +40 -105
  96. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  97. package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
  98. package/.agents/workflows/helpers/deliver-story.md +10 -13
  99. package/.agents/workflows/helpers/plan-reference.md +163 -221
  100. package/.agents/workflows/mandrel-plan.md +31 -40
  101. package/.agents/workflows/memory-consolidate.md +9 -13
  102. package/docs/CHANGELOG.md +36 -0
  103. package/lib/cli/registry.js +98 -2
  104. package/lib/migrations/index.js +4 -0
  105. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  106. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  107. package/package.json +1 -1
  108. package/.agents/scripts/lib/framework-version.js +0 -39
  109. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  110. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  111. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  112. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  113. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  114. 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,107 @@
1
+ /**
2
+ * acceptance-handle-repair.js — repair-before-judging for the `AC-<n>:`
3
+ * presentation handle on authored acceptance items (Story #5323).
4
+ *
5
+ * The handle belongs to the body renderer, which numbers each checkbox from
6
+ * its position in `acceptance[]` (`story-body.js`). An author planning from
7
+ * an existing ticket reads that rendered body as a template and carries the
8
+ * handle forward, so the persisted checkbox reads `- [ ] AC-1: AC-1: …`; a
9
+ * lettered handle copied out of a hand-edited source (`AC-14a:`) misnumbers
10
+ * the rest of the list against its own text.
11
+ *
12
+ * The correction is mechanical and total — strip the handle the renderer
13
+ * will re-apply — so this module applies it and **reports** it, exactly as
14
+ * `changes-repair.js` does for the `{ path, assumption }` formality. Charging
15
+ * the author a re-draft round to paste back text the strip already produced
16
+ * buys nothing.
17
+ *
18
+ * It lives beside the validator rather than inside it for the same reason
19
+ * `changes-repair.js` does: the validator's job is to judge, and mixing a
20
+ * mutating repair pass into a module of pure collectors muddies both.
21
+ * `persist-helpers.js#validateTickets` calls this first, then the validators.
22
+ *
23
+ * @module lib/orchestration/plan-persist/acceptance-handle-repair
24
+ */
25
+
26
+ import { stripAcceptanceHandle } from '../../story-body/story-body.js';
27
+ import { renderChangeRepair } from './changes-repair.js';
28
+
29
+ /**
30
+ * Strip the handle off one surface's `acceptance[]`, recording each distinct
31
+ * strip on `repairs`.
32
+ *
33
+ * @param {object} surface An object that may carry an `acceptance` array.
34
+ * @param {string} slug
35
+ * @param {Set<string>} reported Items already reported for this ticket.
36
+ * @param {object[]} repairs Accumulator, mutated.
37
+ * @returns {void}
38
+ */
39
+ function repairSurface(surface, slug, reported, repairs) {
40
+ if (!Array.isArray(surface.acceptance)) return;
41
+ surface.acceptance = surface.acceptance.map((item) => {
42
+ const { text, stripped } = stripAcceptanceHandle(item);
43
+ if (!stripped) return item;
44
+ const from = String(item ?? '');
45
+ if (!reported.has(from)) {
46
+ reported.add(from);
47
+ repairs.push({ kind: 'acceptance-handle', slug, from, to: text });
48
+ }
49
+ return text;
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Strip the presentation `AC-<n>:` handle off every authored `acceptance[]`
55
+ * item, on both surfaces that can carry one: the ticket's top-level array
56
+ * (the machine contract) and a structured body's.
57
+ *
58
+ * A serialized **string** body needs no pass of its own — `parse()` strips
59
+ * the handle with the same grammar this does, so the two surfaces converge
60
+ * on one list whichever carried the handle, and the contract sync (which
61
+ * fails closed on a disagreement) sees them agree.
62
+ *
63
+ * Mutates `tickets` in place — the persist pipeline threads this same array
64
+ * on to assembly. Total: a non-array argument and non-Story tickets are
65
+ * no-ops.
66
+ *
67
+ * @param {object[]} tickets
68
+ * @returns {Array<{ kind: 'acceptance-handle', slug: string, from: string, to: string }>}
69
+ */
70
+ export function normalizeAcceptanceHandles(tickets) {
71
+ const repairs = [];
72
+ for (const ticket of Array.isArray(tickets) ? tickets : []) {
73
+ if (!ticket || typeof ticket !== 'object' || ticket.type !== 'story') {
74
+ continue;
75
+ }
76
+ const slug = ticket.slug ?? ticket.title ?? '<unknown>';
77
+ // A ticket normally carries the same list on both surfaces, so report
78
+ // each distinct item once — the operator reads one correction, not two.
79
+ const reported = new Set();
80
+ repairSurface(ticket, slug, reported, repairs);
81
+ const body = ticket.body;
82
+ if (body && typeof body === 'object') {
83
+ repairSurface(body, slug, reported, repairs);
84
+ }
85
+ }
86
+ return repairs;
87
+ }
88
+
89
+ /**
90
+ * Render one entry of the dry-run's mixed repair list.
91
+ *
92
+ * The list carries two kinds — a `changes[]` entry repaired by probing base,
93
+ * and an `acceptance[]` item whose handle was normalised off — and both are
94
+ * mechanical corrections the run applied on the author's behalf, so both
95
+ * belong on the one list the operator reads. This module owns the dispatch
96
+ * because it owns the newer kind: it renders its own line and delegates
97
+ * every other kind to `changes-repair.js`, so neither producer has to know
98
+ * the other's shape.
99
+ *
100
+ * @param {{ kind?: string, slug: string, from: string, to?: string }} repair
101
+ * @returns {string}
102
+ */
103
+ export function renderRepair(repair) {
104
+ if (repair.kind !== 'acceptance-handle') return renderChangeRepair(repair);
105
+ const { slug, from, to } = repair;
106
+ return `Story "${slug}": acceptance[] item "${from}" carried an AC-<n> handle — normalised to "${to}"; the body renderer numbers the checkboxes.`;
107
+ }