session-orchestrator 5.4.0 → 5.6.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 (151) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/060-evolve.mdc +5 -5
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +115 -3
  7. package/README.md +30 -15
  8. package/docs/ci-setup.md +120 -23
  9. package/docs/components.md +7 -7
  10. package/docs/events-schema.md +17 -14
  11. package/docs/install.md +14 -2
  12. package/docs/migration-v4.md +24 -17
  13. package/docs/plugin-architecture-v3.md +1 -1
  14. package/docs/scope-collision-guard.md +19 -15
  15. package/docs/session-config-reference.md +7 -7
  16. package/docs/vault-docs-architecture.md +8 -4
  17. package/hooks/_lib/hook-import-set.json +17 -8
  18. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  19. package/hooks/config-protection.mjs +1 -1
  20. package/hooks/enforce-commands.mjs +28 -13
  21. package/hooks/enforce-scope.mjs +201 -23
  22. package/hooks/hooks-codex.json +1 -1
  23. package/hooks/hooks.json +1 -1
  24. package/hooks/on-session-start.mjs +4 -2
  25. package/hooks/on-stop.mjs +77 -171
  26. package/hooks/post-bash-write-verify.mjs +219 -24
  27. package/hooks/post-edit-validate.mjs +15 -5
  28. package/hooks/post-tool-batch-wave-signal.mjs +41 -156
  29. package/hooks/pre-bash-memory-propose-audit.mjs +28 -11
  30. package/hooks/pre-bash-staging-fence.mjs +29 -8
  31. package/hooks/pre-task-scope-disjoint.mjs +603 -241
  32. package/hooks/wave-scope-commit-guard.mjs +235 -73
  33. package/package.json +9 -8
  34. package/scripts/apply-session-learnings.mjs +35 -14
  35. package/scripts/archive-closed-prds.mjs +38 -11
  36. package/scripts/autopilot.mjs +8 -1
  37. package/scripts/backfill-abandoned-sessions.mjs +80 -5
  38. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  39. package/scripts/codex-install.mjs +3 -0
  40. package/scripts/cursor-install.mjs +46 -2
  41. package/scripts/emit-session.mjs +5 -3
  42. package/scripts/export-hw-learnings.mjs +18 -7
  43. package/scripts/fleet-checkin.mjs +26 -3
  44. package/scripts/lib/autopilot/loop.mjs +25 -10
  45. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  46. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  47. package/scripts/lib/common.mjs +41 -0
  48. package/scripts/lib/config/vault-integration.mjs +151 -16
  49. package/scripts/lib/config.mjs +5 -1
  50. package/scripts/lib/eval/engine.mjs +13 -0
  51. package/scripts/lib/events-retention-banner.mjs +393 -0
  52. package/scripts/lib/events-rotation.mjs +21 -6
  53. package/scripts/lib/events.mjs +51 -3
  54. package/scripts/lib/fleet-protocol.mjs +20 -2
  55. package/scripts/lib/hardening.mjs +1 -0
  56. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  57. package/scripts/lib/instruction-budget-guard.mjs +8 -0
  58. package/scripts/lib/learnings/expiry-sweep.mjs +387 -20
  59. package/scripts/lib/learnings/io.mjs +125 -16
  60. package/scripts/lib/lock-reaper.mjs +38 -14
  61. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  62. package/scripts/lib/named-vault-resolver.mjs +48 -5
  63. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  64. package/scripts/lib/orphan-reaper/reaper-audit.mjs +101 -69
  65. package/scripts/lib/orphan-reaper/scan-throttle.mjs +79 -27
  66. package/scripts/lib/orphan-reaper/scan.mjs +4 -0
  67. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  68. package/scripts/lib/orphan-reaper.mjs +5 -1
  69. package/scripts/lib/platform.mjs +206 -2
  70. package/scripts/lib/plugin-update-banner.mjs +3 -1
  71. package/scripts/lib/process-group.mjs +83 -22
  72. package/scripts/lib/scope-baseline.mjs +116 -25
  73. package/scripts/lib/scope-echo.mjs +7 -0
  74. package/scripts/lib/scope-gate.mjs +201 -27
  75. package/scripts/lib/session-close-backfill.mjs +202 -4
  76. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  77. package/scripts/lib/session-lock-shape.mjs +6 -3
  78. package/scripts/lib/session-lock.mjs +320 -67
  79. package/scripts/lib/session-registry.mjs +41 -16
  80. package/scripts/lib/session-start-probes.mjs +242 -68
  81. package/scripts/lib/session-token-rollup.mjs +39 -34
  82. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  83. package/scripts/lib/tail-window.mjs +27 -3
  84. package/scripts/lib/telemetry/pricing.mjs +5 -2
  85. package/scripts/lib/telemetry/sync.mjs +17 -0
  86. package/scripts/lib/tests-src-ratio.mjs +77 -2
  87. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  88. package/scripts/lib/validate/check-test-value-bans.mjs +320 -32
  89. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  90. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  91. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  92. package/scripts/lib/vault-mirror/process.mjs +350 -62
  93. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  94. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  95. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  96. package/scripts/lib/vault-mirror/telemetry.mjs +18 -11
  97. package/scripts/lib/vault-status/board-writer.mjs +5 -1
  98. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  99. package/scripts/materialize-wave-scope.mjs +7 -4
  100. package/scripts/release.mjs +1 -0
  101. package/scripts/run-migrate-v2-cross-repo.mjs +14 -2
  102. package/scripts/run-quality-gate.mjs +8 -5
  103. package/scripts/self-update.mjs +229 -0
  104. package/scripts/sweep-expired-learnings.mjs +560 -28
  105. package/scripts/validate-wave-scope.mjs +60 -6
  106. package/scripts/vault-backfill.mjs +50 -37
  107. package/scripts/vault-mirror.mjs +170 -33
  108. package/skills/_shared/fleet-protocol.md +74 -71
  109. package/skills/_shared/parallel-aware-auq.md +1 -1
  110. package/skills/_shared/state-ownership.md +38 -14
  111. package/skills/autopilot/SKILL.md +1 -1
  112. package/skills/bootstrap/SKILL.md +5 -3
  113. package/skills/bootstrap/_shared-template.md +96 -1
  114. package/skills/bootstrap/fast-template.md +3 -0
  115. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  116. package/skills/bootstrap/standard-template.md +2 -0
  117. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  118. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  119. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  120. package/skills/docs-orchestrator/SKILL.md +12 -5
  121. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  122. package/skills/evolve/SKILL.md +11 -2
  123. package/skills/evolve/references/evolve-analyze-mode.md +37 -17
  124. package/skills/session-end/learning-patterns.md +3 -3
  125. package/skills/session-end/metrics-collection.md +1 -1
  126. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  127. package/skills/session-end/plan-verification.md +25 -9
  128. package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
  129. package/skills/session-end/session-metrics-write.md +45 -10
  130. package/skills/session-start/SKILL.md +3 -3
  131. package/skills/session-start/references/operations-contract.md +5 -3
  132. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  133. package/skills/session-start/references/phase-4-ssot-environment-check.md +8 -1
  134. package/skills/vault-mirror/SKILL.md +21 -11
  135. package/skills/wave-executor/SKILL.md +1 -1
  136. package/skills/wave-executor/circuit-breaker.md +1 -1
  137. package/skills/wave-executor/references/wave-executor-state-init.md +1 -1
  138. package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
  139. package/skills/wave-executor/references/wave-loop-review.md +2 -2
  140. package/skills/wave-executor/references/wave-loop-scope-manifest.md +14 -6
  141. package/.agents/skills/navigator/SKILL.md +0 -22
  142. package/.agents/skills/navigator/agents/openai.yaml +0 -5
  143. package/.codex-plugin/skills/navigator/SKILL.md +0 -22
  144. package/.codex-plugin/skills/navigator/agents/openai.yaml +0 -5
  145. package/.cursor/commands/navigator.md +0 -15
  146. package/.cursor/skills/navigator/SKILL.md +0 -13
  147. package/pi/prompts/navigator.md +0 -12
  148. package/skills/navigator/SKILL.md +0 -193
  149. package/skills/navigator/references/account-check.sh +0 -87
  150. package/skills/navigator/references/fleet-decisions.mjs +0 -231
  151. package/skills/navigator/references/slot-check.sh +0 -114
@@ -16,6 +16,18 @@
16
16
 
17
17
  import fs from 'node:fs';
18
18
 
19
+ /**
20
+ * `open(2)` flags of a `noFollow` read. `O_NOFOLLOW` refuses a symlink as the
21
+ * last path component (ELOOP) in the open itself; `O_NONBLOCK` makes the
22
+ * read-side open of a FIFO return at once instead of blocking until a writer
23
+ * appears (a plain `'r'` open of a planted FIFO blocked a detached scan child
24
+ * for good — reproduced 2026-10-02, #1487). On a regular file `O_NONBLOCK`
25
+ * changes nothing. Both are POSIX-only; Node leaves them undefined on Windows,
26
+ * where the read degrades to a plain open.
27
+ */
28
+ const NO_FOLLOW_READ_FLAGS = fs.constants.O_RDONLY
29
+ | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
30
+
19
31
  /**
20
32
  * Read the last `maxBytes` of `file` as UTF-8.
21
33
  *
@@ -28,14 +40,26 @@ import fs from 'node:fs';
28
40
  * THROWS on any fs error (ENOENT, EACCES, …) — mapping the failure is the
29
41
  * caller's decision.
30
42
  *
43
+ * `opts.noFollow` is for a file a hostile local writer may have replaced: it
44
+ * opens with {@link NO_FOLLOW_READ_FLAGS} and then refuses anything the
45
+ * descriptor's `fstat` does not call a regular file (FIFO, directory, device),
46
+ * so a symlink throws ELOOP and every other non-regular file throws
47
+ * `ERR_NOT_REGULAR_FILE` — never a blocked open, never a read of a link
48
+ * target. The default (`false`) is the plain `'r'` open every other caller uses.
49
+ *
31
50
  * @param {string} file
32
51
  * @param {number} maxBytes window size in bytes (> 0)
52
+ * @param {{noFollow?: boolean}} [opts]
33
53
  * @returns {{ text: string, cut: boolean, size: number }} `size` is the file size at open time
34
54
  */
35
- export function readTailWindow(file, maxBytes) {
36
- const fd = fs.openSync(file, 'r');
55
+ export function readTailWindow(file, maxBytes, { noFollow = false } = {}) {
56
+ const fd = fs.openSync(file, noFollow ? NO_FOLLOW_READ_FLAGS : 'r');
37
57
  try {
38
- const size = fs.fstatSync(fd).size;
58
+ const stats = fs.fstatSync(fd);
59
+ if (noFollow && !stats.isFile()) {
60
+ throw Object.assign(new Error('not a regular file'), { code: 'ERR_NOT_REGULAR_FILE' });
61
+ }
62
+ const size = stats.size;
39
63
  const want = Math.min(size, maxBytes);
40
64
  const start = size - want;
41
65
  const buf = Buffer.allocUnsafe(want);
@@ -122,9 +122,12 @@ export const PRICING_TABLE = Object.freeze({
122
122
  //
123
123
  // Ceiling: a model absent from this table prices as null, never at a neighbour's rate, so the
124
124
  // rollup reports `total_cost_usd: null` and `cost_records_priced < cost_records_total` — the
125
- // intended way missing evidence shows up. That ratio also drops below 1 for records no row can
125
+ // intended way missing evidence shows up. That ratio also drops below 1 for agents no row can
126
126
  // fix: a token-less record (transcript over the hook's size cap, without usage turns, or
127
- // unreadable) and a record with a token-bearing turn that names no model (#1475). Revisit when
127
+ // unreadable), a record with a token-bearing turn that names no model (#1475), a started agent
128
+ // whose stop found no transcript of its own, and an agent seen only in schema_version 1 records
129
+ // (#1487 Pkt 1). The one complete list is the `session-token-rollup.mjs` module doc — the code
130
+ // that decides; this comment and `emit-session.mjs` only point at it (#1498). Revisit when
128
131
  // an unpriced record names a model id absent from this table: add that model as its own
129
132
  // evidenced row, never as a family prefix or an alias to another generation.
130
133
  'claude-fable-5-1': usd({ input: 10.0, cache_read: 0.25, cache_creation: 12.5, output: 50.0 }),
@@ -312,6 +312,23 @@ export function readSessionProfileForMetricsDir(metricsDir) {
312
312
  }
313
313
  }
314
314
 
315
+ /**
316
+ * Events window this reader needs retained (#1401 part 3), in days — a CHOSEN
317
+ * ceiling, not a derived value. `deriveSessionFromEvents` loads the whole
318
+ * rotated ledger (no time filter) and keeps the LATEST
319
+ * `orchestrator.session.started`, so what it needs retained is that one
320
+ * record; 1 day is the assumed maximum age of that record when a flush reads
321
+ * it. Nothing in this module bounds that age: `buildBatch`'s
322
+ * `[now - DAILY_FLUSH_MS, now]` cutoff filters `skill-invocations.jsonl`, not
323
+ * the events ledger. CEILING (BV-004): a latest `session.started` older than a
324
+ * day at flush time (a session running longer than that) is found only if
325
+ * retention happens to reach it; revisit if that becomes common. Read by
326
+ * `scripts/lib/events-retention-banner.mjs`.
327
+ *
328
+ * @type {number}
329
+ */
330
+ export const REQUIRED_EVENTS_WINDOW_DAYS = 1;
331
+
315
332
  /**
316
333
  * Reconstruct the session facts a ping needs (`session_type`, `started_at`,
317
334
  * `completed_at`) from `<metricsDir>/events.jsonl` when `sessions.jsonl` has no
@@ -28,6 +28,8 @@
28
28
  * anywhere: basename `*.test.<ext>` / `*.spec.<ext>`, or any file
29
29
  * under a `__tests__/` directory
30
30
  * denominator every OTHER tracked code file
31
+ * reported tracked shell (EXCLUDED_SHELL_EXTENSIONS) — in NEITHER bucket,
32
+ * its volume shipped as `excludedScope.shell` (#1488, variant B)
31
33
  *
32
34
  * ## The four questions, answered in code rather than left open
33
35
  *
@@ -122,12 +124,32 @@ export const DECLARATION_SUFFIXES = Object.freeze(['.d.ts', '.d.mts', '.d.cts'])
122
124
  /** Top-level path prefix that makes a tracked code file part of the TEST corpus. */
123
125
  export const TEST_PREFIX = 'tests/';
124
126
 
127
+ /**
128
+ * Shell extensions the ratio deliberately does NOT count (#1488, owner decision
129
+ * 2026-10-02, variant B): they stay out of numerator and denominator, but their
130
+ * volume is MEASURED and reported under `excludedScope.shell`, so "this repo has
131
+ * no shell" and "this repo has 2.5k unmeasured shell lines" stop looking alike.
132
+ * Detection is by extension only — an extensionless shebang script (`.husky/*`)
133
+ * is not seen; sniffing shebangs would mean reading every extensionless tracked
134
+ * file inside a synchronous session-start probe.
135
+ */
136
+ export const EXCLUDED_SHELL_EXTENSIONS = Object.freeze(['.sh', '.bash', '.bats']);
137
+
125
138
  /**
126
139
  * Co-located test files, wherever they sit: a `*.test.<ext>` / `*.spec.<ext>`
127
140
  * basename (the vitest/jest default include shape) or a `__tests__/` segment.
128
141
  */
129
142
  const COLOCATED_TEST_RE = /\.(test|spec)\.[^./]+$|(^|\/)__tests__\//;
130
143
 
144
+ /**
145
+ * The one test-corpus rule, shared by the counted code buckets and the reported
146
+ * shell scope so the two can never split a path differently.
147
+ * @param {string} p normalised repo-relative path
148
+ */
149
+ function isTestCorpusPath(p) {
150
+ return p.startsWith(TEST_PREFIX) || COLOCATED_TEST_RE.test(p);
151
+ }
152
+
131
153
  /**
132
154
  * A tracked path that LOOKS like a test, counted or not: a `*.test.*` / `*.spec.*`
133
155
  * JS/TS file, anything under a `__tests__/` segment, or any file under `tests/`
@@ -168,7 +190,21 @@ export function classifyPath(relPath) {
168
190
  const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
169
191
  if (!CODE_EXTENSIONS.some((ext) => p.endsWith(ext))) return null;
170
192
  if (DECLARATION_SUFFIXES.some((ext) => p.endsWith(ext))) return null;
171
- return p.startsWith(TEST_PREFIX) || COLOCATED_TEST_RE.test(p) ? 'test' : 'src';
193
+ return isTestCorpusPath(p) ? 'test' : 'src';
194
+ }
195
+
196
+ /**
197
+ * Which half of the EXCLUDED shell scope a path falls into — reported, never
198
+ * counted in the ratio. `.bats` is a test framework's own extension, so it is
199
+ * test-shell anywhere; any other shell file follows the code rule above.
200
+ *
201
+ * @param {string} relPath repo-relative path
202
+ * @returns {'test'|'src'|null} null = not shell
203
+ */
204
+ function classifyShellPath(relPath) {
205
+ const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
206
+ if (!EXCLUDED_SHELL_EXTENSIONS.some((ext) => p.endsWith(ext))) return null;
207
+ return p.endsWith('.bats') || isTestCorpusPath(p) ? 'test' : 'src';
172
208
  }
173
209
 
174
210
  /**
@@ -209,7 +245,9 @@ export function countPhysicalLines(content) {
209
245
  * @param {number} [opts.ceiling]
210
246
  * @returns {{testFiles:number,testLoc:number,srcFiles:number,srcLoc:number,ratio:number|null,
211
247
  * ceiling:number,withinCorridor:boolean|null,consolidationWaveRequired:boolean|null,
212
- * skipped:number,testLikeFilesSeen:number,reason:'no-tests-found'|null}}
248
+ * skipped:number,testLikeFilesSeen:number,reason:'no-tests-found'|null,
249
+ * excludedScope:{shell:{files:number,loc:number,testFiles:number,testLoc:number,
250
+ * srcFiles:number,srcLoc:number,skipped:number}}}}
213
251
  */
214
252
  export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
215
253
  let testFiles = 0;
@@ -218,9 +256,31 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
218
256
  let srcLoc = 0;
219
257
  let skipped = 0;
220
258
  let testLikeFilesSeen = 0;
259
+ // Reported only — never added to testLoc/srcLoc (#1488 variant B). Always
260
+ // present, zeros included: a measured zero, not an absent measurement.
261
+ const shell = { files: 0, loc: 0, testFiles: 0, testLoc: 0, srcFiles: 0, srcLoc: 0, skipped: 0 };
221
262
 
222
263
  for (const rel of files) {
223
264
  if (isTestLikePath(rel)) testLikeFilesSeen++;
265
+ const shellBucket = classifyShellPath(rel);
266
+ if (shellBucket !== null) {
267
+ const content = readFile(rel);
268
+ if (content === null || content === undefined) {
269
+ shell.skipped++;
270
+ continue;
271
+ }
272
+ const lines = countPhysicalLines(content);
273
+ shell.files++;
274
+ shell.loc += lines;
275
+ if (shellBucket === 'test') {
276
+ shell.testFiles++;
277
+ shell.testLoc += lines;
278
+ } else {
279
+ shell.srcFiles++;
280
+ shell.srcLoc += lines;
281
+ }
282
+ continue;
283
+ }
224
284
  const bucket = classifyPath(rel);
225
285
  if (bucket === null) continue;
226
286
  const content = readFile(rel);
@@ -257,6 +317,7 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
257
317
  skipped,
258
318
  testLikeFilesSeen,
259
319
  reason: 'no-tests-found',
320
+ excludedScope: { shell },
260
321
  };
261
322
  }
262
323
 
@@ -275,6 +336,7 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
275
336
  skipped,
276
337
  testLikeFilesSeen,
277
338
  reason: null,
339
+ excludedScope: { shell },
278
340
  };
279
341
  }
280
342
 
@@ -297,6 +359,11 @@ export function definitionOf(source) {
297
359
  `declaration files (${DECLARATION_SUFFIXES.map((x) => `*${x}`).join(', ')}: types, not ` +
298
360
  'executable code) and non-code extensions (.md, .json, .jsonl, .yml) on BOTH sides; ' +
299
361
  'untracked files (node_modules/, coverage/, scratch)',
362
+ excludedScope:
363
+ `shell (${EXCLUDED_SHELL_EXTENSIONS.join(' ')}, by extension only — extensionless shebang ` +
364
+ 'scripts such as .husky/ hooks are not seen): counted in NEITHER bucket, its volume ' +
365
+ 'reported under excludedScope.shell (#1488). test-shell = .bats anywhere, or the ' +
366
+ 'numerator path rule above; every other shell file is src-shell. Same line rule.',
300
367
  };
301
368
  }
302
369
 
@@ -506,6 +573,9 @@ function main() {
506
573
  console.log(' denominator every other tracked code file (src by negation)');
507
574
  console.log(` code exts ${CODE_EXTENSIONS.join(' ')} (.md / .json never counted)`);
508
575
  console.log(` excluded declaration files ${DECLARATION_SUFFIXES.join(' ')} on both sides`);
576
+ console.log(
577
+ ` shell ${EXCLUDED_SHELL_EXTENSIONS.join(' ')} counted nowhere, volume reported (excludedScope.shell)`,
578
+ );
509
579
  console.log(' lines physical; blanks + comments counted; EOF-newline-insensitive');
510
580
  console.log('');
511
581
  console.log(' --json machine-readable envelope on stdout');
@@ -575,9 +645,14 @@ function main() {
575
645
  ? 'within corridor'
576
646
  : `ABOVE ceiling ${result.ceiling} — TV-003 consolidation wave required`;
577
647
  const at = envelope.ref ? ` @ ${envelope.ref}${dirty ? '+dirty' : ''}` : '';
648
+ const sh = result.excludedScope.shell;
578
649
  writeStdoutLineSync(
579
650
  `tests:src = ${r} (${result.testLoc} test LOC / ${result.srcLoc} src LOC` +
580
651
  `; ${result.testFiles} test + ${result.srcFiles} src files${at}) — ${verdict}` +
652
+ (sh.files > 0
653
+ ? `\n excluded: shell ${sh.files} files / ${sh.loc} lines ` +
654
+ `(${sh.testFiles} test + ${sh.srcFiles} src files) — in neither bucket (#1488)`
655
+ : '') +
581
656
  (dirty ? '\n NOTE: working tree is dirty — this number is NOT reproducible at that SHA' : ''),
582
657
  );
583
658
  }
@@ -23,6 +23,18 @@ const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
23
23
  /** Every event this module reports on carries this prefix. */
24
24
  const TMUX_EVENT_PREFIX = 'tmux-layout.';
25
25
 
26
+ /**
27
+ * Events window this reader needs retained (#1401 part 3): UNBOUNDED, hence
28
+ * `null`. The stats are an ALL-TIME rate, so every record ever written is in
29
+ * scope and no finite `events-rotation` retention satisfies it — a number here
30
+ * could only be invented. Truncation is reported by the envelope's own
31
+ * `complete: false` (pruned archives leave rotation tombstones), not by a
32
+ * retention floor. Read by `scripts/lib/events-retention-banner.mjs`.
33
+ *
34
+ * @type {number|null}
35
+ */
36
+ export const REQUIRED_EVENTS_WINDOW_DAYS = null;
37
+
26
38
  /**
27
39
  * Read the tmux-layout events ACROSS rotation boundaries, together with the
28
40
  * honesty verdict of that read.