mandrel 2.56.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 (106) 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 +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/lib/audit-suite/checklist-threading.js +15 -2
  16. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  17. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  18. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  19. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  20. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  21. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  22. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  23. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  24. package/.agents/scripts/lib/config/explain.js +0 -19
  25. package/.agents/scripts/lib/config/limits.js +18 -78
  26. package/.agents/scripts/lib/config/quality.js +6 -3
  27. package/.agents/scripts/lib/config/runners.js +3 -2
  28. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  29. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  30. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  31. package/.agents/scripts/lib/crap-engine.js +35 -4
  32. package/.agents/scripts/lib/crap-utils.js +17 -1
  33. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  34. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  35. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  36. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  37. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  39. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  40. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  41. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  42. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  43. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  44. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  45. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  46. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  48. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
  49. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  51. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  52. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  53. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  56. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  57. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  58. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  59. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  60. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  61. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  62. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  63. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  64. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  65. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  66. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  67. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  68. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  69. package/.agents/scripts/lib/test-run-credit.js +266 -0
  70. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  71. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  72. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  73. package/.agents/scripts/plan-context.js +7 -9
  74. package/.agents/scripts/plan-critics.js +28 -54
  75. package/.agents/scripts/plan-persist.js +25 -68
  76. package/.agents/scripts/quality-preview.js +51 -0
  77. package/.agents/scripts/run-tests.js +12 -0
  78. package/.agents/scripts/stories-wave-tick.js +23 -45
  79. package/.agents/scripts/test-isolate.js +13 -180
  80. package/.agents/scripts/update-coverage-baseline.js +25 -70
  81. package/.agents/scripts/update-crap-baseline.js +19 -123
  82. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  83. package/.agents/workflows/audit-clean-code.md +4 -3
  84. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  85. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  86. package/.agents/workflows/helpers/code-review.md +2 -3
  87. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  88. package/.agents/workflows/helpers/deliver-light.md +40 -105
  89. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  90. package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
  91. package/.agents/workflows/helpers/deliver-story.md +9 -13
  92. package/.agents/workflows/helpers/plan-reference.md +132 -219
  93. package/.agents/workflows/mandrel-plan.md +27 -40
  94. package/.agents/workflows/memory-consolidate.md +9 -13
  95. package/docs/CHANGELOG.md +23 -0
  96. package/lib/migrations/index.js +4 -0
  97. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  98. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  99. package/package.json +1 -1
  100. package/.agents/scripts/lib/framework-version.js +0 -39
  101. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  102. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  103. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  104. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  105. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  106. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -7,8 +7,9 @@
7
7
  * the Stage-1 split-policy validator (`assertAcceptancePartition`).
8
8
  *
9
9
  * Each Story body is the single executable document: Tech Spec stays inline
10
- * under `## Spec`. Over-budget Specs fail closed (split / tighten) — never
11
- * spill to `docs/`. Top-level `acceptance[]` / `verify[]` are the machine
10
+ * under `## Spec`, at whatever length the work needs (Story #5312 deleted
11
+ * the token budget that used to refuse it) — never spilled to `docs/`.
12
+ * Top-level `acceptance[]` / `verify[]` are the machine
12
13
  * contract and are synced into the body so the GitHub issue stays complete
13
14
  * without requiring the LLM to dual-author the same lists.
14
15
  *
@@ -33,7 +34,6 @@ import {
33
34
  concurrentMap,
34
35
  FANOUT_CONCURRENCY,
35
36
  } from '../../util/concurrent-map.js';
36
- import { assertSpecWithinBudget } from '../spec-spill.js';
37
37
  import { assertAcceptancePartition } from '../split-policy-validator.js';
38
38
  import {
39
39
  externalDependencyId,
@@ -60,13 +60,6 @@ export const PLAN_RUN_LABEL_PREFIX = 'plan-run::';
60
60
  /** Stable color for the cohort grouping label (`ensureLabels`). */
61
61
  const PLAN_RUN_LABEL_COLOR = '#C5DEF5';
62
62
 
63
- /**
64
- * Stable color for the `route::lite` ceremony-route hint (Story #4707;
65
- * hint-only since Story #4722 — `/mandrel-deliver` re-derives the route from the
66
- * Story body's shape).
67
- */
68
- const LITE_ROUTE_LABEL_COLOR = '#D4C5F9';
69
-
70
63
  /** Length of the derived plan-run id (hex chars). */
71
64
  const PLAN_RUN_ID_LENGTH = 8;
72
65
 
@@ -226,11 +219,10 @@ function extractPlanFingerprint(body) {
226
219
  * Labels the authoring pass is never allowed to set. The `agent::*` axis is
227
220
  * the runtime's lifecycle state (persist owns the terminal `agent::ready`
228
221
  * flip itself), `type::*` is fixed to `type::story` by the v2 hierarchy,
229
- * `persona::*` is a retired axis, `plan-run::*` is the runtime-derived
230
- * cohort grouping axis (Story #4692), and `route::*` is the runtime-derived
231
- * ceremony-route axis (Story #4707) — a hand-authored entry on either
232
- * derived axis would compete with the deterministic label persist applies
233
- * itself.
222
+ * `persona::*` and `route::*` are retired axes (the latter with the
223
+ * plan-side lite claim, Story #5312), and `plan-run::*` is the
224
+ * runtime-derived cohort grouping axis (Story #4692) — a hand-authored entry
225
+ * on it would compete with the deterministic label persist applies itself.
234
226
  */
235
227
  const FORBIDDEN_LABEL_PREFIXES = Object.freeze([
236
228
  'agent::',
@@ -299,8 +291,6 @@ function bodyObjectFromTicket(ticket) {
299
291
  verify: ticket.verify ?? [],
300
292
  references: ticket.references ?? [],
301
293
  non_goals: ticket.non_goals ?? [],
302
- wide: ticket.wide ?? null,
303
- reason_to_exist: ticket.reason_to_exist ?? null,
304
294
  depends_on: ticket.depends_on ?? [],
305
295
  }).body;
306
296
  }
@@ -398,7 +388,7 @@ export function normalizeStoryTicket(ticket) {
398
388
 
399
389
  /**
400
390
  * Fold optional shared Tech Spec prose into a Story body when the Story has
401
- * no inline Spec. Specs stay inline; over-budget Specs throw.
391
+ * no inline Spec. Specs stay inline, verbatim, at any length.
402
392
  *
403
393
  * Precedence: per-Story `body.spec` wins; otherwise `sharedSpec` is used
404
394
  * (N===1 convenience only — callers must not share one Spec across N>1).
@@ -430,8 +420,7 @@ export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
430
420
  return { bodyObject: next };
431
421
  }
432
422
 
433
- const { content } = assertSpecWithinBudget({ storyId: slug, spec: inline });
434
- next.spec = content;
423
+ next.spec = inline;
435
424
  return { bodyObject: next };
436
425
  }
437
426
 
@@ -854,16 +843,14 @@ async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
854
843
  }
855
844
 
856
845
  /**
857
- * Ensure a runtime-derived persist label (`plan-run::<id>` cohort grouping,
858
- * `route::lite` route marker) exists before it is applied — GitHub's
859
- * create-issue path does not auto-create unknown labels on every provider
860
- * route, and an opaque derived label never exists yet.
846
+ * Ensure the runtime-derived `plan-run::<id>` cohort label exists before it
847
+ * is applied — GitHub's create-issue path does not auto-create unknown
848
+ * labels on every provider route, and an opaque derived label never exists
849
+ * yet.
861
850
  *
862
851
  * **Non-fatal by design**, matching the native-blocked_by mirroring posture:
863
- * neither label is load-bearing for correctness (grouping is cosmetic, and
864
- * the route label is a human-visible hint only — `/mandrel-deliver` re-derives the
865
- * route from the Story body's shape, Story #4722), so it is never a reason
866
- * to fail persist. On an ensure
852
+ * the label is not load-bearing for correctness (grouping is cosmetic), so
853
+ * it is never a reason to fail persist. On an ensure
867
854
  * failure (throw, or the label reported `missing` by the post-loop
868
855
  * reconcile) the create loop proceeds **without** the label — applying an
869
856
  * unensured label could fail the issue create itself, and the Stories matter
@@ -957,30 +944,16 @@ async function ensurePersistLabel({
957
944
  * every id is known (Story #4544), so plan-created order stops depending on
958
945
  * prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
959
946
  *
960
- * **A lite-routed cohort carries the `route::lite` hint** (Story #4707,
961
- * hint-only since Story #4722). When the caller resolves the plan's
962
- * effective complexity route to `lite` (the planner's recorded verdict,
963
- * upheld by the shape backstop), it passes the label via `opts.routeLabel`
964
- * and every created Story carries it — a **human-visible hint only**, never
965
- * the control signal: `/mandrel-deliver` re-derives the route from each Story body's
966
- * own shape (`resolveStoryDispatchMode`), so a lost or failed label write
967
- * cannot misroute delivery. A full-routed plan passes nothing and its
968
- * Stories carry **no** route label. Like the cohort label, the ensure is
969
- * non-fatal — the label is cosmetic either way.
970
- *
971
947
  * @param {object} args
972
948
  * @param {object} args.provider
973
949
  * @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
974
950
  * @param {object} [args.opts]
975
951
  * @param {boolean} [args.opts.dryRun=false]
976
- * @param {string|null} [args.opts.routeLabel=null] Route marker label to
977
- * apply to every created Story (`route::lite`), or null for none.
978
952
  * @returns {Promise<{
979
953
  * created: Array<{ slug: string, id: number, url?: string, title: string, adopted: boolean }>,
980
954
  * dependencyEdges: { edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null,
981
955
  * planRunLabel: string,
982
956
  * planRunLabelApplied: boolean,
983
- * routeLabel: string|null,
984
957
  * }>}
985
958
  */
986
959
  export async function createStoryIssues({ provider, stories, opts = {} }) {
@@ -991,10 +964,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
991
964
  }
992
965
 
993
966
  const list = Array.isArray(stories) ? stories : [];
994
- const routeLabel =
995
- typeof opts.routeLabel === 'string' && opts.routeLabel.trim() !== ''
996
- ? opts.routeLabel.trim()
997
- : null;
998
967
 
999
968
  // Derived once for the whole cohort, before any write — a pure function of
1000
969
  // the authored artifacts, so dry-run can report it write-free and a resume
@@ -1018,7 +987,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
1018
987
  // the label — the derived id is still reported, the application is not
1019
988
  // claimed.
1020
989
  planRunLabelApplied: false,
1021
- routeLabel,
1022
990
  };
1023
991
  }
1024
992
 
@@ -1031,18 +999,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
1031
999
  'metadata only, never a deliver input.',
1032
1000
  role: 'cohort',
1033
1001
  });
1034
- const applyRouteLabel =
1035
- routeLabel !== null &&
1036
- (await ensurePersistLabel({
1037
- provider,
1038
- label: routeLabel,
1039
- color: LITE_ROUTE_LABEL_COLOR,
1040
- description:
1041
- 'Ceremony-lite hint only: /mandrel-deliver re-derives the route; ' +
1042
- 'every close gate still runs.',
1043
- role: 'route-marker',
1044
- }));
1045
-
1046
1002
  const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
1047
1003
  const created = [];
1048
1004
  const idBySlug = new Map();
@@ -1073,11 +1029,7 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
1073
1029
  const result = await provider.createIssue({
1074
1030
  title: story.title,
1075
1031
  body: renderStoryBodyForCreate(story, idBySlug),
1076
- labels: [
1077
- ...story.labels,
1078
- ...(applyCohortLabel ? [cohortLabel] : []),
1079
- ...(applyRouteLabel ? [routeLabel] : []),
1080
- ],
1032
+ labels: [...story.labels, ...(applyCohortLabel ? [cohortLabel] : [])],
1081
1033
  // Story #5112 — hand the provider the same content-keyed lookup this
1082
1034
  // loop's resume path uses, so a retry after a lost response adopts the
1083
1035
  // issue attempt 1 already filed instead of creating a twin.
@@ -1128,7 +1080,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
1128
1080
  // consult before advertising a `label:` filter that may match nothing.
1129
1081
  planRunLabel: cohortLabel,
1130
1082
  planRunLabelApplied: applyCohortLabel,
1131
- routeLabel: applyRouteLabel ? routeLabel : null,
1132
1083
  };
1133
1084
  }
1134
1085
 
@@ -10,23 +10,19 @@
10
10
  * @module lib/orchestration/plan-persist/wave-serialisation
11
11
  */
12
12
 
13
- import {
14
- detectCollision,
15
- OVERLAP_SOURCES,
16
- renderScrapeAttribution,
17
- } from '../../wave-runner/footprint.js';
13
+ import { detectCollision } from '../../wave-runner/footprint.js';
18
14
 
19
15
  /**
20
16
  * Predict which same-wave pairs the dispatch guard will actually refuse to
21
17
  * co-dispatch (Story #5265).
22
18
  *
23
19
  * The wave table answers a `depends_on` question, and the runtime answers a
24
- * different one: `stories-wave-tick.js` withholds on {@link detectCollision},
25
- * whose footprint is the declared `changes[]` **plus** every path scraped out
26
- * of the Story's title, spec and serialized body — and that body carries
27
- * `## Verify`, so two Stories that merely run the same gate script share a
28
- * path neither will edit. The table therefore promised parallelism the very
29
- * next tick refused, with nothing anywhere reconciling the two.
20
+ * different one: `stories-wave-tick.js` withholds on {@link detectCollision}
21
+ * over the declared `changes[]` (Story #5313 retired the text scrape), so
22
+ * two same-wave Stories that both declare a path — a shared generated
23
+ * baseline, say — are shown in one order and dispatched one at a time. The
24
+ * table promised parallelism the next tick refused, with nothing anywhere
25
+ * reconciling the two.
30
26
  *
31
27
  * This runs the runtime's own exported predicate — not a reimplementation of
32
28
  * it — pairwise within each wave, so the prediction cannot drift from the
@@ -34,8 +30,8 @@ import {
34
30
  *
35
31
  * @param {ReturnType<typeof buildWaveTable>} waveTable
36
32
  * @param {Array<{ slug: string, title?: string, body?: string, spec?: string, changes?: Array }>} stories
37
- * @param {{ tempRoot?: string }} [options]
38
- * @returns {Array<{ wave: number, slugs: [string, string], paths: string[], source: string, attribution: object[] }>}
33
+ * @param {{ tempRoot?: string }} [options] Accepted for compatibility; unread.
34
+ * @returns {Array<{ wave: number, slugs: [string, string], paths: string[], source: string }>}
39
35
  */
40
36
  export function predictWaveSerialisation(waveTable, stories, options = {}) {
41
37
  const bySlug = new Map(
@@ -71,8 +67,7 @@ export function predictWaveSerialisation(waveTable, stories, options = {}) {
71
67
  * against the same promise, and an operator should read them the same way.
72
68
  * The difference is what they know — the shared-editor pass names paths two
73
69
  * Stories both *write*, this one names every pair the dispatcher will refuse
74
- * to run together whatever the reason, including the pairs whose only shared
75
- * path was scraped out of a `## Verify` line.
70
+ * to run together, glob declarations included.
76
71
  *
77
72
  * @param {ReturnType<typeof predictWaveSerialisation>} collisions
78
73
  * @returns {string[]}
@@ -80,31 +75,23 @@ export function predictWaveSerialisation(waveTable, stories, options = {}) {
80
75
  export function renderPredictedSerialisationLines(collisions) {
81
76
  const list = Array.isArray(collisions) ? collisions : [];
82
77
  if (list.length === 0) return [];
83
- const rows = list.map((c) => {
84
- const scraped = renderScrapeAttribution(c.attribution);
85
- return `| \`${c.slugs[0]}\` + \`${c.slugs[1]}\` | ${c.paths
86
- .map((p) => `\`${p}\``)
87
- .join(', ')} | ${c.source} | ${scraped ? `\`${scraped}\`` : '—'} |`;
88
- });
89
- const scrapedOnly = list.filter(
90
- (c) => c.source === OVERLAP_SOURCES.SCRAPED,
91
- ).length;
78
+ const rows = list.map(
79
+ (c) =>
80
+ `| \`${c.slugs[0]}\` + \`${c.slugs[1]}\` | ${c.paths
81
+ .map((p) => `\`${p}\``)
82
+ .join(', ')} | ${c.source} |`,
83
+ );
92
84
  return [
93
85
  '',
94
86
  `#### ⚠️ Predicted serialisation (${list.length} same-order pair(s))`,
95
87
  '',
96
- '| Stories | Colliding paths | Overlap source | Scraped from |',
97
- '| --- | --- | --- | --- |',
88
+ '| Stories | Colliding paths | Overlap source |',
89
+ '| --- | --- | --- |',
98
90
  ...rows,
99
91
  '',
100
- '_The dispatcher compares the **evidence-widened** footprint — declared ' +
101
- '`changes[]` plus every path named in the title, `## Spec` and the rest ' +
102
- 'of the body — so these pairs are shown in one order above but will be ' +
103
- `dispatched one at a time. ${scrapedOnly} pair(s) collide only on ` +
104
- 'scraped paths; the "Scraped from" column names the field each such ' +
105
- 'path was read out of, so a shared `## Verify` command is ' +
106
- 'distinguishable from a genuine unpredicted edit target. The guard is ' +
107
- 'deliberately not narrowed to `changes[]`: the declaration is a lower ' +
108
- 'bound (Story #4875) and under-serialising is the worse failure._',
92
+ '_The dispatcher compares the **declared** `changes[]` footprints ' +
93
+ '(Story #5313 retired the text scrape), so these pairs are shown in ' +
94
+ 'one order above but will be dispatched one at a time: both Stories ' +
95
+ 'declare a colliding path, or one declares a glob._',
109
96
  ];
110
97
  }
@@ -1,33 +1,19 @@
1
1
  /**
2
- * plan-text-hygiene.js — deterministic text-hygiene lints over draft Story
3
- * bodies (Story #4599).
2
+ * plan-text-hygiene.js — the `open-question` lint over draft Story bodies
3
+ * (Story #4599; narrowed to one lint by Story #5312).
4
4
  *
5
- * The text analysis of Stories #4592–#4594 (and domio#1684) surfaced three
6
- * body-defect classes no gate checks: dangling prose citations whose carrier
7
- * document is unlocatable, operator-directed open questions persisted into
8
- * tickets executed by non-interactive sub-agents, and `## Slicing` sections
9
- * carrying more mass than the `## Spec` they are supposed to checkpoint.
10
- * This module makes those classes checkable at the one point a re-author
11
- * loop exists — the pre-persist critic gate (`plan-critics.js`).
5
+ * A Story is executed by a non-interactive sub-agent, so an operator-directed
6
+ * open question persisted into its body ("Flag if…", "TBD", "confirm with the
7
+ * operator", a trailing `?`) can never be answered where it is read. This
8
+ * module makes that class checkable at the one point a re-author loop exists —
9
+ * the persist dry-run, which lists every match as a **warning** and proceeds.
12
10
  *
13
- * Advisory by contract: findings are deterministic text for the workflow's
14
- * re-author round. They never gate persist, never flip a dispatch verdict,
15
- * and spawn nothing.
11
+ * Story #5312 deleted the sibling `dangling-citation` and `slicing-mass`
12
+ * lints with the critic gate that surfaced them: both scored prose shape the
13
+ * authoring model already judges, and neither ever changed a persisted body.
16
14
  *
17
- * Heuristics are deliberately narrow (few false positives over recall):
18
- *
19
- * - **dangling-citation** — a sentence referencing a document section
20
- * (`§`, "design note", "review doc") with no repo-relative path and no
21
- * `#<digits>` issue anchor in the same sentence. An anchor written
22
- * inside a code span counts — the conventional markdown form (Story
23
- * #4906).
24
- * - **open-question** — interrogative-to-operator phrasing ("Flag if",
25
- * "TBD", "confirm with the operator", a trailing `?`) in Goal/Spec
26
- * prose outside code spans. Bodies record decisions; unresolved
27
- * unknowns belong in declarative Key Assumptions.
28
- * - **slicing-mass** — `## Slicing` character mass exceeding `## Spec`
29
- * character mass when both are present: checkpoints carrying
30
- * Spec-grade detail the Spec then re-covers.
15
+ * Advisory by contract: findings are deterministic text for the dry-run's
16
+ * warning list. They never gate persist and spawn nothing.
31
17
  *
32
18
  * Pure, synchronous, no I/O. Operates on the draft `stories.json` array,
33
19
  * reusing `parse()` from `lib/story-body/story-body.js` for section access.
@@ -42,19 +28,6 @@ import { parse } from '../story-body/story-body.js';
42
28
  /** Truncation length for the `evidence` excerpt on each finding. */
43
29
  const EVIDENCE_MAX_CHARS = 160;
44
30
 
45
- /**
46
- * Phrases the citation heuristic treats as a reference to an external
47
- * carrier document. Matched case-insensitively within one sentence.
48
- */
49
- const CITATION_MARKERS = [/§/, /\bdesign note\b/i, /\breview doc\b/i];
50
-
51
- /**
52
- * Anchors that locate a citation: a `#<digits>` issue reference or a
53
- * repo-relative path (a slash-joined token carrying a file-ish segment).
54
- */
55
- const ISSUE_ANCHOR = /#\d+/;
56
- const REPO_PATH_ANCHOR = /[\w.-]+\/[\w./-]+/;
57
-
58
31
  /**
59
32
  * Operator-directed open-question phrasings. Each match is an instruction
60
33
  * or question aimed at a human, which a non-interactive delivery sub-agent
@@ -68,62 +41,43 @@ const OPEN_QUESTION_MARKERS = [
68
41
 
69
42
  /**
70
43
  * @typedef {Object} TextHygieneFinding
71
- * @property {'dangling-citation'|'open-question'|'slicing-mass'} kind
44
+ * @property {'open-question'} kind
72
45
  * @property {string} slug - The draft Story's slug ('' when absent).
73
46
  * @property {string} evidence - Excerpt of the offending text.
74
47
  * @property {string} message - Human-readable, re-author-actionable text.
75
48
  */
76
49
 
77
50
  /**
78
- * Private-use sentinel standing in for one extracted inline code span.
79
- * It carries no citation marker, no anchor and no sentence boundary, so it
80
- * is inert for every marker heuristic while recording where the span sat.
51
+ * Private-use sentinel standing in for one extracted inline code span. It
52
+ * carries no question marker and no sentence boundary, so it is inert for
53
+ * the heuristic while keeping the sentence split where the span sat.
81
54
  */
82
- const CODE_SLOT_PATTERN = /\uE000(\d+)\uE001/g;
55
+ const CODE_SLOT = '\uE000';
83
56
 
84
57
  /**
85
58
  * Replace fenced code blocks with a space and each inline code span with a
86
59
  * positional slot, so code content (shell snippets, grep patterns, JSON)
87
- * never trips a prose heuristic yet stays recoverable for the anchor check.
60
+ * never trips a prose heuristic.
88
61
  *
89
62
  * @param {string} text
90
- * @returns {{ slotted: string, spans: string[] }}
63
+ * @returns {string}
91
64
  */
92
- function slotCodeSpans(text) {
93
- const spans = [];
94
- const slotted = text
95
- .replace(/```[\s\S]*?```/g, ' ')
96
- .replace(/`[^`\n]*`/g, (span) => {
97
- spans.push(span.slice(1, -1));
98
- return `\uE000${spans.length - 1}\uE001`;
99
- });
100
- return { slotted, spans };
65
+ function stripCodeSpans(text) {
66
+ return text.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`\n]*`/g, CODE_SLOT);
101
67
  }
102
68
 
103
69
  /**
104
- * Split prose into sentence-ish units. Newlines are boundaries too, so a
105
- * bullet list yields one unit per bullet. Each unit carries two views of
106
- * the same sentence: `prose`, with code content removed, which every
107
- * marker heuristic reads; and `anchorText`, with inline code content
108
- * restored, so a citation anchored inside a code span is still visible to
109
- * the anchor check. Restoring only ever adds anchors — markers are matched
110
- * against `prose` alone, exactly as before.
70
+ * Split prose into sentence-ish units with code content removed. Newlines
71
+ * are boundaries too, so a bullet list yields one unit per bullet.
111
72
  *
112
73
  * @param {string} text
113
- * @returns {Array<{ prose: string, anchorText: string }>}
74
+ * @returns {string[]}
114
75
  */
115
76
  function splitSentences(text) {
116
- const { slotted, spans } = slotCodeSpans(text);
117
- return slotted
77
+ return stripCodeSpans(text)
118
78
  .split(/(?<=[.!?])\s+|\n+/)
119
- .map((unit) => ({
120
- prose: unit.replace(CODE_SLOT_PATTERN, ' ').trim(),
121
- anchorText: unit.replace(
122
- CODE_SLOT_PATTERN,
123
- (_slot, index) => ` ${spans[Number(index)]} `,
124
- ),
125
- }))
126
- .filter((unit) => unit.prose.length > 0);
79
+ .map((unit) => unit.replaceAll(CODE_SLOT, ' ').trim())
80
+ .filter((unit) => unit.length > 0);
127
81
  }
128
82
 
129
83
  /**
@@ -139,37 +93,6 @@ function excerpt(text) {
139
93
  : flat;
140
94
  }
141
95
 
142
- /**
143
- * dangling-citation: a citation-marker sentence with no locating anchor.
144
- * The marker is matched against the sentence's code-stripped prose; the
145
- * anchor against its code-restored text, so a path or issue reference
146
- * written in a code span — the conventional markdown form — counts.
147
- *
148
- * @param {string} prose - Raw body prose.
149
- * @param {string} slug
150
- * @returns {TextHygieneFinding[]}
151
- */
152
- function findDanglingCitations(prose, slug) {
153
- const findings = [];
154
- for (const { prose: sentence, anchorText } of splitSentences(prose)) {
155
- const cites = CITATION_MARKERS.some((m) => m.test(sentence));
156
- if (!cites) continue;
157
- const anchored =
158
- ISSUE_ANCHOR.test(anchorText) || REPO_PATH_ANCHOR.test(anchorText);
159
- if (anchored) continue;
160
- findings.push({
161
- kind: 'dangling-citation',
162
- slug,
163
- evidence: excerpt(sentence),
164
- message:
165
- 'Citation names a document section but no repo-relative path or ' +
166
- '#<issue> anchor locates it in the same sentence — the executing ' +
167
- 'agent cannot follow it. Anchor the citation or inline the claim.',
168
- });
169
- }
170
- return findings;
171
- }
172
-
173
96
  /**
174
97
  * open-question: operator-directed phrasing (or a trailing `?`) in prose a
175
98
  * non-interactive sub-agent executes.
@@ -180,7 +103,7 @@ function findDanglingCitations(prose, slug) {
180
103
  */
181
104
  function findOpenQuestions(prose, slug) {
182
105
  const findings = [];
183
- for (const { prose: sentence } of splitSentences(prose)) {
106
+ for (const sentence of splitSentences(prose)) {
184
107
  const marked =
185
108
  OPEN_QUESTION_MARKERS.some((m) => m.test(sentence)) ||
186
109
  sentence.endsWith('?');
@@ -200,33 +123,7 @@ function findOpenQuestions(prose, slug) {
200
123
  }
201
124
 
202
125
  /**
203
- * slicing-mass: `## Slicing` outweighing `## Spec` when both are present.
204
- *
205
- * @param {{ slicing?: string, spec?: string }} body - Parsed Story body.
206
- * @param {string} slug
207
- * @returns {TextHygieneFinding[]}
208
- */
209
- function findSlicingMass(body, slug) {
210
- const slicing = typeof body.slicing === 'string' ? body.slicing : '';
211
- const spec = typeof body.spec === 'string' ? body.spec : '';
212
- if (slicing.length === 0 || spec.length === 0) return [];
213
- if (slicing.length <= spec.length) return [];
214
- return [
215
- {
216
- kind: 'slicing-mass',
217
- slug,
218
- evidence: excerpt(slicing),
219
- message:
220
- `## Slicing (${slicing.length} chars) outweighs ## Spec ` +
221
- `(${spec.length} chars) — checkpoints are carrying Spec-grade ` +
222
- 'detail. Keep each Slicing checkpoint to one line and move the ' +
223
- 'detail into ## Spec.',
224
- },
225
- ];
226
- }
227
-
228
- /**
229
- * Evaluate the three text-hygiene lints over a draft Story array.
126
+ * Evaluate the open-question lint over a draft Story array.
230
127
  *
231
128
  * @param {{ draftStories?: Array<object>|null }} args - The draft
232
129
  * `stories.json` array (raw Story objects with top-level `slug` /
@@ -249,13 +146,7 @@ export function evaluateTextHygiene({ draftStories = null } = {}) {
249
146
  }
250
147
  const goal = typeof body.goal === 'string' ? body.goal : '';
251
148
  const spec = typeof body.spec === 'string' ? body.spec : '';
252
- const bodyProse =
253
- typeof story.body === 'string' ? story.body : [goal, spec].join('\n');
254
- findings.push(
255
- ...findDanglingCitations(bodyProse, slug),
256
- ...findOpenQuestions([goal, spec].join('\n'), slug),
257
- ...findSlicingMass(body, slug),
258
- );
149
+ findings.push(...findOpenQuestions([goal, spec].join('\n'), slug));
259
150
  }
260
151
  return { findings };
261
152
  }