@fusengine/harness 0.1.89 → 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 (419) 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 +5 -4
  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-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  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/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,36 @@
1
+ /** How a policy outcome should surface in a harness. */
2
+ export type PromptKind = "ask" | "block" | "inform";
3
+
4
+ /**
5
+ * Portable, harness-agnostic representation of a policy outcome as a prompt.
6
+ * Adapters render this into their harness's native shape (a Claude
7
+ * `permissionDecision`, a Cursor block, a CLI exit + message, ...).
8
+ */
9
+ export interface Prompt {
10
+ kind: PromptKind;
11
+ /** Stable policy identifier for confirmation diagnostics. */
12
+ ruleId?: string;
13
+ /** Short title, e.g. "SOLID file-size limit". */
14
+ title: string;
15
+ /** Why this fired. */
16
+ reason: string;
17
+ /** Concrete next actions to proceed. */
18
+ actions?: string[];
19
+ /**
20
+ * User-facing notice (Python `hook_output.allow_pass`/`post_pass` parity):
21
+ * shown to the HUMAN — Claude Code's `systemMessage` — never to the agent,
22
+ * unlike `reason`/additionalContext. Excluded from {@link formatPrompt}.
23
+ */
24
+ userMessage?: string;
25
+ }
26
+
27
+ /** Render a {@link Prompt} as a consistent, agent-readable memo block. */
28
+ export function formatPrompt(p: Prompt): string {
29
+ const tag = p.kind === "block" ? "BLOCKED" : p.kind === "ask" ? "CONFIRM" : "NOTE";
30
+ const lines = [`[${tag}] ${p.title}`, p.reason];
31
+ if (p.actions?.length) {
32
+ lines.push("Next:");
33
+ p.actions.forEach((a, i) => lines.push(` ${i + 1}. ${a}`));
34
+ }
35
+ return lines.join("\n");
36
+ }
@@ -0,0 +1,69 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { join, delimiter } from "node:path";
3
+
4
+ /** Immediate subdirectory names of `p`, or `[]` when it is missing/inaccessible. */
5
+ function subdirs(p: string): string[] {
6
+ try {
7
+ return readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
8
+ } catch {
9
+ return [];
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Every `skills/` parent dir across the Open-Agent-Skills layouts
15
+ * (agentskills.io) of Claude Code, Codex and Cursor: the standalone skill roots
16
+ * (`<base>/{.claude,.codex,.cursor,.agents}/skills` + `/etc/codex/skills`) plus
17
+ * installed-plugin roots (`<plugin>/skills` under the Claude marketplace and the
18
+ * Claude/Codex version caches). Shallow `readdir` only — no deep walk — so it is
19
+ * cheap enough to run on every hook.
20
+ */
21
+ function skillParents(home: string, cwd: string, marketplaces: string[]): string[] {
22
+ const out: string[] = [];
23
+ for (const base of [cwd, home]) {
24
+ for (const tool of [".claude", ".codex", ".cursor", ".agents"]) out.push(join(base, tool, "skills"));
25
+ }
26
+ out.push(join("/etc", "codex", "skills"));
27
+ // Claude marketplace: <mkt>/plugins/<plugin>/skills.
28
+ const mktRoot = join(home, ".claude", "plugins", "marketplaces");
29
+ for (const mkt of subdirs(mktRoot)) {
30
+ if (!marketplaces.includes(mkt)) continue;
31
+ const pluginsDir = join(mktRoot, mkt, "plugins");
32
+ for (const plugin of subdirs(pluginsDir)) out.push(join(pluginsDir, plugin, "skills"));
33
+ }
34
+ // Version caches: <cache>/<mkt>/<plugin>/<version>/skills.
35
+ for (const cacheRoot of [join(home, ".claude", "plugins", "cache"), join(home, ".codex", "plugins", "cache")]) {
36
+ for (const mkt of subdirs(cacheRoot)) {
37
+ if (!marketplaces.includes(mkt)) continue;
38
+ for (const plugin of subdirs(join(cacheRoot, mkt))) {
39
+ for (const ver of subdirs(join(cacheRoot, mkt, plugin))) out.push(join(cacheRoot, mkt, plugin, ver, "skills"));
40
+ }
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /**
47
+ * Auto-discover SOLID reference dirs (`<skill>/references` for every `solid-*`
48
+ * skill) across the Claude/Codex/Cursor skill layouts — the fallback used when
49
+ * `FUSE_HARNESS_REFS` is unset. Deduped by skill name (first source wins, so a
50
+ * marketplace skill shadows its version-cache copy). Returns a path-delimiter
51
+ * list, or `""` when none is found — with no refs the SOLID-read gate stays off.
52
+ * @param home - User home dir (`os.homedir()`).
53
+ * @param cwd - Current project dir (`process.cwd()`).
54
+ * @param marketplaces - Allowlist of marketplace names to scan (env
55
+ * `FUSE_HARNESS_MARKETPLACES`, default `["fusengine-plugins"]`); a marketplace
56
+ * absent from disk simply contributes nothing. Standalone
57
+ * `.claude`/`.codex`/`.cursor`/`.agents` skill roots are always scanned.
58
+ * @returns A `path.delimiter`-joined list of `references` dirs, or `""`.
59
+ */
60
+ export function discoverRefs(home: string, cwd: string, marketplaces: string[]): string {
61
+ const bySkill = new Map<string, string>();
62
+ for (const parent of skillParents(home, cwd, marketplaces)) {
63
+ for (const skill of subdirs(parent)) {
64
+ if (!skill.startsWith("solid-") || bySkill.has(skill)) continue;
65
+ if (subdirs(join(parent, skill)).includes("references")) bySkill.set(skill, join(parent, skill, "references"));
66
+ }
67
+ }
68
+ return [...bySkill.values()].join(delimiter);
69
+ }
@@ -0,0 +1,24 @@
1
+ /** Extract frontmatter key/value pairs from markdown content (quotes stripped). */
2
+ export function parseFrontmatter(content: string): Record<string, string> {
3
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
4
+ if (!m?.[1]) return {};
5
+ const result: Record<string, string> = {};
6
+ for (const line of m[1].split("\n")) {
7
+ const idx = line.indexOf(":");
8
+ if (idx > 0) {
9
+ result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
10
+ }
11
+ }
12
+ return result;
13
+ }
14
+
15
+ /** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
16
+ export function globToRe(g: string): RegExp {
17
+ const escaped = g
18
+ .trim()
19
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
20
+ .replace(/\*\*/g, "\0")
21
+ .replace(/\*/g, "[^/]*")
22
+ .replace(/\0/g, ".*");
23
+ return new RegExp(`^${escaped}$`);
24
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types";
2
+ export * from "./frontmatter";
3
+ export * from "./router";
4
+ export * from "./loader";
@@ -0,0 +1,59 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join, delimiter } from "node:path";
3
+ import { parseFrontmatter } from "./frontmatter";
4
+ import type { RefMeta } from "./types";
5
+
6
+ function pick(fm: Record<string, string>, ...keys: string[]): string {
7
+ for (const k of keys) {
8
+ const v = fm[k];
9
+ if (v) return v;
10
+ }
11
+ return "";
12
+ }
13
+
14
+ const SOLID_SLUGS = new Set(["single-responsibility", "open-closed", "liskov-substitution", "interface-segregation", "dependency-inversion", "solid-principles"]);
15
+
16
+ /** Infer a ref's level from its path when the frontmatter omits it (templates/→template, SOLID slug→principle). */
17
+ function inferLevel(filePath: string): string {
18
+ if (filePath.includes("/templates/")) return "template";
19
+ const stem = (filePath.split("/").pop() ?? "").replace(/\.md$/, "");
20
+ return SOLID_SLUGS.has(stem) ? "principle" : "architecture";
21
+ }
22
+
23
+ /** Build a {@link RefMeta} from parsed frontmatter, tolerant of kebab/camel keys. */
24
+ export function toRefMeta(fm: Record<string, string>, filePath: string): RefMeta {
25
+ return {
26
+ name: pick(fm, "name") || filePath,
27
+ description: pick(fm, "description"),
28
+ keywords: pick(fm, "keywords"),
29
+ priority: pick(fm, "priority"),
30
+ related: pick(fm, "related"),
31
+ appliesTo: pick(fm, "appliesTo", "applies-to", "applies_to"),
32
+ triggerOnEdit: pick(fm, "triggerOnEdit", "trigger-on-edit", "trigger_on_edit"),
33
+ level: pick(fm, "level") || inferLevel(filePath),
34
+ filePath,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Scan a directory recursively for `.md` reference files and parse each into a
40
+ * {@link RefMeta}. The content is entirely the consumer's — point this at any
41
+ * refs dir (`FUSE_HARNESS_REFS`). Returns an empty list when the dir is absent.
42
+ */
43
+ export async function loadRefs(dirs: string): Promise<RefMeta[]> {
44
+ const refs: RefMeta[] = [];
45
+ for (const dir of dirs.split(delimiter).filter(Boolean)) {
46
+ let entries: string[];
47
+ try {
48
+ entries = (await readdir(dir, { recursive: true })) as string[];
49
+ } catch {
50
+ continue;
51
+ }
52
+ for (const rel of entries) {
53
+ if (!rel.endsWith(".md")) continue;
54
+ const filePath = join(dir, rel);
55
+ refs.push(toRefMeta(parseFrontmatter(await readFile(filePath, "utf8")), filePath));
56
+ }
57
+ }
58
+ return refs;
59
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Path shapes {@link discoverRefs}'s `skillParents` treats as legitimate skill
3
+ * roots: standalone `.claude`/`.codex`/`.cursor`/`.agents` skill dirs, marketplace
4
+ * and version-cache plugins installed UNDER one of those same config dirs, and
5
+ * the system-rooted `/etc/codex/skills`. Kept as a pure path-shape regex (no
6
+ * disk access, no home/cwd) so {@link skillRefKey} stays a cheap string test
7
+ * callable from the synchronous `solidReadGate`. Every alternative uses only
8
+ * bounded `[^/]+` groups — no nested quantifiers, so no ReDoS exposure.
9
+ *
10
+ * ANCHORING: the marketplace/cache branches require the `.claude`/`.codex`/
11
+ * `.cursor`/`.agents` segment to IMMEDIATELY precede `plugins/…/skills/` —
12
+ * a bare `/plugins/cache/<a>/<b>/<c>/skills/` floating anywhere (e.g. under an
13
+ * attacker-writable `/tmp/plugins/cache/...`) is NOT enough. `/etc/codex/skills/`
14
+ * is anchored at the string start (`^`), since that shape is a real absolute
15
+ * system path, not a directory name that can legitimately appear mid-path.
16
+ */
17
+ const RECOGNIZED_SKILL_ROOT_RE =
18
+ /(^|\/)\.(claude|codex|cursor|agents)\/(skills\/|plugins\/(marketplaces\/[^/]+\/plugins\/[^/]+|cache\/[^/]+\/[^/]+\/[^/]+)\/skills\/)|^\/etc\/codex\/skills\//;
19
+
20
+ /**
21
+ * Normalize a reference path to its `skills/<skill>/...` suffix — the part
22
+ * that stays IDENTICAL between a marketplace-first path (what `discoverRefs`
23
+ * returns, carried by `ctx.refs`/`routed.*.filePath`) and the same skill read
24
+ * by a sub-agent/teammate through a different root: a version-cache path
25
+ * (extra version segment, plugin dir name that can diverge from the
26
+ * marketplace one, e.g. `solid` vs `fuse-solid`) or a standalone
27
+ * `.codex`/`.cursor`/`.agents` skills dir.
28
+ *
29
+ * ANTI-FORGERY: returns `null` — never a usable key — unless `path` also
30
+ * matches one of the recognized skill-root SHAPES above. A path like
31
+ * `/tmp/skills/solid-generic/references/solid-principles.md` has the right
32
+ * suffix but no recognized root, so it cannot credit a SOLID read by suffix
33
+ * alone; only an exact byte-for-byte match (same as any unrelated file) would.
34
+ * @param path - Absolute path to normalize (a `ctx.refs` entry or a `refsRead` entry).
35
+ * @returns The `skills/...` suffix, or `null` when `path` isn't a recognized skill path.
36
+ */
37
+ export function skillRefKey(path: string): string | null {
38
+ if (!RECOGNIZED_SKILL_ROOT_RE.test(path)) return null;
39
+ const i = path.lastIndexOf("skills/");
40
+ return i === -1 ? null : path.slice(i);
41
+ }
@@ -0,0 +1,63 @@
1
+ import { globToRe } from "./frontmatter";
2
+ import type { RefMeta, ScoredRef, RouteResult } from "./types";
3
+
4
+ /**
5
+ * Score references against a file edit (pure) — flat scoring, parity with the
6
+ * Python `ref_router._score_ref` (comma-split, single award per category):
7
+ * +10 once if ANY `applies-to` glob matches, +5 once if ANY `trigger-on-edit`
8
+ * fragment (trailing `/` stripped) is a substring of the path, +1 per keyword.
9
+ */
10
+ export function scoreReferences(refs: RefMeta[], filePath: string, content: string): ScoredRef[] {
11
+ const scored: ScoredRef[] = [];
12
+ const hay = `${filePath} ${content}`.toLowerCase();
13
+ for (const meta of refs) {
14
+ let score = 0;
15
+ if (meta.appliesTo?.split(",").some((g) => g.trim() && globToRe(g.trim()).test(filePath))) score += 10;
16
+ if (meta.triggerOnEdit?.split(",").some((f) => { const t = f.trim().replace(/\/+$/, ""); return t && filePath.includes(t); })) score += 5;
17
+ if (meta.keywords) {
18
+ for (const kw of meta.keywords.split(",")) {
19
+ const k = kw.trim().toLowerCase();
20
+ if (k && hay.includes(k)) score += 1;
21
+ }
22
+ }
23
+ if (score > 0) scored.push({ meta, score });
24
+ }
25
+ return scored;
26
+ }
27
+
28
+ /**
29
+ * Route references for a file edit: top-2 required + next-2 optional, ensuring a
30
+ * `principle` and a `template` appear in the top 4. Returns null when nothing scores.
31
+ */
32
+ export function routeReferences(
33
+ refs: RefMeta[],
34
+ filePath: string,
35
+ content: string,
36
+ skillPath = "",
37
+ ): RouteResult | null {
38
+ const scored = scoreReferences(refs, filePath, content);
39
+ if (!scored.length) return null;
40
+ scored.sort((a, b) => b.score - a.score);
41
+ const hoist = (level: string): void => {
42
+ if (scored.slice(0, 4).some((r) => r.meta.level === level)) return;
43
+ const found = scored.find((r) => r.meta.level === level);
44
+ if (found) {
45
+ scored.splice(scored.indexOf(found), 1);
46
+ scored.splice(3, 0, found);
47
+ }
48
+ };
49
+ hoist("principle");
50
+ hoist("template");
51
+ return { required: scored.slice(0, 2), optional: scored.slice(2, 4), skillPath: skillPath || skillPathFromRef(scored[0]?.meta.filePath ?? "") };
52
+ }
53
+
54
+ /**
55
+ * Derive a skill's `SKILL.md` path from one of its `references/` file paths
56
+ * (`<skill>/references/...` → `<skill>/SKILL.md`), matching the on-disk skill
57
+ * layout {@link discoverRefs} walks. Returns "" when the ref's path doesn't
58
+ * follow that layout (e.g. ad-hoc refs dirs used in tests).
59
+ */
60
+ function skillPathFromRef(refFilePath: string): string {
61
+ const i = refFilePath.indexOf("/references/");
62
+ return i === -1 ? "" : `${refFilePath.slice(0, i)}/SKILL.md`;
63
+ }
@@ -0,0 +1,25 @@
1
+ /** Parsed frontmatter metadata from a reference .md file. */
2
+ export interface RefMeta {
3
+ name: string;
4
+ description: string;
5
+ keywords: string;
6
+ priority: string;
7
+ related: string;
8
+ appliesTo: string;
9
+ triggerOnEdit: string;
10
+ level: string;
11
+ filePath: string;
12
+ }
13
+
14
+ /** A reference with its computed routing score. */
15
+ export interface ScoredRef {
16
+ meta: RefMeta;
17
+ score: number;
18
+ }
19
+
20
+ /** Routing result with categorized references. */
21
+ export interface RouteResult {
22
+ required: ScoredRef[];
23
+ optional: ScoredRef[];
24
+ skillPath: string;
25
+ }
@@ -0,0 +1,99 @@
1
+ import type { Activity } from "./record";
2
+ import type { AgentQuality } from "../tracking/session-state";
3
+ import { classifyExplore } from "../freshness/explore-tools";
4
+ import { docFramework } from "../freshness/query-framework";
5
+ import { shellReadRefPaths } from "../policy/shell-read-refs";
6
+ import { isAgentTool } from "./is-agent-tool";
7
+
8
+ /** A live tool-use, already normalized to `tool` + `input` by the adapter. */
9
+ export interface ToolEvent {
10
+ /** The harness's `tool_name` (Cline: `preToolUse.toolName`), as a plain string. */
11
+ tool: string;
12
+ input?: Record<string, unknown>;
13
+ sessionId: string;
14
+ framework: string;
15
+ now: number;
16
+ /** Length of the tool response (POST events) — drives agent `quality`. */
17
+ responseLength?: number;
18
+ }
19
+
20
+ /** Quality thresholds (chars): lead agent calls vs direct explore/research (parity Python `> 50`). */
21
+ const AGENT_QUALITY_MIN = 500;
22
+ const EXPLORE_QUALITY_MIN = 50;
23
+
24
+ /** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
25
+ const READ_TOOLS = new Set(["Read", "read_file", "read_many_files"]);
26
+
27
+ /** Grade a response length against a threshold; `undefined` when length is unknown. */
28
+ function qualityFor(len: number | undefined, min: number): AgentQuality | undefined {
29
+ return len === undefined ? undefined : len > min ? "sufficient" : "insufficient";
30
+ }
31
+
32
+ /** Build an `agent` activity, attaching `quality` only when graded. */
33
+ function agentActivity(name: string, ts: number, quality: AgentQuality | undefined): Activity {
34
+ return quality ? { kind: "agent", name, ts, quality } : { kind: "agent", name, ts };
35
+ }
36
+
37
+ /**
38
+ * The documentation source a tool satisfies for the doc-consultation gate, or
39
+ * undefined. Context7/Exa are the primary sources; fuse-browser is the fallback
40
+ * when Exa is down (`browser_fetch`/`browser_crawl`/`browser_serp_batch`); the
41
+ * built-in WebSearch/WebFetch also count.
42
+ * @param tool - The harness tool name.
43
+ */
44
+ function docSourceOf(tool: string): string | undefined {
45
+ if (/context7/i.test(tool)) return "context7";
46
+ if (/exa/i.test(tool)) return "exa";
47
+ if (/browser_(fetch|crawl|serp)/i.test(tool)) return "fuse-browser";
48
+ if (tool === "WebSearch") return "websearch";
49
+ if (tool === "WebFetch") return "webfetch";
50
+ if (/^mcp__shadcn__/i.test(tool)) return "shadcn-mcp";
51
+ if (/^mcp__gemini-design__/i.test(tool)) return "gemini-mcp";
52
+ return undefined;
53
+ }
54
+
55
+ /**
56
+ * Map a live tool-use to the activity to record, or null when nothing is
57
+ * tracked. Works across harnesses — tool names are globally distinct:
58
+ * - MCP doc calls (`context7` / `exa`, any separator) → `doc`
59
+ * - `Task`/`Agent`/`AgentSwarm` + `subagent_type` (Claude/Cursor/Kimi) → `agent` (bare agent name)
60
+ * - direct exploration/research (Glob/Grep, explore Bash, web, MCP cache reads)
61
+ * → `agent` credited to the matching REQUIRED_AGENTS phase
62
+ * - a read tool opening a `.md` reference → `ref`
63
+ */
64
+ export function activityFor(event: ToolEvent): Activity[] {
65
+ const out: Activity[] = [];
66
+ // Doc consultation is recorded ALONGSIDE the research-phase credit (Python runs
67
+ // track-doc-consultation + track-subagent-research as two independent hooks):
68
+ // context7/exa/WebSearch/WebFetch AND fuse-browser (browser_fetch/crawl/serp_batch,
69
+ // in RESEARCH_TOOLS since explore-tools.ts:38) all credit BOTH doc and research-expert.
70
+ const docSource = docSourceOf(event.tool);
71
+ if (docSource) out.push({ kind: "doc", framework: docFramework(event.input, event.framework), sessionId: event.sessionId, source: docSource, ts: event.now });
72
+
73
+ // AgentSwarm is Kimi Code's batch launcher — same `subagent_type` identity field.
74
+ if (isAgentTool(event.tool)) {
75
+ const name = String(event.input?.subagent_type ?? event.input?.name ?? "").split(":").pop() ?? "";
76
+ if (name) out.push(agentActivity(name, event.now, qualityFor(event.responseLength, AGENT_QUALITY_MIN)));
77
+ return out;
78
+ }
79
+ const hit = classifyExplore(event.tool, event.input);
80
+ if (hit) {
81
+ const quality = hit.cacheHit ? "sufficient" : qualityFor(event.responseLength, EXPLORE_QUALITY_MIN);
82
+ out.push(agentActivity(hit.phase, event.now, quality));
83
+ } else if (READ_TOOLS.has(event.tool)) {
84
+ const path = String(event.input?.file_path ?? event.input?.path ?? "");
85
+ // `event.now` is the event timestamp `recordRefRead` stamps into
86
+ // `refsReadAt` (SOLID read TTL, parity track-solid-reads.py); `record.ts`
87
+ // forwards it as `recordRefRead(track, path, ts)`.
88
+ if (path.endsWith(".md")) out.push({ kind: "ref", path, ts: event.now });
89
+ }
90
+ // Codex teammates often read a skill/SOLID `.md` reference via a shell
91
+ // command (`cat`, `head`, …) instead of a native Read — independent of the
92
+ // classification above (a Bash call can ALSO be classified as `explore`
93
+ // evidence by classifyExplore), so every detected path is credited as its
94
+ // own `ref` activity (see shell-read-refs.ts for the read-only whitelist).
95
+ if (event.tool === "Bash") {
96
+ for (const path of shellReadRefPaths(event.input?.command)) out.push({ kind: "ref", path, ts: event.now });
97
+ }
98
+ return out;
99
+ }
@@ -0,0 +1,41 @@
1
+ import { protectedPathGate } from "../policy/trivial-edits";
2
+ import { evaluate } from "../policy/evaluate";
3
+ import { dryGate } from "./dry";
4
+ import { existingLineCounts } from "./gate-helpers";
5
+ import type { NormalizedFile } from "./normalize";
6
+ import type { Prompt } from "../prompt/types";
7
+
8
+ /**
9
+ * OR the static per-file verdict for a Codex `apply_patch` envelope: run the
10
+ * file-level gates (protected-path, SOLID file-size, DRY) that key off
11
+ * `filePath`/`content` over EACH touched file and return the first blocking
12
+ * {@link Prompt}. One violating hunk blocks the whole patch — the parity the
13
+ * single-file `Write`/`Edit` path already has, extended to the multi-file
14
+ * primitive.
15
+ *
16
+ * Only the read-only/pure gates run here (no session-state writes), so the
17
+ * `~11×` hook fan-out stays idempotent — the stateful APEX freshness/skill
18
+ * gates are tool-level and never policed `apply_patch` (its `filePath` was
19
+ * always undefined), so they are intentionally out of scope.
20
+ *
21
+ * File-size tool mapping mirrors Claude: an `add` is judged like a `Write`
22
+ * (full new content), an `update` like an `Edit` (partial content, compared
23
+ * against the on-disk count so an already-oversized file still blocks).
24
+ * @param files - Per-file changes from {@link NormalizedFile}.
25
+ * @param cwd - Project root for the DRY codebase grep.
26
+ * @returns The first blocking prompt, or null when every file passes.
27
+ */
28
+ export function applyPatchGate(files: readonly NormalizedFile[], cwd: string): Prompt | null {
29
+ for (const f of files) {
30
+ const tool = f.op === "add" ? "Write" : "Edit";
31
+ const protectedDeny = protectedPathGate(tool, f.filePath);
32
+ if (protectedDeny) return protectedDeny;
33
+ if (f.op === "delete") continue;
34
+ const { raw: existingLines } = existingLineCounts(f.filePath);
35
+ const quick = evaluate({ tool, filePath: f.filePath, content: f.content, existingLines });
36
+ if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
37
+ const dry = dryGate(tool, f.filePath, f.content, cwd);
38
+ if (dry) return dry;
39
+ }
40
+ return null;
41
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module burst-window
3
+ * Single source of truth for the multi-plugin hook fan-out window.
4
+ *
5
+ * Every DEPLOYED plugin registers its OWN PreToolUse/PostToolUse hook, so ONE
6
+ * Claude tool event spawns ~11 sibling harness processes that each record the
7
+ * same deny / one-shot / sniper reminder within milliseconds. Left unchecked
8
+ * the deny-loop counter jumped by ~11 per real attempt ([REPEAT] "#9" on the
9
+ * FIRST try), the one-shot metric inflated ~11×, and the sniper reminder was
10
+ * injected ~11× (token noise).
11
+ *
12
+ * A record landing within this window after an identical prior one (same
13
+ * operation hash + same `session_id`) is treated as the SAME event and folded
14
+ * into it instead of re-counted. Two REAL agent retries are always spaced
15
+ * further apart than the burst, so this never masks a genuine loop. No env var:
16
+ * the fan-out is a physical property of the installed plugin set, not policy.
17
+ * @packageDocumentation
18
+ */
19
+
20
+ /** Fan-out dedup window (ms). The ~11 sibling hooks for one event land in <2s. */
21
+ export const BURST_DEDUP_MS = 2000;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @module codex-shell-tool
3
+ * Canonicalizes a Codex-only shell `tool_name` alias to `"Bash"`, so every
4
+ * existing `tool === "Bash"` consumer (activity credit, explore-tools
5
+ * classification, the protected-path guard, the design corpus read gate, …)
6
+ * treats a Code Mode exec dispatch exactly like a native Bash call — no
7
+ * per-consumer change needed.
8
+ *
9
+ * A Code Mode-wrapped call (`functions.exec` -> `tools.exec_command(...)`)
10
+ * surfaces via the generic `CoreToolRuntime` hook default (openai/codex#23757,
11
+ * commit 5c20513), which keeps the raw function-tool name `"exec_command"`
12
+ * verbatim — unlike a DIRECT `exec_command` dispatch, which
13
+ * `ExecCommandHandler`'s own `pre_tool_use_payload` override re-labels
14
+ * `"Bash"` (`tool_name: HookToolName::bash()`). This is the exact call chain
15
+ * a real Codex CLI 0.146.0 session was observed emitting for a `sed -n` skill
16
+ * read. Only ONE alias is confirmed here — `ToolName::plain("exec_command")`
17
+ * in `core/src/tools/handlers/unified_exec/exec_command.rs`; `shell`,
18
+ * `unified_exec`, `local_shell`, and `shell_command` were audited but not
19
+ * evidenced as a distinct hook `tool_name` value, so they are deliberately
20
+ * left out rather than guessed.
21
+ * @packageDocumentation
22
+ */
23
+
24
+ /** Codex `tool_name` values that are a shell execution but not literally `"Bash"`. */
25
+ const CODEX_SHELL_TOOL_ALIASES = new Set(["exec_command"]);
26
+
27
+ /**
28
+ * Canonicalize a Codex-only shell tool alias to `"Bash"`. Scoped to
29
+ * `id === "codex"` — every other harness id, and every non-aliased tool
30
+ * name, passes through unchanged (see `test/codex-shell-read-credit.test.ts`
31
+ * for the non-regression proof).
32
+ * @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
33
+ * @param tool - Raw `tool_name` from the hook payload.
34
+ */
35
+ export function canonicalizeCodexShellTool(id: string, tool: string): string {
36
+ return id === "codex" && CODEX_SHELL_TOOL_ALIASES.has(tool) ? "Bash" : tool;
37
+ }
@@ -0,0 +1,37 @@
1
+ /** Shell basenames whose `-c`-style argv carries the real script in argv[2]. */
2
+ const SHELLS = new Set(["bash", "sh", "zsh", "dash"]);
3
+
4
+ /** True when `arg` is (a path to) a POSIX shell we know wraps a `-c` script. */
5
+ function isShell(arg: string | undefined): boolean {
6
+ if (arg === undefined) return false;
7
+ const base = arg.slice(arg.lastIndexOf("/") + 1);
8
+ return SHELLS.has(base);
9
+ }
10
+
11
+ /** True for a shell command flag (`-c`, `-lc`, `-ic`, …) — anything ending in `c`. */
12
+ function isCommandFlag(arg: string | undefined): boolean {
13
+ return arg !== undefined && /^-[a-z]*c$/.test(arg);
14
+ }
15
+
16
+ /**
17
+ * Coerce a hook payload's `command` field to the shell string the guard chain
18
+ * expects. Harnesses disagree on the wire shape: Claude Code, Cursor, and Hermes
19
+ * send a plain string, but Codex CLI sends an argv ARRAY
20
+ * (`["bash", "-lc", "git commit -m x"]`). A naive `typeof === "string"` check
21
+ * dropped the array to `undefined`, blinding every `ctx.command` guard
22
+ * (destructive-git, mutators, install…) into a silent fail-open. This restores
23
+ * the script so the array and string payloads reach an identical verdict.
24
+ *
25
+ * @param v - Raw `command` value off the payload (string, argv array, or other).
26
+ * @returns A string as-is; a `[shell, -c|-lc, script]` argv reduced to its
27
+ * script; any other all-string array joined with spaces; and `undefined` for
28
+ * empty arrays, arrays holding non-strings, or any other type.
29
+ */
30
+ export function commandToString(v: unknown): string | undefined {
31
+ if (typeof v === "string") return v;
32
+ if (!Array.isArray(v)) return undefined;
33
+ if (v.length === 0 || !v.every((e) => typeof e === "string")) return undefined;
34
+ const argv = v as string[];
35
+ if (argv.length >= 3 && isShell(argv[0]) && isCommandFlag(argv[1])) return argv[2];
36
+ return argv.join(" ");
37
+ }