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,151 @@
1
+ /**
2
+ * close-precheck.mjs — the `/close` pre-check as code (#1661).
3
+ *
4
+ * The pre-check used to be prose that read `<state-dir>/STATE.md` and acted on
5
+ * its `status` alone. It never asked whose STATE.md that is. Two failures
6
+ * followed (2026-10-17, three sessions in one afternoon):
7
+ * - After `/clear` the tab runs under a NEW native id while STATE.md still
8
+ * carries the previous one. A completed foreign STATE.md read as "already
9
+ * finalized", the session answered with the wait sentence although its own
10
+ * handover was written, and the inbox kept nudging.
11
+ * - A foreign ACTIVE STATE.md (a peer working in the same checkout) passed
12
+ * condition 3, so session-end would have closed the peer's session.
13
+ *
14
+ * Ownership is decided on the native id only: STATE.md `session-id` (#1368)
15
+ * against `readProcessLocalSessionIds()`. The session lock is no proof here —
16
+ * it records the pid of the short-lived hook process, and in a shared checkout
17
+ * it may name a peer.
18
+ *
19
+ * Unproven ownership (#1718): a STATE.md without `session-id` (written before
20
+ * #1368, in practice a foreign old file) or a process that cannot name its own
21
+ * id never runs session-end on status alone. A would-be session-end verdict
22
+ * becomes `ownership-unproven` with a `confirmToken` bound to the STATE.md path
23
+ * and its raw bytes; only that token (`--confirm-own`) turns it back, and only while the
24
+ * file is unchanged. Field incident 2026-10-21: a legacy STATE.md of an older,
25
+ * already closed plan read `complete-missing-record`.
26
+ *
27
+ * Ceiling (BV-004): on platforms without a native id (Cursor, Pi, Codex
28
+ * without a thread id) that is one operator confirmation per `/close`.
29
+ * Revisit when those platforms expose a native session id.
30
+ */
31
+
32
+ import { createHash } from 'node:crypto';
33
+ import { existsSync, readFileSync } from 'node:fs';
34
+ import path from 'node:path';
35
+
36
+ import { parseStateMd } from '../state-md.mjs';
37
+ import { findRecordedSession } from '../session-close-backfill.mjs';
38
+ import { readLock } from '../session-lock.mjs';
39
+ import { readProcessLocalSessionIds } from '../session-identity/own-session.mjs';
40
+ import { readJsonlFile } from '../io.mjs';
41
+ import { resolveEventsLedgerRoot, resolveStateDir } from '../platform.mjs';
42
+
43
+ /**
44
+ * Confirmation token for an `ownership-unproven` STATE.md (#1718): bound to the
45
+ * STATE.md path and its raw bytes, so a byte-identical file in another checkout
46
+ * yields a different token.
47
+ *
48
+ * @param {string} statePath path of the STATE.md (resolved to absolute)
49
+ * @param {Buffer|string} raw the raw STATE.md bytes
50
+ * @returns {string} 16 lowercase hex characters
51
+ */
52
+ export function computeConfirmToken(statePath, raw) {
53
+ return createHash('sha256').update(path.resolve(statePath)).update('\0').update(raw).digest('hex').slice(0, 16);
54
+ }
55
+
56
+ /** Verdicts that hand off to session-end; every other verdict stops `/close`. */
57
+ export const RUNS_SESSION_END = new Set(['run-session-end', 'complete-missing-record']);
58
+
59
+ /**
60
+ * Classify `<state-dir>/STATE.md` for `/close`.
61
+ *
62
+ * @param {object} opts
63
+ * @param {string} opts.repoRoot project root (the working tree `/close` runs in)
64
+ * @param {string} [opts.stateDir] `.claude` | `.codex` | `.cursor` | `.pi`; default from the platform
65
+ * @param {object} [opts.env=process.env] injectable for tests
66
+ * @param {string} [opts.confirmToken] token from an earlier `ownership-unproven`
67
+ * result; bound to the STATE.md path and its raw bytes, confirms a legacy/unprovable
68
+ * STATE.md while that file is unchanged (#1718)
69
+ * @returns {{
70
+ * verdict: 'no-state'|'foreign'|'run-session-end'|'complete-missing-record'|'already-finalized'|'unexpected-status'|'inconclusive'|'ownership-unproven',
71
+ * ownership: 'own'|'foreign'|'legacy'|'unprovable'|'confirmed'|null,
72
+ * status: string|null, stateSessionId: string|null, ownSessionIds: string[],
73
+ * statePath: string, reason?: string,
74
+ * wouldRun?: 'run-session-end'|'complete-missing-record', confirmToken?: string,
75
+ * session?: string|null, startedAt?: string|null
76
+ * }} the last four only with `ownership-unproven`
77
+ */
78
+ export function classifyCloseState({ repoRoot, stateDir, env = process.env, confirmToken } = {}) {
79
+ const dir = stateDir ?? resolveStateDir();
80
+ const statePath = path.join(repoRoot, dir, 'STATE.md');
81
+ const ownSessionIds = readProcessLocalSessionIds({ env });
82
+ const base = { ownership: null, status: null, stateSessionId: null, ownSessionIds, statePath };
83
+
84
+ if (!existsSync(statePath)) return { ...base, verdict: 'no-state' };
85
+
86
+ const raw = readFileSync(statePath);
87
+ const parsed = parseStateMd(raw.toString('utf8'));
88
+ if (!parsed) return { ...base, verdict: 'inconclusive', reason: 'STATE.md has no readable frontmatter' };
89
+ const fm = parsed.frontmatter;
90
+ const status = typeof fm.status === 'string' ? fm.status : null;
91
+ const rawId = typeof fm['session-id'] === 'string' ? fm['session-id'].trim() : '';
92
+ const stateSessionId = rawId || null;
93
+
94
+ const ownership = !stateSessionId ? 'legacy'
95
+ : ownSessionIds.length === 0 ? 'unprovable'
96
+ : ownSessionIds.includes(stateSessionId) ? 'own' : 'foreign';
97
+ const known = { ...base, ownership, status, stateSessionId };
98
+
99
+ // A foreign STATE.md is never this session's to close, whatever its status:
100
+ // completed belongs to an earlier tab incarnation or a peer, active/paused to a
101
+ // peer (or to this tab before a /clear, which this process cannot prove).
102
+ if (ownership === 'foreign') return { ...known, verdict: 'foreign' };
103
+
104
+ // #1718: legacy/unprovable never reaches session-end without a token bound
105
+ // to this path and the bytes read here; every non-session-end verdict passes unchanged.
106
+ const gate = (wouldRun) => {
107
+ if (ownership === 'own') return { ...known, verdict: wouldRun };
108
+ const token = computeConfirmToken(statePath, raw);
109
+ if (confirmToken === token) return { ...known, ownership: 'confirmed', verdict: wouldRun };
110
+ const unproven = {
111
+ ...known, verdict: 'ownership-unproven', wouldRun, confirmToken: token,
112
+ session: typeof fm.session === 'string' ? fm.session : null,
113
+ startedAt: typeof fm.started_at === 'string' ? fm.started_at : null,
114
+ };
115
+ if (confirmToken === undefined) return unproven;
116
+ return { ...unproven, reason: 'confirm token does not match STATE.md (changed since the pre-check, or the token belongs to another file)' };
117
+ };
118
+
119
+ if (status === 'active' || status === 'paused') return gate('run-session-end');
120
+ if (status !== 'completed') return { ...known, verdict: 'unexpected-status' };
121
+
122
+ // #429: completed alone does not prove the Phase 3.7 writer ran.
123
+ let records;
124
+ try {
125
+ records = readJsonlFile(path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl'));
126
+ } catch (err) {
127
+ return { ...known, verdict: 'inconclusive', reason: `sessions.jsonl unreadable: ${err.message}` };
128
+ }
129
+ // The lock fallback only for STATE.md without session-id (pre-#1368), and
130
+ // only when the lock names THIS process (#1663). A peer's lock UUID would
131
+ // become the native key in findRecordedSession, skip this session's own
132
+ // record (different UUID) before the label match is reached, and send
133
+ // session-end a second time. Foreign or unprovable lock → no fallback, the
134
+ // label + started_at join decides.
135
+ let lockId = null;
136
+ if (!stateSessionId) {
137
+ let lockRaw;
138
+ try { lockRaw = readLock({ repoRoot })?.session_id ?? null; } catch { lockRaw = null; }
139
+ const trimmed = typeof lockRaw === 'string' ? lockRaw.trim() : '';
140
+ if (trimmed && ownSessionIds.includes(trimmed)) lockId = trimmed;
141
+ }
142
+ const record = findRecordedSession(records, {
143
+ sessionId: stateSessionId ?? lockId,
144
+ semanticSessionId: fm.session ?? null,
145
+ startedAt: fm.started_at ?? null,
146
+ });
147
+ // An abandoned backfill stub is no completion; a legacy record without
148
+ // `status` (written before the field existed) is authoritative.
149
+ const finalized = record && (record.status === undefined || record.status === 'completed');
150
+ return finalized ? { ...known, verdict: 'already-finalized' } : gate('complete-missing-record');
151
+ }
@@ -61,11 +61,11 @@
61
61
  */
62
62
 
63
63
  import { existsSync } from 'node:fs';
64
- import path from 'node:path';
65
64
 
66
65
  import { collectProposals } from '../memory-proposals/collector.mjs';
67
66
  import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
68
- import { readSkillInvocations } from '../skill-invocations-schema.mjs';
67
+ import { readSkillInvocations, defaultSkillInvocationsPath } from '../skill-invocations-schema.mjs';
68
+ import { resolveLedgerPath } from '../ledger-path.mjs';
69
69
  import { runReconcileFromPhaseSkip, resolveEffectiveTargets } from '../reconcile/engine.mjs';
70
70
 
71
71
  // NOTE (2026-09-09): `shouldDispatchAutoDream` / `shouldDispatchAutoDialectic`
@@ -91,10 +91,11 @@ import { runReconcileFromPhaseSkip, resolveEffectiveTargets } from '../reconcile
91
91
  * @returns {{filePath: string, archivePath: string}}
92
92
  */
93
93
  export function resolveLearningsPaths(repoRoot) {
94
- const dir = path.join(repoRoot, '.orchestrator', 'metrics');
94
+ // #1722: each store resolves on its own — a linked worktree of a repo that
95
+ // ignores them maps to the main checkout, a versioned one stays local.
95
96
  return {
96
- filePath: path.join(dir, 'learnings.jsonl'),
97
- archivePath: path.join(dir, 'learnings-archive.jsonl'),
97
+ filePath: resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot),
98
+ archivePath: resolveLedgerPath('.orchestrator/metrics/learnings-archive.jsonl', repoRoot),
98
99
  };
99
100
  }
100
101
 
@@ -232,7 +233,7 @@ async function decideSkillJudge({ repoRoot, cfg, sessionId }) {
232
233
  return mkSkip(phase, 'disabled (skill-evolution.judge=false)', 'config-gate');
233
234
  }
234
235
  if (isPersistenceOff(cfg)) return mkSkip(phase, 'persistence=false', 'config-gate');
235
- const invPath = path.join(repoRoot, '.orchestrator', 'metrics', 'skill-invocations.jsonl');
236
+ const invPath = defaultSkillInvocationsPath(repoRoot);
236
237
  const entries = await readSkillInvocations(invPath);
237
238
  const noSessionFilter = sessionId === null || sessionId === undefined;
238
239
  const judged = new Set(
@@ -272,7 +273,7 @@ async function decideReconcile({ repoRoot, cfg }) {
272
273
  if (cfg?.reconcile?.enabled !== true) {
273
274
  return mkSkip(phase, 'disabled (reconcile.enabled=false)', 'config-gate');
274
275
  }
275
- const learningsPath = path.join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
276
+ const learningsPath = resolveLearningsPaths(repoRoot).filePath;
276
277
  if (!existsSync(learningsPath)) {
277
278
  return mkSkip(phase, 'learnings.jsonl absent', 'learnings.jsonl');
278
279
  }
@@ -41,7 +41,8 @@
41
41
  *
42
42
  * @typedef {Object} SweepResult
43
43
  * @property {boolean} ran - true only when the store was actually rewritten.
44
- * @property {string} [reason] - why it did not run ('plan-skip' | 'no-plan' | 'error').
44
+ * @property {string} [reason] - why it did not run ('plan-skip' | 'no-plan' |
45
+ * 'store-role-inbox' | 'error').
45
46
  * @property {string} [error] - error message, `reason === 'error'` only.
46
47
  * @property {number} [scanned] - entries read from the active store (ran only).
47
48
  * @property {number} [archived] - entries moved to the archive sidecar (ran only).
@@ -50,7 +51,7 @@
50
51
  * @property {string} [archivePath] - the archive sidecar written to (ran only).
51
52
  */
52
53
 
53
- import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
54
+ import { SKIPPED_STORE_ROLE_INBOX, sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
54
55
  import { resolveLearningsPaths } from './phase-skip.mjs';
55
56
 
56
57
  /** Event name — a plain string literal so the events-schema census can see it. */
@@ -151,6 +152,10 @@ export async function runExpiredSweep({ repoRoot, plan, now, graceDays, emit = t
151
152
  ...(graceDays === undefined ? {} : { graceDays }),
152
153
  });
153
154
 
155
+ // Inbox role (#1516): the sweep wrote nothing, so this is not a run and
156
+ // emits no sweep_applied — that record's presence claims a rewrite.
157
+ if (res?.skipped === SKIPPED_STORE_ROLE_INBOX) return { ran: false, reason: SKIPPED_STORE_ROLE_INBOX };
158
+
154
159
  const scanned = res?.scanned ?? 0;
155
160
  const archived = res?.archived ?? 0;
156
161
  // The sweep result omits `malformed` when the read found none, so absent IS
@@ -0,0 +1,205 @@
1
+ /**
2
+ * vault-orphans.mjs - Phase 3.7e Vault Orphans (#1563).
3
+ *
4
+ * Lists the untracked files in the Meta-Vault that were modified since this
5
+ * session started, so the closing session can NAME what it may have left
6
+ * behind there.
7
+ *
8
+ * Why this exists (measured 2026-10-11): one vault checkout had accumulated 268
9
+ * untracked files over five days - website audits, session reports, reviews,
10
+ * lead handovers - written by sessions of OTHER repos into their configured
11
+ * `vault-dir`. None of them was ever committed: a session's close commits only
12
+ * in its own repo, and vault-mirror auto-commits only its own generator-marked
13
+ * files (`scripts/lib/vault-mirror/auto-commit.mjs`). The writing session is the
14
+ * only one that knows it wrote them, and nothing made it look.
15
+ *
16
+ * THE IRON RULE: this module PROPOSES, it never DISPOSES. Its only git
17
+ * invocation is the read-only `ls-files --others --exclude-standard -z`. No
18
+ * add, commit, rm, mv, clean, stash; no file is written, moved or deleted. The
19
+ * vault is another repo with its own rules (PSA-003).
20
+ *
21
+ * The return field is named `candidates`, not `orphans`: mtime is a HINT, not
22
+ * proof of ownership. A concurrent session of another repo may have written the
23
+ * same file in the same window. The coordinator names them and records one Open
24
+ * Question; it never acts on them.
25
+ *
26
+ * Three states, never two (a missing measurement must not look like zero):
27
+ * - `{ status: 'ok', total: 0 }` - measured, nothing found
28
+ * - `{ status: 'ok', total: N }` - measured, N candidates
29
+ * - `{ status: 'unavailable' }` - could not measure (no vault dir, not a
30
+ * git repo, git failed). Never reported as
31
+ * "no orphans".
32
+ *
33
+ * Exclusion - what another writer already owns: any file whose first 4 KB
34
+ * carries a `_generator: session-orchestrator-` frontmatter line. Census
35
+ * 2026-10-11 @ 686e2c12 (`rg -n "GENERATOR_MARKER\s*=" scripts/`): four marker
36
+ * values, all with that prefix - `vault-mirror@1` (40-learnings/, 50-sessions/,
37
+ * monthly rollups), `vault-status-narrative@1` (01-projects/<slug>/
38
+ * _session-narrative.md), `active-sessions@1` (01-projects/_active-sessions.md),
39
+ * `gitlab-portfolio@1`. The match is line-anchored so a hand-written note that
40
+ * merely QUOTES the marker in prose is still reported.
41
+ *
42
+ * There is deliberately NO blanket exclusion of `40-learnings/` / `50-sessions/`:
43
+ * vault-mirror owns only the MARKED files there. `autoCommitVaultMirror` treats
44
+ * an unmarked staged file in those dirs as an offender and skips its whole
45
+ * commit, so an unmarked untracked file there is owned by nobody AND blocks the
46
+ * mirror - the last thing to hide.
47
+ *
48
+ * Never throws. DI seams (#580-DI-001), synchronous like the Phase 4b sibling
49
+ * ./worktree-orphan-sweep.mjs: `execFileFn` (default `execFileSync`) and
50
+ * `statFn` (default `lstatSync` - a symlink is reported as itself, never
51
+ * followed out of the vault). Every git call passes an args ARRAY, no shell
52
+ * (#577 HARDEN-001).
53
+ */
54
+
55
+ import path from 'node:path';
56
+ import { execFileSync } from 'node:child_process';
57
+ import { closeSync, lstatSync, openSync, readSync } from 'node:fs';
58
+
59
+ import { redactHomeDir } from '../common.mjs';
60
+
61
+ /** Upper bound on the returned `candidates` list; `total` still counts all. */
62
+ export const MAX_CANDIDATES = 200;
63
+
64
+ /** Bytes read per file for the generator check - same window as auto-commit.mjs. */
65
+ const HEAD_BYTES = 4096;
66
+
67
+ /** A frontmatter line naming one of this plugin's generators. */
68
+ const GENERATOR_LINE_RE = /^_generator:[ \t]*session-orchestrator-/m;
69
+
70
+ /**
71
+ * Env vars that retarget git away from `-C <dir>`. An inherited `GIT_DIR` beats
72
+ * `-C` (incident 2026-08-19), which here would list ANOTHER repo's untracked
73
+ * files as vault candidates. Stripped for the one git call below.
74
+ */
75
+ const REPO_REDIRECT_ENV = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR'];
76
+
77
+ /**
78
+ * Ceiling for git's NUL-separated path list: 16 MiB is ~150k paths at ~100
79
+ * bytes. Past it, execFileSync throws ENOBUFS and the result is `unavailable`
80
+ * (could not measure), never a silently cut list. Revisit if a vault ever
81
+ * reports that reason.
82
+ */
83
+ const LS_FILES_MAX_BUFFER = 16 * 1024 * 1024;
84
+
85
+ /**
86
+ * A vault on a hung network filesystem must not stall /close: past this the
87
+ * call is killed and the result is `unavailable` (git-failed). 20k untracked
88
+ * files took 0.46 s (2026-10-11), so 20 s is a hang, not a slow vault.
89
+ */
90
+ const LS_FILES_TIMEOUT_MS = 20_000;
91
+
92
+ /** @param {string} absPath @returns {boolean} true when the head names one of our generators */
93
+ function isGeneratorOwned(absPath) {
94
+ let fd;
95
+ try {
96
+ fd = openSync(absPath, 'r');
97
+ const buf = Buffer.alloc(HEAD_BYTES);
98
+ const n = readSync(fd, buf, 0, HEAD_BYTES, 0);
99
+ return GENERATOR_LINE_RE.test(buf.subarray(0, n).toString('utf8'));
100
+ } catch {
101
+ // Unreadable -> not provably someone else's. This module only REPORTS, so
102
+ // the safe direction is to show the file, not to hide it.
103
+ return false;
104
+ } finally {
105
+ if (fd !== undefined) {
106
+ try {
107
+ closeSync(fd);
108
+ } catch {
109
+ /* already closed */
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * @param {string} reason
117
+ * @param {string} [detail]
118
+ * @returns {{status: 'unavailable', reason: string, detail?: string, candidates: []}}
119
+ */
120
+ function unavailable(reason, detail) {
121
+ return detail ? { status: 'unavailable', reason, detail, candidates: [] } : { status: 'unavailable', reason, candidates: [] };
122
+ }
123
+
124
+ /**
125
+ * Find untracked vault files modified at or after `sinceMs`.
126
+ *
127
+ * @param {object} [opts]
128
+ * @param {string} opts.vaultDir - Vault working-tree root (already tilde-expanded).
129
+ * @param {number} opts.sinceMs - Session start, epoch ms. Files with an older
130
+ * mtime are not candidates.
131
+ * @param {Function} [opts.execFileFn] - execFileSync seam: (file, args, options) => string.
132
+ * @param {Function} [opts.statFn] - lstatSync seam: (absPath) => { mtimeMs, isFile() }.
133
+ * @returns {{status: 'ok', candidates: Array<{path: string, mtimeMs: number}>,
134
+ * total: number, truncated: boolean, untracked: number}
135
+ * | {status: 'unavailable', reason: string, detail?: string, candidates: []}}
136
+ * `path` is vault-relative as git reports it. `total` counts every candidate,
137
+ * `candidates` holds at most MAX_CANDIDATES of them (sorted by path),
138
+ * `untracked` is everything git listed before the mtime and generator filters.
139
+ */
140
+ export function findVaultOrphans({ vaultDir, sinceMs, execFileFn, statFn } = {}) {
141
+ try {
142
+ if (typeof vaultDir !== 'string' || vaultDir.trim() === '') return unavailable('no-vault-dir');
143
+ if (typeof sinceMs !== 'number' || !Number.isFinite(sinceMs)) return unavailable('invalid-since');
144
+
145
+ const exec = typeof execFileFn === 'function' ? execFileFn : execFileSync;
146
+ const stat = typeof statFn === 'function' ? statFn : lstatSync;
147
+ const root = path.resolve(vaultDir);
148
+
149
+ const env = { ...process.env };
150
+ for (const key of REPO_REDIRECT_ENV) delete env[key];
151
+
152
+ // -- THE ONLY GIT CALL - read-only --------------------------------------
153
+ let out;
154
+ try {
155
+ out = exec('git', ['-C', root, 'ls-files', '--others', '--exclude-standard', '-z'], {
156
+ encoding: 'utf8',
157
+ env,
158
+ maxBuffer: LS_FILES_MAX_BUFFER,
159
+ timeout: LS_FILES_TIMEOUT_MS,
160
+ stdio: ['ignore', 'pipe', 'pipe'],
161
+ });
162
+ } catch (err) {
163
+ const stderr = typeof err?.stderr === 'string' ? err.stderr : String(err?.stderr ?? '');
164
+ const reason = /not a git repository/i.test(stderr) ? 'not-a-git-repo' : 'git-failed';
165
+ const firstLine = redactHomeDir(stderr.split('\n').find((l) => l.trim() !== '') ?? err?.code ?? '');
166
+ return unavailable(reason, typeof firstLine === 'string' ? firstLine.trim().slice(0, 200) : undefined);
167
+ }
168
+ if (typeof out !== 'string') return unavailable('git-failed', 'non-string git output');
169
+
170
+ const untrackedPaths = out.split('\0').filter((p) => p !== '');
171
+
172
+ // Cost is linear in the untracked count: one lstat per path, plus one 4 KB
173
+ // read per path that passes the mtime filter (cheapest filter first). Fine
174
+ // for the measured 268; revisit if a close reports more than ~10k untracked
175
+ // files, where the per-file read would start to show in /close time.
176
+ const all = [];
177
+ for (const rel of untrackedPaths) {
178
+ const abs = path.join(root, rel);
179
+ let st;
180
+ try {
181
+ st = stat(abs);
182
+ } catch {
183
+ // Vanished between listing and stat (a concurrent writer moved it):
184
+ // nothing left to name.
185
+ continue;
186
+ }
187
+ const mtimeMs = Number(st?.mtimeMs);
188
+ if (!Number.isFinite(mtimeMs) || mtimeMs < sinceMs) continue;
189
+ if (typeof st.isFile === 'function' && st.isFile() && isGeneratorOwned(abs)) continue;
190
+ all.push({ path: rel, mtimeMs });
191
+ }
192
+
193
+ all.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
194
+ return {
195
+ status: 'ok',
196
+ candidates: all.slice(0, MAX_CANDIDATES),
197
+ total: all.length,
198
+ truncated: all.length > MAX_CANDIDATES,
199
+ untracked: untrackedPaths.length,
200
+ };
201
+ } catch (err) {
202
+ // Defensive catch-all - this probe must never throw into /close.
203
+ return unavailable('internal-error', String(err?.message ?? err).slice(0, 200));
204
+ }
205
+ }
@@ -43,6 +43,7 @@ import { readFile } from 'node:fs/promises';
43
43
  import path from 'node:path';
44
44
 
45
45
  import { eventsFilePath } from './events.mjs';
46
+ import { resolveEventsLedgerRoot } from './platform.mjs';
46
47
  import { readCanonicalSessions } from './sessions-canonical.mjs';
47
48
  import { readTailWindow } from './tail-window.mjs';
48
49
  import { withStateMdLock } from './session-lock.mjs';
@@ -144,7 +145,11 @@ function isValidMode(mode) {
144
145
  * only across DIFFERENT ids — never the same id twice, see above).
145
146
  */
146
147
  async function readSessionIdsFromHistory(repoRoot) {
147
- const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
148
+ // #1592: the repo's ONE sessions ledger (main checkout of a linked worktree,
149
+ // where every close writes since #1584). Aggregation is a monotone max, so the
150
+ // wider ledger can only raise n — the events source does not always cover a
151
+ // closed session (rotated ledger, no lock event), the worktree read was empty.
152
+ const filePath = path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl');
148
153
  return readCanonicalSessions({ filePath }).map((rec) => rec.session_id);
149
154
  }
150
155
 
@@ -266,6 +271,9 @@ async function readSessionIdsFromEvents(repoRoot) {
266
271
  * @returns {Promise<string|null>} The session_id string, or null when absent.
267
272
  */
268
273
  async function readSessionIdFromStateMd(repoRoot) {
274
+ // #1636 decision: deliberately the READ resolver, foreign STATE.md included —
275
+ // this source only collects ids already TAKEN, so a foreign one can only raise
276
+ // n (max over a union) and ignoring it could re-mint a peer's id.
269
277
  const filePath = resolveStateMdPath(repoRoot);
270
278
  let raw;
271
279
  try {
@@ -27,6 +27,8 @@
27
27
  */
28
28
 
29
29
  import { readFileSync } from 'node:fs';
30
+ import { execFileSync } from 'node:child_process';
31
+ import { homedir } from 'node:os';
30
32
  import path from 'node:path';
31
33
  import { isLockShape } from '../session-lock-shape.mjs';
32
34
 
@@ -228,6 +230,67 @@ export function readProcessLocalSessionIds({ env = process.env, hookInput = null
228
230
  return ids;
229
231
  }
230
232
 
233
+ /**
234
+ * Read only process parent/name metadata, never command arguments or env.
235
+ * @param {number} pid
236
+ * @returns {{ ppid: number, comm: string } | null}
237
+ */
238
+ function readProcessParent(pid) {
239
+ const output = execFileSync('ps', ['-p', String(pid), '-o', 'ppid=,comm='], {
240
+ encoding: 'utf8', timeout: 1000, maxBuffer: 2048,
241
+ stdio: ['ignore', 'pipe', 'pipe'],
242
+ });
243
+ const match = /^\s*(\d+)\s+(\S+)\s*$/.exec(output);
244
+ return match ? { ppid: Number(match[1]), comm: match[2] } : null;
245
+ }
246
+
247
+ /**
248
+ * Prove an acknowledgement's own invocation using the nearest Claude process.
249
+ * Unlike the general identity union, inherited env and shared repo files are
250
+ * never proof here. Stop at the nearest Claude even when its session is unreadable.
251
+ * Missing, contradictory or unmeasurable evidence returns null; never throws.
252
+ * Reader injection permits deterministic fixtures when native ps is unavailable.
253
+ *
254
+ * @param {{ env?: object, pid?: number, homeDir?: string,
255
+ * readProcess?: (pid: number) => { ppid: number, comm: string } | null,
256
+ * readSession?: (pid: number) => unknown }} [opts]
257
+ * @returns {string|null}
258
+ */
259
+ export function readInvocationSessionId({
260
+ env = process.env, pid = process.pid, homeDir = homedir(),
261
+ readProcess = readProcessParent,
262
+ readSession = (ancestorPid) => JSON.parse(readFileSync(
263
+ path.join(homeDir, '.claude', 'sessions', `${ancestorPid}.json`), 'utf8',
264
+ )),
265
+ } = {}) {
266
+ const seen = new Set();
267
+ try {
268
+ for (let depth = 0; depth < 64; depth++) {
269
+ if (!Number.isSafeInteger(pid) || pid <= 0 || seen.has(pid)) return null;
270
+ seen.add(pid);
271
+ const processInfo = readProcess(pid);
272
+ if (!processInfo || !Number.isSafeInteger(processInfo.ppid) || processInfo.ppid < 0 ||
273
+ seen.has(processInfo.ppid) || typeof processInfo.comm !== 'string' || !processInfo.comm.trim()) return null;
274
+ // macOS reports comm as argv[0]; a Claude started by absolute path must still count.
275
+ if (path.basename(processInfo.comm) === 'claude') {
276
+ const session = readSession(pid);
277
+ const sessionId = typeof session?.sessionId === 'string' ? session.sessionId.trim() : '';
278
+ if (!sessionId) return null;
279
+ // Every present native ID must agree; platform selection cannot hide
280
+ // a contradictory inherited ID from a nested harness.
281
+ for (const key of ['CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID']) {
282
+ const value = env?.[key];
283
+ if (value !== undefined && value !== null &&
284
+ (typeof value !== 'string' || (value.trim() && value.trim() !== sessionId))) return null;
285
+ }
286
+ return sessionId;
287
+ }
288
+ pid = processInfo.ppid;
289
+ }
290
+ } catch { /* Missing process/session evidence must fail closed. */ }
291
+ return null;
292
+ }
293
+
231
294
  /**
232
295
  * Decide whether a wave-scope manifest belongs to THIS session.
233
296
  *
@@ -3,15 +3,26 @@
3
3
  * This is a text boundary, not a shell/option parser: quotes and command syntax
4
4
  * in the context remain data. `unknown` is a ledger value, never a live mode.
5
5
  *
6
+ * A bare `/session` defaults to `deep`. In a repo without any session history
7
+ * of its own that default is not silent: the result carries
8
+ * `defaultReason: 'no-session-history'`, and the caller asks for task and mode
9
+ * first (#1561). `hasHistory` left undefined means "not measured" and keeps
10
+ * the plain default.
11
+ *
6
12
  * @param {string} [argumentsText] Complete text following /session.
7
- * @returns {{sessionType: string, context: string, profile?: string, invalidMode?: string}}
13
+ * @param {{hasHistory?: boolean}} [options]
14
+ * @returns {{sessionType: string, context: string, profile?: string, invalidMode?: string, defaultReason?: string}}
8
15
  */
9
- export function resolveSessionInvocation(argumentsText = '') {
16
+ export function resolveSessionInvocation(argumentsText = '', { hasHistory } = {}) {
10
17
  if (typeof argumentsText !== 'string') {
11
18
  throw new TypeError('session invocation arguments must be a string');
12
19
  }
13
20
  const [_, mode, context = ''] = argumentsText.trimStart().match(/^(\S+)(?:\s+([\s\S]*))?$/u) ?? [];
14
- if (mode === undefined) return { sessionType: 'deep', context: '' };
21
+ if (mode === undefined) {
22
+ return hasHistory === false
23
+ ? { sessionType: 'deep', context: '', defaultReason: 'no-session-history' }
24
+ : { sessionType: 'deep', context: '' };
25
+ }
15
26
  if (mode === 'ultradeep') return { sessionType: 'deep', profile: 'ultradeep', context };
16
27
  if (['housekeeping', 'feature', 'deep'].includes(mode)) {
17
28
  return { sessionType: mode, context };