@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,161 @@
1
+ /**
2
+ * @module native-schemas
3
+ * Per-event field validators for Cursor's documented native stdout contract.
4
+ * Extracted from native-response.ts to keep that module focused on the
5
+ * passthrough decision logic (SOLID file-size split, not a plafond workaround).
6
+ *
7
+ * Field lists are binary-verified against Cursor 3.18.25 (agent-cli
8
+ * `190.index.js` / `workbench.desktop.main.js`, validators `R`/`Ded`) and
9
+ * match the published hooks documentation.
10
+ */
11
+
12
+ /** A single-field runtime type check used to build a {@link NativeSchema}. */
13
+ export type FieldValidator = (value: unknown) => boolean;
14
+
15
+ /** The exact field set (and per-field validator) Cursor reads for one event. */
16
+ export interface NativeSchema {
17
+ fields: Readonly<Record<string, FieldValidator>>;
18
+ required?: readonly string[];
19
+ }
20
+
21
+ const stringValue: FieldValidator = (value) => typeof value === "string";
22
+ const booleanValue: FieldValidator = (value) => typeof value === "boolean";
23
+
24
+ /** A plain `{}`-literal or `Object.create(null)` object — never a class instance or array. */
25
+ export const plainRecord = (value: unknown): value is Record<string, unknown> => {
26
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
27
+ try {
28
+ const prototype = Object.getPrototypeOf(value);
29
+ return prototype === Object.prototype || prototype === null;
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+
35
+ type JsonFrame = { value: unknown; leave?: false } | { value: object; leave: true };
36
+
37
+ function jsonChildren(value: object): unknown[] | null {
38
+ const keys = Reflect.ownKeys(value);
39
+ const descriptors = Object.getOwnPropertyDescriptors(value);
40
+ if (Array.isArray(value)) {
41
+ if (Object.getPrototypeOf(value) !== Array.prototype) return null;
42
+ const length = descriptors.length;
43
+ if (!length || !("value" in length) || !Number.isSafeInteger(length.value) || length.value < 0) return null;
44
+ if (keys.length !== length.value + 1 || keys.some((key) => typeof key === "symbol")) return null;
45
+ const children: unknown[] = [];
46
+ for (let index = 0; index < length.value; index += 1) {
47
+ const descriptor = descriptors[String(index)];
48
+ if (!descriptor?.enumerable || !("value" in descriptor)) return null;
49
+ children.push(descriptor.value);
50
+ }
51
+ return children;
52
+ }
53
+ if (!plainRecord(value) || keys.some((key) => typeof key === "symbol")) return null;
54
+ const children: unknown[] = [];
55
+ for (const key of keys) {
56
+ const descriptor = descriptors[key as string];
57
+ if (!descriptor?.enumerable || !("value" in descriptor)) return null;
58
+ children.push(descriptor.value);
59
+ }
60
+ return children;
61
+ }
62
+
63
+ function jsonValue(root: unknown): boolean {
64
+ const active = new WeakSet<object>();
65
+ const stack: JsonFrame[] = [{ value: root }];
66
+ while (stack.length > 0) {
67
+ const frame = stack.pop()!;
68
+ if (frame.leave) {
69
+ active.delete(frame.value);
70
+ continue;
71
+ }
72
+ const { value } = frame;
73
+ if (value === null || typeof value === "string" || typeof value === "boolean") continue;
74
+ if (typeof value === "number") {
75
+ if (!Number.isFinite(value)) return false;
76
+ continue;
77
+ }
78
+ if (typeof value !== "object" || active.has(value)) return false;
79
+ let children: unknown[] | null;
80
+ try {
81
+ children = jsonChildren(value);
82
+ } catch {
83
+ return false;
84
+ }
85
+ if (!children) return false;
86
+ active.add(value);
87
+ stack.push({ value, leave: true });
88
+ for (let index = children.length - 1; index >= 0; index -= 1) stack.push({ value: children[index] });
89
+ }
90
+ return true;
91
+ }
92
+
93
+ /** A JSON-safe plain object (no cycles, no non-finite numbers, no exotic prototypes). */
94
+ export const recordValue: FieldValidator = (value) => plainRecord(value) && jsonValue(value);
95
+ const stringRecord: FieldValidator = (value) => {
96
+ if (!recordValue(value)) return false;
97
+ try {
98
+ return Object.values(Object.getOwnPropertyDescriptors(value as object))
99
+ .every((descriptor) => "value" in descriptor && typeof descriptor.value === "string");
100
+ } catch {
101
+ return false;
102
+ }
103
+ };
104
+ const stringArray: FieldValidator = (value) => Array.isArray(value) && value.every(stringValue);
105
+ const permission = (...values: string[]): FieldValidator => (value) => typeof value === "string" && values.includes(value);
106
+
107
+ const EMPTY: NativeSchema = { fields: {} };
108
+ const FOLLOWUP: NativeSchema = { fields: { followup_message: stringValue } };
109
+ const PERMISSION_ASK: NativeSchema = {
110
+ fields: { permission: permission("allow", "deny", "ask"), user_message: stringValue, agent_message: stringValue },
111
+ required: ["permission"],
112
+ };
113
+
114
+ const PRE_TOOL_USE: NativeSchema = {
115
+ fields: {
116
+ permission: permission("allow", "deny", "ask"),
117
+ user_message: stringValue,
118
+ agent_message: stringValue,
119
+ updated_input: recordValue,
120
+ additional_context: stringValue,
121
+ },
122
+ required: ["permission"],
123
+ };
124
+
125
+ /**
126
+ * Exact native stdout field set Cursor 3.18.25 reads per hook event.
127
+ * Nothing beyond this list is invented: any additional key on a candidate
128
+ * value fails {@link isNativeCursorResponse} in native-response.ts.
129
+ */
130
+ export const NATIVE_SCHEMAS: Readonly<Record<string, NativeSchema>> = {
131
+ sessionStart: {
132
+ fields: { env: stringRecord, additional_context: stringValue, continue: booleanValue, user_message: stringValue },
133
+ },
134
+ sessionEnd: EMPTY,
135
+ beforeSubmitPrompt: {
136
+ fields: { continue: booleanValue, user_message: stringValue, additional_context: stringValue },
137
+ required: ["continue"],
138
+ },
139
+ preCompact: { fields: { user_message: stringValue } },
140
+ subagentStart: {
141
+ fields: { permission: permission("allow", "deny"), user_message: stringValue }, required: ["permission"],
142
+ },
143
+ subagentStop: FOLLOWUP,
144
+ preToolUse: PRE_TOOL_USE,
145
+ postToolUse: { fields: { updated_mcp_tool_output: recordValue, additional_context: stringValue } },
146
+ postToolUseFailure: { fields: { additional_context: stringValue } },
147
+ beforeShellExecution: PERMISSION_ASK,
148
+ afterShellExecution: EMPTY,
149
+ beforeMCPExecution: PERMISSION_ASK,
150
+ afterMCPExecution: EMPTY,
151
+ beforeReadFile: {
152
+ fields: { permission: permission("allow", "deny"), user_message: stringValue }, required: ["permission"],
153
+ },
154
+ afterFileEdit: EMPTY,
155
+ beforeTabFileRead: { fields: { permission: permission("allow", "deny") }, required: ["permission"] },
156
+ afterTabFileEdit: EMPTY,
157
+ afterAgentResponse: EMPTY,
158
+ afterAgentThought: EMPTY,
159
+ stop: FOLLOWUP,
160
+ workspaceOpen: { fields: { pluginPaths: stringArray } },
161
+ };
@@ -0,0 +1,188 @@
1
+ import { commandToString } from "../../runtime/command-string";
2
+ import { cursorEventContract } from "./events";
3
+ import { cursorAbsolutePath, cursorPath, cursorWorkspaceRoots } from "./context";
4
+ import type { CursorExtractedEvent } from "./interfaces/types";
5
+
6
+ function record(value: unknown): Record<string, unknown> | undefined {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value)
8
+ ? value as Record<string, unknown>
9
+ : undefined;
10
+ }
11
+
12
+ function str(value: unknown): string | undefined {
13
+ return typeof value === "string" ? value : undefined;
14
+ }
15
+
16
+ function inputRecord(value: unknown): Record<string, unknown> | undefined {
17
+ const direct = record(value);
18
+ if (direct || typeof value !== "string") return direct;
19
+ try {
20
+ return record(JSON.parse(value));
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ }
25
+
26
+ const MCP_ROOT_FIELDS = ["mcp_server_name", "mcp_server_url", "url", "result_json", "duration"] as const;
27
+
28
+ function cursorMcpInput(raw: Record<string, unknown>, input: Record<string, unknown>): Record<string, unknown> {
29
+ const merged = { ...input };
30
+ for (const field of MCP_ROOT_FIELDS) {
31
+ if (Object.hasOwn(raw, field)) merged[field] = raw[field];
32
+ }
33
+ return merged;
34
+ }
35
+
36
+ function sanitizedCursorInput(input: Record<string, unknown>): Record<string, unknown> {
37
+ const safe = { ...input };
38
+ if (typeof safe.file_path === "string" && cursorPath(safe.file_path) === undefined) delete safe.file_path;
39
+ if (typeof safe.path === "string" && cursorPath(safe.path) === undefined) delete safe.path;
40
+ if (Object.hasOwn(safe, "cwd")) {
41
+ const cwd = cursorAbsolutePath(safe.cwd);
42
+ if (cwd) safe.cwd = cwd;
43
+ else delete safe.cwd;
44
+ }
45
+ if (Object.hasOwn(safe, "workspace_roots")) {
46
+ if (Array.isArray(safe.workspace_roots)) safe.workspace_roots = cursorWorkspaceRoots(safe.workspace_roots);
47
+ else delete safe.workspace_roots;
48
+ }
49
+ return safe;
50
+ }
51
+
52
+ /**
53
+ * Closed table: Cursor's `MCP:<tool>` tool_name form on preToolUse/
54
+ * postToolUse/postToolUseFailure LOSES the MCP server name (ground truth:
55
+ * Cursor CLI 3.18.25 + official docs — only beforeMCPExecution/
56
+ * afterMCPExecution carry `mcp_server_name`). This reconstructs the real
57
+ * server for the closed set of tool names this repo's gates actually depend
58
+ * on (GATED_TOOLS in doc-cache-gate.ts, CONTEXT7_SOURCE, RESEARCH_TOOLS,
59
+ * SHOT_TOOLS, gemini-mcp-gate, shadcn-skill-gate) — same closed-table
60
+ * philosophy as `mcp-tool-name.ts`'s Codex aliasing, never a blanket
61
+ * reversal. Coordinator decision: a tool name OUTSIDE this table (server
62
+ * genuinely unrecoverable, and no safe placeholder) is left as Cursor's raw
63
+ * `MCP:<tool>` string — `test/cursor-followup-normalize.test.ts` pins this
64
+ * as the committed contract ("commandless MCP tools keep their name"), so a
65
+ * fabricated `mcp__cursor__<tool>` placeholder is never introduced for the
66
+ * unknown case.
67
+ */
68
+ const CURSOR_MCP_TOOL_SERVERS: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
69
+ "query-docs": "context7",
70
+ "resolve-library-id": "context7",
71
+ web_search_exa: "exa",
72
+ get_code_context_exa: "exa",
73
+ deep_researcher_start: "exa",
74
+ deep_researcher_check: "exa",
75
+ create_frontend: "gemini-design",
76
+ modify_frontend: "gemini-design",
77
+ snippet_frontend: "gemini-design",
78
+ search_items_in_registries: "shadcn",
79
+ view_items_in_registries: "shadcn",
80
+ get_item_examples_from_registries: "shadcn",
81
+ get_add_command_for_items: "shadcn",
82
+ get_audit_checklist: "shadcn",
83
+ });
84
+
85
+ /**
86
+ * The real MCP server for a bare Cursor tool name (the part after `MCP:`),
87
+ * or `undefined` when it isn't in the closed table. fuse-browser is inferred
88
+ * from the `browser_*` prefix — every fuse-browser tool is named that way
89
+ * and no other server in this ecosystem uses it — the remaining,
90
+ * non-distinctive tool names go through {@link CURSOR_MCP_TOOL_SERVERS}.
91
+ * NO placeholder fallback (coordinator decision, see {@link CURSOR_MCP_TOOL_SERVERS}):
92
+ * an unknown tool name means the server is genuinely unrecoverable, so the
93
+ * caller leaves the raw `MCP:<tool>` string untouched instead of fabricating one.
94
+ */
95
+ function cursorMcpServer(bareTool: string): string | undefined {
96
+ if (bareTool.startsWith("browser_")) return "fuse-browser";
97
+ return CURSOR_MCP_TOOL_SERVERS[bareTool];
98
+ }
99
+
100
+ /**
101
+ * Canonicalize Cursor's `MCP:<tool>` tool_name (preToolUse/postToolUse/
102
+ * postToolUseFailure) into the shared `mcp__<server>__<tool>` shape every
103
+ * other harness/gate expects. Returns `undefined` — meaning "leave the raw
104
+ * `MCP:<tool>` string as-is" — both when `tool` isn't the `MCP:` form and
105
+ * when the bare tool name is outside the closed {@link CURSOR_MCP_TOOL_SERVERS}
106
+ * table (server unrecoverable, no placeholder fabricated).
107
+ */
108
+ function cursorBareMcpToolName(tool: string | undefined): string | undefined {
109
+ if (!tool || !tool.startsWith("MCP:")) return undefined;
110
+ const bare = tool.slice(4);
111
+ const server = cursorMcpServer(bare);
112
+ return server ? `mcp__${server}__${bare}` : undefined;
113
+ }
114
+
115
+ function cursorToolName(raw: Record<string, unknown>, event: string, tool: string | undefined, hasCommand: boolean): string {
116
+ if (hasCommand) return "Bash";
117
+ const server = str(raw.mcp_server_name)?.trim().replace(/[^A-Za-z0-9_-]+/g, "_");
118
+ if (/^(before|after)MCPExecution$/i.test(event) && server && tool && !tool.startsWith("mcp__")) {
119
+ return `mcp__${server}__${tool}`;
120
+ }
121
+ const bareMcp = cursorBareMcpToolName(tool);
122
+ if (bareMcp) return bareMcp;
123
+ if (tool === "Write") return "Edit";
124
+ return tool ?? "";
125
+ }
126
+
127
+ function cursorCommands(raw: Record<string, unknown>, input: Record<string, unknown>): string[] {
128
+ const candidates = [commandToString(raw.command), commandToString(input.command)]
129
+ .filter((candidate): candidate is string => candidate !== undefined);
130
+ return [...new Set(candidates)];
131
+ }
132
+
133
+ /**
134
+ * Extract one Cursor hook payload into the shared runtime shape.
135
+ *
136
+ * @param payload - Cursor hook stdin payload.
137
+ * @returns Cursor-specific phase, tool, command, and edit fields.
138
+ */
139
+ export function extractCursorEvent(payload: object): CursorExtractedEvent {
140
+ const raw = payload as Record<string, unknown>;
141
+ const hookEvent = str(raw.hook_event_name) ?? "";
142
+ const parsedInput = inputRecord(raw.tool_input);
143
+ const mcpInput = parsedInput && /^(before|after)MCPExecution$/i.test(hookEvent)
144
+ ? cursorMcpInput(raw, parsedInput)
145
+ : parsedInput;
146
+ const input = sanitizedCursorInput(mcpInput ?? raw);
147
+ const contract = cursorEventContract(hookEvent);
148
+ const metadata = {
149
+ eventName: hookEvent,
150
+ lifecycleEvent: contract.lifecycle,
151
+ responseKind: contract.response,
152
+ blockable: contract.blockable,
153
+ cwd: cursorAbsolutePath(raw.cwd),
154
+ workspaceRoots: cursorWorkspaceRoots(raw.workspace_roots),
155
+ };
156
+ if (!contract.known) {
157
+ return { ...metadata, phase: contract.phase, tool: "", input };
158
+ }
159
+ const afterFileEdit = /^afterFileEdit$/i.test(hookEvent);
160
+ const beforeReadFile = /^beforeReadFile$/i.test(hookEvent);
161
+ const beforeTabFileRead = /^beforeTabFileRead$/i.test(hookEvent);
162
+ const edits = Array.isArray(raw.edits)
163
+ ? raw.edits.map(record).filter((edit): edit is Record<string, unknown> => edit !== undefined)
164
+ : [];
165
+ const filePath = cursorPath(raw.file_path);
166
+ if (filePath && edits.length > 0) {
167
+ const files = edits.map((edit) => ({
168
+ filePath,
169
+ oldString: str(edit.old_string),
170
+ content: str(edit.new_string) ?? "",
171
+ op: "update" as const,
172
+ }));
173
+ return { ...metadata, phase: contract.phase, tool: "Edit", input, filePath, content: files.map((file) => file.content).join("\n"), files };
174
+ }
175
+ const commands = cursorCommands(raw, input);
176
+ const command = commands[0];
177
+ return {
178
+ ...metadata,
179
+ phase: contract.phase,
180
+ tool: afterFileEdit ? "Edit" : beforeReadFile || beforeTabFileRead ? "Read" : cursorToolName(raw, hookEvent, str(raw.tool_name), command !== undefined),
181
+ input,
182
+ filePath: cursorPath(input.file_path) ?? cursorPath(input.path),
183
+ content: str(input.content) ?? str(input.new_string),
184
+ oldString: str(input.old_string),
185
+ command,
186
+ commandCandidates: /^beforeMCPExecution$/i.test(hookEvent) && commands.length > 0 ? commands : undefined,
187
+ };
188
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Cursor plugin-root resolution — independent from the rules-plugin probing
3
+ * in `../../runtime/lifecycle/rules-root.ts`. Ground truth (Cursor 3.18.25
4
+ * binary + cursor.com/docs/hooks): `CURSOR_PLUGIN_ROOT` / `CLAUDE_PLUGIN_ROOT`
5
+ * (both equal to the plugin install dir) are injected ONLY into
6
+ * plugin-declared hook processes — never user (`~/.cursor/hooks.json`),
7
+ * project (`.cursor/hooks.json`), or enterprise hooks. A plugin hook's cwd is
8
+ * the plugin install dir, EXCEPT for `stop`/`subagentStop`, where it is the
9
+ * workspace root — callers must pass the right `cwd` for the event they are
10
+ * handling. Precedence: (1) `CURSOR_PLUGIN_ROOT` env, (2) `CLAUDE_PLUGIN_ROOT`
11
+ * env, (3) `cwd` when it carries a Cursor plugin marker
12
+ * (`.cursor-plugin/plugin.json`, `plugin.json` + `hooks/hooks.json`, or a
13
+ * bare `hooks/hooks.json` — matches installed-plugin layouts under
14
+ * `~/.cursor/plugins/cache/**` and `~/.cursor/plugins/local/<name>/`), (4)
15
+ * `none`. Cursor refuses symlinked config paths itself; we do not share that
16
+ * constraint, so every resolved candidate is realpath-followed instead.
17
+ */
18
+ import { existsSync, realpathSync, statSync } from "node:fs";
19
+ import { isAbsolute, join } from "node:path";
20
+
21
+ /** How the resolved Cursor plugin root was determined. */
22
+ export type CursorPluginRootSource =
23
+ | "env:CURSOR_PLUGIN_ROOT"
24
+ | "env:CLAUDE_PLUGIN_ROOT"
25
+ | "cwd:plugin-marker"
26
+ | "none";
27
+
28
+ /** Result of resolving the Cursor plugin install root. */
29
+ export interface CursorPluginRootResult {
30
+ /** Realpath-resolved plugin install directory, or `null` when unproven. */
31
+ root: string | null;
32
+ /** Which precedence step produced `root`. */
33
+ source: CursorPluginRootSource;
34
+ /** One diagnostic entry per candidate that was examined and rejected. */
35
+ checked: string[];
36
+ }
37
+
38
+ /** Validate an env candidate: non-empty, NUL-free, absolute, existing dir. */
39
+ function validateEnvCandidate(label: string, value: string | undefined, checked: string[]): string | null {
40
+ if (value === undefined || value === "") {
41
+ checked.push(`${label}: unset`);
42
+ return null;
43
+ }
44
+ if (value.includes("\0")) {
45
+ checked.push(`${label}: invalid (contains NUL): "${value}"`);
46
+ return null;
47
+ }
48
+ if (!isAbsolute(value)) {
49
+ checked.push(`${label}: invalid (not absolute): "${value}"`);
50
+ return null;
51
+ }
52
+ try {
53
+ if (!statSync(value).isDirectory()) {
54
+ checked.push(`${label}: invalid (not a directory): "${value}"`);
55
+ return null;
56
+ }
57
+ } catch {
58
+ checked.push(`${label}: invalid (no such directory): "${value}"`);
59
+ return null;
60
+ }
61
+ try {
62
+ return realpathSync.native(value);
63
+ } catch {
64
+ checked.push(`${label}: invalid (realpath failed): "${value}"`);
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /** True when `dir` carries a recognized Cursor plugin install marker. */
70
+ function hasPluginMarker(dir: string): boolean {
71
+ if (existsSync(join(dir, ".cursor-plugin", "plugin.json"))) return true;
72
+ if (existsSync(join(dir, "plugin.json")) && existsSync(join(dir, "hooks", "hooks.json"))) return true;
73
+ return existsSync(join(dir, "hooks", "hooks.json"));
74
+ }
75
+
76
+ /**
77
+ * Resolve the Cursor plugin install root a plugin-declared hook runs from.
78
+ * @param env - Environment (defaults to `process.env`).
79
+ * @param cwd - The hook process's cwd for the current event (plugin root for
80
+ * most events, workspace root for `stop`/`subagentStop` — caller's choice).
81
+ * @returns The resolved root, its source, and every rejected candidate.
82
+ */
83
+ export function resolveCursorPluginRoot(
84
+ env: Record<string, string | undefined>,
85
+ cwd: string,
86
+ ): CursorPluginRootResult {
87
+ const checked: string[] = [];
88
+ const fromCursor = validateEnvCandidate("env:CURSOR_PLUGIN_ROOT", env.CURSOR_PLUGIN_ROOT, checked);
89
+ if (fromCursor) return { root: fromCursor, source: "env:CURSOR_PLUGIN_ROOT", checked };
90
+ const fromClaude = validateEnvCandidate("env:CLAUDE_PLUGIN_ROOT", env.CLAUDE_PLUGIN_ROOT, checked);
91
+ if (fromClaude) return { root: fromClaude, source: "env:CLAUDE_PLUGIN_ROOT", checked };
92
+ if (hasPluginMarker(cwd)) {
93
+ let resolved = cwd;
94
+ try {
95
+ resolved = realpathSync.native(cwd);
96
+ } catch {
97
+ /* keep raw cwd when realpath fails (e.g. already-canonical or unreadable parent) */
98
+ }
99
+ return { root: resolved, source: "cwd:plugin-marker", checked };
100
+ }
101
+ checked.push(`cwd:"${cwd}": no plugin marker found`);
102
+ return { root: null, source: "none", checked };
103
+ }
@@ -0,0 +1,170 @@
1
+ import { formatPrompt, type Prompt } from "../../prompt/types";
2
+ import { cursorEventContract } from "./events";
3
+ import { parseNativeCursorStdout } from "./native-response";
4
+ import { capAdditionalContext } from "./context-limit";
5
+ import { capAdditionalContextWithBudget } from "./context-budget";
6
+ import type { CursorBudgetContext } from "./interfaces/context-budget";
7
+
8
+ const AGENT_MESSAGE_EVENTS = new Set([
9
+ "preToolUse",
10
+ "beforeShellExecution",
11
+ "beforeMCPExecution",
12
+ ]);
13
+ const USER_MESSAGE_EVENTS = new Set([
14
+ "preToolUse",
15
+ "beforeShellExecution",
16
+ "beforeMCPExecution",
17
+ "beforeReadFile",
18
+ "subagentStart",
19
+ ]);
20
+
21
+ function permissionMessages(eventName: string, userMessage?: string, agentMessage?: string): Record<string, string> {
22
+ return {
23
+ ...(userMessage && USER_MESSAGE_EVENTS.has(eventName) ? { user_message: userMessage } : {}),
24
+ ...(agentMessage && AGENT_MESSAGE_EVENTS.has(eventName) ? { agent_message: agentMessage } : {}),
25
+ };
26
+ }
27
+
28
+ function joinMessages(...values: unknown[]): string {
29
+ return values.filter((value): value is string => typeof value === "string" && value.length > 0).join("\n");
30
+ }
31
+
32
+ function isRecord(value: unknown): value is Record<string, unknown> {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+
36
+ /**
37
+ * Render a portable policy prompt using the native Cursor event contract.
38
+ * Switches exhaustively on {@link CursorResponseKind} — the `never` default
39
+ * fails to compile if a new kind is ever added without a matching case.
40
+ * `contract.known === false` is not tested separately: the single
41
+ * `UNKNOWN_EVENT` fallback in events.ts always pairs `known: false` with
42
+ * `response: "neutral"`, so both collapse to the same `"{}"` branch.
43
+ */
44
+ export function toCursorResponse(prompt: Prompt, eventName: string): string {
45
+ const contract = cursorEventContract(eventName);
46
+ const message = formatPrompt(prompt);
47
+ switch (contract.response) {
48
+ case "neutral":
49
+ case "plugin-paths":
50
+ return "{}";
51
+ case "post-context":
52
+ case "session-context":
53
+ return capAdditionalContext(JSON.stringify({ additional_context: message }));
54
+ case "followup":
55
+ return JSON.stringify({ followup_message: message });
56
+ case "compact-notice":
57
+ return JSON.stringify({ user_message: prompt.userMessage ?? message });
58
+ case "submit-control":
59
+ return JSON.stringify({ continue: prompt.kind !== "block", user_message: prompt.userMessage ?? message });
60
+ case "permission": {
61
+ if (prompt.kind === "inform") {
62
+ return JSON.stringify({
63
+ permission: "allow",
64
+ ...permissionMessages(eventName, prompt.userMessage, prompt.reason ? message : undefined),
65
+ });
66
+ }
67
+ const userMessage = prompt.kind === "ask"
68
+ ? `[downgraded from ask — Cursor does not enforce approval for this event]\n${message}`
69
+ : message;
70
+ return JSON.stringify({
71
+ permission: "deny",
72
+ ...permissionMessages(eventName, userMessage, userMessage),
73
+ });
74
+ }
75
+ default: {
76
+ const exhaustive: never = contract.response;
77
+ return exhaustive;
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Convert a shared lifecycle handler's output to the native Cursor envelope.
84
+ * The `neutral` and empty-`text` short circuits run before the switch (they
85
+ * apply identically across several {@link CursorResponseKind} values), so
86
+ * only the remaining 7 kinds need a case — `never` below still catches a
87
+ * future kind added without updating this function. This is the single
88
+ * point every Cursor stdout passes through exactly once (see `handle.ts`'s
89
+ * `handleHook`), so `budget` — when supplied — is reserved from and
90
+ * recorded into here, never at the inner `toCursorResponse` pre-cap (that
91
+ * one's output is re-capped here again on the native-passthrough branch
92
+ * below, so budgeting it too would double-count the same contribution).
93
+ * @param stdout - The shared handler's raw stdout for this hook invocation.
94
+ * @param eventName - Cursor's raw `hook_event_name`.
95
+ * @param budget - Shared `additional_context` budget context (see
96
+ * {@link CursorBudgetContext}); `undefined` falls back to the flat
97
+ * per-response 10,000-char cap, unbudgeted.
98
+ */
99
+ export function toCursorLifecycleResponse(stdout: string, eventName: string, budget?: CursorBudgetContext): string {
100
+ const contract = cursorEventContract(eventName);
101
+ const native = parseNativeCursorStdout(stdout, eventName);
102
+ if (native !== null) return capAdditionalContextWithBudget(native, budget);
103
+ let text = stdout;
104
+ let decision: "allow" | "deny" | "ask" | undefined;
105
+ let userMessage = "";
106
+ let agentMessage = "";
107
+ let decisionMessage = "";
108
+ let structured = false;
109
+ try {
110
+ const parsed: unknown = JSON.parse(stdout);
111
+ const subagentAsk = eventName === "subagentStart" && isRecord(parsed)
112
+ && Object.hasOwn(parsed, "permission") && parsed.permission === "ask";
113
+ const shared = (isRecord(parsed) ? parsed : {}) as {
114
+ hookSpecificOutput?: {
115
+ additionalContext?: string;
116
+ permissionDecision?: "allow" | "deny" | "ask";
117
+ permissionDecisionReason?: string;
118
+ };
119
+ systemMessage?: string;
120
+ user_message?: string;
121
+ reason?: string;
122
+ followup_message?: string;
123
+ };
124
+ structured = true;
125
+ decision = shared.hookSpecificOutput?.permissionDecision ?? (subagentAsk ? "ask" : undefined);
126
+ userMessage = shared.systemMessage
127
+ ?? (subagentAsk && Object.hasOwn(shared, "user_message") && typeof shared.user_message === "string"
128
+ ? shared.user_message : "");
129
+ decisionMessage = joinMessages(shared.hookSpecificOutput?.permissionDecisionReason, shared.reason);
130
+ agentMessage = joinMessages(shared.hookSpecificOutput?.additionalContext, decisionMessage);
131
+ text = joinMessages(agentMessage, userMessage, shared.followup_message);
132
+ } catch {
133
+ text = stdout;
134
+ }
135
+ if (contract.response === "neutral") return "{}";
136
+ if (!text) return contract.response === "permission" ? '{"permission":"allow"}' : "{}";
137
+ switch (contract.response) {
138
+ case "session-context":
139
+ case "post-context":
140
+ return capAdditionalContextWithBudget(JSON.stringify({ additional_context: text }), budget);
141
+ case "permission": {
142
+ const permission = decision === "deny" || decision === "ask" ? "deny" : "allow";
143
+ const denied = permission === "deny";
144
+ // Cursor subagentStart can gate creation but has no model-context channel.
145
+ // Drop shared context and its "injected" notice on allow: preserving either
146
+ // would claim delivery the native event contract cannot perform.
147
+ if (eventName === "subagentStart" && !denied) return '{"permission":"allow"}';
148
+ return JSON.stringify({
149
+ permission,
150
+ ...permissionMessages(
151
+ eventName,
152
+ userMessage || (denied ? decisionMessage || agentMessage : ""),
153
+ agentMessage || (denied ? decisionMessage || userMessage : structured ? "" : text),
154
+ ),
155
+ });
156
+ }
157
+ case "followup":
158
+ return JSON.stringify({ followup_message: text });
159
+ case "compact-notice":
160
+ return JSON.stringify({ user_message: text });
161
+ case "submit-control":
162
+ return JSON.stringify({ continue: true, user_message: text });
163
+ case "plugin-paths":
164
+ return "{}";
165
+ default: {
166
+ const exhaustive: never = contract.response;
167
+ return exhaustive;
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Gemini CLI adapter (hook-mode). Schema per google-gemini/gemini-cli docs/hooks (2026):
3
+ * `BeforeTool` blocks via `{ decision: "deny", reason }` (or exit 2).
4
+ */
5
+ import { evaluate } from "../../policy/evaluate";
6
+ import { formatPrompt } from "../../prompt/types";
7
+
8
+ /** `BeforeTool` stdin payload (subset). */
9
+ export interface GeminiHookInput {
10
+ tool_name?: string;
11
+ tool_input?: { command?: string; path?: string; content?: string };
12
+ hook_event_name?: string;
13
+ }
14
+
15
+ /** Hook stdout response. */
16
+ export interface GeminiResponse {
17
+ decision?: "allow" | "deny";
18
+ reason?: string;
19
+ hookSpecificOutput?: { additionalContext?: string };
20
+ }
21
+
22
+ /** Evaluate a tool use; deny on a hard block, otherwise inject context. */
23
+ export function beforeTool(input: GeminiHookInput): GeminiResponse {
24
+ const i = input.tool_input;
25
+ const r = evaluate({
26
+ tool: input.tool_name ?? "write_file",
27
+ filePath: i?.path,
28
+ content: i?.content,
29
+ command: i?.command,
30
+ });
31
+ if (r.decision === "allow" || !r.prompt) return {};
32
+ const msg = formatPrompt(r.prompt);
33
+ return r.prompt.kind === "block"
34
+ ? { decision: "deny", reason: msg }
35
+ : { hookSpecificOutput: { additionalContext: msg } };
36
+ }