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
@@ -23,9 +23,10 @@
23
23
  * queue (bounded, oldest-dropped) and the session closes with zero user-facing
24
24
  * error.
25
25
  *
26
- * Node ESM. The only network dependency is the global `fetch`.
26
+ * Node ESM. Networking uses the Stdlib HTTP helper.
27
27
  */
28
28
 
29
+ import { httpRequest } from '../http-request.mjs';
29
30
  import path from 'node:path';
30
31
  import os from 'node:os';
31
32
  import fs from 'node:fs';
@@ -43,10 +44,11 @@ import { loadOwnerConfig } from '../owner-yaml.mjs';
43
44
  import { readJsonlFile } from '../io.mjs';
44
45
  import { readEventsWithRotations } from '../events.mjs';
45
46
  import { resolveEventsLedgerRoot } from '../platform.mjs';
47
+ import { resolveLedgerPath } from '../ledger-path.mjs';
46
48
  import { readCanonicalSessions } from '../sessions-canonical.mjs';
47
49
  import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
48
50
  import { readSessionProfile } from '../state-md.mjs';
49
- import { resolveStateMdPath } from '../state-md/frontmatter-mutators.mjs';
51
+ import { resolveStateMdWriteTarget } from '../state-md/frontmatter-mutators.mjs';
50
52
 
51
53
  // ---------------------------------------------------------------------------
52
54
  // Constants
@@ -73,7 +75,7 @@ function resolveEndpoint(env) {
73
75
 
74
76
  /**
75
77
  * The default network sender: one POST carrying the whole batch array as the
76
- * JSON body (the server accepts arrays), with an AbortSignal timeout. Resolves
78
+ * JSON body (the server accepts arrays), with a wall-clock timeout. Resolves
77
79
  * on a 2xx status, rejects on anything else (which routes the caller into the
78
80
  * offline queue).
79
81
  *
@@ -85,11 +87,12 @@ function resolveEndpoint(env) {
85
87
  function defaultSender({ env, timeoutMs }) {
86
88
  const endpoint = resolveEndpoint(env);
87
89
  return async (batches) => {
88
- const res = await fetch(endpoint, {
90
+ const res = await httpRequest(endpoint, {
89
91
  method: 'POST',
90
92
  headers: { 'Content-Type': 'application/json' },
91
93
  body: JSON.stringify(batches),
92
- signal: AbortSignal.timeout(timeoutMs),
94
+ timeoutMs,
95
+ maxBytes: 64 * 1024,
93
96
  });
94
97
  if (!res.ok) {
95
98
  // The status travels ON the error: `flush` needs it to tell a TRANSPORT
@@ -307,7 +310,15 @@ export function readSessionProfileForMetricsDir(metricsDir) {
307
310
  // metricsDir is `<repoRoot>/.orchestrator/metrics` by construction (every
308
311
  // caller builds it that way); two levels up is the repo root.
309
312
  const repoRoot = path.resolve(metricsDir, '..', '..');
310
- return readSessionProfile(fs.readFileSync(resolveStateMdPath(repoRoot), 'utf8'));
313
+ // #1636: the write side's ownership rule — another platform's STATE.md not
314
+ // proven to be this process's is no profile source (ABSENT IS NOT EMPTY:
315
+ // null ⇒ the ping omits the field). Deliberately NO warnForeignStateMd line:
316
+ // this runs inside flush, which the hooks call in-process at SessionEnd or
317
+ // spawn detached with `stdio: 'ignore'` — the line would be hook noise or
318
+ // lost, and an omitted optional field needs no operator action.
319
+ const { path: stateMdPath, rejected } = resolveStateMdWriteTarget(repoRoot);
320
+ if (rejected) return null;
321
+ return readSessionProfile(fs.readFileSync(stateMdPath, 'utf8'));
311
322
  } catch {
312
323
  return null;
313
324
  }
@@ -494,6 +505,23 @@ export function deriveSessionFromEvents(metricsDir) {
494
505
  }
495
506
  }
496
507
 
508
+ /**
509
+ * `skill-invocations.jsonl` for the metrics dir `metricsDir` (#1722): a
510
+ * `<root>/.orchestrator/metrics` dir resolves through {@link resolveLedgerPath}
511
+ * for `<root>` — the main checkout's ledger when `<root>` is a linked worktree
512
+ * that ignores it, else the dir's own — and any other dir is read as given.
513
+ *
514
+ * @param {string} metricsDir
515
+ * @returns {string}
516
+ */
517
+ function skillInvocationsPathFor(metricsDir) {
518
+ const parent = path.dirname(metricsDir);
519
+ if (path.basename(metricsDir) === 'metrics' && path.basename(parent) === '.orchestrator') {
520
+ return resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl', path.dirname(parent));
521
+ }
522
+ return path.join(metricsDir, 'skill-invocations.jsonl');
523
+ }
524
+
497
525
  /**
498
526
  * Build ONE whitelist-projected usage-ping record from the local JSONL streams.
499
527
  *
@@ -537,11 +565,19 @@ export function buildBatch({
537
565
  consentState,
538
566
  } = {}) {
539
567
  try {
568
+ // `dir` stays the caller's metrics dir: STATE.md and the events fallback
569
+ // below derive their own roots from it.
570
+ // ledger-root: the caller's dir as given; each ledger in it resolves below
540
571
  const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
541
572
  const nowIso = now || new Date().toISOString();
542
573
 
543
- const sessions = readCanonicalSessions({ filePath: path.join(dir, 'sessions.jsonl') });
544
- const invocations = readJsonlFile(path.join(dir, 'skill-invocations.jsonl'), { skipInvalid: true });
574
+ // #1722: each ledger resolves on its own, an explicit metricsDir included
575
+ // (the SessionEnd flush hands over the LAUNCH dir's, which is a linked
576
+ // worktree for a session started there; deriveSessionFromEvents maps the
577
+ // events fallback the same way) — sessions via the one-ledger root
578
+ // (#1514/#1584), skill-invocations via skillInvocationsPathFor().
579
+ const sessions = readCanonicalSessions({ filePath: path.join(resolveEventsLedgerRoot(dir), 'sessions.jsonl') });
580
+ const invocations = readJsonlFile(skillInvocationsPathFor(dir), { skipInvalid: true });
545
581
 
546
582
  const sessionRecord = mostRecentSession(sessions);
547
583
 
@@ -855,7 +891,7 @@ export function shouldDailyFlush({ statePath, queuePath, metricsDir, now = Date.
855
891
  // (271 records / 292 KB in this repo, 2026-08-23) that is sub-millisecond
856
892
  // and it only runs once the 24h gate above has already passed. Revisit
857
893
  // with a tail-read if any repo's sessions.jsonl passes ~10 MB.
858
- const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
894
+ const dir = metricsDir || path.join(resolveEventsLedgerRoot(process.cwd()), '.orchestrator', 'metrics');
859
895
  const sessions = readCanonicalSessions({ filePath: path.join(dir, 'sessions.jsonl') });
860
896
  const last = mostRecentSession(sessions);
861
897
  const completedMs = Date.parse(last?.completed_at);
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import path from 'node:path';
20
+ import { resolveLedgerPath } from '../ledger-path.mjs';
20
21
 
21
22
  const ROLLUP_REL = '.orchestrator/metrics/test-runs.jsonl';
22
23
  const TEST_RUNS_REL = '.orchestrator/metrics/test-runs';
@@ -62,6 +63,7 @@ export function makeRunId() {
62
63
  * @throws {TypeError} on an invalid runId (see {@link assertRunId})
63
64
  */
64
65
  export function runDirPath(runId) {
66
+ // ledger-root: per-run artifact dir of this working tree, kept local
65
67
  return path.join(TEST_RUNS_REL, assertRunId(runId));
66
68
  }
67
69
 
@@ -112,8 +114,14 @@ export function consoleLogPath(runId) {
112
114
 
113
115
  /**
114
116
  * Path to the shared JSONL rollup file (across all runs).
117
+ *
118
+ * Without `repoRoot` it is the repo-relative constant. With `repoRoot` it is
119
+ * the absolute ledger path, routed to the main checkout when `repoRoot` is a
120
+ * linked worktree of a repo that ignores the rollup (#1722).
121
+ *
122
+ * @param {string} [repoRoot]
115
123
  * @returns {string}
116
124
  */
117
- export function jsonlRollupPath() {
118
- return ROLLUP_REL;
125
+ export function jsonlRollupPath(repoRoot) {
126
+ return typeof repoRoot === 'string' && repoRoot !== '' ? resolveLedgerPath(ROLLUP_REL, repoRoot) : ROLLUP_REL;
119
127
  }
@@ -25,6 +25,8 @@
25
25
 
26
26
  import path from 'node:path';
27
27
  import { resolveStateDir } from '../platform.mjs';
28
+ import { resolveOwnLockRootFor } from '../own-lock-root.mjs';
29
+ import { readProcessLocalSessionIds } from '../session-identity/own-session.mjs';
28
30
  import { shellQuote } from '../sh-quote.mjs';
29
31
  import { detectVcsCommand } from './vcs-detector.mjs';
30
32
  import { isSessionCollision } from './tmux-shell.mjs';
@@ -70,6 +72,11 @@ const STATUS_PANE_POLL_SECONDS = 2;
70
72
  *
71
73
  * Exported so a consumer test can run the render ONCE instead of the poll loop.
72
74
  *
75
+ * `repoRoot` (#1730): the agent-status writer (`hooks/subagent-telemetry.mjs`)
76
+ * writes beside the session's OWN lock, which is not always the checkout the
77
+ * second terminal sits in. A given root is written into the command as a
78
+ * literal; without one the pane reads its own `process.cwd()`, as before.
79
+ *
73
80
  * Shell-shape notes (both load-bearing):
74
81
  * - the `-e` script is wrapped in SINGLE quotes, so the JS below uses only
75
82
  * double-quoted strings: inside single quotes no `$`, backtick or interactive
@@ -77,15 +84,21 @@ const STATUS_PANE_POLL_SECONDS = 2;
77
84
  * - the module path is resolved from THIS file's own URL (never a hardcoded home
78
85
  * path), exactly as the sibling pane commands stay relative-to-cwd.
79
86
  *
87
+ * @param {{ repoRoot?: string|null }} [opts]
80
88
  * @returns {string} a single shell command; prints the pane body once and exits.
81
89
  */
82
- export function buildStatusPaneRenderCommand() {
90
+ export function buildStatusPaneRenderCommand({ repoRoot = null } = {}) {
83
91
  // `%27` guard: a single quote in the repo path would otherwise end the shell
84
92
  // single-quoted `-e` argument.
85
93
  const moduleHref = new URL('../agent-status.mjs', import.meta.url).href.replace(/'/g, '%27');
94
+ // Same guard for the root literal: a quote in the path is written as its
95
+ // JSON unicode escape, which the double-quoted JS string reads as the quote.
96
+ const rootExpr = typeof repoRoot === 'string' && repoRoot !== ''
97
+ ? JSON.stringify(repoRoot).replace(/'/g, '\\u0027')
98
+ : 'process.cwd()';
86
99
  const js = [
87
100
  `import {readCurrentStatus} from ${JSON.stringify(moduleHref)};`,
88
- 'const v=readCurrentStatus({repoRoot:process.cwd()});',
101
+ `const v=readCurrentStatus({repoRoot:${rootExpr}});`,
89
102
  'const ids=Object.keys(v.entries).sort();',
90
103
  // An EMPTY channel (no ledger yet, no cache yet) is the normal fresh-repo
91
104
  // state, not a degradation — marking it would fire the warning on every
@@ -141,7 +154,11 @@ async function _renderDefaultLayoutInner({ sessionName, force, projectRoot, vcsC
141
154
  // file — so the pane can never present a stale `running` as live. The renderer
142
155
  // prints its own "no agent-status yet" line when the channel is empty, and is
143
156
  // exported (buildStatusPaneRenderCommand) so a test can run it once.
144
- const pane5Cmd = `while true; do clear; ${buildStatusPaneRenderCommand()}; sleep ${STATUS_PANE_POLL_SECONDS}; done`;
157
+ // #1730: the root is the one holding this session's own lock, resolved once here; none found → the pane's cwd.
158
+ const own = withStatusPane
159
+ ? resolveOwnLockRootFor({ sessionId: readProcessLocalSessionIds()[0] ?? null, cwd: projectRoot })
160
+ : null;
161
+ const pane5Cmd = `while true; do clear; ${buildStatusPaneRenderCommand({ repoRoot: own && own.source !== 'none' ? own.root : null })}; sleep ${STATUS_PANE_POLL_SECONDS}; done`;
145
162
 
146
163
  // 4. Session-collision check
147
164
  const collision = isSessionCollision(sessionName);
@@ -26,6 +26,7 @@ import path from 'node:path';
26
26
  import { randomBytes } from 'node:crypto';
27
27
 
28
28
  import { digestSha256Short } from '../crypto-digest-utils.mjs';
29
+ import { resolveLedgerPath } from '../ledger-path.mjs';
29
30
  import { RUN_ID_PATTERN, assertRunId } from '../test-runner/artifact-paths.mjs';
30
31
 
31
32
  // Re-exported so existing `ux-grill/paths.mjs` importers keep working; the
@@ -106,6 +107,7 @@ function assertRepoRoot(repoRoot) {
106
107
  * @throws {TypeError} on an invalid repoRoot or runId (path-traversal guard)
107
108
  */
108
109
  export function runDirPath(repoRoot, runId) {
110
+ // ledger-root: per-run artifact dir of this working tree, kept local
109
111
  return path.resolve(assertRepoRoot(repoRoot), UX_GRILL_METRICS_DIR, assertRunId(runId));
110
112
  }
111
113
 
@@ -156,7 +158,9 @@ export function findingsPath(repoRoot, runId) {
156
158
  * @returns {string}
157
159
  */
158
160
  export function runRecordPath(repoRoot) {
159
- return path.resolve(assertRepoRoot(repoRoot), UX_GRILL_LEDGER);
161
+ // #1722: an ignored ledger in a linked worktree maps to the main checkout;
162
+ // a target repo that commits it keeps its own copy.
163
+ return resolveLedgerPath(UX_GRILL_LEDGER, path.resolve(assertRepoRoot(repoRoot)));
160
164
  }
161
165
 
162
166
  /**
@@ -6,9 +6,9 @@
6
6
 
7
7
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
8
8
  import { join, resolve, dirname } from 'node:path';
9
- import { fileURLToPath } from 'node:url';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
10
10
  import yaml from 'js-yaml';
11
- import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE } from '../agent-frontmatter.mjs';
11
+ import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE, checkAgentEffort } from '../agent-frontmatter.mjs';
12
12
  import { extractInitialFrontmatter } from './frontmatter-block.mjs';
13
13
  import { parseFrontmatterDescription } from '../description-surface.mjs';
14
14
 
@@ -18,7 +18,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
18
18
  let _tierModule = null;
19
19
  async function getTierModule() {
20
20
  if (!_tierModule) {
21
- _tierModule = await import(join(__dirname, 'tier-inference.mjs'));
21
+ _tierModule = await import(pathToFileURL(join(__dirname, 'tier-inference.mjs')).href);
22
22
  }
23
23
  return _tierModule;
24
24
  }
@@ -103,13 +103,28 @@ function parseFrontmatterYaml(yamlText) {
103
103
 
104
104
  /**
105
105
  * Get a scalar field value from frontmatter text.
106
- * Returns null if field not present.
106
+ * Returns null if field not present or its value is empty. `[ \t]*` instead of
107
+ * `\s*`: an empty `key:` must not read the NEXT line as its value.
107
108
  */
108
109
  function getField(fm, name) {
109
- const m = fm.match(new RegExp(`^${name}:\\s*(.+)$`, 'm'));
110
+ const m = fm.match(new RegExp(`^${name}:[ \t]*(.+)$`, 'm'));
110
111
  return m ? m[1].trim() : null;
111
112
  }
112
113
 
114
+ /**
115
+ * Like getField, but for plain enum-like scalars (`model`, `effort`): strips one
116
+ * pair of surrounding quotes and a trailing YAML comment, so `effort: "medium"`
117
+ * and `effort: medium # reason` read as `medium`. Not for free text such as
118
+ * `description`, where ` #` may be content.
119
+ */
120
+ function getScalarField(fm, name) {
121
+ const raw = getField(fm, name);
122
+ if (raw === null) return null;
123
+ const quoted = raw.match(/^(["'])(.*)\1(?:\s+#.*)?$/);
124
+ if (quoted) return quoted[2];
125
+ return raw.replace(/(^|\s+)#.*$/, '').trim();
126
+ }
127
+
113
128
  /**
114
129
  * Check whether a field key is present in frontmatter (even with empty/block value).
115
130
  */
@@ -323,7 +338,7 @@ if (mdFiles.length === 0) {
323
338
  // do not re-declare the alias set or the model-ID regex here.
324
339
  // Per https://code.claude.com/docs/en/sub-agents — the canonical doc accepts both.
325
340
  if (hasField(frontmatter, 'model')) {
326
- const modelVal = getField(frontmatter, 'model');
341
+ const modelVal = getScalarField(frontmatter, 'model');
327
342
  if (modelVal === null || (!ALLOWED_MODEL_ALIASES.has(modelVal) && !MODEL_ID_RE.test(modelVal))) {
328
343
  const got = modelVal ?? '';
329
344
  const aliasList = [...ALLOWED_MODEL_ALIASES].join('|');
@@ -331,6 +346,22 @@ if (mdFiles.length === 0) {
331
346
  }
332
347
  }
333
348
 
349
+ // effort: required when model ≠ inherit, enum low|medium|high|xhigh|max (#1614).
350
+ // Rule lives in agent-frontmatter.mjs (checkAgentEffort) — do not re-declare it here.
351
+ const effortError = checkAgentEffort(
352
+ hasField(frontmatter, 'model') ? (getScalarField(frontmatter, 'model') ?? '') : undefined,
353
+ hasField(frontmatter, 'effort') ? (getScalarField(frontmatter, 'effort') ?? '') : undefined,
354
+ );
355
+ if (effortError) fail(`${agentName}: ${effortError.message}`);
356
+
357
+ // omitClaudeMd: optional boolean (#1616) — a typo such as `yes` would be ignored silently.
358
+ if (hasField(frontmatter, 'omitClaudeMd')) {
359
+ const omitVal = getField(frontmatter, 'omitClaudeMd');
360
+ if (omitVal !== 'true' && omitVal !== 'false') {
361
+ fail(`${agentName}: omitClaudeMd must be true or false (got: '${omitVal ?? ''}')`);
362
+ }
363
+ }
364
+
334
365
  // color: canonical Anthropic palette (red|blue|green|yellow|purple|orange|pink|cyan) plus magenta from plugin-dev SKILL.md.
335
366
  // Source: https://code.claude.com/docs/en/sub-agents § Supported frontmatter fields.
336
367
  if (hasField(frontmatter, 'color')) {
@@ -72,7 +72,22 @@ function stripLeadingDotSlash(p) {
72
72
  * @param {boolean} optional - If true, missing conventional path is a PASS
73
73
  */
74
74
  function checkComponentPath(field, conventional, optional = false) {
75
- const relPath = json[field] || '';
75
+ const value = json[field];
76
+ // hooks/mcpServers also accept inline config objects (plugins-reference §
77
+ // Component path forms). An inline object carries no path to resolve; arrays
78
+ // may mix paths and objects, so each string entry is checked as a path.
79
+ if (Array.isArray(value)) {
80
+ for (const entry of value) {
81
+ if (typeof entry === 'string') checkExplicitPath(field, entry);
82
+ else pass(`${field} inline config entry (no path to resolve)`);
83
+ }
84
+ return;
85
+ }
86
+ if (value && typeof value === 'object') {
87
+ pass(`${field} is an inline config object (no path to resolve)`);
88
+ return;
89
+ }
90
+ const relPath = value || '';
76
91
 
77
92
  if (!relPath) {
78
93
  // No explicit value — try conventional auto-discovery
@@ -87,6 +102,15 @@ function checkComponentPath(field, conventional, optional = false) {
87
102
  return;
88
103
  }
89
104
 
105
+ checkExplicitPath(field, relPath);
106
+ }
107
+
108
+ /**
109
+ * Check one explicit component path: must start with "./" and exist.
110
+ * @param {string} field
111
+ * @param {string} relPath
112
+ */
113
+ function checkExplicitPath(field, relPath) {
90
114
  // Explicit value — must start with "./"
91
115
  if (!relPath.startsWith('./')) {
92
116
  fail(`${field} path does not start with ./: ${relPath}`);
@@ -889,7 +889,7 @@ function loadModuleVersions(pluginRoot, modulePath, cache) {
889
889
 
890
890
  const workingSource = readFileSync(modulePath, 'utf8');
891
891
  const working = parseModule(workingSource, modulePath);
892
- const gitPath = relative.split(path.sep).join('/');
892
+ const gitPath = relative.replace(/\\/g, '/');
893
893
  const env = {};
894
894
  for (const key of GIT_ENV_ALLOWLIST) {
895
895
  if (process.env[key] !== undefined) env[key] = process.env[key];
@@ -1027,8 +1027,8 @@ export function inspectGuardRequiresParity(pluginRoot) {
1027
1027
  result.summary.requires += requires.requires.length;
1028
1028
 
1029
1029
  const modulePath = path.resolve(pluginRoot, MODULE_RELATIVE_PREFIX, ...specifier.segments);
1030
- const moduleRelative = path.relative(pluginRoot, modulePath);
1031
- if (moduleRelative.startsWith('..') || path.isAbsolute(moduleRelative) || !moduleRelative.startsWith(`${MODULE_RELATIVE_PREFIX}${path.sep}`)) {
1030
+ const moduleRelative = path.relative(pluginRoot, modulePath).replace(/\\/g, '/');
1031
+ if (moduleRelative.startsWith('..') || path.isAbsolute(moduleRelative) || !moduleRelative.startsWith(`${MODULE_RELATIVE_PREFIX}/`)) {
1032
1032
  report('dynamic-contract', `${namespace}: specifier resolves outside ${MODULE_RELATIVE_PREFIX}`, specifierValue);
1033
1033
  continue;
1034
1034
  }
@@ -10,7 +10,7 @@
10
10
  // Stdout: ' PASS: ...' / ' FAIL: ...' lines + 'Results: N passed, M failed'
11
11
 
12
12
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
13
- import { join } from 'node:path';
13
+ import { basename, join } from 'node:path';
14
14
 
15
15
  // The pi and cursor projections are IMPORTED, not copied: the runtime bridges
16
16
  // rewrite `hook_event_name` for every native event, so they are the source of
@@ -83,16 +83,48 @@ const DOCUMENTED_ASYMMETRIES = {
83
83
  // shape ("PreToolUse hook returned permissionDecision:deny without a
84
84
  // non-empty permissionDecisionReason").
85
85
  //
86
- // THE ACTUAL BLOCKER is the tool-name VOCABULARY, not the payload shape.
87
- // Codex ships no `Bash`, `Edit`, `Write` or `MultiEdit` tool — it has
88
- // `shell` / `exec_command` / `unified_exec` / `apply_patch`
89
- // (`strings -a <codex> | grep -c '"Bash"'` -> 0). Every handler below
90
- // opens with an equality gate on a Claude tool name and returns
91
- // `emitAllow()` otherwise, so wiring one produces a hook that runs,
92
- // matches nothing and allows everything: FALSE enforcement, which is
93
- // strictly worse than the registered gap (#919-P2 class). The repair is
94
- // a tool-name map (see CODEX_TOOL_NAMES in scripts/lib/codex/
95
- // plugin-contract.mjs), not a manifest entry.
86
+ // 2026-09-06 (0.144.4) this block named the tool-name VOCABULARY as the
87
+ // blocker: Codex's model-facing tools are `shell` / `exec_command` /
88
+ // `unified_exec` / `apply_patch`, so a guard gating on
89
+ // `tool_name === 'Bash'` would run, match nothing and allow everything
90
+ // (#919-P2 class). For the SHELL tool that premise is refuted below; for
91
+ // the Edit/Write/MultiEdit gates (apply_patch) it is still unmeasured.
92
+ //
93
+ // REFUTED AGAIN for the shell tool (#1669), measured 2026-10-20 on
94
+ // codex-cli 0.160.1 with a logging PreToolUse hook under `codex exec`:
95
+ // a shell call arrives as `"tool_name":"Bash"` with
96
+ // `"tool_input":{"command":"<string>"}`, and `matcher: "Bash"` fires.
97
+ // The 2026-09-06 evidence (`strings -a <codex> | grep -c '"Bash"'` -> 0)
98
+ // counted a JSON-quoted literal Rust never stores.
99
+ //
100
+ // DENY IS HONOURED (#1691), measured 2026-10-20 on codex-cli 0.160.1:
101
+ // `codex exec` with pre-bash-destructive-guard as the only PreToolUse hook
102
+ // and the prompt `printf x > CLAUDE.md` logged `Command blocked by
103
+ // PreToolUse hook: Destructive command blocked: '> CLAUDE.md' (rule:
104
+ // redirect-truncate-protected)` and left the file unchanged (the rule
105
+ // protects CLAUDE.md and its Codex alias AGENTS.md alike); the control
106
+ // run with `hooks.PreToolUse=[]` executed it and the file became `x`.
107
+ // Measured payload: `session_id`, `turn_id`, `cwd` (= the session workdir,
108
+ // also the hook's process cwd), `hook_event_name`, `model`,
109
+ // `permission_mode`, `tool_name:"Bash"`, `tool_input.command`,
110
+ // `tool_use_id`, `transcript_path: null`; PostToolUse adds
111
+ // `tool_response: ""`. A real Codex session exports NO CLAUDE_* env.
112
+ //
113
+ // Every Bash hook was then fed that payload by hand under a clean env
114
+ // (PATH + HOME only, SO_PLATFORM=codex, mkdtemp git repo) and wired only
115
+ // where it enforces on it (2026-10-20): destructive-guard (deny on
116
+ // `> CLAUDE.md`), sessions-ledger-guard (deny on `>>
117
+ // .orchestrator/metrics/sessions.jsonl`), issue-budget (cap 1, second
118
+ // create denied and parked), enforce-commands (`.codex/wave-scope.json`
119
+ // `blockedCommands` → deny), memory-propose-audit (event appended,
120
+ // observe-only by design), post-bash-write-verify (warn on an out-of-scope
121
+ // write). They read `tool_input.command`, `cwd` and `session_id`; issue-budget also `tool_use_id`.
122
+ // Wiring contract: tests/hooks/codex-bash-guards-wiring.test.mjs.
123
+ //
124
+ // post-bash-issue-budget-refund is no gap on this list: it runs on
125
+ // PostToolUseFailure, an event hooks-codex.json does not carry, so a
126
+ // failed create on Codex keeps its slot (over-count, the fail-closed
127
+ // direction). Revisit when Codex gains a failure event.
96
128
  PreToolUse: [
97
129
  // pre-task-scope-disjoint (#1020): NOT ported by construction. The hook
98
130
  // matches the `Agent` dispatch tool, which this platform does not have —
@@ -118,27 +150,17 @@ const DOCUMENTED_ASYMMETRIES = {
118
150
  // `apply_patch`, a name this hook never matches. Wired, it would run and allow
119
151
  // everything: a silent no-op, not enforcement. Gap registered, not faked.
120
152
  'pre-edit-sessions-ledger-guard.mjs',
121
- 'pre-bash-destructive-guard.mjs',
122
- 'pre-bash-staging-fence.mjs',
123
- 'pre-bash-memory-propose-audit.mjs',
124
- // pre-bash-sessions-ledger-guard (#958): same tool-name-vocabulary
125
- // dependency as the guards above — it gates on tool_name === 'Bash',
126
- // and Codex names its shell tools `shell` / `exec_command` /
127
- // `unified_exec`. The fields it reads ARE delivered; the value it
128
- // compares against never occurs. A manifest entry would be a silent
129
- // no-op, not enforcement — #919-P2 class. Gap registered, not faked.
130
- 'pre-bash-sessions-ledger-guard.mjs',
153
+ // pre-bash-templates-first (#1691): runs on the Codex payload but can
154
+ // only DENY. Both allow routes are unreachable there: the template-Read
155
+ // evidence needs `transcript_path` (hooks/pre-bash-templates-first.mjs:
156
+ // 404-410), which Codex sends as `null` (codex-cli 0.160.1, 2026-10-20),
157
+ // and `/templates-ack` writes an acknowledgement only under a `claude`
158
+ // ancestor process (commands/templates-ack.md:18-23). Fed the Codex
159
+ // payload with `.gitlab/issue_templates/bug.md` present, `glab issue
160
+ // create` was denied with no way to satisfy it. A guard nobody can pass
161
+ // is a blocker, not enforcement; gap registered, not faked.
131
162
  'pre-bash-templates-first.mjs',
132
- 'pre-bash-issue-budget.mjs',
133
- 'enforce-commands.mjs',
134
163
  ],
135
- // post-bash-write-verify (#942): gates on tool_name === 'Bash', a value
136
- // Codex never emits (its shell tools are `shell` / `exec_command` /
137
- // `unified_exec`). Documented exception until a tool-name map exists —
138
- // a manifest entry today would be a silent no-op, not enforcement. Same
139
- // #919-P2 class as the PreToolUse block. `post-tool-use.command.input`
140
- // at 0.144.4 does require `tool_name`, `tool_input` AND `tool_response`,
141
- // so the port is a rename map, not a payload bridge.
142
164
  PostToolUse: [
143
165
  'post-edit-validate.mjs',
144
166
  'post-tooluse-frontend-slop.mjs',
@@ -147,7 +169,6 @@ const DOCUMENTED_ASYMMETRIES = {
147
169
  // tool_input.file_path, neither of which any Codex bridge delivers.
148
170
  // A manifest entry would be a silent no-op, not enforcement.
149
171
  'post-edit-import-probe.mjs',
150
- 'post-bash-write-verify.mjs',
151
172
  ],
152
173
  SubagentStart: ['subagent-telemetry.mjs'],
153
174
  SubagentStop: ['subagent-telemetry.mjs', 'post-subagent-discovery-validator.mjs'],
@@ -247,7 +268,7 @@ function loadJson(filePath, required = true) {
247
268
  try {
248
269
  return JSON.parse(readFileSync(filePath, 'utf8'));
249
270
  } catch (e) {
250
- fail(`${filePath.split('/').pop()}: ${e.message}`);
271
+ fail(`${basename(filePath)}: ${e.message}`);
251
272
  process.exit(1);
252
273
  }
253
274
  }
@@ -85,7 +85,9 @@ if (hooksPath && hooksPath.endsWith('.json')) {
85
85
  console.log('');
86
86
  console.log('--- Check 5b: mcpServers JSON validity ---');
87
87
 
88
- const mcpPath = pluginData.mcpServers ?? '';
88
+ // mcpServers may also be an inline object or an array (plugins-reference §
89
+ // Component path forms); only a .json path string is a file to parse here.
90
+ const mcpPath = typeof pluginData.mcpServers === 'string' ? pluginData.mcpServers : '';
89
91
  if (mcpPath && mcpPath.endsWith('.json')) {
90
92
  const mcpAbs = join(pluginRoot, mcpPath.replace(/^\.\//, ''));
91
93
  if (existsSync(mcpAbs)) {
@@ -134,6 +134,7 @@ import { readLearnings } from '../learnings/io.mjs';
134
134
  // the emitter's formula.
135
135
  import { learningKeyOf } from '../learnings/kebab.mjs';
136
136
  import { isMainModule } from '../is-main-module.mjs';
137
+ import { resolveLedgerPath } from '../ledger-path.mjs';
137
138
 
138
139
  /** Directory holding the rule corpus, relative to the plugin root. */
139
140
  const RULES_REL = path.join('.claude', 'rules');
@@ -272,8 +273,8 @@ async function indexStore(absolutePath) {
272
273
  export async function inspectLearningProvenance(pluginRoot, opts = {}) {
273
274
  /** @type {Finding[]} */
274
275
  const findings = [];
275
- const livePath = opts.livePath ?? path.join(pluginRoot, LIVE_STORE_REL);
276
- const archivePath = opts.archivePath ?? path.join(pluginRoot, ARCHIVE_STORE_REL);
276
+ const livePath = opts.livePath ?? resolveLedgerPath(LIVE_STORE_REL, pluginRoot);
277
+ const archivePath = opts.archivePath ?? resolveLedgerPath(ARCHIVE_STORE_REL, pluginRoot);
277
278
 
278
279
  /** @type {Inspection} */
279
280
  const result = {
@@ -61,10 +61,16 @@ export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', 'docs',
61
61
  /** Lines on either side of an R2 operator searched for an anchor. */
62
62
  const WINDOW = 3;
63
63
 
64
- const MENTION_RE = /learnings\.jsonl/;
65
- const ANCHOR_RE = /learnings\.jsonl|\b(?:the|learnings) store\b|\bresult back\b|\batomic rewrite\b/i;
66
- const SHELL_REDIRECT_RE = /(?:^|[\s|;&(])(>{1,2})[ \t]*["'`]?[^\s"'`<>]*learnings\.jsonl\b/dg;
67
- const TEE_RE = /\btee\b(?:[ \t]+-{1,2}[\w-]+)*[ \t]+["'`]?[^\s"'`|]*learnings\.jsonl\b/g;
64
+ /**
65
+ * The protected store filenames: the live store plus the #1516 inbox and its
66
+ * receipts — append-only stores written only through `learnings/io.mjs`.
67
+ */
68
+ const STORE = String.raw`learnings(?:-inbox(?:-ingested)?)?\.jsonl`;
69
+
70
+ const MENTION_RE = new RegExp(STORE);
71
+ const ANCHOR_RE = new RegExp(String.raw`${STORE}|\b(?:the|learnings) store\b|\bresult back\b|\batomic rewrite\b`, 'i');
72
+ const SHELL_REDIRECT_RE = new RegExp(String.raw`(?:^|[\s|;&(])(>{1,2})[ \t]*["'\`]?[^\s"'\`<>]*${STORE}\b`, 'dg');
73
+ const TEE_RE = new RegExp(String.raw`\btee\b(?:[ \t]+-{1,2}[\w-]+)*[ \t]+["'\`]?[^\s"'\`|]*${STORE}\b`, 'g');
68
74
  const PROSE_OPERATOR_RE = /`>{1,2}`/g;
69
75
  const ANY_OPERATOR_RE = /`>{1,2}`|(?:^|[\s|;&(])>{1,2}|\btee\b/;
70
76
  const PROHIBITION_RE = /\b(NEVER|[Nn]ever|DO NOT|[Dd]o not|[Dd]on't|FORBIDDEN|forbidden|verboten)\b/;