session-orchestrator 3.24.0 → 4.0.1

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 (435) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +3 -2
  47. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  48. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  49. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  50. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  51. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  53. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  54. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  55. package/.codex-plugin/skills/close/SKILL.md +21 -0
  56. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  58. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  59. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  61. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  63. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  64. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  65. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  66. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  67. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  68. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  69. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  70. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  71. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  72. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  73. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  74. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  75. package/.codex-plugin/skills/go/SKILL.md +22 -0
  76. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  77. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  78. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  79. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  80. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  81. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  82. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  83. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  84. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  85. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  86. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  87. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  88. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  89. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  90. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  91. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  92. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  93. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  94. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  95. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  96. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  97. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  98. package/.codex-plugin/skills/release/SKILL.md +22 -0
  99. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  100. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  101. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  102. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  103. package/.codex-plugin/skills/session/SKILL.md +21 -0
  104. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  105. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  106. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  107. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  108. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  109. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  110. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  111. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  112. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  113. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  114. package/.codex-plugin/skills/test/SKILL.md +21 -0
  115. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  116. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  117. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  118. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  119. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  120. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  121. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  122. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  123. package/.cursor/commands/autopilot.md +2 -2
  124. package/.cursor/commands/bootstrap.md +1 -1
  125. package/.cursor/commands/brainstorm.md +1 -1
  126. package/.cursor/commands/debug.md +1 -1
  127. package/.cursor/commands/discovery.md +1 -1
  128. package/.cursor/commands/dispatcher.md +2 -2
  129. package/.cursor/commands/eli5.md +2 -2
  130. package/.cursor/commands/eval.md +2 -2
  131. package/.cursor/commands/evolve.md +1 -1
  132. package/.cursor/commands/go.md +1 -1
  133. package/.cursor/commands/grill.md +2 -2
  134. package/.cursor/commands/memory-cleanup.md +2 -2
  135. package/.cursor/commands/persona-panel.md +1 -1
  136. package/.cursor/commands/plan.md +1 -1
  137. package/.cursor/commands/portfolio.md +1 -1
  138. package/.cursor/commands/reconcile.md +2 -2
  139. package/.cursor/commands/release.md +2 -2
  140. package/.cursor/commands/session.md +2 -2
  141. package/.cursor/commands/spinout.md +2 -2
  142. package/.cursor/commands/sunset-review.md +2 -2
  143. package/.cursor/commands/templates-ack.md +2 -2
  144. package/.cursor/commands/test.md +2 -2
  145. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  146. package/.cursor/skills/eval/SKILL.md +1 -1
  147. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  148. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  149. package/.cursor-plugin/plugin.json +30 -0
  150. package/.orchestrator/policy/blocked-commands.json +121 -0
  151. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  152. package/.orchestrator/policy/quality-gates.example.json +16 -0
  153. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  154. package/.orchestrator/policy/templates-policy.json +27 -0
  155. package/.orchestrator/policy/test-profiles.json +47 -0
  156. package/AGENTS.md +225 -0
  157. package/CHANGELOG.md +1314 -2
  158. package/NOTICE +11 -6
  159. package/README.md +135 -94
  160. package/agents/eval-judge.md +1 -1
  161. package/agents/skill-applied-judge.md +1 -1
  162. package/assets/wave-lifecycle.svg +98 -0
  163. package/commands/release.md +6 -3
  164. package/commands/session.md +18 -3
  165. package/docs/README.md +4 -0
  166. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  167. package/docs/baseline.md +67 -0
  168. package/docs/ci-setup.md +108 -62
  169. package/docs/codex-setup.md +107 -29
  170. package/docs/components.md +38 -16
  171. package/docs/cursor-setup.md +6 -2
  172. package/docs/events-schema.md +9 -6
  173. package/docs/instruction-delivery.md +69 -0
  174. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  175. package/docs/migration-v4.md +365 -0
  176. package/docs/pi-setup.md +6 -1
  177. package/docs/plugin-architecture-v3.md +1 -1
  178. package/docs/rule-authoring.md +85 -19
  179. package/docs/scope-collision-guard.md +5 -5
  180. package/docs/session-config-reference.md +57 -56
  181. package/docs/session-config-template.md +6 -29
  182. package/docs/telemetry.md +157 -3
  183. package/docs/vault-docs-architecture.md +50 -11
  184. package/hooks/_lib/hook-import-set.json +1488 -0
  185. package/hooks/_lib/subagent-transcript.mjs +562 -0
  186. package/hooks/config-protection.mjs +2 -2
  187. package/hooks/cwd-change-restore.mjs +2 -2
  188. package/hooks/enforce-commands.mjs +69 -0
  189. package/hooks/hooks-codex.json +1 -1
  190. package/hooks/hooks-cursor.json +10 -0
  191. package/hooks/hooks-pi.json +5 -0
  192. package/hooks/hooks.json +6 -1
  193. package/hooks/loop-guard.mjs +3 -3
  194. package/hooks/on-session-end.mjs +2 -2
  195. package/hooks/on-session-start.mjs +103 -2
  196. package/hooks/on-stop.mjs +60 -14
  197. package/hooks/operator-steer.mjs +2 -2
  198. package/hooks/post-bash-write-verify.mjs +85 -0
  199. package/hooks/post-edit-import-probe.mjs +344 -0
  200. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  201. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  202. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  203. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  204. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  205. package/hooks/skill-invocation-telemetry.mjs +17 -5
  206. package/hooks/subagent-telemetry.mjs +13 -4
  207. package/monitors/monitors.json +3 -3
  208. package/package.json +9 -1
  209. package/pi/prompts/session.md +2 -2
  210. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  211. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  212. package/scripts/dialectic-deriver.mjs +73 -8
  213. package/scripts/export-hw-learnings.mjs +113 -1
  214. package/scripts/generate-agents-skills.mjs +378 -0
  215. package/scripts/generate-codex-skills.mjs +246 -0
  216. package/scripts/generate-cursor-adapter.mjs +45 -8
  217. package/scripts/generate-hook-import-set.mjs +292 -0
  218. package/scripts/lib/agent-status.mjs +13 -2
  219. package/scripts/lib/auto-dream.mjs +38 -36
  220. package/scripts/lib/autonomy/suitability.mjs +6 -0
  221. package/scripts/lib/autopilot/loop.mjs +2 -2
  222. package/scripts/lib/ci-status-banner.mjs +220 -75
  223. package/scripts/lib/codex/plugin-contract.mjs +88 -6
  224. package/scripts/lib/config/auto-dream.mjs +2 -1
  225. package/scripts/lib/config/block-header.mjs +8 -0
  226. package/scripts/lib/config/block-preprocess.mjs +177 -0
  227. package/scripts/lib/config/broken-window.mjs +2 -1
  228. package/scripts/lib/config/cold-start.mjs +2 -1
  229. package/scripts/lib/config/config-protection.mjs +22 -2
  230. package/scripts/lib/config/context-coverage.mjs +2 -1
  231. package/scripts/lib/config/cross-repo.mjs +2 -1
  232. package/scripts/lib/config/custom-phases.mjs +2 -1
  233. package/scripts/lib/config/dialectic.mjs +2 -1
  234. package/scripts/lib/config/discovery-validator.mjs +2 -1
  235. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  236. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  237. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  238. package/scripts/lib/config/docs-staleness.mjs +2 -1
  239. package/scripts/lib/config/drift-check.mjs +2 -1
  240. package/scripts/lib/config/eval.mjs +2 -1
  241. package/scripts/lib/config/events-rotation.mjs +2 -1
  242. package/scripts/lib/config/evolve.mjs +8 -2
  243. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  244. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  245. package/scripts/lib/config/handover-gate.mjs +2 -1
  246. package/scripts/lib/config/health-endpoints.mjs +7 -2
  247. package/scripts/lib/config/host-paths.mjs +20 -4
  248. package/scripts/lib/config/issue-budget.mjs +2 -1
  249. package/scripts/lib/config/loop-guard.mjs +2 -1
  250. package/scripts/lib/config/memory.mjs +2 -1
  251. package/scripts/lib/config/moc-staleness.mjs +2 -1
  252. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  253. package/scripts/lib/config/private-config-dir.mjs +67 -0
  254. package/scripts/lib/config/reconcile.mjs +2 -1
  255. package/scripts/lib/config/remote-hosts.mjs +2 -1
  256. package/scripts/lib/config/section-extractor.mjs +7 -1
  257. package/scripts/lib/config/skill-evolution.mjs +2 -1
  258. package/scripts/lib/config/slopcheck.mjs +2 -1
  259. package/scripts/lib/config/state-md-lock.mjs +2 -1
  260. package/scripts/lib/config/templates-first.mjs +2 -1
  261. package/scripts/lib/config/test.mjs +2 -1
  262. package/scripts/lib/config/vault-integration.mjs +7 -1
  263. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  264. package/scripts/lib/config/vault-staleness.mjs +2 -1
  265. package/scripts/lib/config/vault-sync.mjs +2 -1
  266. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  267. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  268. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  269. package/scripts/lib/convergence-monitor.mjs +82 -16
  270. package/scripts/lib/dispatcher/rank.mjs +124 -48
  271. package/scripts/lib/ecosystem-health.mjs +16 -2
  272. package/scripts/lib/eval/engine.mjs +9 -1
  273. package/scripts/lib/eval/session-resolve.mjs +23 -4
  274. package/scripts/lib/events.mjs +22 -6
  275. package/scripts/lib/frontmatter-guard.mjs +131 -13
  276. package/scripts/lib/gates/gate-full.mjs +30 -0
  277. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  278. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  279. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  280. package/scripts/lib/host-identity.mjs +50 -11
  281. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  282. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  283. package/scripts/lib/learnings/io.mjs +60 -6
  284. package/scripts/lib/memory-proposals/store.mjs +30 -22
  285. package/scripts/lib/owner-config-banner.mjs +41 -6
  286. package/scripts/lib/owner-config-loader.mjs +21 -10
  287. package/scripts/lib/owner-interview.mjs +3 -3
  288. package/scripts/lib/owner-yaml.mjs +215 -15
  289. package/scripts/lib/platform.mjs +108 -15
  290. package/scripts/lib/plugin-update-banner.mjs +414 -0
  291. package/scripts/lib/project-hygiene.mjs +38 -2
  292. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  293. package/scripts/lib/quality-gate.mjs +133 -44
  294. package/scripts/lib/reconcile/emitter.mjs +68 -6
  295. package/scripts/lib/reconcile/engine.mjs +51 -11
  296. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  297. package/scripts/lib/reconcile/writer.mjs +40 -18
  298. package/scripts/lib/session-close-backfill.mjs +67 -9
  299. package/scripts/lib/session-id.mjs +12 -23
  300. package/scripts/lib/session-identity/own-session.mjs +125 -10
  301. package/scripts/lib/session-lock-shape.mjs +43 -0
  302. package/scripts/lib/session-lock.mjs +5 -10
  303. package/scripts/lib/session-registry.mjs +25 -9
  304. package/scripts/lib/session-schema/constants.mjs +64 -3
  305. package/scripts/lib/session-schema/validator.mjs +38 -4
  306. package/scripts/lib/session-start-probes.mjs +30 -1
  307. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  308. package/scripts/lib/skill-health/join.mjs +17 -4
  309. package/scripts/lib/state-md.mjs +78 -0
  310. package/scripts/lib/sunset/walker.mjs +6 -0
  311. package/scripts/lib/telemetry/schema.mjs +202 -9
  312. package/scripts/lib/telemetry/sync.mjs +368 -12
  313. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  314. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  315. package/scripts/lib/validate/check-agents.mjs +3 -3
  316. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  317. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  318. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  319. package/scripts/lib/validate/check-owner-leakage.mjs +319 -22
  320. package/scripts/lib/validate/check-skill-links.mjs +193 -0
  321. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  322. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  323. package/scripts/lib/validate/check-unwired-features.mjs +163 -15
  324. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  325. package/scripts/lib/validate/confidential-names.mjs +95 -30
  326. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  327. package/scripts/lib/validate/repo-files.mjs +48 -14
  328. package/scripts/lib/vault-backfill/template.mjs +63 -6
  329. package/scripts/lib/vault-mirror/process.mjs +165 -42
  330. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  331. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  332. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  333. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  334. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  335. package/scripts/lib/wave-resource-gate.mjs +8 -2
  336. package/scripts/lib/wave-sizing.mjs +4 -1
  337. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  338. package/scripts/materialize-wave-scope.mjs +12 -5
  339. package/scripts/memory-propose.mjs +19 -5
  340. package/scripts/migrate-cold-start-seed.mjs +4 -1
  341. package/scripts/parse-config.mjs +60 -3
  342. package/scripts/release.mjs +430 -31
  343. package/scripts/repair-invalid-sessions.mjs +3 -3
  344. package/scripts/run-quality-gate.mjs +128 -11
  345. package/scripts/site-numbers.mjs +344 -8
  346. package/scripts/sweep-expired-learnings.mjs +90 -0
  347. package/scripts/sync-vault-schema.mjs +3 -1
  348. package/scripts/telemetry.mjs +2 -2
  349. package/scripts/validate-plugin.mjs +164 -0
  350. package/scripts/validate-wave-scope.mjs +28 -8
  351. package/scripts/wave-scope-binding.mjs +215 -0
  352. package/skills/_shared/instruction-file-resolution.md +10 -0
  353. package/skills/_shared/parallel-aware-preamble.md +1 -0
  354. package/skills/_shared/platform-tools.md +1 -1
  355. package/skills/_shared/state-ownership.md +1 -1
  356. package/skills/architecture/SKILL.md +7 -5
  357. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  358. package/skills/autopilot/SKILL.md +4 -18
  359. package/skills/claude-md-drift-check/SKILL.md +5 -1
  360. package/skills/claude-md-drift-check/checker.mjs +62 -2
  361. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  362. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  363. package/skills/discovery/probes-arch.md +20 -18
  364. package/skills/dispatcher/SKILL.md +3 -2
  365. package/skills/evolve/SKILL.md +65 -26
  366. package/skills/frontmatter-guard/SKILL.md +11 -5
  367. package/skills/npm-publish/SKILL.md +1 -1
  368. package/skills/reconcile/SKILL.md +33 -0
  369. package/skills/remote-offload/SKILL.md +1 -1
  370. package/skills/session-end/SKILL.md +18 -905
  371. package/skills/session-end/phase-3-6-tail.md +10 -3
  372. package/skills/session-end/plan-verification.md +221 -155
  373. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  374. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  375. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  376. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  377. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  378. package/skills/session-end/references/session-summary-template.md +62 -0
  379. package/skills/session-plan/SKILL.md +49 -0
  380. package/skills/session-start/SKILL.md +22 -904
  381. package/skills/session-start/phase-8-5-express-path.md +1 -1
  382. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  383. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  384. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  385. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  386. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  387. package/skills/session-start/references/phase-4-ssot-environment-check.md +160 -0
  388. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  389. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  390. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  391. package/skills/vault-sync/SKILL.md +10 -0
  392. package/skills/vault-sync/validator.mjs +21 -27
  393. package/skills/wave-executor/SKILL.md +15 -1
  394. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  395. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  396. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  397. package/skills/wave-executor/wave-loop.md +14 -1309
  398. package/templates/_shared/journey-manifest.md +10 -6
  399. package/.cursor/commands/autopilot-multi.md +0 -14
  400. package/.cursor/commands/contract-version-bump.md +0 -14
  401. package/.cursor/commands/journey-audit.md +0 -14
  402. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  403. package/.cursor/skills/daily/SKILL.md +0 -12
  404. package/.cursor/skills/domain-model/SKILL.md +0 -13
  405. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  406. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  407. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  408. package/commands/autopilot-multi.md +0 -74
  409. package/commands/contract-version-bump.md +0 -28
  410. package/commands/journey-audit.md +0 -43
  411. package/pi/prompts/autopilot-multi.md +0 -12
  412. package/pi/prompts/contract-version-bump.md +0 -12
  413. package/pi/prompts/journey-audit.md +0 -12
  414. package/scripts/autopilot-multi.mjs +0 -885
  415. package/scripts/backfill-learnings-expires.mjs +0 -196
  416. package/scripts/backfill-learnings.mjs +0 -203
  417. package/scripts/fleet-instruction-scan.mjs +0 -141
  418. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  419. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  420. package/scripts/lib/webhook-url.mjs +0 -105
  421. package/scripts/lifecycle-sim-v6.mjs +0 -347
  422. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  423. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  424. package/scripts/upload-social-preview.mjs +0 -316
  425. package/skills/_shared/model-selection.md +0 -64
  426. package/skills/contract-version-bump/SKILL.md +0 -219
  427. package/skills/daily/SKILL.md +0 -222
  428. package/skills/daily/generate.sh +0 -92
  429. package/skills/daily/templates/daily.md.tpl +0 -36
  430. package/skills/journey-audit/SKILL.md +0 -270
  431. package/skills/skill-creator/SKILL.md +0 -168
  432. package/skills/ubiquitous-language/SKILL.md +0 -97
  433. package/skills/vault-sync/package-lock.json +0 -40
  434. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  435. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -11,6 +11,8 @@ Guide for using Session Orchestrator with OpenAI Codex through Codex's public pl
11
11
 
12
12
  ## Installation
13
13
 
14
+ **Recommended:** the short remote form (`codex plugin marketplace add <owner>/<repo>`) needs no local clone — see "Short-Form Marketplace Add" below. The steps below are the maintainer/local-clone path used by `scripts/codex-install.mjs`.
15
+
14
16
  Clone the repository, install its runtime dependencies, and run the installer from the plugin root:
15
17
 
16
18
  ```bash
@@ -30,7 +32,7 @@ codex plugin list --available --json
30
32
 
31
33
  It operates only through public Codex plugin commands; hook trust remains untouched.
32
34
 
33
- ### Short-Form Marketplace Add (Verified 2026-08-28, codex-cli 0.141.0)
35
+ ### Short-Form Marketplace Add (Recommended — Verified 2026-09-04, codex-cli 0.144.4)
34
36
 
35
37
  `codex plugin marketplace add --help` documents a short remote form:
36
38
 
@@ -38,33 +40,22 @@ It operates only through public Codex plugin commands; hook trust remains untouc
38
40
  codex plugin marketplace add owner/repo --ref main
39
41
  ```
40
42
 
41
- Tested against this repo in a scoped throwaway `CODEX_HOME` (2026-08-28, codex-cli 0.141.0):
43
+ This is the recommended install path no local clone needed. Confirmed end-to-end on codex-cli **0.144.4** (2026-09-04), against this repo's unchanged flat layout (`.codex-plugin/plugin.json` + `.claude-plugin/marketplace.json` at repo root, no `plugins/<name>/`):
42
44
 
43
45
  ```
44
- $ codex plugin marketplace add Kanevry/session-orchestrator --json
45
- {
46
- "marketplaceName": "kanevry",
47
- "installedRoot": ".../.tmp/marketplaces/kanevry",
48
- "alreadyAdded": false
49
- }
46
+ $ codex plugin add session-orchestrator@kanevry --json
47
+ {"pluginId":"session-orchestrator@kanevry","version":"3.22.1+codex.20260825125233","installedPath":"~/.codex/plugins/cache/kanevry/session-orchestrator/<version>","authPolicy":"ON_INSTALL"}
50
48
  $ echo $?
51
49
  0
52
50
  ```
53
51
 
54
- This succeeds and clones the repo via git — no `--ref`/`owner/repo` string is needed beyond the short form; the resulting marketplace name (`kanevry`) is read from `.claude-plugin/marketplace.json`'s `name` field, not from the `owner/repo` argument.
52
+ `codex plugin list --available --json --marketplace kanevry` confirms `installed: true, enabled: true` for the same layout no `plugins/<name>/` restructuring was needed.
55
53
 
56
- **However**, the subsequent install step fails identically for both this short remote form *and* the long-form local install documented above (`codex plugin marketplace add "$PWD"`, same as `scripts/codex-install.mjs` runs):
54
+ **Historical note:** on codex-cli 0.141.0 (2026-08-28), the identical `plugin add` command failed against this same layout with `Error: plugin session-orchestrator was not found in marketplace kanevry`; upgrading to 0.144.4+ resolves it.
57
55
 
58
- ```
59
- $ codex plugin add session-orchestrator@kanevry --json
60
- Error: plugin `session-orchestrator` was not found in marketplace `kanevry`
61
- $ echo $?
62
- 1
63
- ```
56
+ ### Switching Marketplace Sources
64
57
 
65
- `codex plugin list --available --json --marketplace kanevry` returns `{"installed": [], "available": []}` for both forms — the marketplace is configured, but no plugin is discoverable inside it, contradicting item 1 under "Understand the Three States" below. A synthetic marketplace root mirroring this repo's exact layout (`.codex-plugin/plugin.json` directly at root, no `.claude-plugin/marketplace.json`) reproduces the same empty discovery; by contrast, a directory scanned via a `<root>/plugins/<name>/.codex-plugin/plugin.json` layout (the shape `codex plugin marketplace add --help`'s `--sparse plugins/foo` example implies, and the shape this host's own pre-existing `local` marketplace uses via `~/plugins/session-orchestrator`) resolves correctly. This suggests codex's own plugin-discovery convention expects a `plugins/<name>/` marketplace layout that this repo's flat root does not provide, though `.claude-plugin/marketplace.json` (Claude Code's schema) is independently accepted as "a supported manifest" at the `marketplace add` step, without resolving to a discoverable Codex plugin at `list` time.
66
-
67
- **Caveat that limits this finding:** this host's installed codex-cli is **0.141.0**, older than the "0.144.4 or newer" prerequisite this guide states above. This failure was not re-verified against 0.144.4+, so it may be specific to running below the documented minimum rather than a defect in this repo's layout on a supported version. Until re-verified on 0.144.4+, treat both the short remote form and the long-form local install (`node scripts/codex-install.mjs`) as **unconfirmed end-to-end on this host** — the `marketplace add` step succeeds either way, but `plugin add` does not, on 0.141.0. Do not elevate either form to a README-level recommended command until a `plugin add` success is measured and dated.
58
+ `codex plugin marketplace add owner/repo` silently **replaces** an already-registered marketplace of the same declared name the name comes from `marketplace.json`'s `name` field, not the `owner/repo` argument with a fresh git clone under `~/.codex/.tmp/marketplaces/<name>`. Re-adding the original local path afterward then fails with `marketplace '<name>' is already added from a different source; remove it before adding this source`. To switch sources deliberately, run `codex plugin marketplace remove <name>` first.
68
59
 
69
60
  ## Understand the Three States
70
61
 
@@ -76,7 +67,16 @@ Codex reports three distinct states that must not be conflated:
76
67
 
77
68
  ## Refresh and Explicit Cache Invalidation
78
69
 
79
- After pulling changes, rerun the installer:
70
+ **If you installed via the short remote form** (`codex plugin marketplace add owner/repo`), refresh the Git marketplace snapshot before updating the installed plugin. Measured 2026-09-06 on codex-cli 0.144.4 — `codex plugin marketplace upgrade --help`: *"Refresh configured Git marketplace snapshots. Omit MARKETPLACE_NAME to upgrade all configured Git marketplaces."*
71
+
72
+ ```bash
73
+ codex plugin marketplace upgrade kanevry # or omit the name to refresh all
74
+ codex plugin add session-orchestrator@kanevry
75
+ ```
76
+
77
+ `upgrade` re-fetches the Git snapshot; `plugin add` then re-installs the bundle from that refreshed snapshot. There is no local clone in this path, so "re-run the installer" does not apply to it.
78
+
79
+ **If you installed from a local clone** (the maintainer path), rerun the installer after pulling:
80
80
 
81
81
  ```bash
82
82
  git pull
@@ -86,6 +86,8 @@ node scripts/codex-install.mjs
86
86
 
87
87
  Every installer run executes `codex plugin marketplace add` and `codex plugin add`, even when the marketplace is already configured. The repeated `plugin add` refreshes Codex's installed bundle from the current clone instead of treating installation as a one-time copy.
88
88
 
89
+ After either refresh path, confirm the installed version with `codex plugin list --available --json` and start a fresh task. Reopen the skill picker and search for `go` or `close`; if the updated entries are still missing, fully restart Codex. Editing the source clone or regenerating skills alone does not refresh the installed bundle.
90
+
89
91
  The tracked Codex manifest uses a version such as `3.14.0+codex.20260717175716`. The base must match `package.json`; the `+codex.<YYYYMMDDHHmmss>` UTC suffix is the repository's explicit invalidation marker. When a shipped bundle needs a new cache identity, maintainers commit a new timestamp in `.codex-plugin/plugin.json`. The installer validates that committed value and never mutates the tracked manifest.
90
92
 
91
93
  ## Configuration
@@ -126,20 +128,94 @@ The plugin bundle includes the Codex role definitions under `.codex-plugin/agent
126
128
 
127
129
  The Codex hook command uses Codex's native `${PLUGIN_ROOT}` expansion. The wrapper also exports `CODEX_PLUGIN_ROOT="${PLUGIN_ROOT}"` for shared compatibility code and sets `SO_PLATFORM=codex` so Codex wins when multiple harness variables are present.
128
130
 
129
- Claude-only events (`SessionEnd`, `PostToolUseFailure`, `PostToolBatch`, and `CwdChanged`) are intentionally absent because Codex 0.144.4 does not expose them as supported project events. Claude Edit/Write payload handlers are also absent: Codex emits canonical `apply_patch` data, while those handlers currently expect Claude's Edit/Write payload shape. They will remain unwired until a real `apply_patch` adapter exists; pretending the payloads are compatible would create false enforcement. The same applies to the Bash-payload handlers, including `post-bash-write-verify.mjs` (#942): they gate on Claude's `tool_name === 'Bash'`, which no Codex bridge delivers, so wiring them today would be a silent no-op (the #919-P2 class). These per-event gaps are tracked as documented asymmetries in `scripts/lib/validate/check-hooks-symmetry.mjs` (Check 6, `handlerAsymmetries`) — an UNDOCUMENTED one-platform-only handler now fails validation.
131
+ ### What Codex actually exposes (measured 2026-09-06, codex-cli 0.144.4)
132
+
133
+ The Codex runtime knows **ten** hook events. This is read out of the shipped binary, which embeds one JSON-Schema pair per event, not quoted from release notes:
134
+
135
+ ```
136
+ $ strings -a "$(npm root -g)/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin/codex" \
137
+ | grep '"title": "'
138
+ "title": "post-tool-use.command.input" / ".output"
139
+ "title": "permission-request.command.input" / ".output"
140
+ "title": "post-compact.command.input" / ".output"
141
+ "title": "pre-tool-use.command.input" / ".output"
142
+ "title": "pre-compact.command.input" / ".output"
143
+ "title": "session-start.command.input" / ".output"
144
+ "title": "subagent-start.command.input" / ".output"
145
+ "title": "subagent-stop.command.input" / ".output"
146
+ "title": "user-prompt-submit.command.input" / ".output"
147
+ "title": "stop.command.input" / ".output"
148
+ ```
149
+
150
+ | Event | 0.144.4 | Wired here |
151
+ |---|---|---|
152
+ | `SessionStart` | yes | yes — banner + `on-session-start.mjs` |
153
+ | `PostToolUse` | yes | yes — `loop-guard.mjs` |
154
+ | `SubagentStop`, `Stop` | yes | yes — `on-stop.mjs` |
155
+ | `PreToolUse`, `SubagentStart` | yes | declared, **empty** (see below) |
156
+ | `UserPromptSubmit`, `PermissionRequest`, `PreCompact`, `PostCompact` | yes | no — this repo has no handler for them |
157
+ | `SessionEnd`, `Interrupt` | **no — event does not exist** | n/a |
158
+ | `PostToolUseFailure`, `PostToolBatch`, `CwdChanged` | no — Claude-only | n/a |
159
+
160
+ **`SessionEnd` is not "Claude-only", it is absent**, and that distinction is load-bearing: the manifest deserializer rejects unknown keys (`unexpected map key` in the same binary), so adding one does not skip a hook — it can reject the whole manifest and take every already-working hook with it. `Interrupt` arrives in 0.150.0+ and async handlers (`"async": true`) in 0.148+; both are **documented upstream but unverified here**, because this host runs 0.144.4. Re-measure against the shipped binary before widening the set — the machine-readable copy is `CODEX_NATIVE_EVENTS` in `scripts/lib/codex/plugin-contract.mjs`.
161
+
162
+ For the day `SessionEnd` does land: upstream caps it (and `Interrupt`) at a **1 s default / 3 s maximum** timeout, where every other event gets 600 s. `hooks/on-session-end.mjs` measures ~221 ms median, so it fits — but only just, and only while it stays that fast.
163
+
164
+ ### Why our PreToolUse guards stay unwired — the reason, corrected
165
+
166
+ Earlier revisions of this page said the handlers were unwired because "no Codex bridge delivers `tool_name`". **That was measuring the wrong thing** — it grepped our own adapter code rather than the Codex payload contract. There is no bridge because none is needed:
167
+
168
+ - `pre-tool-use.command.input` REQUIRES `tool_name` and `tool_input`, alongside `cwd`, `hook_event_name`, `model`, `permission_mode`, `session_id`, `tool_use_id`, `transcript_path`, `turn_id`.
169
+ - The deny envelope is `hookSpecificOutput.{hookEventName, permissionDecision, permissionDecisionReason}` — byte-identical to what `emitDeny()` already writes, and Codex enforces exactly that shape (its own error text: *"PreToolUse hook returned permissionDecision:deny without a non-empty permissionDecisionReason"*). Codex additionally rejects `permissionDecision: "allow"` and `"ask"`; our allow path is a bare `exit 0` with no stdout, so it is compatible.
170
+
171
+ The real blocker is the **tool-name vocabulary**. Codex has no `Bash`, `Edit`, `Write` or `MultiEdit` tool — `strings -a <codex> | grep -c '"Bash"'` returns `0`; its tools are `shell`, `exec_command`, `unified_exec`, `apply_patch`, `update_plan`, `view_image`. Every PreToolUse guard in `hooks/` opens with an equality gate on a Claude tool name and returns `emitAllow()` otherwise, so wiring `pre-bash-destructive-guard.mjs` or `enforce-scope.mjs` today produces a hook that runs, matches nothing, and allows everything — **false enforcement, which is worse than a registered gap** (#919-P2 class).
172
+
173
+ Consequence to state plainly: **PSA-003 (destructive-command guard) and the file-scope guard are behavioural only on Codex today.** The repair is a tool-name map (`shell`/`exec_command`/`unified_exec` → `Bash`) for the Bash guards, plus an `apply_patch` payload adapter for the Edit/Write matchers specifically. Only the second half needs the adapter.
174
+
175
+ These per-event gaps are tracked as documented asymmetries in `scripts/lib/validate/check-hooks-symmetry.mjs` (Check 6, `handlerAsymmetries`) — an UNDOCUMENTED one-platform-only handler fails validation.
130
176
 
131
177
  An empty `PreToolUse` or `SubagentStart` array means the event belongs to the validated Codex surface but currently has no payload-compatible handler. It does not mean installation or hook trust failed.
132
178
 
133
179
  ## Usage
134
180
 
135
- After installation and hook review, start a fresh task. Session Orchestrator exposes the shared skill surface, including:
181
+ After installation or refresh, start a fresh task. In the desktop composer, open the skill picker, search for `go` or `close`, and select the matching **Session Orchestrator** entry. In Codex CLI or the IDE extension, use `/skills` or mention the namespaced skill directly in your prompt. [OpenAI skill invocation](https://learn.chatgpt.com/docs/build-skills)
182
+
183
+ ```text
184
+ $session-orchestrator:session feature # start a session (housekeeping, feature or deep)
185
+ $session-orchestrator:go # execute the agreed plan
186
+ $session-orchestrator:close # verify and close the session
187
+ $session-orchestrator:plan feature # plan a project or feature (new, feature or retro)
188
+ $session-orchestrator:discovery # run quality probes; optionally add a scope
189
+ $session-orchestrator:evolve analyze # manage learnings (analyze, review or list)
190
+ ```
191
+
192
+ These are skill invocations in the Codex prompt, not shell commands. Invoking `go` reads the full canonical `commands/go.md`, including its Express Path and prechecks; invoking `close` reads `commands/close.md`, including its state and ledger checks before the session-end workflow. Codex's native `/goal` is a separate feature. Typing `/go` or `/close` alone is not a portable invocation contract; select the skill or use its explicit namespaced form.
193
+
194
+ ### Manifest Compatibility
195
+
196
+ The plugin uses `.codex-plugin/plugin.json` for Codex and `.cursor-plugin/plugin.json` for Cursor. It does not ship a root Agent Plugins `plugin.json`: on Codex CLI 0.153.3 and desktop runtime 0.153.4, that standard manifest takes precedence, fixes skill discovery to conventional `skills/`, and supplies the root version. The Codex overlay can supply hooks, apps and interface metadata, but cannot override that skill path or version. This was verified with read-only `plugin/read` probes on 2026-09-07. [Codex manifest parser](https://github.com/openai/codex/blob/main/codex-rs/core-plugins/src/agent_plugin_manifest.rs)
197
+
198
+ Moving the former root metadata to the native Cursor manifest lets Codex load its generated entrypoints and cache suffix. Cursor keeps the declared skills and MCP paths; the manifest explicitly disables discovery of extra rules, agents, commands and hooks. Its existing installer supplies the Cursor command and hook adapters. This follows the [Cursor manifest reference](https://cursor.com/docs/reference/plugins); native Cursor loading has not been runtime-tested as part of this change.
136
199
 
137
- - `/session [housekeeping|feature|deep]` — start a session
138
- - `/go` — execute the agreed plan
139
- - `/close` end the session with verification
140
- - `/plan [new|feature|retro]` — plan a project or feature
141
- - `/discovery [scope]` run quality probes
142
- - `/evolve [analyze|review|list]` — manage learnings
200
+ ### Generated Command Skills
201
+
202
+ The Codex manifest registers one generated skill tree at `.codex-plugin/skills/`. It contains the union of names from `commands/` and `skills/`: when both contain the same name, the command takes precedence, giving the plugin one public entry for that name. OpenAI recommends converting reusable Markdown commands into skills. [OpenAI conversion guidance](https://developers.openai.com/plugins/guides/submit-claude-plugin)
203
+
204
+ The manifest also declares top-level `"commands": []` to suppress the installer's automatic command migration. When that field was omitted, a public install with Codex 0.153.3 added nine `source-command-*` aliases alongside the generated entries, including `source-command-close`, without preserving invocation policy. The empty array selects no command sources; it does not register native slash commands. The generated skill tree remains the public invocation surface. [Codex 0.153.3 command-path parser](https://github.com/openai/codex/blob/rust-v0.153.3/codex-rs/core-plugins/src/manifest.rs#L203), [installer migration](https://github.com/openai/codex/blob/rust-v0.153.3/codex-rs/core-plugins/src/command_migration/plugin.rs)
205
+
206
+ The generated files are adapters, not separate workflow bodies. Each links to its canonical command or skill using a package-relative path, so it also works from the installed bundle. Command adapters read the full command first and resolve internal skill calls directly to `skills/<name>/SKILL.md`, avoiding a recursive call to the public entry. Trailing prompt text supplies the command's `$ARGUMENTS` as data; the adapter does not shell-expand arguments or globally substitute them into command documents.
207
+
208
+ Commands declaring `disable-model-invocation: true`, including `go` and `close`, receive `policy.allow_implicit_invocation: false` in `agents/openai.yaml`. This preserves explicit selection while disabling implicit skill invocation. Other commands retain their source setting. [OpenAI invocation policy](https://learn.chatgpt.com/docs/build-skills#optional-metadata)
209
+
210
+ Maintainers edit the canonical files, then regenerate and check the Codex surface from the plugin root:
211
+
212
+ ```bash
213
+ node scripts/generate-codex-skills.mjs
214
+ node scripts/generate-codex-skills.mjs --check
215
+ node scripts/validate-plugin.mjs
216
+ ```
217
+
218
+ `--check` reports stale generated files without writing them. Plugin validation also checks manifest wiring, command coverage, canonical targets and invocation policy. Commit the generated output with its source change, then follow [Refresh and Explicit Cache Invalidation](#refresh-and-explicit-cache-invalidation) to update the installed copy.
143
219
 
144
220
  ## Key Differences from Claude Code
145
221
 
@@ -159,6 +235,8 @@ Both platforms share session history and learnings through `.orchestrator/metric
159
235
 
160
236
  ## Platform Limitations
161
237
 
238
+ Repository skills under `.agents/skills/` and installed-plugin skills can both appear in the picker. The generated union prevents duplicate names within the plugin; it does not remove pre-existing entries from other discovery scopes. Select the installed command entry whose path is under `.codex-plugin/skills/` when a repository also offers a same-named internal skill.
239
+
162
240
  Claude Code dispatches role-specialized agents with dedicated definitions. Codex maps implementation work through its configured roles, so task prompts carry specialization that is not represented by a dedicated role. A project can add more specific TOML definitions under `.codex/agents/` when needed.
163
241
 
164
242
  Hook enforcement is limited to the validated payload-compatible Codex subset described above. In particular, the absence of Claude-only events and Edit/Write handlers is deliberate rather than an installation workaround.
@@ -175,6 +253,7 @@ codex plugin list --available --json
175
253
  - **The target is missing, disabled, duplicated, or at the wrong version:** run `codex plugin marketplace list --json`, remove the exact target with `codex plugin remove session-orchestrator@kanevry` when present, and rerun `node scripts/codex-install.mjs` to reinstall and verify it.
176
254
  - **A `session-orchestrator@openai-curated` or `session-orchestrator@local` installation remains:** these are the only allowlisted legacy IDs. Remove the exact stale ID with `codex plugin remove session-orchestrator@openai-curated` or `codex plugin remove session-orchestrator@local`; unrelated plugins remain untouched.
177
255
  - **The `kanevry` marketplace points at another source:** confirm the conflict with `codex plugin marketplace list --json`, run `codex plugin marketplace remove kanevry`, then rerun the installer from the intended clone so it performs the public marketplace add and plugin add lifecycle.
256
+ - **`go` or `close` is missing from the skill picker:** follow [the refresh steps](#refresh-and-explicit-cache-invalidation), verify the installed version, and restart Codex if reopening the picker does not load the new entries. Use the namespaced skill form from [Usage](#usage), rather than selecting the unrelated native Goal command.
178
257
  - **Plugin is installed and enabled but hooks do not fire:** start a fresh task or fully restart Codex, run `/hooks`, and review the trust state. Installation does not imply hook approval.
179
258
  - **Other pre-public plugin/config/cache/hook-state residue is suspected:** this state is unsupported. Do not modify private Codex files. File an issue with `codex --version`, `codex plugin list --available --json`, and `codex plugin marketplace list --json` output so the public recovery path can be diagnosed.
180
259
  - **Agent dispatch fails:** verify Codex multi-agent support and inspect the bundled or project-level role TOMLs.
@@ -203,4 +282,3 @@ codex mcp list | grep session-orchestrator # the launch string should mention
203
282
  ```
204
283
 
205
284
  The 0.144.4 minimum-version caveat above still stands: everything in this section was measured on **0.141.0**, below the documented minimum, and has not been re-verified on 0.144.4+ or on the reporter's 0.149.0-alpha.4.3.
206
-
@@ -7,9 +7,9 @@ Detailed component inventory and architecture reference for Session Orchestrator
7
7
  ```mermaid
8
8
  flowchart LR
9
9
  USER([Operator]) -->|invokes /session| COORD[Coordinator]
10
- COORD -->|reads| SK[Skills<br/>49 user-facing]
11
- COORD -->|invokes| CMD[Commands<br/>28 slash-cmds]
12
- COORD -->|dispatches| AG[Agents<br/>15 typed sub-agents]
10
+ COORD -->|reads| SK[Skills<br/>43 user-facing]
11
+ COORD -->|invokes| CMD[Commands<br/>25 slash-cmds]
12
+ COORD -->|dispatches| AG[Agents<br/>14 typed sub-agents]
13
13
  AG -.->|parallel waves| W1[code-implementer]
14
14
  AG -.-> W2[test-writer]
15
15
  AG -.-> W3[security-reviewer]
@@ -18,29 +18,29 @@ flowchart LR
18
18
  COORD -->|writes| METRIC[.orchestrator/metrics/<br/>sessions · learnings · events]
19
19
  ```
20
20
 
21
- ## Skills (49 user-facing)
21
+ ## Skills (43 user-facing)
22
22
 
23
23
  - **Lifecycle:** `session-start`, `session-plan`, `wave-executor`, `session-end`, `quality-gates`, `using-orchestrator`
24
- - **Authoring:** `skill-creator`, `mcp-builder`, `hook-development`, `frontmatter-guard`, `contract-version-bump`
25
- - **Planning & discovery:** `plan`, `discovery`, `journey-audit`, `repo-audit`, `brainstorm`, `write-executable-plan`, `debug`, `claude-md-drift-check`, `grill`
26
- - **Architecture:** `architecture`, `domain-model`, `ubiquitous-language`
24
+ - **Authoring:** `mcp-builder`, `hook-development`, `frontmatter-guard`
25
+ - **Planning & discovery:** `plan`, `discovery`, `repo-audit`, `brainstorm`, `write-executable-plan`, `debug`, `claude-md-drift-check`, `grill`
26
+ - **Architecture:** `architecture` (carries the former `domain-model` grilling flow plus the `CONTEXT.md`/ADR formats under `skills/architecture/references/`)
27
27
  - **Cross-session:** `evolve`, `convergence-monitoring`, `memory-cleanup`, `reconcile`, `sunset-review`, `eval`
28
- - **Vault & docs:** `vault-sync`, `vault-mirror`, `daily`, `docs-orchestrator`
28
+ - **Vault & docs:** `vault-sync`, `vault-mirror`, `docs-orchestrator`
29
29
  - **Ecosystem:** `bootstrap`, `gitlab-ops`, `gitlab-portfolio`, `ecosystem-health`, `mode-selector`, `autopilot`, `dispatcher`, `remote-offload`, `spinout`, `npm-publish`
30
30
  - **Testing:** `test-runner`, `playwright-driver`, `peekaboo-driver`
31
31
  - **Content review:** `persona-panel`
32
32
  - **Operator ergonomics:** `eli5` (plain-language restatement of the last answer)
33
33
  - **Visualization:** `tmux-layout` (opt-in operator side-channel — [ADR-0007](adr/0007-tmux-visualization-substrate.md))
34
34
 
35
- ## Commands (28)
35
+ ## Commands (25)
36
36
 
37
- `/session`, `/go`, `/close`, `/discovery`, `/plan`, `/evolve`, `/bootstrap`, `/harness-audit`, `/autopilot`, `/autopilot-multi`, `/repo-audit`, `/test`, `/memory-cleanup`, `/portfolio`, `/brainstorm`, `/debug`, `/persona-panel`, `/grill`, `/sunset-review`, `/templates-ack`, `/dispatcher`, `/reconcile`, `/spinout`, `/eval`, `/release`, `/contract-version-bump`, `/eli5`, `/journey-audit`.
37
+ `/session`, `/go`, `/close`, `/discovery`, `/plan`, `/evolve`, `/bootstrap`, `/harness-audit`, `/autopilot`, `/repo-audit`, `/test`, `/memory-cleanup`, `/portfolio`, `/brainstorm`, `/debug`, `/persona-panel`, `/grill`, `/sunset-review`, `/templates-ack`, `/dispatcher`, `/reconcile`, `/spinout`, `/eval`, `/release`, `/eli5`.
38
38
 
39
- ## Agents (15 typed sub-agents)
39
+ ## Agents (14 typed sub-agents)
40
40
 
41
- `code-implementer`, `test-writer`, `ui-developer`, `db-specialist`, `security-reviewer`, `session-reviewer`, `docs-writer`, `architect-reviewer`, `qa-strategist`, `analyst`, `ux-evaluator`, `dialectic-deriver`, `memory-proposal-collector`, `skill-applied-judge`, `eval-judge`.
41
+ `code-implementer`, `test-writer`, `ui-developer`, `db-specialist`, `security-reviewer`, `session-reviewer`, `docs-writer`, `architect-reviewer`, `qa-strategist`, `analyst`, `ux-evaluator`, `dialectic-deriver`, `skill-applied-judge`, `eval-judge`.
42
42
 
43
- Custom agents live in `agents/` (plugin) or `.claude/agents/` (project) as Markdown with YAML frontmatter. The authoring spec — required fields, body conventions, validation commands — is in [`agents/AGENTS.md`](../agents/AGENTS.md), following the canonical [code.claude.com/sub-agents](https://code.claude.com/docs/en/sub-agents) contract.
43
+ Custom agents live in `agents/` (plugin) or `.claude/agents/` (project) as Markdown with YAML frontmatter. The authoring spec — required fields, body conventions, validation commands — is in [`docs/agent-authoring.md`](./agent-authoring.md), following the canonical [code.claude.com/sub-agents](https://code.claude.com/docs/en/sub-agents) contract.
44
44
 
45
45
  ## Hook event types (10)
46
46
 
@@ -52,15 +52,37 @@ Codex uses the curated six-event project subset `SessionStart`, `PreToolUse`, `P
52
52
 
53
53
  - **Output Styles (3):** `session-report`, `wave-summary`, `finding-report`.
54
54
  - **Policy & rules:** `.orchestrator/policy/blocked-commands.json` (destructive-command rules); `.claude/rules/parallel-sessions.md` (PSA-001..PSA-004).
55
- - **Codex:** `.codex-plugin/plugin.json` (tracked `+codex.<UTC timestamp>` version), compatibility config, agent role definitions, and the public marketplace/add/list lifecycle implemented by `scripts/codex-install.mjs`. Every run refreshes via `plugin add`; hook trust remains an operator decision in a fresh task through `/hooks`.
55
+ - **Codex:** `.codex-plugin/plugin.json` (tracked `+codex.<UTC timestamp>` version), generated command and skill entrypoints under `.codex-plugin/skills/`, compatibility config, agent role definitions, and the public marketplace/add/list lifecycle implemented by `scripts/codex-install.mjs`. `scripts/generate-codex-skills.mjs` produces the name union with commands taking precedence; [Codex usage](codex-setup.md#usage) explains selection. Every installer run refreshes via `plugin add`; hook trust remains an operator decision in a fresh task through `/hooks`.
56
+ - **Cursor:** `.cursor-plugin/plugin.json` registers canonical skills and `.mcp.json` using Cursor's native manifest format. Additional native component discovery is explicitly disabled; `scripts/cursor-install.mjs` supplies the existing command and hook adapters. The former standard root manifest was moved to prevent [Codex manifest interception](codex-setup.md#manifest-compatibility).
56
57
  - **Pi:** `package.json` `pi` manifest, `pi/extensions/session-orchestrator.ts` bridge, `hooks/hooks-pi.json`, `scripts/pi-install.mjs`.
58
+ - **Portable cross-harness surface (generated, never hand-edited):** root `AGENTS.md` (byte-identical copy of `CLAUDE.md`) and `.agents/skills/<name>/SKILL.md` — mirrors carrying only spec-legal frontmatter plus a pointer body. These two surfaces are written by `scripts/generate-agents-skills.mjs` and drift-checked via its `--check` form inside `scripts/validate-plugin.mjs`. Native plugin manifests are maintained separately, with versions updated by `scripts/release.mjs`.
57
59
  - **Scripts:** deterministic CLI tools (parse-config, run-quality-gate, validate-wave-scope, validate-plugin, token-audit, autopilot) plus shared lib under `scripts/lib/*.mjs`, all covered by the vitest suite.
58
60
 
59
61
  ## `/harness-audit` — Anthropic large-codebase rubric
60
62
 
61
63
  `scripts/harness-audit.mjs` runs **9 deterministic categories / 38 checks** over a repo and emits `.orchestrator/metrics/audit.jsonl`. Category 8 ("Large-Codebase Readiness") operationalises Anthropic's [Claude Code large-codebase best-practices](https://claude.com/blog/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start) checklist — layered `CLAUDE.md` (or `AGENTS.md`), codebase-map presence, LSP/code-intelligence wiring, scoped test/lint commands, `permissions.deny`, and root-file leanness — as scored signals you can run on yourself and on consumer repos. Category 9 ("Skill-Health Surfacing") surfaces the #648 per-skill health pipeline — telemetry hygiene, scorer wiring, and an advisory-only verdict tally that never affects points; non-adoption always scores full points. These checks are intentionally orthogonal to repo-audit's baseline-compliance pass/fail; both surfaces ship.
62
64
 
63
- ## Comparison vs. maestro-orchestrate
65
+ ## Comparisons
66
+
67
+ Moved here from the README so that a claim about another project can carry its measurement next to it. Every row below names when and how it was measured; rows that could not be measured were **removed rather than softened** — an unverifiable comparison row is worse than none.
68
+
69
+ ### vs. `open-gsd/gsd-core`
70
+
71
+ Surface counts measured 2026-09-06 by this repo's 360° ecosystem probe (`docs/audits/2026-09-06-360-audit/w1/d11-github-overlap.md`); session-orchestrator's own counts measured the same day with the commands listed in the [README](../README.md#what-you-get).
72
+
73
+ | Axis | session-orchestrator | `open-gsd/gsd-core` |
74
+ |---|---|---|
75
+ | Commands / skills / agents | 25 / 43 / 14 | 70 / 71 / 35 |
76
+ | Hook guards | 27 hook files, 10 event types | 28 hooks, incl. write / read / prompt / workflow / secret-read / agent-isolation / worktree-path guards |
77
+ | Cross-session learning | `/evolve` + confidence-scored `learnings.jsonl`; reconcile turns eligible learnings into PROPOSED rules an operator approves one by one | `gsd-extract-learnings`, `gsd-mempalace-*` |
78
+ | Harness coverage | Claude Code, Codex CLI, Cursor IDE, Pi (4) | 44 `capabilities/` directories (pi, hermes, kimi, windsurf, opencode, ollama, …) |
79
+ | Install | marketplace / clone + installer script per harness | `npx @opengsd/gsd-core@latest` |
80
+
81
+ **Two README rows were deleted here, not carried over.** The old README comparison table claimed "Scope and command enforcement hooks → Other orchestrators: None" and "Cross-session learning → Other orchestrators: None". Both are **false**: gsd-core ships 28 guard hooks and two learning subsystems (measurement above). Two further rows — "VCS integration → usually GitHub only" and "Circuit breaker → Partial" — were removed because no measurement of any named project backs them.
82
+
83
+ **What is NOT established.** Whether gsd-core's session lock covers the *operator-session* axis (multiple concurrent human sessions in one working copy) is an **open question** — the probe counted 14 code hits in its hooks without reading them. Until that is read, treat the operator-session axis as the plausible distinguishing surface rather than a proven one. The surfaces this repo can point at concretely are: a heartbeat session lock plus peer-scope manifests and the PSA rule set (`.claude/rules/parallel-sessions.md`); owner privacy by construction (`owner.yaml` outside every repo plus a leakage scanner with name redaction); rules derived from this repo's own measured telemetry; Obsidian vault mirroring; and dual GitLab + GitHub auto-detection.
84
+
85
+ ### vs. `maestro-orchestrate`
64
86
 
65
87
  Both [`maestro-orchestrate`](https://github.com/josstei/maestro-orchestrate) and session-orchestrator coordinate multi-agent work in long-running AI coding sessions. They differ in scope and execution model:
66
88
 
@@ -70,6 +92,6 @@ Both [`maestro-orchestrate`](https://github.com/josstei/maestro-orchestrate) and
70
92
  | Runtime coverage | Claude Code + Codex CLI + Cursor IDE + Pi (4) | Gemini CLI + Claude Code + Codex + Qwen Code (4) |
71
93
  | VCS integration | GitLab + GitHub (auto-detected); hook events + commands wire to both | Runtime-agnostic; VCS work delegated to user |
72
94
  | Cross-session learning | Confidence-scored entries surfaced at session-start; opt-in `/evolve` review | Session archival without explicit learning extraction |
73
- | Specialist agents | 15 typed agents | 39 specialist agents across design/impl/review/debugging/security/compliance |
95
+ | Specialist agents | 14 typed agents | 39 specialist agents across design/impl/review/debugging/security/compliance |
74
96
 
75
97
  The two plugins are complementary rather than competing: session-orchestrator focuses on a single wave-based lifecycle with VCS + learning integration, while maestro-orchestrate optimises for multi-runtime parallel specialist delivery.
@@ -29,7 +29,11 @@ Working **in this repo** does not need the installer — those paths are committ
29
29
 
30
30
  ## Configuration
31
31
 
32
- Cursor reads CLAUDE.md natively. Add a `## Session Config` section:
32
+ **Where Cursor picks up project instructions — measured-or-unknown, 2026-09-06.** This page used to state flatly that "Cursor reads CLAUDE.md natively". That claim is **not substantiated**: the current Cursor rules documentation names `.cursor/rules` and `AGENTS.md` as the project-instruction surfaces and does not name `CLAUDE.md`, and nothing in this repo measures the behaviour. Treat native `CLAUDE.md` pickup as **unverified** rather than as fact.
33
+
34
+ What follows from that, practically: put the `## Session Config` block where you can verify Cursor reads it. `scripts/cursor-install.mjs` and the orchestrator's own config parser (`scripts/parse-config.mjs`) read the block out of the repo's project-instruction file regardless of which client loaded it, so the block below is correct wherever you put it — but if Cursor is not surfacing your session settings, mirror the block into `AGENTS.md` (the Codex/Cursor-facing alias, see [`skills/_shared/instruction-file-resolution.md`](../skills/_shared/instruction-file-resolution.md)) before assuming a parser bug.
35
+
36
+ Add a `## Session Config` section:
33
37
 
34
38
  ```markdown
35
39
  ## Session Config
@@ -64,7 +68,7 @@ Cursor has no Skill tool. Commands tell the agent to **Read** `skills/<name>/SKI
64
68
  | Interactive choices | AskUserQuestion tool | Numbered Markdown lists |
65
69
  | Agent dispatch | Agent() tool (parallel) | Sequential execution (no subagents) |
66
70
  | State directory | .claude/ | .cursor/ |
67
- | Config file | CLAUDE.md | CLAUDE.md (same!) |
71
+ | Config file | CLAUDE.md | CLAUDE.md — native pickup **unverified**; Cursor's rules doc names `.cursor/rules` + `AGENTS.md` (see § Configuration) |
68
72
  | Task tracking | TaskCreate/TaskUpdate | Text-based checklists |
69
73
  | Pre-edit enforcement | PreToolUse hook (blocks) | `preToolUse` via cursor-hook-bridge (blocks) |
70
74
  | Command enforcement | PreToolUse hook (blocks) | `beforeShellExecution` via cursor-hook-bridge (blocks) |
@@ -89,7 +89,8 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
89
89
  |---|---|---|
90
90
  | `orchestrator.session.started` | `hooks/on-session-start.mjs` | SessionStart. **Optional, additive:** `peers_superseded` (number, GH#67) — the count of mechanically-detected peers (`mechanicalPeers`) whose `lockSuperseded === true` (a LIVE lock at this root is held by a different raw session_id than that registry-only peer — a HINT, not a verdict; see the GH#67 discussion above). Computed as `mechanicalPeersSuperseded` and rendered inline per peer via `supersessionMarker()` in the banner text. `peer_count` is deliberately left unchanged by this addition, so the supersession rate is measurable (`peers_superseded` / `peer_count`) instead of only inferred from banner prose (HR-105) |
91
91
  | `orchestrator.session.ended` | `hooks/on-session-end.mjs` | SessionEnd. **Payload:** `reason` (always), plus `session_id` / `semantic_session_id` / `duration_ms` — **each OMITTED, never fabricated, when it could not be measured** (#1068 AC1; `duration_ms` since the W5 F1 sweep). `duration_ms` is written ONLY when the ending session IS the one `.orchestrator/current-session.json` records (the `isRecordedSession` predicate, decided on the RAW stdin UUID) AND that file's start timestamp parsed. Until that fix it fell back to a hard `0`: **1082 of 1498** fleet records (72,2 % — 415 nonzero, 1 key absent, measured 2026-09-02) carried a zero that reads as a MEASURED zero-length session and is indistinguishable from one. An ABSENT `duration_ms` means NOT MEASURED, never "instant" — same omit-never-fabricate contract as `session.stopped` / `agent.stopped` above |
92
- | `orchestrator.session.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop. **Payload:** `session_id`, `semantic_session_id`, `wave`, optional `branch` / `commit`, plus the pair `duration_ms` + `duration_source` (`stdin-start-ms` | `session-lock`). **The pair is written TOGETHER or omitted together never `0`.** Until this change `duration_ms` was a hard `0` in **8.127 of 8.127** fleet records (measured 2026-09-02): the expression fell back to `0` because the harness never sends `start_ms`, and a fabricated zero reads as a MEASURED zero-length turn, indistinguishable from one. The span is now derived from `.orchestrator/session.lock` `started_at` **ownership-gated on the RAW stdin `session_id`**, because a lock in this working copy routinely names a live PEER session (the resolved id is deliberately not used: it falls back to `current-session.json`, which is the foreign-identity inheritance the guard refuses). No owned, readable, parseable lock ⇒ both keys absent, which means NOT MEASURED and never "instant". **`duration_source` says WHICH span the number is, and the two are not the same quantity:** `session-lock` is SESSION-elapsed measured at this turn's end Stop fires per TURN while `started_at` is stamped once per SESSION, so it GROWS MONOTONICALLY across a session's turns (the last Stop of a 3-hour session reports ~3 hours, not its final turn); `stdin-start-ms` is TURN-elapsed, the only first-party measurement of the turn itself, and the harness has never sent it. Do not sum `session-lock` spans over a session that double-counts |
92
+ | `orchestrator.turn.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop, once per ASSISTANT TURN. **The canonical name for this event since 2026-09-06 (GitLab #1234); payload identical to the deprecated `orchestrator.session.stopped` row below, minus its `deprecated` marker.** **Why the rename:** the old name says *session*, the emitter fires per *turn*. Measured 2026-09-06 over the 90-day fleet window: **15.538 records against 2.016 distinct `orchestrator.session.started` ids = 7,7 per session**, with **184 for a single id**. Six consumers read it as a session-lifecycle signal and were therefore wrong by that factor any "sessions stopped" count derived from it is a turn count. **A turn is not a session, and the count is not a rate:** to count sessions, count `session.started` ids; to count closes, count `sessions.jsonl` records with `status: completed`. **Migration:** both names carry the same payload for one generation; the legacy name additionally carries `deprecated: true`. Removal of `orchestrator.session.stopped`: **2027-03-06**. Readers should switch the name they match on and change nothing else. **NOT affected:** the SubagentStop branch keeps emitting `orchestrator.agent.stopped` a different event whose per-agent cardinality is correct |
93
+ | `orchestrator.session.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop. **Payload:** `session_id`, `semantic_session_id`, `wave`, optional `branch` / `commit`, plus the pair `duration_ms` + `duration_source` (`stdin-start-ms` | `session-lock`). **The pair is written TOGETHER or omitted together — never `0`.** Until this change `duration_ms` was a hard `0` in **8.127 of 8.127** fleet records (measured 2026-09-02): the expression fell back to `0` because the harness never sends `start_ms`, and a fabricated zero reads as a MEASURED zero-length turn, indistinguishable from one. The span is now derived from `.orchestrator/session.lock` `started_at` — **ownership-gated on the RAW stdin `session_id`**, because a lock in this working copy routinely names a live PEER session (the resolved id is deliberately not used: it falls back to `current-session.json`, which is the foreign-identity inheritance the guard refuses). No owned, readable, parseable lock ⇒ both keys absent, which means NOT MEASURED and never "instant". **`duration_source` says WHICH span the number is, and the two are not the same quantity:** `session-lock` is SESSION-elapsed measured at this turn's end — Stop fires per TURN while `started_at` is stamped once per SESSION, so it GROWS MONOTONICALLY across a session's turns (the last Stop of a 3-hour session reports ~3 hours, not its final turn); `stdin-start-ms` is TURN-elapsed, the only first-party measurement of the turn itself, and the harness has never sent it. Do not sum `session-lock` spans over a session — that double-counts. **DEPRECATED since 2026-09-06 (GitLab #1234), removal 2027-03-06** — superseded by `orchestrator.turn.stopped` above, which carries the identical payload under the name that matches what the emitter actually measures. Every record emitted under this name since the rename additionally carries `deprecated: true`, so a reader can tell at a glance that it matched the legacy name; a record WITHOUT that key predates 2026-09-06. Both names are emitted from the same payload object, so they can never disagree |
93
94
  | `orchestrator.session.backfill_completed` | `hooks/on-session-end.mjs` (`emitBackfillOutcome`) · `scripts/backfill-abandoned-sessions.mjs` (`emitBackfillCompleted`, the startup/CLI path, #1167) | SessionEnd, once per backfill call (#1068 AC2 — the backfill outcome is canonically queryable, the side-log is no longer the only result source) — and separately, once per record the startup/CLI path itself writes, which until #1167 wrote SILENTLY: the SessionEnd hook was the only emitter, so nothing distinguished "the backfill never ran" from "it ran at startup". Both producers mirror the same payload shape so one filter queries either. Payload: `kind` (`abandoned`\|`state-md-completed`), `action` (the backfill result action, e.g. `appended`\|`superseded`\|`skipped-already-recorded`\|`unknown`), plus `session_id`, `semantic_session_id`, `record_id`, `supersedes`, `reason` — each OMITTED when unknown, never `null`. Best-effort on both paths: emission is wrapped in try/catch so a telemetry failure never blocks the backfill it describes |
94
95
  | `orchestrator.session.root_left` | `scripts/lib/session-transition.mjs` (`leaveSourceRoot`) | a session left a repo root for good — the process-boundary teardown of Worktree-Auto-Promotion (#1069), emitted into the OLD root's stream after `deregisterSelf()` + `release()`, whether or not either found anything. NOT emitted when the teardown ABORTED (invalid args, a lock owned by another session, an unparseable lock): the event asserts a root was left, and on those branches none was. Payload: `session_id`, `semantic_session_id` (OMITTED when unknown, never `null`), `from_root_hash` (`repoPathHash()` of the abandoned root — the SAME hash the session registry keys its entries by, so a departure joins to the entry it removed), `from_root_basename` (matching the registry's `repo_name`), `reason` (e.g. `worktree-promotion`). **Never the absolute root:** this payload also travels over the optional Clank webhook with no redaction, and an absolute root on a developer host is `/Users/<operator>/…` — same rule as `board_written` / `mirror_completed` and `relativeWorktreePath` in `worktree-pipeline.mjs`. Its absence beside a live-looking registry entry is the phantom-peer signature the event exists to make visible |
95
96
  | `orchestrator.session.lock.acquired` | `hooks/_lib/lock-bootstrap.mjs` | SessionStart |
@@ -99,25 +100,27 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
99
100
  | `orchestrator.session.lock.reaped` | `scripts/lib/lock-reaper.mjs` | a dead lease was reaped. Payload: `session_id`, `semantic_session_id`, `host`, `pid`, `age_hours`, `reap_mode`, `current_session` |
100
101
  | `orchestrator.agent.stopped` | `hooks/on-stop.mjs` (`handleSubagentStop`) | SubagentStop, once per stopping subagent — the fleet's most frequent event (103.763 records / 19 repos, measured 2026-09-02). **Payload (#1190) — every field is OPTIONAL and the KEY IS OMITTED when the measurement could not be made; never `null`, never `'unknown'`, never a stand-in `0`/`false`:** `agent` (the stdin `agent_type`/`subagent_type`, trimmed) — **omitted when empty**, which is the #1190 fix itself: the previous `input?.agent_type ?? 'unknown'` never fired on the EMPTY STRING the harness actually sends, so 89.991 of the 103.763 historical records (86,7%) carry `agent: ""`. A consumer must read a MISSING `agent` as "the harness did not name the type", not as a broken emitter. `agent_id` (stdin `agent_id`\|`subagent_id`; opaque id, charset-guarded with `/^[A-Za-z0-9_-]{1,64}$/` before it is interpolated into any path). The remaining fields are derived from the agent's sidecar pair `<transcript-dir>/<parent-basename>/subagents/agent-<agent_id>.{jsonl,meta.json}` and are all omitted when that derivation is not possible (no `agent_id`, no `transcript_path`, or a rejected id): `transcript_found` (boolean — `false` here is a MEASURED absence, the probe ran; the key is ABSENT when it could not run), `tool_use_id` + `agent_type_meta` (from the `.meta.json` keys `toolUseId` / `agentType`; `agent_type_meta` is a SECOND witness for the type and is deliberately NOT merged into `agent`, so the empty-`agent_type` rate stays measurable), `duration_ms` + `duration_source` (`meta-birthtime` — the sidecar carries no spawn timestamp, so its birthtime IS the spawn moment; both keys omitted together when the stat fails, never a fabricated `0`), and `status` (`done`\|`partial`\|`blocked`\|`failed`\|`no-tests-needed`, from the last LINE-ANCHORED `STATUS:` marker in the final 64 KiB of the agent transcript — the anchoring rationale is `scripts/lib/wave-transcript-tail.mjs:105-112`, since a free-floating match fires on any agent that merely QUOTES the token). **`status` coverage is partial by measurement, not by accident: 61,7 %** — 71 `done` / 3 `partial` / 46 absent over **120 COMPLETED sidecars**, population: one operator's `~/.claude/projects/<this-repo>` directory, sidecars idle ≥ 30 min (`find … -path "*/subagents/agent-*.jsonl" -mmin +30 | head -120`), each read through `readStatusFromTranscriptTail`, measured 2026-09-02. Re-cut it with that command; the earlier "roughly a quarter to a half" came from an n=4 IN-FLIGHT sample and undercounted, because a running agent has not written its STATUS line yet. **An absent `status` means NOT FOUND, never success.** **Never the transcript text, the meta `description`, or an absolute path:** this payload also travels over the optional Clank webhook with no redaction — same rule as `board_written` / `session.root_left`. `session_id` / `semantic_session_id` / `schema_version` are stamped by `emitEvent()` |
101
102
  | `orchestrator.memory.propose_invoked` | `hooks/pre-bash-memory-propose-audit.mjs` | PreToolUse(Bash) |
102
- | `orchestrator.wave.started` / `.completed` | `hooks/post-tool-batch-wave-signal.mjs` · `hooks/on-session-end.mjs` (`emitFinalWaveCompleted`, `.completed` only) | PostToolBatch — fires live via `.claude/wave-scope.json` `.wave` increase (mechanical fallback, #612); an explicit injected `wave_signal` still takes precedence. **Second `.completed` emitter (#1193):** the batch hook closes wave N-1 only at an N-1→N transition, so the LAST wave of every session never received a completion — measured fleet-wide 2026-09-02 as **296 gaps over 296 wave runs** (1018 started vs 722 completed), exactly one missing final completion per run. SessionEnd now emits it. **Payload of that record:** `wave_number` (the `current-session.json` `last_wave`), `reason: 'session-end'`, `emitted_by: 'on-session-end'`, plus `session_id` / `semantic_session_id` — **omitted when unattested**, never fabricated. **Idempotent** via the `last_wave_completed` high-water mark in `.orchestrator/current-session.json`, written by BOTH emitters and preserved across `/clear`+compact by `on-session-start.mjs`; `last_wave` absent or `0` emits nothing (an Express-Path or coordinator-direct session never batched, and zero waves is the correct reading, not a gap). **Two gates on the SessionEnd emitter, both load-bearing:** (a) OWNERSHIP — `current-session.json` is a single repo-global file describing whichever session most recently ran SessionStart, routinely a different still-live session in a shared working copy, so the emit reuses `resolveSession()`'s `isRecordedSession` predicate; when false it emits nothing AND writes nothing, since writing the marker into a peer's file would silence the peer's own SessionEnd and preserve this very gap on the wrong session. (b) REASON — `reason === 'clear'` **and `reason === 'resume'`** are SKIPPED: the SessionEnd matcher is empty, so `/clear` fires the hook mid-wave while the LOGICAL session continues, and `on-session-start.mjs` preserves `last_wave` / `last_wave_completed` across a resume of the SAME logical session exactly as it does across a clear — so both end the HARNESS session, not the logical one. Closing the live wave on either is premature, and the preserved marker would then suppress the real completion later. Resume is the MORE common of the two (fleet n = 1498 `session.ended`, 2026-09-02: 12 `resume` vs 9 `clear`). Deliberately SessionEnd-only (`on-stop.mjs` is not mirrored) so the closed-vs-abandoned split stays measurable |
103
+ | `orchestrator.wave.started` / `.completed` | `hooks/post-tool-batch-wave-signal.mjs` · `hooks/on-session-end.mjs` (`emitFinalWaveCompleted`, `.completed` only) | PostToolBatch — fires live via `.claude/wave-scope.json` `.wave` increase (mechanical fallback, #612); an explicit injected `wave_signal` still takes precedence. **Second `.completed` emitter (#1193):** the batch hook closes wave N-1 only at an N-1→N transition, so the LAST wave of every session never received a completion — measured fleet-wide 2026-09-02 as **296 gaps over 296 wave runs** (1018 started vs 722 completed), exactly one missing final completion per run. SessionEnd now emits it. **Payload of that record:** `wave_number` (the `current-session.json` `last_wave`), `reason: 'session-end'`, `emitted_by: 'on-session-end'`, plus `session_id` / `semantic_session_id` — **omitted when unattested**, never fabricated. **Idempotent** via the `last_wave_completed` high-water mark in `.orchestrator/current-session.json`, written by BOTH emitters and preserved across `/clear`+compact by `on-session-start.mjs`; `last_wave` absent or `0` emits nothing (an Express-Path or coordinator-direct session never batched, and zero waves is the correct reading, not a gap). **Two gates on the SessionEnd emitter, both load-bearing:** (a) OWNERSHIP — `current-session.json` is a single repo-global file describing whichever session most recently ran SessionStart, routinely a different still-live session in a shared working copy, so the emit reuses `resolveSession()`'s `isRecordedSession` predicate; when false it emits nothing AND writes nothing, since writing the marker into a peer's file would silence the peer's own SessionEnd and preserve this very gap on the wrong session. (b) REASON — `reason === 'clear'` **and `reason === 'resume'`** are SKIPPED: the SessionEnd matcher is empty, so `/clear` fires the hook mid-wave while the LOGICAL session continues, and `on-session-start.mjs` preserves `last_wave` / `last_wave_completed` across a resume of the SAME logical session exactly as it does across a clear — so both end the HARNESS session, not the logical one. Closing the live wave on either is premature, and the preserved marker would then suppress the real completion later. Resume is the MORE common of the two (fleet n = 1498 `session.ended`, 2026-09-02: 12 `resume` vs 9 `clear`). Deliberately SessionEnd-only (`on-stop.mjs` is not mirrored) so the closed-vs-abandoned split stays measurable. **Diff-size keys on the BATCH-HOOK emitter only (#980):** `files_changed` (integer) + `files_changed_source: 'worktree-vs-wave-start-sha'` (the only value emitted today; present iff `files_changed` is). Measurement: the DEDUPED union of `git diff --name-only <wave_start_sha>` and `git ls-files --others --exclude-standard`, run in the project dir at the N→N+1 transition, where `wave_start_sha` is the `git rev-parse HEAD` the batch hook persisted into `.orchestrator/current-session.json` when wave N was OPENED (same ownership gate as the wave keys; written as `null` when git is unreadable, so a previous wave's sha can never inflate the next count). Worktree-vs-sha rather than `<sha>..HEAD` because the coordinator commits at session close, not per wave — a commit-only diff reads 0 for every wave of a normal session. **Both keys are OPTIONAL and absent-is-not-zero:** any git failure, a 1.5 s timeout, or a missing `wave_start_sha` omits them, and `scripts/lib/convergence-monitor.mjs` reads an absent key as `null`, so the `shrinking_diff` signal simply does not fire (it never reads a fabricated 0). The `on-session-end.mjs` final-wave `.completed` carries NEITHER key by design — no wave-open transition runs there, so it has no start sha to measure against |
103
104
  | `orchestrator.wave.final_refused` | `hooks/on-session-end.mjs` (`emitFinalWaveCompleted` → `emitFinalRefused`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`, wrapped in its OWN try/catch — independent of the caller's outer catch — so a telemetry failure on one refusal can never surface as a teardown failure) | the SIBLING event to `orchestrator.wave.completed` above (#1201 Part B / Discovery D8) — fires exactly once per SessionEnd in which the final `.completed` was NOT emitted, one row per refusal. Deliberately a SEPARATE event name rather than `.completed` carrying `emitted:false`: existing consumers of `.completed` treat every row as a finished wave, and overloading it would silently corrupt that count. **Payload:** `reason` (always present, closed enum — verified against `emitFinalWaveCompleted`'s own call sites) `not-recorded` \| `clear` \| `resume` \| `unreadable` \| `session-id-mismatch` \| `no-wave` \| `already-completed` \| `exception`, `emitted_by: 'on-session-end'` (always), plus `session_id` / `semantic_session_id` — omitted when `null`, never fabricated. **Optional, absent-is-not-zero:** `wave_number` — present ONLY on the `already-completed` reason, the sole call site that passes a resolved `last_wave` through to `emitFinalRefused`; every other reason, INCLUDING `no-wave`, omits it (that path never resolved a wave number at all — the function's own JSDoc pins this: "only when `last_wave` was resolved to a positive number before the refusal (currently only `already-completed`)"). Six of the eight reasons were previously SILENT refusal paths with no trace anywhere (`.claude/rules/host-resources.md` § HR-105: "a refusal that writes nothing is unfalsifiable") |
104
105
  | `orchestrator.quality_gate.passed` / `.failed` | `scripts/run-quality-gate.mjs` (the gate CLI — live between waves) · `scripts/lib/quality-gate.mjs` (`emitGateEvent` inside `runQualityGateWithRetry`, reached ONLY under `verification-auto-fix.enabled: true` — default `false`, and `false` in this repo) | CLI: once per gate-CLI run. Library: once per `runQualityGateWithRetry` **call**, never per retry attempt (`attempts` carries that detail). The two paths never nest, so one run passes through exactly one emitter. **Payload (both):** `variant`, `exit_code`, plus `session_id` / `semantic_session_id` when `sessionAttribution()` finds a session lock (both omitted when it does not). CLI `variant` is the `--variant` value (`baseline`\|`incremental`\|`full-gate`\|`per-file`); the library pins `variant: 'auto-fix-loop'`. **CLI only:** `wave_number`. **Library only:** `attempts` (1…`maxRetries+1`) and `gate` (`lint`\|`typecheck`\|`test` — the fail-fast gate of the last attempt; omitted on the passing path). **Optional on both:** `counts: {passed, failed, total}`. **Absent is not zero — for both optional fields.** `counts` is admitted by the ONE shared policy `admitSuiteCounts()` (`scripts/lib/gates/gate-helpers.mjs`, #967 item 2), which returns `null` — never a zero triple — for an unmeasured or inconsistent input (test gate skipped/stubbed, fail-fast on lint or typecheck before the test step, no parseable `<N> passed` marker, or `passed + failed !== total`); both callers spread `...(counts ? { counts } : {})`, so the KEY is missing in those runs. A present `counts.failed: 0` therefore means "measured, zero failures", while an absent `counts` means "not measured" — reading a missing field as `0` mis-analyses the ledger in both directions. Same contract for `wave_number` (CLI, #966 step 1): resolved from the `.{pi,cursor,codex,claude}/wave-scope.json` sidecar, **omitted** — never `0` — when there is no sidecar or its `wave` is non-numeric/non-positive; a human running `npm run quality-gate` from a `git push` has no wave at all, so an invented wave 0 would have to be special-cased by every consumer. Note `total` is `passed + failed` and EXCLUDES skipped/todo (see `extractTestCounts`) |
105
- | `orchestrator.scope.foreign_session_ignored` | `hooks/enforce-scope.mjs` (Gate 3b, #1123) | PreToolUse Edit/Write exactly once per gated tool call while a FOREIGN-session `wave-scope.json` is live: the manifest's `session`/`semantic_session` provably name another session, so the hook allows instead of enforcing. **Payload:** `hook`, `manifest` (path), `manifest_session` (string[]), `own_session` (string[]), `wave`, `file_path`. Legacy manifests without a `session` field never emit this — they stay enforced |
106
+ | `orchestrator.scope.foreign_session_ignored` | `hooks/enforce-scope.mjs` (Gate 3b, #1123) · `hooks/enforce-commands.mjs` · `hooks/post-bash-write-verify.mjs` (both Gate 3b, #1153 P1) | exactly once per gated tool call while a FOREIGN-session `wave-scope.json` is live: the manifest's `session_id`/`semantic_session_id` provably name another session (legacy `session`/`semantic_session` still read, #1153 P2), so the hook stands down instead of enforcing. **Payload:** `hook`, `manifest` (path), `manifest_session` (string[]), `own_session` (string[]), `wave`; additionally `file_path` from `enforce-scope` (PreToolUse Edit/Write) and `command` from `enforce-commands` (PreToolUse Bash). `post-bash-write-verify` (PostToolUse Bash) carries neither — it reports on the working tree, not on one tool input. Legacy manifests without a session-binding field never emit this — they stay enforced |
107
+ | `orchestrator.scope.unbound_manifest` | `scripts/wave-scope-binding.mjs` (#1153 P4) | wave-executor § Scope Manifest, when the binding step resolves to `{}` — `attributionForRecord()` found no `.orchestrator/session.lock`, or the lock's `session_id` did not match this process's own identity, so the manifest about to be written names NOBODY. **Payload:** `wave` (number\|string\|null), `role` (string\|null), `reason` (currently only `no-confirmed-session-attribution`). Exactly one per invocation, and only on the unbound path — a bound binding emits nothing. An unbound manifest is the FAIL-CLOSED direction (it enforces against every session in the checkout) and is therefore otherwise silent; this event is what makes it countable instead of indistinguishable from a coordinator who skipped the step |
106
108
  | `orchestrator.grounding.injected` | `scripts/compute-grounding-injection.sh` (via `scripts/emit-event.mjs`) | grounding injection, when `PERSISTENCE=true` |
107
109
  | `orchestrator.handover.gated` | `skills/session-end/SKILL.md` Phase 1.65 (skill-prose, via `scripts/emit-event.mjs`) | Handover-Alignment-Gate outcome (#773). Payload: `candidates_total`, `auto_carry`, `asked`, `dropped`, `questions_asked`, `questions_answered`, `questions_deferred`, `path` (`fast_path`\|`triage`\|`weiterarbeiten`\|`fail_open`). Emitted exactly once per close — including the fail-open skip and the "Weiterarbeiten" abort — so never-measured paths become observable |
108
110
  | `orchestrator.vault.board_written` | `scripts/lib/vault-status/board-writer.mjs` (`emitBoardEvent`; name const `BOARD_EVENT`) | exactly ONE record per `mirrorBoard()` call, and therefore per `sweepBoard()` call — the sweep never double-emits. Call sites: the `mirrorBoard` wrapper through which all six inner return points funnel, and `sweepBoard`'s two paths (happy + enumeration-failure fallback). **Payload:** `action` (always — including every no-op: the five `skipped-vault-disabled` guards at `:803/:812/:817/:821/:829`, plus `skipped-handwritten`, `skipped-noop`, `skipped-write-failed`, `dry-run`, `written`), `caller` (always, `mirrorBoard`\|`sweepBoard`). **Optional, absent-is-not-zero:** `path_tail` (the BASENAME only — never the full path: under `01-projects/` the parent directory is the private project slug, and this payload also travels over the optional Clank webhook with no redaction), `rows`, `repos_swept`, `duration_ms`, plus `session_id` / `semantic_session_id` via `sessionAttribution(repoRoot)` (#1147 — the SAME root the record is pinned to, so attribution can never name a different tree than the ledger line; both keys omitted, never fabricated, when no `session.lock` is readable) — the numeric three admitted via `Number.isFinite(...)` / `typeof === 'string'`, NOT truthiness, so a measured `repos_swept: 0` survives while an unmeasured field is omitted. `lock` — an additive diagnostic object, `{ locked: boolean, reason?: string, stale_override?: string, waited_ms: number }` (snake_case like the sibling keys) — is present whenever `withBoardLock()`'s `onLockOutcome` fired, i.e. every non-dry-run path; `stale_override` carries the file-lock reason TOKEN (e.g. `mtime age 600002ms > 60000ms`) only when a stale lock was force-overridden, and the key is entirely absent on `dryRun`, which never takes the lock. This makes an unlocked fail-open write and a stale-override observable in aggregate for the first time — until now `onLockOutcome` had no production caller at all. The enumeration-failure fallback deliberately omits `repos_swept`. **A throw from the inner function emits nothing** — `action` is mandatory and a throw has no action the code knows; inventing one would put a fictional state in the ledger (#1073) |
109
111
  | `orchestrator.vault.narrative_mirrored` | `scripts/lib/vault-status/narrative-mirror.mjs` (`emitNarrativeEvent`; name const `NARRATIVE_EVENT`) | one record per `mirrorNarrative()` call, from the thin wrapper — every outcome plus the throw path (`action: 'error'`, then re-throws). The old body became `runNarrativeMirror()`, so an early return added later is telemetered by construction. Covered: `skipped-vault-disabled` ×4, `skipped-invalid-path`, `skipped-no-statemd`, `written`, `skipped-noop`, `skipped-handwritten`, `dry-run`. **Payload:** `action` (always); optional `path_tail` (BASENAME only, same reason as `board_written`), `chars`, `session_id`, `semantic_session_id` (via `sessionAttribution`), `error_code` (throw path only — the error MESSAGE is deliberately not recorded, it can quote a path or STATE.md prose). **Named gap with a revisit trigger:** when `repoRoot` is absent, NOTHING is emitted — `emitEvent` would fall back to `SO_PROJECT_DIR` and the two rootless unit tests would append synthetic records to this repo's real ledger on every suite run. This deliberately diverges from `board-writer.mjs`, which emits there (#1073) |
110
112
  | `orchestrator.vault.mirror_completed` | `scripts/lib/vault-mirror/telemetry.mjs` (`emitMirrorEvent`; name const `MIRROR_EVENT`), called from `scripts/lib/vault-mirror/process.mjs` (`emitAction`, reached from all **18** of its call sites — census `grep -n 'emitEntryAction' scripts/lib/vault-mirror/process.mjs`, 2026-08-23) and from the two `skipped-invalid` branches in `scripts/vault-mirror.mjs` | **ONE record per JSONL entry processed, EXCEPT `skipped-noop`** (#1151: noop dominates a steady-state run; its count survives in the run-event's `skipped` total + `action_breakdown`) — `created`, `updated`, every other `skipped-*`, and both invalid paths. Until #1147 it was **failure-only**: only the two `skipped-invalid` branches emitted, which is why this repo's ledger held **0** records of it against 1272 `orchestrator.secret_masker.applied` from the same CLI (measured 2026-08-23). A healthy run was therefore indistinguishable from a broken emitter — the gap the sibling `orchestrator.vault.mirror_run_completed` row below closes. **Payload:** `action` (the SAME string the entry wrote to stdout), `kind`, `line` (1-based JSONL line — the only locator when a record has no id). **Optional, absent-is-not-zero:** `record_id` (the record's `id` / `session_id`), `path` (**vault-RELATIVE**, never absolute: this payload also travels over the optional Clank webhook with no redaction — omitted on `skipped-invalid` and on the pre-path quality skips, which are reached before a target path exists), `skip_class` (`validation` | `mapper-crash`, invalid branches only — mirrors the stdout `reason` verbatim so the failure class stays groupable without string-matching), `reason` (the renderer's message on the invalid branches, or the existing `meta.reason` string on a quality skip — `confidence:X < min:Y` / `narrative:N < min:M` / `status:…` — REUSED from the stdout payload rather than recomputed; clamped to 300 chars), `dry_run`, `session_id` / `semantic_session_id`. `record_id` / `path` / `skip_class` / `reason` treat **`null` as not-measured** and are omitted: a `record_id: null` would read as "measured, empty id" rather than "this record had none". **Attribution is read at `SO_PROJECT_DIR`, explicitly** — `readLock()` defaults to `process.cwd()`, so a bare `sessionAttribution()` would attribute the record to whatever tree the process happens to run in while the ledger line lands under `CLAUDE_PROJECT_DIR`. Same root for both halves or neither. The ledger destination stays the 2-arg `emitEvent` default so every event of one run shares it: this CLI has no repo-root flag, and deriving one from `--source` would split a single run's telemetry across two ledgers |
111
113
  | `orchestrator.vault.mirror_run_completed` | `scripts/lib/vault-mirror/telemetry.mjs` (`emitMirrorRunEvent`; name const `MIRROR_RUN_EVENT`), called from `finishRun()` in `scripts/vault-mirror.mjs` — the ONE close-out function every exit routes through (the happy tail, the malformed-JSON abort, the filesystem-error abort, and the top-level `main().catch`), latched so it can only fire once | exactly ONE record per CLI run, **unconditionally** — beside the `orchestrator.secret_masker.applied` emit and BEFORE the `--strict-schema` abort, so a failing run still reports its denominator. **This event is the denominator the per-entry row above lacks:** a healthy run over an empty source emits zero per-entry records, and so does a run whose emitter is broken — from the ledger the two are identical (`.claude/rules/host-resources.md` § HR-105). **Payload:** `kind`, `total` (non-blank JSONL entries attempted), `created`, `updated`, `skipped` (every non-failure `skipped-*` class), `failed` (`skipped-invalid` — validation error or mapper crash; split out because those are the entries whose session silently ends up WITHOUT a vault note), `dry_run`. **These five counters are ALWAYS present, including as `0`** — this is the one place a written zero is the payload rather than a violation of "absent is not zero", because each was measured over the whole run; `total: 0` is a measured empty run and the record's ABSENCE is the broken-emitter signal. `created + updated + skipped + failed === total` for any run that does not abort (pinned by a test). **Optional, and the discriminator that keeps the counters honest:** `aborted` (`malformed-json` | `filesystem-error` | `unexpected-error` | `missing-vault-dir` | `vault-not-canonical` | `missing-source` (#1151: the three pre-loop exit-2 aborts now close the run out through finishRun())) — present ONLY when the run exited before its tail, absent means "ran to the end" and never "unknown". Its presence says the five counters are PARTIAL (every line after the abort was never attempted), so the classes stop partitioning `total` and that gap must be read as an abort, not as producer/consumer drift. Until it existed, the two `process.exit` calls inside the entry loop and the `main().catch` jumped straight over this emit: the runs an operator most wants counted were the ones that vanished from the ledger, in the exact shape ("no record") the paragraph above reserves for a broken emitter. **Optional:** `action_breakdown` (per-`action` counts, keyed by the same strings the entries wrote to stdout — enumerates only actions that OCCURRED, so a missing key there means zero occurrences; the always-present `total` makes that reading unambiguous, and the key itself is omitted when nothing was processed), plus `session_id` / `semantic_session_id` via the same `SO_PROJECT_DIR`-pinned `sessionAttribution` as the per-entry event |
112
- | `orchestrator.secret_masker.applied` | `scripts/vault-mirror.mjs` (beside the vault-mirror run) | once per vault-mirror run, unconditionally. Payload carries `channel: 'vault-mirror'`. **Undocumented until 2026-08-23, at which point it had over a thousand records** — and that omission actively misled: a census grepping event NAMES for `board\|mirror` returns 0 and reads as "the mirror emits nothing", while run-level presence was in fact already observable through THIS event's payload. Grep the `channel`, not the name |
114
+ | `orchestrator.secret_masker.applied` | Three producers, one per masking call site: `scripts/vault-mirror.mjs` (beside the vault-mirror run, `channel: 'vault-mirror'`), `scripts/lib/vault-status/narrative-mirror.mjs` (`channel: 'narrative-mirror'`), `scripts/export-hw-learnings.mjs` (`channel: 'export-hw-learnings'`) | once per masking pass at each producer, unconditionally. **Undocumented until 2026-08-23, at which point it had over a thousand records** — and that omission actively misled: a census grepping event NAMES for `board\|mirror` returns 0 and reads as "the mirror emits nothing", while run-level presence was in fact already observable through THIS event's payload. Grep the `channel`, not the name — the three channels above are DISTINCT producers, not one call site with three labels, so a per-channel count is a per-producer count |
113
115
  | `orchestrator.probes.completed` | `scripts/lib/session-start-probes.mjs` (`runSessionStartProbes`), called from `hooks/on-session-start.mjs` | once per SessionStart, after the Phase-4 measurement probes run. **This event is the whole point of #1073:** the 18 module-backed probes had **zero** mechanical callers across `hooks/`, npm scripts, CI and husky — their only caller was prose in `skills/session-start/SKILL.md` — and across the 336 session starts recorded up to 2026-08-23 there was **no banner event at all**, so whether they ever ran was unfalsifiable (`.claude/rules/host-resources.md` § HR-105). **Payload:** `total`, `ran`, `warned`, `skipped`, `errored`, `timed_out`, `duration_ms`, and `probes` — one `{id, outcome, reason?}` per probe — `reason` travels whenever one was recorded, because `module-absent` (a permanently dead entry) must be distinguishable from `network-probe-opt-in` (the intended default); `outcome` ∈ `ran-clean`\|`ran-warn`\|`ran-alert`\|`skipped`\|`timeout`\|`error`. Two invariants are asserted by tests: `total === probes.length` and `ran + skipped + errored + timed_out === total`. **The count is 18, not the 19 Phase 4 appears to list:** four Phase-4 items are prose-only measurements with no module and no entry function (SSOT freshness, quality baseline, Pencil design status, plugin freshness) — 22 measurements, 18 wireable probes. **Network probes (`ci-status`, `mirror-issues`) are excluded by default** and appear as `outcome: 'skipped', reason: 'network-probe-opt-in'` — never omitted, because omitting them would rebuild the defect one layer down. Opt in with `SO_PROBES_INCLUDE_NETWORK=1`. The grounds are measured, not assumed: `hooks/hooks.json` gives the WHOLE SessionStart hook `timeout: 5` seconds while each network probe carries its own 8 s CLI timeout, so one slow network probe alone exceeds the hook's entire budget and takes the started-event and the banner down with it; warm-and-authenticated best case measured 520 ms / 498 ms, paid on every start of every repo. **Budget:** `PROBE_BUDGET_MS = 2000`; measured median against this repo **968 ms** (5 runs, 855–1104), 130–229 ms in a fresh tmp repo. Revisit trigger: median past HALF the budget, or any single probe past the budget → move the slow probes off the hook's critical path, do NOT raise the number. **Named ceiling:** the deadline is enforced at await points, so it is hard for async/network probes and advisory for the two that shell out synchronously (`project-hygiene`, `tests-src-ratio`). Escape hatch: `SO_DISABLE_STARTUP_PROBES=1`. **Deliberately NOT gated on `enable-host-banner: false`** — that preference governs DISPLAY; gating the RUN on it would rebuild exactly the unfalsifiable blind spot this event removes |
114
116
  | `orchestrator.express_path.evaluated` | `scripts/lib/express-path.mjs` (`evaluateExpressPath`, emit in `_emitEvaluated`; name const `EXPRESS_PATH_EVENT`) | once per Phase-8.5 evaluation — **on refusal as well as activation**. Until #1119 this was unrecordable twice over: `scripts/lib/config.mjs` discarded the `express-path` key **even when the block was present** (synthetic probe: 88 keys emitted, none of them this one), and the decision lived only in `skills/session-start/phase-8-5-express-path.md` prose, so it fired only when a coordinator read that prose. Ledger evidence, measured 2026-08-23 @ `34321bc` (a count, so read it as history, not as state): **0** express events at that point, against 22 of the last 30 sessions running with no wave at all — every one of them `housekeeping`, the exact population the path targets. **Payload:** `activated` (always, boolean), `reasons` (always — the BLOCKING codes on refusal, the satisfied ones on activation; nothing short-circuits, so a refusal names every blocker and a reader can tell whether trimming the issue list alone would have helped). **Optional, absent-is-not-zero:** `enabled`, `session_type`, `task_count`, `parallel_agents_required`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. An unmeasured `sessionType` or `taskCount` fails CLOSED (`reasons: ['session-type-unknown','task-count-unknown']`) — defaulting unknown scope to 0 would activate a gate-skipping path on data nobody supplied. **Four inputs, not three:** activation condition 3 carries two clauses (`≤ 3 issues` AND no parallel agents), which both condition matrices list as a non-activating row. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` — that is the wave-1 incident of this session (a probe with an unexported var wrote a synthetic record into the real fleet ledger) made structurally impossible; a regression test reproduces it. `events.mjs` is imported lazily so `config.mjs`'s 48-file import graph does not gain `platform.mjs`, which runs filesystem walk-ups at module load |
115
117
  | `orchestrator.foreign_dispatch.completed` | `scripts/lib/wave-executor/foreign-dispatch.mjs` (`dispatchForeign`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | once per foreign-model dispatch (#1150) — the replacement for `SubagentStop` telemetry, which cannot fire for a Bash-spawned `cursor-agent` child (no hook in the chain sees it). **Payload:** `model`, `role`, `ok`, `exit_code`, `timed_out`, `duration_s`, `changed_files` (count, tracked-modified ∪ untracked-new — `git diff` alone is blind to new files), `reason` (present on every refusal — `never-foreign-role`, `empty-diff`, `channel-unavailable`, `unsafe-*` — and on the failure classes of a completed run, so no failure class is reasonless), `hook_tampering` (tri-state: `true` = the child repointed/rewrote the shared `.git` hooks path, invalidates the run regardless of `ok`; `false` = fingerprint matched; absent/`null` = not measured, never read as clean), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). Emitted on refusals too (`ok:false`), so a blocked dispatch is a record, not a silence |
116
118
  | `orchestrator.remote_dispatch.completed` | `scripts/lib/wave-executor/remote-dispatch.mjs` (`dispatchRemote`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`; name const `REMOTE_DISPATCH_EVENT`) | once per REMOTE-host dispatch over the `offload` CLI (#1160) — the sibling of `foreign_dispatch.completed` on the other channel: that one sends a task to a foreign MODEL on this machine, this one sends a task to Claude on ANOTHER machine. Same reason for existing — a Bash-spawned `offload` child fires no `SubagentStop` hook, so this is the only ledger record a remote dispatch produces. **Payload:** `host` (the `offload` alias, never a hostname or an IP), `role`, `run_id`, `ok`, `exit_code`, `duration_ms`, `patch_files` (COUNT of paths parsed from the returned patch — `+++ b/` plus the `diff --git` header, because a DELETED file's `+++` is `/dev/null`), `patch_bytes`, `reason` (present on every refusal — `never-foreign-role`, `unsafe-run-id`, `unsafe-host`, `unsafe-patch-path` — and on every failure class of a completed run: `usage-config`, `host-unreachable`, `remote-command-failed`, `sync-failed`, `timeout`, `empty-diff`, `rate-limited`, `write-lock-busy`, `channel-unavailable`; absent means success, so no failure class is reasonless), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). **Emitted on refusals too** (`ok:false`, `exit_code: null`, `duration_ms: 0`, `patch_files: 0`) — a blocked dispatch is a record, not a silence, and the null exit code is what keeps "refused" distinguishable from "attempted and measured empty". **Deliberately EXCLUDED, pinned by a test:** the prompt text, the patch BODY, and `patch_path` — this payload also travels over the optional Clank webhook with no redaction, and a tmp patch path names the run id and the operator's host |
117
119
  | `orchestrator.wave_dispatch.scope_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `SCOPE_EVENT`; built by `decide()` as `verdict.telemetry`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`) | PreToolUse `Agent` — **once per dispatch DECISION** (#1092), awaited BEFORE the terminal `emitAllow`/`emitDeny`/`emitWarn`, all of which `process.exit()` and would discard a pending append. **Payload:** `hook`, `agent_id` (the coordinator's `description` + `subagent_type`, clamped to 120 chars), `declared_path_count`, `injected` (a `FILE-SCOPE` declaration was found AND at least one path survived parsing), `shape` (`fenced` \| `inline` \| `none` — which PARSER won, deliberately not a second spelling of `signal`: a fenced block whose lines are prose is `signal: 'unparseable', shape: 'none'`), `signal` (`marker-absent` \| `unparseable` \| `extracted` — the row-5-vs-row-6 distinction of the hook's error-class matrix), `ledger_result` (`no-scope` \| `allow` \| `allow-finished` \| `deny` \| `warn-ledger-corrupt` \| `warn-not-evaluable`), `collision_count` (collisions involving THIS dispatch, live or already-finished — `ledger_result` says which). **Optional, absent-is-not-zero:** `wave` (the number out of `waveKeyOf()`'s `w<N>` segment — **omitted, never `0`**, under the `<session>|w?|?` fallback, same contract as `quality_gate`'s `wave_number`), plus `session_id` / `semantic_session_id` via `sessionAttribution` — omitted, never fabricated, without a readable `session.lock`. **What it proves and what it does not:** that the hook SAW (or did not see) a declaration in the prompt the coordinator handed to the dispatch tool, and what the guard decided — the SEND side. It proves nothing about the block reaching the agent's context or the agent reading it; that receive-side half of #1092 stays open for want of a platform prompt-assembly boundary (`docs/scope-collision-guard.md` § 4.2). **No prompt body and no declared path is in the payload** (issue #1092 acceptance criterion 3) — counts and closed enums only, because this record also travels over the optional Clank webhook with no redaction. Rows 1–4 of the matrix emit nothing (no decision was made) and neither do the two crash rows 2/12 — a hook that fell over cannot describe itself, which is what the `GUARD INACTIVE` stderr banner is for |
120
+ | `orchestrator.hook.import_probe_failed` | `hooks/post-edit-import-probe.mjs` | PostToolUse(Edit\|Write\|MultiEdit), after a module listed in `hooks/_lib/hook-import-set.json` (the committed hook-reachable allowlist) fails the probe. **Payload:** `file` (repo-relative), `check` (`eslint` \| `import` — which of the two checks caught it), `error` (the first offending message: a `no-undef`/fatal ESLint message, or the import diagnostic line), `reachable_from` (the hook entry basenames that import this module — the blast radius, since a throwing helper turns every tool call into "Internal hook error — request blocked" host-wide, #1224), `duration_ms`. Emitted ONLY on failure; a clean edit produces no record, so the event count IS the incident count |
118
121
  | `orchestrator.reconcile.completed` | `scripts/lib/reconcile/engine.mjs` (`emitReconcileCompleted`, called from the thin `runReconcile` wrapper; name const `RECONCILE_EVENT`) | one record per `runReconcile` call (#1192) — from the WRAPPER, so all three return points are covered: the empty short-circuit, the normal tail, and the never-throws catch. An inline emit would have missed two of them, including the empty corpus and the error path — the two runs an operator most needs recorded (`.claude/rules/host-resources.md` § HR-105). Same shape as `narrative_mirrored`'s wrapper, and the emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which would otherwise break `runReconcile`'s never-throws contract. **Payload, all ALWAYS present including as `0`** (each was measured over the whole run, like `mirror_run_completed`'s counters): `trigger` (`skill` | `session-end` | `phase-skip` | `unknown` — written always, so the per-trigger denominator is complete; the two markdown callers depend on a coordinator passing it, hence the honest `unknown` default), `dry_run`, `learnings_total`, `eligible`, `proposals`, `rejected`, `capped`, `already_materialized` (`summary.alreadyMaterialized` — the REAL idempotent-skip count, #484), `written`, `duration_ms`. **Optional, absent-is-not-zero:** `targets` (the caller's effective target list; absent ⇒ none asserted), `store_records_dropped` (`summary.skipped` — absent ⇒ the candidate store was never INSPECTED: under `dryRun`, on the empty short-circuit, on the error path; a `0` there would be a false all-clear), and `aborted: 'engine-error'` + `reason` (clamped to 300 chars) — present ONLY when the never-throws guard fired; their absence means "ran to the end", never "unknown". **`dry_run` is the discriminator, not the event's absence:** the `phase-skip` caller runs dry on EVERY close and is the highest-volume trigger, so consumers filter `dry_run: false` for real runs. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling back to `SO_PROJECT_DIR` — most engine tests pass none, and the fallback would append synthetic records to the real fleet ledger on every `npm test` (#1119, same contract as `express_path.evaluated` and `narrative_mirrored`); a regression test pins it. **Engine identities — the counters are NOT a flat partition, and a live payload reads as inconsistent without them** (source: `scripts/lib/reconcile/engine.mjs:64-83`): `learnings_total === proposals + rejected`, and `capped` + `already_materialized` are DIAGNOSTIC SUB-COUNTS *inside* `rejected`, not siblings of it — each capped or already-materialized learning is also counted as rejected. Within the eligible set: `eligible − proposals − capped === already_materialized`. Worked against a live record: `learnings_total 164 = proposals 10 + rejected 154`, and `eligible 102 − proposals 10 − capped 72 = already_materialized 20` — with `capped 72` and `already_materialized 20` both sitting inside those 154. `already_materialized` is computed BEFORE the volume brake, so a terminal learning never consumes a new learning's quota. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
119
- | `orchestrator.evolve.completed` | `skills/evolve/SKILL.md` Phase 1 (persistence/no-session-data aborts) and Phase 3 Step 3.5 (skill-prose, via `scripts/emit-event.mjs`) | once per `/evolve analyze` run (default mode; #1200) — emitted immediately after Step 3.5's "Saved N new learnings, updated M existing. Total active: K." report, and on the two hard-abort paths that precede it (persistence disabled at Phase 1.2; no session data at Step 3.1). Until this addition `/evolve` reported completion in prose only — the whole class of `orchestrator.evolve.*` / `orchestrator.dialectic.*` events was **0 records across 164k fleet events** despite every run reporting success (#1200). **Payload (success), all FOUR counters ALWAYS present including as `0`** (same contract as `mirror_run_completed`'s counters): `appended` (new learnings written, Step 3.5(4)), `boosted` (existing learnings reinforced, Step 3.5(2)), `pruned` (`$PRUNE.archived` from the Step 3.5(5) `sweep-expired-learnings.mjs --prune` call total entries archived this run, across every `_archive_reason`), `promoted` (always `0` from THIS call site — promotion to `public` scope is a separate CLI, `npm run share:hw-learnings -- --promote`, never invoked by `/evolve analyze` itself), `duration_ms`. **Payload (abort):** `aborted` (`persistence-disabled` \| `no-session-data`), `reason` (the abort message shown to the user, clamped to 300 chars), `duration_ms` |
120
- | `orchestrator.dialectic.completed` | `skills/evolve/SKILL.md` Phase 6 Step 6.4 / Step 6.5 (skill-prose, via `scripts/emit-event.mjs`) | once per `/evolve --dialectic` run (#1200)emitted after Step 6.4's "Dialectic-derived: M deltas to USER.md, N deltas to AGENT.md…" report on both the dry-run (default) and `--apply` paths, and once per Step 6.5 error outcome (a dialectic run reaching Step 6.5 never also reaches Step 6.4, so exactly one of the two payload shapes below is emitted per run). Same fleet-zero gap as `evolve.completed` above. **Payload (success):** `mode` (`dry-run` \| `apply`), `user_deltas`, `agent_deltas`, `tokens_in`, `tokens_out`, `duration_ms`. **Payload (abort):** `aborted` (`unknown-model` \| `budget-exceeded` \| `would-empty-card` \| `empty-input` \| `subagent-crash`), `duration_ms` |
122
+ | `orchestrator.evolve.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`emitEvolveCompleted`), called from `scripts/sweep-expired-learnings.mjs`'s `--prune --apply` exit path (#1206) for the success form, and from `skills/evolve/SKILL.md` Phase 1 (persistence/no-session-data aborts, skill-prose via `scripts/emit-event.mjs` — no mechanical pipeline call site precedes either gate) for the two abort forms | once per `/evolve analyze` run (default mode; #1200, mechanized #1206) — the success form is now the SAME command that performs the Step 3.5(5) store write, so the event can no longer be forgotten independently of the write it reports on (previously a separate `emit-event.mjs` call in skill prose, one edit away from drifting out of sync). Until #1200 `/evolve` reported completion in prose only — the whole class of `orchestrator.evolve.*` / `orchestrator.dialectic.*` events was **0 records across 164k fleet events** despite every run reporting success. **Payload (success), all FOUR counters ALWAYS present including as `0`** (same contract as `mirror_run_completed`'s counters): `appended` (new learnings written, Step 3.5(4)), `boosted` (existing learnings reinforced, Step 3.5(2)), `pruned` (`$PRUNE.archived` this SAME call's own returned `archived` total, across every `_archive_reason`), `promoted` (always `0` from THIS call site — promotion to `public` scope is a separate CLI, `npm run share:hw-learnings -- --promote`, never invoked by `/evolve analyze` itself), `duration_ms`. **Optional, absent-is-not-zero:** `skipped` (HR-105 — an array of optional-step slugs, e.g. `skill-evolution-off` \| `vault-mirror-off`, that RAN but were themselves skipped this run; present only when non-empty, and distinct from the `aborted` form below — "ran, a step inside it skipped" is not "did not run at all"). **Payload (abort):** `aborted` (`persistence-disabled` \| `no-session-data`), `reason` (the abort message shown to the user, clamped to 300 chars), `duration_ms`. A `--prune --dry-run` preview run emits NOTHING — a preview never wrote anything, so it must not report a completed run either; `emitEvolveCompleted()` also refuses to emit (stderr WARN, never a throw) without an explicit `repoRoot`, same #1119 fail-closed contract as `emitReconcileCompleted` |
123
+ | `orchestrator.dialectic.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`recordDialecticRun`), called from `scripts/dialectic-deriver.mjs`'s `runDialecticDeriver()` for its FOUR return-based outcomes (`empty-input`, `budget-exceeded`, `would-empty-card`, and the dry-run `ok` success formall four are values the function itself returns, so it records each one mechanically at the exact return point), and from `skills/evolve/SKILL.md` Step 6.4's apply branch / Step 6.5's two THROWN outcomes (`unknown-model` from `validateModel()`, `subagent-crash` from a failed `dispatchAgent`/`Agent()` call neither is a `runDialecticDeriver()` return value, so only the skill-prose catcher one layer up can record them; apply-mode success similarly needs the post-merge `mergePeerCard()` stats `runDialecticDeriver()` does not have) | once per `/evolve --dialectic` run (#1200, mechanized #1206). Same fleet-zero gap as `evolve.completed` above, now closed the same way: the pipeline function records itself for every outcome it alone can determine, and only the two throw-based aborts (of Step 6.5's five-slug abort enum) plus apply-mode's merge-dependent success remain a skill-prose call site. **Payload (success):** `mode` (`dry-run` \| `apply`), `user_deltas`, `agent_deltas` (dry-run: `countManagedSections()` on the proposed diff text — a pure helper counting `<!-- BEGIN MANAGED: … -->` sentinels, or 1 for a bare full-body replacement with none; apply: `mergePeerCard()`'s own `stats.replaced + stats.appended` per target), `tokens_in`, `tokens_out`, `duration_ms`. **Payload (abort):** `aborted` (`unknown-model` \| `budget-exceeded` \| `would-empty-card` \| `empty-input` \| `subagent-crash`), `duration_ms`. Same #1119 refusal as `emitEvolveCompleted` — `recordDialecticRun()` skips the emit (stderr WARN) without an explicit `repoRoot` |
121
124
  | `orchestrator.dialectic.nudge_decided` | `scripts/lib/auto-dialectic.mjs` (`decideAndRecordAutoDialectic`, wrapping `shouldDispatchAutoDialectic` with an additive `emitEvent` call) | once per `decideAndRecordAutoDialectic()` call made with `record !== false` (#1200 part c) — the MECHANICAL sibling of the two skill-prose events above: those two cover a manual `/evolve` run, this one covers the session-end Phase 3.6.7 auto-dialectic NUDGE DECISION itself, which previously computed a trigger every close and recorded nothing. Emitted on ALL FOUR `shouldDispatchAutoDialectic()` return paths (kill-switch, no-new-input, cadence-threshold-met, under-threshold) — try/catch-wrapped, because `emitEvent()` throws `EventValidationError` on a malformed record and a telemetry failure must never change the decision the caller already has in hand. **Payload:** `decided` (boolean, mirrors `trigger` verbatim), `reason`, `cadence`, `sessions_since`, `learnings_since`, plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). **Deliberately never emitted** when the caller passes `record: false` — the read-only PROBE call site (`scripts/lib/session-end/phase-skip.mjs`'s Phase-3.6.x tail-skip aggregator) keeps calling the un-wrapped `shouldDispatchAutoDialectic()` directly instead, so its own documented side-effect-free contract ("reads only … writes NOTHING") is not broken by this event's write |
122
125
 
123
126
  Non-orchestrator names still present in the stream: `tmux-layout.{invoked,completed,degraded}`