mandrel 2.1.0 → 2.2.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 (40) hide show
  1. package/.agents/agents/acceptance-critic.md +11 -2
  2. package/.agents/agents/story-worker.md +4 -2
  3. package/.agents/docs/SDLC.md +11 -4
  4. package/.agents/docs/configuration.md +1 -1
  5. package/.agents/docs/quality-gates.md +3 -3
  6. package/.agents/rules/gherkin-standards.md +10 -0
  7. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  8. package/.agents/schemas/agentrc.schema.json +1 -1
  9. package/.agents/scripts/acceptance-eval.js +2 -2
  10. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  11. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  12. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  13. package/.agents/scripts/lib/orchestration/code-review.js +24 -35
  14. package/.agents/scripts/lib/orchestration/plan-context.js +2 -9
  15. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +17 -16
  16. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +28 -15
  17. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +0 -25
  18. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  19. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  20. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  21. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +97 -255
  22. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  23. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  24. package/.agents/scripts/lib/story-body/story-body.js +75 -8
  25. package/.agents/scripts/lib/templates/decomposer-prompts.js +8 -13
  26. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  27. package/.agents/scripts/plan-context.js +0 -1
  28. package/.agents/scripts/plan-critics.js +203 -0
  29. package/.agents/scripts/quality-preview.js +13 -6
  30. package/.agents/scripts/stories-wave-tick.js +307 -55
  31. package/.agents/workflows/deliver.md +50 -15
  32. package/.agents/workflows/helpers/acceptance-self-eval.md +14 -5
  33. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -4
  34. package/.agents/workflows/helpers/code-review.md +2 -2
  35. package/.agents/workflows/helpers/deliver-story.md +22 -6
  36. package/.agents/workflows/plan.md +55 -0
  37. package/docs/CHANGELOG.md +22 -0
  38. package/lib/migrations/index.js +6 -1
  39. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  40. package/package.json +2 -2
@@ -15,8 +15,10 @@ read the same numbers from here so a "high cyclomatic complexity" finding in
15
15
 
16
16
  Run [`npm run quality:preview`](../../../package.json) before committing
17
17
  on any Story that touches production source. The preview runs
18
- `quality-preview.js` with `--changed-since HEAD`, which exercises the
19
- same maintainability and CRAP engines (`escomplex` + `c8` coverage) that
18
+ `quality-preview.js`, which scopes the diff to `HEAD` by default (the
19
+ alias passes no `--changed-since`; the script defaults to `HEAD`) and
20
+ exercises the same maintainability and CRAP engines (`escomplex` +
21
+ `c8` coverage) that
20
22
  `check-baselines.js` enforces at merge time, then merges the results
21
23
  into a single per-file delta table. A clean preview means the commit
22
24
  will not bounce off the unified baselines gate. The `.husky/pre-commit`
@@ -66,8 +68,9 @@ must-refactor ceiling — any drop past it is treated as a regression that
66
68
  must be undone or offset, not absorbed. Set `tolerance` higher only when the
67
69
  project deliberately wants a looser MI-drop budget.
68
70
 
69
- `quality:preview --changed-since HEAD` shows the per-file MI delta in the
70
- working tree before the commit lands.
71
+ `quality:preview` shows the per-file MI delta in the working tree before
72
+ the commit lands (scoped to `HEAD` by default — the alias passes no
73
+ `--changed-since`; the script defaults to `HEAD`).
71
74
 
72
75
  ## Rename = baseline-refresh
73
76
 
@@ -30,7 +30,7 @@ the change set is reviewed by a process the maker cannot influence. The
30
30
  enforcing code path is
31
31
  [`runStoryScopeReview`](../../scripts/lib/orchestration/single-story-close/phases/code-review.js)
32
32
  → shared
33
- [`runStoryReviewCore`](../../scripts/lib/orchestration/story-close/phases/code-review.js).
33
+ [`runStoryReviewCore`](../../scripts/lib/orchestration/story-close/phases/review-core.js).
34
34
  A future refactor MUST preserve this isolation: do not move Story-scope
35
35
  review into the maker's context or run it as a step of the delivering
36
36
  child.
@@ -122,7 +122,7 @@ The pipeline will:
122
122
  ### Step 1a — Story-scope local-lens pass (`scope: story` only, Epic #4405)
123
123
 
124
124
  When `scope === 'story'`, the shared review spine
125
- [`runStoryReviewCore`](../../scripts/lib/orchestration/story-close/phases/code-review.js)
125
+ [`runStoryReviewCore`](../../scripts/lib/orchestration/story-close/phases/review-core.js)
126
126
  runs a **shift-left local-lens pass** in the same close subprocess, *before*
127
127
  returning the review envelope. It:
128
128
 
@@ -205,12 +205,28 @@ Story-path specifics:
205
205
  Per-Story ceremony is selected by `delivery.routing.ceremonyProfile`
206
206
  (`minimal` | `standard` | `strict`, default `standard`) and the Story's
207
207
  **derived change level** — not a planner-authored verdict (Story #4542 retired
208
- that). Derive the level with
209
- [`deriveChangeLevel`](../../scripts/lib/orchestration/review-depth.js) over the
210
- Story's changed files (`git diff --name-only main...story-<id>`): a diff
211
- touching a sensitive path registered in `.agents/schemas/audit-rules.json`
212
- derives `high`, one touching none derives `low`, and an unenumerable diff
213
- derives `null`.
208
+ that).
209
+
210
+ **Compute the change set once** (Story #4593) with the shared enumerator
211
+ [`computeChangeSet`](../../scripts/lib/orchestration/change-set.js) — the same
212
+ module close uses and reuse that one list for everything downstream:
213
+
214
+ ```bash
215
+ node --input-type=module -e '
216
+ import { computeChangeSet } from "<main-repo>/.agents/scripts/lib/orchestration/change-set.js";
217
+ const { files } = computeChangeSet({ baseRef: "main", headRef: "story-<storyId>" });
218
+ console.log(JSON.stringify(files));
219
+ '
220
+ ```
221
+
222
+ Then derive the level with
223
+ [`deriveChangeLevel`](../../scripts/lib/orchestration/review-depth.js) over that
224
+ list: a diff touching a sensitive path registered in
225
+ `.agents/schemas/audit-rules.json` derives `high`, one touching none derives
226
+ `low`, and an unenumerable diff (`files === null`) derives `null`. Hand the
227
+ **same** list to every acceptance critic you spawn (Step 1a) — a critic that
228
+ re-ran its own `git diff` could score against a different set than the one that
229
+ routed it.
214
230
 
215
231
  Resolve fresh-vs-inline acceptance critics per AC-cluster with
216
232
  [`resolveCeremonyForRisk`](../../scripts/lib/orchestration/ceremony-routing.js)
@@ -141,6 +141,61 @@ total by default — an authored map is the only thing that can say
141
141
  `#4525-#4528 → #4530` while `#4529 → #4531`, which a blanket "superseded by
142
142
  this plan-run" reference could not.
143
143
 
144
+ ### 2.5 Critics
145
+
146
+ Evaluate the critic-dispatch conditions against the authored draft — here,
147
+ **before** persist, because this is the last point where a finding can still
148
+ be folded into a re-author round rather than into live issues:
149
+
150
+ ```bash
151
+ node .agents/scripts/plan-critics.js \
152
+ --stories temp/plan-<slug>/stories.json \
153
+ [--tech-spec temp/plan-<slug>/techspec.md]
154
+ ```
155
+
156
+ It prints a verdict on stdout and exits 0 on **any** verdict — the verdict
157
+ routes work, it does not gate the run. It exits **1** only on a usage/IO
158
+ error (an unreadable or malformed `--stories` / `--tech-spec` path). That is
159
+ not an advisory "proceed": no critic ran and no skip was ledgered, so **do
160
+ not proceed to Persist** — fix the path and re-run:
161
+
162
+ ```jsonc
163
+ {
164
+ "consolidation": { "critic": "consolidation", "dispatch": false, "reasons": ["…"] },
165
+ "premortem": { "critic": "pre-mortem", "dispatch": true, "reasons": ["…"] },
166
+ "textHygiene": { "critic": "text-hygiene", "findings": [] }
167
+ }
168
+ ```
169
+
170
+ The verdict's third entry, `textHygiene`, is advisory-only (Story #4599): it
171
+ carries deterministic body lints (`dangling-citation` / `open-question` /
172
+ `slicing-mass`) with no dispatch semantics — it spawns nothing and never
173
+ gates the run. Fold `textHygiene.findings[]` into the re-author round the
174
+ same way critic findings fold in: fix each named defect in `stories.json`
175
+ (anchor or inline the citation, resolve the question into a declarative
176
+ assumption, thin the Slicing checkpoint) and re-run this step. Empty
177
+ `findings` add nothing to the round.
178
+
179
+ - **Both `dispatch: false`** — proceed straight to Persist. The conditions
180
+ provably have nothing for a critic to find, and each skip is recorded on the
181
+ plan-metrics ledger so under-firing stays auditable.
182
+ - **Either `dispatch: true`** — dispatch **one fresh-context sub-agent per
183
+ firing critic** (a generic sub-agent), then fold its findings into the
184
+ Gate #2 view or a re-author round before persist. Each critic is
185
+ **maker-blind**: hand it the draft artifacts (`stories.json`, and
186
+ `techspec.md` when present) plus its charter below — never the authoring
187
+ transcript or the reasons the planner believed its own draft is sound. A
188
+ critic that reads the maker's case grades the case, not the draft.
189
+ - `consolidation` — the draft's shape: Stories that should be one cohesive
190
+ slice, a slice split per-module rather than per-capability, and
191
+ `depends_on` edges that disagree with the Delivery Slicing table.
192
+ - `pre-mortem` — assume the plan shipped and failed: name the most likely
193
+ failure modes and what the draft would have to say to prevent them.
194
+
195
+ Fold what survives back into `stories.json` and re-run this step. Findings are
196
+ advisory input to the operator's Gate #2 decision, not an automatic re-author
197
+ mandate.
198
+
144
199
  ### 3. Persist
145
200
 
146
201
  **Gate #2** — when the operator passed `--force-review`, STOP for approval of
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.2.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.1.0...mandrel-v2.2.0) (2026-07-17)
6
+
7
+
8
+ ### Added
9
+
10
+ * **deliver:** probe live state per beat, retiring hand-maintained --done/--in-flight (refs [#4594](https://github.com/dsj1984/mandrel/issues/4594)) ([#4597](https://github.com/dsj1984/mandrel/issues/4597)) ([96b2ef6](https://github.com/dsj1984/mandrel/commit/96b2ef6ce5f0526de2e5033e8cdb0ca02f6d5d89))
11
+ * **plan:** add deterministic text-hygiene lints to the critic gate and codify authoring conventions (refs [#4599](https://github.com/dsj1984/mandrel/issues/4599)) ([#4607](https://github.com/dsj1984/mandrel/issues/4607)) ([8794459](https://github.com/dsj1984/mandrel/commit/87944593f8ba5c304cdbaef40a4d3e4430e5d71d))
12
+ * **plan:** retire the [@epic](https://github.com/epic)-&lt;id&gt;-ac-N Gherkin AC tag namespace (refs [#4604](https://github.com/dsj1984/mandrel/issues/4604)) ([#4608](https://github.com/dsj1984/mandrel/issues/4608)) ([7388c58](https://github.com/dsj1984/mandrel/commit/7388c581d43e10a26c538e19a15897d2b5adafa7))
13
+ * rewire the /plan critics into a live pre-persist gate ([#4592](https://github.com/dsj1984/mandrel/issues/4592)) ([#4598](https://github.com/dsj1984/mandrel/issues/4598)) ([349367d](https://github.com/dsj1984/mandrel/commit/349367d46bfbe59be5ce56952bca46cac8e5828c))
14
+ * **story-body:** render numbered ACs, humanized path bullets, and visible wide rationale (refs [#4600](https://github.com/dsj1984/mandrel/issues/4600)) ([#4606](https://github.com/dsj1984/mandrel/issues/4606)) ([843779e](https://github.com/dsj1984/mandrel/commit/843779e2730832a1d7a6e081a0a18b40dd621981))
15
+
16
+
17
+ ### Fixed
18
+
19
+ * fix probe mode's incomplete live classification: double-dispatch window and blocked-Story wedge ([#4601](https://github.com/dsj1984/mandrel/issues/4601)) ([#4605](https://github.com/dsj1984/mandrel/issues/4605)) ([0875b6a](https://github.com/dsj1984/mandrel/commit/0875b6af857fcfadc423e4e3731d792dc2a5fe60))
20
+ * **story-close:** close [#4593](https://github.com/dsj1984/mandrel/issues/4593)'s single-enumeration leak and root-cause the un-fired MI gate ([#4610](https://github.com/dsj1984/mandrel/issues/4610)) ([1edf5a6](https://github.com/dsj1984/mandrel/commit/1edf5a67ceaf868f8faae92aec1c6280f191d33f))
21
+
22
+
23
+ ### Changed
24
+
25
+ * **orchestration:** compute the Story change-set once and thread it through delivery (refs [#4593](https://github.com/dsj1984/mandrel/issues/4593)) ([#4595](https://github.com/dsj1984/mandrel/issues/4595)) ([d116015](https://github.com/dsj1984/mandrel/commit/d1160150836e8bf7107fac24189cde258ea93458))
26
+
5
27
  ## [2.1.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.0.0...mandrel-v2.1.0) (2026-07-17)
6
28
 
7
29
 
@@ -54,6 +54,7 @@
54
54
 
55
55
  import { retireMiDropKnobs } from './steps/2.1.0-retire-mi-drop-knobs.js';
56
56
  import { retireVerifyConcurrencyCap } from './steps/2.1.0-retire-verify-concurrency-cap.js';
57
+ import { retireEpicAcTags } from './steps/2.2.0-retire-epic-ac-tags.js';
57
58
 
58
59
  /**
59
60
  * Ordered registry of migration steps. MUST stay sorted ascending by
@@ -66,7 +67,11 @@ import { retireVerifyConcurrencyCap } from './steps/2.1.0-retire-verify-concurre
66
67
  * apply: (ctx: unknown) => void,
67
68
  * }>}
68
69
  */
69
- export const migrations = [retireMiDropKnobs, retireVerifyConcurrencyCap];
70
+ export const migrations = [
71
+ retireMiDropKnobs,
72
+ retireVerifyConcurrencyCap,
73
+ retireEpicAcTags,
74
+ ];
70
75
 
71
76
  /**
72
77
  * Parse a dotted semver-ish string into a numeric tuple for comparison.
@@ -0,0 +1,154 @@
1
+ // lib/migrations/steps/2.2.0-retire-epic-ac-tags.js
2
+ /**
3
+ * Story #4604 — strip the retired `@epic-<id>-ac-N` Gherkin AC tag namespace
4
+ * from consumer feature files.
5
+ *
6
+ * The v2 Epic removal deleted `acceptance-spec-reconciler.js`, the only
7
+ * consumer of the namespaced per-Epic AC tags, and the `/plan` authoring
8
+ * prompt no longer mandates them. Surviving tags in consumer `.feature`
9
+ * files are inert and violate the gherkin-standards tag taxonomy's
10
+ * no-ad-hoc-tags rule, so this step removes them: each `@epic-<digits>-ac-<digits>`
11
+ * token is deleted from tag lines, a tag line left with no tags is dropped
12
+ * entirely, and every other tag and line is preserved byte-for-byte. Files
13
+ * with no stale tags are never rewritten.
14
+ */
15
+
16
+ import nodeFs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ /**
20
+ * Mirror of `CANONICAL_FEATURE_ROOTS` in
21
+ * `.agents/scripts/lib/bdd-runner-detect.js`. Duplicated deliberately:
22
+ * `lib/` runs from the installed npm package inside a consumer project and
23
+ * must not import from the materialized `.agents/` payload.
24
+ */
25
+ const CANONICAL_FEATURE_ROOTS = Object.freeze([
26
+ 'tests/features',
27
+ 'features',
28
+ 'test/features',
29
+ ]);
30
+
31
+ const EPIC_AC_TAG_RE = /@epic-\d+-ac-\d+/;
32
+
33
+ /**
34
+ * A Gherkin tag line: optional indentation followed by one or more
35
+ * whitespace-separated `@tag` tokens and nothing else.
36
+ */
37
+ const TAG_LINE_RE = /^(\s*)(@\S+(?:\s+@\S+)*)\s*$/;
38
+
39
+ /**
40
+ * Recursively collect `.feature` file paths under `root`.
41
+ *
42
+ * @param {string} root
43
+ * @param {typeof nodeFs} fsImpl
44
+ * @returns {string[]}
45
+ */
46
+ function collectFeatureFiles(root, fsImpl) {
47
+ /** @type {string[]} */
48
+ const found = [];
49
+ /** @type {string[]} */
50
+ const queue = [root];
51
+ while (queue.length > 0) {
52
+ const dir = queue.pop();
53
+ /** @type {import('node:fs').Dirent[]} */
54
+ let entries;
55
+ try {
56
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
57
+ } catch {
58
+ continue;
59
+ }
60
+ for (const entry of entries) {
61
+ const full = path.join(dir, entry.name);
62
+ if (entry.isDirectory()) {
63
+ queue.push(full);
64
+ } else if (entry.isFile() && entry.name.endsWith('.feature')) {
65
+ found.push(full);
66
+ }
67
+ }
68
+ }
69
+ return found.sort();
70
+ }
71
+
72
+ /**
73
+ * @param {unknown} ctx
74
+ * @param {typeof nodeFs} fsImpl
75
+ * @returns {string[]} Absolute paths of every `.feature` file under the
76
+ * canonical feature roots that exist in the consumer tree.
77
+ */
78
+ function resolveFeatureFiles(ctx, fsImpl) {
79
+ const projectRoot = ctx?.projectRoot ?? process.cwd();
80
+ return CANONICAL_FEATURE_ROOTS.flatMap((root) =>
81
+ collectFeatureFiles(path.join(projectRoot, root), fsImpl),
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Strip retired `@epic-<id>-ac-<n>` tokens from one file's content.
87
+ * Only tag lines are touched; a tag line whose every tag was retired is
88
+ * dropped. Returns the original string when nothing matched.
89
+ *
90
+ * @param {string} content
91
+ * @returns {string}
92
+ */
93
+ function stripEpicAcTags(content) {
94
+ if (!EPIC_AC_TAG_RE.test(content)) return content;
95
+ const newline = content.includes('\r\n') ? '\r\n' : '\n';
96
+ const lines = content.split(newline);
97
+ /** @type {string[]} */
98
+ const out = [];
99
+ for (const line of lines) {
100
+ const match = line.match(TAG_LINE_RE);
101
+ if (!match || !EPIC_AC_TAG_RE.test(line)) {
102
+ out.push(line);
103
+ continue;
104
+ }
105
+ const [, indent, tagBlock] = match;
106
+ const kept = tagBlock
107
+ .split(/\s+/)
108
+ .filter((tag) => !EPIC_AC_TAG_RE.test(tag));
109
+ if (kept.length === 0) continue;
110
+ out.push(`${indent}${kept.join(' ')}`);
111
+ }
112
+ return out.join(newline);
113
+ }
114
+
115
+ export const retireEpicAcTags = {
116
+ version: '2.2.0',
117
+ description:
118
+ 'strip retired @epic-<id>-ac-N Gherkin AC tags from feature files ' +
119
+ '(their reconciler consumer was deleted in the v2 Epic removal)',
120
+ /**
121
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
122
+ * @returns {boolean}
123
+ */
124
+ detect(ctx) {
125
+ const fsImpl = ctx?.fs ?? nodeFs;
126
+ return resolveFeatureFiles(ctx, fsImpl).some((file) => {
127
+ try {
128
+ return EPIC_AC_TAG_RE.test(fsImpl.readFileSync(file, 'utf8'));
129
+ } catch {
130
+ return false;
131
+ }
132
+ });
133
+ },
134
+ /**
135
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
136
+ * @returns {void}
137
+ */
138
+ apply(ctx) {
139
+ const fsImpl = ctx?.fs ?? nodeFs;
140
+ for (const file of resolveFeatureFiles(ctx, fsImpl)) {
141
+ /** @type {string} */
142
+ let content;
143
+ try {
144
+ content = fsImpl.readFileSync(file, 'utf8');
145
+ } catch {
146
+ continue;
147
+ }
148
+ const stripped = stripEpicAcTags(content);
149
+ if (stripped !== content) {
150
+ fsImpl.writeFileSync(file, stripped);
151
+ }
152
+ }
153
+ },
154
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.1.0",
3
+ "version": "2.2.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/",
@@ -45,7 +45,7 @@
45
45
  "crap:update": "node .agents/scripts/update-crap-baseline.js",
46
46
  "duplication:check": "node .agents/scripts/check-baselines.js --gate duplication",
47
47
  "duplication:update": "node .agents/scripts/update-duplication-baseline.js",
48
- "quality:preview": "node .agents/scripts/quality-preview.js --changed-since HEAD && node .agents/scripts/check-dead-exports.js",
48
+ "quality:preview": "node .agents/scripts/check-dead-exports.js && node .agents/scripts/quality-preview.js",
49
49
  "quality:watch": "node .agents/scripts/quality-watch.js",
50
50
  "sync:commands": "node bin/mandrel.js sync-commands",
51
51
  "sync:agents": "node .agents/scripts/sync-claude-agents.js",