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
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ // scripts/check-sessions-integrity.mjs
3
+ //
4
+ // CLI front-end for `checkSessionsIntegrity` (GitLab #1417).
5
+ //
6
+ // WHY THIS EXISTS:
7
+ // session-end step 4a (`skills/session-end/session-metrics-write.md`) has to
8
+ // prove that the session record it JUST appended to
9
+ // `.orchestrator/metrics/sessions.jsonl` is schema-valid AND renderable by
10
+ // vault-mirror — step 4 before it only proves the line is JSON. The #1408
11
+ // record parsed fine and was still invalid (`ended_at` instead of
12
+ // `completed_at`, four required fields missing), so vault-mirror dropped it as
13
+ // `skipped-invalid` and that session got no vault note; nobody was told until
14
+ // the NEXT session-start banner. Until now the check existed only as a
15
+ // `node --input-type=module -e` one-liner inside the skill body, because the
16
+ // function had no CLI entry point (`scripts/repair-invalid-sessions.mjs` is a
17
+ // REPAIR path whose `--dry-run` exit reports repairability, not validity).
18
+ //
19
+ // WHAT IT CHECKS (exactly what the banner checks — no second opinion here):
20
+ // every parseable record in the ledger is run through `validateSession()` AND
21
+ // through vault-mirror's real render path. `--session-id` narrows the VERDICT
22
+ // to one record; other sessions' findings are still printed (they are not this
23
+ // close's to fix) but never decide the exit code.
24
+ //
25
+ // WHY A MISSING RECORD EXITS 1 (the deliberate call, #1417):
26
+ // `checkSessionsIntegrity` is silent both when a named record is sound and
27
+ // when it does not exist — "no finding" cannot tell those apart. Reporting the
28
+ // absent case as success would make this guard fail-open in precisely the
29
+ // shape it exists to catch: a close that proceeds with NO ledger record, one
30
+ // notch worse than the #1408 invalid one. So with `--session-id` the ledger is
31
+ // additionally read for that id and its absence is a named failure ("record
32
+ // not found"), never a silent pass. Without `--session-id` there is no id to
33
+ // be absent, and an empty/missing ledger is simply nothing to judge (exit 0).
34
+ //
35
+ // USAGE:
36
+ // node scripts/check-sessions-integrity.mjs [--repo-root <path>] [--session-id <id>] [--json]
37
+ //
38
+ // EXIT CODES:
39
+ // 0 every judged record validates and mirrors
40
+ // 1 a judged record is schema-invalid, is dropped by vault-mirror, or (with
41
+ // --session-id) is not in the ledger at all
42
+ // 2 tool error — unknown/incomplete flag, or an unreadable repo root
43
+ //
44
+ // Node stdlib only apart from two repo-local imports; safe to call from a hook,
45
+ // a skill body or CI without any dependency install.
46
+
47
+ import { existsSync, readFileSync, statSync } from 'node:fs';
48
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
49
+ import { fileURLToPath } from 'node:url';
50
+
51
+ import { checkSessionsIntegrity, SESSIONS_PATH } from './lib/sessions-integrity-banner.mjs';
52
+ import { isMainModule } from './lib/is-main-module.mjs';
53
+
54
+ const SELF_DIR = dirname(fileURLToPath(import.meta.url));
55
+
56
+ const HELP_TEXT = `check-sessions-integrity.mjs — session-ledger schema + vault-mirror gate (#1417)
57
+
58
+ USAGE
59
+ node scripts/check-sessions-integrity.mjs [options]
60
+
61
+ DESCRIPTION
62
+ Runs every parseable record in .orchestrator/metrics/sessions.jsonl through
63
+ validateSession() AND through vault-mirror's real render path — the same two
64
+ populations the session-start banner reports. A record can pass one and fail
65
+ the other, so both are checked and reported separately.
66
+
67
+ This is the command form of session-end step 4a: after appending a session
68
+ record, prove it is sound BEFORE the close continues.
69
+
70
+ OPTIONS
71
+ --repo-root <path> Repo root holding .orchestrator/metrics/ (default: cwd).
72
+ --session-id <id> Judge only the record(s) carrying this session_id. Other
73
+ sessions' findings are still printed but never decide the
74
+ exit code — they are pre-existing, not this close's to fix.
75
+ An id with NO record in the ledger is a FAILURE (exit 1),
76
+ not a silent pass: absent and sound are indistinguishable
77
+ to the checker, and the fail-open reading is the dangerous
78
+ one.
79
+ --json Emit one JSON result object on stdout (diagnostics stay
80
+ on stderr).
81
+ --help, -h Show this help and exit.
82
+ --version Print the package version and exit.
83
+
84
+ EXIT CODES
85
+ 0 every judged record validates and mirrors
86
+ 1 a judged record is schema-invalid, is dropped by vault-mirror, or (with
87
+ --session-id) is not in the ledger at all
88
+ 2 tool error — unknown/incomplete flag, or an unreadable repo root
89
+
90
+ EXAMPLES
91
+ node scripts/check-sessions-integrity.mjs
92
+ node scripts/check-sessions-integrity.mjs --session-id main-2026-09-21-session-4
93
+ node scripts/check-sessions-integrity.mjs --json | jq '.findings'
94
+ `;
95
+
96
+ /**
97
+ * Parse argv. Returns `error` (a message) instead of throwing, so the caller
98
+ * owns the exit code.
99
+ *
100
+ * @param {string[]} argv
101
+ * @returns {{opts: {repoRoot: string|null, sessionId: string|null, json: boolean,
102
+ * help: boolean, version: boolean}, error: string|null}}
103
+ */
104
+ export function parseArgs(argv) {
105
+ const opts = { repoRoot: null, sessionId: null, json: false, help: false, version: false };
106
+ for (let i = 0; i < argv.length; i++) {
107
+ const arg = argv[i];
108
+ switch (arg) {
109
+ case '--repo-root':
110
+ case '--session-id': {
111
+ const value = argv[i + 1];
112
+ // A flag whose value is missing — or is itself the next flag — must be a
113
+ // usage error, never a silently empty filter that judges nothing.
114
+ if (typeof value !== 'string' || value === '' || value.startsWith('--')) {
115
+ return { opts, error: `${arg} requires a value` };
116
+ }
117
+ if (arg === '--repo-root') opts.repoRoot = value;
118
+ else opts.sessionId = value;
119
+ i += 1;
120
+ break;
121
+ }
122
+ case '--json':
123
+ opts.json = true;
124
+ break;
125
+ case '--help':
126
+ case '-h':
127
+ opts.help = true;
128
+ break;
129
+ case '--version':
130
+ opts.version = true;
131
+ break;
132
+ default:
133
+ return { opts, error: `unknown flag: ${arg}` };
134
+ }
135
+ }
136
+ return { opts, error: null };
137
+ }
138
+
139
+ /**
140
+ * Count the ledger records carrying `sessionId`. Unparseable lines are skipped —
141
+ * the same posture the banner takes: this judges schema integrity, not file
142
+ * corruption.
143
+ *
144
+ * @param {string} repoRoot
145
+ * @param {string} sessionId
146
+ * @returns {number}
147
+ */
148
+ function countLedgerRecords(repoRoot, sessionId) {
149
+ const filePath = join(repoRoot, SESSIONS_PATH);
150
+ if (!existsSync(filePath)) return 0;
151
+ let matched = 0;
152
+ for (const line of readFileSync(filePath, 'utf8').split('\n')) {
153
+ if (line.trim() === '') continue;
154
+ let record;
155
+ try {
156
+ record = JSON.parse(line);
157
+ } catch {
158
+ continue;
159
+ }
160
+ if (record && typeof record === 'object' && record.session_id === sessionId) matched += 1;
161
+ }
162
+ return matched;
163
+ }
164
+
165
+ /**
166
+ * Run the check and return a plain result object. Never throws; never exits.
167
+ *
168
+ * @param {{repoRoot?: string, sessionId?: string|null}} [opts]
169
+ * @returns {{ok: boolean, exitCode: 0|1|2, repoRoot: string, sessionId: string|null,
170
+ * total: number|null, matched: number|null, severity: 'warn'|'alert'|null,
171
+ * message: string|null, findings: {kind: string, line: number, sessionId: string, error: string}[],
172
+ * error: string|null}}
173
+ */
174
+ export function runSessionsIntegrityCheck({ repoRoot, sessionId = null } = {}) {
175
+ const root = resolve(repoRoot ?? process.cwd());
176
+ const base = {
177
+ ok: false,
178
+ exitCode: 2,
179
+ repoRoot: root,
180
+ sessionId: sessionId ?? null,
181
+ total: null,
182
+ matched: null,
183
+ severity: null,
184
+ message: null,
185
+ findings: [],
186
+ error: null,
187
+ };
188
+
189
+ try {
190
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
191
+ return { ...base, error: `repo root is not a directory: ${root}` };
192
+ }
193
+ } catch (err) {
194
+ return { ...base, error: `repo root unreadable: ${err?.message ?? String(err)}` };
195
+ }
196
+
197
+ const banner = checkSessionsIntegrity({ repoRoot: root });
198
+
199
+ const findings = banner
200
+ ? [
201
+ ...banner.schemaInvalid.map((f) => ({ kind: 'schema-invalid', ...f })),
202
+ ...banner.mirrorSkipped.map((f) => ({ kind: 'mirror-skipped', ...f })),
203
+ ]
204
+ : [];
205
+
206
+ const result = {
207
+ ...base,
208
+ total: banner ? banner.total : null,
209
+ severity: banner ? banner.severity : null,
210
+ message: banner ? banner.message : null,
211
+ findings,
212
+ };
213
+
214
+ if (sessionId === null || sessionId === undefined) {
215
+ // No filter: every finding decides.
216
+ const ok = findings.length === 0;
217
+ return { ...result, ok, exitCode: ok ? 0 : 1 };
218
+ }
219
+
220
+ const matched = countLedgerRecords(root, sessionId);
221
+ const mine = findings.filter((f) => f.sessionId === sessionId);
222
+
223
+ if (matched === 0) {
224
+ return {
225
+ ...result,
226
+ matched,
227
+ ok: false,
228
+ exitCode: 1,
229
+ error:
230
+ `no record with session_id "${sessionId}" in ${SESSIONS_PATH} — ` +
231
+ 'it was never written, or it was written under a different id. ' +
232
+ 'Re-emit it via scripts/emit-session.mjs.',
233
+ };
234
+ }
235
+
236
+ const ok = mine.length === 0;
237
+ return { ...result, matched, ok, exitCode: ok ? 0 : 1 };
238
+ }
239
+
240
+ function readVersion() {
241
+ try {
242
+ const pkg = JSON.parse(readFileSync(join(SELF_DIR, '..', 'package.json'), 'utf8'));
243
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
244
+ } catch {
245
+ return 'unknown';
246
+ }
247
+ }
248
+
249
+ // CLI entry — only when run directly, never on import (an import must do
250
+ // nothing: this module is loaded by tests and is reachable from hook code).
251
+ if (isMainModule(import.meta.url)) {
252
+ const { opts, error } = parseArgs(process.argv.slice(2));
253
+
254
+ if (error !== null) {
255
+ process.stderr.write(`[check-sessions-integrity] ${error}\n`);
256
+ process.stderr.write('Run with --help for usage.\n');
257
+ process.exit(2);
258
+ }
259
+
260
+ if (opts.help) {
261
+ process.stdout.write(HELP_TEXT);
262
+ process.exit(0);
263
+ }
264
+
265
+ if (opts.version) {
266
+ process.stdout.write(`${readVersion()}\n`);
267
+ process.exit(0);
268
+ }
269
+
270
+ const repoRootArg =
271
+ opts.repoRoot === null ? undefined : isAbsolute(opts.repoRoot) ? opts.repoRoot : resolve(process.cwd(), opts.repoRoot);
272
+
273
+ const result = runSessionsIntegrityCheck({ repoRoot: repoRootArg, sessionId: opts.sessionId });
274
+
275
+ if (opts.json) {
276
+ process.stdout.write(`${JSON.stringify(result)}\n`);
277
+ process.exit(result.exitCode);
278
+ }
279
+
280
+ // The banner text covers EVERY record, including other sessions' pre-existing
281
+ // findings — printed for visibility, even when the verdict below is clean.
282
+ if (result.message) process.stderr.write(`${result.message}\n`);
283
+ if (result.error) process.stderr.write(`[check-sessions-integrity] ${result.error}\n`);
284
+
285
+ if (result.exitCode === 2) process.exit(2);
286
+
287
+ if (result.ok) {
288
+ const scope = result.sessionId ? `record "${result.sessionId}"` : 'every ledger record';
289
+ process.stdout.write(`[check-sessions-integrity] OK — ${scope} validates and mirrors\n`);
290
+ process.exit(0);
291
+ }
292
+
293
+ const scope = result.sessionId ? `session "${result.sessionId}"` : 'the sessions ledger';
294
+ process.stderr.write(
295
+ `[check-sessions-integrity] FAIL — ${scope} is not sound. ` +
296
+ 'Re-emit the affected record(s) via scripts/emit-session.mjs ' +
297
+ '(bulk repair: node scripts/repair-invalid-sessions.mjs --apply).\n',
298
+ );
299
+ process.exit(1);
300
+ }
@@ -49,6 +49,7 @@ import { readPeerCards } from './lib/peer-cards/reader.mjs';
49
49
  import { filterRealSessions } from './lib/session-schema/filters.mjs';
50
50
  import { readCanonicalSessions } from './lib/sessions-canonical.mjs';
51
51
  import { recordDialecticRun } from './lib/learnings/evolve-telemetry.mjs';
52
+ import { DEFAULT_BUDGET_TOKENS } from './lib/config/dialectic.mjs';
52
53
 
53
54
  // ---------------------------------------------------------------------------
54
55
  // Constants
@@ -57,8 +58,11 @@ import { recordDialecticRun } from './lib/learnings/evolve-telemetry.mjs';
57
58
  /** Allowed LLM model identifiers. Per #506 EARS unwanted-behaviour: fail-fast on others. */
58
59
  export const ALLOWED_MODELS = Object.freeze(['haiku', 'sonnet', 'opus']);
59
60
 
60
- /** Default per-call budget (input + output tokens). Per #506 AC2. */
61
- export const DEFAULT_BUDGET = Object.freeze({ input: 8000, output: 4000 });
61
+ /**
62
+ * Default per-call budget (input + output tokens). Per #506 AC2; the input
63
+ * ceiling is `DEFAULT_BUDGET_TOKENS` from the config parser (SSOT, #1380).
64
+ */
65
+ export const DEFAULT_BUDGET = Object.freeze({ input: DEFAULT_BUDGET_TOKENS, output: 4000 });
62
66
 
63
67
  /** Industry-standard heuristic: ~4 chars per token for English prose. */
64
68
  const CHARS_PER_TOKEN = 4;
@@ -309,19 +313,21 @@ export function buildPrompt(payload, model, randomNonce = () => randomBytes(4).t
309
313
  '',
310
314
  '```diff',
311
315
  '# target: user',
312
- '<full proposed body of USER.md, replacing existing content>',
316
+ '<the `## ` sections of USER.md you change or add>',
313
317
  '```',
314
318
  '',
315
319
  '```diff',
316
320
  '# target: agent',
317
- '<full proposed body of AGENT.md, replacing existing content>',
321
+ '<the `## ` sections of AGENT.md you change or add>',
318
322
  '```',
319
323
  '',
320
324
  'Rules:',
321
325
  '- Emit at most one block per target. Omit a target entirely when no update is warranted.',
322
- '- The block body is the FULL replacement body — not a unified diff hunk.',
326
+ '- The block body is a set of whole `## ` sections — not a unified diff hunk. It is merged per section:',
327
+ ' omitted sections stay UNCHANGED (nothing is deleted), so emit ONLY the sections you change or newly',
328
+ ' ground, and reuse an existing heading verbatim to update that section.',
323
329
  '- Preserve existing peer-card frontmatter; do not include `---` frontmatter lines in your block.',
324
- '- Do not invent new sections that are not grounded in the supplied learnings or sessions.',
330
+ '- Do not invent sections without evidence from the supplied learnings or sessions — nothing downstream rejects them.',
325
331
  `- Operate within the dialectic budget appropriate for model '${model}'.`,
326
332
  '',
327
333
  ].join('\n');
@@ -505,6 +511,26 @@ async function readSteering(repoRoot) {
505
511
  * @property {number} [budget]
506
512
  */
507
513
 
514
+ /**
515
+ * Record a THROWN abort (`unknown-model`, `subagent-crash`) before the caller
516
+ * rethrows the original error (#1221). The record is guarded on its own:
517
+ * `recordDialecticRun()` is best-effort today, but the rethrow contract must
518
+ * not depend on that staying true — a telemetry failure never replaces the
519
+ * error the caller is about to rethrow.
520
+ *
521
+ * @param {string} repoRoot
522
+ * @param {'unknown-model' | 'subagent-crash'} status
523
+ * @param {number} t0 — `Date.now()` at run start
524
+ * @returns {Promise<void>}
525
+ */
526
+ async function recordThrownAbort(repoRoot, status, t0) {
527
+ try {
528
+ await recordDialecticRun({ repoRoot, status, durationMs: Date.now() - t0 });
529
+ } catch {
530
+ /* swallowed on purpose — see docblock */
531
+ }
532
+ }
533
+
508
534
  /**
509
535
  * Run the deriver loop: load inputs → build payload → check budget → dispatch
510
536
  * agent → parse response → guard empty-card. Returns a structured verdict.
@@ -546,15 +572,20 @@ export async function runDialecticDeriver({
546
572
 
547
573
  // #1206 — telemetry start marker for the mechanical `orchestrator.dialectic.
548
574
  // completed` emits below. Mechanical (measured), not the coordinator's own
549
- // typed DURATION_MS placeholder — covers every return path THIS function
550
- // can determine on its own; see the module header for the two abort classes
551
- // (`unknown-model`, thrown above; `subagent-crash`) and APPLY-mode success
552
- // that only the skill-prose caller can record (merge + Agent() dispatch
553
- // both happen one layer up, outside this pure pipeline).
575
+ // typed DURATION_MS placeholder — covers every outcome THIS function can
576
+ // see: the returns, and since #1221 the two THROWN aborts (`unknown-model`,
577
+ // `subagent-crash`), recorded at the throw point before the original error
578
+ // is rethrown. Only APPLY-mode success stays with the skill-prose caller
579
+ // the merge happens one layer up, outside this pure pipeline.
554
580
  const t0 = Date.now();
555
581
 
556
582
  // Gate 1: model fail-fast. Throws Error with the canonical message.
557
- validateModel(model);
583
+ try {
584
+ validateModel(model);
585
+ } catch (err) {
586
+ await recordThrownAbort(repoRoot, 'unknown-model', t0);
587
+ throw err;
588
+ }
558
589
 
559
590
  // Load inputs (best-effort, never throws on missing).
560
591
  const [learnings, sessions, peerCardsResult, steering] = await Promise.all([
@@ -611,7 +642,13 @@ export async function runDialecticDeriver({
611
642
 
612
643
  // Dispatch — DI boundary. Caller wires the real Agent({...}) wrapper or a mock.
613
644
  const maxTokens = typeof budget?.output === 'number' ? budget.output : DEFAULT_BUDGET.output;
614
- const response = await dispatchAgent({ model, prompt, maxTokens });
645
+ let response;
646
+ try {
647
+ response = await dispatchAgent({ model, prompt, maxTokens });
648
+ } catch (err) {
649
+ await recordThrownAbort(repoRoot, 'subagent-crash', t0);
650
+ throw err;
651
+ }
615
652
 
616
653
  const text = typeof response?.text === 'string' ? response.text : '';
617
654
  const { diff } = parseResponse(text);
@@ -40,11 +40,16 @@ import {
40
40
  CURRENT_SESSION_SCHEMA_VERSION,
41
41
  clampTimestampsMonotonic,
42
42
  aliasLegacyEndedAt,
43
+ normalizeWaveKeys,
43
44
  } from './lib/session-schema.mjs';
44
45
  import { isMainModule } from './lib/is-main-module.mjs';
45
46
 
46
47
  export { serializeSessionLineChecked };
47
48
 
49
+ // A FULL object name only (SHA-1 or SHA-256 repo). A short sha or a symbolic
50
+ // ref (`HEAD`, a branch) would resolve to a DIFFERENT commit when read later.
51
+ const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
52
+
48
53
  function parseArgs(argv) {
49
54
  const args = { file: '.orchestrator/metrics/sessions.jsonl', entry: null };
50
55
  for (let i = 0; i < argv.length; i++) {
@@ -118,6 +123,14 @@ async function main() {
118
123
  );
119
124
  }
120
125
 
126
+ // 3. Alias legacy per-wave agent-count keys (`agents_completed`, …) onto
127
+ // their canonical `agent_count_*` spelling (#1390 P1), non-clobbering.
128
+ // ONLY this pass of normalizeSession runs on the write path: the rest
129
+ // would tag a missing `schema_version` as 0 (validateSession stamps the
130
+ // current version) and apply top-level aliases that validateSession's
131
+ // contract deliberately does not.
132
+ repaired = normalizeWaveKeys(repaired);
133
+
121
134
  // `memory_cleanup_at` derivation (#699 follow-up — Disziplin statt Mechanik).
122
135
  // The flag used to be a boolean the coordinator-LLM remembered to pass at
123
136
  // session-end; it was forgotten on 2026-08-14 and the cadence marker stalled
@@ -154,71 +167,104 @@ async function main() {
154
167
  }
155
168
  }
156
169
 
157
- // `session_profile` derivation (#1247 GitLab issue "key the effective-sizing
158
- // row on waves x profile"). The mandatory write path is the coordinator
159
- // composing `$METRICS_ENTRY` in prose (session-metrics-write.md), which never
160
- // reliably set this field so it is read here instead, from this repo's own
161
- // STATE.md frontmatter via `readSessionProfile()` (scripts/lib/state-md.mjs).
170
+ // STATE.md-derived enrichmentONE read, ONE parse, ONE ownership check for
171
+ // both fields below.
172
+ //
173
+ // `session_profile` (#1247GitLab issue "key the effective-sizing row on
174
+ // waves x profile"). The mandatory write path is the coordinator composing
175
+ // `$METRICS_ENTRY` in prose (session-metrics-write.md), which never reliably
176
+ // set this field — so it is read here instead, from this repo's own STATE.md
177
+ // frontmatter via `readSessionProfile()` (scripts/lib/state-md.mjs).
178
+ //
179
+ // `session_start_ref` (#1339 P8) — frontmatter `session-start-ref`, the HEAD
180
+ // sha the wave-executor pinned at session start. `/evolve analyze` needs it
181
+ // for an attributable `<start>..<end>` commit range; without it, it falls
182
+ // back to a time window that also attributes a PARALLEL session's commits to
183
+ // this one (skills/evolve/references/evolve-analyze-mode.md). Only a full
184
+ // hex sha (FULL_SHA_RE) is adopted; anything else is omitted with a WARN.
185
+ // Ceiling: the frontmatter parser coerces an all-decimal-digit value to a
186
+ // number, so such a sha (p ~ 7e-9) is omitted, never adopted as a number.
187
+ // NO END REF, deliberately: this runs at session-end Phase 3.7, BEFORE the
188
+ // Phase 4 close commit exists, so HEAD here would cut the session's own
189
+ // commit out of its range. Consumers use HEAD + `--until=<completed_at>`.
190
+ // Revisit when a post-commit writer exists that can stamp `session_end_ref`.
162
191
  //
163
- // Precedence: an EXPLICIT `session_profile` KEY on the incoming record (even
164
- // `null`) wins and is never overwritten — same "explicit assertion beats
165
- // derivation" convention as `memory_cleanup_at` above. Absence of a profile in
166
- // STATE.md (readSessionProfile returns null) leaves the field OMITTED
167
- // never coerced to `''`/`'none'`/a literal `"null"` string.
192
+ // Precedence: an EXPLICIT key on the incoming record (even `null`) wins and
193
+ // is never overwritten — same "explicit assertion beats derivation"
194
+ // convention as `memory_cleanup_at` above. Absence in STATE.md leaves the
195
+ // field OMITTED never coerced to `''`/`'none'`/a literal `"null"` string.
168
196
  //
169
197
  // OWNERSHIP: STATE.md belongs to the session named in its OWN frontmatter
170
198
  // (`session:`), which in a shared working copy need not be the session this
171
199
  // record describes — two parallel sessions, or a `/close` run after a
172
200
  // foreign `/plan` session left its STATE.md behind (see
173
201
  // `.claude/rules/parallel-sessions.md`). Filing session B's waves under
174
- // session A's profile is exactly the cross-contamination #1247 removes, only
175
- // inverted. So the profile is adopted ONLY when the frontmatter `session`
176
- // equals this record's `session_id`; on any mismatch (or an unprovable
177
- // ownership, i.e. no `session` key) the field is OMITTED — never guessed.
202
+ // session A's profile — or A's start ref — is exactly the cross-contamination
203
+ // both fields exist to remove. So they are adopted ONLY when the frontmatter
204
+ // `session` equals this record's `session_id`; on any mismatch (or an
205
+ // unprovable ownership, i.e. no `session` key) they are OMITTED — never guessed.
178
206
  //
179
207
  // FAIL-SAFE: the read is enrichment; the session record is the load-bearing
180
208
  // artefact. Any failure reading or parsing STATE.md (EISDIR when the path is
181
- // a directory, EACCES/EPERM, a truncated file) omits the field and lets the
209
+ // a directory, EACCES/EPERM, a truncated file) omits the fields and lets the
182
210
  // record through — it must never be the reason `sessions.jsonl` gains no
183
211
  // line at all.
184
- if (!Object.prototype.hasOwnProperty.call(repaired, 'session_profile')) {
212
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(repaired, key);
213
+ const needsProfile = !hasOwn('session_profile');
214
+ const needsStartRef = !hasOwn('session_start_ref');
215
+ if (needsProfile || needsStartRef) {
185
216
  let stateMdContents = '';
186
217
  try {
187
218
  const stateMdPath = resolveStateMdPath(process.cwd());
188
219
  if (existsSync(stateMdPath)) stateMdContents = readFileSync(stateMdPath, 'utf8');
189
220
  } catch (err) {
190
221
  process.stderr.write(
191
- `emit-session: WARN could not read STATE.md for session_profile derivation ` +
192
- `(${err?.message ?? err}); omitting session_profile\n`
222
+ `emit-session: WARN could not read STATE.md for session_profile/session_start_ref ` +
223
+ `derivation (${err?.message ?? err}); omitting both\n`
193
224
  );
194
225
  stateMdContents = '';
195
226
  }
196
227
  // No initialiser: both the try and the catch below assign `profile`.
197
228
  let profile;
229
+ let startRef = null;
198
230
  let owner = null;
199
231
  try {
200
232
  profile = readSessionProfile(stateMdContents);
201
- const parsed = parseStateMd(stateMdContents);
202
- const rawOwner = parsed?.frontmatter?.session;
233
+ const fm = parseStateMd(stateMdContents)?.frontmatter;
234
+ const rawOwner = fm?.session;
203
235
  owner = typeof rawOwner === 'string' && rawOwner.trim().length > 0 ? rawOwner.trim() : null;
236
+ const rawRef = fm?.['session-start-ref'];
237
+ if (typeof rawRef === 'string' && FULL_SHA_RE.test(rawRef.trim())) {
238
+ startRef = rawRef.trim();
239
+ } else if (needsStartRef && rawRef !== undefined && rawRef !== null && rawRef !== '') {
240
+ process.stderr.write(
241
+ `emit-session: WARN STATE.md session-start-ref=${rawRef} is not a full hex sha; ` +
242
+ `omitting session_start_ref\n`
243
+ );
244
+ }
204
245
  } catch (err) {
205
246
  process.stderr.write(
206
- `emit-session: WARN could not parse STATE.md for session_profile derivation ` +
207
- `(${err?.message ?? err}); omitting session_profile\n`
247
+ `emit-session: WARN could not parse STATE.md for session_profile/session_start_ref ` +
248
+ `derivation (${err?.message ?? err}); omitting both\n`
208
249
  );
209
250
  profile = null;
251
+ startRef = null;
210
252
  }
211
- if (profile !== null) {
253
+ const derived = {};
254
+ if (needsProfile && profile !== null) derived.session_profile = profile;
255
+ if (needsStartRef && startRef !== null) derived.session_start_ref = startRef;
256
+ const derivedKeys = Object.keys(derived);
257
+ if (derivedKeys.length > 0) {
212
258
  if (owner !== null && owner === repaired.session_id) {
213
- repaired = { ...repaired, session_profile: profile };
259
+ repaired = { ...repaired, ...derived };
214
260
  } else {
215
261
  // A visible omission: silence here is indistinguishable from "STATE.md
216
- // carries no profile", and a foreign STATE.md in this working copy is
217
- // precisely what the operator wants to know about.
262
+ // carries no such field", and a foreign STATE.md in this working copy
263
+ // is precisely what the operator wants to know about.
218
264
  process.stderr.write(
219
- `emit-session: WARN STATE.md session_profile=${profile} belongs to ` +
220
- `session=${owner ?? '<absent>'}, not session_id=${repaired.session_id ?? '<unknown>'}; ` +
221
- `omitting session_profile\n`
265
+ `emit-session: WARN STATE.md ${derivedKeys.map((k) => `${k}=${derived[k]}`).join(' ')} ` +
266
+ `belongs to session=${owner ?? '<absent>'}, not session_id=${repaired.session_id ?? '<unknown>'}; ` +
267
+ `omitting ${derivedKeys.join(', ')}\n`
222
268
  );
223
269
  }
224
270
  }