mandrel 2.56.0 → 2.58.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/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +277 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +46 -55
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -13,70 +13,42 @@
|
|
|
13
13
|
* verdict nothing read. This emits one advisory the `/mandrel-plan` spine
|
|
14
14
|
* surfaces at Gate #1, recommending `/memory-consolidate`.
|
|
15
15
|
*
|
|
16
|
-
* It
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* It renders **no per-entry verdict at all**. A memory citing a closed issue
|
|
17
|
+
* is a delivery retrospective whose subject is that issue — not a stale entry
|
|
18
|
+
* — and only the attended `/memory-consolidate` pass, reading content, can
|
|
19
|
+
* tell the difference. This module measures one thing; it never judges an
|
|
20
|
+
* entry.
|
|
21
21
|
*
|
|
22
|
-
* **
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* simply stays silent and only the age arm can speak, until the next pass
|
|
35
|
-
* writes a baseline.
|
|
36
|
-
*
|
|
37
|
-
* **The index byte arm (Story #5285).** Age and growth both measure the
|
|
38
|
-
* *pool*; neither measures the one artifact a session actually loads. The
|
|
39
|
-
* harness reads `MEMORY.md` into every session under a hard byte cap and
|
|
40
|
-
* **truncates** past it, so an index over that cap loses its tail entries
|
|
41
|
-
* silently — the pointers are on disk, indexed, and unreachable. That is a
|
|
42
|
-
* loss in progress, not a hygiene forecast, so this arm is independent of the
|
|
43
|
-
* other two: it fires on a fresh, zero-growth pool whose index has simply
|
|
44
|
-
* outgrown the cap. It measures the index file's size, never the pool's, and
|
|
45
|
-
* a pass that rewrites long index lines short clears it without pruning a
|
|
46
|
-
* single entry.
|
|
47
|
-
*
|
|
48
|
-
* **A future-dated stamp is no stamp.** `lastConsolidatedAt` ahead of `now`
|
|
49
|
-
* cannot describe a pass that happened — it is a clock skew, a hand-edit, or
|
|
50
|
-
* a timezone bug. Scored as-is it yields a negative age that silences the age
|
|
51
|
-
* arm *forever*, which is the loudest possible failure for an advisory whose
|
|
52
|
-
* only job is to speak up. It reads as unstamped instead, so the
|
|
53
|
-
* never-consolidated reason fires and the next real pass overwrites it.
|
|
22
|
+
* **One arm: the index byte ceiling (Story #5285, sole survivor after Story
|
|
23
|
+
* #5312).** The harness reads `MEMORY.md` into every session under a hard
|
|
24
|
+
* byte cap and **truncates** past it, so an index over that cap loses its
|
|
25
|
+
* tail entries silently — the pointers are on disk, indexed, and unreachable.
|
|
26
|
+
* That is a loss in progress, not a hygiene forecast, and it is the only
|
|
27
|
+
* signal that measures the artifact a session actually loads. The stamp-age
|
|
28
|
+
* and growth-delta arms that used to sit beside it measured the *pool*, fired
|
|
29
|
+
* on every plan once a pool was mature, and were learned-ignored by exactly
|
|
30
|
+
* the operators they nagged; Story #5312 deleted them with their
|
|
31
|
+
* `planning.memoryPool.{staleAfterDays, growthDelta}` knobs. A pass that
|
|
32
|
+
* rewrites long index lines short clears this arm without pruning a single
|
|
33
|
+
* entry.
|
|
54
34
|
*
|
|
55
35
|
* Detection is filesystem-only — no child processes, no `gh` probes, no
|
|
56
36
|
* network. Every failure path fails soft to "no pool, no recommendation": the
|
|
57
37
|
* advisory can degrade the nudge, never a plan.
|
|
58
38
|
*
|
|
59
39
|
* Test seams: `cwd`, `env`, `fsImpl` (node:fs-compatible `statSync` /
|
|
60
|
-
* `readdirSync`
|
|
61
|
-
* (`staleAfterDays`, `growthDelta`, `indexByteCeiling`).
|
|
40
|
+
* `readdirSync`), and the `indexByteCeiling` threshold.
|
|
62
41
|
*
|
|
63
42
|
* `buildMemoryPoolAdvisory` is the **only** export: the helpers below have no
|
|
64
43
|
* caller outside this module, and exporting one solely for a test would add a
|
|
65
|
-
* row to the `dead-exports-production` ratchet
|
|
66
|
-
*
|
|
67
|
-
* "fix" the missing exports.
|
|
44
|
+
* row to the `dead-exports-production` ratchet. Tests reach every branch
|
|
45
|
+
* through the seams above — do not "fix" the missing exports.
|
|
68
46
|
*/
|
|
69
47
|
|
|
70
48
|
import * as defaultFs from 'node:fs';
|
|
71
49
|
import * as os from 'node:os';
|
|
72
50
|
import * as path from 'node:path';
|
|
73
51
|
|
|
74
|
-
/** Recommend a consolidation pass once the stamp is this old. */
|
|
75
|
-
const STALE_AFTER_DAYS = 30;
|
|
76
|
-
|
|
77
|
-
/** Recommend a pass once this many entries were written since the last one. */
|
|
78
|
-
const GROWTH_DELTA = 25;
|
|
79
|
-
|
|
80
52
|
/**
|
|
81
53
|
* Recommend a pass once `MEMORY.md` exceeds this many bytes.
|
|
82
54
|
*
|
|
@@ -87,14 +59,9 @@ const GROWTH_DELTA = 25;
|
|
|
87
59
|
*/
|
|
88
60
|
const INDEX_BYTE_CEILING = 24_576;
|
|
89
61
|
|
|
90
|
-
/** Stamp file written by `/memory-consolidate` after its operator gate. */
|
|
91
|
-
const STAMP_FILENAME = '.consolidation-stamp.json';
|
|
92
|
-
|
|
93
62
|
/** The index file is not itself a memory entry. */
|
|
94
63
|
const INDEX_FILENAME = 'MEMORY.md';
|
|
95
64
|
|
|
96
|
-
const MS_PER_DAY = 86_400_000;
|
|
97
|
-
|
|
98
65
|
/**
|
|
99
66
|
* Slugify an absolute path the way the harness names its per-project
|
|
100
67
|
* directories: every `/` and `.` becomes `-`. Verified against real
|
|
@@ -141,10 +108,8 @@ function resolveMemoryPoolDir({ cwd, env = process.env, homedir } = {}) {
|
|
|
141
108
|
* Run one filesystem probe, falling back on any failure.
|
|
142
109
|
*
|
|
143
110
|
* Every read here is fail-soft by design — the advisory may degrade its nudge
|
|
144
|
-
* but never a plan — so
|
|
145
|
-
* around one expression. One helper states the rule once
|
|
146
|
-
* forget it, and a `catch` that ever needs to do more than fall back would
|
|
147
|
-
* have to be written out, which is the signal it deserves.
|
|
111
|
+
* but never a plan — so every probe has the same try/catch shape wrapped
|
|
112
|
+
* around one expression. One helper states the rule once.
|
|
148
113
|
*
|
|
149
114
|
* @template T
|
|
150
115
|
* @param {() => T} read
|
|
@@ -159,61 +124,14 @@ function probe(read, fallback = null) {
|
|
|
159
124
|
}
|
|
160
125
|
}
|
|
161
126
|
|
|
162
|
-
/**
|
|
163
|
-
* The growth baseline a stamp records: its entry count, or `null` when it
|
|
164
|
-
* records none. `null` is *unmeasured*, never zero — a zero baseline would
|
|
165
|
-
* score every entry in the pool as newly written.
|
|
166
|
-
*
|
|
167
|
-
* @param {unknown} count
|
|
168
|
-
* @returns {number|null}
|
|
169
|
-
*/
|
|
170
|
-
function readBaseline(count) {
|
|
171
|
-
return Number.isInteger(count) && count >= 0 ? count : null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Read the consolidation stamp.
|
|
176
|
-
*
|
|
177
|
-
* `at` is the ISO timestamp of the last pass, or `null` when there was none:
|
|
178
|
-
* a missing, unreadable, unparseable, date-less or **future-dated** stamp is
|
|
179
|
-
* indistinguishable from "never consolidated" — all five mean the same thing
|
|
180
|
-
* to the advisory. A future date is the one that has to be caught here rather
|
|
181
|
-
* than downstream: it is arithmetically valid, so the age arm would score it
|
|
182
|
-
* as a negative age and stay silent for as long as the clock stays behind it.
|
|
183
|
-
* A stamp whose date is unusable carries no baseline either, so `baseline`
|
|
184
|
-
* follows it to `null` rather than describing a pass that cannot be dated.
|
|
185
|
-
*
|
|
186
|
-
* `baseline` is the entry count that pass left behind — the growth arm's
|
|
187
|
-
* reference point. It is `null` on a stamp that predates Story #5182 (date
|
|
188
|
-
* only) and on a malformed count, which reads as *unmeasured growth*, never
|
|
189
|
-
* as zero growth: a `0` baseline would score the whole pool as new.
|
|
190
|
-
*
|
|
191
|
-
* @param {{ poolDir: string, fsImpl: object, now: Date|string|number }} args
|
|
192
|
-
* @returns {{ at: string|null, baseline: number|null }}
|
|
193
|
-
*/
|
|
194
|
-
function readStamp({ poolDir, fsImpl, now }) {
|
|
195
|
-
const parsed = probe(() =>
|
|
196
|
-
JSON.parse(fsImpl.readFileSync(path.join(poolDir, STAMP_FILENAME), 'utf8')),
|
|
197
|
-
);
|
|
198
|
-
const at = parsed?.lastConsolidatedAt;
|
|
199
|
-
// `Date.parse` rejects the empty string as NaN, so one test covers both an
|
|
200
|
-
// absent date and an unusable one; `> now` covers the future-dated stamp.
|
|
201
|
-
// Equality is not the future, so a stamp written this instant still counts.
|
|
202
|
-
const at_ms = typeof at === 'string' ? Date.parse(at) : Number.NaN;
|
|
203
|
-
if (Number.isNaN(at_ms) || at_ms > new Date(now).getTime()) {
|
|
204
|
-
return { at: null, baseline: null };
|
|
205
|
-
}
|
|
206
|
-
return { at, baseline: readBaseline(parsed.entryCount) };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
127
|
/**
|
|
210
128
|
* The index file's size in bytes.
|
|
211
129
|
*
|
|
212
130
|
* `null` when it cannot be stat'd — an absent or unreadable `MEMORY.md`
|
|
213
|
-
* leaves the
|
|
214
|
-
*
|
|
215
|
-
* the
|
|
216
|
-
*
|
|
131
|
+
* leaves the arm silent rather than guessing a size, on the same fail-soft
|
|
132
|
+
* rule every other probe here follows. Stat'd rather than read: the arm needs
|
|
133
|
+
* the length, never the content, and this module deliberately never reads a
|
|
134
|
+
* memory's text.
|
|
217
135
|
*
|
|
218
136
|
* @returns {number|null}
|
|
219
137
|
*/
|
|
@@ -247,122 +165,40 @@ function countEntries({ poolDir, fsImpl }) {
|
|
|
247
165
|
*
|
|
248
166
|
* @param {object} fields
|
|
249
167
|
* @returns {{ present: boolean, entryCount: number, indexBytes: number|null,
|
|
250
|
-
*
|
|
251
|
-
* entriesSinceConsolidation: number|null, recommend: boolean,
|
|
252
|
-
* reasons: string[] }}
|
|
168
|
+
* recommend: boolean, reasons: string[] }}
|
|
253
169
|
*/
|
|
254
170
|
function envelope(fields) {
|
|
255
171
|
return {
|
|
256
172
|
present: false,
|
|
257
173
|
entryCount: 0,
|
|
258
174
|
indexBytes: null,
|
|
259
|
-
lastConsolidatedAt: null,
|
|
260
|
-
entriesSinceConsolidation: null,
|
|
261
175
|
recommend: false,
|
|
262
176
|
reasons: [],
|
|
263
177
|
...fields,
|
|
264
178
|
};
|
|
265
179
|
}
|
|
266
180
|
|
|
267
|
-
/**
|
|
268
|
-
* Collect the reasons a pool wants a consolidation pass. An empty array is
|
|
269
|
-
* the quiet verdict; the caller turns it into `recommend` and supplies the
|
|
270
|
-
* standing-down sentence, so every arm lives in one place.
|
|
271
|
-
*
|
|
272
|
-
* The three arms are independent and every one that fires is reported.
|
|
273
|
-
*
|
|
274
|
-
* @param {{ stamp: { at: string|null, baseline: number|null },
|
|
275
|
-
* growth: number|null, indexBytes: number|null,
|
|
276
|
-
* now: Date|string|number, staleAfterDays: number,
|
|
277
|
-
* growthDelta: number, indexByteCeiling: number }} args
|
|
278
|
-
* @returns {string[]}
|
|
279
|
-
*/
|
|
280
|
-
function collectReasons({
|
|
281
|
-
stamp,
|
|
282
|
-
growth,
|
|
283
|
-
indexBytes,
|
|
284
|
-
now,
|
|
285
|
-
staleAfterDays,
|
|
286
|
-
growthDelta,
|
|
287
|
-
indexByteCeiling,
|
|
288
|
-
}) {
|
|
289
|
-
const reasons = [];
|
|
290
|
-
|
|
291
|
-
if (stamp.at === null) {
|
|
292
|
-
reasons.push(
|
|
293
|
-
'no consolidation stamp — this pool has never been consolidated',
|
|
294
|
-
);
|
|
295
|
-
} else {
|
|
296
|
-
const ageDays =
|
|
297
|
-
(new Date(now).getTime() - Date.parse(stamp.at)) / MS_PER_DAY;
|
|
298
|
-
if (ageDays > staleAfterDays) {
|
|
299
|
-
reasons.push(
|
|
300
|
-
`last consolidated ${Math.floor(ageDays)} days ago (over the ${staleAfterDays}-day threshold)`,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// `growth === null` is unmeasured, not zero — a pre-#5182 stamp carries no
|
|
306
|
-
// baseline, and guessing one would re-invent the ceiling this arm replaced.
|
|
307
|
-
if (growth !== null && growth >= growthDelta) {
|
|
308
|
-
reasons.push(
|
|
309
|
-
`${growth} entries written since the last consolidation (at or over the ${growthDelta}-entry growth delta)`,
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// `indexBytes === null` is an unreadable index, not a small one.
|
|
314
|
-
if (indexBytes !== null && indexBytes > indexByteCeiling) {
|
|
315
|
-
reasons.push(
|
|
316
|
-
`${INDEX_FILENAME} is ${indexBytes} bytes, ${indexBytes - indexByteCeiling} over the ${indexByteCeiling}-byte index ceiling — the index is truncated at the cap, so every entry listed after the cut is invisible to every session`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return reasons;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* The sentence a quiet pool explains itself with — one per reason it is quiet,
|
|
325
|
-
* so "nothing to do" never reads the same as "nothing measurable".
|
|
326
|
-
*
|
|
327
|
-
* @param {{ growth: number|null, growthDelta: number }} args
|
|
328
|
-
* @returns {string}
|
|
329
|
-
*/
|
|
330
|
-
function quietReason({ growth, growthDelta }) {
|
|
331
|
-
if (growth === null) {
|
|
332
|
-
return 'memory pool is within the freshness and index-size thresholds; growth is unmeasured until the next /memory-consolidate stamps an entry count';
|
|
333
|
-
}
|
|
334
|
-
return `memory pool is within every threshold — ${growth} entries written since the last consolidation (under the ${growthDelta}-entry growth delta)`;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
181
|
/**
|
|
338
182
|
* Build the `memoryPoolAdvisory` envelope field.
|
|
339
183
|
*
|
|
340
|
-
* Advisory only — it carries **no routing authority
|
|
341
|
-
*
|
|
342
|
-
*
|
|
184
|
+
* Advisory only — it carries **no routing authority**. The `/mandrel-plan`
|
|
185
|
+
* spine surfaces `recommend` at Gate #1 on one advisory line; nothing
|
|
186
|
+
* auto-runs, and nothing here mutates the operator's memory store.
|
|
343
187
|
*
|
|
344
188
|
* @param {object} [opts]
|
|
345
189
|
* @param {string} [opts.cwd] — defaults to `process.cwd()`
|
|
346
190
|
* @param {Record<string,string|undefined>} [opts.env]
|
|
347
191
|
* @param {object} [opts.fsImpl] — node:fs-compatible seam
|
|
348
192
|
* @param {string} [opts.homedir]
|
|
349
|
-
* @param {Date|string|number} [opts.now]
|
|
350
|
-
* @param {number} [opts.staleAfterDays]
|
|
351
|
-
* @param {number} [opts.growthDelta]
|
|
352
193
|
* @param {number} [opts.indexByteCeiling]
|
|
353
194
|
* @returns {{ present: boolean, entryCount: number, indexBytes: number|null,
|
|
354
|
-
*
|
|
355
|
-
* entriesSinceConsolidation: number|null, recommend: boolean,
|
|
356
|
-
* reasons: string[] }}
|
|
195
|
+
* recommend: boolean, reasons: string[] }}
|
|
357
196
|
*/
|
|
358
197
|
export function buildMemoryPoolAdvisory({
|
|
359
198
|
cwd = process.cwd(),
|
|
360
199
|
env = process.env,
|
|
361
200
|
fsImpl = defaultFs,
|
|
362
201
|
homedir,
|
|
363
|
-
now = new Date(),
|
|
364
|
-
staleAfterDays = STALE_AFTER_DAYS,
|
|
365
|
-
growthDelta = GROWTH_DELTA,
|
|
366
202
|
indexByteCeiling = INDEX_BYTE_CEILING,
|
|
367
203
|
} = {}) {
|
|
368
204
|
const absent = (reason) => envelope({ reasons: [reason] });
|
|
@@ -384,21 +220,10 @@ export function buildMemoryPoolAdvisory({
|
|
|
384
220
|
return absent(`memory pool at ${poolDir} could not be listed`);
|
|
385
221
|
}
|
|
386
222
|
|
|
387
|
-
const stamp = readStamp({ poolDir, fsImpl, now });
|
|
388
|
-
// Reported raw: a pruning pass can leave this negative, and saying the pool
|
|
389
|
-
// shrank by 7 is more use to the operator than clamping it to zero.
|
|
390
|
-
const growth = stamp.baseline === null ? null : entryCount - stamp.baseline;
|
|
391
223
|
const indexBytes = readIndexBytes({ poolDir, fsImpl });
|
|
224
|
+
const found = { present: true, entryCount, indexBytes };
|
|
392
225
|
|
|
393
|
-
|
|
394
|
-
present: true,
|
|
395
|
-
entryCount,
|
|
396
|
-
indexBytes,
|
|
397
|
-
lastConsolidatedAt: stamp.at,
|
|
398
|
-
entriesSinceConsolidation: growth,
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
// An empty pool has nothing to consolidate, whatever the stamp says.
|
|
226
|
+
// An empty pool has nothing to consolidate, whatever the index says.
|
|
402
227
|
if (entryCount === 0) {
|
|
403
228
|
return envelope({
|
|
404
229
|
...found,
|
|
@@ -406,20 +231,33 @@ export function buildMemoryPoolAdvisory({
|
|
|
406
231
|
});
|
|
407
232
|
}
|
|
408
233
|
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
indexBytes,
|
|
413
|
-
now,
|
|
414
|
-
staleAfterDays,
|
|
415
|
-
growthDelta,
|
|
416
|
-
indexByteCeiling,
|
|
417
|
-
});
|
|
234
|
+
const { recommend, reason } = judgeIndex(indexBytes, indexByteCeiling);
|
|
235
|
+
return envelope({ ...found, recommend, reasons: [reason] });
|
|
236
|
+
}
|
|
418
237
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
238
|
+
/**
|
|
239
|
+
* The index byte arm's verdict. `indexBytes === null` is an unreadable index,
|
|
240
|
+
* not a small one, so it stays quiet and says why.
|
|
241
|
+
*
|
|
242
|
+
* @param {number|null} indexBytes
|
|
243
|
+
* @param {number} indexByteCeiling
|
|
244
|
+
* @returns {{ recommend: boolean, reason: string }}
|
|
245
|
+
*/
|
|
246
|
+
function judgeIndex(indexBytes, indexByteCeiling) {
|
|
247
|
+
if (indexBytes === null) {
|
|
248
|
+
return {
|
|
249
|
+
recommend: false,
|
|
250
|
+
reason: `memory pool present but ${INDEX_FILENAME} could not be measured — the index ceiling is the only arm and it is unmeasured`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (indexBytes > indexByteCeiling) {
|
|
254
|
+
return {
|
|
255
|
+
recommend: true,
|
|
256
|
+
reason: `${INDEX_FILENAME} is ${indexBytes} bytes, ${indexBytes - indexByteCeiling} over the ${indexByteCeiling}-byte index ceiling — the index is truncated at the cap, so every entry listed after the cut is invisible to every session`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
recommend: false,
|
|
261
|
+
reason: `${INDEX_FILENAME} is ${indexBytes} bytes, within the ${indexByteCeiling}-byte index ceiling`,
|
|
262
|
+
};
|
|
425
263
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/review-base-ref.js — the base ref a Story-scope review is
|
|
3
|
+
* allowed to measure against (Story #5325).
|
|
4
|
+
*
|
|
5
|
+
* ## The defect this closes
|
|
6
|
+
*
|
|
7
|
+
* A close runs base-sync and the Story-scope review against what everyone
|
|
8
|
+
* called "the base branch" — but the two meant different refs. Base-sync
|
|
9
|
+
* fetches and merges `origin/<base>`; the review passed the **bare** branch
|
|
10
|
+
* name, which git resolves to the LOCAL `refs/heads/<base>`, i.e. to whatever
|
|
11
|
+
* that ref last fast-forwarded to. On a checkout whose local base is behind
|
|
12
|
+
* its remote, the review's `<base>...<head>` diff therefore contains every
|
|
13
|
+
* commit the local ref is missing — other people's landed work, scored as if
|
|
14
|
+
* this Story had written it. Those findings gate the land, and the operator's
|
|
15
|
+
* only exit is a `review-block-overridden` on blockers that were never real.
|
|
16
|
+
*
|
|
17
|
+
* ## Contract
|
|
18
|
+
*
|
|
19
|
+
* One resolution, at the review phase boundary, threaded into the change-set
|
|
20
|
+
* enumeration, the provider review and the local lens pass — so both arms of
|
|
21
|
+
* the review agree about what changed and neither can inherit local drift.
|
|
22
|
+
*
|
|
23
|
+
* Resolution **fails safe rather than falling back**. Base-sync normally
|
|
24
|
+
* guarantees the remote ref is present, but a `--skip-sync` close or a
|
|
25
|
+
* remote-less checkout can reach the review with no `origin/<base>` at all.
|
|
26
|
+
* Reviewing the local ref anyway is the defect, so an unresolvable base
|
|
27
|
+
* produces a degradation record — carried on the review's existing
|
|
28
|
+
* `degraded` / `degradations[]` envelope — and no findings whatsoever.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { gitSpawn } from '../git-utils.js';
|
|
32
|
+
import { degradationEnvelope } from './review-providers/degraded-gates.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The remote-tracking spelling of a base branch — `main` → `origin/main`.
|
|
36
|
+
* Already-qualified input passes through, so a caller naming `origin/main`
|
|
37
|
+
* is not double-prefixed.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} baseBranch
|
|
40
|
+
* @returns {string|null} the remote-tracking ref, or `null` when unnameable.
|
|
41
|
+
*/
|
|
42
|
+
export function remoteBaseRef(baseBranch) {
|
|
43
|
+
const base = typeof baseBranch === 'string' ? baseBranch.trim() : '';
|
|
44
|
+
if (base.length === 0) return null;
|
|
45
|
+
return base.startsWith('origin/') ? base : `origin/${base}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the base ref base-sync merged from, **verified present** in this
|
|
50
|
+
* clone. See the module header for why there is no local-ref fallback.
|
|
51
|
+
*
|
|
52
|
+
* @param {{
|
|
53
|
+
* baseBranch: string,
|
|
54
|
+
* cwd?: string,
|
|
55
|
+
* gitSpawnFn?: typeof gitSpawn,
|
|
56
|
+
* }} args
|
|
57
|
+
* @returns {{ ref: string|null, resolved: boolean, remoteRef: string|null }}
|
|
58
|
+
* `ref` is non-null only when `resolved`; `remoteRef` is the ref that was
|
|
59
|
+
* probed, for the caller's degradation surface.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveSharedBaseRef({
|
|
62
|
+
baseBranch,
|
|
63
|
+
cwd = process.cwd(),
|
|
64
|
+
gitSpawnFn = gitSpawn,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const remoteRef = remoteBaseRef(baseBranch);
|
|
67
|
+
if (remoteRef === null) {
|
|
68
|
+
return { ref: null, resolved: false, remoteRef: null };
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const probe = gitSpawnFn(
|
|
72
|
+
cwd,
|
|
73
|
+
'rev-parse',
|
|
74
|
+
'--verify',
|
|
75
|
+
'--quiet',
|
|
76
|
+
`${remoteRef}^{commit}`,
|
|
77
|
+
);
|
|
78
|
+
if (probe?.status === 0) {
|
|
79
|
+
return { ref: remoteRef, resolved: true, remoteRef };
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// A spawn failure and a missing ref are the same answer here: the shared
|
|
83
|
+
// base cannot be vouched for.
|
|
84
|
+
}
|
|
85
|
+
return { ref: null, resolved: false, remoteRef };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The review outcome for a close that cannot establish the shared base.
|
|
90
|
+
*
|
|
91
|
+
* Shaped as the same envelope a completed review returns — an all-zero
|
|
92
|
+
* severity tally, nothing posted, and the `degraded` / `degradations[]` pair
|
|
93
|
+
* the close and the rendered comment already read — so the surfaces reading
|
|
94
|
+
* it cannot mistake "no findings" for "reviewed and clean". Reported, not
|
|
95
|
+
* blocking, matching the posture of every other degraded review gate.
|
|
96
|
+
*
|
|
97
|
+
* @param {{
|
|
98
|
+
* storyId: number|string,
|
|
99
|
+
* baseBranch: string,
|
|
100
|
+
* remoteRef: string|null,
|
|
101
|
+
* progress: (tag: string, msg: string) => void,
|
|
102
|
+
* progressTag?: string,
|
|
103
|
+
* }} args
|
|
104
|
+
* @returns {object}
|
|
105
|
+
*/
|
|
106
|
+
export function unresolvedBaseReviewOutcome({
|
|
107
|
+
storyId,
|
|
108
|
+
baseBranch,
|
|
109
|
+
remoteRef,
|
|
110
|
+
progress,
|
|
111
|
+
progressTag = 'REVIEW',
|
|
112
|
+
}) {
|
|
113
|
+
const surface = remoteRef ?? `origin/${baseBranch}`;
|
|
114
|
+
progress(
|
|
115
|
+
progressTag,
|
|
116
|
+
`⚠️ Story-scope review for Story #${storyId} did not run: cannot resolve ` +
|
|
117
|
+
`${surface}, the base ref base-sync merges from. Diffing the local ` +
|
|
118
|
+
`${baseBranch} instead would score commits this Story never made, so no ` +
|
|
119
|
+
'findings are raised. Fetch the base ref (or re-run without ' +
|
|
120
|
+
'--skip-sync) to restore the review.',
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
halted: false,
|
|
124
|
+
skipped: true,
|
|
125
|
+
severity: { critical: 0, high: 0, medium: 0, suggestion: 0 },
|
|
126
|
+
posted: false,
|
|
127
|
+
postedCommentId: null,
|
|
128
|
+
...degradationEnvelope([
|
|
129
|
+
{
|
|
130
|
+
tool: 'story-scope-review',
|
|
131
|
+
gate: 'base-ref-resolution',
|
|
132
|
+
surface,
|
|
133
|
+
reason: 'remote-base-ref-unresolved',
|
|
134
|
+
},
|
|
135
|
+
]),
|
|
136
|
+
crossRefPosted: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -78,6 +78,7 @@ import { runPreGateSteps as defaultRunPreGateSteps } from './pre-gate-steps.js';
|
|
|
78
78
|
* runPreGateSteps?: typeof defaultRunPreGateSteps,
|
|
79
79
|
* runScopedFormatAutofix?: Function,
|
|
80
80
|
* runBaselineUpwardWriteback?: Function,
|
|
81
|
+
* runContextBudgetWriteback?: Function,
|
|
81
82
|
* createGateLogSink?: typeof defaultCreateGateLogSink,
|
|
82
83
|
* }} args
|
|
83
84
|
* @returns {Promise<{ gates: Record<string, 'passed'|'skipped'> }>} Per-gate
|
|
@@ -98,6 +99,7 @@ export async function runCloseValidationPhase({
|
|
|
98
99
|
runPreGateSteps = defaultRunPreGateSteps,
|
|
99
100
|
runScopedFormatAutofix,
|
|
100
101
|
runBaselineUpwardWriteback,
|
|
102
|
+
runContextBudgetWriteback,
|
|
101
103
|
createGateLogSink = defaultCreateGateLogSink,
|
|
102
104
|
}) {
|
|
103
105
|
await runPreGateSteps({
|
|
@@ -110,6 +112,7 @@ export async function runCloseValidationPhase({
|
|
|
110
112
|
progress,
|
|
111
113
|
runScopedFormatAutofix,
|
|
112
114
|
runBaselineUpwardWriteback,
|
|
115
|
+
runContextBudgetWriteback,
|
|
113
116
|
});
|
|
114
117
|
|
|
115
118
|
progress(
|
|
@@ -124,6 +127,8 @@ export async function runCloseValidationPhase({
|
|
|
124
127
|
baseBranch,
|
|
125
128
|
cwd: worktreePath || cwd,
|
|
126
129
|
log: gateLog.log,
|
|
130
|
+
storyId, // Story #5313 — a credited bare `npm test` registers `test`.
|
|
131
|
+
evidenceCwd: cwd,
|
|
127
132
|
});
|
|
128
133
|
let validation;
|
|
129
134
|
try {
|
|
@@ -25,9 +25,21 @@
|
|
|
25
25
|
* shares a single invocation pattern (Story #3653). Review depth needs no
|
|
26
26
|
* input here: it is derived from this Story's own diff inside `runCodeReview`
|
|
27
27
|
* (Story #4542).
|
|
28
|
+
*
|
|
29
|
+
* Story #5325 — this phase boundary is where the base ref is resolved, once.
|
|
30
|
+
* The resolved ref threads into `runStoryReviewCore`, which hands it to both
|
|
31
|
+
* the change-set enumeration (and through it the provider review) and the
|
|
32
|
+
* local lens pass, so one resolution corrects both arms. It resolves to
|
|
33
|
+
* `origin/<baseBranch>` — the ref base-sync merged from — rather than the bare
|
|
34
|
+
* branch name git would resolve to the local `refs/heads/<baseBranch>`, whose
|
|
35
|
+
* drift would otherwise be scored as this Story's own change.
|
|
28
36
|
*/
|
|
29
37
|
|
|
30
38
|
import { parsePrNumberFromUrl } from '../../../github-url.js';
|
|
39
|
+
import {
|
|
40
|
+
resolveSharedBaseRef,
|
|
41
|
+
unresolvedBaseReviewOutcome,
|
|
42
|
+
} from '../../review-base-ref.js';
|
|
31
43
|
import { degradationEnvelope } from '../../review-providers/degraded-gates.js';
|
|
32
44
|
import { runStoryReviewCore } from '../../story-close/phases/review-core.js';
|
|
33
45
|
import { postStructuredComment } from '../../ticketing/state.js';
|
|
@@ -78,23 +90,25 @@ export function buildStoryReviewCrossRefBody({
|
|
|
78
90
|
async function invokeStoryReviewCore({
|
|
79
91
|
storyId,
|
|
80
92
|
storyBranch,
|
|
81
|
-
|
|
93
|
+
baseRef,
|
|
82
94
|
prNumber,
|
|
83
95
|
provider,
|
|
84
96
|
runCodeReviewFn,
|
|
85
97
|
runLocalLensReviewFn,
|
|
86
98
|
appendFindingsYieldFn,
|
|
99
|
+
gitSpawnFn,
|
|
87
100
|
progress,
|
|
88
101
|
}) {
|
|
89
102
|
return runStoryReviewCore({
|
|
90
103
|
storyId,
|
|
91
|
-
baseRef
|
|
104
|
+
baseRef,
|
|
92
105
|
headRef: storyBranch,
|
|
93
106
|
commentTargetId: prNumber,
|
|
94
107
|
provider,
|
|
95
108
|
progress,
|
|
96
109
|
progressTag: 'REVIEW',
|
|
97
110
|
runCodeReviewFn,
|
|
111
|
+
gitSpawnFn,
|
|
98
112
|
// Forward the seams only when the caller injects them; otherwise
|
|
99
113
|
// `runStoryReviewCore` uses its defaults. `undefined` deep-merges to
|
|
100
114
|
// the default via the destructuring default there.
|
|
@@ -153,6 +167,9 @@ async function postStoryReviewCrossRef({
|
|
|
153
167
|
* Failure modes:
|
|
154
168
|
* - When `prNumber` is null (couldn't parse), the review is skipped
|
|
155
169
|
* and the function returns `{ halted: false, skipped: true }`.
|
|
170
|
+
* - When `origin/<baseBranch>` cannot be resolved, the review is skipped,
|
|
171
|
+
* a `base-ref-resolution` degradation is recorded on the returned
|
|
172
|
+
* envelope, and no findings are raised (Story #5325).
|
|
156
173
|
* - When the runner throws, the close fails non-zero (the throw
|
|
157
174
|
* propagates) — a Story-scope review failure is not silently
|
|
158
175
|
* ignored.
|
|
@@ -170,6 +187,7 @@ async function postStoryReviewCrossRef({
|
|
|
170
187
|
* runCodeReviewFn: Function,
|
|
171
188
|
* runLocalLensReviewFn?: Function,
|
|
172
189
|
* appendFindingsYieldFn?: Function,
|
|
190
|
+
* gitSpawnFn?: Function,
|
|
173
191
|
* progress: (tag: string, msg: string) => void,
|
|
174
192
|
* }} args
|
|
175
193
|
* @returns {Promise<{
|
|
@@ -185,7 +203,7 @@ async function postStoryReviewCrossRef({
|
|
|
185
203
|
* }>}
|
|
186
204
|
*/
|
|
187
205
|
export async function runStoryScopeReview({
|
|
188
|
-
cwd
|
|
206
|
+
cwd,
|
|
189
207
|
storyId,
|
|
190
208
|
storyBranch,
|
|
191
209
|
baseBranch,
|
|
@@ -195,6 +213,7 @@ export async function runStoryScopeReview({
|
|
|
195
213
|
runCodeReviewFn,
|
|
196
214
|
runLocalLensReviewFn,
|
|
197
215
|
appendFindingsYieldFn,
|
|
216
|
+
gitSpawnFn,
|
|
198
217
|
progress,
|
|
199
218
|
}) {
|
|
200
219
|
if (prNumber == null) {
|
|
@@ -205,20 +224,33 @@ export async function runStoryScopeReview({
|
|
|
205
224
|
return { halted: false, skipped: true };
|
|
206
225
|
}
|
|
207
226
|
|
|
227
|
+
// One resolution per close, at the phase boundary: `baseRef` threads from
|
|
228
|
+
// here into the change set, the provider review and the local lens pass.
|
|
229
|
+
const base = resolveSharedBaseRef({ baseBranch, cwd, gitSpawnFn });
|
|
230
|
+
if (!base.resolved) {
|
|
231
|
+
return unresolvedBaseReviewOutcome({
|
|
232
|
+
storyId,
|
|
233
|
+
baseBranch,
|
|
234
|
+
remoteRef: base.remoteRef,
|
|
235
|
+
progress,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
208
239
|
progress(
|
|
209
240
|
'REVIEW',
|
|
210
|
-
`Running Story-scope code review for Story #${storyId} (${
|
|
241
|
+
`Running Story-scope code review for Story #${storyId} (${base.ref}...${storyBranch}) → PR #${prNumber}...`,
|
|
211
242
|
);
|
|
212
243
|
|
|
213
244
|
const result = await invokeStoryReviewCore({
|
|
214
245
|
storyId,
|
|
215
246
|
storyBranch,
|
|
216
|
-
|
|
247
|
+
baseRef: base.ref,
|
|
217
248
|
prNumber,
|
|
218
249
|
provider,
|
|
219
250
|
runCodeReviewFn,
|
|
220
251
|
runLocalLensReviewFn,
|
|
221
252
|
appendFindingsYieldFn,
|
|
253
|
+
gitSpawnFn,
|
|
222
254
|
progress,
|
|
223
255
|
});
|
|
224
256
|
|