@fusengine/harness 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +5 -4
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-BjG6unTj.mjs +317 -0
  28. package/dist/permission-mode-B-SFyR0X.mjs +226 -0
  29. package/dist/policy/index.d.mts +1 -1
  30. package/dist/policy/index.mjs +2 -2
  31. package/dist/prompt/index.d.mts +1 -1
  32. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  33. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  34. package/dist/runtime/index.d.mts +27 -16
  35. package/dist/runtime/index.mjs +2 -2
  36. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  37. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  38. package/package.json +3 -2
  39. package/src/adapters/claude/index.ts +92 -0
  40. package/src/adapters/cline/index.ts +36 -0
  41. package/src/adapters/codex/apply-patch.ts +94 -0
  42. package/src/adapters/codex/index.ts +84 -0
  43. package/src/adapters/codex/permission-mode.ts +16 -0
  44. package/src/adapters/cursor/context.ts +46 -0
  45. package/src/adapters/cursor/events.ts +38 -0
  46. package/src/adapters/cursor/index.ts +43 -0
  47. package/src/adapters/cursor/interfaces/types.ts +88 -0
  48. package/src/adapters/cursor/native-response.ts +158 -0
  49. package/src/adapters/cursor/normalize.ts +123 -0
  50. package/src/adapters/cursor/respond.ts +123 -0
  51. package/src/adapters/gemini/index.ts +36 -0
  52. package/src/adapters/hermes/index.ts +55 -0
  53. package/src/adapters/hermes/interfaces/types.ts +26 -0
  54. package/src/adapters/kimi/index.ts +96 -0
  55. package/src/adapters/kimi/interfaces/types.ts +55 -0
  56. package/src/cache/compact.ts +55 -0
  57. package/src/cache/index.ts +5 -0
  58. package/src/cache/io.ts +38 -0
  59. package/src/cache/mcp-response.ts +39 -0
  60. package/src/cache/mcp-store.ts +70 -0
  61. package/src/cache/store.ts +93 -0
  62. package/src/changelog/fetch.ts +79 -0
  63. package/src/cli/bin.ts +100 -0
  64. package/src/cli/cursor-event-scanner.ts +175 -0
  65. package/src/cli/cursor-stdin-reader.ts +65 -0
  66. package/src/cli/doctor.ts +96 -0
  67. package/src/cli/hook-io.ts +130 -0
  68. package/src/cli/hook-sound.ts +49 -0
  69. package/src/cli/index.ts +1 -0
  70. package/src/cli/json-primitive-scanner.ts +83 -0
  71. package/src/cli/run.ts +36 -0
  72. package/src/cli/scope.ts +31 -0
  73. package/src/codex-rules/build.ts +63 -0
  74. package/src/codex-rules/index.ts +8 -0
  75. package/src/codex-rules/mapping.ts +42 -0
  76. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  77. package/src/codex-rules/rules/ask.ts +44 -0
  78. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  79. package/src/codex-rules/rules/dd.ts +23 -0
  80. package/src/codex-rules/rules/diskutil.ts +33 -0
  81. package/src/codex-rules/rules/mkfs.ts +42 -0
  82. package/src/codex-rules/rules/privilege.ts +49 -0
  83. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  84. package/src/codex-rules/rules/rm-variants.ts +58 -0
  85. package/src/codex-rules/rules/single-commands.ts +57 -0
  86. package/src/codex-rules/skip-list.ts +25 -0
  87. package/src/codex-rules/starlark.ts +46 -0
  88. package/src/codex-rules/types.ts +36 -0
  89. package/src/config/dotenv.ts +55 -0
  90. package/src/config/env.ts +10 -0
  91. package/src/config/home-dir.ts +36 -0
  92. package/src/config/index.ts +5 -0
  93. package/src/config/layout.ts +52 -0
  94. package/src/config/limits.ts +56 -0
  95. package/src/config/ttl.ts +30 -0
  96. package/src/detect/harness.ts +73 -0
  97. package/src/detect/index.ts +1 -0
  98. package/src/detect/interfaces/types.ts +18 -0
  99. package/src/freshness/agent-evidence-record.ts +95 -0
  100. package/src/freshness/agent-evidence.ts +95 -0
  101. package/src/freshness/codex-spawn-evidence.ts +91 -0
  102. package/src/freshness/doc-helpers.ts +90 -0
  103. package/src/freshness/evidence-harvest-io.ts +72 -0
  104. package/src/freshness/evidence-harvest.ts +61 -0
  105. package/src/freshness/explore-tools.ts +95 -0
  106. package/src/freshness/index.ts +2 -0
  107. package/src/freshness/query-framework.ts +73 -0
  108. package/src/freshness/ref-evidence.ts +49 -0
  109. package/src/freshness/ref-journal.ts +98 -0
  110. package/src/freshness/trivial-edit-counter.ts +30 -0
  111. package/src/index.ts +11 -0
  112. package/src/init/index.ts +2 -0
  113. package/src/init/run.ts +32 -0
  114. package/src/init/templates.ts +75 -0
  115. package/src/memory/gitignore.ts +14 -0
  116. package/src/memory/index.ts +3 -0
  117. package/src/memory/registry.ts +39 -0
  118. package/src/memory/session-roots.ts +99 -0
  119. package/src/memory/state.ts +58 -0
  120. package/src/policy/apex-authorization.ts +100 -0
  121. package/src/policy/apex-gates.ts +91 -0
  122. package/src/policy/apex-target-steps.ts +73 -0
  123. package/src/policy/apex-target.ts +49 -0
  124. package/src/policy/apex-task-context.ts +81 -0
  125. package/src/policy/apex.ts +94 -0
  126. package/src/policy/cartographer/build-tree.ts +69 -0
  127. package/src/policy/cartographer/describe.ts +53 -0
  128. package/src/policy/cartographer/entry.ts +38 -0
  129. package/src/policy/cartographer/frontmatter.ts +48 -0
  130. package/src/policy/cartographer/indicators.ts +34 -0
  131. package/src/policy/claude-md-context.ts +66 -0
  132. package/src/policy/conventions/index.ts +11 -0
  133. package/src/policy/conventions/interfaces.ts +71 -0
  134. package/src/policy/conventions/langs.ts +53 -0
  135. package/src/policy/conventions/query.ts +49 -0
  136. package/src/policy/conventions/react-hooks.ts +18 -0
  137. package/src/policy/conventions/store-or-query.ts +21 -0
  138. package/src/policy/conventions/stores.ts +56 -0
  139. package/src/policy/conventions/strip.ts +87 -0
  140. package/src/policy/conventions/verdict.ts +38 -0
  141. package/src/policy/creation-intent.ts +11 -0
  142. package/src/policy/deny-loop.ts +88 -0
  143. package/src/policy/design/allowed-write.ts +28 -0
  144. package/src/policy/design/content-checks.ts +45 -0
  145. package/src/policy/design/corpus-fs.ts +21 -0
  146. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  147. package/src/policy/design/corpus-resolve.ts +99 -0
  148. package/src/policy/design/corpus.ts +92 -0
  149. package/src/policy/design/design-system-rules.ts +45 -0
  150. package/src/policy/design/flag.ts +30 -0
  151. package/src/policy/design/gates-pipeline.ts +105 -0
  152. package/src/policy/design/gates.ts +87 -0
  153. package/src/policy/design/screenshot-tools.ts +24 -0
  154. package/src/policy/design/skill-evidence.ts +34 -0
  155. package/src/policy/design/skill-gate.ts +81 -0
  156. package/src/policy/design/skill-triggers.ts +51 -0
  157. package/src/policy/design/state.ts +92 -0
  158. package/src/policy/design/template-urls.ts +54 -0
  159. package/src/policy/design/transitions.ts +68 -0
  160. package/src/policy/detect-claude-md-project-type.ts +32 -0
  161. package/src/policy/detect-framework.ts +99 -0
  162. package/src/policy/detect-primitive-checks.ts +100 -0
  163. package/src/policy/detect-primitive-lib.ts +55 -0
  164. package/src/policy/detect-project.ts +99 -0
  165. package/src/policy/edit-outcome.ts +53 -0
  166. package/src/policy/evaluate.ts +81 -0
  167. package/src/policy/expert-agents.ts +60 -0
  168. package/src/policy/file-size-scope.ts +32 -0
  169. package/src/policy/file-size.ts +99 -0
  170. package/src/policy/framework-solid-exclude.ts +52 -0
  171. package/src/policy/framework-solid-extended.ts +93 -0
  172. package/src/policy/framework-solid-gates-systems.ts +99 -0
  173. package/src/policy/framework-solid-gates.ts +98 -0
  174. package/src/policy/framework-solid.ts +88 -0
  175. package/src/policy/gemini-mcp-gate.ts +83 -0
  176. package/src/policy/git-gates.ts +40 -0
  177. package/src/policy/guards/bash-command-anchor.ts +40 -0
  178. package/src/policy/guards/bash-write-patterns.ts +140 -0
  179. package/src/policy/guards/bash-write-redirects.ts +201 -0
  180. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  181. package/src/policy/guards/bash-write.ts +76 -0
  182. package/src/policy/guards/context.ts +12 -0
  183. package/src/policy/guards/index.ts +61 -0
  184. package/src/policy/guards/install.ts +22 -0
  185. package/src/policy/guards/interface-separation-ext.ts +41 -0
  186. package/src/policy/guards/interface-separation.ts +100 -0
  187. package/src/policy/guards/protected-path.ts +100 -0
  188. package/src/policy/guards/security.ts +97 -0
  189. package/src/policy/index.ts +19 -0
  190. package/src/policy/interfaces/types.ts +46 -0
  191. package/src/policy/js-gate-route.ts +51 -0
  192. package/src/policy/lessons/lesson-gate.ts +0 -0
  193. package/src/policy/lessons/trigger-index.ts +84 -0
  194. package/src/policy/lessons/types.ts +31 -0
  195. package/src/policy/module-layout.ts +31 -0
  196. package/src/policy/nearest-manifest.ts +75 -0
  197. package/src/policy/never-approval.ts +95 -0
  198. package/src/policy/patterns.ts +80 -0
  199. package/src/policy/seo/validate.ts +51 -0
  200. package/src/policy/shadcn-project.ts +22 -0
  201. package/src/policy/shadcn-skill-gate.ts +97 -0
  202. package/src/policy/shell-read-refs.ts +96 -0
  203. package/src/policy/skill-path.ts +46 -0
  204. package/src/policy/skill-patterns/laravel.ts +40 -0
  205. package/src/policy/skill-patterns/nextjs.ts +29 -0
  206. package/src/policy/skill-patterns/react.ts +27 -0
  207. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  208. package/src/policy/skill-patterns/shadcn.ts +66 -0
  209. package/src/policy/skill-patterns/swift.ts +30 -0
  210. package/src/policy/skill-patterns/tailwind.ts +40 -0
  211. package/src/policy/skill-source.ts +52 -0
  212. package/src/policy/skill-trigger-patterns.ts +33 -0
  213. package/src/policy/skill-triggers.ts +108 -0
  214. package/src/policy/tailwind-skill-gate.ts +63 -0
  215. package/src/policy/trivial-edits.ts +52 -0
  216. package/src/policy/verbosity.ts +39 -0
  217. package/src/prompt/index.ts +1 -0
  218. package/src/prompt/types.ts +36 -0
  219. package/src/refs/discover.ts +69 -0
  220. package/src/refs/frontmatter.ts +24 -0
  221. package/src/refs/index.ts +4 -0
  222. package/src/refs/loader.ts +59 -0
  223. package/src/refs/ref-key.ts +41 -0
  224. package/src/refs/router.ts +63 -0
  225. package/src/refs/types.ts +25 -0
  226. package/src/runtime/activity.ts +99 -0
  227. package/src/runtime/apply-patch-gate.ts +41 -0
  228. package/src/runtime/burst-window.ts +21 -0
  229. package/src/runtime/codex-shell-tool.ts +37 -0
  230. package/src/runtime/command-string.ts +37 -0
  231. package/src/runtime/confirm/codex-confirm.ts +126 -0
  232. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  233. package/src/runtime/confirm/confirm-code.ts +33 -0
  234. package/src/runtime/confirm/confirm-gate.ts +65 -0
  235. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  236. package/src/runtime/confirm/confirm-pending.ts +34 -0
  237. package/src/runtime/confirm/confirm-state.ts +56 -0
  238. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  239. package/src/runtime/confirm/confirm-submit.ts +38 -0
  240. package/src/runtime/core-guards-active.ts +84 -0
  241. package/src/runtime/core-guards-wired.ts +47 -0
  242. package/src/runtime/deny-loop-store.ts +94 -0
  243. package/src/runtime/deny-notice.ts +52 -0
  244. package/src/runtime/design-cache-resolve.ts +51 -0
  245. package/src/runtime/design-content-gate.ts +95 -0
  246. package/src/runtime/design-files-gate.ts +114 -0
  247. package/src/runtime/design-helpers.ts +93 -0
  248. package/src/runtime/design-lifecycle.ts +39 -0
  249. package/src/runtime/design-read-credit.ts +28 -0
  250. package/src/runtime/design.ts +116 -0
  251. package/src/runtime/dev-context.ts +43 -0
  252. package/src/runtime/dry-patterns.ts +32 -0
  253. package/src/runtime/dry.ts +96 -0
  254. package/src/runtime/fragment-registry.ts +33 -0
  255. package/src/runtime/framework-skill-gate.ts +79 -0
  256. package/src/runtime/fs-cleanup.ts +71 -0
  257. package/src/runtime/gate-apex.ts +81 -0
  258. package/src/runtime/gate-helpers.ts +52 -0
  259. package/src/runtime/gate-input.ts +26 -0
  260. package/src/runtime/gate.ts +128 -0
  261. package/src/runtime/handle-post.ts +104 -0
  262. package/src/runtime/handle-pre.ts +137 -0
  263. package/src/runtime/handle-scope-async.ts +29 -0
  264. package/src/runtime/handle-types.ts +27 -0
  265. package/src/runtime/handle.ts +116 -0
  266. package/src/runtime/home-state.ts +55 -0
  267. package/src/runtime/index.ts +18 -0
  268. package/src/runtime/inform.ts +28 -0
  269. package/src/runtime/inject-budget-recap.ts +47 -0
  270. package/src/runtime/inject-budget.ts +81 -0
  271. package/src/runtime/inject-context.ts +85 -0
  272. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  273. package/src/runtime/inject-dedup.ts +89 -0
  274. package/src/runtime/is-agent-tool.ts +19 -0
  275. package/src/runtime/lifecycle/agent-files.ts +49 -0
  276. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  277. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  278. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  279. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  280. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  281. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  282. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  283. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  284. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  285. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  287. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  288. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  289. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  290. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  291. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  294. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  295. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  296. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  297. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  298. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  299. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  300. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  301. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  302. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  303. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  304. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  305. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  306. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  307. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  308. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  309. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  310. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  311. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  312. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  313. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  314. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  315. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  316. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  317. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  318. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  319. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  320. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  321. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  323. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  324. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  325. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  326. package/src/runtime/lifecycle/dispatch.ts +109 -0
  327. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  328. package/src/runtime/lifecycle/index.ts +34 -0
  329. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  330. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  331. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  332. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  333. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  334. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  335. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  336. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  337. package/src/runtime/lifecycle/memory/client.ts +65 -0
  338. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  339. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  340. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  341. package/src/runtime/lifecycle/memory/state.ts +49 -0
  342. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  343. package/src/runtime/lifecycle/post-compact.ts +38 -0
  344. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  345. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  346. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  347. package/src/runtime/lifecycle/rules-root.ts +83 -0
  348. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  349. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  350. package/src/runtime/lifecycle/security/scan.ts +76 -0
  351. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  352. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  353. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  354. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  355. package/src/runtime/lifecycle/session-end.ts +17 -0
  356. package/src/runtime/lifecycle/session-start.ts +57 -0
  357. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  358. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  359. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  360. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  361. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  362. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  363. package/src/runtime/lifecycle/stop-core.ts +35 -0
  364. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  365. package/src/runtime/lifecycle/task-completed.ts +94 -0
  366. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  367. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  368. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  369. package/src/runtime/lifecycle/track-changes.ts +61 -0
  370. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  371. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  372. package/src/runtime/lifecycle-bridge.ts +74 -0
  373. package/src/runtime/mcp-key.ts +44 -0
  374. package/src/runtime/mcp-tool-name.ts +92 -0
  375. package/src/runtime/mcp.ts +91 -0
  376. package/src/runtime/modular.ts +77 -0
  377. package/src/runtime/normalize.ts +107 -0
  378. package/src/runtime/notices.ts +91 -0
  379. package/src/runtime/notification-sound.ts +49 -0
  380. package/src/runtime/notifications.ts +77 -0
  381. package/src/runtime/paths.ts +59 -0
  382. package/src/runtime/post-fanout.ts +41 -0
  383. package/src/runtime/post-outcome.ts +61 -0
  384. package/src/runtime/pre-allow.ts +97 -0
  385. package/src/runtime/precommit.ts +60 -0
  386. package/src/runtime/prompt-text.ts +38 -0
  387. package/src/runtime/receipt-capture.ts +34 -0
  388. package/src/runtime/record.ts +26 -0
  389. package/src/runtime/respond.ts +92 -0
  390. package/src/runtime/solid-file-size-gate.ts +58 -0
  391. package/src/runtime/solid-pre.ts +53 -0
  392. package/src/runtime/storage.ts +9 -0
  393. package/src/statusline/colors.ts +31 -0
  394. package/src/statusline/constants.ts +17 -0
  395. package/src/statusline/formatters.ts +54 -0
  396. package/src/statusline/index.ts +4 -0
  397. package/src/statusline/progress-bar.ts +46 -0
  398. package/src/tracking/codex-post-failure.ts +73 -0
  399. package/src/tracking/index.ts +2 -0
  400. package/src/tracking/integrity.ts +99 -0
  401. package/src/tracking/one-shot-dedup.ts +34 -0
  402. package/src/tracking/one-shot-failure.ts +42 -0
  403. package/src/tracking/one-shot-store.ts +95 -0
  404. package/src/tracking/one-shot.ts +93 -0
  405. package/src/tracking/receipts.ts +95 -0
  406. package/src/tracking/session-state.ts +94 -0
  407. package/src/tracking/store.ts +96 -0
  408. package/src/tracking/track-compact.ts +100 -0
  409. package/src/tracking/track-diff.ts +50 -0
  410. package/src/tracking/track-journal.ts +97 -0
  411. package/src/tracking/track-lock-sync.ts +83 -0
  412. package/src/tracking/track-lock.ts +67 -0
  413. package/src/util/compact-json.ts +23 -0
  414. package/src/util/index.ts +2 -0
  415. package/src/util/json-io.ts +47 -0
  416. package/src/util/project-root.ts +41 -0
  417. package/src/util/runtime-io.ts +85 -0
  418. package/src/util/semver.ts +30 -0
  419. package/dist/permission-mode-B0MUl7FT.mjs +0 -136
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @module store
3
+ * Session-track persistence — TWO modes behind an env kill-switch:
4
+ * - JOURNAL (default): writes append signed event lines to `track-<sid>.log`
5
+ * (O_APPEND, lock-free — see track-journal.ts), reads fold the log over the
6
+ * legacy `track-<sid>.json` snapshot. No lock on the hot path ⇒ the fan-out
7
+ * can no longer lose a write to "track lock busy, write skipped" — compaction
8
+ * included (it is rename-atomic, see track-compact.ts).
9
+ * - LEGACY (`FUSE_TRACK_JOURNAL=0`): the original locked read-modify-write,
10
+ * kept fully intact below as the instant rollback (G1) — no redeploy needed.
11
+ * NOTE: =0 does NOT honour the uncompacted journal (the track reads as its
12
+ * last snapshot, possibly EMPTY); the events are recoverable by switching
13
+ * back to journal mode — never delete a `.log` during a =0 rollback.
14
+ * The flag is read at CALL time so the switch is live.
15
+ * @packageDocumentation
16
+ */
17
+ import { readJsonFile, writeJsonFile } from "../util/json-io";
18
+ import { emptyTrack, type SessionTrack } from "./session-state";
19
+ import { signTrack, verifyTrack, writeLastNonce, type TrackEnvelope } from "./integrity";
20
+ import { LOCK_FAILED, withTrackLock } from "./track-lock";
21
+ import { dirname } from "node:path";
22
+ import { appendEvent, foldEvents } from "./track-journal";
23
+ import { diffTrackEvents } from "./track-diff";
24
+ import { journalLogPath, maybeCompactJournal, readEvents } from "./track-compact";
25
+
26
+ /** Journal mode is the default; `FUSE_TRACK_JOURNAL=0` reverts to the legacy locked RMW (see the module NOTE: =0 ignores the uncompacted journal — recoverable, keep the `.log`). */
27
+ export function trackJournalEnabled(): boolean {
28
+ return process.env.FUSE_TRACK_JOURNAL !== "0";
29
+ }
30
+
31
+ /** Verified legacy snapshot (fail closed: absent/corrupt/tampered → emptyTrack). */
32
+ async function loadSnapshot(file: string): Promise<SessionTrack> {
33
+ const envelope = await readJsonFile<TrackEnvelope>(file);
34
+ if (!envelope) return emptyTrack();
35
+ return verifyTrack(envelope) ?? emptyTrack();
36
+ }
37
+
38
+ /**
39
+ * Load a session track: the verified legacy snapshot (the migration base — an
40
+ * old `track-<sid>.json` with no log reads exactly as before) with the journal
41
+ * folded on top. Fail closed on tampering (envelope MAC, per-line MAC).
42
+ */
43
+ export async function loadTrack(file: string): Promise<SessionTrack> {
44
+ const base = await loadSnapshot(file);
45
+ if (!trackJournalEnabled()) return base;
46
+ return foldEvents(readEvents(journalLogPath(file)), base);
47
+ }
48
+
49
+ /** Journal-mode write: append the mutation's events, then maybe compact. */
50
+ async function appendDiff(file: string, prev: SessionTrack, next: SessionTrack): Promise<boolean> {
51
+ const log = journalLogPath(file);
52
+ let ok = true;
53
+ for (const ev of diffTrackEvents(prev, next, Date.now())) ok = appendEvent(log, ev.field, ev.op, ev.value, ev.ts) && ok;
54
+ await maybeCompactJournal(file);
55
+ return ok;
56
+ }
57
+
58
+ /**
59
+ * Persist a session track. Journal mode appends the diff as signed events;
60
+ * legacy mode signs and persists the tamper-evident envelope, then writes the
61
+ * advisory nonce watermark.
62
+ */
63
+ export async function saveTrack(file: string, track: SessionTrack): Promise<void> {
64
+ if (trackJournalEnabled()) {
65
+ await appendDiff(file, await loadTrack(file), track);
66
+ return;
67
+ }
68
+ const envelope = signTrack(track);
69
+ await writeJsonFile(file, envelope);
70
+ writeLastNonce(envelope.nonce);
71
+ }
72
+
73
+ /**
74
+ * Mutate a session track. Journal mode: load (snapshot ⊕ log), run `mutate`,
75
+ * append the resulting events — lock-free, so a write is only ever lost to a
76
+ * genuine I/O error (reported as `false`). Legacy mode: locked RMW; on lock
77
+ * contention the write is SKIPPED (fail-open, stderr logged) and `false` is
78
+ * returned — a named lost write, never a crash.
79
+ * @param file - The session track file.
80
+ * @param mutate - Pure mutation over the freshly loaded track.
81
+ * @returns True when the write landed.
82
+ */
83
+ export async function withTrack(
84
+ file: string,
85
+ mutate: (track: SessionTrack) => SessionTrack | Promise<SessionTrack>,
86
+ ): Promise<boolean> {
87
+ if (trackJournalEnabled()) {
88
+ const track = await loadTrack(file);
89
+ return appendDiff(file, track, await mutate(track));
90
+ }
91
+ const ran = await withTrackLock(dirname(file), async () => {
92
+ const track = await loadTrack(file);
93
+ await saveTrack(file, await mutate(track));
94
+ });
95
+ return ran !== LOCK_FAILED;
96
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @module track-compact
3
+ * Read side + compaction for the track journal (see track-journal.ts):
4
+ * - {@link parseEvents}/{@link readEvents}: MAC-verified replay — a tampered
5
+ * line is dropped, never the whole file (fail-closed PER LINE);
6
+ * - {@link readTrackSync}: the sync snapshot ⊕ journal read for the sync gates;
7
+ * - {@link maybeCompactJournal}: past {@link COMPACT_BYTES}, fold the log into
8
+ * the signed snapshot — RENAME-ATOMIC (logrotate pattern) under the EXISTING
9
+ * track lock (rare, skipped on contention). Lock-free appends stay SAFE in
10
+ * both race windows: before the rename → captured by the fold; after → a
11
+ * fresh log at the original path (appendFileSync keeps no fd across calls).
12
+ * @packageDocumentation
13
+ */
14
+ import { existsSync, readFileSync, renameSync, statSync, unlinkSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
+ import { atomicWrite } from "../util/json-io";
17
+ import { emptyTrack, type SessionTrack } from "./session-state";
18
+ import { computeMac, loadOrCreateKey, signTrack, verifyTrack, writeLastNonce, type TrackEnvelope } from "./integrity";
19
+ import { foldEvents, type TrackEvent } from "./track-journal";
20
+ import { withTrackLock } from "./track-lock";
21
+
22
+ /** Compact when the log exceeds this size (bytes). */
23
+ export const COMPACT_BYTES: number = 128 * 1024;
24
+
25
+ /** The journal log path twin of a track snapshot path. */
26
+ export function journalLogPath(trackPath: string): string {
27
+ return trackPath.replace(/\.json$/, ".log");
28
+ }
29
+
30
+ /** Parse & MAC-verify journal text; malformed or tampered lines are skipped. */
31
+ export function parseEvents(text: string): TrackEvent[] {
32
+ const key = loadOrCreateKey(), out: TrackEvent[] = [];
33
+ for (const line of text.split("\n")) {
34
+ if (!line) continue;
35
+ try {
36
+ const ev = JSON.parse(line) as TrackEvent;
37
+ if (ev?.v !== 1 || typeof ev.nonce !== "string" || typeof ev.ts !== "number") continue;
38
+ if (ev.mac !== computeMac(key, JSON.stringify({ field: ev.field, op: ev.op, value: ev.value, ts: ev.ts }), ev.nonce)) continue; // fail-closed per line
39
+ out.push(ev);
40
+ } catch { /* skip the bad line, keep the rest */ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /** Read & verify every event of a log; absent/unreadable → [] (fail-open read). */
46
+ export function readEvents(logPath: string): TrackEvent[] {
47
+ try { return parseEvents(readFileSync(logPath, "utf8")); } catch { return []; }
48
+ }
49
+
50
+ /** Verified legacy snapshot only (fail-closed → emptyTrack), sync. */
51
+ function readSnapshotSync(file: string): SessionTrack {
52
+ try {
53
+ return verifyTrack(JSON.parse(readFileSync(file, "utf8")) as TrackEnvelope) ?? emptyTrack();
54
+ } catch {
55
+ return emptyTrack();
56
+ }
57
+ }
58
+
59
+ /** Sync snapshot ⊕ journal read for the sync gates; `journal=false` = legacy kill-switch path. */
60
+ export function readTrackSync(file: string, journal: boolean): SessionTrack {
61
+ const base = readSnapshotSync(file);
62
+ return journal ? foldEvents(readEvents(journalLogPath(file)), base) : base;
63
+ }
64
+
65
+ /** Fold a whole log file into the signed snapshot (signTrack + nonce, unchanged). */
66
+ function foldIntoSnapshot(file: string, logPath: string): void {
67
+ const envelope = signTrack(foldEvents(readEvents(logPath), readSnapshotSync(file)));
68
+ atomicWrite(file, JSON.stringify(envelope, null, 2));
69
+ writeLastNonce(envelope.nonce);
70
+ }
71
+
72
+ /** Rename-atomic compaction: rename captures the WHOLE log, fold, unlink. A crash leaves `.folding` (recovered first next run); on fold failure BEFORE commit the log is renamed back — never a lost event, and never an event in BOTH snapshot and log (no double-count). */
73
+ function compactSync(file: string): void {
74
+ const log = journalLogPath(file), folding = `${log}.folding`;
75
+ if (existsSync(folding)) { foldIntoSnapshot(file, folding); unlinkSync(folding); } // crashed compaction recovery
76
+ renameSync(log, folding); // atomic capture; the next append recreates a fresh log
77
+ let committed = false;
78
+ try {
79
+ foldIntoSnapshot(file, folding);
80
+ committed = true; // the snapshot now HOLDS the events: never return them to the log
81
+ unlinkSync(folding);
82
+ } catch (err) {
83
+ if (!committed) { try { renameSync(folding, log); } catch { /* keep .folding for recovery */ } }
84
+ else { try { unlinkSync(folding); } catch { /* residue iff unlink itself failed */ } }
85
+ throw err;
86
+ }
87
+ }
88
+
89
+ /** Trigger compaction past the cap (`FUSE_TRACK_COMPACT_BYTES` overrides {@link COMPACT_BYTES}), under the existing lock (skipped on contention). */
90
+ export async function maybeCompactJournal(file: string): Promise<void> {
91
+ try {
92
+ const cap = Number(process.env.FUSE_TRACK_COMPACT_BYTES) || COMPACT_BYTES;
93
+ if (statSync(journalLogPath(file)).size < cap) return;
94
+ } catch {
95
+ return; // no log yet
96
+ }
97
+ await withTrackLock(dirname(file), async () => {
98
+ try { compactSync(file); } catch { /* rare path: the next trigger retries */ }
99
+ });
100
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @module track-diff
3
+ * Derive journal events from a track mutation (prev → next), so journal-mode
4
+ * writers keep the existing immutable-mutator API unchanged: run the pure
5
+ * mutator in memory, then append ONLY what changed. The per-field mapping
6
+ * mirrors the fold table in track-journal.ts (adds/maxes/appends/merges/sets;
7
+ * evictions are NOT events — the fold's sliding-window re-filter owns those).
8
+ * @packageDocumentation
9
+ */
10
+ import type { SessionTrack } from "./session-state";
11
+
12
+ /** An unsigned event ready for {@link appendEvent}. */
13
+ export type RawEvent = { field: string; op: "add" | "max" | "append" | "merge" | "set"; value: unknown; ts: number };
14
+
15
+ /** Elements of `next` beyond the longest prefix shared with `prev` (append-only arrays). */
16
+ function tail<T>(prev: T[], next: T[]): T[] {
17
+ let i = 0;
18
+ while (i < prev.length && i < next.length && JSON.stringify(prev[i]) === JSON.stringify(next[i])) i++;
19
+ return next.slice(i);
20
+ }
21
+
22
+ /** Multiset of added numbers (next counts minus prev counts). */
23
+ function added(xs: number[], ys: number[]): number[] {
24
+ const left = new Map<number, number>();
25
+ for (const x of xs) left.set(x, (left.get(x) ?? 0) + 1);
26
+ const out: number[] = [];
27
+ for (const y of ys) {
28
+ const n = left.get(y) ?? 0;
29
+ if (n > 0) left.set(y, n - 1);
30
+ else out.push(y);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /** Events replaying `next` over `prev` (empty when the mutation was a no-op). */
36
+ export function diffTrackEvents(prev: SessionTrack, next: SessionTrack, now: number): RawEvent[] {
37
+ if (prev === next) return [];
38
+ const out: RawEvent[] = [];
39
+ for (const p of next.refsRead) if (!prev.refsRead.includes(p)) out.push({ field: "refsRead", op: "add", value: p, ts: next.refsReadAt?.[p] ?? now });
40
+ for (const [p, ts] of Object.entries(next.refsReadAt ?? {})) if (ts !== prev.refsReadAt?.[p]) out.push({ field: "refsReadAt", op: "max", value: [p, ts], ts });
41
+ for (const a of tail(prev.agents, next.agents)) out.push({ field: "agents", op: "append", value: a, ts: a.ts ?? now });
42
+ for (const r of tail(prev.receipts ?? [], next.receipts ?? [])) out.push({ field: "receipts", op: "append", value: r, ts: r.ts ?? now });
43
+ for (const [key, entry] of Object.entries(next.authorizations)) {
44
+ if (JSON.stringify(entry) !== JSON.stringify(prev.authorizations[key])) out.push({ field: "authorizations", op: "merge", value: { key, entry }, ts: Date.parse(entry.doc_consulted ?? "") || now });
45
+ }
46
+ for (const ts of added(prev.trivialEdits ?? [], next.trivialEdits ?? [])) out.push({ field: "trivialEdits", op: "add", value: ts, ts });
47
+ if (next.target && JSON.stringify(next.target) !== JSON.stringify(prev.target)) out.push({ field: "target", op: "set", value: next.target, ts: Date.parse(next.target.set_at) || now });
48
+ if (next.brainstormRequired !== prev.brainstormRequired) out.push({ field: "brainstormRequired", op: "set", value: next.brainstormRequired, ts: now });
49
+ return out;
50
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @module track-journal
3
+ * Append-only, per-line-signed event journal for {@link SessionTrack} — the
4
+ * fan-out-immune replacement for the locked RMW (generalises the freshness/
5
+ * ref-journal.ts pattern to every field). Appends serialise on a SHORT
6
+ * BLOCKING track lock (never skipped, sub-ms wait — see appendEvent) so they
7
+ * can never race the rename-atomic compaction: zero lost write. Per-line HMAC
8
+ * (same key/scheme as integrity.ts): a tampered line is dropped, never the
9
+ * whole file (fail-closed PER LINE).
10
+ * @packageDocumentation
11
+ */
12
+ import { appendFileSync } from "node:fs";
13
+ import { dirname } from "node:path";
14
+ import { randomBytes } from "node:crypto";
15
+ import { computeMac, loadOrCreateKey } from "./integrity";
16
+ import { withTrackLockSyncBlocking } from "./track-lock-sync";
17
+ import { emptyTrack, type SessionTrack } from "./session-state";
18
+ import type { AuthEntry } from "../freshness/doc-helpers";
19
+ import type { SessionTarget } from "../policy/apex-authorization";
20
+ import type { Receipt } from "./receipts";
21
+
22
+ /** One signed journal line: `mac` = HMAC over `"<nonce>:<JSON{field,op,value,ts}>"`. */
23
+ export type TrackEvent = { v: 1; field: string; op: "add" | "max" | "append" | "merge" | "set"; value: unknown; ts: number; nonce: string; mac: string };
24
+
25
+ /** Hard cap per line — short lines stay non-interleaved under O_APPEND. */
26
+ const MAX_LINE = 32 * 1024;
27
+ /** Trivial-edit sliding window for the fold (mirrors gate.ts DEFAULT_WINDOW_MS). */
28
+ const TRIVIAL_WINDOW_MS = 120_000;
29
+
30
+ /** Sign one event: an oversized STRING value is truncated then re-signed (never an unsigned partial line); an oversized non-string is dropped (null). */
31
+ export function signEvent(field: string, op: TrackEvent["op"], value: unknown, ts: number): TrackEvent | null {
32
+ let data = JSON.stringify({ field, op, value, ts });
33
+ if (data.length > MAX_LINE - 256) {
34
+ if (typeof value !== "string") return null;
35
+ value = value.slice(0, Math.max(0, value.length - (data.length - (MAX_LINE - 256))));
36
+ data = JSON.stringify({ field, op, value, ts });
37
+ }
38
+ const nonce = randomBytes(8).toString("hex");
39
+ return { v: 1, field, op, value, ts, nonce, mac: computeMac(loadOrCreateKey(), data, nonce) };
40
+ }
41
+
42
+ /** Append one signed event line under the BLOCKING track lock (same `track.lock` as the compaction — an append can never straddle rename/fold/unlink; never skipped). Fail-open on I/O error. */
43
+ export function appendEvent(logPath: string, field: string, op: TrackEvent["op"], value: unknown, ts: number): boolean {
44
+ try {
45
+ const ev = signEvent(field, op, value, ts);
46
+ if (!ev) return false;
47
+ withTrackLockSyncBlocking(dirname(logPath), () => appendFileSync(logPath, JSON.stringify(ev) + "\n", "utf8"));
48
+ return true;
49
+ } catch { return false; }
50
+ }
51
+
52
+ const union = (a?: string[], b?: string[]): string[] => [...new Set([...(a ?? []), ...(b ?? [])])];
53
+
54
+ /** Merge one auth entry: non-empty list fields union, scalars of the later-folded (newer) event win. */
55
+ function mergeAuth(p: AuthEntry | undefined, e: AuthEntry): AuthEntry {
56
+ const b = p ?? {}, out: AuthEntry = { ...b, ...e };
57
+ const sessions = union(b.sessions, e.sessions);
58
+ if (sessions.length) out.sessions = sessions;
59
+ const docSessions = union(b.doc_sessions, e.doc_sessions);
60
+ if (docSessions.length) out.doc_sessions = docSessions;
61
+ const sources = union(b.sources ?? (b.source ? [b.source] : []), e.sources ?? (e.source ? [e.source] : []));
62
+ if (sources.length) out.sources = sources;
63
+ const readPaths = union(b.read_paths, e.read_paths);
64
+ if (readPaths.length) out.read_paths = readPaths;
65
+ return out;
66
+ }
67
+
68
+ /**
69
+ * Replay events over `base` (the legacy snapshot — emptyTrack when absent).
70
+ * Deterministic CRDT fold with a STABLE sort on ts (log order breaks ties):
71
+ * refsRead = dedup union; refsReadAt = per-key max; agents/receipts =
72
+ * ts-ordered append, never deduped; authorizations = per-key merge (list
73
+ * unions); trivialEdits = union re-filtered by the sliding window;
74
+ * target/brainstormRequired = last-writer-wins on ts.
75
+ */
76
+ export function foldEvents(events: TrackEvent[], base: SessionTrack = emptyTrack()): SessionTrack {
77
+ const t: SessionTrack = { ...base, refsRead: [...base.refsRead], agents: [...base.agents], authorizations: { ...base.authorizations }, trivialEdits: [] };
78
+ const at: Record<string, number> = { ...base.refsReadAt };
79
+ const receipts: Receipt[] = [...(base.receipts ?? [])];
80
+ const trivial = new Set(base.trivialEdits ?? []);
81
+ let targetTs = base.target ? Date.parse(base.target.set_at) || 0 : 0, brainstormTs = 0; // a base flag is unstamped: any event overrides it
82
+ for (const ev of [...events].sort((a, b) => a.ts - b.ts)) { // stable: log order breaks ts ties
83
+ if (ev.field === "refsRead") { const p = String(ev.value); if (!t.refsRead.includes(p)) t.refsRead.push(p); }
84
+ else if (ev.field === "refsReadAt") { const [p, ts] = ev.value as [string, number]; if ((at[p] ?? -1) < ts) at[p] = ts; }
85
+ else if (ev.field === "agents") t.agents.push(ev.value as SessionTrack["agents"][number]);
86
+ else if (ev.field === "receipts") receipts.push(ev.value as Receipt);
87
+ else if (ev.field === "authorizations") { const m = ev.value as { key: string; entry: AuthEntry }; t.authorizations[m.key] = mergeAuth(t.authorizations[m.key], m.entry); }
88
+ else if (ev.field === "trivialEdits") trivial.add(Number(ev.value));
89
+ else if (ev.field === "target" && ev.ts >= targetTs) { t.target = ev.value as SessionTarget; targetTs = ev.ts; }
90
+ else if (ev.field === "brainstormRequired" && ev.ts >= brainstormTs) { t.brainstormRequired = Boolean(ev.value); brainstormTs = ev.ts; }
91
+ }
92
+ if (Object.keys(at).length) t.refsReadAt = at;
93
+ if (receipts.length) t.receipts = receipts;
94
+ const maxT = Math.max(0, ...trivial);
95
+ t.trivialEdits = [...trivial].filter((x) => x > maxT - TRIVIAL_WINDOW_MS).sort((a, b) => a - b);
96
+ return t;
97
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Synchronous twin of {@link withTrackLock} for lifecycle paths that CANNOT
3
+ * float async work (the SubagentStop dispatch runs synchronously in a
4
+ * short-lived hook process — an async write could be dropped before exit,
5
+ * see `evidence-harvest-io.ts`). Same semantics: `wx` lockfile, bounded total
6
+ * retry, named skipped write on contention, stale reclamation. The wait uses
7
+ * `Atomics.wait` (allowed on the main thread in both Node and Bun).
8
+ */
9
+ import { closeSync, mkdirSync, openSync, statSync, unlinkSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { LOCK_FAILED, LOCK_RETRY_TOTAL_MS, LOCK_TTL_MS } from "./track-lock";
12
+
13
+ export { LOCK_FAILED } from "./track-lock";
14
+
15
+ /** Block the thread for `ms` (bounded by the caller's total budget). */
16
+ function sleepSync(ms: number): void {
17
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
18
+ }
19
+
20
+ /** True when the lockfile is older than the TTL (orphaned by a dead process). */
21
+ function isStale(lock: string): boolean {
22
+ try { return Date.now() - statSync(lock).mtimeMs > LOCK_TTL_MS; } catch { return false; }
23
+ }
24
+
25
+ /**
26
+ * Run `fn` under the track lock of `dir`, synchronously.
27
+ * @param dir - The track state directory (lockfile lives inside).
28
+ * @param fn - The protected read-modify-write.
29
+ * @returns `fn`'s result, or {@link LOCK_FAILED} when the lock stayed busy.
30
+ */
31
+ export function withTrackLockSync<T>(dir: string, fn: () => T): T | typeof LOCK_FAILED {
32
+ mkdirSync(dir, { recursive: true });
33
+ const lock = join(dir, "track.lock");
34
+ const deadline = Date.now() + LOCK_RETRY_TOTAL_MS;
35
+ for (;;) {
36
+ try {
37
+ const fd = openSync(lock, "wx");
38
+ closeSync(fd);
39
+ break;
40
+ } catch {
41
+ if (isStale(lock)) { try { unlinkSync(lock); } catch { /* raced */ } continue; }
42
+ if (Date.now() >= deadline) {
43
+ process.stderr.write(`harness: track lock busy, write skipped (${lock})\n`);
44
+ return LOCK_FAILED;
45
+ }
46
+ sleepSync(8);
47
+ }
48
+ }
49
+ try {
50
+ return fn();
51
+ } finally {
52
+ try { unlinkSync(lock); } catch { /* best-effort release */ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Blocking twin of {@link withTrackLockSync}: NEVER skips — spins (1 ms step)
58
+ * until the lock is acquired. Used by the journal append path (track-journal
59
+ * `appendEvent`): an append must wait out an in-flight compaction (same
60
+ * `track.lock`), never race its rename/fold/unlink, never be skipped — a lost
61
+ * write is not an option. The stale-lock TTL (dead-process reclamation) is the
62
+ * only anti-deadlock guard. Do NOT use on paths that may already hold the lock.
63
+ */
64
+ export function withTrackLockSyncBlocking<T>(dir: string, fn: () => T): T {
65
+ mkdirSync(dir, { recursive: true });
66
+ const lock = join(dir, "track.lock");
67
+ for (;;) {
68
+ try {
69
+ const fd = openSync(lock, "wx");
70
+ closeSync(fd);
71
+ break;
72
+ } catch {
73
+ if (isStale(lock)) { try { unlinkSync(lock); } catch { /* raced */ } }
74
+ sleepSync(1);
75
+ }
76
+ }
77
+ try {
78
+ return fn();
79
+ } finally {
80
+ try { unlinkSync(lock); } catch { /* best-effort release */ }
81
+ }
82
+ }
83
+
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Cross-process lock for session-track read-modify-write. The ~11-hook
3
+ * fan-out races plain `loadTrack → mutate → saveTrack` (writes silently
4
+ * lost — `refsRead` was the only mitigated channel, via its journal). This
5
+ * lockfile (`wx`, atomic create) serializes the whole RMW:
6
+ * - retry is bounded in TOTAL time (400 ms, far under any hook timeout);
7
+ * - after the budget, NEVER crash: stderr is logged and the caller skips the
8
+ * write — a named lost write beats a fail-open crash (hook fail-open
9
+ * semantics preserved);
10
+ * - an orphaned lockfile (dead process) is reclaimed after a 10 s TTL, so a
11
+ * crash mid-write never blocks a project forever.
12
+ */
13
+ import { closeSync, existsSync, mkdirSync, openSync, statSync, unlinkSync } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ /** Orphaned lockfile lifetime before reclamation. */
17
+ export const LOCK_TTL_MS = 10_000;
18
+ /** Total bounded retry budget before the write is skipped (fail-open). */
19
+ export const LOCK_RETRY_TOTAL_MS = 400;
20
+ /** Delay between two lock acquisition attempts. */
21
+ const RETRY_STEP_MS = 8;
22
+
23
+ /** Sentinel returned by {@link withTrackLock} when the lock could not be taken. */
24
+ export const LOCK_FAILED: unique symbol = Symbol("track-lock-failed");
25
+
26
+ /** True when the lockfile is older than the TTL (orphaned by a dead process). */
27
+ function isStale(lock: string): boolean {
28
+ try { return Date.now() - statSync(lock).mtimeMs > LOCK_TTL_MS; } catch { return false; }
29
+ }
30
+
31
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
32
+
33
+ /**
34
+ * Run `fn` under the track lock of `dir`, or skip with a stderr note.
35
+ * @param dir - The track state directory (lockfile lives inside).
36
+ * @param fn - The protected read-modify-write.
37
+ * @returns `fn`'s result, or {@link LOCK_FAILED} when the lock stayed busy.
38
+ */
39
+ export async function withTrackLock<T>(dir: string, fn: () => Promise<T>): Promise<T | typeof LOCK_FAILED> {
40
+ mkdirSync(dir, { recursive: true });
41
+ const lock = join(dir, "track.lock");
42
+ const deadline = Date.now() + LOCK_RETRY_TOTAL_MS;
43
+ for (;;) {
44
+ try {
45
+ const fd = openSync(lock, "wx");
46
+ closeSync(fd);
47
+ break;
48
+ } catch {
49
+ if (isStale(lock)) { try { unlinkSync(lock); } catch { /* raced */ } continue; }
50
+ if (Date.now() >= deadline) {
51
+ process.stderr.write(`harness: track lock busy, write skipped (${lock})\n`);
52
+ return LOCK_FAILED;
53
+ }
54
+ await sleep(RETRY_STEP_MS);
55
+ }
56
+ }
57
+ try {
58
+ return await fn();
59
+ } finally {
60
+ try { unlinkSync(lock); } catch { /* best-effort release */ }
61
+ }
62
+ }
63
+
64
+ /** Test helper: true when a lockfile currently exists in `dir`. */
65
+ export function trackLockExists(dir: string): boolean {
66
+ return existsSync(join(dir, "track.lock"));
67
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Compact JSON serializer for cache/state files.
3
+ * Top-level keys are indented one per line; nested objects/arrays render inline.
4
+ * Behaviour preserved from the fusengine hooks.
5
+ */
6
+ export function compactJson(data: unknown): string {
7
+ if (typeof data !== "object" || data === null) return `${JSON.stringify(data)}\n`;
8
+ if (Array.isArray(data)) {
9
+ if (data.length === 0) return "[]\n";
10
+ const items = data.map((item) => ` ${JSON.stringify(item)}`);
11
+ return `[\n${items.join(",\n")}\n]\n`;
12
+ }
13
+ const lines: string[] = [];
14
+ for (const [key, val] of Object.entries(data as Record<string, unknown>)) {
15
+ if (Array.isArray(val) && val.length > 0) {
16
+ const items = val.map((item) => ` ${JSON.stringify(item)}`);
17
+ lines.push(` "${key}": [\n${items.join(",\n")}\n ]`);
18
+ } else {
19
+ lines.push(` "${key}": ${JSON.stringify(val)}`);
20
+ }
21
+ }
22
+ return `{\n${lines.join(",\n")}\n}\n`;
23
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./compact-json";
2
+ export * from "./project-root";
@@ -0,0 +1,47 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import {
3
+ chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync,
4
+ } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { compactJson } from "./compact-json";
8
+
9
+ /** Atomically write `data` to `path` (temp + rename, 0o600). Cross-FS safe on macOS/Linux. */
10
+ export function atomicWrite(path: string, data: string): void {
11
+ const parent = dirname(path);
12
+ mkdirSync(parent, { recursive: true });
13
+ const tmp = `${path}.${randomUUID()}.tmp`;
14
+ try {
15
+ writeFileSync(tmp, data, { encoding: "utf8" });
16
+ chmodSync(tmp, 0o600);
17
+ renameSync(tmp, path);
18
+ } catch (err) {
19
+ try { unlinkSync(tmp); } catch { /* noop */ }
20
+ throw err;
21
+ }
22
+ }
23
+
24
+ /** Ensure a directory exists. */
25
+ export async function ensureDir(dir: string): Promise<void> {
26
+ await mkdir(dir, { recursive: true });
27
+ }
28
+
29
+ /** Read & JSON-parse a file; null on missing/corrupt. */
30
+ export async function readJsonFile<T>(path: string): Promise<T | null> {
31
+ try {
32
+ if (!existsSync(path)) return null;
33
+ return JSON.parse(readFileSync(path, "utf8")) as T;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** Atomically write JSON (compact form when `compact`). */
40
+ export async function writeJsonFile(path: string, data: unknown, compact = false): Promise<void> {
41
+ atomicWrite(path, compact ? compactJson(data) : JSON.stringify(data, null, 2));
42
+ }
43
+
44
+ /** 8-char MD5 of text (cache key; non-cryptographic). Portable Node+Bun. */
45
+ export function hashText(text: string): string {
46
+ return createHash("md5").update(text).digest("hex").slice(0, 8);
47
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ /** Source-code file extensions worth tracking as "code was written". */
5
+ const CODE_EXT =
6
+ /\.(ts|tsx|js|jsx|py|php|swift|go|rs|rb|java|vue|svelte|astro|css|kt|dart|cpp|c)$/;
7
+ /** Generated/vendored directories that never count as code. */
8
+ const SKIP_DIRS = /(node_modules|vendor|dist|build|\.next|DerivedData|\.git)/;
9
+
10
+ /** True for a source-code file outside generated/vendored directories. */
11
+ export function isCodeFile(p: string): boolean {
12
+ return CODE_EXT.test(p) && !SKIP_DIRS.test(p);
13
+ }
14
+
15
+ /**
16
+ * Walk up from `dir` to the nearest ancestor directory containing `marker`.
17
+ * @param dir - Starting directory.
18
+ * @param marker - Sibling file/dir name to look for at each level.
19
+ * @returns The ancestor holding `marker`, or `null` at filesystem root.
20
+ */
21
+ export function walkUpFor(dir: string, marker: string): string | null {
22
+ let current = resolve(dir);
23
+ while (current !== "/") {
24
+ if (existsSync(`${current}/${marker}`)) return current;
25
+ current = dirname(current);
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Resolve the project root, preferring the repo boundary (`.git`) over a
32
+ * nested `package.json` — avoids false roots in monorepos. Null if none found.
33
+ */
34
+ export function projectRootOrNull(dir: string): string | null {
35
+ return walkUpFor(dir, ".git") ?? walkUpFor(dir, "package.json");
36
+ }
37
+
38
+ /** Like {@link projectRootOrNull} but falls back to `process.cwd()`. */
39
+ export function projectRoot(dir: string): string {
40
+ return projectRootOrNull(dir) ?? process.cwd();
41
+ }