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
@@ -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) {
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-test-posix-shell.mjs — tests must not spawn a POSIX shell by bare name.
4
+ *
5
+ * WHY. On native Windows `sh` is not on PATH and `bash` resolves to
6
+ * C:\Windows\System32\bash.exe, which is WSL and cannot see Windows paths
7
+ * (measured 2026-10-22 on the Windows 11 test PC, `where sh & where bash`,
8
+ * SO#1613). A test that runs `spawnSync('sh', …)` or `execFileSync('/bin/bash', …)`
9
+ * is therefore red on Windows for a reason that has nothing to do with the code
10
+ * under test. The native run at fd8d3673 counted 301 red tests in exactly this
11
+ * class; the same session moved ~25 test files to `posixShell()` from
12
+ * `tests/_helpers/posix-shell.mjs`, which returns the name unchanged off Windows
13
+ * and Git Bash's sh.exe/bash.exe on win32. This check keeps the class from
14
+ * coming back.
15
+ *
16
+ * RULE. In tracked `tests/**\/*.mjs`, a finding is:
17
+ * - a call to `spawn` / `spawnSync` / `execFile` / `execFileSync` (bare or as a
18
+ * member, e.g. `cp.spawnSync`) whose FIRST argument is the string literal
19
+ * `sh`, `bash`, `/bin/sh` or `/bin/bash` in any quote style;
20
+ * - a call to `exec` / `execSync` whose command string literal starts with
21
+ * `sh `, `bash `, `/bin/sh ` or `/bin/bash `.
22
+ * A call through `posixShell(...)` is never a finding (its first argument is not
23
+ * a literal). `tests/_helpers/posix-shell.mjs` itself is exempt.
24
+ *
25
+ * METHOD. Textual, over the shared lexer `scripts/lib/js-mask.mjs`: the call
26
+ * name and its opening quote are matched on the fully masked source (comments
27
+ * and literal interiors blanked), so a commented-out spawn and fixture source
28
+ * embedded in a template literal are not scanned as code; the literal VALUE is
29
+ * then read at the same offset from the comments-only mask (offsets are
30
+ * preserved by both modes).
31
+ * Named ceiling (BV-004): a shell name passed through a variable
32
+ * (`const SH = 'sh'; spawnSync(SH, …)`) is not seen. REVISIT if a native
33
+ * Windows run shows that shape red.
34
+ *
35
+ * BASELINE. Three files are changed by open merge requests (!239/!237, !240) at the
36
+ * time this check landed, so the #1613 session left them untouched; each is
37
+ * baselined below (WARN, not FAIL) and gets converted after those MRs land. An
38
+ * entry whose file no longer has a finding FAILs as `baseline-stale` so a
39
+ * converted file does not leave a permanent exemption behind; an entry with an
40
+ * empty reason FAILs as `baseline-missing-reason` (same discipline as
41
+ * `check-hooks-emit-event-guard.mjs`). Every other site FAILs.
42
+ *
43
+ * Usage: check-test-posix-shell.mjs <repo-root>
44
+ * Output: ` PASS: …` / ` WARN: …` / ` FAIL: …` lines, then
45
+ * `Results: N passed, M failed`. Exit 0 = clean (baselined WARNs allowed),
46
+ * 1 = at least one new finding or baseline defect, 2 = tool error.
47
+ *
48
+ * Import-safety: importing this module executes nothing — the isMain guard at
49
+ * the bottom is the only side-effecting path.
50
+ */
51
+
52
+ import { readFileSync } from 'node:fs';
53
+ import path from 'node:path';
54
+ import { maskSource } from '../js-mask.mjs';
55
+ import { listRepoFiles } from './repo-files.mjs';
56
+ import { isMainModule } from '../is-main-module.mjs';
57
+
58
+ const SCAN_DIR = 'tests';
59
+
60
+ /** The helper that resolves the shell; it may name the shells itself. */
61
+ const EXEMPT_FILES = new Set(['tests/_helpers/posix-shell.mjs']);
62
+
63
+ /**
64
+ * Baseline — Map<repo-relative file, reason>. Keyed per file, not per line:
65
+ * the open MRs reshape these files, and a line key would silently stop matching.
66
+ */
67
+ const BASELINE_FILES = new Map([
68
+ [
69
+ 'tests/skills/tmux-layout.test.mjs',
70
+ '#1613: file is changed by an open MR (!240); convert to posixShell() after it lands',
71
+ ],
72
+ [
73
+ 'tests/so-hud/rules.test.mjs',
74
+ '#1613: file is changed by an open MR (!239/!237); convert to posixShell() after it lands',
75
+ ],
76
+ [
77
+ 'tests/scripts/sweep-expired-learnings-cli.test.mjs',
78
+ '#1613: file is changed by an open MR (!239/!237); convert to posixShell() after it lands',
79
+ ],
80
+ ]);
81
+
82
+ const SHELL_NAMES = new Set(['sh', 'bash', '/bin/sh', '/bin/bash']);
83
+ const SHELL_COMMAND_PREFIX_RE = /^(?:\/bin\/)?(?:sh|bash) /;
84
+
85
+ /** Call name plus open paren plus a quote, on the masked source. */
86
+ const CALL_RE = /\b(spawn|spawnSync|execFile|execFileSync|exec|execSync)\s*\(\s*(['"`])/g;
87
+
88
+ /**
89
+ * Read the string literal that opens at `start` (a quote char) in `text`.
90
+ *
91
+ * @param {string} text comments-only masked source
92
+ * @param {number} start offset of the opening quote
93
+ * @returns {string | null} the literal's value, or null when it is not a
94
+ * single-line literal without `${` interpolation
95
+ */
96
+ function literalAt(text, start) {
97
+ const quote = text[start];
98
+ const end = text.indexOf(quote, start + 1);
99
+ if (end === -1) return null;
100
+ const value = text.slice(start + 1, end);
101
+ if (value.includes('\n') || value.includes('${')) return null;
102
+ return value;
103
+ }
104
+
105
+ /**
106
+ * Find bare-shell spawn call sites in one source text.
107
+ *
108
+ * @param {string} source
109
+ * @returns {{line: number, call: string, shell: string}[]}
110
+ */
111
+ export function findBareShellCalls(source) {
112
+ const masked = maskSource(source);
113
+ const literals = maskSource(source, { keepLiterals: true });
114
+ /** @type {{line: number, call: string, shell: string}[]} */
115
+ const calls = [];
116
+ for (const m of masked.matchAll(CALL_RE)) {
117
+ const call = m[1];
118
+ const quoteAt = m.index + m[0].length - 1;
119
+ const value = literalAt(literals, quoteAt);
120
+ if (value === null) continue;
121
+ const isExecFamily = call === 'exec' || call === 'execSync';
122
+ const hit = isExecFamily ? SHELL_COMMAND_PREFIX_RE.test(value) : SHELL_NAMES.has(value);
123
+ if (!hit) continue;
124
+ const line = source.slice(0, m.index).split('\n').length;
125
+ calls.push({ line, call, shell: isExecFamily ? value.split(' ')[0] : value });
126
+ }
127
+ return calls;
128
+ }
129
+
130
+ /**
131
+ * @typedef {{kind: 'bare-shell', file: string, line: number, call: string, shell: string, baselined: boolean}} ShellFinding
132
+ * @typedef {{kind: 'baseline-stale' | 'baseline-missing-reason', file: string, message: string}} BaselineFinding
133
+ */
134
+
135
+ /**
136
+ * Scan tracked `tests/**\/*.mjs` under `repoRoot`.
137
+ *
138
+ * @param {string} repoRoot
139
+ * @param {{baseline?: Map<string, string>}} [options] `baseline` defaults to the
140
+ * shipped {@link BASELINE_FILES}; injectable so tests exercise the baseline
141
+ * paths against a tmp fixture.
142
+ * @returns {{findings: (ShellFinding | BaselineFinding)[], readErrors: {file: string, message: string}[]}}
143
+ */
144
+ export function scanTestPosixShell(repoRoot, { baseline = BASELINE_FILES } = {}) {
145
+ const files = listRepoFiles(repoRoot, { dirs: [SCAN_DIR], exts: ['mjs'] });
146
+ /** @type {(ShellFinding | BaselineFinding)[]} */
147
+ const findings = [];
148
+ /** @type {{file: string, message: string}[]} */
149
+ const readErrors = [];
150
+ /** @type {Set<string>} */
151
+ const matchedBaseline = new Set();
152
+
153
+ for (const absFile of files) {
154
+ const rel = path.relative(repoRoot, absFile).split(path.sep).join('/');
155
+ if (EXEMPT_FILES.has(rel)) continue;
156
+ let source;
157
+ try {
158
+ source = readFileSync(absFile, 'utf8');
159
+ } catch (err) {
160
+ readErrors.push({ file: rel, message: `unreadable: ${err?.message ?? String(err)}` });
161
+ continue;
162
+ }
163
+ if (!/\b(?:spawn|exec)/.test(source)) continue; // cheap textual pre-filter
164
+
165
+ const calls = findBareShellCalls(source);
166
+ if (calls.length === 0) continue;
167
+ const baselined = baseline.has(rel);
168
+ if (baselined) {
169
+ matchedBaseline.add(rel);
170
+ if (String(baseline.get(rel) ?? '').trim() === '') {
171
+ findings.push({
172
+ kind: 'baseline-missing-reason',
173
+ file: rel,
174
+ message: 'baseline entry has no reason — name the linked issue or remove the entry',
175
+ });
176
+ }
177
+ }
178
+ for (const c of calls) findings.push({ kind: 'bare-shell', file: rel, ...c, baselined });
179
+ }
180
+
181
+ for (const file of baseline.keys()) {
182
+ if (matchedBaseline.has(file)) continue;
183
+ findings.push({
184
+ kind: 'baseline-stale',
185
+ file,
186
+ message: 'baseline entry no longer has a bare shell spawn (converted or removed) — remove the entry',
187
+ });
188
+ }
189
+
190
+ return { findings, readErrors };
191
+ }
192
+
193
+ /**
194
+ * Run the check and print the validate-plugin line vocabulary.
195
+ *
196
+ * @param {string} repoRoot
197
+ * @param {{baseline?: Map<string, string>}} [options] forwarded to {@link scanTestPosixShell}
198
+ * @returns {number} 0 = clean, 1 = new finding(s), 2 = tool error
199
+ */
200
+ export function runCheckTestPosixShell(repoRoot, options) {
201
+ console.log('--- Check: tests spawn a POSIX shell via posixShell(), not by bare name (#1613) ---');
202
+
203
+ const { findings, readErrors } = scanTestPosixShell(repoRoot, options);
204
+ if (readErrors.length > 0) {
205
+ for (const e of readErrors) console.log(` FAIL: ${e.file} — ${e.message}`);
206
+ console.log('');
207
+ console.log(`Results: 0 passed, ${readErrors.length} failed`);
208
+ return 2;
209
+ }
210
+
211
+ const shellFindings = /** @type {ShellFinding[]} */ (findings.filter((f) => f.kind === 'bare-shell'));
212
+ const baselineIssues = /** @type {BaselineFinding[]} */ (findings.filter((f) => f.kind !== 'bare-shell'));
213
+ const fresh = shellFindings.filter((f) => !f.baselined);
214
+ const old = shellFindings.filter((f) => f.baselined);
215
+
216
+ if (fresh.length === 0 && baselineIssues.length === 0) {
217
+ console.log(
218
+ old.length === 0
219
+ ? ' PASS: no test spawns sh/bash by bare name'
220
+ : ` PASS: no NEW bare sh/bash spawn in tests/ (${old.length} pre-existing, baselined — see #1613)`,
221
+ );
222
+ }
223
+ for (const f of old) {
224
+ console.log(` WARN: ${f.file}:${f.line} — ${f.call}('${f.shell}' …) by bare name (baselined #1613, file is in an open MR)`);
225
+ }
226
+ for (const f of fresh) {
227
+ console.log(
228
+ ` FAIL: ${f.file}:${f.line} — ${f.call}('${f.shell}' …) by bare name; on native Windows sh is not on PATH and bash is WSL — use posixShell() from tests/_helpers/posix-shell.mjs`,
229
+ );
230
+ }
231
+ for (const f of baselineIssues) console.log(` FAIL: ${f.file} — ${f.message} (${f.kind})`);
232
+
233
+ const failCount = fresh.length + baselineIssues.length;
234
+ console.log('');
235
+ console.log(`Results: ${failCount === 0 ? 1 : 0} passed, ${failCount} failed`);
236
+ return failCount > 0 ? 1 : 0;
237
+ }
238
+
239
+ if (isMainModule(import.meta.url)) {
240
+ const root = process.argv[2];
241
+ if (!root) {
242
+ console.error('Usage: check-test-posix-shell.mjs <repo-root>');
243
+ process.exit(2);
244
+ }
245
+ process.exit(runCheckTestPosixShell(path.resolve(root)));
246
+ }
@@ -292,6 +292,8 @@ import path from 'node:path';
292
292
 
293
293
  import { sizingSubject } from '../learnings/sizing-subject.mjs';
294
294
  import { isMainModule } from '../is-main-module.mjs';
295
+ import { resolveLedgerPath } from '../ledger-path.mjs';
296
+ import { resolveEventsLedgerRoot } from '../platform.mjs';
295
297
 
296
298
  /** Documented config surface — every `yaml` fence in this file is a declaration. */
297
299
  const TEMPLATE_REL = 'docs/session-config-template.md';
@@ -1248,7 +1250,7 @@ export function collectHandKeyedLearningSubjects(pluginRoot) {
1248
1250
  let judged = 0;
1249
1251
  let unattributed = 0;
1250
1252
 
1251
- const learnings = readJsonl(path.join(pluginRoot, LEARNINGS_REL));
1253
+ const learnings = readJsonl(resolveLedgerPath(LEARNINGS_REL, pluginRoot));
1252
1254
  if (learnings.length === 0) return { findings, scanned: { judged, unattributed } };
1253
1255
 
1254
1256
  // A session record can be addressed by either identity form (see
@@ -1257,7 +1259,7 @@ export function collectHandKeyedLearningSubjects(pluginRoot) {
1257
1259
  // a resolvable record is never mis-counted as unattributed.
1258
1260
  /** @type {Map<string, Record<string, unknown>>} */
1259
1261
  const sessionsById = new Map();
1260
- for (const record of readJsonl(path.join(pluginRoot, SESSIONS_REL))) {
1262
+ for (const record of readJsonl(path.join(resolveEventsLedgerRoot(pluginRoot), SESSIONS_REL))) {
1261
1263
  for (const field of ['session_id', 'semantic_session_id']) {
1262
1264
  const id = record[field];
1263
1265
  if (typeof id === 'string' && id !== '' && !sessionsById.has(id)) sessionsById.set(id, record);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Separate WARN-only check for gh pr commands rejected with --repo/-R (#1024).
4
+ * Shares corpus discovery and call-shape parsing with the bare-repo census,
5
+ * but its findings advise naming the PR or dropping the flag, never adding it.
6
+ * Coverage ceilings (literal argv, shell fences/strings) are shared with that census.
7
+ */
8
+ import path from 'node:path';
9
+ import { inspectVcsRepoFlag } from './check-vcs-repo-flag.mjs';
10
+ import { isMainModule } from '../is-main-module.mjs';
11
+
12
+ /** @param {string} pluginRoot independent check envelope */
13
+ export function inspectVcsRepoFlagPositional(pluginRoot) {
14
+ const result = inspectVcsRepoFlag(pluginRoot, { rejected: true });
15
+ return {
16
+ ok: result.ok,
17
+ toolError: result.toolError,
18
+ summary: {
19
+ filesScanned: result.summary.filesScanned,
20
+ unresolvedArgv: result.summary.unresolvedArgv,
21
+ findings: result.findings.length,
22
+ byKind: result.summary.byKind,
23
+ },
24
+ findings: result.findings,
25
+ };
26
+ }
27
+
28
+ if (isMainModule(import.meta.url)) {
29
+ const args = process.argv.slice(2);
30
+ const usage =
31
+ 'Usage: check-vcs-repo-flag-positional.mjs [<plugin-root>] [--json]\n' +
32
+ 'Exit: 0 check completed (findings WARN-only) · 1 usage error · 2 tool error';
33
+ const unknown = args.filter((arg) => arg.startsWith('-') && !['--json', '--help'].includes(arg));
34
+ const roots = args.filter((arg) => !arg.startsWith('-'));
35
+ if (unknown.length || roots.length > 1) {
36
+ console.error(usage);
37
+ process.exitCode = 1;
38
+ } else if (args.includes('--help')) {
39
+ console.log(usage);
40
+ } else {
41
+ const result = inspectVcsRepoFlagPositional(path.resolve(roots[0] ?? process.cwd()));
42
+ if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
43
+ else {
44
+ console.log('--- Check: --repo/-R requiring a PR positional (WARN-only) ---');
45
+ for (const finding of result.findings) {
46
+ console.log(
47
+ ` ${result.toolError ? 'FAIL' : 'WARN'}: [${finding.kind}] ${finding.file}:${finding.line} — ${finding.message}`,
48
+ );
49
+ }
50
+ if (!result.toolError)
51
+ console.log(
52
+ ` PASS: scanned ${result.summary.filesScanned} file(s), ${result.findings.length} rejected repo flag(s), ${result.summary.unresolvedArgv} variable argv array(s) unjudged`,
53
+ );
54
+ console.log(`Results: ${result.toolError ? '0 passed, 1 failed' : '1 passed, 0 failed'}`);
55
+ }
56
+ process.exitCode = result.toolError ? 2 : 0;
57
+ }
58
+ }