session-orchestrator 5.11.0 → 5.13.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 (394) hide show
  1. package/.claude-plugin/marketplace.json +12 -1
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +180 -3
  11. package/README.md +126 -117
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +4 -2
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +102 -25
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +55 -21
  47. package/docs/session-config-template.md +32 -24
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +158 -59
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -3
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +73 -3
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/remote-hosts.mjs +2 -2
  143. package/scripts/lib/config/section-extractor.mjs +65 -11
  144. package/scripts/lib/config/template-keys.mjs +153 -0
  145. package/scripts/lib/config/vault-integration.mjs +1 -0
  146. package/scripts/lib/config.mjs +3 -3
  147. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  148. package/scripts/lib/discovery/triage-state.mjs +2 -1
  149. package/scripts/lib/dispatcher/rank.mjs +5 -7
  150. package/scripts/lib/ecosystem-health.mjs +2 -2
  151. package/scripts/lib/eval/engine.mjs +3 -2
  152. package/scripts/lib/eval/session-resolve.mjs +6 -2
  153. package/scripts/lib/eval/sink.mjs +4 -3
  154. package/scripts/lib/events-retention-banner.mjs +3 -2
  155. package/scripts/lib/events-schema.mjs +10 -0
  156. package/scripts/lib/events.mjs +23 -12
  157. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  158. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  159. package/scripts/lib/fleet-protocol.mjs +21 -0
  160. package/scripts/lib/gates/gate-full.mjs +172 -11
  161. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  162. package/scripts/lib/git-config-drift.mjs +141 -7
  163. package/scripts/lib/github-export.mjs +447 -0
  164. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  165. package/scripts/lib/handover-queue-marks.mjs +159 -0
  166. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  167. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  168. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  169. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  170. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  171. package/scripts/lib/host-identity.mjs +9 -5
  172. package/scripts/lib/http-request.mjs +64 -0
  173. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  174. package/scripts/lib/io.mjs +6 -3
  175. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  176. package/scripts/lib/issue-budget.mjs +36 -7
  177. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  178. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  179. package/scripts/lib/learnings/io.mjs +289 -15
  180. package/scripts/lib/learnings/select.mjs +53 -4
  181. package/scripts/lib/learnings/store-role.mjs +209 -0
  182. package/scripts/lib/learnings/surface.mjs +46 -7
  183. package/scripts/lib/ledger-path.mjs +113 -0
  184. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  185. package/scripts/lib/memory-banner.mjs +5 -6
  186. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  187. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  188. package/scripts/lib/memory-proposals/store.mjs +22 -5
  189. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  190. package/scripts/lib/no-follow-open.mjs +57 -0
  191. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  192. package/scripts/lib/own-lock-root.mjs +95 -0
  193. package/scripts/lib/owner-interview.mjs +2 -1
  194. package/scripts/lib/owner-yaml.mjs +70 -7
  195. package/scripts/lib/platform.mjs +161 -9
  196. package/scripts/lib/plugin-update-banner.mjs +15 -11
  197. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  198. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  199. package/scripts/lib/process-group.mjs +270 -34
  200. package/scripts/lib/project-hygiene.mjs +28 -0
  201. package/scripts/lib/quality-gate.mjs +33 -9
  202. package/scripts/lib/reconcile/backlog.mjs +29 -15
  203. package/scripts/lib/reconcile/engine.mjs +26 -2
  204. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  205. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  206. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  207. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  208. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  209. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  210. package/scripts/lib/rule-loader.mjs +20 -10
  211. package/scripts/lib/scope-baseline.mjs +123 -8
  212. package/scripts/lib/scope-echo.mjs +5 -2
  213. package/scripts/lib/scope-gate.mjs +55 -20
  214. package/scripts/lib/session-close-backfill.mjs +75 -7
  215. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  216. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  217. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  218. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  219. package/scripts/lib/session-id.mjs +9 -1
  220. package/scripts/lib/session-identity/own-session.mjs +63 -0
  221. package/scripts/lib/session-invocation.mjs +14 -3
  222. package/scripts/lib/session-lock.mjs +114 -0
  223. package/scripts/lib/session-record-repair.mjs +245 -35
  224. package/scripts/lib/session-registry.mjs +85 -12
  225. package/scripts/lib/session-schema/constants.mjs +9 -0
  226. package/scripts/lib/session-schema/filters.mjs +56 -1
  227. package/scripts/lib/session-schema/validator.mjs +15 -5
  228. package/scripts/lib/session-start-probes.mjs +149 -22
  229. package/scripts/lib/session-token-rollup.mjs +2 -1
  230. package/scripts/lib/session-work-trees.mjs +166 -0
  231. package/scripts/lib/sessions-canonical.mjs +61 -3
  232. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  233. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  234. package/scripts/lib/sh-quote.mjs +1 -0
  235. package/scripts/lib/skill-evidence-window.mjs +5 -1
  236. package/scripts/lib/skill-health/join.mjs +10 -9
  237. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  238. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  239. package/scripts/lib/snapshot-probe.mjs +414 -0
  240. package/scripts/lib/so-hud/collect.mjs +174 -0
  241. package/scripts/lib/so-hud/runtime.mjs +87 -0
  242. package/scripts/lib/spiral-carryover.mjs +8 -1
  243. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  244. package/scripts/lib/state-md/idle-reset.mjs +248 -0
  245. package/scripts/lib/state-md/mission-status.mjs +5 -1
  246. package/scripts/lib/state-md/recommendations.mjs +26 -4
  247. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  248. package/scripts/lib/state-md.mjs +31 -8
  249. package/scripts/lib/sunset/walker.mjs +4 -3
  250. package/scripts/lib/telemetry/consent.mjs +4 -2
  251. package/scripts/lib/telemetry/queue.mjs +3 -3
  252. package/scripts/lib/telemetry/sync.mjs +45 -9
  253. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  254. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  255. package/scripts/lib/ux-grill/paths.mjs +5 -1
  256. package/scripts/lib/validate/check-agents.mjs +37 -6
  257. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  258. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  259. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  260. package/scripts/lib/validate/check-json-files.mjs +3 -1
  261. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  262. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  263. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  264. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  265. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  266. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  267. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  268. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  269. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  270. package/scripts/lib/validate/confidential-names.mjs +3 -1
  271. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  272. package/scripts/lib/vault-mirror/namespace.mjs +22 -9
  273. package/scripts/lib/vault-mirror/process.mjs +197 -65
  274. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  275. package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
  276. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  277. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  278. package/scripts/lib/vault-mirror/utils.mjs +106 -13
  279. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  280. package/scripts/lib/vault-status/board-writer.mjs +34 -5
  281. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  282. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  283. package/scripts/lib/win-process-tree.mjs +137 -0
  284. package/scripts/lib/workspace.mjs +1 -1
  285. package/scripts/lib/worktree/constants.mjs +1 -1
  286. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  287. package/scripts/lib/worktree/listing.mjs +1 -1
  288. package/scripts/lib/zx.mjs +50 -0
  289. package/scripts/mcp-server.mjs +16 -7
  290. package/scripts/mcp-server.sh +13 -83
  291. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  292. package/scripts/memory-propose.mjs +10 -3
  293. package/scripts/migrate-cold-start-seed.mjs +7 -0
  294. package/scripts/migrate-legacy-learnings.sh +22 -0
  295. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  296. package/scripts/migrate-vault-paths.mjs +29 -13
  297. package/scripts/parse-config.mjs +71 -14
  298. package/scripts/print-learnings-index.mjs +25 -2
  299. package/scripts/release-session-lock.mjs +94 -7
  300. package/scripts/release.mjs +451 -24
  301. package/scripts/relocate-vault-corpus.mjs +11 -7
  302. package/scripts/repair-invalid-sessions.mjs +68 -3
  303. package/scripts/resolve-session-invocation.mjs +57 -8
  304. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  305. package/scripts/run-quality-gate.mjs +33 -7
  306. package/scripts/self-update.mjs +56 -7
  307. package/scripts/session-commit-files.mjs +133 -0
  308. package/scripts/site-numbers.mjs +52 -9
  309. package/scripts/snapshot-probe.mjs +156 -0
  310. package/scripts/so-hud.mjs +68 -0
  311. package/scripts/state-md-idle-reset.mjs +82 -0
  312. package/scripts/sweep-expired-learnings.mjs +54 -6
  313. package/scripts/sweep-expired-rules.mjs +4 -1
  314. package/scripts/validate-plugin.mjs +22 -0
  315. package/scripts/vault-integration-watcher.mjs +54 -6
  316. package/scripts/vault-mirror.mjs +9 -0
  317. package/scripts/vault-orphans.mjs +171 -0
  318. package/skills/_shared/fleet-protocol.md +166 -3
  319. package/skills/_shared/instruction-file-resolution.md +1 -1
  320. package/skills/_shared/monitor-patterns.md +44 -25
  321. package/skills/_shared/platform-tools.md +7 -0
  322. package/skills/_shared/state-ownership.md +5 -4
  323. package/skills/architecture/references/domain-model.md +1 -1
  324. package/skills/autopilot/SKILL.md +3 -2
  325. package/skills/bootstrap/SKILL.md +3 -1
  326. package/skills/bootstrap/_shared-template.md +4 -4
  327. package/skills/bootstrap/fast-template.md +1 -1
  328. package/skills/bootstrap/private-contract.md +1 -1
  329. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  330. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  331. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  332. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  333. package/skills/claude-md-drift-check/SKILL.md +3 -3
  334. package/skills/claude-md-drift-check/checker.mjs +61 -101
  335. package/skills/close/SKILL.md +20 -7
  336. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  337. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  338. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  339. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  340. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  341. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  342. package/skills/discovery/probes-session.md +3 -0
  343. package/skills/discovery/probes-vault.md +2 -0
  344. package/skills/dispatcher/SKILL.md +1 -1
  345. package/skills/eval/SKILL.md +3 -1
  346. package/skills/evolve/SKILL.md +2 -0
  347. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  348. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  349. package/skills/gitlab-ops/SKILL.md +7 -6
  350. package/skills/gitlab-portfolio/SKILL.md +2 -2
  351. package/skills/go/SKILL.md +13 -1
  352. package/skills/npm-publish/SKILL.md +5 -2
  353. package/skills/plan/SKILL.md +23 -10
  354. package/skills/plan/mode-feature.md +13 -8
  355. package/skills/plan/mode-new.md +6 -14
  356. package/skills/plan/prd-feature-template.md +19 -0
  357. package/skills/plan/prd-full-template.md +12 -5
  358. package/skills/plan/prd-premortem-panel.md +48 -0
  359. package/skills/plan/prd-reviewer-prompt.md +11 -3
  360. package/skills/plan/soul.md +1 -1
  361. package/skills/portfolio/SKILL.md +5 -6
  362. package/skills/quality-gates/SKILL.md +2 -4
  363. package/skills/reconcile/SKILL.md +27 -4
  364. package/skills/release/SKILL.md +16 -8
  365. package/skills/session-end/SKILL.md +97 -13
  366. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  367. package/skills/session-end/phase-3-6-tail.md +11 -7
  368. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  369. package/skills/session-end/plan-verification.md +16 -1
  370. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  371. package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
  372. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  373. package/skills/session-end/references/session-summary-template.md +8 -0
  374. package/skills/session-end/session-metrics-write.md +7 -4
  375. package/skills/session-plan/SKILL.md +5 -3
  376. package/skills/session-start/SKILL.md +10 -4
  377. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  378. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  379. package/skills/session-start/phase-8-5-express-path.md +4 -3
  380. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  381. package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
  382. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  383. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  384. package/skills/tmux-layout/SKILL.md +2 -2
  385. package/skills/vault-sync/validator.mjs +3 -2
  386. package/skills/wave-executor/SKILL.md +5 -4
  387. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  388. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  389. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  390. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  391. package/AGENTS.md +0 -207
  392. package/assets/og-card.png +0 -0
  393. package/assets/og-card.svg +0 -47
  394. package/assets/wave-lifecycle.svg +0 -98
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * close-precheck.mjs — classify `<state-dir>/STATE.md` before `/close` hands
4
+ * off to session-end (#1661). Logic: `lib/session-end/close-precheck.mjs`.
5
+ *
6
+ * Usage:
7
+ * node scripts/close-precheck.mjs [--repo-root <dir>] [--state-dir <dir>] [--confirm-own <token>]
8
+ *
9
+ * `--confirm-own` takes the 16-hex `confirmToken` of an `ownership-unproven`
10
+ * result and confirms that STATE.md as this session's while unchanged (#1718).
11
+ *
12
+ * Prints one JSON object: `verdict`, `runsSessionEnd`, `ownership`, `status`,
13
+ * `stateSessionId`, `ownSessionIds`, `statePath`, optional `reason`; with
14
+ * `ownership-unproven` also `wouldRun`, `confirmToken`, `session`, `startedAt`.
15
+ * Exit 0 for every verdict (a stop is an answer, not an error; Claude Code
16
+ * marks every non-zero exit as a tool failure) · 2 usage error.
17
+ */
18
+
19
+ import { classifyCloseState, RUNS_SESSION_END } from './lib/session-end/close-precheck.mjs';
20
+
21
+ function parseArgs(argv) {
22
+ const out = { repoRoot: process.cwd(), stateDir: undefined, confirmToken: undefined };
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const a = argv[i];
25
+ if (a === '--repo-root' || a === '--state-dir') {
26
+ const v = argv[++i];
27
+ if (!v) throw new Error(`${a} needs a value`);
28
+ if (a === '--repo-root') out.repoRoot = v; else out.stateDir = v;
29
+ } else if (a === '--confirm-own') {
30
+ const v = argv[++i];
31
+ if (!v || !/^[0-9a-f]{16}$/.test(v)) throw new Error('--confirm-own needs a 16-hex token');
32
+ out.confirmToken = v;
33
+ } else if (a === '--help' || a === '-h') {
34
+ out.help = true;
35
+ } else {
36
+ throw new Error(`unknown argument: ${a}`);
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ let args;
43
+ try {
44
+ args = parseArgs(process.argv.slice(2));
45
+ } catch (err) {
46
+ process.stderr.write(`close-precheck: ${err.message}\n`);
47
+ process.exit(2);
48
+ }
49
+ if (args.help) {
50
+ process.stdout.write('Usage: node scripts/close-precheck.mjs [--repo-root <dir>] [--state-dir <dir>] [--confirm-own <token>]\n');
51
+ process.exit(0);
52
+ }
53
+
54
+ const result = classifyCloseState({ repoRoot: args.repoRoot, stateDir: args.stateDir, confirmToken: args.confirmToken });
55
+ process.stdout.write(`${JSON.stringify({ ...result, runsSessionEnd: RUNS_SESSION_END.has(result.verdict) })}\n`);
@@ -46,10 +46,11 @@ import { join } from 'node:path';
46
46
  import { randomBytes } from 'node:crypto';
47
47
 
48
48
  import { readPeerCards } from './lib/peer-cards/reader.mjs';
49
- import { filterRealSessions } from './lib/session-schema/filters.mjs';
50
- import { readCanonicalSessions } from './lib/sessions-canonical.mjs';
49
+ import { readCanonicalSessions, isMeasuredSession } from './lib/sessions-canonical.mjs';
51
50
  import { recordDialecticRun } from './lib/learnings/evolve-telemetry.mjs';
52
51
  import { DEFAULT_BUDGET_TOKENS } from './lib/config/dialectic.mjs';
52
+ import { withVisibleInboxText } from './lib/learnings/surface.mjs';
53
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
53
54
 
54
55
  // ---------------------------------------------------------------------------
55
56
  // Constants
@@ -400,17 +401,24 @@ function parseTargetFromInfo(info) {
400
401
  * Best-effort read of a JSONL file: line-by-line JSON.parse with malformed-
401
402
  * line skip. Returns [] when the file is missing or empty.
402
403
  *
404
+ * For the canonical `learnings.jsonl` the visible records of a sibling
405
+ * `learnings-inbox.jsonl` are appended (#1516, #1685) via the shared
406
+ * visibility rule in `withVisibleInboxText` — also when the live file is
407
+ * absent (inbox host). An unreadable inbox never costs the live read.
408
+ *
403
409
  * @param {string} absPath
404
410
  * @returns {Promise<Array<object>>}
405
411
  */
406
412
  async function readJsonlBestEffort(absPath) {
407
- if (!existsSync(absPath)) return [];
408
- let raw;
409
- try {
410
- raw = await readFile(absPath, 'utf8');
411
- } catch {
412
- return [];
413
+ let raw = '';
414
+ if (existsSync(absPath)) {
415
+ try {
416
+ raw = await readFile(absPath, 'utf8');
417
+ } catch {
418
+ return [];
419
+ }
413
420
  }
421
+ raw = await withVisibleInboxText(absPath, raw);
414
422
  /** @type {Array<object>} */
415
423
  const out = [];
416
424
  for (const line of raw.split('\n')) {
@@ -435,7 +443,7 @@ async function readJsonlBestEffort(absPath) {
435
443
  * @returns {Promise<Array<object>>}
436
444
  */
437
445
  async function readTopLearnings(repoRoot, topN) {
438
- const path = join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
446
+ const path = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot); // #1722
439
447
  const entries = await readJsonlBestEffort(path);
440
448
  entries.sort((a, b) => {
441
449
  const ca = typeof a?.confidence === 'number' ? a.confidence : 0;
@@ -451,8 +459,8 @@ async function readTopLearnings(repoRoot, topN) {
451
459
  /**
452
460
  * Read and rank sessions: collapse ledger duplicates (#1209 — the ledger is
453
461
  * append-only and can carry more than one raw line per physical session, see
454
- * `scripts/lib/sessions-canonical.mjs` header), filter out phantom
455
- * `status: 'abandoned'` stubs (#834, session-close-backfill), sort by
462
+ * `scripts/lib/sessions-canonical.mjs` header), retain measured work
463
+ * via isMeasuredSession (#1584), excluding synthetic completed stubs, sort by
456
464
  * `completed_at` DESC, take last K.
457
465
  *
458
466
  * Without the dedup, a duplicate ledger line for the SAME session can occupy
@@ -467,7 +475,7 @@ async function readTopLearnings(repoRoot, topN) {
467
475
  * @returns {Array<object>}
468
476
  */
469
477
  function readLastSessions(repoRoot, lastK) {
470
- const entries = filterRealSessions(readCanonicalSessions({ repoRoot }));
478
+ const entries = readCanonicalSessions({ repoRoot }).filter(isMeasuredSession);
471
479
  entries.sort((a, b) => {
472
480
  const ta = typeof a?.completed_at === 'string' ? a.completed_at : '';
473
481
  const tb = typeof b?.completed_at === 'string' ? b.completed_at : '';
@@ -31,7 +31,7 @@
31
31
 
32
32
  import { parseArgs } from 'node:util';
33
33
  import { dirname, join } from 'node:path';
34
- import { fileURLToPath } from 'node:url';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
 
36
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
37
 
@@ -141,7 +141,7 @@ if (payloadRaw !== undefined) {
141
141
 
142
142
  let emitEvent;
143
143
  try {
144
- ({ emitEvent } = await import(join(__dirname, 'lib', 'events.mjs')));
144
+ ({ emitEvent } = await import(pathToFileURL(join(__dirname, 'lib', 'events.mjs')).href));
145
145
  } catch (err) {
146
146
  fail(`failed to load events.mjs: ${err.message}`, 2, jsonMode);
147
147
  }
@@ -27,17 +27,21 @@
27
27
  * {"action":"appended","path":"<file>","session_id":"<id>","schema_version":2}
28
28
  */
29
29
 
30
- import { existsSync, readFileSync } from 'node:fs';
31
- import { dirname, join } from 'node:path';
32
- import { resolveEventsLedgerRoot } from './lib/platform.mjs';
30
+ import { execFileSync } from 'node:child_process';
31
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
32
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
33
+ import { explainEventsLedgerRoot, resolveEventsLedgerRoot } from './lib/platform.mjs';
33
34
  import { appendJsonl } from './lib/common.mjs';
35
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
34
36
  import {
35
37
  MEMORY_CLEANUP_EVENT,
36
38
  deriveMemoryCleanupSignal,
37
39
  stampMemoryCleanup,
38
40
  } from './lib/memory-cleanup-stamp.mjs';
39
- import { parseStateMd, readSessionProfile, resolveStateMdPath } from './lib/state-md.mjs';
41
+ import { parseStateMd, readSessionProfile } from './lib/state-md.mjs';
42
+ import { resolveStateMdWriteTarget, warnForeignStateMd } from './lib/state-md/frontmatter-mutators.mjs';
40
43
  import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
44
+ import { readSessionWorkTrees } from './lib/session-work-trees.mjs';
41
45
  import { rollupSessionTokens } from './lib/session-token-rollup.mjs';
42
46
  import { COST_BASIS, PRICING_TABLE_DATE } from './lib/telemetry/pricing.mjs';
43
47
  import { serializeSessionLineChecked } from './lib/session-schema/serializer.mjs';
@@ -50,6 +54,7 @@ import {
50
54
  normalizeWaveKeys,
51
55
  } from './lib/session-schema.mjs';
52
56
  import { stableHostname } from './lib/host-identity.mjs';
57
+ import { activeDir } from './lib/session-registry.mjs';
53
58
  import { isMainModule } from './lib/is-main-module.mjs';
54
59
 
55
60
  /**
@@ -181,18 +186,23 @@ function resolveOwnSessionUuid({ override, recordSessionId }) {
181
186
  * Whole-file read, like deriveMemoryCleanupSignal() on the same file — fine at
182
187
  * today's events.jsonl size; revisit if the file outgrows a single read.
183
188
  *
189
+ * `behind` (#1668) is that same event's count of upstream commits the checkout
190
+ * lacked at start: a non-negative integer when measured, null when the event
191
+ * carries none (no upstream, or a pre-#1668 writer) — never read as 0.
192
+ *
184
193
  * Never throws.
185
194
  *
186
195
  * @param {string} eventsFile
187
196
  * @param {string} uuid
188
- * @returns {string|null}
197
+ * @returns {{ headSha: string|null, behind: number|null }}
189
198
  */
190
199
  function readOwnStartHeadSha(eventsFile, uuid) {
200
+ const none = { headSha: null, behind: null };
191
201
  let raw;
192
202
  try {
193
203
  raw = readFileSync(eventsFile, 'utf8');
194
204
  } catch {
195
- return null;
205
+ return none;
196
206
  }
197
207
  for (const line of raw.split('\n')) {
198
208
  if (!line.includes(uuid)) continue;
@@ -203,14 +213,115 @@ function readOwnStartHeadSha(eventsFile, uuid) {
203
213
  continue;
204
214
  }
205
215
  if (ev?.event !== 'orchestrator.session.started' || ev.session_id !== uuid) continue;
206
- if (ev.native_source === 'compact' || ev.native_source === 'resume') return null;
207
- return typeof ev.head_sha === 'string' && FULL_SHA_RE.test(ev.head_sha) ? ev.head_sha : null;
216
+ if (ev.native_source === 'compact' || ev.native_source === 'resume') return none;
217
+ return {
218
+ headSha: typeof ev.head_sha === 'string' && FULL_SHA_RE.test(ev.head_sha) ? ev.head_sha : null,
219
+ behind: Number.isInteger(ev.behind) && ev.behind >= 0 ? ev.behind : null,
220
+ };
221
+ }
222
+ return none;
223
+ }
224
+
225
+ /**
226
+ * True when THIS session's registry entry proves it works in a different tree
227
+ * than the one it started in (#1652): `work_root` (refreshed at every turn end,
228
+ * #1605) differs from `session_root` (the SessionStart project root, the tree
229
+ * `head_sha` was measured in). Then the start event's `head_sha` names the
230
+ * wrong tree — a `<head_sha>..HEAD` range would attribute that tree's history,
231
+ * not the session's commits — so the fallback must not adopt it.
232
+ *
233
+ * Absent entry, absent roots or an unreadable file → false (no proof of a
234
+ * divergence; the pre-#1652 behaviour). Ceiling: `work_root` follows the
235
+ * harness cwd, so a coordinator that commits in a worktree through absolute
236
+ * paths without changing its cwd is not detected — the primary source is the
237
+ * STATE.md `session-start-ref` (HEAD of the session's own checkout — repoRoot,
238
+ * the tree STATE.md lives in; commits in additional worktrees: #1659).
239
+ *
240
+ * Never throws.
241
+ *
242
+ * @param {string} uuid
243
+ * @returns {boolean}
244
+ */
245
+ function startTreeDiverged(uuid) {
246
+ if (!/^[A-Za-z0-9._-]+$/.test(uuid) || uuid.startsWith('.')) return false;
247
+ try {
248
+ const entry = JSON.parse(readFileSync(join(activeDir(), `${uuid}.json`), 'utf8'));
249
+ const { session_root: sessionRoot, work_root: workRoot } = entry ?? {};
250
+ return typeof sessionRoot === 'string' && typeof workRoot === 'string' && sessionRoot !== workRoot;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Realpath, or the resolved path when it does not exist (any more).
258
+ *
259
+ * @param {string} p
260
+ * @returns {string}
261
+ */
262
+ function realOrResolved(p) {
263
+ try {
264
+ return realpathSync(p);
265
+ } catch {
266
+ return resolve(p);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Keep only well-formed `session_work_trees` entries (#1659): an absolute
272
+ * `tree` and a FULL hex `start_ref` — the shape the work_tree reader accepts.
273
+ * Invalid entries are dropped, never the record.
274
+ *
275
+ * @param {unknown} value
276
+ * @returns {{ entries: Array<{ tree: string, start_ref: string }>, dropped: number } | null}
277
+ * null when `value` is not an array at all.
278
+ */
279
+ function sanitizeWorkTrees(value) {
280
+ if (!Array.isArray(value)) return null;
281
+ const entries = [];
282
+ for (const item of value) {
283
+ if (item && typeof item.tree === 'string' && isAbsolute(item.tree)
284
+ && typeof item.start_ref === 'string' && FULL_SHA_RE.test(item.start_ref)) {
285
+ entries.push({ tree: item.tree, start_ref: item.start_ref });
286
+ }
287
+ }
288
+ return { entries, dropped: value.length - entries.length };
289
+ }
290
+
291
+ /**
292
+ * The own checkout's top level — the tree emit-session runs in (process.cwd(),
293
+ * where it also reads STATE.md and current-session.json). null outside a git
294
+ * repo. Never throws.
295
+ *
296
+ * @returns {string|null}
297
+ */
298
+ function ownCheckoutTop() {
299
+ try {
300
+ const top = execFileSync('git', ['rev-parse', '--show-toplevel'], {
301
+ encoding: 'utf8',
302
+ timeout: 10_000,
303
+ stdio: ['ignore', 'pipe', 'ignore'],
304
+ }).trim();
305
+ return top ? realOrResolved(top) : null;
306
+ } catch {
307
+ return null;
208
308
  }
209
- return null;
210
309
  }
211
310
 
212
311
  function parseArgs(argv) {
213
- const args = { file: '.orchestrator/metrics/sessions.jsonl', entry: null, sessionUuid: null };
312
+ // The default shares the events ledger's main-checkout root (#1584).
313
+ // Explicit --file paths below remain verbatim, including relative paths.
314
+ // #1659: a linked worktree whose common dir is not named `.git`
315
+ // (`--separate-git-dir`, bare repo) has no main checkout to map to, and the
316
+ // resolver falls back to the WORKTREE — a second, per-worktree ledger. The
317
+ // default is therefore resolved after parsing and refused when unmapped.
318
+ const args = {
319
+ file: null,
320
+ // true when --file was not given and the default sessions ledger is used
321
+ fileDefaulted: false,
322
+ entry: null,
323
+ sessionUuid: null,
324
+ };
214
325
  for (let i = 0; i < argv.length; i++) {
215
326
  const a = argv[i];
216
327
  if (a === '--file') args.file = argv[++i];
@@ -219,7 +330,7 @@ function parseArgs(argv) {
219
330
  else if (a === '--help' || a === '-h') {
220
331
  process.stdout.write(
221
332
  'Usage: node scripts/emit-session.mjs [--file PATH] [--entry JSON]\n' +
222
- ' --file target JSONL file (default: .orchestrator/metrics/sessions.jsonl)\n' +
333
+ ' --file explicit target JSONL file (default: main-checkout/.orchestrator/metrics/sessions.jsonl)\n' +
223
334
  ' --entry entry JSON (if omitted, read from stdin)\n' +
224
335
  ' --session-uuid raw harness UUID (default: owned .orchestrator/current-session.json)\n' +
225
336
  'Exit codes: 0 append ok, 1 validation error, 2 I/O error\n'
@@ -230,6 +341,22 @@ function parseArgs(argv) {
230
341
  process.exit(2);
231
342
  }
232
343
  }
344
+ if (args.file === null) {
345
+ // The cwd is the EXPLICIT root (like the former resolveEventsLedgerRoot
346
+ // call and every other sessions.jsonl reader), so a launch-dir env var
347
+ // never redirects the default --file; an unmapped worktree is still
348
+ // detected (#1659).
349
+ const ledger = explainEventsLedgerRoot(process.cwd());
350
+ if (!ledger.resolved) {
351
+ process.stderr.write(
352
+ `emit-session: refusing the default --file: ${ledger.reason}; pass an explicit --file\n`
353
+ );
354
+ process.exit(2);
355
+ }
356
+ // ledger-root: ledger.root comes from explainEventsLedgerRoot (sessions rule)
357
+ args.file = join(ledger.root, '.orchestrator', 'metrics', 'sessions.jsonl');
358
+ args.fileDefaulted = true;
359
+ }
233
360
  return args;
234
361
  }
235
362
 
@@ -416,8 +543,15 @@ async function main() {
416
543
  if (needsProfile || needsStartRef) {
417
544
  let stateMdContents = '';
418
545
  try {
419
- const stateMdPath = resolveStateMdPath(process.cwd());
420
- if (existsSync(stateMdPath)) stateMdContents = readFileSync(stateMdPath, 'utf8');
546
+ // #1622: the write side's ownership rule — another platform's STATE.md
547
+ // not proven to be this process's counts as no STATE.md.
548
+ const { path: stateMdPath, rejected } = resolveStateMdWriteTarget(process.cwd());
549
+ if (rejected) {
550
+ warnForeignStateMd('emit-session', rejected, stateMdPath);
551
+ stateMdStartRefNote = "STATE.md foreign (another platform's, not this process's session-id)";
552
+ } else if (existsSync(stateMdPath)) {
553
+ stateMdContents = readFileSync(stateMdPath, 'utf8');
554
+ }
421
555
  } catch (err) {
422
556
  process.stderr.write(
423
557
  `emit-session: WARN could not read STATE.md for session_profile/session_start_ref ` +
@@ -483,10 +617,28 @@ async function main() {
483
617
  // with a WARN whether or not an own UUID was available (#1457 point 2) —
484
618
  // without one the fallback cannot run, which is exactly when silence hid it.
485
619
  if (!hasOwn('session_start_ref')) {
486
- const headSha = ownUuid !== null
620
+ const { headSha, behind } = ownUuid !== null
487
621
  ? readOwnStartHeadSha(siblingEventsFile(args.file), ownUuid)
488
- : null;
489
- if (headSha !== null) {
622
+ : { headSha: null, behind: null };
623
+ if (headSha !== null && behind !== null && behind > 0) {
624
+ // #1668 — the checkout was provably behind its upstream at start. A pull
625
+ // during the session makes `<head_sha>..HEAD` span the upstream commits it
626
+ // pulled in; no sha known at start fixes that (merge-base(head_sha,
627
+ // upstream) IS head_sha when merely behind, and upstream_sha is wrong when
628
+ // nothing was pulled), so the ref is dropped rather than overstated.
629
+ // behind 0 or not measured (absent) keep the pre-#1668 adoption below.
630
+ process.stderr.write(
631
+ `emit-session: WARN no session_start_ref — ${stateMdStartRefNote}, and the own ` +
632
+ `session.started head_sha=${headSha} was ${behind} commit(s) behind its upstream; ` +
633
+ 'a range from it can include pulled-in upstream commits (#1668)\n'
634
+ );
635
+ } else if (headSha !== null && startTreeDiverged(ownUuid)) {
636
+ process.stderr.write(
637
+ `emit-session: WARN no session_start_ref — ${stateMdStartRefNote}, and the own ` +
638
+ `session.started head_sha=${headSha} was measured in session_root, not the work_root ` +
639
+ 'this session works in (#1652)\n'
640
+ );
641
+ } else if (headSha !== null) {
490
642
  repaired = { ...repaired, session_start_ref: headSha };
491
643
  process.stderr.write(
492
644
  `emit-session: derived session_start_ref=${headSha} from the own orchestrator.session.started event\n`
@@ -503,6 +655,61 @@ async function main() {
503
655
  repaired = { ...repaired, raw_session_id: ownUuid };
504
656
  }
505
657
 
658
+ // `session_work_trees` (#1659) — the (tree, start_ref) pairs the staging
659
+ // fence recorded before each of THIS session's commits
660
+ // (`orchestrator.session.work_tree`, joined on the own raw id — never on a
661
+ // repo-global file). `session_start_ref` names one sha in the own checkout,
662
+ // so the field is written ONLY when it adds something: ≥2 trees, or one tree
663
+ // that is not the own checkout. An explicit key on the record wins, but its
664
+ // malformed entries are dropped with a WARN (like session_start_ref above —
665
+ // enforced here, not in validateSession()). Best-effort: any failure omits
666
+ // the field with a WARN; it never fails the emit.
667
+ if (hasOwn('session_work_trees')) {
668
+ const clean = sanitizeWorkTrees(repaired.session_work_trees);
669
+ if (clean === null) {
670
+ process.stderr.write('emit-session: WARN session_work_trees is not an array; dropping it\n');
671
+ const { session_work_trees: _dropped, ...rest } = repaired;
672
+ repaired = rest;
673
+ } else if (clean.dropped > 0) {
674
+ process.stderr.write(
675
+ `emit-session: WARN session_work_trees: dropping ${clean.dropped} malformed entry/entries\n`
676
+ );
677
+ repaired = { ...repaired, session_work_trees: clean.entries };
678
+ }
679
+ } else if (ownUuid !== null) {
680
+ try {
681
+ const read = readSessionWorkTrees({ sessionId: ownUuid, eventsFile: siblingEventsFile(args.file) });
682
+ if (read.error) {
683
+ process.stderr.write(
684
+ `emit-session: WARN could not read work_tree events (${read.error}); omitting session_work_trees\n`
685
+ );
686
+ } else if (read.malformed > 0) {
687
+ // Unreadable lines may hide own work_tree events — warn even when no
688
+ // tree was read, since then the omission itself may be the gap (#1659).
689
+ process.stderr.write(
690
+ `emit-session: WARN ${read.malformed} unreadable events ledger line(s); session_work_trees may be incomplete\n`
691
+ );
692
+ }
693
+ if (!read.error && read.trees.length > 0) {
694
+ if (read.invalid > 0) {
695
+ process.stderr.write(
696
+ `emit-session: WARN ignored ${read.invalid} own work_tree event(s) without a valid tree/start_ref\n`
697
+ );
698
+ }
699
+ // A single tree is only worth recording when it provably differs from
700
+ // the own checkout; outside a git repo that is unprovable → omitted.
701
+ const ownTop = read.trees.length >= 2 ? null : ownCheckoutTop();
702
+ const addsInfo = read.trees.length >= 2
703
+ || (ownTop !== null && realOrResolved(read.trees[0].tree) !== ownTop);
704
+ if (addsInfo) repaired = { ...repaired, session_work_trees: read.trees };
705
+ }
706
+ } catch (err) {
707
+ process.stderr.write(
708
+ `emit-session: WARN session_work_trees derivation failed (${err?.message ?? err}); omitting it\n`
709
+ );
710
+ }
711
+ }
712
+
506
713
  // Token rollup (#1436) — mechanical, replacing the session-metrics-write.md
507
714
  // prose step that had no production caller. Joins the sibling subagents.jsonl
508
715
  // on the own raw UUID and fills ONLY keys the entry does not carry; an
@@ -518,10 +725,22 @@ async function main() {
518
725
  if (ownUuid !== null && !hasOwn('total_tokens')) {
519
726
  let rollup = null;
520
727
  try {
521
- rollup = rollupSessionTokens({
522
- parentSessionId: ownUuid,
523
- subagentsPath: join(dirname(args.file), 'subagents.jsonl'),
524
- });
728
+ // A defaulted --file reads the subagents ledger where the hook writer
729
+ // (hooks/subagent-telemetry.mjs) puts it: resolved from the cwd first, then
730
+ // from the ledger root — the hook resolves from the LAUNCH dir, which is
731
+ // the main checkout when the session entered this worktree later and the
732
+ // repo versions the ledger. First copy with a record of this session wins,
733
+ // so nothing is counted twice. An explicit --file keeps its sibling.
734
+ const candidates = args.fileDefaulted
735
+ ? [...new Set([
736
+ resolveLedgerPath('.orchestrator/metrics/subagents.jsonl', process.cwd()),
737
+ resolveLedgerPath('.orchestrator/metrics/subagents.jsonl', resolveEventsLedgerRoot(process.cwd())),
738
+ ])]
739
+ : [join(dirname(args.file), 'subagents.jsonl')];
740
+ for (const subagentsPath of candidates) {
741
+ rollup = rollupSessionTokens({ parentSessionId: ownUuid, subagentsPath });
742
+ if (rollup.matched_records > 0) break;
743
+ }
525
744
  } catch (err) {
526
745
  process.stderr.write(
527
746
  `emit-session: WARN token rollup failed (${err?.message ?? err}); omitting token fields\n`
@@ -74,6 +74,7 @@ import { appendEvalRecord, readEvalRecords } from './lib/eval/sink.mjs';
74
74
  import { writeStdoutLineSync } from './lib/io.mjs';
75
75
  import { VALID_MODEL_SOURCES } from './lib/eval/schema.mjs';
76
76
  import { SessionResolutionError } from './lib/eval/session-resolve.mjs';
77
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
77
78
 
78
79
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
79
80
 
@@ -103,7 +104,8 @@ OPTIONS
103
104
  --session <id> session_id to evaluate (default: last completed session)
104
105
  --json emit the record (or verify result) as JSON on stdout
105
106
  --no-write evaluate without appending to the eval journal
106
- --metrics-dir <path> metrics directory (default: .orchestrator/metrics)
107
+ --metrics-dir <path> metrics directory (default: .orchestrator/metrics of this repo;
108
+ in a linked worktree the main checkout's when it is gitignored)
107
109
  --rubric <path> rubric file to hash for provenance (default: ${DEFAULT_RUBRIC_PATH})
108
110
  --model-id <id> evaluated model id (default: $ANTHROPIC_MODEL or "unknown")
109
111
  --model-source <src> ${VALID_MODEL_SOURCES.join(' | ')} (default: self-report)
@@ -159,8 +161,20 @@ function renderHuman(record, summary, writeResult) {
159
161
  return lines.join('\n');
160
162
  }
161
163
 
164
+ /**
165
+ * The metrics directory: `--metrics-dir` as given, else this repo's
166
+ * `.orchestrator/metrics` — from a linked worktree the main checkout's when
167
+ * `eval.jsonl` is gitignored there, so the record survives the worktree (#1722).
168
+ * The engine reads `sessions.jsonl` beside it through the same main-checkout
169
+ * mapping (engine.mjs, #1592).
170
+ */
171
+ function resolveMetricsDir(flag) {
172
+ if (flag) return path.resolve(flag);
173
+ return path.dirname(resolveLedgerPath('.orchestrator/metrics/eval.jsonl', process.cwd()));
174
+ }
175
+
162
176
  function runEvaluate(values) {
163
- const metricsDir = values['metrics-dir'] || '.orchestrator/metrics';
177
+ const metricsDir = resolveMetricsDir(values['metrics-dir']);
164
178
  const rubricPath = values.rubric || DEFAULT_RUBRIC_PATH;
165
179
  const modelSource = values['model-source'] || 'self-report';
166
180
  if (!VALID_MODEL_SOURCES.includes(modelSource)) {
@@ -219,7 +233,7 @@ function runEvaluate(values) {
219
233
  }
220
234
 
221
235
  function runVerify(runId, values) {
222
- const metricsDir = values['metrics-dir'] || '.orchestrator/metrics';
236
+ const metricsDir = resolveMetricsDir(values['metrics-dir']);
223
237
  const rubricPath = values.rubric || DEFAULT_RUBRIC_PATH;
224
238
 
225
239
  const records = readEvalRecords(path.join(metricsDir, 'eval.jsonl'));
@@ -60,12 +60,14 @@ import {
60
60
  CURRENT_ANONYMIZATION_VERSION,
61
61
  } from './lib/learnings.mjs';
62
62
  import { withLearningsLock } from './lib/learnings/io.mjs';
63
+ import { assertStoreRoleUsable, isCanonicalStore, resolveStoreRole } from './lib/learnings/store-role.mjs';
63
64
  import { findProjectRoot, resolveInstructionFile, expandTilde, redactHomeDir } from './lib/common.mjs';
64
65
  import { parseSessionConfig } from './lib/config.mjs';
65
66
  import { createSecretValueMasker } from './lib/secret-masker.mjs';
66
67
  import { stableHostname, readHostAliases } from './lib/host-identity.mjs';
67
68
  import { emitEvent, sessionAttribution } from './lib/events.mjs';
68
69
  import { isMainModule } from './lib/is-main-module.mjs';
70
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
69
71
 
70
72
  // Vault-relative default write target (Epic #774 — docs Public-Split removed
71
73
  // the prior in-repo generated telemetry doc in favor of the private Meta-Vault).
@@ -572,6 +574,7 @@ function parseArgs(argv) {
572
574
  const out = {
573
575
  dryRun: false,
574
576
  promote: false,
577
+ // ledger-root: parse default, routed after parsing unless --input is explicit
575
578
  input: '.orchestrator/metrics/learnings.jsonl',
576
579
  output: undefined, // resolved lazily below (default = vault-dir target)
577
580
  generatedAt: new Date().toISOString(),
@@ -581,11 +584,12 @@ function parseArgs(argv) {
581
584
  // themselves (see their opts.repoRoot doc for why).
582
585
  repoRoot: findProjectRoot(),
583
586
  };
587
+ let inputExplicit = false;
584
588
  for (let i = 0; i < argv.length; i++) {
585
589
  const a = argv[i];
586
590
  if (a === '--dry-run') out.dryRun = true;
587
591
  else if (a === '--promote') out.promote = true;
588
- else if (a === '--input') out.input = argv[++i];
592
+ else if (a === '--input') { out.input = argv[++i]; inputExplicit = true; }
589
593
  else if (a === '--output') out.output = argv[++i];
590
594
  else if (a === '--generated-at') out.generatedAt = argv[++i];
591
595
  else if (a === '--help' || a === '-h') {
@@ -598,7 +602,12 @@ function parseArgs(argv) {
598
602
  ' (backup created first). Use with --dry-run to preview.\n' +
599
603
  ' --dry-run Print markdown to stdout; do not write any files.\n' +
600
604
  ' --output FILE Override the default write target. Default: resolved from\n' +
601
- ' vault-integration.vault-dir (see script header doc).\n'
605
+ ' vault-integration.vault-dir (see script header doc).\n' +
606
+ '\n' +
607
+ 'Exit codes: 0 success, 1 export failed (incl. --promote without --dry-run on a\n' +
608
+ ' host whose learnings-store role is inbox or invalid, #1516:\n' +
609
+ ' nothing written),\n' +
610
+ ' 2 unknown argument.\n'
602
611
  );
603
612
  process.exit(0);
604
613
  } else {
@@ -619,6 +628,13 @@ function parseArgs(argv) {
619
628
  }
620
629
  out.output = resolved;
621
630
  }
631
+ // #1722: the DEFAULT input routes through the ledger resolver (main checkout
632
+ // when cwd is a linked worktree that gitignores it); outside one the relative
633
+ // default is kept verbatim. An explicit --input stays as given.
634
+ if (!inputExplicit) {
635
+ const routed = resolveLedgerPath(out.input, process.cwd());
636
+ if (routed !== path.resolve(out.input)) out.input = routed;
637
+ }
622
638
  return out;
623
639
  }
624
640
 
@@ -650,6 +666,17 @@ export async function promoteHwLearnings(opts) {
650
666
  // nothing and takes no lock. A lock not acquired throws LearningsLockError
651
667
  // (nothing written), which the CLI reports as `export failed: …`, exit 1.
652
668
  if (opts.dryRun) return promoteHwLearningsUnlocked(opts);
669
+ // #1516 fail-closed: an invalid store role throws LearningsStoreRoleError
670
+ // (CLI: `export failed: …`, exit 1) before anything is read, locked or emitted.
671
+ assertStoreRoleUsable(opts.input);
672
+ // #1516: on an inbox-role host the live store is never rewritten, and a
673
+ // promoted twin keeps its private original's id, so the inbox rewrite would
674
+ // drop it silently. Refuse before anything is read, locked or emitted.
675
+ if (isCanonicalStore(opts.input) && resolveStoreRole().role === 'inbox') {
676
+ throw new Error(
677
+ 'store-role inbox: --promote rewrites learnings.jsonl, which this host never does — run it on the writer host'
678
+ );
679
+ }
653
680
  return withLearningsLock(opts.input, () => promoteHwLearningsUnlocked(opts));
654
681
  }
655
682
 
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /** Read-only CLI for explicitly supplied, stable snapshot copies (#1462). */
3
- import { constants, promises as fs } from 'node:fs';
3
+ import { promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { TextDecoder } from 'node:util';
6
6
  import { auditCheckins } from './lib/fleet-checkin-audit.mjs';
7
7
  import { isMainModule } from './lib/is-main-module.mjs';
8
+ import { openNoFollow } from './lib/no-follow-open.mjs';
8
9
 
9
10
  const HELP = `Usage: fleet-checkin-audit --input-root DIR --registry FILE --checkins FILE --repo KEY [--repo KEY] --at UTC [--json]
10
11
  All five input flags are required. --repo may repeat with distinct explicit keys.
@@ -80,10 +81,13 @@ async function readSnapshot({ target, stat }) {
80
81
  // The parent check above and this open are not atomic: a same-user process can swap a parent
81
82
  // directory in between. Fine while the operator names the snapshot files and the result grants
82
83
  // nothing; revisit (#1549) once paths come from a foreign owner or the result gates a decision.
83
- if (typeof constants.O_NOFOLLOW !== 'number' || typeof constants.O_NONBLOCK !== 'number') {
84
- throw failure('unsupported-safe-open', 2);
84
+ let handle;
85
+ try { handle = await openNoFollow(target); }
86
+ catch (error) {
87
+ // inspect() already proved a regular non-symlink file: any of these means it changed since.
88
+ if (['ERR_FILE_CHANGED', 'ELOOP', 'ERR_NOT_REGULAR_FILE'].includes(error?.code)) throw failure('changed-input-file', 1);
89
+ throw error;
85
90
  }
86
- const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
87
91
  let bytes;
88
92
  try {
89
93
  const actual = await handle.stat();