mandrel 2.56.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- 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/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/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/plan-context.js +181 -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/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -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 +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- 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 +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -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 +266 -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 +41 -57
- 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 +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- 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
|
}
|
|
@@ -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 {
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* maintainability rows this branch improved on files it touched, as a
|
|
19
19
|
* `baseline-refresh:` commit, so the committed baseline stops falling
|
|
20
20
|
* behind the tree in the upward direction.
|
|
21
|
+
* 3. **Context-budget write-back** (Story #5313) — persist the lower
|
|
22
|
+
* documentation-tier totals this branch measured, so a trimmed tier's
|
|
23
|
+
* gain locks in without `check-context-budget.js` ever going red on it.
|
|
21
24
|
*
|
|
22
25
|
* Extracted from `close-validation.js` when the second step landed. The phase
|
|
23
26
|
* module's job is the gate chain, the evidence keyspace and the gate-log sink;
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
|
|
30
33
|
import { Logger } from '../../../Logger.js';
|
|
31
34
|
import { runBaselineUpwardWriteback as defaultRunBaselineUpwardWriteback } from '../../story-close/baseline-upward-writeback.js';
|
|
35
|
+
import { runContextBudgetWriteback as defaultRunContextBudgetWriteback } from '../../story-close/context-budget-writeback.js';
|
|
32
36
|
import { runScopedFormatAutofix as defaultRunScopedFormatAutofix } from '../../story-close/format-autofix.js';
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -69,22 +73,22 @@ function formatAutofixStep({
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* Run the
|
|
76
|
+
* Run one of the two baseline write-backs and report it on one progress line.
|
|
77
|
+
*
|
|
78
|
+
* Both steps (Story #5224's maintainability rows, Story #5313's context-budget
|
|
79
|
+
* totals) take the same context and answer in the same shape — `committed`
|
|
80
|
+
* with a `sha`, or a named `reason` — so one wrapper serves both; `describe`
|
|
81
|
+
* renders the committed outcome in the step's own words.
|
|
73
82
|
*
|
|
74
83
|
* @param {object} ctx the shared step context (see {@link runPreGateSteps})
|
|
84
|
+
* @param {{ tag: string, run: Function, describe: (w: object) => string, noun: string }} step
|
|
75
85
|
* @returns {Promise<void>}
|
|
76
86
|
*/
|
|
77
|
-
async function
|
|
78
|
-
cwd,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
storyBranch,
|
|
83
|
-
config,
|
|
84
|
-
progress,
|
|
85
|
-
runBaselineUpwardWriteback,
|
|
86
|
-
}) {
|
|
87
|
-
const writeback = await runBaselineUpwardWriteback({
|
|
87
|
+
async function writebackStep(
|
|
88
|
+
{ cwd, worktreePath, storyId, baseBranch, storyBranch, config, progress },
|
|
89
|
+
{ tag, run, describe, noun },
|
|
90
|
+
) {
|
|
91
|
+
const writeback = await run({
|
|
88
92
|
cwd,
|
|
89
93
|
worktreePath,
|
|
90
94
|
storyId,
|
|
@@ -94,10 +98,10 @@ async function baselineWritebackStep({
|
|
|
94
98
|
logger: Logger,
|
|
95
99
|
});
|
|
96
100
|
progress(
|
|
97
|
-
|
|
101
|
+
tag,
|
|
98
102
|
writeback?.committed
|
|
99
|
-
? `✅
|
|
100
|
-
: `⏭ No
|
|
103
|
+
? `✅ ${describe(writeback)} as ${writeback.sha} on ${storyBranch}.`
|
|
104
|
+
: `⏭ No ${noun} (${writeback?.reason ?? 'nothing to write'}).`,
|
|
101
105
|
);
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -142,18 +146,24 @@ async function bestEffort({ tag, label, progress, run }) {
|
|
|
142
146
|
* progress: (tag: string, msg: string) => void,
|
|
143
147
|
* runScopedFormatAutofix?: typeof defaultRunScopedFormatAutofix,
|
|
144
148
|
* runBaselineUpwardWriteback?: typeof defaultRunBaselineUpwardWriteback,
|
|
149
|
+
* runContextBudgetWriteback?: typeof defaultRunContextBudgetWriteback,
|
|
145
150
|
* }} args
|
|
146
151
|
* @returns {Promise<void>}
|
|
147
152
|
*/
|
|
148
153
|
export async function runPreGateSteps({
|
|
149
154
|
runScopedFormatAutofix = defaultRunScopedFormatAutofix,
|
|
150
155
|
runBaselineUpwardWriteback = defaultRunBaselineUpwardWriteback,
|
|
156
|
+
runContextBudgetWriteback = defaultRunContextBudgetWriteback,
|
|
151
157
|
...ctx
|
|
152
158
|
}) {
|
|
153
159
|
const { storyBranch, progress } = ctx;
|
|
154
160
|
if (!storyBranch) {
|
|
155
161
|
progress('FORMAT', '⏭ Skipped scoped format-autofix (no story branch).');
|
|
156
162
|
progress('BASELINE', '⏭ Skipped baseline write-back (no story branch).');
|
|
163
|
+
progress(
|
|
164
|
+
'BUDGET',
|
|
165
|
+
'⏭ Skipped context-budget write-back (no story branch).',
|
|
166
|
+
);
|
|
157
167
|
return;
|
|
158
168
|
}
|
|
159
169
|
await bestEffort({
|
|
@@ -166,6 +176,26 @@ export async function runPreGateSteps({
|
|
|
166
176
|
tag: 'BASELINE',
|
|
167
177
|
label: 'baseline write-back',
|
|
168
178
|
progress,
|
|
169
|
-
run: () =>
|
|
179
|
+
run: () =>
|
|
180
|
+
writebackStep(ctx, {
|
|
181
|
+
tag: 'BASELINE',
|
|
182
|
+
run: runBaselineUpwardWriteback,
|
|
183
|
+
noun: 'baseline write-back',
|
|
184
|
+
describe: (w) =>
|
|
185
|
+
`Wrote back ${w.improvedPaths?.length ?? 0} improved maintainability row(s)`,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
await bestEffort({
|
|
189
|
+
tag: 'BUDGET',
|
|
190
|
+
label: 'context-budget write-back',
|
|
191
|
+
progress,
|
|
192
|
+
run: () =>
|
|
193
|
+
writebackStep(ctx, {
|
|
194
|
+
tag: 'BUDGET',
|
|
195
|
+
run: runContextBudgetWriteback,
|
|
196
|
+
noun: 'context-budget write-back',
|
|
197
|
+
describe: (w) =>
|
|
198
|
+
`Wrote back lower context-budget totals (${w.tiers?.join(', ')})`,
|
|
199
|
+
}),
|
|
170
200
|
});
|
|
171
201
|
}
|