@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,63 @@
1
+ /**
2
+ * @module teammate-idle-check
3
+ * TeammateIdle anti-"false done": alongside the existing sniper suggestion
4
+ * ({@link validateTeammateOutput}), verify that the files this teammate ANNOUNCED
5
+ * (session-changes `modifiedFiles`) actually exist on disk. A claimed deliverable
6
+ * missing on disk is a mechanically-verifiable false-done signal → warn the lead.
7
+ * Deduped across the fan-out; silent when nothing is verifiable. Fail-open.
8
+ *
9
+ * Claude-Code-only: no equivalent `TeammateIdle` hook exists on Codex or Hermes,
10
+ * so this handler is never reached through those adapters.
11
+ * @packageDocumentation
12
+ */
13
+ import { existsSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { systemMessage } from "../../adapters/claude";
16
+ import { loadSessionState, sanitizeSessionId } from "../home-state";
17
+ import { oncePerWindow } from "../inject-dedup";
18
+ import { defaultStateDir } from "../paths";
19
+ import { notify } from "../notifications";
20
+ import { validateTeammateOutput } from "./teammate-idle";
21
+
22
+ /** Re-warn about the same idle teammate at most once per 30s (fan-out + retries). */
23
+ const IDLE_DEDUP_MS = 30_000;
24
+
25
+ /** The `changes` block written by `track-changes.ts` into unified session state. */
26
+ interface Changes { modifiedFiles?: string[]; }
27
+
28
+ /** Announced files (session changes) that are ABSENT on disk (the false-done set). */
29
+ function missingDeliverables(sessionId: string, home: string): string[] {
30
+ const changes = loadSessionState(sessionId, home).changes as Changes | undefined;
31
+ return (changes?.modifiedFiles ?? []).filter((f) => typeof f === "string" && f !== "" && !existsSync(f));
32
+ }
33
+
34
+ /**
35
+ * Handle TeammateIdle: merge the existing sniper suggestion with a missing-
36
+ * deliverable warning (deduped) into one advisory `systemMessage`, or "" when
37
+ * neither fires. TeammateIdle rejects `hookSpecificOutput` (no `additionalContext`
38
+ * channel) — `systemMessage` is the only user-visible, non-blocking channel it
39
+ * accepts.
40
+ * @param data - The raw TeammateIdle payload (`teammate_name`, `session_id`).
41
+ * @param cwd - Project root (state dir for the dedup sidecar).
42
+ * @param home - Home dir (defaults to `~`).
43
+ * @param now - Clock (defaults to `Date.now()`).
44
+ * @returns The native hook stdout, or "".
45
+ */
46
+ export function teammateIdleContext(data: Record<string, unknown>, cwd: string, home: string = homedir(), now: number = Date.now()): string {
47
+ const sniper = validateTeammateOutput(data, home);
48
+ const sessionId = sanitizeSessionId(data.session_id);
49
+ const teammate = String(data.teammate_name ?? data.team_name ?? "unknown");
50
+ let notice = "";
51
+ if (sessionId) {
52
+ const missing = missingDeliverables(sessionId, home);
53
+ if (missing.length > 0 && oncePerWindow(`idle:${sessionId}:${teammate}`, IDLE_DEDUP_MS, { now, dir: defaultStateDir(cwd) })) {
54
+ notice = `Teammate '${teammate}' idle but expected deliverable(s) not found on disk: ${missing.slice(0, 5).join(", ")} — verify before treating as done.`;
55
+ }
56
+ }
57
+ const merged = [sniper, notice].filter(Boolean).join("\n\n");
58
+ if (!merged) return "";
59
+ // An actionable idle signal (false-done or sniper suggestion) warrants human
60
+ // attention — voice the "human" sound (fire-and-forget, fail-open no-op).
61
+ notify("human");
62
+ return systemMessage(merged);
63
+ }
@@ -0,0 +1,28 @@
1
+ import { homedir } from "node:os";
2
+ import { loadSessionState, sanitizeSessionId } from "../home-state";
3
+
4
+ /** The `changes` block written by `track-changes.ts` into unified session state. */
5
+ interface Changes {
6
+ cumulativeCodeFiles?: number;
7
+ modifiedFiles?: string[];
8
+ }
9
+
10
+ /**
11
+ * Handle TeammateIdle: when the teammate's session-changes file shows code was
12
+ * modified, suggest sniper validation as plain text — TeammateIdle has no
13
+ * `additionalContext` channel (rejects `hookSpecificOutput`), so the caller
14
+ * ({@link module:teammate-idle-check.teammateIdleContext}) rides this text on
15
+ * the `systemMessage` channel instead. Ports `teammate-idle/validate-teammate-output.py`.
16
+ * @param data - The raw hook payload.
17
+ * @param home - Home dir (defaults to `~`).
18
+ * @returns The plain notice text, or "" when no code file was modified.
19
+ */
20
+ export function validateTeammateOutput(data: Record<string, unknown>, home: string = homedir()): string {
21
+ const teammate = String(data.teammate_name ?? "unknown");
22
+ const sessionId = sanitizeSessionId(data.session_id) ?? "unknown";
23
+ const changes = loadSessionState(sessionId, home).changes as Changes | undefined;
24
+ const count = changes?.cumulativeCodeFiles ?? 0;
25
+ if (count === 0) return "";
26
+ const files = (changes?.modifiedFiles ?? []).slice(0, 5).join(", ");
27
+ return `Teammate '${teammate}' going idle after modifying ${count} code file(s): ${files}. Consider running sniper validation.`;
28
+ }
@@ -0,0 +1,24 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ /**
6
+ * Handle PostToolUseFailure: append a `TOOL_FAILURE` line to
7
+ * `~/.claude/logs/tool-failures.log`, skipping user interrupts. Ports
8
+ * `post-tool-use/log-tool-failure.py`. No stdout (logging only).
9
+ * @param data - The raw hook payload.
10
+ * @param home - Home dir (defaults to `~`).
11
+ * @param now - Clock (defaults to `Date.now()`).
12
+ */
13
+ export function logToolFailure(data: Record<string, unknown>, home: string = homedir(), now: number = Date.now()): void {
14
+ if (data.is_interrupt === true) return;
15
+ const tool = String(data.tool_name ?? "unknown");
16
+ const error = String(data.error ?? "unknown error");
17
+ const sessionId = String(data.session_id ?? "unknown");
18
+ const ts = new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
19
+ const dir = join(home, ".claude", "logs");
20
+ try {
21
+ mkdirSync(dir, { recursive: true });
22
+ appendFileSync(join(dir, "tool-failures.log"), `[${ts}] TOOL_FAILURE session=${sessionId} tool=${tool} error=${error}\n`, "utf-8");
23
+ } catch { /* best effort */ }
24
+ }
@@ -0,0 +1,61 @@
1
+ import { basename } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { attachSystemMessage, contextResponse } from "../../adapters/claude";
4
+ import { loadSessionState, sanitizeSessionId, saveSessionState, sessionsDir } from "../home-state";
5
+ import { onceExclusive } from "../inject-dedup";
6
+ import { BURST_DEDUP_MS } from "../burst-window";
7
+ import { sniperRequiredNotice } from "../notices";
8
+
9
+ /** Code-file extensions tracked for sniper (mirrors track-session-changes.py). */
10
+ const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelte|astro)$/;
11
+
12
+ /** Shape of the `changes` block persisted in per-session state. */
13
+ interface Changes {
14
+ cumulativeCodeFiles: number;
15
+ modifiedFiles: string[];
16
+ lastModifiedFile?: string;
17
+ lastCheck?: string;
18
+ }
19
+
20
+ /**
21
+ * Handle PostToolUse Write/Edit: track the cumulative set of modified code
22
+ * files per session and emit the mandatory "SNIPER VALIDATION REQUIRED"
23
+ * additionalContext. Ports `post-tool-use/track-session-changes.py`.
24
+ * @param sessionIdRaw - Raw session id from the payload.
25
+ * @param filePath - The edited file path.
26
+ * @param home - Home dir (defaults to `~`).
27
+ * @param now - Clock (defaults to `Date.now()`).
28
+ * @returns The native hook stdout (possibly empty when not a code file).
29
+ */
30
+ export function trackSessionChanges(sessionIdRaw: unknown, filePath: string, home: string = homedir(), now: number = Date.now()): string {
31
+ if (!filePath || !CODE_EXT.test(filePath)) return "";
32
+ const sid = sanitizeSessionId(sessionIdRaw) ?? "unknown";
33
+ const state = loadSessionState(sid, home);
34
+ const prev = (state.changes as Changes | undefined) ?? { cumulativeCodeFiles: 0, modifiedFiles: [] };
35
+ const files = [...prev.modifiedFiles];
36
+ let count = prev.cumulativeCodeFiles;
37
+ if (!files.includes(filePath)) { count += 1; files.push(filePath); }
38
+ state.changes = {
39
+ cumulativeCodeFiles: count,
40
+ modifiedFiles: files,
41
+ lastModifiedFile: filePath,
42
+ lastCheck: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z"),
43
+ } satisfies Changes;
44
+ saveSessionState(sid, state, home);
45
+ // Emit the mandatory reminder ONCE per (file+session) burst: the ~11 sibling
46
+ // plugin hooks of one PostToolUse event would otherwise inject it ~11× (token
47
+ // noise). A real re-edit past the window reminds again. See burst-window.
48
+ // Exclusive mode (not the JSON oncePerWindow): this call is exactly the
49
+ // high-concurrency shape that mode is best-effort against under the real
50
+ // ~11-process fan-out (lesson 2026-07-05 16:00).
51
+ if (!onceExclusive(`sniper:${sid}:${filePath}`, BURST_DEDUP_MS, { now, dir: sessionsDir(home) })) return "";
52
+ const fname = basename(filePath);
53
+ const stdout = contextResponse(
54
+ "PostToolUse",
55
+ `SNIPER VALIDATION REQUIRED: Code file '${fname}' was modified. You MUST now run the sniper agent (fuse-ai-pilot:sniper) to validate this modification before continuing. This is mandatory per CLAUDE.md rules.`,
56
+ );
57
+ // User-visible companion to the additionalContext reminder above (which only
58
+ // reaches the agent, never the human) — rides the SAME oncePerWindow gate above,
59
+ // so the ×11 hook fan-out never duplicates it.
60
+ return attachSystemMessage(stdout, sniperRequiredNotice(fname));
61
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PreToolUse (matcher "Write|Edit") for the solid scope: deny a Go top-level
3
+ * interface or a Python `ABC` subclass defined outside its language's
4
+ * `/interfaces/` directory. Ports `solid/scripts/validate-solid.py`'s
5
+ * `check_go`/`check_python` only — its `check_nextjs`/`check_laravel`/
6
+ * `check_swift` siblings are NOT ported here: they are a strict subset of the
7
+ * already-ported `framework-solid-gates.ts` (react/nextjs/laravel/swift),
8
+ * which fire unconditionally on file extension/content rather than being
9
+ * gated behind `SOLID_PROJECT_TYPE`. Inert when `SOLID_PROJECT_TYPE` is
10
+ * absent/"unknown"/nextjs/laravel/swift/rust (parity: the Python hook only
11
+ * has a `go`/`python` validator).
12
+ *
13
+ * Detection rides the conventions module (MASKED content, column-0 anchored
14
+ * for Go): the old naive regexes matched inside comments/strings and
15
+ * indented locals — all killed. LEGACY-level detections keep the exact hard
16
+ * deny (byte-parity); EXTENDED-level ones (Go `interface{` without the
17
+ * space, Python `Protocol`) follow `FUSE_CONVENTIONS_MODE` — advisory
18
+ * (non-blocking additionalContext) by default, deny only on `"deny"`
19
+ * (owner Amendment 5, external audit D0.2).
20
+ */
21
+ import { contextResponse, denyResponse } from "../../adapters/claude";
22
+ import { interfaceDeclLevel } from "../../policy/conventions/interfaces";
23
+ import { conventionsMode } from "../../policy/conventions/verdict";
24
+
25
+ /** Go: deny a top-level interface declared outside `/interfaces/`. */
26
+ function checkGo(filePath: string, content: string, env: Record<string, string | undefined>): string | null {
27
+ if (!filePath.endsWith(".go") || filePath.includes("/interfaces/")) return null;
28
+ const level = interfaceDeclLevel(filePath, content);
29
+ if (level === null) return null;
30
+ const msg = "SOLID: Interfaces must be in internal/interfaces/";
31
+ if (level === "legacy" || conventionsMode(env) === "deny") return denyResponse("PreToolUse", msg);
32
+ return contextResponse("PreToolUse", msg);
33
+ }
34
+
35
+ /** Python: deny an ABC subclass declared outside `/interfaces/`. */
36
+ function checkPython(filePath: string, content: string, env: Record<string, string | undefined>): string | null {
37
+ if (!filePath.endsWith(".py") || filePath.includes("/interfaces/")) return null;
38
+ const level = interfaceDeclLevel(filePath, content);
39
+ if (level === null) return null;
40
+ const msg = "SOLID: Abstract classes must be in src/interfaces/";
41
+ if (level === "legacy" || conventionsMode(env) === "deny") return denyResponse("PreToolUse", msg);
42
+ return contextResponse("PreToolUse", msg);
43
+ }
44
+
45
+ /** `SOLID_PROJECT_TYPE` values this gate validates (parity subset — see module doc). */
46
+ const VALIDATORS: Record<string, (filePath: string, content: string, env: Record<string, string | undefined>) => string | null> = {
47
+ go: checkGo,
48
+ python: checkPython,
49
+ };
50
+
51
+ /**
52
+ * Deny (or advise on) a Write/Edit that violates the Go/Python interface-location SOLID rule.
53
+ * @param tool - The tool name (only "Write"/"Edit" are checked).
54
+ * @param filePath - The written file's absolute path.
55
+ * @param content - The written/edited content (`new_string` snippet on Edit).
56
+ * @param env - Environment (defaults to `process.env`).
57
+ * @returns The PreToolUse response, or `""` when clean/inert.
58
+ */
59
+ export function validateSolidGate(
60
+ tool: string,
61
+ filePath: string,
62
+ content: string,
63
+ env: Record<string, string | undefined> = process.env,
64
+ ): string {
65
+ const ptype = env.SOLID_PROJECT_TYPE ?? "";
66
+ if (!ptype || ptype === "unknown") return "";
67
+ if (tool !== "Write" && tool !== "Edit") return "";
68
+ if (!filePath) return "";
69
+ return VALIDATORS[ptype]?.(filePath, content, env) ?? "";
70
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * PostToolUse (matcher "Write|Edit") for the tailwindcss scope: warn
3
+ * (non-blocking) on a deprecated `@tailwind` directive, excessive `@apply`
4
+ * usage, and an overlong `className` string on a just-written CSS/TSX/JSX
5
+ * file inside a detected Tailwind project. Ports `validate-tailwind.py`.
6
+ */
7
+ import { dirname, join } from "node:path";
8
+ import { readText, pathExists } from "../../util/runtime-io";
9
+ import { contextResponse } from "../../adapters/claude";
10
+
11
+ /** Tailwind config file names checked at each ancestor directory. */
12
+ const TAILWIND_CONFIGS = ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"];
13
+
14
+ /** Walk up to 20 levels from `filePath`'s directory to the nearest ancestor holding `package.json` or a Tailwind config (parity `find_project_root`). */
15
+ function findProjectRoot(filePath: string): string | null {
16
+ let dir = dirname(filePath);
17
+ for (let i = 0; i < 20; i++) {
18
+ if (pathExists(join(dir, "package.json")) || TAILWIND_CONFIGS.some((cfg) => pathExists(join(dir, cfg)))) return dir;
19
+ const parent = dirname(dir);
20
+ if (parent === dir) break;
21
+ dir = parent;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ /** True when `filePath` belongs to a Tailwind project (v3 config, or `tailwindcss` in package.json deps). Parity `is_tailwind_project`. */
27
+ function isTailwindProject(filePath: string): boolean {
28
+ const root = findProjectRoot(filePath);
29
+ if (!root) return false;
30
+ if (TAILWIND_CONFIGS.some((cfg) => pathExists(join(root, cfg)))) return true;
31
+ const pkgPath = join(root, "package.json");
32
+ if (!pathExists(pkgPath)) return false;
33
+ try {
34
+ const pkg = JSON.parse(readText(pkgPath)) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
35
+ return Boolean(pkg.dependencies?.tailwindcss) || Boolean(pkg.devDependencies?.tailwindcss);
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Validate Tailwind best practices on a just-written file.
43
+ * @param tool - The tool name (only "Write"/"Edit" are checked).
44
+ * @param filePath - The written file's absolute path.
45
+ * @returns The PostToolUse `additionalContext` response, or `""` when clean/inert.
46
+ */
47
+ export function validateTailwind(tool: string, filePath: string): string {
48
+ if (tool !== "Write" && tool !== "Edit") return "";
49
+ if (!/\.(css|tsx|jsx)$/.test(filePath)) return "";
50
+ if (!pathExists(filePath) || !isTailwindProject(filePath)) return "";
51
+
52
+ let content: string;
53
+ try {
54
+ content = readText(filePath);
55
+ } catch {
56
+ return "";
57
+ }
58
+
59
+ const warnings: string[] = [];
60
+
61
+ if (filePath.endsWith(".css")) {
62
+ if (/@tailwind (base|components|utilities)/.test(content)) {
63
+ warnings.push("Tailwind v4: @tailwind directives are deprecated - use @import 'tailwindcss'.");
64
+ }
65
+ const applyCount = (content.match(/@apply/g) ?? []).length;
66
+ if (applyCount > 10) {
67
+ warnings.push(`Excessive @apply usage (${applyCount}) - prefer utility classes directly.`);
68
+ }
69
+ }
70
+
71
+ if (/\.(tsx|jsx)$/.test(filePath)) {
72
+ const longClasses = (content.match(/className="[^"]{150,}"/g) ?? []).length;
73
+ if (longClasses > 0) {
74
+ warnings.push(`Very long className (${longClasses} lines) - extract to @utility or use cn().`);
75
+ }
76
+ }
77
+
78
+ if (warnings.length === 0) return "";
79
+ return contextResponse("PostToolUse", warnings.join(" "));
80
+ }
@@ -0,0 +1,74 @@
1
+ import { dispatchLifecycle, postEditTypescript, trackSessionChanges, type PluginScope } from "./lifecycle";
2
+ import { autoDocumentRead } from "./lifecycle/auto-document-reads";
3
+ import { contextResponse } from "../adapters/claude";
4
+ import { cursorEventContract } from "../adapters/cursor/events";
5
+ import { toCursorLifecycleResponse } from "../adapters/cursor/respond";
6
+ import type { NormalizedEvent } from "./normalize";
7
+
8
+ /** Raw event name from a payload (Cline lacks one; lifecycle is Claude-only). */
9
+ function rawEvent(payload: Record<string, unknown>): string {
10
+ return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
11
+ }
12
+
13
+ /** Extract the `additionalContext` body from a `contextResponse(...)` stdout string ("" when empty/unparseable). */
14
+ function additionalContextOf(stdout: string): string {
15
+ if (!stdout) return "";
16
+ try {
17
+ const parsed = JSON.parse(stdout) as { hookSpecificOutput?: { additionalContext?: string } };
18
+ return parsed.hookSpecificOutput?.additionalContext ?? "";
19
+ } catch {
20
+ return "";
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Run the ported lifecycle/session/context hooks (SessionStart, SubagentStart/
26
+ * Stop, TeammateIdle, PostToolUseFailure, PreCompact, SessionEnd,
27
+ * InstructionsLoaded, rules-scope UserPromptSubmit). Returns the native stdout
28
+ * when handled, or `null` to fall through to the tool-use pipeline.
29
+ * @param payload - The raw hook payload.
30
+ * @param cwd - Project root.
31
+ * @param scope - The invoking plugin scope (defaults to `core`).
32
+ * @param now - Clock.
33
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
34
+ * @returns The native stdout, or `null` when unhandled.
35
+ */
36
+ export function lifecycleStdout(payload: Record<string, unknown>, cwd: string, scope: PluginScope, now: number, id: string = "claude-code"): string | null {
37
+ const wireEvent = rawEvent(payload);
38
+ if (id !== "cursor") return dispatchLifecycle({ event: wireEvent, payload, cwd, scope, now, id });
39
+ const lifecycleEvent = cursorEventContract(wireEvent).lifecycle;
40
+ if (lifecycleEvent === null) return null;
41
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : payload.conversation_id;
42
+ const stdout = dispatchLifecycle({
43
+ event: lifecycleEvent,
44
+ payload: { ...payload, session_id: sessionId },
45
+ cwd,
46
+ scope,
47
+ now,
48
+ id,
49
+ });
50
+ return stdout === null ? null : toCursorLifecycleResponse(stdout, wireEvent);
51
+ }
52
+
53
+ /**
54
+ * Post-edit additions for core-scope PostToolUse: auto-document a Read of a
55
+ * SKILL.md/README/docs file; else (Write/Edit) track cumulative session
56
+ * changes (sniper reminder) AND report eslint/prettier issues — parity
57
+ * core-guards hooks.json, which runs both as independent PostToolUse commands
58
+ * rather than short-circuiting one on the other. Returns the combined extra
59
+ * stdout, or "" when nothing to emit.
60
+ * @param scope - The invoking plugin scope.
61
+ * @param event - The normalized event.
62
+ * @param now - Clock.
63
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
64
+ * @returns The extra stdout (possibly empty).
65
+ */
66
+ export async function postEditContext(scope: PluginScope, event: NormalizedEvent, now: number, id: string = "claude-code"): Promise<string> {
67
+ if (scope !== "core" || !event.filePath) return "";
68
+ if (event.tool === "Read") return autoDocumentRead(event.filePath, now, id);
69
+ if (event.tool !== "Write" && event.tool !== "Edit") return "";
70
+ const sniper = trackSessionChanges(event.sessionId, event.filePath, undefined, now);
71
+ const lint = postEditTypescript(event.filePath);
72
+ if (!sniper || !lint) return sniper || lint;
73
+ return contextResponse("PostToolUse", `${additionalContextOf(sniper)}\n\n${additionalContextOf(lint)}`);
74
+ }
@@ -0,0 +1,44 @@
1
+ /** Cache-key + tool-class helpers for the MCP/WebFetch interception pipeline. */
2
+ import { parseEnvInt } from "../config/env";
3
+
4
+ /** Default cached-MCP freshness in seconds (48h). */
5
+ const MCP_TTL_DEFAULT_SEC = 172_800;
6
+ /** Default WebFetch freshness in seconds (24h, parity with Python). */
7
+ const WEBFETCH_TTL_DEFAULT_SEC = 86_400;
8
+
9
+ /** Cached-MCP freshness (ms) from `FUSE_MCP_TTL_SEC` (default 48h). */
10
+ export const MCP_TTL_MS: number = parseEnvInt(process.env.FUSE_MCP_TTL_SEC, MCP_TTL_DEFAULT_SEC) * 1000;
11
+
12
+ /** WebFetch freshness (ms) from `FUSE_WEBFETCH_TTL_SEC` (default 24h; pages stale faster than docs). */
13
+ export const WEBFETCH_TTL_MS: number = parseEnvInt(process.env.FUSE_WEBFETCH_TTL_SEC, WEBFETCH_TTL_DEFAULT_SEC) * 1000;
14
+
15
+ /** WebFetch prompt slice folded into its cache key (parity with Python prompt[:500]). */
16
+ const PROMPT_TRUNC = 500;
17
+
18
+ /** True for the WebFetch tool (exact name) or Kimi Code CLI's `FetchURL` equivalent. */
19
+ export function isWebFetch(tool: string): boolean {
20
+ return tool === "WebFetch" || tool === "FetchURL";
21
+ }
22
+
23
+ /** MCP doc tools + WebFetch/FetchURL whose calls are cached / verbosity-capped. */
24
+ export function isMcpTool(tool: string): boolean {
25
+ return /context7|exa|webfetch|web_fetch/i.test(tool) || isWebFetch(tool);
26
+ }
27
+
28
+ /** The query/url that keys the cache. */
29
+ export function queryOf(input: Record<string, unknown>): string {
30
+ const q = input.query ?? input.url ?? input.libraryId ?? "";
31
+ return typeof q === "string" ? q : JSON.stringify(q);
32
+ }
33
+
34
+ /**
35
+ * The string that keys the cache for `tool`. WebFetch folds `url + "\n" +
36
+ * prompt[:500]` (parity with Python) so distinct prompts on the same URL never
37
+ * collide; every other tool keys on {@link queryOf}.
38
+ */
39
+ export function cacheQueryOf(tool: string, input: Record<string, unknown>): string {
40
+ if (!isWebFetch(tool)) return queryOf(input);
41
+ const url = typeof input.url === "string" ? input.url.trim() : "";
42
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
43
+ return `${url}\n${prompt.slice(0, PROMPT_TRUNC)}`;
44
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @module mcp-tool-name
3
+ * Canonicalizes a Codex-only MCP `tool_name` alias back to its original
4
+ * hyphenated form, so every existing dash-keyed consumer (`SHOT_TOOLS`,
5
+ * `RESEARCH_TOOLS`/`classifyExplore`, `docSourceOf`, the design-pipeline
6
+ * `NAV`/`SCROLL`/`GEMINI` constants, …) recognizes a Codex MCP call exactly
7
+ * like the same call on Claude/Kimi/Cursor/Cline/Gemini — no per-consumer
8
+ * change needed.
9
+ *
10
+ * Codex CLI globally rewrites every `-` to `_` across the FULL qualified MCP
11
+ * tool identifier before it ever reaches a hook, in `sanitize_responses_api_tool_name()`
12
+ * (`codex-rs/core/src/mcp_connection_manager.rs`, introduced by
13
+ * openai/codex#14605 — "Normalize MCP tool names to code-mode safe form",
14
+ * code-mode forbids `-` in identifiers). openai/codex#18385's own
15
+ * `SCENARIO_DASH_TOOL` test proves the REWRITTEN form is what reaches hooks:
16
+ * the `echo-tool` tool of server `rmcp` is delivered to hooks as
17
+ * `mcp__rmcp__echo_tool`. Confirmed independently in THIS repo's terrain:
18
+ * `~/.codex/config.toml` declares `[mcp_servers.fuse-browser]` (dash) while
19
+ * Codex session logs and this harness's own dual-marketplace hook payloads
20
+ * (`~/.codex/plugins/.../hooks.json` matchers) observe `mcp__fuse_browser__*`
21
+ * (underscore) — Claude never emits that form (0 occurrences across its
22
+ * transcripts vs 137x the dash form).
23
+ *
24
+ * The rewrite is deliberately NOT undone with a generic `_` -> `-` regex:
25
+ * openai/codex#15832 and #15565 document it as NON-INVERSIBLE — a server
26
+ * literally named `autok_local` collides with a dash-named `autok-local`
27
+ * once both pass through the same rewrite, and OpenAI's own fix was a closed
28
+ * canonical table, never a general regex. The SAME ambiguity exists on our
29
+ * side: a generic reversal would corrupt tool segments that legitimately
30
+ * contain an underscore already (`browser_screenshot`, `node_repl`,
31
+ * `build_sim`, …). So this module only reverses the FOUR entries proven
32
+ * above to matter to this repo's gates — every other segment passes through
33
+ * untouched.
34
+ * @packageDocumentation
35
+ */
36
+
37
+ /**
38
+ * Codex-mangled MCP server segment -> its real (dash) server name. Closed set
39
+ * — see module doc. `Object.create(null)` (never a `{}` literal): `serverKey`
40
+ * is an attacker-influenced string (an MCP server can register itself as
41
+ * `"__proto__"`/`"constructor"`/`"toString"`), and a `{}`-literal lookup by
42
+ * such a key returns the INHERITED `Object.prototype` member instead of
43
+ * `undefined` — an object/function, so `?? serverKey` never falls back and
44
+ * the result silently corrupts to `"mcp__[object Object]__…"` (proven in
45
+ * `test/mcp-tool-name.test.ts`, prototype-pollution-read case). A null-
46
+ * prototype object has no inherited members, so every lookup outside the two
47
+ * literal keys below is a clean miss.
48
+ */
49
+ const SERVER_ALIASES: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
50
+ fuse_browser: "fuse-browser",
51
+ gemini_design: "gemini-design",
52
+ });
53
+
54
+ /**
55
+ * Codex-mangled MCP tool segment -> its real (dash) tool name. Both entries
56
+ * are `context7`-exclusive tool names — applied ONLY when the canonical
57
+ * server is `"context7"` (see the explicit guard in
58
+ * {@link canonicalizeMcpToolName}), never globally. A same-named tool on any
59
+ * other server (e.g. a hypothetical `exa__query_docs`) passes through
60
+ * untouched: openai/codex#15832 documents exactly this class of collision as
61
+ * the reason the rewrite must stay a closed, scoped table, never a blanket
62
+ * one. Closed set, same null-prototype rationale as {@link SERVER_ALIASES}.
63
+ */
64
+ const TOOL_ALIASES: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
65
+ query_docs: "query-docs",
66
+ resolve_library_id: "resolve-library-id",
67
+ });
68
+
69
+ /**
70
+ * Canonicalize a Codex-only MCP tool-name alias to its dash form. Scoped to
71
+ * `id === "codex"` — every other harness id passes through unchanged. The
72
+ * server segment is rewritten via the closed {@link SERVER_ALIASES} table;
73
+ * the tool segment is rewritten via {@link TOOL_ALIASES} ONLY when the
74
+ * (already-resolved) canonical server is `"context7"` — every other server's
75
+ * tool segment is left untouched even if it happens to share a name with a
76
+ * context7 tool. Every other `mcp__<server>__<tool>` segment (or non-MCP
77
+ * tool name) is returned byte-identical (see `test/mcp-tool-name.test.ts`
78
+ * for the non-regression proof across ids, trap tool names, and the
79
+ * server-scoping cases).
80
+ * @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
81
+ * @param tool - Raw `tool_name` from the hook payload.
82
+ */
83
+ export function canonicalizeMcpToolName(id: string, tool: string): string {
84
+ if (id !== "codex") return tool;
85
+ const parts = tool.split("__");
86
+ if (parts.length < 3 || parts[0] !== "mcp") return tool;
87
+ const serverKey = parts[1] ?? "";
88
+ const toolKey = parts.slice(2).join("__");
89
+ const server = SERVER_ALIASES[serverKey] ?? serverKey;
90
+ const rest = server === "context7" ? (TOOL_ALIASES[toolKey] ?? toolKey) : toolKey;
91
+ return `mcp__${server}__${rest}`;
92
+ }
@@ -0,0 +1,91 @@
1
+ import { capVerbosity } from "../policy/verbosity";
2
+ import { cacheLookupMeta, cacheLookupSubstringMeta, type CacheHit } from "../cache/store";
3
+ import { extractText } from "../cache/mcp-response";
4
+ import { mcpCacheWrite, webfetchCacheWrite } from "../cache/mcp-store";
5
+ import { WEBFETCH_TTL_MS, cacheQueryOf, isMcpTool, isWebFetch } from "./mcp-key";
6
+ import { kimiDenyResponse } from "../adapters/kimi";
7
+
8
+ export { MCP_TTL_MS, WEBFETCH_TTL_MS, cacheQueryOf, isMcpTool, queryOf } from "./mcp-key";
9
+
10
+ function denyWith(id: string, content: string): string {
11
+ if (id === "claude-code" || id === "codex") {
12
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: content } });
13
+ }
14
+ if (id === "gemini-cli") return JSON.stringify({ decision: "deny", reason: content });
15
+ if (id === "kimi") return kimiDenyResponse(content);
16
+ return "";
17
+ }
18
+
19
+ function mutateWith(id: string, input: Record<string, unknown>): string {
20
+ if (id === "claude-code" || id === "codex") {
21
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: input } });
22
+ }
23
+ if (id === "gemini-cli") return JSON.stringify({ hookSpecificOutput: { tool_input: input } });
24
+ // Kimi has no input-mutation contract (`updatedInput` is undocumented
25
+ // upstream) — the exa verbosity cap stays inoperative there.
26
+ return "";
27
+ }
28
+
29
+ /** The doc provider a served cache-hit satisfies (`exa`/`context7`), else undefined. */
30
+ function docSourceOf(tool: string): string | undefined {
31
+ if (/exa/i.test(tool)) return "exa";
32
+ if (/context7/i.test(tool)) return "context7";
33
+ return undefined;
34
+ }
35
+
36
+ /**
37
+ * Wrap a cache hit's body in the harness-facing `CACHE HIT` notice — parity with
38
+ * `webfetch-cache-lookup.py` / `mcp-cache-lookup.py` (KB = body chars/1024 + 1,
39
+ * age hours = ageMs/3_600_000, both floored).
40
+ */
41
+ function cacheHitText(web: boolean, hit: CacheHit): string {
42
+ const kb = Math.floor(hit.body.length / 1024) + 1;
43
+ const hours = Math.floor(hit.ageMs / 3_600_000);
44
+ return web
45
+ ? `CACHE HIT WebFetch (~${kb}KB saved, cached ${hours}h ago):\n\n${hit.body}\n\nModify the URL or query to force a fresh fetch.`
46
+ : `CACHE HIT (~${kb}KB saved, cached ${hours}h ago): ${hit.body}\n\nRephrase to force a re-call.`;
47
+ }
48
+
49
+ /** A pre-event MCP interception: the native response + any doc source it satisfied. */
50
+ export interface McpIntercept {
51
+ stdout: string;
52
+ docSource?: string;
53
+ }
54
+
55
+ /**
56
+ * Pre-event MCP interception: serve a fresh cache hit (deny + cached content),
57
+ * else cap exa verbosity (allow + mutated input), else null to allow normally.
58
+ * Harnesses without input-mutation/cache support fall through to null.
59
+ */
60
+ export function mcpPreIntercept(id: string, tool: string, input: Record<string, unknown>, dir: string, ttlMs: number, now: number): McpIntercept | null {
61
+ if (!isMcpTool(tool)) return null;
62
+ const web = isWebFetch(tool);
63
+ const ttl = web ? WEBFETCH_TTL_MS : ttlMs;
64
+ const key = cacheQueryOf(tool, input);
65
+ // Exact key first; for MCP docs fall back to a substring hit (Python `rg -i -F`).
66
+ const hit = cacheLookupMeta(dir, tool, key, ttl, now) ?? (web ? null : cacheLookupSubstringMeta(dir, key, ttl, now));
67
+ if (hit) {
68
+ const served = denyWith(id, cacheHitText(web, hit));
69
+ if (served) return { stdout: served, docSource: docSourceOf(tool) };
70
+ }
71
+ const capped = capVerbosity(tool, input);
72
+ if (capped) {
73
+ const mutated = mutateWith(id, capped);
74
+ if (mutated) return { stdout: mutated };
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * Post-event: persist the MCP/WebFetch response (extracted to markdown). MCP docs
81
+ * go through {@link mcpCacheWrite} (compact + Jaccard-dedup + `index.json`);
82
+ * WebFetch uses {@link webfetchCacheWrite} (compact, exact key, no index).
83
+ * @param now - Current epoch ms (timestamp source; defaults to `Date.now()`).
84
+ */
85
+ export function mcpPostStore(tool: string, input: Record<string, unknown>, response: unknown, dir: string, now: number = Date.now()): void {
86
+ if (!isMcpTool(tool)) return;
87
+ const text = extractText(response);
88
+ const key = cacheQueryOf(tool, input);
89
+ if (isWebFetch(tool)) webfetchCacheWrite(dir, tool, key, text, now);
90
+ else mcpCacheWrite(dir, tool, key, text, now);
91
+ }