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
@@ -53,6 +53,8 @@ import { probe, evaluate, DEFAULT_RESOURCE_THRESHOLDS } from './lib/resource-pro
53
53
  import { detectPeers } from './lib/session-registry.mjs';
54
54
  import { normalizeSession } from './lib/session-schema.mjs';
55
55
  import { isBackfillStub, readCanonicalSessions } from './lib/sessions-canonical.mjs';
56
+ import { resolveEventsLedgerRoot } from './lib/platform.mjs';
57
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
56
58
 
57
59
  // ---------------------------------------------------------------------------
58
60
  // CLI-level flag extraction
@@ -152,7 +154,7 @@ const thresholds = loadResourceThresholds();
152
154
  // decay-floor-factor) actually take effect here (#670). Errors swallowed → [].
153
155
 
154
156
  const surfacedLearnings = await surfaceTopN(
155
- resolve('.orchestrator/metrics/learnings.jsonl'),
157
+ resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', process.cwd()), // #1722
156
158
  15,
157
159
  { decay: decayOptsFromConfig(config && config['evolve.decay']) }
158
160
  );
@@ -182,7 +184,10 @@ const branch = detectBranch();
182
184
  // sessions.jsonl helpers for sessionRunner
183
185
  // ---------------------------------------------------------------------------
184
186
 
185
- const SESSIONS_JSONL_PATH = resolve('.orchestrator/metrics/sessions.jsonl');
187
+ // #1592: the child runs with cwd = process.cwd() and its close writes the
188
+ // ledger at resolveEventsLedgerRoot(process.cwd()) (emit-session.mjs) — read
189
+ // the same file, or a run from a linked worktree finds no record.
190
+ const SESSIONS_JSONL_PATH = resolve(resolveEventsLedgerRoot(process.cwd()), '.orchestrator/metrics/sessions.jsonl');
186
191
 
187
192
  /**
188
193
  * Read the child's canonical record by its raw harness UUID, independent of
@@ -253,9 +258,10 @@ async function modeSelector() {
253
258
  // in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`). The hand-written 50 that
254
259
  // stood here under-read this repo's own backlog and hid its critical issues.
255
260
  // `repoRoot` is passed EXPLICITLY (#1071) rather than left to the helper's
256
- // cwd default: this driver already reads learnings.jsonl and sessions.jsonl
257
- // relative to the invocation directory, and an implicit default is the seam
258
- // where those two roots silently drift apart.
261
+ // cwd default: this driver already reads learnings.jsonl relative to the
262
+ // invocation directory (and sessions.jsonl from that directory's ledger root,
263
+ // `SESSIONS_JSONL_PATH`, #1592), and an implicit default is the seam where
264
+ // those roots silently drift apart.
259
265
  const signals = await buildLiveSignals({
260
266
  repoRoot: process.cwd(),
261
267
  learnings: surfacedLearnings,
@@ -44,7 +44,7 @@ import { parseArgs } from 'node:util';
44
44
  import { backfillAbandonedSession, isUuid } from './lib/session-close-backfill.mjs';
45
45
  import { SCAN_CHUNK_BYTES, emitEvent, eventsFilePath, listEventSourcesNewestFirst, tombstoneGap } from './lib/events.mjs';
46
46
  import { ARCHIVE_DIR_NAME } from './lib/events-schema.mjs';
47
- import { getProjectDir } from './lib/platform.mjs';
47
+ import { getProjectDir, resolveEventsLedgerRoot } from './lib/platform.mjs';
48
48
  import { isMainModule } from './lib/is-main-module.mjs';
49
49
  import { isLockLive, readLock } from './lib/session-lock.mjs';
50
50
  import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
@@ -277,7 +277,13 @@ function forEachJsonlRecord(filePath, onRecord) {
277
277
  return;
278
278
  }
279
279
  try {
280
- const size = fs.fstatSync(fd).size;
280
+ const stats = fs.fstatSync(fd);
281
+ // Windows opens a directory "as a file" with size 0; surface it through the
282
+ // same warning POSIX reaches via EISDIR on the first read.
283
+ if (!stats.isFile()) {
284
+ throw Object.assign(new Error('EISDIR: illegal operation on a directory, read'), { code: 'EISDIR' });
285
+ }
286
+ const size = stats.size;
281
287
  const buf = Buffer.alloc(SCAN_CHUNK_BYTES);
282
288
  let pos = 0;
283
289
  let carry = Buffer.alloc(0);
@@ -365,7 +371,10 @@ function readRunningSessionIds(repoRoot) {
365
371
  * @returns {Set<string>}
366
372
  */
367
373
  function readRecordedIds(repoRoot) {
368
- const records = readJsonl(path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl'));
374
+ // #1592: the SAME ledger the core dedupes and writes against (main checkout
375
+ // of a linked worktree) — a worktree-local read let already-recorded
376
+ // candidates through and spent the SessionStart latency cap on them.
377
+ const records = readJsonl(path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl'));
369
378
  const ids = new Set();
370
379
  for (const r of records) {
371
380
  if (!r) continue;
@@ -655,6 +664,7 @@ async function flushTelemetryOnSessionStart(repoRoot) {
655
664
  try {
656
665
  const { flush } = await import('./lib/telemetry/sync.mjs');
657
666
  await flush({
667
+ // ledger-root: metricsDir for the telemetry flush; buildBatch resolves each ledger in it
658
668
  metricsDir: path.join(repoRoot, '.orchestrator', 'metrics'),
659
669
  timeoutMs: STARTUP_FLUSH_TIMEOUT_MS,
660
670
  });
@@ -51,6 +51,7 @@ import { join, relative, resolve } from 'node:path';
51
51
 
52
52
  import { computeEvidenceDigest } from './lib/reconcile/renderer.mjs';
53
53
  import { isMainModule } from './lib/is-main-module.mjs';
54
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
54
55
 
55
56
  /** ISO-8601 instant — mirrors the renderer's `EVIDENCE_RECORDED_AT_RE`. */
56
57
  const EVIDENCE_RECORDED_AT_RE = /^\d{4}-\d{2}-\d{2}T[0-9:.]+(?:Z|[+-]\d{2}:\d{2})$/;
@@ -198,7 +199,7 @@ const kebab = (s) =>
198
199
  */
199
200
  function loadCreatedAtByKey(repoRoot) {
200
201
  const map = new Map();
201
- const p = join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
202
+ const p = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot); // #1722
202
203
  if (!existsSync(p)) return map;
203
204
  for (const line of readFileSync(p, 'utf8').split('\n')) {
204
205
  const trimmed = line.trim();
@@ -72,7 +72,15 @@ import { subjectToSlug, parseFrontmatter } from './lib/vault-mirror/utils.mjs';
72
72
  import { kebab } from './lib/learnings/kebab.mjs';
73
73
  import { validateLearning } from './lib/learnings/schema.mjs';
74
74
  import { appendLearning, isBackupOf, withLearningsLock } from './lib/learnings/io.mjs';
75
+ import {
76
+ isCanonicalStore,
77
+ inboxPathFor,
78
+ receiptsPathFor,
79
+ archivePathFor,
80
+ resolveStoreRole,
81
+ } from './lib/learnings/store-role.mjs';
75
82
  import { isMainModule } from './lib/is-main-module.mjs';
83
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
76
84
 
77
85
  const DEFAULT_RULES_DIR = '.claude/rules';
78
86
  const DEFAULT_STORE = '.orchestrator/metrics/learnings.jsonl';
@@ -621,6 +629,39 @@ function idsInStore(filePath) {
621
629
  return ids;
622
630
  }
623
631
 
632
+ /**
633
+ * Inbox ids that count as already present for the canonical store (#1516,
634
+ * #1685). The rule depends on this host's store role:
635
+ *
636
+ * - `inbox`: EVERY inbox id is present. `appendLearning()` lands a restore in
637
+ * the inbox here, and re-appending a receipted or archived id is pointless —
638
+ * the receipt or archive hides it again, so every --apply would re-append.
639
+ * - any other role (`writer`, `invalid` — invalid refuses the write anyway):
640
+ * only VISIBLE inbox ids, i.e. not receipted and not archived, mirroring
641
+ * `readInboxView()` in io.mjs. A receipted id that later left the live store
642
+ * is seen by no reader, so it must stay an orphan the backfill restores.
643
+ * Live ids need no exclusion here: callers check the live store themselves.
644
+ *
645
+ * `hidden` is the subset of `present` that no reader sees (receipted or
646
+ * archived) — non-empty only on an inbox host, where such ids still count as
647
+ * present. The report gives them their own status (`present-in-inbox-hidden`,
648
+ * #1689) instead of claiming they sit in the store's read view.
649
+ *
650
+ * Both empty for a non-canonical store path, which has no inbox semantics.
651
+ *
652
+ * @param {string} storeAbs
653
+ * @returns {{ present: Set<string>, hidden: Set<string> }}
654
+ */
655
+ function presentInboxIds(storeAbs) {
656
+ if (!isCanonicalStore(storeAbs)) return { present: new Set(), hidden: new Set() };
657
+ const inboxIds = idsInStore(inboxPathFor(storeAbs));
658
+ // Receipt lines are `{id, ingested_at}` — idsInStore reads their ids.
659
+ const hiddenAny = new Set([...idsInStore(receiptsPathFor(storeAbs)), ...idsInStore(archivePathFor(storeAbs))]);
660
+ const hidden = new Set([...inboxIds].filter((id) => hiddenAny.has(id)));
661
+ if (resolveStoreRole().role === 'inbox') return { present: inboxIds, hidden };
662
+ return { present: new Set([...inboxIds].filter((id) => !hidden.has(id))), hidden: new Set() };
663
+ }
664
+
624
665
  /**
625
666
  * List `<store>.bak[-.]*` siblings (a verbatim original beats any
626
667
  * reconstruction). Both delimiter forms count — the legacy dot form
@@ -681,7 +722,7 @@ function printHuman(report) {
681
722
  out.push(` vault : ${report.vault_learnings_dir} (${s.vault_notes_indexed} learning notes indexed)`);
682
723
  out.push('');
683
724
  out.push(
684
- `Rules with provenance: ${s.rules_with_provenance} · resolved in store: ${s.present_in_store} · in archive: ${s.present_in_archive} · recoverable verbatim from a .bak: ${s.recoverable_from_backup} · ORPHANED: ${s.orphans}`
725
+ `Rules with provenance: ${s.rules_with_provenance} · resolved in store: ${s.present_in_store} · in archive: ${s.present_in_archive} · in inbox, hidden: ${s.present_in_inbox_hidden} · recoverable verbatim from a .bak: ${s.recoverable_from_backup} · ORPHANED: ${s.orphans}`
685
726
  );
686
727
  out.push('');
687
728
 
@@ -757,6 +798,8 @@ export async function main(argv = [], deps = {}) {
757
798
  let rulesDir = DEFAULT_RULES_DIR;
758
799
  let storeArg = DEFAULT_STORE;
759
800
  let archiveArg = DEFAULT_ARCHIVE;
801
+ let storeExplicit = false;
802
+ let archiveExplicit = false;
760
803
 
761
804
  for (let i = 0; i < argv.length; i++) {
762
805
  const a = argv[i];
@@ -767,8 +810,8 @@ export async function main(argv = [], deps = {}) {
767
810
  else if (a === '--vault-dir') vaultDirArg = argv[++i];
768
811
  else if (a === '--vault-subdir') vaultSubdir = argv[++i];
769
812
  else if (a === '--rules-dir') rulesDir = argv[++i];
770
- else if (a === '--store') storeArg = argv[++i];
771
- else if (a === '--archive') archiveArg = argv[++i];
813
+ else if (a === '--store') { storeArg = argv[++i]; storeExplicit = true; }
814
+ else if (a === '--archive') { archiveArg = argv[++i]; archiveExplicit = true; }
772
815
  else {
773
816
  process.stderr.write(`backfill-learnings-from-vault: unknown argument: ${a}\n`);
774
817
  return { code: 1 };
@@ -819,8 +862,10 @@ export async function main(argv = [], deps = {}) {
819
862
  // `resolve` (not `join`) so an ABSOLUTE --store/--archive/--rules-dir wins over
820
863
  // the repo root instead of being silently nested under it.
821
864
  const rulesAbs = resolve(root, rulesDir);
822
- const storeAbs = resolve(root, storeArg);
823
- const archiveAbs = resolve(root, archiveArg);
865
+ // #1722: the DEFAULT store/archive route through the ledger resolver (main
866
+ // checkout when root is a linked worktree that gitignores them).
867
+ const storeAbs = storeExplicit ? resolve(root, storeArg) : resolveLedgerPath(DEFAULT_STORE, root);
868
+ const archiveAbs = archiveExplicit ? resolve(root, archiveArg) : resolveLedgerPath(DEFAULT_ARCHIVE, root);
824
869
  const learningsAbs = resolve(vaultDir, vaultSubdir);
825
870
 
826
871
  if (!existsSync(rulesAbs)) {
@@ -834,6 +879,7 @@ export async function main(argv = [], deps = {}) {
834
879
 
835
880
  // ── Load inputs ──────────────────────────────────────────────────────────
836
881
  const storeIds = idsInStore(storeAbs);
882
+ const { present: inboxIds, hidden: hiddenInboxIds } = presentInboxIds(storeAbs);
837
883
  const archiveIds = idsInStore(archiveAbs);
838
884
  const backups = backupPaths(storeAbs).map((p) => ({ path: p, ids: idsInStore(p) }));
839
885
  const { notes, unreadable } = indexVaultNotes(learningsAbs);
@@ -868,12 +914,20 @@ export async function main(argv = [], deps = {}) {
868
914
  const id = prov.learningId;
869
915
  const foundIn = [];
870
916
  if (storeIds.has(id)) foundIn.push('store');
917
+ if (inboxIds.has(id)) foundIn.push('inbox');
871
918
  if (archiveIds.has(id)) foundIn.push('archive');
872
919
  for (const b of backups) if (b.ids.has(id)) foundIn.push(basename(b.path));
873
920
 
874
921
  let status;
922
+ // A visible inbox id is part of the store's read view (readers union it
923
+ // in), so it counts as present-in-store — after the archive, which wins
924
+ // for an id in both; `found_in` names the file. A present but HIDDEN one
925
+ // (inbox host, receipted/archived) is seen by no reader, so it is not
926
+ // claimed as part of the store (#1689).
875
927
  if (storeIds.has(id)) status = 'present-in-store';
876
928
  else if (archiveIds.has(id)) status = 'present-in-archive';
929
+ else if (hiddenInboxIds.has(id)) status = 'present-in-inbox-hidden';
930
+ else if (inboxIds.has(id)) status = 'present-in-store';
877
931
  else if (foundIn.length > 0) status = 'recoverable-verbatim-from-backup';
878
932
  else status = 'orphan';
879
933
 
@@ -942,7 +996,7 @@ export async function main(argv = [], deps = {}) {
942
996
  // appendLearning() re-enters the held lock (reentrant per async call
943
997
  // chain, io.mjs).
944
998
  await withLearningsLock(storeAbs, async () => {
945
- const present = new Set([...idsInStore(storeAbs), ...idsInStore(archiveAbs)]);
999
+ const present = new Set([...idsInStore(storeAbs), ...presentInboxIds(storeAbs).present, ...idsInStore(archiveAbs)]);
946
1000
  for (const r of candidates) {
947
1001
  // This is what makes a second --apply a no-op rather than a
948
1002
  // duplicate append; `present.add` below covers two rules naming one id.
@@ -983,6 +1037,7 @@ export async function main(argv = [], deps = {}) {
983
1037
  vault_notes_indexed: notes.length,
984
1038
  present_in_store: records.filter((r) => r.status === 'present-in-store').length,
985
1039
  present_in_archive: records.filter((r) => r.status === 'present-in-archive').length,
1040
+ present_in_inbox_hidden: records.filter((r) => r.status === 'present-in-inbox-hidden').length,
986
1041
  recoverable_from_backup: records.filter((r) => r.status === 'recoverable-verbatim-from-backup').length,
987
1042
  orphans: orphans.length,
988
1043
  located_in_vault: orphans.filter((r) => r.vault_note).length,
@@ -29,9 +29,10 @@
29
29
  */
30
30
 
31
31
  import { readFileSync, copyFileSync, existsSync } from 'node:fs';
32
- import { basename } from 'node:path';
32
+ import { basename, join, resolve } from 'node:path';
33
33
  import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
34
34
  import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
35
+ import { resolveEventsLedgerRoot } from './lib/platform.mjs';
35
36
 
36
37
  const DEFAULT_FILE = '.orchestrator/metrics/sessions.jsonl';
37
38
 
@@ -115,8 +116,21 @@ function detectUnmappableReason(entry) {
115
116
  return null;
116
117
  }
117
118
 
119
+ /**
120
+ * Default target: sessions.jsonl of the repo's one ledger root (#1514/#1584,
121
+ * #1722) — the main checkout when cwd is a linked worktree. Outside one the
122
+ * relative DEFAULT_FILE is kept verbatim, so paths in output do not change.
123
+ *
124
+ * @returns {string}
125
+ */
126
+ function defaultFile() {
127
+ const root = resolveEventsLedgerRoot(process.cwd());
128
+ // ledger-root: `root` is resolveEventsLedgerRoot() of the line above
129
+ return root === resolve(process.cwd()) ? DEFAULT_FILE : join(root, DEFAULT_FILE);
130
+ }
131
+
118
132
  function parseArgs(argv) {
119
- const args = { file: DEFAULT_FILE, mode: 'dry-run' };
133
+ const args = { file: defaultFile(), mode: 'dry-run' };
120
134
  for (let i = 0; i < argv.length; i++) {
121
135
  const a = argv[i];
122
136
  if (a === '--file') args.file = argv[++i];
@@ -30,6 +30,10 @@
30
30
  // `prepare: husky` script never ran because ignore-scripts=true,
31
31
  // so commit-msg/pre-commit/pre-push are inactive. Warns locally
32
32
  // and recommends `npx husky`; silent under CI.
33
+ // h2 core.hooksPath IS `.husky/_` but that directory is missing in
34
+ // this worktree (#1662) — a linked worktree inherits the value from
35
+ // the shared `.git/config`, yet `.husky/_` is never generated there
36
+ // because `prepare: husky` does not run. Same advisory treatment.
33
37
  //
34
38
  // SCOPE GUARD:
35
39
  // This script only guards the REPOSITORY ROOT package.json. If invoked
@@ -275,21 +279,34 @@ export function parseCiEnv(value) {
275
279
  export const HUSKY_INACTIVE_WARNING =
276
280
  '⚠ check-package-manager: git core.hooksPath is not set to .husky/_ — Husky hooks (commit-msg, pre-commit, pre-push) are INACTIVE in this clone because .npmrc ignore-scripts=true skips the prepare script (#1458). Run: npx husky';
277
281
 
282
+ /** One-line stderr WARN for a set-but-missing hooks directory (#1662, linked worktrees). */
283
+ export const HUSKY_MISSING_DIR_WARNING =
284
+ '⚠ check-package-manager: git core.hooksPath is .husky/_ but that directory is MISSING in this worktree — Husky hooks (commit-msg, pre-commit, pre-push) are INACTIVE here; a linked worktree inherits core.hooksPath from the shared .git/config, but ignore-scripts=true skips the prepare script that generates .husky/_ (#1662). Run: npx husky';
285
+
278
286
  /**
279
287
  * Advisory check (#1458): `.npmrc` `ignore-scripts=true` (SEC-020) means the
280
288
  * `prepare: husky` script never runs on `npm ci`, so a fresh clone has no
281
289
  * `core.hooksPath` and every Husky hook is silently inactive. This only
282
290
  * REPORTS the state — it never runs `npx husky`, never writes git config and
283
291
  * never affects the guard's `ok`/exit code. Silent under CI.
292
+ *
293
+ * A matching value alone is not enough (#1662): a linked worktree inherits
294
+ * `core.hooksPath=.husky/_` from the shared `.git/config` while `.husky/_`
295
+ * itself was never generated there. The directory is therefore resolved —
296
+ * a relative hooksPath against `repoRoot` (the current worktree top-level),
297
+ * an absolute one as is — and a missing directory reports `active: false`
298
+ * with `missingDir: true`.
284
299
  * @param {string} repoRoot
285
300
  * @param {{ env?: NodeJS.ProcessEnv, ci?: boolean }} [opts] — `env` is also
286
301
  * passed to the git subprocess (lets tests isolate from global git config).
287
- * @returns {{ hooksPath: string, active: boolean, skipped: 'ci'|'no-husky-dir'|null }}
302
+ * @returns {{ hooksPath: string, active: boolean, missingDir: boolean, skipped: 'ci'|'no-husky-dir'|null }}
288
303
  */
289
304
  export function checkHuskyHooksPath(repoRoot, { env = process.env, ci } = {}) {
290
- if (ci ?? parseCiEnv(env.CI)) return { hooksPath: '', active: false, skipped: 'ci' };
305
+ if (ci ?? parseCiEnv(env.CI)) {
306
+ return { hooksPath: '', active: false, missingDir: false, skipped: 'ci' };
307
+ }
291
308
  if (!existsSync(join(repoRoot, '.husky'))) {
292
- return { hooksPath: '', active: false, skipped: 'no-husky-dir' };
309
+ return { hooksPath: '', active: false, missingDir: false, skipped: 'no-husky-dir' };
293
310
  }
294
311
 
295
312
  let hooksPath;
@@ -305,11 +322,12 @@ export function checkHuskyHooksPath(repoRoot, { env = process.env, ci } = {}) {
305
322
  }
306
323
 
307
324
  const norm = hooksPath.replace(/\\/g, '/').replace(/\/+$/, '');
308
- const active =
325
+ const matches =
309
326
  norm !== '' &&
310
327
  (isAbsolute(hooksPath) ? norm.endsWith('/.husky/_') : posix.normalize(norm) === '.husky/_');
328
+ const missingDir = matches && !existsSync(resolve(repoRoot, hooksPath));
311
329
 
312
- return { hooksPath, active, skipped: null };
330
+ return { hooksPath, active: matches && !missingDir, missingDir, skipped: null };
313
331
  }
314
332
 
315
333
  /**
@@ -317,7 +335,7 @@ export function checkHuskyHooksPath(repoRoot, { env = process.env, ci } = {}) {
317
335
  * only side effects are read-only fs/git calls. Never throws or exits.
318
336
  * `hooks` carries the advisory Husky check (#1458); it never affects `ok`.
319
337
  * @param {{ cwd?: string, ci?: boolean, allowMissingNodeModules?: boolean, repoRoot?: string, env?: NodeJS.ProcessEnv }} [opts]
320
- * @returns {{ ok: boolean, ci: boolean, skipped: boolean, repoRoot: string, findings: Array<{code: string, severity: string, effectiveSeverity: string, message: string}>, hooks?: { hooksPath: string, active: boolean, skipped: 'ci'|'no-husky-dir'|null } }}
338
+ * @returns {{ ok: boolean, ci: boolean, skipped: boolean, repoRoot: string, findings: Array<{code: string, severity: string, effectiveSeverity: string, message: string}>, hooks?: { hooksPath: string, active: boolean, missingDir: boolean, skipped: 'ci'|'no-husky-dir'|null } }}
321
339
  */
322
340
  export function runPackageManagerGuard(opts = {}) {
323
341
  const cwd = opts.cwd ?? process.cwd();
@@ -464,9 +482,9 @@ if (isMain) {
464
482
 
465
483
  const result = runPackageManagerGuard({ allowMissingNodeModules: opts.allowMissingNodeModules });
466
484
 
467
- // Advisory only (#1458) — never changes the exit code.
485
+ // Advisory only (#1458, #1662) — never changes the exit code.
468
486
  if (result.hooks && !result.hooks.active && result.hooks.skipped === null) {
469
- console.error(HUSKY_INACTIVE_WARNING);
487
+ console.error(result.hooks.missingDir ? HUSKY_MISSING_DIR_WARNING : HUSKY_INACTIVE_WARNING);
470
488
  }
471
489
 
472
490
  if (opts.json) {
@@ -50,6 +50,7 @@ import { fileURLToPath } from 'node:url';
50
50
 
51
51
  import { checkSessionsIntegrity, SESSIONS_PATH } from './lib/sessions-integrity-banner.mjs';
52
52
  import { isMainModule } from './lib/is-main-module.mjs';
53
+ import { resolveEventsLedgerRoot } from './lib/platform.mjs';
53
54
 
54
55
  const SELF_DIR = dirname(fileURLToPath(import.meta.url));
55
56
 
@@ -146,7 +147,9 @@ export function parseArgs(argv) {
146
147
  * @returns {number}
147
148
  */
148
149
  function countLedgerRecords(repoRoot, sessionId) {
149
- const filePath = join(repoRoot, SESSIONS_PATH);
150
+ // #1592: the same file checkSessionsIntegrity reads — the main checkout's
151
+ // ledger from a linked worktree, or a record written there reads as missing.
152
+ const filePath = join(resolveEventsLedgerRoot(repoRoot), SESSIONS_PATH);
150
153
  if (!existsSync(filePath)) return 0;
151
154
  let matched = 0;
152
155
  for (const line of readFileSync(filePath, 'utf8').split('\n')) {
@@ -96,6 +96,9 @@ export function verifyVitestResult(result, opts = {}) {
96
96
  if (success !== true) {
97
97
  return { ok: false, reason: `success is ${JSON.stringify(success)} (expected true)`, summary };
98
98
  }
99
+ if (numPassedTests <= 0 || result.numPendingTests >= numTotalTests) {
100
+ return { ok: false, reason: 'no executed passing tests (all tests pending/skipped)', summary };
101
+ }
99
102
  // Floor guard: a suspiciously small run signals a truncated/partial/empty
100
103
  // execution masquerading as green. The real suite is ~12.1k tests (2026-07-17).
101
104
  if (numTotalTests < minTests) {
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // Native Windows regression probes (#1612). Fresh SessionStart needs npm-registry
3
+ // access; unavailable network is explicitly unmeasurable, never a measured PASS.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { spawn, spawnSync } from 'node:child_process';
9
+
10
+ if (process.platform !== 'win32') {
11
+ console.log('windows-smoke: nur win32');
12
+ console.log('SUMMARY 0/0');
13
+ } else {
14
+ const repo = fileURLToPath(new URL('../../', import.meta.url));
15
+ const { runCheck } = await import('../lib/gates/gate-helpers.mjs');
16
+ const { spawnInGroup } = await import('../lib/process-group.mjs');
17
+ const { REGISTRY_URL, CACHE_FILENAME, readCachedLatest } = await import('../lib/plugin-update-banner.mjs');
18
+ const { httpRequest } = await import('../lib/http-request.mjs');
19
+ // Hook stdout is untrusted: a crashed hook prints nothing or a stack trace.
20
+ const parseJson = (text) => { try { return JSON.parse(text); } catch { return null; } };
21
+ const results = [];
22
+ const record = (name, ok, detail) => {
23
+ results.push(ok);
24
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name} :: ${JSON.stringify(detail)}`);
25
+ };
26
+ const probe = async (name, fn) => {
27
+ try { await fn(); } catch (error) { record(name, false, error.message); }
28
+ };
29
+ let work;
30
+ try {
31
+ work = fs.mkdtempSync(path.join(os.tmpdir(), 'so windows smoke '));
32
+ const config = path.join(work, 'private config');
33
+ const env = {
34
+ ...process.env,
35
+ SO_CONFIG_HOME: config,
36
+ SO_SESSION_REGISTRY_DIR: path.join(config, 'sessions'),
37
+ SO_HOST_ALIASES_FILE: path.join(config, 'host-aliases.json'),
38
+ SO_LOOP_GUARD_DIR: path.join(work, 'loop guard'),
39
+ SO_VAULT_DIR: path.join(work, 'vault'),
40
+ NAVIGATOR_CONFIG_DIR: path.join(work, 'navigator'),
41
+ SO_EVENTS_LEDGER_SANDBOX: path.join(work, 'events.jsonl'),
42
+ CLANK_EVENT_URL: '', CLANK_EVENT_SECRET: '',
43
+ CLAUDE_CONFIG_DIR: path.join(work, 'claude config'),
44
+ XDG_CONFIG_HOME: path.join(work, 'xdg'),
45
+ TMPDIR: work, TMP: work, TEMP: work,
46
+ SO_TELEMETRY_DISABLED: '1',
47
+ SO_DISABLE_UPDATE_CHECK: '1',
48
+ SO_DISABLE_STARTUP_PROBES: '1',
49
+ SO_HOOK_PROFILE: 'full', SO_DISABLED_HOOKS: '',
50
+ };
51
+ for (const dir of [config, env.SO_SESSION_REGISTRY_DIR, env.SO_LOOP_GUARD_DIR,
52
+ env.SO_VAULT_DIR, env.CLAUDE_CONFIG_DIR, env.XDG_CONFIG_HOME]) {
53
+ fs.mkdirSync(dir, { recursive: true });
54
+ }
55
+ const node = `"${process.execPath}"`;
56
+ await probe('gate capture', async () => {
57
+ const r = await runCheck(`${node} -e "console.log('out-äöü-€'); console.error('err-line')"`, { repoRoot: work, cwd: work, env });
58
+ record('gate capture', r.status === 'pass' && r.fullOutput.includes('out-äöü-€') && r.fullOutput.includes('err-line'), r.fullOutput);
59
+ });
60
+ for (const code of [0, 3]) await probe(`gate exit ${code}`, async () => {
61
+ const r = await runCheck(`${node} -e "process.exit(${code})"`, { repoRoot: work, cwd: work, env });
62
+ record(`gate exit ${code}`, r.exitCode === code && r.status === (code === 0 ? 'pass' : 'fail'), { exit: r.exitCode, status: r.status });
63
+ });
64
+ await probe('gate missing command', async () => {
65
+ const r = await runCheck('so-windows-smoke-definitely-not-a-command', { repoRoot: work, cwd: work, env });
66
+ // `cmd /d /s /c` exits 1 for an unknown command (9009 only inside batch
67
+ // files), so the hint text is absent on Windows; what must hold is that a
68
+ // missing command is a failure, never a skip (#1612, GH#76 point 5).
69
+ record('gate missing command', r.status === 'fail' && r.exitCode !== 0, { status: r.status, exitCode: r.exitCode });
70
+ });
71
+ await probe('gate timeout tree', async () => {
72
+ const pidsDir = path.join(work, 'pids');
73
+ fs.mkdirSync(pidsDir);
74
+ const grandchild = path.join(work, 'grandchild.mjs');
75
+ const child = path.join(work, 'child.mjs');
76
+ fs.writeFileSync(grandchild, `import fs from 'node:fs'; fs.writeFileSync(${JSON.stringify(pidsDir)} + '/gc-' + process.pid, ''); setInterval(() => {}, 1000);`);
77
+ fs.writeFileSync(child, `import fs from 'node:fs'; import { spawn } from 'node:child_process'; fs.writeFileSync(${JSON.stringify(pidsDir)} + '/ch-' + process.pid, ''); for (let i=0;i<2;i++) spawn(process.execPath, [${JSON.stringify(grandchild)}], { stdio: 'ignore' }); setInterval(() => {}, 1000);`);
78
+ const r = await runCheck(`${node} "${child}"`, { repoRoot: work, cwd: work, env, timeoutMs: 4000 });
79
+ const pids = fs.readdirSync(pidsDir).map(f => Number(f.split('-')[1]));
80
+ await new Promise(resolve => setTimeout(resolve, 1500));
81
+ const still = pids.filter(pid => {
82
+ const check = spawnSync('tasklist.exe', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], { encoding: 'utf8', timeout: 5000 });
83
+ if (check.error || check.status !== 0) throw new Error(`tasklist failed: ${check.error?.message ?? check.status}`);
84
+ return check.stdout.includes(`"${pid}"`);
85
+ });
86
+ record('gate timeout tree', r.status === 'fail' && r.timedOut && pids.length === 3 && still.length === 0 && r.survivors?.length === 0,
87
+ { pids, still, timedOut: r.timedOut, survivors: r.survivors, output: r.output });
88
+ for (const pid of still) {
89
+ const killed = spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf8', timeout: 5000 });
90
+ if (killed.error || killed.status !== 0) throw new Error(`survivor cleanup failed: ${pid}`);
91
+ }
92
+ });
93
+ await probe('argv literal', async () => {
94
+ const dir = path.join(work, "dir with space's");
95
+ fs.mkdirSync(dir);
96
+ const script = path.join(dir, 'argv.mjs');
97
+ fs.writeFileSync(script, 'console.log(JSON.stringify(process.argv.slice(1)));');
98
+ const args = [script, 'a b', "it's", '%PATH%', '&echo x'];
99
+ const r = await spawnInGroup([process.execPath, ...args], { repoRoot: work, cwd: work, env, timeoutMs: 20000 });
100
+ const parsed = JSON.parse(r.fullOutput.trim());
101
+ record('argv literal', r.exitCode === 0 && JSON.stringify(parsed) === JSON.stringify(args), parsed);
102
+ });
103
+
104
+ const bash = path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Git', 'bin', 'bash.exe');
105
+ const hooks = JSON.parse(fs.readFileSync(path.join(repo, 'hooks/hooks.json'), 'utf8')).hooks;
106
+ const project = path.join(work, 'project with spaces');
107
+ fs.mkdirSync(project);
108
+ fs.writeFileSync(path.join(project, 'transcript.jsonl'), '');
109
+ const runtimes = [];
110
+ const runHook = async (event, matcher, command, cwd = project, extra = {}, overrides = {}) => {
111
+ const tool = (matcher === '*' ? 'Bash' : matcher || 'Bash').split('|')[0];
112
+ const input = tool === 'Bash' ? { command: 'ls -la' }
113
+ : tool === 'Skill' ? { skill: 'session-orchestrator:session' }
114
+ : tool === 'Agent' ? { description: 'smoke', prompt: 'smoke' }
115
+ : tool === 'AskUserQuestion' ? { questions: [] }
116
+ : { file_path: path.join(cwd, 'src/a.mjs'), content: 'export const a = 1;\n', old_string: 'x', new_string: 'y' };
117
+ const payload = { session_id: `windows-smoke-${path.basename(cwd)}`, cwd,
118
+ transcript_path: path.join(cwd, 'transcript.jsonl'), permission_mode: 'default', hook_event_name: event,
119
+ ...(event.includes('Tool') ? { tool_name: tool, tool_input: input, tool_response: {} } : {}),
120
+ ...(event === 'SessionStart' ? { source: 'startup' } : {}),
121
+ ...(event === 'SessionEnd' ? { reason: 'other' } : {}), ...extra };
122
+ // Reuse the production timeout/tree cleanup; only adapt stdin for hook payloads.
123
+ const stdoutChunks = [];
124
+ let stdinError = null;
125
+ const r = await spawnInGroup([bash, '-c', command], { cwd, repoRoot: cwd, timeoutMs: 5000,
126
+ env: { ...env, CLAUDE_PLUGIN_ROOT: repo, CLAUDE_PROJECT_DIR: cwd, ...overrides },
127
+ spawnFn: (executable, args, options) => {
128
+ const child = spawn(executable, args, { ...options, stdio: ['pipe', 'pipe', 'pipe'] });
129
+ child.stdout.on('data', chunk => { stdoutChunks.push(chunk); });
130
+ child.stdin.on('error', error => {
131
+ stdinError = error.message;
132
+ });
133
+ child.stdin.end(JSON.stringify(payload));
134
+ return child;
135
+ } });
136
+ runtimes.push({ event, command, ms: r.durationMs });
137
+ return { ...r, stdout: Buffer.concat(stdoutChunks).toString('utf8'), stdinError };
138
+ };
139
+ for (const [event, groups] of Object.entries(hooks)) for (const group of groups) for (const hook of group.hooks) {
140
+ const name = `${event}/${path.basename(hook.command.split('"').at(-2) || hook.command)}`;
141
+ await probe(name, async () => {
142
+ const r = await runHook(event, group.matcher, hook.command);
143
+ record(name, r.exitCode === 0 && !r.timedOut && !r.stdinError,
144
+ { exit: r.exitCode, ms: r.durationMs, stdinError: r.stdinError, output: r.output });
145
+ });
146
+ }
147
+ const guard = hooks.PreToolUse.flatMap(group => group.hooks).find(hook => hook.command.includes('pre-bash-destructive-guard'));
148
+ await probe('destructive guard', async () => {
149
+ // Payload only: never execute this command. Split for the outer shell guard.
150
+ const command = ['git', 'reset', '--hard', 'HEAD~1'].join(' ');
151
+ const r = await runHook('PreToolUse', 'Bash', guard.command, project, { tool_input: { command } });
152
+ const output = parseJson(r.stdout.trim());
153
+ record('destructive guard', r.exitCode === 0 && !r.timedOut && !r.stdinError && output?.hookSpecificOutput?.permissionDecision === 'deny', output ?? r.output);
154
+ });
155
+ const start = hooks.SessionStart.flatMap(group => group.hooks).find(hook => hook.command.includes('on-session-start'));
156
+ // Do not inherit any update kill-switch into the first-start regression path.
157
+ const freshEnv = { SO_DISABLE_UPDATE_CHECK: undefined, DO_NOT_TRACK: undefined,
158
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: undefined, SO_DISABLE_STARTUP_PROBES: undefined };
159
+ for (let i = 0; i < 5; i++) await probe(`fresh SessionStart ${i + 1}`, async () => {
160
+ const fresh = fs.mkdtempSync(path.join(work, 'fresh project '));
161
+ // Minimal empty git repository fixture; no git-writing commands are needed.
162
+ for (const dir of ['.git/objects', '.git/refs/heads']) fs.mkdirSync(path.join(fresh, dir), { recursive: true });
163
+ fs.writeFileSync(path.join(fresh, '.git/HEAD'), 'ref: refs/heads/main\n');
164
+ fs.writeFileSync(path.join(fresh, '.git/config'), '[core]\nrepositoryformatversion = 0\nbare = false\n');
165
+ fs.writeFileSync(path.join(fresh, 'transcript.jsonl'), '');
166
+ const r = await runHook('SessionStart', 'startup', start.command, fresh, {}, freshEnv);
167
+ const output = parseJson(r.stdout.trim());
168
+ const ok = r.exitCode === 0 && !r.timedOut && !r.stdinError && typeof output === 'object' && output !== null && !/Assertion failed/.test(r.fullOutput);
169
+ record(`fresh SessionStart ${i + 1}`, ok, { exit: r.exitCode, ms: r.durationMs, output: r.output });
170
+ if (!ok) return;
171
+ const cacheDir = path.join(fresh, '.orchestrator/runtime');
172
+ const cache = readCachedLatest({ cacheDir });
173
+ if (cache) record(`fresh update ${i + 1}`, true, { version: cache.version });
174
+ else {
175
+ // A missing cache alone cannot distinguish offline from a broken hook.
176
+ let reachable = false;
177
+ try {
178
+ // Never fetch here: fetch + exit is the Windows crash this smoke guards (#1612).
179
+ const response = await httpRequest(REGISTRY_URL, { timeoutMs: 2000 });
180
+ reachable = response.ok && typeof (await response.json())?.version === 'string';
181
+ } catch (error) {
182
+ console.log(`INFO fresh update ${i + 1} :: ${JSON.stringify(error.message)}`);
183
+ }
184
+ if (reachable || fs.existsSync(path.join(cacheDir, CACHE_FILENAME))) {
185
+ record(`fresh update ${i + 1}`, false, 'registry reachable or cache invalid, but hook wrote no usable cache');
186
+ } else console.log(`SKIP fresh update ${i + 1} :: nicht messbar: npm registry unavailable (network required)`);
187
+ }
188
+ });
189
+ record('Hook-Laufzeit', runtimes.length > 0 && runtimes.every(r => r.ms < 4000),
190
+ { maxMs: Math.max(...runtimes.map(r => r.ms)), slow: runtimes.filter(r => r.ms >= 4000) });
191
+ } catch (error) {
192
+ record('smoke setup', false, error.message);
193
+ } finally {
194
+ if (work) {
195
+ try { fs.rmSync(work, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }); }
196
+ catch (error) { record('cleanup', false, error.message); }
197
+ }
198
+ console.log(`SUMMARY ${results.filter(Boolean).length}/${results.length}`);
199
+ process.exitCode = results.every(Boolean) ? 0 : 1;
200
+ }
201
+ }