mandrel 2.56.0 → 2.57.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 (106) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  16. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  17. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  18. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  19. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  20. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  21. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  22. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  23. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  24. package/.agents/scripts/lib/config/explain.js +0 -19
  25. package/.agents/scripts/lib/config/limits.js +18 -78
  26. package/.agents/scripts/lib/config/quality.js +6 -3
  27. package/.agents/scripts/lib/config/runners.js +3 -2
  28. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  29. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  30. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  31. package/.agents/scripts/lib/crap-engine.js +35 -4
  32. package/.agents/scripts/lib/crap-utils.js +17 -1
  33. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  34. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  35. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  36. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  37. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  39. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  40. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  41. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  42. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  43. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  44. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  45. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  46. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  48. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
  49. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  51. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  52. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  53. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  56. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  57. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  58. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  59. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  60. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  61. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  62. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  63. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  64. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  65. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  66. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  67. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  68. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  69. package/.agents/scripts/lib/test-run-credit.js +266 -0
  70. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  71. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  72. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  73. package/.agents/scripts/plan-context.js +7 -9
  74. package/.agents/scripts/plan-critics.js +28 -54
  75. package/.agents/scripts/plan-persist.js +25 -68
  76. package/.agents/scripts/quality-preview.js +51 -0
  77. package/.agents/scripts/run-tests.js +12 -0
  78. package/.agents/scripts/stories-wave-tick.js +23 -45
  79. package/.agents/scripts/test-isolate.js +13 -180
  80. package/.agents/scripts/update-coverage-baseline.js +25 -70
  81. package/.agents/scripts/update-crap-baseline.js +19 -123
  82. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  83. package/.agents/workflows/audit-clean-code.md +4 -3
  84. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  85. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  86. package/.agents/workflows/helpers/code-review.md +2 -3
  87. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  88. package/.agents/workflows/helpers/deliver-light.md +40 -105
  89. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  90. package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
  91. package/.agents/workflows/helpers/deliver-story.md +9 -13
  92. package/.agents/workflows/helpers/plan-reference.md +132 -219
  93. package/.agents/workflows/mandrel-plan.md +27 -40
  94. package/.agents/workflows/memory-consolidate.md +9 -13
  95. package/docs/CHANGELOG.md +23 -0
  96. package/lib/migrations/index.js +4 -0
  97. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  98. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  99. package/package.json +1 -1
  100. package/.agents/scripts/lib/framework-version.js +0 -39
  101. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  102. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  103. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  104. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  105. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  106. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -0,0 +1,213 @@
1
+ /**
2
+ * context-budget-writeback.js — lock a context-budget gain in on the branch
3
+ * that earned it (Story #5313).
4
+ *
5
+ * `check-context-budget.js` used to fail a gated documentation tier that came
6
+ * in **under** its recorded total (Story #4872), so a Story that trimmed prose
7
+ * paid for the trim twice: once in the edit and once in the red gate whose
8
+ * only remedy was a hand-run `--update`. Story #5313 reverses that rule —
9
+ * shrinkage exits 0 and is reported — and this module answers the concern
10
+ * the rule existed for: a stale recorded total silently absorbs the next
11
+ * growth. When the tree the close is scoring measures under its recorded
12
+ * totals, the lower totals are written back into
13
+ * `baselines/context-budget.json` and folded into one `baseline-refresh:`
14
+ * commit on the Story branch, so the gain lands in the branch's own PR.
15
+ *
16
+ * ## Where it runs, and why not post-land
17
+ *
18
+ * The Story names the post-land tail. This step runs from the close's
19
+ * **pre-gate write-back seam** (`phases/pre-gate-steps.js`, beside the
20
+ * maintainability write-back of Story #5224) instead: the only sanctioned
21
+ * landing is the Story branch → PR → `main`, so a post-land write would have
22
+ * to commit straight to the base branch. Writing on the branch ahead of the
23
+ * gates keeps the row inside the PR the gates score and lets
24
+ * `refresh-ack.js` vouch for it through the same `baseline-refresh:` marker.
25
+ *
26
+ * Constraints, each a "must not":
27
+ *
28
+ * 1. **Only a downward move is written.** Growth past tolerance and an
29
+ * unbacked recorded row must still fail `check-context-budget.js` exactly
30
+ * as before; when either is present this step skips and the gate speaks.
31
+ * 2. **Idempotent and silent.** No shrink, no write, no commit.
32
+ * 3. **Never fails the close.** Every failure is a named skip.
33
+ */
34
+
35
+ import fs from 'node:fs';
36
+ import path from 'node:path';
37
+
38
+ import {
39
+ DEFAULT_TOLERANCE_BYTES,
40
+ buildBaseline as defaultBuildBaseline,
41
+ diffBudget as defaultDiffBudget,
42
+ loadBaseline as defaultLoadBaseline,
43
+ } from '../../../check-context-budget.js';
44
+ import { resolveDocTiers as defaultResolveDocTiers } from '../../doc-tiers.js';
45
+ import { gitSync as defaultGitSync } from '../../git-utils.js';
46
+ import { Logger as DefaultLogger } from '../../Logger.js';
47
+
48
+ const TAG = '[context-budget-writeback]';
49
+
50
+ /** Repo-relative location of the committed budget. */
51
+ const BASELINE_REL_PATH = 'baselines/context-budget.json';
52
+
53
+ /**
54
+ * Build the commit subject — conventional, carrying the `baseline-refresh:`
55
+ * marker `refresh-ack.js` recognises, fixed-length but for the Story id.
56
+ *
57
+ * @param {number|string} storyId
58
+ * @returns {string}
59
+ */
60
+ function buildCommitSubject(storyId) {
61
+ return `chore(baselines): baseline-refresh: lower context-budget totals (story #${storyId})`;
62
+ }
63
+
64
+ /**
65
+ * One line per tier that moved down, before → after.
66
+ *
67
+ * @param {Array<{ tier: string, current: number, baseline: number }>} shrunk
68
+ * @returns {string}
69
+ */
70
+ function buildCommitBody(shrunk) {
71
+ return [
72
+ 'Documentation tiers this branch trimmed under their recorded totals,',
73
+ 'written back so the budget stops holding slack the tree no longer',
74
+ 'spends. Growth and unbacked rows are never rewritten here.',
75
+ '',
76
+ ...shrunk.map((s) => `- ${s.tier}: ${s.baseline} -> ${s.current} bytes`),
77
+ ].join('\n');
78
+ }
79
+
80
+ /**
81
+ * Report a no-op by name.
82
+ *
83
+ * @param {object} logger
84
+ * @param {string} reason
85
+ */
86
+ function skip(logger, reason) {
87
+ logger.info?.(`${TAG} no write-back (${reason}).`);
88
+ return { ran: false, committed: false, reason };
89
+ }
90
+
91
+ /**
92
+ * Everything that must hold before a write. Returns a skip reason or `null`.
93
+ *
94
+ * @param {{ workTree: string, storyBranch: string, git: Function, relPath: string }} ctx
95
+ * @returns {string|null}
96
+ */
97
+ function precheck({ workTree, storyBranch, git, relPath }) {
98
+ const onBranch = git(['rev-parse', '--abbrev-ref', 'HEAD'], {
99
+ cwd: workTree,
100
+ });
101
+ if (onBranch !== storyBranch) return 'wrong-branch';
102
+ const status = git(['status', '--porcelain', '--', relPath], {
103
+ cwd: workTree,
104
+ });
105
+ if (status.length > 0) return 'dirty-tree';
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Write the envelope and fold it into one commit. On a rejected commit the
111
+ * file is restored so the gates score the tree the close found.
112
+ *
113
+ * @param {{ workTree: string, git: Function, relPath: string, envelope: object, storyId: number|string, shrunk: object[] }} args
114
+ * @returns {{ sha: string }}
115
+ */
116
+ function commitBaseline({ workTree, git, relPath, envelope, storyId, shrunk }) {
117
+ fs.writeFileSync(
118
+ path.join(workTree, relPath),
119
+ `${JSON.stringify(envelope, null, 2)}\n`,
120
+ );
121
+ git(['add', '--', relPath], { cwd: workTree });
122
+ try {
123
+ git(
124
+ [
125
+ 'commit',
126
+ '-m',
127
+ buildCommitSubject(storyId),
128
+ '-m',
129
+ buildCommitBody(shrunk),
130
+ ],
131
+ { cwd: workTree },
132
+ );
133
+ } catch (err) {
134
+ git(['restore', '--staged', '--worktree', '--', relPath], {
135
+ cwd: workTree,
136
+ });
137
+ throw err;
138
+ }
139
+ return { sha: git(['rev-parse', '--short', 'HEAD'], { cwd: workTree }) };
140
+ }
141
+
142
+ /**
143
+ * Write the lower context-budget totals back on the Story branch.
144
+ *
145
+ * Total: a throw anywhere is a named skip, never a failed close.
146
+ *
147
+ * @param {{
148
+ * cwd: string,
149
+ * worktreePath?: string|null,
150
+ * storyId: number|string,
151
+ * storyBranch?: string,
152
+ * config: object,
153
+ * logger?: object,
154
+ * gitSync?: (cwd: string, ...args: string[]) => string,
155
+ * resolveDocTiersImpl?: typeof defaultResolveDocTiers,
156
+ * loadBaselineImpl?: typeof defaultLoadBaseline,
157
+ * diffBudgetImpl?: typeof defaultDiffBudget,
158
+ * buildBaselineImpl?: typeof defaultBuildBaseline,
159
+ * }} opts
160
+ * @returns {{ ran: boolean, committed: boolean, sha?: string, tiers?: string[], reason?: string }}
161
+ */
162
+ export function runContextBudgetWriteback({
163
+ cwd,
164
+ worktreePath,
165
+ storyId,
166
+ storyBranch,
167
+ config,
168
+ logger = DefaultLogger,
169
+ gitSync = defaultGitSync,
170
+ resolveDocTiersImpl = defaultResolveDocTiers,
171
+ loadBaselineImpl = defaultLoadBaseline,
172
+ diffBudgetImpl = defaultDiffBudget,
173
+ buildBaselineImpl = defaultBuildBaseline,
174
+ } = {}) {
175
+ if (!cwd || !storyBranch) return skip(logger, 'missing-context');
176
+ const workTree = worktreePath || cwd;
177
+ const git = (args, opts = {}) => gitSync(opts.cwd ?? workTree, ...args);
178
+ try {
179
+ const blocked = precheck({
180
+ workTree,
181
+ storyBranch,
182
+ git,
183
+ relPath: BASELINE_REL_PATH,
184
+ });
185
+ if (blocked) return skip(logger, blocked);
186
+ const baseline = loadBaselineImpl(path.join(workTree, BASELINE_REL_PATH));
187
+ if (!baseline) return skip(logger, 'no-baseline');
188
+ const tierMap = resolveDocTiersImpl(config, { root: workTree });
189
+ const diff = diffBudgetImpl(tierMap, baseline);
190
+ if (diff.grown.length > 0 || diff.absent.length > 0) {
191
+ return skip(logger, 'drift-not-downward');
192
+ }
193
+ if (diff.shrunk.length === 0) return skip(logger, 'no-shrink');
194
+ const tolerance = Number.isFinite(baseline.toleranceBytes)
195
+ ? baseline.toleranceBytes
196
+ : DEFAULT_TOLERANCE_BYTES;
197
+ const { sha } = commitBaseline({
198
+ workTree,
199
+ git,
200
+ relPath: BASELINE_REL_PATH,
201
+ envelope: buildBaselineImpl(tierMap, tolerance),
202
+ storyId,
203
+ shrunk: diff.shrunk,
204
+ });
205
+ const tiers = diff.shrunk.map((s) => s.tier);
206
+ logger.warn?.(
207
+ `${TAG} wrote back lower totals for ${tiers.join(', ')} on story #${storyId}; committed as ${sha}.`,
208
+ );
209
+ return { ran: true, committed: true, sha, tiers };
210
+ } catch (err) {
211
+ return skip(logger, `failed: ${err?.message ?? err}`);
212
+ }
213
+ }
@@ -10,9 +10,8 @@
10
10
  * object. To make the rules below actually fire on canonical plans, a string
11
11
  * body is **parsed** back into its structured form via `parseStoryBody`
12
12
  * before the section checks run (Story #3906 — previously the validator
13
- * `shouldSkipTicket`-skipped every string body, so the verify-tier suffix
14
- * rule, vague-verb check, and non-empty-goal check never ran on any real
15
- * decomposition). A still-structured object body (e.g. a caller that passes
13
+ * `shouldSkipTicket`-skipped every string body, so the vague-verb check and
14
+ * non-empty-goal check never ran on any real decomposition). A still-structured object body (e.g. a caller that passes
16
15
  * the pre-serialize shape directly) is validated as-is.
17
16
  *
18
17
  * Only `type: 'story'` tickets are validated; Feature/Epic tickets and
@@ -42,44 +41,23 @@
42
41
  * array uses the same object shape and is the home for paths the Story
43
42
  * reads but does not modify (test fixtures, sibling modules, etc.).
44
43
  *
45
- * `body.verify` entries must either name a testing tier in parentheses
46
- * drawn from `VERIFY_TIER_VALUES` (e.g. `npm run test (unit)`) or be the
47
- * literal `manual:<reason>` escape hatch when the Story is genuinely
48
- * unverifiable in isolation. `verify-tier-repair.js#normalizeVerifyTiers` runs
49
- * first on the persist path (Story #5005) and **repairs** the entries whose
50
- * tier `suggestVerifyFix` can infer, so the hard error below is reserved for
51
- * the entries only the author can resolve.
44
+ * `body.verify` entries are commands, nothing more: Story #5312 deleted the
45
+ * `(<tier>)` suffix, the `manual:<reason>` escape and the repair pass that
46
+ * appended the suffix for the author. The only verify rule left is that the
47
+ * list is non-empty.
52
48
  *
53
49
  * The errors are batched and surfaced as a single thrown Error so the
54
50
  * planner can see every offending slug in one pass instead of fixing one
55
51
  * at a time.
56
52
  */
57
53
 
58
- import {
59
- suggestPathEntryFix,
60
- suggestVerifyFix,
61
- } from '../story-body/body-format-lints.js';
54
+ import { suggestPathEntryFix } from '../story-body/body-format-lints.js';
62
55
  import {
63
56
  parse as parseStoryBody,
64
57
  StoryBodyParseError,
65
58
  } from '../story-body/story-body.js';
66
59
  import { FILE_ASSUMPTION_VALUES } from './file-assumption-enum.js';
67
60
 
68
- /**
69
- * Canonical testing-tier labels that a `verify[]` entry must name (in
70
- * parentheses) to pass plan-time validation. Mirrors the verify-rules contract
71
- * in `.agents/scripts/lib/templates/decomposer-prompts.js`.
72
- *
73
- * Entries that do not end with `(<tier>)` and are not `manual:<reason>` are
74
- * rejected by `collectVerifyErrors`.
75
- */
76
- export const VERIFY_TIER_VALUES = Object.freeze([
77
- 'unit',
78
- 'contract',
79
- 'e2e',
80
- 'validate',
81
- ]);
82
-
83
61
  /**
84
62
  * Predicate: should the validator skip this ticket entirely? Skip when:
85
63
  * - it is not a Story (only `type: 'story'` tickets are validated here),
@@ -91,7 +69,7 @@ export const VERIFY_TIER_VALUES = Object.freeze([
91
69
  * Story body to a markdown string, so a *string* body is NOT skipped here
92
70
  * (Story #3906) — `validateTaskBodyShape` parses it back into structured
93
71
  * form via `parseStoryBody` before applying the section rules. This is what
94
- * makes the verify-tier / vague-verb / non-empty-goal checks actually fire
72
+ * makes the non-empty-verify / vague-verb / non-empty-goal checks actually fire
95
73
  * on real plans. Features (and everything else) use narrative string bodies
96
74
  * and are skipped by the `type !== 'story'` guard.
97
75
  *
@@ -207,7 +185,6 @@ export function validateTaskBodyShape(ticket) {
207
185
  }
208
186
  errors.push(...collectChangesErrors(prefix, body.changes));
209
187
  errors.push(...collectAcceptanceErrors(prefix, body.acceptance));
210
- // Tier-suffix validation is always enforced on Story bodies (2-tier world).
211
188
  errors.push(...collectVerifyErrors(prefix, body.verify));
212
189
  errors.push(...collectReferencesErrors(prefix, body.references));
213
190
  return errors;
@@ -324,15 +301,6 @@ function collectAcceptanceErrors(prefix, rawAcceptance) {
324
301
  return [];
325
302
  }
326
303
 
327
- /**
328
- * Regex that matches a valid tier suffix at the end of a verify entry:
329
- * a parenthesised word drawn from `VERIFY_TIER_VALUES` (e.g. `(unit)`).
330
- * Whitespace before the opening paren is tolerated.
331
- */
332
- const VERIFY_TIER_RE = new RegExp(
333
- `\\((?:${VERIFY_TIER_VALUES.join('|')})\\)\\s*$`,
334
- );
335
-
336
304
  /**
337
305
  * @param {string} prefix
338
306
  * @param {unknown} rawVerify
@@ -342,31 +310,10 @@ function collectVerifyErrors(prefix, rawVerify) {
342
310
  const verify = Array.isArray(rawVerify) ? rawVerify : [];
343
311
  if (verify.length === 0) {
344
312
  return [
345
- `${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section. Use "manual:<reason>" only when truly unverifiable in isolation.`,
313
+ `${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section.`,
346
314
  ];
347
315
  }
348
- const errors = [];
349
- for (const v of verify) {
350
- if (typeof v !== 'string') continue;
351
- if (v.startsWith('manual:')) {
352
- const reason = v.slice('manual:'.length).trim();
353
- if (reason === '') {
354
- errors.push(
355
- `${prefix}: body.verify "manual:" entry has no reason after the colon.`,
356
- );
357
- }
358
- // manual: entries are exempt from the tier-suffix check.
359
- continue;
360
- }
361
- if (!VERIFY_TIER_RE.test(v)) {
362
- const fix = suggestVerifyFix(v);
363
- const fixIt = fix === null ? '' : ` Suggested fix: "${fix}".`;
364
- errors.push(
365
- `${prefix}: body.verify entry must end with a tier in parentheses — one of (${VERIFY_TIER_VALUES.join('|')}). Got: "${v}".${fixIt}`,
366
- );
367
- }
368
- }
369
- return errors;
316
+ return [];
370
317
  }
371
318
 
372
319
  /**