session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -93,11 +93,27 @@
93
93
  * pure input→output date function is out of scope by
94
94
  * construction rather than by exception list.
95
95
  *
96
- * NOT flagged: blocks that control the clock (`now:` arg,
97
- * `vi.useFakeTimers` / `vi.setSystemTime`); date literals in
98
- * INPUT position (only `.toBe`/`.toEqual`/`.toStrictEqual`
99
- * expected values are read), which leaves the passthrough
100
- * class (input date === output date) untouched.
96
+ * NOT flagged: blocks that control the clock (`now:`, `{ now }`
97
+ * or a positional `now` — wider than the seam proof, #1478;
98
+ * SCREAMING_CASE `NOW` / `FIXED_NOW` / `NOW_MS` count as
99
+ * clock names too, #1499 item 7 — or a fake timer that pins
100
+ * the DATE: `vi.setSystemTime(<date>)`,
101
+ * `vi.useFakeTimers({ now: <date> })`, also when called in
102
+ * the setup at module level or in a `describe` that
103
+ * encloses the block; a bare `vi.useFakeTimers()` installs
104
+ * at the real clock and pins nothing, #1499 item 8). A
105
+ * handover whose value IS the real clock is no control
106
+ * (#1487): inline (`{ now: Date.now() }`) or through a
107
+ * binding in the same block (`const now = new Date();
108
+ * fn(now)`) or in the shared setup — module level,
109
+ * `describe` body, `beforeEach` (#1489 item 16). A freeze
110
+ * does not exempt a block that hands over a real clock bound
111
+ * at collection time (module level, `describe` body): that
112
+ * value was read before any hook ran (#1499 item 9). Date
113
+ * literals in INPUT position (only `.toBe`/`.toEqual`/
114
+ * `.toStrictEqual` expected values are read), which leaves
115
+ * the passthrough class (input date === output date)
116
+ * untouched.
101
117
  *
102
118
  * Per-file opt-out: `// @test-value-bans-allowed` in the first 5 lines skips
103
119
  * the file entirely (same convention as check-test-fixture-shapes.mjs's
@@ -335,18 +351,98 @@ const keyLiteralRe = (key) => new RegExp(`["'\\\\]+${key}["'\\\\]+\\s*:`);
335
351
  * An absolute date pinned as the EXPECTED value of an equality assertion.
336
352
  * Input-position date literals (`created_at: '2026-06-21T…'`) do not match —
337
353
  * that is what keeps the passthrough class (input date === output date) out.
354
+ *
355
+ * CEILING (#1473): a fixed INPUT date that a production TTL ages out later is
356
+ * not flagged either, on purpose. Measured 2026-09-29 with a shim that moves
357
+ * `Date.now()` and `new Date()` forward in every vitest worker: the 34 test
358
+ * files (plus one fixture module) holding a fixed `created_at: '2026-05…09'`
359
+ * (1,211 tests) stayed green at +120, +365 and +3,650 days, so a rule keyed on
360
+ * fixed input dates would have reported false positives only. The one real
361
+ * bomb of that shape, the phase-skip fixture before 6670149c, was green at
362
+ * -60 days and red at +0 and +120 days under the same shim — and
363
+ * scanClockBombs() reaches that file since #1476 (alias imports are read) but
364
+ * stays silent on it: the file hands no clock argument to anything. REVISIT when a
365
+ * second fixed-input-date bomb lands on main: the class then recurs, and a
366
+ * clock-shift run of the suite is the check that finds it without guessing.
338
367
  */
339
368
  const DATE_EXPECTATION =
340
369
  /\.(?:toBe|toEqual|toStrictEqual)\(\s*(['"`])(\d{4}-\d{2}-\d{2}(?:[T ][^'"`]*)?)\1\s*\)/;
341
370
 
342
- /** An explicit clock handed to a callee — the seam this ban asks tests to use. */
343
- const CLOCK_ARG = /\b(?:now|nowMs|nowIso|clock|currentDate)\s*:/;
371
+ /**
372
+ * A clock-named token not read off an object (`Date.now`, `opts.now`) — shared
373
+ * head of the patterns below. Group 1 is the name.
374
+ *
375
+ * SCREAMING_CASE module constants are clock names too (#1499 item 7): `NOW`,
376
+ * `NOW_MS`, `NOW_ISO`, and any `<WORD>_`-prefixed form (`FIXED_NOW`,
377
+ * `FROZEN_NOW`). An `i` flag would not reach them — `_` is a `\w`, so the
378
+ * lookbehind and the terminators below never see a `FIXED_NOW` boundary.
379
+ * Measured 2026-10-02 over 722 test files: `NOW` is handed over in 29 files,
380
+ * `FIXED_NOW` in 10, `NOW_MS` in 5, `NOW_ISO` in 2.
381
+ */
382
+ const CLOCK_NAME = String.raw`(?<![.\w$])(now|nowMs|nowIso|clock|currentDate|(?:[A-Z][A-Z0-9]*_)*(?:NOW(?:_MS|_ISO)?|CLOCK|CURRENT_DATE))\s*`;
383
+
384
+ /**
385
+ * Pass 1, the seam PROOF: a NAMED clock key (`{ now: … }`) — only that shape says
386
+ * the callee's API takes a clock, whatever value it is handed. Every line it
387
+ * matches also matches CONTROL_CLOCK_ARG, so a block that proves a seam is never
388
+ * flagged itself — UNLESS the value it hands over is the real clock (#1487):
389
+ * then the block proves the seam and is a bomb in the same breath.
390
+ */
391
+ const SEAM_CLOCK_ARG = new RegExp(`${CLOCK_NAME}:`);
392
+
393
+ /**
394
+ * Pass 2, is this block clock-CONTROLLED: any clock handover. Property shorthand
395
+ * (`{ now }`, `{ repoRoot, now }`) and a positional `now` count too; `Date.now()`
396
+ * and `const now =` do not. Wider is the safe direction here — it can only
397
+ * remove findings. Group 2 is the terminator: `:` means the name is a KEY and
398
+ * the handed-over value follows it; any other terminator means the name IS the
399
+ * value. A handover only counts when that value is not the real clock — see
400
+ * {@link handsOverControlledClock}.
401
+ *
402
+ * CEILING (#1478): pass 1 reads only the named form, because the wide form also
403
+ * turns helpers into seams — +20 seamed (file, id) pairs in 15 files
404
+ * (172 → 192, measured 2026-10-01 @ ffc5929f), e.g. `expectDeny` via a
405
+ * `{ manifestMtimeMs: now }` fixture. Cost: a file whose ONLY clock handover is
406
+ * `{ now }` or a positional `now` loses its seam proof and its bombs go unseen.
407
+ * REVISIT on the first B5 finding whose subject is a helper, or the first CI
408
+ * `test-value-bans` B5 finding the ±days clock-shift probe shows to be false.
409
+ */
410
+ const CONTROL_CLOCK_ARG = new RegExp(String.raw`${CLOCK_NAME}(:|,|\}|\))`, 'g');
411
+
412
+ /** A read of the real wall clock: `Date.now()`, an argument-less `new Date()`, `performance.now()`. */
413
+ const REAL_CLOCK_READ = /\bDate\.now\(\)|\bnew Date\(\s*\)|\bperformance\.now\(\)/;
414
+
415
+ /**
416
+ * `const now = <rhs>` / `now = <rhs>` — a clock-named binding (not `==`, not an
417
+ * arrow `now =>`). Group 1 is the name, group 2 the right-hand side.
418
+ */
419
+ const CLOCK_BINDING = new RegExp(String.raw`${CLOCK_NAME}=(?![=>])(.*)`);
420
+
421
+ /**
422
+ * Freezing the global clock — equally valid control, but not a seam PROOF. Only
423
+ * the INSTALLERS count: `advanceTimersByTime` / `runAllTimers` move a clock some
424
+ * installer already faked and freeze nothing on their own, so a block that only
425
+ * advances timers is controlled exactly when an installer reaches it. Whether an
426
+ * installer pins the DATE is {@link pinsDate}'s call. Group 1 is the installer.
427
+ */
428
+ const DATE_PIN_CALL = /\b(useFakeTimers|setSystemTime)\s*\(/g;
344
429
 
345
- /** Freezing the global clock — equally valid control, but not a seam PROOF. */
346
- const FAKE_TIMER = /\b(?:useFakeTimers|setSystemTime|advanceTimersByTime|runAllTimers)\b/;
430
+ /**
431
+ * Vitest aliases that resolve to the repo's own modules — source of truth is
432
+ * `resolve.alias` in vitest.config.mjs (today only `@lib`). CEILING: one entry
433
+ * per alias, kept by hand; REVISIT when vitest.config.mjs gains an alias — the
434
+ * parametrised B5 test reads that config and goes red until the key is listed
435
+ * here. Dynamic imports (`await import('@lib/…')`) are not read in either form.
436
+ */
437
+ const ALIAS_PREFIXES = ['@lib/'];
347
438
 
348
- /** `import { a, b as c } from './rel.mjs'` — SUT candidates live behind these. */
349
- const RELATIVE_IMPORT = /import\s+([^;]+?)\s+from\s+['"](\.[^'"]+)['"]/g;
439
+ /** `import { a, b as c } from './rel.mjs'` or `'@lib/x.mjs'` — SUT candidates live behind these. */
440
+ const LOCAL_IMPORT = new RegExp(
441
+ String.raw`import\s+([^;]+?)\s+from\s+['"]((?:\.|` +
442
+ ALIAS_PREFIXES.map((p) => p.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')).join('|') +
443
+ String.raw`)[^'"]+)['"]`,
444
+ 'g',
445
+ );
350
446
 
351
447
  // ---------------------------------------------------------------------------
352
448
  // File enumeration
@@ -564,16 +660,46 @@ function declaredHookSubjects(lines) {
564
660
  * @returns {Array<{start: number, end: number}>} half-open [start, end) indices
565
661
  */
566
662
  function testBlocks(lines) {
567
- const OPENER = /^(\s*)(?:it|test)(?:\.\w+)*\s*\(/;
568
- const SIBLING = /^\s*(?:it|test|describe)(?:\.\w+)*\s*\(/;
663
+ return callRanges(lines, /^(\s*)(?:it|test)(?:\.\w+)*\s*\(/, /^\s*(?:it|test|describe)(?:\.\w+)*\s*\(/);
664
+ }
665
+
666
+ /**
667
+ * Split a file into `describe(` blocks, segmented like {@link testBlocks} but
668
+ * with no sibling fallback: a `describe` without its own same-indentation `});`
669
+ * runs to the end of the file. For its one consumer (B5's setup fake-timer
670
+ * scope) over-inclusion is NOT safe: a wider `describe` exempts more blocks, so
671
+ * a fake timer in it hides a sibling describe's time bomb — the false negative
672
+ * this scoping exists to remove. Ceiling (BV-004): two shapes still widen —
673
+ * a `describe` closed with options (`}, 10_000);`) runs to end of file, and a
674
+ * tagged-template `describe.each\`…\`` never matches the opener, so its timer
675
+ * reads as module level. Measured 2026-10-02 over 722 test files: 0 of the 13
676
+ * fake-timer files use either shape. Revisit when one appears in a fake-timer
677
+ * file, or when a B5 miss traces to one.
678
+ * @param {string[]} lines
679
+ * @returns {Array<{start: number, end: number}>} half-open [start, end) indices
680
+ */
681
+ function describeBlocks(lines) {
682
+ return callRanges(lines, /^(\s*)describe(?:\.\w+)*\s*\(/, null);
683
+ }
684
+
685
+ /**
686
+ * Shared segmenter: every line matching `opener` (group 1 = its indentation)
687
+ * opens a range that ends after the first later line closing at the SAME
688
+ * indentation (`});`), or before the first `sibling` line, or at end of file.
689
+ * @param {string[]} lines
690
+ * @param {RegExp} opener
691
+ * @param {RegExp|null} sibling
692
+ * @returns {Array<{start: number, end: number}>} half-open [start, end) indices
693
+ */
694
+ function callRanges(lines, opener, sibling) {
569
695
  const out = [];
570
696
  for (let i = 0; i < lines.length; i++) {
571
- const m = OPENER.exec(lines[i]);
697
+ const m = opener.exec(lines[i]);
572
698
  if (!m) continue;
573
699
  const closer = new RegExp(`^${m[1]}\\}\\)`);
574
700
  let end = lines.length;
575
701
  for (let j = i + 1; j < lines.length; j++) {
576
- if (SIBLING.test(lines[j])) {
702
+ if (sibling?.test(lines[j])) {
577
703
  end = j;
578
704
  break;
579
705
  }
@@ -588,17 +714,17 @@ function testBlocks(lines) {
588
714
  }
589
715
 
590
716
  /**
591
- * Identifiers this test file imports from the repo's OWN modules (relative
592
- * specifiers). These are the subject-under-test candidates for B5; framework
593
- * (`vitest`) and stdlib (`node:*`) imports are structurally excluded because
594
- * their specifiers are not relative.
717
+ * Identifiers this test file imports from the repo's OWN modules (relative or
718
+ * vitest-alias specifiers). These are the subject-under-test candidates for B5;
719
+ * framework (`vitest`) and stdlib (`node:*`) imports are structurally excluded
720
+ * because their specifiers match neither form.
595
721
  * @param {string} content
596
722
  * @returns {string[]}
597
723
  */
598
724
  function importedLocalIdentifiers(content) {
599
725
  /** @type {Set<string>} */
600
726
  const ids = new Set();
601
- for (const m of content.matchAll(RELATIVE_IMPORT)) {
727
+ for (const m of content.matchAll(LOCAL_IMPORT)) {
602
728
  const clause = m[1];
603
729
  // `{ a, b as c }` → c ; `x` / `* as ns` → x / ns
604
730
  for (const part of clause.replace(/[{}]/g, ',').split(',')) {
@@ -612,6 +738,144 @@ function importedLocalIdentifiers(content) {
612
738
  return [...ids];
613
739
  }
614
740
 
741
+ /**
742
+ * The clock-named bindings in these lines (`const now = Date.now()`,
743
+ * `now = new Date('2026-01-01')`): per name, whether ANY binding reads the REAL
744
+ * clock and whether ANY binding does not. Handing a real-clock binding over is
745
+ * reading the wall clock with an extra step, not controlling it.
746
+ *
747
+ * CEILING (#1487): a real-clock value laundered through a non-clock name
748
+ * (`const t = Date.now(); fn({ now: t })`) still counts as control. REVISIT
749
+ * when a clock-shift run of the suite finds a bomb fed by that route.
750
+ * @param {string[]} live non-comment lines
751
+ * @returns {Map<string, {real: boolean, other: boolean}>}
752
+ */
753
+ function clockBindings(live) {
754
+ /** @type {Map<string, {real: boolean, other: boolean}>} */
755
+ const out = new Map();
756
+ for (const line of live) {
757
+ const m = CLOCK_BINDING.exec(line);
758
+ if (!m) continue;
759
+ const seen = out.get(m[1]) ?? { real: false, other: false };
760
+ if (REAL_CLOCK_READ.test(m[2])) seen.real = true;
761
+ else seen.other = true;
762
+ out.set(m[1], seen);
763
+ }
764
+ return out;
765
+ }
766
+
767
+ /**
768
+ * The clock-named names a block hands over as the REAL clock: its own real-clock
769
+ * bindings, plus the shared setup's (module level, `describe` body,
770
+ * `beforeEach`) for every name the block does not rebind itself (#1489 item 16:
771
+ * read block-wise only, `const now = Date.now()` at module level counted as
772
+ * control and kept a genuine bomb silent).
773
+ *
774
+ * A setup name counts only when EVERY setup binding of it reads the real clock.
775
+ * Setup is file-wide, not per-`describe`, so a name bound to the real clock in
776
+ * one `beforeEach` and to a fixed date in another is ambiguous for any given
777
+ * block — read as control, the scan's precision-first direction.
778
+ * @param {Map<string, {real: boolean, other: boolean}>} own the block's bindings
779
+ * @param {Map<string, {real: boolean, other: boolean}>} setup the shared setup's bindings
780
+ * @returns {Set<string>}
781
+ */
782
+ function realClockNames(own, setup) {
783
+ /** @type {Set<string>} */
784
+ const names = new Set();
785
+ for (const [name, b] of own) if (b.real) names.add(name);
786
+ for (const [name, b] of setup) if (b.real && !b.other && !own.has(name)) names.add(name);
787
+ return names;
788
+ }
789
+
790
+ /**
791
+ * The value expression starting at `from`, up to the first `,` or closing
792
+ * bracket at nesting depth 0 (`now: new Date(x), other` → `new Date(x)`).
793
+ * Brackets inside string literals are not tracked; a cut-short value can only
794
+ * hide a real-clock read, which leaves the block counted as controlled — the
795
+ * pre-#1487 behaviour, never a new finding.
796
+ */
797
+ function valueExpressionAt(line, from) {
798
+ let depth = 0;
799
+ for (let i = from; i < line.length; i++) {
800
+ const c = line[i];
801
+ if (c === '(' || c === '[' || c === '{') depth++;
802
+ else if (c === ')' || c === ']' || c === '}') {
803
+ if (depth === 0) return line.slice(from, i);
804
+ depth--;
805
+ } else if (c === ',' && depth === 0) return line.slice(from, i);
806
+ }
807
+ return line.slice(from);
808
+ }
809
+
810
+ /**
811
+ * True when this line hands over a clock that is NOT the real one: a clock-named
812
+ * handover (CONTROL_CLOCK_ARG) whose value neither reads the wall clock inline
813
+ * (`{ now: Date.now() }`) nor names a real-clock binding in the block's scope
814
+ * (`const now = Date.now(); fn(now)`, see `realClockNames`). #1487: before this,
815
+ * both shapes counted as control and kept a genuine time bomb silent.
816
+ * @param {string} line
817
+ * @param {Set<string>} realClock names bound to the real clock in this block's scope
818
+ */
819
+ function handsOverControlledClock(line, realClock) {
820
+ return handoverValues(line).some((v) => !REAL_CLOCK_READ.test(v) && !namesAny(v, realClock));
821
+ }
822
+
823
+ /**
824
+ * The values this line hands over as a clock (CONTROL_CLOCK_ARG): after a
825
+ * `name:` key the expression that follows it, otherwise the name itself.
826
+ * @param {string} line
827
+ * @returns {string[]}
828
+ */
829
+ function handoverValues(line) {
830
+ return [...line.matchAll(CONTROL_CLOCK_ARG)].map((m) =>
831
+ m[2] === ':' ? valueExpressionAt(line, m.index + m[0].length) : m[1],
832
+ );
833
+ }
834
+
835
+ /**
836
+ * True when `value` names one of `names` as a whole identifier.
837
+ * @param {string} value
838
+ * @param {Iterable<string>} names
839
+ */
840
+ function namesAny(value, names) {
841
+ return [...names].some((n) => new RegExp(String.raw`(?<![.\w$])${n}(?![\w$])`).test(value));
842
+ }
843
+
844
+ /**
845
+ * True when this line freezes the calendar DATE, not just the timers (#1499
846
+ * item 8). vitest 4.1 installs `useFakeTimers()` at `this._fakingDate ||
847
+ * Date.now()` — the real clock unless a date was set first — so only
848
+ * `setSystemTime(<date>)` or a `useFakeTimers` config naming `now` pins a day;
849
+ * `{ toFake: ['setTimeout'] }` leaves Date real. A pin whose value reads the wall
850
+ * clock pins nothing: inline (`setSystemTime(new Date())`, `{ now: Date.now() }`)
851
+ * or through a clock-named binding the scanner knows reads it
852
+ * (`const now = new Date(); vi.setSystemTime(now)`, `useFakeTimers({ now })` —
853
+ * review of ac3ded47). An argument the line cannot read — a variable, a config
854
+ * spread over lines, a quoted key — counts as a pin: precision first, as
855
+ * everywhere in B5. CEILING: a pin value laundered through a NON-clock name
856
+ * (`const t = new Date(); vi.setSystemTime(t)`) still counts — the
857
+ * {@link clockBindings} ceiling.
858
+ * @param {string} line
859
+ * @param {Set<string>} realClock names bound to the real clock in this line's scope
860
+ */
861
+ function pinsDate(line, realClock) {
862
+ for (const m of line.matchAll(DATE_PIN_CALL)) {
863
+ const arg = valueExpressionAt(line, m.index + m[0].length).trim();
864
+ let value = arg;
865
+ if (m[1] === 'useFakeTimers') {
866
+ if (arg === '') continue;
867
+ if (!arg.startsWith('{') || !arg.endsWith('}')) return true;
868
+ const key = /(?<![\w$])now(?![\w$])\s*([:,}])?/.exec(arg);
869
+ if (!key) continue;
870
+ if (!key[1]) return true;
871
+ // `now:` hands over the expression after it; shorthand `{ now }` hands over the binding `now`.
872
+ value = key[1] === ':' ? valueExpressionAt(arg, key.index + key[0].length) : 'now';
873
+ }
874
+ if (!REAL_CLOCK_READ.test(value) && !namesAny(value, realClock)) return true;
875
+ }
876
+ return false;
877
+ }
878
+
615
879
  /** True when `id` is invoked anywhere in these lines. */
616
880
  function callsIdentifier(lines, id) {
617
881
  const re = new RegExp(`\\b${id}\\s*\\(`);
@@ -623,8 +887,9 @@ function callsIdentifier(lines, id) {
623
887
  *
624
888
  * Two passes over the file's `it`/`test` blocks:
625
889
  * 1. PROVE the seam — an imported id called from a block that also hands over
626
- * an explicit clock argument is clock-seamed. A public API only grows a
627
- * `now` parameter because the function reads the clock on its main path.
890
+ * a NAMED clock key (SEAM_CLOCK_ARG) is clock-seamed. A public API only
891
+ * grows a `now` parameter because the function reads the clock on its
892
+ * main path.
628
893
  * 2. FLAG — in blocks with NO clock control at all, any equality assertion
629
894
  * pinning an absolute date against such a subject is a time bomb.
630
895
  *
@@ -642,33 +907,129 @@ function scanClockBombs(relPath, content, lines) {
642
907
  const sutIds = importedLocalIdentifiers(content);
643
908
  if (sutIds.length === 0) return findings;
644
909
 
645
- const blocks = testBlocks(lines).map(({ start, end }) => {
910
+ const ranges = testBlocks(lines);
911
+
912
+ // The setup every block shares (helpers, constants, `describe` bodies,
913
+ // beforeEach): where an echo may come from, and where a clock may be bound or
914
+ // frozen for the blocks below it. Never a sibling test — its assertions,
915
+ // titles and clock literals say nothing about this block's inputs.
916
+ // CEILING (BV-004): a block without its own same-indentation `});` (a
917
+ // `}, 10_000);` close, an expression-bodied arrow) runs on to the next sibling
918
+ // and takes the setup lines in between out of sharedSetup. Both directions
919
+ // follow: a swallowed `now = Date.now()` hides a finding, a swallowed fake
920
+ // timer adds one. Measured 2026-10-02 @ 46e6695b over 722 test files: 221 of
921
+ // 15,644 blocks lack the closer, 1 of them swallows a beforeAll/beforeEach,
922
+ // none a fake-timer install; 0 blocks are one-line its. REVISIT when a
923
+ // swallowed setup line binds or freezes a clock — then end such blocks on
924
+ // their own close instead of the sibling (testBlocks() also feeds B2/B3).
925
+ /** @type {Set<number>} */
926
+ const inBlock = new Set();
927
+ for (const { start, end } of ranges) for (let q = start; q < end; q++) inBlock.add(q);
928
+ const sharedSetup = lines.filter((_, q) => !inBlock.has(q));
929
+ const setupLive = sharedSetup.filter((l) => !isCommentLine(l));
930
+ const setupBindings = clockBindings(setupLive);
931
+ // A clock frozen in setup (`beforeEach(() => vi.setSystemTime(…))`) controls
932
+ // every block it reaches, including a `now = Date.now()` bound after it — and
933
+ // it reaches only the blocks vitest runs it for: all of them from module level,
934
+ // only its own `describe`'s from inside one. A fake timer in a SIBLING
935
+ // `describe` exempted every block in the file until 2026-10-02 and kept a
936
+ // genuine bomb silent (review of 3206ab24). `null` = module level.
937
+ const describes = describeBlocks(lines);
938
+ /**
939
+ * Innermost `describe` holding line q — ranges nest, so the latest start that
940
+ * still holds it — or `null` at module level.
941
+ * @param {number} q
942
+ */
943
+ const innermostDescribe = (q) => {
944
+ const holders = describes.filter((d) => d.start <= q && q < d.end);
945
+ return holders.length > 0 ? holders[holders.length - 1] : null;
946
+ };
947
+ const setupRealClock = realClockNames(new Map(), setupBindings);
948
+ /** @type {Array<{start: number, end: number}|null>} */
949
+ const setupTimerScopes = [];
950
+ lines.forEach((l, q) => {
951
+ if (inBlock.has(q) || isCommentLine(l) || !pinsDate(l, setupRealClock)) return;
952
+ setupTimerScopes.push(innermostDescribe(q));
953
+ });
954
+ /** @param {number} start */
955
+ const setupFreezes = (start) =>
956
+ setupTimerScopes.some((d) => d === null || (d.start < start && start < d.end));
957
+ // A real-clock name bound by a statement of the module itself or of a
958
+ // `describe` body is read at COLLECTION time, before vitest runs any hook
959
+ // (#1499 item 9). No freeze, in the setup or in the block, reaches that value,
960
+ // so a block handing it over is not exempted by one. Every other setup binding
961
+ // is DEFERRED — a hook callback, or a helper function's body (`validRecord()`,
962
+ // `writeLock()`), which runs when called, after any freeze: 12 of the 13 setup
963
+ // real-clock bindings outside a hook were such helper locals (measured
964
+ // 2026-10-02 over 722 test files, review of ac3ded47). A name qualifies only
965
+ // with no deferred binding at all — anything else reads as control, the
966
+ // precision-first direction. "Statement of" is read from indentation: column 0
967
+ // at module level, the enclosing `describe`'s column + 2 inside one (the
968
+ // prettier indent of this repo). A misread only moves names OUT of the set: a
969
+ // deeper or other indent style counts as deferred, and a hook range that
970
+ // over-extends (a one-line `beforeEach(…);`) does the same. CEILINGS: a
971
+ // one-line helper (`function f() { const now = Date.now(); … }`) at
972
+ // `describe`-body indent counts as collection time; a freeze that itself runs
973
+ // at collection time (a module-level `vi.setSystemTime` above the binding) is
974
+ // not read. Measured 2026-10-02 over 722 test files: 1 collection-time binding
975
+ // (tests/hooks/lock-reconcile.test.mjs:24), 0 installers outside a hook or test
976
+ // block. REVISIT on the first B5 finding traced to either shape.
977
+ const hooks = callRanges(lines, /^(\s*)(?:before|after)(?:Each|All)\s*\(/, null);
978
+ const indentOf = (/** @type {string} */ l) => /^\s*/.exec(l)[0].length;
979
+ /** @param {number} q */
980
+ const atCollection = (q) => {
981
+ if (hooks.some((h) => h.start <= q && q < h.end)) return false;
982
+ const d = innermostDescribe(q);
983
+ return indentOf(lines[q]) === (d ? indentOf(lines[d.start]) + 2 : 0);
984
+ };
985
+ const deferredBindings = clockBindings(
986
+ lines.filter((l, q) => !inBlock.has(q) && !isCommentLine(l) && !atCollection(q)),
987
+ );
988
+ const collectionClock = [...setupBindings]
989
+ .filter(([name, b]) => b.real && !b.other && !deferredBindings.has(name))
990
+ .map(([name]) => name);
991
+
992
+ const blocks = ranges.map(({ start, end }) => {
646
993
  const body = lines.slice(start, end);
647
994
  const live = body.filter((l) => !isCommentLine(l));
995
+ const own = clockBindings(live);
996
+ const realClock = realClockNames(own, setupBindings);
997
+ const unfrozen = collectionClock.filter((name) => !own.has(name));
998
+ const handsOverUnfrozen = live.some((l) => handoverValues(l).some((v) => namesAny(v, unfrozen)));
648
999
  return {
649
1000
  start,
650
1001
  body,
651
- hasClockArg: live.some((l) => CLOCK_ARG.test(l)),
652
- hasFakeTimer: live.some((l) => FAKE_TIMER.test(l)),
1002
+ provesSeam: live.some((l) => SEAM_CLOCK_ARG.test(l)),
1003
+ hasClockArg: live.some((l) => handsOverControlledClock(l, realClock)),
1004
+ freezesDate: !handsOverUnfrozen && (setupFreezes(start) || live.some((l) => pinsDate(l, realClock))),
653
1005
  };
654
1006
  });
655
1007
 
656
1008
  /** @type {Set<string>} */
657
1009
  const seamed = new Set();
658
1010
  for (const b of blocks) {
659
- if (!b.hasClockArg) continue;
1011
+ if (!b.provesSeam) continue;
660
1012
  for (const id of sutIds) if (callsIdentifier(b.body, id)) seamed.add(id);
661
1013
  }
662
1014
  if (seamed.size === 0) return findings;
663
1015
 
664
1016
  for (const b of blocks) {
665
- if (b.hasClockArg || b.hasFakeTimer) continue;
1017
+ if (b.hasClockArg || b.freezesDate) continue;
666
1018
  const subject = [...seamed].find((id) => callsIdentifier(b.body, id));
667
1019
  if (!subject) continue;
1020
+ const echoSources = [...b.body, ...sharedSetup].filter(
1021
+ (l) => !isCommentLine(l) && !DATE_EXPECTATION.test(l),
1022
+ );
668
1023
  b.body.forEach((line, k) => {
669
1024
  if (isCommentLine(line)) return;
670
1025
  const m = DATE_EXPECTATION.exec(line);
671
1026
  if (!m) return;
1027
+ // Echo: the pinned value is also an input of this block or its shared setup,
1028
+ // not a clock reading (the CEILING above keeps fixed INPUT dates unflagged on
1029
+ // purpose). CEILING: a real bomb whose date also stands in its own block or
1030
+ // the shared setup (its title, a trailing comment, a helper fixture) is
1031
+ // swallowed; REVISIT when a clock-shift run finds a bomb this scan missed.
1032
+ if (echoSources.some((l) => l.includes(m[2]))) return;
672
1033
  findings.push({
673
1034
  file: relPath,
674
1035
  line: b.start + k + 1,
@@ -204,6 +204,7 @@ const ALLOWED_EMOJI_CODEPOINTS = new Set([
204
204
  0x1f5a5, // 🖥
205
205
  0x1f6a6, // 🚦
206
206
  0x1f6a8, // 🚨
207
+ 0x1f9ed, // 🧭 (navigator banner line in hooks/on-session-start.mjs — #1462)
207
208
  0x2139, // ℹ
208
209
  0x2194, // ↔
209
210
  0x26a0, // ⚠
@@ -74,7 +74,7 @@
74
74
  * a backlog, printed as an aggregate like S4 and enumerated under `--list`.
75
75
  *
76
76
  * S6 carries no camelCase fallback ON PURPOSE: adding one re-hides both reaper
77
- * keys, whose camelCase names exist in `orphan-reaper.mjs` as that module's own
77
+ * keys, whose camelCase names exist in `orphan-reaper/defaults.mjs` as that module's own
78
78
  * defaults. A name is not a read.
79
79
  *
80
80
  * S2 applies to TOP-LEVEL keys only — a nested key reaches code through its
@@ -41,7 +41,7 @@
41
41
  * a string literal counts only when the command STARTS the literal (after
42
42
  * optional whitespace) AND the literal carries no `${` interpolation. That
43
43
  * second half is what separates
44
- * `' glab ci status --pipeline-id LATEST'` (a real command handed to a
44
+ * `' glab ci status --branch main'` (a real command handed to a
45
45
  * shell) from `` `glab note add spawn error: ${err}` `` (a log message).
46
46
  * 4. **The first token after the CLI must be a REAL top-level subcommand** of
47
47
  * that CLI, measured from `gh --help` / `glab --help` (gh 2.86.0,