@fusengine/harness 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Framework classification for doc consultations, keyed on the QUERY text
3
+ * (parity ai-pilot/scripts/track_doc_helpers.py::detect_framework): an MCP
4
+ * doc call (context7/exa/web) has no filePath, so the file-based
5
+ * `detectFramework()` always yields "generic" — the query keywords are the
6
+ * only framework signal a doc consultation carries.
7
+ */
8
+
9
+ /**
10
+ * Ordered patterns, EXACT parity with the Python list (same order, same
11
+ * case-sensitive alternations, substring search — only `go` is word-bounded).
12
+ * Quirks are inherited deliberately: "javascript" hits `java` (but only after
13
+ * nextjs/react/laravel/swift/tailwind all missed) and all-caps "NEXT" misses.
14
+ * The output domain matches `detectFramework()` exactly, so every credit
15
+ * lands on a key the apex authorization gate actually reads.
16
+ */
17
+ const QUERY_FRAMEWORKS: ReadonlyArray<readonly [RegExp, string]> = [
18
+ [/(next|nextjs|Next)/, "nextjs"],
19
+ [/(react|React)/, "react"],
20
+ [/(laravel|Laravel|php|PHP)/, "laravel"],
21
+ [/(swift|Swift|swiftui|SwiftUI)/, "swift"],
22
+ [/(tailwind|Tailwind)/, "tailwind"],
23
+ [/(java|Java|spring|Spring)/, "java"],
24
+ [/\b(go|Go|golang)\b/, "go"],
25
+ [/(ruby|Ruby|rails|Rails)/, "ruby"],
26
+ [/(rust|Rust|cargo|Cargo)/, "rust"],
27
+ ];
28
+
29
+ /**
30
+ * The framework a doc query targets, or null when no keyword matches (the
31
+ * Python returns "generic"; null here lets the caller fall back to the
32
+ * event's file-detected framework instead of hardcoding generic).
33
+ * @param query - The query/url/prompt text of the doc tool call.
34
+ */
35
+ export function frameworkFromQuery(query: string): string | null {
36
+ for (const [re, fw] of QUERY_FRAMEWORKS) if (re.test(query)) return fw;
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * tool_input fields carrying query text across doc tools: context7
42
+ * (`libraryId`/`libraryName`, parity extract_tool_info), exa/WebSearch
43
+ * (`query`), fuse-browser serp (`queries[]`), WebFetch (`url` + `prompt`).
44
+ */
45
+ const QUERY_FIELDS = ["query", "queries", "libraryId", "libraryName", "prompt", "url"] as const;
46
+
47
+ /**
48
+ * Fold every query-ish tool_input field into one searchable string. Distinct
49
+ * from mcp-key.ts `queryOf`, which picks a SINGLE field to key the response
50
+ * cache — classification wants ALL the text (e.g. WebFetch url + prompt).
51
+ * @param input - The normalized tool_input (absent on some harnesses).
52
+ */
53
+ export function docQueryOf(input: Record<string, unknown> | undefined): string {
54
+ if (!input) return "";
55
+ const parts: string[] = [];
56
+ for (const key of QUERY_FIELDS) {
57
+ const v = input[key];
58
+ if (typeof v === "string") parts.push(v);
59
+ else if (Array.isArray(v)) for (const s of v) if (typeof s === "string") parts.push(s);
60
+ }
61
+ return parts.join(" ");
62
+ }
63
+
64
+ /**
65
+ * The framework to credit for a doc consultation: the query's framework when
66
+ * a keyword matches (query wins over file-based detection), else `fallback`
67
+ * (the event's framework — "generic" for pathless MCP calls).
68
+ * @param input - The doc tool's tool_input.
69
+ * @param fallback - The event's file-detected framework.
70
+ */
71
+ export function docFramework(input: Record<string, unknown> | undefined, fallback: string): string {
72
+ return frameworkFromQuery(docQueryOf(input)) ?? fallback;
73
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Platform-transcript reconciliation for `.md` reference reads — the durable
3
+ * counterpart to {@link agentsRanFromTranscript} for agent freshness.
4
+ *
5
+ * WHY: the session track is persisted by a non-atomic load→mutate→save. Under
6
+ * the multi-plugin hook fan-out (one hook process per installed plugin, ×N) plus
7
+ * back-to-back tool events, concurrent writers clobber each other (lost update).
8
+ * `agents`/`authorizations` self-heal — every explore/research/doc call rewrites
9
+ * them, so a lost update lands again on the next of hundreds of writes — but a
10
+ * `refsRead` entry is written ONCE (when the `.md` is Read), so a single lost
11
+ * update erases it permanently, and the lead has no SubagentStop
12
+ * {@link harvestAgentEvidence} pass to reconcile it (sub-agents do, which is why
13
+ * only the LEAD's solidReadGate never credited). The Claude-authored transcript
14
+ * is append-only and race-immune, so folding its `.md` Reads back into the track
15
+ * restores the lost evidence; each gate still applies its own TTL/session policy.
16
+ */
17
+ import { readAgentToolUses } from "../runtime/lifecycle/agent-transcript";
18
+ import { recordRefRead, type SessionTrack } from "../tracking/session-state";
19
+
20
+ /**
21
+ * Fold every `.md` `Read` in the transcript into `track` as a timestamped ref
22
+ * read (immutably). PURE reconciliation — the caller owns the track; each read
23
+ * is stamped with its transcript timestamp (unstamped → `now`, lenient, matching
24
+ * {@link agentsRanFromTranscript}), and an existing MORE-recent stamp is never
25
+ * rolled back. Fail-open: an absent/unreadable transcript returns `track`
26
+ * unchanged (same reference).
27
+ * @param track - The current (possibly race-damaged) session track.
28
+ * @param transcriptPath - Claude `transcript_path` for this session.
29
+ * @param now - Fallback epoch-ms for transcript entries the platform left unstamped.
30
+ * @returns The track with transcript `.md` reads merged into `refsRead`/`refsReadAt`.
31
+ */
32
+ export function reconcileRefReadsFromTranscript(
33
+ track: SessionTrack,
34
+ transcriptPath: string | undefined,
35
+ now: number,
36
+ ): SessionTrack {
37
+ const uses = readAgentToolUses(transcriptPath);
38
+ if (!uses) return track; // unreadable → unchanged (no regression)
39
+ let next = track;
40
+ for (const u of uses) {
41
+ if (u.name !== "Read") continue;
42
+ const path = String(u.input?.file_path ?? u.input?.path ?? "");
43
+ if (!path.endsWith(".md")) continue;
44
+ const ts = u.ts ?? now;
45
+ const prev = next.refsReadAt?.[path];
46
+ if (prev === undefined || prev < ts) next = recordRefRead(next, path, ts);
47
+ }
48
+ return next;
49
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Append-only journal of `.md` reference reads — the FRESH, race-immune companion
3
+ * to {@link reconcileRefReadsFromTranscript}.
4
+ *
5
+ * WHY a third source (track + transcript were not enough for teammates): the live
6
+ * session track write is a non-atomic load→mutate→save, so under the multi-plugin
7
+ * hook fan-out (one process per plugin, ×N) a lone `refsRead` write is clobbered
8
+ * (lost update). The transcript reconcile recovers the LEAD's lost reads because,
9
+ * by the time the lead edits, its Read has flushed to the platform transcript. But
10
+ * the platform flushes that JSONL to disk with a MULTI-MINUTE lag (measured ~230s,
11
+ * well past the 120s freshness TTL): a background TEAMMATE reads the exact listed
12
+ * ref then edits within seconds — far faster than the flush — so the transcript on
13
+ * disk does NOT yet contain the teammate's read, reconcile misses it, and (its live
14
+ * track write having been lost to the fan-out) solidReadGate blocks despite a
15
+ * genuine read. That is the "teammate solidRead" gap.
16
+ *
17
+ * This journal closes it: every credited `.md` read is appended (one JSON line) the
18
+ * instant PostToolUse fires — O_APPEND is per-write atomic, so concurrent fan-out
19
+ * processes each add their own line and none is lost, and there is no flush lag. The
20
+ * gate folds it back BEFORE any refsRead consumer, alongside the transcript.
21
+ *
22
+ * ANTI-FORGERY: it lives in the out-of-tree state dir the protected-path guard
23
+ * denies agents from writing (SAME boundary as the signed track); the only writer is
24
+ * our PostToolUse on a real `.md` Read. COST: the gate reads it once per edit, bounded
25
+ * by {@link appendRefRead}'s trim cap — cheaper than the multi-MB transcript parse.
26
+ */
27
+ import { appendFileSync, mkdirSync } from "node:fs";
28
+ import { join } from "node:path";
29
+ import { readText } from "../util/runtime-io";
30
+ import { trimLogFile } from "../runtime/fs-cleanup";
31
+ import { recordRefRead, type SessionTrack } from "../tracking/session-state";
32
+
33
+ /** Journal filename inside the per-session state dir. */
34
+ const JOURNAL = "refs-read.log";
35
+
36
+ /** One journalled read: `.md` path plus the tool event's epoch-ms timestamp. */
37
+ interface RefReadLine {
38
+ p: string;
39
+ t: number;
40
+ }
41
+
42
+ /**
43
+ * Append a credited `.md` read to the state-dir journal (O_APPEND, atomic under
44
+ * the fan-out). No-op for non-`.md` paths (parity with the reconcile filter).
45
+ * Fully fail-open: a mkdir/append error is swallowed so recording never blocks the
46
+ * PostToolUse path.
47
+ * @param dir - Per-session state dir (`dirname(trackFile)`).
48
+ * @param path - The read file's absolute path.
49
+ * @param ts - The tool event's epoch-ms timestamp.
50
+ */
51
+ export function appendRefRead(dir: string, path: string, ts: number): void {
52
+ if (!path.endsWith(".md")) return;
53
+ try {
54
+ mkdirSync(dir, { recursive: true });
55
+ const file = join(dir, JOURNAL);
56
+ appendFileSync(file, JSON.stringify({ p: path, t: ts } satisfies RefReadLine) + "\n", "utf-8");
57
+ trimLogFile(file, 128 * 1024, 1_000); // bound the SHARED cross-session log: keep the recent tail (only fresh reads credit a gate)
58
+ } catch {
59
+ /* fail-open: a missing dir or write error must never break recording */
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Fold every `.md` read in the state-dir journal into `track` (immutably), each
65
+ * stamped with its journalled timestamp and never rolling back a MORE-recent
66
+ * existing stamp — identical merge semantics to
67
+ * {@link reconcileRefReadsFromTranscript}, but from the fresh append-only journal
68
+ * instead of the lagged transcript. Fail-open: an absent/unreadable journal returns
69
+ * `track` unchanged (same reference).
70
+ * @param track - The current (possibly race-damaged) session track.
71
+ * @param dir - Per-session state dir (`dirname(trackFile)`).
72
+ * @param now - Fallback epoch-ms for entries with an invalid timestamp.
73
+ * @returns The track with journalled `.md` reads merged into `refsRead`/`refsReadAt`.
74
+ */
75
+ export function reconcileRefReadsFromJournal(track: SessionTrack, dir: string, now: number): SessionTrack {
76
+ let text: string;
77
+ try {
78
+ text = readText(join(dir, JOURNAL));
79
+ } catch {
80
+ return track; // unreadable → unchanged (no regression)
81
+ }
82
+ let next = track;
83
+ for (const line of text.split("\n")) {
84
+ if (!line.trim()) continue;
85
+ let entry: RefReadLine;
86
+ try {
87
+ entry = JSON.parse(line) as RefReadLine;
88
+ } catch {
89
+ continue; // tolerate malformed lines
90
+ }
91
+ const path = typeof entry.p === "string" ? entry.p : "";
92
+ if (!path.endsWith(".md")) continue;
93
+ const ts = typeof entry.t === "number" && Number.isFinite(entry.t) ? entry.t : now;
94
+ const prev = next.refsReadAt?.[path];
95
+ if (prev === undefined || prev < ts) next = recordRefRead(next, path, ts);
96
+ }
97
+ return next;
98
+ }
@@ -0,0 +1,30 @@
1
+ import { dirname } from "node:path";
2
+ import { ensureDir, readJsonFile, writeJsonFile } from "../util/json-io";
3
+
4
+ interface TrivialEditState {
5
+ trivial_edits?: number[];
6
+ [key: string]: unknown;
7
+ }
8
+
9
+ /**
10
+ * Increment a session's trivial-edit counter, evicting timestamps older than
11
+ * `windowMs`. Decoupled + injectable `now` for testability.
12
+ * @param filePath - session state file path
13
+ * @param windowMs - sliding window in ms
14
+ * @param now - current epoch ms (defaults to `Date.now()`)
15
+ * @returns number of trivial edits within the window (including this one)
16
+ */
17
+ export async function incrementTrivialEditCounter(
18
+ filePath: string,
19
+ windowMs: number,
20
+ now: number = Date.now(),
21
+ ): Promise<number> {
22
+ await ensureDir(dirname(filePath));
23
+ const state: TrivialEditState = (await readJsonFile<TrivialEditState>(filePath)) ?? {};
24
+ const cutoff = now - windowMs;
25
+ const edits = (state.trivial_edits ?? []).filter((ts) => ts > cutoff);
26
+ edits.push(now);
27
+ state.trivial_edits = edits;
28
+ await writeJsonFile(filePath, state);
29
+ return edits.length;
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** @fusengine/harness - harness-agnostic toolkit for AI coding agents. */
2
+ export * from "./config";
3
+ export * from "./util";
4
+ export * from "./detect";
5
+ export * from "./policy";
6
+ export * from "./prompt";
7
+ export * from "./memory";
8
+ export * from "./cache";
9
+ export * from "./freshness";
10
+ export * from "./refs";
11
+ export * from "./statusline";
@@ -0,0 +1,2 @@
1
+ export * from "./templates";
2
+ export * from "./run";
@@ -0,0 +1,32 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import type { HarnessId } from "../detect/harness";
4
+ import { claudeInit, clineInit, codexInit, cursorInit, geminiInit, type InitFile } from "./templates";
5
+ import { STATE_GITIGNORE, STATE_ROOT } from "../config/layout";
6
+
7
+ const RUNNERS: Partial<Record<HarnessId, (command: string) => InitFile[]>> = {
8
+ "claude-code": claudeInit,
9
+ codex: codexInit,
10
+ cursor: cursorInit,
11
+ "gemini-cli": geminiInit,
12
+ cline: clineInit,
13
+ };
14
+
15
+ /**
16
+ * Build the wiring file(s) for a harness, or null when it has no hook
17
+ * integration (cli-mode harnesses use `harness check` in a pre-commit step).
18
+ */
19
+ export function initFor(id: HarnessId, command: string = `npx -y @fusengine/harness hook ${id}`): InitFile[] | null {
20
+ const make = RUNNERS[id];
21
+ if (!make) return null;
22
+ return [...make(command), { path: `${STATE_ROOT}/.gitignore`, content: STATE_GITIGNORE }];
23
+ }
24
+
25
+ /** Write an {@link InitFile} under `root` (creates dirs; `chmod +x` when executable). */
26
+ export function writeInitFile(root: string, file: InitFile): string {
27
+ const full = join(root, file.path);
28
+ mkdirSync(dirname(full), { recursive: true });
29
+ writeFileSync(full, file.content);
30
+ if (file.executable) chmodSync(full, 0o755);
31
+ return full;
32
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Wiring file templates per harness, registering `harness hook <id>` for BOTH
3
+ * phases: PRE (gate the edit) and POST (catch-all → `activityFor` filters what
4
+ * to record). Formats verified against each harness's 2026 hook docs.
5
+ */
6
+
7
+ /** A file to write during `harness init`. */
8
+ export interface InitFile {
9
+ path: string;
10
+ content: string;
11
+ executable?: boolean;
12
+ }
13
+
14
+ function json(obj: unknown): string {
15
+ return `${JSON.stringify(obj, null, 2)}\n`;
16
+ }
17
+
18
+ /** Claude Code: `.claude/settings.json` — PreToolUse(Write|Edit) + PostToolUse(all). */
19
+ export function claudeInit(command: string): InitFile[] {
20
+ return [{
21
+ path: ".claude/settings.json",
22
+ content: json({ hooks: {
23
+ PreToolUse: [{ matcher: "Write|Edit|Bash", hooks: [{ type: "command", command }] }],
24
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command }] }],
25
+ } }),
26
+ }];
27
+ }
28
+
29
+ /** Codex CLI: `.codex/hooks.json` (Claude-compatible shape). */
30
+ export function codexInit(command: string): InitFile[] {
31
+ return [{
32
+ path: ".codex/hooks.json",
33
+ content: json({ hooks: {
34
+ PreToolUse: [{ matcher: "Bash|apply_patch", hooks: [{ type: "command", command }] }],
35
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command }] }],
36
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command }] }],
37
+ } }),
38
+ }];
39
+ }
40
+
41
+ /** Cursor: `.cursor/hooks.json` (version 1) — supported pre gates and post observers. */
42
+ export function cursorInit(command: string): InitFile[] {
43
+ return [{
44
+ path: ".cursor/hooks.json",
45
+ content: json({ version: 1, hooks: {
46
+ beforeShellExecution: [{ command, failClosed: true }],
47
+ preToolUse: [{ command, failClosed: true }],
48
+ beforeMCPExecution: [{ command, failClosed: true }],
49
+ beforeReadFile: [{ command, failClosed: true }],
50
+ afterShellExecution: [{ command }],
51
+ postToolUse: [{ command }],
52
+ afterFileEdit: [{ command }],
53
+ } }),
54
+ }];
55
+ }
56
+
57
+ /** Gemini CLI: `.gemini/settings.json` — BeforeTool(edits) + AfterTool(all, regex). */
58
+ export function geminiInit(command: string): InitFile[] {
59
+ return [{
60
+ path: ".gemini/settings.json",
61
+ content: json({ hooks: {
62
+ BeforeTool: [{ matcher: "write_file|edit_file|replace", hooks: [{ type: "command", command, timeout: 30000 }] }],
63
+ AfterTool: [{ matcher: ".*", hooks: [{ type: "command", command, timeout: 30000 }] }],
64
+ } }),
65
+ }];
66
+ }
67
+
68
+ /** Cline: executable `.clinerules/hooks/PreToolUse` + `PostToolUse` piping stdin. */
69
+ export function clineInit(command: string): InitFile[] {
70
+ const script = (phase: string): string => `#!/usr/bin/env bash\n# fuse-harness Cline ${phase} hook\ncat | ${command}\n`;
71
+ return [
72
+ { path: ".clinerules/hooks/PreToolUse", content: script("pre"), executable: true },
73
+ { path: ".clinerules/hooks/PostToolUse", content: script("post"), executable: true },
74
+ ];
75
+ }
@@ -0,0 +1,14 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+
3
+ /** Ensure `<memoryDir>/.gitignore` ignores the machine-local `state.json`. */
4
+ export function ensureMemoryGitignore(memoryDir: string): void {
5
+ const file = `${memoryDir}/.gitignore`;
6
+ try {
7
+ const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
8
+ if (/^state\.json$/m.test(existing)) return;
9
+ const next = existing.trim() ? `${existing.trimEnd()}\nstate.json\n` : "state.json\n";
10
+ writeFileSync(file, next);
11
+ } catch {
12
+ /* non-fatal: memory must never block a session */
13
+ }
14
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./state";
2
+ export * from "./registry";
3
+ export * from "./gitignore";
@@ -0,0 +1,39 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ /** Registry path relative to the home dir. */
5
+ const SUBPATH = ".fuse-harness/cache/lessons/roots.json";
6
+
7
+ /** Absolute path of the global roots registry, or null when home is unusable. */
8
+ export function registryFile(home: string | undefined = process.env.HOME): string | null {
9
+ const h = home?.trim();
10
+ if (!h || !h.startsWith("/")) return null;
11
+ return `${h}/${SUBPATH}`;
12
+ }
13
+
14
+ /** Read the registered project roots (deduplicated string entries only). */
15
+ export function readRoots(home?: string): string[] {
16
+ const file = registryFile(home);
17
+ if (!file) return [];
18
+ try {
19
+ const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
20
+ return Array.isArray(parsed) ? parsed.filter((e): e is string => typeof e === "string") : [];
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ /** Register a project root once (deduplicated, read-modify-write, non-throwing). */
27
+ export function addRoot(root: string, home?: string): void {
28
+ const file = registryFile(home);
29
+ if (!file) return;
30
+ try {
31
+ const roots = new Set(readRoots(home));
32
+ if (roots.has(root)) return;
33
+ roots.add(root);
34
+ mkdirSync(dirname(file), { recursive: true });
35
+ writeFileSync(file, JSON.stringify([...roots]));
36
+ } catch {
37
+ /* non-fatal: a missed root never blocks the session */
38
+ }
39
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Session-scoped lessons roots registry. The flat {@link module:memory/registry}
3
+ * keeps ONE global list of pending roots — correct mono-session, but wrong with
4
+ * several concurrent Claude Code sessions: at Stop, one session lists (and, by
5
+ * bumping the throttle, STEALS) another session's pending lesson on a project it
6
+ * never touched. This registry keys "which project got code edits, and was its
7
+ * Stop reminder already fired" by `session_id`, so each Stop sees and consumes
8
+ * ONLY its own roots. Stored at `$HOME/.fuse-harness/cache/lessons/session-roots.json`;
9
+ * non-fatal on any I/O failure (a missed reminder never blocks a session).
10
+ */
11
+ import { mkdirSync, readFileSync } from "node:fs";
12
+ import { dirname } from "node:path";
13
+ import { atomicWrite } from "../util/json-io";
14
+
15
+ /** Per-(session, root) throttle: last code edit vs. last reminder emitted. */
16
+ export interface RootMark {
17
+ editedAt: number;
18
+ remindedAt: number;
19
+ }
20
+ /** One session's edited roots plus an `updatedAt` purge cursor. */
21
+ interface SessionEntry {
22
+ updatedAt: number;
23
+ roots: Record<string, RootMark>;
24
+ }
25
+ /** Whole registry keyed by sanitized `session_id`. */
26
+ type Registry = Record<string, SessionEntry>;
27
+
28
+ /** Registry path (rel. home) + stale-bucket purge horizon (bounds growth). */
29
+ const SUBPATH = ".fuse-harness/cache/lessons/session-roots.json";
30
+ const PURGE_MS = 7 * 24 * 60 * 60 * 1000;
31
+
32
+ /** Absolute registry path, or null when home is unusable. */
33
+ function file(home: string | undefined): string | null {
34
+ const h = home?.trim();
35
+ return h && h.startsWith("/") ? `${h}/${SUBPATH}` : null;
36
+ }
37
+
38
+ /** Read the registry; missing/corrupt/legacy (array) shapes collapse to `{}`. */
39
+ function read(home: string | undefined): Registry {
40
+ const f = file(home);
41
+ if (!f) return {};
42
+ try {
43
+ const parsed: unknown = JSON.parse(readFileSync(f, "utf8"));
44
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Registry) : {};
45
+ } catch {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ /** Purge stale buckets, then atomically persist (unique tmp + rename). Non-throwing. */
51
+ function write(home: string | undefined, reg: Registry, now: number): void {
52
+ const f = file(home);
53
+ if (!f) return;
54
+ for (const [sid, entry] of Object.entries(reg)) {
55
+ if (!entry || now - (entry.updatedAt ?? 0) > PURGE_MS) delete reg[sid];
56
+ }
57
+ try {
58
+ mkdirSync(dirname(f), { recursive: true });
59
+ atomicWrite(f, JSON.stringify(reg));
60
+ } catch {
61
+ /* non-fatal: a missed root never blocks the session */
62
+ }
63
+ }
64
+
65
+ /** Record `field` for `(sid, root)`, refreshing the purge cursor. `home` defaults to `$HOME`. */
66
+ export function markSessionRoot(
67
+ sid: string, root: string, field: keyof RootMark, value: number, home: string | undefined = process.env.HOME,
68
+ ): void {
69
+ const reg = read(home);
70
+ const prev = reg[sid];
71
+ const entry: SessionEntry = prev && typeof prev.roots === "object" && prev.roots !== null ? prev : { updatedAt: value, roots: {} };
72
+ const mark = entry.roots[root] ?? { editedAt: 0, remindedAt: 0 };
73
+ entry.roots[root] = { ...mark, [field]: value };
74
+ entry.updatedAt = value;
75
+ reg[sid] = entry;
76
+ write(home, reg, value);
77
+ }
78
+
79
+ /**
80
+ * Roots of `sid` with an unsaved code edit past the `window`; each returned
81
+ * root's `remindedAt` is bumped to `now` so the reminder fires at most once per
82
+ * window and is consumed ONLY by this session. `home` defaults to `$HOME`.
83
+ */
84
+ export function collectSessionPending(
85
+ sid: string, now: number, window: number, home: string | undefined = process.env.HOME,
86
+ ): string[] {
87
+ const reg = read(home);
88
+ const entry = reg[sid];
89
+ if (!entry || typeof entry.roots !== "object" || entry.roots === null) return [];
90
+ const pending: string[] = [];
91
+ for (const [root, mark] of Object.entries(entry.roots)) {
92
+ if (mark.editedAt <= mark.remindedAt) continue;
93
+ if (now - mark.remindedAt < window) continue;
94
+ pending.push(root);
95
+ entry.roots[root] = { ...mark, remindedAt: now };
96
+ }
97
+ if (pending.length > 0) write(home, reg, now);
98
+ return pending;
99
+ }
@@ -0,0 +1,58 @@
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { atomicWrite } from "../util/json-io";
4
+ import { projectLayout } from "../config/layout";
5
+ import { ensureMemoryGitignore } from "./gitignore";
6
+
7
+ /** Per-project reminder throttle state, stored at `<root>/MEMORY/state.json`. */
8
+ export interface ReminderState {
9
+ lastRemindedAt: number;
10
+ lastCodeEditAt: number;
11
+ }
12
+
13
+ /** Absolute path of the per-project throttle state file. */
14
+ export function stateFileFor(root: string): string {
15
+ return projectLayout(root).memoryStateFile;
16
+ }
17
+
18
+ /** Absolute path of the per-project curated lessons file (committable). */
19
+ export function lessonsFileFor(root: string): string {
20
+ return projectLayout(root).lessonsFile;
21
+ }
22
+
23
+ function num(v: unknown): number {
24
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
25
+ }
26
+
27
+ /** Read both throttle timestamps; missing/corrupt fields default to 0. */
28
+ export function readState(file: string): ReminderState {
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as Partial<ReminderState>;
31
+ return { lastRemindedAt: num(parsed?.lastRemindedAt), lastCodeEditAt: num(parsed?.lastCodeEditAt) };
32
+ } catch {
33
+ return { lastRemindedAt: 0, lastCodeEditAt: 0 };
34
+ }
35
+ }
36
+
37
+ /** Persist one field without clobbering the other (read-modify-write). */
38
+ export function setStateField(file: string, key: keyof ReminderState, value: number): void {
39
+ const next: ReminderState = { ...readState(file), [key]: value };
40
+ const dir = dirname(file);
41
+ mkdirSync(dir, { recursive: true });
42
+ ensureMemoryGitignore(dir);
43
+ atomicWrite(file, JSON.stringify(next));
44
+ }
45
+
46
+ /** Local wall-clock timestamp for a lesson bullet: `YYYY-MM-DD HH:MM`. */
47
+ export function nowStamp(): string {
48
+ const d = new Date();
49
+ const p = (n: number): string => String(n).padStart(2, "0");
50
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
51
+ }
52
+
53
+ /** Throttle window (ms) from `FUSE_LESSONS_THROTTLE_MIN` (default 5 min). */
54
+ export function throttleMs(env: Record<string, string | undefined> = process.env): number {
55
+ const raw = env.FUSE_LESSONS_THROTTLE_MIN?.trim();
56
+ const min = raw ? Number(raw) : 5;
57
+ return (Number.isFinite(min) ? Math.max(0, min) : 5) * 60_000;
58
+ }