mandrel 2.18.0 → 2.20.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 (34) hide show
  1. package/.agents/docs/SDLC.md +1 -1
  2. package/.agents/docs/agentrc-reference.json +0 -21
  3. package/.agents/docs/configuration.md +11 -14
  4. package/.agents/docs/execution-reference.md +8 -5
  5. package/.agents/schemas/agentrc.schema.json +0 -31
  6. package/.agents/scripts/check-doc-links.js +141 -9
  7. package/.agents/scripts/check-test-temp-hygiene.js +153 -14
  8. package/.agents/scripts/lib/baselines/env-overrides.js +40 -48
  9. package/.agents/scripts/lib/baselines/kinds/maintainability.js +3 -4
  10. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -2
  11. package/.agents/scripts/lib/config/explain.js +0 -8
  12. package/.agents/scripts/lib/config/temp-paths.js +12 -1
  13. package/.agents/scripts/lib/config-settings-schema.js +8 -24
  14. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +51 -77
  15. package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +20 -12
  16. package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -2
  17. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +2 -1
  18. package/.agents/scripts/lib/orchestration/plan-context.js +13 -14
  19. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +12 -66
  20. package/.agents/scripts/lib/test-env.js +15 -3
  21. package/.agents/scripts/lib/test-temp.js +311 -0
  22. package/.agents/workflows/audit-performance.md +2 -2
  23. package/.agents/workflows/helpers/diagnose.md +1 -1
  24. package/.agents/workflows/helpers/plan-reference.md +19 -4
  25. package/.agents/workflows/helpers/signals.md +2 -2
  26. package/.agents/workflows/mandrel-update.md +4 -4
  27. package/.agents/workflows/plan.md +7 -6
  28. package/docs/CHANGELOG.md +16 -0
  29. package/lib/migrations/index.js +2 -0
  30. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +113 -0
  31. package/package.json +1 -1
  32. package/.agents/scripts/lib/codebase-snapshot.js +0 -513
  33. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +0 -147
  34. package/.agents/scripts/lib/orchestration/spec-freshness.js +0 -129
@@ -83,69 +83,61 @@ export function resolveCrapEnvOverrides(crapConfig, env) {
83
83
  }
84
84
 
85
85
  /**
86
- * Pure helper: resolve the one-shot bundle-size refresh/acknowledge flag
87
- * (Story #151). Unlike `coverage` / `crap` / `maintainability`, the
88
- * bundle-size gate has no scorer of its own the measured sizes come from
89
- * a build step the operator already runs, not a source-tree rescan — so
90
- * there is no `refreshBaseline({ kind: 'bundle-size', ... })` path to
91
- * regenerate a "corrected" baseline. Instead, `BUNDLE_SIZE_REFRESH=1`
92
- * (mirroring `CRAP_TOLERANCE`'s env-override precedent) tells
93
- * `check-baselines --gate bundle-size` to treat this run's head
94
- * measurements as the newly acknowledged baseline: head-vs-base
95
- * regressions are demoted to `unchanged` for this invocation only. Floors
96
- * still apply — an acknowledged PR can still fail on an absolute budget
97
- * breach, only the ratchet-vs-`origin/main` comparison is suspended.
86
+ * The env var that acknowledges a deliberate baseline refresh for `kind`.
87
+ * Upper-snakes the kind name, so `bundle-size` `BUNDLE_SIZE_REFRESH` and
88
+ * `coverage` `COVERAGE_REFRESH`. The two names that predate the generic
89
+ * mechanism (`BUNDLE_SIZE_REFRESH`, Story #151; `MAINTAINABILITY_REFRESH`,
90
+ * Story #4731) are exactly what this rule produces, so generalizing kept
91
+ * both working unchanged.
98
92
  *
99
- * The flag is **not persisted** anywhere (no config write, no committed
100
- * tag): the very next `check-baselines` invocation without the env var
101
- * i.e. the next PR — reverts to full strict enforcement automatically, so
102
- * there is no lingering loosened tolerance to remember to reset (AC-3).
93
+ * Module-local: `resolveKindRefreshOverrides` is the public surface, and the
94
+ * naming rule is pinned through it rather than exported for its own sake.
103
95
  *
104
- * Accepted truthy values: `1`, `true` (case-insensitive). Anything else
105
- * (including unset/empty) resolves to `acknowledged: false`.
106
- *
107
- * @param {NodeJS.ProcessEnv} env
108
- * @returns {{ acknowledged: boolean, overrides: string[] }}
96
+ * @param {string} kind
97
+ * @returns {string|null} null when `kind` is not a usable kind name
109
98
  */
110
- export function resolveBundleSizeEnvOverrides(env) {
111
- const raw = env?.BUNDLE_SIZE_REFRESH;
112
- const acknowledged =
113
- typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
114
- const overrides = acknowledged
115
- ? [`acknowledged=true (BUNDLE_SIZE_REFRESH=${raw})`]
116
- : [];
117
- return { acknowledged, overrides };
99
+ function kindRefreshEnvVar(kind) {
100
+ if (typeof kind !== 'string' || kind.length === 0) return null;
101
+ return `${kind.toUpperCase().replace(/-/g, '_')}_REFRESH`;
118
102
  }
119
103
 
120
104
  /**
121
- * Pure helper: resolve the one-shot maintainability refresh/acknowledge flag
122
- * (Story #4731). This is the env-parity sibling of
123
- * `resolveBundleSizeEnvOverrides`: `MAINTAINABILITY_REFRESH=1` (or `true`,
124
- * case-insensitive) tells `check-baselines --gate maintainability` to demote
125
- * this run's head-vs-base maintainability regressions to `unchanged` for this
126
- * invocation only. Floors still apply — an acknowledged run can still fail on
127
- * an absolute floor breach (e.g. a row below `min` 70); only the
128
- * ratchet-vs-base regression comparison is suspended.
105
+ * Pure helper: resolve the one-shot baseline refresh/acknowledge flag for any
106
+ * ratcheted kind (Story #4802, generalizing Story #151 / Story #4731).
107
+ *
108
+ * `<KIND>_REFRESH=1` tells `check-baselines --gate <kind>` to demote this
109
+ * run's head-vs-base regressions to `unchanged` for this invocation only.
110
+ * Floors still apply — an acknowledged run can still fail on an absolute
111
+ * floor breach; only the ratchet-vs-base comparison is suspended.
112
+ *
113
+ * Why every kind needs this: a diff-scope baseline is an accretion of many
114
+ * partial runs, not one measurement. Replacing it with a single full-scope
115
+ * measurement necessarily produces row deltas in both directions that are
116
+ * arithmetic, not behavioural — so without an acknowledgment path the gate
117
+ * blocks precisely the correction it should encourage.
129
118
  *
130
- * Unlike bundle-size, maintainability also has a **commit-tagged** trigger
131
- * (a `baseline-refresh:`-tagged commit in the compared range that touches the
132
- * maintainability baseline file) resolved in the evaluate phase this env
133
- * flag is the manual override the two share by shape. Neither is persisted:
134
- * the next run without the flag / tag re-enforces the ratchet at full
135
- * strength automatically.
119
+ * The flag is **not persisted** anywhere (no config write, no committed tag):
120
+ * the very next invocation without the env var reverts to full strict
121
+ * enforcement automatically, so there is no lingering loosened tolerance to
122
+ * remember to reset. The evaluate phase pairs this with a commit-tagged
123
+ * trigger that is likewise one-shot by construction.
136
124
  *
137
- * Accepted truthy values: `1`, `true` (case-insensitive). Anything else
138
- * (including unset/empty) resolves to `acknowledged: false`.
125
+ * Accepted truthy values: `1`, `true` (case-insensitive), with surrounding
126
+ * whitespace trimmed. Anything else — including unset, empty, `0`, `false`,
127
+ * and non-string values — resolves to `acknowledged: false`.
139
128
  *
129
+ * @param {string} kind
140
130
  * @param {NodeJS.ProcessEnv} env
141
131
  * @returns {{ acknowledged: boolean, overrides: string[] }}
142
132
  */
143
- export function resolveMaintainabilityRefreshOverrides(env) {
144
- const raw = env?.MAINTAINABILITY_REFRESH;
133
+ export function resolveKindRefreshOverrides(kind, env) {
134
+ const varName = kindRefreshEnvVar(kind);
135
+ if (!varName) return { acknowledged: false, overrides: [] };
136
+ const raw = env?.[varName];
145
137
  const acknowledged =
146
138
  typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
147
139
  const overrides = acknowledged
148
- ? [`acknowledged=true (MAINTAINABILITY_REFRESH=${raw})`]
140
+ ? [`acknowledged=true (${varName}=${raw})`]
149
141
  : [];
150
142
  return { acknowledged, overrides };
151
143
  }
@@ -60,11 +60,10 @@ export const MAINTAINABILITY_EXCLUSIONS = Object.freeze(
60
60
  // the audit-to-stories parser reuses the same regex-property scan
61
61
  // patterns as acceptance-spec-reconciler.
62
62
  '.agents/scripts/lib/audit-to-stories/parse-audit-md.js',
63
- // escomplex: same "pattern" parse failure family — BDD scanner and
64
- // codebase snapshot helpers walk source trees with regex visitors that
65
- // hit the upstream destructuring bug.
63
+ // escomplex: same "pattern" parse failure family — the BDD scanner walks
64
+ // source trees with regex visitors that hit the upstream destructuring
65
+ // bug.
66
66
  '.agents/scripts/lib/bdd-scenario-scanner.js',
67
- '.agents/scripts/lib/codebase-snapshot.js',
68
67
  // escomplex: same "pattern" parse failure — the wave-runner tick uses
69
68
  // the regex-property destructuring escomplex chokes on.
70
69
  '.agents/scripts/lib/wave-runner/tick.js',
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * bdd-scenario-scanner.js — Gherkin scenario index for /plan Phase 7.
3
3
  *
4
- * Story #2637 (sibling to #2634 codebase-snapshot, #2635 spec-freshness,
5
- * #2636 file-assumption gate). The Acceptance Engineer step of
4
+ * Story #2637 (sibling to #2636's file-assumption gate; the #2634 and #2635
5
+ * planner-grounding siblings were retired in Story #4811). The Acceptance
6
+ * Engineer step of
6
7
  * `epic-plan-spec-author` currently writes ACs from Epic/Tech Spec narrative
7
8
  * alone — it never inspects the consumer project's existing `.feature`
8
9
  * files. Planned ACs frequently duplicate scenarios that already exist or
@@ -114,14 +114,6 @@ const KEY_MEANINGS = Object.freeze({
114
114
  'Allowlist of events that fire a webhook notification.',
115
115
 
116
116
  // planning.*
117
- 'planning.codebaseSnapshot.tier':
118
- 'Depth of the structural codebase view threaded into spec authoring.',
119
- 'planning.codebaseSnapshot.include':
120
- 'Glob patterns included in the codebase snapshot.',
121
- 'planning.codebaseSnapshot.exclude':
122
- 'Glob patterns excluded from the codebase snapshot.',
123
- 'planning.codebaseSnapshot.recentCommitWindow':
124
- 'How many recent commits the snapshot summarizes.',
125
117
  'planning.riskHeuristics':
126
118
  'Phrases that flag a Story as high-risk for HITL escalation.',
127
119
  'planning.failOnSharedEditors':
@@ -56,6 +56,8 @@ import { mkdtempSync } from 'node:fs';
56
56
  import os from 'node:os';
57
57
  import path from 'node:path';
58
58
 
59
+ import { reapOnExit } from '../test-temp.js';
60
+
59
61
  /**
60
62
  * Cache the resolved main-checkout root per spawn cwd so the
61
63
  * `git rev-parse` shell-out runs at most once per distinct working
@@ -223,7 +225,7 @@ function inNodeTestContext(env, execArgv) {
223
225
  *
224
226
  * @param {string} tempRoot
225
227
  * @param {NodeJS.ProcessEnv} [env=process.env]
226
- * @param {{ mkdtemp?: typeof mkdtempSync, execArgv?: string[] }} [deps]
228
+ * @param {{ mkdtemp?: typeof mkdtempSync, execArgv?: string[], onExit?: (fn: () => void) => void }} [deps]
227
229
  * Injectable for tests.
228
230
  * @returns {string}
229
231
  */
@@ -238,9 +240,18 @@ export function anchorTempRoot(tempRoot, env = process.env, deps = {}) {
238
240
  ) {
239
241
  if (_testContextScratchDir === null) {
240
242
  const mkdtemp = deps.mkdtemp ?? mkdtempSync;
243
+ // test-temp-allow: published to children below, so it must live
244
+ // outside the per-process suite root that this process reaps.
241
245
  _testContextScratchDir = mkdtemp(
242
246
  path.join(os.tmpdir(), 'mandrel-test-temp-'),
243
247
  );
248
+ // Creator-only reaping (Story #4808): a process that read the root
249
+ // from the env returned at `scratch` above and never reaches here,
250
+ // so it can never remove a root its parent is still writing to.
251
+ reapOnExit(
252
+ _testContextScratchDir,
253
+ deps.onExit ? { onExit: deps.onExit } : {},
254
+ );
244
255
  if (env === process.env) {
245
256
  // Children spawned by this test process inherit the same scratch.
246
257
  process.env[TEST_TEMP_ROOT_ENV] = _testContextScratchDir;
@@ -249,35 +249,19 @@ const GITHUB_SCHEMA = {
249
249
  // rejected as an additional property, so a resurrected key fails loudly rather
250
250
  // than silently doing nothing.
251
251
 
252
- /**
253
- * Story #2634 `planning.codebaseSnapshot` controls the structural
254
- * view of the consumer repo threaded into `/plan` Phase 7 spec
255
- * authoring. Absent / partial entries resolve to defaults inside
256
- * `lib/codebase-snapshot.js#resolveSnapshotConfig` the schema only
257
- * enforces shape (correct enum value, well-formed glob arrays).
258
- */
259
- const CODEBASE_SNAPSHOT_SCHEMA = {
260
- type: 'object',
261
- properties: {
262
- tier: { type: 'string', enum: ['skinny', 'medium'] },
263
- include: {
264
- type: 'array',
265
- items: { type: 'string', minLength: 1 },
266
- },
267
- exclude: {
268
- type: 'array',
269
- items: { type: 'string', minLength: 1 },
270
- },
271
- recentCommitWindow: { type: 'integer', minimum: 1 },
272
- },
273
- additionalProperties: false,
274
- };
252
+ // Story #4811: the `planning` block's structural-snapshot key was retired
253
+ // along with the snapshot itself. The pre-computed view it configured grounded
254
+ // nothing its default include globs missed the standard monorepo layout, and
255
+ // its knobs only re-filtered the same matched set. Spec authoring is grounded
256
+ // by the author's own targeted repo retrieval plus the Phase 8
257
+ // `validateStoryFileAssumptions` gate, neither of which is configurable here.
258
+ // `planning` carries `additionalProperties: false`, so a resurrected key fails
259
+ // loudly; the 2.20.0 retirement migration strips it on upgrade.
275
260
 
276
261
  const PLANNING_SCHEMA = {
277
262
  type: 'object',
278
263
  properties: {
279
264
  riskHeuristics: LIST_OR_EXTENDER_OF_STRINGS,
280
- codebaseSnapshot: CODEBASE_SNAPSHOT_SCHEMA,
281
265
  // Story #4722 (superseding #4683's word-count gate) — shape-derived
282
266
  // ceremony-lite routing. Complexity routes on the objective shape of the
283
267
  // authored work (changes[] count, acceptance count, creates-vs-refactors
@@ -7,10 +7,7 @@
7
7
  * @module lib/orchestration/check-baselines/phases/evaluate
8
8
  */
9
9
 
10
- import {
11
- resolveBundleSizeEnvOverrides,
12
- resolveMaintainabilityRefreshOverrides,
13
- } from '../../../baselines/env-overrides.js';
10
+ import { resolveKindRefreshOverrides } from '../../../baselines/env-overrides.js';
14
11
  import { readRangeSubjectsTouchingFile } from '../../../baselines/git-base.js';
15
12
  import {
16
13
  checkBaselineSemantics,
@@ -91,57 +88,34 @@ function loadHeadBaseline(kind, cwd, configPath) {
91
88
  }
92
89
 
93
90
  /**
94
- * One-shot bundle-size refresh/acknowledge (Story #151). When
95
- * `BUNDLE_SIZE_REFRESH=1` is set, demote every `bundle-size` regression to
96
- * `unchanged` for this run only — floors still apply, so a genuine budget
97
- * breach is still caught. The flag is read fresh on every invocation and
98
- * never persisted, so the ratchet returns to full strength automatically on
99
- * the very next run (no lingering loosened tolerance to remember to reset).
100
- *
101
- * No-op for every other kind.
102
- */
103
- function applyBundleSizeAcknowledgment(kind, compareOutput, env) {
104
- if (kind !== 'bundle-size') return { compareOutput, acknowledged: false };
105
- const { acknowledged, overrides } = resolveBundleSizeEnvOverrides(env);
106
- if (!acknowledged || compareOutput.regressions.length === 0) {
107
- return { compareOutput, acknowledged: false };
108
- }
109
- Logger.warn(
110
- `[bundle-size] ⚠ ${overrides.join(', ')} — ` +
111
- `${compareOutput.regressions.length} regression(s) acknowledged for this run only; ` +
112
- 'floors still enforced. This does not persist: the next run without ' +
113
- 'BUNDLE_SIZE_REFRESH re-enforces the ratchet at full strength.',
114
- );
115
- return {
116
- acknowledged: true,
117
- compareOutput: {
118
- ...compareOutput,
119
- regressions: [],
120
- unchanged: [...compareOutput.unchanged, ...compareOutput.regressions],
121
- },
122
- };
123
- }
124
-
125
- /**
126
- * Resolve the maintainability refresh trigger (Story #4731). Two paths, either
127
- * of which acknowledges — mirroring the bundle-size acknowledge but adding the
128
- * commit-tagged trigger the breach message already documents:
91
+ * Resolve the one-shot refresh trigger for any ratcheted kind (Story #4802,
92
+ * generalizing Story #151's bundle-size env flag and Story #4731's
93
+ * maintainability env-or-commit-tag pair). Two paths, either of which
94
+ * acknowledges:
129
95
  *
130
- * 1. Env parity: `MAINTAINABILITY_REFRESH=1` (the manual override).
96
+ * 1. Env parity: `<KIND>_REFRESH=1` (the manual override) — upper-snaked,
97
+ * so the two pre-existing names (`BUNDLE_SIZE_REFRESH`,
98
+ * `MAINTAINABILITY_REFRESH`) keep working unchanged.
131
99
  * 2. Commit tag: a commit in the compared range `<baseRef>..HEAD` whose
132
- * subject contains the configured `refreshTag` AND whose diff touches the
133
- * maintainability baseline file. One-shot by construction — once merged,
134
- * the refreshed baseline becomes the base and the tag leaves the range.
100
+ * subject contains the configured `refreshTag` AND whose diff touches
101
+ * that kind's baseline file. One-shot by construction — once merged, the
102
+ * refreshed baseline becomes the base and the tag leaves the range.
135
103
  *
136
104
  * The tag is matched as a plain substring of a conventional commit subject, so
137
105
  * commitlint stays satisfied (e.g. `chore(baselines): baseline-refresh: …`).
138
106
  *
107
+ * Fails closed: a kind whose baseline path is neither configured nor present
108
+ * in `DEFAULT_BASELINE_PATHS` simply skips the commit-tag path rather than
109
+ * throwing, leaving the run un-acknowledged.
110
+ *
139
111
  * @returns {{ triggered: boolean, reasons: string[] }}
140
112
  */
141
- function resolveMaintainabilityRefreshTrigger({ gateBlock, cmp, cwd, env }) {
113
+ function resolveRefreshTrigger({ kind, gateBlock, cmp, cwd, env }) {
142
114
  const reasons = [];
143
- const { acknowledged: envAck, overrides } =
144
- resolveMaintainabilityRefreshOverrides(env);
115
+ const { acknowledged: envAck, overrides } = resolveKindRefreshOverrides(
116
+ kind,
117
+ env,
118
+ );
145
119
  if (envAck) reasons.push(...overrides);
146
120
 
147
121
  const baseRef = cmp?.baseRef ?? null;
@@ -154,15 +128,17 @@ function resolveMaintainabilityRefreshTrigger({ gateBlock, cmp, cwd, env }) {
154
128
  typeof gateBlock?.baselinePath === 'string' &&
155
129
  gateBlock.baselinePath.length
156
130
  ? gateBlock.baselinePath
157
- : DEFAULT_BASELINE_PATHS.maintainability;
158
- const subjects = readRangeSubjectsTouchingFile(baseRef, baselinePath, {
159
- cwd,
160
- });
161
- const match = subjects.find((s) => s.includes(refreshTag));
162
- if (match) {
163
- reasons.push(
164
- `refresh commit "${match}" (subject contains ${JSON.stringify(refreshTag)}, touches ${baselinePath})`,
165
- );
131
+ : DEFAULT_BASELINE_PATHS[kind];
132
+ if (typeof baselinePath === 'string' && baselinePath.length) {
133
+ const subjects = readRangeSubjectsTouchingFile(baseRef, baselinePath, {
134
+ cwd,
135
+ });
136
+ const match = subjects.find((s) => s.includes(refreshTag));
137
+ if (match) {
138
+ reasons.push(
139
+ `refresh commit "${match}" (subject contains ${JSON.stringify(refreshTag)}, touches ${baselinePath})`,
140
+ );
141
+ }
166
142
  }
167
143
  }
168
144
 
@@ -170,26 +146,24 @@ function resolveMaintainabilityRefreshTrigger({ gateBlock, cmp, cwd, env }) {
170
146
  }
171
147
 
172
148
  /**
173
- * One-shot maintainability refresh/acknowledge (Story #4731). When triggered
174
- * (env flag OR a `baseline-refresh:`-tagged range commit touching the baseline),
175
- * demote every maintainability head-vs-base regression to `unchanged` for this
176
- * run only — floors still apply, so a row below its `min` floor still breaches.
177
- * The trigger is read fresh every run and never persisted: post-merge the
178
- * refreshed baseline is the new base and the tag leaves the range, so the
179
- * ratchet returns to full strength automatically.
149
+ * One-shot baseline refresh/acknowledge for any ratcheted kind (Story #4802).
150
+ * When triggered (env flag OR a `baseline-refresh:`-tagged range commit
151
+ * touching that kind's baseline), demote every head-vs-base regression to
152
+ * `unchanged` for this run only — floors still apply, so a row below its floor
153
+ * still breaches. The trigger is read fresh every run and never persisted:
154
+ * post-merge the refreshed baseline is the new base and the tag leaves the
155
+ * range, so the ratchet returns to full strength automatically.
180
156
  *
181
- * No-op for every other kind.
157
+ * A no-op absent a trigger, so an unacknowledged run of any kind reports its
158
+ * regressions exactly as before.
182
159
  */
183
- function applyMaintainabilityAcknowledgment(kind, compareOutput, ctx) {
184
- if (kind !== 'maintainability') {
185
- return { compareOutput, acknowledged: false };
186
- }
187
- const { triggered, reasons } = resolveMaintainabilityRefreshTrigger(ctx);
160
+ function applyRefreshAcknowledgment(kind, compareOutput, ctx) {
161
+ const { triggered, reasons } = resolveRefreshTrigger({ ...ctx, kind });
188
162
  if (!triggered || compareOutput.regressions.length === 0) {
189
163
  return { compareOutput, acknowledged: false };
190
164
  }
191
165
  Logger.warn(
192
- `[maintainability] ⚠ ${reasons.join('; ')} — ` +
166
+ `[${kind}] ⚠ ${reasons.join('; ')} — ` +
193
167
  `${compareOutput.regressions.length} regression(s) acknowledged for this run only; ` +
194
168
  'floors still enforced. This does not persist: once the refresh is the ' +
195
169
  'new base the ratchet re-enforces at full strength.',
@@ -274,14 +248,14 @@ export async function evaluateKind({
274
248
  rawCompare,
275
249
  gateBlock.tolerance ?? null,
276
250
  );
277
- const bundleAck = applyBundleSizeAcknowledgment(kind, toleratedCompare, env);
278
- const miAck = applyMaintainabilityAcknowledgment(
279
- kind,
280
- bundleAck.compareOutput,
281
- { gateBlock, cmp, cwd, env },
282
- );
283
- const compareOutput = miAck.compareOutput;
284
- const acknowledged = bundleAck.acknowledged || miAck.acknowledged;
251
+ const ack = applyRefreshAcknowledgment(kind, toleratedCompare, {
252
+ gateBlock,
253
+ cmp,
254
+ cwd,
255
+ env,
256
+ });
257
+ const compareOutput = ack.compareOutput;
258
+ const acknowledged = ack.acknowledged;
285
259
  return buildGateReport({
286
260
  kind,
287
261
  gateBlock,
@@ -44,18 +44,26 @@ compare) over every configured gate, with centralised friction emission and
44
44
  aggregated exit codes.
45
45
 
46
46
  Env vars:
47
- BUNDLE_SIZE_REFRESH=1 One-shot acknowledge for an intentional bundle-size
48
- growth: demotes bundle-size regressions to
49
- "unchanged" for this run only (floors still
50
- enforced). Never persisted the next run without
51
- this flag re-enforces the ratchet.
52
- MAINTAINABILITY_REFRESH=1
53
- One-shot acknowledge for a deliberate maintainability
54
- baseline refresh: demotes maintainability head-vs-base
55
- regressions to "unchanged" for this run only (floors
56
- still enforced). Env-parity override for the
57
- 'baseline-refresh:'-tagged range commit that touches
58
- baselines/maintainability.json. Never persisted.
47
+ <KIND>_REFRESH=1 One-shot acknowledge for a deliberate baseline
48
+ refresh of that kind: demotes its head-vs-base
49
+ regressions to "unchanged" for this run only. Floors
50
+ are STILL enforced, so a genuine breach is still
51
+ caught. The kind name is upper-snaked, e.g.
52
+ COVERAGE_REFRESH, CRAP_REFRESH, DUPLICATION_REFRESH,
53
+ MAINTAINABILITY_REFRESH, BUNDLE_SIZE_REFRESH.
54
+ Never persisted the next run without the flag
55
+ re-enforces the ratchet at full strength.
56
+
57
+ Equivalent commit-tagged trigger: a commit in the
58
+ compared range whose subject contains the gate's
59
+ 'refreshTag' (default 'baseline-refresh:') AND whose
60
+ diff touches that kind's baseline file. One-shot by
61
+ construction — once merged the refreshed baseline is
62
+ the new base and the tag leaves the range.
63
+
64
+ Use these when replacing a diff-scope baseline with a
65
+ full-scope measurement: the resulting row deltas are
66
+ arithmetic, not behavioural.
59
67
 
60
68
  Exit codes:
61
69
  0 every enabled gate passes
@@ -60,8 +60,10 @@ import { isObjectPathEntry } from './task-body-validator.js';
60
60
  /**
61
61
  * Default git probe — returns `true` when `path` exists at
62
62
  * `baseBranchRef`. Mirrors the existence check used by
63
- * {@link ./ticket-validator.js#validateAcFreshness} and
64
- * {@link ./spec-freshness.js} so all three gates share semantics.
63
+ * {@link ./ticket-validator.js#validateAcFreshness} so both gates share
64
+ * semantics. (Story #4811 deleted the third sharer, `spec-freshness.js`,
65
+ * along with the codebase snapshot it grounded; this gate — Phase 8 — is
66
+ * now the grounding gate and its behaviour is unchanged.)
65
67
  *
66
68
  * @param {{ baseBranchRef: string, path: string, cwd?: string }} opts
67
69
  * @returns {boolean}
@@ -2,7 +2,8 @@
2
2
 
3
3
  Each listener in this directory subscribes to one or more lifecycle bus
4
4
  events and performs a single side effect. The full close-tail roster and
5
- event taxonomy live in [`docs/LIFECYCLE.md`](../../../../../../docs/LIFECYCLE.md)
5
+ event taxonomy live in
6
+ [`docs/LIFECYCLE.md`](https://github.com/dsj1984/mandrel/blob/main/docs/LIFECYCLE.md)
6
7
  — that document is the SSOT. This README only indexes the **files that still
7
8
  live in this folder**.
8
9
 
@@ -48,15 +48,16 @@ const SOURCE_TICKET_FETCH_CONCURRENCY = 4;
48
48
  * body and ship the raw seed on `seed.content` instead — the budget bounded
49
49
  * a field that never left the function.
50
50
  *
51
- * The envelope's bounded parts are: the tier-capped codebase snapshot
52
- * (~35 KB skinny on this repo), the three rendered system prompts (~15 KB),
53
- * and the digest-first `docsContext` (outline-only, or inline digest in
54
- * one-pager/seed mode). The seed itself is operator-supplied and carried
55
- * verbatim. Measured folded envelopes on this repo land at ~42 KB; 256 KB
56
- * (~64K tokens at the ≈4-chars/token estimate) gives >2× headroom over a
57
- * worst-case seed + medium-tier snapshot while staying an order of magnitude
58
- * under the session budget. The test suite asserts serialized envelopes stay
59
- * under this value — raise it only with a measured justification.
51
+ * A measured seed-mode envelope on this repo is ~120 KB, dominated by the
52
+ * digest-first `docsContext` (~63 KB inline digest) and the rendered
53
+ * `systemPrompts` (~54 KB); every other field is under 1 KB. Story #4811
54
+ * retired the tier-capped codebase snapshot that used to sit alongside them
55
+ * (~35 KB skinny here). The seed itself is operator-supplied, carried
56
+ * verbatim, and is the only unbounded contributor. 256 KB (~64K tokens at the
57
+ * ≈4-chars/token estimate) leaves roughly headroom over that measurement
58
+ * while staying well under the session budget. The test suite asserts
59
+ * serialized envelopes stay under this value — raise it only with a measured
60
+ * justification.
60
61
  */
61
62
  export const PLAN_CONTEXT_ENVELOPE_BYTE_CEILING = 256_000;
62
63
 
@@ -112,9 +113,9 @@ function assertPlanContextWithinCeiling(envelope, opts = {}) {
112
113
  `[plan-context] the assembled "${envelope?.mode}" envelope is ` +
113
114
  `${Math.round(bytes / 1024)} KB, over the ` +
114
115
  `${Math.round(ceiling / 1024)} KB planner-context ceiling. Largest ` +
115
- `fields: ${largest}. Trim the seed, plan fewer --tickets source issues ` +
116
- 'in one run, or narrow `planning.codebaseSnapshot`. Raising the ceiling ' +
117
- 'needs a measured justification — see PLAN_CONTEXT_ENVELOPE_BYTE_CEILING.',
116
+ `fields: ${largest}. Trim the seed, or plan fewer --tickets source ` +
117
+ 'issues in one run. Raising the ceiling needs a measured ' +
118
+ 'justification — see PLAN_CONTEXT_ENVELOPE_BYTE_CEILING.',
118
119
  );
119
120
  }
120
121
 
@@ -890,7 +891,6 @@ async function buildSeedFileModeEnvelope({
890
891
  ),
891
892
  duplicates,
892
893
  docsContext,
893
- codebaseSnapshot: authoring.codebaseSnapshot,
894
894
  bddRunner: authoring.bddRunner,
895
895
  bddScenarios: authoring.bddScenarios,
896
896
  memoryFreshness: authoring.memoryFreshness,
@@ -1052,7 +1052,6 @@ async function buildTicketsModeEnvelope({
1052
1052
  ),
1053
1053
  duplicates,
1054
1054
  docsContext,
1055
- codebaseSnapshot: authoring.codebaseSnapshot,
1056
1055
  bddRunner: authoring.bddRunner,
1057
1056
  bddScenarios: authoring.bddScenarios,
1058
1057
  memoryFreshness: authoring.memoryFreshness,