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.
- package/.agents/agents/story-worker.md +24 -14
- package/.agents/docs/agentrc-reference.json +11 -2
- package/.agents/docs/configuration.md +9 -3
- package/.agents/docs/workflows.md +1 -1
- package/.agents/schemas/agentrc.schema.json +37 -3
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/acceptance-eval.js +68 -3
- package/.agents/scripts/coverage-capture.js +25 -8
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +7 -2
- package/.agents/scripts/lib/baselines/git-base.js +74 -38
- package/.agents/scripts/lib/close-validation/gates.js +153 -25
- package/.agents/scripts/lib/close-validation/process.js +30 -1
- package/.agents/scripts/lib/close-validation/runner.js +5 -0
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +33 -12
- package/.agents/scripts/lib/config/quality.js +36 -21
- package/.agents/scripts/lib/config-settings-schema-delivery.js +6 -0
- package/.agents/scripts/lib/config-settings-schema.js +29 -1
- package/.agents/scripts/lib/coverage-capture-incremental.js +12 -6
- package/.agents/scripts/lib/crap-baseline-join.js +11 -7
- package/.agents/scripts/lib/full-suite-lock.js +311 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +11 -104
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +320 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/plan-context.js +4 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +9 -1
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +159 -55
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +83 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +39 -7
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +70 -18
- package/.agents/scripts/lib/orchestration/verify-credit.js +207 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +24 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +12 -0
- package/.agents/workflows/helpers/deliver-digest.md +31 -10
- package/.agents/workflows/helpers/deliver-story-reference.md +50 -30
- package/.agents/workflows/helpers/deliver-story.md +23 -21
- package/.agents/workflows/memory-consolidate.md +18 -6
- package/docs/CHANGELOG.md +25 -0
- 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,
|
|
6
|
-
terminal envelope contract — so the engine reads one
|
|
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
|
|
31
|
-
|
|
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.
|
|
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`, `
|
|
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
|
-
##
|
|
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
|
-
**
|
|
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
|
|
240
|
+
the cost of one full-suite run in the worktree.
|
|
241
241
|
|
|
242
|
-
**Run it so close can credit it.**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
504
|
-
|
|
505
|
-
|
|
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.
|
|
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.
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
+
6. Reaps the worktree when `delivery.worktreeIsolation.reapOnSuccess`
|
|
561
579
|
is enabled.
|
|
562
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 —
|
|
96
|
+
### Step 2.5 — The creditable full-suite run, then push and hand off
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 §
|
|
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 §
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 §
|
|
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
|
|
6
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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