session-orchestrator 5.12.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 (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  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 +162 -4
  11. package/README.md +126 -127
  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 +1 -1
  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 +90 -17
  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 +49 -15
  47. package/docs/session-config-template.md +30 -22
  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 +139 -49
  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 -2
  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 +39 -27
  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/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -33,6 +33,11 @@
33
33
  // through, so this guard is the only hard PSA-004 stop left for a
34
34
  // lint-staged sweep — softening it would weaken detection for the very
35
35
  // class it exists to catch.
36
+ // - A path outside allowedPaths that an EARLIER wave of this session
37
+ // declared (`filescopes/wave-<k>.scopes.json`, k < the manifest's wave,
38
+ // mtime not older than the session start) passes with an info line
39
+ // (#1680): the coordinator commits after the last wave, when the active
40
+ // manifest names only that wave's paths.
36
41
  // - EMPTY allowedPaths is no longer a silent pass: a Discovery (read-only)
37
42
  // manifest means no agent is committing, i.e. a coordinator commit between
38
43
  // waves → allow with a note; a manifest older than the newest session
@@ -49,7 +54,7 @@
49
54
 
50
55
  import { execSync } from 'node:child_process';
51
56
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
52
- import { isAbsolute, join, relative, sep } from 'node:path';
57
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path';
53
58
 
54
59
  // Re-use existing helpers from scripts/lib/hardening.mjs.
55
60
  // IMPORTANT: import is resolved relative to THIS file's location (the hook
@@ -59,12 +64,14 @@ import { isAbsolute, join, relative, sep } from 'node:path';
59
64
  import { pathMatchesPattern } from '../scripts/lib/hardening.mjs';
60
65
  import { withStagingFenceLock } from '../scripts/lib/session-lock.mjs';
61
66
  import { isMainModule } from '../scripts/lib/is-main-module.mjs';
62
- import { resolveSessionRoot } from '../scripts/lib/platform.mjs';
63
- import { classifyEmptyScope, findOwnScopeFile, sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
67
+ import { resolveEventsLedgerRoot, resolveSessionRoot } from '../scripts/lib/platform.mjs';
68
+ import { classifyEmptyScope, findOwnScopeFile, sessionAgeMs, unionFileScopes } from '../scripts/lib/scope-gate.mjs';
69
+ import { readTailWindow } from '../scripts/lib/tail-window.mjs';
64
70
  import {
65
71
  classifyManifestSession,
66
72
  readProcessLocalSessionIds,
67
73
  } from '../scripts/lib/session-identity/own-session.mjs';
74
+ import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
68
75
 
69
76
  // Resolve the "current" agent id from SO_WAVE_AGENT_ID (when set by the
70
77
  // caller) or fall back to a PID-derived marker. The fence files themselves
@@ -157,7 +164,8 @@ function findOverlaps(fenceJsonPath, ourStaged) {
157
164
  /** Remediation for an allowedPaths violation, printed once per commit. */
158
165
  const ALLOWED_PATHS_HINT = [
159
166
  '',
160
- 'These files were likely added by lint-staged eslint --fix / prettier --write.',
167
+ 'Each path is either from a previous session or wave that no wave scope of this',
168
+ 'session declares, or a file lint-staged (eslint --fix / prettier --write) re-staged.',
161
169
  'To proceed:',
162
170
  ' 1) git restore --staged <path> # for each foreign path',
163
171
  ' 2) git commit # retry',
@@ -186,6 +194,110 @@ function mtimeMsOf(file) {
186
194
  }
187
195
  }
188
196
 
197
+ /** Emitted by `hooks/_lib/lock-bootstrap.mjs` on every session-lock acquire. */
198
+ const LOCK_ACQUIRED_EVENT = 'orchestrator.session.lock.acquired';
199
+
200
+ /**
201
+ * Tail window of events.jsonl searched for the genesis anchor (1 MiB, the same
202
+ * window `session-id.mjs` reads). CEILING (BV-004): a session whose first
203
+ * acquire scrolled out of it gets its re-acquires only, i.e. a later start —
204
+ * fewer earlier waves count, the guard blocks (safe direction). Revisit when a
205
+ * compacted session reports a block on its own earlier-wave path.
206
+ */
207
+ const GENESIS_TAIL_BYTES = 1024 * 1024;
208
+
209
+ /**
210
+ * Epoch ms of the FIRST `orchestrator.session.lock.acquired` event carrying one
211
+ * of this process's raw session ids, or `null` (#1681). A compact re-acquires
212
+ * the lock under the SAME raw id and rewrites both session clocks
213
+ * (`.claude/rules/identity-and-locks.md`); the first acquire is the start that
214
+ * survives it. Same anchor as `ownGenesisMs()` in
215
+ * `scripts/lib/sessions-staleness-banner.mjs`. Raw ids only, never the semantic
216
+ * label (two sessions can mint the same one, #1066). Never throws: a missing,
217
+ * unreadable or non-regular ledger yields `null`.
218
+ *
219
+ * @param {string} lockRoot root holding this session's lock
220
+ * @param {Set<string>} ownIds this process's raw session ids
221
+ * @returns {number|null}
222
+ */
223
+ function ownGenesisMs(lockRoot, ownIds) {
224
+ if (ownIds.size === 0) return null;
225
+ let window;
226
+ try {
227
+ const ledger = join(resolveEventsLedgerRoot(lockRoot), '.orchestrator', 'metrics', 'events.jsonl');
228
+ window = readTailWindow(ledger, GENESIS_TAIL_BYTES, { noFollow: true });
229
+ } catch {
230
+ return null;
231
+ }
232
+ const lines = window.text.split('\n');
233
+ if (window.cut) lines.shift(); // the window began mid-record
234
+ let genesis = null;
235
+ for (const line of lines) {
236
+ if (!line.includes(LOCK_ACQUIRED_EVENT)) continue;
237
+ let record;
238
+ try {
239
+ record = JSON.parse(line);
240
+ } catch {
241
+ continue;
242
+ }
243
+ if (record?.event !== LOCK_ACQUIRED_EVENT || !ownIds.has(record.session_id)) continue;
244
+ const ms = Date.parse(record.timestamp);
245
+ if (Number.isFinite(ms) && (genesis === null || ms < genesis)) genesis = ms;
246
+ }
247
+ return genesis;
248
+ }
249
+
250
+ /**
251
+ * The declared files of every EARLIER wave of this session, read from the
252
+ * per-wave aggregate sidecars `<manifest dir>/filescopes/wave-<k>.scopes.json`
253
+ * (`scripts/materialize-wave-scope.mjs`) for k < the manifest's `wave` (#1680).
254
+ * The coordinator commits after the LAST wave, when the active manifest names
255
+ * only that wave's paths; without this, a file an earlier wave of the same
256
+ * session changed blocked the commit.
257
+ *
258
+ * A sidecar counts only when its mtime is not older than this session's start
259
+ * (`now - sessionAge`); an unknown session clock reads none — fail closed, the
260
+ * pre-#1680 behaviour — so a stale `filescopes/` of an earlier session never
261
+ * widens the scope. Peer records are dropped by `unionFileScopes` (the same
262
+ * exclusion `--union` applies). An unreadable or malformed sidecar is skipped.
263
+ *
264
+ * CEILING (BV-004): consulted for every non-foreign manifest — own AND unknown
265
+ * (unbound, or a shell without a session id) — so a peer session sharing this
266
+ * working copy that materializes sidecars in the same state dir after our
267
+ * session start widens our coverage: the sidecars carry no session binding.
268
+ * Revisit when they gain one.
269
+ *
270
+ * #1681: `sessionAge` follows the NEWEST clock, and a compact rewrites both
271
+ * (`current-session.json.timestamp`, `session.lock.started_at`), so the start
272
+ * is pulled back to `genesisMs` — the first lock acquire of this session's raw
273
+ * id ({@link ownGenesisMs}). Only ever pulled back, never forward; without a
274
+ * genesis the clock alone decides, as before. A `/clear` mints a NEW raw id, so
275
+ * the waves before it stay outside (safe direction: the guard blocks).
276
+ *
277
+ * @param {string} scopePath absolute path of the active manifest
278
+ * @param {unknown} wave the manifest's `wave` field
279
+ * @param {number|null} sessionAge ms since the newest session clock
280
+ * @param {number|null} [genesisMs] epoch ms of this session's first lock acquire
281
+ * @returns {Array<{wave: number, files: string[]}>}
282
+ */
283
+ function earlierWaveScopes(scopePath, wave, sessionAge, genesisMs = null, now = Date.now()) {
284
+ if (typeof sessionAge !== 'number' || !Number.isInteger(wave) || wave < 2) return [];
285
+ const sessionStartMs = typeof genesisMs === 'number' ? Math.min(now - sessionAge, genesisMs) : now - sessionAge;
286
+ const out = [];
287
+ for (let k = 1; k < wave; k++) {
288
+ const sidecar = join(dirname(scopePath), 'filescopes', `wave-${k}.scopes.json`);
289
+ const mtime = mtimeMsOf(sidecar);
290
+ if (mtime === null || mtime < sessionStartMs) continue;
291
+ try {
292
+ const files = unionFileScopes(JSON.parse(readFileSync(sidecar, 'utf8')));
293
+ if (files.length > 0) out.push({ wave: k, files });
294
+ } catch {
295
+ // unreadable / malformed: grants nothing
296
+ }
297
+ }
298
+ return out;
299
+ }
300
+
189
301
  /**
190
302
  * Sub-mode B's decision, pure: which lines to print and whether to block.
191
303
  *
@@ -211,11 +323,14 @@ function mtimeMsOf(file) {
211
323
  * @param {Set<string>} p.ownIds this process's session ids
212
324
  * @param {number|null} p.scopeMtimeMs
213
325
  * @param {number|null} p.sessionAge ms since the newest session clock
326
+ * @param {Array<{wave: number, files: string[]}>} [p.earlierWaves] declared
327
+ * files of this session's earlier waves (`earlierWaveScopes`, #1680); a
328
+ * violating path one of them covers passes with an info line
214
329
  * @returns {{action: 'pass'|'report'|'block', lines: string[], hint?: string[]}}
215
330
  * `hint` = the remediation block; `main()` prints it ONCE after every
216
331
  * manifest was judged, so two blocking manifests do not repeat it (#1514.3).
217
332
  */
218
- function scopeCommitVerdict({ scope, stagedFiles, scopePathOf = (f) => f, scopeRel, ownIds, scopeMtimeMs, sessionAge }) {
333
+ function scopeCommitVerdict({ scope, stagedFiles, scopePathOf = (f) => f, scopeRel, ownIds, scopeMtimeMs, sessionAge, earlierWaves = [] }) {
219
334
  if (stagedFiles.length === 0) return { action: 'pass', lines: [] };
220
335
 
221
336
  // Same classifier and same disposition as enforce-scope Gate 3b: only a
@@ -232,18 +347,25 @@ function scopeCommitVerdict({ scope, stagedFiles, scopePathOf = (f) => f, scopeR
232
347
 
233
348
  const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
234
349
  if (allowedPaths.length > 0) {
235
- const violations = stagedFiles.filter(
236
- (f) => {
237
- const scoped = scopePathOf(f);
238
- return scoped === null || !allowedPaths.some((pattern) => pathMatchesPattern(scoped, pattern));
239
- },
240
- );
241
- if (violations.length === 0) return { action: 'pass', lines: [] };
350
+ const violations = [];
351
+ const notes = [];
352
+ for (const f of stagedFiles) {
353
+ const scoped = scopePathOf(f);
354
+ if (scoped !== null && allowedPaths.some((pattern) => pathMatchesPattern(scoped, pattern))) continue;
355
+ // #1680: declared files only — no test-sibling expansion, no widening.
356
+ const earlier = scoped === null
357
+ ? undefined
358
+ : earlierWaves.find((w) => w.files.some((pattern) => pathMatchesPattern(scoped, pattern)));
359
+ if (earlier) notes.push(`wave-scope-commit-guard: ${f} belongs to wave ${earlier.wave} of this session — allowed.`);
360
+ else violations.push(f);
361
+ }
362
+ if (violations.length === 0) return { action: 'pass', lines: notes };
242
363
  // Blocks under `warn` too — see the header: in a warn-mode wave this is
243
364
  // the only hard stop a lint-staged sweep meets before the commit exists.
244
365
  return {
245
366
  action: 'block',
246
367
  lines: [
368
+ ...notes,
247
369
  `✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths (${scopeRel}):`,
248
370
  ...violations.map((v) => ` - ${v}`),
249
371
  ],
@@ -398,14 +520,21 @@ async function main() {
398
520
  process.exit(1);
399
521
  }
400
522
 
523
+ const manifest = scope !== null && typeof scope === 'object' ? scope : {};
524
+ // #1634: read the clock beside this session's own lock (its
525
+ // `session_root` after ExitWorktree), not at the commit's root.
526
+ const lockRoot = resolveOwnLockRoot({ session_id: [...ownIds][0] ?? null, cwd: root }, root);
527
+ const sessionAge = sessionAgeMs(lockRoot);
401
528
  const verdict = scopeCommitVerdict({
402
- scope: scope !== null && typeof scope === 'object' ? scope : {},
529
+ scope: manifest,
403
530
  stagedFiles,
404
531
  scopePathOf,
405
532
  scopeRel,
406
533
  ownIds,
407
534
  scopeMtimeMs: mtimeMsOf(scopePath),
408
- sessionAge: sessionAgeMs(root),
535
+ sessionAge,
536
+ // Consulted for any non-foreign (own or unknown) manifest: the verdict returns on a foreign one first.
537
+ earlierWaves: earlierWaveScopes(scopePath, manifest.wave, sessionAge, ownGenesisMs(lockRoot, ownIds)),
409
538
  });
410
539
  for (const line of verdict.lines) process.stderr.write(`${line}\n`);
411
540
  if (verdict.hint) hint = verdict.hint;
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "so-hud",
3
+ "version": "0.2.0",
4
+ "author": { "name": "session-orchestrator contributors" },
5
+ "description": "See Session Orchestrator status, warnings and session beacons in Claude Code ($.so contract v1), with protection gates for secret-printing commands, rule traps, outward actions and scope collisions.",
6
+ "types": "./types/index.d.ts",
7
+ "userConfig": {
8
+ "strictness": { "title": "Strictness profile", "type": "string", "description": "Strictness profile shown in the status line and returned by $.so.profile(). locker: rule traps and scope collisions only warn. ausgewogen (default): rule traps are refused and a scope collision asks first. streng: rule traps and scope collisions are refused. On every profile, secret-printing commands are refused and outward actions ask twice in an interactive session.", "options": ["locker", "ausgewogen", "streng"], "default": "ausgewogen" },
9
+ "soRoot": { "title": "Session Orchestrator path", "type": "string", "description": "Optional absolute path to the Session Orchestrator installation", "required": false }
10
+ }
11
+ }
@@ -0,0 +1,57 @@
1
+ # so-hud 0.2.0
2
+
3
+ A native Session Orchestrator display for Claude Code (package A, #1508): a status line for the running session, a warning band when something needs attention, and one beacon file per session that other sessions can read through `$.so.beacons()`. The status line shows how many peer sessions are running (from the session registry). Since 0.2.0 it also checks tool calls before they run (#1551, see [Protection gates](#protection-gates)).
4
+
5
+ ## Requirements
6
+
7
+ - Claude Code with mod support; tested with **2.1.296**. Claude Code only — Codex, Cursor and Pi do not load this mod.
8
+ - **Node.js 24** or later.
9
+ - **Session Orchestrator** from the release that ships this mod or later, as the plugin or as a checkout set through `soRoot`: the mod reads all state through its `scripts/so-hud.mjs`, which earlier releases do not have.
10
+
11
+ ## Install
12
+
13
+ ```text
14
+ /plugin marketplace add Kanevry/session-orchestrator
15
+ /plugin install so-hud@kanevry
16
+ ```
17
+
18
+ The mod finds the Session Orchestrator installation in this order: the `soRoot` setting (an absolute path), then the path cached from an earlier lookup, then a root registered with `node scripts/so-hud.mjs --register-root` inside that installation, then `claude plugin list --json`. The first hit is cached, so after a move only `soRoot` replaces it. None of these install actions is run by the development tests. For a local checkout you can also load this directory directly with `claude --plugin-dir ./mods/so-hud`.
19
+
20
+ Settings: `strictness` is `locker` (relaxed), `ausgewogen` (balanced, the default) or `streng` (strict); it is shown in the status line, returned by `$.so.profile()` and decides how the protection gates react. Until one is chosen, a band above the prompt shows the current profile with one button per profile (no hotkeys: a digit typed into an empty prompt would press one); a press saves it in this mod's `strictness` row of `/config`, used by every new session. `/profile` shows the profile and its effect without a model turn and opens the band, with `Schließen` to hide it for the session (#1552). The profile steers only these gates, not Session Orchestrator's own guards. `soRoot` is optional.
21
+
22
+ **Upgrading from 0.1.0:** 0.1.0 only displayed state; 0.2.0 adds the [protection gates](#protection-gates), so outward actions such as push, merge, release and publish ask for two confirmations within 30 seconds, and commands that would print a secret are refused. `/plugin install` uses scope `user` by default, and an update at that scope changes every Claude Code session on the host at once, including lead sessions that merge through the API on the owner's word. Set `strictness` per host first (0.1.0 already has the row in `/config`), try 0.2.0 in one session (`claude --plugin-dir ./mods/so-hud`, or an install at project scope), and only then update the user-scope install: [docs/mods.md § Upgrading from 0.1.0 with scope user](../../docs/mods.md#upgrading-from-010-with-scope-user).
23
+
24
+ ## What it does
25
+
26
+ Session Orchestrator state comes only from `scripts/so-hud.mjs`, called as `--json --repo-root <cwd> --session-id <raw-uuid>` every 15 seconds with a 3-second timeout. With `--session-id` the collector counts the session's agents beside its own `session.lock`, which can sit in another checkout than the cwd (#1730). A collector older than #1730 rejects the flag with exit 2; the mod then calls it without the flag for the rest of the session. STATE.md, locks, dispatch and events are never changed. The mod writes only its own beacon file, its install-path cache, the receipt lines in `aktionen.jsonl` (see below), and, when a profile is picked in the band, its own `strictness` row in `/config` and the store key `profile-chosen`. Without a Session Orchestrator root it asks `claude plugin list` at most once every 5 minutes.
27
+
28
+ The status line says `nicht eingerichtet` for a part that is not set up (no Session Orchestrator installation found, no `vault-integration` block) and `noch kein Lauf` for an enabled vault mirror that never ran; a repository where no session has written events yet is not shown as `teilweise gemessen`.
29
+
30
+ `$.so.version()` is 1; `snapshot()` and `beacons()` are asynchronous engine calls (`await`); `profile()` returns `locker`, `ausgewogen` or `streng`. A cold snapshot is `null`; an old one keeps its measurement time and carries `age_ms`; after a failed refresh it is kept and marked `degraded`.
31
+
32
+ **Language:** the status texts and the texts of the protection gates are German. An English text table is prepared but not selectable yet, because JSON v1 has no field for the owner's language.
33
+
34
+ Beacons: the `$.fs` API of this Claude Code build offers no rename, chmod or unlink, so `hooks/beacon-io.mjs` is called through `$.process.run` with argv and JSON on stdin: temporary file plus rename, mode 0600, beacon directory 0700, at most 4 KB. `hud/` and `hud/beacons/` must be real directories owned by the user, not symlinks, checked through an `O_NOFOLLOW` handle; where Node has no `O_NOFOLLOW` (native Windows) an `lstat` check stands in, and a link swapped in between check and `chmod` would be followed. Removing a beacon follows no symlink. The helper's parent PID stands for the starting engine process; that mapping still has to be confirmed in a real engine host. Foreign beacons are validated; one older than 120 seconds whose PID cannot be shown to be alive becomes state unknown. `tool.check: ask` means handing the decision on (also in auto mode), not proof that a dialog was shown.
35
+
36
+ ## Protection gates
37
+
38
+ Before a shell command (`Bash`, `Monitor`, `PowerShell`) or a file edit runs, so-hud checks it, and it checks tool results before they reach the model (#1551). This is a behaviour change against 0.1.0: outward actions now ask twice by default, secret-printing commands are refused, and on the default profile `ausgewogen` rule traps such as `git add -A` are refused too.
39
+
40
+ - **SEC-008 traps** (commands that would print a secret, such as `cat .env`, a bare `env`, `echo $TOKEN`, `docker inspect` without `--format`, a token in curl arguments or in a curl URL) are refused on every profile; the refusal names the safe replacement command.
41
+ - **Rule traps** (`git add .`/`-A`/`--all`, `claude -p` without `env -u ANTHROPIC_API_KEY`, `${PIPESTATUS[`, `grep -c … || echo 0`) show a toast on `locker` and are refused on `ausgewogen` and `streng`.
42
+ - **Outward actions** (push to `main`/`master`, force, tags or delete; merge; release; publish; production deploy) are held wherever a surface is attached (terminal, desktop, IDE, phone) until the owner confirms twice within 30 seconds; declining, closing the dialog or letting the time run out refuses the call. Only a plain push of a feature branch runs without a question (#1569): `git push [-u] origin feat/x`, `HEAD` or a bare `git push`; exact options; every earlier command on the line a local git call from an allowlist (`fetch` with no option and no `::` word, `log`/`show`/`diff` with display options only — `--output`, `--ext-diff`, `--textconv`, `--upload-pack` ask), so any earlier command that is not git (`npm test && git push origin feat/x`), and any earlier output redirection other than onto exactly `/dev/null` or between stdout and stderr (`git log … >> .git/config`, also on `cd`, an assignment or `exec`), makes every push form ask; after `add`, `commit`, `status`, `diff`, `fetch` or `checkout -- <paths>` on the line, only when so-hud measured that none of the hooks those steps can fire exists (a hook file of that name, in husky v9 the user's `.husky/<name>`, or a `hook.*` config entry for that event; see docs/mods.md § Limits) and the step ran in the push's own folder as written (`git -C ../b commit && git push …` asks; in a husky repository with a `.husky/pre-commit`, `git commit … && git push …` on one line asks); behind any git call, only when that call ran in the push's own folder as written and every key of the repository's own git config is on an allowlist (`core.fsmonitor`, `diff.external`, filters, textconv, `core.sshCommand`, `credential.helper`, `include.path` and any other key ask, and so does any submodule or embedded repository in the index, #1573); no earlier step that changes the branch, git config or the folder; harmless variable names only; a remote defined in git config; and the standard push configuration (`push.*`, `remote.<r>.push`, `remote.<r>.mirror`, push remotes and `submodule.recurse` are read). Lowercase `head`, `--no-verify` and every other shape ask. A run with no surface at all (a plain `claude -p`) cannot ask: so-hud writes a receipt and lets the call run, and there merges, releases, publishes and deploys are checked by no Session Orchestrator gate.
43
+ - **Collision guard:** an edit of a file in the declared wave scope of another live Session Orchestrator session in the same working copy gives a toast on `locker`, a question on `ausgewogen` and a refusal on `streng`. Sessions that do not run Session Orchestrator are invisible to it.
44
+ - **Secret shield:** a token in a tool result reaches the model as `[<kind> entfernt · sha256:<12 hex>]`, with a toast to rotate it.
45
+
46
+ Every decision on an outward action appends one receipt line per held action, and every asked collision one line, to `~/.config/session-orchestrator/hud/aktionen.jsonl` (0600, append-only, a regular file with one link); it carries the SHA-256 of `JSON.stringify([tool, command])` — `[tool, file path]` for a collision — never the command. If the gate itself fails, the call is refused, except on `locker` for calls that are neither outward nor SEC-008. The gates are a hurdle, not the authority: pre-push hook, destructive-command guard and CI stay authoritative where they exist. Profiles, receipt fields, the full list of checked commands and the limits: [docs/mods.md](../../docs/mods.md).
47
+
48
+ ## Limits
49
+
50
+ - A status without a session or wave binding is not attributed to the current wave.
51
+ - Dispatch records do not always carry agent type or model; those fields stay null.
52
+ - Peer scope is unknown when either scope source is missing or the scope signals have gaps.
53
+ - Events are read only from the last 512 KB, reported as `events:tail-only`. There is no complete history.
54
+
55
+ ## Checks
56
+
57
+ `npx vitest --run tests/so-hud` in the Session Orchestrator repo (CI job `test`). The CI job `mods:plugin` runs `claude plugin validate --strict --json` and `claude plugin test` for every mod. Engine integration separately with `claude plugin test mods/so-hud`; under Claude Code 2.1.296 the kit tests pass. `tsconfig.json` follows the header of the type file; the engine creates `.claude-plugin/types` when it loads the mod. No rollout, no commands from later packages and no second Session Orchestrator state.
@@ -0,0 +1,101 @@
1
+ // Host-only helper: $.fs in build 2.1.296 has no append, rename, chmod or unlink.
2
+ import fs from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+ // Closed receipt shape: no free-text field, so a command (which can carry a token) never reaches the file —
8
+ // argv_sha256 is its only reference. Allowlist via Set, never `in` (that would admit `toString` and friends).
9
+ const RECEIPT_KEYS = new Set(['zeit', 'session_id', 'vorgang', 'aktion', 'argv_sha256', 'exit', 'profil', 'ergebnis', 'art']);
10
+ const RECEIPT_ENUMS = { vorgang: ['pane-aktion'], aktion: ['aussenwirkung', 'kollision'], profil: ['locker', 'ausgewogen', 'streng'], ergebnis: ['ausgefuehrt', 'abgelehnt', 'verfallen', 'ohne-zeichenflaeche', 'fehler'] };
11
+ const validReceipt = (r, id) => r !== null && typeof r === 'object' && !Array.isArray(r)
12
+ && Object.keys(r).every(k => RECEIPT_KEYS.has(k)) && r.session_id === id
13
+ && Object.entries(RECEIPT_ENUMS).every(([k, allowed]) => allowed.includes(r[k]))
14
+ && typeof r.zeit === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(r.zeit) && Number.isFinite(Date.parse(r.zeit))
15
+ && typeof r.argv_sha256 === 'string' && /^[0-9a-f]{64}$/.test(r.argv_sha256)
16
+ && (r.exit === null || Number.isSafeInteger(r.exit))
17
+ && (r.art === undefined || (typeof r.art === 'string' && /^[a-z][a-z0-9-]{0,39}$/.test(r.art)));
18
+ // A planted symlink (or a foreign-owned directory) at hud/ or hud/beacons/ would otherwise receive our chmod and our
19
+ // writes. `uid` is undefined on native Windows (no process.getuid), where only the symlink/directory check applies.
20
+ const ownDir = (st, uid) => {
21
+ if (st.isSymbolicLink() || !st.isDirectory() || (uid !== undefined && st.uid !== uid)) throw new Error('not a private directory');
22
+ };
23
+ async function privateDir(dir, { noFollow, uid }) {
24
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
25
+ if (noFollow) {
26
+ // The handle pins the checked inode: a symlink at `dir` fails this open, so check and chmod hit one directory.
27
+ const fh = await fs.open(dir, constants.O_RDONLY | constants.O_DIRECTORY | noFollow);
28
+ try { ownDir(await fh.stat(), uid); await fh.chmod(0o700); } finally { await fh.close(); }
29
+ return;
30
+ }
31
+ // No O_NOFOLLOW (native Windows): lstat, then chmod by path. Ceiling: a link swapped in between the two is followed;
32
+ // creating one needs admin or Developer Mode there. Revisit if Node exposes O_NOFOLLOW on Windows.
33
+ ownDir(await fs.lstat(dir), uid);
34
+ await fs.chmod(dir, 0o700);
35
+ }
36
+ // Ceiling for every path below a checked directory: Node has no openat(), so a process that can rename entries in
37
+ // ~/.config/session-orchestrator could swap hud/ between the check and the file open. That is the user's own config
38
+ // directory today; revisit if a shared or world-writable parent is ever supported.
39
+ export async function beaconIO(action, id, input, { home = os.homedir(), pid = process.ppid, noFollow = constants.O_NOFOLLOW, uid = process.getuid?.() } = {}) {
40
+ if (action === 'identity') return { pid };
41
+ if (action === 'alive') {
42
+ const alive = [];
43
+ for (const p of input ?? []) {
44
+ if (!Number.isSafeInteger(p) || p <= 0) continue;
45
+ try { process.kill(p, 0); alive.push(p); } catch (e) { if (e.code === 'EPERM') alive.push(p); }
46
+ }
47
+ return alive;
48
+ }
49
+ if (!/^[a-zA-Z0-9_-]{1,200}$/.test(id ?? '')) throw new Error('invalid session');
50
+ const hud = path.join(home, '.config/session-orchestrator/hud');
51
+ if (action === 'aktion') {
52
+ if (!validReceipt(input, id)) throw new Error('invalid receipt');
53
+ const line = `${JSON.stringify(input)}\n`;
54
+ if (Buffer.byteLength(line) > 1024) throw new Error('receipt too large');
55
+ await privateDir(hud, { noFollow, uid });
56
+ const file = path.join(hud, 'aktionen.jsonl');
57
+ if (!noFollow) {
58
+ // Without O_NOFOLLOW, refuse a planted link or non-regular file by lstat first. Ceiling: same window as privateDir.
59
+ const st = await fs.lstat(file).catch(e => { if (e.code === 'ENOENT') return null; throw e; });
60
+ if (st && (st.isSymbolicLink() || !st.isFile() || st.nlink !== 1)) throw new Error('receipt path is not a regular file');
61
+ }
62
+ // Shared with other writers (owner-inbox): append-only, never read-modify-write; O_NOFOLLOW refuses a planted symlink.
63
+ const fh = await fs.open(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | (noFollow || 0), 0o600);
64
+ try {
65
+ // A hard link planted at the path is a second name for a foreign file: refuse it before chmod or write touch it.
66
+ const st = await fh.stat();
67
+ if (!st.isFile() || st.nlink !== 1) throw new Error('receipt path is not a private regular file');
68
+ await fh.chmod(0o600);
69
+ await fh.write(line);
70
+ } finally { await fh.close(); }
71
+ return null;
72
+ }
73
+ const dir = path.join(hud, 'beacons');
74
+ const target = path.join(dir, `${id}.json`);
75
+ if (action === 'remove') {
76
+ // Never unlink through a planted link: check both directories, but create nothing.
77
+ try { for (const d of [hud, dir]) ownDir(await fs.lstat(d), uid); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
78
+ await fs.unlink(target).catch(e => { if (e.code !== 'ENOENT') throw e; });
79
+ return null;
80
+ }
81
+ if (action !== 'write' || input?.session_id !== id) throw new Error('invalid action');
82
+ const body = JSON.stringify(input);
83
+ if (Buffer.byteLength(body) > 4096) throw new Error('beacon too large');
84
+ await privateDir(hud, { noFollow, uid });
85
+ await privateDir(dir, { noFollow, uid });
86
+ // `wx` refuses a link planted at the temp name (EEXIST), and rename replaces a link at the target instead of following it.
87
+ const temp = `${target}.${process.pid}.tmp`;
88
+ try {
89
+ await fs.writeFile(temp, body, { mode: 0o600, flag: 'wx' });
90
+ await fs.rename(temp, target);
91
+ } finally { await fs.unlink(temp).catch(() => {}); }
92
+ return null;
93
+ }
94
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
95
+ try {
96
+ let input = '';
97
+ for await (const chunk of process.stdin) { input += chunk; if (input.length > 65536) throw new Error('input too large'); }
98
+ const out = await beaconIO(process.argv[2], process.argv[3], input ? JSON.parse(input) : null);
99
+ process.stdout.write(JSON.stringify(out));
100
+ } catch { process.stderr.write('so-hud: beacon I/O failed\n'); process.exitCode = 2; }
101
+ }