@fusengine/harness 0.1.90 → 0.1.92

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 (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -3,11 +3,11 @@ import { a as harnessHome, c as resolveTtlSec } from "./dotenv-BLBkBTww.mjs";
3
3
  import { r as projectLayout } from "./layout-KWoE_Mqn.mjs";
4
4
  import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
5
5
  import { t as detectHarness } from "./harness-BMuLJ9lm.mjs";
6
- import { $ as matchPatterns, D as isVendorPath, G as isFileSizeScoped, J as GIT_BLOCKED, K as resolveSolidRefFramework, W as computeEditResultLines, b as interfaceDeclLevel, et as detectFramework, g as conventionsMode, lt as requiredArchSkill, n as FAIL_CLOSED, ot as detectModularArchitecture, rt as maskCommentsAndStrings, st as detectProjectType$1, t as evaluate } from "./evaluate-C1VSI5pb.mjs";
6
+ import { D as isVendorPath, G as computeEditResultLines, K as isFileSizeScoped, V as shellOutputRedirects, Y as GIT_BLOCKED, b as interfaceDeclLevel, ct as detectProjectType$1, et as matchPatterns, g as conventionsMode, it as maskCommentsAndStrings, n as FAIL_CLOSED, q as resolveSolidRefFramework, st as detectModularArchitecture, t as evaluate, tt as detectFramework, ut as requiredArchSkill } from "./evaluate-CMiIqHeH.mjs";
7
7
  import { a as sanitizeSessionId, c as sessionsDir, d as countFrameworkCodeLines, f as countLines, i as loadSessionState, l as PLUGINS_DIR, n as fuseHarnessHome, o as saveSessionState, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-oUGFB4ds.mjs";
8
8
  import { a as writeJsonFile, i as readJsonFile, r as hashText, t as atomicWrite } from "./json-io-DisYd2fb.mjs";
9
9
  import { r as isDocConsulted } from "./doc-helpers-CWZegVdR.mjs";
10
- import { D as frameworkSolidGate, F as isExcludedSwiftPath, G as evaluateApex, H as PRE_AUTH_GATES, P as isExcludedJsPath, R as capVerbosity, S as parseField, T as usesTailwindUtilities, V as POST_AUTH_GATES, W as docConsultedGate, b as scanPlugin, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, g as buildClaudeMdContext, l as PROJECT_INDICATORS, n as missingSeoElements, o as parseEnrichment, p as apexAgentSteps, r as descFromText, s as parseEntry, t as isHtmlLike, v as apexDocName, w as skillTriggerGate, y as harnessHomeSegment, z as detectCreationIntent } from "./validate-DZFtAbLP.mjs";
10
+ import { D as frameworkSolidGate, F as isExcludedSwiftPath, G as evaluateApex, H as PRE_AUTH_GATES, P as isExcludedJsPath, R as capVerbosity, S as parseField, T as usesTailwindUtilities, V as POST_AUTH_GATES, W as docConsultedGate, b as scanPlugin, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, g as buildClaudeMdContext, l as PROJECT_INDICATORS, n as missingSeoElements, o as parseEnrichment, p as apexAgentSteps, r as descFromText, s as parseEntry, t as isHtmlLike, v as apexDocName, w as skillTriggerGate, y as harnessHomeSegment, z as detectCreationIntent } from "./validate-KjZ1X9tH.mjs";
11
11
  import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-DVML3zfp.mjs";
12
12
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
13
13
  import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-IhHk2KlT.mjs";
@@ -15,11 +15,13 @@ import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cac
15
15
  import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
16
16
  import { C as apexAuthorizationGate, S as trivialCount, _ as recordBrainstormRequired, a as journalLogPath, b as recordTarget, c as appendEvent, d as signTrack, f as verifyTrack, g as recordAgent, h as emptyTrack, i as withTrack, l as withTrackLockSync, m as agentsFresh, o as readTrackSync, p as writeLastNonce, r as trackJournalEnabled, s as diffTrackEvents, t as loadTrack, u as LOCK_FAILED, v as recordDoc, x as recordTrivialEdit, y as recordRefRead } from "./store-5-ZPKb0u.mjs";
17
17
  import { i as parseApplyPatch, n as canonicalizeMcpToolName, r as canonicalizeCodexShellTool, t as isBypassPermissions } from "./permission-mode-B-SFyR0X.mjs";
18
- import { _ as commandToString, d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText$1, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-5bC8TDAz.mjs";
19
- import { i as toKimiResponse, n as kimiDenyResponse } from "./kimi-D6X7b8wF.mjs";
20
- import { r as toHermesResponse } from "./hermes-kP6NUQlG.mjs";
18
+ import { t as commandToString } from "./command-string-CALMTnwN.mjs";
19
+ import { n as cursorProjectCwd, r as cursorEventContract, t as extractCursorEvent } from "./normalize-Dy8g9Ybl.mjs";
20
+ import { d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText$1, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-Ckv2_TgP.mjs";
21
+ import { i as toKimiResponse, n as kimiDenyResponse } from "./kimi-G2wcSh5-.mjs";
22
+ import { r as toHermesResponse } from "./hermes-B9-p_3IF.mjs";
21
23
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
22
- import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
24
+ import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
23
25
  import { homedir } from "node:os";
24
26
  import { mkdir, rmdir } from "node:fs/promises";
25
27
  import { createHash } from "node:crypto";
@@ -69,9 +71,7 @@ function str(v) {
69
71
  return typeof v === "string" ? v : void 0;
70
72
  }
71
73
  /**
72
- * Normalize a harness hook payload into a uniform event. Handles Cline's nested
73
- * `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
74
- * shape used by Claude, Codex, Gemini, and Cursor.
74
+ * Normalize a hook payload, including Cline nesting and native Cursor events.
75
75
  */
76
76
  function normalizeEvent(id, payload) {
77
77
  if (id === "cline") {
@@ -88,6 +88,12 @@ function normalizeEvent(id, payload) {
88
88
  command: str(params.command)
89
89
  };
90
90
  }
91
+ if (id === "cursor") return {
92
+ ...extractCursorEvent(payload),
93
+ sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
94
+ agentType: str(payload.agent_type),
95
+ permissionMode: str(payload.permission_mode)
96
+ };
91
97
  const event = str(payload.hook_event_name) ?? "";
92
98
  const input = payload.tool_input ?? payload;
93
99
  const tool = canonicalizeCodexShellTool(id, canonicalizeMcpToolName(id, str(payload.tool_name) ?? ""));
@@ -97,7 +103,9 @@ function normalizeEvent(id, payload) {
97
103
  input,
98
104
  sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
99
105
  agentType: str(payload.agent_type) ?? str(input.subagent_type),
100
- permissionMode: str(payload.permission_mode)
106
+ permissionMode: str(payload.permission_mode),
107
+ toolUseId: str(payload.tool_use_id),
108
+ cwd: str(payload.cwd)
101
109
  };
102
110
  if (tool === "apply_patch") {
103
111
  const files = parseApplyPatch(str(input.command) ?? str(payload.command) ?? "").map((f) => ({
@@ -2136,13 +2144,114 @@ function postEditTypescript(filePath) {
2136
2144
  return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
2137
2145
  }
2138
2146
  //#endregion
2147
+ //#region src/adapters/cursor/plugin-root.ts
2148
+ /**
2149
+ * Cursor plugin-root resolution — independent from the rules-plugin probing
2150
+ * in `../../runtime/lifecycle/rules-root.ts`. Ground truth (Cursor 3.18.25
2151
+ * binary + cursor.com/docs/hooks): `CURSOR_PLUGIN_ROOT` / `CLAUDE_PLUGIN_ROOT`
2152
+ * (both equal to the plugin install dir) are injected ONLY into
2153
+ * plugin-declared hook processes — never user (`~/.cursor/hooks.json`),
2154
+ * project (`.cursor/hooks.json`), or enterprise hooks. A plugin hook's cwd is
2155
+ * the plugin install dir, EXCEPT for `stop`/`subagentStop`, where it is the
2156
+ * workspace root — callers must pass the right `cwd` for the event they are
2157
+ * handling. Precedence: (1) `CURSOR_PLUGIN_ROOT` env, (2) `CLAUDE_PLUGIN_ROOT`
2158
+ * env, (3) `cwd` when it carries a Cursor plugin marker
2159
+ * (`.cursor-plugin/plugin.json`, `plugin.json` + `hooks/hooks.json`, or a
2160
+ * bare `hooks/hooks.json` — matches installed-plugin layouts under
2161
+ * `~/.cursor/plugins/cache/**` and `~/.cursor/plugins/local/<name>/`), (4)
2162
+ * `none`. Cursor refuses symlinked config paths itself; we do not share that
2163
+ * constraint, so every resolved candidate is realpath-followed instead.
2164
+ */
2165
+ /** Validate an env candidate: non-empty, NUL-free, absolute, existing dir. */
2166
+ function validateEnvCandidate(label, value, checked) {
2167
+ if (value === void 0 || value === "") {
2168
+ checked.push(`${label}: unset`);
2169
+ return null;
2170
+ }
2171
+ if (value.includes("\0")) {
2172
+ checked.push(`${label}: invalid (contains NUL): "${value}"`);
2173
+ return null;
2174
+ }
2175
+ if (!isAbsolute(value)) {
2176
+ checked.push(`${label}: invalid (not absolute): "${value}"`);
2177
+ return null;
2178
+ }
2179
+ try {
2180
+ if (!statSync(value).isDirectory()) {
2181
+ checked.push(`${label}: invalid (not a directory): "${value}"`);
2182
+ return null;
2183
+ }
2184
+ } catch {
2185
+ checked.push(`${label}: invalid (no such directory): "${value}"`);
2186
+ return null;
2187
+ }
2188
+ try {
2189
+ return realpathSync.native(value);
2190
+ } catch {
2191
+ checked.push(`${label}: invalid (realpath failed): "${value}"`);
2192
+ return null;
2193
+ }
2194
+ }
2195
+ /** True when `dir` carries a recognized Cursor plugin install marker. */
2196
+ function hasPluginMarker(dir) {
2197
+ if (existsSync(join(dir, ".cursor-plugin", "plugin.json"))) return true;
2198
+ if (existsSync(join(dir, "plugin.json")) && existsSync(join(dir, "hooks", "hooks.json"))) return true;
2199
+ return existsSync(join(dir, "hooks", "hooks.json"));
2200
+ }
2201
+ /**
2202
+ * Resolve the Cursor plugin install root a plugin-declared hook runs from.
2203
+ * @param env - Environment (defaults to `process.env`).
2204
+ * @param cwd - The hook process's cwd for the current event (plugin root for
2205
+ * most events, workspace root for `stop`/`subagentStop` — caller's choice).
2206
+ * @returns The resolved root, its source, and every rejected candidate.
2207
+ */
2208
+ function resolveCursorPluginRoot(env, cwd) {
2209
+ const checked = [];
2210
+ const fromCursor = validateEnvCandidate("env:CURSOR_PLUGIN_ROOT", env.CURSOR_PLUGIN_ROOT, checked);
2211
+ if (fromCursor) return {
2212
+ root: fromCursor,
2213
+ source: "env:CURSOR_PLUGIN_ROOT",
2214
+ checked
2215
+ };
2216
+ const fromClaude = validateEnvCandidate("env:CLAUDE_PLUGIN_ROOT", env.CLAUDE_PLUGIN_ROOT, checked);
2217
+ if (fromClaude) return {
2218
+ root: fromClaude,
2219
+ source: "env:CLAUDE_PLUGIN_ROOT",
2220
+ checked
2221
+ };
2222
+ if (hasPluginMarker(cwd)) {
2223
+ let resolved = cwd;
2224
+ try {
2225
+ resolved = realpathSync.native(cwd);
2226
+ } catch {}
2227
+ return {
2228
+ root: resolved,
2229
+ source: "cwd:plugin-marker",
2230
+ checked
2231
+ };
2232
+ }
2233
+ checked.push(`cwd:"${cwd}": no plugin marker found`);
2234
+ return {
2235
+ root: null,
2236
+ source: "none",
2237
+ checked
2238
+ };
2239
+ }
2240
+ //#endregion
2139
2241
  //#region src/runtime/lifecycle/rules-root.ts
2140
2242
  /**
2141
2243
  * Dynamic rules-plugin root resolution. The historical `CLAUDE_PLUGIN_ROOT ??
2142
2244
  * cwd` chain only worked when the harness exported the plugin root — Kimi
2143
2245
  * injects `KIMI_PLUGIN_ROOT` instead, and a bare cwd fallback never held a
2144
- * `rules/` dir. Resolution order (first hit wins):
2145
- * 1. `CLAUDE_PLUGIN_ROOT` (claude-code/codex plugin-declared hooks);
2246
+ * `rules/` dir.
2247
+ *
2248
+ * `id === "cursor"` is resolved by a SEPARATE branch (`resolveCursorPluginRoot`)
2249
+ * before any of the below, because Cursor's env contract differs from the
2250
+ * other harnesses (see `../../adapters/cursor/plugin-root.ts`) — it is never
2251
+ * folded into the shared switch. For every other id, resolution order (first
2252
+ * hit wins, unchanged):
2253
+ * 1. `CLAUDE_PLUGIN_ROOT` (read for ALL non-cursor ids, historical quirk —
2254
+ * frozen by non-regression tests, do not "fix" without an explicit ask);
2146
2255
  * 2. `KIMI_PLUGIN_ROOT` (kimi plugin-declared hooks);
2147
2256
  * 3. Per-harness install probe (claude marketplace, codex versioned cache,
2148
2257
  * kimi managed plugins) — first `<plugin>/rules` dir whose plugin folder
@@ -2196,6 +2305,12 @@ function probeKimi(env, home) {
2196
2305
  * @returns The plugin root to read `rules/` from.
2197
2306
  */
2198
2307
  function resolveRulesRoot(id, cwd, env = process.env) {
2308
+ if (id === "cursor") {
2309
+ const result = resolveCursorPluginRoot(env, cwd);
2310
+ if (result.root) return result.root;
2311
+ process.stderr.write(`[fuse-harness] cursor: no plugin root proven (checked: ${result.checked.join("; ")}); rules root falls back to ${cwd}\n`);
2312
+ return cwd;
2313
+ }
2199
2314
  if (env.CLAUDE_PLUGIN_ROOT) return env.CLAUDE_PLUGIN_ROOT;
2200
2315
  if (env.KIMI_PLUGIN_ROOT) return env.KIMI_PLUGIN_ROOT;
2201
2316
  const home = env.HOME ?? homedir();
@@ -3064,7 +3179,7 @@ function lessonsStateFileFor(root) {
3064
3179
  //#endregion
3065
3180
  //#region src/tracking/one-shot-store.ts
3066
3181
  /** A fresh, empty state — always spread (`{ ...EMPTY }`) so the const is never shared. */
3067
- const EMPTY = {
3182
+ const EMPTY$1 = {
3068
3183
  gates: {},
3069
3184
  firstTry: 0,
3070
3185
  corrected: 0,
@@ -3076,7 +3191,7 @@ const EMPTY = {
3076
3191
  * gates/pending older than `windowMs`. Keeps the "7d" window honest, bounds size.
3077
3192
  */
3078
3193
  function pruneState(s, now, windowMs) {
3079
- if (now - s.updatedAt >= windowMs) return { ...EMPTY };
3194
+ if (now - s.updatedAt >= windowMs) return { ...EMPTY$1 };
3080
3195
  const gates = {};
3081
3196
  for (const [k, g] of Object.entries(s.gates)) if (now - g.lastTs < windowMs) gates[k] = g;
3082
3197
  const pending = {};
@@ -3309,16 +3424,16 @@ const SIDECAR$1 = "one-shot.json";
3309
3424
  /** Retention window: 7 days. Aggregates and pending denies older than this are pruned. */
3310
3425
  const WINDOW_MS = 10080 * 60 * 1e3;
3311
3426
  /** Load the state, or a fresh copy when missing/corrupt. */
3312
- function loadState(path) {
3427
+ function loadState$1(path) {
3313
3428
  try {
3314
- if (!existsSync(path)) return { ...EMPTY };
3429
+ if (!existsSync(path)) return { ...EMPTY$1 };
3315
3430
  const d = JSON.parse(readFileSync(path, "utf8"));
3316
3431
  return d && typeof d === "object" && !Array.isArray(d) ? {
3317
- ...EMPTY,
3432
+ ...EMPTY$1,
3318
3433
  ...d
3319
- } : { ...EMPTY };
3434
+ } : { ...EMPTY$1 };
3320
3435
  } catch {
3321
- return { ...EMPTY };
3436
+ return { ...EMPTY$1 };
3322
3437
  }
3323
3438
  }
3324
3439
  /**
@@ -3341,7 +3456,7 @@ function recordOneShot(prompt, input, opts) {
3341
3456
  });
3342
3457
  if (!burstFirst(op, prompt ? `deny:${prompt.title}` : "allow", opts)) return;
3343
3458
  const path = join(opts.dir, SIDECAR$1);
3344
- let s = pruneState(loadState(path), opts.now, WINDOW_MS);
3459
+ let s = pruneState(loadState$1(path), opts.now, WINDOW_MS);
3345
3460
  s = prompt ? applyDeny(s, prompt.title, op, opts.now) : applyAllow(s, op, opts.now, input.content != null || input.command != null);
3346
3461
  atomicWrite(path, JSON.stringify(s));
3347
3462
  } catch {}
@@ -3356,7 +3471,7 @@ function recordOneShot(prompt, input, opts) {
3356
3471
  */
3357
3472
  function oneShotSummary(cwd) {
3358
3473
  try {
3359
- return formatSummary(pruneState(loadState(join(defaultStateDir(cwd), SIDECAR$1)), Date.now(), WINDOW_MS));
3474
+ return formatSummary(pruneState(loadState$1(join(defaultStateDir(cwd), SIDECAR$1)), Date.now(), WINDOW_MS));
3360
3475
  } catch {
3361
3476
  return "";
3362
3477
  }
@@ -3401,7 +3516,7 @@ function recordFailure(tool, opts) {
3401
3516
  try {
3402
3517
  if (!burstFirst(`fail:${tool}`, "failure", opts)) return;
3403
3518
  const path = join(opts.dir, SIDECAR$1);
3404
- const next = applyFailure(pruneState(loadState(path), opts.now, WINDOW_MS), tool, opts.now);
3519
+ const next = applyFailure(pruneState(loadState$1(path), opts.now, WINDOW_MS), tool, opts.now);
3405
3520
  atomicWrite(path, JSON.stringify(next));
3406
3521
  } catch {}
3407
3522
  }
@@ -3414,8 +3529,12 @@ function recordFailure(tool, opts) {
3414
3529
  * matches the failure message — reusing the PreToolUse {@link lessonFor} index and
3415
3530
  * its cooldown (idempotent under the ~11-process fan-out). Fail-open throughout.
3416
3531
  *
3417
- * Claude-Code-only: no equivalent `PostToolUseFailure` hook exists on Codex or
3418
- * Hermes, so this handler is never reached through those adapters.
3532
+ * Claude-Code-only in the sense that no equivalent `PostToolUseFailure` hook
3533
+ * exists on Codex or Hermes but Cursor's own `postToolUseFailure` DOES
3534
+ * arrive here too, via `lifecycle-bridge.ts`'s `lifecycleStdout` translating
3535
+ * the wire event name and forwarding to `dispatchLifecycle`'s
3536
+ * `"PostToolUseFailure"` case (see `handle.ts`'s `cursorRawPayloadProjection`
3537
+ * for how `data.tool_name` below arrives already canonicalized on Cursor).
3419
3538
  * @packageDocumentation
3420
3539
  */
3421
3540
  /** The failure message across the documented `error` field and defensive fallbacks; "" when none. */
@@ -6024,7 +6143,7 @@ async function dispatchAipilot(event, payload, cwd, now, home = homedir(), id =
6024
6143
  await cacheAnalyticsSave(home, now);
6025
6144
  return "";
6026
6145
  }
6027
- if (event === "PreToolUse") return docCacheGate(payload, cwd, now, home, id);
6146
+ if (event === "PreToolUse" || id === "cursor" && event === "BeforeMCPExecution") return docCacheGate(payload, cwd, now, home, id);
6028
6147
  return null;
6029
6148
  }
6030
6149
  /** PostToolUse (Write/Edit SOLID check, else TaskCreate/TaskUpdate sync) for the ai-pilot scope; `id` selects the harness target (defaults to "claude-code"). */
@@ -6646,13 +6765,571 @@ async function autoDocumentRead(filePath, now = Date.now(), id = "claude-code")
6646
6765
  return systemMessage(`📖 [${docType}] ${fname} logged`);
6647
6766
  }
6648
6767
  //#endregion
6768
+ //#region src/adapters/cursor/native-schemas.ts
6769
+ const stringValue = (value) => typeof value === "string";
6770
+ const booleanValue = (value) => typeof value === "boolean";
6771
+ /** A plain `{}`-literal or `Object.create(null)` object — never a class instance or array. */
6772
+ const plainRecord = (value) => {
6773
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
6774
+ try {
6775
+ const prototype = Object.getPrototypeOf(value);
6776
+ return prototype === Object.prototype || prototype === null;
6777
+ } catch {
6778
+ return false;
6779
+ }
6780
+ };
6781
+ function jsonChildren(value) {
6782
+ const keys = Reflect.ownKeys(value);
6783
+ const descriptors = Object.getOwnPropertyDescriptors(value);
6784
+ if (Array.isArray(value)) {
6785
+ if (Object.getPrototypeOf(value) !== Array.prototype) return null;
6786
+ const length = descriptors.length;
6787
+ if (!length || !("value" in length) || !Number.isSafeInteger(length.value) || length.value < 0) return null;
6788
+ if (keys.length !== length.value + 1 || keys.some((key) => typeof key === "symbol")) return null;
6789
+ const children = [];
6790
+ for (let index = 0; index < length.value; index += 1) {
6791
+ const descriptor = descriptors[String(index)];
6792
+ if (!descriptor?.enumerable || !("value" in descriptor)) return null;
6793
+ children.push(descriptor.value);
6794
+ }
6795
+ return children;
6796
+ }
6797
+ if (!plainRecord(value) || keys.some((key) => typeof key === "symbol")) return null;
6798
+ const children = [];
6799
+ for (const key of keys) {
6800
+ const descriptor = descriptors[key];
6801
+ if (!descriptor?.enumerable || !("value" in descriptor)) return null;
6802
+ children.push(descriptor.value);
6803
+ }
6804
+ return children;
6805
+ }
6806
+ function jsonValue(root) {
6807
+ const active = /* @__PURE__ */ new WeakSet();
6808
+ const stack = [{ value: root }];
6809
+ while (stack.length > 0) {
6810
+ const frame = stack.pop();
6811
+ if (frame.leave) {
6812
+ active.delete(frame.value);
6813
+ continue;
6814
+ }
6815
+ const { value } = frame;
6816
+ if (value === null || typeof value === "string" || typeof value === "boolean") continue;
6817
+ if (typeof value === "number") {
6818
+ if (!Number.isFinite(value)) return false;
6819
+ continue;
6820
+ }
6821
+ if (typeof value !== "object" || active.has(value)) return false;
6822
+ let children;
6823
+ try {
6824
+ children = jsonChildren(value);
6825
+ } catch {
6826
+ return false;
6827
+ }
6828
+ if (!children) return false;
6829
+ active.add(value);
6830
+ stack.push({
6831
+ value,
6832
+ leave: true
6833
+ });
6834
+ for (let index = children.length - 1; index >= 0; index -= 1) stack.push({ value: children[index] });
6835
+ }
6836
+ return true;
6837
+ }
6838
+ /** A JSON-safe plain object (no cycles, no non-finite numbers, no exotic prototypes). */
6839
+ const recordValue = (value) => plainRecord(value) && jsonValue(value);
6840
+ const stringRecord = (value) => {
6841
+ if (!recordValue(value)) return false;
6842
+ try {
6843
+ return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor && typeof descriptor.value === "string");
6844
+ } catch {
6845
+ return false;
6846
+ }
6847
+ };
6848
+ const stringArray = (value) => Array.isArray(value) && value.every(stringValue);
6849
+ const permission = (...values) => (value) => typeof value === "string" && values.includes(value);
6850
+ const EMPTY = { fields: {} };
6851
+ const FOLLOWUP = { fields: { followup_message: stringValue } };
6852
+ const PERMISSION_ASK = {
6853
+ fields: {
6854
+ permission: permission("allow", "deny", "ask"),
6855
+ user_message: stringValue,
6856
+ agent_message: stringValue
6857
+ },
6858
+ required: ["permission"]
6859
+ };
6860
+ const PRE_TOOL_USE = {
6861
+ fields: {
6862
+ permission: permission("allow", "deny", "ask"),
6863
+ user_message: stringValue,
6864
+ agent_message: stringValue,
6865
+ updated_input: recordValue,
6866
+ additional_context: stringValue
6867
+ },
6868
+ required: ["permission"]
6869
+ };
6870
+ /**
6871
+ * Exact native stdout field set Cursor 3.18.25 reads per hook event.
6872
+ * Nothing beyond this list is invented: any additional key on a candidate
6873
+ * value fails {@link isNativeCursorResponse} in native-response.ts.
6874
+ */
6875
+ const NATIVE_SCHEMAS = {
6876
+ sessionStart: { fields: {
6877
+ env: stringRecord,
6878
+ additional_context: stringValue,
6879
+ continue: booleanValue,
6880
+ user_message: stringValue
6881
+ } },
6882
+ sessionEnd: EMPTY,
6883
+ beforeSubmitPrompt: {
6884
+ fields: {
6885
+ continue: booleanValue,
6886
+ user_message: stringValue,
6887
+ additional_context: stringValue
6888
+ },
6889
+ required: ["continue"]
6890
+ },
6891
+ preCompact: { fields: { user_message: stringValue } },
6892
+ subagentStart: {
6893
+ fields: {
6894
+ permission: permission("allow", "deny"),
6895
+ user_message: stringValue
6896
+ },
6897
+ required: ["permission"]
6898
+ },
6899
+ subagentStop: FOLLOWUP,
6900
+ preToolUse: PRE_TOOL_USE,
6901
+ postToolUse: { fields: {
6902
+ updated_mcp_tool_output: recordValue,
6903
+ additional_context: stringValue
6904
+ } },
6905
+ postToolUseFailure: { fields: { additional_context: stringValue } },
6906
+ beforeShellExecution: PERMISSION_ASK,
6907
+ afterShellExecution: EMPTY,
6908
+ beforeMCPExecution: PERMISSION_ASK,
6909
+ afterMCPExecution: EMPTY,
6910
+ beforeReadFile: {
6911
+ fields: {
6912
+ permission: permission("allow", "deny"),
6913
+ user_message: stringValue
6914
+ },
6915
+ required: ["permission"]
6916
+ },
6917
+ afterFileEdit: EMPTY,
6918
+ beforeTabFileRead: {
6919
+ fields: { permission: permission("allow", "deny") },
6920
+ required: ["permission"]
6921
+ },
6922
+ afterTabFileEdit: EMPTY,
6923
+ afterAgentResponse: EMPTY,
6924
+ afterAgentThought: EMPTY,
6925
+ stop: FOLLOWUP,
6926
+ workspaceOpen: { fields: { pluginPaths: stringArray } }
6927
+ };
6928
+ //#endregion
6929
+ //#region src/adapters/cursor/native-response.ts
6930
+ /**
6931
+ * Check that every enumerable own key of `value` is a documented field for
6932
+ * `eventName` and passes its validator, and that every required field is
6933
+ * present. Rejects prototype-polluted or exotic-shaped candidates via
6934
+ * {@link recordValue}.
6935
+ * @param value - Parsed JSON candidate.
6936
+ * @param eventName - The Cursor hook event the candidate would answer.
6937
+ */
6938
+ function isNativeCursorResponse(value, eventName) {
6939
+ try {
6940
+ if (!recordValue(value)) return false;
6941
+ const schema = NATIVE_SCHEMAS[eventName];
6942
+ if (!schema) return false;
6943
+ const descriptors = Object.getOwnPropertyDescriptors(value);
6944
+ if (schema.required?.some((field) => !Object.hasOwn(descriptors, field))) return false;
6945
+ return Reflect.ownKeys(descriptors).every((field) => {
6946
+ if (typeof field !== "string") return false;
6947
+ const descriptor = descriptors[field];
6948
+ if (!Object.hasOwn(schema.fields, field)) return false;
6949
+ const validate = schema.fields[field];
6950
+ return descriptor?.enumerable === true && "value" in descriptor && validate !== void 0 && validate(descriptor.value);
6951
+ });
6952
+ } catch {
6953
+ return false;
6954
+ }
6955
+ }
6956
+ /** Preserve raw stdout only when JSON.parse proves a documented native Cursor response. */
6957
+ function parseNativeCursorStdout(stdout, eventName) {
6958
+ if (typeof stdout !== "string") return null;
6959
+ try {
6960
+ return isNativeCursorResponse(JSON.parse(stdout), eventName) ? stdout : null;
6961
+ } catch {
6962
+ return null;
6963
+ }
6964
+ }
6965
+ //#endregion
6966
+ //#region src/adapters/cursor/context-limit.ts
6967
+ /**
6968
+ * @module context-limit
6969
+ * Cursor 3.18.25's `hooks-carriers` drops an `additional_context` carrier
6970
+ * once `o.length>1e4` — but `o` is the MERGED text of every hook's
6971
+ * `additional_context` for that event (concatenated with `"\n\n---\n\n"`
6972
+ * before the 10,000-char check), not this harness's response in isolation.
6973
+ * Capping our own contribution at {@link ADDITIONAL_CONTEXT_LIMIT} is
6974
+ * therefore the LAST-RESORT guard, not the real protection: on its own it
6975
+ * only proves OUR piece stays under 10,000, while the total across every
6976
+ * hook plugin configured on the same event can still exceed it and get
6977
+ * dropped wholesale — measured at ~8,400 chars on `sessionStart` from core
6978
+ * plugins alone, close enough to the ceiling that one more plugin tips it
6979
+ * over. The actual protection is the cross-process shared budget registry
6980
+ * in `./context-budget.ts` (Cursor id only), which reserves a slice of the
6981
+ * 10,000 ceiling per (session, event, generation) key BEFORE calling
6982
+ * {@link truncateAdditionalContext} here with the reserved amount instead of
6983
+ * the flat {@link ADDITIONAL_CONTEXT_LIMIT} — this module stays a pure,
6984
+ * budget-agnostic primitive so it keeps working unbudgeted (its historical,
6985
+ * still-correct behavior) wherever no budget context is available. The
6986
+ * limit unit is UTF-16 code units (`String.prototype.length`), matching
6987
+ * `value.length` here exactly. Only 5 events carry `additional_context`
6988
+ * through this carrier — sessionStart, beforeSubmitPrompt, preToolUse,
6989
+ * postToolUse, postToolUseFailure — subagentStart/subagentStop use a
6990
+ * different, unlimited channel. "Drops silently" also only holds when no
6991
+ * `failClosed: true` hook is declared on that step/tool: with one declared,
6992
+ * an oversized carrier REJECTS the tool call instead of being dropped quiet.
6993
+ */
6994
+ /** Cursor's hard `additional_context` character ceiling. */
6995
+ const ADDITIONAL_CONTEXT_LIMIT = 1e4;
6996
+ /** Suffix appended by {@link truncateAdditionalContext} once a value is cut. */
6997
+ const TRUNCATION_MARKER = "\n[fuse-harness] additional_context truncated to Cursor's 10000-char limit";
6998
+ function isPlainObject(value) {
6999
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7000
+ }
7001
+ /**
7002
+ * Truncate a string to end with {@link TRUNCATION_MARKER} once its length
7003
+ * exceeds `limit`. Leaves shorter values untouched. Idempotent under a
7004
+ * SHRINKING `limit` across repeated calls (e.g. an unbudgeted flat-cap pass
7005
+ * followed by a budgeted re-cap of the same stdout — see `./respond.ts`'s
7006
+ * `toCursorLifecycleResponse` doc): when `value` already ends with
7007
+ * {@link TRUNCATION_MARKER}, that marker is stripped BEFORE re-slicing so the
7008
+ * result carries exactly one marker instead of risking a duplicated/cut one.
7009
+ * @param value - Candidate `additional_context` body.
7010
+ * @param limit - Effective ceiling for this call (defaults to the flat
7011
+ * {@link ADDITIONAL_CONTEXT_LIMIT}; a shared-budget caller passes a smaller,
7012
+ * per-reservation value instead).
7013
+ */
7014
+ function truncateAdditionalContext(value, limit = ADDITIONAL_CONTEXT_LIMIT) {
7015
+ const alreadyMarked = value.endsWith(TRUNCATION_MARKER);
7016
+ if (!alreadyMarked && value.length <= limit) return value;
7017
+ if (limit <= 73) return TRUNCATION_MARKER.slice(0, Math.max(0, limit));
7018
+ return (alreadyMarked ? value.slice(0, value.length - 73) : value).slice(0, limit - 73) + TRUNCATION_MARKER;
7019
+ }
7020
+ /**
7021
+ * Length of a Cursor stdout JSON's `additional_context` string field, or 0
7022
+ * when the stdout is not JSON, has no such field, or that field isn't a
7023
+ * string.
7024
+ * @param stdout - A native Cursor JSON stdout candidate.
7025
+ */
7026
+ function additionalContextLength(stdout) {
7027
+ let parsed;
7028
+ try {
7029
+ parsed = JSON.parse(stdout);
7030
+ } catch {
7031
+ return 0;
7032
+ }
7033
+ return isPlainObject(parsed) && typeof parsed.additional_context === "string" ? parsed.additional_context.length : 0;
7034
+ }
7035
+ /**
7036
+ * Re-serialize a Cursor stdout string with its `additional_context` field
7037
+ * dropped entirely — used once the shared budget has no room left even for
7038
+ * a truncated marker. Returns the input byte-for-byte unchanged when it is
7039
+ * not JSON or has no string `additional_context` field.
7040
+ * @param stdout - A native Cursor JSON stdout candidate.
7041
+ */
7042
+ function omitAdditionalContext(stdout) {
7043
+ let parsed;
7044
+ try {
7045
+ parsed = JSON.parse(stdout);
7046
+ } catch {
7047
+ return stdout;
7048
+ }
7049
+ if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
7050
+ const { additional_context: _omitted, ...rest } = parsed;
7051
+ return JSON.stringify(rest);
7052
+ }
7053
+ /**
7054
+ * Re-serialize a Cursor stdout string with its `additional_context` field
7055
+ * capped at `limit` characters. Returns the input byte-for-byte unchanged
7056
+ * when it is not JSON, has no string `additional_context` field, or that
7057
+ * field is already within the limit — so callers can wrap every return path
7058
+ * unconditionally.
7059
+ * @param stdout - A native Cursor JSON stdout candidate.
7060
+ * @param limit - Effective ceiling for this call (see {@link truncateAdditionalContext}).
7061
+ */
7062
+ function capAdditionalContext(stdout, limit = ADDITIONAL_CONTEXT_LIMIT) {
7063
+ let parsed;
7064
+ try {
7065
+ parsed = JSON.parse(stdout);
7066
+ } catch {
7067
+ return stdout;
7068
+ }
7069
+ if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
7070
+ const truncated = truncateAdditionalContext(parsed.additional_context, limit);
7071
+ if (truncated === parsed.additional_context) return stdout;
7072
+ return JSON.stringify({
7073
+ ...parsed,
7074
+ additional_context: truncated
7075
+ });
7076
+ }
7077
+ //#endregion
7078
+ //#region src/adapters/cursor/context-budget.ts
7079
+ /**
7080
+ * @module context-budget
7081
+ * Cursor-only shared `additional_context` budget registry. Cursor 3.18.25
7082
+ * runs every hook plugin configured on an event in its OWN process, then
7083
+ * merges their `additional_context` outputs with a 9-char `"\n\n---\n\n"`
7084
+ * separator and drops the WHOLE merge past 10,000 UTF-16 units — so no
7085
+ * single process can know the total by itself. This module gives every
7086
+ * plugin's process a shared, best-effort view of that total via a small
7087
+ * JSON registry file under the project's state dir (see `../../runtime/paths.ts`),
7088
+ * keyed by `${sessionId}|${event}|${generationId ?? ""}|${toolUseId ?? ""}`
7089
+ * (one key per merge group — Cursor merges preToolUse/postToolUse/
7090
+ * postToolUseFailure PER TOOL CALL, so `toolUseId` joins the key on those
7091
+ * three events), with entries older than 10s ignored (concurrent hooks on one
7092
+ * event fire within the same second). Best-effort, fail-open throughout: any
7093
+ * I/O or JSON error degrades to "no shared budget", i.e. the flat
7094
+ * per-response cap in `./context-limit.ts` alone — never a thrown error, and
7095
+ * never a Cursor-side regression.
7096
+ */
7097
+ const REGISTRY_FILE = "cursor-context-budget.json";
7098
+ /** Matches Cursor 3.18.25's observed `"\n\n---\n\n"` merge separator length. */
7099
+ const SEPARATOR_LENGTH = 9;
7100
+ const ENTRY_WINDOW_MS = 1e4;
7101
+ /** Below this, a truncated value would carry more marker than budget — omit the field instead. */
7102
+ const OMIT_THRESHOLD = 173;
7103
+ function isRegistry(value) {
7104
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7105
+ }
7106
+ function registryPath(stateDir) {
7107
+ return join(stateDir, REGISTRY_FILE);
7108
+ }
7109
+ function budgetKey(ctx) {
7110
+ return `${ctx.sessionId}|${ctx.event}|${ctx.generationId ?? ""}|${ctx.toolUseId ?? ""}`;
7111
+ }
7112
+ function loadRegistry(path) {
7113
+ try {
7114
+ if (!existsSync(path)) return {};
7115
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
7116
+ return isRegistry(parsed) ? parsed : {};
7117
+ } catch {
7118
+ return {};
7119
+ }
7120
+ }
7121
+ function freshEntries(entries, now) {
7122
+ return (entries ?? []).filter((entry) => now - entry.at <= ENTRY_WINDOW_MS);
7123
+ }
7124
+ /** Sum of a key's fresh entry lengths plus the separators already joining them. */
7125
+ function consumed(entries) {
7126
+ return entries.reduce((total, entry) => total + entry.length, 0) + SEPARATOR_LENGTH * Math.max(0, entries.length - 1);
7127
+ }
7128
+ /**
7129
+ * Reserve room in the shared budget for one hook's `additional_context`
7130
+ * contribution to one (session, event, generation) merge group. `wanted` is
7131
+ * accepted for a symmetric call shape with {@link recordAdditionalContext}
7132
+ * but does not shrink `allowed` itself — the ceiling only depends on what
7133
+ * OTHER entries already hold; a smaller `wanted` simply means the caller
7134
+ * won't need all of it. Best-effort, fail-open: any I/O/JSON error returns
7135
+ * the full flat ceiling, as if no other plugin had run.
7136
+ * @param input - Registry location, reservation key, and the wanted length.
7137
+ */
7138
+ function reserveAdditionalContext(input) {
7139
+ try {
7140
+ const now = input.now ?? Date.now();
7141
+ const fresh = freshEntries(loadRegistry(registryPath(input.stateDir))[budgetKey(input)], now);
7142
+ const separator = fresh.length > 0 ? SEPARATOR_LENGTH : 0;
7143
+ return { allowed: Math.max(0, ADDITIONAL_CONTEXT_LIMIT - consumed(fresh) - separator) };
7144
+ } catch {
7145
+ return { allowed: ADDITIONAL_CONTEXT_LIMIT };
7146
+ }
7147
+ }
7148
+ /**
7149
+ * Record the length actually emitted for one reservation, best-effort.
7150
+ * Prunes every key's stale entries while it holds the write so the registry
7151
+ * file stays bounded. Silently no-ops on any I/O error (fail-open).
7152
+ * @param input - Registry location, reservation key, and the emitted length.
7153
+ */
7154
+ function recordAdditionalContext(input) {
7155
+ try {
7156
+ const now = input.now ?? Date.now();
7157
+ const path = registryPath(input.stateDir);
7158
+ const registry = loadRegistry(path);
7159
+ const pruned = {};
7160
+ for (const [key, entries] of Object.entries(registry)) {
7161
+ const fresh = freshEntries(entries, now);
7162
+ if (fresh.length > 0) pruned[key] = fresh;
7163
+ }
7164
+ const key = budgetKey(input);
7165
+ pruned[key] = [...pruned[key] ?? [], {
7166
+ at: now,
7167
+ length: input.emitted
7168
+ }];
7169
+ atomicWrite(path, JSON.stringify(pruned));
7170
+ } catch {}
7171
+ }
7172
+ /**
7173
+ * Cap a Cursor stdout JSON's `additional_context` against the shared budget
7174
+ * instead of the flat per-response ceiling alone. Falls back to the plain
7175
+ * cap (`./context-limit.ts`), unbudgeted, when `budget` is `undefined` or
7176
+ * the stdout carries no `additional_context` at all.
7177
+ * @param stdout - A native Cursor JSON stdout candidate.
7178
+ * @param budget - Shared budget context, or `undefined` to skip it.
7179
+ */
7180
+ function capAdditionalContextWithBudget(stdout, budget) {
7181
+ if (!budget) return capAdditionalContext(stdout);
7182
+ const wanted = additionalContextLength(stdout);
7183
+ if (wanted === 0) return stdout;
7184
+ const { allowed } = reserveAdditionalContext({
7185
+ ...budget,
7186
+ wanted
7187
+ });
7188
+ if (allowed < OMIT_THRESHOLD) {
7189
+ process.stderr.write(`[fuse-harness] cursor: additional_context budget exhausted for ${budget.event} (allowed=${allowed})\n`);
7190
+ return omitAdditionalContext(stdout);
7191
+ }
7192
+ const capped = capAdditionalContext(stdout, Math.min(ADDITIONAL_CONTEXT_LIMIT, allowed));
7193
+ recordAdditionalContext({
7194
+ ...budget,
7195
+ emitted: additionalContextLength(capped)
7196
+ });
7197
+ return capped;
7198
+ }
7199
+ //#endregion
7200
+ //#region src/adapters/cursor/respond.ts
7201
+ const AGENT_MESSAGE_EVENTS = /* @__PURE__ */ new Set([
7202
+ "preToolUse",
7203
+ "beforeShellExecution",
7204
+ "beforeMCPExecution"
7205
+ ]);
7206
+ const USER_MESSAGE_EVENTS = /* @__PURE__ */ new Set([
7207
+ "preToolUse",
7208
+ "beforeShellExecution",
7209
+ "beforeMCPExecution",
7210
+ "beforeReadFile",
7211
+ "subagentStart"
7212
+ ]);
7213
+ function permissionMessages(eventName, userMessage, agentMessage) {
7214
+ return {
7215
+ ...userMessage && USER_MESSAGE_EVENTS.has(eventName) ? { user_message: userMessage } : {},
7216
+ ...agentMessage && AGENT_MESSAGE_EVENTS.has(eventName) ? { agent_message: agentMessage } : {}
7217
+ };
7218
+ }
7219
+ function joinMessages(...values) {
7220
+ return values.filter((value) => typeof value === "string" && value.length > 0).join("\n");
7221
+ }
7222
+ function isRecord(value) {
7223
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7224
+ }
7225
+ /**
7226
+ * Render a portable policy prompt using the native Cursor event contract.
7227
+ * Switches exhaustively on {@link CursorResponseKind} — the `never` default
7228
+ * fails to compile if a new kind is ever added without a matching case.
7229
+ * `contract.known === false` is not tested separately: the single
7230
+ * `UNKNOWN_EVENT` fallback in events.ts always pairs `known: false` with
7231
+ * `response: "neutral"`, so both collapse to the same `"{}"` branch.
7232
+ */
7233
+ function toCursorResponse(prompt, eventName) {
7234
+ const contract = cursorEventContract(eventName);
7235
+ const message = formatPrompt(prompt);
7236
+ switch (contract.response) {
7237
+ case "neutral":
7238
+ case "plugin-paths": return "{}";
7239
+ case "post-context":
7240
+ case "session-context": return capAdditionalContext(JSON.stringify({ additional_context: message }));
7241
+ case "followup": return JSON.stringify({ followup_message: message });
7242
+ case "compact-notice": return JSON.stringify({ user_message: prompt.userMessage ?? message });
7243
+ case "submit-control": return JSON.stringify({
7244
+ continue: prompt.kind !== "block",
7245
+ user_message: prompt.userMessage ?? message
7246
+ });
7247
+ case "permission": {
7248
+ if (prompt.kind === "inform") return JSON.stringify({
7249
+ permission: "allow",
7250
+ ...permissionMessages(eventName, prompt.userMessage, prompt.reason ? message : void 0)
7251
+ });
7252
+ const userMessage = prompt.kind === "ask" ? `[downgraded from ask — Cursor does not enforce approval for this event]\n${message}` : message;
7253
+ return JSON.stringify({
7254
+ permission: "deny",
7255
+ ...permissionMessages(eventName, userMessage, userMessage)
7256
+ });
7257
+ }
7258
+ default: return contract.response;
7259
+ }
7260
+ }
7261
+ /**
7262
+ * Convert a shared lifecycle handler's output to the native Cursor envelope.
7263
+ * The `neutral` and empty-`text` short circuits run before the switch (they
7264
+ * apply identically across several {@link CursorResponseKind} values), so
7265
+ * only the remaining 7 kinds need a case — `never` below still catches a
7266
+ * future kind added without updating this function. This is the single
7267
+ * point every Cursor stdout passes through exactly once (see `handle.ts`'s
7268
+ * `handleHook`), so `budget` — when supplied — is reserved from and
7269
+ * recorded into here, never at the inner `toCursorResponse` pre-cap (that
7270
+ * one's output is re-capped here again on the native-passthrough branch
7271
+ * below, so budgeting it too would double-count the same contribution).
7272
+ * @param stdout - The shared handler's raw stdout for this hook invocation.
7273
+ * @param eventName - Cursor's raw `hook_event_name`.
7274
+ * @param budget - Shared `additional_context` budget context (see
7275
+ * {@link CursorBudgetContext}); `undefined` falls back to the flat
7276
+ * per-response 10,000-char cap, unbudgeted.
7277
+ */
7278
+ function toCursorLifecycleResponse(stdout, eventName, budget) {
7279
+ const contract = cursorEventContract(eventName);
7280
+ const native = parseNativeCursorStdout(stdout, eventName);
7281
+ if (native !== null) return capAdditionalContextWithBudget(native, budget);
7282
+ let text = stdout;
7283
+ let decision;
7284
+ let userMessage = "";
7285
+ let agentMessage = "";
7286
+ let decisionMessage = "";
7287
+ let structured = false;
7288
+ try {
7289
+ const parsed = JSON.parse(stdout);
7290
+ const subagentAsk = eventName === "subagentStart" && isRecord(parsed) && Object.hasOwn(parsed, "permission") && parsed.permission === "ask";
7291
+ const shared = isRecord(parsed) ? parsed : {};
7292
+ structured = true;
7293
+ decision = shared.hookSpecificOutput?.permissionDecision ?? (subagentAsk ? "ask" : void 0);
7294
+ userMessage = shared.systemMessage ?? (subagentAsk && Object.hasOwn(shared, "user_message") && typeof shared.user_message === "string" ? shared.user_message : "");
7295
+ decisionMessage = joinMessages(shared.hookSpecificOutput?.permissionDecisionReason, shared.reason);
7296
+ agentMessage = joinMessages(shared.hookSpecificOutput?.additionalContext, decisionMessage);
7297
+ text = joinMessages(agentMessage, userMessage, shared.followup_message);
7298
+ } catch {
7299
+ text = stdout;
7300
+ }
7301
+ if (contract.response === "neutral") return "{}";
7302
+ if (!text) return contract.response === "permission" ? "{\"permission\":\"allow\"}" : "{}";
7303
+ switch (contract.response) {
7304
+ case "session-context":
7305
+ case "post-context": return capAdditionalContextWithBudget(JSON.stringify({ additional_context: text }), budget);
7306
+ case "permission": {
7307
+ const permission = decision === "deny" || decision === "ask" ? "deny" : "allow";
7308
+ const denied = permission === "deny";
7309
+ if (eventName === "subagentStart" && !denied) return "{\"permission\":\"allow\"}";
7310
+ return JSON.stringify({
7311
+ permission,
7312
+ ...permissionMessages(eventName, userMessage || (denied ? decisionMessage || agentMessage : ""), agentMessage || (denied ? decisionMessage || userMessage : structured ? "" : text))
7313
+ });
7314
+ }
7315
+ case "followup": return JSON.stringify({ followup_message: text });
7316
+ case "compact-notice": return JSON.stringify({ user_message: text });
7317
+ case "submit-control": return JSON.stringify({
7318
+ continue: true,
7319
+ user_message: text
7320
+ });
7321
+ case "plugin-paths": return "{}";
7322
+ default: return contract.response;
7323
+ }
7324
+ }
7325
+ //#endregion
6649
7326
  //#region src/runtime/lifecycle-bridge.ts
6650
7327
  /** Raw event name from a payload (Cline lacks one; lifecycle is Claude-only). */
6651
7328
  function rawEvent(payload) {
6652
7329
  return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
6653
7330
  }
6654
7331
  /** Extract the `additionalContext` body from a `contextResponse(...)` stdout string ("" when empty/unparseable). */
6655
- function additionalContextOf(stdout) {
7332
+ function additionalContextOf$1(stdout) {
6656
7333
  if (!stdout) return "";
6657
7334
  try {
6658
7335
  return JSON.parse(stdout).hookSpecificOutput?.additionalContext ?? "";
@@ -6673,14 +7350,30 @@ function additionalContextOf(stdout) {
6673
7350
  * @returns The native stdout, or `null` when unhandled.
6674
7351
  */
6675
7352
  function lifecycleStdout(payload, cwd, scope, now, id = "claude-code") {
6676
- return dispatchLifecycle({
6677
- event: rawEvent(payload),
7353
+ const wireEvent = rawEvent(payload);
7354
+ if (id !== "cursor") return dispatchLifecycle({
7355
+ event: wireEvent,
6678
7356
  payload,
6679
7357
  cwd,
6680
7358
  scope,
6681
7359
  now,
6682
7360
  id
6683
7361
  });
7362
+ const lifecycleEvent = cursorEventContract(wireEvent).lifecycle;
7363
+ if (lifecycleEvent === null) return null;
7364
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : payload.conversation_id;
7365
+ const stdout = dispatchLifecycle({
7366
+ event: lifecycleEvent,
7367
+ payload: {
7368
+ ...payload,
7369
+ session_id: sessionId
7370
+ },
7371
+ cwd,
7372
+ scope,
7373
+ now,
7374
+ id
7375
+ });
7376
+ return stdout === null ? null : toCursorLifecycleResponse(stdout, wireEvent);
6684
7377
  }
6685
7378
  /**
6686
7379
  * Post-edit additions for core-scope PostToolUse: auto-document a Read of a
@@ -6702,7 +7395,7 @@ async function postEditContext(scope, event, now, id = "claude-code") {
6702
7395
  const sniper = trackSessionChanges(event.sessionId, event.filePath, void 0, now);
6703
7396
  const lint = postEditTypescript(event.filePath);
6704
7397
  if (!sniper || !lint) return sniper || lint;
6705
- return contextResponse("PostToolUse", `${additionalContextOf(sniper)}\n\n${additionalContextOf(lint)}`);
7398
+ return contextResponse("PostToolUse", `${additionalContextOf$1(sniper)}\n\n${additionalContextOf$1(lint)}`);
6706
7399
  }
6707
7400
  //#endregion
6708
7401
  //#region src/policy/trivial-edits.ts
@@ -7694,11 +8387,37 @@ const TRIVIAL_BUDGET = 4;
7694
8387
  * {@link withDenyLoop} rewrite an identical retried deny (decision unchanged).
7695
8388
  */
7696
8389
  async function gate(input) {
7697
- const prompt = await runGates(input);
8390
+ return finalizeGate(input, await runGates(input), input.command);
8391
+ }
8392
+ /**
8393
+ * Gate independent Cursor command candidates without merging their shell syntax.
8394
+ * Stateful outcome bookkeeping runs once, against the command that decided the
8395
+ * aggregate result. A single distinct candidate follows {@link gate} verbatim.
8396
+ */
8397
+ async function gateCommandCandidates(input, candidates) {
8398
+ const distinct = [...new Set(candidates)];
8399
+ if (distinct.length <= 1) return gate(input);
8400
+ let decisive;
8401
+ for (const command of distinct) {
8402
+ const prompt = await runGates({
8403
+ ...input,
8404
+ command
8405
+ });
8406
+ if (!prompt) continue;
8407
+ if (!decisive || promptRank(prompt) > promptRank(decisive.prompt)) decisive = {
8408
+ prompt,
8409
+ command
8410
+ };
8411
+ if (prompt.kind === "block") break;
8412
+ }
8413
+ return finalizeGate(input, decisive?.prompt ?? null, decisive?.command ?? input.command);
8414
+ }
8415
+ /** Persist one final gate outcome and apply deny-loop enrichment exactly once. */
8416
+ function finalizeGate(input, prompt, command) {
7698
8417
  const op = {
7699
8418
  filePath: input.filePath,
7700
8419
  content: input.content,
7701
- command: input.command
8420
+ command
7702
8421
  };
7703
8422
  const dir = dirname(input.trackFile);
7704
8423
  recordOneShot(prompt, op, {
@@ -7713,6 +8432,10 @@ async function gate(input) {
7713
8432
  sessionId: input.sessionId
7714
8433
  });
7715
8434
  }
8435
+ /** Higher numeric values dominate while equal values preserve wire order. */
8436
+ function promptRank(prompt) {
8437
+ return prompt.kind === "block" ? 3 : prompt.kind === "ask" ? 2 : 1;
8438
+ }
7716
8439
  /** Stateless guards, then the trivial fast path, then the stateful APEX gates. */
7717
8440
  async function runGates(input) {
7718
8441
  const precommit = preCommitGate(input.tool, input.command, input.cwd);
@@ -7900,8 +8623,8 @@ async function recordActivity(file, activity) {
7900
8623
  * - gemini-cli/cline: their real hook schemas have no interactive "ask"
7901
8624
  * state (deny is the only blocking outcome), so `ask` and `inform` already
7902
8625
  * both resolve to non-blocking context injection — unchanged.
7903
- * - cursor: `ask` keeps its current best-effort `permission:"ask"` shape;
7904
- * only `inform` is fixed to a non-blocking `permission:"allow"` note.
8626
+ * - cursor: `ask` degrades to a native `permission:"deny"` because Cursor does
8627
+ * not reliably apply `ask` on `preToolUse`; `inform` stays non-blocking.
7905
8628
  * - hermes: delegated to the adapter's `toHermesResponse` — `block` ->
7906
8629
  * `{decision:"block",reason}`; `ask`/`inform` degrade to non-blocking
7907
8630
  * `{context}` (Hermes has no interactive "ask" state).
@@ -7955,25 +8678,7 @@ function respond(id, prompt, event = "PreToolUse") {
7955
8678
  systemMessage: userMessage
7956
8679
  });
7957
8680
  return JSON.stringify({ hookSpecificOutput: { additionalContext: message } });
7958
- case "cursor":
7959
- if (kind === "inform") {
7960
- if (userMessage) return JSON.stringify({
7961
- permission: "allow",
7962
- user_message: userMessage,
7963
- ...reason ? { agent_message: message } : {}
7964
- });
7965
- return JSON.stringify({
7966
- permission: "allow",
7967
- user_message: message,
7968
- agent_message: message
7969
- });
7970
- }
7971
- return JSON.stringify({
7972
- permission: kind === "block" ? "deny" : "ask",
7973
- continue: false,
7974
- user_message: message,
7975
- agent_message: message
7976
- });
8681
+ case "cursor": return toCursorResponse(prompt, event === "PreToolUse" ? "preToolUse" : event);
7977
8682
  case "hermes": return toHermesResponse(prompt);
7978
8683
  case "kimi": return toKimiResponse(prompt);
7979
8684
  case "cline":
@@ -8658,8 +9363,8 @@ function segments(command) {
8658
9363
  }
8659
9364
  /** Strip a redirection (`>`, `>>`, `<`, `2>`, `&>`, …) and everything after — its target is WRITTEN, not read. */
8660
9365
  function beforeRedirect(segment) {
8661
- const m = segment.match(/\s(?:\d*>{1,2}|<|&>)\s*\S/);
8662
- return m ? segment.slice(0, m.index) : segment;
9366
+ const first = shellOutputRedirects(segment)[0];
9367
+ return first ? segment.slice(0, first.start) : segment;
8663
9368
  }
8664
9369
  /** Naive shell tokenizer: whitespace-split, stripping one matching layer of quotes per token. */
8665
9370
  function tokenize(segment) {
@@ -9155,7 +9860,7 @@ async function allowOutcome(id, event, payload, mcpDir, cwd, evidence, corpusRoo
9155
9860
  stdout: respond(id, userMessage ? {
9156
9861
  ...lesson,
9157
9862
  userMessage
9158
- } : lesson),
9863
+ } : lesson, event.eventName ?? "PreToolUse"),
9159
9864
  exit: 0
9160
9865
  };
9161
9866
  }
@@ -9170,12 +9875,12 @@ async function allowOutcome(id, event, payload, mcpDir, cwd, evidence, corpusRoo
9170
9875
  title: "APEX freshness",
9171
9876
  reason: "",
9172
9877
  userMessage
9173
- }),
9878
+ }, event.eventName ?? "PreToolUse"),
9174
9879
  exit: 0
9175
9880
  };
9176
9881
  }
9177
9882
  return {
9178
- stdout: notice ? respond(id, notice) : "",
9883
+ stdout: notice ? respond(id, notice, event.eventName ?? "PreToolUse") : "",
9179
9884
  exit: 0
9180
9885
  };
9181
9886
  }
@@ -9316,7 +10021,7 @@ function getPendingDeny(sessionIdRaw, home = homedir()) {
9316
10021
  //#endregion
9317
10022
  //#region src/runtime/confirm/confirm-state.ts
9318
10023
  /** Confirmation freshness window (G2) — 5 minutes, matches the validated prototype. */
9319
- const TTL_MS = 300 * 1e3;
10024
+ const TTL_MS$1 = 300 * 1e3;
9320
10025
  /**
9321
10026
  * Pose a confirmation token for this session's pending action. No-op while
9322
10027
  * {@link isSubagentActive} (G0, confirm-subagent.ts) — the sole gate that
@@ -9356,7 +10061,7 @@ function consumeConfirmToken(sessionIdRaw, hash, now, home = homedir()) {
9356
10061
  if (!sid) return false;
9357
10062
  const tok = loadSessionState(sid, home).confirmToken;
9358
10063
  if (!tok || tok.hash !== hash) return false;
9359
- if (now - tok.ts > TTL_MS) {
10064
+ if (now - tok.ts > TTL_MS$1) {
9360
10065
  dropConfirmToken(sid, home);
9361
10066
  return false;
9362
10067
  }
@@ -9364,6 +10069,141 @@ function consumeConfirmToken(sessionIdRaw, hash, now, home = homedir()) {
9364
10069
  return true;
9365
10070
  }
9366
10071
  //#endregion
10072
+ //#region src/runtime/confirm/codex-confirm.ts
10073
+ const TTL_MS = 300 * 1e3;
10074
+ const STATE_VERSION = 1;
10075
+ function statePath(sid, home) {
10076
+ return join(sessionsDir(home), `codex-confirm-${sid}.json`);
10077
+ }
10078
+ function lockDir(sid, home) {
10079
+ return join(sessionsDir(home), ".confirm-locks", sid);
10080
+ }
10081
+ function loadState(sid, home) {
10082
+ const path = statePath(sid, home);
10083
+ if (!existsSync(path)) return {};
10084
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
10085
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
10086
+ }
10087
+ function saveState(sid, state, home) {
10088
+ mkdirSync(sessionsDir(home), {
10089
+ recursive: true,
10090
+ mode: 448
10091
+ });
10092
+ atomicWrite(statePath(sid, home), JSON.stringify(state, null, 2));
10093
+ }
10094
+ /** Canonical identity of one Codex shell action and its display token. */
10095
+ function codexAction(tool, cwd, command, now) {
10096
+ const canonicalCommand = commandToString(command);
10097
+ if (!canonicalCommand) return null;
10098
+ const identity = JSON.stringify({
10099
+ version: STATE_VERSION,
10100
+ harness: "codex",
10101
+ tool,
10102
+ cwd: resolve(cwd),
10103
+ command: canonicalCommand
10104
+ });
10105
+ return {
10106
+ hash: hashForAction(identity),
10107
+ code: displayCodeForAction(identity),
10108
+ command: canonicalCommand,
10109
+ ts: now
10110
+ };
10111
+ }
10112
+ /** Atomically authorize or reject a Codex action, recording the current denial when needed. */
10113
+ function authorizeCodexAction(sessionIdRaw, action, toolUseId, now, home = homedir()) {
10114
+ const sid = sanitizeSessionId(sessionIdRaw);
10115
+ if (!sid) return {
10116
+ allow: false,
10117
+ reason: "state-io"
10118
+ };
10119
+ try {
10120
+ const result = withTrackLockSync(lockDir(sid, home), () => {
10121
+ const state = loadState(sid, home);
10122
+ const token = state.codexConfirmToken;
10123
+ const receipt = state.codexConfirmReceipt;
10124
+ const receiptExpired = receipt !== void 0 && now - receipt.ts > TTL_MS;
10125
+ if (!receiptExpired && receipt?.hash === action.hash && receipt.toolUseId === toolUseId && toolUseId) return { allow: true };
10126
+ let reason = "no-token";
10127
+ if (token) if (token.hash !== action.hash) reason = "mismatch";
10128
+ else if (now - token.ts > TTL_MS) reason = "expired";
10129
+ else if (!toolUseId) reason = "missing-tool-use-id";
10130
+ else {
10131
+ const { codexConfirmToken: _token, codexConfirmPending: _pending, ...rest } = state;
10132
+ saveState(sid, {
10133
+ ...rest,
10134
+ codexConfirmReceipt: {
10135
+ ...action,
10136
+ toolUseId
10137
+ }
10138
+ }, home);
10139
+ return { allow: true };
10140
+ }
10141
+ else if (receipt?.hash === action.hash) reason = receiptExpired ? "expired" : "already-consumed";
10142
+ if (reason === "missing-tool-use-id") return {
10143
+ allow: false,
10144
+ reason
10145
+ };
10146
+ const pending = state.codexConfirmPending;
10147
+ const stablePending = pending?.hash === action.hash ? pending : action;
10148
+ const { codexConfirmToken: _token, codexConfirmReceipt: _receipt, ...withoutReceipt } = state;
10149
+ const rest = receiptExpired ? withoutReceipt : {
10150
+ ...withoutReceipt,
10151
+ codexConfirmReceipt: receipt
10152
+ };
10153
+ saveState(sid, {
10154
+ ...rest,
10155
+ codexConfirmPending: stablePending
10156
+ }, home);
10157
+ return {
10158
+ allow: false,
10159
+ reason
10160
+ };
10161
+ });
10162
+ return result === LOCK_FAILED ? {
10163
+ allow: false,
10164
+ reason: "state-io"
10165
+ } : result;
10166
+ } catch {
10167
+ return {
10168
+ allow: false,
10169
+ reason: "state-io"
10170
+ };
10171
+ }
10172
+ }
10173
+ /**
10174
+ * Atomically arm the last Codex denial, consuming its pending record exactly once.
10175
+ * A classified root prompt may bypass G0; classified subagent/unknown prompts fail closed.
10176
+ */
10177
+ function submitCodexConfirmation(sessionIdRaw, text, now, home = homedir(), origin) {
10178
+ const sid = sanitizeSessionId(sessionIdRaw);
10179
+ if (!sid) return;
10180
+ const refusal = /\b(non|no|stop|annule|cancel|abort|nope|laisse tomber|pas maintenant)\b/i.test(text);
10181
+ const typedCode = text.trim().match(/^confirm[ _-]*([0-9a-f]{4})$/i)?.[1];
10182
+ try {
10183
+ withTrackLockSync(lockDir(sid, home), () => {
10184
+ const state = loadState(sid, home);
10185
+ if (origin !== void 0 && origin !== "root") return;
10186
+ if (refusal) {
10187
+ const { codexConfirmPending: _pending, codexConfirmToken: _token, ...rest } = state;
10188
+ saveState(sid, rest, home);
10189
+ return;
10190
+ }
10191
+ if (!typedCode) return;
10192
+ if (origin === void 0 && isSubagentActive(sid, now, home)) return;
10193
+ const pending = state.codexConfirmPending;
10194
+ if (!pending || pending.code.toLowerCase() !== typedCode.toLowerCase()) return;
10195
+ const { codexConfirmPending: _pending, ...rest } = state;
10196
+ saveState(sid, {
10197
+ ...rest,
10198
+ codexConfirmToken: {
10199
+ ...pending,
10200
+ ts: now
10201
+ }
10202
+ }, home);
10203
+ });
10204
+ } catch {}
10205
+ }
10206
+ //#endregion
9367
10207
  //#region src/runtime/confirm/confirm-gate.ts
9368
10208
  /**
9369
10209
  * Harnesses where `respond.ts` silently downgrades `kind: "ask"` to a hard
@@ -9387,9 +10227,23 @@ const DEGRADES_ASK_TO_DENY = /* @__PURE__ */ new Set(["codex", "kimi"]);
9387
10227
  * @param now - Epoch ms.
9388
10228
  * @param home - Test-only OS home override.
9389
10229
  */
9390
- function confirmGate(id, prompt, command, sessionId, now, home) {
10230
+ function confirmGate(id, prompt, command, sessionId, now, home, codex) {
9391
10231
  if (prompt.kind !== "ask" || !DEGRADES_ASK_TO_DENY.has(id) || !command || isIrreversible(command)) return null;
9392
10232
  try {
10233
+ if (id === "codex" && codex) {
10234
+ const action = codexAction(codex.tool, codex.cwd, command, now);
10235
+ if (!action) return null;
10236
+ const verdict = authorizeCodexAction(sessionId, action, codex.toolUseId, now, home);
10237
+ if (verdict.allow) return verdict;
10238
+ const diagnostic = `rule ID: ${prompt.ruleId ?? "policy:ask"}\ncanonical command: ${action.command}\nexpected token: CONFIRM ${action.code}\nrejection: ${verdict.reason}`;
10239
+ return {
10240
+ allow: false,
10241
+ prompt: {
10242
+ ...prompt,
10243
+ reason: `${prompt.reason}\nPour autoriser, réponds : CONFIRM ${action.code}\n${diagnostic}`
10244
+ }
10245
+ };
10246
+ }
9393
10247
  const hash = hashForAction(command);
9394
10248
  if (consumeConfirmToken(sessionId, hash, now, home)) return { allow: true };
9395
10249
  const code = displayCodeForAction(command);
@@ -9430,9 +10284,19 @@ async function handlePre(ctx) {
9430
10284
  }
9431
10285
  const designBlock = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
9432
10286
  if (designBlock) return {
9433
- stdout: withDenyNotice(id, respond(id, designBlock), designBlock, event.sessionId, dirname(file), opts.now),
10287
+ stdout: withDenyNotice(id, respond(id, designBlock, event.eventName ?? "PreToolUse"), designBlock, event.sessionId, dirname(file), opts.now),
9434
10288
  exit: 0
9435
10289
  };
10290
+ if (id === "cursor" && (event.eventName === "beforeReadFile" || event.eventName === "beforeTabFileRead")) {
10291
+ const readPolicy = evaluate({
10292
+ tool: event.tool,
10293
+ filePath: event.filePath
10294
+ });
10295
+ return {
10296
+ stdout: readPolicy.prompt ? respond(id, readPolicy.prompt, event.eventName) : JSON.stringify({ permission: "allow" }),
10297
+ exit: 0
10298
+ };
10299
+ }
9436
10300
  if (opts.scope === "security") return {
9437
10301
  stdout: event.files?.length ? securityAdvisoryForPatch(event.files, opts.now) : securityAdvisory(event.tool, event.filePath ?? "", opts.now),
9438
10302
  exit: 0
@@ -9448,11 +10312,12 @@ async function handlePre(ctx) {
9448
10312
  if (event.files && event.files.length > 0) {
9449
10313
  const patchPrompt = applyPatchGate(event.files, opts.cwd);
9450
10314
  if (patchPrompt) return {
9451
- stdout: withDenyNotice(id, respond(id, patchPrompt), patchPrompt, event.sessionId, dirname(file), opts.now),
10315
+ stdout: withDenyNotice(id, respond(id, patchPrompt, event.eventName ?? "PreToolUse"), patchPrompt, event.sessionId, dirname(file), opts.now),
9452
10316
  exit: 0
9453
10317
  };
9454
10318
  }
9455
- const prompt = await gate({
10319
+ const refs = opts.refsDir ? await loadRefs(opts.refsDir) : void 0;
10320
+ const gateInput = {
9456
10321
  sessionId: event.sessionId,
9457
10322
  framework,
9458
10323
  tool: event.tool,
@@ -9460,7 +10325,7 @@ async function handlePre(ctx) {
9460
10325
  content: event.content,
9461
10326
  command: event.command,
9462
10327
  cwd: opts.cwd,
9463
- refs: opts.refsDir ? await loadRefs(opts.refsDir) : void 0,
10328
+ refs,
9464
10329
  isReplaceAll: event.input.replace_all === true,
9465
10330
  oldString: event.oldString,
9466
10331
  agentType: event.agentType,
@@ -9470,9 +10335,14 @@ async function handlePre(ctx) {
9470
10335
  trackFile: file,
9471
10336
  transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : void 0,
9472
10337
  neverApproval: id === "codex" && isBypassPermissions(event.permissionMode)
9473
- });
10338
+ };
10339
+ const prompt = id === "cursor" && (event.commandCandidates?.length ?? 0) > 1 ? await gateCommandCandidates(gateInput, event.commandCandidates) : await gate(gateInput);
9474
10340
  if (prompt) {
9475
- const confirm = confirmGate(id, prompt, event.command, event.sessionId, opts.now, opts.home);
10341
+ const confirm = confirmGate(id, prompt, event.command, event.sessionId, opts.now, opts.home, id === "codex" ? {
10342
+ tool: event.tool,
10343
+ cwd: event.cwd ?? opts.cwd,
10344
+ toolUseId: event.toolUseId
10345
+ } : void 0);
9476
10346
  if (confirm?.allow) return allowOutcome(id, event, payload, designCacheDir, opts.cwd, {
9477
10347
  trackFile: file,
9478
10348
  windowMs: opts.windowMs,
@@ -9480,7 +10350,7 @@ async function handlePre(ctx) {
9480
10350
  }, opts.corpusRoot);
9481
10351
  const finalPrompt = confirm ? confirm.prompt : prompt;
9482
10352
  return {
9483
- stdout: withDenyNotice(id, respond(id, finalPrompt), finalPrompt, event.sessionId, dirname(file), opts.now),
10353
+ stdout: withDenyNotice(id, respond(id, finalPrompt, event.eventName ?? "PreToolUse"), finalPrompt, event.sessionId, dirname(file), opts.now),
9484
10354
  exit: 0
9485
10355
  };
9486
10356
  }
@@ -9847,14 +10717,14 @@ function recordCodexPostFailure(tool, result, opts) {
9847
10717
  //#endregion
9848
10718
  //#region src/runtime/post-fanout.ts
9849
10719
  /**
9850
- * Fan a Codex `apply_patch` envelope's `event.files` into one synthetic
10720
+ * Fan Codex `apply_patch` or Cursor `afterFileEdit` `event.files` into one
9851
10721
  * per-file event per touched file, so the per-file PostToolUse gates (SOLID
9852
10722
  * size, Tailwind, tracking, post-edit context) see EACH file instead of the
9853
10723
  * whole patch (whose own `filePath`/`content` are always undefined). `add`
9854
10724
  * maps to `Write`, `update` to `Edit` (mirrors the Pre-phase `applyPatchGate`'s
9855
10725
  * tool mapping); `delete`/`move` map to a tool none of those Write|Edit-gated
9856
10726
  * checks recognize, so they no-op on it without special-casing each gate.
9857
- * Returns `[event]` unchanged for every non-`apply_patch` tool/harness.
10727
+ * Returns `[event]` unchanged when no per-file changes are present.
9858
10728
  * @param event - The normalized PostToolUse event.
9859
10729
  * @returns One event per touched file, or the original event.
9860
10730
  */
@@ -9864,7 +10734,8 @@ function fanOutFiles(event) {
9864
10734
  ...event,
9865
10735
  tool: f.op === "add" ? "Write" : f.op === "update" ? "Edit" : "apply_patch:delete",
9866
10736
  filePath: f.filePath,
9867
- content: f.op === "delete" ? void 0 : f.content
10737
+ content: f.op === "delete" ? void 0 : f.content,
10738
+ oldString: f.oldString
9868
10739
  }));
9869
10740
  }
9870
10741
  /**
@@ -9884,7 +10755,106 @@ function firstFileMatch(files, check) {
9884
10755
  return "";
9885
10756
  }
9886
10757
  //#endregion
10758
+ //#region src/runtime/post-outcome.ts
10759
+ /** Extract the additional-context body from a Claude-shaped post response. */
10760
+ function additionalContextOf(stdout) {
10761
+ if (!stdout) return "";
10762
+ try {
10763
+ return JSON.parse(stdout).hookSpecificOutput?.additionalContext ?? "";
10764
+ } catch {
10765
+ return "";
10766
+ }
10767
+ }
10768
+ function cursorPostToolOutput(eventName, designWarn, userMessage, extra) {
10769
+ if (!/^postToolUse$/i.test(eventName)) return "";
10770
+ const context = [
10771
+ designWarn ? formatPrompt(designWarn) : "",
10772
+ designWarn?.userMessage,
10773
+ userMessage,
10774
+ additionalContextOf(extra)
10775
+ ].filter(Boolean).join("\n");
10776
+ return context ? JSON.stringify({ additional_context: context }) : "";
10777
+ }
10778
+ /**
10779
+ * Assemble the final PostToolUse response after all side effects and deny paths.
10780
+ * @param input - Final assembly values captured at their original evaluation times.
10781
+ * @returns The native hook outcome.
10782
+ */
10783
+ function postOutcome(input) {
10784
+ const { id, agentId, sessionId, now, cwd, activities, files, designCacheDir, designWarn, extra, cursorAfterFileEdit, cursorEventName } = input;
10785
+ const noticeLines = [];
10786
+ for (const f of files) {
10787
+ const notice = designPassNotice({
10788
+ agentId,
10789
+ tool: f.tool,
10790
+ filePath: f.filePath ?? "",
10791
+ content: f.content ?? "",
10792
+ url: "",
10793
+ phase: "post"
10794
+ }, designCacheDir);
10795
+ if (notice?.userMessage) noticeLines.push(notice.userMessage);
10796
+ }
10797
+ const notice = noticeLines.length ? {
10798
+ kind: "inform",
10799
+ title: "Design pipeline",
10800
+ reason: "",
10801
+ userMessage: noticeLines.join("\n")
10802
+ } : null;
10803
+ const refNotice = refCreditNoticeFor(activities, sessionId, now, defaultStateDir(cwd));
10804
+ const userMessage = [notice?.userMessage, refNotice].filter(Boolean).join("\n") || void 0;
10805
+ if (cursorAfterFileEdit) return {
10806
+ stdout: "{}",
10807
+ exit: 0
10808
+ };
10809
+ if (id === "cursor") return {
10810
+ stdout: cursorPostToolOutput(cursorEventName, designWarn, userMessage, extra),
10811
+ exit: 0
10812
+ };
10813
+ if (designWarn) return {
10814
+ stdout: respond(id, userMessage ? {
10815
+ ...designWarn,
10816
+ userMessage
10817
+ } : designWarn, "PostToolUse"),
10818
+ exit: 0
10819
+ };
10820
+ if (!userMessage) return {
10821
+ stdout: extra,
10822
+ exit: 0
10823
+ };
10824
+ const withUserMessage = notice ? {
10825
+ ...notice,
10826
+ userMessage
10827
+ } : {
10828
+ kind: "inform",
10829
+ title: "Compliance",
10830
+ reason: "",
10831
+ userMessage
10832
+ };
10833
+ if (!extra) return {
10834
+ stdout: respond(id, withUserMessage, "PostToolUse"),
10835
+ exit: 0
10836
+ };
10837
+ if (id === "claude-code" || id === "codex") return {
10838
+ stdout: attachSystemMessage(extra, userMessage),
10839
+ exit: 0
10840
+ };
10841
+ if (id === "kimi") return {
10842
+ stdout: respond(id, withUserMessage, "PostToolUse"),
10843
+ exit: 0
10844
+ };
10845
+ return {
10846
+ stdout: respond(id, withUserMessage, "PostToolUse") || extra,
10847
+ exit: 0
10848
+ };
10849
+ }
10850
+ //#endregion
9887
10851
  //#region src/runtime/handle-post.ts
10852
+ function isCursorAfterFileEdit(id, payload) {
10853
+ if (id !== "cursor") return false;
10854
+ const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
10855
+ if (hookEvent) return /^afterFileEdit$/i.test(hookEvent);
10856
+ return typeof payload.file_path === "string" && Array.isArray(payload.edits);
10857
+ }
9888
10858
  /**
9889
10859
  * Run the PostToolUse pipeline: store the MCP response, emit a design warning,
9890
10860
  * record the activity into the session track, apply per-scope side-effects (SEO
@@ -9907,8 +10877,14 @@ function firstFileMatch(files, check) {
9907
10877
  */
9908
10878
  async function handlePost(ctx) {
9909
10879
  const { id, payload, event, framework, mcpDir, file, opts } = ctx;
10880
+ const cursorAfterFileEdit = isCursorAfterFileEdit(id, payload);
9910
10881
  const designCacheDir = ctx.designCacheDir ?? mcpDir;
9911
- const response = payload.tool_response ?? payload.tool_output;
10882
+ let response = payload.tool_response ?? payload.tool_output;
10883
+ if (id === "cursor" && /^afterMCPExecution$/i.test(event.eventName ?? "")) try {
10884
+ response = typeof payload.result_json === "string" ? JSON.parse(payload.result_json) : payload.result_json;
10885
+ } catch {
10886
+ response = void 0;
10887
+ }
9912
10888
  mcpPostStore(event.tool, event.input, response, mcpDir);
9913
10889
  const designWarn = designGate(payload, event, designCacheDir, opts.cwd, opts.corpusRoot);
9914
10890
  const activities = activityFor({
@@ -9932,27 +10908,27 @@ async function handlePost(ctx) {
9932
10908
  const files = fanOutFiles(event);
9933
10909
  for (const f of files) postTrackingSideEffects(opts.scope ?? "core", f, f.input, opts.now, payload, opts.cwd);
9934
10910
  const seoDeny = opts.scope === "seo" ? seoPostToolUseResponse(payload) : null;
9935
- if (seoDeny) return {
10911
+ if (seoDeny && !cursorAfterFileEdit) return {
9936
10912
  stdout: seoDeny,
9937
10913
  exit: 0
9938
10914
  };
9939
10915
  if (opts.scope === "solid") {
9940
10916
  const solidWarn = firstFileMatch(files, checkFileSize);
9941
- if (solidWarn) return {
10917
+ if (solidWarn && !cursorAfterFileEdit) return {
9942
10918
  stdout: solidWarn,
9943
10919
  exit: 0
9944
10920
  };
9945
10921
  }
9946
10922
  if (opts.scope === "tailwindcss") {
9947
10923
  const tailwindWarn = firstFileMatch(files, validateTailwind);
9948
- if (tailwindWarn) return {
10924
+ if (tailwindWarn && !cursorAfterFileEdit) return {
9949
10925
  stdout: tailwindWarn,
9950
10926
  exit: 0
9951
10927
  };
9952
10928
  }
9953
10929
  if (opts.scope === "aipilot" && (event.tool === "TaskCreate" || event.tool === "TaskUpdate" || event.tool === "Write" || event.tool === "Edit")) {
9954
10930
  const out = await aipilotPostToolUse(payload, opts.cwd, id);
9955
- if (out) return {
10931
+ if (out && !cursorAfterFileEdit) return {
9956
10932
  stdout: out,
9957
10933
  exit: 0
9958
10934
  };
@@ -9962,63 +10938,20 @@ async function handlePost(ctx) {
9962
10938
  extra = await postEditContext(opts.scope ?? "core", f, opts.now, id);
9963
10939
  if (extra) break;
9964
10940
  }
9965
- const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
9966
- const noticeLines = [];
9967
- for (const f of files) {
9968
- const n = designPassNotice({
9969
- agentId,
9970
- tool: f.tool,
9971
- filePath: f.filePath ?? "",
9972
- content: f.content ?? "",
9973
- url: "",
9974
- phase: "post"
9975
- }, designCacheDir);
9976
- if (n?.userMessage) noticeLines.push(n.userMessage);
9977
- }
9978
- const notice = noticeLines.length ? {
9979
- kind: "inform",
9980
- title: "Design pipeline",
9981
- reason: "",
9982
- userMessage: noticeLines.join("\n")
9983
- } : null;
9984
- const refNotice = refCreditNoticeFor(activities, event.sessionId, opts.now, defaultStateDir(opts.cwd));
9985
- const userMessage = [notice?.userMessage, refNotice].filter(Boolean).join("\n") || void 0;
9986
- if (designWarn) return {
9987
- stdout: respond(id, userMessage ? {
9988
- ...designWarn,
9989
- userMessage
9990
- } : designWarn, "PostToolUse"),
9991
- exit: 0
9992
- };
9993
- if (!userMessage) return {
9994
- stdout: extra,
9995
- exit: 0
9996
- };
9997
- const withUserMessage = notice ? {
9998
- ...notice,
9999
- userMessage
10000
- } : {
10001
- kind: "inform",
10002
- title: "Compliance",
10003
- reason: "",
10004
- userMessage
10005
- };
10006
- if (!extra) return {
10007
- stdout: respond(id, withUserMessage, "PostToolUse"),
10008
- exit: 0
10009
- };
10010
- if (id === "claude-code" || id === "codex") return {
10011
- stdout: attachSystemMessage(extra, userMessage),
10012
- exit: 0
10013
- };
10014
- if (id === "kimi") return {
10015
- stdout: respond(id, withUserMessage, "PostToolUse"),
10016
- exit: 0
10017
- };
10018
- return {
10019
- stdout: respond(id, withUserMessage, "PostToolUse") || extra,
10020
- exit: 0
10021
- };
10941
+ return postOutcome({
10942
+ id,
10943
+ agentId: typeof payload.agent_id === "string" ? payload.agent_id : "",
10944
+ sessionId: event.sessionId,
10945
+ now: opts.now,
10946
+ cwd: opts.cwd,
10947
+ activities,
10948
+ files,
10949
+ designCacheDir,
10950
+ designWarn,
10951
+ extra,
10952
+ cursorAfterFileEdit,
10953
+ cursorEventName: typeof payload.hook_event_name === "string" ? payload.hook_event_name : ""
10954
+ });
10022
10955
  }
10023
10956
  //#endregion
10024
10957
  //#region src/runtime/handle-scope-async.ts
@@ -10038,9 +10971,12 @@ async function handlePost(ctx) {
10038
10971
  * @returns The native stdout when intercepted, or `null` to fall through.
10039
10972
  */
10040
10973
  async function asyncScopeStdout(scope, event, payload, cwd, now, id = "claude-code") {
10041
- if (scope === "aipilot") return dispatchAipilot(event, payload, cwd, now, void 0, id);
10042
- if (scope === "memory") return dispatchMemory(event, payload, cwd, now);
10043
- return null;
10974
+ const dispatchedEvent = id === "cursor" ? cursorEventContract(event).lifecycle : event;
10975
+ if (dispatchedEvent === null) return null;
10976
+ let stdout = null;
10977
+ if (scope === "aipilot") stdout = await dispatchAipilot(dispatchedEvent, payload, cwd, now, void 0, id);
10978
+ if (scope === "memory") stdout = await dispatchMemory(dispatchedEvent, payload, cwd, now);
10979
+ return id === "cursor" && stdout !== null ? toCursorLifecycleResponse(stdout, event) : stdout;
10044
10980
  }
10045
10981
  //#endregion
10046
10982
  //#region src/runtime/design-cache-resolve.ts
@@ -10509,19 +11445,99 @@ function handleConfirmSubmit(sessionId, text, now, home) {
10509
11445
  } catch {}
10510
11446
  }
10511
11447
  //#endregion
11448
+ //#region src/runtime/confirm/codex-prompt-origin.ts
11449
+ const AGENT_FIELDS = ["agent_id", "agent_type"];
11450
+ /**
11451
+ * Classify Codex prompt provenance from its optional agent metadata.
11452
+ * Missing metadata is the observed root shape; malformed metadata fails closed.
11453
+ */
11454
+ function codexPromptOrigin(payload) {
11455
+ const present = AGENT_FIELDS.filter((field) => Object.hasOwn(payload, field));
11456
+ if (present.length === 0) return "root";
11457
+ return present.every((field) => typeof payload[field] === "string" && payload[field].trim().length > 0) ? "subagent" : "unknown";
11458
+ }
11459
+ //#endregion
10512
11460
  //#region src/runtime/handle.ts
10513
11461
  /** Raw Claude hook event name from a payload (empty when absent). */
10514
11462
  function rawEventName(payload) {
10515
11463
  return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
10516
11464
  }
10517
11465
  /**
11466
+ * `payload.tool_input` parsed into an object when it's a JSON STRING —
11467
+ * Cursor's real wire format for `beforeMCPExecution`/`afterMCPExecution`
11468
+ * (ground truth), unlike every other harness (and Cursor's own
11469
+ * `preToolUse`/`postToolUse`), which always sends it as an object already.
11470
+ * `undefined` when `tool_input` is already an object, absent, or fails to
11471
+ * parse into one (fail-open — the caller then keeps the original value).
11472
+ * @param payload - The raw hook payload.
11473
+ */
11474
+ function cursorParsedToolInput(payload) {
11475
+ const raw = payload.tool_input;
11476
+ if (typeof raw !== "string") return void 0;
11477
+ try {
11478
+ const parsed = JSON.parse(raw);
11479
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
11480
+ } catch {
11481
+ return;
11482
+ }
11483
+ }
11484
+ /**
11485
+ * `id === "cursor"` only: project the already-resolved canonical `tool_name`
11486
+ * (`event.tool`, normalized by {@link normalizeEvent}) and `cwd` (the project
11487
+ * root resolved via `cursorProjectCwd`, already applied to `opts.cwd`) onto a
11488
+ * shallow payload copy — the single passage point for every downstream
11489
+ * consumer that reads `payload.tool_name`/`payload.cwd`/`payload.tool_input`
11490
+ * RAW instead of `event.tool`/`opts.cwd`/`event.input` (lifecycle-bridge's
11491
+ * `failure-lesson.ts`/`agent-memory.ts`, handle-scope-async's aipilot/memory
11492
+ * dispatchers — including `doc-cache-gate.ts`'s `libraryOf`, which never
11493
+ * `JSON.parse`s a string `tool_input` itself — and the seo scope's
11494
+ * `post-tool-use.ts`). `tool_input` is additionally replaced by its parsed
11495
+ * object form via {@link cursorParsedToolInput} when Cursor sent it as a
11496
+ * JSON string (`beforeMCPExecution`/`afterMCPExecution`). Cursor's own wire
11497
+ * values ("Shell", `MCP:<tool>`, a bare `workspace_roots` array with no
11498
+ * `cwd` field, a stringified `tool_input`, …) are preserved under
11499
+ * `cursor_tool_name`/`cursor_cwd`/`cursor_tool_input` so nothing is lost.
11500
+ * Every other harness id is untouched (returns the SAME object,
11501
+ * byte-identical).
11502
+ * @param payload - The raw hook payload.
11503
+ * @param event - The already-normalized event (`event.tool` is canonical).
11504
+ * @param cwd - The resolved project root for this invocation.
11505
+ * @param id - Harness adapter id.
11506
+ */
11507
+ function cursorRawPayloadProjection(payload, event, cwd, id) {
11508
+ if (id !== "cursor") return payload;
11509
+ const parsedToolInput = cursorParsedToolInput(payload);
11510
+ return {
11511
+ ...payload,
11512
+ cursor_tool_name: payload.tool_name,
11513
+ cursor_cwd: payload.cwd,
11514
+ tool_name: event.tool,
11515
+ cwd,
11516
+ ...parsedToolInput ? {
11517
+ cursor_tool_input: payload.tool_input,
11518
+ tool_input: parsedToolInput
11519
+ } : {}
11520
+ };
11521
+ }
11522
+ /**
10518
11523
  * The full hook handler: on a PRE event it gates the tool-use (stateless guards
10519
11524
  * then APEX gates from the session track) and returns the native response; on a
10520
11525
  * POST event it records the activity into the track. The loop that makes the
10521
11526
  * package behave like the Claude plugin, on any harness.
10522
11527
  */
10523
- async function handleHook(id, payload, opts) {
11528
+ async function handleHookCore(id, payload, opts) {
10524
11529
  const event = normalizeEvent(id, payload);
11530
+ if (id === "cursor") {
11531
+ const cursorCwd = cursorProjectCwd(event.cwd, event.workspaceRoots ?? [], event.filePath, opts.cwd);
11532
+ if (cursorCwd !== opts.cwd) opts = {
11533
+ ...opts,
11534
+ cwd: cursorCwd
11535
+ };
11536
+ }
11537
+ const hookPayload = cursorRawPayloadProjection(payload, event, opts.cwd, id);
11538
+ const rawPrompt = payload.prompt;
11539
+ const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : void 0;
11540
+ if (id === "codex" && rawEventName(payload) === "UserPromptSubmit" && userPrompt !== void 0) submitCodexConfirmation(event.sessionId, userPrompt, opts.now, opts.home, codexPromptOrigin(payload));
10525
11541
  resetFragmentRegistry();
10526
11542
  const layout = projectLayout(opts.cwd);
10527
11543
  const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
@@ -10533,20 +11549,18 @@ async function handleHook(id, payload, opts) {
10533
11549
  exit: 0
10534
11550
  };
10535
11551
  if (id === "codex" && rawEventName(payload) === "SessionStart") resyncCodexAgents();
10536
- const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload), payload, opts.cwd, opts.now, id);
11552
+ const asyncOut = await asyncScopeStdout(opts.scope, rawEventName(payload), hookPayload, opts.cwd, opts.now, id);
10537
11553
  if (asyncOut !== null) return {
10538
11554
  stdout: asyncOut,
10539
11555
  exit: 0
10540
11556
  };
10541
- const life = lifecycleStdout(payload, opts.cwd, opts.scope ?? "core", opts.now, id);
11557
+ const life = lifecycleStdout(hookPayload, opts.cwd, opts.scope ?? "core", opts.now, id);
10542
11558
  if (life !== null) return {
10543
11559
  stdout: id === "claude-code" ? attachBudgetRecap(life, rawEventName(payload), event.sessionId, opts.cwd, opts.now) : life,
10544
11560
  exit: 0
10545
11561
  };
10546
- const rawPrompt = payload.prompt;
10547
- const userPrompt = typeof rawPrompt === "string" || Array.isArray(rawPrompt) ? promptText(rawPrompt) : void 0;
10548
11562
  if (userPrompt !== void 0) {
10549
- handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
11563
+ if (id !== "codex") handleConfirmSubmit(event.sessionId, userPrompt, opts.now, opts.home);
10550
11564
  await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
10551
11565
  return {
10552
11566
  stdout: promptSubmitContext(userPrompt, opts.cwd, id),
@@ -10555,7 +11569,7 @@ async function handleHook(id, payload, opts) {
10555
11569
  }
10556
11570
  if (event.phase === "post") return handlePost({
10557
11571
  id,
10558
- payload,
11572
+ payload: hookPayload,
10559
11573
  event,
10560
11574
  framework,
10561
11575
  mcpDir,
@@ -10565,7 +11579,7 @@ async function handleHook(id, payload, opts) {
10565
11579
  });
10566
11580
  return handlePre({
10567
11581
  id,
10568
- payload,
11582
+ payload: hookPayload,
10569
11583
  event,
10570
11584
  framework,
10571
11585
  mcpDir,
@@ -10574,5 +11588,41 @@ async function handleHook(id, payload, opts) {
10574
11588
  opts
10575
11589
  });
10576
11590
  }
11591
+ /**
11592
+ * Run one hook and adapt every Cursor scope outcome at the common runtime exit.
11593
+ * Other harnesses retain the core handler's stdout and exit status unchanged.
11594
+ * Cursor's shared `additional_context` budget context (see
11595
+ * `../adapters/cursor/context-budget.ts`) is assembled here too — this is
11596
+ * the single point every Cursor stdout passes through exactly once, so it's
11597
+ * also the single point that reserves from and records into the registry.
11598
+ * With no `session_id`/`conversation_id` at all, `sessionId` is `""` — the
11599
+ * registry key would degenerate to one bucket shared by every session-less
11600
+ * call on the same (cwd, event) pair, so `budget` stays `undefined` instead
11601
+ * (falls back to the flat per-response cap in `toCursorLifecycleResponse`,
11602
+ * with zero registry I/O). `stateDir` honors `opts.home` (test-only OS home
11603
+ * override, see `HandleOptions`) so tests never need the real `os.homedir()`.
11604
+ */
11605
+ async function handleHook(id, payload, opts) {
11606
+ const outcome = await handleHookCore(id, payload, opts);
11607
+ if (id !== "cursor") return outcome;
11608
+ const eventName = rawEventName(payload);
11609
+ const cursorEvent = normalizeEvent(id, payload);
11610
+ const cwd = cursorProjectCwd(cursorEvent.cwd, cursorEvent.workspaceRoots ?? [], cursorEvent.filePath, opts.cwd);
11611
+ const sessionId = cursorEvent.sessionId;
11612
+ const generationId = typeof payload.generation_id === "string" && payload.generation_id ? payload.generation_id : void 0;
11613
+ const toolUseId = typeof payload.tool_use_id === "string" && payload.tool_use_id ? payload.tool_use_id : void 0;
11614
+ const stateDir = join(fuseHarnessHome(opts.home), "state", projectHash$1(cwd));
11615
+ const budget = sessionId ? {
11616
+ stateDir,
11617
+ sessionId,
11618
+ event: eventName,
11619
+ generationId,
11620
+ toolUseId
11621
+ } : void 0;
11622
+ return {
11623
+ ...outcome,
11624
+ stdout: toCursorLifecycleResponse(outcome.stdout, eventName, budget)
11625
+ };
11626
+ }
10577
11627
  //#endregion
10578
- export { runningVersion as $, trackMcpResearch as A, defaultStateDir as At, generateProjectMap as B, seoPostToolUse as C, trimLogFile as Ct, securityAdvisoryForPatch as D, claudeMdKey as Dt, securityAdvisory as E, projectContext as Et, dispatchAipilot as F, loadSecurityState as Ft, countFiles as G, writeTree as H, dispatchLessons as I, saveSecurityState as It, lessonsArchiveFileFor as J, getFileDesc as K, cartoSessionStart as L, securityStateDir as Lt, trackEnrichment as M, trackFile as Mt, dispatchLifecycle as N, normalizeEvent as Nt, postTrackingSideEffects as O, promptSubmitContext as Ot, aipilotPostToolUse as P, isoUtc as Pt, runDoctor as Q, generateEcosystemMap as R, securityStatePath as Rt, postEditContext as S, removeOldFiles as St, dispatchMemory as T, gitContext as Tt, loadEnriched as U, isProject as V, mergeLines as W, lessonsStateFileFor as X, lessonsFileFor as Y, notify as Z, preCommitGate as _, readRules as _t, recordActivity as a, saveApexState as at, extractSymbols as b, pruneEmptyDirs as bt, MCP_TTL_MS as c, trackAgentMemory as ct, isMcpTool as d, validateSolidGate as dt, versionBanner as et, queryOf as f, checkFileSize as ft, gate as g, injectRules as gt, TRIVIAL_BUDGET as h, solidDetectStart as ht, respond as i, cleanupSession as it, trackSkillRead as j, projectHash$1 as jt, trackWatchResearch as k, taskContext as kt, WEBFETCH_TTL_MS as l, subagentCacheContext as lt, REQUIRED_AGENTS as m, detectSolidProfile as mt, activityFor as n, trackSessionChanges as nt, mcpPostStore as o, logToolFailure as ot, DEFAULT_WINDOW_MS as p, countLoc as pt, listChildren as q, handlePre as r, validateRulesLoaded as rt, mcpPreIntercept as s, validateTeammateOutput as st, handleHook as t, postEditTypescript as tt, cacheQueryOf as u, validateTailwind as ut, detectDuplication as v, runSessionStartCleanups as vt, seoPostToolUseResponse as w, devContext as wt, lifecycleStdout as x, purgeTtlTree as xt, dryGate as y, sessionStartCore as yt, writePluginMap as z, todayUtc as zt };
11628
+ export { runDoctor as $, trackWatchResearch as A, taskContext as At, writePluginMap as B, todayUtc as Bt, postEditContext as C, removeOldFiles as Ct, securityAdvisory as D, projectContext as Dt, dispatchMemory as E, gitContext as Et, aipilotPostToolUse as F, isoUtc as Ft, mergeLines as G, isProject as H, dispatchAipilot as I, loadSecurityState as It, listChildren as J, countFiles as K, dispatchLessons as L, saveSecurityState as Lt, trackSkillRead as M, projectHash$1 as Mt, trackEnrichment as N, trackFile as Nt, securityAdvisoryForPatch as O, claudeMdKey as Ot, dispatchLifecycle as P, normalizeEvent as Pt, notify as Q, cartoSessionStart as R, securityStateDir as Rt, lifecycleStdout as S, purgeTtlTree as St, seoPostToolUseResponse as T, devContext as Tt, writeTree as U, generateProjectMap as V, loadEnriched as W, lessonsFileFor as X, lessonsArchiveFileFor as Y, lessonsStateFileFor as Z, gateCommandCandidates as _, injectRules as _t, recordActivity as a, cleanupSession as at, dryGate as b, sessionStartCore as bt, MCP_TTL_MS as c, validateTeammateOutput as ct, isMcpTool as d, validateTailwind as dt, runningVersion as et, queryOf as f, validateSolidGate as ft, gate as g, solidDetectStart as gt, TRIVIAL_BUDGET as h, detectSolidProfile as ht, respond as i, validateRulesLoaded as it, trackMcpResearch as j, defaultStateDir as jt, postTrackingSideEffects as k, promptSubmitContext as kt, WEBFETCH_TTL_MS as l, trackAgentMemory as lt, REQUIRED_AGENTS as m, countLoc as mt, activityFor as n, postEditTypescript as nt, mcpPostStore as o, saveApexState as ot, DEFAULT_WINDOW_MS as p, checkFileSize as pt, getFileDesc as q, handlePre as r, trackSessionChanges as rt, mcpPreIntercept as s, logToolFailure as st, handleHook as t, versionBanner as tt, cacheQueryOf as u, subagentCacheContext as ut, preCommitGate as v, readRules as vt, seoPostToolUse as w, trimLogFile as wt, extractSymbols as x, pruneEmptyDirs as xt, detectDuplication as y, runSessionStartCleanups as yt, generateEcosystemMap as z, securityStatePath as zt };