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
@@ -14,22 +14,23 @@
14
14
  * G3 file_path present
15
15
  * G4 file extension filter — only .ts|.tsx|.js|.jsx|.mjs|.cjs
16
16
  * G5 gate check — wave-scope.json.gates['post-edit-validate'] === false → exit 0
17
- * G6 resolve typecheck command: Session Config → tsgo → tsc → npx tsc → skip
18
- * G7 run typecheck with 2s AbortController timeout
17
+ * G6 resolve typecheck command: Session Config → tsgo → tsc → local tsc → skip
18
+ * G7 run typecheck with 2s process-group timeout
19
19
  * G8 emit JSONL result to stderr
20
20
  *
21
- * Output (stderr): {"check":"typecheck","status":"pass|fail|skip","file":"<rel>","duration_ms":<int>}
21
+ * Output (stderr): {"check":"typecheck","status":"pass|fail|timeout|skip","file":"<rel>","duration_ms":<int>}
22
22
  * Exit codes: 0 — always (PostToolUse hooks are informational)
23
23
  *
24
24
  * SECURITY notes:
25
25
  * REQ-01 top-level try/catch → silent exit 0 on unexpected error (never blocks)
26
- * REQ-02 typecheck process uses AbortController (not shell `timeout` — cross-platform)
26
+ * REQ-02 typecheck runs the intact shell command; timeout kills its process tree
27
27
  * REQ-03 relative path computed via path.relative(projectRoot, absFilePath)
28
28
  */
29
29
 
30
30
  import path from 'node:path';
31
+ import { existsSync } from 'node:fs';
31
32
  import { spawnSync } from 'node:child_process';
32
- import { performance } from 'node:perf_hooks';
33
+ import { spawnInGroup } from '../scripts/lib/process-group.mjs';
33
34
 
34
35
  import { shouldRunHook } from './_lib/profile-gate.mjs';
35
36
  import { isMainModule } from '../scripts/lib/is-main-module.mjs';
@@ -54,9 +55,9 @@ const TS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
54
55
  /**
55
56
  * Emit the JSONL result line to stderr.
56
57
  * @param {string} file - relative file path
57
- * @param {'pass'|'fail'|'skip'} status
58
+ * @param {'pass'|'fail'|'timeout'|'skip'} status
58
59
  * @param {number} [duration_ms] - omit for skip
59
- * @param {string} [reason] - human-readable reason for skip or fail
60
+ * @param {string} [reason] - human-readable reason for skip, timeout or fail
60
61
  * @param {string} [remediation] - actionable fix hint for fail status only
61
62
  */
62
63
  function emitResult(file, status, duration_ms, reason, remediation) {
@@ -69,10 +70,10 @@ function emitResult(file, status, duration_ms, reason, remediation) {
69
70
 
70
71
  /**
71
72
  * Try to resolve the typecheck command.
72
- * Order: Session Config typecheck-command → tsgo → tsc → npx tsc → null
73
+ * Order: Session Config typecheck-command → tsgo → tsc → local tsc → null
73
74
  *
74
75
  * @param {string} projectRoot
75
- * @returns {Promise<{cmd: string, args: string[]}|null>}
76
+ * @returns {Promise<string|string[]|null>}
76
77
  */
77
78
  async function resolveTypecheckCommand(projectRoot) {
78
79
  // 1. Session Config typecheck-command
@@ -81,8 +82,8 @@ async function resolveTypecheckCommand(projectRoot) {
81
82
  const config = parseSessionConfig(md);
82
83
  const cmd = config['typecheck-command'];
83
84
  if (cmd && typeof cmd === 'string') {
84
- const parts = cmd.trim().split(/\s+/);
85
- return { cmd: parts[0], args: parts.slice(1) };
85
+ const command = cmd.trim();
86
+ return command === 'skip' ? null : command || null;
86
87
  }
87
88
  } catch {
88
89
  // config unavailable — fall through
@@ -90,15 +91,16 @@ async function resolveTypecheckCommand(projectRoot) {
90
91
 
91
92
  // 2. tsgo
92
93
  const tsgoParts = _tryWhich('tsgo');
93
- if (tsgoParts) return { cmd: tsgoParts, args: ['--noEmit'] };
94
+ if (tsgoParts) return 'tsgo --noEmit';
94
95
 
95
96
  // 3. tsc
96
97
  const tscParts = _tryWhich('tsc');
97
- if (tscParts) return { cmd: tscParts, args: ['--noEmit'] };
98
+ if (tscParts) return 'tsc --noEmit';
98
99
 
99
- // 4. npx tsc
100
- const npxParts = _tryWhich('npx');
101
- if (npxParts) return { cmd: npxParts, args: ['tsc', '--noEmit'] };
100
+ // 4. Only an installed project compiler; npx may install the unrelated
101
+ // npm package `tsc` in projects without TypeScript (#1612).
102
+ const localTsc = path.join(projectRoot, 'node_modules', 'typescript', 'bin', 'tsc');
103
+ if (existsSync(localTsc)) return [process.execPath, localTsc, '--noEmit'];
102
104
 
103
105
  return null;
104
106
  }
@@ -111,9 +113,11 @@ async function resolveTypecheckCommand(projectRoot) {
111
113
  * @returns {string|null}
112
114
  */
113
115
  function _tryWhich(name) {
114
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
116
+ const whichCmd = process.platform === 'win32' ? 'where.exe' : 'which';
117
+ // Two lookups get at most 1s total; revisit together with the outer
118
+ // 5s hook budget if a host needs slower executable discovery.
115
119
  try {
116
- const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout: 2000 });
120
+ const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout: 500 });
117
121
  if (result.status === 0 && result.stdout.trim()) return name;
118
122
  } catch {
119
123
  // ignore
@@ -122,43 +126,32 @@ function _tryWhich(name) {
122
126
  }
123
127
 
124
128
  /**
125
- * Run the typecheck command with a 2s AbortController timeout.
126
- *
127
- * @param {string} cmd
128
- * @param {string[]} args
129
- * @returns {{status: 'pass'|'fail', duration_ms: number, reason?: string}}
129
+ * Configured commands retain shell semantics; the local compiler uses Node argv.
130
+ * Keep cleanup short for hooks.json's 5s budget; Windows skips CIM verification.
131
+ * @param {string|string[]} cmd
132
+ * @param {string} projectRoot
133
+ * @returns {Promise<{status: 'pass'|'fail'|'timeout', duration_ms: number, reason?: string}>}
130
134
  */
131
- function runTypecheck(cmd, args) {
132
- const controller = new AbortController();
133
- const timer = setTimeout(() => controller.abort(), 2000);
134
-
135
- const start = performance.now();
136
- let spawnResult;
137
- try {
138
- spawnResult = spawnSync(cmd, args, {
139
- signal: controller.signal,
140
- encoding: 'utf8',
141
- stdio: ['ignore', 'pipe', 'pipe'],
142
- timeout: 2000,
143
- });
144
- } finally {
145
- clearTimeout(timer);
146
- }
147
-
148
- const duration_ms = Math.max(0, Math.round(performance.now() - start));
149
-
150
- // AbortError or SIGTERM from timeout
151
- if (spawnResult.error?.name === 'AbortError' || spawnResult.signal === 'SIGTERM') {
152
- return { status: 'fail', duration_ms, reason: 'typecheck timed out after 2s' };
135
+ async function runTypecheck(cmd, projectRoot) {
136
+ const result = await spawnInGroup(cmd, {
137
+ cwd: projectRoot,
138
+ timeoutMs: 2000,
139
+ // Hook-specific ceiling: escalate quickly so an uncooperative compiler
140
+ // cannot consume the outer 5s hook budget. Revisit if compiler cleanup
141
+ // needs more than 100ms; the hook's outer budget must change with it.
142
+ killGraceMs: 100,
143
+ verifyWaitMs: 100,
144
+ // Two PowerShell startups would exceed the remaining hook budget (R1-04).
145
+ verifyTree: false,
146
+ });
147
+ const duration_ms = Math.max(0, Math.round(result.durationMs));
148
+ if (result.timedOut) {
149
+ return { status: 'timeout', duration_ms, reason: 'typecheck timed out after 2s' };
153
150
  }
154
-
155
- if (spawnResult.status !== 0) {
156
- // Capture the first non-empty line of stderr/stdout as a compact reason.
157
- const combined = ((spawnResult.stderr ?? '') + (spawnResult.stdout ?? '')).trim();
158
- const firstLine = combined.split('\n').find((l) => l.trim()) ?? 'typecheck exited non-zero';
151
+ if (result.exitCode !== 0) {
152
+ const firstLine = result.fullOutput.split('\n').find((line) => line.trim()) ?? 'typecheck exited non-zero';
159
153
  return { status: 'fail', duration_ms, reason: firstLine.slice(0, 256) };
160
154
  }
161
-
162
155
  return { status: 'pass', duration_ms };
163
156
  }
164
157
 
@@ -216,7 +209,7 @@ async function main() {
216
209
  }
217
210
 
218
211
  // G7: run typecheck with 2s timeout (REQ-02)
219
- const { status, duration_ms, reason } = runTypecheck(tc.cmd, tc.args);
212
+ const { status, duration_ms, reason } = await runTypecheck(tc, projectRoot);
220
213
 
221
214
  // G8: emit JSONL result to stderr
222
215
  const remediation =
@@ -226,7 +226,7 @@ const AGENT_DESCRIPTION_MAX = 120;
226
226
  * Resolve WHO made the claim, and say where the answer came from (#1218).
227
227
  *
228
228
  * Measured 2026-09-06 over the fleet's worst-affected repo
229
- * (`extern/aiat-barrierefrei-engine`, 3,360 `discovery_validator_violation`
229
+ * (a consumer repo, 3,360 `discovery_validator_violation`
230
230
  * records): `agent` was the literal string `"unknown"` on 3,026 of them
231
231
  * (90.1%), and NOT ONE record carried an `agent_id`. Both the sidecar-meta
232
232
  * fallback and the `agent_id` field landed in the same commit (936dae8a,
@@ -364,7 +364,7 @@ function dedupSentinelPath({ projectRoot, sessionId, agentId, claimText }) {
364
364
  * same sentence re-asserted by a second agent of the same session is the
365
365
  * same finding, and writing it twice is what produced a duplication factor
366
366
  * of 16.4 (3,360 records over 205 distinct `claim_text` values, measured
367
- * 2026-09-06 in `extern/aiat-barrierefrei-engine`).
367
+ * 2026-09-06 in a consumer repo).
368
368
  *
369
369
  * Returns null without a session id — a claim that cannot be bound to a session
370
370
  * must not collide with an unrelated one under a global "unknown" key, so it is
@@ -74,6 +74,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
74
74
  import { isMainModule } from '../scripts/lib/is-main-module.mjs';
75
75
 
76
76
  import { getProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
77
+ import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
77
78
  import { emitEvent } from '../scripts/lib/events.mjs';
78
79
  import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
79
80
  import {
@@ -356,13 +357,16 @@ async function main() {
356
357
  };
357
358
 
358
359
  // Two roots, by writer (#1511 point a). `current-session.json` and
359
- // `session.lock` stay at the LAUNCH dir: on-session-start.mjs writes both at
360
- // `resolveProjectDir()`, and a reader elsewhere finds no record. Everything
360
+ // `session.lock` are read where on-session-start.mjs wrote them: the launch
361
+ // dir, or the entered worktree after a /clear, compact or resume there
362
+ // (#1605), found by the lock carrying this session's raw id
363
+ // (`resolveOwnLockRoot`); a reader elsewhere finds no record. Everything
361
364
  // about the WAVE — its manifest, its start sha, the files it changed — is read
362
365
  // at the SESSION root: after EnterWorktree the launch dir is a different
363
366
  // checkout, whose HEAD and working tree say nothing about this wave.
364
367
  const sessionRoot = resolveSessionRoot(input?.cwd);
365
- const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
368
+ const lockRoot = resolveOwnLockRoot(input, getProjectDir());
369
+ const sessionFile = path.join(lockRoot, '.orchestrator', 'current-session.json');
366
370
 
367
371
  const lastBatchResult = await atomicMutateJson(sessionFile, {}, (current) => ({
368
372
  ...current,
@@ -391,7 +395,7 @@ async function main() {
391
395
  const sessionId = await resolveSessionIdForHeartbeat(input, sessionFile);
392
396
  if (typeof sessionId === 'string' && sessionId.length > 0) {
393
397
  const { updateHeartbeat } = await import('../scripts/lib/session-lock.mjs');
394
- updateHeartbeat({ repoRoot: getProjectDir(), sessionId });
398
+ updateHeartbeat({ repoRoot: lockRoot, sessionId });
395
399
  }
396
400
  } catch { /* best effort — hook must remain non-blocking */ }
397
401
 
@@ -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
@@ -178,7 +179,10 @@ async function main() {
178
179
  exit_code: exitCode,
179
180
  };
180
181
 
181
- const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
182
+ // #1605 B5 — annotate THIS session's current-session.json where
183
+ // on-session-start.mjs put it (the entered worktree after a /clear there),
184
+ // found by the lock carrying the raw payload id; launch dir otherwise.
185
+ const sessionFile = path.join(resolveOwnLockRoot(input, getProjectDir()), '.orchestrator', 'current-session.json');
182
186
 
183
187
  const result = await atomicMutateJson(sessionFile, {}, (current) => {
184
188
  const existing = Array.isArray(current.corrective_context)
@@ -35,6 +35,10 @@
35
35
  * redirect class answers "what does this command WRITE"; nothing
36
36
  * answered "what does it REMOVE", so `rm -f <ledger>` was allowed
37
37
  * while `> <ledger>` was denied — 53,896 lines of telemetry lost.
38
+ * rule.type === 'hooks-path-missing' (#1732): decided by the directory
39
+ * of each `git commit`/`git push` (findHookedGitCalls) and the repo's
40
+ * relative core.hooksPath (missingWorktreeHooks) — a linked worktree
41
+ * without the generated hooks dir runs no hook at all, silently.
38
42
  * G6 no match → exit 0
39
43
  *
40
44
  * Telemetry (Epic #803 process-safety dimension): best-effort
@@ -45,6 +49,7 @@
45
49
  * finalized; a telemetry failure never changes the guard's decision.
46
50
  */
47
51
 
52
+ import { execFileSync } from 'node:child_process';
48
53
  import fs from 'node:fs';
49
54
  import os from 'node:os';
50
55
  import path from 'node:path';
@@ -326,7 +331,7 @@ async function blockCommand(pattern, ruleId, rationale, command, sessionId) {
326
331
  */
327
332
  async function isGitStashNonEmpty(projectDir) {
328
333
  try {
329
- const { $: zx$ } = await import('zx');
334
+ const { $: zx$ } = await import('../scripts/lib/zx.mjs');
330
335
  zx$.verbose = false;
331
336
  zx$.quiet = true;
332
337
  const cwd = projectDir || process.cwd();
@@ -479,6 +484,181 @@ function commandHasRecursiveForceRm(command) {
479
484
  return false;
480
485
  }
481
486
 
487
+ /** #1732 — git subcommands that run the repo's own hooks: pre-commit/commit-msg and pre-push. */
488
+ const HOOKED_GIT_SUBCOMMANDS = new Set(['commit', 'push']);
489
+ /** git global options that take their value as the NEXT token (`git -c k=v push`). */
490
+ const GIT_OPTIONS_WITH_VALUE = new Set(['-c', '--namespace', '--config-env', '--attr-source']);
491
+ /** Options and env prefixes after which the guard cannot tell which repo or hooks git uses. */
492
+ const GIT_UNKNOWABLE_OPTION = /^--(git-dir|work-tree)(=|$)/;
493
+ const GIT_UNKNOWABLE_ENV = /^GIT_(DIR|WORK_TREE|COMMON_DIR|CONFIG\w*)=/;
494
+
495
+ /**
496
+ * Resolve a literal path operand against `base`, or null when the shell would
497
+ * expand it into something the guard cannot read (`$X`, a substitution, `cd -`).
498
+ *
499
+ * @param {string|null} base
500
+ * @param {{ text: string } | undefined} tok
501
+ * @returns {string|null}
502
+ */
503
+ function literalDir(base, tok) {
504
+ if (base === null || tok === undefined) return null;
505
+ const text = tok.text;
506
+ if (text === '-' || /[$`*?[]/.test(text)) return null;
507
+ if (text === '~' || text.startsWith('~/')) return path.join(os.homedir(), text.slice(1));
508
+ if (text.startsWith('~')) return null;
509
+ return path.resolve(base, text);
510
+ }
511
+
512
+ /**
513
+ * Every `git commit` / `git push` in the command with the directory it runs in
514
+ * (#1732). The directory starts at `cwd` and follows literal `cd <dir>` segments
515
+ * and `git -C <dir>`. A `cd` the guard cannot read (variable, `cd -`,
516
+ * `pushd`/`popd`) turns the directory unknown (`dir: null`) for everything after
517
+ * it. The tokenizer appends command-substitution bodies after a `substitution`
518
+ * marker and frames `( … )` with `subshell` markers, so only the tokens before
519
+ * the first substitution marker are the main shell's, and a `cd` after the
520
+ * first subshell marker turns the directory unknown (`(cd /wt && make) && git
521
+ * push` pushes from the start directory). A `)` inside a quoted commit message
522
+ * changes nothing. The caller never judges an unknown directory — unknown is
523
+ * "not judged", never a hit. An explicit skip (`--no-verify`, `commit -n` also
524
+ * inside a short-flag group, `HUSKY=0`) is not judged either: the skip this rule
525
+ * catches is the silent one.
526
+ *
527
+ * @param {string} command
528
+ * @param {string} cwd
529
+ * @returns {Array<{ subcommand: string, dir: string|null }>}
530
+ */
531
+ function findHookedGitCalls(command, cwd) {
532
+ const calls = [];
533
+ const tokens = blocker.tokenizeCommand(command);
534
+ const substitutionAt = tokens.findIndex((t) => t.operator === 'substitution');
535
+ const mainShell = substitutionAt === -1 ? tokens : tokens.slice(0, substitutionAt);
536
+ const subshellAt = mainShell.findIndex((t) => t.operator === 'subshell');
537
+ const parts = subshellAt === -1
538
+ ? [{ tokens: mainShell, cdKnown: true }]
539
+ : [{ tokens: mainShell.slice(0, subshellAt), cdKnown: true }, { tokens: mainShell.slice(subshellAt), cdKnown: false }];
540
+ let dir = cwd;
541
+ for (const part of parts) {
542
+ for (const segment of blocker.splitChainSegments(part.tokens)) {
543
+ const { verb, index } = blocker.resolveSegmentVerb(segment);
544
+ if (verb === 'pushd' || verb === 'popd') { dir = null; continue; }
545
+ if (verb === 'cd') {
546
+ const operands = segment.slice(index + 1).filter((t) => t.quoted || !/^-[LPe@]+$/.test(t.text));
547
+ if (!part.cdKnown) dir = null;
548
+ else if (operands.length === 0) dir = os.homedir();
549
+ else dir = operands.length === 1 ? literalDir(dir, operands[0]) : null;
550
+ continue;
551
+ }
552
+ if (verb !== 'git' || segment[index].quoted) continue;
553
+ const call = hookedGitCall(segment, index, dir);
554
+ if (call !== null) calls.push(call);
555
+ }
556
+ }
557
+ return calls;
558
+ }
559
+
560
+ /** Short `git commit` options that take a value: in `-mn` the `n` is the message. */
561
+ const COMMIT_SHORT_WITH_VALUE = 'mFCctSu';
562
+
563
+ /**
564
+ * Whether a short-flag group of `git commit` carries `-n` (`-nm x` does, `-mn` does not).
565
+ *
566
+ * @param {string} group
567
+ * @returns {boolean}
568
+ */
569
+ function commitGroupHasNoVerify(group) {
570
+ for (const ch of group.slice(1)) {
571
+ if (ch === 'n') return true;
572
+ if (COMMIT_SHORT_WITH_VALUE.includes(ch)) return false;
573
+ }
574
+ return false;
575
+ }
576
+
577
+ /**
578
+ * Read one `git` segment: its subcommand and the directory it runs in, or null
579
+ * when it is no hooked call or skips hooks explicitly.
580
+ *
581
+ * @param {Array<{ text: string, quoted: boolean }>} segment
582
+ * @param {number} index - the `git` token
583
+ * @param {string|null} dir - the directory before git's own `-C`
584
+ * @returns {{ subcommand: string, dir: string|null } | null}
585
+ */
586
+ function hookedGitCall(segment, index, dir) {
587
+ const env = segment.slice(0, index);
588
+ if (env.some((t) => t.text === 'HUSKY=0')) return null;
589
+ let gitDir = env.some((t) => GIT_UNKNOWABLE_ENV.test(t.text)) ? null : dir;
590
+ let i = index + 1;
591
+ for (; i < segment.length; i++) {
592
+ const t = segment[i];
593
+ if (t.quoted || !t.text.startsWith('-')) break;
594
+ if (t.text === '-C') { gitDir = literalDir(gitDir, segment[++i]); continue; }
595
+ if (GIT_UNKNOWABLE_OPTION.test(t.text)) {
596
+ gitDir = null;
597
+ if (!t.text.includes('=')) i++;
598
+ continue;
599
+ }
600
+ if (GIT_OPTIONS_WITH_VALUE.has(t.text)) {
601
+ const value = segment[++i]?.text ?? '';
602
+ if (t.text === '-c' && /^core\.hookspath=/i.test(value)) gitDir = null;
603
+ }
604
+ }
605
+ const sub = segment[i];
606
+ if (sub === undefined || sub.quoted || !HOOKED_GIT_SUBCOMMANDS.has(sub.text)) return null;
607
+ const flags = segment.slice(i + 1).filter((t) => !t.quoted).map((t) => t.text);
608
+ if (flags.includes('--no-verify')) return null;
609
+ if (sub.text === 'commit' && flags.some((f) => /^-[a-zA-Z]+$/.test(f) && commitGroupHasNoVerify(f))) return null;
610
+ return { subcommand: sub.text, dir: gitDir };
611
+ }
612
+
613
+ /**
614
+ * Run git read-only in `dir`; trimmed stdout, or null on any failure.
615
+ *
616
+ * @param {string} dir
617
+ * @param {string[]} args
618
+ * @returns {string|null}
619
+ */
620
+ function gitRead(dir, args) {
621
+ try {
622
+ const out = execFileSync('git', ['-C', dir, ...args], {
623
+ encoding: 'utf8',
624
+ stdio: ['ignore', 'pipe', 'ignore'],
625
+ timeout: 3000,
626
+ }).trim();
627
+ return out.length > 0 ? out : null;
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+
633
+ /**
634
+ * The silent hookless case of #1732: a LINKED worktree whose relative
635
+ * `core.hooksPath` (husky: `.husky/_`) does not exist in it, while the main
636
+ * checkout has it. `.husky/_` is generated by `husky` (npm `prepare`), and
637
+ * `ignore-scripts=true` keeps `npm ci` from creating it in a fresh worktree, so
638
+ * every commit and push there runs no hook and says nothing (measured
639
+ * 2026-10-22: a push went out without its pre-push full gate).
640
+ *
641
+ * Narrow on purpose — each condition that does not hold is an allow: no
642
+ * hooksPath, an absolute or `~` one (a global setting), the directory exists, the
643
+ * main checkout has none either (the repo installs no hooks), or this IS the main
644
+ * checkout. Any git failure is an allow.
645
+ *
646
+ * @param {string} dir
647
+ * @returns {{ hooksPath: string, worktree: string } | null}
648
+ */
649
+ function missingWorktreeHooks(dir) {
650
+ const hooksPath = gitRead(dir, ['config', '--get', 'core.hooksPath']);
651
+ if (hooksPath === null || path.isAbsolute(hooksPath) || hooksPath.startsWith('~')) return null;
652
+ const worktree = gitRead(dir, ['rev-parse', '--show-toplevel']);
653
+ if (worktree === null || fs.existsSync(path.join(worktree, hooksPath))) return null;
654
+ const commonDir = gitRead(dir, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
655
+ if (commonDir === null) return null;
656
+ const mainCheckout = path.dirname(commonDir);
657
+ if (path.resolve(mainCheckout) === path.resolve(worktree)) return null;
658
+ if (!fs.existsSync(path.join(mainCheckout, hooksPath))) return null;
659
+ return { hooksPath, worktree };
660
+ }
661
+
482
662
  /** Probe operator per redirect mode — see findMatchedRedirectEntry. */
483
663
  const REDIRECT_PROBE_OPS = { truncate: '>', append: '>>', read: '<' };
484
664
 
@@ -939,6 +1119,31 @@ async function main() {
939
1119
  continue; // unreachable (blockCommand never returns) — kept for clarity
940
1120
  }
941
1121
 
1122
+ // #1732 — a `git commit`/`git push` that would run WITHOUT the repo's hooks,
1123
+ // because a linked worktree lacks the generated hooks directory. Decided by
1124
+ // the git call's directory and the repo's hooksPath, never by the generic
1125
+ // pattern path below (its `pattern` is a label). An unknown directory is
1126
+ // not judged.
1127
+ if (rule.type === 'hooks-path-missing') {
1128
+ const cwd = typeof input.cwd === 'string' && input.cwd.length > 0 ? input.cwd : process.cwd();
1129
+ let hit = null;
1130
+ for (const call of findHookedGitCalls(command, cwd)) {
1131
+ if (call.dir === null) continue;
1132
+ const missing = missingWorktreeHooks(call.dir);
1133
+ if (missing) { hit = { ...missing, subcommand: call.subcommand }; break; }
1134
+ }
1135
+ if (!hit) continue;
1136
+ const label = `git ${hit.subcommand} in ${hit.worktree} — core.hooksPath ${hit.hooksPath} is missing there`;
1137
+ if (severity !== 'block') {
1138
+ const msg = `pre-bash-destructive-guard: ${label} (rule: ${id}) — ${rationale}`;
1139
+ process.stderr.write(`⚠ ${msg}\n`);
1140
+ notices.push(msg);
1141
+ continue;
1142
+ }
1143
+ await blockCommand(label, id, rationale, command, sessionId);
1144
+ continue; // unreachable (blockCommand never returns) — kept for clarity
1145
+ }
1146
+
942
1147
  // The rm-rf-destructive rule also fires for recursive+force rm flag variants
943
1148
  // the literal "rm -rf" pattern misses (`rm -r -f`, `rm -fr`) — #641 gap closure.
944
1149
  const matched = id === 'rm-rf-destructive'
@@ -1020,9 +1225,13 @@ async function main() {
1020
1225
  // crash with 0 bytes of stdout — indistinguishable from an allow, and
1021
1226
  // therefore invisible. Now it exits 0 (still fail-open, so a broken module
1022
1227
  // cannot brick the session) but SAYS SO, loudly, once per session.
1023
- // 2. RUNTIME failure inside `main()`: pre-existing behaviour, unchanged.
1024
- // The guard armed and then tripped over a specific command; that is a
1025
- // narrower blast radius and keeps its historical `internal error` line.
1228
+ // 2. RUNTIME failure inside `main()` (#1671 item 1): the guard armed and then
1229
+ // tripped over a specific command. That command is DENIED (fail-closed),
1230
+ // like enforce-commands / enforce-scope (SECURITY-REQ-01). It used to exit
1231
+ // 0 = ALLOW, so any future crash on an input shape (the measured trigger was
1232
+ // a tokenizer RangeError on a long command) silently opened the guard for
1233
+ // exactly the inputs it cannot parse. The blast radius is one command, and
1234
+ // the reason tells the operator how out: split or simplify it.
1026
1235
  // ---------------------------------------------------------------------------
1027
1236
  try {
1028
1237
  await bootstrap();
@@ -1054,11 +1263,14 @@ if (isMainModule(import.meta.url)) {
1054
1263
  // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
1055
1264
  if (!shouldRunHook('pre-bash-destructive-guard')) process.exit(0);
1056
1265
 
1057
- // Top-level error handler — never let exit 1 leak
1266
+ // Top-level error handler — never let exit 1 leak, never fail open (#1671).
1267
+ // emitDeny is bound here: bootstrap() succeeded, and it never throws.
1058
1268
  main().catch((e) => {
1059
- process.stderr.write(
1060
- `⚠ pre-bash-destructive-guard: internal error — ${e?.message || e}\n`
1269
+ const detail = String(e?.message || e).split('\n')[0];
1270
+ process.stderr.write(`⚠ pre-bash-destructive-guard: internal error — ${detail}\n`);
1271
+ emitDeny(
1272
+ `pre-bash-destructive-guard hit an internal error and blocked this command for safety (${detail}).`,
1273
+ 'Split the command into smaller, simpler Bash calls and retry. See issue #1671.'
1061
1274
  );
1062
- process.exit(0); // fail-open on internal errors to avoid blocking legitimate work
1063
1275
  });
1064
1276
  }
@@ -106,7 +106,12 @@ async function resolveSessionId(input, projectDir) {
106
106
 
107
107
  let currentSession = null;
108
108
  if (projectDir) {
109
- const persisted = path.join(projectDir, '.orchestrator', 'current-session.json');
109
+ // #1605 — read THIS session's record where on-session-start.mjs wrote it
110
+ // (the entered worktree after a /clear there), found by the lock carrying
111
+ // the raw id. Lazy: only a matched issue command pays for the lookup.
112
+ const { resolveOwnLockRoot } = await import('./_lib/hook-root.mjs');
113
+ const ownRoot = resolveOwnLockRoot({ session_id: nativeRawId, cwd: input?.cwd }, projectDir);
114
+ const persisted = path.join(ownRoot, '.orchestrator', 'current-session.json');
110
115
  if (existsSync(persisted)) {
111
116
  try {
112
117
  currentSession = await readJson(persisted);