session-orchestrator 5.2.0 → 5.3.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 (295) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. package/pi/prompts/npm-publish.md +0 -11
@@ -531,15 +531,23 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
531
531
  const BACKFILL_STUB_STATUSES = new Set(['abandoned']);
532
532
 
533
533
  /**
534
- * True when a sessions.jsonl record is a backfilled STUB reconstructed
535
- * provenance (`_backfill_source`) AND a stub status. Both are required: a
536
- * hand-written `abandoned` record with no backfill provenance is somebody's
537
- * deliberate statement and is never superseded on our own initiative.
534
+ * True when a sessions.jsonl record is a backfilled stub this module may
535
+ * SUPERSEDE — reconstructed provenance (`_backfill_source`) AND a stub status.
536
+ * Both are required: a hand-written `abandoned` record with no backfill
537
+ * provenance is somebody's deliberate statement and is never superseded on our
538
+ * own initiative.
539
+ *
540
+ * Named for the decision it makes, not for the record class, because
541
+ * `sessions-staleness-banner.mjs` asks a DIFFERENT question about the same
542
+ * records — `isNonAnchorStub()` there accepts EITHER marker, since a record
543
+ * only needs to be doubtful to be disqualified as a time anchor, where this
544
+ * one must be proven reconstructed before it may be overwritten. Both are
545
+ * module-local on purpose; they must not be shared.
538
546
  *
539
547
  * @param {unknown} record
540
548
  * @returns {boolean}
541
549
  */
542
- function isBackfillStub(record) {
550
+ function isSupersedableStub(record) {
543
551
  if (!record || typeof record !== 'object' || Array.isArray(record)) return false;
544
552
  if (typeof record._backfill_source !== 'string') return false;
545
553
  return BACKFILL_STUB_STATUSES.has(record.status);
@@ -636,7 +644,7 @@ function classifyExisting(readFileSync, sessionsPath, { recordId, sessionId, sem
636
644
  const sessionRecords = readJsonlSafe(readFileSync, sessionsPath);
637
645
  const newest = findRecordedSession(sessionRecords, { sessionId, semanticSessionId, startedAt });
638
646
  if (!newest) return { kind: 'absent' };
639
- if (isBackfillStub(newest)) {
647
+ if (isSupersedableStub(newest)) {
640
648
  return { kind: 'stub', matchedId: newest.session_id, stubId: newest.session_id };
641
649
  }
642
650
  return { kind: 'canonical', matchedId: newest.session_id };
@@ -1034,6 +1042,9 @@ export async function backfillAbandonedSession({
1034
1042
  * { action: 'skipped-not-completed', status } — STATE.md status isn't 'completed'
1035
1043
  * { action: 'skipped-no-session-id' } — completed but no `session:` field
1036
1044
  * { action: 'skipped-already-recorded', sessionId } — sessions.jsonl already has it
1045
+ * { action: 'skipped-key-occupied', sessionId } — identity absent, but a canonical
1046
+ * non-stub record of a DIFFERENT session
1047
+ * already holds this key (#1388 P8)
1037
1048
  * { action: 'skipped-marker-exists', sessionId } — lost the TOCTOU claim
1038
1049
  * { action: 'error', error, sessionId? } — any failure, swallowed
1039
1050
  *
@@ -1118,6 +1129,22 @@ export async function backfillCompletedFromStateMd({
1118
1129
  if (existing.kind === 'canonical') {
1119
1130
  return { action: 'skipped-already-recorded', sessionId: existing.matchedId };
1120
1131
  }
1132
+ // Key-occupancy guard (#1380 P6) — the counterpart of the abandoned
1133
+ // path's occupied-key check. Identity can say `absent` (label drift
1134
+ // veto, conflicting native join) while a canonical, authoritative record
1135
+ // already holds `recordId`; canonical readers collapse by session_id
1136
+ // newest-wins, so appending here would displace that record with this
1137
+ // zero-counter one. `recordId` is already the native id whenever STATE.md
1138
+ // carries one, so there is no alternate key to fall back to: skip.
1139
+ if (existing.kind === 'absent'
1140
+ && canonicalizeSessions(readJsonlSafe(readFileSync, sessionsPath))
1141
+ .some((record) => record?.session_id === recordId && !isSupersedableStub(record))) {
1142
+ // Distinct action (#1388 P8): `skipped-already-recorded` above means
1143
+ // "this very session is already on file"; here identity said ABSENT and
1144
+ // a DIFFERENT session's record occupies the key. Sharing one string made
1145
+ // the two causes indistinguishable in the backfill_completed telemetry.
1146
+ return { action: 'skipped-key-occupied', sessionId: recordId };
1147
+ }
1121
1148
  const supersedes = existing.kind === 'stub' ? existing.stubId : null;
1122
1149
  // Preserve a matched stub's key for its append-only replacement. Without
1123
1150
  // event timestamps or a raw join on a legacy stub, a new UUID key would
@@ -39,10 +39,11 @@
39
39
  * - Production code is silent: no console.log, no console.warn.
40
40
  */
41
41
 
42
- import { open, readFile } from 'node:fs/promises';
42
+ import { readFile } from 'node:fs/promises';
43
43
  import path from 'node:path';
44
44
 
45
45
  import { readCanonicalSessions } from './sessions-canonical.mjs';
46
+ import { readTailWindow } from './tail-window.mjs';
46
47
  import { withStateMdLock } from './session-lock.mjs';
47
48
  import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
48
49
  import { parseStateMd } from './state-md/yaml-parser.mjs';
@@ -208,31 +209,19 @@ const EVENTS_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
208
209
  async function readSessionIdsFromEvents(repoRoot) {
209
210
  const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
210
211
 
211
- let text = '';
212
- // Assigned in the try below; every path that reaches its read site has
213
- // passed through that assignment (the catch returns early).
214
- let windowIsPartial;
215
- let handle = null;
212
+ let window;
216
213
  try {
217
- handle = await open(filePath, 'r');
218
- const { size } = await handle.stat();
219
- const start = size > EVENTS_TAIL_BYTES ? size - EVENTS_TAIL_BYTES : 0;
220
- windowIsPartial = start > 0;
221
- const length = size - start;
222
- if (length > 0) {
223
- const buf = Buffer.alloc(length);
224
- const { bytesRead } = await handle.read(buf, 0, length, start);
225
- text = buf.subarray(0, bytesRead).toString('utf8');
226
- }
214
+ // Sync read inside an async function: 1 MiB ceiling, one call per n-claim —
215
+ // the shared primitive (tail-window.mjs) owns the byte window, this reader
216
+ // keeps its own fail-open mapping.
217
+ window = readTailWindow(filePath, EVENTS_TAIL_BYTES);
227
218
  } catch {
228
219
  return [];
229
- } finally {
230
- if (handle !== null) await handle.close().catch(() => {});
231
220
  }
232
221
 
233
- const lines = text.split(/\r?\n/);
222
+ const lines = window.text.split(/\r?\n/);
234
223
  // The window started mid-file, so line 0 may be a truncated record.
235
- if (windowIsPartial) lines.shift();
224
+ if (window.cut) lines.shift();
236
225
 
237
226
  const cutoff = Date.now() - EVENTS_MAX_AGE_MS;
238
227
  const ids = [];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve the leading /session mode independently of the user's task context.
3
+ * This is a text boundary, not a shell/option parser: quotes and command syntax
4
+ * in the context remain data. `unknown` is a ledger value, never a live mode.
5
+ *
6
+ * @param {string} [argumentsText] Complete text following /session.
7
+ * @returns {{sessionType: string, context: string, profile?: string, invalidMode?: string}}
8
+ */
9
+ export function resolveSessionInvocation(argumentsText = '') {
10
+ if (typeof argumentsText !== 'string') {
11
+ throw new TypeError('session invocation arguments must be a string');
12
+ }
13
+ const [_, mode, context = ''] = argumentsText.trimStart().match(/^(\S+)(?:\s+([\s\S]*))?$/u) ?? [];
14
+ if (mode === undefined) return { sessionType: 'deep', context: '' };
15
+ if (mode === 'ultradeep') return { sessionType: 'deep', profile: 'ultradeep', context };
16
+ if (['housekeeping', 'feature', 'deep'].includes(mode)) {
17
+ return { sessionType: mode, context };
18
+ }
19
+ return { sessionType: 'deep', context, invalidMode: mode };
20
+ }
@@ -5,8 +5,8 @@
5
5
  * Leaf module — no imports from siblings or parent.
6
6
  *
7
7
  * Exports: CURRENT_SESSION_SCHEMA_VERSION, SESSION_KEY_ALIASES,
8
- * VALID_SESSION_TYPES, VALID_SESSION_PROFILES, REQUIRED_FIELDS,
9
- * AGENT_SUMMARY_FIELDS
8
+ * WAVE_KEY_ALIASES, VALID_SESSION_TYPES, VALID_SESSION_PROFILES,
9
+ * REQUIRED_FIELDS, AGENT_SUMMARY_FIELDS, OPTIONAL_FIELDS
10
10
  */
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -53,6 +53,24 @@ export const SESSION_KEY_ALIASES = Object.freeze({
53
53
  isolation_override: 'isolation',
54
54
  });
55
55
 
56
+ /**
57
+ * Safe per-wave key aliases (legacy -> canonical) — the `waves[]` counterpart of
58
+ * SESSION_KEY_ALIASES, same-shape renames only. Applied by normalizeWaveKeys
59
+ * (normalizer.mjs) on read AND by scripts/emit-session.mjs on write (#1390 P1).
60
+ *
61
+ * The canonical spelling is `agent_count_*` (skills/session-end/
62
+ * metrics-collection.md). The ledger carried 23 sessions in that spelling and
63
+ * exactly one, `main-2026-09-18-session-1` (5 waves), in the de-underscored
64
+ * `agents_*` spelling (measured 2026-09-19 @ 8f6ac022 over
65
+ * .orchestrator/metrics/sessions.jsonl) — a consumer reading
66
+ * `w.agent_count_completed` got `undefined` for every one of its waves.
67
+ */
68
+ export const WAVE_KEY_ALIASES = Object.freeze({
69
+ agents_planned: 'agent_count_planned',
70
+ agents_started: 'agent_count_started',
71
+ agents_completed: 'agent_count_completed',
72
+ });
73
+
56
74
  // ---------------------------------------------------------------------------
57
75
  // Enums / required field lists
58
76
  // ---------------------------------------------------------------------------
@@ -198,4 +216,14 @@ export const OPTIONAL_FIELDS = Object.freeze([
198
216
  // lacking the field validates unchanged. Value set + the full rationale for
199
217
  // why it is NOT a VALID_SESSION_TYPES member: VALID_SESSION_PROFILES above.
200
218
  'session_profile',
219
+ // #1339 P8 — HEAD sha at session start. Gives `/evolve analyze` an
220
+ // attributable commit range instead of a time window that also catches
221
+ // parallel sessions' commits. Absent = not measured. NOT guaranteed to be a
222
+ // full sha: scripts/emit-session.mjs adopts STATE.md `session-start-ref`
223
+ // only when it is a full 40/64-hex sha, but an explicit key already on the
224
+ // record wins unchecked (even `null`), and every record before that
225
+ // derivation was hand-composed — 19 with a 40-char and 9 with a 7-char
226
+ // value (measured 2026-09-19 @ 8f6ac022, jq over sessions.jsonl).
227
+ // Consumers must accept a short sha or `null`, never assume a full one.
228
+ 'session_start_ref',
201
229
  ]);
@@ -5,12 +5,12 @@
5
5
  * Imports: constants.mjs. No imports from siblings (validator, timestamps,
6
6
  * aliases) or parent barrel.
7
7
  *
8
- * Exports: normalizeSession
8
+ * Exports: normalizeSession, normalizeWaveKeys
9
9
  * Module-private: _warnedMissingSchemaVersion (Set, per-process dedupe),
10
- * isPlainObject, _canonicalizeExpressPath
10
+ * isPlainObject, _canonicalizeExpressPath, _aliasWaves
11
11
  */
12
12
 
13
- import { SESSION_KEY_ALIASES } from './constants.mjs';
13
+ import { SESSION_KEY_ALIASES, WAVE_KEY_ALIASES } from './constants.mjs';
14
14
 
15
15
  // ---------------------------------------------------------------------------
16
16
  // Module-level state
@@ -79,12 +79,61 @@ function _canonicalizeExpressPath(next) {
79
79
  next.express_path = raw.activated;
80
80
  }
81
81
 
82
+ /**
83
+ * Apply WAVE_KEY_ALIASES to every plain-object wave, mirroring the top-level
84
+ * SESSION_KEY_ALIASES rule: the legacy key is preserved, and an existing
85
+ * canonical key is never clobbered. Copy-on-write — the caller's wave objects
86
+ * are never mutated (normalizeSession's `{ ...entry }` is shallow, so `waves`
87
+ * is still the caller's array).
88
+ *
89
+ * @param {any[]} waves
90
+ * @returns {any[]} the same array reference when no wave needed an alias
91
+ */
92
+ function _aliasWaves(waves) {
93
+ let changed = false;
94
+ const out = waves.map((w) => {
95
+ if (!isPlainObject(w)) return w;
96
+ let copy = null;
97
+ for (const [oldKey, newKey] of Object.entries(WAVE_KEY_ALIASES)) {
98
+ if (oldKey in w && !(newKey in w)) {
99
+ copy ??= { ...w };
100
+ copy[newKey] = w[oldKey];
101
+ }
102
+ }
103
+ if (copy === null) return w;
104
+ changed = true;
105
+ return copy;
106
+ });
107
+ return changed ? out : waves;
108
+ }
109
+
82
110
  // ---------------------------------------------------------------------------
83
111
  // Public API
84
112
  // ---------------------------------------------------------------------------
85
113
 
86
114
  /**
87
- * Normalize a session entry read from disk. Applies SAFE key aliases, collapses
115
+ * Apply ONLY the per-wave key aliases (WAVE_KEY_ALIASES) to a session entry
116
+ * the subset of normalizeSession that is safe on the WRITE path
117
+ * (scripts/emit-session.mjs, #1390 P1). The rest of normalizeSession is not:
118
+ * it tags a missing `schema_version` as 0 where validateSession stamps the
119
+ * current version, and applies top-level aliases the write-path contract
120
+ * explicitly excludes.
121
+ *
122
+ * Never throws. Non-objects and entries without a `waves` array are returned
123
+ * unchanged; so is an entry none of whose waves needs an alias (same reference).
124
+ *
125
+ * @param {any} entry
126
+ * @returns {any} a new entry with aliased waves, or the input unchanged
127
+ */
128
+ export function normalizeWaveKeys(entry) {
129
+ if (!isPlainObject(entry) || !Array.isArray(entry.waves)) return entry;
130
+ const waves = _aliasWaves(entry.waves);
131
+ return waves === entry.waves ? entry : { ...entry, waves };
132
+ }
133
+
134
+ /**
135
+ * Normalize a session entry read from disk. Applies SAFE key aliases (top-level
136
+ * SESSION_KEY_ALIASES and per-wave WAVE_KEY_ALIASES), collapses
88
137
  * the legacy object form of `express_path` onto its canonical boolean, and tags
89
138
  * legacy entries without `schema_version` as 0 (distinct from
90
139
  * CURRENT_SESSION_SCHEMA_VERSION=2 which is stamped on new writes; bumped
@@ -112,6 +161,9 @@ export function normalizeSession(entry) {
112
161
  }
113
162
  }
114
163
 
164
+ // Per-wave aliases (#1390 P1) — same non-clobber rule, one level down.
165
+ if (Array.isArray(next.waves)) next.waves = _aliasWaves(next.waves);
166
+
115
167
  // express_path — same key, two shapes. Collapse onto the canonical boolean.
116
168
  _canonicalizeExpressPath(next);
117
169
 
@@ -59,7 +59,7 @@
59
59
  * SESSION_KEY_ALIASES, VALID_SESSION_TYPES,
60
60
  * REQUIRED_FIELDS, AGENT_SUMMARY_FIELDS
61
61
  * session-schema/validator.mjs — ValidationError, validateSession
62
- * session-schema/normalizer.mjs — normalizeSession
62
+ * session-schema/normalizer.mjs — normalizeSession, normalizeWaveKeys
63
63
  * session-schema/timestamps.mjs — clampTimestampsMonotonic
64
64
  * session-schema/aliases.mjs — aliasLegacyEndedAt
65
65
  */
@@ -67,7 +67,12 @@
67
67
  export { CURRENT_SESSION_SCHEMA_VERSION, SESSION_KEY_ALIASES } from './session-schema/constants.mjs';
68
68
  export { ValidationError, validateSession } from './session-schema/validator.mjs';
69
69
  export { serializeSessionLineChecked } from './session-schema/serializer.mjs';
70
- export { normalizeSession } from './session-schema/normalizer.mjs';
70
+ export { normalizeSession, normalizeWaveKeys } from './session-schema/normalizer.mjs';
71
71
  export { clampTimestampsMonotonic } from './session-schema/timestamps.mjs';
72
72
  export { aliasLegacyEndedAt } from './session-schema/aliases.mjs';
73
- export { isRealSession, filterRealSessions, tailRealSessions } from './session-schema/filters.mjs';
73
+ export {
74
+ isRealSession,
75
+ filterRealSessions,
76
+ tailRealSessions,
77
+ isCoordinatorDirectHousekeeping,
78
+ } from './session-schema/filters.mjs';
@@ -70,11 +70,22 @@ const local = (rel) => pathToFileURL(path.join(import.meta.dirname, rel)).href;
70
70
  const CI_UNKNOWN_HINT_DEFAULT = 'run `glab ci status` on demand';
71
71
 
72
72
  /**
73
- * `ci-status` "no data for HEAD" reasons after which the last PUSHED commit is
74
- * worth asking about: GitLab's `no-pipeline-for-head-sha` and GitHub's
75
- * `no-check-runs-for-head` (both emitted by `checkCiStatus`).
73
+ * `ci-status` "no usable reading for HEAD" reasons after which the last PUSHED
74
+ * commit is worth asking about: GitLab's `no-pipeline-for-head-sha`, GitHub's
75
+ * `no-check-runs-for-head`, and GitLab's `pipeline-unmatched-ref` (all emitted
76
+ * by `checkCiStatus`).
77
+ *
78
+ * `pipeline-unmatched-ref` (#857) is not a "no data" state — pipelines EXIST for
79
+ * the sha, they just all sit on a foreign ref, so the reading stays `unknown`.
80
+ * For this consumer that is the same DARK outcome: the operator learns nothing
81
+ * about HEAD. Routing it to the pushed-commit re-query at least yields the
82
+ * pushed sha's own verdict instead of an unactionable line.
76
83
  */
77
- const PUSHED_FOLLOW_UP_REASONS = new Set(['no-pipeline-for-head-sha', 'no-check-runs-for-head']);
84
+ const PUSHED_FOLLOW_UP_REASONS = new Set([
85
+ 'no-pipeline-for-head-sha',
86
+ 'no-check-runs-for-head',
87
+ 'pipeline-unmatched-ref',
88
+ ]);
78
89
 
79
90
  /**
80
91
  * Full SHAs for `refs`, in order, from ONE `git rev-parse` — or `null` when
@@ -190,6 +201,22 @@ function pushedVerdictText(v) {
190
201
  return `CI ${v.status}${parts.length > 0 ? ` (${parts.join(', ')})` : ''}`;
191
202
  }
192
203
 
204
+ /**
205
+ * On-demand command listing every CI run of ONE commit, in the CLI that read
206
+ * CI — a glab command cannot answer in a GitHub repo (#1339 P1).
207
+ *
208
+ * Takes the FULL sha. Measured 2026-09-19, glab 1.117.0: `glab ci status` has
209
+ * no `--ref` flag ("Unknown flag: --ref.", exit 1), and `glab ci list --sha`
210
+ * matches exactly, so a short sha lists nothing.
211
+ *
212
+ * @param {boolean} gh The reading came from the gh CLI
213
+ * @param {string} sha Full commit sha
214
+ * @returns {string}
215
+ */
216
+ function commitRunsCommand(gh, sha) {
217
+ return gh ? `gh run list --commit ${sha}` : `glab ci list --sha ${sha}`;
218
+ }
219
+
193
220
  /**
194
221
  * Hint text for a `status: 'unknown'` ci-status reading.
195
222
  *
@@ -212,8 +239,7 @@ function ciUnknownHint(result, ctx) {
212
239
  if (pushed?.sameAsHead) return `last pushed: ${short} = HEAD — ${onDemand}`;
213
240
  const verdict = pushedVerdictText(pushed?.verdict);
214
241
  if (verdict) return `last pushed: ${short} — ${verdict}`;
215
- const check = gh ? `gh run list --commit ${sha}` : `glab ci status --ref ${short}`;
216
- return `last pushed: ${short} (pipeline not checked; run \`${check}\`)`;
242
+ return `last pushed: ${short} (pipeline not checked; run \`${commitRunsCommand(gh, sha)}\`)`;
217
243
  }
218
244
 
219
245
  /**
@@ -229,6 +255,53 @@ function hasFailedAllowFailureJobs(r) {
229
255
  return Array.isArray(r?.allowFailureJobs) && r.allowFailureJobs.length > 0;
230
256
  }
231
257
 
258
+ /**
259
+ * How many same-commit pipelines the ref preference SET ASIDE ended `failed`
260
+ * (#1390 P5) — the ONE predicate the `ci-status` renderer and its `severityOf`
261
+ * share, for the reason {@link hasFailedAllowFailureJobs} records (#1333).
262
+ * Only `failed` counts: a set-aside `canceled`/`skipped` run says nothing about
263
+ * the commit, and a line that fires on those would teach the operator to skip
264
+ * the banner (HR-101).
265
+ *
266
+ * @param {*} r
267
+ * @returns {number}
268
+ */
269
+ function droppedFailedCount(r) {
270
+ const statuses = r?.details?.droppedStatuses;
271
+ return Array.isArray(statuses) ? statuses.filter((s) => s === 'failed').length : 0;
272
+ }
273
+
274
+ /**
275
+ * Banner line for a `green` reading beside a FAILED pipeline of the same
276
+ * commit that the ref preference set aside (#1390 P5). The verdict stays the
277
+ * #857 ref-preference result; this only stops the failure from vanishing
278
+ * behind a silent green.
279
+ *
280
+ * NAMED CEILING (BV-004): one `git rev-parse HEAD` (2s timeout), run only on
281
+ * this branch — a green reading WITH a set-aside `failed` run, behind the
282
+ * network opt-in. The reading carries no sha, and the GitLab path read the
283
+ * local HEAD, so HEAD is the commit it spoke for. REVISIT TRIGGER: once
284
+ * `checkCiStatus` publishes the sha it read, take it from the reading.
285
+ *
286
+ * Only the GitLab path publishes `droppedStatuses` today (`candidateEvidence`
287
+ * is called from `checkGitlab` alone); the gh form exists so a GitHub producer
288
+ * can never print a glab command (#1339 P1 class) — see {@link commitRunsCommand}.
289
+ *
290
+ * @param {*} r A green reading with `droppedFailedCount(r) > 0`
291
+ * @param {{repoRoot?: string}} [ctx]
292
+ * @returns {string}
293
+ */
294
+ function droppedFailedLine(r, ctx) {
295
+ const sha = revParseShas(ctx?.repoRoot, ['HEAD'])?.[0] ?? null;
296
+ const gh = r.details.cliUsed === 'gh';
297
+ let check;
298
+ if (sha) check = commitRunsCommand(gh, sha);
299
+ else check = gh ? 'gh run list' : 'glab ci list --status=failed';
300
+ const ref = r.details.matchedRef ?? 'HEAD';
301
+ const on = sha ? `${ref} for ${sha.slice(0, 8)}` : ref;
302
+ return `⚠ CI green on ${on}, but ${droppedFailedCount(r)} other pipeline(s) for the same commit FAILED (set-aside statuses: ${r.details.droppedStatuses.join(', ')}) — check \`${check}\``;
303
+ }
304
+
232
305
  // ---------------------------------------------------------------------------
233
306
  // Budget
234
307
  // ---------------------------------------------------------------------------
@@ -476,9 +549,17 @@ export const PROBES = [
476
549
  const job = r.failingJobName ? ` Failing job: ${r.failingJobName}` : '';
477
550
  return `🚨 CI RED on HEAD (pipeline #${pid})${green}.${job}`;
478
551
  }
479
- if (r.status === 'green' && hasFailedAllowFailureJobs(r)) {
480
- const names = r.allowFailureJobs.map((j) => j?.name ?? String(j)).join(', ');
481
- return `⚠ CI green on HEAD, but ${r.allowFailureJobs.length} allow_failure job(s) FAILED: ${names}. A pipeline reports success regardless of these.`;
552
+ if (r.status === 'green') {
553
+ const lines = [];
554
+ if (hasFailedAllowFailureJobs(r)) {
555
+ const names = r.allowFailureJobs.map((j) => j?.name ?? String(j)).join(', ');
556
+ lines.push(`⚠ CI green on HEAD, but ${r.allowFailureJobs.length} allow_failure job(s) FAILED: ${names}. A pipeline reports success regardless of these.`);
557
+ }
558
+ // #1390 P5: a failed same-commit pipeline the ref preference set aside.
559
+ if (droppedFailedCount(r) > 0) lines.push(droppedFailedLine(r, ctx));
560
+ // Both findings can hold at once; `pushBanner` takes a multi-line
561
+ // string verbatim, so neither swallows the other.
562
+ return lines.length > 0 ? lines.join('\n') : null;
482
563
  }
483
564
  return null;
484
565
  },
@@ -493,7 +574,11 @@ export const PROBES = [
493
574
  if (!r || typeof r !== 'object') return 'ok';
494
575
  if (r.degraded) return 'warn';
495
576
  if (r.status === 'red') return 'alert';
496
- if (r.status === 'green') return hasFailedAllowFailureJobs(r) ? 'warn' : 'ok';
577
+ // A set-aside failed run (#1390 P5) is `warn`, never `alert`: the reading
578
+ // itself is green, and `alert` is what a red pipeline scores.
579
+ if (r.status === 'green') {
580
+ return hasFailedAllowFailureJobs(r) || droppedFailedCount(r) > 0 ? 'warn' : 'ok';
581
+ }
497
582
  // #1337: HEAD undetermined but the pushed commit's pipeline is red —
498
583
  // the code on origin is broken, so this is an alert, not a warning.
499
584
  if (r.pushed?.verdict?.status === 'red') return 'alert';
@@ -399,6 +399,29 @@ export function countSessionsInJsonl(raw) {
399
399
  * is skipped rather than aborting the whole read (same posture as the readers
400
400
  * in `session-close-backfill.mjs` and `backfill-abandoned-sessions.mjs`).
401
401
  *
402
+ * NOT EVERY RAW READ OF `sessions.jsonl` IS A GAP TO CLOSE (#1221 P2). Five
403
+ * more readers bypass this module's collapse on purpose — verified via
404
+ * `rg -n "sessions.jsonl|SESSIONS"` against each file — and none of them
405
+ * should be "migrated" to `readCanonicalSessions`:
406
+ * - `scripts/lib/sessions-integrity-banner.mjs` reads every raw line
407
+ * (`readJsonlLines`) because it EXISTS to count malformed/mirror-invalid
408
+ * records; canonicalizing first would hide the corruption it reports on.
409
+ * - `scripts/lib/session-record-repair.mjs` is the repair tool itself — it
410
+ * must see every raw record, malformed and duplicate alike, to fix them;
411
+ * a canonical read would collapse or drop the very rows it needs to repair.
412
+ * - `scripts/migrate-sessions-jsonl.mjs` rewrites legacy-shaped records one
413
+ * raw line at a time; a canonicalized read could silently skip a
414
+ * superseded or duplicate legacy record before the migration ever sees it.
415
+ * - `scripts/gc-stale-worktrees.mjs` (`loadRecentSessionRefs`) scans every
416
+ * raw line's string content for worktree references within a 7-day
417
+ * window; a canonical read could drop a duplicate/superseded stub that
418
+ * still names a worktree, making a live worktree look unreferenced and
419
+ * eligible for garbage collection.
420
+ * - `readTailSession()` in `scripts/autopilot.mjs` parses only the LAST raw
421
+ * line to project the record this run's own session just appended,
422
+ * verbatim; canonicalizing could return a different record than the tail
423
+ * line if a collapse rule folded it away.
424
+ *
402
425
  * @param {object} [args]
403
426
  * @param {string} [args.repoRoot] project root; the ledger is resolved as
404
427
  * `<repoRoot>/.orchestrator/metrics/sessions.jsonl`. Defaults to
@@ -103,7 +103,13 @@ import {
103
103
  } from './vault-mirror/render-sessions.mjs';
104
104
 
105
105
  /** Repo-relative path to the session ledger (one record per closed session). */
106
- const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
106
+ /**
107
+ * Ledger path, relative to the repo root. EXPORTED (#1417) so the CLI front-end
108
+ * `scripts/check-sessions-integrity.mjs` addresses the same file this banner
109
+ * judges instead of re-typing the literal — two spellings of one path is how a
110
+ * guard ends up measuring a file nobody writes.
111
+ */
112
+ export const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
107
113
 
108
114
  /** Max session_ids listed inline before the message collapses to "+N more". */
109
115
  export const MAX_LISTED_IDS = 5;