mandrel 2.19.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.
@@ -67,27 +67,6 @@
67
67
  }
68
68
  },
69
69
  "planning": {
70
- "codebaseSnapshot": {
71
- "tier": "skinny",
72
- "include": [
73
- ".agents/scripts/**",
74
- "src/**",
75
- "lib/**",
76
- "app/**",
77
- "packages/**"
78
- ],
79
- "exclude": [
80
- "**/node_modules/**",
81
- "**/dist/**",
82
- "**/build/**",
83
- "**/.next/**",
84
- "**/.turbo/**",
85
- "**/coverage/**",
86
- "**/*.test.*",
87
- "**/*.spec.*"
88
- ],
89
- "recentCommitWindow": 30
90
- },
91
70
  "riskHeuristics": [
92
71
  "Destructive or irreversible data mutations (dropping tables, deleting rows without soft-delete or backup, truncating production state).",
93
72
  "Modifications to shared security or auth infrastructure (IAM policies, auth middleware, session or token handling, secret rotation).",
@@ -26,7 +26,7 @@ with the runtime validators.
26
26
  "$schema": "./.agents/schemas/agentrc.schema.json",
27
27
  "project": { /* paths, commands, baseBranch, docsContextFiles */ },
28
28
  "github": { /* owner, repo, branchProtection, mergeMethods, notifications */ },
29
- "planning": { /* riskHeuristics, conflict gates, codebaseSnapshot, context */ },
29
+ "planning": { /* riskHeuristics, conflict gates, complexityGate, navigation */ },
30
30
  "delivery": { /* execution, quality, worktreeIsolation, deliverRunner, ... */ }
31
31
  }
32
32
  ```
@@ -38,7 +38,7 @@ top-level keys are validation errors.
38
38
  | ------------- | -------- | ---------------------------------------------------------------------------------- |
39
39
  | `project` | **Yes** | Project-local paths, base branch, validation commands, and context-hydration files. |
40
40
  | `github` | No | Ticketing provider config: owner/repo, branch protection, merge methods, notifications. |
41
- | `planning` | No | `/plan` tuning: conflict advisories, codebase snapshot, context cap. (Story sizing ceilings are code-absolute — not agentrc.) |
41
+ | `planning` | No | `/plan` tuning: conflict advisories, complexity routing, navigability gate. (Story sizing ceilings and the planner-context cap are code-absolute — not agentrc.) |
42
42
  | `delivery` | No | `/deliver` tuning: quality gates, worktree isolation, runners, CI watch, code-review providers. |
43
43
  | `$schema` | No | JSON Schema pointer for editor tooling. |
44
44
 
@@ -97,11 +97,6 @@ top-level keys are validation errors.
97
97
  | Key | Required | Type | Default | Description |
98
98
  | --- | --- | --- | --- | --- |
99
99
  | `riskHeuristics` | No | `string[]` or `{ append?, prepend? }` | — | — |
100
- | `codebaseSnapshot` | No | `object` | — | Nested configuration block. |
101
- | `codebaseSnapshot.tier` | No | `"skinny"` \| `"medium"` | — | — |
102
- | `codebaseSnapshot.include` | No | `array<string>` | — | — |
103
- | `codebaseSnapshot.exclude` | No | `array<string>` | — | — |
104
- | `codebaseSnapshot.recentCommitWindow` | No | `integer` | — | — |
105
100
  | `complexityGate` | No | `object` | — | Shape-derived ceremony-lite complexity routing. A lite claim is validated against the authored Story shape at persist and re-derived from the Story body at dispatch; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables. |
106
101
  | `complexityGate.enabled` | No | `boolean` | — | Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true. |
107
102
  | `complexityGate.maxArtifacts` | No | `integer` | — | Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict — carries no routing authority. Default 1. |
@@ -352,13 +347,15 @@ Defaults are **advisory, not Story-width ceilings**. Session-mass ceilings live
352
347
  as absolute authored-token constants on `DEFAULT_MODEL_CAPACITY` in
353
348
  `ticket-validator-sizing.js` (soft 30k / hard 75k); there is no `maxTokenBudget`
354
349
  envelope, and cohesion / split policy / conflict advisories are the primary
355
- sizing signal. The `codebaseSnapshot` `skinny` tier caps at 250 paths using
356
- **per-top-level-directory proportional budgeting** (round-robin across matched
357
- trees) so a large dot-prefixed tree like `.agents/scripts/**` cannot monopolise
358
- the budget and truncate away the consumer's own source; the shipped `include`
359
- scans `.agents/scripts/**`, `src/**`, `lib/**`, `app/**`, `packages/**` and
360
- `exclude` drops `node_modules`, build dirs, and test files. Override `include`
361
- only when the project's source layout differs.
350
+ sizing signal.
351
+
352
+ There is no snapshot knob to tune: Story #4811 retired the structural-snapshot
353
+ block outright. Spec authoring is grounded by the author's own targeted repo
354
+ retrieval plus the Phase 8 `validateStoryFileAssumptions` gate, which probes
355
+ every authored `{path, assumption}` against the real tree as a hard error — a
356
+ pre-computed inventory added a second, staler answer to the same question.
357
+ A config still carrying the retired key is a hard validation failure; the
358
+ 2.20.0 retirement migration strips it on upgrade.
362
359
 
363
360
  - **`complexityGate`.** Shape-derived ceremony-lite routing (Story #4722,
364
361
  superseding the word-count gate of Stories #4683/#4707). The full ceremony
@@ -96,12 +96,15 @@ over-ceiling envelope or an over-budget Story count.
96
96
  - **`PLAN_CONTEXT_ENVELOPE_BYTE_CEILING`** (`lib/orchestration/plan-context.js`):
97
97
  256 KB (≈64K tokens at the ≈4-chars/token estimate) on the serialized
98
98
  envelope `buildPlanContext` assembles, checked at the single choke point
99
- every mode returns through. Measured envelopes on this repo land at ~42 KB,
100
- so the ceiling is >2× headroom over a worst-case seed plus a medium-tier
101
- codebase snapshot.
99
+ every mode returns through. A measured seed-mode envelope on this repo is
100
+ ~120 KB `docsContext` (~63 KB) and `systemPrompts` (~54 KB) are the whole
101
+ of it, every other field under 1 KB — so the ceiling carries roughly 2×
102
+ headroom. Story #4811 retired the codebase snapshot that used to sit
103
+ alongside them, leaving the operator-supplied seed the only unbounded
104
+ contributor.
102
105
  - **On refusal**, the error names the envelope's largest fields. Trim the seed,
103
- plan fewer `--tickets` source issues in one run, or narrow
104
- `planning.codebaseSnapshot`. The seed is carried **verbatim** by design — it
106
+ or plan fewer `--tickets` source issues in one run. The seed is carried
107
+ **verbatim** by design — it
105
108
  is the operator's request, and summarizing it silently would degrade planning
106
109
  quality precisely when the input is richest — so there is no elision path to
107
110
  fall back on. Raising the ceiling needs a measured justification.
@@ -324,9 +324,6 @@
324
324
  "riskHeuristics": {
325
325
  "$ref": "#/$defs/listOrExtenderOfStrings"
326
326
  },
327
- "codebaseSnapshot": {
328
- "$ref": "#/$defs/codebaseSnapshot"
329
- },
330
327
  "complexityGate": {
331
328
  "type": "object",
332
329
  "description": "Shape-derived ceremony-lite complexity routing. A lite claim is validated against the authored Story shape at persist and re-derived from the Story body at dispatch; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables.",
@@ -392,34 +389,6 @@
392
389
  },
393
390
  "additionalProperties": false
394
391
  },
395
- "codebaseSnapshot": {
396
- "type": "object",
397
- "properties": {
398
- "tier": {
399
- "type": "string",
400
- "enum": ["skinny", "medium"]
401
- },
402
- "include": {
403
- "type": "array",
404
- "items": {
405
- "type": "string",
406
- "minLength": 1
407
- }
408
- },
409
- "exclude": {
410
- "type": "array",
411
- "items": {
412
- "type": "string",
413
- "minLength": 1
414
- }
415
- },
416
- "recentCommitWindow": {
417
- "type": "integer",
418
- "minimum": 1
419
- }
420
- },
421
- "additionalProperties": false
422
- },
423
392
  "execution": {
424
393
  "type": "object",
425
394
  "properties": {
@@ -15,16 +15,41 @@
15
15
  * per-process scratch dir. This script is the regression guard that keeps
16
16
  * the fix honest, plus a local cleanup mode for the accumulated noise:
17
17
  *
18
+ * The guard covers two distinct temp roots, and conflating them is how the
19
+ * second one went unmeasured for so long:
20
+ *
21
+ * 1. The repo's own `temp/` telemetry tree — the original dimension above.
22
+ * 2. The **OS temp root** (Story #4808). The redirect in (1) sends stray
23
+ * writes into `os.tmpdir()` scratch dirs, and nothing ever reaped them:
24
+ * the remedy for (1) became the largest single leaker into (2). Since
25
+ * the damaging axis there is entry *count*, the suite now nests every
26
+ * managed dir inside one per-process `mandrel-suite-*` root
27
+ * (`lib/test-temp.js`) and reaps it, and this guard asserts that no
28
+ * such root survives a run.
29
+ *
18
30
  * --snapshot Record a fingerprint (size + sha256) of every stream
19
- * file under `temp/` to the snapshot baseline. Run this
20
- * before the suite.
31
+ * file under `temp/`, plus the `mandrel-suite-*` roots
32
+ * already present in the OS temp root, to the snapshot
33
+ * baseline. Run this before the suite.
21
34
  * --assert Re-scan and fail if any stream file was added or grew
22
- * relative to the snapshot. Run this after the suite. A
23
- * missing snapshot is a hard failure ("snapshot missing
35
+ * relative to the snapshot, or if a suite root appeared
36
+ * and survived. Run this after the suite. A missing
37
+ * snapshot is a hard failure ("snapshot missing
24
38
  * — guard cannot attest"), never a silent re-baseline:
25
39
  * the baseline lives *outside* the protected `temp/`
26
40
  * tree (Story #4711), so a test wiping `temp/` can no
27
41
  * longer destroy the baseline and fail the guard open.
42
+ * Recording pre-existing suite roots (rather than
43
+ * asserting an empty set) is what keeps a concurrent
44
+ * suite in another checkout from failing this one.
45
+ * --lint-globs <g> Comma-separated repo-relative globs to scan for test
46
+ * files that call `mkdtemp` against `os.tmpdir()`
47
+ * directly instead of going through `makeTempDir`.
48
+ * **Off unless passed**: this script ships in the
49
+ * materialized `.agents/` payload and a consumer's
50
+ * tests are none of this rule's business. A line (or
51
+ * the line above it) carrying `test-temp-allow` opts
52
+ * out.
28
53
  * --baseline <path> Explicit snapshot-baseline path (CI sets this to a
29
54
  * runner-temp path). Defaults to an OS scratch location
30
55
  * keyed by the resolved repo root. Refused when it
@@ -53,6 +78,12 @@ import os from 'node:os';
53
78
  import path from 'node:path';
54
79
  import { fileURLToPath } from 'node:url';
55
80
  import { runAsCli } from './lib/cli-utils.js';
81
+ import {
82
+ findRawTmpdirMkdtemp,
83
+ listSuiteTempRoots,
84
+ SUITE_ROOTS_KEY,
85
+ survivingSuiteTempRoots,
86
+ } from './lib/test-temp.js';
56
87
 
57
88
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
58
89
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
@@ -193,17 +224,27 @@ export function buildManifest(tempDir) {
193
224
  * `defaultBaselinePath` — never inside `temp/`).
194
225
  * @param {string} repoRoot
195
226
  * @param {string} [baselinePath]
196
- * @returns {{ snapshotPath: string, count: number }}
227
+ * @param {{ tmpDir?: string }} [deps] Injectable OS temp root for tests.
228
+ * @returns {{ snapshotPath: string, count: number, suiteRoots: number }}
197
229
  */
198
- export function writeSnapshot(repoRoot, baselinePath) {
230
+ export function writeSnapshot(repoRoot, baselinePath, { tmpDir } = {}) {
199
231
  const snapshotPath = checkedBaselinePath(
200
232
  repoRoot,
201
233
  baselinePath ?? defaultBaselinePath(repoRoot),
202
234
  );
203
235
  const manifest = buildManifest(tempDirFor(repoRoot));
236
+ const count = Object.keys(manifest).length;
237
+ // Reserved key: stream entries are always `*.ndjson` relative paths, so
238
+ // this cannot shadow one, and `diffAgainstSnapshot` only ever looks up
239
+ // keys derived from the tree it just walked.
240
+ manifest[SUITE_ROOTS_KEY] = listSuiteTempRoots(tmpDir ?? os.tmpdir());
204
241
  mkdirSync(path.dirname(snapshotPath), { recursive: true });
205
242
  writeFileSync(snapshotPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
206
- return { snapshotPath, count: Object.keys(manifest).length };
243
+ return {
244
+ snapshotPath,
245
+ count,
246
+ suiteRoots: manifest[SUITE_ROOTS_KEY].length,
247
+ };
207
248
  }
208
249
 
209
250
  /**
@@ -331,7 +372,7 @@ export function cleanFixtureDirs({
331
372
  /**
332
373
  * Parse the CLI argv into a normalised options object.
333
374
  * @param {string[]} argv
334
- * @returns {{ mode: 'snapshot'|'assert'|'clean', apply: boolean, ids: number[]|null, repoRoot: string, baseline: string|null }}
375
+ * @returns {{ mode: 'snapshot'|'assert'|'clean', apply: boolean, ids: number[]|null, repoRoot: string, baseline: string|null, lintGlobs: string[] }}
335
376
  */
336
377
  export function parseArgv(argv) {
337
378
  let mode = 'assert';
@@ -339,13 +380,20 @@ export function parseArgv(argv) {
339
380
  let ids = null;
340
381
  let repoRoot = REPO_ROOT;
341
382
  let baseline = null;
383
+ let lintGlobs = [];
342
384
  for (let i = 0; i < argv.length; i += 1) {
343
385
  const arg = argv[i];
344
386
  if (arg === '--snapshot') mode = 'snapshot';
345
387
  else if (arg === '--assert') mode = 'assert';
346
388
  else if (arg === '--clean') mode = 'clean';
347
389
  else if (arg === '--yes') apply = true;
348
- else if (arg === '--ids') {
390
+ else if (arg === '--lint-globs') {
391
+ i += 1;
392
+ lintGlobs = String(argv[i] ?? '')
393
+ .split(',')
394
+ .map((s) => s.trim())
395
+ .filter(Boolean);
396
+ } else if (arg === '--ids') {
349
397
  i += 1;
350
398
  ids = String(argv[i] ?? '')
351
399
  .split(',')
@@ -359,7 +407,7 @@ export function parseArgv(argv) {
359
407
  baseline = path.resolve(String(argv[i] ?? '.'));
360
408
  }
361
409
  }
362
- return { mode, apply, ids, repoRoot, baseline };
410
+ return { mode, apply, ids, repoRoot, baseline, lintGlobs };
363
411
  }
364
412
 
365
413
  /**
@@ -369,14 +417,23 @@ export function parseArgv(argv) {
369
417
  *
370
418
  * @param {ReturnType<typeof parseArgv>} opts
371
419
  * @param {(line: string) => void} [log]
420
+ * @param {{ tmpDir?: string }} [deps] Injectable OS temp root for tests.
372
421
  * @returns {number}
373
422
  */
374
- export function runHygiene(opts, log = (l) => process.stdout.write(`${l}\n`)) {
375
- const { mode, apply, ids, repoRoot, baseline = null } = opts;
423
+ export function runHygiene(
424
+ opts,
425
+ log = (l) => process.stdout.write(`${l}\n`),
426
+ { tmpDir = os.tmpdir() } = {},
427
+ ) {
428
+ const { mode, apply, ids, repoRoot, baseline = null, lintGlobs = [] } = opts;
376
429
  if (mode === 'snapshot') {
377
- const { snapshotPath, count } = writeSnapshot(repoRoot, baseline);
430
+ const { snapshotPath, count, suiteRoots } = writeSnapshot(
431
+ repoRoot,
432
+ baseline,
433
+ { tmpDir },
434
+ );
378
435
  log(
379
- `[test-temp-hygiene] snapshot recorded (${count} stream file(s)) → ${snapshotPath}`,
436
+ `[test-temp-hygiene] snapshot recorded (${count} stream file(s), ${suiteRoots} pre-existing suite root(s)) → ${snapshotPath}`,
380
437
  );
381
438
  return 0;
382
439
  }
@@ -409,6 +466,26 @@ export function runHygiene(opts, log = (l) => process.stdout.write(`${l}\n`)) {
409
466
  );
410
467
  return 1;
411
468
  }
469
+ // Every dimension runs and reports; a failure in one must not hide a
470
+ // failure in another, so the exit code is the max rather than an
471
+ // early return.
472
+ const codes = [
473
+ assertStreamTree(repoRoot, snapshot, log),
474
+ assertNoSurvivingSuiteRoots(snapshot, log, tmpDir),
475
+ assertNoRawTmpdirMkdtemp(repoRoot, lintGlobs, log),
476
+ ];
477
+ return Math.max(...codes);
478
+ }
479
+
480
+ /**
481
+ * Dimension 1 — the repo's own `temp/` telemetry tree (Story #4696).
482
+ *
483
+ * @param {string} repoRoot
484
+ * @param {Record<string, unknown>} snapshot
485
+ * @param {(line: string) => void} log
486
+ * @returns {number} exit code
487
+ */
488
+ function assertStreamTree(repoRoot, snapshot, log) {
412
489
  const { added, changed } = diffAgainstSnapshot(
413
490
  tempDirFor(repoRoot),
414
491
  snapshot,
@@ -428,6 +505,68 @@ export function runHygiene(opts, log = (l) => process.stdout.write(`${l}\n`)) {
428
505
  return 1;
429
506
  }
430
507
 
508
+ /**
509
+ * Dimension 2 — the OS temp root (Story #4808). Fails when a suite root
510
+ * appeared since the snapshot and is still on disk, which means the run
511
+ * created it and never reaped it.
512
+ *
513
+ * @param {Record<string, unknown>} snapshot
514
+ * @param {(line: string) => void} log
515
+ * @param {string} tmpDir
516
+ * @returns {number} exit code
517
+ */
518
+ function assertNoSurvivingSuiteRoots(snapshot, log, tmpDir) {
519
+ const before = Array.isArray(snapshot[SUITE_ROOTS_KEY])
520
+ ? snapshot[SUITE_ROOTS_KEY]
521
+ : [];
522
+ const surviving = survivingSuiteTempRoots(tmpDir, before);
523
+ if (surviving.length === 0) {
524
+ log('[test-temp-hygiene] OK — no suite temp roots survived the run.');
525
+ return 0;
526
+ }
527
+ log(
528
+ `[test-temp-hygiene] FAIL — ${surviving.length} suite temp root(s) survived in ${tmpDir}:`,
529
+ );
530
+ for (const name of surviving) log(` + leaked ${name}`);
531
+ log(
532
+ '[test-temp-hygiene] a process minted a suite root and exited without reaping it. Do not delete these by hand — find the writer that bypassed makeTempDir().',
533
+ );
534
+ return 1;
535
+ }
536
+
537
+ /**
538
+ * Dimension 3 — the static backstop (Story #4808). Skipped, and reported
539
+ * as skipped, unless the caller passed `--lint-globs`.
540
+ *
541
+ * @param {string} repoRoot
542
+ * @param {string[]} globs
543
+ * @param {(line: string) => void} log
544
+ * @returns {number} exit code
545
+ */
546
+ function assertNoRawTmpdirMkdtemp(repoRoot, globs, log) {
547
+ if (!globs || globs.length === 0) {
548
+ log(
549
+ '[test-temp-hygiene] SKIP — raw-tmpdir lint not requested (pass --lint-globs to enable).',
550
+ );
551
+ return 0;
552
+ }
553
+ const findings = findRawTmpdirMkdtemp(repoRoot, globs);
554
+ if (findings.length === 0) {
555
+ log(
556
+ '[test-temp-hygiene] OK — no test file mints OS temp dirs outside makeTempDir().',
557
+ );
558
+ return 0;
559
+ }
560
+ log(
561
+ `[test-temp-hygiene] FAIL — ${findings.length} raw os.tmpdir() mkdtemp call(s) in test files:`,
562
+ );
563
+ for (const f of findings) log(` ${f.file}:${f.line} ${f.text}`);
564
+ log(
565
+ "[test-temp-hygiene] use makeTempDir() from .agents/scripts/lib/test-temp.js so teardown is registered, or mark the line 'test-temp-allow: <reason>' when the real root is genuinely required.",
566
+ );
567
+ return 1;
568
+ }
569
+
431
570
  runAsCli(
432
571
  import.meta.url,
433
572
  async () => {
@@ -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
@@ -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}
@@ -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,