@fusengine/harness 0.1.90 → 0.1.91

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 (417) 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-C43gA-Pr.mjs} +701 -134
  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-BjG6unTj.mjs +317 -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 +27 -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.ts +46 -0
  44. package/src/adapters/cursor/events.ts +38 -0
  45. package/src/adapters/cursor/index.ts +43 -0
  46. package/src/adapters/cursor/interfaces/types.ts +88 -0
  47. package/src/adapters/cursor/native-response.ts +158 -0
  48. package/src/adapters/cursor/normalize.ts +123 -0
  49. package/src/adapters/cursor/respond.ts +123 -0
  50. package/src/adapters/gemini/index.ts +36 -0
  51. package/src/adapters/hermes/index.ts +55 -0
  52. package/src/adapters/hermes/interfaces/types.ts +26 -0
  53. package/src/adapters/kimi/index.ts +96 -0
  54. package/src/adapters/kimi/interfaces/types.ts +55 -0
  55. package/src/cache/compact.ts +55 -0
  56. package/src/cache/index.ts +5 -0
  57. package/src/cache/io.ts +38 -0
  58. package/src/cache/mcp-response.ts +39 -0
  59. package/src/cache/mcp-store.ts +70 -0
  60. package/src/cache/store.ts +93 -0
  61. package/src/changelog/fetch.ts +79 -0
  62. package/src/cli/bin.ts +100 -0
  63. package/src/cli/cursor-event-scanner.ts +175 -0
  64. package/src/cli/cursor-stdin-reader.ts +65 -0
  65. package/src/cli/doctor.ts +96 -0
  66. package/src/cli/hook-io.ts +130 -0
  67. package/src/cli/hook-sound.ts +49 -0
  68. package/src/cli/index.ts +1 -0
  69. package/src/cli/json-primitive-scanner.ts +83 -0
  70. package/src/cli/run.ts +36 -0
  71. package/src/cli/scope.ts +31 -0
  72. package/src/codex-rules/build.ts +63 -0
  73. package/src/codex-rules/index.ts +8 -0
  74. package/src/codex-rules/mapping.ts +42 -0
  75. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  76. package/src/codex-rules/rules/ask.ts +44 -0
  77. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  78. package/src/codex-rules/rules/dd.ts +23 -0
  79. package/src/codex-rules/rules/diskutil.ts +33 -0
  80. package/src/codex-rules/rules/mkfs.ts +42 -0
  81. package/src/codex-rules/rules/privilege.ts +49 -0
  82. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  83. package/src/codex-rules/rules/rm-variants.ts +58 -0
  84. package/src/codex-rules/rules/single-commands.ts +57 -0
  85. package/src/codex-rules/skip-list.ts +25 -0
  86. package/src/codex-rules/starlark.ts +46 -0
  87. package/src/codex-rules/types.ts +36 -0
  88. package/src/config/dotenv.ts +55 -0
  89. package/src/config/env.ts +10 -0
  90. package/src/config/home-dir.ts +36 -0
  91. package/src/config/index.ts +5 -0
  92. package/src/config/layout.ts +52 -0
  93. package/src/config/limits.ts +56 -0
  94. package/src/config/ttl.ts +30 -0
  95. package/src/detect/harness.ts +73 -0
  96. package/src/detect/index.ts +1 -0
  97. package/src/detect/interfaces/types.ts +18 -0
  98. package/src/freshness/agent-evidence-record.ts +95 -0
  99. package/src/freshness/agent-evidence.ts +95 -0
  100. package/src/freshness/codex-spawn-evidence.ts +91 -0
  101. package/src/freshness/doc-helpers.ts +90 -0
  102. package/src/freshness/evidence-harvest-io.ts +72 -0
  103. package/src/freshness/evidence-harvest.ts +61 -0
  104. package/src/freshness/explore-tools.ts +95 -0
  105. package/src/freshness/index.ts +2 -0
  106. package/src/freshness/query-framework.ts +73 -0
  107. package/src/freshness/ref-evidence.ts +49 -0
  108. package/src/freshness/ref-journal.ts +98 -0
  109. package/src/freshness/trivial-edit-counter.ts +30 -0
  110. package/src/index.ts +11 -0
  111. package/src/init/index.ts +2 -0
  112. package/src/init/run.ts +32 -0
  113. package/src/init/templates.ts +75 -0
  114. package/src/memory/gitignore.ts +14 -0
  115. package/src/memory/index.ts +3 -0
  116. package/src/memory/registry.ts +39 -0
  117. package/src/memory/session-roots.ts +99 -0
  118. package/src/memory/state.ts +58 -0
  119. package/src/policy/apex-authorization.ts +100 -0
  120. package/src/policy/apex-gates.ts +91 -0
  121. package/src/policy/apex-target-steps.ts +73 -0
  122. package/src/policy/apex-target.ts +49 -0
  123. package/src/policy/apex-task-context.ts +81 -0
  124. package/src/policy/apex.ts +94 -0
  125. package/src/policy/cartographer/build-tree.ts +69 -0
  126. package/src/policy/cartographer/describe.ts +53 -0
  127. package/src/policy/cartographer/entry.ts +38 -0
  128. package/src/policy/cartographer/frontmatter.ts +48 -0
  129. package/src/policy/cartographer/indicators.ts +34 -0
  130. package/src/policy/claude-md-context.ts +66 -0
  131. package/src/policy/conventions/index.ts +11 -0
  132. package/src/policy/conventions/interfaces.ts +71 -0
  133. package/src/policy/conventions/langs.ts +53 -0
  134. package/src/policy/conventions/query.ts +49 -0
  135. package/src/policy/conventions/react-hooks.ts +18 -0
  136. package/src/policy/conventions/store-or-query.ts +21 -0
  137. package/src/policy/conventions/stores.ts +56 -0
  138. package/src/policy/conventions/strip.ts +87 -0
  139. package/src/policy/conventions/verdict.ts +38 -0
  140. package/src/policy/creation-intent.ts +11 -0
  141. package/src/policy/deny-loop.ts +88 -0
  142. package/src/policy/design/allowed-write.ts +28 -0
  143. package/src/policy/design/content-checks.ts +45 -0
  144. package/src/policy/design/corpus-fs.ts +21 -0
  145. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  146. package/src/policy/design/corpus-resolve.ts +99 -0
  147. package/src/policy/design/corpus.ts +92 -0
  148. package/src/policy/design/design-system-rules.ts +45 -0
  149. package/src/policy/design/flag.ts +30 -0
  150. package/src/policy/design/gates-pipeline.ts +105 -0
  151. package/src/policy/design/gates.ts +87 -0
  152. package/src/policy/design/screenshot-tools.ts +24 -0
  153. package/src/policy/design/skill-evidence.ts +34 -0
  154. package/src/policy/design/skill-gate.ts +81 -0
  155. package/src/policy/design/skill-triggers.ts +51 -0
  156. package/src/policy/design/state.ts +92 -0
  157. package/src/policy/design/template-urls.ts +54 -0
  158. package/src/policy/design/transitions.ts +68 -0
  159. package/src/policy/detect-claude-md-project-type.ts +32 -0
  160. package/src/policy/detect-framework.ts +99 -0
  161. package/src/policy/detect-primitive-checks.ts +100 -0
  162. package/src/policy/detect-primitive-lib.ts +55 -0
  163. package/src/policy/detect-project.ts +99 -0
  164. package/src/policy/edit-outcome.ts +53 -0
  165. package/src/policy/evaluate.ts +81 -0
  166. package/src/policy/expert-agents.ts +60 -0
  167. package/src/policy/file-size-scope.ts +32 -0
  168. package/src/policy/file-size.ts +99 -0
  169. package/src/policy/framework-solid-exclude.ts +52 -0
  170. package/src/policy/framework-solid-extended.ts +93 -0
  171. package/src/policy/framework-solid-gates-systems.ts +99 -0
  172. package/src/policy/framework-solid-gates.ts +98 -0
  173. package/src/policy/framework-solid.ts +88 -0
  174. package/src/policy/gemini-mcp-gate.ts +83 -0
  175. package/src/policy/git-gates.ts +40 -0
  176. package/src/policy/guards/bash-command-anchor.ts +40 -0
  177. package/src/policy/guards/bash-write-patterns.ts +140 -0
  178. package/src/policy/guards/bash-write-redirects.ts +201 -0
  179. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  180. package/src/policy/guards/bash-write.ts +76 -0
  181. package/src/policy/guards/context.ts +12 -0
  182. package/src/policy/guards/index.ts +61 -0
  183. package/src/policy/guards/install.ts +22 -0
  184. package/src/policy/guards/interface-separation-ext.ts +41 -0
  185. package/src/policy/guards/interface-separation.ts +100 -0
  186. package/src/policy/guards/protected-path.ts +100 -0
  187. package/src/policy/guards/security.ts +97 -0
  188. package/src/policy/index.ts +19 -0
  189. package/src/policy/interfaces/types.ts +46 -0
  190. package/src/policy/js-gate-route.ts +51 -0
  191. package/src/policy/lessons/lesson-gate.ts +0 -0
  192. package/src/policy/lessons/trigger-index.ts +84 -0
  193. package/src/policy/lessons/types.ts +31 -0
  194. package/src/policy/module-layout.ts +31 -0
  195. package/src/policy/nearest-manifest.ts +75 -0
  196. package/src/policy/never-approval.ts +95 -0
  197. package/src/policy/patterns.ts +80 -0
  198. package/src/policy/seo/validate.ts +51 -0
  199. package/src/policy/shadcn-project.ts +22 -0
  200. package/src/policy/shadcn-skill-gate.ts +97 -0
  201. package/src/policy/shell-read-refs.ts +96 -0
  202. package/src/policy/skill-path.ts +46 -0
  203. package/src/policy/skill-patterns/laravel.ts +40 -0
  204. package/src/policy/skill-patterns/nextjs.ts +29 -0
  205. package/src/policy/skill-patterns/react.ts +27 -0
  206. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  207. package/src/policy/skill-patterns/shadcn.ts +66 -0
  208. package/src/policy/skill-patterns/swift.ts +30 -0
  209. package/src/policy/skill-patterns/tailwind.ts +40 -0
  210. package/src/policy/skill-source.ts +52 -0
  211. package/src/policy/skill-trigger-patterns.ts +33 -0
  212. package/src/policy/skill-triggers.ts +108 -0
  213. package/src/policy/tailwind-skill-gate.ts +63 -0
  214. package/src/policy/trivial-edits.ts +52 -0
  215. package/src/policy/verbosity.ts +39 -0
  216. package/src/prompt/index.ts +1 -0
  217. package/src/prompt/types.ts +36 -0
  218. package/src/refs/discover.ts +69 -0
  219. package/src/refs/frontmatter.ts +24 -0
  220. package/src/refs/index.ts +4 -0
  221. package/src/refs/loader.ts +59 -0
  222. package/src/refs/ref-key.ts +41 -0
  223. package/src/refs/router.ts +63 -0
  224. package/src/refs/types.ts +25 -0
  225. package/src/runtime/activity.ts +99 -0
  226. package/src/runtime/apply-patch-gate.ts +41 -0
  227. package/src/runtime/burst-window.ts +21 -0
  228. package/src/runtime/codex-shell-tool.ts +37 -0
  229. package/src/runtime/command-string.ts +37 -0
  230. package/src/runtime/confirm/codex-confirm.ts +126 -0
  231. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  232. package/src/runtime/confirm/confirm-code.ts +33 -0
  233. package/src/runtime/confirm/confirm-gate.ts +65 -0
  234. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  235. package/src/runtime/confirm/confirm-pending.ts +34 -0
  236. package/src/runtime/confirm/confirm-state.ts +56 -0
  237. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  238. package/src/runtime/confirm/confirm-submit.ts +38 -0
  239. package/src/runtime/core-guards-active.ts +84 -0
  240. package/src/runtime/core-guards-wired.ts +47 -0
  241. package/src/runtime/deny-loop-store.ts +94 -0
  242. package/src/runtime/deny-notice.ts +52 -0
  243. package/src/runtime/design-cache-resolve.ts +51 -0
  244. package/src/runtime/design-content-gate.ts +95 -0
  245. package/src/runtime/design-files-gate.ts +114 -0
  246. package/src/runtime/design-helpers.ts +93 -0
  247. package/src/runtime/design-lifecycle.ts +39 -0
  248. package/src/runtime/design-read-credit.ts +28 -0
  249. package/src/runtime/design.ts +116 -0
  250. package/src/runtime/dev-context.ts +43 -0
  251. package/src/runtime/dry-patterns.ts +32 -0
  252. package/src/runtime/dry.ts +96 -0
  253. package/src/runtime/fragment-registry.ts +33 -0
  254. package/src/runtime/framework-skill-gate.ts +79 -0
  255. package/src/runtime/fs-cleanup.ts +71 -0
  256. package/src/runtime/gate-apex.ts +81 -0
  257. package/src/runtime/gate-helpers.ts +52 -0
  258. package/src/runtime/gate-input.ts +26 -0
  259. package/src/runtime/gate.ts +128 -0
  260. package/src/runtime/handle-post.ts +104 -0
  261. package/src/runtime/handle-pre.ts +137 -0
  262. package/src/runtime/handle-scope-async.ts +29 -0
  263. package/src/runtime/handle-types.ts +27 -0
  264. package/src/runtime/handle.ts +116 -0
  265. package/src/runtime/home-state.ts +55 -0
  266. package/src/runtime/index.ts +18 -0
  267. package/src/runtime/inform.ts +28 -0
  268. package/src/runtime/inject-budget-recap.ts +47 -0
  269. package/src/runtime/inject-budget.ts +81 -0
  270. package/src/runtime/inject-context.ts +85 -0
  271. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  272. package/src/runtime/inject-dedup.ts +89 -0
  273. package/src/runtime/is-agent-tool.ts +19 -0
  274. package/src/runtime/lifecycle/agent-files.ts +49 -0
  275. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  276. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  277. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  278. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  279. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  280. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  281. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  282. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  283. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  284. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  285. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  286. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  287. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  288. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  290. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  291. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  294. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  295. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  296. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  297. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  298. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  299. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  300. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  301. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  302. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  303. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  304. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  305. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  306. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  307. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  308. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  309. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  310. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  311. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  312. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  313. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  314. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  315. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  316. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  317. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  318. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  319. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  320. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  321. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  323. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  324. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  325. package/src/runtime/lifecycle/dispatch.ts +109 -0
  326. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  327. package/src/runtime/lifecycle/index.ts +34 -0
  328. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  329. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  330. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  331. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  332. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  333. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  334. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  335. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  336. package/src/runtime/lifecycle/memory/client.ts +65 -0
  337. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  338. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  339. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  340. package/src/runtime/lifecycle/memory/state.ts +49 -0
  341. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  342. package/src/runtime/lifecycle/post-compact.ts +38 -0
  343. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  344. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  345. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  346. package/src/runtime/lifecycle/rules-root.ts +83 -0
  347. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  348. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  349. package/src/runtime/lifecycle/security/scan.ts +76 -0
  350. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  351. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  352. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  353. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  354. package/src/runtime/lifecycle/session-end.ts +17 -0
  355. package/src/runtime/lifecycle/session-start.ts +57 -0
  356. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  357. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  358. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  359. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  360. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  361. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  362. package/src/runtime/lifecycle/stop-core.ts +35 -0
  363. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  364. package/src/runtime/lifecycle/task-completed.ts +94 -0
  365. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  366. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  367. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  368. package/src/runtime/lifecycle/track-changes.ts +61 -0
  369. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  370. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  371. package/src/runtime/lifecycle-bridge.ts +74 -0
  372. package/src/runtime/mcp-key.ts +44 -0
  373. package/src/runtime/mcp-tool-name.ts +92 -0
  374. package/src/runtime/mcp.ts +91 -0
  375. package/src/runtime/modular.ts +77 -0
  376. package/src/runtime/normalize.ts +107 -0
  377. package/src/runtime/notices.ts +91 -0
  378. package/src/runtime/notification-sound.ts +49 -0
  379. package/src/runtime/notifications.ts +77 -0
  380. package/src/runtime/paths.ts +59 -0
  381. package/src/runtime/post-fanout.ts +41 -0
  382. package/src/runtime/post-outcome.ts +61 -0
  383. package/src/runtime/pre-allow.ts +97 -0
  384. package/src/runtime/precommit.ts +60 -0
  385. package/src/runtime/prompt-text.ts +38 -0
  386. package/src/runtime/receipt-capture.ts +34 -0
  387. package/src/runtime/record.ts +26 -0
  388. package/src/runtime/respond.ts +92 -0
  389. package/src/runtime/solid-file-size-gate.ts +58 -0
  390. package/src/runtime/solid-pre.ts +53 -0
  391. package/src/runtime/storage.ts +9 -0
  392. package/src/statusline/colors.ts +31 -0
  393. package/src/statusline/constants.ts +17 -0
  394. package/src/statusline/formatters.ts +54 -0
  395. package/src/statusline/index.ts +4 -0
  396. package/src/statusline/progress-bar.ts +46 -0
  397. package/src/tracking/codex-post-failure.ts +73 -0
  398. package/src/tracking/index.ts +2 -0
  399. package/src/tracking/integrity.ts +99 -0
  400. package/src/tracking/one-shot-dedup.ts +34 -0
  401. package/src/tracking/one-shot-failure.ts +42 -0
  402. package/src/tracking/one-shot-store.ts +95 -0
  403. package/src/tracking/one-shot.ts +93 -0
  404. package/src/tracking/receipts.ts +95 -0
  405. package/src/tracking/session-state.ts +94 -0
  406. package/src/tracking/store.ts +96 -0
  407. package/src/tracking/track-compact.ts +100 -0
  408. package/src/tracking/track-diff.ts +50 -0
  409. package/src/tracking/track-journal.ts +97 -0
  410. package/src/tracking/track-lock-sync.ts +83 -0
  411. package/src/tracking/track-lock.ts +67 -0
  412. package/src/util/compact-json.ts +23 -0
  413. package/src/util/index.ts +2 -0
  414. package/src/util/json-io.ts +47 -0
  415. package/src/util/project-root.ts +41 -0
  416. package/src/util/runtime-io.ts +85 -0
  417. package/src/util/semver.ts +30 -0
@@ -0,0 +1,46 @@
1
+ import { mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ /** Stale-after window (ms) — survives a session crashing before its `finally`. */
5
+ const STALE_MS = 30_000;
6
+
7
+ function lockPath(codexHome: string): string {
8
+ return join(codexHome, "fusengine", "state", "agents-resync.lock");
9
+ }
10
+
11
+ /**
12
+ * Best-effort inter-process lock for the agents resync. `writeFileSync(path,
13
+ * pid, { flag: "wx" })` uses `O_EXCL`: it fails with `EEXIST` when the lock
14
+ * file already exists, so two Codex sessions starting at once cannot both
15
+ * resolve then write DIFFERENT versions of the agents cache in interleave (a
16
+ * torn TOML). Stale after {@link STALE_MS} to survive a crash that never
17
+ * reached its `finally`. No live-PID check (a permanent, accepted limit of any
18
+ * mtime-based lock — same tradeoff `proper-lockfile` makes).
19
+ * @param codexHome - The Codex home directory.
20
+ * @returns `true` when the lock was acquired; `false` when another live
21
+ * holder has it.
22
+ */
23
+ export function acquireResyncLock(codexHome: string): boolean {
24
+ const path = lockPath(codexHome);
25
+ mkdirSync(dirname(path), { recursive: true });
26
+ try {
27
+ writeFileSync(path, String(process.pid), { flag: "wx" });
28
+ return true;
29
+ } catch {
30
+ try {
31
+ if (Date.now() - statSync(path).mtimeMs <= STALE_MS) return false;
32
+ unlinkSync(path);
33
+ writeFileSync(path, String(process.pid), { flag: "wx" });
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ }
40
+
41
+ /** Release the lock. Best-effort — never throws. */
42
+ export function releaseResyncLock(codexHome: string): void {
43
+ try {
44
+ unlinkSync(lockPath(codexHome));
45
+ } catch { /* best-effort */ }
46
+ }
@@ -0,0 +1,39 @@
1
+ import { lstatSync, readFileSync, readlinkSync, unlinkSync } from "node:fs";
2
+
3
+ /** Marker line stamped at the top of a materialized (non-symlinked) agent TOML. */
4
+ export const MANAGED_AGENT_MARKER = "# Managed by fusengine-codex; source:";
5
+
6
+ /** True when a symlink target lives under a plugin cache dir (ours to manage). */
7
+ function isManagedPluginTarget(target: string): boolean {
8
+ return target.includes("/plugins/") || target.includes("\\plugins\\");
9
+ }
10
+
11
+ /**
12
+ * Clear a previously-installed destination before reinstalling it, but ONLY
13
+ * when it is clearly ours: a symlink into a plugin cache dir, or a text file
14
+ * stamped with {@link MANAGED_AGENT_MARKER}. Anything else (a user's own file,
15
+ * a foreign symlink) is left untouched. Ports the merged behavior of
16
+ * `plugin-managed-destination.ts` + the local helper in
17
+ * `plugin-file-symlinks.ts` (this harness always runs hook-silent, so the
18
+ * `@clack/prompts` warnings those ports had are dropped, not translated).
19
+ * @param path - The destination path to check/clear.
20
+ * @returns `"missing"` (nothing there), `"removed"` (ours, cleared), or
21
+ * `"skip"` (foreign — left alone).
22
+ */
23
+ export function clearManagedDestination(path: string): "missing" | "removed" | "skip" {
24
+ try {
25
+ const stat = lstatSync(path);
26
+ if (stat.isSymbolicLink()) {
27
+ if (!isManagedPluginTarget(readlinkSync(path))) return "skip";
28
+ unlinkSync(path);
29
+ return "removed";
30
+ }
31
+ if (readFileSync(path, "utf8").startsWith(MANAGED_AGENT_MARKER)) {
32
+ unlinkSync(path);
33
+ return "removed";
34
+ }
35
+ return "skip";
36
+ } catch {
37
+ return "missing";
38
+ }
39
+ }
@@ -0,0 +1,57 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { clearManagedDestination, MANAGED_AGENT_MARKER } from "./managed-destination";
4
+ import type { PluginFile } from "./plugin-files";
5
+
6
+ const PORTABLE_SKILL_PATH_RE = /^plugins\/([^/]+)\/skills\/(.+)$/;
7
+ const CACHE_SKILL_PATH_RE = /\/\.codex\/plugins\/cache\/fusengine-codex\/([^/]+)\/[^/]+\/skills\/(.+)$/;
8
+
9
+ function tomlString(value: string): string {
10
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11
+ }
12
+
13
+ /** Rewrite one `path = "..."` value to an absolute path (portable/cache skill refs, or relative-to-source). */
14
+ function rewriteAgentSkillPath(value: string, item: PluginFile, pluginRoots: Map<string, string>): string {
15
+ const portable = PORTABLE_SKILL_PATH_RE.exec(value);
16
+ if (portable) {
17
+ const root = pluginRoots.get(portable[1] ?? "");
18
+ return root ? join(root, "skills", portable[2] ?? "") : value;
19
+ }
20
+ const cached = CACHE_SKILL_PATH_RE.exec(value);
21
+ if (cached) {
22
+ const root = pluginRoots.get(cached[1] ?? "");
23
+ return root ? join(root, "skills", cached[2] ?? "") : value;
24
+ }
25
+ if (value.startsWith("./") || value.startsWith("../")) return resolve(dirname(item.src), value);
26
+ return value;
27
+ }
28
+
29
+ /** Rewrite every `path = "..."` line in an agent TOML + stamp the managed-source marker. */
30
+ export function materializeAgentToml(raw: string, item: PluginFile, pluginRoots: Map<string, string>): string {
31
+ const rewritten = raw.replace(/^(\s*path\s*=\s*)"([^"]+)"/gm, (_m, prefix: string, value: string) =>
32
+ `${prefix}${tomlString(rewriteAgentSkillPath(value, item, pluginRoots))}`);
33
+ return rewritten.startsWith(MANAGED_AGENT_MARKER) ? rewritten : `${MANAGED_AGENT_MARKER} ${item.src}\n${rewritten}`;
34
+ }
35
+
36
+ /**
37
+ * Materialize agent TOMLs into `destDir` (Codex ignores symlinked agent
38
+ * TOMLs — openai/codex#15345, never fixed — so these are COPIED, not linked).
39
+ * Silent: no `@clack/prompts` (this always runs inside a hook, which must
40
+ * stay stdout-clean). Ports `agent-materializer.ts::materializeAgentFiles`.
41
+ * @param files - Discovered agent files (see {@link import("./plugin-files").listPluginFiles}).
42
+ * @param destDir - Destination dir (`<codexHome>/agents`).
43
+ * @param pluginRoots - Resolved plugin roots, for `path=` rewriting.
44
+ */
45
+ export function materializeAgentFiles(files: PluginFile[], destDir: string, pluginRoots: Map<string, string>): void {
46
+ mkdirSync(destDir, { recursive: true });
47
+ const seen = new Set<string>();
48
+ for (const item of files) {
49
+ if (seen.has(item.file)) continue;
50
+ seen.add(item.file);
51
+ clearManagedDestination(join(destDir, `${item.plugin}-${item.file}`)); // legacy naming cleanup
52
+ const destPath = join(destDir, item.file);
53
+ if (clearManagedDestination(destPath) === "skip") continue;
54
+ const raw = readFileSync(item.src, "utf8");
55
+ writeFileSync(destPath, materializeAgentToml(raw, item, pluginRoots));
56
+ }
57
+ }
@@ -0,0 +1,65 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { compareVersionsDesc } from "./plugin-roots";
4
+
5
+ /** One discovered plugin file: which plugin owns it, its filename, and its source path. */
6
+ export interface PluginFile {
7
+ plugin: string;
8
+ file: string;
9
+ src: string;
10
+ }
11
+
12
+ /** Files matching `extension` directly under `dir`, tagged with `plugin`. */
13
+ function filesIn(dir: string, plugin: string, extension: string): PluginFile[] {
14
+ if (!existsSync(dir)) return [];
15
+ return readdirSync(dir)
16
+ .filter((file) => file.endsWith(extension))
17
+ .map((file) => ({ plugin, file, src: join(dir, file) }));
18
+ }
19
+
20
+ /** Versioned plugin layout: newest version subdir wins, first match returned. */
21
+ function listVersionedFiles(pluginRoot: string, plugin: string, subdir: string, extension: string): PluginFile[] {
22
+ let versions: string[];
23
+ try {
24
+ versions = readdirSync(pluginRoot, { withFileTypes: true })
25
+ .filter((v) => v.isDirectory() && !v.name.startsWith("."))
26
+ .map((v) => v.name)
27
+ .sort(compareVersionsDesc);
28
+ } catch {
29
+ return [];
30
+ }
31
+ for (const version of versions) {
32
+ const found = filesIn(join(pluginRoot, version, subdir), plugin, extension);
33
+ if (found.length > 0) return found;
34
+ }
35
+ return [];
36
+ }
37
+
38
+ /**
39
+ * Discover every plugin's files of a given kind (agents `.toml`, commands
40
+ * `.md`) across a plugins cache root — unversioned layout first, else the
41
+ * newest version subdir. Ports `plugin-file-discovery.ts::listPluginFiles`.
42
+ * @param pluginsRoot - The plugin cache root.
43
+ * @param subdir - `"agents"` or `"commands"`.
44
+ * @param extension - File extension to match, including the dot.
45
+ */
46
+ export function listPluginFiles(pluginsRoot: string, subdir: string, extension: string): PluginFile[] {
47
+ const out: PluginFile[] = [];
48
+ let entries: string[];
49
+ try {
50
+ entries = readdirSync(pluginsRoot, { withFileTypes: true })
51
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "_shared")
52
+ .map((e) => e.name);
53
+ } catch {
54
+ return out;
55
+ }
56
+ for (const plugin of entries) {
57
+ const direct = filesIn(join(pluginsRoot, plugin, subdir), plugin, extension);
58
+ if (direct.length > 0) {
59
+ out.push(...direct);
60
+ continue;
61
+ }
62
+ out.push(...listVersionedFiles(join(pluginsRoot, plugin), plugin, subdir, extension));
63
+ }
64
+ return out;
65
+ }
@@ -0,0 +1,62 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /** Frozen marketplace name (matches codex-plugins' own `scripts/install-codex.ts`). */
5
+ export const MARKETPLACE = "fusengine-codex";
6
+
7
+ /** The plugin cache root under a Codex home. */
8
+ export function pluginsCacheRoot(codexHome: string): string {
9
+ return join(codexHome, "plugins", "cache", MARKETPLACE);
10
+ }
11
+
12
+ /** Descending semver-ish compare (`"2.1" > "10.0"` stays false — numeric per segment). */
13
+ export function compareVersionsDesc(a: string, b: string): number {
14
+ const left = a.split(/[.-]/).map((part) => Number.parseInt(part, 10));
15
+ const right = b.split(/[.-]/).map((part) => Number.parseInt(part, 10));
16
+ const max = Math.max(left.length, right.length);
17
+ for (let i = 0; i < max; i++) {
18
+ const av = Number.isNaN(left[i]) ? 0 : (left[i] ?? 0);
19
+ const bv = Number.isNaN(right[i]) ? 0 : (right[i] ?? 0);
20
+ if (av !== bv) return bv - av;
21
+ }
22
+ return b.localeCompare(a);
23
+ }
24
+
25
+ /** Resolve one plugin's active root: unversioned dir first, else the newest version dir. */
26
+ function resolvePluginRoot(pluginsRoot: string, plugin: string): string | undefined {
27
+ const root = join(pluginsRoot, plugin);
28
+ if (existsSync(join(root, ".codex-plugin")) || existsSync(join(root, "skills"))) return root;
29
+ let entries: string[];
30
+ try {
31
+ entries = readdirSync(root, { withFileTypes: true })
32
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
33
+ .map((e) => e.name);
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ const versions = entries.sort(compareVersionsDesc);
38
+ return versions[0] ? join(root, versions[0]) : undefined;
39
+ }
40
+
41
+ /**
42
+ * Resolve the active root directory of every installed plugin under a plugins
43
+ * cache root, keyed by plugin name. Ports `plugin-root-resolver.ts::buildPluginRoots`.
44
+ * @param pluginsRoot - The plugin cache root (see {@link pluginsCacheRoot}).
45
+ * @returns Plugin name -> resolved root dir (missing entries are silently skipped).
46
+ */
47
+ export function buildPluginRoots(pluginsRoot: string): Map<string, string> {
48
+ const roots = new Map<string, string>();
49
+ let entries: string[];
50
+ try {
51
+ entries = readdirSync(pluginsRoot, { withFileTypes: true })
52
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "_shared")
53
+ .map((e) => e.name);
54
+ } catch {
55
+ return roots;
56
+ }
57
+ for (const plugin of entries) {
58
+ const root = resolvePluginRoot(pluginsRoot, plugin);
59
+ if (root) roots.set(plugin, root);
60
+ }
61
+ return roots;
62
+ }
@@ -0,0 +1,45 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { acquireResyncLock, releaseResyncLock } from "./lock";
4
+ import { needsResync, resolveCurrentFingerprint, writeFingerprint } from "./fingerprint";
5
+ import { pluginsCacheRoot } from "./plugin-roots";
6
+ import { listPluginFiles } from "./plugin-files";
7
+ import { materializeAgentFiles } from "./materialize-agents";
8
+ import { symlinkPluginFiles } from "./symlink-commands";
9
+
10
+ /** The Codex home dir: `$CODEX_HOME`, else `~/.codex`. */
11
+ export function defaultCodexHome(): string {
12
+ return process.env.CODEX_HOME ?? join(homedir(), ".codex");
13
+ }
14
+
15
+ /**
16
+ * Re-materialize the Codex plugin agents/commands cache on SessionStart, but
17
+ * only when the plugin-cache fingerprint changed (or a command symlink dangles)
18
+ * since the last apply — otherwise a cheap no-op. Agents are COPIED into
19
+ * `<codexHome>/agents` (Codex won't load symlinked agent TOMLs), commands are
20
+ * SYMLINKED into `<codexHome>/prompts`. Guarded by a best-effort inter-process
21
+ * lock so two sessions starting at once can't write a torn cache; the sha256
22
+ * fingerprint is the real correctness backstop (idempotent skip), the lock only
23
+ * reduces redundant concurrent rebuilds. ABSOLUTELY fail-open: any error is
24
+ * swallowed so a resync can never break SessionStart.
25
+ * @param codexHome - The Codex home directory (defaults to {@link defaultCodexHome}).
26
+ */
27
+ export function resyncCodexAgents(codexHome: string = defaultCodexHome()): void {
28
+ try {
29
+ const pluginsRoot = pluginsCacheRoot(codexHome);
30
+ const current = resolveCurrentFingerprint(pluginsRoot);
31
+ if (!current) return; // nothing cached — the installer, not this hook, owns first-time setup
32
+ const promptsDir = join(codexHome, "prompts");
33
+ if (!needsResync(codexHome, current.value, promptsDir)) return; // unchanged since last apply — no-op
34
+ if (!acquireResyncLock(codexHome)) return; // another session already holds the resync lock
35
+ try {
36
+ materializeAgentFiles(listPluginFiles(pluginsRoot, "agents", ".toml"), join(codexHome, "agents"), current.roots);
37
+ symlinkPluginFiles(listPluginFiles(pluginsRoot, "commands", ".md"), promptsDir);
38
+ writeFingerprint(codexHome, current.value);
39
+ } finally {
40
+ releaseResyncLock(codexHome); // best-effort, never throws (see lock.ts)
41
+ }
42
+ } catch {
43
+ /* absolute fail-open: a resync must never break SessionStart */
44
+ }
45
+ }
@@ -0,0 +1,25 @@
1
+ import { mkdirSync, symlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { clearManagedDestination } from "./managed-destination";
4
+ import type { PluginFile } from "./plugin-files";
5
+
6
+ /**
7
+ * Symlink command files (`.md`) into `destDir` — unlike agents, Codex has no
8
+ * known symlink-loading issue for prompts, so these stay linked rather than
9
+ * copied. Silent, no `@clack/prompts` (see {@link materializeAgentFiles} for
10
+ * why). Ports `plugin-file-symlinks.ts::symlinkPluginFiles`.
11
+ * @param files - Discovered command files (see {@link import("./plugin-files").listPluginFiles}).
12
+ * @param destDir - Destination dir (`<codexHome>/prompts`).
13
+ */
14
+ export function symlinkPluginFiles(files: PluginFile[], destDir: string): void {
15
+ mkdirSync(destDir, { recursive: true });
16
+ const seen = new Set<string>();
17
+ for (const item of files) {
18
+ if (seen.has(item.file)) continue;
19
+ seen.add(item.file);
20
+ clearManagedDestination(join(destDir, `${item.plugin}-${item.file}`)); // legacy naming cleanup
21
+ const linkPath = join(destDir, item.file);
22
+ if (clearManagedDestination(linkPath) === "skip") continue;
23
+ symlinkSync(item.src, linkPath);
24
+ }
25
+ }
@@ -0,0 +1,109 @@
1
+ import { sessionStartCore } from "./session-start";
2
+ import { injectRules } from "./inject-rules";
3
+ import { resolveRulesRoot } from "./rules-root";
4
+ import { solidDetectStart } from "./solid-detect";
5
+ import { subagentCacheContext } from "./subagent-cache";
6
+ import { trackAgentMemory } from "./agent-memory";
7
+ import { harvestSubagentTrack } from "../../freshness/evidence-harvest-io";
8
+ import { markSubagentSeen } from "../confirm/confirm-subagent";
9
+ import { teammateIdleContext } from "./teammate-idle-check";
10
+ import { failureLessonContext } from "./failure-lesson";
11
+ import { postCompactContext } from "./post-compact";
12
+ import { saveApexState } from "./pre-compact";
13
+ import { cleanupSession } from "./session-end";
14
+ import { validateRulesLoaded } from "./instructions-loaded";
15
+ import { validateTaskSolid } from "./task-completed";
16
+ import { cartoSessionStart } from "./cartographer/session-start";
17
+ import { dispatchLessons } from "./lessons/dispatch";
18
+ import { withSnapshot } from "./snapshot";
19
+ import { stopCore } from "./stop-core";
20
+
21
+ /** Which plugin's hooks.json invoked the harness (selects SessionStart behavior). */
22
+ export type PluginScope = "core" | "solid" | "rules" | "carto" | "security" | "changelog" | "aipilot" | "lessons" | "seo" | "memory" | "tailwindcss";
23
+
24
+ /** Inputs the lifecycle dispatcher needs (clock + roots injected). */
25
+ export interface LifecycleInput {
26
+ event: string;
27
+ payload: Record<string, unknown>;
28
+ cwd: string;
29
+ scope: PluginScope;
30
+ now: number;
31
+ /** Harness target id (defaults to "claude-code" — zero-regression default). */
32
+ id?: string;
33
+ }
34
+
35
+ /** SessionStart handler keyed on plugin scope. */
36
+ function sessionStart(input: LifecycleInput): string {
37
+ if (input.scope === "solid") return solidDetectStart();
38
+ if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
39
+ if (input.scope === "carto") return cartoSessionStart(input.cwd, input.now);
40
+ if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
41
+ const core = sessionStartCore(input.cwd, undefined, input.now, input.id ?? "claude-code");
42
+ // core scope only: concatenate the reconciliation snapshot onto the existing
43
+ // additionalContext (CLAUDE.md + dev-context) — never replaces it, fail-safe.
44
+ return input.scope === "core" ? withSnapshot(core, input.cwd, import.meta.url) : core;
45
+ }
46
+
47
+ /**
48
+ * Route a lifecycle/session/context hook event to its ported handler. Returns
49
+ * the native stdout when handled, or `null` when the event is not a lifecycle
50
+ * event (so the caller falls through to the PreToolUse/PostToolUse pipeline).
51
+ * @param input - The dispatch input.
52
+ * @returns The native hook stdout, or `null` when unhandled.
53
+ */
54
+ export function dispatchLifecycle(input: LifecycleInput): string | null {
55
+ switch (input.event) {
56
+ case "SessionStart":
57
+ return sessionStart(input);
58
+ case "UserPromptSubmit":
59
+ if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
60
+ if (input.scope === "lessons") return dispatchLessons("UserPromptSubmit", input.payload, input.cwd, input.now, input.id ?? "claude-code");
61
+ return null;
62
+ case "SubagentStart":
63
+ // G0 (CONFIRM-token mechanism, confirm-subagent.ts): mark this session
64
+ // as having recent sub-agent activity BEFORE the scope branching below,
65
+ // so it fires regardless of which plugin scope dispatched it. A
66
+ // monotone max-write timestamp, NOT a counter — see confirm-subagent.ts's
67
+ // subagentWindowMs doc for why a start/stop counter desyncs unsafely
68
+ // under this same multi-plugin fan-out.
69
+ markSubagentSeen(input.payload.session_id, input.now);
70
+ if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
71
+ if (input.scope === "aipilot") return "";
72
+ if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
73
+ return subagentCacheContext(input.payload.session_id);
74
+ case "Stop":
75
+ if (input.scope === "lessons") return dispatchLessons("Stop", input.payload, input.cwd, input.now, input.id ?? "claude-code");
76
+ return input.scope === "core" ? stopCore(input.payload, input.cwd, input.now) : null;
77
+ case "SubagentStop":
78
+ // G0 counterpart of the SubagentStart branch above — the SAME
79
+ // monotone max-write, never a decrement (see confirm-subagent.ts).
80
+ markSubagentSeen(input.payload.session_id, input.now);
81
+ if (input.scope === "aipilot") return "";
82
+ // Retroactively harvest the finishing sub-agent's transcript into the session
83
+ // track BEFORE the reminder — so next turn's freshness gate sees research/
84
+ // explore evidence even when sidechain PostToolUse hooks never fired
85
+ // (#43612/#27655/#34692). SubagentStop is main-session-dispatched (reliable).
86
+ harvestSubagentTrack(input.payload, input.cwd, input.now);
87
+ return trackAgentMemory(input.payload, undefined, input.now);
88
+ case "TeammateIdle":
89
+ return teammateIdleContext(input.payload, input.cwd, undefined, input.now);
90
+ case "PostToolUseFailure":
91
+ return failureLessonContext(input.payload, input.cwd, undefined, input.now);
92
+ case "PostCompact":
93
+ return input.scope === "core" ? postCompactContext(input.payload, input.cwd, import.meta.url, input.now) : "";
94
+ case "PreCompact":
95
+ return saveApexState(input.cwd, input.now, input.id ?? "claude-code");
96
+ case "SessionEnd":
97
+ if (input.scope !== "aipilot") cleanupSession(undefined, input.now);
98
+ return "";
99
+ case "InstructionsLoaded":
100
+ validateRulesLoaded(input.payload);
101
+ return "";
102
+ case "TaskCompleted":
103
+ return validateTaskSolid(input.payload);
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+
109
+ export { dispatchAipilot, aipilotPostToolUse } from "./aipilot/dispatch-aipilot";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @module failure-lesson
3
+ * PostToolUseFailure handler: keep the failure log, record the failure in the
4
+ * one-shot metric, and inject the ONE most-specific lesson whose `error:` trigger
5
+ * matches the failure message — reusing the PreToolUse {@link lessonFor} index and
6
+ * its cooldown (idempotent under the ~11-process fan-out). Fail-open throughout.
7
+ *
8
+ * Claude-Code-only: no equivalent `PostToolUseFailure` hook exists on Codex or
9
+ * Hermes, so this handler is never reached through those adapters.
10
+ * @packageDocumentation
11
+ */
12
+ import { homedir } from "node:os";
13
+ import { contextResponse } from "../../adapters/claude";
14
+ import { lessonFor } from "../../policy/lessons/lesson-gate";
15
+ import { lessonsFileFor } from "./lessons/state";
16
+ import { projectRoot } from "../../util/project-root";
17
+ import { oncePerWindow } from "../inject-dedup";
18
+ import { defaultStateDir } from "../paths";
19
+ import { recordFailure } from "../../tracking/one-shot-failure";
20
+ import { logToolFailure } from "./tool-failure";
21
+ import type { OncePerWindow } from "../../policy/lessons/types";
22
+
23
+ /** The failure message across the documented `error` field and defensive fallbacks; "" when none. */
24
+ function failureError(data: Record<string, unknown>): string {
25
+ const raw = data.error ?? data.tool_error ?? data.tool_output;
26
+ if (typeof raw === "string") return raw;
27
+ return raw != null ? JSON.stringify(raw) : "";
28
+ }
29
+
30
+ /**
31
+ * Handle PostToolUseFailure: log the failure, tally it per tool, and inject the
32
+ * matching `error:`-triggered lesson as `additionalContext` ("" when none).
33
+ * @param data - The raw PostToolUseFailure payload (`tool_name`, `error`, `session_id`).
34
+ * @param cwd - Project root.
35
+ * @param home - Home dir (defaults to `~`).
36
+ * @param now - Clock (defaults to `Date.now()`).
37
+ * @param once - Cooldown gate (injected for tests; defaults to {@link oncePerWindow}).
38
+ * @returns The native hook stdout, or "" when nothing to inject.
39
+ */
40
+ export function failureLessonContext(data: Record<string, unknown>, cwd: string, home: string = homedir(), now: number = Date.now(), once: OncePerWindow = oncePerWindow): string {
41
+ logToolFailure(data, home, now);
42
+ const tool = typeof data.tool_name === "string" ? data.tool_name : "unknown";
43
+ const sessionId = typeof data.session_id === "string" ? data.session_id : undefined;
44
+ try { recordFailure(tool, { now, dir: defaultStateDir(cwd), sessionId }); } catch { /* metric never breaks the hook */ }
45
+ const errorMsg = failureError(data);
46
+ if (!errorMsg) return "";
47
+ // tool="" + empty input isolates matching to the `error:` regex (rank 1),
48
+ // reusing the PreToolUse gate's index + cooldown — no second lesson engine.
49
+ const lesson = lessonFor("", {}, { file: lessonsFileFor(projectRoot(cwd)), once, prevError: errorMsg });
50
+ return lesson?.reason ? contextResponse("PostToolUseFailure", lesson.reason) : "";
51
+ }
@@ -0,0 +1,34 @@
1
+ export * from "./session-start";
2
+ export * from "./inject-rules";
3
+ export * from "./solid-detect";
4
+ export * from "./check-file-size";
5
+ export * from "./validate-solid";
6
+ export * from "./validate-tailwind";
7
+ export * from "./subagent-cache";
8
+ export * from "./agent-memory";
9
+ export * from "./teammate-idle";
10
+ export * from "./tool-failure";
11
+ export * from "./pre-compact";
12
+ export * from "./session-end";
13
+ export * from "./instructions-loaded";
14
+ export * from "./track-changes";
15
+ export * from "./post-edit-ts";
16
+ export * from "./dispatch";
17
+ export * from "./post-tracking";
18
+ export * from "./changelog-research";
19
+ export * from "./cartographer/session-start";
20
+ export * from "./cartographer/project-map";
21
+ export * from "./cartographer/track-enrichment";
22
+ export * from "./cartographer/write-tree";
23
+ export * from "./cartographer/merge";
24
+ export * from "./cartographer/fs-util";
25
+ export * from "./security/check-skill";
26
+ export * from "./security/track-skill-read";
27
+ export * from "./security/track-mcp";
28
+ export * from "./security/skill-state";
29
+ export * from "./lessons/dispatch";
30
+ export * from "./lessons/state";
31
+ export * from "./memory/dispatch";
32
+ export * from "./seo/post-tool-use";
33
+ export * from "./cartographer/ecosystem-map";
34
+ export * from "./cartographer/write-plugin-map";
@@ -0,0 +1,50 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { renderInform } from "../inform";
4
+ import { rulesInAgentsMd } from "./kimi-rules-native";
5
+
6
+ /** Read & concatenate all `*.md` files (sorted) under `rulesDir`. */
7
+ export function readRules(rulesDir: string): string {
8
+ if (!existsSync(rulesDir)) return "";
9
+ let names: string[];
10
+ try {
11
+ names = readdirSync(rulesDir).filter((n) => n.endsWith(".md")).sort();
12
+ } catch {
13
+ return "";
14
+ }
15
+ const parts: string[] = [];
16
+ for (const name of names) {
17
+ try {
18
+ parts.push(readFileSync(join(rulesDir, name), "utf-8"));
19
+ } catch {
20
+ /* skip unreadable rule file */
21
+ }
22
+ }
23
+ return parts.join("\n\n");
24
+ }
25
+
26
+ /**
27
+ * Build the rules injection for claude-rules (SessionStart, UserPromptSubmit,
28
+ * SubagentStart): read `<pluginRoot>/rules/*.md` and emit via {@link renderInform}
29
+ * (Claude-shaped `additionalContext`, or raw text for kimi). Tags the output
30
+ * with the *actual* `hookEventName` — the spec requires it to match the firing
31
+ * event (a hardcoded "SessionStart" is non-conforming and may be dropped on
32
+ * UserPromptSubmit/SubagentStart).
33
+ *
34
+ * On kimi's `UserPromptSubmit` specifically, when the corpus is already fenced
35
+ * inside `<kimiHome>/AGENTS.md` (native load, {@link rulesInAgentsMd}), only the
36
+ * notice is emitted — the re-injected corpus would otherwise dump into the
37
+ * user's terminal on every single prompt for no model-context benefit.
38
+ * `SessionStart`/`SubagentStart` always keep the full corpus (session bootstrap
39
+ * / sub-agent contexts need it regardless of the native AGENTS.md load).
40
+ * @param pluginRoot - `CLAUDE_PLUGIN_ROOT` of the claude-rules plugin.
41
+ * @param event - The firing hook event name (e.g. "SessionStart").
42
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
43
+ * @returns The native hook stdout (possibly empty).
44
+ */
45
+ export function injectRules(pluginRoot: string, event: string, id: string = "claude-code"): string {
46
+ const content = readRules(join(pluginRoot, "rules"));
47
+ if (!content) return "";
48
+ if (id === "kimi" && event === "UserPromptSubmit" && rulesInAgentsMd()) return "rules 00-08 injected";
49
+ return renderInform(id, event, content, "rules 00-08 injected");
50
+ }
@@ -0,0 +1,22 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ /**
6
+ * Handle InstructionsLoaded: append `load_reason | memory_type | file_path` to
7
+ * the per-session debug log. Ports `instructions-loaded/validate-rules-loaded.py`.
8
+ * No stdout (logging only; InstructionsLoaded has no decision control).
9
+ * @param data - The raw hook payload.
10
+ * @param home - Home dir (defaults to `~`).
11
+ */
12
+ export function validateRulesLoaded(data: Record<string, unknown>, home: string = homedir()): void {
13
+ const filePath = String(data.file_path ?? "");
14
+ const loadReason = String(data.load_reason ?? "");
15
+ const memoryType = String(data.memory_type ?? "");
16
+ const sessionId = String(data.session_id ?? "unknown");
17
+ const dir = join(home, ".claude", "logs", "instructions-loaded");
18
+ try {
19
+ mkdirSync(dir, { recursive: true });
20
+ appendFileSync(join(dir, `${sessionId}.log`), `${loadReason} | ${memoryType} | ${filePath}\n`, "utf-8");
21
+ } catch { /* best effort */ }
22
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Guard for the kimi notice-only rules injection. Kimi Code CLI has no
3
+ * model-only hook channel (`src/runtime/inform.ts`) — every `UserPromptSubmit`
4
+ * stdout is BOTH appended to the model's context AND rendered raw in the
5
+ * user's terminal. `injectRules` (`inject-rules.ts`) normally re-injects the
6
+ * full ~18 Ko rules corpus on every prompt; when the installer has already
7
+ * merged that same corpus into `<kimiHome>/AGENTS.md` — which Kimi loads
8
+ * natively at session start via its documented `${agents_md}` mechanism,
9
+ * independent of hooks — the re-injection is pure noise the user has to
10
+ * scroll past. Confirmed present, fenced, in the local install:
11
+ * `~/.kimi-code/AGENTS.md:87` / `:335`.
12
+ *
13
+ * Fail-safe semantics: fences absent, file absent, or any read error ⟹
14
+ * `false` — the caller then falls back to the full corpus. A dump is
15
+ * verbose but never wrong; a false `true` would silently drop the rules.
16
+ */
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { harnessHome } from "../../config/home-dir";
20
+
21
+ /** Opening fence the fusengine installer writes around the merged corpus. */
22
+ const FENCE_START = "<!-- fusengine:kimi-rules:start -->";
23
+
24
+ /** Closing fence the fusengine installer writes around the merged corpus. */
25
+ const FENCE_END = "<!-- fusengine:kimi-rules:end -->";
26
+
27
+ /**
28
+ * Check whether the kimi rules corpus is already present, fenced, inside
29
+ * `<kimiHome>/AGENTS.md`.
30
+ * @param home - Kimi home dir override (defaults to `harnessHome("kimi")`,
31
+ * which honors `KIMI_CODE_HOME`).
32
+ * @returns `true` only when both fences are found in a readable file.
33
+ */
34
+ export function rulesInAgentsMd(home?: string): boolean {
35
+ try {
36
+ const kimiHome = home ?? harnessHome("kimi");
37
+ const agentsMd = join(kimiHome, "AGENTS.md");
38
+ if (!existsSync(agentsMd)) return false;
39
+ const content = readFileSync(agentsMd, "utf-8");
40
+ return content.includes(FENCE_START) && content.includes(FENCE_END);
41
+ } catch {
42
+ return false;
43
+ }
44
+ }