session-orchestrator 3.22.0 → 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  74. package/.cursor/skills/session-end/SKILL.md +13 -0
  75. package/.cursor/skills/session-plan/SKILL.md +13 -0
  76. package/.cursor/skills/session-start/SKILL.md +13 -0
  77. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  78. package/.cursor/skills/spinout/SKILL.md +12 -0
  79. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  80. package/.cursor/skills/test-runner/SKILL.md +13 -0
  81. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  82. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  83. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  84. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  85. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  86. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  87. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  88. package/.mcp.json +4 -1
  89. package/CHANGELOG.md +168 -0
  90. package/README.md +18 -15
  91. package/agents/AGENTS.md +23 -4
  92. package/agents/code-implementer.md +2 -1
  93. package/agents/db-specialist.md +2 -1
  94. package/agents/docs-writer.md +3 -1
  95. package/agents/eval-judge.md +1 -1
  96. package/agents/session-reviewer.md +7 -1
  97. package/agents/test-writer.md +2 -1
  98. package/agents/ui-developer.md +2 -1
  99. package/commands/bootstrap.md +2 -2
  100. package/commands/close.md +3 -1
  101. package/commands/go.md +1 -1
  102. package/commands/journey-audit.md +43 -0
  103. package/docs/USER-GUIDE.md +2 -2
  104. package/docs/ci-setup.md +14 -0
  105. package/docs/codex-setup.md +64 -0
  106. package/docs/components.md +6 -6
  107. package/docs/cursor-setup.md +26 -47
  108. package/docs/events-schema.md +76 -4
  109. package/docs/github-mirror-protection.md +197 -0
  110. package/docs/pi-setup.md +2 -0
  111. package/docs/rule-authoring.md +3 -1
  112. package/docs/scope-collision-guard.md +49 -2
  113. package/docs/session-config-reference.md +26 -4
  114. package/docs/session-config-template.md +4 -3
  115. package/docs/telemetry.md +22 -0
  116. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  117. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  118. package/hooks/enforce-scope.mjs +64 -0
  119. package/hooks/hooks-codex.json +1 -1
  120. package/hooks/hooks-cursor.json +201 -20
  121. package/hooks/hooks-pi.json +1 -1
  122. package/hooks/hooks.json +2 -2
  123. package/hooks/on-session-end.mjs +211 -10
  124. package/hooks/on-session-start.mjs +214 -11
  125. package/hooks/on-stop.mjs +48 -9
  126. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  127. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  128. package/hooks/pre-bash-issue-budget.mjs +117 -4
  129. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  130. package/hooks/pre-bash-staging-fence.mjs +4 -0
  131. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  132. package/hooks/skill-invocation-telemetry.mjs +21 -10
  133. package/monitors/monitors.json +6 -0
  134. package/package.json +1 -1
  135. package/pi/prompts/journey-audit.md +12 -0
  136. package/rules/_index.md +9 -1
  137. package/rules/always-on/ask-via-tool.md +62 -0
  138. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  139. package/rules/always-on/build-value.md +47 -0
  140. package/rules/always-on/cross-session-messaging.md +59 -0
  141. package/rules/always-on/loop-and-monitor.md +221 -0
  142. package/rules/always-on/parallel-sessions.md +142 -12
  143. package/rules/always-on/receiving-review.md +108 -0
  144. package/rules/always-on/test-value.md +40 -0
  145. package/rules/always-on/verification-before-completion.md +77 -0
  146. package/scripts/archive-closed-prds.mjs +258 -18
  147. package/scripts/autopilot.mjs +5 -0
  148. package/scripts/backfill-evidence-digest.mjs +376 -0
  149. package/scripts/cursor-install.mjs +89 -48
  150. package/scripts/export-hw-learnings.mjs +143 -2
  151. package/scripts/express-path.mjs +299 -0
  152. package/scripts/generate-cursor-adapter.mjs +253 -0
  153. package/scripts/github-protection-audit.mjs +358 -0
  154. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  155. package/scripts/lib/build-live-signals.mjs +24 -5
  156. package/scripts/lib/ci-status-banner.mjs +158 -11
  157. package/scripts/lib/command-blocker.mjs +70 -0
  158. package/scripts/lib/config/reconcile.mjs +79 -4
  159. package/scripts/lib/config/section-extractor.mjs +235 -36
  160. package/scripts/lib/config-schema.mjs +9 -1
  161. package/scripts/lib/config.mjs +57 -6
  162. package/scripts/lib/convergence-monitor.mjs +13 -2
  163. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  164. package/scripts/lib/dispatcher/cli.mjs +2 -2
  165. package/scripts/lib/express-path.mjs +327 -0
  166. package/scripts/lib/file-lock.mjs +22 -4
  167. package/scripts/lib/gates/gate-full.mjs +81 -8
  168. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  169. package/scripts/lib/git-config-drift.mjs +134 -5
  170. package/scripts/lib/host-identity.mjs +247 -2
  171. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  172. package/scripts/lib/issue-budget.mjs +229 -30
  173. package/scripts/lib/learnings/io.mjs +55 -10
  174. package/scripts/lib/learnings/schema.mjs +95 -28
  175. package/scripts/lib/lock-reaper.mjs +7 -1
  176. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  177. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  178. package/scripts/lib/memory-banner.mjs +5 -2
  179. package/scripts/lib/memory-paths.mjs +15 -6
  180. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  181. package/scripts/lib/platform.mjs +72 -9
  182. package/scripts/lib/plugin-root.mjs +143 -19
  183. package/scripts/lib/project-hygiene.mjs +43 -3
  184. package/scripts/lib/quality-gate.mjs +271 -13
  185. package/scripts/lib/reconcile/emitter.mjs +87 -19
  186. package/scripts/lib/reconcile/engine.mjs +281 -13
  187. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  188. package/scripts/lib/reconcile/renderer.mjs +148 -3
  189. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  190. package/scripts/lib/reconcile/writer.mjs +415 -84
  191. package/scripts/lib/rule-loader.mjs +37 -2
  192. package/scripts/lib/rules-sync.mjs +51 -8
  193. package/scripts/lib/scope-gate.mjs +90 -0
  194. package/scripts/lib/session-close-backfill.mjs +369 -28
  195. package/scripts/lib/session-discovery.mjs +13 -3
  196. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  197. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  198. package/scripts/lib/session-id.mjs +30 -14
  199. package/scripts/lib/session-identity/own-session.mjs +159 -0
  200. package/scripts/lib/session-lock.mjs +85 -30
  201. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  202. package/scripts/lib/session-schema/validator.mjs +40 -0
  203. package/scripts/lib/session-start-probes.mjs +608 -0
  204. package/scripts/lib/session-transition.mjs +277 -0
  205. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  206. package/scripts/lib/spiral-carryover.mjs +90 -9
  207. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  208. package/scripts/lib/state-md/mission-status.mjs +350 -52
  209. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  210. package/scripts/lib/state-md.mjs +12 -2
  211. package/scripts/lib/telemetry/sync.mjs +46 -8
  212. package/scripts/lib/validate/check-agents.mjs +66 -0
  213. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  214. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  215. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  216. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  217. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  218. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  219. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  220. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  221. package/scripts/lib/validate/repo-files.mjs +275 -0
  222. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  223. package/scripts/lib/vault-mirror/process.mjs +99 -43
  224. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  225. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  226. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  227. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  228. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  229. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  230. package/scripts/materialize-wave-scope.mjs +209 -12
  231. package/scripts/mcp-server.sh +11 -2
  232. package/scripts/parse-config.mjs +65 -0
  233. package/scripts/token-audit.sh +9 -2
  234. package/scripts/validate-plugin.mjs +3 -0
  235. package/scripts/validate-wave-scope.mjs +67 -0
  236. package/scripts/vault-mirror.mjs +203 -34
  237. package/skills/_shared/monitor-patterns.md +31 -5
  238. package/skills/_shared/parallel-aware-auq.md +1 -1
  239. package/skills/_shared/parallel-aware-preamble.md +4 -2
  240. package/skills/_shared/platform-tools.md +11 -5
  241. package/skills/_shared/state-ownership.md +29 -2
  242. package/skills/autopilot/SKILL.md +5 -1
  243. package/skills/bootstrap/SKILL.md +3 -3
  244. package/skills/bootstrap/_shared-template.md +18 -10
  245. package/skills/bootstrap/deep-template.md +10 -6
  246. package/skills/bootstrap/fast-template.md +15 -8
  247. package/skills/bootstrap/standard-template.md +10 -6
  248. package/skills/claude-md-drift-check/checker.mjs +39 -11
  249. package/skills/dispatcher/SKILL.md +1 -1
  250. package/skills/journey-audit/SKILL.md +269 -0
  251. package/skills/peekaboo-driver/SKILL.md +15 -3
  252. package/skills/persona-panel/SKILL.md +1 -1
  253. package/skills/reconcile/SKILL.md +41 -1
  254. package/skills/session-end/SKILL.md +17 -4
  255. package/skills/session-end/metrics-collection.md +7 -4
  256. package/skills/session-end/phase-3-6-tail.md +11 -3
  257. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  258. package/skills/session-plan/SKILL.md +6 -1
  259. package/skills/session-plan/wave-template.md +1 -0
  260. package/skills/session-start/SKILL.md +30 -16
  261. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  262. package/skills/session-start/phase-8-5-express-path.md +77 -12
  263. package/skills/vault-sync/validator.mjs +31 -0
  264. package/skills/wave-executor/SKILL.md +4 -2
  265. package/skills/wave-executor/circuit-breaker.md +34 -9
  266. package/skills/wave-executor/wave-loop.md +102 -19
  267. package/templates/_shared/journey-manifest.md +110 -0
  268. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -1,24 +1,205 @@
1
1
  {
2
- "_comment": "Cursor IDE hook mapping reference. Configure hooks in Cursor Settings > Hooks.",
3
- "_note": "Cursor hooks fire at different lifecycle points than Claude Code. See docs/cursor-setup.md for details.",
4
- "_limitation": "SessionStart greeting (session-start skill) is NOT available on Cursor. Cursor has no conversation-start lifecycle event equivalent to Claude Code's UserPromptSubmit hook. Session initialisation must be triggered manually.",
5
- "_enforcement": "reference-only",
6
- "_enforcement_note": "MACHINE-READABLE STATUS (#919): every entry below is an intended mapping, NOT live enforcement. No Cursor payload adapter exists (Pi has scripts/lib/pi-hook-bridge.mjs; Cursor has nothing), so each handler short-circuits on a Cursor-shaped payload and writes 0 bytes with exit 0. Operator decision 2026-07-31: gap registered, not closed. The machine-readable counterpart of this key is DOCUMENTED_ASYMMETRIES.handlerAsymmetries.cursor in scripts/lib/validate/check-hooks-symmetry.mjs, which Check 6 counts per event; the projection that makes that comparison non-vacuous is DOCUMENTED_ASYMMETRIES.cursorEventMap.",
7
- "_issues": ["#919", "#946"],
2
+ "_comment": "Cursor IDE hook mapping. Event names are Cursor-native; commands receive Claude/Codex-compatible hook JSON on stdin after scripts/lib/cursor-hook-bridge.mjs normalises the payload. CURSOR_PLUGIN_ROOT is set by the bridge; CURSOR_RULES_DIR is the compatibility alias used by plugin-root resolution.",
3
+ "_enforcement": "live",
4
+ "_enforcement_note": "LIVE via scripts/lib/cursor-hook-bridge.mjs and .cursor/hooks.json. The bridge maps Cursor stdin onto tool_name / tool_input and maps emitDeny envelopes onto Cursor { permission }. afterFileEdit remains post-hoc (cannot prevent an edit already written).",
5
+ "_issues": ["#919"],
8
6
  "hooks": {
9
- "afterFileEdit": {
10
- "script": "hooks/enforce-scope.mjs",
11
- "enforcement": "none",
12
- "issue": "#919",
13
- "mapsToClaudeEvent": "PostToolUse",
14
- "note": "Post-hoc scope warning — Cursor fires AFTER the edit (not before), so this maps onto PostToolUse, never PreToolUse. Cannot prevent out-of-scope edits, only warn — and today not even that: on a Cursor afterFileEdit payload enforce-scope.mjs writes 0 bytes to stdout AND stderr with exit 0 (measured 2026-07-31), because it reads Claude Code's PreToolUse field names."
15
- },
16
- "beforeShellExecution": {
17
- "script": "hooks/enforce-commands.mjs",
18
- "enforcement": "none",
19
- "issue": "#919",
20
- "mapsToClaudeEvent": "PreToolUse",
21
- "note": "NOT WIRED (#919) — silent no-op on a Cursor payload, NOT a block. The handler reads Claude Code's PreToolUse field names (tool_name === 'Bash', tool_input.command) and emits a Claude Code PreToolUse envelope (hookSpecificOutput.permissionDecision). Fed a Cursor beforeShellExecution payload it short-circuits at gate G1 and writes 0 bytes to stdout AND stderr with exit 0 — the harness sees no decision, so the command runs. Cursor needs an input/output adapter like scripts/lib/pi-hook-bridge.mjs; none exists. Treat this entry as the intended mapping, not as live enforcement."
22
- }
7
+ "sessionStart": [
8
+ {
9
+ "matcher": "",
10
+ "hooks": [
11
+ {
12
+ "type": "command",
13
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/on-session-start.mjs\"",
14
+ "timeout": 5
15
+ }
16
+ ]
17
+ }
18
+ ],
19
+ "sessionEnd": [
20
+ {
21
+ "matcher": "",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/on-session-end.mjs\"",
26
+ "timeout": 5
27
+ }
28
+ ]
29
+ }
30
+ ],
31
+ "beforeShellExecution": [
32
+ {
33
+ "matcher": "*",
34
+ "hooks": [
35
+ {
36
+ "type": "command",
37
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-bash-destructive-guard.mjs\"",
38
+ "timeout": 5
39
+ },
40
+ {
41
+ "type": "command",
42
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-bash-staging-fence.mjs\"",
43
+ "timeout": 5
44
+ },
45
+ {
46
+ "type": "command",
47
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-bash-memory-propose-audit.mjs\"",
48
+ "timeout": 5
49
+ },
50
+ {
51
+ "type": "command",
52
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-bash-sessions-ledger-guard.mjs\"",
53
+ "timeout": 5
54
+ },
55
+ {
56
+ "type": "command",
57
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-bash-templates-first.mjs\"",
58
+ "timeout": 5
59
+ },
60
+ {
61
+ "type": "command",
62
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-bash-issue-budget.mjs\"",
63
+ "timeout": 5
64
+ },
65
+ {
66
+ "type": "command",
67
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/enforce-commands.mjs\"",
68
+ "timeout": 5
69
+ }
70
+ ]
71
+ }
72
+ ],
73
+ "preToolUse": [
74
+ {
75
+ "matcher": "Write|StrReplace|TabWrite|Edit|MultiEdit",
76
+ "hooks": [
77
+ {
78
+ "type": "command",
79
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/enforce-scope.mjs\"",
80
+ "timeout": 5
81
+ },
82
+ {
83
+ "type": "command",
84
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/config-protection.mjs\"",
85
+ "timeout": 5
86
+ }
87
+ ]
88
+ }
89
+ ],
90
+ "afterShellExecution": [
91
+ {
92
+ "matcher": "*",
93
+ "hooks": [
94
+ {
95
+ "type": "command",
96
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-bash-write-verify.mjs\"",
97
+ "timeout": 5
98
+ }
99
+ ]
100
+ }
101
+ ],
102
+ "postToolUse": [
103
+ {
104
+ "matcher": "Write|StrReplace|TabWrite|Edit|MultiEdit",
105
+ "hooks": [
106
+ {
107
+ "type": "command",
108
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-edit-validate.mjs\"",
109
+ "timeout": 5
110
+ },
111
+ {
112
+ "type": "command",
113
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
114
+ "timeout": 5
115
+ }
116
+ ]
117
+ },
118
+ {
119
+ "matcher": "*",
120
+ "hooks": [
121
+ {
122
+ "type": "command",
123
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/loop-guard.mjs\"",
124
+ "timeout": 5
125
+ }
126
+ ]
127
+ }
128
+ ],
129
+ "afterFileEdit": [
130
+ {
131
+ "matcher": "*",
132
+ "hooks": [
133
+ {
134
+ "type": "command",
135
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-edit-validate.mjs\"",
136
+ "timeout": 5
137
+ },
138
+ {
139
+ "type": "command",
140
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
141
+ "timeout": 5
142
+ }
143
+ ]
144
+ }
145
+ ],
146
+ "postToolUseFailure": [
147
+ {
148
+ "matcher": "",
149
+ "hooks": [
150
+ {
151
+ "type": "command",
152
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tool-failure-corrective-context.mjs\"",
153
+ "timeout": 5
154
+ }
155
+ ]
156
+ }
157
+ ],
158
+ "stop": [
159
+ {
160
+ "matcher": "",
161
+ "hooks": [
162
+ {
163
+ "type": "command",
164
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/on-stop.mjs\"",
165
+ "timeout": 5
166
+ }
167
+ ]
168
+ }
169
+ ],
170
+ "subagentStart": [
171
+ {
172
+ "matcher": "",
173
+ "hooks": [
174
+ {
175
+ "type": "command",
176
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/subagent-telemetry.mjs\"",
177
+ "timeout": 5
178
+ }
179
+ ]
180
+ }
181
+ ],
182
+ "subagentStop": [
183
+ {
184
+ "matcher": "",
185
+ "hooks": [
186
+ {
187
+ "type": "command",
188
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/on-stop.mjs\"",
189
+ "timeout": 5
190
+ },
191
+ {
192
+ "type": "command",
193
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/subagent-telemetry.mjs\"",
194
+ "timeout": 5
195
+ },
196
+ {
197
+ "type": "command",
198
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-subagent-discovery-validator.mjs\"",
199
+ "timeout": 5
200
+ }
201
+ ]
202
+ }
203
+ ]
23
204
  }
24
205
  }
@@ -20,7 +20,7 @@
20
20
  {
21
21
  "type": "command",
22
22
  "command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/on-session-end.mjs\"",
23
- "timeout": 5
23
+ "timeout": 10
24
24
  }
25
25
  ]
26
26
  }
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v3.22.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v3.23.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -25,7 +25,7 @@
25
25
  {
26
26
  "type": "command",
27
27
  "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/on-session-end.mjs\"",
28
- "timeout": 5
28
+ "timeout": 10
29
29
  }
30
30
  ]
31
31
  }
@@ -10,7 +10,8 @@
10
10
  * `session.started` had no terminal partner.
11
11
  *
12
12
  * JSONL format (`.orchestrator/metrics/events.jsonl`):
13
- * {"timestamp":<ISO>,"event":"orchestrator.session.ended","session_id":"...","reason":"<reason>","duration_ms":<int>}
13
+ * {"timestamp":<ISO>,"event":"orchestrator.session.ended","session_id":"...","semantic_session_id":"...","reason":"<reason>","duration_ms":<int>}
14
+ * (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.)
14
15
  *
15
16
  * Exit codes: 0 always (informational hook — must never block session teardown).
16
17
  * stdin: optional JSON { hook_event_name:"SessionEnd", session_id?, reason?, cwd? }.
@@ -30,14 +31,20 @@ if (!shouldRunHook('on-session-end')) process.exit(0);
30
31
 
31
32
  import { emitEvent } from '../scripts/lib/events.mjs';
32
33
  import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
33
- import { backfillAbandonedSession } from '../scripts/lib/session-close-backfill.mjs';
34
+ import {
35
+ backfillAbandonedSession,
36
+ backfillCompletedFromStateMd,
37
+ } from '../scripts/lib/session-close-backfill.mjs';
34
38
  import {
35
39
  readLockDetailed,
36
40
  release,
37
41
  loadOwnerProof,
38
42
  OWNER_PROOF_RELPATH,
39
43
  } from '../scripts/lib/session-lock.mjs';
44
+ import { parseSessionId } from '../scripts/lib/session-id.mjs';
40
45
  import { deregisterSelf, logSweepEvent } from '../scripts/lib/session-registry.mjs';
46
+ import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
47
+ import { flush } from '../scripts/lib/telemetry/sync.mjs';
41
48
  import { attemptLockReconciliation } from './_lib/lock-reconcile.mjs';
42
49
 
43
50
  // ---------------------------------------------------------------------------
@@ -72,11 +79,26 @@ async function readStdinJson() {
72
79
  }
73
80
 
74
81
  /**
75
- * Resolve this session's id + duration + semantic id. Stdin session_id wins;
76
- * otherwise fall back to `.orchestrator/current-session.json` (written by
77
- * on-session-start.mjs). duration_ms is only computed when the ENDING session
78
- * is the one recorded in current-session.json never fabricated for a
79
- * mismatched / unknown session.
82
+ * Resolve this session's id + duration + semantic id. A stdin session_id wins
83
+ * ONLY when it parses as a UUID; otherwise fall back to
84
+ * `.orchestrator/current-session.json` (written by on-session-start.mjs).
85
+ * duration_ms is only computed when the ENDING session is the one recorded in
86
+ * current-session.json — never fabricated for a mismatched / unknown session.
87
+ *
88
+ * #1091 / Kanevry#66 — WRITER/READER SYMMETRY. `on-session-start.mjs`
89
+ * (`resolveSessionId`, :316-317) accepts a stdin raw id only when
90
+ * `parseSessionId(fromStdin)?.format === 'uuid'` and otherwise mints a
91
+ * `randomUUID()`; `current-session.json`, `session.lock` and the host registry
92
+ * are therefore ALWAYS keyed by a UUID. This reader used to accept ANY
93
+ * non-empty stdin string, so a harness that passed a non-UUID id
94
+ * (`{"session_id":"not-a-uuid"}`) resolved a key that matches nothing written
95
+ * at start: the raw-ID ownership compare below (:330) fails, the lock is
96
+ * neither released nor reconciled, and it LEAKS until its TTL expires. Same
97
+ * for `deregisterSelf()`, whose registry file is named after the id
98
+ * `registerSelf()` used. Mirroring the writer's rule here makes the fallback
99
+ * (which reads exactly those artifacts) the single source of the identity.
100
+ * The ownership compare itself stays an exact string `===` — this changes
101
+ * WHICH id is compared, never HOW.
80
102
  *
81
103
  * `semanticSessionId` is read from current-session.json (present since #587):
82
104
  * it is the SEMANTIC id (`<branch>-<date>-<mode>-<n>`) that sessions.jsonl is
@@ -98,7 +120,8 @@ async function readStdinJson() {
98
120
  */
99
121
  async function resolveSession(input, projectRoot) {
100
122
  const fromStdin = input?.session_id ?? input?.sessionId ?? null;
101
- let sessionId = (typeof fromStdin === 'string' && fromStdin.length > 0) ? fromStdin : null;
123
+ // UUID-only, exactly as the writer decides it (see the docblock above).
124
+ let sessionId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
102
125
 
103
126
  let recordedId = null;
104
127
  let semanticSessionId = null;
@@ -138,6 +161,152 @@ async function resolveSession(input, projectRoot) {
138
161
  return { sessionId, semanticSessionId: resolvedSemanticSessionId, durationMs };
139
162
  }
140
163
 
164
+ /**
165
+ * POST budget for the close-time telemetry flush (ms).
166
+ *
167
+ * NAMED CEILING, and the reason it is BELOW the module default POST_TIMEOUT_MS
168
+ * (3000): the two timeouts fail differently.
169
+ *
170
+ * - This one expiring is LOSSLESS. `flush()` catches the abort and routes the
171
+ * record into the bounded offline queue; the daily fallback
172
+ * (`shouldDailyFlush`) drains it on a later session.
173
+ * - The HARNESS timeout expiring is LOSSY. Claude Code kills the hook process
174
+ * mid-flight, so the enqueue never runs and the record is simply gone —
175
+ * which is the exact failure #1138 exists to remove.
176
+ *
177
+ * So the internal bound must stay comfortably under the harness bound, not
178
+ * merely below it. Budget (hooks/hooks.json `SessionEnd.timeout: 10` s, raised
179
+ * from 5 with this change): up to 500 ms stdin + a backfill measured at a
180
+ * ~845 ms median (its TAIL, not its median, is what would collide) + lock
181
+ * release + deregistration, then this 2 s. That leaves several seconds of slack
182
+ * for the backfill tail while still covering a normal round-trip many times
183
+ * over.
184
+ *
185
+ * Revisit if the hooks.json SessionEnd timeout changes — the pin lives in
186
+ * tests/hooks/on-session-end.test.mjs.
187
+ */
188
+ const TELEMETRY_FLUSH_TIMEOUT_MS = 2000;
189
+
190
+ /**
191
+ * Read `persistence` from the repo's Session Config. Defaults to `true` — the
192
+ * same default `scripts/lib/config.mjs` applies — so an unreadable or absent
193
+ * CLAUDE.md (or its Codex alias AGENTS.md) never silently disables the flush.
194
+ *
195
+ * @param {string} projectRoot
196
+ * @returns {Promise<boolean>}
197
+ */
198
+ async function readPersistence(projectRoot) {
199
+ try {
200
+ const md = await readConfigFile(projectRoot);
201
+ return parseSessionConfig(md).persistence !== false;
202
+ } catch {
203
+ return true;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Reduce a `flush()` result to the two-field breadcrumb the event carries.
209
+ *
210
+ * `reason` is normalised to its head token because `flush()` may return
211
+ * `build-error: <message>`, and a raw error message is unbounded free text in a
212
+ * stream whose whole purpose is aggregation by class.
213
+ *
214
+ * @param {{sent?: boolean, queued?: boolean, reason?: string}|null|undefined} res
215
+ * @returns {{outcome: 'sent'|'queued'|'gated'|'skipped', reason: string}}
216
+ */
217
+ function classifyFlush(res) {
218
+ const reason = String(res?.reason ?? 'unknown').split(':')[0];
219
+ if (res?.sent === true) return { outcome: 'sent', reason };
220
+ if (res?.queued === true) return { outcome: 'queued', reason };
221
+ if (reason === 'gated') return { outcome: 'gated', reason };
222
+ return { outcome: 'skipped', reason };
223
+ }
224
+
225
+ /**
226
+ * #1138 — the MECHANICAL telemetry flush.
227
+ *
228
+ * Until now the close-time flush existed only as prose in
229
+ * `skills/session-end/SKILL.md` § Phase 3.45, i.e. it ran only when the
230
+ * coordinator LLM happened to execute that phase. Measured 2026-08-23: 588
231
+ * session closes across 13 repos produced 82 ingest records (~14%). A hook is
232
+ * the only caller that fires on EVERY close, including the ones that never
233
+ * reach `/close` at all.
234
+ *
235
+ * Strictly best-effort: never throws, and bounded by TELEMETRY_FLUSH_TIMEOUT_MS.
236
+ * The consent gate lives INSIDE `flush()` (`resolveConsent()` is its first
237
+ * statement) — this function deliberately does not re-implement it, so there is
238
+ * exactly one place where "may we send?" is decided.
239
+ *
240
+ * Always emits `orchestrator.telemetry.flush` with `{ outcome, reason }` and
241
+ * NOTHING else — no payload, no anon_id. Per `.claude/rules/host-resources.md`
242
+ * HR-105, a mechanism whose firing rate nothing records cannot be falsified;
243
+ * this event is what makes the flush rate measurable next time.
244
+ *
245
+ * @param {string} projectRoot
246
+ * @returns {Promise<void>}
247
+ */
248
+ async function flushTelemetry(projectRoot) {
249
+ let result;
250
+ try {
251
+ result = (await readPersistence(projectRoot))
252
+ ? classifyFlush(await flush({
253
+ metricsDir: path.join(projectRoot, '.orchestrator', 'metrics'),
254
+ timeoutMs: TELEMETRY_FLUSH_TIMEOUT_MS,
255
+ }))
256
+ // `persistence: false` means this session leaves no durable local trace;
257
+ // a telemetry ping is a durable record too, so it honours the same switch.
258
+ : { outcome: 'skipped', reason: 'persistence-disabled' };
259
+ } catch {
260
+ result = { outcome: 'skipped', reason: 'error' };
261
+ }
262
+
263
+ // `result` is passed through verbatim: it holds EXACTLY {outcome, reason}, so
264
+ // no payload field can leak into the event by accident.
265
+ try {
266
+ await emitEvent('orchestrator.telemetry.flush', result);
267
+ } catch { /* observability is best-effort */ }
268
+ }
269
+
270
+ /**
271
+ * #1068 AC2 — emit ONE canonically queryable outcome per backfill attempt.
272
+ *
273
+ * Until now a backfill result reached exactly one place:
274
+ * `.orchestrator/metrics/session-close-backfill.log`, a side-log no lifecycle
275
+ * consumer reads and no staleness/integrity check joins against. The event
276
+ * stream is where every other lifecycle fact already lives, so the outcome goes
277
+ * there too — carrying IDENTITY (`session_id` raw UUID + the record id the
278
+ * backfill acted on + the attested semantic id), ACTION, and, when the action
279
+ * was an error, its REASON. The side-log is unchanged and remains the verbose
280
+ * copy; this is the queryable one.
281
+ *
282
+ * `kind` distinguishes the two backfill classes, which share the log file and
283
+ * would otherwise be indistinguishable in aggregate: `'abandoned'`
284
+ * (`backfillAbandonedSession`) vs `'state-md-completed'`
285
+ * (`backfillCompletedFromStateMd`).
286
+ *
287
+ * Every id key is OMITTED when unknown — never `""`, never a guess (#1068 AC1).
288
+ * Best-effort throughout: observability must never block teardown.
289
+ *
290
+ * @param {'abandoned'|'state-md-completed'} kind
291
+ * @param {{action?: string, sessionId?: string|null, supersedes?: string, error?: string}|null|undefined} result
292
+ * @param {{sessionId: string|null, semanticSessionId: string|null}} ids
293
+ * @returns {Promise<void>}
294
+ */
295
+ async function emitBackfillOutcome(kind, result, { sessionId, semanticSessionId }) {
296
+ try {
297
+ const recordId = typeof result?.sessionId === 'string' ? result.sessionId : null;
298
+ await emitEvent('orchestrator.session.backfill_completed', {
299
+ kind,
300
+ action: typeof result?.action === 'string' ? result.action : 'unknown',
301
+ ...(sessionId !== null ? { session_id: sessionId } : {}),
302
+ ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
303
+ ...(recordId !== null ? { record_id: recordId } : {}),
304
+ ...(typeof result?.supersedes === 'string' ? { supersedes: result.supersedes } : {}),
305
+ ...(typeof result?.error === 'string' ? { reason: result.error } : {}),
306
+ });
307
+ } catch { /* observability is best-effort */ }
308
+ }
309
+
141
310
  // ---------------------------------------------------------------------------
142
311
  // main
143
312
  // ---------------------------------------------------------------------------
@@ -152,8 +321,20 @@ async function main() {
152
321
 
153
322
  // Single emission path: emitEvent writes the canonical {timestamp, event, ...payload}
154
323
  // JSONL record AND fires the optional Clank webhook with the SAME event name.
324
+ //
325
+ // #1068 AC1 — the terminal event carries the raw UUID *and* the semantic id
326
+ // whenever the latter is ATTESTED, so a lifecycle outcome is joinable by
327
+ // identity from events.jsonl alone (previously only `session.lock.acquired`
328
+ // carried both, and only ~1/3 of sessions emit one). The key is OMITTED, never
329
+ // written as `""` or `null`, when `resolveSession()` could not attest it —
330
+ // "identity unresolved" must stay visibly unresolved rather than become a
331
+ // guessed id (#1068 AC1's explicit "niemals eine geratene ID"). Note the
332
+ // attestation bar is the #863 defect (c) guard inside `resolveSession()`: an
333
+ // ending session that is NOT the one current-session.json describes resolves
334
+ // `semanticSessionId: null` and therefore emits no key here.
155
335
  await emitEvent('orchestrator.session.ended', {
156
336
  ...(sessionId !== null ? { session_id: sessionId } : {}),
337
+ ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
157
338
  reason,
158
339
  duration_ms: durationMs,
159
340
  });
@@ -172,7 +353,21 @@ async function main() {
172
353
  // historical migration CLI) is NEVER passed here: a foreign lock that is live
173
354
  // at hook-time is, by definition, a real active session, not stale history.
174
355
  try {
175
- await backfillAbandonedSession({ repoRoot: projectRoot, sessionId, semanticSessionId });
356
+ const res = await backfillAbandonedSession({ repoRoot: projectRoot, sessionId, semanticSessionId });
357
+ await emitBackfillOutcome('abandoned', res, { sessionId, semanticSessionId });
358
+ } catch { /* best-effort — never block teardown */ }
359
+
360
+ // (a2) #429 — STATE.md `status: completed` self-heal. Orthogonal to (a)
361
+ // above and to THIS session's own id: it reads STATE.md directly and
362
+ // repairs a PAST session whose `status: completed` was set (by hand or
363
+ // otherwise) without session-end's Phase 3.7 ever writing the matching
364
+ // sessions.jsonl record — the exact state `commands/close.md`'s
365
+ // Pre-Check then reads as "already finalized" forever after. Cheap
366
+ // no-op on the overwhelmingly common path (STATE.md status is
367
+ // 'active'/'paused'/'idle', or the record already exists).
368
+ try {
369
+ const res = await backfillCompletedFromStateMd({ repoRoot: projectRoot });
370
+ await emitBackfillOutcome('state-md-completed', res, { sessionId, semanticSessionId });
176
371
  } catch { /* best-effort — never block teardown */ }
177
372
 
178
373
  // (b) Deterministic lock release — ONLY a lock whose raw/native session_id
@@ -276,7 +471,7 @@ async function main() {
276
471
  await emitEvent('orchestrator.session.lock.released', {
277
472
  session_id: sessionId,
278
473
  lock_session_id: lock.session_id,
279
- semantic_session_id: semanticSessionId,
474
+ ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
280
475
  end_reason: reason,
281
476
  caller: 'on-session-end',
282
477
  outcome: benignAlreadyGone ? 'already-gone' : 'deleted',
@@ -348,6 +543,12 @@ async function main() {
348
543
  logSweepEvent({ event: 'deregister-failed', session_id: sessionId, error: err?.message ?? String(err) });
349
544
  }
350
545
  }
546
+
547
+ // (d) #1138 — mechanical telemetry flush. Deliberately LAST: it is the only
548
+ // step here that may touch the network, so every local-fs guarantee above
549
+ // (backfill, lock release, deregistration) is already durable before the
550
+ // hook spends any of its remaining budget on a POST.
551
+ await flushTelemetry(projectRoot);
351
552
  }
352
553
 
353
554
  // Exit 0 always — informational hook must never block session teardown.