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.
- package/.agents/README.md +6 -3
- package/.agents/agents/story-worker.md +12 -11
- package/.agents/docs/SDLC.md +6 -7
- package/.agents/docs/quality-gates.md +1 -1
- package/.agents/instructions.md +2 -3
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- package/.agents/scripts/lib/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- package/.agents/scripts/lib/maintainability-engine.js +3 -3
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +41 -27
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +6 -1
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +14 -9
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +45 -31
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +8 -9
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +15 -5
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/story-body/story-body.js +36 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +73 -21
- package/.agents/scripts/lib/test-run-credit.js +23 -12
- package/.agents/scripts/plan-persist.js +0 -11
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-to-stories.md +14 -11
- package/.agents/workflows/helpers/deliver-digest.md +22 -15
- package/.agents/workflows/helpers/deliver-story-reference.md +31 -11
- package/.agents/workflows/helpers/deliver-story.md +6 -5
- package/.agents/workflows/helpers/plan-reference.md +53 -13
- package/.agents/workflows/mandrel-plan.md +19 -14
- package/README.md +3 -3
- package/docs/CHANGELOG.md +21 -0
- package/lib/cli/registry.js +143 -27
- package/package.json +7 -2
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- 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
|
|
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
|
|
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/`)
|
|
96
|
-
`techspec.md` (**N===1 only**, folded into `## Spec`)
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
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
|
|
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`
|
|
90
|
-
>
|
|
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
|
|
package/lib/cli/registry.js
CHANGED
|
@@ -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
|
|
8
|
-
*
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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.
|
|
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-
|
|
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).`;
|