mandrel 2.58.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 (42) hide show
  1. package/.agents/README.md +6 -3
  2. package/.agents/docs/SDLC.md +6 -7
  3. package/.agents/docs/quality-gates.md +1 -1
  4. package/.agents/instructions.md +2 -3
  5. package/.agents/runtime-deps.json +7 -2
  6. package/.agents/schemas/crap-baseline.schema.json +1 -1
  7. package/.agents/schemas/crap-report.schema.json +1 -1
  8. package/.agents/scripts/install-matrix-assert.js +48 -3
  9. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
  10. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
  11. package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
  12. package/.agents/scripts/lib/crap-engine.js +2 -2
  13. package/.agents/scripts/lib/crap-utils.js +21 -5
  14. package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
  15. package/.agents/scripts/lib/escomplex-kernel.js +298 -0
  16. package/.agents/scripts/lib/maintainability-engine.js +3 -3
  17. package/.agents/scripts/lib/orchestration/plan-context.js +31 -25
  18. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +28 -24
  19. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +8 -9
  20. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +1 -1
  21. package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
  22. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
  23. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
  24. package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
  25. package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
  26. package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
  27. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
  28. package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
  29. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  30. package/.agents/scripts/lib/templates/decomposer-prompts.js +21 -18
  31. package/.agents/scripts/plan-persist.js +0 -11
  32. package/.agents/skills/skills.index.json +1 -11
  33. package/.agents/workflows/audit-to-stories.md +14 -11
  34. package/.agents/workflows/helpers/plan-reference.md +18 -7
  35. package/.agents/workflows/mandrel-plan.md +14 -13
  36. package/README.md +3 -3
  37. package/docs/CHANGELOG.md +8 -0
  38. package/lib/cli/registry.js +45 -25
  39. package/package.json +7 -2
  40. package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
  41. package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
  42. package/.agents/skills/core/scope-triage/SKILL.md +0 -48
@@ -41,6 +41,8 @@ import {
41
41
  } from '../../.agents/scripts/lib/bootstrap/project-bootstrap.js';
42
42
  import { isCommandExcluded } from '../../.agents/scripts/lib/command-header.js';
43
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';
44
46
  import {
45
47
  defaultResolvePackageRoot,
46
48
  listFiles as listPayloadFiles,
@@ -519,6 +521,30 @@ function runAgentsInSync({
519
521
  // check: runtime-deps
520
522
  // ---------------------------------------------------------------------------
521
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
+
522
548
  /**
523
549
  * Verify that the framework's required runtime dependencies are resolvable
524
550
  * from the project's node_modules.
@@ -536,6 +562,8 @@ function runAgentsInSync({
536
562
  * is missing.
537
563
  * - `manifestRequired` — array of required package names, skips the
538
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.
539
567
  *
540
568
  * @param {{ projectRoot?: string, resolve?: (dep: string) => string, manifestRequired?: string[] }} [opts]
541
569
  * @returns {{ ok: boolean, detail: string, remedy?: string }}
@@ -544,6 +572,7 @@ function runRuntimeDeps({
544
572
  projectRoot,
545
573
  resolve: resolveSeam,
546
574
  manifestRequired,
575
+ parserMajorError = describeParserMajorError,
547
576
  } = {}) {
548
577
  // Anchor at process.cwd() (the consumer root), not resolveProjectRoot() (the
549
578
  // package root). Under pnpm isolated-mode the consumer's node_modules are not
@@ -569,33 +598,24 @@ function runRuntimeDeps({
569
598
 
570
599
  const missing = [];
571
600
 
572
- if (resolveSeam) {
573
- for (const dep of required) {
574
- try {
575
- resolveSeam(dep);
576
- } catch {
577
- missing.push(dep);
578
- }
579
- }
580
- } else {
581
- // Anchor resolution to the consumer project root so it mirrors the context
582
- // in which the framework scripts run (they free-ride on the consumer's
583
- // node_modules). Under pnpm isolated-mode the consumer's node_modules are
584
- // not reachable from inside node_modules/mandrel/; anchoring at process.cwd()
585
- // finds them correctly.
586
- const req = createRequire(path.join(root, 'package.json'));
587
- for (const dep of required) {
588
- try {
589
- req.resolve(dep);
590
- } catch {
591
- missing.push(dep);
592
- }
593
- }
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);
594
616
  }
595
617
 
596
- if (missing.length === 0) {
597
- return { ok: true, detail: 'all dependencies found' };
598
- }
618
+ if (missing.length === 0) return allPresentVerdict(parserMajorError);
599
619
  return {
600
620
  ok: false,
601
621
  detail: `missing: ${missing.join(', ')}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.58.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).`;
@@ -1,48 +0,0 @@
1
- ---
2
- name: scope-triage
3
- description:
4
- Optional split-advisory for `/mandrel-plan`. Under v2 there is no epic|story routing
5
- verdict — `/mandrel-plan` always authors Stories. Use this skill only when judging
6
- whether a draft should stay one Story or legitimately split (near-zero
7
- overlap or an architectural seam).
8
- ---
9
-
10
- # scope-triage (split advisory)
11
-
12
- ## Policy Capsule
13
-
14
- - There is **no** `epic | story | borderline` routing verdict in v2. `/mandrel-plan`
15
- is a single path that emits **one Story by default**.
16
- - This skill is an optional **split advisory**: should the author keep one
17
- Story, or does the seed clear the default-single split policy?
18
- - Anchor sizing judgment to `DELIVERABLE_GRANULARITY_GUIDANCE` in
19
- [`ticket-validator-sizing.js`](../../../scripts/lib/orchestration/ticket-validator-sizing.js).
20
- There is no numeric threshold to restate — Story #5312 deleted the
21
- plan-time capacity ceilings.
22
- - Lead with **cohesion**: one Story is one coherent change with one reason
23
- to exist. Coupled work stays one Story and uses `## Slicing` for
24
- intra-session checkpoints.
25
- - Emit an **advisory only** — `keep-single` (the default) or `split` with its
26
- seam/overlap rationale. **Never auto-route**: the operator decides, and
27
- `--yes` defaults to `keep-single`.
28
-
29
- ## Split policy (when N>1 is allowed)
30
-
31
- Split into sibling Stories **only** when:
32
-
33
- 1. **Near-zero overlap** — genuinely independent capabilities that happen to
34
- share a seed idea, or
35
- 2. **Architectural seam** — different deployables, or a migration vs the
36
- feature that consumes it.
37
-
38
- Otherwise keep one Story. Persist refuses coupled splits via
39
- `assertAcceptancePartition` (identical AC text across Stories).
40
-
41
- ## Advisory output
42
-
43
- Emit one of:
44
-
45
- - `keep-single` — default; fold complexity into `## Spec` / `## Slicing`
46
- - `split` — list the proposed Stories and the seam/overlap rationale
47
-
48
- Never auto-route. The operator (or `--yes` default = `keep-single`) decides.