@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,47 @@
1
+ /**
2
+ * @module inject-budget-recap
3
+ * ONE event-level recap of every {@link module:inject-budget.capFragment}
4
+ * -tracked fragment injected for a SessionStart/SubagentStart event — the
5
+ * aggregated view the per-fragment caps never gave: each injection point
6
+ * (dev-context, snapshot sections, lessons, apex-subagent, ...) caps and
7
+ * reports itself in ISOLATION, with no total across the whole event.
8
+ *
9
+ * Rides the user-visible `systemMessage` channel, Claude-Code-only (gate at
10
+ * the call site — mirrors the existing `id === "claude-code"` pattern for
11
+ * `designLifecycle` in handle.ts): other adapters do not all re-export
12
+ * `attachSystemMessage`, and stamping a Claude-shaped envelope onto another
13
+ * harness's stdout shape would be silently wrong there.
14
+ *
15
+ * Deduped via {@link module:inject-dedup.onceExclusive}, not the JSON
16
+ * `oncePerWindow`: SessionStart/SubagentStart fan out across every installed
17
+ * plugin exactly like PostToolUse does (see burst-window.ts) — a shared-JSON
18
+ * read-modify-write here would risk the same lost-update race already fixed
19
+ * for the sniper reminder (lesson 2026-07-05 16:00).
20
+ * @packageDocumentation
21
+ */
22
+ import { attachSystemMessage } from "../adapters/claude";
23
+ import { budgetReport } from "./inject-budget";
24
+ import { fragmentRegistry } from "./fragment-registry";
25
+ import { onceExclusive } from "./inject-dedup";
26
+ import { BURST_DEDUP_MS } from "./burst-window";
27
+ import { defaultStateDir } from "./paths";
28
+
29
+ /**
30
+ * Attach the aggregated recap onto `stdout` when 2+ fragments were recorded
31
+ * for `rawEvent`. A lone fragment already carries its own visibility (its
32
+ * producer's own per-fragment report, when it has one) — no recap is added
33
+ * for that case, so the common single-fragment event stays noise-free.
34
+ * @param stdout - The already-rendered hook stdout for this event.
35
+ * @param rawEvent - The raw hook event name (only SessionStart/SubagentStart qualify; others pass through unchanged).
36
+ * @param sessionId - Current session id (dedup scope).
37
+ * @param cwd - Project root (state-dir scope for the dedup marker).
38
+ * @param now - Clock.
39
+ * @returns `stdout` with a `systemMessage` recap attached, or `stdout` unchanged.
40
+ */
41
+ export function attachBudgetRecap(stdout: string, rawEvent: string, sessionId: string, cwd: string, now: number): string {
42
+ if (rawEvent !== "SessionStart" && rawEvent !== "SubagentStart") return stdout;
43
+ const fragments = fragmentRegistry();
44
+ if (fragments.length <= 1) return stdout;
45
+ if (!onceExclusive(`budget:${sessionId}:${rawEvent}`, BURST_DEDUP_MS, { now, dir: defaultStateDir(cwd) })) return stdout;
46
+ return attachSystemMessage(stdout, budgetReport(fragments));
47
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @module inject-budget
3
+ * Hard per-fragment ceiling for harness-PRODUCED context injections (lessons,
4
+ * snapshot sections, APEX task context, cartographer/dev-context blocks) —
5
+ * inspired by Codex's "ContextualUserFragment, no item > 10K tokens" pattern.
6
+ * NEVER apply this to owner-authored content (CLAUDE.md/rules): that path is
7
+ * a locked invariant (see the "owner invariant" tests in
8
+ * test/dedup-inject.test.ts) and must always ship uncapped and in full.
9
+ *
10
+ * A real regression motivated this: MEMORY/LESSON.md's SessionStart injection
11
+ * had grown to ~44k tokens with no per-fragment or total alarm, silently
12
+ * bloating every SessionStart/SubagentStart turn.
13
+ * @packageDocumentation
14
+ */
15
+ import { recordFragment } from "./fragment-registry";
16
+
17
+ /**
18
+ * Hard cap per fragment, in characters. ~8000 chars ≈ 2000 tokens at the
19
+ * conservative ~4 chars/token ratio for mixed FR/EN prose — the accepted
20
+ * approximation absent a real tokenizer (cf. LangChain's `trim_messages`
21
+ * fallback token_counter). Chosen because a SINGLE SessionStart already
22
+ * stacks several harness fragments (lessons, snapshot, dev-context,
23
+ * cartographer) on top of the uncapped CLAUDE.md block, and the known 11x
24
+ * multi-plugin hook fan-out can multiply whatever ships — 2k tokens/fragment
25
+ * keeps the harness-produced share bounded even under that fan-out, while
26
+ * still leaving room for a few dozen useful lines of content.
27
+ */
28
+ export const FRAGMENT_CHAR_CAP = 8000;
29
+
30
+ /**
31
+ * Cap `text` at {@link FRAGMENT_CHAR_CAP} characters, cutting at the last
32
+ * newline within budget so no line is chopped mid-sentence. Text at or under
33
+ * the cap is returned byte-identical (no-op). Over the cap, a single English
34
+ * truncation line is appended so the reader knows content was dropped and
35
+ * that the SOURCE FILE itself is untouched (only this injected view is cut).
36
+ * @param label - Short identifier of the fragment (e.g. "lessons", "Git").
37
+ * @param text - The candidate fragment body.
38
+ * @returns `text` unchanged, or a truncated copy ending in the cut notice — always ≤ the cap.
39
+ */
40
+ export function capFragment(label: string, text: string): string {
41
+ if (text.length <= FRAGMENT_CHAR_CAP) {
42
+ recordFragment(label, text.length);
43
+ return text;
44
+ }
45
+ const totalLen = text.length;
46
+ // Bound the label so a pathological (very long) label can never, by itself,
47
+ // push the suffix — and thus the output — past the cap. Real labels are short
48
+ // internal constants, so this is byte-identical for every actual caller.
49
+ const safeLabel = label.length <= 80 ? label : `${label.slice(0, 77)}...`;
50
+ const suffixFor = (keptLen: number): string =>
51
+ `\n[truncated ${safeLabel}: kept ${keptLen} of ${totalLen} chars — source file unchanged]`;
52
+ // Reserve worst-case suffix size using totalLen's digit count: kept length
53
+ // can only be <= totalLen, so its digit count never exceeds this reserve.
54
+ const reserve = suffixFor(totalLen).length;
55
+ const budget = Math.max(0, FRAGMENT_CHAR_CAP - reserve);
56
+ const slice = text.slice(0, budget);
57
+ const lastNl = slice.lastIndexOf("\n");
58
+ const kept = (lastNl > 0 ? slice.slice(0, lastNl) : slice).trimEnd();
59
+ const result = kept + suffixFor(kept.length);
60
+ recordFragment(label, result.length);
61
+ return result;
62
+ }
63
+
64
+ /** One named fragment's injected size, for {@link budgetReport}. */
65
+ export interface FragmentSize {
66
+ label: string;
67
+ chars: number;
68
+ }
69
+
70
+ /**
71
+ * One-line numeric recap of what a batch of fragments actually injected —
72
+ * the owner-requested visibility so a silent blowup (like the 44k-token
73
+ * lessons block) shows up as a number instead of going unnoticed.
74
+ * @param fragments - The injected fragments (post-cap sizes).
75
+ * @returns e.g. `"injected 5 fragments, 14.2k chars"`, or `"injected 0 fragments"` when empty.
76
+ */
77
+ export function budgetReport(fragments: FragmentSize[]): string {
78
+ if (fragments.length === 0) return "injected 0 fragments";
79
+ const total = fragments.reduce((sum, f) => sum + f.chars, 0);
80
+ return `injected ${fragments.length} fragments, ${(total / 1000).toFixed(1)}k chars`;
81
+ }
@@ -0,0 +1,85 @@
1
+ import { attachSystemMessage, contextResponse } from "../adapters/claude";
2
+ import { renderInform } from "./inform";
3
+ import { buildClaudeMdContext } from "../policy/claude-md-context";
4
+ import { buildApexTaskInjection } from "../policy/apex-task-context";
5
+ import { apexDocName } from "../policy/apex-target";
6
+ import { hashText } from "../util/json-io";
7
+ import { oncePerWindow, DEDUP_WINDOW_MS } from "./inject-dedup";
8
+ import { capFragment } from "./inject-budget";
9
+
10
+ /**
11
+ * Build the {@link oncePerWindow} key for the CLAUDE.md preamble gate. The
12
+ * prompt hash keeps two distinct legitimate turns from colliding — even non-dev
13
+ * prompts, whose block is prompt-independent (just CLAUDE.md) and would
14
+ * otherwise hash-collide within the window — while the content hash still lets a
15
+ * same-turn double-fire of an identical block be suppressed. Single source of
16
+ * truth so the owner invariant test guards the real production key.
17
+ * @param prompt - The raw user prompt.
18
+ * @param ctx - The rendered CLAUDE.md (+ optional APEX) block.
19
+ * @returns The namespaced dedup key.
20
+ */
21
+ export function claudeMdKey(prompt: string, ctx: string): string {
22
+ return `claude-md:${hashText(prompt)}:${hashText(ctx)}`;
23
+ }
24
+
25
+ /**
26
+ * Extract the APEX preamble portion of a `buildClaudeMdContext` result, i.e.
27
+ * everything BEFORE the `# <docName>\n` root-doc block it prepends on dev
28
+ * prompts. Returns "" when `ctx` has no preamble (plain prompt: `ctx` starts
29
+ * directly with the root-doc heading).
30
+ * @param ctx - The full `buildClaudeMdContext` return value.
31
+ * @param docName - Root doc file name (e.g. "AGENTS.md").
32
+ * @returns The preamble text, or "" when absent.
33
+ */
34
+ function extractApexPreamble(ctx: string, docName: string): string {
35
+ const heading = `# ${docName}\n`;
36
+ if (ctx.startsWith(heading)) return "";
37
+ const sep = `\n\n${heading}`;
38
+ const idx = ctx.indexOf(sep);
39
+ return idx === -1 ? "" : ctx.slice(0, idx);
40
+ }
41
+
42
+ /**
43
+ * UserPromptSubmit context injection: render the CLAUDE.md (+ optional APEX)
44
+ * preamble as a Claude `additionalContext` response, or "" when nothing to emit.
45
+ * Guarded by {@link oncePerWindow} via {@link claudeMdKey}: only a
46
+ * near-simultaneous double-fire of the SAME turn (identical prompt AND identical
47
+ * block, within {@link DEDUP_WINDOW_MS}) is suppressed. The invariant "CLAUDE.md
48
+ * is emitted on EVERY message" is thus preserved.
49
+ *
50
+ * On kimi specifically, the root doc body is dropped from the emitted text:
51
+ * Kimi loads `<kimiHome>/AGENTS.md` natively at session start (`apex-target.ts`),
52
+ * so re-injecting its full body on every prompt is redundant terminal noise
53
+ * (kimi has no model-only hook channel — `runtime/inform.ts`). The APEX
54
+ * preamble, which is NOT part of `AGENTS.md`, is still emitted in full when the
55
+ * prompt is dev-shaped; a plain prompt leaves only the notice.
56
+ * @param prompt - The raw user prompt.
57
+ * @param cwd - Project root (for project-type detection).
58
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
59
+ * @returns The native hook stdout (possibly empty).
60
+ */
61
+ export function promptSubmitContext(prompt: string, cwd: string, id: string = "claude-code"): string {
62
+ const ctx = buildClaudeMdContext(prompt, cwd, id);
63
+ if (!ctx) return "";
64
+ if (!oncePerWindow(claudeMdKey(prompt, ctx), DEDUP_WINDOW_MS)) return "";
65
+ const notice = `${apexDocName(id)} injected`;
66
+ if (id === "kimi") {
67
+ const preamble = extractApexPreamble(ctx, apexDocName(id));
68
+ return preamble ? renderInform(id, "UserPromptSubmit", preamble, notice) : notice;
69
+ }
70
+ return renderInform(id, "UserPromptSubmit", ctx, notice);
71
+ }
72
+
73
+ /**
74
+ * PreToolUse Task context injection: render the APEX sub-agent context as a
75
+ * Claude `additionalContext` response when `.claude/apex/` exists, else "".
76
+ * Harness-produced (not owner CLAUDE.md content), so it is subject to the
77
+ * per-fragment {@link capFragment} budget — unlike {@link promptSubmitContext}.
78
+ * @param cwd - Fallback project root when `CLAUDE_PROJECT_DIR` is unset.
79
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
80
+ * @returns The native hook stdout (possibly empty).
81
+ */
82
+ export function taskContext(cwd: string, id: string = "claude-code"): string {
83
+ const ctx = buildApexTaskInjection(process.env.CLAUDE_PROJECT_DIR ?? cwd, id);
84
+ return ctx ? contextResponse("PreToolUse", capFragment("apex-task", ctx)) : "";
85
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @module inject-dedup-exclusive
3
+ * Cross-process-exact-once cooldown gate, via EXCLUSIVE file creation.
4
+ *
5
+ * {@link module:inject-dedup.oncePerWindow}'s shared-JSON read-modify-write is
6
+ * best-effort under true concurrency: the ~11-process plugin hook fan-out for
7
+ * one Claude event can lose an update and let 2-3 siblings all observe "not
8
+ * seen yet" (lesson 2026-07-05 16:00 — the `saveTrack` lost-update race, same
9
+ * shape). `writeFileSync(path, data, { flag: "wx" })` sidesteps this: the OS
10
+ * guarantees exclusive creation is atomic, so of N concurrent siblings calling
11
+ * this for the SAME key, exactly one observes success and the rest get
12
+ * `EEXIST` — never a double-win, with no lock file or retry loop needed.
13
+ *
14
+ * One marker file per key (not a shared map) is the tradeoff for that
15
+ * guarantee. A bounded sweep on every call deletes markers older than
16
+ * `windowMs` so the directory never grows unbounded under many distinct keys.
17
+ *
18
+ * Reserve this for HIGH-CONCURRENCY callers on a short burst window (same
19
+ * tool-use/lifecycle event fanned out to every installed plugin) — e.g. the
20
+ * sniper reminder ({@link module:lifecycle/track-changes}) and compliance
21
+ * notices ({@link module:notices}). Low-frequency, long-window callers (e.g.
22
+ * the 30-min lessons Stop-reminder cooldown) are fine on the JSON mode: a
23
+ * single real Stop event per session is not concurrent with itself the way
24
+ * one PostToolUse's ~11 sibling hooks are within the same 2s burst.
25
+ * @packageDocumentation
26
+ */
27
+ import { createHash } from "node:crypto";
28
+ import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { defaultStateDir } from "./paths";
31
+ import type { OnceOpts } from "./inject-dedup";
32
+
33
+ /** Subdirectory (under the state dir) holding one marker file per dedup key. */
34
+ const EXCLUSIVE_SUBDIR = "inject-dedup-locks";
35
+
36
+ /**
37
+ * Filesystem-safe, collision-resistant basename for `key`. Full MD5 hex
38
+ * (unlike {@link module:util/json-io.hashText}'s 8-char truncation) — a
39
+ * collision here would silently merge two unrelated keys' exclusivity.
40
+ */
41
+ function lockFileName(key: string): string {
42
+ return `${createHash("md5").update(key).digest("hex")}.lock`;
43
+ }
44
+
45
+ /**
46
+ * Delete marker files older than `windowMs` in `dir` — O(n) per call, bounds
47
+ * directory growth. Compares against the creation timestamp STORED IN the
48
+ * marker's content (written by {@link onceExclusive} below), not the file's
49
+ * fs `mtime`: callers may pass a fake logical `now` (tests), which would never
50
+ * agree with the real OS clock backing `mtime`.
51
+ */
52
+ function sweepExclusiveDir(dir: string, now: number, windowMs: number): void {
53
+ let entries: string[];
54
+ try {
55
+ entries = readdirSync(dir);
56
+ } catch {
57
+ return;
58
+ }
59
+ for (const entry of entries) {
60
+ const path = join(dir, entry);
61
+ try {
62
+ const createdAt = Number(readFileSync(path, "utf8"));
63
+ if (!Number.isFinite(createdAt) || now - createdAt >= windowMs) unlinkSync(path);
64
+ } catch {
65
+ /* a sibling process already swept/removed it, or a transient read race — ignore */
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Cooldown gate via exclusive marker-file creation. Returns `true` exactly
72
+ * once per `key` within `windowMs` across ALL concurrent processes sharing
73
+ * `opts.dir` (the caller MAY emit), `false` for every other concurrent or
74
+ * subsequent call inside the same window (the caller SHOULD suppress).
75
+ *
76
+ * Fails open on any unwritable state dir or unexpected fs error: the emission
77
+ * is allowed rather than silently dropped.
78
+ * @param key - Stable identity of the block (same semantics as {@link module:inject-dedup.oncePerWindow}).
79
+ * @param windowMs - Suppression window in ms (also the sweep threshold).
80
+ * @param opts - Optional clock + state-dir overrides (for tests).
81
+ * @returns `true` to proceed/emit, `false` to suppress.
82
+ */
83
+ export function onceExclusive(key: string, windowMs: number, opts: OnceOpts = {}): boolean {
84
+ const now = opts.now ?? Date.now();
85
+ const dir = join(opts.dir ?? defaultStateDir(), EXCLUSIVE_SUBDIR);
86
+ try {
87
+ mkdirSync(dir, { recursive: true });
88
+ } catch {
89
+ return true;
90
+ }
91
+ sweepExclusiveDir(dir, now, windowMs);
92
+ try {
93
+ writeFileSync(join(dir, lockFileName(key)), String(now), { flag: "wx" });
94
+ return true;
95
+ } catch (err) {
96
+ return (err as NodeJS.ErrnoException).code !== "EEXIST";
97
+ }
98
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @module inject-dedup
3
+ * Generic "once per window" guard for hook context injection.
4
+ *
5
+ * Persists a tiny sidecar map `{ key -> lastEmittedEpochMs }` in the existing
6
+ * per-project state dir (`~/.fuse-harness/state/<projectHash>/inject-dedup.json`,
7
+ * the same dir as the session track — see {@link defaultStateDir}). Lets a caller
8
+ * suppress a block it has already emitted within a short window, defending
9
+ * against the "same content emitted by two near-simultaneous hooks" family
10
+ * (cf. the 0.1.48 SubagentStart double-inject). Also reused as a cooldown gate.
11
+ *
12
+ * Best-effort under true process-level concurrency (the read-modify-write is not
13
+ * locked); reliable for the sequential single-process callers that use it.
14
+ * @packageDocumentation
15
+ */
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { atomicWrite } from "../util/json-io";
19
+ import { defaultStateDir } from "./paths";
20
+
21
+ /** Default suppression window (ms): catches same-turn double-fire, not next-turn re-emits. */
22
+ export const DEDUP_WINDOW_MS = 3000;
23
+
24
+ /** Sidecar basename under the per-project state dir. */
25
+ const SIDECAR = "inject-dedup.json";
26
+
27
+ /** Optional overrides — tests inject a clock + a scratch dir; production omits both. */
28
+ export interface OnceOpts {
29
+ /** Clock in epoch-ms (default `Date.now()`). */
30
+ now?: number;
31
+ /** State-dir override (default {@link defaultStateDir}). */
32
+ dir?: string;
33
+ }
34
+
35
+ /** Load the `{ key -> epochMs }` map, or `{}` when missing/corrupt. */
36
+ function loadMap(path: string): Record<string, number> {
37
+ try {
38
+ if (!existsSync(path)) return {};
39
+ const data: unknown = JSON.parse(readFileSync(path, "utf8"));
40
+ return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, number>) : {};
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+
46
+ /** Keep only entries newer than `windowMs` before `now` (bounds sidecar size). */
47
+ function prune(map: Record<string, number>, now: number, windowMs: number): Record<string, number> {
48
+ const out: Record<string, number> = {};
49
+ for (const [k, t] of Object.entries(map)) {
50
+ if (typeof t === "number" && now - t < windowMs) out[k] = t;
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * Cooldown gate. Returns `true` when `key` has NOT been recorded within the last
57
+ * `windowMs` (the caller MAY emit — and the emission is recorded now), or `false`
58
+ * when it was (the caller SHOULD suppress). The first call in a window wins;
59
+ * subsequent identical keys are throttled until the window elapses.
60
+ *
61
+ * Fails open: if the sidecar is unwritable, the emission is allowed rather than
62
+ * silently dropping context.
63
+ * @param key - Stable identity of the block (e.g. a content hash, or `lesson:<id>`).
64
+ * @param windowMs - Suppression window in ms.
65
+ * @param opts - Optional clock + state-dir overrides (for tests).
66
+ * @returns `true` to proceed/emit, `false` to suppress.
67
+ */
68
+ export function oncePerWindow(key: string, windowMs: number, opts: OnceOpts = {}): boolean {
69
+ const now = opts.now ?? Date.now();
70
+ const path = join(opts.dir ?? defaultStateDir(), SIDECAR);
71
+ const map = prune(loadMap(path), now, windowMs);
72
+ const last = map[key];
73
+ if (typeof last === "number" && now - last < windowMs) return false;
74
+ map[key] = now;
75
+ try {
76
+ atomicWrite(path, JSON.stringify(map));
77
+ } catch {
78
+ /* sidecar unwritable → fail-open: allow the emission rather than lose context */
79
+ }
80
+ return true;
81
+ }
82
+
83
+ /**
84
+ * Cross-process-exact-once variant (exclusive file creation, not the shared
85
+ * JSON above) for high-concurrency callers — split out to
86
+ * {@link module:inject-dedup-exclusive} to keep this file under the SOLID
87
+ * line budget; re-exported here so both modes share one import surface.
88
+ */
89
+ export { onceExclusive } from "./inject-dedup-exclusive";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Sub-agent dispatch tool detection — the single source of truth for "this
3
+ * tool call spawns a sub-agent". Claude Code and Codex dispatch via `Task`;
4
+ * Kimi Code CLI dispatches via `Agent` (and `AgentSwarm` for parallel fan-out).
5
+ * Centralized so the APEX task-context injection, the activity tracking and
6
+ * the PostToolUse evidence credit stop drifting on inline `"Task" | "Agent"`
7
+ * unions: every consumer imports from HERE, never re-declares the list.
8
+ */
9
+
10
+ /** Canonical set of sub-agent dispatch tool names (the ONLY definition). */
11
+ export const AGENT_TOOLS: ReadonlySet<string> = new Set(["Task", "Agent", "AgentSwarm"]);
12
+
13
+ /**
14
+ * True when the tool is a sub-agent dispatch primitive on any harness.
15
+ * @param tool - The normalized tool name from the hook payload.
16
+ */
17
+ export function isAgentTool(tool: string): boolean {
18
+ return AGENT_TOOLS.has(tool);
19
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Per-agent file attribution for SubagentStop. Uses the shared transcript parser
3
+ * ({@link readAgentToolUses}) to recover the exact set of files a sub-agent wrote
4
+ * via Write/Edit/MultiEdit/NotebookEdit, so the sniper reminder attributes only
5
+ * the files THIS agent touched instead of every file changed in the whole session
6
+ * (which cross-attributes other teammates' work — the bug this fixes).
7
+ */
8
+ import { basename } from "node:path";
9
+ import { readAgentToolUses } from "./agent-transcript";
10
+
11
+ /** Tools whose `input.file_path`/`notebook_path` names a file the agent authored. */
12
+ const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
13
+
14
+ /**
15
+ * Return the distinct file paths written by the sub-agent whose own transcript is
16
+ * at `transcriptPath` (Write/Edit/MultiEdit/NotebookEdit). `null` when unreadable
17
+ * (caller MUST fall back to the session-wide list — no regression); a possibly-
18
+ * empty array when parsed (empty = authored nothing, so it should NOT be told to
19
+ * validate files other agents changed).
20
+ * @param transcriptPath - Absolute path to the sub-agent's own `.jsonl` transcript.
21
+ * @returns Distinct written paths (order-preserving), or `null` when unreadable.
22
+ */
23
+ export function filesWrittenByAgent(transcriptPath: string | undefined): string[] | null {
24
+ const uses = readAgentToolUses(transcriptPath);
25
+ if (uses === null) return null;
26
+ const out: string[] = [];
27
+ const seen = new Set<string>();
28
+ for (const u of uses) {
29
+ if (!WRITE_TOOLS.has(u.name)) continue;
30
+ const fp = u.input?.file_path ?? u.input?.notebook_path;
31
+ if (typeof fp === "string" && fp && !seen.has(fp)) { seen.add(fp); out.push(fp); }
32
+ }
33
+ return out;
34
+ }
35
+
36
+ /**
37
+ * Filter `sessionFiles` down to the ones this agent actually wrote. Matches the
38
+ * exact recorded string first, then tolerantly on `basename` so a relative-vs-
39
+ * absolute drift between the PostToolUse record and the transcript input never
40
+ * loses an owned file.
41
+ * @param sessionFiles - Session-wide modified files.
42
+ * @param written - Paths the agent wrote (from {@link filesWrittenByAgent}).
43
+ * @returns The subset of `sessionFiles` attributable to this agent.
44
+ */
45
+ export function attributeFiles(sessionFiles: readonly string[], written: readonly string[]): string[] {
46
+ const exact = new Set(written);
47
+ const bases = new Set(written.map((f) => basename(f)));
48
+ return sessionFiles.filter((f) => exact.has(f) || bases.has(basename(f)));
49
+ }
@@ -0,0 +1,82 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { contextResponse } from "../../adapters/claude";
5
+ import { resolveTtlSec } from "../../config/ttl";
6
+ import { loadSessionState, sanitizeSessionId, saveSessionState, sessionsDir } from "../home-state";
7
+ import { defaultStateDir, trackFile } from "../paths";
8
+ import { freshReceiptFromFile } from "../../tracking/receipts";
9
+ import { attributeFiles, filesWrittenByAgent } from "./agent-files";
10
+
11
+ /** The `changes` block written by `track-changes.ts` into unified session state. */
12
+ interface Changes {
13
+ cumulativeCodeFiles?: number;
14
+ modifiedFiles?: string[];
15
+ }
16
+
17
+ /** `~/.claude/memory/agents` — agent completion history dir. */
18
+ function memoryDir(home: string): string {
19
+ return join(home, ".claude", "memory", "agents");
20
+ }
21
+
22
+ const SKIP_AGENTS = /(sniper|sniper-faster|explore-codebase|research-expert|claude-code-guide|Explore|Plan)/;
23
+
24
+ /** Append the agent completion record to `agent-history.jsonl` (best effort). */
25
+ function recordHistory(home: string, agentId: string, agentType: string, ts: string): void {
26
+ const dir = memoryDir(home);
27
+ try {
28
+ mkdirSync(dir, { recursive: true });
29
+ appendFileSync(join(dir, "agent-history.jsonl"), JSON.stringify({ agentId, agentType, completedAt: ts }) + "\n", "utf-8");
30
+ } catch { /* best effort */ }
31
+ }
32
+
33
+ /**
34
+ * Handle SubagentStop: append the completion to agent-history.jsonl and, for a
35
+ * non-skipped agent that touched code, emit the sniper reminder + reset the
36
+ * counter. Ports `subagent-stop/track-agent-memory.py`.
37
+ * @param data - The raw hook payload.
38
+ * @param home - Home dir (defaults to `~`).
39
+ * @param now - Clock (defaults to `Date.now()`).
40
+ * @returns The native hook stdout (always a JSON message).
41
+ */
42
+ export function trackAgentMemory(data: Record<string, unknown>, home: string = homedir(), now: number = Date.now()): string {
43
+ mkdirSync(sessionsDir(home), { recursive: true });
44
+ const agentType = String(data.agent_type ?? data.subagent_type ?? "unknown");
45
+ const sessionId = sanitizeSessionId(data.session_id) ?? "unknown";
46
+ const ts = new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
47
+ recordHistory(home, String(data.agent_id ?? "unknown"), agentType, ts);
48
+ if (SKIP_AGENTS.test(agentType)) return JSON.stringify({ message: `Agent ${agentType} completed` });
49
+ const state = loadSessionState(sessionId, home);
50
+ const changes = state.changes as Changes | undefined;
51
+ const count = changes?.cumulativeCodeFiles ?? 0;
52
+ if (count > 0) {
53
+ // Attribute only the files THIS agent actually wrote, parsed from its own
54
+ // transcript (SubagentStop `agent_transcript_path`, CLI v2.0.42+). When the
55
+ // field is absent or the transcript is unreadable, `filesWrittenByAgent`
56
+ // returns null → fall back to the session-wide list (no regression).
57
+ const written = filesWrittenByAgent(typeof data.agent_transcript_path === "string" ? data.agent_transcript_path : undefined);
58
+ const owned = written === null ? (changes?.modifiedFiles ?? []) : attributeFiles(changes?.modifiedFiles ?? [], written);
59
+ // Only reset the session counter when this agent owns changes — so an agent
60
+ // that touched none of the tracked files never clears them for the real
61
+ // author, and never gets told to validate another teammate's work.
62
+ if (owned.length > 0) {
63
+ saveSessionState(sessionId, { ...state, changes: { ...changes, cumulativeCodeFiles: 0 } }, home);
64
+ // Never demand sniper on a file the agent deleted before stopping (e.g. a
65
+ // scratch probe rm -rf'd): the tracked list records writes, never removals,
66
+ // so re-check the disk (a relative path resolves against the hook cwd) and
67
+ // keep only files still present. All gone → nothing to validate → "no changes".
68
+ const hookCwd = typeof data.cwd === "string" ? data.cwd : process.cwd();
69
+ const present = owned.filter((f) => existsSync(resolve(hookCwd, f)));
70
+ if (present.length > 0) {
71
+ // Advisory (no hard block here — the TaskCompleted gate enforces): flag the
72
+ // missing proof when the agent owns code but posted no fresh passing receipt.
73
+ // Window = TTL×5, matching the TaskCompleted receipt gate.
74
+ const windowMs = resolveTtlSec(process.env) * 1000 * 5;
75
+ const noReceipt = freshReceiptFromFile(trackFile(sessionId, defaultStateDir(process.cwd())), windowMs, now) === null;
76
+ const note = noReceipt ? " NO VERIFICATION RECEIPT — run tsc + tests before reporting done." : "";
77
+ return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${present.length} code file(s): ${present.join(", ")}. Run sniper agent now.${note}`);
78
+ }
79
+ }
80
+ }
81
+ return JSON.stringify({ message: `Agent ${agentType} completed (no code changes)` });
82
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared low-level parser for a sub-agent's OWN transcript (`agent_transcript_path`
3
+ * — the clean JSONL the Claude Code platform writes per sub-agent, CLI v2.0.42+).
4
+ * Per LESSON.md this is the RELIABLE anchor for evidence: sidechain PostToolUse
5
+ * hooks are not guaranteed (issues #43612/#27655/#34692). One parser, reused by
6
+ * file-attribution (`agent-files.ts`) and retroactive evidence harvesting
7
+ * (`src/freshness/evidence-harvest.ts`) — no duplication.
8
+ */
9
+ import { readText } from "../../util/runtime-io";
10
+
11
+ /** A tool_use content block inside a transcript message. */
12
+ interface ToolUseBlock {
13
+ type: string;
14
+ /** Tool name, e.g. "Write", "Edit", "WebSearch", "Grep". */
15
+ name?: string;
16
+ /** Raw tool input — shape varies per tool; narrowed at the call-site. */
17
+ input?: Record<string, unknown>;
18
+ }
19
+
20
+ /** One JSONL line of a Claude Code transcript. */
21
+ interface TranscriptLine {
22
+ /** ISO-8601 or epoch-ms timestamp written by the platform (may be absent). */
23
+ timestamp?: string | number;
24
+ message?: { content?: unknown[] };
25
+ }
26
+
27
+ /** A parsed `tool_use`, carrying its message timestamp when the platform stamped one. */
28
+ export interface TranscriptToolUse {
29
+ /** Tool name, e.g. "Write", "WebSearch", "Read". */
30
+ name: string;
31
+ /** Raw tool input payload (shape varies per tool). */
32
+ input: Record<string, unknown> | undefined;
33
+ /** Epoch-ms of the enclosing message, or `undefined` when unstamped. */
34
+ ts?: number;
35
+ }
36
+
37
+ /** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
38
+ function parseTs(raw: string | number | undefined): number | undefined {
39
+ if (raw === undefined) return undefined;
40
+ if (typeof raw === "number") return raw;
41
+ const ms = Date.parse(raw);
42
+ return Number.isFinite(ms) ? ms : undefined;
43
+ }
44
+
45
+ /**
46
+ * Scan a sub-agent transcript into its ordered `tool_use` blocks (each with the
47
+ * enclosing message timestamp). Distinguishes "no tool use" from "cannot read":
48
+ * returns `null` when the path is absent or the transcript is unreadable (callers
49
+ * fail-open), or a possibly-empty array when parsed. Malformed lines/blocks are
50
+ * tolerated and skipped.
51
+ * @param transcriptPath - Absolute path to the sub-agent's own `.jsonl` transcript.
52
+ * @returns Ordered tool uses, or `null` when unreadable.
53
+ */
54
+ export function readAgentToolUses(transcriptPath: string | undefined): TranscriptToolUse[] | null {
55
+ if (!transcriptPath) return null;
56
+ let text: string;
57
+ try {
58
+ text = readText(transcriptPath);
59
+ } catch {
60
+ return null; // fail-open: caller keeps its pre-harvest state
61
+ }
62
+ const out: TranscriptToolUse[] = [];
63
+ for (const line of text.split("\n")) {
64
+ if (!line.trim()) continue;
65
+ let entry: TranscriptLine;
66
+ try {
67
+ entry = JSON.parse(line) as TranscriptLine;
68
+ } catch {
69
+ continue; // tolerate malformed lines
70
+ }
71
+ const content = entry.message?.content;
72
+ if (!Array.isArray(content)) continue;
73
+ const ts = parseTs(entry.timestamp);
74
+ for (const block of content as ToolUseBlock[]) {
75
+ if (block?.type !== "tool_use" || !block.name) continue;
76
+ out.push({ name: block.name, input: block.input, ts });
77
+ }
78
+ }
79
+ return out;
80
+ }