@fusengine/harness 0.1.90 → 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 (417) 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-C43gA-Pr.mjs} +701 -134
  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/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 +27 -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.ts +46 -0
  44. package/src/adapters/cursor/events.ts +38 -0
  45. package/src/adapters/cursor/index.ts +43 -0
  46. package/src/adapters/cursor/interfaces/types.ts +88 -0
  47. package/src/adapters/cursor/native-response.ts +158 -0
  48. package/src/adapters/cursor/normalize.ts +123 -0
  49. package/src/adapters/cursor/respond.ts +123 -0
  50. package/src/adapters/gemini/index.ts +36 -0
  51. package/src/adapters/hermes/index.ts +55 -0
  52. package/src/adapters/hermes/interfaces/types.ts +26 -0
  53. package/src/adapters/kimi/index.ts +96 -0
  54. package/src/adapters/kimi/interfaces/types.ts +55 -0
  55. package/src/cache/compact.ts +55 -0
  56. package/src/cache/index.ts +5 -0
  57. package/src/cache/io.ts +38 -0
  58. package/src/cache/mcp-response.ts +39 -0
  59. package/src/cache/mcp-store.ts +70 -0
  60. package/src/cache/store.ts +93 -0
  61. package/src/changelog/fetch.ts +79 -0
  62. package/src/cli/bin.ts +100 -0
  63. package/src/cli/cursor-event-scanner.ts +175 -0
  64. package/src/cli/cursor-stdin-reader.ts +65 -0
  65. package/src/cli/doctor.ts +96 -0
  66. package/src/cli/hook-io.ts +130 -0
  67. package/src/cli/hook-sound.ts +49 -0
  68. package/src/cli/index.ts +1 -0
  69. package/src/cli/json-primitive-scanner.ts +83 -0
  70. package/src/cli/run.ts +36 -0
  71. package/src/cli/scope.ts +31 -0
  72. package/src/codex-rules/build.ts +63 -0
  73. package/src/codex-rules/index.ts +8 -0
  74. package/src/codex-rules/mapping.ts +42 -0
  75. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  76. package/src/codex-rules/rules/ask.ts +44 -0
  77. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  78. package/src/codex-rules/rules/dd.ts +23 -0
  79. package/src/codex-rules/rules/diskutil.ts +33 -0
  80. package/src/codex-rules/rules/mkfs.ts +42 -0
  81. package/src/codex-rules/rules/privilege.ts +49 -0
  82. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  83. package/src/codex-rules/rules/rm-variants.ts +58 -0
  84. package/src/codex-rules/rules/single-commands.ts +57 -0
  85. package/src/codex-rules/skip-list.ts +25 -0
  86. package/src/codex-rules/starlark.ts +46 -0
  87. package/src/codex-rules/types.ts +36 -0
  88. package/src/config/dotenv.ts +55 -0
  89. package/src/config/env.ts +10 -0
  90. package/src/config/home-dir.ts +36 -0
  91. package/src/config/index.ts +5 -0
  92. package/src/config/layout.ts +52 -0
  93. package/src/config/limits.ts +56 -0
  94. package/src/config/ttl.ts +30 -0
  95. package/src/detect/harness.ts +73 -0
  96. package/src/detect/index.ts +1 -0
  97. package/src/detect/interfaces/types.ts +18 -0
  98. package/src/freshness/agent-evidence-record.ts +95 -0
  99. package/src/freshness/agent-evidence.ts +95 -0
  100. package/src/freshness/codex-spawn-evidence.ts +91 -0
  101. package/src/freshness/doc-helpers.ts +90 -0
  102. package/src/freshness/evidence-harvest-io.ts +72 -0
  103. package/src/freshness/evidence-harvest.ts +61 -0
  104. package/src/freshness/explore-tools.ts +95 -0
  105. package/src/freshness/index.ts +2 -0
  106. package/src/freshness/query-framework.ts +73 -0
  107. package/src/freshness/ref-evidence.ts +49 -0
  108. package/src/freshness/ref-journal.ts +98 -0
  109. package/src/freshness/trivial-edit-counter.ts +30 -0
  110. package/src/index.ts +11 -0
  111. package/src/init/index.ts +2 -0
  112. package/src/init/run.ts +32 -0
  113. package/src/init/templates.ts +75 -0
  114. package/src/memory/gitignore.ts +14 -0
  115. package/src/memory/index.ts +3 -0
  116. package/src/memory/registry.ts +39 -0
  117. package/src/memory/session-roots.ts +99 -0
  118. package/src/memory/state.ts +58 -0
  119. package/src/policy/apex-authorization.ts +100 -0
  120. package/src/policy/apex-gates.ts +91 -0
  121. package/src/policy/apex-target-steps.ts +73 -0
  122. package/src/policy/apex-target.ts +49 -0
  123. package/src/policy/apex-task-context.ts +81 -0
  124. package/src/policy/apex.ts +94 -0
  125. package/src/policy/cartographer/build-tree.ts +69 -0
  126. package/src/policy/cartographer/describe.ts +53 -0
  127. package/src/policy/cartographer/entry.ts +38 -0
  128. package/src/policy/cartographer/frontmatter.ts +48 -0
  129. package/src/policy/cartographer/indicators.ts +34 -0
  130. package/src/policy/claude-md-context.ts +66 -0
  131. package/src/policy/conventions/index.ts +11 -0
  132. package/src/policy/conventions/interfaces.ts +71 -0
  133. package/src/policy/conventions/langs.ts +53 -0
  134. package/src/policy/conventions/query.ts +49 -0
  135. package/src/policy/conventions/react-hooks.ts +18 -0
  136. package/src/policy/conventions/store-or-query.ts +21 -0
  137. package/src/policy/conventions/stores.ts +56 -0
  138. package/src/policy/conventions/strip.ts +87 -0
  139. package/src/policy/conventions/verdict.ts +38 -0
  140. package/src/policy/creation-intent.ts +11 -0
  141. package/src/policy/deny-loop.ts +88 -0
  142. package/src/policy/design/allowed-write.ts +28 -0
  143. package/src/policy/design/content-checks.ts +45 -0
  144. package/src/policy/design/corpus-fs.ts +21 -0
  145. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  146. package/src/policy/design/corpus-resolve.ts +99 -0
  147. package/src/policy/design/corpus.ts +92 -0
  148. package/src/policy/design/design-system-rules.ts +45 -0
  149. package/src/policy/design/flag.ts +30 -0
  150. package/src/policy/design/gates-pipeline.ts +105 -0
  151. package/src/policy/design/gates.ts +87 -0
  152. package/src/policy/design/screenshot-tools.ts +24 -0
  153. package/src/policy/design/skill-evidence.ts +34 -0
  154. package/src/policy/design/skill-gate.ts +81 -0
  155. package/src/policy/design/skill-triggers.ts +51 -0
  156. package/src/policy/design/state.ts +92 -0
  157. package/src/policy/design/template-urls.ts +54 -0
  158. package/src/policy/design/transitions.ts +68 -0
  159. package/src/policy/detect-claude-md-project-type.ts +32 -0
  160. package/src/policy/detect-framework.ts +99 -0
  161. package/src/policy/detect-primitive-checks.ts +100 -0
  162. package/src/policy/detect-primitive-lib.ts +55 -0
  163. package/src/policy/detect-project.ts +99 -0
  164. package/src/policy/edit-outcome.ts +53 -0
  165. package/src/policy/evaluate.ts +81 -0
  166. package/src/policy/expert-agents.ts +60 -0
  167. package/src/policy/file-size-scope.ts +32 -0
  168. package/src/policy/file-size.ts +99 -0
  169. package/src/policy/framework-solid-exclude.ts +52 -0
  170. package/src/policy/framework-solid-extended.ts +93 -0
  171. package/src/policy/framework-solid-gates-systems.ts +99 -0
  172. package/src/policy/framework-solid-gates.ts +98 -0
  173. package/src/policy/framework-solid.ts +88 -0
  174. package/src/policy/gemini-mcp-gate.ts +83 -0
  175. package/src/policy/git-gates.ts +40 -0
  176. package/src/policy/guards/bash-command-anchor.ts +40 -0
  177. package/src/policy/guards/bash-write-patterns.ts +140 -0
  178. package/src/policy/guards/bash-write-redirects.ts +201 -0
  179. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  180. package/src/policy/guards/bash-write.ts +76 -0
  181. package/src/policy/guards/context.ts +12 -0
  182. package/src/policy/guards/index.ts +61 -0
  183. package/src/policy/guards/install.ts +22 -0
  184. package/src/policy/guards/interface-separation-ext.ts +41 -0
  185. package/src/policy/guards/interface-separation.ts +100 -0
  186. package/src/policy/guards/protected-path.ts +100 -0
  187. package/src/policy/guards/security.ts +97 -0
  188. package/src/policy/index.ts +19 -0
  189. package/src/policy/interfaces/types.ts +46 -0
  190. package/src/policy/js-gate-route.ts +51 -0
  191. package/src/policy/lessons/lesson-gate.ts +0 -0
  192. package/src/policy/lessons/trigger-index.ts +84 -0
  193. package/src/policy/lessons/types.ts +31 -0
  194. package/src/policy/module-layout.ts +31 -0
  195. package/src/policy/nearest-manifest.ts +75 -0
  196. package/src/policy/never-approval.ts +95 -0
  197. package/src/policy/patterns.ts +80 -0
  198. package/src/policy/seo/validate.ts +51 -0
  199. package/src/policy/shadcn-project.ts +22 -0
  200. package/src/policy/shadcn-skill-gate.ts +97 -0
  201. package/src/policy/shell-read-refs.ts +96 -0
  202. package/src/policy/skill-path.ts +46 -0
  203. package/src/policy/skill-patterns/laravel.ts +40 -0
  204. package/src/policy/skill-patterns/nextjs.ts +29 -0
  205. package/src/policy/skill-patterns/react.ts +27 -0
  206. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  207. package/src/policy/skill-patterns/shadcn.ts +66 -0
  208. package/src/policy/skill-patterns/swift.ts +30 -0
  209. package/src/policy/skill-patterns/tailwind.ts +40 -0
  210. package/src/policy/skill-source.ts +52 -0
  211. package/src/policy/skill-trigger-patterns.ts +33 -0
  212. package/src/policy/skill-triggers.ts +108 -0
  213. package/src/policy/tailwind-skill-gate.ts +63 -0
  214. package/src/policy/trivial-edits.ts +52 -0
  215. package/src/policy/verbosity.ts +39 -0
  216. package/src/prompt/index.ts +1 -0
  217. package/src/prompt/types.ts +36 -0
  218. package/src/refs/discover.ts +69 -0
  219. package/src/refs/frontmatter.ts +24 -0
  220. package/src/refs/index.ts +4 -0
  221. package/src/refs/loader.ts +59 -0
  222. package/src/refs/ref-key.ts +41 -0
  223. package/src/refs/router.ts +63 -0
  224. package/src/refs/types.ts +25 -0
  225. package/src/runtime/activity.ts +99 -0
  226. package/src/runtime/apply-patch-gate.ts +41 -0
  227. package/src/runtime/burst-window.ts +21 -0
  228. package/src/runtime/codex-shell-tool.ts +37 -0
  229. package/src/runtime/command-string.ts +37 -0
  230. package/src/runtime/confirm/codex-confirm.ts +126 -0
  231. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  232. package/src/runtime/confirm/confirm-code.ts +33 -0
  233. package/src/runtime/confirm/confirm-gate.ts +65 -0
  234. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  235. package/src/runtime/confirm/confirm-pending.ts +34 -0
  236. package/src/runtime/confirm/confirm-state.ts +56 -0
  237. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  238. package/src/runtime/confirm/confirm-submit.ts +38 -0
  239. package/src/runtime/core-guards-active.ts +84 -0
  240. package/src/runtime/core-guards-wired.ts +47 -0
  241. package/src/runtime/deny-loop-store.ts +94 -0
  242. package/src/runtime/deny-notice.ts +52 -0
  243. package/src/runtime/design-cache-resolve.ts +51 -0
  244. package/src/runtime/design-content-gate.ts +95 -0
  245. package/src/runtime/design-files-gate.ts +114 -0
  246. package/src/runtime/design-helpers.ts +93 -0
  247. package/src/runtime/design-lifecycle.ts +39 -0
  248. package/src/runtime/design-read-credit.ts +28 -0
  249. package/src/runtime/design.ts +116 -0
  250. package/src/runtime/dev-context.ts +43 -0
  251. package/src/runtime/dry-patterns.ts +32 -0
  252. package/src/runtime/dry.ts +96 -0
  253. package/src/runtime/fragment-registry.ts +33 -0
  254. package/src/runtime/framework-skill-gate.ts +79 -0
  255. package/src/runtime/fs-cleanup.ts +71 -0
  256. package/src/runtime/gate-apex.ts +81 -0
  257. package/src/runtime/gate-helpers.ts +52 -0
  258. package/src/runtime/gate-input.ts +26 -0
  259. package/src/runtime/gate.ts +128 -0
  260. package/src/runtime/handle-post.ts +104 -0
  261. package/src/runtime/handle-pre.ts +137 -0
  262. package/src/runtime/handle-scope-async.ts +29 -0
  263. package/src/runtime/handle-types.ts +27 -0
  264. package/src/runtime/handle.ts +116 -0
  265. package/src/runtime/home-state.ts +55 -0
  266. package/src/runtime/index.ts +18 -0
  267. package/src/runtime/inform.ts +28 -0
  268. package/src/runtime/inject-budget-recap.ts +47 -0
  269. package/src/runtime/inject-budget.ts +81 -0
  270. package/src/runtime/inject-context.ts +85 -0
  271. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  272. package/src/runtime/inject-dedup.ts +89 -0
  273. package/src/runtime/is-agent-tool.ts +19 -0
  274. package/src/runtime/lifecycle/agent-files.ts +49 -0
  275. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  276. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  277. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  278. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  279. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  280. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  281. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  282. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  283. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  284. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
  285. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  286. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  287. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  288. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  290. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  291. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  292. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  293. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  294. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  295. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  296. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  297. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  298. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  299. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  300. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  301. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  302. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  303. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  304. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  305. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  306. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  307. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  308. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  309. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  310. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  311. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  312. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  313. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  314. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  315. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  316. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  317. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  318. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  319. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  320. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  321. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  322. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  323. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  324. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  325. package/src/runtime/lifecycle/dispatch.ts +109 -0
  326. package/src/runtime/lifecycle/failure-lesson.ts +51 -0
  327. package/src/runtime/lifecycle/index.ts +34 -0
  328. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  329. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  330. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  331. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  332. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  333. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  334. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  335. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  336. package/src/runtime/lifecycle/memory/client.ts +65 -0
  337. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  338. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  339. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  340. package/src/runtime/lifecycle/memory/state.ts +49 -0
  341. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  342. package/src/runtime/lifecycle/post-compact.ts +38 -0
  343. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  344. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  345. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  346. package/src/runtime/lifecycle/rules-root.ts +83 -0
  347. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  348. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  349. package/src/runtime/lifecycle/security/scan.ts +76 -0
  350. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  351. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  352. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  353. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  354. package/src/runtime/lifecycle/session-end.ts +17 -0
  355. package/src/runtime/lifecycle/session-start.ts +57 -0
  356. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  357. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  358. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  359. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  360. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  361. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  362. package/src/runtime/lifecycle/stop-core.ts +35 -0
  363. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  364. package/src/runtime/lifecycle/task-completed.ts +94 -0
  365. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  366. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  367. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  368. package/src/runtime/lifecycle/track-changes.ts +61 -0
  369. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  370. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  371. package/src/runtime/lifecycle-bridge.ts +74 -0
  372. package/src/runtime/mcp-key.ts +44 -0
  373. package/src/runtime/mcp-tool-name.ts +92 -0
  374. package/src/runtime/mcp.ts +91 -0
  375. package/src/runtime/modular.ts +77 -0
  376. package/src/runtime/normalize.ts +107 -0
  377. package/src/runtime/notices.ts +91 -0
  378. package/src/runtime/notification-sound.ts +49 -0
  379. package/src/runtime/notifications.ts +77 -0
  380. package/src/runtime/paths.ts +59 -0
  381. package/src/runtime/post-fanout.ts +41 -0
  382. package/src/runtime/post-outcome.ts +61 -0
  383. package/src/runtime/pre-allow.ts +97 -0
  384. package/src/runtime/precommit.ts +60 -0
  385. package/src/runtime/prompt-text.ts +38 -0
  386. package/src/runtime/receipt-capture.ts +34 -0
  387. package/src/runtime/record.ts +26 -0
  388. package/src/runtime/respond.ts +92 -0
  389. package/src/runtime/solid-file-size-gate.ts +58 -0
  390. package/src/runtime/solid-pre.ts +53 -0
  391. package/src/runtime/storage.ts +9 -0
  392. package/src/statusline/colors.ts +31 -0
  393. package/src/statusline/constants.ts +17 -0
  394. package/src/statusline/formatters.ts +54 -0
  395. package/src/statusline/index.ts +4 -0
  396. package/src/statusline/progress-bar.ts +46 -0
  397. package/src/tracking/codex-post-failure.ts +73 -0
  398. package/src/tracking/index.ts +2 -0
  399. package/src/tracking/integrity.ts +99 -0
  400. package/src/tracking/one-shot-dedup.ts +34 -0
  401. package/src/tracking/one-shot-failure.ts +42 -0
  402. package/src/tracking/one-shot-store.ts +95 -0
  403. package/src/tracking/one-shot.ts +93 -0
  404. package/src/tracking/receipts.ts +95 -0
  405. package/src/tracking/session-state.ts +94 -0
  406. package/src/tracking/store.ts +96 -0
  407. package/src/tracking/track-compact.ts +100 -0
  408. package/src/tracking/track-diff.ts +50 -0
  409. package/src/tracking/track-journal.ts +97 -0
  410. package/src/tracking/track-lock-sync.ts +83 -0
  411. package/src/tracking/track-lock.ts +67 -0
  412. package/src/util/compact-json.ts +23 -0
  413. package/src/util/index.ts +2 -0
  414. package/src/util/json-io.ts +47 -0
  415. package/src/util/project-root.ts +41 -0
  416. package/src/util/runtime-io.ts +85 -0
  417. package/src/util/semver.ts +30 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Filesystem helpers for the cartographer tree walk. Ports the fs parts of
3
+ * `describe.py` (file desc) and `write_recursive.py` (children + counts).
4
+ */
5
+ import { readFileSync, readdirSync } from "node:fs";
6
+ import { extname, join } from "node:path";
7
+ import { descFromText } from "../../../policy/cartographer/describe";
8
+ import { parseField } from "../../../policy/cartographer/frontmatter";
9
+
10
+ /**
11
+ * Read a file and derive its one-line description (frontmatter / heading /
12
+ * comment). "" on any error or when nothing is found.
13
+ * @param filePath - Absolute path to the file.
14
+ * @returns The description, or "".
15
+ */
16
+ export function getFileDesc(filePath: string): string {
17
+ let text = "";
18
+ try {
19
+ text = readFileSync(filePath, "utf-8");
20
+ } catch {
21
+ return "";
22
+ }
23
+ const suffix = extname(filePath);
24
+ const mdField = suffix === ".md" ? parseField(text, "description") : "";
25
+ return descFromText(suffix, text, mdField);
26
+ }
27
+
28
+ /**
29
+ * Recursively count files whose relative path parts are all visible (no leading
30
+ * "." or "_") and none excluded. Best-effort (partial count on errors).
31
+ * @param dir - Directory to count under.
32
+ * @param exclude - Directory/name set to skip.
33
+ * @returns The file count.
34
+ */
35
+ export function countFiles(dir: string, exclude: ReadonlySet<string>): number {
36
+ let total = 0;
37
+ try {
38
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
39
+ if (e.name.startsWith(".") || e.name.startsWith("_") || exclude.has(e.name)) continue;
40
+ if (e.isDirectory()) total += countFiles(join(dir, e.name), exclude);
41
+ else if (e.isFile()) total += 1;
42
+ }
43
+ } catch { /* best effort */ }
44
+ return total;
45
+ }
46
+
47
+ /** Absolute children of `source`, split into dirs/files, sorted by full path. */
48
+ export function listChildren(source: string, exclude: ReadonlySet<string>): { dirs: string[]; files: string[] } {
49
+ const dirs: string[] = [];
50
+ const files: string[] = [];
51
+ let entries;
52
+ try {
53
+ entries = readdirSync(source, { withFileTypes: true });
54
+ } catch {
55
+ return { dirs, files };
56
+ }
57
+ for (const e of entries) {
58
+ if (e.name.startsWith(".") || e.name.startsWith("_") || exclude.has(e.name)) continue;
59
+ const abs = join(source, e.name);
60
+ if (e.isDirectory()) dirs.push(abs);
61
+ else if (e.isFile()) files.push(abs);
62
+ }
63
+ return { dirs: dirs.sort(), files: files.sort() };
64
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Index merge — preserves enriched descriptions across regenerations. Ports
3
+ * `merge_index.py` (merge_lines + .enriched.json sidecar).
4
+ */
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { parseEntry } from "../../../policy/cartographer/entry";
8
+
9
+ /**
10
+ * Load the `.enriched.json` sidecar's `entries` map for an output index.
11
+ * @param outputIndexPath - Path to the index.md being written.
12
+ * @returns The path→desc enrichment map (possibly empty).
13
+ */
14
+ export function loadEnriched(outputIndexPath: string): Record<string, string> {
15
+ const sidecar = join(dirname(outputIndexPath), ".enriched.json");
16
+ try {
17
+ if (!existsSync(sidecar)) return {};
18
+ const data = JSON.parse(readFileSync(sidecar, "utf-8")) as { entries?: Record<string, string> };
19
+ return data.entries ?? {};
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Merge freshly generated lines with prior descriptions: enriched sidecar wins,
27
+ * else a longer pre-existing description is preserved.
28
+ * @param newLines - The freshly generated index lines.
29
+ * @param outputIndexPath - Path to the existing index.md (if any).
30
+ * @returns The merged lines.
31
+ */
32
+ export function mergeLines(newLines: string[], outputIndexPath: string): string[] {
33
+ const enriched = loadEnriched(outputIndexPath);
34
+ const existingDescs: Record<string, string> = {};
35
+ if (existsSync(outputIndexPath)) {
36
+ try {
37
+ for (const line of readFileSync(outputIndexPath, "utf-8").split("\n")) {
38
+ const e = parseEntry(line);
39
+ if (e) existingDescs[e.path] = e.desc;
40
+ }
41
+ } catch { /* ignore */ }
42
+ }
43
+ return newLines.map((line) => {
44
+ const e = parseEntry(line);
45
+ if (!e) return line;
46
+ if (e.path in enriched) return `${e.prefix}[${e.name}](${e.path}) — ${enriched[e.path]}`;
47
+ const old = existingDescs[e.path] ?? "";
48
+ if (old.length > e.desc.length) return `${e.prefix}[${e.name}](${e.path}) — ${old}`;
49
+ return line;
50
+ });
51
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Project map generation. Ports `generate_project_map.py` (project map only).
3
+ */
4
+ import { existsSync, statSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+ import { EXCLUDE_DIRS, PROJECT_INDICATORS } from "../../../policy/cartographer/indicators";
8
+ import { writeTree } from "./write-tree";
9
+
10
+ /** True when `dir` is a real directory. */
11
+ function isDirectory(dir: string): boolean {
12
+ try {
13
+ return statSync(dir).isDirectory();
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * True when `dir` looks like a project root (has an indicator file) and is not
21
+ * the home directory or filesystem root.
22
+ * @param dir - Directory to test.
23
+ * @returns Whether `dir` is a project root.
24
+ */
25
+ export function isProject(dir: string): boolean {
26
+ const resolved = resolve(dir);
27
+ const home = resolve(homedir());
28
+ if (resolved === home || resolved === "/") return false;
29
+ for (const f of PROJECT_INDICATORS) {
30
+ if (existsSync(join(dir, f))) return true;
31
+ }
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * Generate the `.cartographer/project` index tree for `cwd` when it is a real
37
+ * project directory. Always returns "" (no additionalContext emitted).
38
+ * @param cwd - The working directory.
39
+ * @param outputDir - Override for the output tree root.
40
+ * @returns "" (side-effect only).
41
+ */
42
+ export function generateProjectMap(cwd: string, outputDir?: string): string {
43
+ const projectDir = resolve(cwd);
44
+ const out = outputDir ?? join(projectDir, ".cartographer", "project");
45
+ if (!isDirectory(projectDir)) return "";
46
+ if (!isProject(projectDir)) return "";
47
+ writeTree(projectDir, out, "", EXCLUDE_DIRS);
48
+ return "";
49
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Hook scanning (fs). Ports `_scan_hooks` from `scan_plugins.py`: reads a
3
+ * plugin's `hooks/hooks.json` and reduces it to a single `("hooks", "<events>", "")`
4
+ * row. The `hooks` value (or the whole document) is either a MAP of event →
5
+ * entries (keys are the events) or a LIST of entries each carrying an `event`.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import type { ScanRow } from "../../../policy/cartographer/build-tree";
10
+
11
+ /** True for a non-null plain object (excludes arrays), mirroring Python `isinstance(x, dict)`. */
12
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
13
+ return typeof v === "object" && v !== null && !Array.isArray(v);
14
+ }
15
+
16
+ /** Sort unique non-empty strings alpha (byte-order). */
17
+ function sortedEvents(events: Iterable<string>): string[] {
18
+ return [...new Set([...events].filter((e) => e))].sort((a, b) => a.localeCompare(b, "en"));
19
+ }
20
+
21
+ /** Derive hook event names from the `hooks.json` data (map keys, or list `event` fields). */
22
+ function hookEvents(hooksData: unknown): string[] {
23
+ if (isPlainObject(hooksData)) {
24
+ return sortedEvents(Object.keys(hooksData).filter((k) => !k.startsWith("_")));
25
+ }
26
+ if (Array.isArray(hooksData)) {
27
+ return sortedEvents(hooksData.map((h) => (isPlainObject(h) ? String(h.event ?? "") : "")));
28
+ }
29
+ return [];
30
+ }
31
+
32
+ /**
33
+ * Scan `hooks/hooks.json` into a single `("hooks", "<events>", "")` row.
34
+ * @param root - Absolute plugin directory.
35
+ * @returns The single hooks row, or `[]` when absent/empty/unreadable.
36
+ */
37
+ export function scanHooks(root: string): ScanRow[] {
38
+ const file = join(root, "hooks", "hooks.json");
39
+ if (!existsSync(file)) return [];
40
+ try {
41
+ const raw: unknown = JSON.parse(readFileSync(file, "utf-8"));
42
+ const hooksData = isPlainObject(raw) ? (raw.hooks ?? raw) : {};
43
+ const events = hookEvents(hooksData);
44
+ return events.length ? [["hooks", events.join(", "), ""]] : [];
45
+ } catch {
46
+ return [];
47
+ }
48
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Plugin scanning (fs). Ports `scan_plugins.py`: turns a plugin's
3
+ * agents/skills/commands/hooks into ordered `[type, name, desc]` rows.
4
+ */
5
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
+ import { extname, join } from "node:path";
7
+ import type { ScanRow } from "../../../policy/cartographer/build-tree";
8
+ import { parseBodyDesc, parseField } from "../../../policy/cartographer/frontmatter";
9
+ import { scanHooks } from "./scan-hooks";
10
+
11
+ /** Sorted entry names of `dir` (alpha, byte-order), or `[]` on error. */
12
+ function sortedNames(dir: string): string[] {
13
+ try {
14
+ return readdirSync(dir).sort((a, b) => a.localeCompare(b, "en"));
15
+ } catch {
16
+ return [];
17
+ }
18
+ }
19
+
20
+ /** Read a `.md` frontmatter field from a file path, "" when missing/unreadable. */
21
+ function fileField(path: string, field: string): string {
22
+ try {
23
+ return parseField(readFileSync(path, "utf-8"), field);
24
+ } catch {
25
+ return "";
26
+ }
27
+ }
28
+
29
+ /** Scan `agents/*.md` → `("agent", name, desc[:50])` rows. */
30
+ export function scanAgents(root: string): ScanRow[] {
31
+ const dir = join(root, "agents");
32
+ return sortedNames(dir)
33
+ .filter((n) => extname(n) === ".md")
34
+ .map((n): ScanRow => {
35
+ const f = join(dir, n);
36
+ const name = fileField(f, "name") || n.replace(/\.md$/, "");
37
+ return ["agent", name, fileField(f, "description").slice(0, 50)];
38
+ });
39
+ }
40
+
41
+ /** Scan `skills/<dir>/SKILL.md` → `("skill", dir, desc)` rows. */
42
+ function scanSkills(root: string): ScanRow[] {
43
+ const dir = join(root, "skills");
44
+ const rows: ScanRow[] = [];
45
+ for (const name of sortedNames(dir)) {
46
+ try {
47
+ if (!statSync(join(dir, name)).isDirectory()) continue;
48
+ } catch {
49
+ continue;
50
+ }
51
+ const skillMd = join(dir, name, "SKILL.md");
52
+ let desc = "";
53
+ if (existsSync(skillMd)) {
54
+ desc = fileField(skillMd, "description");
55
+ if (!desc) {
56
+ try {
57
+ desc = parseBodyDesc(readFileSync(skillMd, "utf-8"));
58
+ } catch { /* keep "" */ }
59
+ }
60
+ }
61
+ rows.push(["skill", name, desc || "(no description)"]);
62
+ }
63
+ return rows;
64
+ }
65
+
66
+ /** Scan `commands/*.md` → `("command", "/name", desc[:50])` rows. */
67
+ function scanCommands(root: string): ScanRow[] {
68
+ const dir = join(root, "commands");
69
+ return sortedNames(dir)
70
+ .filter((n) => extname(n) === ".md")
71
+ .map((n): ScanRow => ["command", `/${n.replace(/\.md$/, "")}`, fileField(join(dir, n), "description").slice(0, 50)]);
72
+ }
73
+
74
+ /**
75
+ * Scan a single plugin directory into ordered `[type, name, desc]` rows.
76
+ * @param pluginDir - Absolute plugin directory.
77
+ * @returns The agents + skills + commands + hooks rows.
78
+ */
79
+ export function scanPlugin(pluginDir: string): ScanRow[] {
80
+ return [...scanAgents(pluginDir), ...scanSkills(pluginDir), ...scanCommands(pluginDir), ...scanHooks(pluginDir)];
81
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cartographer SessionStart handler. Ports BOTH halves of the Python maps:
3
+ * `generate_project_map.py` (regenerate `.cartographer/project`) and
4
+ * `generate_map.py` (regenerate the plugin ecosystem map), emitting the
5
+ * navigation context from the latter as additionalContext.
6
+ */
7
+ import { resolve } from "node:path";
8
+ import { generateProjectMap } from "./project-map";
9
+ import { generateEcosystemMap } from "./ecosystem-map";
10
+ import { contextResponse } from "../../../adapters/claude";
11
+
12
+ /**
13
+ * Resolve the marketplace plugins dir from `CLAUDE_PLUGIN_ROOT`, mirroring the
14
+ * Python hook which passes `${CLAUDE_PLUGIN_ROOT}/..` to `generate_map.py`.
15
+ * @returns The plugins dir (env `/..`), or `undefined` to fall back to auto-detect.
16
+ */
17
+ function pluginsDirFromEnv(): string | undefined {
18
+ const root = process.env.CLAUDE_PLUGIN_ROOT;
19
+ return root ? resolve(root, "..") : undefined;
20
+ }
21
+
22
+ /**
23
+ * Regenerate the project map + plugin ecosystem map for `cwd` on SessionStart.
24
+ * Emits the ecosystem navigation context as additionalContext (or "").
25
+ * @param cwd - The working directory.
26
+ * @param now - Clock for the ecosystem map banner timestamp.
27
+ * @returns The SessionStart additionalContext response, or "".
28
+ */
29
+ export function cartoSessionStart(cwd: string, now: number = Date.now()): string {
30
+ generateProjectMap(cwd);
31
+ const ctx = generateEcosystemMap(now, pluginsDirFromEnv());
32
+ return ctx ? contextResponse("SessionStart", ctx) : "";
33
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Enrichment tracker (PostToolUse Edit/Write on `.cartographer/**\/index.md`).
3
+ * Ports `track-enrichment.py`: persists manual descriptions to a sidecar so the
4
+ * next regeneration can preserve them.
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { parseEnrichment } from "../../../policy/cartographer/entry";
9
+
10
+ /** Shape of the `.enriched.json` sidecar. */
11
+ interface EnrichedSidecar {
12
+ version: number;
13
+ entries: Record<string, string>;
14
+ }
15
+
16
+ /**
17
+ * Record manually-edited descriptions from a cartographer `index.md` into the
18
+ * adjacent `.enriched.json` sidecar. No-op for unrelated paths. No stdout.
19
+ * @param filePath - The edited file path.
20
+ */
21
+ export function trackEnrichment(filePath: string): void {
22
+ if (!filePath || !filePath.includes(".cartographer") || !filePath.endsWith("index.md")) return;
23
+ if (!existsSync(filePath)) return;
24
+ const sidecar = join(dirname(filePath), ".enriched.json");
25
+ let existing: EnrichedSidecar = { version: 1, entries: {} };
26
+ if (existsSync(sidecar)) {
27
+ try {
28
+ existing = JSON.parse(readFileSync(sidecar, "utf-8")) as EnrichedSidecar;
29
+ } catch { /* keep default */ }
30
+ }
31
+ const entries = (existing.entries ??= {});
32
+ let text = "";
33
+ try {
34
+ text = readFileSync(filePath, "utf-8");
35
+ } catch {
36
+ return;
37
+ }
38
+ for (const line of text.split("\n")) {
39
+ const e = parseEnrichment(line);
40
+ if (e) entries[e[0]] = e[1];
41
+ }
42
+ try {
43
+ writeFileSync(sidecar, JSON.stringify(existing, null, 2) + "\n", "utf-8");
44
+ } catch { /* best effort */ }
45
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Per-plugin map writer (fs). Ports `write_plugin_map.py`: writes a level-2
3
+ * `<plugin>/index.md` (indented linked tree) then recurses agents/skills/
4
+ * commands into deeper index trees. Reuses `buildTree`, `mergeLines`, `writeTree`.
5
+ */
6
+ import { mkdirSync, statSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { buildTree, type ScanRow } from "../../../policy/cartographer/build-tree";
9
+ import { mergeLines } from "./merge";
10
+ import { writeTree } from "./write-tree";
11
+
12
+ /** True when `dir` is a real directory. */
13
+ function isDir(dir: string): boolean {
14
+ try {
15
+ return statSync(dir).isDirectory();
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Write `<outputDir>/<pluginName>/index.md` (indented linked tree) and recurse
23
+ * agents/skills/commands into their own index trees rooted there.
24
+ * @param outputDir - The map root directory.
25
+ * @param pluginName - Display name of the plugin (the index subfolder).
26
+ * @param version - Plugin version ("" to omit).
27
+ * @param items - The scanned `[type, name, desc]` rows.
28
+ * @param pluginPath - Absolute source plugin directory (for recursion).
29
+ */
30
+ export function writePluginMap(outputDir: string, pluginName: string, version: string, items: ReadonlyArray<ScanRow>, pluginPath: string): void {
31
+ const pluginDir = join(outputDir, pluginName);
32
+ mkdirSync(pluginDir, { recursive: true });
33
+ const ver = version ? ` (v${version})` : "";
34
+ const tree = items.length ? buildTree(items, true) : "└── (empty)";
35
+ const newLines = `# ${pluginName}${ver}\n\n${tree}`.split("\n");
36
+ const indexPath = join(pluginDir, "index.md");
37
+ writeFileSync(indexPath, mergeLines(newLines, indexPath).join("\n") + "\n", "utf-8");
38
+
39
+ for (const section of ["agents", "skills", "commands"]) {
40
+ const src = join(pluginPath, section);
41
+ if (isDir(src)) writeTree(src, join(pluginDir, section), "../index.md");
42
+ }
43
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Recursive index.md tree writer. Ports `write_recursive.py`.
3
+ */
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { basename, join } from "node:path";
6
+ import { countFiles, getFileDesc, listChildren } from "./fs-util";
7
+ import { mergeLines } from "./merge";
8
+
9
+ /**
10
+ * Write `index.md` files mirroring `source` under `output`, recursing into
11
+ * subdirectories. Directory lines carry a file-count hint; file lines carry a
12
+ * derived description and link to the real absolute source path.
13
+ * @param source - Absolute source directory.
14
+ * @param output - Absolute output directory for the index tree.
15
+ * @param back - Relative `← back` link target ("" at the root).
16
+ * @param exclude - Directory/name set to skip.
17
+ */
18
+ export function writeTree(source: string, output: string, back: string = "", exclude?: ReadonlySet<string>): void {
19
+ const ex = exclude ?? new Set<string>();
20
+ mkdirSync(output, { recursive: true });
21
+ const { dirs, files } = listChildren(source, ex);
22
+ const lines: string[] = [`# ${basename(source)}\n`];
23
+ if (back) lines.push(`> [← back](${back})\n`);
24
+ const total = dirs.length + files.length;
25
+ let idx = 0;
26
+ for (const d of dirs) {
27
+ idx += 1;
28
+ const conn = idx === total ? "└──" : "├──";
29
+ const count = countFiles(d, ex);
30
+ const hint = count ? ` — ${count} files` : "";
31
+ lines.push(`${conn} [${basename(d)}/](./${basename(d)}/index.md)${hint}`);
32
+ writeTree(d, join(output, basename(d)), "../index.md", exclude);
33
+ }
34
+ for (const f of files) {
35
+ idx += 1;
36
+ const conn = idx === total ? "└──" : "├──";
37
+ const desc = getFileDesc(f);
38
+ const suffix = desc ? ` — ${desc}` : "";
39
+ lines.push(`${conn} [${basename(f)}](${f})${suffix}`);
40
+ }
41
+ const indexPath = join(output, "index.md");
42
+ const merged = mergeLines(lines, indexPath);
43
+ writeFileSync(indexPath, merged.join("\n") + "\n", "utf-8");
44
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Changelog research tracker (PostToolUse exa/WebFetch/WebSearch). Ports
3
+ * `track-watch-research.py`: logs research queries to
4
+ * `~/.claude/logs/00-changelog/<utc-date>-research.json`.
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { claudeHome } from "../home-state";
10
+ import { isoUtc, todayUtc } from "./security/skill-state";
11
+
12
+ /** A recorded changelog research query. */
13
+ interface ChangelogQuery {
14
+ timestamp: string;
15
+ tool: string;
16
+ query: string;
17
+ }
18
+
19
+ /**
20
+ * Append an exa/WebFetch/WebSearch query to today's changelog research log.
21
+ * No-op for other tools. No stdout (errors swallowed).
22
+ * @param tool - The tool name.
23
+ * @param input - The tool input (query/url/prompt).
24
+ * @param now - Clock.
25
+ * @param home - Home dir.
26
+ */
27
+ export function trackWatchResearch(tool: string, input: Record<string, unknown>, now: number = Date.now(), home: string = homedir()): void {
28
+ if (!tool.includes("exa") && !tool.includes("WebFetch") && !tool.includes("WebSearch")) return;
29
+ const query = String(input.query ?? input.url ?? input.prompt ?? "");
30
+ const dir = join(claudeHome(home), "logs", "00-changelog");
31
+ try {
32
+ mkdirSync(dir, { recursive: true });
33
+ const path = join(dir, `${todayUtc(now)}-research.json`);
34
+ let state: { queries: ChangelogQuery[] } = { queries: [] };
35
+ if (existsSync(path)) {
36
+ try {
37
+ state = JSON.parse(readFileSync(path, "utf-8")) as { queries: ChangelogQuery[] };
38
+ } catch { state = { queries: [] }; }
39
+ }
40
+ state.queries.push({ timestamp: isoUtc(now), tool, query });
41
+ writeFileSync(path, JSON.stringify(state, null, 2), "utf-8");
42
+ } catch { /* best effort */ }
43
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * PostToolUse (matcher "Write|Edit") for the solid scope: warn when a file
3
+ * exceeds the adaptive `SOLID_FILE_LIMIT` (set by `solidDetectStart` at
4
+ * SessionStart, keyed off the detected project type). Ports `check-file-size.py`.
5
+ * Non-blocking (`additionalContext`), unlike the fixed-ceiling core-guards gate.
6
+ */
7
+ import { extname } from "node:path";
8
+ import { readText, pathExists } from "../../util/runtime-io";
9
+ import { contextResponse } from "../../adapters/claude";
10
+ import { parseEnvInt } from "../../config/env";
11
+
12
+ /** Per-language "blank or comment" line pattern (parity `count_loc`'s `code_comment` map). */
13
+ const COMMENT_RE: Record<string, RegExp> = {
14
+ ts: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
15
+ tsx: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
16
+ js: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
17
+ jsx: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
18
+ go: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
19
+ rs: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
20
+ java: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
21
+ swift: /^\s*$|^\s*\/\/|^\s*\/\*|^\s*\*/,
22
+ php: /^\s*$|^\s*\/\/|^\s*#|^\s*\/\*|^\s*\*/,
23
+ py: /^\s*$|^\s*#|^\s*"""|^\s*'''/,
24
+ };
25
+
26
+ /** Count lines of code excluding comments/blanks for `filePath`'s extension (raw length when unmapped). */
27
+ export function countLoc(filePath: string, content: string): number {
28
+ const pattern = COMMENT_RE[extname(filePath).slice(1)];
29
+ const lines = content.split("\n");
30
+ return pattern ? lines.filter((l) => !pattern.test(l)).length : lines.length;
31
+ }
32
+
33
+ /**
34
+ * Warn when a written/edited file exceeds `SOLID_FILE_LIMIT` (default 100).
35
+ * Inert when `SOLID_PROJECT_TYPE` is absent/"unknown" (parity: the Python hook
36
+ * no-ops outside a detected project).
37
+ * @param tool - The tool name (only "Write"/"Edit" are checked).
38
+ * @param filePath - The written file's absolute path.
39
+ * @param env - Environment (defaults to `process.env`).
40
+ * @returns The PostToolUse `additionalContext` response, or `""` when clean/inert.
41
+ */
42
+ export function checkFileSize(tool: string, filePath: string, env: Record<string, string | undefined> = process.env): string {
43
+ const ptype = env.SOLID_PROJECT_TYPE ?? "";
44
+ if (!ptype || ptype === "unknown") return "";
45
+ if (tool !== "Write" && tool !== "Edit") return "";
46
+ if (!filePath || !pathExists(filePath)) return "";
47
+
48
+ let content: string;
49
+ try {
50
+ content = readText(filePath);
51
+ } catch {
52
+ return "";
53
+ }
54
+
55
+ const limit = parseEnvInt(env.SOLID_FILE_LIMIT, 100);
56
+ const loc = countLoc(filePath, content);
57
+ if (loc <= limit) return "";
58
+
59
+ const name = filePath.split("/").pop() ?? filePath;
60
+ return contextResponse("PostToolUse", `SOLID: ${name} has ${loc} lines (limit: ${limit}). Consider splitting into smaller modules.`);
61
+ }
@@ -0,0 +1,63 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { atomicWrite } from "../../../util/json-io";
5
+ import { pathExists } from "../../../util/runtime-io";
6
+ import { buildPluginRoots } from "./plugin-roots";
7
+
8
+ /** Where the last-applied plugin-cache fingerprint is persisted. */
9
+ function manifestPath(codexHome: string): string {
10
+ return join(codexHome, "fusengine", "state", "agents-cache-fingerprint.json");
11
+ }
12
+
13
+ /** sha256 over sorted "plugin=root" pairs — stable while cache resolution is unchanged. */
14
+ export function fingerprint(roots: Map<string, string>): string {
15
+ const sorted = [...roots.entries()].sort(([a], [b]) => a.localeCompare(b));
16
+ const hash = createHash("sha256");
17
+ for (const [plugin, root] of sorted) hash.update(`${plugin}=${root}\n`);
18
+ return hash.digest("hex");
19
+ }
20
+
21
+ /** The last-persisted fingerprint, or `undefined` (missing/corrupt — treated as "never resynced"). */
22
+ export function readFingerprint(codexHome: string): string | undefined {
23
+ try {
24
+ const raw = JSON.parse(readFileSync(manifestPath(codexHome), "utf8")) as { fingerprint?: string };
25
+ return raw.fingerprint;
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ /** Persist the applied fingerprint atomically (tmp+rename via {@link atomicWrite}). */
32
+ export function writeFingerprint(codexHome: string, value: string): void {
33
+ atomicWrite(manifestPath(codexHome), JSON.stringify({ fingerprint: value }));
34
+ }
35
+
36
+ /** True when a direct symlink child of `dir` points at a now-missing target. */
37
+ export function hasDanglingSymlink(dir: string): boolean {
38
+ if (!existsSync(dir)) return false;
39
+ try {
40
+ for (const name of readdirSync(dir)) {
41
+ const full = join(dir, name);
42
+ if (lstatSync(full).isSymbolicLink() && !existsSync(full)) return true;
43
+ }
44
+ } catch { /* best-effort */ }
45
+ return false;
46
+ }
47
+
48
+ /**
49
+ * Resolve the current plugin roots + fingerprint. `undefined` = nothing cached
50
+ * (fail-open — the initial installer, not this hook, owns that case).
51
+ * @param pluginsRoot - The plugin cache root.
52
+ */
53
+ export function resolveCurrentFingerprint(pluginsRoot: string): { roots: Map<string, string>; value: string } | undefined {
54
+ if (!pathExists(pluginsRoot)) return undefined;
55
+ const roots = buildPluginRoots(pluginsRoot);
56
+ if (roots.size === 0) return undefined;
57
+ return { roots, value: fingerprint(roots) };
58
+ }
59
+
60
+ /** True when a resync is due: fingerprint changed/never recorded, or a command symlink is dangling. */
61
+ export function needsResync(codexHome: string, currentValue: string, promptsDir: string): boolean {
62
+ return currentValue !== readFingerprint(codexHome) || hasDanglingSymlink(promptsDir);
63
+ }