@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,97 @@
1
+ /**
2
+ * PreToolUse ALLOW-path response assembly. Reached only after every gate
3
+ * allowed (a deny/ask already returned upstream), so nothing here can block nor
4
+ * override a decision. Combines the Python-parity pass notice (systemMessage)
5
+ * with the single most-specific decision-time lesson (additionalContext), plus
6
+ * a compact "evidence fresh" compliance notice on the first APEX-scoped code
7
+ * Write/Edit that clears the freshness gate.
8
+ */
9
+ import { designPassNotice } from "../policy/design/gates";
10
+ import { resolveCorpusRoot } from "../policy/design/corpus";
11
+ import { lessonFor } from "../policy/lessons/lesson-gate";
12
+ import { lessonsFileFor } from "./lifecycle/lessons/state";
13
+ import { projectRoot } from "../util/project-root";
14
+ import { oncePerWindow } from "./inject-dedup";
15
+ import { defaultStateDir } from "./paths";
16
+ import { isApexScoped } from "./gate-helpers";
17
+ import { REQUIRED_AGENTS, DEFAULT_WINDOW_MS } from "./gate";
18
+ import { agentsFreshInTrack } from "../freshness/agent-evidence-record";
19
+ import { loadTrack } from "../tracking/store";
20
+ import { evidenceFreshNotice } from "./notices";
21
+ import { respond } from "./respond";
22
+ import type { NormalizedEvent } from "./normalize";
23
+ import type { HandleOutcome } from "./handle";
24
+ import type { Prompt } from "../prompt/types";
25
+
26
+ /** Session-track + clock inputs the "evidence fresh" notice needs (absent = feature off, e.g. no caller-provided track file). */
27
+ export interface EvidenceCheck {
28
+ /** Session track file path (same one the gate chain reads/writes). */
29
+ trackFile: string;
30
+ /** APEX freshness window in ms (defaults to {@link DEFAULT_WINDOW_MS}). */
31
+ windowMs?: number;
32
+ /** Event clock. */
33
+ now: number;
34
+ }
35
+
36
+ /**
37
+ * `✓ evidence fresh (explore+research)` for the first APEX-scoped Write/Edit
38
+ * that finds explore-codebase + research-expert evidence still within the
39
+ * freshness window — the user-visible confirmation for a gate that, until now,
40
+ * only ever spoke up when it BLOCKED. Deduped once per freshness window per
41
+ * session (same window the gate itself re-validates on), so it reads as "just
42
+ * confirmed", not a notice on every edit. Returns null for anything but a
43
+ * Write/Edit on an APEX-scoped path, or when evidence isn't fresh.
44
+ */
45
+ async function freshEvidenceNotice(event: NormalizedEvent, evidence: EvidenceCheck, cwd: string): Promise<string | null> {
46
+ if ((event.tool !== "Write" && event.tool !== "Edit") || !isApexScoped(event.filePath)) return null;
47
+ const windowMs = evidence.windowMs ?? DEFAULT_WINDOW_MS;
48
+ const track = await loadTrack(evidence.trackFile);
49
+ if (!agentsFreshInTrack(track, REQUIRED_AGENTS, windowMs, evidence.now)) return null;
50
+ if (!oncePerWindow(`evidence-fresh:${event.sessionId}`, windowMs, { now: evidence.now, dir: defaultStateDir(cwd) })) return null;
51
+ return evidenceFreshNotice();
52
+ }
53
+
54
+ /**
55
+ * Build the native outcome for a PreToolUse call that passed every gate: emit a
56
+ * user-visible pass notice (once per allowed call), the "evidence fresh"
57
+ * compliance notice when applicable, and, when its TRIGGERS match this call,
58
+ * the one cooldown-guarded decision-time lesson. All 3 channels ride a single
59
+ * response (lesson → additionalContext, notices → systemMessage).
60
+ * @param id - Harness id for {@link respond}.
61
+ * @param event - The normalized PreToolUse event.
62
+ * @param payload - The raw hook payload (for `agent_id`).
63
+ * @param mcpDir - MCP state dir backing the pass-notice throttle.
64
+ * @param cwd - Project root (lesson file + notice scope).
65
+ * @param evidence - Session track + clock for the "evidence fresh" notice (omit to disable it).
66
+ * @returns The native hook outcome (empty stdout when nothing to emit).
67
+ */
68
+ export async function allowOutcome(
69
+ id: string,
70
+ event: NormalizedEvent,
71
+ payload: Record<string, unknown>,
72
+ mcpDir: string,
73
+ cwd: string,
74
+ evidence?: EvidenceCheck,
75
+ corpusRoot?: string,
76
+ ): Promise<HandleOutcome> {
77
+ const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
78
+ const notice = designPassNotice({
79
+ agentId,
80
+ tool: event.tool, filePath: event.filePath ?? "", content: event.content ?? "",
81
+ url: typeof event.input.url === "string" ? event.input.url : "", phase: "pre",
82
+ corpusMissing: agentId !== "" && (corpusRoot ?? resolveCorpusRoot()) === "",
83
+ }, mcpDir);
84
+ const lesson = lessonFor(event.tool, event.input, { file: lessonsFileFor(projectRoot(cwd)), once: oncePerWindow });
85
+ const evidenceNotice = evidence ? await freshEvidenceNotice(event, evidence, cwd) : null;
86
+ if (lesson) {
87
+ const userMessage = [notice?.userMessage, evidenceNotice].filter(Boolean).join("\n") || undefined;
88
+ const merged = userMessage ? { ...lesson, userMessage } : lesson;
89
+ return { stdout: respond(id, merged, event.eventName ?? "PreToolUse"), exit: 0 };
90
+ }
91
+ if (evidenceNotice) {
92
+ const userMessage = [notice?.userMessage, evidenceNotice].filter(Boolean).join("\n");
93
+ const prompt: Prompt = notice ? { ...notice, userMessage } : { kind: "inform", title: "APEX freshness", reason: "", userMessage };
94
+ return { stdout: respond(id, prompt, event.eventName ?? "PreToolUse"), exit: 0 };
95
+ }
96
+ return { stdout: notice ? respond(id, notice, event.eventName ?? "PreToolUse") : "", exit: 0 };
97
+ }
@@ -0,0 +1,60 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { Prompt } from "../prompt/types";
5
+
6
+ const TIMEOUT_MS = 30_000;
7
+ const ESLINT_CONFIGS: readonly string[] = [".eslintrc.json", ".eslintrc.js", "eslint.config.js", "eslint.config.mjs", "eslint.config.ts"];
8
+ const PRETTIER_CONFIGS: readonly string[] = [".prettierrc", ".prettierrc.json", "prettier.config.js"];
9
+
10
+ /** Run a linter; returns its error output, or "" if it passed / spawn-failed / timed out (fail-open). */
11
+ function runLinter(file: string, args: string[], label: string, cwd: string): string {
12
+ try {
13
+ execFileSync(file, args, { cwd, timeout: TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"] });
14
+ return "";
15
+ } catch (e: unknown) {
16
+ const err = e as { status?: number | null; stdout?: Buffer; stderr?: Buffer };
17
+ if (err.status === undefined || err.status === null) return "";
18
+ const out = (err.stdout?.toString() ?? err.stderr?.toString() ?? "").trim();
19
+ return out ? `[${label}]\n${out}` : "";
20
+ }
21
+ }
22
+
23
+ /** Run the applicable linters in `cwd`, returning a block of errors per failing tool. */
24
+ function collectErrors(cwd: string): string[] {
25
+ const has = (f: string): boolean => existsSync(join(cwd, f));
26
+ const errors: string[] = [];
27
+ if (has("package.json")) {
28
+ if (ESLINT_CONFIGS.some(has)) {
29
+ const m = runLinter("bunx", ["eslint", ".", "--max-warnings", "0"], "ESLint", cwd);
30
+ if (m) errors.push(m);
31
+ }
32
+ if (has("tsconfig.json")) {
33
+ const m = runLinter("bunx", ["tsc", "--noEmit"], "TypeScript", cwd);
34
+ if (m) errors.push(m);
35
+ }
36
+ if (PRETTIER_CONFIGS.some(has)) {
37
+ const m = runLinter("bunx", ["prettier", "--check", "."], "Prettier", cwd);
38
+ if (m) errors.push(m);
39
+ }
40
+ }
41
+ if (has("requirements.txt") || has("pyproject.toml")) {
42
+ const m = runLinter("ruff", ["check", "."], "Ruff", cwd);
43
+ if (m) errors.push(m);
44
+ }
45
+ return errors;
46
+ }
47
+
48
+ /** Block a `git commit` when linters fail (effectful: runs eslint/tsc/prettier/ruff, never auto-fixes). */
49
+ export function preCommitGate(tool: string, command: string | undefined, cwd: string | undefined): Prompt | null {
50
+ if (tool !== "Bash" || !command || !cwd) return null;
51
+ if (!command.startsWith("git") || !command.includes("commit")) return null;
52
+ const errors = collectErrors(cwd);
53
+ if (!errors.length) return null;
54
+ return {
55
+ kind: "block",
56
+ title: "Pre-commit checks failed",
57
+ reason: `COMMIT BLOCKED — fix then retry:\n\n${errors.join("\n\n")}`,
58
+ actions: ["Fix the linter/type errors above", "Re-run the commit"],
59
+ };
60
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Normalizes a hook payload's `prompt` field across harness shapes: a plain
3
+ * string (Claude Code, Codex) or an array of content blocks (Kimi 0.31.1 —
4
+ * captured live: `[{"type":"text","text":"..."}]` on `UserPromptSubmit`).
5
+ * Never throws: this runs inside a hook, where an exception breaks the harness.
6
+ */
7
+
8
+ /** A Kimi-shaped prompt content block: any object carrying a `text` string. */
9
+ interface PromptTextBlock {
10
+ text: string;
11
+ }
12
+
13
+ /** True for an object whose `text` property is a string (the only field this reads). */
14
+ function hasTextField(value: unknown): value is PromptTextBlock {
15
+ return typeof value === "object" && value !== null && typeof (value as { text?: unknown }).text === "string";
16
+ }
17
+
18
+ /**
19
+ * Coerce a hook payload's `prompt` field to plain text.
20
+ *
21
+ * - A string is returned AS-IS (same reference, no trim/normalize) — this is
22
+ * the identity branch that guarantees zero regression on Claude Code/Codex,
23
+ * whose `prompt` is already a string.
24
+ * - An array (Kimi's content-block shape) is flattened: every element with a
25
+ * string `.text` is kept, joined with `"\n"` — one block per line, since
26
+ * downstream consumers ({@link detectCreationIntent}, {@link detectMode})
27
+ * only run `\b`-anchored regexes with no line-start/end assumption, so a
28
+ * newline join is safe and reads like the multi-turn/multi-block source.
29
+ * - Anything else (`undefined`, `null`, a number, a plain object) yields `""`.
30
+ *
31
+ * @param value - The raw `prompt` field from a hook payload (unknown shape).
32
+ * @returns The prompt text, or `""` when it cannot be recovered.
33
+ */
34
+ export function promptText(value: unknown): string {
35
+ if (typeof value === "string") return value;
36
+ if (Array.isArray(value)) return value.filter(hasTextField).map((b) => b.text).join("\n");
37
+ return "";
38
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bash verification-receipt extraction for PostToolUse: a `tsc`/`bun test`
3
+ * run is parsed (exit code + output) into a signed receipt the TaskCompleted
4
+ * gate later demands. Structured responses ONLY: Kimi Code CLI sends
5
+ * `tool_output` as a truncated STRING (no exit_code, no stdout/stderr
6
+ * channels) — parsing it would forge a success receipt (`exit 0`, empty
7
+ * output) for possibly-failed runs, so a non-object source is skipped.
8
+ */
9
+ import { captureReceipt } from "../tracking/receipts";
10
+
11
+ /**
12
+ * Record a verification receipt for a Bash run when the response is structured.
13
+ * @param file - Session track file.
14
+ * @param tool - The tool name (only "Bash" receipts exist).
15
+ * @param command - The executed command.
16
+ * @param toolResult - `payload.tool_result` (preferred), when present.
17
+ * @param response - `payload.tool_response ?? payload.tool_output` fallback.
18
+ * @param now - Clock.
19
+ */
20
+ export async function captureBashReceipt(
21
+ file: string,
22
+ tool: string,
23
+ command: string | undefined,
24
+ toolResult: unknown,
25
+ response: unknown,
26
+ now: number,
27
+ ): Promise<void> {
28
+ if (tool !== "Bash" || !command) return;
29
+ const r = (toolResult ?? response) as { exit_code?: unknown; stdout?: unknown; stderr?: unknown } | undefined;
30
+ if (!r || typeof r !== "object") return;
31
+ const out = `${typeof r.stdout === "string" ? r.stdout : ""}\n${typeof r.stderr === "string" ? r.stderr : ""}`;
32
+ const exit = Number(r.exit_code ?? 0);
33
+ await captureReceipt(file, command, out, Number.isFinite(exit) ? exit : 0, now);
34
+ }
@@ -0,0 +1,26 @@
1
+ import { dirname } from "node:path";
2
+ import { withTrack } from "../tracking/store";
3
+ import { recordAgent, recordDoc, recordRefRead, type AgentQuality } from "../tracking/session-state";
4
+ import { appendRefRead } from "../freshness/ref-journal";
5
+
6
+ /** A unit of session activity to record (discriminated union on `kind`). */
7
+ export type Activity =
8
+ | { kind: "agent"; name: string; ts: number; quality?: AgentQuality }
9
+ | { kind: "doc"; framework: string; sessionId: string; source: string; ts?: number }
10
+ | { kind: "ref"; path: string; ts?: number };
11
+
12
+ /** Apply an activity to a session's track and persist it (PostToolUse path). */
13
+ export async function recordActivity(file: string, activity: Activity): Promise<void> {
14
+ await withTrack(file, (track) =>
15
+ activity.kind === "agent"
16
+ ? recordAgent(track, activity.name, activity.ts, activity.quality)
17
+ : activity.kind === "doc"
18
+ ? recordDoc(track, activity.framework, activity.sessionId, activity.source, activity.ts)
19
+ : recordRefRead(track, activity.path, activity.ts),
20
+ );
21
+ // Mirror a `.md` ref read into the append-only journal: the racy load→save above
22
+ // loses a lone write under the hook fan-out, and a teammate's read has not yet
23
+ // flushed to the platform transcript at edit time (multi-minute lag > TTL) — the
24
+ // journal is the fresh, race-immune source the gate folds back (see ref-journal.ts).
25
+ if (activity.kind === "ref") appendRefRead(dirname(file), activity.path, activity.ts ?? Date.now());
26
+ }
@@ -0,0 +1,92 @@
1
+ import { formatPrompt, type Prompt } from "../prompt/types";
2
+ import { denyResponse, contextResponse, informResponse } from "../adapters/claude";
3
+ import { toHermesResponse } from "../adapters/hermes";
4
+ import { toKimiResponse } from "../adapters/kimi";
5
+ import { toCursorResponse } from "../adapters/cursor/respond";
6
+
7
+ /**
8
+ * Map a portable {@link Prompt} to a harness's native hook response, honoring
9
+ * all 3 prompt kinds (`block`/`ask`/`inform`) instead of collapsing `ask` and
10
+ * `inform` together. Reuses the Claude adapter's `denyResponse`/
11
+ * `contextResponse`/`informResponse` builders for claude-code/codex (same hook
12
+ * shape) to avoid duplicating the JSON.
13
+ * - claude-code/codex: `block` -> deny, `ask` -> interactive
14
+ * `permissionDecision:"ask"`, `inform` -> non-blocking `additionalContext`.
15
+ * - gemini-cli/cline: their real hook schemas have no interactive "ask"
16
+ * state (deny is the only blocking outcome), so `ask` and `inform` already
17
+ * both resolve to non-blocking context injection — unchanged.
18
+ * - cursor: `ask` degrades to a native `permission:"deny"` because Cursor does
19
+ * not reliably apply `ask` on `preToolUse`; `inform` stays non-blocking.
20
+ * - hermes: delegated to the adapter's `toHermesResponse` — `block` ->
21
+ * `{decision:"block",reason}`; `ask`/`inform` degrade to non-blocking
22
+ * `{context}` (Hermes has no interactive "ask" state).
23
+ * - kimi: delegated to the adapter's `toKimiResponse` — `block`/`ask` ->
24
+ * `hookSpecificOutput.permissionDecision:"deny"` (Kimi Code CLI's hooks
25
+ * contract documents only "deny"; `ask` downgrades with a prefix, mirroring
26
+ * the codex branch above), `inform` -> raw text on stdout (Kimi appends
27
+ * stdout to context on exit 0 — no `hookSpecificOutput` envelope documented
28
+ * for non-blocking context injection).
29
+ *
30
+ * User-visible pass notices (`prompt.userMessage`, Python `hook_output.allow_pass`
31
+ * / `post_pass` parity), by harness:
32
+ * - claude-code/codex: top-level `systemMessage` ("warning message shown to the
33
+ * user"); a pure notice (empty `reason`) emits `{systemMessage}` alone. Codex
34
+ * consumes the Claude hook schema, so it rides the same field.
35
+ * - gemini-cli: common `systemMessage` output field ("displayed immediately to
36
+ * the user in the terminal") — the notice rides there.
37
+ * - cursor: `user_message` is its user-visible channel (see case comment).
38
+ * - cline: NO user-visible channel (`cancel`/`contextModification`/
39
+ * `errorMessage`-on-cancel only) — the notice is dropped silently (a pure
40
+ * notice returns "" = plain allow).
41
+ * @param id - Harness id.
42
+ * @param prompt - The portable prompt to render.
43
+ * @param event - The firing hook event name for the claude-code/codex branch
44
+ * (defaults to `"PreToolUse"`, the only phase every existing caller but
45
+ * `handle-post.ts` renders) — a POST-phase caller MUST pass `"PostToolUse"`
46
+ * (or its real raw event name) so `hookEventName` matches the event that
47
+ * actually fired instead of a hardcoded, wrong `"PreToolUse"`.
48
+ */
49
+ export function respond(id: string, prompt: Prompt, event: string = "PreToolUse"): string {
50
+ const message = formatPrompt(prompt);
51
+ const { kind, userMessage, reason } = prompt;
52
+ switch (id) {
53
+ case "claude-code":
54
+ case "codex":
55
+ if (kind === "block") return denyResponse(event, message);
56
+ if (kind === "inform") {
57
+ return userMessage ? informResponse(event, userMessage, reason ? message : "") : contextResponse(event, message);
58
+ }
59
+ // Codex parses but NEVER honors `permissionDecision: "ask"` (deny-only) — an
60
+ // `ask` would silently fail open. Downgrade it to an explicit deny so the
61
+ // gate still bites; the reason is prefixed so the honest limit is visible.
62
+ if (id === "codex") {
63
+ return denyResponse(event, `[downgraded from ask — Codex has no interactive approval]\n${message}`);
64
+ }
65
+ return JSON.stringify({
66
+ hookSpecificOutput: {
67
+ hookEventName: event,
68
+ permissionDecision: "ask",
69
+ permissionDecisionReason: message,
70
+ },
71
+ });
72
+ case "gemini-cli":
73
+ if (kind === "block") return JSON.stringify({ decision: "deny", reason: message });
74
+ if (userMessage) return JSON.stringify({ ...(reason ? { hookSpecificOutput: { additionalContext: message } } : {}), systemMessage: userMessage });
75
+ return JSON.stringify({ hookSpecificOutput: { additionalContext: message } });
76
+ case "cursor":
77
+ return toCursorResponse(prompt, event === "PreToolUse" ? "preToolUse" : event);
78
+ case "hermes":
79
+ // Single source of truth for the Hermes wire shape lives in the adapter
80
+ // (same JSON.stringify contract as the inline cases above).
81
+ return toHermesResponse(prompt);
82
+ case "kimi":
83
+ // Single source of truth for the Kimi wire shape lives in the adapter.
84
+ return toKimiResponse(prompt);
85
+ case "cline":
86
+ if (kind === "block") return JSON.stringify({ cancel: true, errorMessage: message });
87
+ if (userMessage && !reason) return ""; // pure notice: no user-visible channel — silent allow
88
+ return JSON.stringify({ contextModification: message });
89
+ default:
90
+ return "";
91
+ }
92
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * PreToolUse file-size deny for the solid scope — the same SOLID ceiling the
3
+ * core gate chain enforces (`policy/evaluate.ts`), extracted so
4
+ * `harness hook <id> solid` blocks oversized Write/Edit WITHOUT the core-only
5
+ * APEX freshness gates (they require explore/research agents that not every
6
+ * harness runs). Pure and read-only (on-disk read via {@link existingLineCounts},
7
+ * no session state), so the sibling-plugin fan-out stays idempotent.
8
+ */
9
+ import { countLines, evaluateFileSize } from "../policy/file-size";
10
+ import { isFileSizeScoped, resolveSolidRefFramework } from "../policy/file-size-scope";
11
+ import { computeEditResultLines } from "../policy/edit-outcome";
12
+ import { resolveMaxLines } from "../config/limits";
13
+ import { existingLineCounts } from "./gate-helpers";
14
+ import type { Prompt } from "../prompt/types";
15
+
16
+ /**
17
+ * Deny a Write/Edit whose resulting file would exceed the SOLID line ceiling.
18
+ * Mirrors `policy/evaluate.ts` exactly: a computable Edit outcome passes when
19
+ * the result is compliant OR strictly shrinks an already-oversized file; the
20
+ * block message reports the pre-existing on-disk count for a Write.
21
+ * @param tool - The tool name (only "Write"/"Edit" are checked).
22
+ * @param filePath - The written file's absolute path.
23
+ * @param content - Write: the full new content; Edit: the `new_string` snippet.
24
+ * @param oldString - Edit only: the `old_string` being replaced.
25
+ * @param isReplaceAll - Edit only: whether every occurrence is replaced.
26
+ * @param agentType - Subagent type, when known (Explore/Plan are exempt).
27
+ * @returns The blocking prompt, or null when compliant or out of scope.
28
+ */
29
+ export function solidFileSizeGate(
30
+ tool: string,
31
+ filePath: string | undefined,
32
+ content: string | undefined,
33
+ oldString?: string,
34
+ isReplaceAll: boolean = false,
35
+ agentType?: string,
36
+ ): Prompt | null {
37
+ if (tool !== "Write" && tool !== "Edit") return null;
38
+ if (!filePath || !isFileSizeScoped(filePath)) return null;
39
+ if (agentType === "Explore" || agentType === "Plan") return null;
40
+ const { raw: existingLines, content: existingContent } = existingLineCounts(filePath);
41
+ const incoming = content !== undefined ? countLines(content) : 0;
42
+ const existing = existingLines ?? 0;
43
+ const max = resolveMaxLines();
44
+ const editResult = tool === "Edit" && existingContent !== undefined && content !== undefined
45
+ ? computeEditResultLines(existingContent, oldString, content, isReplaceAll)
46
+ : null;
47
+ if (editResult !== null && (editResult <= max || editResult < existing)) return null;
48
+ const lines = tool === "Edit" ? (editResult ?? Math.max(incoming, existing)) : incoming || existing;
49
+ const displayLines = tool === "Write" ? (existingLines ?? lines) : lines;
50
+ const verdict = evaluateFileSize(lines, max, filePath, resolveSolidRefFramework(filePath), displayLines);
51
+ if (lines === 0 || verdict.ok) return null;
52
+ return {
53
+ kind: "block",
54
+ title: "SOLID file-size limit",
55
+ reason: verdict.message ?? "",
56
+ actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"],
57
+ };
58
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Solid-scope PreToolUse outcome: the file-size deny ({@link solidFileSizeGate})
3
+ * first — rendered via {@link respond} so every harness gets its native shape
4
+ * (Kimi finally receives a real `hookSpecificOutput` deny instead of a leaked
5
+ * Claude envelope) — then the ported Go/Python interface-location check
6
+ * ({@link validateSolidGate}, kept byte-identical for claude-code/codex).
7
+ * Owns the whole solid-scope branch so `handle-pre` stays a dispatcher and the
8
+ * branch NEVER falls through to the core APEX/SOLID gate chain: core-guards
9
+ * owns it, so falling through would run `gate()` twice per edit when both
10
+ * plugins wire PreToolUse Write|Edit (duplicate denies + added latency). For
11
+ * the same reason the file-size gate itself ABSTAINS when core-guards is
12
+ * installed ({@link coreGuardsWired}): one tool-call, one deny, whoever fires.
13
+ */
14
+ import { dirname } from "node:path";
15
+ import { withDenyNotice } from "./deny-notice";
16
+ import { respond } from "./respond";
17
+ import { validateSolidGate } from "./lifecycle";
18
+ import { solidFileSizeGate } from "./solid-file-size-gate";
19
+ import { coreGuardsWired } from "./core-guards-wired";
20
+ import type { NormalizedEvent } from "./normalize";
21
+ import type { HandleOutcome } from "./handle-types";
22
+
23
+ /**
24
+ * Run the solid-scope PreToolUse gates and render the native hook outcome.
25
+ * @param id - Harness id (selects the response shape via {@link respond}).
26
+ * @param event - The normalized hook event.
27
+ * @param file - Session track file (the deny-notice state derives from its dir).
28
+ * @param now - Current time in ms.
29
+ * @param env - Environment for the core-guards wiring probe (injectable).
30
+ * @returns The hook outcome: file-size deny, Go/Python deny, or allow.
31
+ */
32
+ export function solidScopeOutcome(
33
+ id: string,
34
+ event: NormalizedEvent,
35
+ file: string,
36
+ now: number,
37
+ env: Record<string, string | undefined> = process.env,
38
+ ): HandleOutcome {
39
+ const prompt = coreGuardsWired(id, env)
40
+ ? null
41
+ : solidFileSizeGate(
42
+ event.tool,
43
+ event.filePath,
44
+ event.content,
45
+ event.oldString,
46
+ event.input.replace_all === true,
47
+ event.agentType,
48
+ );
49
+ if (prompt) {
50
+ return { stdout: withDenyNotice(id, respond(id, prompt), prompt, event.sessionId, dirname(file), now), exit: 0 };
51
+ }
52
+ return { stdout: validateSolidGate(event.tool, event.filePath ?? "", event.content ?? ""), exit: 0 };
53
+ }
@@ -0,0 +1,9 @@
1
+ import { projectLayout } from "../config/layout";
2
+
3
+ /**
4
+ * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
5
+ * derived from {@link projectLayout}. (Was the per-harness `.claude/harness`.)
6
+ */
7
+ export function harnessStateDir(root: string): string {
8
+ return projectLayout(root).stateDir;
9
+ }
@@ -0,0 +1,31 @@
1
+ import { COLOR_THRESHOLDS } from "./constants";
2
+
3
+ /** A text-decorating function. */
4
+ export type ColorFn = (text: string) => string;
5
+
6
+ const ansi = (code: string): ColorFn => (text) => `\x1b[${code}m${text}\x1b[0m`;
7
+ const ansi256 = (code: number): ColorFn => (text) => `\x1b[38;5;${code}m${text}\x1b[0m`;
8
+
9
+ /** Forced ANSI palette (every entry is a colorizer, plus reset + support flag). */
10
+ export interface Palette {
11
+ blue: ColorFn; cyan: ColorFn; green: ColorFn; yellow: ColorFn; red: ColorFn;
12
+ magenta: ColorFn; white: ColorFn; gray: ColorFn; purple: ColorFn; orange: ColorFn;
13
+ brightRed: ColorFn; brightYellow: ColorFn; brightGreen: ColorFn;
14
+ bold: ColorFn; dim: ColorFn; reset: string; isSupported: boolean;
15
+ }
16
+
17
+ /** Forced ANSI color helpers (ignore TTY detection). */
18
+ export const colors: Palette = {
19
+ blue: ansi("0;34"), cyan: ansi("0;36"), green: ansi("0;32"),
20
+ yellow: ansi("0;33"), red: ansi("0;31"), magenta: ansi("0;35"),
21
+ white: ansi("0;37"), gray: ansi256(240), purple: ansi256(135), orange: ansi256(208),
22
+ brightRed: ansi("1;91"), brightYellow: ansi("1;93"), brightGreen: ansi("1;92"),
23
+ bold: ansi("1"), dim: ansi("2"), reset: "\x1b[0m", isSupported: true,
24
+ };
25
+
26
+ /** Color `text` by threshold: green < WARNING <= yellow < CRITICAL <= red. */
27
+ export function progressiveColor(value: number, text: string): string {
28
+ if (value >= COLOR_THRESHOLDS.CRITICAL) return colors.brightRed(text);
29
+ if (value >= COLOR_THRESHOLDS.WARNING) return colors.brightYellow(text);
30
+ return colors.green(text);
31
+ }
@@ -0,0 +1,17 @@
1
+ /** Time interval constants (ms). */
2
+ export const TIME_INTERVALS = { MINUTE_MS: 60_000, HOUR_MS: 3_600_000 } as const;
3
+
4
+ /** Percentage thresholds for progressive coloring. */
5
+ export const COLOR_THRESHOLDS = { WARNING: 70, CRITICAL: 90 } as const;
6
+
7
+ /** Fill/empty glyphs per progress-bar style. */
8
+ export const PROGRESS_CHARS = {
9
+ blocks: { fill: "█", empty: "░" },
10
+ bars: { fill: "▰", empty: "▱" },
11
+ } as const;
12
+
13
+ /** Defaults for {@link generateProgressBar}. */
14
+ export const PROGRESS_BAR_DEFAULTS = { STYLE: "blocks", LENGTH: 10 } as const;
15
+
16
+ /** 9 gradient blocks from empty to full. */
17
+ export const GRADIENT_BLOCKS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"] as const;
@@ -0,0 +1,54 @@
1
+ import { basename } from "node:path";
2
+ import { TIME_INTERVALS } from "./constants";
3
+
4
+ /** Format a path: truncated (default), full, relative (~), or basename. */
5
+ export function formatPath(
6
+ path: string,
7
+ style: "truncated" | "full" | "relative" | "basename" = "truncated",
8
+ ): string {
9
+ const home = process.env.HOME || "";
10
+ const isUnderHome = home !== "" && path.startsWith(home);
11
+ const withTilde = isUnderHome ? path.replace(home, "~") : path;
12
+ const parts = withTilde.split("/").filter(Boolean);
13
+ const name = parts[parts.length - 1] || withTilde;
14
+ switch (style) {
15
+ case "full": return path;
16
+ case "basename": return name;
17
+ case "relative": return withTilde;
18
+ default:
19
+ if (isUnderHome && parts.length > 2) return `~/../${name}`;
20
+ if (isUnderHome) return withTilde;
21
+ if (parts.length > 3) return `/../${name}`;
22
+ return withTilde;
23
+ }
24
+ }
25
+
26
+ /** Basename of a path. */
27
+ export function formatBasename(path: string): string {
28
+ return basename(path);
29
+ }
30
+
31
+ /** Humanize a remaining duration in ms (e.g. "2h - 5m"). */
32
+ export function formatTimeLeft(ms: number): string {
33
+ if (ms <= 0) return "0m";
34
+ const dayMs = TIME_INTERVALS.HOUR_MS * 24;
35
+ const days = Math.floor(ms / dayMs);
36
+ const hours = Math.floor((ms % dayMs) / TIME_INTERVALS.HOUR_MS);
37
+ const minutes = Math.floor((ms % TIME_INTERVALS.HOUR_MS) / TIME_INTERVALS.MINUTE_MS);
38
+ if (days > 0 && hours > 0) return `${days}d - ${hours}h`;
39
+ if (days > 0) return `${days}d`;
40
+ if (hours > 0 && minutes > 0) return `${hours}h - ${minutes}m`;
41
+ if (hours > 0) return `${hours}h`;
42
+ return `${minutes}m`;
43
+ }
44
+
45
+ /** Format a token count as "12K" (or "12.3K" with decimals). */
46
+ export function formatTokens(tokens: number, showDecimals = false): string {
47
+ const k = tokens / 1000;
48
+ return showDecimals ? `${k.toFixed(1)}K` : `${Math.round(k)}K`;
49
+ }
50
+
51
+ /** Format a cost as "$1.23". */
52
+ export function formatCost(cost: number, decimals = 2): string {
53
+ return `$${cost.toFixed(decimals)}`;
54
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./constants";
2
+ export * from "./colors";
3
+ export * from "./formatters";
4
+ export * from "./progress-bar";
@@ -0,0 +1,46 @@
1
+ import { GRADIENT_BLOCKS, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS } from "./constants";
2
+ import { progressiveColor } from "./colors";
3
+
4
+ /** Options for {@link generateProgressBar}. */
5
+ export interface ProgressBarOptions {
6
+ style?: keyof typeof PROGRESS_CHARS;
7
+ length?: number;
8
+ useProgressiveColor?: boolean;
9
+ showPercentage?: boolean;
10
+ }
11
+
12
+ const clampPct = (p: number): number => Math.max(0, Math.min(100, Number.isNaN(p) ? 0 : p));
13
+
14
+ /** Render a fill/empty progress bar. */
15
+ export function generateProgressBar(percentage: number, options: ProgressBarOptions = {}): string {
16
+ const {
17
+ style = PROGRESS_BAR_DEFAULTS.STYLE, length = PROGRESS_BAR_DEFAULTS.LENGTH,
18
+ useProgressiveColor = false, showPercentage = false,
19
+ } = options;
20
+ const pct = clampPct(percentage);
21
+ const filled = Math.round((pct / 100) * length);
22
+ const empty = Math.max(0, length - filled);
23
+ const chars = PROGRESS_CHARS[style];
24
+ let bar = chars.fill.repeat(filled) + chars.empty.repeat(empty);
25
+ if (useProgressiveColor) bar = progressiveColor(pct, bar);
26
+ if (showPercentage) {
27
+ const pctText = `${Math.round(pct)}%`;
28
+ bar += ` ${useProgressiveColor ? progressiveColor(pct, pctText) : pctText}`;
29
+ }
30
+ return bar;
31
+ }
32
+
33
+ /** Render a fine-grained gradient bar (sub-block resolution). */
34
+ export function generateGradientBar(percentage: number, length = 10): string {
35
+ const pct = clampPct(percentage);
36
+ const exact = (pct / 100) * length;
37
+ const full = Math.floor(exact);
38
+ const remainder = exact - full;
39
+ let bar = GRADIENT_BLOCKS[8].repeat(full);
40
+ if (full < length && remainder > 0) {
41
+ bar += GRADIENT_BLOCKS[Math.floor(remainder * 8)] ?? "";
42
+ }
43
+ const remaining = length - bar.length;
44
+ if (remaining > 0) bar += " ".repeat(remaining);
45
+ return progressiveColor(pct, bar);
46
+ }