mandrel 2.41.0 → 2.43.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 (39) hide show
  1. package/.agents/agents/story-worker.md +24 -14
  2. package/.agents/docs/agentrc-reference.json +11 -2
  3. package/.agents/docs/configuration.md +9 -3
  4. package/.agents/docs/workflows.md +1 -1
  5. package/.agents/schemas/agentrc.schema.json +37 -3
  6. package/.agents/schemas/validation-evidence.schema.json +3 -1
  7. package/.agents/scripts/acceptance-eval.js +68 -3
  8. package/.agents/scripts/coverage-capture.js +25 -8
  9. package/.agents/scripts/lib/baselines/crap-preview-incremental.js +7 -2
  10. package/.agents/scripts/lib/baselines/git-base.js +74 -38
  11. package/.agents/scripts/lib/close-validation/gates.js +153 -25
  12. package/.agents/scripts/lib/close-validation/process.js +30 -1
  13. package/.agents/scripts/lib/close-validation/runner.js +5 -0
  14. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +33 -12
  15. package/.agents/scripts/lib/config/quality.js +36 -21
  16. package/.agents/scripts/lib/config-settings-schema-delivery.js +6 -0
  17. package/.agents/scripts/lib/config-settings-schema.js +29 -1
  18. package/.agents/scripts/lib/coverage-capture-incremental.js +12 -6
  19. package/.agents/scripts/lib/crap-baseline-join.js +11 -7
  20. package/.agents/scripts/lib/full-suite-lock.js +311 -0
  21. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  22. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +11 -104
  23. package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +320 -0
  24. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
  25. package/.agents/scripts/lib/orchestration/plan-context.js +4 -0
  26. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +9 -1
  27. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +159 -55
  28. package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +83 -4
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +39 -7
  30. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +70 -18
  31. package/.agents/scripts/lib/orchestration/verify-credit.js +207 -0
  32. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +24 -0
  33. package/.agents/workflows/helpers/acceptance-self-eval.md +12 -0
  34. package/.agents/workflows/helpers/deliver-digest.md +31 -10
  35. package/.agents/workflows/helpers/deliver-story-reference.md +50 -30
  36. package/.agents/workflows/helpers/deliver-story.md +23 -21
  37. package/.agents/workflows/memory-consolidate.md +18 -6
  38. package/docs/CHANGELOG.md +25 -0
  39. package/package.json +1 -1
@@ -0,0 +1,207 @@
1
+ /**
2
+ * verify-credit.js — decide whether a Story `verify[]` entry has already been
3
+ * paid for by the delivery's single credited full-suite run (Story #5174).
4
+ *
5
+ * A Story's `verify[]` is meant to be *scoped* entries plus the one credited
6
+ * full-suite run the worker makes just before the hand-off push
7
+ * (`helpers/deliver-digest.md` § 5). When a `verify[]` entry is itself a
8
+ * full-suite command, running it spends a second whole-suite spawn for a
9
+ * result the credited run already established — and the close gate chain then
10
+ * makes a third. This module is the read side of that credit: given the
11
+ * entry's command it consults **the same stamp close consults** and reports
12
+ * the entry as credited instead of telling the caller to spawn it.
13
+ *
14
+ * It only ever *reads*. Nothing here writes a capture stamp or an evidence
15
+ * record — an entry that is not covered by a fresh stamp is reported
16
+ * `spawn: true` and runs for real, so the credit can never manufacture a pass.
17
+ *
18
+ * @see .agents/scripts/lib/coverage-capture.js (`isCoverageFresh`)
19
+ * @see .agents/scripts/lib/validation-evidence.js (`shouldSkip`)
20
+ */
21
+
22
+ import { getQuality, resolveConfig } from '../config-resolver.js';
23
+ import { isCoverageFresh } from '../coverage-capture.js';
24
+ import { gitSpawn } from '../git-utils.js';
25
+ import { hasNpmScript, readPackageScripts } from '../npm-scripts.js';
26
+ import { hashCommandConfig, shouldSkip } from '../validation-evidence.js';
27
+
28
+ /**
29
+ * The shape a `verify[]` array is supposed to have, stated once so the
30
+ * warning a caller surfaces and the prose in `deliver-digest.md` § 5 say the
31
+ * same thing.
32
+ * @type {string}
33
+ */
34
+ export const FULL_SUITE_SHAPE_WARNING =
35
+ 'verify[] should be scoped entries plus the single credited full-suite run ' +
36
+ '(deliver-digest.md § 5) — a full-suite command listed in verify[] is ' +
37
+ 'reported credited against that run, never respawned.';
38
+
39
+ /** Package managers whose `test` script means "the whole suite". */
40
+ const PACKAGE_MANAGERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
41
+
42
+ /** Script names that mean "the whole suite" rather than a scoped subset. */
43
+ const FULL_SUITE_SCRIPTS = new Set(['test', 'test:coverage']);
44
+
45
+ /**
46
+ * Split a Story `verify[]` line into its command and its tier tag.
47
+ *
48
+ * Story bodies write entries as `` `<command>` (<tier>) `` — the tier is
49
+ * planning metadata, not part of the command, and leaving it attached would
50
+ * make every entry look scoped.
51
+ *
52
+ * @param {string} entry
53
+ * @returns {{ command: string, tier: string|null }}
54
+ */
55
+ export function parseVerifyEntry(entry) {
56
+ const text = String(entry ?? '').trim();
57
+ const tagged = /^(.*?)\s*\(([a-z-]+)\)$/i.exec(text);
58
+ const command = (tagged ? tagged[1] : text).trim().replace(/^`|`$/g, '');
59
+ return { command: command.trim(), tier: tagged ? tagged[2] : null };
60
+ }
61
+
62
+ /**
63
+ * Is this command a whole-suite run?
64
+ *
65
+ * Deliberately narrow. A false positive here would report a *scoped* command
66
+ * as credited without ever running it, which is how a gate stops gating — so
67
+ * anything carrying its own positional argument (`npm test -- tests/x.js`,
68
+ * `node --test tests/x.js`) is scoped by construction.
69
+ *
70
+ * @param {string} command
71
+ * @returns {boolean}
72
+ */
73
+ export function isFullSuiteCommand(command) {
74
+ const tokens = String(command ?? '')
75
+ .trim()
76
+ .split(/\s+/)
77
+ .filter(Boolean);
78
+ if (tokens.length === 0) return false;
79
+
80
+ if (tokens[0] === 'node') {
81
+ // `node --test` with no path argument walks the default test globs.
82
+ const rest = tokens.slice(1);
83
+ return rest.length > 0 && rest.every((t) => t.startsWith('-'));
84
+ }
85
+
86
+ if (!PACKAGE_MANAGERS.has(tokens[0])) return false;
87
+ const rest = tokens[1] === 'run' ? tokens.slice(2) : tokens.slice(1);
88
+ if (rest.length === 0 || !FULL_SUITE_SCRIPTS.has(rest[0])) return false;
89
+ // `npm test -- <path>` narrows the run; only a bare invocation is the suite.
90
+ return rest.length === 1;
91
+ }
92
+
93
+ /**
94
+ * Read HEAD from a worktree. `null` when the tree cannot be read — which
95
+ * routes to `spawn`, never to a credit.
96
+ *
97
+ * @param {string} cwd
98
+ * @param {Function} gitSpawnFn
99
+ * @returns {string|null}
100
+ */
101
+ function readHeadSha(cwd, gitSpawnFn) {
102
+ const res = gitSpawnFn(cwd, 'rev-parse', 'HEAD');
103
+ if (res?.status !== 0) return null;
104
+ const sha = String(res.stdout ?? '').trim();
105
+ return sha.length > 0 ? sha : null;
106
+ }
107
+
108
+ /**
109
+ * Decide how a single `verify[]` entry should be executed.
110
+ *
111
+ * @param {object} input
112
+ * @param {string} input.command — the entry's command (tier tag already off).
113
+ * @param {number|string} input.storyId
114
+ * @param {string} input.worktree — ABSOLUTE path to the Story worktree.
115
+ * @param {string} [input.cwd] — main checkout (evidence keyspace root).
116
+ * Defaults to `worktree`.
117
+ * @param {object} [deps] — test seams; every one defaults to the real impl.
118
+ * @returns {{
119
+ * command: string, fullSuite: boolean, credited: boolean, spawn: boolean,
120
+ * mode: 'capture'|'evidence'|null, reason: string, warning: string|null
121
+ * }}
122
+ */
123
+ export function resolveVerifyCredit(
124
+ { command, storyId, worktree, cwd = worktree },
125
+ deps = {},
126
+ ) {
127
+ const {
128
+ resolveConfigImpl = resolveConfig,
129
+ getQualityImpl = getQuality,
130
+ readPackageScriptsImpl = readPackageScripts,
131
+ hasNpmScriptImpl = hasNpmScript,
132
+ isCoverageFreshImpl = isCoverageFresh,
133
+ shouldSkipImpl = shouldSkip,
134
+ hashCommandConfigImpl = hashCommandConfig,
135
+ gitSpawnFn = gitSpawn,
136
+ } = deps;
137
+
138
+ const base = { command, fullSuite: false, mode: null, warning: null };
139
+ if (!isFullSuiteCommand(command)) {
140
+ return { ...base, credited: false, spawn: true, reason: 'scoped' };
141
+ }
142
+
143
+ const scoped = {
144
+ ...base,
145
+ fullSuite: true,
146
+ warning: FULL_SUITE_SHAPE_WARNING,
147
+ };
148
+ const { crap } = getQualityImpl(resolveConfigImpl({ cwd: worktree }));
149
+ const mode =
150
+ crap?.enabled !== false &&
151
+ hasNpmScriptImpl(readPackageScriptsImpl(worktree), 'test:coverage')
152
+ ? 'capture'
153
+ : 'evidence';
154
+
155
+ if (mode === 'capture') {
156
+ const freshness = isCoverageFreshImpl({
157
+ coveragePath: crap.coveragePath,
158
+ targetDirs: crap.targetDirs,
159
+ cwd: worktree,
160
+ });
161
+ const fresh = freshness?.fresh === true;
162
+ return {
163
+ ...scoped,
164
+ mode,
165
+ credited: fresh,
166
+ spawn: !fresh,
167
+ reason: fresh ? 'capture-stamp-fresh' : (freshness?.reason ?? 'unknown'),
168
+ };
169
+ }
170
+
171
+ const headSha = readHeadSha(worktree, gitSpawnFn);
172
+ if (!headSha) {
173
+ return { ...scoped, mode, credited: false, spawn: true, reason: 'no-head' };
174
+ }
175
+ const [cmd, ...args] = command.split(/\s+/).filter(Boolean);
176
+ const verdict = shouldSkipImpl(
177
+ {
178
+ storyId,
179
+ gateName: 'test',
180
+ currentSha: headSha,
181
+ configHash: hashCommandConfigImpl({ cmd, args, cwd: worktree }),
182
+ },
183
+ { cwd, standalone: true },
184
+ );
185
+ return {
186
+ ...scoped,
187
+ mode,
188
+ credited: verdict.skip === true,
189
+ spawn: verdict.skip !== true,
190
+ reason: verdict.reason,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Classify a whole `verify[]` array in one pass.
196
+ *
197
+ * @param {string[]} entries — raw `verify[]` lines, tier tags included.
198
+ * @param {{ storyId: number|string, worktree: string, cwd?: string }} context
199
+ * @param {object} [deps]
200
+ * @returns {Array<ReturnType<typeof resolveVerifyCredit> & { tier: string|null }>}
201
+ */
202
+ export function planVerifyExecution(entries, context, deps = {}) {
203
+ return (Array.isArray(entries) ? entries : []).map((entry) => {
204
+ const { command, tier } = parseVerifyEntry(entry);
205
+ return { ...resolveVerifyCredit({ ...context, command }, deps), tier };
206
+ });
207
+ }
@@ -161,6 +161,30 @@ function readLockOwner(lockPath, fsImpl = fs) {
161
161
  }
162
162
  }
163
163
 
164
+ /**
165
+ * Read the pid a lockfile was created by (its third line — see
166
+ * {@link tryCreateLock}'s body format). Returns `null` when the file is
167
+ * absent, unreadable, or its pid line is not a positive integer.
168
+ *
169
+ * Exists so a *waiting* caller can name the holder in its wait line: a bounded
170
+ * wait with no attribution is indistinguishable from a hang, and the pid is
171
+ * the one field an operator can act on (`ps`, `kill`). Reading it is
172
+ * advisory — a `null` just means the wait line says less.
173
+ *
174
+ * @param {string} lockPath
175
+ * @param {object} [fsImpl]
176
+ * @returns {number|null}
177
+ */
178
+ export function readLockHolderPid(lockPath, fsImpl = fs) {
179
+ try {
180
+ const lines = String(fsImpl.readFileSync(lockPath, 'utf8')).split('\n');
181
+ const pid = Number.parseInt(lines[2] ?? '', 10);
182
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
164
188
  /**
165
189
  * Pure: do two identity tuples describe the same lockfile instance? A `null`
166
190
  * on either side is "not the same" — an absent file is never the file we
@@ -113,6 +113,18 @@ mid-delivery, and evaluates the actual work product.
113
113
  optional advisory pre-flight — a criterion cannot be scored `met` without
114
114
  the supporting `verify[]` evidence where a `verify[]` command is relevant
115
115
  to it.
116
+ - **Reuses the credited full-suite run instead of re-paying for it.**
117
+ Before spawning a `verify[]` entry, classify it with `resolveVerifyCredit`
118
+ from
119
+ [`verify-credit.js`](../../scripts/lib/orchestration/verify-credit.js): an
120
+ entry that is itself a full-suite command (`npm test`, `pnpm run test`,
121
+ a bare `node --test`) is consulted against the **same stamp close reads**
122
+ and, when that stamp is fresh, recorded as `pass` with a `detail` naming
123
+ the credit — **never respawned**. A stale or absent stamp reports
124
+ `spawn: true` and the command runs for real, so the credit can never
125
+ manufacture a pass. The gate warns on any such entry: the intended shape
126
+ is scoped `verify[]` entries **plus** the one credited run
127
+ ([`deliver-digest.md`](deliver-digest.md) § 5).
116
128
  - **Shares `lint` / `typecheck` evidence with close.** When a
117
129
  `verify[]` command is **byte-identical** to a close-validation gate — in
118
130
  practice only the cheap, command-identical `lint` and `typecheck` gates
@@ -2,9 +2,9 @@
2
2
  description: >-
3
3
  The deliver path's one bundled framework read. Carries what
4
4
  every Story delivery always needs — dispatch decision, engine invariants,
5
- the change-set/ceremony incantation, the acceptance-eval gate, and the
6
- terminal envelope contract — so the engine reads one file instead of
7
- re-reading the helper/schema set each session.
5
+ the change-set/ceremony incantation, the acceptance-eval gate, the credited
6
+ full-suite run, and the terminal envelope contract — so the engine reads one
7
+ file instead of re-reading the helper/schema set each session.
8
8
  ---
9
9
 
10
10
  # Deliver digest (read once per session)
@@ -27,10 +27,8 @@ rule produces it:
27
27
  whatever its shape — sub-agent isolation is load-bearing only against a
28
28
  *concurrent* sibling racing the same checkout, and a one-Story run has none.
29
29
  2. **Every other run is `subagent`.** A multi-Story run dispatches every Story
30
- as a sub-agent however trivial its shape a lite body does not conjure a
31
- second session for a sibling, and the wave tick may hand you the whole set
32
- on one beat. Shape still sets ceremony; the `route::lite` label is a
33
- human-visible hint, never the control signal.
30
+ as a sub-agent however trivial its shape. Shape still sets ceremony; the
31
+ `route::lite` label is a human-visible hint, never the control signal.
34
32
 
35
33
  `inline` removes model-side fan-out only — no `story-worker` boot, no fresh
36
34
  acceptance-critic spawn. **`subagent` and `inline` run the same engine**: same
@@ -112,7 +110,30 @@ an unmerged cluster verdict scores a fraction of the criteria and still reports
112
110
  not close**: post a `friction` comment and flip `agent::blocked`.
113
111
  Per-round mechanics: [`acceptance-self-eval.md`](acceptance-self-eval.md).
114
112
 
115
- ## 5. Terminal envelope the return contract
113
+ ## 5. The one creditable full-suite run
114
+
115
+ **After the self-eval loop's last fix commit, immediately before the push** —
116
+ the credit is keyed on the tree, so any later commit invalidates it. Redraft
117
+ rounds run scoped tests; only this final run needs credit, and a bare
118
+ `npm test` / `pnpm run test` deposits **none**, so close re-runs the identical
119
+ suite. Shape it by the predicate `close-validation/gates.js` uses for its test
120
+ gate:
121
+
122
+ ```bash
123
+ # CRAP gate on (default) + a `test:coverage` script — writes the stamp the
124
+ # close `coverage-capture` gate reads:
125
+ node <main-repo>/.agents/scripts/coverage-capture.js --cwd <workCwd>
126
+ # otherwise — the record the close `test` gate reads. <workCwd> ABSOLUTE,
127
+ # runner exactly `npm test`: both sides hash {cmd, args, cwd}.
128
+ node <main-repo>/.agents/scripts/evidence-gate.js --standalone \
129
+ --scope-id <storyId> --gate test --worktree <workCwd> -- npm test
130
+ ```
131
+
132
+ `verify[]` is scoped entries **plus** this one run: an entry that is itself a
133
+ full-suite command is reported credited against the same stamp, never
134
+ respawned.
135
+
136
+ ## 6. Terminal envelope — the return contract
116
137
 
117
138
  `single-story-close.js` emits exactly one envelope on stdout between
118
139
  `--- STORY DELIVER TERMINAL ---` markers, schema-validated against
@@ -129,7 +150,7 @@ Relay it verbatim; never hand-compose one, never substitute prose.
129
150
 
130
151
  Required fields: `kind` (`story-deliver-terminal`), `storyId`, `status`,
131
152
  `phase`, `elapsedSeconds`, `nextCommand`. `phase` is one of `init`,
132
- `wrong-tree-guard`, `close-validation`, `base-sync`, `push`, `pull-request`,
153
+ `wrong-tree-guard`, `base-sync`, `close-validation`, `push`, `pull-request`,
133
154
  `code-review`, `auto-merge`, `confirm-merge`, `post-land`, `done`. `gates`
134
155
  reports every gate as `passed` / `failed` / `skipped` — a skipped gate is
135
156
  reported, never omitted, so a missing gate is never read as a passing one.
@@ -139,7 +160,7 @@ reported, never omitted, so a missing gate is never read as a passing one.
139
160
  success; a failed gate replays its tail inline. `AGENT_LOG_LEVEL=verbose`
140
161
  restores live streaming.
141
162
 
142
- ## 6. When to leave this file
163
+ ## 7. When to leave this file
143
164
 
144
165
  - Unclear state / a re-run refusal → `deliver-recover.js --story <id>` (read-only).
145
166
  - Lease, sweep, worktree-scope detail → [`deliver-story-reference.md`](deliver-story-reference.md).
@@ -233,17 +233,19 @@ runs maker-blind at Story-scope review inside the close subprocess. The
233
233
  dispatch step produces `checklistPath` from the Story's predicted footprint
234
234
  before it spawns the worker — see [`/mandrel-deliver`](../mandrel-deliver.md).
235
235
 
236
- **Pre-eval full-suite discipline (spine step 1.3).** Repo-invariant guards —
236
+ **Full-suite discipline (spine Step 2.5).** Repo-invariant guards —
237
237
  drift-guard and schema tests living outside the Story's scoped greps — are
238
238
  the failure class that actually bounces deliveries: close-validation
239
239
  discovers them only after the whole close pipeline has run, at several times
240
- the cost of one pre-eval full-suite run.
240
+ the cost of one full-suite run in the worktree.
241
241
 
242
- **Run it so close can credit it.** Close skips a gate that already passed at
243
- the current HEAD, but a bare `npm test` deposits no such record — the suite
244
- then runs twice per delivery, once here and once in the close gate chain.
245
- Pick the invocation by the same predicate `close-validation/gates.js` uses to
246
- choose its test gate:
242
+ **Run it once, last, so close can credit it.** The run belongs **after** the
243
+ self-eval loop's last fix commit and immediately **before** the hand-off push,
244
+ so its stamp describes the tree that is pushed; redraft rounds run scoped
245
+ tests. Close skips a gate that already passed at the current HEAD, but a bare
246
+ `npm test` deposits no such record — the suite then runs twice per delivery,
247
+ once here and once in the close gate chain. Pick the invocation by the same
248
+ predicate `close-validation/gates.js` uses to choose its test gate:
247
249
 
248
250
  ```bash
249
251
  # CRAP gate enabled (default) + a `test:coverage` script — writes the stamp
@@ -259,7 +261,15 @@ node <main-repo>/.agents/scripts/evidence-gate.js --standalone \
259
261
  The credit expires the moment it stops describing the tree: evidence is keyed
260
262
  on HEAD, the capture stamp on a content digest of `crap.targetDirs`. A
261
263
  self-eval fix — or any commit — invalidates it and close re-runs the suite for
262
- real, so this never trades away the gate.
264
+ real, so this never trades away the gate. That keying is exactly why the run
265
+ comes last.
266
+
267
+ **`verify[]` reuses the same stamp.** A `verify[]` entry that is itself a
268
+ full-suite command is reported **credited** against that stamp rather than
269
+ respawned (`resolveVerifyCredit` in
270
+ [`verify-credit.js`](../../scripts/lib/orchestration/verify-credit.js)), and the
271
+ self-eval gate warns when it sees one: the intended shape is scoped `verify[]`
272
+ entries **plus** the single credited run.
263
273
 
264
274
  **Conflict with `main` mid-implementation** → resolve as you would any branch
265
275
  rebase. There is no `epic/<id>` intermediate, so the rebase base is `main`
@@ -500,22 +510,16 @@ judgment that help text cannot carry.
500
510
 
501
511
  The `single-story-close.js` script, in order:
502
512
 
503
- 1. Runs the close-validation gates against `baseBranch` as the baseline.
504
- On any gate failure it throws the operator fixes and re-runs close.
505
- **Gate output is captured, not streamed.** Every gate line
506
- goes to `temp/orchestration/close-gates-<storyId>.log`; a clean run reports
507
- one digest line naming that artifact, and a **failed** gate replays its
508
- captured tail inline so the evidence is in front of you without opening a
509
- file. Read the artifact when you need the full text — or re-run under
510
- `AGENT_LOG_LEVEL=verbose` for live streaming.
511
- 1a. **Syncs the Story branch from `origin/<baseBranch>`** before push.
512
- Runs `git fetch origin <baseBranch>` followed by
513
- `git merge --no-edit origin/<baseBranch>` inside the worktree. This
513
+ 1. **Syncs the Story branch from `origin/<baseBranch>`** before the gates,
514
+ not after them. Runs `git fetch origin <baseBranch>` followed
515
+ by `git merge --no-edit origin/<baseBranch>` inside the worktree. This
514
516
  defends against the parallel-`/deliver-story` race: when
515
517
  multiple sessions run in parallel, the Story that auto-merges first
516
518
  bumps `baseBranch`, and without this sync the lagging Stories open
517
519
  PRs that are "behind base" and stall against branch-protection's
518
- `up-to-date branch` rule. Outcomes:
520
+ `up-to-date branch` rule. Running it first also means a conflict costs
521
+ no gate run at all, and — the load-bearing half — the tree the gates
522
+ validate is the tree the push sends. Outcomes:
519
523
  - **No-op / fast-forward / clean merge-commit** → close proceeds to
520
524
  push.
521
525
  - **Merge conflict** → the merge is aborted, a `friction` structured
@@ -533,12 +537,26 @@ The `single-story-close.js` script, in order:
533
537
  closes the PR-open-time race but a residual race remains between PR
534
538
  open and auto-merge fire.
535
539
 
536
- 2. Pushes `story-<id>` to `origin`.
537
- 3. Probes for an existing open PR with `head = story-<id>`. If none
540
+ 2. Runs the close-validation gates against `baseBranch` as the baseline.
541
+ On any gate failure it throws the operator fixes and re-runs close.
542
+ The chain fails cheapest-first: `typecheck`, `lint`, `format` and the
543
+ coverage-independent half of the baselines gate
544
+ (`check-baselines-independent`) run in parallel, and only once they are
545
+ green does the serial walk pay for `coverage-capture` and the
546
+ coverage-consuming half (`check-baselines-coverage`).
547
+ **Gate output is captured, not streamed.** Every gate line
548
+ goes to `temp/orchestration/close-gates-<storyId>.log`; a clean run reports
549
+ one digest line naming that artifact, and a **failed** gate replays its
550
+ captured tail inline so the evidence is in front of you without opening a
551
+ file. Read the artifact when you need the full text — or re-run under
552
+ `AGENT_LOG_LEVEL=verbose` for live streaming.
553
+
554
+ 3. Pushes `story-<id>` to `origin`.
555
+ 4. Probes for an existing open PR with `head = story-<id>`. If none
538
556
  exists, opens one via `gh pr create --base <baseBranch>`. The PR
539
557
  body carries `Closes #<storyId>` so the GitHub merge auto-closes the
540
558
  issue.
541
- 3a. **Enables GitHub native auto-merge by default** via
559
+ 4a. **Enables GitHub native auto-merge by default** via
542
560
  `gh pr merge <prNumber> --auto --squash --delete-branch`. Once CI's
543
561
  required checks turn green, GitHub squash-merges the PR and deletes
544
562
  the source branch — the operator does not need to babysit the merge
@@ -546,7 +564,7 @@ The `single-story-close.js` script, in order:
546
564
  non-fatal: the operator retains the manual merge surface in the
547
565
  GitHub UI. Pass `--no-auto-merge` to opt out when the PR needs a
548
566
  pre-merge eyeball.
549
- 4. Flips the Story to **`agent::closing`** (NOT `agent::done`) and leaves
567
+ 5. Flips the Story to **`agent::closing`** (NOT `agent::done`) and leaves
550
568
  the GitHub issue **OPEN**. Auto-merge completes
551
569
  asynchronously _after_ this script exits, so closing the issue here
552
570
  would strand a CLOSED issue with no merged work if the PR later failed
@@ -557,9 +575,9 @@ The `single-story-close.js` script, in order:
557
575
  `--no-wait-merge` run, or the in-close confirm phase on the
558
576
  close-and-land default. (Step 5.5 is the Status-column resync.) A Story
559
577
  only reaches `agent::done` once its PR to `main` is confirmed merged.
560
- 5. Reaps the worktree when `delivery.worktreeIsolation.reapOnSuccess`
578
+ 6. Reaps the worktree when `delivery.worktreeIsolation.reapOnSuccess`
561
579
  is enabled.
562
- 6. **Releases the Story lease.** Clears the Story assignment
580
+ 7. **Releases the Story lease.** Clears the Story assignment
563
581
  that init claimed so the next `/deliver-story` run sees an
564
582
  unclaimed ticket. The release is a no-op when the operator no longer
565
583
  holds the claim (a later run took over via reclaim/steal), so a late
@@ -572,13 +590,15 @@ The `single-story-close.js` script, in order:
572
590
  de-assigning the ticket. The close result carries
573
591
  `leaseReleased: <boolean>`.
574
592
 
575
- `--skip-validation` bypasses Step 1 (gates). Use only when re-running
593
+ `--skip-validation` bypasses the gate step. Use only when re-running
576
594
  close after a fixed gate failure that's already known to pass.
577
595
 
578
- `--skip-sync` bypasses Step 1a (base-sync). Use only when re-running
579
- close after a hand-resolved sync, or in tests.
596
+ `--skip-sync` bypasses the base-sync step. Use only when re-running
597
+ close after a hand-resolved sync, or in tests. The two flags are
598
+ independent: either, both or neither may be set, and each elides exactly
599
+ its own phase.
580
600
 
581
- `--no-auto-merge` disables Step 3a. Use when the PR materially changes
601
+ `--no-auto-merge` disables the auto-merge arm (step 4a). Use when the PR materially changes
582
602
  behaviour and warrants pre-merge review.
583
603
 
584
604
  ---
@@ -40,7 +40,7 @@ that dispatched the work**, never to a spawned worker.
40
40
 
41
41
  **A worker returning no terminal envelope is expected, not a failure** — only
42
42
  Step 3 mints one. Never re-dispatch the Story on it; resume per § Recovery
43
- (reference § Idempotence and the standing constraints).
43
+ (reference § Idempotence).
44
44
 
45
45
  ## Step 0 — Initialize (`single-story-init.js`)
46
46
 
@@ -73,12 +73,8 @@ One branch, one PR to `main`, commits against the inline `acceptance[]` /
73
73
  digest-first; read a caller-provided `checklistPath` first, and walk any
74
74
  `## Slicing` rows as **intra-session checkpoints** (reference § Step 1).
75
75
  2. Implement and commit on the Story branch, iterating with quick advisory
76
- gates (`typecheck`, `lint`, scoped tests) — the full chain runs in Step 3.
77
- 3. Run the full suite once in the worktree **before Step 1a**: repo-invariant
78
- guards outside the Story's scoped greps are the failure class that bounces
79
- deliveries. Fix and commit first, then run the self-eval loop. Run it **so
80
- Step 3 credits it** — a bare `npm test` records nothing, so close re-runs
81
- the identical suite (reference § Step 1, "Pre-eval full-suite discipline").
76
+ gates (`typecheck`, `lint`, scoped tests) — the full chain runs in Step 3,
77
+ and the **one** creditable full-suite run at Step 2.5.
82
78
 
83
79
  ### Step 1a — Bounded acceptance self-eval loop (**required**)
84
80
 
@@ -94,14 +90,21 @@ Ceremony is `delivery.routing.ceremonyProfile` × the **derived change level**,
94
90
  never a planner-authored verdict. **Digest § 3** is the incantation (change set
95
91
  once, derive the level, resolve critics with `ceremony-routing.js`); edge cases
96
92
  are reference § Step 2. Hard gates always run in Step 3 — the derived level
97
- never disables them; do **not** pre-run the chain here.
93
+ never disables them; do **not** pre-run the chain here — Step 2.5's credited
94
+ suite run is the sole exception.
98
95
 
99
- ### Step 2.5 — Push and hand off (sub-agent dispatch only)
96
+ ### Step 2.5 — The creditable full-suite run, then push and hand off
100
97
 
101
- Push `story-<storyId>` to `origin` and confirm the remote ref moved. Return the
102
- hand-off Story id, `workCwd`, branch, pushed head SHA, self-eval verdict,
103
- `verify[]` evidence then stop. Do not open the PR; do not compose a terminal
104
- envelope. An inline run skips this.
98
+ Run the full suite **once**, after the self-eval loop's last fix commit and
99
+ immediately **before** the push, in the shape close credits (**digest § 5**):
100
+ the credit is keyed on the tree, so any later commit invalidates it, and a bare
101
+ `npm test` deposits none. Red → fix, commit, re-run. An inline run makes the
102
+ same run before Step 3.
103
+
104
+ Then (sub-agent dispatch only) push `story-<storyId>` to `origin`, confirm the
105
+ remote ref moved, and return the hand-off — Story id, `workCwd`, branch, pushed
106
+ head SHA, self-eval verdict, `verify[]` evidence — then stop. Do not open the
107
+ PR; do not compose a terminal envelope.
105
108
 
106
109
  ## Step 3 — Close and land (`single-story-close.js`)
107
110
 
@@ -115,7 +118,7 @@ node <main-repo>/.agents/scripts/single-story-close.js --story <storyId> --cwd <
115
118
  **The whole delivery tail** — gates, PR, merge wait, `agent::done` flip,
116
119
  post-land tail in one process. Never background it, never delegate it to a
117
120
  child, and never end your turn while it is still running: "close is running"
118
- is not a return value. Branch on the envelope's `status` per **digest § 5**
121
+ is not a return value. Branch on the envelope's `status` per **digest § 6**
119
122
  (`landed` → Step 7; `pending` → run `nextCommand`; `blocked`/`checks-failed`
120
123
  → Step 4; `failed` → diagnose, re-run). Gate output is captured.
121
124
 
@@ -126,7 +129,7 @@ Internals, merge-wait budgets, the slow-CI **async** confirm mode, the
126
129
 
127
130
  Relay the validated envelope close emits between its
128
131
  `--- STORY DELIVER TERMINAL ---` markers — never free-form prose, never a
129
- hand-composed object. Statuses, exits and fields: **digest § 5** (SSOT: the
132
+ hand-composed object. Statuses, exits and fields: **digest § 6** (SSOT: the
130
133
  shipped [schema](../../schemas/story-deliver-terminal.schema.json)).
131
134
  `pending` is the only sanctioned no-merge ending.
132
135
 
@@ -139,15 +142,14 @@ so only a green on a NEW head SHA re-arms it — a re-run is refused; fix at sou
139
142
  and push ([`rules/ci-remediation.md`](../../rules/ci-remediation.md)). And a
140
143
  `tail.*: false` degrades the report, never the land.
141
144
 
142
- **Watch exit codes** — `pr-watch-with-update.js` exits 0 green, 1 only when a
143
- required check genuinely failed (or the PR is unreadable), 2 slow-but-not-red:
144
- still-running, unresolved, **or** `notYetStarted` no required context attached
145
- inside `attachWindowMs`. Never route a 2 onto the red path; nothing is broken
146
- and no digest exists to read.
145
+ **Watch exit codes** — `pr-watch-with-update.js` exits 0 green, 1 on a genuine
146
+ red, 2 slow-but-not-red (still-running, unresolved, or `notYetStarted`). Never
147
+ route a 2 onto the red path: nothing is broken and no digest exists to read.
148
+ Which slow condition, and what to do: reference § Step 4.
147
149
 
148
150
  **Lost envelope first: read it off disk.** Close persists each to
149
151
  `temp/orchestration/story-deliver-terminal-<storyId>.json`; branch on it per
150
- digest § 5. Otherwise do not guess — probe **read-only** with
152
+ digest § 6. Otherwise do not guess — probe **read-only** with
151
153
  `node .agents/scripts/deliver-recover.js --story <storyId>`; it prints the
152
154
  **one** next command with its evidence, never a menu. A live close answers
153
155
  `close-in-flight`: wait, never re-init underneath it.
@@ -2,8 +2,8 @@
2
2
  description: >-
3
3
  Attended consolidation pass over this project's agent memory pool — merge
4
4
  duplicates, verify claims against the current tree, prune with operator
5
- confirmation, rewrite the index, and stamp the pool so the /mandrel-plan advisory
6
- goes quiet.
5
+ confirmation, rewrite the index, and stamp the pool with the date and entry
6
+ count the /mandrel-plan advisory measures its next nudge against.
7
7
  ---
8
8
 
9
9
  # /memory-consolidate [--dry-run]
@@ -93,14 +93,26 @@ pointers only, never memory content.
93
93
  Then write the receipt to `.consolidation-stamp.json` in the pool root:
94
94
 
95
95
  ```json
96
- { "lastConsolidatedAt": "<ISO-8601 timestamp>" }
96
+ { "lastConsolidatedAt": "<ISO-8601 timestamp>", "entryCount": 42 }
97
97
  ```
98
98
 
99
- The `/mandrel-plan` Phase 0 advisory reads this file; until it is written, the nudge
100
- keeps firing. Write it **only** after Gate #2 the stamp asserts an operator
101
- reviewed the pass, so writing it early makes it a lie.
99
+ `entryCount` is the surviving non-index `*.md` count **after** the rewrite
100
+ count the directory, never the plan. It is the baseline the next run measures
101
+ growth against, so a wrong number silently mis-arms the nudge.
102
+
103
+ The `/mandrel-plan` Phase 0 advisory re-arms on exactly two conditions: the
104
+ stamp aging past `planning.memoryPool.staleAfterDays` (30), or
105
+ `planning.memoryPool.growthDelta` (25) entries written since that count. Pool
106
+ size alone never triggers it — a pass that keeps every entry still quiets the
107
+ nudge. A stamp with no `entryCount` leaves growth unmeasured, and only the age
108
+ arm can speak until the next pass writes one.
109
+
110
+ Write it **only** after Gate #2 — the stamp asserts an operator reviewed the
111
+ pass, so writing it early makes it a lie.
102
112
 
103
113
  Close with counts: entries read, corrected, merged, pruned, and the new total.
114
+ Then the forecast the operator would otherwise derive by hand: when the
115
+ advisory next fires, and which arm reaches it first.
104
116
 
105
117
  ## Constraints
106
118
 
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,31 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.43.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.42.0...mandrel-v2.43.0) (2026-09-07)
19
+
20
+
21
+ ### Added
22
+
23
+ * memory-pool advisory measures growth since the last consolidation instead of an absolute entry ceiling ([#5182](https://github.com/dsj1984/mandrel/issues/5182)) ([#5183](https://github.com/dsj1984/mandrel/issues/5183)) ([cccdd69](https://github.com/dsj1984/mandrel/commit/cccdd69546e5dfb4c6d1e205423ff52f07982ade))
24
+
25
+ ## [2.42.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.41.0...mandrel-v2.42.0) (2026-09-06)
26
+
27
+
28
+ ### Added
29
+
30
+ * full-suite capture economy: default the coverage-independent skip on, and serialize concurrent captures behind a host lock ([#5173](https://github.com/dsj1984/mandrel/issues/5173)) ([#5178](https://github.com/dsj1984/mandrel/issues/5178)) ([ed79751](https://github.com/dsj1984/mandrel/commit/ed7975199b6e2a3279a5d0d02b50e3068509987f))
31
+ * story-worker digest carries the creditable full-suite invocation, runs it after self-eval, and the critic reuses its stamp ([#5174](https://github.com/dsj1984/mandrel/issues/5174)) ([#5175](https://github.com/dsj1984/mandrel/issues/5175)) ([1e46457](https://github.com/dsj1984/mandrel/commit/1e464574aa6305dcc8f1ff8522015ae0f9a5301a))
32
+
33
+
34
+ ### Fixed
35
+
36
+ * scope the baseline refresh acknowledgment to the rows the refresh commit actually refreshed ([#5179](https://github.com/dsj1984/mandrel/issues/5179)) ([#5180](https://github.com/dsj1984/mandrel/issues/5180)) ([e8758b9](https://github.com/dsj1984/mandrel/commit/e8758b909ec19ed55c0678c05f481d515937f978))
37
+
38
+
39
+ ### Changed
40
+
41
+ * close fails fast: base-sync before validation, and coverage-independent baseline kinds in the parallel gate phase ([#5172](https://github.com/dsj1984/mandrel/issues/5172)) ([#5177](https://github.com/dsj1984/mandrel/issues/5177)) ([dff33d5](https://github.com/dsj1984/mandrel/commit/dff33d530965ef7da8ee3dd4f1f218d11805273f))
42
+
18
43
  ## [2.41.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.40.0...mandrel-v2.41.0) (2026-09-06)
19
44
 
20
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.41.0",
3
+ "version": "2.43.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/",