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
@@ -35,8 +35,10 @@
35
35
  */
36
36
 
37
37
  import fs from 'node:fs';
38
+ import os from 'node:os';
38
39
  import path from 'node:path';
39
40
  import { writeJsonAtomicSync } from '../../scripts/lib/io.mjs';
41
+ import { readNavigatorLease } from '../../scripts/lib/fleet-protocol.mjs';
40
42
 
41
43
  /**
42
44
  * Native SessionStart `source` values that are a RE-ENTRY into the same logical
@@ -55,6 +57,184 @@ import { writeJsonAtomicSync } from '../../scripts/lib/io.mjs';
55
57
  */
56
58
  export const SAME_LOGICAL_SESSION_SOURCES = new Set(['resume', 'clear', 'compact']);
57
59
 
60
+ /**
61
+ * Native `source` values on which a fleet lead's lease keeps this session out
62
+ * of the MAIN checkout (#1635). A subset of SAME_LOGICAL_SESSION_SOURCES:
63
+ * `compact` keeps the lock it already holds, `startup` stays untouched. A
64
+ * session that yielded holds none: the yield leaves a #1651 release marker, so
65
+ * its later `compact` stays lockless too (#1747).
66
+ *
67
+ * @type {ReadonlySet<string>}
68
+ */
69
+ const LEAD_YIELD_SOURCES = new Set(['clear', 'resume']);
70
+
71
+ /**
72
+ * Native `source` values on which a lock this same raw session released on
73
+ * purpose stays released (#1651). A subset of SAME_LOGICAL_SESSION_SOURCES:
74
+ * `clear` starts new work and takes a lock as before, `startup` too.
75
+ *
76
+ * @type {ReadonlySet<string>}
77
+ */
78
+ const RELEASED_LOCK_STAYS_RELEASED_SOURCES = new Set(['resume', 'compact']);
79
+
80
+ /**
81
+ * #1651 — true when `sessionId` left a release marker in this repo. Never
82
+ * throws: a missing module or any error reads as "no marker" (the pre-#1651
83
+ * acquire path).
84
+ *
85
+ * @param {string} repoRoot
86
+ * @param {string} sessionId
87
+ * @returns {Promise<boolean>}
88
+ */
89
+ async function hasOwnReleaseMarker(repoRoot, sessionId) {
90
+ try {
91
+ const { hasReleaseMarker } = await import('../../scripts/lib/session-lock.mjs');
92
+ return hasReleaseMarker({ repoRoot, sessionId }) === true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /** #1747 — `reason` of a release marker the lead yield writes. */
99
+ const LEAD_YIELD_REASON = 'lead-yield';
100
+
101
+ /**
102
+ * #1747 — the #1651 marker body for `sessionId`, or null. Never throws.
103
+ *
104
+ * @param {string} repoRoot
105
+ * @param {string} sessionId
106
+ * @returns {Promise<object|null>}
107
+ */
108
+ async function readOwnReleaseMarker(repoRoot, sessionId) {
109
+ try {
110
+ const { readReleaseMarker } = await import('../../scripts/lib/session-lock.mjs');
111
+ return readReleaseMarker({ repoRoot, sessionId });
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * #1747 — a lead yield is a release too: leave the #1651 marker for `sessionId`,
119
+ * tagged `reason: 'lead-yield'`, so a later `compact` or `resume` of this same
120
+ * session stays lockless while the lead's lease is active (compact is no yield
121
+ * source, and without the marker it took the free lock). Once the lease is
122
+ * gone, re-entry acquires and acquire() removes the marker as before. Never
123
+ * throws; a failed write is one stderr WARN and leaves the pre-#1747 behaviour.
124
+ *
125
+ * A marker that is already there and was not written by a yield stays as it is:
126
+ * without `reason` it is an explicit release (release-session-lock.mjs), which
127
+ * keeps its #1651 behaviour after the lease ends. Rewriting it as `lead-yield`
128
+ * made the next compaction after the lease take the lock back (review of !244,
129
+ * M1). An unreadable marker is kept for the same reason: the branch below
130
+ * already reads it as explicit.
131
+ *
132
+ * @param {string} repoRoot
133
+ * @param {string} sessionId
134
+ * @returns {Promise<void>}
135
+ */
136
+ async function writeOwnReleaseMarker(repoRoot, sessionId) {
137
+ if (await hasOwnReleaseMarker(repoRoot, sessionId)) {
138
+ const existing = await readOwnReleaseMarker(repoRoot, sessionId);
139
+ if (existing?.reason !== LEAD_YIELD_REASON) return;
140
+ }
141
+ let reason;
142
+ try {
143
+ const { writeReleaseMarker } = await import('../../scripts/lib/session-lock.mjs');
144
+ const result = writeReleaseMarker({ repoRoot, sessionId, reason: LEAD_YIELD_REASON });
145
+ if (result?.ok !== true) reason = result?.reason ?? 'unknown';
146
+ } catch (err) {
147
+ reason = err?.message ?? 'unknown';
148
+ }
149
+ if (reason !== undefined) {
150
+ process.stderr.write(
151
+ `⚠ lock-bootstrap: no release marker after the lead yield (${reason}) — a compaction may take the lock again\n`,
152
+ );
153
+ }
154
+ }
155
+
156
+ /**
157
+ * #1641 — the Claude Code process id this hook runs under, or null. The value
158
+ * comes from `CLAUDE_PID`, but it counts ONLY when that process is this hook's
159
+ * direct parent: the variable is inherited by everything a Claude session
160
+ * starts, so a Codex child launched from a session's Bash would otherwise carry
161
+ * the parent's pid into its own SessionStart (`hooks-codex.json` matches
162
+ * resume|clear|compact) and take over the parent's LIVE lock (REFUTE review,
163
+ * reproduced against the real acquire/forceAcquire).
164
+ *
165
+ * Measured 2026-10-15 (claude 2.1.300, macOS): a SessionStart hook launched as
166
+ * `sh run-node.sh <hook>` runs node with `process.ppid === CLAUDE_PID` and the
167
+ * parent's comm `claude` — both shells exec. A Codex hook's parent is Codex or
168
+ * its shell, never Claude, so it reads null here. Depth 1, deliberately: a
169
+ * depth-2 walk would admit a hook whose parent is a Codex process that bash
170
+ * exec'd straight under Claude. Spawn-free.
171
+ *
172
+ * #1717: Claude runs each hook as `/bin/sh -c "<command>"`. On Linux/WSL
173
+ * /bin/sh is dash, and the WSL dash 0.5.12 FORKS a single `-c` command
174
+ * instead of exec'ing it, so node's parent was that intermediate sh and this
175
+ * read null on every hook (measured 2026-10-21 on win-wsl, 3/3 runs; with an
176
+ * `exec ` prefix the ppid matched). Every run-node.sh command in
177
+ * `hooks/hooks.json` therefore starts with `exec sh …`; run-node.sh itself
178
+ * execs node on every branch, so both hops are exec and depth 1 holds.
179
+ * The other manifests (hooks-codex/-cursor/-pi.json) deliberately keep the
180
+ * plain form: only a Claude hook can match its own CLAUDE_PID, so the prefix
181
+ * would buy nothing there (their parent is that harness either way).
182
+ * Measured 2026-10-21 on macOS: /bin/sh (bash 3.2) and /bin/dash (dash-16)
183
+ * both match ppid with AND without `exec`, and exit status 2, stderr and stdin
184
+ * pass through unchanged in both forms — the prefix costs nothing there.
185
+ * Ceiling (BV-004): a harness whose launcher is not `/bin/sh -c` (or that
186
+ * wraps the command in a further shell) still reads null and keeps the
187
+ * pre-#1641 behaviour (fail-closed). Revisit if `reclaimed: own-process`
188
+ * never appears on a host where /clear without SessionEnd does.
189
+ *
190
+ * The process survives `/clear` while the raw session id is minted anew, so
191
+ * this is the witness of "the same terminal session" that the label and the
192
+ * raw id cannot give.
193
+ *
194
+ * @param {NodeJS.ProcessEnv} [env]
195
+ * @param {number} [ppid] — this process' parent pid (DI for tests).
196
+ * @returns {number|null}
197
+ */
198
+ export function readClaudePid(env = process.env, ppid = process.ppid) {
199
+ const raw = typeof env?.CLAUDE_PID === 'string' ? env.CLAUDE_PID.trim() : '';
200
+ if (!/^[1-9][0-9]{0,9}$/.test(raw)) return null;
201
+ const pid = Number(raw);
202
+ return Number.isSafeInteger(pid) && pid === ppid ? pid : null;
203
+ }
204
+
205
+ /**
206
+ * #1641 — true when `lock` was written under the Claude Code process this hook
207
+ * runs under, on this host. Only then may a lock with a DIFFERENT raw id count
208
+ * as our own predecessor (startup → /clear mints a new raw id in the same
209
+ * process). #1746 judges the `claude_pid`/`host` pair current-session.json
210
+ * carries with this same predicate, so both witnesses mean the same thing.
211
+ *
212
+ * Why the pid alone is enough, without a start time: both sides come from
213
+ * readClaudePid(), which admits a pid only as the writer hook's DIRECT parent.
214
+ * So the lock's pid names the Claude process that wrote it, and equal to ours
215
+ * it names either this process or a dead one whose pid the OS recycled to us —
216
+ * not another live Claude process in the same pid namespace.
217
+ * The host check keeps a lock on a shared filesystem from another machine out.
218
+ * Ceilings (BV-004), named rather than claimed closed: (1) two pid namespaces
219
+ * that report the same hostname (WSL2 distros, a container and its host on one
220
+ * checkout) can hold equal pids; (2) an in-process agent team whose teammates
221
+ * each ran SessionStart under one Claude process (measured: subagents fire no
222
+ * SessionStart). Revisit when either shape works in a shared checkout.
223
+ *
224
+ * @param {object|null} lock
225
+ * @param {number|null} claudePid — from readClaudePid().
226
+ * @returns {Promise<boolean>}
227
+ */
228
+ async function isSameClaudeProcess(lock, claudePid) {
229
+ if (typeof claudePid !== 'number' || !lock || lock.claude_pid !== claudePid) return false;
230
+ try {
231
+ const { hostnamesMatch, lockHostCandidate } = await import('../../scripts/lib/host-identity.mjs');
232
+ return hostnamesMatch(lockHostCandidate(lock), os.hostname());
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
58
238
  /** Re-reads of a just-acquired lock that answers ENOENT, and the pause between them (#1494). */
59
239
  const ENRICH_READ_RETRIES = 5;
60
240
  const ENRICH_READ_RETRY_MS = 10;
@@ -81,14 +261,34 @@ const ENRICH_READ_RETRY_MS = 10;
81
261
  * Only consulted for the same-logical-session force-refresh below; every
82
262
  * existing caller that omits it keeps its pre-#1091 behaviour exactly.
83
263
  * @param {string|null} [opts.predecessorSessionId=null] — the raw `session_id`
84
- * recorded in `.orchestrator/current-session.json` by our OWN previous run of
85
- * this hook. It is the third, load-bearing conjunct of the re-entry
264
+ * the last session start in this checkout recorded in
265
+ * `.orchestrator/current-session.json` — our own previous run of this hook
266
+ * unless a peer started here since (every start rewrites the file, #1746).
267
+ * It is the third, load-bearing conjunct of the re-entry
86
268
  * force-refresh below: without it a same-host semantic-label collision (#1066)
87
269
  * lets one session take over a live peer's lock. Omitting it (the default)
88
270
  * disables the re-entry force-refresh entirely — fail-closed.
271
+ * @param {{ claude_pid?: unknown, host?: unknown }|null} [opts.predecessorProcess=null]
272
+ * — the `claude_pid`/`host` pair the writer of that `current-session.json`
273
+ * recorded (#1746), unvalidated. The #1635 lead yield takes
274
+ * `predecessorSessionId` for our own predecessor only when this pair names
275
+ * our own Claude Code process (isSameClaudeProcess); null or any mismatch
276
+ * yields.
277
+ * @param {number|null} [opts.claudePid=null] — the Claude Code process id from
278
+ * readClaudePid() (#1641). Stamped on the lock as `claude_pid`; a lock that
279
+ * carries this same pid on this host is our own predecessor after /clear, for
280
+ * the re-entry force-refresh AND the #1635 lead-yield release. Null (Codex,
281
+ * Cursor, tests that omit it) leaves both exactly as before.
89
282
  * @param {Function} [opts._acquireImpl] — DI for tests (defaults to importing acquire from session-lock.mjs).
90
283
  * @param {Function} [opts._forceAcquireImpl] — DI for tests (defaults to importing forceAcquire from session-lock.mjs).
91
284
  * @param {Function} [opts._emitEventImpl] — DI for tests (defaults to importing emitEvent from events.mjs).
285
+ * @param {(context: string, banner: string) => void} [opts.onNotice] — receives
286
+ * the model notice and the operator banner line when the lock is deliberately
287
+ * NOT taken because a fleet lead holds the main checkout (#1635), or because
288
+ * this raw id released its lock on purpose and the source is `resume` /
289
+ * `compact` (#1651).
290
+ * @param {Function} [opts._readLeaseImpl] — DI for tests (defaults to readNavigatorLease from fleet-protocol.mjs).
291
+ * @param {Function} [opts._releaseImpl] — DI for tests (defaults to release from session-lock.mjs).
92
292
  * @returns {Promise<object|null>} the enriched v2 lock body on success, null on any failure.
93
293
  */
94
294
  export async function bootstrapLock({
@@ -99,15 +299,92 @@ export async function bootstrapLock({
99
299
  ttlHours = 4,
100
300
  nativeSource = null,
101
301
  predecessorSessionId = null,
302
+ predecessorProcess = null,
303
+ claudePid = null,
102
304
  _acquireImpl,
103
305
  _forceAcquireImpl,
104
306
  _emitEventImpl,
307
+ onNotice,
308
+ _readLeaseImpl = readNavigatorLease,
309
+ _releaseImpl,
105
310
  } = {}) {
106
311
  // Sanity-check required inputs. Anything missing → bail silently.
107
312
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
108
313
  if (typeof sessionId !== 'string' || sessionId.length === 0) return null;
109
314
  if (typeof mode !== 'string' || mode.length === 0) return null;
110
315
 
316
+ // #1635: after /clear or resume in the MAIN checkout, a lead's active lease
317
+ // means the lead runs headless jobs here — our lock would make its slot check
318
+ // report the repo as occupied. Yield instead of acquiring, and give back a
319
+ // lock this same raw id already holds here.
320
+ const leadHolder = await leadHoldingMainCheckout({
321
+ repoRoot,
322
+ sessionId,
323
+ predecessorSessionId,
324
+ predecessorProcess,
325
+ claudePid,
326
+ nativeSource,
327
+ readLease: _readLeaseImpl,
328
+ });
329
+ if (leadHolder !== null) {
330
+ // Released: a lock with our own raw id (a resume where the harness kept the
331
+ // id), or one written under our own Claude Code process (#1641 — startup →
332
+ // /clear mints a new raw id in the same process). Never a lock whose only
333
+ // "it was us" witness is current-session.json: every session start in this
334
+ // checkout rewrites that file, so a peer's /clear or resume would release a
335
+ // LIVE foreign lock. A predecessor from before #1641 carries no
336
+ // `claude_pid` and still stays until its TTL.
337
+ await releaseOwnLockForLead({ repoRoot, sessionId, claudePid, releaseImpl: _releaseImpl });
338
+ await writeOwnReleaseMarker(repoRoot, sessionId);
339
+ if (typeof onNotice === 'function') {
340
+ // The lease reader admits only [A-Za-z0-9._-] ids, so the id is banner-safe.
341
+ const sid8 = leadHolder.slice(0, 8);
342
+ try {
343
+ onNotice(
344
+ `Haupt-Checkout belegt durch Lead ${sid8}, arbeite im Worktree`,
345
+ `🧭 Haupt-Checkout belegt durch Lead ${sid8} — kein Session-Lock, arbeite im Worktree`,
346
+ );
347
+ } catch { /* the notice is informational */ }
348
+ }
349
+ return null;
350
+ }
351
+
352
+ // #1651: this raw session gave its lock back on purpose
353
+ // (scripts/release-session-lock.mjs), typically so a headless run can use
354
+ // the slot. A compaction or resume of that same session is not new work and
355
+ // must not take the lock again; startup and clear acquire as before. The
356
+ // marker is removed by the session's next deliberate acquire().
357
+ // #1747: a marker the lead yield above wrote counts only while a lead lease
358
+ // is still active; once the lead is gone the session acquires again.
359
+ if (
360
+ typeof nativeSource === 'string' &&
361
+ RELEASED_LOCK_STAYS_RELEASED_SOURCES.has(nativeSource) &&
362
+ (await hasOwnReleaseMarker(repoRoot, sessionId))
363
+ ) {
364
+ const marker = await readOwnReleaseMarker(repoRoot, sessionId);
365
+ const yielded = marker?.reason === LEAD_YIELD_REASON;
366
+ const leadId = yielded ? await activeLeadSessionId(_readLeaseImpl) : null;
367
+ if (!yielded || leadId !== null) {
368
+ if (typeof onNotice === 'function') {
369
+ try {
370
+ if (yielded) {
371
+ const sid8 = leadId.slice(0, 8);
372
+ onNotice(
373
+ `Haupt-Checkout belegt durch Lead ${sid8}, arbeite im Worktree`,
374
+ `🧭 Haupt-Checkout belegt durch Lead ${sid8} — kein Session-Lock, arbeite im Worktree`,
375
+ );
376
+ } else {
377
+ onNotice(
378
+ 'Session-Lock wurde in dieser Session bewusst freigegeben und nach dem Neueinstieg nicht neu angelegt',
379
+ 'ℹ Session-Lock bewusst freigegeben — nach dem Neueinstieg nicht neu angelegt',
380
+ );
381
+ }
382
+ } catch { /* the notice is informational */ }
383
+ }
384
+ return null;
385
+ }
386
+ }
387
+
111
388
  // Resolve DI shims at call time so test mocks can replace the imports.
112
389
  let acquireFn = _acquireImpl;
113
390
  let forceAcquireFn = _forceAcquireImpl;
@@ -181,10 +458,23 @@ export async function bootstrapLock({
181
458
  // live lock, conjunct (c) holds for the wrong session. `current-session.json`
182
459
  // carries no session field of its own (see
183
460
  // `.claude/rules/identity-and-locks.md` § shared repo artefacts), which is
184
- // exactly why this is a ceiling and not a proof. Revisit when the lock or
185
- // current-session.json carries a durable logical-session id of its own.
461
+ // exactly why this is a ceiling and not a proof.
462
+ //
463
+ // #1641 — (b) never holds after /clear: resolveSessionId() mints the label
464
+ // anew as maxN+1, because the predecessor's own lock is counted (measured
465
+ // 2026-10-15: lock `…-session-1`, new label `…-session-2`). The process-bound
466
+ // alternative replaces (b)+(c) with ONE witness the lock itself carries:
467
+ // `claude_pid` equal to the Claude Code process this hook runs under, on this
468
+ // host (isSameClaudeProcess). It needs no repo-global artefact, so the
469
+ // ceiling above does not apply to it. (a) still gates it.
186
470
  const isSameLogicalReentry =
187
471
  typeof nativeSource === 'string' && SAME_LOGICAL_SESSION_SOURCES.has(nativeSource);
472
+ const ownProcessPredecessor =
473
+ acquireResult.ok !== true &&
474
+ acquireResult.reason === 'active' &&
475
+ isSameLogicalReentry &&
476
+ acquireResult.existingLock?.session_id !== sessionId &&
477
+ (await isSameClaudeProcess(acquireResult.existingLock, claudePid));
188
478
 
189
479
  const shouldForce =
190
480
  acquireResult.ok !== true && (
@@ -200,6 +490,7 @@ export async function bootstrapLock({
200
490
  // which this predicate could never force: one damaged file wedged every
201
491
  // new session in the repo.
202
492
  acquireResult.reason === 'corrupt' ||
493
+ ownProcessPredecessor ||
203
494
  (acquireResult.reason === 'active' &&
204
495
  acquireResult.existingLock &&
205
496
  (acquireResult.existingLock.session_id === sessionId ||
@@ -238,6 +529,9 @@ export async function bootstrapLock({
238
529
  (forcedFrom === 'corrupt' || (forcedFrom === 'stale-heartbeat' && priorOwner !== sessionId))
239
530
  ) {
240
531
  reclaimed = forcedFrom;
532
+ } else if (acquireResult?.ok === true && ownProcessPredecessor) {
533
+ // #1641: countable, so the rate of /clear takeovers can be measured.
534
+ reclaimed = 'own-process';
241
535
  }
242
536
  if (reclaimingCorrupt) {
243
537
  // The file's content is not echoed: it is unbounded and may be anything.
@@ -348,6 +642,10 @@ export async function bootstrapLock({
348
642
  typeof semanticSessionId === 'string' && semanticSessionId.length > 0
349
643
  ? semanticSessionId
350
644
  : (typeof baseLock.session_id === 'string' ? baseLock.session_id : sessionId),
645
+ // #1641: the process-bound witness a later /clear in this same process
646
+ // recognises its predecessor by (isSameClaudeProcess). Absent, not null,
647
+ // when the harness sets no CLAUDE_PID.
648
+ ...(typeof claudePid === 'number' ? { claude_pid: claudePid } : {}),
351
649
  };
352
650
 
353
651
  {
@@ -383,6 +681,15 @@ export async function bootstrapLock({
383
681
  } catch { /* best-effort — a missing proof never breaks session-start */ }
384
682
 
385
683
  // Step 3: best-effort observability breadcrumb.
684
+ // #1636 Pkt 2: a live peer that works here while its lock sits in another
685
+ // checkout is invisible to acquire(); record it (true/false/null) so its rate
686
+ // is measured before any conflict banner exists. Absent only if the reader
687
+ // itself cannot be loaded.
688
+ let foreignWorkRoot;
689
+ try {
690
+ const { foreignWorkRootIn } = await import('./hook-root.mjs');
691
+ foreignWorkRoot = foreignWorkRootIn(repoRoot, sessionId);
692
+ } catch { /* the measurement is best-effort */ }
386
693
  await emitBestEffort(_emitEventImpl, 'orchestrator.session.lock.acquired', {
387
694
  session_id: enriched.session_id,
388
695
  semantic_session_id: enriched.semantic_session_id,
@@ -391,11 +698,134 @@ export async function bootstrapLock({
391
698
  host: enriched.host,
392
699
  ttl_hours: enriched.ttl_hours,
393
700
  ...(reclaimed ? { reclaimed } : {}),
701
+ ...(foreignWorkRoot !== undefined ? { foreign_work_root: foreignWorkRoot } : {}),
394
702
  });
395
703
 
396
704
  return enriched;
397
705
  }
398
706
 
707
+ /**
708
+ * #1635 — on a lead yield, release the main checkout's lock IFF it carries our
709
+ * own raw `session_id`, or was written under our own Claude Code process on this
710
+ * host (#1641, isSameClaudeProcess); any other lock is never touched. The delete
711
+ * goes through session-lock.mjs `release()` under the LOCK's raw id with the
712
+ * repo's persisted owner proof, so release() re-checks both the id and the proof
713
+ * itself. Never throws; a failed release is one stderr WARN and changes nothing
714
+ * else — the caller still skips the acquire.
715
+ *
716
+ * @param {{ repoRoot: string, sessionId: string, claudePid?: number|null, releaseImpl?: Function }} args
717
+ * @returns {Promise<void>}
718
+ */
719
+ async function releaseOwnLockForLead({ repoRoot, sessionId, claudePid = null, releaseImpl }) {
720
+ let reason = null;
721
+ try {
722
+ const lockMod = await import('../../scripts/lib/session-lock.mjs');
723
+ const existing = lockMod.readLock({ repoRoot });
724
+ if (!existing || typeof existing.session_id !== 'string') return;
725
+ if (existing.session_id !== sessionId && !(await isSameClaudeProcess(existing, claudePid))) return;
726
+ const result = (releaseImpl ?? lockMod.release)({
727
+ sessionId: existing.session_id,
728
+ repoRoot,
729
+ proof: lockMod.loadOwnerProof({ repoRoot }),
730
+ });
731
+ if (!result || result.ok !== true) reason = result?.reason ?? 'unknown';
732
+ else if (result.deleted !== true) reason = result.reason ?? 'not-deleted';
733
+ else if (result.verified === false) reason = 'lock-still-present';
734
+ } catch (err) {
735
+ reason = err?.message ?? 'unknown';
736
+ }
737
+ if (reason !== null) {
738
+ process.stderr.write(
739
+ `⚠ lock-bootstrap: own session.lock could not be released for the fleet lead (${reason}) — it blocks the lead until its TTL\n`,
740
+ );
741
+ }
742
+ }
743
+
744
+ /**
745
+ * #1635 — the session id of a fleet lead whose ACTIVE lease keeps this session
746
+ * out of the main checkout, or null when the lock is taken as before.
747
+ *
748
+ * Non-null only when ALL hold: `nativeSource` is clear/resume; `repoRoot/.git`
749
+ * is a directory (the main worktree — a linked worktree or submodule has a
750
+ * `.git` FILE, no subprocess needed); `repoRoot/.orchestrator/runtime/fleet`
751
+ * is a directory (the lease is HOST-global, this marks the repos the fleet
752
+ * works in); the lease reads `active`; and its `session_id` is neither our
753
+ * raw id nor our predecessor's (the lead's own tab after /clear has a new raw
754
+ * id while the lease still carries the old one — it keeps the old behaviour).
755
+ *
756
+ * #1746 — "our predecessor's" needs a process witness. `predecessorSessionId`
757
+ * comes from current-session.json, which every session start in this checkout
758
+ * rewrites: once the lead started here LAST, its raw id was every other tab's
759
+ * "predecessor", and their /clear took the lock (LSO #778, twice on
760
+ * 2026-10-22). The id counts only when `predecessorProcess` names our own
761
+ * Claude Code process on this host (isSameClaudeProcess, the #1641 witness).
762
+ * Cost, accepted: the lead's own tab now also yields when the entry carries no
763
+ * witness — no readable CLAUDE_PID (Codex, Cursor) or a file written before
764
+ * #1746. Harmless: the lead works through its lease, not this lock. (When
765
+ * another session started in between, the tab yielded before #1746 already:
766
+ * the file then names that session.)
767
+ * Ceiling (BV-004): a dead lead process whose pid the OS recycled to our
768
+ * Claude process still reads as "ours" (the same residual isSameClaudeProcess
769
+ * names) and takes the lock as before #1746.
770
+ *
771
+ * Fail direction: every other outcome — no lease (no fleet: silent), an
772
+ * expired lease, an unreadable one (one stderr WARN), any exception — returns
773
+ * null, i.e. the pre-#1635 acquire path. It never blocks the session.
774
+ *
775
+ * @param {{ repoRoot: string, sessionId: string, predecessorSessionId: string|null, predecessorProcess: object|null, claudePid: number|null, nativeSource: string|null, readLease: Function }} args
776
+ * @returns {Promise<string|null>}
777
+ */
778
+ async function leadHoldingMainCheckout({ repoRoot, sessionId, predecessorSessionId, predecessorProcess, claudePid, nativeSource, readLease }) {
779
+ try {
780
+ if (typeof nativeSource !== 'string' || !LEAD_YIELD_SOURCES.has(nativeSource)) return null;
781
+ if (!isDirectory(path.join(repoRoot, '.git'))) return null;
782
+ if (!isDirectory(path.join(repoRoot, '.orchestrator', 'runtime', 'fleet'))) return null;
783
+ const verdict = await readLease();
784
+ if (verdict?.state === 'unreadable') {
785
+ process.stderr.write(
786
+ `⚠ lock-bootstrap: navigator lease unreadable (${verdict.reason ?? 'unknown'}) — lead occupancy not measurable, taking the lock as before\n`,
787
+ );
788
+ return null;
789
+ }
790
+ if (verdict?.state !== 'active') return null;
791
+ const leadId = verdict.lease?.session_id;
792
+ if (typeof leadId !== 'string' || leadId.length === 0) return null;
793
+ if (leadId === sessionId) return null;
794
+ if (leadId === predecessorSessionId && (await isSameClaudeProcess(predecessorProcess, claudePid))) return null;
795
+ return leadId;
796
+ } catch {
797
+ return null;
798
+ }
799
+ }
800
+
801
+ /**
802
+ * #1747 — the session id of an ACTIVE fleet-lead lease, or null for every
803
+ * other outcome (none, expired, unreadable, a throwing reader). The marker
804
+ * was written only in the main checkout under a foreign lead, so the lease
805
+ * alone decides here. Never throws.
806
+ *
807
+ * @param {Function} readLease
808
+ * @returns {Promise<string|null>}
809
+ */
810
+ async function activeLeadSessionId(readLease) {
811
+ try {
812
+ const verdict = await readLease();
813
+ const leadId = verdict?.state === 'active' ? verdict.lease?.session_id : null;
814
+ return typeof leadId === 'string' && leadId.length > 0 ? leadId : null;
815
+ } catch {
816
+ return null;
817
+ }
818
+ }
819
+
820
+ /** True when `p` is a directory itself (lstat: a symlink does not count). Never throws. */
821
+ function isDirectory(p) {
822
+ try {
823
+ return fs.lstatSync(p).isDirectory();
824
+ } catch {
825
+ return false;
826
+ }
827
+ }
828
+
399
829
  /**
400
830
  * Emit one event; every failure is swallowed so a missing events module never
401
831
  * breaks the hook (observability is best-effort).
@@ -52,6 +52,9 @@ export const GENERATED_IGNORE = [
52
52
  '!*/',
53
53
  '!/bootstrap.lock',
54
54
  '!/metrics/learnings.jsonl',
55
+ // #1516: the inbox-role store and its receipts travel with learnings.jsonl.
56
+ '!/metrics/learnings-inbox.jsonl',
57
+ '!/metrics/learnings-inbox-ingested.jsonl',
55
58
  '!/metrics/sessions.jsonl',
56
59
  '!/metrics/README.md',
57
60
  '!/policy/quality-gates.json',
@@ -27,7 +27,7 @@
27
27
  * claim, with an `occurrences` count (#1198: the fleet's worst repo held
28
28
  * 3,360 records over 205 distinct `claim_text` values = duplication
29
29
  * factor 16.4, measured 2026-09-06 over
30
- * `extern/aiat-barrierefrei-engine/.orchestrator/metrics/events.jsonl`).
30
+ * a consumer repo's `.orchestrator/metrics/events.jsonl`).
31
31
  *
32
32
  * @module hooks/_lib/subagent-transcript
33
33
  */
@@ -495,7 +495,7 @@ const MAX_OBSERVATIONS = 50;
495
495
  * of them was measured as a live false-positive class.
496
496
  *
497
497
  * Measured 2026-09-06 over the 205 DISTINCT `claim_text` values in
498
- * `~/Projects/extern/aiat-barrierefrei-engine/.orchestrator/metrics/events.jsonl`
498
+ * a consumer repo's `.orchestrator/metrics/events.jsonl`
499
499
  * (the fleet's worst-affected repo; command in the module test header):
500
500
  * 201/205 re-fire on the pre-#1218 matcher, of which 35 are markdown table
501
501
  * rows and 9 are ATX headings — 44 firings, 21.9% of the corpus, with a true
@@ -88,11 +88,11 @@
88
88
  * ## Named ceilings (BV-004)
89
89
  *
90
90
  * - A create call reached only through a command substitution
91
- * (`x=$(glab issue create …)`) is NOT matched: the substitution body never
92
- * becomes a segment of its own. Unchanged since before #1106. Likewise a
93
- * create carried as an interpreter PAYLOAD (`bash -c 'glab issue create'`)
94
- * — `resolveSegmentVerb` reports the payload string, but this matcher does
95
- * not recurse into it. Revisit either if the shape shows up in the overflow
91
+ * (`x=$(glab issue create …)`) IS matched since #1667: the shared lexer
92
+ * hoists the substitution body into a segment of its own. A create carried
93
+ * as an interpreter PAYLOAD (`bash -c 'glab issue create'`) is still NOT
94
+ * matched — `resolveSegmentVerb` reports the payload string, but this
95
+ * matcher does not recurse into it. Revisit if the shape shows up in the overflow
96
96
  * triage of a per-session counter file
97
97
  * (`.orchestrator/runtime/issue-budget/<hash>.json`, #1141) or in a
98
98
  * transcript census of real create calls.
@@ -115,11 +115,10 @@
115
115
  * `hooks/pre-bash-issue-budget.mjs` treats it exactly like a loop body —
116
116
  * deny in `strict`, report the undercount in `warn`. The remaining ceiling
117
117
  * is the option grammar, named at {@link xargsDrivenRemainder}.
118
- * - A paren glued to the verb (`(glab issue create …)`) is not reached: it
119
- * lexes as the single word `(glab`. This is `command-blocker.mjs`'s own
120
- * named ceiling on `COMMAND_POSITION_KEYWORDS` (#1145), inherited here
121
- * rather than re-patched — peeling the paren belongs in the lexer, whose
122
- * tokens five consumers read positionally.
118
+ * - A subshell paren before the verb (`(glab issue create …)`) IS reached
119
+ * since #1667: the lexer turns an unquoted word-initial `(` and its `)`
120
+ * into statement breaks — the fix landed in the lexer, as this ceiling
121
+ * asked, not as a re-patch here.
123
122
  * - {@link matchesBypass} still compares from token index 0, NOT from the
124
123
  * resolved verb index. So a wrapped create (`nohup gh pr create --dry-run`)
125
124
  * is gated even when `gh pr create --dry-run` is a bypass entry. That is
@@ -514,9 +513,10 @@ const CAT_SUBST_RE = /^\$\(cat\s+(\S+)\)$/;
514
513
  /**
515
514
  * Read the value of a flag in `names` (`--x v`, `--x=v`, `-x v`). When the flag
516
515
  * repeats, the LAST value wins — the pflag semantics `gh`/`glab` apply.
517
- * An UNQUOTED `$(cat p)` is split by the lexer into `$(cat` + `p)`; that pair is
518
- * rejoined so the substitution survives, in both the `--x $(cat p)` and the
519
- * `--x=$(cat p)` spelling.
516
+ * An UNQUOTED `$(cat p)` reaches this function as ONE token: since #1667 the
517
+ * lexer keeps a substitution's raw text in the outer word (and hoists its body
518
+ * as a statement of its own), so `--x $(cat p)` and `--x=$(cat p)` need no
519
+ * rejoining.
520
520
  *
521
521
  * @param {Array<{ text: string, quoted: boolean }>} tokens
522
522
  * @param {string[]} names
@@ -537,10 +537,6 @@ function flagValue(tokens, names) {
537
537
  } else {
538
538
  continue;
539
539
  }
540
- if (value === '$(cat' && typeof tokens[i + 1]?.text === 'string') {
541
- value = `$(cat ${tokens[i + 1].text}`;
542
- i += 1;
543
- }
544
540
  found = value;
545
541
  break;
546
542
  }
@@ -31,6 +31,7 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
31
31
 
32
32
  import { getProjectDir } from '../scripts/lib/platform.mjs';
33
33
  import { atomicMutateJson } from './_lib/atomic-json.mjs';
34
+ import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
34
35
 
35
36
  // ---------------------------------------------------------------------------
36
37
  // Constants
@@ -88,7 +89,10 @@ async function main() {
88
89
  new_cwd: newCwd,
89
90
  };
90
91
 
91
- const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
92
+ // #1605 B5 — annotate THIS session's current-session.json where
93
+ // on-session-start.mjs put it (the entered worktree after a /clear there),
94
+ // found by the lock carrying the raw payload id; launch dir otherwise.
95
+ const sessionFile = path.join(resolveOwnLockRoot(input, getProjectDir()), '.orchestrator', 'current-session.json');
92
96
 
93
97
  const result = await atomicMutateJson(sessionFile, {}, (current) => {
94
98
  const existing = Array.isArray(current.cwd_changes)
@@ -317,9 +317,10 @@ async function main() {
317
317
  // (#1194).
318
318
  //
319
319
  // CEILING (BV-004): on a harness that exports no session env var and puts no
320
- // `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are
321
- // empty, so this gate is permanently `'unknown'` = enforce = pre-#1153
322
- // behaviour there. Revisit when those hook payloads carry a session id.
320
+ // `session_id` in the hook payload (Cursor today) both tiers are empty, so
321
+ // this gate is permanently `'unknown'` = enforce = pre-#1153 behaviour there.
322
+ // Codex CLI does send `session_id` (measured 2026-10-20, codex-cli 0.160.1,
323
+ // #1691), so a manifest bound to another id classifies `'foreign'` there.
323
324
  {
324
325
  // `ownIds` is the process-local set built above G3.
325
326
  const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);