mandrel 2.36.0 → 2.37.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 (88) hide show
  1. package/.agents/docs/configuration.md +25 -0
  2. package/.agents/rules/testing-standards.md +56 -0
  3. package/.agents/scripts/boot-sweep.js +7 -1
  4. package/.agents/scripts/check-context-budget.js +195 -103
  5. package/.agents/scripts/check-cyclomatic.js +112 -42
  6. package/.agents/scripts/check-generated-validator.js +202 -0
  7. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  8. package/.agents/scripts/check-schema-references.js +2 -28
  9. package/.agents/scripts/drain-pending-cleanup.js +18 -1
  10. package/.agents/scripts/lib/baselines/components.js +32 -2
  11. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  12. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  13. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  14. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  15. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  16. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  17. package/.agents/scripts/lib/cli-args.js +0 -2
  18. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  19. package/.agents/scripts/lib/close-validation/process.js +1 -1
  20. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  21. package/.agents/scripts/lib/config-settings-schema.js +46 -6
  22. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  23. package/.agents/scripts/lib/cpu-pool.js +90 -10
  24. package/.agents/scripts/lib/crap-utils.js +6 -2
  25. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  26. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  27. package/.agents/scripts/lib/dependency-version.js +86 -0
  28. package/.agents/scripts/lib/duplicate-search.js +1 -1
  29. package/.agents/scripts/lib/errors/index.js +1 -1
  30. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  31. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  32. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  33. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  34. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  35. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  36. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  37. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  38. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
  39. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  40. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  41. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  42. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  43. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  44. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  45. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  46. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  47. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  48. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  49. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  50. package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
  51. package/.agents/scripts/lib/preflight-runner.js +3 -3
  52. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  53. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  54. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  55. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  56. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  57. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  58. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  59. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  60. package/.agents/scripts/lib/test-tiers.js +135 -18
  61. package/.agents/scripts/lib/transpile.js +16 -3
  62. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
  63. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  64. package/.agents/scripts/pr-watch-with-update.js +305 -137
  65. package/.agents/scripts/providers/github/tickets.js +109 -16
  66. package/.agents/scripts/quality-preview.js +162 -70
  67. package/.agents/scripts/run-test-profile.js +8 -5
  68. package/.agents/scripts/run-tests.js +79 -14
  69. package/.agents/scripts/single-story-close.js +0 -2
  70. package/.agents/scripts/single-story-init.js +5 -1
  71. package/docs/CHANGELOG.md +32 -0
  72. package/lib/cli/migrate.js +100 -94
  73. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  74. package/lib/migrations/index.js +32 -33
  75. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  76. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  77. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  78. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  79. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  80. package/package.json +7 -3
  81. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  82. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  83. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  84. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  85. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  86. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  87. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  88. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -1,18 +1,11 @@
1
1
  // lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js
2
2
  /**
3
- * Story #4545retire `delivery.deliverRunner.verifyConcurrencyCap`.
4
- *
5
- * The knob was schema-validated, defaulted to 4, resolved by `getRunners`,
6
- * and documented as bounding "the per-wave verifyWaveResults loop" a loop
7
- * that does not exist anywhere in the tree. Its only reader was
8
- * `analyze-execution.js`, which echoed the number into a perf report rather
9
- * than bounding anything; that CLI is deleted in the same Story. Setting the
10
- * knob to any value only ever changed a number printed in a report.
11
- *
12
- * `delivery.deliverRunner` declares `additionalProperties: false`, so a
13
- * consumer whose config still carries the key hits a hard AJV validation
14
- * failure on upgrade — every script that resolves config, not just the one
15
- * that read it. This step strips it before that check runs.
3
+ * Story #4531 — `delivery.deliverRunner.verifyConcurrencyCap` was retired
4
+ * when verification stopped being separately capped, but it stayed
5
+ * schema-validated in consumers' configs under a block carrying
6
+ * `additionalProperties: false`, so a config still setting it fails AJV
7
+ * validation on upgrade rather than warning. This step strips it before
8
+ * that check runs.
16
9
  *
17
10
  * Unlike the 2.1.0 mi-drop step, this one also sweeps `.agentrc.local.json`:
18
11
  * the resolver deep-merges the local override over `.agentrc.json` and
@@ -20,82 +13,17 @@
20
13
  * same way a key in the committed file does.
21
14
  */
22
15
 
23
- import nodeFs from 'node:fs';
24
- import path from 'node:path';
25
-
26
- const AGENTRC_FILENAMES = ['.agentrc.json', '.agentrc.local.json'];
27
-
28
- /**
29
- * @param {unknown} ctx
30
- * @param {string} filename
31
- * @returns {string}
32
- */
33
- function resolveAgentrcPath(ctx, filename) {
34
- const projectRoot = ctx?.projectRoot ?? process.cwd();
35
- return path.join(projectRoot, filename);
36
- }
37
-
38
- /**
39
- * @param {unknown} ctx
40
- * @param {string} filename
41
- * @param {typeof nodeFs} fsImpl
42
- * @returns {object | null}
43
- */
44
- function readAgentrcConfig(ctx, filename, fsImpl) {
45
- try {
46
- const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx, filename), 'utf8');
47
- return JSON.parse(raw);
48
- } catch {
49
- return null;
50
- }
51
- }
16
+ import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
52
17
 
53
- /**
54
- * @param {object | null} config
55
- * @returns {boolean}
56
- */
57
- function hasRetiredKey(config) {
58
- const deliverRunner = config?.delivery?.deliverRunner;
59
- return (
60
- Boolean(deliverRunner) &&
61
- Object.hasOwn(deliverRunner, 'verifyConcurrencyCap')
62
- );
63
- }
64
-
65
- export const retireVerifyConcurrencyCap = {
18
+ export const retireVerifyConcurrencyCap = createRetireAgentrcKeyStep({
66
19
  version: '2.1.0',
67
20
  description:
68
21
  'strip retired delivery.deliverRunner.verifyConcurrencyCap from ' +
69
22
  '.agentrc.json and .agentrc.local.json',
70
- /**
71
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
72
- * @returns {boolean}
73
- */
74
- detect(ctx) {
75
- const fsImpl = ctx?.fs ?? nodeFs;
76
- return AGENTRC_FILENAMES.some((filename) =>
77
- hasRetiredKey(readAgentrcConfig(ctx, filename, fsImpl)),
78
- );
79
- },
80
- /**
81
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
82
- * @returns {void}
83
- */
84
- apply(ctx) {
85
- const fsImpl = ctx?.fs ?? nodeFs;
86
- for (const filename of AGENTRC_FILENAMES) {
87
- const config = readAgentrcConfig(ctx, filename, fsImpl);
88
- if (!hasRetiredKey(config)) continue;
89
-
90
- delete config.delivery.deliverRunner.verifyConcurrencyCap;
91
- if (Object.keys(config.delivery.deliverRunner).length === 0) {
92
- delete config.delivery.deliverRunner;
93
- }
94
-
95
- fsImpl.writeFileSync(
96
- resolveAgentrcPath(ctx, filename),
97
- `${JSON.stringify(config, null, 2)}\n`,
98
- );
99
- }
100
- },
101
- };
23
+ keys: [
24
+ {
25
+ path: ['delivery', 'deliverRunner', 'verifyConcurrencyCap'],
26
+ pruneDepth: 1,
27
+ },
28
+ ],
29
+ });
@@ -12,81 +12,23 @@
12
12
  * `maxSeedWords` hits a hard validation failure on upgrade, not a
13
13
  * warning. This step strips the key before that check runs — the same
14
14
  * contract-cutover pattern as `2.1.0-retire-mi-drop-knobs.js`.
15
+ *
16
+ * `planning` is an optional block, so an emptied `complexityGate` and an
17
+ * emptied `planning` are both pruned rather than left as orphan objects.
15
18
  */
16
19
 
17
- import nodeFs from 'node:fs';
18
- import path from 'node:path';
19
-
20
- const AGENTRC_FILENAME = '.agentrc.json';
21
-
22
- /**
23
- * @param {unknown} ctx
24
- * @returns {string}
25
- */
26
- function resolveAgentrcPath(ctx) {
27
- const projectRoot = ctx?.projectRoot ?? process.cwd();
28
- return path.join(projectRoot, AGENTRC_FILENAME);
29
- }
30
-
31
- /**
32
- * @param {unknown} ctx
33
- * @param {typeof nodeFs} fsImpl
34
- * @returns {object | null}
35
- */
36
- function readAgentrcConfig(ctx, fsImpl) {
37
- try {
38
- const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx), 'utf8');
39
- return JSON.parse(raw);
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- /**
46
- * @param {object | null} config
47
- * @returns {boolean}
48
- */
49
- function hasRetiredKey(config) {
50
- const gate = config?.planning?.complexityGate;
51
- return Boolean(gate) && Object.hasOwn(gate, 'maxSeedWords');
52
- }
20
+ import {
21
+ AGENTRC_BASE_FILENAME,
22
+ createRetireAgentrcKeyStep,
23
+ } from '../helpers/retire-agentrc-key.js';
53
24
 
54
- export const retireMaxSeedWords = {
25
+ export const retireMaxSeedWords = createRetireAgentrcKeyStep({
55
26
  version: '2.11.0',
56
27
  description:
57
28
  'strip retired planning.complexityGate.maxSeedWords from .agentrc.json ' +
58
29
  '(complexity routes on Story shape, never seed word count — Story #4722)',
59
- /**
60
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
61
- * @returns {boolean}
62
- */
63
- detect(ctx) {
64
- const fsImpl = ctx?.fs ?? nodeFs;
65
- return hasRetiredKey(readAgentrcConfig(ctx, fsImpl));
66
- },
67
- /**
68
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
69
- * @returns {void}
70
- */
71
- apply(ctx) {
72
- const fsImpl = ctx?.fs ?? nodeFs;
73
- const config = readAgentrcConfig(ctx, fsImpl);
74
- if (!config) return;
75
-
76
- const gate = config.planning?.complexityGate;
77
- if (gate && Object.hasOwn(gate, 'maxSeedWords')) {
78
- delete gate.maxSeedWords;
79
- if (Object.keys(gate).length === 0) {
80
- delete config.planning.complexityGate;
81
- if (Object.keys(config.planning).length === 0) {
82
- delete config.planning;
83
- }
84
- }
85
- }
86
-
87
- fsImpl.writeFileSync(
88
- resolveAgentrcPath(ctx),
89
- `${JSON.stringify(config, null, 2)}\n`,
90
- );
91
- },
92
- };
30
+ filenames: [AGENTRC_BASE_FILENAME],
31
+ keys: [
32
+ { path: ['planning', 'complexityGate', 'maxSeedWords'], pruneDepth: 2 },
33
+ ],
34
+ });
@@ -1,113 +1,25 @@
1
1
  // lib/migrations/steps/2.20.0-retire-codebase-snapshot.js
2
2
  /**
3
3
  * Story #4811 — strip the retired `planning.codebaseSnapshot` block from a
4
- * consumer's `.agentrc.json` / `.agentrc.local.json`.
5
- *
6
- * #4811 hard-cutover-removed the `/plan` codebase snapshot: the pre-computed
7
- * structural view grounded nothing it promised (its default include globs
8
- * missed the standard monorepo layout, and its knobs only re-filtered the same
9
- * matched set), and spec authoring is grounded instead by the author's own
10
- * targeted repo retrieval plus the Phase 8 `validateStoryFileAssumptions`
11
- * gate. The whole block was dropped from the runtime AJV schema and the
12
- * published mirror. `planning` carries `additionalProperties: false`, so a
13
- * consumer whose config still sets `codebaseSnapshot` hits a hard validation
14
- * failure on upgrade, not a warning. This step strips the key before that
15
- * check runs — the same contract-cutover pattern as
16
- * `2.11.0-retire-max-seed-words.js`, widened to the local override file
17
- * because a snapshot narrowed for one checkout is exactly the kind of knob an
4
+ * consumer's `.agentrc.json` and from the `.agentrc.local.json` overlay an
18
5
  * operator pins in `.agentrc.local.json`.
19
- */
20
-
21
- import nodeFs from 'node:fs';
22
- import path from 'node:path';
23
-
24
- /**
25
- * Both config files the resolver deep-merges. A key surviving in either one
26
- * fails validation, so both are stripped.
27
- */
28
- const AGENTRC_FILENAMES = ['.agentrc.json', '.agentrc.local.json'];
29
-
30
- const RETIRED_KEY = 'codebaseSnapshot';
31
-
32
- /**
33
- * @param {unknown} ctx
34
- * @param {string} filename
35
- * @returns {string}
36
- */
37
- function resolveAgentrcPath(ctx, filename) {
38
- const projectRoot = ctx?.projectRoot ?? process.cwd();
39
- return path.join(projectRoot, filename);
40
- }
41
-
42
- /**
43
- * @param {unknown} ctx
44
- * @param {string} filename
45
- * @param {typeof nodeFs} fsImpl
46
- * @returns {object | null}
47
- */
48
- function readAgentrcConfig(ctx, filename, fsImpl) {
49
- try {
50
- const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx, filename), 'utf8');
51
- return JSON.parse(raw);
52
- } catch {
53
- return null;
54
- }
55
- }
56
-
57
- /**
58
- * @param {object | null} config
59
- * @returns {boolean}
60
- */
61
- function hasRetiredKey(config) {
62
- const planning = config?.planning;
63
- return Boolean(planning) && Object.hasOwn(planning, RETIRED_KEY);
64
- }
65
-
66
- /**
67
- * Strip the key and prune the containers it emptied, so a config that carried
68
- * nothing but the snapshot does not keep an orphan `planning: {}`.
69
6
  *
70
- * @param {object} config
71
- * @returns {void}
7
+ * Spec authoring is grounded by targeted retrieval plus the Phase 8
8
+ * file-assumption gate, so the snapshot block has no consumer; the block
9
+ * carries `additionalProperties: false`, so a config still setting it fails
10
+ * validation on upgrade rather than warning.
11
+ *
12
+ * `planning` is optional, so a config that carried nothing but the snapshot
13
+ * does not keep an orphan `planning: {}`.
72
14
  */
73
- function stripRetiredKey(config) {
74
- delete config.planning[RETIRED_KEY];
75
- if (Object.keys(config.planning).length === 0) {
76
- delete config.planning;
77
- }
78
- }
79
15
 
80
- export const retireCodebaseSnapshot = {
16
+ import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
17
+
18
+ export const retireCodebaseSnapshot = createRetireAgentrcKeyStep({
81
19
  version: '2.20.0',
82
20
  description:
83
21
  'strip retired planning.codebaseSnapshot from .agentrc.json / ' +
84
22
  '.agentrc.local.json (spec authoring is grounded by targeted retrieval ' +
85
23
  'plus the Phase 8 file-assumption gate — Story #4811)',
86
- /**
87
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
88
- * @returns {boolean}
89
- */
90
- detect(ctx) {
91
- const fsImpl = ctx?.fs ?? nodeFs;
92
- return AGENTRC_FILENAMES.some((filename) =>
93
- hasRetiredKey(readAgentrcConfig(ctx, filename, fsImpl)),
94
- );
95
- },
96
- /**
97
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
98
- * @returns {void}
99
- */
100
- apply(ctx) {
101
- const fsImpl = ctx?.fs ?? nodeFs;
102
- for (const filename of AGENTRC_FILENAMES) {
103
- const config = readAgentrcConfig(ctx, filename, fsImpl);
104
- if (!hasRetiredKey(config)) continue;
105
-
106
- stripRetiredKey(config);
107
- fsImpl.writeFileSync(
108
- resolveAgentrcPath(ctx, filename),
109
- `${JSON.stringify(config, null, 2)}\n`,
110
- );
111
- }
112
- },
113
- };
24
+ keys: [{ path: ['planning', 'codebaseSnapshot'], pruneDepth: 1 }],
25
+ });
@@ -1,19 +1,9 @@
1
1
  // lib/migrations/steps/2.32.0-retire-lint-baseline-command.js
2
2
  /**
3
- * Story #5004 follow-up — strip the retired `project.commands.lintBaseline`
4
- * key from a consumer's `.agentrc.json`.
5
- *
6
- * #5004 (PR #5019) deleted `lint-baseline.js` and
7
- * `lib/orchestration/lint-baseline-service.js`: the CLI spawned the configured
8
- * command and parsed ESLint-shaped JSON, a shape this repo's own `npm run
9
- * lint` (Biome + markdownlint fan-out) never produced, and nothing invoked the
10
- * service. The key left the runtime AJV schema, the published mirror,
11
- * `.agentrc.json` and `agentrc-reference.json` in the same change.
12
- *
13
- * `project.commands` carries `additionalProperties: false`, so a consumer
14
- * whose config still sets `lintBaseline` hits a hard validation failure on
15
- * upgrade, not a warning. This step strips the key before that check runs —
16
- * the same contract-cutover pattern as `2.11.0-retire-max-seed-words.js`.
3
+ * Story #5004 — strip the retired `project.commands.lintBaseline` from a
4
+ * consumer's config. The framework-owned lint-baseline capture CLI is gone,
5
+ * and the `commands` block carries `additionalProperties: false`, so a
6
+ * config still naming the command fails validation on upgrade.
17
7
  *
18
8
  * It sweeps **both** config surfaces. `config-resolver.js` deep-merges
19
9
  * `.agentrc.local.json` over `.agentrc.json` and validates the result, so a
@@ -27,6 +17,11 @@
27
17
  * `baselines/lint.json`; only the framework-owned capture shell is gone, so
28
18
  * nothing here touches the baseline file or the gate config.
29
19
  *
20
+ * An emptied `commands` block is deliberately left in place (`pruneDepth: 0`):
21
+ * unlike `planning.complexityGate`, `project` is a required block and an empty
22
+ * `commands` object is valid against the schema, so pruning it would be a
23
+ * cosmetic edit to a config the consumer owns.
24
+ *
30
25
  * This step is also the carrier for the release note #5004 never emitted: its
31
26
  * commit ships the `BREAKING CHANGE:` footer that the squash subject on `main`
32
27
  * (`227e1af4`) lacks, and which release-please therefore could not surface.
@@ -34,94 +29,12 @@
34
29
  * fix that stops the next one being lost.
35
30
  */
36
31
 
37
- import nodeFs from 'node:fs';
38
- import path from 'node:path';
39
-
40
- /**
41
- * Both config surfaces the resolver reads. `.agentrc.local.json` is deep-merged
42
- * over the base *before* the AJV gate runs
43
- * (`.agents/scripts/lib/config-resolver.js`), so a `lintBaseline` surviving in
44
- * the overlay fails validation exactly as one in the base would — and a step
45
- * that swept only the base would leave that consumer hard-broken with no
46
- * self-service remedy, since re-running `mandrel update` would keep reporting
47
- * clean. The overlay is operator-owned and gitignored, which is why it is easy
48
- * to forget and why the sweep has to name it explicitly.
49
- */
50
- const AGENTRC_FILENAMES = Object.freeze([
51
- '.agentrc.json',
52
- '.agentrc.local.json',
53
- ]);
54
-
55
- /**
56
- * @param {unknown} ctx
57
- * @param {string} filename
58
- * @returns {string}
59
- */
60
- function resolveAgentrcPath(ctx, filename) {
61
- const projectRoot = ctx?.projectRoot ?? process.cwd();
62
- return path.join(projectRoot, filename);
63
- }
32
+ import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
64
33
 
65
- /**
66
- * @param {unknown} ctx
67
- * @param {typeof nodeFs} fsImpl
68
- * @param {string} filename
69
- * @returns {object | null}
70
- */
71
- function readAgentrcConfig(ctx, fsImpl, filename) {
72
- try {
73
- const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx, filename), 'utf8');
74
- return JSON.parse(raw);
75
- } catch {
76
- return null;
77
- }
78
- }
79
-
80
- /**
81
- * @param {object | null} config
82
- * @returns {boolean}
83
- */
84
- function hasRetiredKey(config) {
85
- const commands = config?.project?.commands;
86
- return Boolean(commands) && Object.hasOwn(commands, 'lintBaseline');
87
- }
88
-
89
- export const retireLintBaselineCommand = {
34
+ export const retireLintBaselineCommand = createRetireAgentrcKeyStep({
90
35
  version: '2.32.0',
91
36
  description:
92
37
  'strip retired project.commands.lintBaseline from .agentrc.json ' +
93
38
  '(the framework lint-baseline capture CLI is gone — Story #5004)',
94
- /**
95
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
96
- * @returns {boolean}
97
- */
98
- detect(ctx) {
99
- const fsImpl = ctx?.fs ?? nodeFs;
100
- return AGENTRC_FILENAMES.some((filename) =>
101
- hasRetiredKey(readAgentrcConfig(ctx, fsImpl, filename)),
102
- );
103
- },
104
- /**
105
- * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
106
- * @returns {void}
107
- */
108
- apply(ctx) {
109
- const fsImpl = ctx?.fs ?? nodeFs;
110
- for (const filename of AGENTRC_FILENAMES) {
111
- const config = readAgentrcConfig(ctx, fsImpl, filename);
112
- // An absent overlay is the common case, not an error.
113
- if (!config || !hasRetiredKey(config)) continue;
114
-
115
- delete config.project.commands.lintBaseline;
116
- // An emptied `commands` block is left in place: unlike
117
- // `planning.complexityGate`, `project` is a required block and an empty
118
- // `commands` object is valid against the schema, so pruning it would be
119
- // a cosmetic edit to a config the consumer owns.
120
-
121
- fsImpl.writeFileSync(
122
- resolveAgentrcPath(ctx, filename),
123
- `${JSON.stringify(config, null, 2)}\n`,
124
- );
125
- }
126
- },
127
- };
39
+ keys: [{ path: ['project', 'commands', 'lintBaseline'], pruneDepth: 0 }],
40
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.36.0",
3
+ "version": "2.37.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/",
@@ -19,7 +19,9 @@
19
19
  "test": "node .agents/scripts/run-tests.js",
20
20
  "test:quick": "node .agents/scripts/run-tests.js --tier quick",
21
21
  "test:integration": "node .agents/scripts/run-tests.js --tier integration",
22
+ "test:e2e": "node .agents/scripts/run-tests.js --tier e2e",
22
23
  "test:profile": "node .agents/scripts/run-test-profile.js",
24
+ "test:census": "SPAWN_CENSUS_OUT=\"${SPAWN_CENSUS_OUT:-temp/census.json}\" NODE_OPTIONS=\"--require $PWD/tests/fixtures/spawn-census.cjs\" node .agents/scripts/run-tests.js",
23
25
  "test:isolate": "node .agents/scripts/test-isolate.js",
24
26
  "verify": "node .agents/scripts/run-verify.js",
25
27
  "typecheck": "node -e \"process.exit(0)\"",
@@ -31,7 +33,7 @@
31
33
  "baselines:scope": "node .agents/scripts/check-baseline-scope.js",
32
34
  "baselines:prune": "node .agents/scripts/prune-baseline-orphans.js",
33
35
  "lint:md": "markdownlint-cli2 \".agents/**/*.md\" \"*.md\" \"!node_modules/**\" \"!.worktrees/**\"",
34
- "lint": "node .agents/scripts/run-lint.js && npm run docs:check",
36
+ "lint": "node .agents/scripts/run-lint.js && node .agents/scripts/check-generated-validator.js --check && npm run docs:check",
35
37
  "docs:gen": "node .agents/scripts/generate-config-docs.js && node .agents/scripts/generate-workflows-doc.js && node .agents/scripts/generate-lens-checklists.js",
36
38
  "docs:check": "node .agents/scripts/generate-config-docs.js --check && node .agents/scripts/generate-workflows-doc.js --check && node .agents/scripts/generate-lens-checklists.js --check && node .agents/scripts/check-doc-links.js && npm run skills:check",
37
39
  "skills:index": "node .agents/scripts/generate-skills-index.js",
@@ -47,6 +49,8 @@
47
49
  "cyclomatic:update": "node .agents/scripts/check-cyclomatic.js --update",
48
50
  "dead-exports:update": "node .agents/scripts/update-dead-exports-baseline.js && node .agents/scripts/update-dead-exports-baseline.js --production",
49
51
  "check:knip-entries": "node .agents/scripts/check-knip-entries.js",
52
+ "validator:gen": "node .agents/scripts/check-generated-validator.js",
53
+ "validator:check": "node .agents/scripts/check-generated-validator.js --check",
50
54
  "check:schema-refs": "node .agents/scripts/check-schema-references.js",
51
55
  "check:workflow-citations": "node .agents/scripts/check-workflow-citations.js",
52
56
  "crap:check": "node .agents/scripts/check-baselines.js --gate crap",
@@ -101,7 +105,7 @@
101
105
  "markdownlint-cli2": "^0.22.1",
102
106
  "memfs": "^4.57.7",
103
107
  "node-pty": "^1.0.0",
104
- "typescript": ">=5.0.0"
108
+ "typescript": "^6.0.3"
105
109
  },
106
110
  "dependencies": {
107
111
  "ajv": "^8.20.0",
@@ -1,32 +0,0 @@
1
- /**
2
- * lib/audit-suite/frontmatter-lint.js — workflow frontmatter linter.
3
- *
4
- * Lives next to `frontmatter.js` but stays in its own module so adding
5
- * lints doesn't drag the summary helper's maintainability score down.
6
- * Pure: no IO, no provider calls, safe to unit-test in isolation.
7
- *
8
- * Story #1324, Epic #1185 — Dispatch performance pass.
9
- *
10
- * Story #2824, Epic #2815 — Model-hint frontmatter was removed from
11
- * every workflow and from the validator's field list. The function is
12
- * retained as a no-op safety net so callers (and any future frontmatter
13
- * lints) keep a stable entry point.
14
- */
15
-
16
- import { extractFrontmatter } from './frontmatter.js';
17
-
18
- /**
19
- * Pure: lint a frontmatter map (or raw workflow content). Currently a
20
- * no-op — no frontmatter fields are validated after the model-hint
21
- * removal. The signature is preserved so future field-level lints can
22
- * slot in without churning callers.
23
- *
24
- * @param {string | Record<string, string>} input
25
- * @returns {{ ok: boolean, errors: Array<{ field: string, value: string, message: string }> }}
26
- */
27
- export function validateFrontmatter(input) {
28
- // Extract so callers still pay the same "is this parseable?" cost as
29
- // before; we just have no field-level rules to enforce right now.
30
- if (typeof input === 'string') extractFrontmatter(input);
31
- return { ok: true, errors: [] };
32
- }
@@ -1,37 +0,0 @@
1
- import path from 'node:path';
2
- import {
3
- write as writeBaselineEnvelope,
4
- writeFile as writeBaselineFile,
5
- } from './writer.js';
6
-
7
- /**
8
- * Saves a new maintainability baseline to disk at `baselinePath`.
9
- *
10
- * Accepts the legacy flat `{ path: mi }` shape for backwards compatibility
11
- * with existing callers (`regenerateMainFromTree`, refresh helpers). The
12
- * map is transformed into the canonical envelope shape (`$schema`,
13
- * `kernelVersion`, `generatedAt`, `rollup`, `rows`) via the shared
14
- * `lib/baselines/writer.js` pipeline before being persisted, so every
15
- * write produces a file that round-trips through `lib/baselines/reader.js`
16
- * without schema errors.
17
- *
18
- * @param {Record<string, number>} baseline path→MI flat map.
19
- * @param {string} baselinePath Required — caller supplies via getBaselines().
20
- */
21
- export function saveBaseline(baseline, baselinePath) {
22
- if (typeof baselinePath !== 'string' || baselinePath.length === 0) {
23
- throw new TypeError(
24
- 'maintainability-utils.saveBaseline: baselinePath is required.',
25
- );
26
- }
27
- const abs = path.isAbsolute(baselinePath)
28
- ? baselinePath
29
- : path.resolve(process.cwd(), baselinePath);
30
-
31
- const rows = Object.entries(baseline ?? {}).map(([p, mi]) => ({
32
- path: p,
33
- mi,
34
- }));
35
- const envelope = writeBaselineEnvelope({ kind: 'maintainability', rows });
36
- writeBaselineFile(abs, envelope);
37
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * parse-numeric.js — shared CLI integer-flag validators (Story #2993).
3
- *
4
- * Extracted from `lifecycle-emit-story-dispatch.js` so future scripts that
5
- * need a strict positive-integer or non-negative-integer flag value share
6
- * a single validator instead of hand-rolling another `Number.parseInt`
7
- * dance. Both helpers throw with a clear "required" / "must be …" message
8
- * shape so a `runAsCli` boundary surfaces a clean exit-1.
9
- *
10
- * Contract:
11
- * - Empty / `null` / `undefined` raw values throw "is required".
12
- * - Non-integer or out-of-range values throw "must be a … integer".
13
- * - A leading `#` on the raw value is stripped (matches the ticket-ID
14
- * ergonomic on the rest of the framework).
15
- *
16
- * @module lib/cli/parse-numeric
17
- */
18
-
19
- /**
20
- * Parse a flag value as a strict positive integer (≥ 1). Leading `#` is
21
- * stripped to match the `parseTicketId` ergonomic.
22
- *
23
- * @param {string|number|null|undefined} raw
24
- * @param {string} flag Flag label used in the thrown error message.
25
- * @param {string} [tool='cli'] Optional tool prefix for the error message.
26
- * @returns {number}
27
- */
28
- export function parseRequiredPositiveInt(raw, flag, tool = 'cli') {
29
- if (raw === undefined || raw === null || raw === '') {
30
- throw new Error(`${tool}: ${flag} is required`);
31
- }
32
- const n = Number.parseInt(String(raw).replace(/^#/, ''), 10);
33
- if (!Number.isInteger(n) || n < 1) {
34
- throw new Error(`${tool}: ${flag} must be a positive integer (got ${raw})`);
35
- }
36
- return n;
37
- }
38
-
39
- /**
40
- * Parse a flag value as a strict non-negative integer (≥ 0). Leading `#`
41
- * is NOT stripped — non-negative use cases (wave index, attempt offset)
42
- * do not carry the ticket-ID convention.
43
- *
44
- * @param {string|number|null|undefined} raw
45
- * @param {string} flag
46
- * @param {string} [tool='cli']
47
- * @returns {number}
48
- */
49
- export function parseRequiredNonNegativeInt(raw, flag, tool = 'cli') {
50
- if (raw === undefined || raw === null || raw === '') {
51
- throw new Error(`${tool}: ${flag} is required`);
52
- }
53
- const n = Number.parseInt(String(raw), 10);
54
- if (!Number.isInteger(n) || n < 0) {
55
- throw new Error(
56
- `${tool}: ${flag} must be a non-negative integer (got ${raw})`,
57
- );
58
- }
59
- return n;
60
- }