mandrel 2.57.0 → 2.59.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 (58) hide show
  1. package/.agents/README.md +6 -3
  2. package/.agents/agents/story-worker.md +12 -11
  3. package/.agents/docs/SDLC.md +6 -7
  4. package/.agents/docs/quality-gates.md +1 -1
  5. package/.agents/instructions.md +2 -3
  6. package/.agents/runtime-deps.json +7 -2
  7. package/.agents/schemas/crap-baseline.schema.json +1 -1
  8. package/.agents/schemas/crap-report.schema.json +1 -1
  9. package/.agents/scripts/evidence-gate.js +17 -1
  10. package/.agents/scripts/install-matrix-assert.js +48 -3
  11. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
  12. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
  13. package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
  14. package/.agents/scripts/lib/crap-engine.js +2 -2
  15. package/.agents/scripts/lib/crap-utils.js +21 -5
  16. package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
  17. package/.agents/scripts/lib/escomplex-kernel.js +298 -0
  18. package/.agents/scripts/lib/maintainability-engine.js +3 -3
  19. package/.agents/scripts/lib/orchestration/code-review.js +7 -3
  20. package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
  21. package/.agents/scripts/lib/orchestration/plan-context.js +41 -27
  22. package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
  23. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +6 -1
  24. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +14 -9
  25. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +45 -31
  26. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +8 -9
  27. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +1 -1
  28. package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
  29. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +15 -5
  30. package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
  32. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
  33. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
  34. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
  35. package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
  36. package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
  37. package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
  38. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
  39. package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
  40. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  41. package/.agents/scripts/lib/story-body/story-body.js +36 -2
  42. package/.agents/scripts/lib/templates/decomposer-prompts.js +73 -21
  43. package/.agents/scripts/lib/test-run-credit.js +23 -12
  44. package/.agents/scripts/plan-persist.js +0 -11
  45. package/.agents/skills/skills.index.json +1 -11
  46. package/.agents/workflows/audit-to-stories.md +14 -11
  47. package/.agents/workflows/helpers/deliver-digest.md +22 -15
  48. package/.agents/workflows/helpers/deliver-story-reference.md +31 -11
  49. package/.agents/workflows/helpers/deliver-story.md +6 -5
  50. package/.agents/workflows/helpers/plan-reference.md +53 -13
  51. package/.agents/workflows/mandrel-plan.md +19 -14
  52. package/README.md +3 -3
  53. package/docs/CHANGELOG.md +21 -0
  54. package/lib/cli/registry.js +143 -27
  55. package/package.json +7 -2
  56. package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
  57. package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
  58. package/.agents/skills/core/scope-triage/SKILL.md +0 -48
@@ -11,7 +11,7 @@ description:
11
11
 
12
12
  ## Inputs
13
13
 
14
- Single planning path — there is no Epic/Story router, no scope-triage
14
+ Single planning path — there is no Epic/Story router, no split-triage
15
15
  `epic|story` verdict (Gate #3's container groups, never routes). **Derive the
16
16
  mode from what the operator typed, announce it, act**:
17
17
 
@@ -57,7 +57,8 @@ and derives source ids from its `sourceTickets[]`; it also writes
57
57
  **`stories.template.json`**, step 2's skeleton.
58
58
 
59
59
  The envelope carries docs context, the story-author prompt (`systemPrompts.story`,
60
- plus `systemPrompts.storySplitRules` for an N>1 draft), `sourceTickets[]`,
60
+ plus `systemPrompts.storySplitRules` for an N>1 draft and
61
+ `systemPrompts.storyTicketsRules` in tickets mode), `sourceTickets[]`,
61
62
  `duplicates[]` (open **Stories**, never Epics), `epicCandidates[]` +
62
63
  `dependencyCandidates[]` (Gate #3; path collisions), `priorFeedback` and
63
64
  advisory `complexitySignals` (**no routing authority**). An envelope over the
@@ -92,12 +93,14 @@ included. One rescue: a **never-tracked** one normalises to `creates`. Fields:
92
93
  [ref](helpers/plan-reference.md).
93
94
 
94
95
  Artifacts under `temp/plan-<slug>/`: `stories.json` (**length 1 by default**;
95
- a Spec is as long as the work needs, inline, never under `docs/`); optional
96
- `techspec.md` (**N===1 only**, folded into `## Spec`) and
97
- `acceptance-manifest.json` (N>1 — `--plan-acceptance`). Use the envelope
96
+ a Spec is as long as the work needs, inline, never under `docs/`) and optional
97
+ `techspec.md` (**N===1 only**, folded into `## Spec`). Use the envelope
98
98
  `systemPrompts.story`; split only under the policy above, and when you do,
99
- read `systemPrompts.storySplitRules` too — it carries the schedule and
100
- partition rules the core omits.
99
+ read `systemPrompts.storySplitRules` too — it carries the schedule rules and
100
+ the same-wave collision refusal the core omits. In **tickets mode** also read
101
+ `systemPrompts.storyTicketsRules`: the source ticket is evidence, not a
102
+ template — re-derive `acceptance[]` rather than carrying its list, handles
103
+ and tier suffixes forward.
101
104
 
102
105
  **Tickets mode:** every Story authors a top-level `supersedes[]`; persist
103
106
  refuses a partial map ([shape](helpers/plan-reference.md)).
@@ -108,15 +111,20 @@ The maker-blind **pre-mortem** critic is not a step of this spine: run
108
111
 
109
112
  ### 3. Persist
110
113
 
111
- **Gate #2** — STOP for approval before persist **only** when the operator asked
112
- to review (`--force-review`). Under `--yes`, auto-proceed.
114
+ **Gate #2** — STOP for approval before persist when the draft carries **more
115
+ than one Story** (a split always earns operator eyes, `--force-review` or
116
+ not), or when the operator asked to review (`--force-review`).
117
+ Under `--yes`, auto-proceed.
113
118
 
114
119
  **Gate #3 — adopt, else create.** Offer the top `epicCandidates[]` Epic at
115
120
  **any N** (`--epic <id>`); else, at **N>2**, a new container (`--epic-title` /
116
121
  `--epic-goal`). Never unasked ([ref](helpers/plan-reference.md)).
117
122
 
118
123
  Run persist `--dry-run` **first** — same command, writes suppressed; every gate
119
- runs before the first `createIssue`, and the run **lists its warnings**
124
+ runs before the first `createIssue` — including the **same-wave collision
125
+ refusal**, which rejects an N>1 draft whose siblings declare a common path
126
+ (merge them, or order them with `depends_on`) — and the run **lists its
127
+ warnings**
120
128
  (a `creates` / `refactors-existing` the base branch disagrees with, a goal or
121
129
  acceptance path absent at base, an open question in a body) and the
122
130
  `changes[]` repairs it applied ([list](helpers/plan-reference.md)). Read
@@ -126,7 +134,6 @@ them; they never stop the persist:
126
134
  node .agents/scripts/plan-persist.js \
127
135
  --stories temp/plan-<slug>/stories.json \
128
136
  --plan-dir temp/plan-<slug> \
129
- [--plan-acceptance temp/plan-<slug>/acceptance-manifest.json] \
130
137
  [--tech-spec temp/plan-<slug>/techspec.md] \
131
138
  [--source-tickets 123,456] \
132
139
  [--epic <id> | --epic-title "<name>" --epic-goal "<one paragraph>"]
@@ -154,6 +161,4 @@ also comments on and closes each source id ([ref](helpers/plan-reference.md)).
154
161
  ## See also
155
162
 
156
163
  [`/mandrel-deliver`](mandrel-deliver.md), [`/audit-to-stories`](audit-to-stories.md),
157
- [`helpers/plan-reference.md`](helpers/plan-reference.md) (on-demand detail),
158
- [`core/scope-triage`](../skills/core/scope-triage/SKILL.md) — optional
159
- split-advisory notes only (no routing verdict).
164
+ [`helpers/plan-reference.md`](helpers/plan-reference.md) (on-demand detail).
package/README.md CHANGED
@@ -86,9 +86,9 @@ time to confirm the install is healthy.
86
86
  >
87
87
  > Prefer a surgical alternative? Replace `shamefully-hoist` with a scoped
88
88
  > `public-hoist-pattern[]=` line per package listed in
89
- > `.agents/runtime-deps.json` (`ajv`, `ajv-formats`, `js-yaml`, `minimatch`,
90
- > `picomatch`, `typhonjs-escomplex`). If `mandrel doctor`
91
- > reports `runtime-deps missing: …`, this is the fix.
89
+ > `.agents/runtime-deps.json` — read the file rather than copying a list from
90
+ > here, since the complexity kernel's closure is several packages. If
91
+ > `mandrel doctor` reports `runtime-deps missing: …`, this is the fix.
92
92
 
93
93
  `bootstrap.js` is interactive on a TTY and auto-accepts the
94
94
  owner/repo/base branch/operator handle it can infer from your local
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,27 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.59.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.58.0...mandrel-v2.59.0) (2026-09-14)
19
+
20
+
21
+ ### Added
22
+
23
+ * planning stops fragmenting cohesive work: the acceptance band goes, the dispatcher's own collision predicate becomes the split gate, and the audit seed stops pre-cutting the partition ([#5332](https://github.com/dsj1984/mandrel/issues/5332)) ([#5334](https://github.com/dsj1984/mandrel/issues/5334)) ([e6408e4](https://github.com/dsj1984/mandrel/commit/e6408e44d3f9a7fcbdf8c9a7df4a1b68fdffd88b))
24
+ * replace the complexity kernel's parse and dispatch layers and declare the runtime closure it leaves behind, so the framework's dependency guards describe what actually loads ([#5336](https://github.com/dsj1984/mandrel/issues/5336)) ([#5337](https://github.com/dsj1984/mandrel/issues/5337)) ([821c4f5](https://github.com/dsj1984/mandrel/commit/821c4f55fe3fe9b91b83a4ef9fe9ddee5da9db8f))
25
+
26
+ ## [2.58.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.57.0...mandrel-v2.58.0) (2026-09-12)
27
+
28
+
29
+ ### Added
30
+
31
+ * make the close test credit earnable on any test runner, and surface the gap at setup instead of mid-close ([#5324](https://github.com/dsj1984/mandrel/issues/5324)) ([#5329](https://github.com/dsj1984/mandrel/issues/5329)) ([04db145](https://github.com/dsj1984/mandrel/commit/04db145b8f1017121e129897f9207ab51db1c3f8))
32
+ * tickets-mode planning re-derives the Story instead of carrying the source ticket's shape: acceptance handles, tier suffixes, generated-artifact footprints and pinned identifiers ([#5323](https://github.com/dsj1984/mandrel/issues/5323)) ([#5326](https://github.com/dsj1984/mandrel/issues/5326)) ([5563e7a](https://github.com/dsj1984/mandrel/commit/5563e7a264864f78001beb19ce58160a1b483cda))
33
+
34
+
35
+ ### Fixed
36
+
37
+ * give base-sync and the Story-scope code review one base ref, so a stale local base branch cannot raise false blockers ([#5325](https://github.com/dsj1984/mandrel/issues/5325)) ([#5328](https://github.com/dsj1984/mandrel/issues/5328)) ([c4032b6](https://github.com/dsj1984/mandrel/commit/c4032b63a63c4cb8cc3f9c9956b6c646e5acd460))
38
+
18
39
  ## [2.57.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.56.0...mandrel-v2.57.0) (2026-09-12)
19
40
 
20
41
 
@@ -4,8 +4,11 @@
4
4
  *
5
5
  * Exports an ordered array of check objects each shaped `{ name, run() }`.
6
6
  * `run()` returns `{ ok, detail, remedy? }` — `remedy` is present and
7
- * non-empty only when `ok` is false. The registry is the single source of
8
- * truth for which checks the doctor command runs and in what order.
7
+ * non-empty whenever `ok` is false, and on an `advisory` check that passes
8
+ * while still having something actionable to say (`test-credit-path`), which
9
+ * repeats it in `detail` because that is the field the doctor prints for a
10
+ * passing check. The registry is the single source of truth for which checks
11
+ * the doctor command runs and in what order.
9
12
  *
10
13
  * Checks run sequentially in the doctor runner (not in parallel) because
11
14
  * some checks are meaningless without a prerequisite having passed first
@@ -38,6 +41,8 @@ import {
38
41
  } from '../../.agents/scripts/lib/bootstrap/project-bootstrap.js';
39
42
  import { isCommandExcluded } from '../../.agents/scripts/lib/command-header.js';
40
43
  import { getDeliveryRouting } from '../../.agents/scripts/lib/config/delivery-routing.js';
44
+ import { isResolvable } from '../../.agents/scripts/lib/runtime-deps/dep-resolution.js';
45
+ import { describeParserMajorError } from '../../.agents/scripts/lib/runtime-deps/parser-major.js';
41
46
  import {
42
47
  defaultResolvePackageRoot,
43
48
  listFiles as listPayloadFiles,
@@ -516,6 +521,30 @@ function runAgentsInSync({
516
521
  // check: runtime-deps
517
522
  // ---------------------------------------------------------------------------
518
523
 
524
+ /**
525
+ * The verdict when every required package is present.
526
+ *
527
+ * Presence is not the whole contract: `.agents/` resolves from the consumer's
528
+ * node_modules, so a declared range cannot enforce which `@babel/parser` major
529
+ * the complexity kernel actually gets, and the wrong major fails scoring
530
+ * mid-scan with an opaque plugin-list error. Reporting it here puts it where a
531
+ * consumer is already looking for what to fix.
532
+ *
533
+ * @param {() => string|null} parserMajorError
534
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
535
+ */
536
+ function allPresentVerdict(parserMajorError) {
537
+ const parserProblem = parserMajorError();
538
+ if (parserProblem === null) {
539
+ return { ok: true, detail: 'all dependencies found' };
540
+ }
541
+ return {
542
+ ok: false,
543
+ detail: 'dependency version unsupported',
544
+ remedy: parserProblem,
545
+ };
546
+ }
547
+
519
548
  /**
520
549
  * Verify that the framework's required runtime dependencies are resolvable
521
550
  * from the project's node_modules.
@@ -533,6 +562,8 @@ function runAgentsInSync({
533
562
  * is missing.
534
563
  * - `manifestRequired` — array of required package names, skips the
535
564
  * filesystem read of `runtime-deps.json`.
565
+ * - `parserMajorError()` — replaces the resolved-parser-major probe, so the
566
+ * unsupported-major report is assertable without installing one.
536
567
  *
537
568
  * @param {{ projectRoot?: string, resolve?: (dep: string) => string, manifestRequired?: string[] }} [opts]
538
569
  * @returns {{ ok: boolean, detail: string, remedy?: string }}
@@ -541,6 +572,7 @@ function runRuntimeDeps({
541
572
  projectRoot,
542
573
  resolve: resolveSeam,
543
574
  manifestRequired,
575
+ parserMajorError = describeParserMajorError,
544
576
  } = {}) {
545
577
  // Anchor at process.cwd() (the consumer root), not resolveProjectRoot() (the
546
578
  // package root). Under pnpm isolated-mode the consumer's node_modules are not
@@ -566,33 +598,24 @@ function runRuntimeDeps({
566
598
 
567
599
  const missing = [];
568
600
 
569
- if (resolveSeam) {
570
- for (const dep of required) {
571
- try {
572
- resolveSeam(dep);
573
- } catch {
574
- missing.push(dep);
575
- }
576
- }
577
- } else {
578
- // Anchor resolution to the consumer project root so it mirrors the context
579
- // in which the framework scripts run (they free-ride on the consumer's
580
- // node_modules). Under pnpm isolated-mode the consumer's node_modules are
581
- // not reachable from inside node_modules/mandrel/; anchoring at process.cwd()
582
- // finds them correctly.
583
- const req = createRequire(path.join(root, 'package.json'));
584
- for (const dep of required) {
585
- try {
586
- req.resolve(dep);
587
- } catch {
588
- missing.push(dep);
589
- }
590
- }
601
+ // Anchor resolution to the consumer project root so it mirrors the context
602
+ // in which the framework scripts run (they free-ride on the consumer's
603
+ // node_modules). Under pnpm isolated-mode the consumer's node_modules are
604
+ // not reachable from inside node_modules/mandrel/; anchoring at process.cwd()
605
+ // finds them correctly.
606
+ //
607
+ // `isResolvable` is shared with the framework-side preflight rather than
608
+ // reimplemented here: it probes the bare specifier AND `<name>/package.json`,
609
+ // because a dependency with no `main` and no `exports` cannot be resolved by
610
+ // name at all. Probing only the bare name reports such a package missing
611
+ // while it sits installed, and this check gates `mandrel doctor`.
612
+ const resolve =
613
+ resolveSeam ?? createRequire(path.join(root, 'package.json')).resolve;
614
+ for (const dep of required) {
615
+ if (!isResolvable(dep, resolve)) missing.push(dep);
591
616
  }
592
617
 
593
- if (missing.length === 0) {
594
- return { ok: true, detail: 'all dependencies found' };
595
- }
618
+ if (missing.length === 0) return allPresentVerdict(parserMajorError);
596
619
  return {
597
620
  ok: false,
598
621
  detail: `missing: ${missing.join(', ')}`,
@@ -1176,6 +1199,90 @@ function probeConfiguredDriver(command, runner, projectRoot) {
1176
1199
  };
1177
1200
  }
1178
1201
 
1202
+ // ---------------------------------------------------------------------------
1203
+ // check: test-credit-path
1204
+ // ---------------------------------------------------------------------------
1205
+
1206
+ /**
1207
+ * The runner whose own green full run deposits the close `test` credit as a
1208
+ * side effect (`.agents/scripts/run-tests.js` → `lib/test-run-credit.js`).
1209
+ */
1210
+ const MANDREL_TEST_RUNNER = 'run-tests.js';
1211
+
1212
+ /**
1213
+ * The deposit that works whatever `npm test` resolves to: it spawns the
1214
+ * project's own suite and stamps the result, so it is honest by construction.
1215
+ */
1216
+ const TEST_CREDIT_DEPOSIT_COMMAND =
1217
+ 'node .agents/scripts/evidence-gate.js --standalone --scope-id <storyId> --gate test --worktree <workCwd> -- npm test';
1218
+
1219
+ /** Remedy shared by every shape that does not earn the credit on its own. */
1220
+ const TEST_CREDIT_REMEDY = `run the suite through the depositor instead of bare \`npm test\`: ${TEST_CREDIT_DEPOSIT_COMMAND}`;
1221
+
1222
+ /**
1223
+ * The project's `test` script — what `npm test` runs, and therefore what the
1224
+ * close `test` gate spawns. Deliberately read from `package.json` rather than
1225
+ * `project.commands.test`: the close gate's argv is the literal `npm test`, so
1226
+ * the npm script is the command whose shape decides whether a bare run
1227
+ * deposits anything.
1228
+ *
1229
+ * @param {string} projectRoot
1230
+ * @param {typeof fs.readFileSync} readFileImpl
1231
+ * @returns {string|null} The trimmed script, or null when there is none.
1232
+ */
1233
+ function readProjectTestScript(projectRoot, readFileImpl) {
1234
+ try {
1235
+ const raw = readFileImpl(path.join(projectRoot, 'package.json'), 'utf8');
1236
+ const script = JSON.parse(raw)?.scripts?.test;
1237
+ return typeof script === 'string' && script.trim().length > 0
1238
+ ? script.trim()
1239
+ : null;
1240
+ } catch {
1241
+ return null;
1242
+ }
1243
+ }
1244
+
1245
+ /**
1246
+ * Report whether this project's test command reaches mandrel's own runner,
1247
+ * and therefore whether a bare `npm test` deposits the close `test` credit
1248
+ * by itself (Story #5324).
1249
+ *
1250
+ * **Always `ok: true`.** A project on `vitest`, `jest` or any other runner is
1251
+ * a supported setup, not a broken install — the only thing it lacks is the
1252
+ * runner-side bonus deposit, and the remedy command covers it. So the check
1253
+ * informs rather than gates, and (unlike every other entry here) carries its
1254
+ * `remedy` alongside a passing verdict; the same guidance also rides in
1255
+ * `detail`, which is the field the doctor prints for a passing check.
1256
+ *
1257
+ * The runner is detected by name, never by executing anything: a script that
1258
+ * reaches `run-tests.js` indirectly reads as "own runner", which errs toward
1259
+ * naming the deposit command — correct on either shape.
1260
+ *
1261
+ * @param {{ projectRoot?: string, readFile?: typeof fs.readFileSync }} [opts]
1262
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
1263
+ */
1264
+ function runTestCreditPath({ projectRoot, readFile = fs.readFileSync } = {}) {
1265
+ const script = readProjectTestScript(projectRoot ?? process.cwd(), readFile);
1266
+ if (script === null) {
1267
+ return {
1268
+ ok: true,
1269
+ detail: `no \`test\` script in package.json — close still spawns \`npm test\`, so ${TEST_CREDIT_REMEDY}`,
1270
+ remedy: TEST_CREDIT_REMEDY,
1271
+ };
1272
+ }
1273
+ if (script.includes(MANDREL_TEST_RUNNER)) {
1274
+ return {
1275
+ ok: true,
1276
+ detail: `\`npm test\` → \`${script}\` reaches mandrel's runner — a green full run on a story branch deposits the close \`test\` credit itself`,
1277
+ };
1278
+ }
1279
+ return {
1280
+ ok: true,
1281
+ detail: `\`npm test\` → \`${script}\` is this project's own runner and never reaches \`${MANDREL_TEST_RUNNER}\`, so it deposits no close \`test\` credit and prints nothing — ${TEST_CREDIT_REMEDY}`,
1282
+ remedy: TEST_CREDIT_REMEDY,
1283
+ };
1284
+ }
1285
+
1179
1286
  /**
1180
1287
  * Ordered array of doctor checks. Each entry follows the
1181
1288
  * `{ name: string, run(opts?): { ok: boolean, detail: string, remedy?: string } }` contract.
@@ -1243,6 +1350,15 @@ export const registry = [
1243
1350
  advisory: true,
1244
1351
  run: (opts) => runVersionCurrent(opts),
1245
1352
  },
1353
+ {
1354
+ name: 'test-credit-path',
1355
+ // Non-fatal: `run()` always returns ok:true. A project on its own test
1356
+ // runner is a supported setup that simply has to deposit the close
1357
+ // `test` credit explicitly, so this reports a condition rather than
1358
+ // failing the install (Story #5324).
1359
+ advisory: true,
1360
+ run: (opts) => runTestCreditPath(opts),
1361
+ },
1246
1362
  ];
1247
1363
 
1248
1364
  export default registry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.57.0",
3
+ "version": "2.59.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",
@@ -109,12 +109,17 @@
109
109
  "typescript": "^6.0.3"
110
110
  },
111
111
  "dependencies": {
112
+ "@babel/parser": "^7.29.3",
112
113
  "ajv": "^8.20.0",
113
114
  "ajv-formats": "^3.0.1",
115
+ "babel-runtime": "^6.26.0",
116
+ "escomplex-plugin-metrics-module": "^0.1.0",
117
+ "escomplex-plugin-syntax-babylon": "^0.1.0",
114
118
  "js-yaml": "^4.3.2",
115
119
  "minimatch": "^10.0.0",
116
120
  "picomatch": "^4.0.4",
117
- "typhonjs-escomplex": "^0.1.0"
121
+ "typhonjs-ast-walker": "^0.2.1",
122
+ "typhonjs-escomplex-commons": "^0.1.1"
118
123
  },
119
124
  "peerDependencies": {
120
125
  "@cucumber/gherkin": ">=32.0.0",
@@ -1,188 +0,0 @@
1
- /**
2
- * v2 split-policy validator — the plan-time "one-owner-AC" split rejector.
3
- *
4
- * Under the v2 default-single split policy (`docs/roadmap.md` § v2.0.0), a
5
- * plan authors **one Story by default**; it splits into N>1 Stories only when
6
- * the pieces have near-zero overlap or sit across an architectural seam. This
7
- * validator is the deterministic guardrail on that policy: **every acceptance
8
- * criterion must belong to exactly one Story.** An identical AC appearing in
9
- * two Stories is evidence the split coupled what should have stayed one Story,
10
- * so the plan is refused rather than reconciled at delivery time (there is no
11
- * epic-level acceptance reconcile in v2).
12
- *
13
- * Scope of the deterministic check:
14
- * - **Cross-Story duplication** (always): the same normalized AC text must
15
- * not appear in more than one Story.
16
- * - **Full coverage** (optional, when a plan-level `acceptance` manifest is
17
- * supplied): every manifest AC is claimed by exactly one Story, and no
18
- * Story claims an AC absent from the manifest.
19
- *
20
- * Semantic overlap between differently-worded ACs is **not** caught here — it
21
- * is a gate-#2 review call. This validator only sees identical (normalized)
22
- * text, keeping it deterministic and false-positive-free.
23
- *
24
- * Normalization for comparison: trim, collapse internal whitespace, and
25
- * lower-case. Reporting always uses the first-seen original text.
26
- */
27
-
28
- /**
29
- * Normalize an acceptance string for equality comparison. Trims, collapses
30
- * runs of whitespace to a single space, and lower-cases. Non-strings and
31
- * empty/whitespace-only strings normalize to `null` (ignored).
32
- *
33
- * @param {unknown} ac
34
- * @returns {string | null}
35
- */
36
- export function normalizeAcceptance(ac) {
37
- if (typeof ac !== 'string') return null;
38
- const norm = ac.trim().replace(/\s+/g, ' ').toLowerCase();
39
- return norm === '' ? null : norm;
40
- }
41
-
42
- /**
43
- * @typedef {object} StorySlice
44
- * @property {string} [id] Story identifier for reporting (slug or #id).
45
- * @property {string} [slug] Alternate identifier (used when `id` absent).
46
- * @property {string[]} acceptance Acceptance criteria this Story claims.
47
- */
48
-
49
- /**
50
- * @typedef {object} SplitPolicyViolation
51
- * @property {'cross-story-duplicate'|'orphan-ac'|'unclaimed-manifest-ac'} kind
52
- * @property {string} acceptance The original (first-seen) AC text.
53
- * @property {string[]} [stories] Story ids sharing a duplicated AC (`cross-story-duplicate`).
54
- * @property {string} [story] Story id owning an AC absent from the manifest (`orphan-ac`).
55
- */
56
-
57
- /**
58
- * Resolve a Story's reporting id.
59
- *
60
- * @param {StorySlice} story
61
- * @param {number} index
62
- * @returns {string}
63
- */
64
- function storyId(story, index) {
65
- if (story && typeof story.id === 'string' && story.id.trim() !== '') {
66
- return story.id;
67
- }
68
- if (story && typeof story.slug === 'string' && story.slug.trim() !== '') {
69
- return story.slug;
70
- }
71
- return `story[${index}]`;
72
- }
73
-
74
- /**
75
- * Validate that acceptance criteria partition cleanly across Stories.
76
- *
77
- * @param {StorySlice[]} stories The plan's Stories, each with `acceptance[]`.
78
- * @param {object} [opts]
79
- * @param {string[]} [opts.planAcceptance] Optional plan-level acceptance
80
- * manifest. When supplied, coverage is enforced (every manifest AC claimed
81
- * exactly once; no Story claims an off-manifest AC).
82
- * @returns {{ ok: boolean, violations: SplitPolicyViolation[] }}
83
- */
84
- export function validateAcceptancePartition(stories, opts = {}) {
85
- const violations = [];
86
- const list = Array.isArray(stories) ? stories : [];
87
-
88
- // normalized AC → { original, owners: Set<storyId> }
89
- const owners = new Map();
90
- list.forEach((story, index) => {
91
- const id = storyId(story, index);
92
- const acceptance = Array.isArray(story?.acceptance) ? story.acceptance : [];
93
- for (const ac of acceptance) {
94
- const norm = normalizeAcceptance(ac);
95
- if (norm === null) continue;
96
- const existing = owners.get(norm);
97
- if (existing) {
98
- existing.owners.add(id);
99
- } else {
100
- owners.set(norm, { original: ac.trim(), owners: new Set([id]) });
101
- }
102
- }
103
- });
104
-
105
- // Cross-Story duplication: any AC owned by more than one Story.
106
- for (const { original, owners: set } of owners.values()) {
107
- if (set.size > 1) {
108
- violations.push({
109
- kind: 'cross-story-duplicate',
110
- acceptance: original,
111
- stories: [...set],
112
- });
113
- }
114
- }
115
-
116
- // Optional coverage check against a plan-level manifest.
117
- const manifest = Array.isArray(opts.planAcceptance)
118
- ? opts.planAcceptance
119
- : null;
120
- if (manifest !== null) {
121
- const manifestNorms = new Map();
122
- for (const ac of manifest) {
123
- const norm = normalizeAcceptance(ac);
124
- if (norm !== null && !manifestNorms.has(norm)) {
125
- manifestNorms.set(norm, ac.trim());
126
- }
127
- }
128
- // Every manifest AC must be claimed by exactly one Story.
129
- for (const [norm, original] of manifestNorms) {
130
- if (!owners.has(norm)) {
131
- violations.push({
132
- kind: 'unclaimed-manifest-ac',
133
- acceptance: original,
134
- });
135
- }
136
- }
137
- // No Story may claim an AC absent from the manifest.
138
- for (const [norm, { original, owners: set }] of owners) {
139
- if (!manifestNorms.has(norm)) {
140
- violations.push({
141
- kind: 'orphan-ac',
142
- acceptance: original,
143
- story: [...set][0],
144
- });
145
- }
146
- }
147
- }
148
-
149
- return { ok: violations.length === 0, violations };
150
- }
151
-
152
- /**
153
- * Render a single violation as a human-readable line.
154
- *
155
- * @param {SplitPolicyViolation} v
156
- * @returns {string}
157
- */
158
- function formatViolation(v) {
159
- switch (v.kind) {
160
- case 'cross-story-duplicate':
161
- return `acceptance criterion appears in ${v.stories.length} Stories (${v.stories.join(', ')}) — a coupled split; keep it one Story: "${v.acceptance}"`;
162
- case 'unclaimed-manifest-ac':
163
- return `plan acceptance criterion is claimed by no Story: "${v.acceptance}"`;
164
- case 'orphan-ac':
165
- return `Story ${v.story} claims an acceptance criterion absent from the plan manifest: "${v.acceptance}"`;
166
- default:
167
- return `unknown split-policy violation: "${v.acceptance}"`;
168
- }
169
- }
170
-
171
- /**
172
- * Throwing wrapper for the persist path: throws a single batched error when
173
- * the acceptance criteria do not partition cleanly, otherwise returns
174
- * `stories` unchanged. Wired into `plan-persist` in Stage 3.
175
- *
176
- * @param {StorySlice[]} stories
177
- * @param {object} [opts] See {@link validateAcceptancePartition}.
178
- * @returns {StorySlice[]}
179
- */
180
- export function assertAcceptancePartition(stories, opts = {}) {
181
- const { ok, violations } = validateAcceptancePartition(stories, opts);
182
- if (ok) return stories;
183
- throw new Error(
184
- `[split-policy] ${violations.length} acceptance-partition violation(s) — the plan splits coupled work; refuse:\n${violations
185
- .map((v) => ` - ${formatViolation(v)}`)
186
- .join('\n')}`,
187
- );
188
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * spec-author-prompts.js — Story-scoped Spec / Acceptance authoring prompts.
3
- *
4
- * v2 keeps a single executable document per Story. These prompts used to
5
- * author a separate Epic Tech Spec + Acceptance Spec that were folded into
6
- * the Epic body and then restated on Stories — a duplication source. They
7
- * now author only Story `## Spec` approach prose and remind authors that
8
- * acceptance lives once on the Story (`acceptance[]` / `## Acceptance`).
9
- */
10
-
11
- /**
12
- * @returns {string}
13
- */
14
- export function renderTechSpecSystemPrompt() {
15
- return TECH_SPEC_SYSTEM_PROMPT;
16
- }
17
-
18
- /**
19
- * @returns {string}
20
- */
21
- export function renderAcceptanceSpecSystemPrompt() {
22
- return ACCEPTANCE_SPEC_SYSTEM_PROMPT;
23
- }
24
-
25
- const TECH_SPEC_SYSTEM_PROMPT = `You are an expert Engineering Architect.
26
- Your job is to author the Story's \`## Spec\` approach section — the technical
27
- how for one cohesive, executable Story. There is no separate Epic Tech Spec
28
- document and no spill-to-docs path.
29
-
30
- The Spec should outline only what an implementing agent needs beyond Goal /
31
- Changes / Acceptance:
32
- 1. Architecture & Design (approach, seams, reuse)
33
- 2. Data Models (if any)
34
- 3. API Changes (if any)
35
- 4. Core Components
36
- 5. Security & Privacy Considerations
37
-
38
- CRITICAL REQUIREMENTS:
39
- - Respond ONLY with valid Markdown suitable to paste into a Story \`## Spec\`.
40
- - Do not use top-level <h1> (# ) tags. Prefer \`##\` / \`###\` under Spec.
41
- - Do NOT restate the Story's Goal, Acceptance, Verify, Changes, or Non-Goals —
42
- those sections already live on the same Story body. Restatement is
43
- duplication and a drift risk. If a brief orientation helps, keep a
44
- \`## Technical Overview\` to 2–3 sentences naming the technical approach only.
45
- - Do NOT author a Delivery Slicing / fan-out table. If the work needs multiple
46
- independent Stories, say so in one short note and stop — oversized Specs
47
- mean the Story should be split, not documented elsewhere.
48
- - Format architectural decisions clearly with bullet points.
49
- - Keep the Spec lean enough to stay inline on the Story (persist rejects
50
- over-budget Specs; they are never written under docs/).`;
51
-
52
- const ACCEPTANCE_SPEC_SYSTEM_PROMPT = `You are an expert Acceptance Engineer.
53
- Your job is to help author the Story's binding acceptance contract — not a
54
- separate Acceptance Spec document.
55
-
56
- v2 rule: acceptance lives **once**, on the Story:
57
- - Machine contract: top-level \`acceptance[]\` (and \`verify[]\`) on the ticket JSON
58
- - Human document: the same items rendered under \`## Acceptance\` / \`## Verify\`
59
- on the Story body (persist syncs top-level into the body)
60
-
61
- Do **not** author an Epic Acceptance Table, PRD restatement, or a second
62
- criteria list inside \`## Spec\`.
63
-
64
- CRITICAL REQUIREMENTS:
65
- - Respond ONLY with guidance or a draft \`acceptance[]\` / \`verify[]\` list for
66
- one Story — never a parallel "Acceptance Spec" markdown artifact.
67
- - Every acceptance item MUST be observable from outside the agent (command
68
- exits 0, file exists, selector resolves, fixture count matches). Reject
69
- vague "matches the spec" / "looks good" items.
70
- - Every verify entry MUST name a tier in parentheses: unit | contract | e2e |
71
- validate (or \`manual:<reason>\` when genuinely unverifiable in isolation).
72
- - Do NOT re-elaborate Goal or Spec prose inside acceptance items — bind the
73
- outcome, not the approach.
74
- - Acceptance Outcomes MUST NOT prescribe a commit subject that begins with a
75
- non-Conventional-Commits prefix (allowed leading types: feat|fix|chore|
76
- refactor|perf|docs|style|test|build|ci|revert).`;