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,248 @@
1
+ /**
2
+ * Idle Reset for STATE.md (issue #1560) — the code form of session-start
3
+ * Phase 1.5 § "Idle Reset" (skills/session-start/references/phase-1-5-session-continuity.md).
4
+ *
5
+ * Until #1560 the reset existed only as prose and every coordinator rewrote
6
+ * STATE.md by hand. Its Rule 6 deleted the five recommendation keys from the
7
+ * frontmatter, which is exactly what Phase 7.5's `buildLiveSignals()` reads —
8
+ * so after every clean `/close` the Mode-Selector saw no recommendation and
9
+ * fell back to `feature` at confidence 0. The keys now STAY in the frontmatter:
10
+ * they are the session-to-session handoff, and session-end Phase 3.7a (their
11
+ * sole writer) overwrites them at the next close.
12
+ *
13
+ * Deliberately NOT re-exported from `scripts/lib/state-md.mjs`: that barrel is
14
+ * part of the hook import closure (`hooks/_lib/hook-import-set.json`), and no
15
+ * hook needs the reset. The CLI `scripts/state-md-idle-reset.mjs` imports this
16
+ * module directly.
17
+ */
18
+
19
+ import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
20
+ import { existsSync } from 'node:fs';
21
+ import { resolveStateMdWriteTarget, updateFrontmatterFields, warnForeignStateMd, writeStateMd } from './frontmatter-mutators.mjs';
22
+ import { parseRecommendations } from './recommendations.mjs';
23
+
24
+ /** Rule 7: the drift-baseline keys that must not survive into session N+1. */
25
+ const SCOPE_BASELINE_KEYS = [
26
+ 'scope-baseline-intent',
27
+ 'scope-baseline-owner-boundary',
28
+ 'scope-baseline-planned-files',
29
+ 'scope-baseline-session',
30
+ 'scope-baseline-frozen-at',
31
+ ];
32
+
33
+ /** A level-2 heading (`## X`), never a deeper one (`### X`). */
34
+ const H2_RE = /^##(?!#)\s/;
35
+
36
+ /** Rule 6: heading of the human-readable recommendations copy. */
37
+ const RECOMMENDATIONS_HEADING = '### Recommendations (archived from v1.1 frontmatter)';
38
+
39
+ /**
40
+ * Splits a STATE.md body into the text before the first level-2 heading and
41
+ * one record per level-2 section. Every record keeps its raw lines (heading
42
+ * included, up to the next level-2 heading), so re-joining an untouched
43
+ * section is byte-identical.
44
+ *
45
+ * KNOWN CEILING: a `## ` line inside a fenced code block is read as a heading,
46
+ * the same limitation every helper in body-sections.mjs shares. No STATE.md
47
+ * section observed so far carries fenced code.
48
+ *
49
+ * @param {string} body
50
+ * @returns {{preamble: string[], sections: Array<{heading: string, lines: string[]}>}}
51
+ */
52
+ function splitSections(body) {
53
+ const preamble = [];
54
+ const sections = [];
55
+ for (const line of body.split('\n')) {
56
+ if (H2_RE.test(line)) sections.push({ heading: line, lines: [line] });
57
+ else if (sections.length === 0) preamble.push(line);
58
+ else sections[sections.length - 1].lines.push(line);
59
+ }
60
+ return { preamble, sections };
61
+ }
62
+
63
+ /** @param {string} heading @param {string} title */
64
+ function isSection(heading, title) {
65
+ // The title must END there (or open a parenthesis): `## Wave History Archive`
66
+ // is a different section.
67
+ return new RegExp(`^##\\s+${title}(?:\\s*\\(|\\s*$)`).test(heading);
68
+ }
69
+
70
+ /** Drops leading and trailing blank lines. */
71
+ function trimBlankLines(lines) {
72
+ let start = 0;
73
+ let end = lines.length;
74
+ while (start < end && lines[start].trim() === '') start++;
75
+ while (end > start && lines[end - 1].trim() === '') end--;
76
+ return lines.slice(start, end);
77
+ }
78
+
79
+ /** @param {number} x a ratio in 0..1 */
80
+ function pct(x) {
81
+ return `${Math.round(x * 100)}%`;
82
+ }
83
+
84
+ /**
85
+ * Rule 6: the human-readable copy of the recommendation fields. Null fields
86
+ * are omitted; `null` overall when there is nothing to show.
87
+ *
88
+ * @param {object} frontmatter
89
+ * @returns {string|null}
90
+ */
91
+ function renderRecommendationsBlock(frontmatter) {
92
+ const rec = parseRecommendations(frontmatter);
93
+ if (rec === null) return null;
94
+ const bullets = [];
95
+ if (rec.mode !== null) bullets.push(`- **Recommended mode:** ${rec.mode}`);
96
+ if (rec.rationale !== null) bullets.push(`- **Rationale:** ${rec.rationale}`);
97
+ if (rec.completionRate !== null) bullets.push(`- **Completion rate:** ${pct(rec.completionRate)}`);
98
+ if (rec.carryoverRatio !== null) bullets.push(`- **Carryover ratio:** ${pct(rec.carryoverRatio)}`);
99
+ if (rec.priorities !== null) {
100
+ const list = rec.priorities.length > 0 ? rec.priorities.map((id) => `#${id}`).join(', ') : 'none';
101
+ bullets.push(`- **Top priorities:** ${list}`);
102
+ }
103
+ if (bullets.length === 0) return null;
104
+ return [RECOMMENDATIONS_HEADING, ...bullets].join('\n');
105
+ }
106
+
107
+ /**
108
+ * Removes one `### <heading>` subsection (up to the next `### ` or `## ` line).
109
+ *
110
+ * @param {string[]} lines
111
+ * @param {string} heading the full `### …` line
112
+ * @returns {string[]}
113
+ */
114
+ function dropSubsection(lines, heading) {
115
+ const out = [];
116
+ let skipping = false;
117
+ for (const line of lines) {
118
+ if (line.trimEnd() === heading) skipping = true;
119
+ else if (skipping && /^#{2,3}\s/.test(line)) skipping = false;
120
+ if (!skipping) out.push(line);
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Resets a `status: completed` STATE.md to idle (Phase 1.5 § Idle Reset,
127
+ * Rules 1-7). Pure: string in, result out, no file I/O.
128
+ *
129
+ * 1. `status: idle`. 2. `current-wave: 0`. 3. Every `## Wave History` body moves
130
+ * into a new `## Previous Session` section (appended at the end of the body,
131
+ * replacing any earlier one) under `### Wave History (<session>)`; the original
132
+ * section is removed. 4. `## Deviations` keeps its heading with an empty body;
133
+ * `## What Not To Retry` and `## Open Questions` stay byte-for-byte. 5. Other
134
+ * frontmatter keys are untouched. 6. The five recommendation keys STAY in the
135
+ * frontmatter (Phase 7.5 reads them); a readable copy heads `## Previous Session`.
136
+ * 7. The five `scope-baseline-*` keys are deleted, no other key.
137
+ *
138
+ * When neither a Wave History section nor a recommendation exists, an existing
139
+ * `## Previous Session` is left as it is — there is nothing to replace it with.
140
+ * With recommendations but no Wave History (a coordinator-direct session), the
141
+ * earlier archive body stays below the new recommendations block.
142
+ *
143
+ * @param {string} contents STATE.md contents
144
+ * @returns {{changed: boolean, contents: string, reason?: 'status-not-completed'|'unparseable'}}
145
+ */
146
+ export function resetStateMdToIdle(contents) {
147
+ const parsed = parseStateMd(contents);
148
+ if (parsed === null) return { changed: false, contents, reason: 'unparseable' };
149
+ if (parsed.frontmatter.status !== 'completed') {
150
+ return { changed: false, contents, reason: 'status-not-completed' };
151
+ }
152
+
153
+ const { preamble, sections } = splitSections(parsed.body);
154
+ const waveHistory = sections.filter((s) => isSection(s.heading, 'Wave History'));
155
+ const recBlock = renderRecommendationsBlock(parsed.frontmatter);
156
+ const archive = waveHistory.length > 0 || recBlock !== null;
157
+ // Rule 6 PREPENDS: without a new Wave History, the earlier archive stays below
158
+ // the fresh recommendations block (minus its own, now superseded, copy).
159
+ const keptArchive =
160
+ waveHistory.length === 0
161
+ ? trimBlankLines(
162
+ dropSubsection(
163
+ sections.filter((s) => isSection(s.heading, 'Previous Session')).flatMap((s) => s.lines.slice(1)),
164
+ RECOMMENDATIONS_HEADING,
165
+ ),
166
+ )
167
+ : [];
168
+
169
+ const lines = [...preamble];
170
+ for (const { heading, lines: own } of sections) {
171
+ if (isSection(heading, 'Wave History')) continue;
172
+ if (archive && isSection(heading, 'Previous Session')) continue;
173
+ lines.push(...(isSection(heading, 'Deviations') ? [heading, ''] : own));
174
+ }
175
+
176
+ let body = lines.join('\n');
177
+ if (archive) {
178
+ const blocks = [];
179
+ if (recBlock !== null) blocks.push(recBlock);
180
+ if (waveHistory.length > 0) {
181
+ const session = typeof parsed.frontmatter.session === 'string' ? parsed.frontmatter.session : '';
182
+ const heading = session ? `### Wave History (${session})` : '### Wave History';
183
+ const moved = trimBlankLines(waveHistory.flatMap((s) => s.lines.slice(1)));
184
+ blocks.push([heading, ...moved].join('\n'));
185
+ }
186
+ if (keptArchive.length > 0) blocks.push(keptArchive.join('\n'));
187
+ // Only ever ADD newlines before the new section — trimming would change the
188
+ // trailing bytes of whatever section now precedes it (possibly WNTR).
189
+ if (body.length > 0 && !body.endsWith('\n\n')) body += body.endsWith('\n') ? '\n' : '\n\n';
190
+ body += `## Previous Session\n\n${blocks.join('\n\n')}\n`;
191
+ }
192
+
193
+ const fields = { status: 'idle', 'current-wave': 0 };
194
+ for (const key of SCOPE_BASELINE_KEYS) fields[key] = null;
195
+ const next = updateFrontmatterFields(
196
+ serializeStateMd({ frontmatter: parsed.frontmatter, body }),
197
+ fields,
198
+ );
199
+ return { changed: next !== contents, contents: next };
200
+ }
201
+
202
+ /**
203
+ * Applies `resetStateMdToIdle` to the repo's STATE.md under the STATE.md lock
204
+ * (`writeStateMd`). A missing or empty STATE.md is a no-op with reason `missing`;
205
+ * when the only STATE.md belongs to another platform's foreign session it is
206
+ * `foreign-state-md` (#1618), with one stderr WARN naming that file.
207
+ * A write the size-ceiling / frontmatter-safe guards refuse comes back as
208
+ * `changed: false` with that guard's reason.
209
+ *
210
+ * @param {string} repoRoot absolute repository root (required)
211
+ * @param {object} [opts] passed through to `writeStateMd`
212
+ * @returns {Promise<{changed: boolean, reason?: string, path: string}>}
213
+ * @throws {Error} when repoRoot is missing
214
+ */
215
+ export async function resetStateMdToIdleOnDisk(repoRoot, opts = {}) {
216
+ if (!repoRoot) {
217
+ throw new Error(`resetStateMdToIdleOnDisk: repoRoot is required (got ${typeof repoRoot}).`);
218
+ }
219
+ // No STATE.md: answer before the lock, which would otherwise create
220
+ // <repoRoot>/.orchestrator/ in whatever directory a mistyped root names.
221
+ // Probe the same path writeStateMd will target (#1618), never a foreign one.
222
+ // The reset is the hand-over that turns the PREVIOUS session's completed
223
+ // record into idle, so a preferred `status: completed` file qualifies (#1640).
224
+ const writeOpts = { ...opts, completedHandoff: true };
225
+ const { path: statePath, rejected } = resolveStateMdWriteTarget(repoRoot, writeOpts);
226
+ if (!existsSync(statePath)) {
227
+ if (rejected) warnForeignStateMd('resetStateMdToIdleOnDisk', rejected, statePath);
228
+ return { changed: false, reason: rejected ? 'foreign-state-md' : 'missing', path: statePath };
229
+ }
230
+ let reason;
231
+ const result = await writeStateMd(
232
+ repoRoot,
233
+ (contents) => {
234
+ if (contents === '') {
235
+ reason = 'missing';
236
+ return null;
237
+ }
238
+ const reset = resetStateMdToIdle(contents);
239
+ reason = reset.reason;
240
+ return reset.changed ? reset.contents : null;
241
+ },
242
+ writeOpts,
243
+ );
244
+ const out = { changed: result.written, path: result.path };
245
+ const why = result.reason ?? reason;
246
+ if (why !== undefined) out.reason = why;
247
+ return out;
248
+ }
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
26
- import { updateFrontmatterFields, writeStateMd } from './frontmatter-mutators.mjs';
26
+ import { resolveStateMdWriteTarget, updateFrontmatterFields, writeStateMd } from './frontmatter-mutators.mjs';
27
27
 
28
28
  const MISSION_STATUS_HEADING_RE = /^##\s+Mission Status\s*$/;
29
29
  const WRITER_TIMESTAMP_SOURCE = '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z';
@@ -677,9 +677,13 @@ const REFUSAL_REASONS = Object.freeze({
677
677
  */
678
678
  export async function setMissionStatusOnDisk(repoRoot, taskId, status, opts = {}) {
679
679
  let refused;
680
+ // A fail-closed write target (#1618) hands an empty buffer; writeStateMd
681
+ // already names the rejected foreign STATE.md, so do not blame its content.
682
+ const foreign = resolveStateMdWriteTarget(repoRoot, opts).rejected !== null;
680
683
  const result = await writeStateMd(
681
684
  repoRoot,
682
685
  (contents) => {
686
+ if (foreign && contents === '') return contents;
683
687
  const outcome = setMissionStatusDetailed(contents, taskId, status);
684
688
  refused = outcome.refused;
685
689
  if (outcome.refused) {
@@ -37,10 +37,7 @@ export function parseRecommendations(frontmatter) {
37
37
  const mode = typeof frontmatter['recommended-mode'] === 'string'
38
38
  ? frontmatter['recommended-mode']
39
39
  : null;
40
- const priorities = Array.isArray(frontmatter['top-priorities'])
41
- && frontmatter['top-priorities'].every((x) => Number.isInteger(x))
42
- ? frontmatter['top-priorities'].slice()
43
- : null;
40
+ const priorities = parsePriorities(frontmatter['top-priorities']);
44
41
  const carryoverRatio = typeof frontmatter['carryover-ratio'] === 'number'
45
42
  && !Number.isNaN(frontmatter['carryover-ratio'])
46
43
  ? frontmatter['carryover-ratio']
@@ -55,3 +52,28 @@ export function parseRecommendations(frontmatter) {
55
52
 
56
53
  return { mode, priorities, carryoverRatio, completionRate, rationale };
57
54
  }
55
+
56
+ /** An entry that is exactly an issue reference of THIS repo: `42` or `#42`. */
57
+ const ISSUE_REF_RE = /^#?\d+$/;
58
+
59
+ /**
60
+ * Normalises `top-priorities` to issue numbers. Integers pass; strings that are
61
+ * exactly an issue ref (`"#42"`, `"42"`) become integers; every other entry is
62
+ * dropped. A hand-written `"#370 fix banner"` or `"other-repo#74"` is never
63
+ * mined for digits, because the number may belong to another repo. An array
64
+ * that held entries but keeps none is a type mismatch (`null`), as before.
65
+ *
66
+ * @param {unknown} raw
67
+ * @returns {number[]|null}
68
+ */
69
+ function parsePriorities(raw) {
70
+ if (!Array.isArray(raw)) return null;
71
+ const ids = [];
72
+ for (const entry of raw) {
73
+ if (Number.isInteger(entry)) ids.push(entry);
74
+ else if (typeof entry === 'string' && ISSUE_REF_RE.test(entry.trim())) {
75
+ ids.push(Number.parseInt(entry.trim().replace('#', ''), 10));
76
+ }
77
+ }
78
+ return raw.length > 0 && ids.length === 0 ? null : ids;
79
+ }
@@ -55,6 +55,8 @@ function orderedStateMdCandidates() {
55
55
  * @returns {string} Absolute path (may not exist on disk).
56
56
  */
57
57
  function _resolveStateMdPath(repoRoot) {
58
+ // #1636 decision: no ownership filter here — this guard exists to SEE another
59
+ // session's STATE.md, so a foreign file is its input, not noise to drop.
58
60
  const candidates = orderedStateMdCandidates();
59
61
  for (const candidate of candidates) {
60
62
  const abs = join(repoRoot, candidate);
@@ -19,6 +19,7 @@
19
19
  import { parseStateMd as _parseStateMd } from './state-md/yaml-parser.mjs';
20
20
  import { updateFrontmatterFields as _updateFrontmatterFields } from './state-md/frontmatter-mutators.mjs';
21
21
  import { readLock as _readLock } from './session-lock.mjs';
22
+ import { readProcessLocalSessionIds as _readProcessLocalSessionIds } from './session-identity/own-session.mjs';
22
23
 
23
24
  export { parseStateMd, serializeStateMd } from './state-md/yaml-parser.mjs';
24
25
 
@@ -195,27 +196,49 @@ export function resolveSessionStartedAt({ repoRoot } = {}) {
195
196
  /**
196
197
  * The session ids a STATE.md writer may stamp into frontmatter: `session-id`
197
198
  * (the RAW/native harness id) and `session` (the semantic attribution label),
198
- * both read from the live lock.
199
+ * both read from the live lock — and `session-id` only when the lock is
200
+ * provably THIS process's own (#1663).
199
201
  *
200
- * Both fields are `null` when no usable lock exists — ABSENCE IS NEVER
201
- * COERCED, and a writer must then OMIT the key rather than write a
202
- * placeholder. Neither id grants ownership of anything (see
202
+ * The lock is repo-global: a session that lost the acquire race, or a
203
+ * pre-#1641 lock without `claude_pid` that survives until its TTL, names a
204
+ * PEER. Since #1661 `/close` decides ownership by comparing STATE.md
205
+ * `session-id` against `readProcessLocalSessionIds()`, so stamping the peer's
206
+ * id would make this session's own STATE.md read `foreign` and stop its own
207
+ * close. Hence:
208
+ * - `session_id` is the lock's raw id only when it is contained in
209
+ * `readProcessLocalSessionIds({ env })`; otherwise `null`. A process that
210
+ * cannot name its own id (Cursor, Pi, Codex without a thread id) gets
211
+ * `null` too — unprovable is not own.
212
+ * - `semantic_session_id` is `null` when the lock is PROVABLY foreign (this
213
+ * process has an id and the lock names another). Ceiling (BV-004): when
214
+ * the process has no id at all, the label is still taken from the lock,
215
+ * as before #1663, so the #429 label join keeps working on those
216
+ * harnesses — it may then name a peer. Revisit when every harness
217
+ * exposes a native id.
218
+ *
219
+ * ABSENCE IS NEVER COERCED: a writer must OMIT a `null` key rather than write
220
+ * a placeholder. Neither id grants ownership of anything (see
203
221
  * `skills/_shared/state-ownership.md`); `session-id` exists so `/close`'s #429
204
222
  * pre-check can join STATE.md to sessions.jsonl on a native identity instead
205
223
  * of falling through to the label + timestamp path.
206
224
  *
207
225
  * Never throws.
208
226
  *
209
- * @param {{ repoRoot?: string }} [opts]
227
+ * @param {{ repoRoot?: string, env?: object }} [opts] `env` defaults to
228
+ * `process.env`; injectable for tests.
210
229
  * @returns {{ session_id: string|null, semantic_session_id: string|null }}
211
230
  */
212
- export function resolveSessionIds({ repoRoot } = {}) {
231
+ export function resolveSessionIds({ repoRoot, env = process.env } = {}) {
213
232
  const pick = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : null);
214
233
  try {
215
234
  const lock = _readLock({ repoRoot });
235
+ const lockId = pick(lock?.session_id);
236
+ const ownIds = _readProcessLocalSessionIds({ env });
237
+ const own = lockId !== null && ownIds.includes(lockId);
238
+ const provablyForeign = lockId !== null && ownIds.length > 0 && !own;
216
239
  return {
217
- session_id: pick(lock?.session_id),
218
- semantic_session_id: pick(lock?.semantic_session_id),
240
+ session_id: own ? lockId : null,
241
+ semantic_session_id: provablyForeign ? null : pick(lock?.semantic_session_id),
219
242
  };
220
243
  } catch {
221
244
  return { session_id: null, semantic_session_id: null };
@@ -42,6 +42,7 @@ import path from 'node:path';
42
42
  import { normalizeSkillInvocation } from '../skill-invocations-schema.mjs';
43
43
  import { normalizeSkillJudgment } from '../skill-judgments-schema.mjs';
44
44
  import { isMainModule } from '../is-main-module.mjs';
45
+ import { resolveLedgerPath } from '../ledger-path.mjs';
45
46
  import { userInvocableSkills } from '../user-invocable-skills.mjs';
46
47
 
47
48
  // ---------------------------------------------------------------------------
@@ -948,15 +949,15 @@ export function runSunsetWalk(repoRoot, opts = {}) {
948
949
 
949
950
  const subagentsPath =
950
951
  opts.subagentsPath ??
951
- path.join(repoRoot, '.orchestrator', 'metrics', 'subagents.jsonl');
952
+ resolveLedgerPath('.orchestrator/metrics/subagents.jsonl', repoRoot);
952
953
 
953
954
  const invocationsPath =
954
955
  opts.invocationsPath ??
955
- path.join(repoRoot, '.orchestrator', 'metrics', 'skill-invocations.jsonl');
956
+ resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl', repoRoot);
956
957
 
957
958
  const judgmentsPath =
958
959
  opts.judgmentsPath ??
959
- path.join(repoRoot, '.orchestrator', 'metrics', 'skill-judgments.jsonl');
960
+ resolveLedgerPath('.orchestrator/metrics/skill-judgments.jsonl', repoRoot);
960
961
 
961
962
  const surface = enumerateSurface(repoRoot);
962
963
  const dispatch = readDispatchCounts(subagentsPath, { windowDays, now: nowMs });
@@ -159,11 +159,13 @@ export function readTelemetryState({ path } = {}) {
159
159
  export function writeTelemetryState(record, { path } = {}) {
160
160
  const target = path || TELEMETRY_JSON_PATH;
161
161
  try {
162
- mkdirSync(dirname(target), { recursive: true });
162
+ // 0o700: the default target is the private config dir, which must not come
163
+ // out 0777 under umask 000 (#1629).
164
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
163
165
  } catch (err) {
164
166
  return { ok: false, error: err?.message ?? String(err) };
165
167
  }
166
- const res = writeJsonAtomicSync(target, record);
168
+ const res = writeJsonAtomicSync(target, record, { mode: 0o600 }); // consent + anon_id are host-private (#1632)
167
169
  return res.ok ? { ok: true } : { ok: false, error: res.error };
168
170
  }
169
171
 
@@ -91,7 +91,7 @@ function _serializedByteLength(entries) {
91
91
  * via tmp-file + `renameSync` in the SAME directory (same-filesystem rename
92
92
  * is atomic on POSIX — mirrors `writeJsonAtomicSync` in `scripts/lib/io.mjs`,
93
93
  * which has no NDJSON-array variant). Creates the parent directory with
94
- * `mkdirSync(dir, { recursive: true })` first.
94
+ * `mkdirSync(dir, { recursive: true, mode: 0o700 })` first.
95
95
  *
96
96
  * @param {string} filePath
97
97
  * @param {Array<object>} entries
@@ -101,10 +101,10 @@ function _serializedByteLength(entries) {
101
101
  */
102
102
  function _writeEntriesAtomicSync(filePath, entries) {
103
103
  const dir = dirname(filePath);
104
- mkdirSync(dir, { recursive: true });
104
+ mkdirSync(dir, { recursive: true, mode: 0o700 }); // private config dir; 0777 under umask 000 otherwise (#1629)
105
105
  const tmpSuffix = randomBytes(6).toString('hex');
106
106
  const tmpFile = join(dir, `.tmp.${tmpSuffix}`);
107
- writeFileSync(tmpFile, _toNdjson(entries), 'utf8');
107
+ writeFileSync(tmpFile, _toNdjson(entries), { encoding: 'utf8', mode: 0o600 }); // rename keeps this mode (#1632)
108
108
  renameSync(tmpFile, filePath);
109
109
  }
110
110
 
@@ -23,9 +23,10 @@
23
23
  * queue (bounded, oldest-dropped) and the session closes with zero user-facing
24
24
  * error.
25
25
  *
26
- * Node ESM. The only network dependency is the global `fetch`.
26
+ * Node ESM. Networking uses the Stdlib HTTP helper.
27
27
  */
28
28
 
29
+ import { httpRequest } from '../http-request.mjs';
29
30
  import path from 'node:path';
30
31
  import os from 'node:os';
31
32
  import fs from 'node:fs';
@@ -43,10 +44,11 @@ import { loadOwnerConfig } from '../owner-yaml.mjs';
43
44
  import { readJsonlFile } from '../io.mjs';
44
45
  import { readEventsWithRotations } from '../events.mjs';
45
46
  import { resolveEventsLedgerRoot } from '../platform.mjs';
47
+ import { resolveLedgerPath } from '../ledger-path.mjs';
46
48
  import { readCanonicalSessions } from '../sessions-canonical.mjs';
47
49
  import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
48
50
  import { readSessionProfile } from '../state-md.mjs';
49
- import { resolveStateMdPath } from '../state-md/frontmatter-mutators.mjs';
51
+ import { resolveStateMdWriteTarget } from '../state-md/frontmatter-mutators.mjs';
50
52
 
51
53
  // ---------------------------------------------------------------------------
52
54
  // Constants
@@ -73,7 +75,7 @@ function resolveEndpoint(env) {
73
75
 
74
76
  /**
75
77
  * The default network sender: one POST carrying the whole batch array as the
76
- * JSON body (the server accepts arrays), with an AbortSignal timeout. Resolves
78
+ * JSON body (the server accepts arrays), with a wall-clock timeout. Resolves
77
79
  * on a 2xx status, rejects on anything else (which routes the caller into the
78
80
  * offline queue).
79
81
  *
@@ -85,11 +87,12 @@ function resolveEndpoint(env) {
85
87
  function defaultSender({ env, timeoutMs }) {
86
88
  const endpoint = resolveEndpoint(env);
87
89
  return async (batches) => {
88
- const res = await fetch(endpoint, {
90
+ const res = await httpRequest(endpoint, {
89
91
  method: 'POST',
90
92
  headers: { 'Content-Type': 'application/json' },
91
93
  body: JSON.stringify(batches),
92
- signal: AbortSignal.timeout(timeoutMs),
94
+ timeoutMs,
95
+ maxBytes: 64 * 1024,
93
96
  });
94
97
  if (!res.ok) {
95
98
  // The status travels ON the error: `flush` needs it to tell a TRANSPORT
@@ -307,7 +310,15 @@ export function readSessionProfileForMetricsDir(metricsDir) {
307
310
  // metricsDir is `<repoRoot>/.orchestrator/metrics` by construction (every
308
311
  // caller builds it that way); two levels up is the repo root.
309
312
  const repoRoot = path.resolve(metricsDir, '..', '..');
310
- return readSessionProfile(fs.readFileSync(resolveStateMdPath(repoRoot), 'utf8'));
313
+ // #1636: the write side's ownership rule — another platform's STATE.md not
314
+ // proven to be this process's is no profile source (ABSENT IS NOT EMPTY:
315
+ // null ⇒ the ping omits the field). Deliberately NO warnForeignStateMd line:
316
+ // this runs inside flush, which the hooks call in-process at SessionEnd or
317
+ // spawn detached with `stdio: 'ignore'` — the line would be hook noise or
318
+ // lost, and an omitted optional field needs no operator action.
319
+ const { path: stateMdPath, rejected } = resolveStateMdWriteTarget(repoRoot);
320
+ if (rejected) return null;
321
+ return readSessionProfile(fs.readFileSync(stateMdPath, 'utf8'));
311
322
  } catch {
312
323
  return null;
313
324
  }
@@ -494,6 +505,23 @@ export function deriveSessionFromEvents(metricsDir) {
494
505
  }
495
506
  }
496
507
 
508
+ /**
509
+ * `skill-invocations.jsonl` for the metrics dir `metricsDir` (#1722): a
510
+ * `<root>/.orchestrator/metrics` dir resolves through {@link resolveLedgerPath}
511
+ * for `<root>` — the main checkout's ledger when `<root>` is a linked worktree
512
+ * that ignores it, else the dir's own — and any other dir is read as given.
513
+ *
514
+ * @param {string} metricsDir
515
+ * @returns {string}
516
+ */
517
+ function skillInvocationsPathFor(metricsDir) {
518
+ const parent = path.dirname(metricsDir);
519
+ if (path.basename(metricsDir) === 'metrics' && path.basename(parent) === '.orchestrator') {
520
+ return resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl', path.dirname(parent));
521
+ }
522
+ return path.join(metricsDir, 'skill-invocations.jsonl');
523
+ }
524
+
497
525
  /**
498
526
  * Build ONE whitelist-projected usage-ping record from the local JSONL streams.
499
527
  *
@@ -537,11 +565,19 @@ export function buildBatch({
537
565
  consentState,
538
566
  } = {}) {
539
567
  try {
568
+ // `dir` stays the caller's metrics dir: STATE.md and the events fallback
569
+ // below derive their own roots from it.
570
+ // ledger-root: the caller's dir as given; each ledger in it resolves below
540
571
  const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
541
572
  const nowIso = now || new Date().toISOString();
542
573
 
543
- const sessions = readCanonicalSessions({ filePath: path.join(dir, 'sessions.jsonl') });
544
- const invocations = readJsonlFile(path.join(dir, 'skill-invocations.jsonl'), { skipInvalid: true });
574
+ // #1722: each ledger resolves on its own, an explicit metricsDir included
575
+ // (the SessionEnd flush hands over the LAUNCH dir's, which is a linked
576
+ // worktree for a session started there; deriveSessionFromEvents maps the
577
+ // events fallback the same way) — sessions via the one-ledger root
578
+ // (#1514/#1584), skill-invocations via skillInvocationsPathFor().
579
+ const sessions = readCanonicalSessions({ filePath: path.join(resolveEventsLedgerRoot(dir), 'sessions.jsonl') });
580
+ const invocations = readJsonlFile(skillInvocationsPathFor(dir), { skipInvalid: true });
545
581
 
546
582
  const sessionRecord = mostRecentSession(sessions);
547
583
 
@@ -855,7 +891,7 @@ export function shouldDailyFlush({ statePath, queuePath, metricsDir, now = Date.
855
891
  // (271 records / 292 KB in this repo, 2026-08-23) that is sub-millisecond
856
892
  // and it only runs once the 24h gate above has already passed. Revisit
857
893
  // with a tail-read if any repo's sessions.jsonl passes ~10 MB.
858
- const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
894
+ const dir = metricsDir || path.join(resolveEventsLedgerRoot(process.cwd()), '.orchestrator', 'metrics');
859
895
  const sessions = readCanonicalSessions({ filePath: path.join(dir, 'sessions.jsonl') });
860
896
  const last = mostRecentSession(sessions);
861
897
  const completedMs = Date.parse(last?.completed_at);
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import path from 'node:path';
20
+ import { resolveLedgerPath } from '../ledger-path.mjs';
20
21
 
21
22
  const ROLLUP_REL = '.orchestrator/metrics/test-runs.jsonl';
22
23
  const TEST_RUNS_REL = '.orchestrator/metrics/test-runs';
@@ -62,6 +63,7 @@ export function makeRunId() {
62
63
  * @throws {TypeError} on an invalid runId (see {@link assertRunId})
63
64
  */
64
65
  export function runDirPath(runId) {
66
+ // ledger-root: per-run artifact dir of this working tree, kept local
65
67
  return path.join(TEST_RUNS_REL, assertRunId(runId));
66
68
  }
67
69
 
@@ -112,8 +114,14 @@ export function consoleLogPath(runId) {
112
114
 
113
115
  /**
114
116
  * Path to the shared JSONL rollup file (across all runs).
117
+ *
118
+ * Without `repoRoot` it is the repo-relative constant. With `repoRoot` it is
119
+ * the absolute ledger path, routed to the main checkout when `repoRoot` is a
120
+ * linked worktree of a repo that ignores the rollup (#1722).
121
+ *
122
+ * @param {string} [repoRoot]
115
123
  * @returns {string}
116
124
  */
117
- export function jsonlRollupPath() {
118
- return ROLLUP_REL;
125
+ export function jsonlRollupPath(repoRoot) {
126
+ return typeof repoRoot === 'string' && repoRoot !== '' ? resolveLedgerPath(ROLLUP_REL, repoRoot) : ROLLUP_REL;
119
127
  }