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
@@ -0,0 +1,443 @@
1
+ /**
2
+ * cursor-hook-bridge.mjs — map Cursor IDE hook events onto existing handlers.
3
+ *
4
+ * Cursor emits native events (`beforeShellExecution`, `preToolUse`, …) with its
5
+ * own stdin JSON and reads `{ permission, user_message, agent_message }` back.
6
+ * Existing Session Orchestrator hooks speak the Claude/Codex contract:
7
+ * `{ hook_event_name, tool_name, tool_input, ... }` plus an exit-0
8
+ * `hookSpecificOutput.permissionDecision` envelope (#906).
9
+ *
10
+ * This bridge is the Cursor counterpart of `pi-hook-bridge.mjs`. It closes the
11
+ * #919 silent-no-op: without it, `enforce-commands.mjs` sees no `tool_name ===
12
+ * "Bash"` on a Cursor payload and writes 0 bytes / exit 0, so the command runs.
13
+ *
14
+ * Usage (`.cursor/hooks.json`):
15
+ * sh hooks/run-node.sh scripts/lib/cursor-hook-bridge.mjs --event beforeShellExecution
16
+ */
17
+
18
+ import { spawnSync } from 'node:child_process';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import path from 'node:path';
21
+ import { pathToFileURL } from 'node:url';
22
+
23
+ import { matcherMatches, readHookDecision } from './pi-hook-bridge.mjs';
24
+ import { resolvePluginRoot } from './plugin-root.mjs';
25
+
26
+ export const CURSOR_TO_CANONICAL_EVENT = Object.freeze({
27
+ sessionStart: 'SessionStart',
28
+ sessionEnd: 'SessionEnd',
29
+ beforeShellExecution: 'PreToolUse',
30
+ afterShellExecution: 'PostToolUse',
31
+ preToolUse: 'PreToolUse',
32
+ postToolUse: 'PostToolUse',
33
+ afterFileEdit: 'PostToolUse',
34
+ postToolUseFailure: 'PostToolUseFailure',
35
+ stop: 'Stop',
36
+ subagentStart: 'SubagentStart',
37
+ subagentStop: 'SubagentStop',
38
+ });
39
+
40
+ export const CURSOR_BLOCKING_EVENTS = Object.freeze([
41
+ 'beforeShellExecution',
42
+ 'preToolUse',
43
+ 'beforeMCPExecution',
44
+ 'subagentStart',
45
+ ]);
46
+
47
+ const TOOL_NAME_MAP = Object.freeze({
48
+ shell: 'Bash',
49
+ bash: 'Bash',
50
+ write: 'Write',
51
+ tabwrite: 'Write',
52
+ strreplace: 'Edit',
53
+ edit: 'Edit',
54
+ multiedit: 'MultiEdit',
55
+ read: 'Read',
56
+ tabread: 'Read',
57
+ delete: 'Write',
58
+ task: 'Agent',
59
+ });
60
+
61
+ /**
62
+ * @param {unknown} value
63
+ * @returns {Record<string, unknown>}
64
+ */
65
+ function objectOrEmpty(value) {
66
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
67
+ }
68
+
69
+ /**
70
+ * @param {string|undefined} toolName
71
+ * @returns {string|undefined}
72
+ */
73
+ export function mapCursorToolName(toolName) {
74
+ if (typeof toolName !== 'string' || toolName.length === 0) return undefined;
75
+ const key = toolName.toLowerCase();
76
+ return TOOL_NAME_MAP[key] ?? toolName;
77
+ }
78
+
79
+ /**
80
+ * @param {unknown} value
81
+ * @returns {string|undefined}
82
+ */
83
+ function firstString(...values) {
84
+ for (const value of values) {
85
+ if (typeof value === 'string' && value.length > 0) return value;
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ /**
91
+ * @param {string} cursorEventName
92
+ * @param {Record<string, unknown>} event
93
+ * @returns {string|undefined}
94
+ */
95
+ function inferCursorToolName(cursorEventName, event) {
96
+ if (cursorEventName === 'beforeShellExecution' || cursorEventName === 'afterShellExecution') {
97
+ return 'Bash';
98
+ }
99
+ if (cursorEventName === 'afterFileEdit') {
100
+ return mapCursorToolName(typeof event.tool_type === 'string' ? event.tool_type : 'Write');
101
+ }
102
+ const raw = firstString(
103
+ typeof event.tool_name === 'string' ? event.tool_name : undefined,
104
+ typeof event.toolName === 'string' ? event.toolName : undefined,
105
+ typeof event.tool_type === 'string' ? event.tool_type : undefined,
106
+ typeof event.toolType === 'string' ? event.toolType : undefined,
107
+ );
108
+ return mapCursorToolName(raw);
109
+ }
110
+
111
+ /**
112
+ * @param {Record<string, unknown>} event
113
+ * @returns {Record<string, unknown>}
114
+ */
115
+ function collectToolInput(event) {
116
+ const nested = objectOrEmpty(event.tool_input ?? event.arguments ?? event.args ?? event.input);
117
+ const next = { ...nested };
118
+ const command = firstString(
119
+ typeof event.command === 'string' ? event.command : undefined,
120
+ typeof next.command === 'string' ? next.command : undefined,
121
+ );
122
+ if (command) next.command = command;
123
+
124
+ const filePath = firstString(
125
+ typeof next.file_path === 'string' ? next.file_path : undefined,
126
+ typeof event.file_path === 'string' ? event.file_path : undefined,
127
+ typeof event.filePath === 'string' ? event.filePath : undefined,
128
+ typeof event.path === 'string' ? event.path : undefined,
129
+ typeof next.path === 'string' ? next.path : undefined,
130
+ );
131
+ if (filePath) {
132
+ next.file_path = filePath;
133
+ if (typeof next.path !== 'string') next.path = filePath;
134
+ }
135
+
136
+ if (typeof next.old_string !== 'string' && typeof next.oldText === 'string') {
137
+ next.old_string = next.oldText;
138
+ }
139
+ if (typeof next.new_string !== 'string' && typeof next.newText === 'string') {
140
+ next.new_string = next.newText;
141
+ }
142
+
143
+ return next;
144
+ }
145
+
146
+ /**
147
+ * Build the hook stdin payload expected by existing session-orchestrator hooks.
148
+ *
149
+ * @param {string} cursorEventName
150
+ * @param {Record<string, unknown>} [event]
151
+ * @param {Record<string, unknown>} [ctx]
152
+ * @returns {Record<string, unknown>}
153
+ */
154
+ export function normalizeCursorHookPayload(cursorEventName, event = {}, ctx = {}) {
155
+ const eventObject = objectOrEmpty(event);
156
+ const ctxObject = objectOrEmpty(ctx);
157
+ const toolName = inferCursorToolName(cursorEventName, eventObject);
158
+ const toolInput = collectToolInput(eventObject);
159
+ const cwd = firstString(
160
+ typeof ctxObject.cwd === 'string' ? ctxObject.cwd : undefined,
161
+ typeof eventObject.cwd === 'string' ? eventObject.cwd : undefined,
162
+ process.cwd(),
163
+ );
164
+
165
+ return {
166
+ hook_event_name: CURSOR_TO_CANONICAL_EVENT[cursorEventName] ?? cursorEventName,
167
+ cursor_event_name: cursorEventName,
168
+ session_id: eventObject.session_id ?? eventObject.sessionId ?? ctxObject.sessionId,
169
+ cwd,
170
+ reason: eventObject.reason,
171
+ tool_name: toolName,
172
+ tool_input: toolInput,
173
+ tool_call_id: eventObject.tool_call_id ?? eventObject.toolCallId,
174
+ tool_response: eventObject.tool_response ?? eventObject.result ?? eventObject.output,
175
+ is_error: eventObject.is_error ?? eventObject.isError,
176
+ timestamp: eventObject.timestamp ?? new Date().toISOString(),
177
+ };
178
+ }
179
+
180
+ /**
181
+ * @param {string} pluginRoot
182
+ * @returns {Record<string, unknown>}
183
+ */
184
+ export function loadCursorHookManifest(pluginRoot = resolvePluginRoot()) {
185
+ const manifestPath = path.join(pluginRoot, 'hooks', 'hooks-cursor.json');
186
+ if (!existsSync(manifestPath)) {
187
+ throw new Error(`Cursor hook manifest not found at ${manifestPath}`);
188
+ }
189
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
190
+ }
191
+
192
+ /**
193
+ * @param {string} cursorEventName
194
+ * @param {Record<string, unknown>} event
195
+ * @param {string|undefined} canonicalToolName
196
+ * @returns {string[]}
197
+ */
198
+ function matcherTargets(cursorEventName, event, canonicalToolName) {
199
+ const rawToolName = firstString(
200
+ typeof event.tool_name === 'string' ? event.tool_name : undefined,
201
+ typeof event.toolName === 'string' ? event.toolName : undefined,
202
+ typeof event.tool_type === 'string' ? event.tool_type : undefined,
203
+ );
204
+ return [
205
+ cursorEventName,
206
+ typeof event.reason === 'string' ? event.reason : '',
207
+ rawToolName ?? '',
208
+ canonicalToolName ?? '',
209
+ cursorEventName === 'beforeShellExecution' || cursorEventName === 'afterShellExecution' ? 'bash' : '',
210
+ cursorEventName === 'beforeShellExecution' || cursorEventName === 'afterShellExecution' ? 'shell' : '',
211
+ ].map((v) => v.toLowerCase());
212
+ }
213
+
214
+ /**
215
+ * @param {Record<string, unknown>} manifest
216
+ * @param {string} cursorEventName
217
+ * @param {Record<string, unknown>} [event]
218
+ * @returns {Array<Record<string, unknown>>}
219
+ */
220
+ export function selectCursorHooks(manifest, cursorEventName, event = {}) {
221
+ const hooksByEvent = objectOrEmpty(manifest.hooks);
222
+ const entry = hooksByEvent[cursorEventName];
223
+ const eventObject = objectOrEmpty(event);
224
+ const canonicalToolName = inferCursorToolName(cursorEventName, eventObject);
225
+ const targets = matcherTargets(cursorEventName, eventObject, canonicalToolName);
226
+ const selected = [];
227
+
228
+ if (entry && !Array.isArray(entry) && typeof objectOrEmpty(entry).script === 'string') {
229
+ const script = objectOrEmpty(entry).script;
230
+ selected.push({
231
+ type: 'command',
232
+ command: `sh "$CURSOR_PLUGIN_ROOT/hooks/run-node.sh" "$CURSOR_PLUGIN_ROOT/${script}"`,
233
+ });
234
+ return selected;
235
+ }
236
+
237
+ const matchers = Array.isArray(entry) ? entry : [];
238
+ for (const matcherConfig of matchers) {
239
+ const config = objectOrEmpty(matcherConfig);
240
+ const matcher = typeof config.matcher === 'string' ? config.matcher : '';
241
+ if (!matcherMatches(matcher, targets)) continue;
242
+ const hooks = Array.isArray(config.hooks) ? config.hooks : [];
243
+ for (const hook of hooks) {
244
+ selected.push(objectOrEmpty(hook));
245
+ }
246
+ }
247
+
248
+ return selected;
249
+ }
250
+
251
+ /**
252
+ * @param {Record<string, unknown>} hook
253
+ * @param {Record<string, unknown>} payload
254
+ * @param {{ pluginRoot: string, cwd?: string, env?: NodeJS.ProcessEnv }} options
255
+ * @returns {{ command: string, status: number|null, stdout: string, stderr: string, blocked: boolean, reason: string|null }}
256
+ */
257
+ export function runCursorHookCommand(hook, payload, options) {
258
+ const command = typeof hook.command === 'string' ? hook.command : '';
259
+ if (!command) {
260
+ return { command, status: 0, stdout: '', stderr: '', blocked: false, reason: null };
261
+ }
262
+
263
+ const timeoutSeconds = Number.isFinite(hook.timeout) ? Number(hook.timeout) : 5;
264
+ const cwd = options.cwd || (typeof payload.cwd === 'string' ? payload.cwd : process.cwd());
265
+ const env = {
266
+ ...process.env,
267
+ ...options.env,
268
+ };
269
+ delete env.CLAUDE_PLUGIN_ROOT;
270
+ delete env.CODEX_PLUGIN_ROOT;
271
+ delete env.PI_PLUGIN_ROOT;
272
+ delete env.CLAUDE_PROJECT_DIR;
273
+ delete env.CODEX_PROJECT_DIR;
274
+ delete env.PI_PROJECT_DIR;
275
+ env.CURSOR_PLUGIN_ROOT = options.pluginRoot;
276
+ env.CURSOR_RULES_DIR = options.pluginRoot;
277
+ env.PLUGIN_ROOT = options.pluginRoot;
278
+ env.SO_PLATFORM = 'cursor';
279
+ if (typeof payload.cwd === 'string') env.CURSOR_PROJECT_DIR = payload.cwd;
280
+
281
+ // `hook.command` is a shell line from the repo-committed hooks.json, so a
282
+ // shell is the intended interpreter — the same commit-gated trust class as
283
+ // the `*-command` keys run by scripts/lib/quality-gate.mjs (see
284
+ // .claude/rules/security.md § Session Config Command Trust). Nothing here
285
+ // interpolates user or payload input into the command string; the payload
286
+ // travels on stdin.
287
+ // nosemgrep: unsafe-shell-spawn
288
+ const result = spawnSync(command, {
289
+ shell: true,
290
+ cwd,
291
+ env,
292
+ input: JSON.stringify(payload) + '\n',
293
+ encoding: 'utf8',
294
+ timeout: timeoutSeconds * 1000,
295
+ });
296
+
297
+ const stdout = result.stdout ?? '';
298
+ const stderr = result.stderr ?? '';
299
+ const decision = readHookDecision(stdout);
300
+ const isPreToolUse = payload.hook_event_name === 'PreToolUse';
301
+ const infraFailure = Boolean(result.error) ||
302
+ result.status === null ||
303
+ (result.status !== 0 && result.status !== 2) ||
304
+ decision.malformed;
305
+ const blocked = result.status === 2 ||
306
+ decision.deny ||
307
+ (isPreToolUse && infraFailure);
308
+
309
+ let reason = decision.reason;
310
+ if (reason === null && blocked) {
311
+ reason = result.error?.message
312
+ || (decision.malformed
313
+ ? `Hook stdout is not a parseable decision envelope (possible truncated output) — failing closed: ${command}`
314
+ : '')
315
+ || stdout.trim()
316
+ || stderr.trim()
317
+ || `Hook command blocked: ${command}`;
318
+ }
319
+
320
+ return {
321
+ command,
322
+ status: result.status,
323
+ stdout,
324
+ stderr,
325
+ blocked,
326
+ reason,
327
+ };
328
+ }
329
+
330
+ /**
331
+ * @param {string} cursorEventName
332
+ * @param {Record<string, unknown>} [event]
333
+ * @param {Record<string, unknown>} [ctx]
334
+ * @param {{ pluginRoot?: string, manifest?: Record<string, unknown>, env?: NodeJS.ProcessEnv }} [options]
335
+ * @returns {Promise<{ block: boolean, reason: string|null, payload: Record<string, unknown>, results: Array<object> }>}
336
+ */
337
+ export async function runCursorHookEvent(cursorEventName, event = {}, ctx = {}, options = {}) {
338
+ const pluginRoot = options.pluginRoot ?? resolvePluginRoot();
339
+ const manifest = options.manifest ?? loadCursorHookManifest(pluginRoot);
340
+ const payload = normalizeCursorHookPayload(cursorEventName, event, ctx);
341
+ const hooks = selectCursorHooks(manifest, cursorEventName, event);
342
+ const results = [];
343
+
344
+ for (const hook of hooks) {
345
+ const result = runCursorHookCommand(hook, payload, {
346
+ pluginRoot,
347
+ cwd: typeof payload.cwd === 'string' ? payload.cwd : process.cwd(),
348
+ env: options.env,
349
+ });
350
+ results.push(result);
351
+ if (result.blocked) {
352
+ return {
353
+ block: true,
354
+ reason: result.reason,
355
+ payload,
356
+ results,
357
+ };
358
+ }
359
+ }
360
+
361
+ return {
362
+ block: false,
363
+ reason: null,
364
+ payload,
365
+ results,
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Map a bridge result onto Cursor's hook stdout contract.
371
+ *
372
+ * @param {string} cursorEventName
373
+ * @param {{ block: boolean, reason: string|null }} result
374
+ * @returns {Record<string, unknown>}
375
+ */
376
+ export function toCursorHookOutput(cursorEventName, result) {
377
+ const blocking = CURSOR_BLOCKING_EVENTS.includes(cursorEventName);
378
+ if (result.block && blocking) {
379
+ const reason = result.reason || 'Blocked by Session Orchestrator';
380
+ return {
381
+ permission: 'deny',
382
+ user_message: reason,
383
+ agent_message: reason,
384
+ };
385
+ }
386
+ if (result.block && (cursorEventName === 'afterFileEdit' || cursorEventName === 'postToolUse')) {
387
+ return {
388
+ additional_context: result.reason || 'Session Orchestrator post-hoc warning',
389
+ };
390
+ }
391
+ if (blocking) {
392
+ return { permission: 'allow' };
393
+ }
394
+ return {};
395
+ }
396
+
397
+ function readStdinSync() {
398
+ try {
399
+ return readFileSync(0, 'utf8');
400
+ } catch {
401
+ return '';
402
+ }
403
+ }
404
+
405
+ async function main() {
406
+ const eventIdx = process.argv.indexOf('--event');
407
+ const eventName = eventIdx >= 0 ? process.argv[eventIdx + 1] : '';
408
+ if (!eventName || typeof CURSOR_TO_CANONICAL_EVENT[eventName] !== 'string') {
409
+ process.stderr.write(`cursor-hook-bridge: missing or unknown --event ${eventName || '(none)'}\n`);
410
+ process.stdout.write(`${JSON.stringify({ permission: 'allow' })}\n`);
411
+ return;
412
+ }
413
+
414
+ const raw = readStdinSync();
415
+ let event;
416
+ try {
417
+ event = raw.trim() ? JSON.parse(raw) : {};
418
+ } catch (err) {
419
+ const blocking = CURSOR_BLOCKING_EVENTS.includes(eventName);
420
+ if (blocking) {
421
+ const reason = `Cursor hook payload is not valid JSON: ${err instanceof Error ? err.message : err}`;
422
+ process.stdout.write(`${JSON.stringify({
423
+ permission: 'deny',
424
+ user_message: reason,
425
+ agent_message: reason,
426
+ })}\n`);
427
+ return;
428
+ }
429
+ process.stdout.write('{}\n');
430
+ return;
431
+ }
432
+
433
+ const result = await runCursorHookEvent(eventName, objectOrEmpty(event), { cwd: process.cwd() });
434
+ process.stdout.write(`${JSON.stringify(toCursorHookOutput(eventName, result))}\n`);
435
+ }
436
+
437
+ const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
438
+ if (isMain) {
439
+ main().catch((err) => {
440
+ process.stderr.write(`cursor-hook-bridge: ${err instanceof Error ? err.message : err}\n`);
441
+ process.stdout.write(`${JSON.stringify({ permission: 'allow' })}\n`);
442
+ });
443
+ }
@@ -99,8 +99,8 @@ export async function runDispatch({ startDir, now, deps } = {}) {
99
99
  * session-lock.mjs — returns its result VERBATIM so callers (the skill prose,
100
100
  * Wave-4 tests) can branch on the exact `{ ok, reason, ... }` contract.
101
101
  *
102
- * On `ok:false` (race lost / busy — reasons: active, stale-pid-alive,
103
- * stale-pid-dead, fs-error, ...) the dispatcher must exclude that repo and
102
+ * On `ok:false` (race lost / busy — reasons: active, stale-heartbeat,
103
+ * fs-error, ...) the dispatcher must exclude that repo and
104
104
  * re-rank the remaining free candidates. On `ok:true` the claim is held and the
105
105
  * coordinator may launch the chosen entry command.
106
106
  *