mandrel 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +4 -30
  4. package/.agents/docs/configuration.md +11 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/rules/ci-remediation.md +39 -21
  9. package/.agents/schemas/agentrc.schema.json +28 -185
  10. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  11. package/.agents/scripts/acceptance-eval.js +107 -17
  12. package/.agents/scripts/audit-to-stories.js +222 -75
  13. package/.agents/scripts/ceremony-derive.js +191 -0
  14. package/.agents/scripts/check-context-budget.js +28 -33
  15. package/.agents/scripts/check-cyclomatic.js +4 -3
  16. package/.agents/scripts/deliver-light.js +31 -94
  17. package/.agents/scripts/file-ci-gap.js +306 -0
  18. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  19. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
  20. package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
  21. package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
  22. package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
  23. package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
  24. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
  25. package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
  26. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
  27. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  28. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  29. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  30. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  31. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  32. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  33. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  34. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  35. package/.agents/scripts/lib/config/explain.js +0 -19
  36. package/.agents/scripts/lib/config/limits.js +18 -78
  37. package/.agents/scripts/lib/config/quality.js +6 -3
  38. package/.agents/scripts/lib/config/runners.js +3 -2
  39. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  40. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  41. package/.agents/scripts/lib/config-settings-schema.js +49 -143
  42. package/.agents/scripts/lib/crap-engine.js +35 -4
  43. package/.agents/scripts/lib/crap-utils.js +17 -1
  44. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  45. package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
  46. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
  47. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
  48. package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
  49. package/.agents/scripts/lib/findings/route-finding.js +38 -0
  50. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  51. package/.agents/scripts/lib/github/framework-repo.js +148 -2
  52. package/.agents/scripts/lib/label-constants.js +6 -1
  53. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  54. package/.agents/scripts/lib/observability/source-classifier.js +2 -0
  55. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  56. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  57. package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
  58. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
  59. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  60. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  61. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  62. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  63. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  64. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  65. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  66. package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
  67. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  68. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  69. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +133 -299
  70. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  71. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  72. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  73. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  74. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  75. package/.agents/scripts/lib/orchestration/run-epilogue.js +4 -4
  76. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  77. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  78. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  79. package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
  80. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  81. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  82. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  83. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  84. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  85. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  86. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  87. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  88. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  89. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  90. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  91. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  92. package/.agents/scripts/lib/test-run-credit.js +266 -0
  93. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  94. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  95. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  96. package/.agents/scripts/plan-context.js +7 -9
  97. package/.agents/scripts/plan-critics.js +28 -54
  98. package/.agents/scripts/plan-persist.js +25 -68
  99. package/.agents/scripts/pr-watch-with-update.js +3 -2
  100. package/.agents/scripts/quality-preview.js +51 -0
  101. package/.agents/scripts/run-tests.js +12 -0
  102. package/.agents/scripts/stories-wave-tick.js +23 -45
  103. package/.agents/scripts/test-isolate.js +13 -180
  104. package/.agents/scripts/update-coverage-baseline.js +25 -70
  105. package/.agents/scripts/update-crap-baseline.js +19 -123
  106. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  107. package/.agents/workflows/audit-clean-code.md +4 -3
  108. package/.agents/workflows/audit-to-stories.md +63 -27
  109. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  110. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  111. package/.agents/workflows/helpers/code-review.md +2 -3
  112. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  113. package/.agents/workflows/helpers/deliver-light.md +40 -105
  114. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  115. package/.agents/workflows/helpers/deliver-story-reference.md +56 -62
  116. package/.agents/workflows/helpers/deliver-story.md +9 -13
  117. package/.agents/workflows/helpers/plan-reference.md +132 -196
  118. package/.agents/workflows/mandrel-plan.md +28 -41
  119. package/.agents/workflows/memory-consolidate.md +9 -13
  120. package/docs/CHANGELOG.md +33 -0
  121. package/lib/migrations/index.js +4 -0
  122. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  123. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  124. package/package.json +1 -1
  125. package/.agents/scripts/lib/framework-version.js +0 -39
  126. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  127. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  128. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  129. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  130. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  131. 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 also drops the semantic that made the old scanner unfixable: it renders
17
- * **no per-entry verdict at all**. A memory citing a closed issue is a
18
- * delivery retrospective whose subject is that issue — not a stale entry — and
19
- * only the attended `/memory-consolidate` pass, reading content, can tell the
20
- * difference. This module counts and stats; it never judges an entry.
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
- * **Growth, never size (Story #5182).** The second arm used to be an absolute
23
- * ceiling of a hundred entries. A consolidation pass prefers `correct` over
24
- * `dead` by design, so a pool that crosses a fixed ceiling stays over it
25
- * forever: the nudge then fired on every plan however fresh the stamp, and a
26
- * permanent recommendation is one the operator learns to ignore. The arm now
27
- * measures **entries written since the last pass** — the one quantity a pass
28
- * actually resets, because Step 6 records the post-rewrite entry count in the
29
- * stamp as the next run's growth baseline.
30
- *
31
- * A stamp carrying a date but no usable `entryCount` (every stamp written
32
- * before that Story) leaves growth **unmeasured**. That is not
33
- * "never consolidated" — an operator did review the pool — so the growth arm
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` / `readFileSync`), `now`, and the three thresholds
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 (the `buildUiSurfaceSignal`
66
- * precedent). Tests reach every branch through the seams above — do not
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 all four probes had the same try/catch shape wrapped
145
- * around one expression. One helper states the rule once; a new probe cannot
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 byte arm silent rather than guessing a size, on the same
214
- * fail-soft rule every other probe here follows. Stat'd rather than read:
215
- * the arm needs the length, never the content, and this module deliberately
216
- * never reads a memory's text.
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
- * lastConsolidatedAt: string|null,
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**, mirroring
341
- * `deliverLightSuggestion`. The `/mandrel-plan` spine surfaces `recommend` at Gate #1;
342
- * nothing auto-runs, and nothing here mutates the operator's memory store.
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
- * lastConsolidatedAt: string|null,
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
- const found = {
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 reasons = collectReasons({
410
- stamp,
411
- growth,
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
- return envelope({
420
- ...found,
421
- recommend: reasons.length > 0,
422
- reasons:
423
- reasons.length > 0 ? reasons : [quietReason({ growth, growthDelta })],
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
  }
@@ -683,10 +683,10 @@ async function executeFollowUpRollup({
683
683
  provider,
684
684
  config,
685
685
  currentRepo: repos.currentRepo,
686
- frameworkRepo: (() => {
687
- const [owner, repo] = repos.frameworkRepo.split('/');
688
- return { owner, repo };
689
- })(),
686
+ // The resolved bucket object, not a re-split of the slug: routing is
687
+ // decided once in `github/framework-repo.js`.
688
+ frameworkRepo: repos.repos.framework,
689
+ platformRepo: repos.repos.platform,
690
690
  routedProposals: proposals,
691
691
  cwd,
692
692
  });
@@ -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 upward maintainability write-back.
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 baselineWritebackStep({
78
- cwd,
79
- worktreePath,
80
- storyId,
81
- baseBranch,
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
- 'BASELINE',
101
+ tag,
98
102
  writeback?.committed
99
- ? `✅ Wrote back ${writeback.improvedPaths?.length ?? 0} improved maintainability row(s) as ${writeback.sha} on ${storyBranch}.`
100
- : `⏭ No baseline write-back (${writeback?.reason ?? 'nothing to write'}).`,
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: () => baselineWritebackStep({ ...ctx, runBaselineUpwardWriteback }),
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
  }