session-orchestrator 5.12.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +162 -4
  11. package/README.md +126 -127
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +1 -1
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +90 -17
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +49 -15
  47. package/docs/session-config-template.md +30 -22
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +139 -49
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -2
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +39 -27
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Check: every line under `scripts/`, `hooks/` and `skills/` that builds a path
4
+ * into `.orchestrator/metrics` routes it through a ledger resolver (GitLab
5
+ * #1722). BLOCKING.
6
+ *
7
+ * ## Why
8
+ *
9
+ * The ledgers under `.orchestrator/metrics/` are gitignored in this repo
10
+ * (`.gitignore:57`). A writer that joins the path to the cwd, `getProjectDir()`
11
+ * or an unmapped repo root writes, from a linked worktree, a copy that is lost
12
+ * with the worktree, and a reader built the same way sees an empty ledger.
13
+ * Measured 2026-10-22 before #1722: 43 of 62 sites in 54 unrouted modules,
14
+ * two stranded ledgers in live worktrees (`eval.jsonl`, `skill-invocations.jsonl`).
15
+ * A first, per-FILE version of this check passed while five readers in
16
+ * resolver-calling modules still read the worktree copy (review of the same
17
+ * day) — hence the per-LINE rule below.
18
+ *
19
+ * The resolvers that answer correctly:
20
+ * - `resolveLedgerPath()` / `resolveLedgerRoot()` (`scripts/lib/ledger-path.mjs`)
21
+ * for every ledger except events and sessions — main checkout only when
22
+ * the ledger is gitignored in the worktree, so repos that version it keep
23
+ * it on the branch;
24
+ * - `resolveEventsLedgerRoot()` / `explainEventsLedgerRoot()` /
25
+ * `eventsFilePath()` (`scripts/lib/platform.mjs`, `scripts/lib/events.mjs`)
26
+ * for `events.jsonl` and `sessions.jsonl` (#1514, #1584);
27
+ * - helpers built on them (`resolveLearningsPaths`, `defaultSkillInvocationsPath`).
28
+ *
29
+ * ## Rule
30
+ *
31
+ * A HIT is a non-comment line with a `.orchestrator/metrics` string literal or
32
+ * the segment pair `'.orchestrator', 'metrics'` (also split over two lines,
33
+ * reported on the `'.orchestrator',` line). It passes when
34
+ * (a) a resolver is called on that line or on one of the {@link LOOKBACK}
35
+ * lines above it that belong to the same statement (a multi-line
36
+ * `path.join(` whose root argument comes first),
37
+ * (b) it declares an UPPER_CASE relative constant (string, `path.join(…)` or
38
+ * array of segments) — the join site of the constant is checked by (2), or
39
+ * (c) it, or the line above, carries the marker `ledger-root: <reason>` —
40
+ * for a line that is no ledger access or whose root is already resolved.
41
+ * (2) A line that passes such a constant to `join(`/`resolve(` follows the same
42
+ * (a)/(c) rule.
43
+ * A file on {@link ALLOWLIST} is skipped whole; an allowlisted file without a
44
+ * HIT fails, so the list cannot rot.
45
+ *
46
+ * Ceiling: (2) sees constants declared in the same file only, and (a) trusts
47
+ * any resolver within the lookback window. Revisit if a stranded ledger is
48
+ * traced to an imported constant or to a line the window wrongly covered.
49
+ *
50
+ * @module scripts/lib/validate/check-metrics-ledger-root
51
+ */
52
+
53
+ import { readFileSync } from 'node:fs';
54
+ import path from 'node:path';
55
+ import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
56
+ import { isMainModule } from '../is-main-module.mjs';
57
+
58
+ export const SCAN_DIRS = Object.freeze(['scripts', 'hooks', 'skills']);
59
+
60
+ /** Lines above a HIT in which a resolver call still counts (multi-line joins). */
61
+ export const LOOKBACK = 3;
62
+
63
+ /** A string literal starting with the metrics dir, or the two-segment join. */
64
+ const METRICS_PATH_RE = /['"`]\.orchestrator\/metrics|['"]\.orchestrator['"]\s*,\s*['"]metrics['"]/;
65
+
66
+ /** The two-segment join split over two lines: `'.orchestrator',` ⏎ `'metrics'`. */
67
+ const SPLIT_HEAD_RE = /['"]\.orchestrator['"]\s*,\s*$/;
68
+ const SPLIT_TAIL_RE = /^\s*['"]metrics['"]/;
69
+
70
+ function isHit(lines, i) {
71
+ return METRICS_PATH_RE.test(lines[i]) || (SPLIT_HEAD_RE.test(lines[i]) && SPLIT_TAIL_RE.test(lines[i + 1] ?? ''));
72
+ }
73
+
74
+ const RESOLVER_CALL_RE =
75
+ /\b(?:resolveLedgerPath|resolveLedgerRoot|resolveEventsLedgerRoot|explainEventsLedgerRoot|eventsFilePath|resolveLearningsPaths|defaultSkillInvocationsPath)\s*\(/;
76
+
77
+ const MARKER_RE = /ledger-root:/;
78
+
79
+ /** `const NAME = '…'`, `= path.join('…'`, `= join('…'`, `= ['…'`, optionally inside Object.freeze( */
80
+ const CONST_DECL_RE =
81
+ /^\s*(?:export\s+)?const\s+([A-Z][A-Z0-9_]*)\s*=\s*(?:Object\.freeze\(\s*)?(?:(?:path\.)?join\(\s*|\[\s*)?['"`]\.orchestrator/;
82
+
83
+ const PATH_FN_RE = /\b(?:join|resolve)\s*\(/;
84
+
85
+ /**
86
+ * Files that build a metrics path on purpose without a resolver. Each reason
87
+ * is the census verdict (2026-10-22): INTENDED-LOCAL (the data belongs to this
88
+ * working tree), NOT-A-SINK (the string is no ledger path being opened) or
89
+ * ROUTED-DOWNSTREAM (the path is handed to a callee that resolves each ledger).
90
+ * Prefer the line marker `ledger-root: <reason>` for a single line in a file
91
+ * that otherwise reads ledgers.
92
+ */
93
+ export const ALLOWLIST = Object.freeze({
94
+ 'hooks/on-session-end.mjs':
95
+ 'ROUTED-DOWNSTREAM: hands the launch dir\'s metrics dir to the telemetry flush; buildBatch resolves each ledger in it (scripts/lib/telemetry/sync.mjs)',
96
+ 'hooks/pre-bash-sessions-ledger-guard.mjs':
97
+ 'NOT-A-SINK/INTENDED-LOCAL: matches command text, and probes the exact --repo-root the repair CLI is given',
98
+ 'scripts/lib/autopilot/flags.mjs': 'constant only: the relative DEFAULT_JSONL_PATH; the loop resolves it',
99
+ 'scripts/lib/convergence-monitor.mjs': 'NOT-A-SINK: a label and help text; the read goes through the events ledger root',
100
+ 'scripts/lib/playwright-driver/runner.mjs':
101
+ 'INTENDED-LOCAL: guards the per-run artifact dir, which the artifact builder roots in this tree',
102
+ 'scripts/lib/project-hygiene.mjs': 'NOT-A-SINK: a git ls-files pathspec over this tree\'s own index',
103
+ 'scripts/lib/quality-gate.mjs':
104
+ 'INTENDED-LOCAL: verification-failure bundles of this tree\'s gate run, which can carry secrets',
105
+ 'scripts/lib/quality-gates-cache.mjs':
106
+ 'INTENDED-LOCAL: cache keyed to this tree\'s session start ref and dependency hash',
107
+ 'scripts/lib/scope-baseline.mjs': 'NOT-A-SINK: an exclusion glob for drift counting',
108
+ 'scripts/lib/ux-grill/manifest.mjs': 'NOT-A-SINK: a .gitignore line for the run-artifact dir',
109
+ 'scripts/lib/vault-mirror/retention.mjs': 'NOT-A-SINK: text inside a generated note body',
110
+ 'scripts/lib/wave-transcript-tail.mjs': 'NOT-A-SINK: help text',
111
+ 'scripts/relocate-vault-corpus.mjs':
112
+ 'INTENDED-LOCAL: reads each child of --repos-root as named; mapping a worktree child would count its main twice',
113
+ 'scripts/run-migrate-v2-cross-repo.mjs': 'INTENDED-LOCAL: migrates the repos the operator names, as named',
114
+ });
115
+
116
+ function isCommentLine(line) {
117
+ const t = line.trim();
118
+ return t.startsWith('//') || t.startsWith('*') || t.startsWith('/*');
119
+ }
120
+
121
+ /**
122
+ * Line `i` plus up to {@link LOOKBACK} lines above it that belong to the SAME
123
+ * statement — the walk stops at a line ending in `;`, `{` or `}`, or at a blank
124
+ * line, so a resolver call in the previous statement does not cover this one.
125
+ */
126
+ function statementWindow(lines, i) {
127
+ const window = [lines[i]];
128
+ for (let j = i - 1; j >= 0 && window.length <= LOOKBACK; j -= 1) {
129
+ const prev = lines[j];
130
+ if (prev.trim() === '' || /[;{}]\s*$/.test(prev)) break;
131
+ window.unshift(prev);
132
+ }
133
+ return window.join('\n');
134
+ }
135
+
136
+ /** (a) or (c) for line `i`. */
137
+ function covered(lines, i) {
138
+ if (RESOLVER_CALL_RE.test(statementWindow(lines, i))) return true;
139
+ return MARKER_RE.test(lines[i]) || (i > 0 && MARKER_RE.test(lines[i - 1]));
140
+ }
141
+
142
+ /**
143
+ * @param {{ pluginRoot: string, dirs?: readonly string[], allowlist?: Record<string, string> }} opts
144
+ * @returns {{ ok: boolean, findings: Array<{ file: string, line: number, kind: 'unrouted'|'unrouted-constant'|'stale-allowlist', text?: string }>, summary: { filesScanned: number, hits: number, resolved: number, constants: number, marked: number, allowlisted: number } }}
145
+ */
146
+ export function scanMetricsLedgerRoot({ pluginRoot, dirs = SCAN_DIRS, allowlist = ALLOWLIST }) {
147
+ const files = enumerateRepoFiles({ repoRoot: pluginRoot, dirs: [...dirs], exts: ['.mjs'] });
148
+ const findings = [];
149
+ const seenAllowlisted = new Set();
150
+ const summary = { filesScanned: files.length, hits: 0, resolved: 0, constants: 0, marked: 0, allowlisted: 0 };
151
+ for (const abs of files) {
152
+ const rel = path.relative(pluginRoot, abs).split(path.sep).join('/');
153
+ if (/\.(test|spec)\.mjs$/.test(rel)) continue;
154
+ const lines = readFileSync(abs, 'utf8').split('\n');
155
+ const constNames = [];
156
+ const fileFindings = [];
157
+ let fileHits = 0;
158
+ lines.forEach((line, i) => {
159
+ if (isCommentLine(line) || !isHit(lines, i)) return;
160
+ fileHits += 1;
161
+ const decl = CONST_DECL_RE.exec(line);
162
+ if (decl) {
163
+ constNames.push(decl[1]);
164
+ summary.constants += 1;
165
+ } else if (covered(lines, i)) {
166
+ summary[RESOLVER_CALL_RE.test(statementWindow(lines, i)) ? 'resolved' : 'marked'] += 1;
167
+ } else {
168
+ fileFindings.push({ file: rel, line: i + 1, kind: 'unrouted', text: line.trim().slice(0, 160) });
169
+ }
170
+ });
171
+ if (constNames.length > 0) {
172
+ const uses = new RegExp(`\\b(?:${constNames.join('|')})\\b`);
173
+ lines.forEach((line, i) => {
174
+ if (isCommentLine(line) || CONST_DECL_RE.test(line)) return;
175
+ if (!uses.test(line) || !PATH_FN_RE.test(line) || covered(lines, i)) return;
176
+ fileFindings.push({ file: rel, line: i + 1, kind: 'unrouted-constant', text: line.trim().slice(0, 160) });
177
+ });
178
+ }
179
+ if (fileHits === 0) continue;
180
+ summary.hits += fileHits;
181
+ if (Object.hasOwn(allowlist, rel)) {
182
+ seenAllowlisted.add(rel);
183
+ continue;
184
+ }
185
+ findings.push(...fileFindings);
186
+ }
187
+ for (const rel of Object.keys(allowlist)) {
188
+ if (!seenAllowlisted.has(rel)) findings.push({ file: rel, line: 0, kind: 'stale-allowlist' });
189
+ }
190
+ summary.allowlisted = seenAllowlisted.size;
191
+ return { ok: findings.length === 0, findings, summary };
192
+ }
193
+
194
+ /**
195
+ * @param {string} pluginRoot absolute plugin root
196
+ * @returns {number} 0 = clean, 1 = findings
197
+ */
198
+ export function runCheckMetricsLedgerRoot(pluginRoot) {
199
+ console.log('--- Check: .orchestrator/metrics paths go through a ledger resolver (#1722) ---');
200
+ const inspection = scanMetricsLedgerRoot({ pluginRoot });
201
+ for (const f of inspection.findings) {
202
+ if (f.kind === 'stale-allowlist') {
203
+ console.log(` FAIL: ${f.file} is on the ALLOWLIST but builds no .orchestrator/metrics path any more — drop the entry`);
204
+ } else {
205
+ console.log(
206
+ ` FAIL: ${f.file}:${f.line} builds a .orchestrator/metrics path without resolveLedgerPath() ` +
207
+ '(events/sessions: resolveEventsLedgerRoot()) — from a linked worktree it reads or writes a copy ' +
208
+ `lost with the worktree; route it, or mark a non-ledger line with \`// ledger-root: <reason>\`: ${f.text}`,
209
+ );
210
+ }
211
+ }
212
+ const s = inspection.summary;
213
+ if (inspection.ok) {
214
+ console.log(
215
+ ` PASS: ${s.hits} metrics path line(s) in ${s.filesScanned} module(s): ${s.resolved} resolved, ` +
216
+ `${s.constants} relative constant(s), ${s.marked} marked, ${s.allowlisted} allowlisted module(s)`,
217
+ );
218
+ }
219
+ console.log('');
220
+ console.log(`Results: ${inspection.ok ? 1 : 0} passed, ${inspection.findings.length} failed`);
221
+ return inspection.ok ? 0 : 1;
222
+ }
223
+
224
+ if (isMainModule(import.meta.url)) {
225
+ const args = process.argv.slice(2).filter((arg) => arg !== '--json');
226
+ const root = path.resolve(args[0] || process.cwd());
227
+ if (process.argv.includes('--json')) {
228
+ const inspection = scanMetricsLedgerRoot({ pluginRoot: root });
229
+ process.stdout.write(`${JSON.stringify(inspection, null, 2)}\n`);
230
+ process.exitCode = inspection.ok ? 0 : 1;
231
+ } else {
232
+ process.exitCode = runCheckMetricsLedgerRoot(root);
233
+ }
234
+ }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, readdirSync, statSync, existsSync, realpathSync, lstatSync, openSync, fstatSync, closeSync, constants } from 'node:fs';
3
+ import { readFileSync, readdirSync, statSync, existsSync, realpathSync, lstatSync, openSync, fstatSync, closeSync, readSync, constants } from 'node:fs';
4
4
  import { join, extname, relative, basename, sep, resolve, dirname, isAbsolute } from 'node:path';
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import { argv } from 'node:process';
@@ -21,18 +21,75 @@ const isMain =
21
21
 
22
22
  const pluginRoot = argv[2] ? canonicalPath(resolve(argv[2])) : argv[2];
23
23
  const includeUntracked = argv.slice(3).includes('--include-untracked');
24
- const requireOwnerPatterns = argv.slice(3).includes('--require-owner-patterns');
24
+ // --public-export (#1631): the root is a curated public snapshot. Adds the path check against
25
+ // the export policy and implies --require-owner-patterns (a public tree is never scanned blind).
26
+ const publicExport = argv.slice(3).includes('--public-export');
27
+ const requireOwnerPatterns = argv.slice(3).includes('--require-owner-patterns') || publicExport;
25
28
  const packedIndex = argv.indexOf('--packed-files');
26
29
  const packedInventoryPath = packedIndex >= 0 ? argv[packedIndex + 1] : null;
27
30
  if (isMain && packedIndex >= 0 && (!packedInventoryPath || packedInventoryPath.startsWith('--'))) {
28
31
  console.error('packed-files requires an inventory path'); process.exit(1);
29
32
  }
30
33
  if (isMain && !pluginRoot) {
31
- console.error('Usage: check-owner-leakage.mjs <plugin-root> [--include-untracked]');
34
+ console.error('Usage: check-owner-leakage.mjs <plugin-root> [--include-untracked] [--require-owner-patterns] [--public-export]');
32
35
  process.exit(1);
33
36
  }
34
37
 
35
38
  // Host policy stays outside source and package; this file remains standalone.
39
+
40
+ // Public-export policy (#1631, #1594). Lives here, not in scripts/lib/github-export.mjs,
41
+ // because vendored copies carry this file alone; the export CLI imports it from here.
42
+ export const PUBLIC_EXPORT_POLICY_FILE = '.orchestrator/policy/public-export.json';
43
+
44
+ /**
45
+ * Validate a public-export policy document.
46
+ * Each `exclude[].path` is an exact repo-relative file, or a directory prefix ending in
47
+ * `/` — no globs, no absolute paths, no `.`/`..` segments.
48
+ * @param {unknown} policy parsed JSON
49
+ * @returns {{ ok: true, excludes: string[] } | { ok: false, reason: string }}
50
+ */
51
+ export function validatePublicExportPolicy(policy) {
52
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy) || policy.version !== 1) return { ok: false, reason: 'policy must be an object with version 1' };
53
+ if (!Array.isArray(policy.exclude) || policy.exclude.length === 0) return { ok: false, reason: 'exclude must be a non-empty array' };
54
+ const excludes = [];
55
+ for (const entry of policy.exclude) {
56
+ const p = entry?.path;
57
+ if (typeof p !== 'string' || !p || p.startsWith('/') || /[*?[\]{}\\]/.test(p) || [...p].some((c) => c.charCodeAt(0) < 32)
58
+ || p.replace(/\/$/, '').split('/').some((part) => !part || part === '.' || part === '..')) {
59
+ return { ok: false, reason: 'invalid exclude path (exact file or dir prefix ending in "/", no globs)' };
60
+ }
61
+ excludes.push(p);
62
+ }
63
+ return { ok: true, excludes };
64
+ }
65
+
66
+ /**
67
+ * Read and validate `<root>/.orchestrator/policy/public-export.json`.
68
+ * @param {string} root
69
+ * @returns {{ ok: true, excludes: string[] } | { ok: false, reason: string }}
70
+ */
71
+ export function loadPublicExportPolicy(root) {
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(readFileSync(join(root, PUBLIC_EXPORT_POLICY_FILE), 'utf8'));
75
+ } catch (err) {
76
+ return { ok: false, reason: err?.code === 'ENOENT' ? `${PUBLIC_EXPORT_POLICY_FILE} missing` : `${PUBLIC_EXPORT_POLICY_FILE} unreadable or not JSON` };
77
+ }
78
+ return validatePublicExportPolicy(parsed);
79
+ }
80
+
81
+ /**
82
+ * True when a repo-relative path falls under a public-export exclude. A directory
83
+ * prefix matches only at a path boundary: `docs/audits/` excludes `docs/audits/x.md`
84
+ * but never the sibling `docs/auditsX.md`.
85
+ * @param {string} relPath forward-slash repo-relative path
86
+ * @param {string[]} excludes
87
+ * @returns {boolean}
88
+ */
89
+ export function isPublicExportExcluded(relPath, excludes) {
90
+ const norm = relPath.replace(/\\/g, '/');
91
+ return excludes.some((p) => (p.endsWith('/') ? norm.startsWith(p) : norm === p));
92
+ }
36
93
  export const VAULT_CLEAR_SLUGS = new Set();
37
94
 
38
95
  // Mirrors the zero-import private-config-dir resolver because vendored copies carry this file alone.
@@ -47,12 +104,24 @@ function policySource(env = process.env) {
47
104
  const dir = own || (xdg ? join(xdg, 'session-orchestrator') : join(homedir(), '.config', 'session-orchestrator'));
48
105
  return (env.SO_OWNER_PATTERNS_FILE || '').trim() || join(dir, 'owner-patterns.json');
49
106
  }
107
+ // #1629: a refused DIRECTORY is a host-setup fault (umask 000 leaves the dir 0777), not a
108
+ // leak — it carries the dir so the one-time warning can name it instead of a bare CP1.
109
+ function unsafeDir(dir, st) {
110
+ return Object.assign(new Error('unsafe-dir'), { dir, why: st.isSymbolicLink() ? 'a symlink' : !st.isDirectory() ? 'not a directory' : `mode ${(st.mode & 0o777).toString(8)}, writable by group/others` });
111
+ }
50
112
  function checkedHostFile(filename) {
51
113
  if (!isAbsolute(filename)) throw new Error('unsafe');
114
+ // Windows has no POSIX mode bits: Node reports 0o666 (0o444 read-only) for every
115
+ // file and directory, so the group/other-write and 0600 checks below would refuse
116
+ // EVERY policy file there (measured on Windows 11, #1612). They apply on POSIX only;
117
+ // the symlink, hard-link, repo-ancestor and size checks still apply everywhere.
118
+ // Ceiling (BV-004): Windows ACLs are not inspected — revisit if the policy file
119
+ // ever carries secrets rather than private names.
120
+ const posixModes = process.platform !== 'win32';
52
121
  // Canonical OS aliases (/tmp on macOS) are allowed above the private source parent.
53
122
  let cursor = dirname(filename);
54
123
  const parent = lstatSync(cursor);
55
- if (!parent.isDirectory() || parent.isSymbolicLink() || (parent.mode & 0o022)) throw new Error('unsafe');
124
+ if (!parent.isDirectory() || parent.isSymbolicLink() || (posixModes && (parent.mode & 0o022))) throw unsafeDir(cursor, parent);
56
125
  const canonicalParent = realpathSync(cursor);
57
126
  // Reject repo-local data even if ignored; walk every ancestor for a .git marker.
58
127
  cursor = canonicalParent;
@@ -64,27 +133,50 @@ function checkedHostFile(filename) {
64
133
  cursor = dirname(filename);
65
134
  while (cursor !== dirname(cursor)) {
66
135
  const st = lstatSync(cursor);
67
- if ((st.isSymbolicLink() && st.uid !== 0) || (st.uid !== 0 && (st.mode & 0o022))) throw new Error('unsafe');
136
+ if ((st.isSymbolicLink() && st.uid !== 0) || (posixModes && st.uid !== 0 && (st.mode & 0o022))) throw unsafeDir(cursor, st);
68
137
  cursor = dirname(cursor);
69
138
  }
70
139
  const st = lstatSync(filename);
71
- if (!st.isFile() || st.isSymbolicLink() || (st.mode & 0o7777) !== 0o600 || st.nlink !== 1 || (typeof process.getuid === 'function' && st.uid !== process.getuid())) throw new Error('unsafe');
140
+ if (!st.isFile() || st.isSymbolicLink() || (posixModes && (st.mode & 0o7777) !== 0o600) || st.nlink !== 1 || (typeof process.getuid === 'function' && st.uid !== process.getuid())) throw new Error('unsafe');
72
141
  if (st.size > POLICY_LIMIT_BYTES) throw new Error('invalid');
73
142
  const fd = openSync(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
74
143
  try {
75
144
  const opened = fstatSync(fd);
76
- if (!opened.isFile() || opened.ino !== st.ino || opened.dev !== st.dev || (opened.mode & 0o7777) !== 0o600 || opened.size > POLICY_LIMIT_BYTES) throw new Error('unsafe');
145
+ if (!opened.isFile() || opened.ino !== st.ino || opened.dev !== st.dev || (posixModes && (opened.mode & 0o7777) !== 0o600) || opened.size > POLICY_LIMIT_BYTES) throw new Error('unsafe');
77
146
  const raw = readFileSync(fd, 'utf8');
78
147
  if (Buffer.byteLength(raw) > POLICY_LIMIT_BYTES) throw new Error('invalid');
79
148
  return raw;
80
149
  } finally { closeSync(fd); }
81
150
  }
151
+ // Redacted label for a refused directory: env-var name or `~`-relative, never the raw path
152
+ // (a custom SO_CONFIG_HOME may carry private names). Ancestors render as `<label>/..`.
153
+ function configDirLabel(dir, filename, env) {
154
+ const patterns = (env.SO_OWNER_PATTERNS_FILE || '').trim(), own = (env.SO_CONFIG_HOME || '').trim(), xdg = (env.XDG_CONFIG_HOME || '').trim();
155
+ const [base, label] = patterns ? [dirname(filename), 'dirname($SO_OWNER_PATTERNS_FILE)'] : own ? [own, '$SO_CONFIG_HOME'] : xdg ? [join(xdg, 'session-orchestrator'), '$XDG_CONFIG_HOME/session-orchestrator'] : [homedir(), '~'];
156
+ const rel = relative(base, dir);
157
+ if (!rel) return label;
158
+ return rel.split(sep).every((part) => part === '..') || label === '~' && !rel.startsWith('..') ? `${label}/${rel.split(sep).join('/')}` : 'an ancestor directory of the owner policy';
159
+ }
160
+ const warnedUnsafeDirs = new Set();
161
+ function warnUnsafeConfigDir(err, filename, env) {
162
+ if (warnedUnsafeDirs.has(err.dir)) return;
163
+ warnedUnsafeDirs.add(err.dir);
164
+ const label = configDirLabel(err.dir, filename, env);
165
+ // chmod cannot repair a symlink or a non-directory — name the fix that works.
166
+ const fix = err.why === 'a symlink' ? `replace the symlink ${label} with a real directory (mode 700)`
167
+ : err.why === 'not a directory' ? `replace ${label} with a real directory (mode 700)`
168
+ : err.dir === dirname(filename) ? `chmod 700 ${label}` : `chmod go-w ${label}`;
169
+ console.error(`WARN owner-patterns: unsafe config directory ${label} (${err.why}); owner policy not loaded, so every segment fails closed as CP1 — not a leak. Fix: ${fix}`);
170
+ }
82
171
  export function inspectOwnerPatterns({ filePath, env = process.env, refresh = false } = {}) {
83
172
  const filename = filePath ?? policySource(env);
84
173
  if (!refresh && ownerCache?.filename === filename) return ownerCache.result;
85
174
  let result;
86
175
  try {
87
- const policy = JSON.parse(checkedHostFile(filename));
176
+ // #1613: Windows PowerShell 5.1 (`Set-Content -Encoding utf8`) writes a UTF-8 BOM,
177
+ // which JSON.parse rejects — strip it so such a policy is not read as invalid.
178
+ const raw = checkedHostFile(filename);
179
+ const policy = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
88
180
  if (!policy || Array.isArray(policy) || policy.version !== 1 || Object.keys(policy).some((key) => key !== 'version' && !POLICY_KEYS.includes(key))) throw new Error('invalid');
89
181
  for (const key of POLICY_KEYS) {
90
182
  if (!Object.hasOwn(policy, key)) policy[key] = [];
@@ -94,7 +186,13 @@ export function inspectOwnerPatterns({ filePath, env = process.env, refresh = fa
94
186
  if (!POLICY_KEYS.slice(0, 6).concat('personalNames').some((key) => policy[key].length)) throw new Error('invalid');
95
187
  result = { status: 'ok', policy };
96
188
  } catch (err) {
97
- result = { status: err.code === 'ENOENT' ? 'missing' : err.message === 'unsafe' || err.code === 'ELOOP' ? 'unsafe' : 'invalid', reason: err.code ?? (err.message === 'unsafe' ? 'unsafe-source' : 'invalid-source') };
189
+ if (err.message === 'unsafe-dir') {
190
+ // Fail-closed unchanged: an unsafe directory never supplies (or waives) a policy.
191
+ result = { status: 'unsafe', reason: 'unsafe-config-dir' };
192
+ warnUnsafeConfigDir(err, filename, env);
193
+ } else {
194
+ result = { status: err.code === 'ENOENT' ? 'missing' : err.message === 'unsafe' || err.code === 'ELOOP' ? 'unsafe' : 'invalid', reason: err.code ?? (err.message === 'unsafe' ? 'unsafe-source' : 'invalid-source') };
195
+ }
98
196
  }
99
197
  ownerCache = {filename, result};
100
198
  VAULT_CLEAR_SLUGS.clear();
@@ -394,9 +492,14 @@ export async function getConfidentialNamePatterns({ loadHostPaths } = {}) {
394
492
  const TEXT_EXTS = new Set([
395
493
  '.md',
396
494
  '.mdx',
495
+ '.mdc',
397
496
  '.mjs',
497
+ '.cjs',
398
498
  '.js',
399
499
  '.ts',
500
+ '.tsx',
501
+ '.py',
502
+ '.toml',
400
503
  '.json',
401
504
  '.jsonl',
402
505
  '.yml',
@@ -411,11 +514,41 @@ const TEXT_EXTS = new Set([
411
514
 
412
515
  const DOTFILE_ALLOWLIST = new Set(['.env.example', '.nvmrc', '.vault.yaml']);
413
516
 
517
+ const BINARY_SNIFF_BYTES = 8192;
518
+
519
+ /**
520
+ * Cheap binary sniff for extensionless files (LICENSE, Dockerfile, .husky hooks,
521
+ * fixture launchers): a NUL byte in the first 8 KB means binary, the same
522
+ * heuristic git uses. An unreadable file counts as text so the scan loop
523
+ * reports it as "file unreadable" instead of skipping it silently.
524
+ * @param {string} filePath
525
+ * @returns {boolean}
526
+ */
527
+ function looksLikeText(filePath) {
528
+ let fd;
529
+ try {
530
+ fd = openSync(filePath, 'r');
531
+ const buf = Buffer.alloc(BINARY_SNIFF_BYTES);
532
+ const n = readSync(fd, buf, 0, BINARY_SNIFF_BYTES, 0);
533
+ return !buf.subarray(0, n).includes(0);
534
+ } catch {
535
+ return true;
536
+ } finally {
537
+ if (fd !== undefined) closeSync(fd);
538
+ }
539
+ }
540
+
541
+ // Formats that are binary by definition: never scanned, whatever their first bytes look like.
542
+ const BINARY_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.icns', '.pdf', '.woff', '.woff2', '.ttf', '.otf', '.eot', '.zip', '.gz', '.tgz', '.bz2', '.xz', '.7z', '.mp3', '.mp4', '.mov', '.webm', '.wav', '.ogg', '.wasm']);
543
+
414
544
  function isTextFile(filePath) {
415
545
  const base = basename(filePath);
416
546
  if (DOTFILE_ALLOWLIST.has(base)) return true;
417
547
  const ext = extname(filePath);
418
- return ext ? TEXT_EXTS.has(ext) : false;
548
+ // A listed extension is text; any other file (no extension, or an unknown one such as .tmpl,
549
+ // .template, .lock) is scanned unless it looks binary, so a new text format is never skipped.
550
+ if (TEXT_EXTS.has(ext)) return true;
551
+ return !BINARY_EXTS.has(ext.toLowerCase()) && looksLikeText(filePath);
419
552
  }
420
553
 
421
554
  function getTrackedFiles() {
@@ -534,7 +667,9 @@ const SELF_EXCLUSIONS = new Set([
534
667
  const scanFiles = textFiles.filter((f) => {
535
668
  const rel = relative(pluginRoot, f).replace(/\\/g, '/');
536
669
  if (!packedInventoryPath && rel.startsWith('.orchestrator/audits/')) return false;
537
- if (!packedInventoryPath && SELF_EXCLUSIONS.has(rel)) return false;
670
+ // --public-export scans everything that ships: these self-exclusions exist for the
671
+ // private repo's own CI, and a public snapshot must not carry their literals unscanned.
672
+ if (!packedInventoryPath && !publicExport && SELF_EXCLUSIONS.has(rel)) return false;
538
673
  return true;
539
674
  });
540
675
 
@@ -548,6 +683,19 @@ if (cp11.disabledReason) {
548
683
  fail(`<scan-wide> — CP11 (guard disabled): ${cp11.disabledReason}`);
549
684
  }
550
685
 
686
+ // --public-export: path-based, because a content pattern cannot recognise e.g. a
687
+ // preference card or an audit report. Runs over ALL tracked files, not only text files.
688
+ if (publicExport) {
689
+ const exportPolicy = loadPublicExportPolicy(pluginRoot);
690
+ if (!exportPolicy.ok) {
691
+ fail(`<scan-wide> — PX (public-export policy unavailable): ${exportPolicy.reason}`);
692
+ } else {
693
+ const leakedPaths = allFiles.map((f) => relative(pluginRoot, f).replace(/\\/g, '/')).filter((rel) => isPublicExportExcluded(rel, exportPolicy.excludes));
694
+ for (const rel of leakedPaths) fail(`${safeDiagnosticPath(rel, cp11Patterns)} — PX (path excluded from public export)`);
695
+ if (leakedPaths.length === 0) pass(`no public-export excluded path among ${allFiles.length} files`);
696
+ }
697
+ }
698
+
551
699
  const violations = [];
552
700
 
553
701
  for (const filePath of scanFiles) {