@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
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @module context-budget
3
+ * Cursor-only shared `additional_context` budget registry. Cursor 3.18.25
4
+ * runs every hook plugin configured on an event in its OWN process, then
5
+ * merges their `additional_context` outputs with a 9-char `"\n\n---\n\n"`
6
+ * separator and drops the WHOLE merge past 10,000 UTF-16 units — so no
7
+ * single process can know the total by itself. This module gives every
8
+ * plugin's process a shared, best-effort view of that total via a small
9
+ * JSON registry file under the project's state dir (see `../../runtime/paths.ts`),
10
+ * keyed by `${sessionId}|${event}|${generationId ?? ""}|${toolUseId ?? ""}`
11
+ * (one key per merge group — Cursor merges preToolUse/postToolUse/
12
+ * postToolUseFailure PER TOOL CALL, so `toolUseId` joins the key on those
13
+ * three events), with entries older than 10s ignored (concurrent hooks on one
14
+ * event fire within the same second). Best-effort, fail-open throughout: any
15
+ * I/O or JSON error degrades to "no shared budget", i.e. the flat
16
+ * per-response cap in `./context-limit.ts` alone — never a thrown error, and
17
+ * never a Cursor-side regression.
18
+ */
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { atomicWrite } from "../../util/json-io";
22
+ import {
23
+ ADDITIONAL_CONTEXT_LIMIT, TRUNCATION_MARKER, additionalContextLength, capAdditionalContext, omitAdditionalContext,
24
+ } from "./context-limit";
25
+ import type { CursorBudgetContext } from "./interfaces/context-budget";
26
+
27
+ const REGISTRY_FILE = "cursor-context-budget.json";
28
+ /** Matches Cursor 3.18.25's observed `"\n\n---\n\n"` merge separator length. */
29
+ const SEPARATOR_LENGTH = 9;
30
+ const ENTRY_WINDOW_MS = 10_000;
31
+ /** Below this, a truncated value would carry more marker than budget — omit the field instead. */
32
+ const OMIT_THRESHOLD = TRUNCATION_MARKER.length + 100;
33
+
34
+ interface BudgetEntry {
35
+ at: number;
36
+ length: number;
37
+ }
38
+ type BudgetRegistry = Record<string, BudgetEntry[]>;
39
+
40
+ function isRegistry(value: unknown): value is BudgetRegistry {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+
44
+ function registryPath(stateDir: string): string {
45
+ return join(stateDir, REGISTRY_FILE);
46
+ }
47
+
48
+ function budgetKey(ctx: Pick<CursorBudgetContext, "sessionId" | "event" | "generationId" | "toolUseId">): string {
49
+ return `${ctx.sessionId}|${ctx.event}|${ctx.generationId ?? ""}|${ctx.toolUseId ?? ""}`;
50
+ }
51
+
52
+ function loadRegistry(path: string): BudgetRegistry {
53
+ try {
54
+ if (!existsSync(path)) return {};
55
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
56
+ return isRegistry(parsed) ? parsed : {};
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+
62
+ function freshEntries(entries: BudgetEntry[] | undefined, now: number): BudgetEntry[] {
63
+ return (entries ?? []).filter((entry) => now - entry.at <= ENTRY_WINDOW_MS);
64
+ }
65
+
66
+ /** Sum of a key's fresh entry lengths plus the separators already joining them. */
67
+ function consumed(entries: BudgetEntry[]): number {
68
+ return entries.reduce((total, entry) => total + entry.length, 0) + SEPARATOR_LENGTH * Math.max(0, entries.length - 1);
69
+ }
70
+
71
+ /** {@link reserveAdditionalContext} input: budget context plus the length the caller wants to emit. */
72
+ export type ReserveInput = CursorBudgetContext & { wanted: number };
73
+ /** {@link recordAdditionalContext} input: budget context plus the length actually emitted. */
74
+ export type RecordInput = CursorBudgetContext & { emitted: number };
75
+
76
+ /**
77
+ * Reserve room in the shared budget for one hook's `additional_context`
78
+ * contribution to one (session, event, generation) merge group. `wanted` is
79
+ * accepted for a symmetric call shape with {@link recordAdditionalContext}
80
+ * but does not shrink `allowed` itself — the ceiling only depends on what
81
+ * OTHER entries already hold; a smaller `wanted` simply means the caller
82
+ * won't need all of it. Best-effort, fail-open: any I/O/JSON error returns
83
+ * the full flat ceiling, as if no other plugin had run.
84
+ * @param input - Registry location, reservation key, and the wanted length.
85
+ */
86
+ export function reserveAdditionalContext(input: ReserveInput): { allowed: number } {
87
+ try {
88
+ const now = input.now ?? Date.now();
89
+ const registry = loadRegistry(registryPath(input.stateDir));
90
+ const fresh = freshEntries(registry[budgetKey(input)], now);
91
+ const separator = fresh.length > 0 ? SEPARATOR_LENGTH : 0;
92
+ return { allowed: Math.max(0, ADDITIONAL_CONTEXT_LIMIT - consumed(fresh) - separator) };
93
+ } catch {
94
+ return { allowed: ADDITIONAL_CONTEXT_LIMIT };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Record the length actually emitted for one reservation, best-effort.
100
+ * Prunes every key's stale entries while it holds the write so the registry
101
+ * file stays bounded. Silently no-ops on any I/O error (fail-open).
102
+ * @param input - Registry location, reservation key, and the emitted length.
103
+ */
104
+ export function recordAdditionalContext(input: RecordInput): void {
105
+ try {
106
+ const now = input.now ?? Date.now();
107
+ const path = registryPath(input.stateDir);
108
+ const registry = loadRegistry(path);
109
+ const pruned: BudgetRegistry = {};
110
+ for (const [key, entries] of Object.entries(registry)) {
111
+ const fresh = freshEntries(entries, now);
112
+ if (fresh.length > 0) pruned[key] = fresh;
113
+ }
114
+ const key = budgetKey(input);
115
+ pruned[key] = [...(pruned[key] ?? []), { at: now, length: input.emitted }];
116
+ atomicWrite(path, JSON.stringify(pruned));
117
+ } catch {
118
+ // Best-effort: a lost entry only makes the NEXT reservation over-generous
119
+ // (never under), which is the safe direction to fail in.
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Cap a Cursor stdout JSON's `additional_context` against the shared budget
125
+ * instead of the flat per-response ceiling alone. Falls back to the plain
126
+ * cap (`./context-limit.ts`), unbudgeted, when `budget` is `undefined` or
127
+ * the stdout carries no `additional_context` at all.
128
+ * @param stdout - A native Cursor JSON stdout candidate.
129
+ * @param budget - Shared budget context, or `undefined` to skip it.
130
+ */
131
+ export function capAdditionalContextWithBudget(stdout: string, budget: CursorBudgetContext | undefined): string {
132
+ if (!budget) return capAdditionalContext(stdout);
133
+ const wanted = additionalContextLength(stdout);
134
+ if (wanted === 0) return stdout;
135
+ const { allowed } = reserveAdditionalContext({ ...budget, wanted });
136
+ if (allowed < OMIT_THRESHOLD) {
137
+ process.stderr.write(`[fuse-harness] cursor: additional_context budget exhausted for ${budget.event} (allowed=${allowed})\n`);
138
+ return omitAdditionalContext(stdout);
139
+ }
140
+ const limit = Math.min(ADDITIONAL_CONTEXT_LIMIT, allowed);
141
+ const capped = capAdditionalContext(stdout, limit);
142
+ recordAdditionalContext({ ...budget, emitted: additionalContextLength(capped) });
143
+ return capped;
144
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @module context-limit
3
+ * Cursor 3.18.25's `hooks-carriers` drops an `additional_context` carrier
4
+ * once `o.length>1e4` — but `o` is the MERGED text of every hook's
5
+ * `additional_context` for that event (concatenated with `"\n\n---\n\n"`
6
+ * before the 10,000-char check), not this harness's response in isolation.
7
+ * Capping our own contribution at {@link ADDITIONAL_CONTEXT_LIMIT} is
8
+ * therefore the LAST-RESORT guard, not the real protection: on its own it
9
+ * only proves OUR piece stays under 10,000, while the total across every
10
+ * hook plugin configured on the same event can still exceed it and get
11
+ * dropped wholesale — measured at ~8,400 chars on `sessionStart` from core
12
+ * plugins alone, close enough to the ceiling that one more plugin tips it
13
+ * over. The actual protection is the cross-process shared budget registry
14
+ * in `./context-budget.ts` (Cursor id only), which reserves a slice of the
15
+ * 10,000 ceiling per (session, event, generation) key BEFORE calling
16
+ * {@link truncateAdditionalContext} here with the reserved amount instead of
17
+ * the flat {@link ADDITIONAL_CONTEXT_LIMIT} — this module stays a pure,
18
+ * budget-agnostic primitive so it keeps working unbudgeted (its historical,
19
+ * still-correct behavior) wherever no budget context is available. The
20
+ * limit unit is UTF-16 code units (`String.prototype.length`), matching
21
+ * `value.length` here exactly. Only 5 events carry `additional_context`
22
+ * through this carrier — sessionStart, beforeSubmitPrompt, preToolUse,
23
+ * postToolUse, postToolUseFailure — subagentStart/subagentStop use a
24
+ * different, unlimited channel. "Drops silently" also only holds when no
25
+ * `failClosed: true` hook is declared on that step/tool: with one declared,
26
+ * an oversized carrier REJECTS the tool call instead of being dropped quiet.
27
+ */
28
+
29
+ /** Cursor's hard `additional_context` character ceiling. */
30
+ export const ADDITIONAL_CONTEXT_LIMIT = 10_000;
31
+
32
+ /** Suffix appended by {@link truncateAdditionalContext} once a value is cut. */
33
+ export const TRUNCATION_MARKER = "\n[fuse-harness] additional_context truncated to Cursor's 10000-char limit";
34
+
35
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+
39
+ /**
40
+ * Truncate a string to end with {@link TRUNCATION_MARKER} once its length
41
+ * exceeds `limit`. Leaves shorter values untouched. Idempotent under a
42
+ * SHRINKING `limit` across repeated calls (e.g. an unbudgeted flat-cap pass
43
+ * followed by a budgeted re-cap of the same stdout — see `./respond.ts`'s
44
+ * `toCursorLifecycleResponse` doc): when `value` already ends with
45
+ * {@link TRUNCATION_MARKER}, that marker is stripped BEFORE re-slicing so the
46
+ * result carries exactly one marker instead of risking a duplicated/cut one.
47
+ * @param value - Candidate `additional_context` body.
48
+ * @param limit - Effective ceiling for this call (defaults to the flat
49
+ * {@link ADDITIONAL_CONTEXT_LIMIT}; a shared-budget caller passes a smaller,
50
+ * per-reservation value instead).
51
+ */
52
+ export function truncateAdditionalContext(value: string, limit: number = ADDITIONAL_CONTEXT_LIMIT): string {
53
+ const alreadyMarked = value.endsWith(TRUNCATION_MARKER);
54
+ if (!alreadyMarked && value.length <= limit) return value;
55
+ if (limit <= TRUNCATION_MARKER.length) return TRUNCATION_MARKER.slice(0, Math.max(0, limit));
56
+ const base = alreadyMarked ? value.slice(0, value.length - TRUNCATION_MARKER.length) : value;
57
+ return base.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
58
+ }
59
+
60
+ /**
61
+ * Length of a Cursor stdout JSON's `additional_context` string field, or 0
62
+ * when the stdout is not JSON, has no such field, or that field isn't a
63
+ * string.
64
+ * @param stdout - A native Cursor JSON stdout candidate.
65
+ */
66
+ export function additionalContextLength(stdout: string): number {
67
+ let parsed: unknown;
68
+ try {
69
+ parsed = JSON.parse(stdout);
70
+ } catch {
71
+ return 0;
72
+ }
73
+ return isPlainObject(parsed) && typeof parsed.additional_context === "string" ? parsed.additional_context.length : 0;
74
+ }
75
+
76
+ /**
77
+ * Re-serialize a Cursor stdout string with its `additional_context` field
78
+ * dropped entirely — used once the shared budget has no room left even for
79
+ * a truncated marker. Returns the input byte-for-byte unchanged when it is
80
+ * not JSON or has no string `additional_context` field.
81
+ * @param stdout - A native Cursor JSON stdout candidate.
82
+ */
83
+ export function omitAdditionalContext(stdout: string): string {
84
+ let parsed: unknown;
85
+ try {
86
+ parsed = JSON.parse(stdout);
87
+ } catch {
88
+ return stdout;
89
+ }
90
+ if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
91
+ const { additional_context: _omitted, ...rest } = parsed;
92
+ return JSON.stringify(rest);
93
+ }
94
+
95
+ /**
96
+ * Re-serialize a Cursor stdout string with its `additional_context` field
97
+ * capped at `limit` characters. Returns the input byte-for-byte unchanged
98
+ * when it is not JSON, has no string `additional_context` field, or that
99
+ * field is already within the limit — so callers can wrap every return path
100
+ * unconditionally.
101
+ * @param stdout - A native Cursor JSON stdout candidate.
102
+ * @param limit - Effective ceiling for this call (see {@link truncateAdditionalContext}).
103
+ */
104
+ export function capAdditionalContext(stdout: string, limit: number = ADDITIONAL_CONTEXT_LIMIT): string {
105
+ let parsed: unknown;
106
+ try {
107
+ parsed = JSON.parse(stdout);
108
+ } catch {
109
+ return stdout;
110
+ }
111
+ if (!isPlainObject(parsed) || typeof parsed.additional_context !== "string") return stdout;
112
+ const truncated = truncateAdditionalContext(parsed.additional_context, limit);
113
+ if (truncated === parsed.additional_context) return stdout;
114
+ return JSON.stringify({ ...parsed, additional_context: truncated });
115
+ }
@@ -0,0 +1,65 @@
1
+ import { isAbsolute, normalize, relative } from "node:path";
2
+ import { realpathSync } from "node:fs";
3
+
4
+ /** Preserve a Cursor path value only when it is non-empty and NUL-free. */
5
+ export function cursorPath(value: unknown): string | undefined {
6
+ return typeof value === "string" && value.length > 0 && !value.includes("\0") ? value : undefined;
7
+ }
8
+
9
+ /** Validate and normalize an absolute path supplied by Cursor. */
10
+ export function cursorAbsolutePath(value: unknown): string | undefined {
11
+ const candidate = cursorPath(value);
12
+ if (!candidate || !isAbsolute(candidate)) return undefined;
13
+ const path = normalize(candidate);
14
+ try {
15
+ return realpathSync.native(path);
16
+ } catch {
17
+ return path;
18
+ }
19
+ }
20
+
21
+ /** Preserve distinct, validated Cursor workspace roots in wire order. */
22
+ export function cursorWorkspaceRoots(value: unknown): string[] {
23
+ if (!Array.isArray(value)) return [];
24
+ const roots = value.map(cursorAbsolutePath).filter((root): root is string => root !== undefined);
25
+ return [...new Set(roots)];
26
+ }
27
+
28
+ function contains(root: string, filePath: string): boolean {
29
+ const rel = relative(root, filePath);
30
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
31
+ }
32
+
33
+ /**
34
+ * Select Cursor's project scope without replacing a valid payload cwd. Order:
35
+ * payload `cwd` -> longest workspace root containing `filePath` ->
36
+ * `workspaceRoots[0]` -> `CURSOR_PROJECT_DIR` env -> `CLAUDE_PROJECT_DIR` env
37
+ * -> `fallback`. Both env vars are validated the same way as any other Cursor
38
+ * path (`cursorAbsolutePath`: absolute, NUL-free, realpath-resolved), so an
39
+ * unset or malformed value is silently skipped rather than trusted.
40
+ * @param cwd - Cursor payload `cwd`, when present.
41
+ * @param workspaceRoots - Validated, deduped Cursor `workspace_roots`.
42
+ * @param filePath - The file the current event targets, when present.
43
+ * @param fallback - Caller-supplied last resort (never `process.cwd()`).
44
+ * @param env - Environment (defaults to `process.env`).
45
+ * @returns The resolved project root.
46
+ */
47
+ export function cursorProjectCwd(
48
+ cwd: string | undefined,
49
+ workspaceRoots: readonly string[],
50
+ filePath: string | undefined,
51
+ fallback: string,
52
+ env: Record<string, string | undefined> = process.env,
53
+ ): string {
54
+ if (cwd) return cwd;
55
+ if (filePath) {
56
+ const matches = workspaceRoots.filter((root) => contains(root, filePath));
57
+ if (matches.length > 0) return matches.sort((a, b) => b.length - a.length)[0]!;
58
+ }
59
+ if (workspaceRoots[0]) return workspaceRoots[0];
60
+ const fromCursorEnv = cursorAbsolutePath(env.CURSOR_PROJECT_DIR);
61
+ if (fromCursorEnv) return fromCursorEnv;
62
+ const fromClaudeEnv = cursorAbsolutePath(env.CLAUDE_PROJECT_DIR);
63
+ if (fromClaudeEnv) return fromClaudeEnv;
64
+ return fallback;
65
+ }
@@ -0,0 +1,38 @@
1
+ import type { CursorEventContract } from "./interfaces/types";
2
+
3
+ const EVENT_CONTRACTS = {
4
+ sessionStart: { phase: "pre", lifecycle: "SessionStart", response: "session-context", blockable: false, known: true },
5
+ sessionEnd: { phase: "post", lifecycle: "SessionEnd", response: "neutral", blockable: false, known: true },
6
+ beforeSubmitPrompt: { phase: "pre", lifecycle: "UserPromptSubmit", response: "submit-control", blockable: true, known: true },
7
+ preCompact: { phase: "pre", lifecycle: "PreCompact", response: "compact-notice", blockable: false, known: true },
8
+ subagentStart: { phase: "pre", lifecycle: "SubagentStart", response: "permission", blockable: true, known: true },
9
+ subagentStop: { phase: "post", lifecycle: "SubagentStop", response: "followup", blockable: false, known: true },
10
+ preToolUse: { phase: "pre", lifecycle: "PreToolUse", response: "permission", blockable: true, known: true },
11
+ postToolUse: { phase: "post", lifecycle: "PostToolUse", response: "post-context", blockable: false, known: true },
12
+ postToolUseFailure: { phase: "post", lifecycle: "PostToolUseFailure", response: "neutral", blockable: false, known: true },
13
+ beforeShellExecution: { phase: "pre", lifecycle: "BeforeShellExecution", response: "permission", blockable: true, known: true },
14
+ afterShellExecution: { phase: "post", lifecycle: "AfterShellExecution", response: "neutral", blockable: false, known: true },
15
+ beforeMCPExecution: { phase: "pre", lifecycle: "BeforeMCPExecution", response: "permission", blockable: true, known: true },
16
+ afterMCPExecution: { phase: "post", lifecycle: "AfterMCPExecution", response: "neutral", blockable: false, known: true },
17
+ beforeReadFile: { phase: "pre", lifecycle: "BeforeReadFile", response: "permission", blockable: true, known: true },
18
+ afterFileEdit: { phase: "post", lifecycle: "AfterFileEdit", response: "neutral", blockable: false, known: true },
19
+ beforeTabFileRead: { phase: "pre", lifecycle: "BeforeTabFileRead", response: "permission", blockable: true, known: true },
20
+ afterTabFileEdit: { phase: "post", lifecycle: "AfterTabFileEdit", response: "neutral", blockable: false, known: true },
21
+ afterAgentResponse: { phase: "post", lifecycle: "AfterAgentResponse", response: "neutral", blockable: false, known: true },
22
+ afterAgentThought: { phase: "post", lifecycle: "AfterAgentThought", response: "neutral", blockable: false, known: true },
23
+ stop: { phase: "post", lifecycle: "Stop", response: "followup", blockable: false, known: true },
24
+ workspaceOpen: { phase: "pre", lifecycle: "WorkspaceOpen", response: "plugin-paths", blockable: false, known: true },
25
+ } as const satisfies Record<string, CursorEventContract>;
26
+
27
+ const UNKNOWN_EVENT: CursorEventContract = {
28
+ phase: "post",
29
+ lifecycle: null,
30
+ response: "neutral",
31
+ blockable: false,
32
+ known: false,
33
+ };
34
+
35
+ /** Return explicit routing and response metadata for a Cursor event name. */
36
+ export function cursorEventContract(eventName: string): CursorEventContract {
37
+ return EVENT_CONTRACTS[eventName as keyof typeof EVENT_CONTRACTS] ?? UNKNOWN_EVENT;
38
+ }
@@ -0,0 +1,43 @@
1
+ /** Cursor hook adapter; post-edit handling remains observe-only. */
2
+ import { evaluate } from "../../policy/evaluate";
3
+ import { formatPrompt } from "../../prompt/types";
4
+ import { extractCursorEvent } from "./normalize";
5
+ import type { CursorShellPayload, CursorToolPayload, CursorEditPayload, CursorResponse, CursorEditResponse } from "./interfaces/types";
6
+
7
+ export type { CursorShellPayload, CursorToolPayload, CursorEditPayload, CursorResponse, CursorEditResponse } from "./interfaces/types";
8
+
9
+ function guardCursor(payload: object): CursorResponse {
10
+ const event = extractCursorEvent(payload);
11
+ const result = evaluate({ tool: event.tool, filePath: event.filePath, content: event.content, oldString: event.oldString, command: event.command });
12
+ if (result.decision === "allow" || !result.prompt) return { permission: "allow" };
13
+ const message = formatPrompt(result.prompt);
14
+ const userMessage = result.prompt.kind === "ask"
15
+ ? `[downgraded from ask — Cursor preToolUse does not reliably enforce ask]\n${message}`
16
+ : message;
17
+ return { permission: "deny", user_message: userMessage, agent_message: message };
18
+ }
19
+
20
+ function namedPayload(payload: object, eventName: string): object {
21
+ return Object.hasOwn(payload, "hook_event_name") ? payload : { ...payload, hook_event_name: eventName };
22
+ }
23
+
24
+ /** Guard a shell command (git/install policies). */
25
+ export function beforeShellExecution(payload: CursorShellPayload): CursorResponse {
26
+ return guardCursor(namedPayload(payload, "beforeShellExecution"));
27
+ }
28
+
29
+ /** Guard a generic Cursor tool call using the same extraction as the runtime. */
30
+ export function preToolUse(payload: CursorToolPayload): CursorResponse {
31
+ return guardCursor(namedPayload(payload, "preToolUse"));
32
+ }
33
+
34
+ /**
35
+ * Complete an observed file edit without emitting pre-execution permission
36
+ * fields. Cursor does not define a callback schema for this post hook.
37
+ * @param payload - The `afterFileEdit` stdin payload.
38
+ * @returns An empty successful response.
39
+ */
40
+ export function afterFileEdit(payload: CursorEditPayload): CursorEditResponse {
41
+ void payload;
42
+ return {};
43
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Reservation key + registry location for one hook invocation's slice of
3
+ * Cursor's shared, cross-process `additional_context` budget (see
4
+ * `../context-budget.ts`). `undefined` at a call site means "no shared
5
+ * budget available" — callers then fall back to the flat per-response cap.
6
+ */
7
+ export interface CursorBudgetContext {
8
+ /** Project state directory the registry file lives under (see `defaultStateDir`). */
9
+ stateDir: string;
10
+ /** Cursor `session_id` (its `conversation_id`). */
11
+ sessionId: string;
12
+ /** Raw Cursor `hook_event_name` (e.g. `"sessionStart"`). */
13
+ event: string;
14
+ /** Cursor `generation_id`; absent on `sessionStart`/`workspaceOpen`. */
15
+ generationId?: string;
16
+ /**
17
+ * Cursor `tool_use_id`; present on preToolUse/postToolUse/postToolUseFailure
18
+ * — Cursor merges `additional_context` PER TOOL CALL for these events, not
19
+ * once per (session, event, generation), so this must join the key or
20
+ * concurrent tool calls in the same generation would wrongly share one slice.
21
+ */
22
+ toolUseId?: string;
23
+ /** Test seam: injectable clock (defaults to `Date.now()`). */
24
+ now?: number;
25
+ }
@@ -0,0 +1,88 @@
1
+ /** `beforeShellExecution` stdin payload (subset). */
2
+ export interface CursorShellPayload {
3
+ command?: string;
4
+ cwd?: string;
5
+ sandbox?: boolean;
6
+ workspace_roots?: string[];
7
+ hook_event_name?: string;
8
+ }
9
+
10
+ /** `preToolUse` stdin payload subset consumed by the adapter. */
11
+ export interface CursorToolPayload {
12
+ tool_name?: string;
13
+ tool_input?: Record<string, unknown>;
14
+ }
15
+
16
+ /** `afterFileEdit` stdin payload (subset). */
17
+ export interface CursorEditPayload {
18
+ file_path?: string;
19
+ edits?: { old_string: string; new_string: string }[];
20
+ }
21
+
22
+ /** One extracted Cursor file edit used by runtime post fan-out. */
23
+ export interface CursorExtractedFile {
24
+ filePath: string;
25
+ oldString?: string;
26
+ content: string;
27
+ op: "update";
28
+ }
29
+
30
+ /** Shared Cursor extraction result consumed by runtime and public adapter. */
31
+ export interface CursorExtractedEvent {
32
+ eventName: string;
33
+ lifecycleEvent: string | null;
34
+ responseKind: CursorResponseKind;
35
+ blockable: boolean;
36
+ cwd?: string;
37
+ workspaceRoots?: string[];
38
+ phase: "pre" | "post";
39
+ tool: string;
40
+ input: Record<string, unknown>;
41
+ filePath?: string;
42
+ content?: string;
43
+ oldString?: string;
44
+ command?: string;
45
+ /** Distinct beforeMCPExecution root and nested commands, in wire order. */
46
+ commandCandidates?: string[];
47
+ files?: CursorExtractedFile[];
48
+ }
49
+
50
+ /** Native Cursor stdout contract selected for one hook event. */
51
+ export type CursorResponseKind =
52
+ | "permission"
53
+ | "post-context"
54
+ | "session-context"
55
+ | "submit-control"
56
+ | "followup"
57
+ | "compact-notice"
58
+ | "plugin-paths"
59
+ | "neutral";
60
+
61
+ /** Routing metadata for a documented Cursor lifecycle event. */
62
+ export interface CursorEventContract {
63
+ phase: "pre" | "post";
64
+ lifecycle: string | null;
65
+ response: CursorResponseKind;
66
+ blockable: boolean;
67
+ known: boolean;
68
+ }
69
+
70
+ /**
71
+ * Empty afterFileEdit callback. Cursor documents no output fields for this
72
+ * post hook, so pre-execution permission fields are intentionally impossible.
73
+ */
74
+ export type CursorEditResponse = Record<string, never>;
75
+
76
+ /**
77
+ * `beforeShellExecution` stdout response. Message keys are snake_case:
78
+ * Cursor silently ignores camelCase `userMessage`/`agentMessage` (#141516,
79
+ * regression persists through v2.0.77+ — forum #142589), matching the
80
+ * schema respond.ts emits for the cursor harness.
81
+ */
82
+ export interface CursorResponse {
83
+ permission: "allow" | "deny" | "ask";
84
+ /** User-visible message — snake_case required (#141516, #142589). */
85
+ user_message?: string;
86
+ /** Agent-visible message — snake_case required (#141516, #142589). */
87
+ agent_message?: string;
88
+ }
@@ -0,0 +1,39 @@
1
+ import { NATIVE_SCHEMAS, recordValue, type NativeSchema } from "./native-schemas";
2
+
3
+ /**
4
+ * Check that every enumerable own key of `value` is a documented field for
5
+ * `eventName` and passes its validator, and that every required field is
6
+ * present. Rejects prototype-polluted or exotic-shaped candidates via
7
+ * {@link recordValue}.
8
+ * @param value - Parsed JSON candidate.
9
+ * @param eventName - The Cursor hook event the candidate would answer.
10
+ */
11
+ function isNativeCursorResponse(value: unknown, eventName: string): boolean {
12
+ try {
13
+ if (!recordValue(value)) return false;
14
+ const schema = NATIVE_SCHEMAS[eventName as keyof typeof NATIVE_SCHEMAS] as NativeSchema | undefined;
15
+ if (!schema) return false;
16
+ const descriptors = Object.getOwnPropertyDescriptors(value as object);
17
+ if (schema.required?.some((field) => !Object.hasOwn(descriptors, field))) return false;
18
+ return Reflect.ownKeys(descriptors).every((field) => {
19
+ if (typeof field !== "string") return false;
20
+ const descriptor = descriptors[field];
21
+ if (!Object.hasOwn(schema.fields, field)) return false;
22
+ const validate = schema.fields[field];
23
+ return descriptor?.enumerable === true && "value" in descriptor
24
+ && validate !== undefined && validate(descriptor.value);
25
+ });
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /** Preserve raw stdout only when JSON.parse proves a documented native Cursor response. */
32
+ export function parseNativeCursorStdout(stdout: string, eventName: string): string | null {
33
+ if (typeof stdout !== "string") return null;
34
+ try {
35
+ return isNativeCursorResponse(JSON.parse(stdout), eventName) ? stdout : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }