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.
- package/.agents/agents/acceptance-critic.md +11 -2
- package/.agents/agents/story-worker.md +4 -2
- package/.agents/docs/SDLC.md +11 -4
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +24 -35
- package/.agents/scripts/lib/orchestration/plan-context.js +2 -9
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +17 -16
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +28 -15
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +0 -25
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +97 -255
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/story-body/story-body.js +75 -8
- package/.agents/scripts/lib/templates/decomposer-prompts.js +8 -13
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/plan-context.js +0 -1
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/quality-preview.js +13 -6
- package/.agents/scripts/stories-wave-tick.js +307 -55
- package/.agents/workflows/deliver.md +50 -15
- package/.agents/workflows/helpers/acceptance-self-eval.md +14 -5
- package/.agents/workflows/helpers/code-quality-guardrails.md +7 -4
- package/.agents/workflows/helpers/code-review.md +2 -2
- package/.agents/workflows/helpers/deliver-story.md +22 -6
- package/.agents/workflows/plan.md +55 -0
- package/docs/CHANGELOG.md +22 -0
- package/lib/migrations/index.js +6 -1
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- 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
|
|
19
|
-
|
|
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
|
|
70
|
-
|
|
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/
|
|
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/
|
|
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).
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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)-<id>-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
|
|
package/lib/migrations/index.js
CHANGED
|
@@ -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 = [
|
|
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.
|
|
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/
|
|
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",
|