@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,95 @@
1
+ /**
2
+ * Session-track agent evidence — writer (parity track-subagent-research.py) +
3
+ * reader scan (parity apex_agent_helpers._scan_agents). Sub-agent hooks fire
4
+ * with the LEAD's `session_id`, so evidence recorded here lands in the ONE
5
+ * session track the freshness gate scans FIRST — sidechain research and
6
+ * Workflow-spawned agents count, unlike the lead-transcript scan.
7
+ */
8
+ import { classifyExplore } from "./explore-tools";
9
+ import { withTrack } from "../tracking/store";
10
+ import { recordAgent, type AgentQuality, type SessionTrack } from "../tracking/session-state";
11
+ import { isAgentTool } from "../runtime/is-agent-tool";
12
+
13
+ /** One classified PostToolUse call, ready to persist as freshness evidence. */
14
+ export interface AgentEvidence {
15
+ /** Track entry name — `subagent-` + APEX phase (the reader substring-matches). */
16
+ name: "subagent-explore-codebase" | "subagent-research-expert";
17
+ /** `sufficient` when the call hit an MCP cache OR its response JSON is >50 chars. */
18
+ quality: AgentQuality;
19
+ }
20
+
21
+ /**
22
+ * `JSON.stringify` length of the raw `tool_response` OBJECT (parity Python
23
+ * `len(str(tool_response))`); 0 when absent or unserializable (circular).
24
+ */
25
+ function responseJsonLength(toolResponse: unknown): number {
26
+ if (toolResponse === undefined || toolResponse === null) return 0;
27
+ try { return (JSON.stringify(toolResponse) ?? "").length; } catch { return 0; }
28
+ }
29
+
30
+ /**
31
+ * Classify one PostToolUse call into session evidence, or null when it is
32
+ * neither exploration nor research (parity track-subagent-research._classify,
33
+ * reusing the shared {@link classifyExplore} tables — research/explore/Bash/
34
+ * cache-read). ANTI-DOUBLE-COUNT: calls classified as agent launches
35
+ * (`Task`/`Agent`) are skipped — the existing Task tracking credits those; the
36
+ * criterion is the CALL's classification, never its provenance (lead vs sub).
37
+ * @param tool - Harness tool name (e.g. "Glob", "Bash", "Read").
38
+ * @param input - Raw tool input payload.
39
+ * @param toolResponse - Raw `tool_response` OBJECT from the hook payload.
40
+ * @returns The evidence to record, or null to skip.
41
+ */
42
+ export function classifyAgentEvidence(tool: string, input: Record<string, unknown> | undefined, toolResponse: unknown): AgentEvidence | null {
43
+ if (isAgentTool(tool)) return null;
44
+ const hit = classifyExplore(tool, input);
45
+ if (!hit) return null;
46
+ const name = hit.phase === "explore-codebase" ? "subagent-explore-codebase" : "subagent-research-expert";
47
+ return { name, quality: hit.cacheHit || responseJsonLength(toolResponse) > 50 ? "sufficient" : "insufficient" };
48
+ }
49
+
50
+ /**
51
+ * Persist evidence into the SESSION track via {@link recordAgent} — keyed by
52
+ * `session_id` alone (sub-agent hooks carry the lead's `session_id`); the TTL
53
+ * anchors on `ts`, the tool call's own timestamp. `agentId` is tagged as
54
+ * metadata when present, NEVER used as a condition (unreliable field —
55
+ * anthropics/claude-code#22348).
56
+ * @param file - Session track file path.
57
+ * @param evidence - Classified evidence from {@link classifyAgentEvidence}.
58
+ * @param ts - Epoch-ms timestamp of the tool call (the hook event's `now`).
59
+ * @param agentId - Optional Claude `agent_id` — metadata tag only.
60
+ */
61
+ export async function recordAgentEvidence(file: string, evidence: AgentEvidence, ts: number, agentId?: string): Promise<void> {
62
+ await withTrack(file, (track) => {
63
+ const next = recordAgent(track, evidence.name, ts, evidence.quality);
64
+ const last = next.agents[next.agents.length - 1];
65
+ if (agentId && last) {
66
+ const tagged: SessionTrack["agents"][number] & { agentId?: string } = { ...last, agentId };
67
+ next.agents[next.agents.length - 1] = tagged;
68
+ }
69
+ return next;
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Parity `_scan_agents` (apex_agent_helpers.py): reverse-scan `track.agents`,
75
+ * STOPPING at the first entry older than `windowMs` (entries append in time
76
+ * order), matching each required name by SUBSTRING (`research-expert` matches
77
+ * `subagent-research-expert`) and counting ONLY `quality === "sufficient"`.
78
+ * @param track - The loaded session track.
79
+ * @param names - Required agent names — ALL must match to return true.
80
+ * @param windowMs - Freshness window (ms), anchored on each entry's `ts`.
81
+ * @param now - Current epoch ms.
82
+ * @returns True when every required name has fresh, sufficient evidence.
83
+ */
84
+ export function agentsFreshInTrack(track: SessionTrack, names: readonly string[], windowMs: number, now: number): boolean {
85
+ const found = new Set<string>();
86
+ for (let i = track.agents.length - 1; i >= 0; i--) {
87
+ const entry = track.agents[i];
88
+ if (!entry) continue;
89
+ if (now - entry.ts > windowMs) break;
90
+ if (entry.quality !== "sufficient") continue;
91
+ for (const req of names) if (entry.name.includes(req)) found.add(req);
92
+ if (found.size === names.length) return true;
93
+ }
94
+ return names.every((n) => found.has(n));
95
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Platform-authored transcript evidence for APEX agent freshness. Parses the
3
+ * Claude Code session JSONL transcript for genuine dispatch tool_use entries
4
+ * — forging requires writing into the platform-controlled transcript file.
5
+ */
6
+ import { readText } from "../util/runtime-io";
7
+ import { classifyExplore } from "./explore-tools";
8
+ import { isAgentTool } from "../runtime/is-agent-tool";
9
+
10
+ /** Raw shape of one JSONL line in a Claude Code transcript. */
11
+ interface TranscriptLine {
12
+ /** ISO-8601 or epoch-ms timestamp written by the platform. */
13
+ timestamp?: string | number;
14
+ message?: { content?: unknown[] };
15
+ }
16
+
17
+ /** A tool_use content block inside a transcript message. */
18
+ interface ToolUseBlock {
19
+ type: string;
20
+ name?: string;
21
+ input?: Record<string, unknown>;
22
+ }
23
+
24
+ /** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
25
+ function parseTs(raw: string | number | undefined): number | undefined {
26
+ if (raw === undefined) return undefined;
27
+ if (typeof raw === "number") return raw;
28
+ const ms = Date.parse(raw);
29
+ return Number.isFinite(ms) ? ms : undefined;
30
+ }
31
+
32
+ /**
33
+ * Return `true` ONLY when, for EVERY name in `names`, the transcript at
34
+ * `transcriptPath` shows a within-window genuine `tool_use`: a dispatch tool
35
+ * ({@link isAgentTool}) whose `subagent_type`/`name` (plugin prefix stripped)
36
+ * matches, OR a direct exploration/research call classified into that name by
37
+ * {@link classifyExplore} — issued by any sub-agent or the lead (the transcript
38
+ * carries no author field). Entries without `timestamp` count as within-window
39
+ * (format-evolution leniency; tamper-resistance comes from platform authoring).
40
+ * @param transcriptPath - Session `.jsonl` transcript (`transcript_path`); false when undefined.
41
+ * @param names - Required agent `subagent_type` values — ALL must appear.
42
+ * @param windowMs - Freshness window in milliseconds.
43
+ * @param now - Current epoch ms (pass `Date.now()` at the call-site).
44
+ * @returns `true` when ALL agents have real, within-window transcript evidence.
45
+ */
46
+ export function agentsRanFromTranscript(
47
+ transcriptPath: string | undefined,
48
+ names: readonly string[],
49
+ windowMs: number,
50
+ now: number,
51
+ ): boolean {
52
+ if (!transcriptPath || names.length === 0) return false;
53
+ let text: string;
54
+ try {
55
+ text = readText(transcriptPath);
56
+ } catch {
57
+ return false;
58
+ }
59
+ const cutoff = now - windowMs;
60
+ const found = new Set<string>();
61
+ for (const line of text.split("\n")) {
62
+ if (!line.trim()) continue;
63
+ let entry: TranscriptLine;
64
+ try {
65
+ entry = JSON.parse(line) as TranscriptLine;
66
+ } catch {
67
+ continue; // tolerate malformed lines
68
+ }
69
+ const ts = parseTs(entry.timestamp);
70
+ if (ts !== undefined && ts <= cutoff) continue;
71
+ const content = entry.message?.content;
72
+ if (!Array.isArray(content)) continue;
73
+ for (const block of content as ToolUseBlock[]) {
74
+ if (block?.type !== "tool_use") continue;
75
+ if (block.name !== undefined && isAgentTool(block.name)) {
76
+ const raw = block.input?.subagent_type ?? block.input?.name;
77
+ // Strip any plugin prefix (`fuse-ai-pilot:research-expert` → `research-expert`)
78
+ // before matching the bare REQUIRED_AGENTS names.
79
+ const agent = typeof raw === "string" ? raw.split(":").pop() ?? raw : undefined;
80
+ if (agent !== undefined && (names as string[]).includes(agent)) found.add(agent);
81
+ continue;
82
+ }
83
+ // Direct exploration/research tool_use — credited to the matching phase
84
+ // regardless of which sub-agent (or the lead) issued it: the transcript
85
+ // carries no author field distinguishing sidechains, so — mirroring the
86
+ // pre-existing self-recorded-track behavior in `runtime/activity.ts` —
87
+ // ANY Glob/Grep/mcp__context7/mcp__exa/WebSearch/WebFetch/explore-Bash
88
+ // tool_use in this session's transcript counts as freshness evidence.
89
+ const hit = classifyExplore(block.name ?? "", block.input);
90
+ if (hit && (names as string[]).includes(hit.phase)) found.add(hit.phase);
91
+ }
92
+ if (found.size === names.length) return true;
93
+ }
94
+ return names.every((n) => found.has(n));
95
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Codex multi_agent_v2 `spawn_agent` -> session evidence bridge. Codex's custom
3
+ * agents (fusengine multi_agent_v2, e.g. sniper/explore-codebase) are launched
4
+ * via a `spawn_agent` TOOL CALL inside the SAME session -- Codex has no
5
+ * SubagentStart/SubagentStop lifecycle for them, so the existing Claude-side
6
+ * evidence paths (agent-evidence-record.ts `classifyAgentEvidence`, the
7
+ * SubagentStop harvest) never observe them; the harness only ever saw a
8
+ * generic tool call. This module credits the SAME session track
9
+ * ({@link recordAgent}, read back by `agent-evidence-record.ts`
10
+ * `agentsFreshInTrack`) straight from that PostToolUse `spawn_agent` call.
11
+ *
12
+ * SOURCE (openai/codex@44918ea1, tag rust-v0.144.1 -- verified upstream,
13
+ * not re-checked here): the hook `tool_name` is `"spawn_agent"` normally, or
14
+ * the SEPARATOR-LESS namespace-prefixed `{namespace}spawn_agent` when the
15
+ * `namespace_tools` capability provider is active (e.g.
16
+ * `"fusengine_agentsspawn_agent"`, `"collaborationspawn_agent"`). Detection is
17
+ * `tool_name === "spawn_agent" || tool_name.endsWith("spawn_agent")` -- a tool
18
+ * that merely CONTAINS the substring without being an exact/suffix match
19
+ * (`"spawn_agentX"`, `"myspawn_agent_tool"`) is correctly rejected.
20
+ * `tool_input.agent_type` carries the spawned agent's type only when Codex's
21
+ * `hide_spawn_agent_metadata=false`; treat it as always-optional.
22
+ *
23
+ * INVARIANT (multi-harness parity): every entry point here is gated on
24
+ * `id === "codex"` FIRST -- claude-code/cursor/cline/gemini-cli/hermes always
25
+ * take the early return, touching neither the track nor disk. Byte-identical
26
+ * elsewhere by construction.
27
+ */
28
+ import { recordAgent, type SessionTrack } from "../tracking/session-state";
29
+ import { withTrack } from "../tracking/store";
30
+
31
+ /**
32
+ * True when `tool` is Codex's `spawn_agent` primitive, bare or
33
+ * namespace-prefixed (no separator). Rejects any tool that merely contains
34
+ * the substring elsewhere in its name.
35
+ * @param tool - Raw/normalized `tool_name` from the hook payload.
36
+ */
37
+ export function isCodexSpawnAgentTool(tool: string): boolean {
38
+ return tool === "spawn_agent" || tool.endsWith("spawn_agent");
39
+ }
40
+
41
+ /**
42
+ * Extract the spawned agent's type from a `spawn_agent` `tool_input`, or
43
+ * `undefined` when absent/blank (`hide_spawn_agent_metadata=true`, or a
44
+ * future Codex build omitting it) -- never throws.
45
+ * @param input - Raw `tool_input` payload (harness `event.input`).
46
+ */
47
+ export function codexSpawnAgentType(input: Record<string, unknown> | undefined): string | undefined {
48
+ const v = input?.agent_type;
49
+ return typeof v === "string" && v.trim() !== "" ? v.trim() : undefined;
50
+ }
51
+
52
+ /**
53
+ * Pure classifier + folder: credit a Codex `spawn_agent` call into
54
+ * `track.agents` under the SAME `subagent-<name>` convention the Claude-side
55
+ * explore/research evidence uses -- so an `agent_type` that happens to match
56
+ * a `REQUIRED_AGENTS` name (e.g. `"explore-codebase"`) is picked up by
57
+ * `agentsFreshInTrack`'s substring match exactly like genuine Claude
58
+ * Task/Agent evidence. Immutable: returns `track` UNCHANGED (same reference)
59
+ * for any other harness `id`, any non-spawn tool, or a missing `agent_type`.
60
+ * @param id - Harness id (only `"codex"` ever credits).
61
+ * @param tool - Normalized event tool name (raw `tool_name`).
62
+ * @param input - Raw `tool_input` payload.
63
+ * @param track - The current session track.
64
+ * @param ts - Epoch-ms timestamp of the tool call.
65
+ * @returns The credited track, or `track` itself when nothing was credited.
66
+ */
67
+ export function creditCodexSpawnAgent(id: string, tool: string, input: Record<string, unknown> | undefined, track: SessionTrack, ts: number): SessionTrack {
68
+ if (id !== "codex" || !isCodexSpawnAgentTool(tool)) return track;
69
+ const agentType = codexSpawnAgentType(input);
70
+ if (!agentType) return track;
71
+ return recordAgent(track, `subagent-${agentType}`, ts, "sufficient");
72
+ }
73
+
74
+ /**
75
+ * I/O wiring for {@link creditCodexSpawnAgent}: load the session track, fold
76
+ * in the credit, and persist only when something actually changed (skips a
77
+ * write for the strict no-op cases). Mirrors `agent-evidence-record.ts`
78
+ * `recordAgentEvidence`'s async load/save shape.
79
+ * @param file - Session track file path (same one the freshness gate reads).
80
+ * @param id - Harness id (only `"codex"` ever credits).
81
+ * @param tool - Normalized event tool name (raw `tool_name`).
82
+ * @param input - Raw `tool_input` payload.
83
+ * @param ts - Epoch-ms timestamp of the tool call.
84
+ */
85
+ export async function recordCodexSpawnEvidence(file: string, id: string, tool: string, input: Record<string, unknown> | undefined, ts: number): Promise<void> {
86
+ if (id !== "codex" || !isCodexSpawnAgentTool(tool)) return;
87
+ await withTrack(file, (track) => {
88
+ const next = creditCodexSpawnAgent(id, tool, input, track, ts);
89
+ return next !== track ? next : track;
90
+ });
91
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Documentation-consultation freshness: was ANY documentation source consulted
3
+ * this session — Context7, Exa, or WebSearch/WebFetch — via a live call or a
4
+ * Read on a cached MCP result file?
5
+ */
6
+
7
+ /** Authorization entry from APEX state (legacy session + new sessions[]). */
8
+ export interface AuthEntry {
9
+ source?: string;
10
+ sources?: string[];
11
+ sessions?: string[];
12
+ session?: string;
13
+ doc_sessions?: string[];
14
+ read_paths?: string[];
15
+ /** ISO stamp of the latest doc/skill consultation for this framework (Check-1 TTL, parity enforce-apex-phases.ts). */
16
+ doc_consulted?: string;
17
+ }
18
+
19
+ /** Resolve the sessions array from an auth entry (legacy fallback). */
20
+ export function resolveSessions(auth: AuthEntry | undefined): string[] {
21
+ if (!auth) return [];
22
+ return auth.sessions ?? (auth.session ? [auth.session] : []);
23
+ }
24
+
25
+ function sessionAuthsFor(
26
+ authorizations: Record<string, AuthEntry> | undefined,
27
+ sessionId: string,
28
+ ): AuthEntry[] {
29
+ if (!authorizations) return [];
30
+ return Object.values(authorizations).filter((a) => a.doc_sessions?.includes(sessionId));
31
+ }
32
+
33
+ /** Per-source satisfaction status for a session. */
34
+ export interface DocSatisfactionStatus {
35
+ context7: boolean;
36
+ exa: boolean;
37
+ web: boolean;
38
+ viaCache: boolean;
39
+ }
40
+
41
+ function evaluateDoc(auths: AuthEntry[]): DocSatisfactionStatus {
42
+ const sources = auths.flatMap((a) => a.sources ?? [a.source ?? ""]);
43
+ const readPaths = auths.flatMap((a) => a.read_paths ?? []);
44
+ const liveC7 = sources.some((s) => /context7/i.test(s));
45
+ const liveExa = sources.some((s) => /exa/i.test(s));
46
+ const liveWeb = sources.some((s) => /web(search|fetch)|fuse-browser/i.test(s));
47
+ const cacheC7 = readPaths.some((p) => /\/context\/mcp\/context7-/.test(p));
48
+ const cacheExa = readPaths.some((p) => /\/context\/mcp\/(exa-search|exa-code-context)-/.test(p));
49
+ return {
50
+ context7: liveC7 || cacheC7,
51
+ exa: liveExa || cacheExa,
52
+ web: liveWeb,
53
+ viaCache: (!liveC7 && cacheC7) || (!liveExa && cacheExa),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * True when documentation research is satisfied for the session. Primary path
59
+ * is parity with the Python `mcp_research_done`
60
+ * (`_shared/scripts/check_skill_common.py`): `"context7:" in content and
61
+ * "exa:" in content`, a strict AND (the Python source has no web-alone path).
62
+ * `web` (WebSearch/WebFetch/fuse-browser) is a deliberate TS-side addition —
63
+ * it alone satisfies the gate, since fuse-browser is this harness's preferred
64
+ * doc-research fast-path (see CLAUDE.md) and Context7/Exa can be unavailable.
65
+ */
66
+ export function isDocConsulted(
67
+ authorizations: Record<string, AuthEntry> | undefined,
68
+ sessionId: string,
69
+ ): boolean {
70
+ const s = evaluateDoc(sessionAuthsFor(authorizations, sessionId));
71
+ return (s.context7 && s.exa) || s.web;
72
+ }
73
+
74
+ /** Report how each doc source was satisfied for a session. */
75
+ export function formatDocSatisfactionStatus(
76
+ authorizations: Record<string, AuthEntry> | undefined,
77
+ sessionId: string,
78
+ ): DocSatisfactionStatus {
79
+ return evaluateDoc(sessionAuthsFor(authorizations, sessionId));
80
+ }
81
+
82
+ /** Deny message when online documentation has not been consulted. */
83
+ export function formatDocDeny(framework: string): string {
84
+ return [
85
+ `BLOCKED: No MCP research done for ${framework}. Use BOTH:`,
86
+ "1) mcp__context7__query-docs AND 2) mcp__exa__web_search_exa",
87
+ "— OR a web fallback alone: mcp__fuse-browser__browser_fetch / browser_crawl / browser_serp_batch, WebSearch, or WebFetch.",
88
+ "This check is once per session — after consulting the required source(s), Write/Edit will be allowed.",
89
+ ].join("\n");
90
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * SubagentStop wiring for {@link harvestAgentEvidence}. The core SubagentStop
3
+ * dispatch (`runtime/lifecycle/dispatch.ts`) is SYNCHRONOUS and runs in a
4
+ * short-lived hook process, so this does SYNC track I/O (load → harvest → save)
5
+ * rather than the async `loadTrack`/`saveTrack`: a floating async write could be
6
+ * dropped before the process exits. Mirrors the sync read precedent in
7
+ * `policy/design/skill-evidence.ts` and the exact save body of `tracking/store.ts`
8
+ * (`signTrack` → atomic write → `writeLastNonce`) — kept sync on purpose here.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { harvestAgentEvidence } from "./evidence-harvest";
12
+ import { defaultStateDir, trackFile } from "../runtime/paths";
13
+ import { emptyTrack, type SessionTrack } from "../tracking/session-state";
14
+ import { signTrack, verifyTrack, writeLastNonce, type TrackEnvelope } from "../tracking/integrity";
15
+ import { atomicWrite } from "../util/json-io";
16
+ import { LOCK_FAILED, withTrackLockSync } from "../tracking/track-lock-sync";
17
+ import { trackJournalEnabled } from "../tracking/store";
18
+ import { appendEvent } from "../tracking/track-journal";
19
+ import { diffTrackEvents } from "../tracking/track-diff";
20
+ import { journalLogPath, readTrackSync } from "../tracking/track-compact";
21
+ import { dirname } from "node:path";
22
+
23
+ /** Load the current track synchronously; `emptyTrack()` when absent/corrupt (fail-closed read). */
24
+ function loadTrackSync(file: string): SessionTrack {
25
+ try {
26
+ return verifyTrack(JSON.parse(readFileSync(file, "utf8")) as TrackEnvelope) ?? emptyTrack();
27
+ } catch {
28
+ return emptyTrack();
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Harvest the finishing sub-agent's transcript into its session track. Runs for
34
+ * EVERY agent type (unlike the sniper reminder, which skips research/explore) —
35
+ * those are exactly the agents whose research/explore evidence we want to credit.
36
+ * Fully fail-open: a missing `agent_transcript_path`, an unreadable transcript, or
37
+ * a write error leaves the track untouched and never throws out of the hook.
38
+ * @param payload - The raw SubagentStop hook payload.
39
+ * @param cwd - Project root (selects the per-project state dir).
40
+ * @param now - Fallback epoch-ms for unstamped transcript tool_uses.
41
+ * @param baseDir - Override the track base dir (tests); defaults to the project state dir.
42
+ */
43
+ export function harvestSubagentTrack(payload: Record<string, unknown>, cwd: string, now: number, baseDir: string = defaultStateDir(cwd)): void {
44
+ const transcriptPath = typeof payload.agent_transcript_path === "string" ? payload.agent_transcript_path : undefined;
45
+ if (!transcriptPath) return;
46
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
47
+ const file = trackFile(sessionId, baseDir);
48
+ // Journal mode (default): snapshot ⊕ log read, then lock-free signed event
49
+ // appends — same fail-open contract, never a "write skipped" under fan-out.
50
+ if (trackJournalEnabled()) {
51
+ const track = readTrackSync(file, true);
52
+ const next = harvestAgentEvidence(transcriptPath, track, now);
53
+ if (next === track) return; // nothing harvested → no write
54
+ const log = journalLogPath(file);
55
+ for (const ev of diffTrackEvents(track, next, now)) appendEvent(log, ev.field, ev.op, ev.value, ev.ts);
56
+ return;
57
+ }
58
+ // Locked RMW (legacy kill-switch path, sync variant — this path cannot float
59
+ // async work): on contention the harvest is skipped like any other write
60
+ // error, fail-open.
61
+ const ran = withTrackLockSync(dirname(file), () => {
62
+ const track = loadTrackSync(file);
63
+ const next = harvestAgentEvidence(transcriptPath, track, now);
64
+ if (next === track) return; // nothing harvested (or unreadable) → no rewrite
65
+ try {
66
+ const envelope = signTrack(next);
67
+ atomicWrite(file, JSON.stringify(envelope, null, 2));
68
+ writeLastNonce(envelope.nonce);
69
+ } catch { /* fail-open: a lifecycle hook must never throw */ }
70
+ });
71
+ if (ran === LOCK_FAILED) return;
72
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Retroactive evidence harvest at SubagentStop. Parses a completed sub-agent's
3
+ * OWN transcript (`agent_transcript_path`) and backfills the session track with
4
+ * the research/explore tool_use it performed (→ `agents`) and the `.md` reference
5
+ * files it read (→ `refsReadAt`). Per LESSON.md + issues #43612/#27655/#34692,
6
+ * sidechain PostToolUse hooks are NOT guaranteed to fire, so live in-session
7
+ * crediting is only opportunistic; SubagentStop is dispatched by the main session
8
+ * (reliable), and its transcript is the durable anchor this harvest reads.
9
+ *
10
+ * QUALITY: harvested agent evidence is credited `sufficient`. The freshness reader
11
+ * (`agentsFreshInTrack`) ignores `insufficient` entries, so anything less would
12
+ * make the harvest inert. This is sound: the platform-authored transcript proves
13
+ * the tool genuinely ran to completion in a finished sub-agent — a STRONGER
14
+ * guarantee than the live path's response-size heuristic (an anti-gaming measure
15
+ * for the self-recorded track, which the transcript's provenance supersedes).
16
+ */
17
+ import { classifyAgentEvidence } from "./agent-evidence-record";
18
+ import { readAgentToolUses, type TranscriptToolUse } from "../runtime/lifecycle/agent-transcript";
19
+ import { recordAgent, recordRefRead, type SessionTrack } from "../tracking/session-state";
20
+
21
+ /** Dedup tolerance (ms) against evidence a live sidechain hook may already hold. */
22
+ const DEDUP_MS = 2000;
23
+
24
+ /** True when an `agents` entry with `name` already sits within ±{@link DEDUP_MS} of `ts`. */
25
+ function agentAlreadyRecorded(track: SessionTrack, name: string, ts: number): boolean {
26
+ return track.agents.some((a) => a.name === name && Math.abs(a.ts - ts) <= DEDUP_MS);
27
+ }
28
+
29
+ /**
30
+ * Fold one transcript tool_use into the track (immutably): credit classified
31
+ * research/explore evidence to `agents` (deduped, forced `sufficient`) and any
32
+ * `.md` Read to `refsReadAt` (never restamping a MORE-recent existing read).
33
+ * Returns the same reference when the call contributes nothing.
34
+ */
35
+ function applyToolUse(track: SessionTrack, u: TranscriptToolUse, fallbackNow: number): SessionTrack {
36
+ const ts = u.ts ?? fallbackNow;
37
+ let next = track;
38
+ const ev = classifyAgentEvidence(u.name, u.input, undefined);
39
+ if (ev && !agentAlreadyRecorded(next, ev.name, ts)) next = recordAgent(next, ev.name, ts, "sufficient");
40
+ if (u.name === "Read") {
41
+ const path = String(u.input?.file_path ?? u.input?.path ?? "");
42
+ const prev = next.refsReadAt?.[path];
43
+ if (path.endsWith(".md") && (prev === undefined || prev < ts)) next = recordRefRead(next, path, ts);
44
+ }
45
+ return next;
46
+ }
47
+
48
+ /**
49
+ * Backfill `track` from the sub-agent transcript at `transcriptPath`. PURE: the
50
+ * caller owns load/save. Fail-open — an absent or unreadable transcript returns
51
+ * `track` UNCHANGED (same reference), never throwing.
52
+ * @param transcriptPath - Sub-agent's own `.jsonl` (SubagentStop `agent_transcript_path`).
53
+ * @param track - The current session track.
54
+ * @param now - Fallback epoch-ms for tool_uses the platform left unstamped.
55
+ * @returns The backfilled track, or `track` itself when nothing was harvested.
56
+ */
57
+ export function harvestAgentEvidence(transcriptPath: string | undefined, track: SessionTrack, now: number): SessionTrack {
58
+ const uses = readAgentToolUses(transcriptPath);
59
+ if (uses === null) return track; // unreadable → unchanged (no regression)
60
+ return uses.reduce((acc, u) => applyToolUse(acc, u, now), track);
61
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Classifier crediting DIRECT (lead-agent) exploration/research tool use as the
3
+ * matching APEX phase, so the freshness gate's self-recorded fallback accepts
4
+ * genuine direct work — not only `Task`/`Agent` sub-agent spawns. Mirrors the
5
+ * Python core-guards `_shared/apex_constants.py` + `track-subagent-research.py`.
6
+ */
7
+
8
+ /** A REQUIRED_AGENTS name credited for a classified direct tool use. */
9
+ export type ExplorePhase = "explore-codebase" | "research-expert";
10
+
11
+ /** Outcome of classifying one direct tool use. */
12
+ export interface ExploreHit {
13
+ /** APEX phase credited (a REQUIRED_AGENTS name). */
14
+ phase: ExplorePhase;
15
+ /** Reads of a cached MCP result count as `sufficient` regardless of length. */
16
+ cacheHit: boolean;
17
+ }
18
+
19
+ /** Native exploration tools (parity `apex_constants.EXPLORE_TOOLS`). */
20
+ const EXPLORE_TOOLS = new Set(["Glob", "Grep"]);
21
+
22
+ /**
23
+ * Research tools — MCP docs + web (parity `apex_constants.RESEARCH_TOOLS`).
24
+ * The fuse-browser fast-path entries are a deliberate TS addition: CLAUDE.md
25
+ * mandates fuse-browser FIRST for web research, and `docSourceOf` (activity.ts)
26
+ * already credits it for doc consultation — without them here, an agent
27
+ * following the rules would never satisfy the freshness gate (same
28
+ * cross-consumer inconsistency class as the solidReadGate lesson).
29
+ */
30
+ const RESEARCH_TOOLS = new Set([
31
+ "mcp__context7__query-docs",
32
+ "mcp__context7__resolve-library-id",
33
+ "mcp__exa__web_search_exa",
34
+ "mcp__exa__get_code_context_exa",
35
+ "mcp__exa__deep_researcher_start",
36
+ "WebSearch",
37
+ "WebFetch",
38
+ "FetchURL", // Kimi Code CLI's WebFetch-equivalent tool name (docs/reference/tools.html)
39
+ "mcp__fuse-browser__browser_fetch",
40
+ "mcp__fuse-browser__browser_fetch_batch",
41
+ "mcp__fuse-browser__browser_crawl",
42
+ "mcp__fuse-browser__browser_serp_batch",
43
+ ]);
44
+
45
+ /** Bash executables that count as exploration (parity `EXPLORE_BASH_CMDS`). */
46
+ const EXPLORE_BASH_CMDS = new Set(["grep", "rg", "find", "ls", "fd", "ast-grep", "tree", "cat", "head", "tail"]);
47
+
48
+ /** Legacy Python cache names still credited (parity `CACHE_READ_RE` + doc-helpers). */
49
+ const CACHE_READ_RE = /\/context\/mcp\/(exa-search|exa-code-context|context7)-/;
50
+
51
+ /**
52
+ * Real TS cache stores: `<root>/.harness/cache/<fnv16>.md` (core MCP store,
53
+ * `projectLayout().cacheDir`) and `~/.fuse-harness/cache/**` (ai-pilot doc
54
+ * caches, `cache-base.cacheBaseDir`). Segment-matched — not resolved against
55
+ * `homedir()` — so tilde-prefixed and absolute paths both hit; the `.md`
56
+ * suffix keeps session-state JSON reads from counting as research.
57
+ */
58
+ const TS_CACHE_READ_RE = /\.(?:fuse-)?harness[\\/]cache[\\/].*\.md$/;
59
+
60
+ /**
61
+ * First non-assignment shell token's basename, or "" — mirrors Python
62
+ * `_bash_executable` (skips leading `VAR=value` env prefixes, e.g. `FOO=1 grep`).
63
+ * @param cmd - Raw Bash `command` string.
64
+ * @returns The executable basename, or "" when none.
65
+ */
66
+ function bashExecutable(cmd: string): string {
67
+ for (const token of cmd.trim().split(/\s+/)) {
68
+ if (!token) continue;
69
+ const last = token.split("/").pop() ?? token;
70
+ if (!last.includes("=")) return last;
71
+ }
72
+ return "";
73
+ }
74
+
75
+ /**
76
+ * Classify a direct tool use into an APEX phase, or `null` when it is neither
77
+ * exploration nor research. Mirrors Python `track-subagent-research._classify`.
78
+ * @param tool - Harness tool name (e.g. "Glob", "Bash", "WebSearch").
79
+ * @param input - Tool input payload.
80
+ * @returns The credited phase + cache flag, or `null`.
81
+ */
82
+ export function classifyExplore(tool: string, input: Record<string, unknown> | undefined): ExploreHit | null {
83
+ if (RESEARCH_TOOLS.has(tool)) return { phase: "research-expert", cacheHit: false };
84
+ if (EXPLORE_TOOLS.has(tool)) return { phase: "explore-codebase", cacheHit: false };
85
+ if (tool === "Read") {
86
+ const path = String(input?.file_path ?? input?.path ?? "");
87
+ if (path && (CACHE_READ_RE.test(path) || TS_CACHE_READ_RE.test(path))) return { phase: "research-expert", cacheHit: true };
88
+ return null;
89
+ }
90
+ if (tool === "Bash") {
91
+ const cmd = String(input?.command ?? "").trim();
92
+ if (EXPLORE_BASH_CMDS.has(bashExecutable(cmd))) return { phase: "explore-codebase", cacheHit: false };
93
+ }
94
+ return null;
95
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./doc-helpers";
2
+ export * from "./trivial-edit-counter";