@fusengine/harness 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +2 -2
  5. package/dist/adapters/codex/index.mjs +3 -2
  6. package/dist/adapters/cursor/index.d.mts +15 -28
  7. package/dist/adapters/cursor/index.mjs +35 -46
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +1 -1
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +1 -1
  12. package/dist/adapters/kimi/index.mjs +1 -1
  13. package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
  14. package/dist/cli/bin.mjs +429 -16
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/command-string-CALMTnwN.mjs +42 -0
  17. package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
  18. package/dist/{handle-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
  19. package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
  20. package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
  21. package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/init/index.d.mts +1 -1
  25. package/dist/init/index.mjs +1 -1
  26. package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
  27. package/dist/normalize-Dy8g9Ybl.mjs +397 -0
  28. package/dist/policy/index.d.mts +1 -1
  29. package/dist/policy/index.mjs +2 -2
  30. package/dist/prompt/index.d.mts +1 -1
  31. package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
  32. package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
  33. package/dist/runtime/index.d.mts +37 -16
  34. package/dist/runtime/index.mjs +2 -2
  35. package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
  36. package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
  37. package/package.json +3 -2
  38. package/src/adapters/claude/index.ts +92 -0
  39. package/src/adapters/cline/index.ts +36 -0
  40. package/src/adapters/codex/apply-patch.ts +94 -0
  41. package/src/adapters/codex/index.ts +84 -0
  42. package/src/adapters/codex/permission-mode.ts +16 -0
  43. package/src/adapters/cursor/context-budget.ts +144 -0
  44. package/src/adapters/cursor/context-limit.ts +115 -0
  45. package/src/adapters/cursor/context.ts +65 -0
  46. package/src/adapters/cursor/events.ts +38 -0
  47. package/src/adapters/cursor/index.ts +43 -0
  48. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  49. package/src/adapters/cursor/interfaces/types.ts +88 -0
  50. package/src/adapters/cursor/native-response.ts +39 -0
  51. package/src/adapters/cursor/native-schemas.ts +161 -0
  52. package/src/adapters/cursor/normalize.ts +188 -0
  53. package/src/adapters/cursor/plugin-root.ts +103 -0
  54. package/src/adapters/cursor/respond.ts +170 -0
  55. package/src/adapters/gemini/index.ts +36 -0
  56. package/src/adapters/hermes/index.ts +55 -0
  57. package/src/adapters/hermes/interfaces/types.ts +26 -0
  58. package/src/adapters/kimi/index.ts +96 -0
  59. package/src/adapters/kimi/interfaces/types.ts +55 -0
  60. package/src/cache/compact.ts +55 -0
  61. package/src/cache/index.ts +5 -0
  62. package/src/cache/io.ts +38 -0
  63. package/src/cache/mcp-response.ts +39 -0
  64. package/src/cache/mcp-store.ts +70 -0
  65. package/src/cache/store.ts +93 -0
  66. package/src/changelog/fetch.ts +79 -0
  67. package/src/cli/bin.ts +100 -0
  68. package/src/cli/cursor-event-scanner.ts +175 -0
  69. package/src/cli/cursor-stdin-reader.ts +65 -0
  70. package/src/cli/doctor.ts +96 -0
  71. package/src/cli/hook-io.ts +130 -0
  72. package/src/cli/hook-sound.ts +49 -0
  73. package/src/cli/index.ts +1 -0
  74. package/src/cli/json-primitive-scanner.ts +83 -0
  75. package/src/cli/run.ts +36 -0
  76. package/src/cli/scope.ts +31 -0
  77. package/src/codex-rules/build.ts +63 -0
  78. package/src/codex-rules/index.ts +8 -0
  79. package/src/codex-rules/mapping.ts +42 -0
  80. package/src/codex-rules/rules/absolute-paths.ts +52 -0
  81. package/src/codex-rules/rules/ask.ts +44 -0
  82. package/src/codex-rules/rules/chmod-ask.ts +57 -0
  83. package/src/codex-rules/rules/dd.ts +23 -0
  84. package/src/codex-rules/rules/diskutil.ts +33 -0
  85. package/src/codex-rules/rules/mkfs.ts +42 -0
  86. package/src/codex-rules/rules/privilege.ts +49 -0
  87. package/src/codex-rules/rules/remote-fetch.ts +31 -0
  88. package/src/codex-rules/rules/rm-variants.ts +58 -0
  89. package/src/codex-rules/rules/single-commands.ts +57 -0
  90. package/src/codex-rules/skip-list.ts +25 -0
  91. package/src/codex-rules/starlark.ts +46 -0
  92. package/src/codex-rules/types.ts +36 -0
  93. package/src/config/dotenv.ts +55 -0
  94. package/src/config/env.ts +10 -0
  95. package/src/config/home-dir.ts +36 -0
  96. package/src/config/index.ts +5 -0
  97. package/src/config/layout.ts +52 -0
  98. package/src/config/limits.ts +56 -0
  99. package/src/config/ttl.ts +30 -0
  100. package/src/detect/harness.ts +73 -0
  101. package/src/detect/index.ts +1 -0
  102. package/src/detect/interfaces/types.ts +18 -0
  103. package/src/freshness/agent-evidence-record.ts +95 -0
  104. package/src/freshness/agent-evidence.ts +95 -0
  105. package/src/freshness/codex-spawn-evidence.ts +91 -0
  106. package/src/freshness/doc-helpers.ts +90 -0
  107. package/src/freshness/evidence-harvest-io.ts +72 -0
  108. package/src/freshness/evidence-harvest.ts +61 -0
  109. package/src/freshness/explore-tools.ts +95 -0
  110. package/src/freshness/index.ts +2 -0
  111. package/src/freshness/query-framework.ts +73 -0
  112. package/src/freshness/ref-evidence.ts +49 -0
  113. package/src/freshness/ref-journal.ts +98 -0
  114. package/src/freshness/trivial-edit-counter.ts +30 -0
  115. package/src/index.ts +11 -0
  116. package/src/init/index.ts +2 -0
  117. package/src/init/run.ts +32 -0
  118. package/src/init/templates.ts +75 -0
  119. package/src/memory/gitignore.ts +14 -0
  120. package/src/memory/index.ts +3 -0
  121. package/src/memory/registry.ts +39 -0
  122. package/src/memory/session-roots.ts +99 -0
  123. package/src/memory/state.ts +58 -0
  124. package/src/policy/apex-authorization.ts +100 -0
  125. package/src/policy/apex-gates.ts +91 -0
  126. package/src/policy/apex-target-steps.ts +73 -0
  127. package/src/policy/apex-target.ts +49 -0
  128. package/src/policy/apex-task-context.ts +81 -0
  129. package/src/policy/apex.ts +94 -0
  130. package/src/policy/cartographer/build-tree.ts +69 -0
  131. package/src/policy/cartographer/describe.ts +53 -0
  132. package/src/policy/cartographer/entry.ts +38 -0
  133. package/src/policy/cartographer/frontmatter.ts +48 -0
  134. package/src/policy/cartographer/indicators.ts +34 -0
  135. package/src/policy/claude-md-context.ts +66 -0
  136. package/src/policy/conventions/index.ts +11 -0
  137. package/src/policy/conventions/interfaces.ts +71 -0
  138. package/src/policy/conventions/langs.ts +53 -0
  139. package/src/policy/conventions/query.ts +49 -0
  140. package/src/policy/conventions/react-hooks.ts +18 -0
  141. package/src/policy/conventions/store-or-query.ts +21 -0
  142. package/src/policy/conventions/stores.ts +56 -0
  143. package/src/policy/conventions/strip.ts +87 -0
  144. package/src/policy/conventions/verdict.ts +38 -0
  145. package/src/policy/creation-intent.ts +11 -0
  146. package/src/policy/deny-loop.ts +88 -0
  147. package/src/policy/design/allowed-write.ts +28 -0
  148. package/src/policy/design/content-checks.ts +45 -0
  149. package/src/policy/design/corpus-fs.ts +21 -0
  150. package/src/policy/design/corpus-resolve-cache.ts +49 -0
  151. package/src/policy/design/corpus-resolve.ts +99 -0
  152. package/src/policy/design/corpus.ts +92 -0
  153. package/src/policy/design/design-system-rules.ts +45 -0
  154. package/src/policy/design/flag.ts +30 -0
  155. package/src/policy/design/gates-pipeline.ts +105 -0
  156. package/src/policy/design/gates.ts +87 -0
  157. package/src/policy/design/screenshot-tools.ts +24 -0
  158. package/src/policy/design/skill-evidence.ts +34 -0
  159. package/src/policy/design/skill-gate.ts +81 -0
  160. package/src/policy/design/skill-triggers.ts +51 -0
  161. package/src/policy/design/state.ts +92 -0
  162. package/src/policy/design/template-urls.ts +54 -0
  163. package/src/policy/design/transitions.ts +68 -0
  164. package/src/policy/detect-claude-md-project-type.ts +32 -0
  165. package/src/policy/detect-framework.ts +99 -0
  166. package/src/policy/detect-primitive-checks.ts +100 -0
  167. package/src/policy/detect-primitive-lib.ts +55 -0
  168. package/src/policy/detect-project.ts +99 -0
  169. package/src/policy/edit-outcome.ts +53 -0
  170. package/src/policy/evaluate.ts +81 -0
  171. package/src/policy/expert-agents.ts +60 -0
  172. package/src/policy/file-size-scope.ts +32 -0
  173. package/src/policy/file-size.ts +99 -0
  174. package/src/policy/framework-solid-exclude.ts +52 -0
  175. package/src/policy/framework-solid-extended.ts +93 -0
  176. package/src/policy/framework-solid-gates-systems.ts +99 -0
  177. package/src/policy/framework-solid-gates.ts +98 -0
  178. package/src/policy/framework-solid.ts +88 -0
  179. package/src/policy/gemini-mcp-gate.ts +83 -0
  180. package/src/policy/git-gates.ts +40 -0
  181. package/src/policy/guards/bash-command-anchor.ts +40 -0
  182. package/src/policy/guards/bash-write-patterns.ts +140 -0
  183. package/src/policy/guards/bash-write-redirects.ts +201 -0
  184. package/src/policy/guards/bash-write-safe-paths.ts +62 -0
  185. package/src/policy/guards/bash-write.ts +76 -0
  186. package/src/policy/guards/context.ts +12 -0
  187. package/src/policy/guards/index.ts +61 -0
  188. package/src/policy/guards/install.ts +22 -0
  189. package/src/policy/guards/interface-separation-ext.ts +41 -0
  190. package/src/policy/guards/interface-separation.ts +100 -0
  191. package/src/policy/guards/protected-path.ts +100 -0
  192. package/src/policy/guards/security.ts +97 -0
  193. package/src/policy/index.ts +19 -0
  194. package/src/policy/interfaces/types.ts +46 -0
  195. package/src/policy/js-gate-route.ts +51 -0
  196. package/src/policy/lessons/lesson-gate.ts +0 -0
  197. package/src/policy/lessons/trigger-index.ts +84 -0
  198. package/src/policy/lessons/types.ts +31 -0
  199. package/src/policy/module-layout.ts +31 -0
  200. package/src/policy/nearest-manifest.ts +75 -0
  201. package/src/policy/never-approval.ts +95 -0
  202. package/src/policy/patterns.ts +80 -0
  203. package/src/policy/seo/validate.ts +51 -0
  204. package/src/policy/shadcn-project.ts +22 -0
  205. package/src/policy/shadcn-skill-gate.ts +97 -0
  206. package/src/policy/shell-read-refs.ts +96 -0
  207. package/src/policy/skill-path.ts +46 -0
  208. package/src/policy/skill-patterns/laravel.ts +40 -0
  209. package/src/policy/skill-patterns/nextjs.ts +29 -0
  210. package/src/policy/skill-patterns/react.ts +27 -0
  211. package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
  212. package/src/policy/skill-patterns/shadcn.ts +66 -0
  213. package/src/policy/skill-patterns/swift.ts +30 -0
  214. package/src/policy/skill-patterns/tailwind.ts +40 -0
  215. package/src/policy/skill-source.ts +52 -0
  216. package/src/policy/skill-trigger-patterns.ts +33 -0
  217. package/src/policy/skill-triggers.ts +108 -0
  218. package/src/policy/tailwind-skill-gate.ts +63 -0
  219. package/src/policy/trivial-edits.ts +52 -0
  220. package/src/policy/verbosity.ts +39 -0
  221. package/src/prompt/index.ts +1 -0
  222. package/src/prompt/types.ts +36 -0
  223. package/src/refs/discover.ts +69 -0
  224. package/src/refs/frontmatter.ts +24 -0
  225. package/src/refs/index.ts +4 -0
  226. package/src/refs/loader.ts +59 -0
  227. package/src/refs/ref-key.ts +41 -0
  228. package/src/refs/router.ts +63 -0
  229. package/src/refs/types.ts +25 -0
  230. package/src/runtime/activity.ts +99 -0
  231. package/src/runtime/apply-patch-gate.ts +41 -0
  232. package/src/runtime/burst-window.ts +21 -0
  233. package/src/runtime/codex-shell-tool.ts +37 -0
  234. package/src/runtime/command-string.ts +37 -0
  235. package/src/runtime/confirm/codex-confirm.ts +126 -0
  236. package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
  237. package/src/runtime/confirm/confirm-code.ts +33 -0
  238. package/src/runtime/confirm/confirm-gate.ts +65 -0
  239. package/src/runtime/confirm/confirm-irreversible.ts +32 -0
  240. package/src/runtime/confirm/confirm-pending.ts +34 -0
  241. package/src/runtime/confirm/confirm-state.ts +56 -0
  242. package/src/runtime/confirm/confirm-subagent.ts +112 -0
  243. package/src/runtime/confirm/confirm-submit.ts +38 -0
  244. package/src/runtime/core-guards-active.ts +84 -0
  245. package/src/runtime/core-guards-wired.ts +47 -0
  246. package/src/runtime/deny-loop-store.ts +94 -0
  247. package/src/runtime/deny-notice.ts +52 -0
  248. package/src/runtime/design-cache-resolve.ts +51 -0
  249. package/src/runtime/design-content-gate.ts +95 -0
  250. package/src/runtime/design-files-gate.ts +114 -0
  251. package/src/runtime/design-helpers.ts +93 -0
  252. package/src/runtime/design-lifecycle.ts +39 -0
  253. package/src/runtime/design-read-credit.ts +28 -0
  254. package/src/runtime/design.ts +116 -0
  255. package/src/runtime/dev-context.ts +43 -0
  256. package/src/runtime/dry-patterns.ts +32 -0
  257. package/src/runtime/dry.ts +96 -0
  258. package/src/runtime/fragment-registry.ts +33 -0
  259. package/src/runtime/framework-skill-gate.ts +79 -0
  260. package/src/runtime/fs-cleanup.ts +71 -0
  261. package/src/runtime/gate-apex.ts +81 -0
  262. package/src/runtime/gate-helpers.ts +52 -0
  263. package/src/runtime/gate-input.ts +26 -0
  264. package/src/runtime/gate.ts +128 -0
  265. package/src/runtime/handle-post.ts +104 -0
  266. package/src/runtime/handle-pre.ts +137 -0
  267. package/src/runtime/handle-scope-async.ts +29 -0
  268. package/src/runtime/handle-types.ts +27 -0
  269. package/src/runtime/handle.ts +197 -0
  270. package/src/runtime/home-state.ts +55 -0
  271. package/src/runtime/index.ts +18 -0
  272. package/src/runtime/inform.ts +28 -0
  273. package/src/runtime/inject-budget-recap.ts +47 -0
  274. package/src/runtime/inject-budget.ts +81 -0
  275. package/src/runtime/inject-context.ts +85 -0
  276. package/src/runtime/inject-dedup-exclusive.ts +98 -0
  277. package/src/runtime/inject-dedup.ts +89 -0
  278. package/src/runtime/is-agent-tool.ts +19 -0
  279. package/src/runtime/lifecycle/agent-files.ts +49 -0
  280. package/src/runtime/lifecycle/agent-memory.ts +82 -0
  281. package/src/runtime/lifecycle/agent-transcript.ts +80 -0
  282. package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
  283. package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
  284. package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
  285. package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
  286. package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
  287. package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
  288. package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
  289. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +106 -0
  290. package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
  291. package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
  292. package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
  293. package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
  294. package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
  295. package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
  296. package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
  297. package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
  298. package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
  299. package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
  300. package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
  301. package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
  302. package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
  303. package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
  304. package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
  305. package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
  306. package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
  307. package/src/runtime/lifecycle/aipilot/types.ts +72 -0
  308. package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
  309. package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
  310. package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
  311. package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
  312. package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
  313. package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
  314. package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
  315. package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
  316. package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
  317. package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
  318. package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
  319. package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
  320. package/src/runtime/lifecycle/changelog-research.ts +43 -0
  321. package/src/runtime/lifecycle/check-file-size.ts +61 -0
  322. package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
  323. package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
  324. package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
  325. package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
  326. package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
  327. package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
  328. package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
  329. package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
  330. package/src/runtime/lifecycle/dispatch.ts +109 -0
  331. package/src/runtime/lifecycle/failure-lesson.ts +55 -0
  332. package/src/runtime/lifecycle/index.ts +34 -0
  333. package/src/runtime/lifecycle/inject-rules.ts +50 -0
  334. package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
  335. package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
  336. package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
  337. package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
  338. package/src/runtime/lifecycle/lessons/state.ts +23 -0
  339. package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
  340. package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
  341. package/src/runtime/lifecycle/memory/client.ts +65 -0
  342. package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
  343. package/src/runtime/lifecycle/memory/recall.ts +30 -0
  344. package/src/runtime/lifecycle/memory/salience.ts +31 -0
  345. package/src/runtime/lifecycle/memory/state.ts +49 -0
  346. package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
  347. package/src/runtime/lifecycle/post-compact.ts +38 -0
  348. package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
  349. package/src/runtime/lifecycle/post-tracking.ts +40 -0
  350. package/src/runtime/lifecycle/pre-compact.ts +38 -0
  351. package/src/runtime/lifecycle/rules-root.ts +99 -0
  352. package/src/runtime/lifecycle/security/check-skill.ts +63 -0
  353. package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
  354. package/src/runtime/lifecycle/security/scan.ts +76 -0
  355. package/src/runtime/lifecycle/security/skill-state.ts +47 -0
  356. package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
  357. package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
  358. package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
  359. package/src/runtime/lifecycle/session-end.ts +17 -0
  360. package/src/runtime/lifecycle/session-start.ts +57 -0
  361. package/src/runtime/lifecycle/snapshot/board.ts +27 -0
  362. package/src/runtime/lifecycle/snapshot/format.ts +60 -0
  363. package/src/runtime/lifecycle/snapshot/git.ts +71 -0
  364. package/src/runtime/lifecycle/snapshot/index.ts +52 -0
  365. package/src/runtime/lifecycle/snapshot/version.ts +34 -0
  366. package/src/runtime/lifecycle/solid-detect.ts +56 -0
  367. package/src/runtime/lifecycle/stop-core.ts +35 -0
  368. package/src/runtime/lifecycle/subagent-cache.ts +61 -0
  369. package/src/runtime/lifecycle/task-completed.ts +94 -0
  370. package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
  371. package/src/runtime/lifecycle/teammate-idle.ts +28 -0
  372. package/src/runtime/lifecycle/tool-failure.ts +24 -0
  373. package/src/runtime/lifecycle/track-changes.ts +61 -0
  374. package/src/runtime/lifecycle/validate-solid.ts +70 -0
  375. package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
  376. package/src/runtime/lifecycle-bridge.ts +74 -0
  377. package/src/runtime/mcp-key.ts +44 -0
  378. package/src/runtime/mcp-tool-name.ts +92 -0
  379. package/src/runtime/mcp.ts +91 -0
  380. package/src/runtime/modular.ts +77 -0
  381. package/src/runtime/normalize.ts +107 -0
  382. package/src/runtime/notices.ts +91 -0
  383. package/src/runtime/notification-sound.ts +49 -0
  384. package/src/runtime/notifications.ts +77 -0
  385. package/src/runtime/paths.ts +59 -0
  386. package/src/runtime/post-fanout.ts +41 -0
  387. package/src/runtime/post-outcome.ts +61 -0
  388. package/src/runtime/pre-allow.ts +97 -0
  389. package/src/runtime/precommit.ts +60 -0
  390. package/src/runtime/prompt-text.ts +38 -0
  391. package/src/runtime/receipt-capture.ts +34 -0
  392. package/src/runtime/record.ts +26 -0
  393. package/src/runtime/respond.ts +92 -0
  394. package/src/runtime/solid-file-size-gate.ts +58 -0
  395. package/src/runtime/solid-pre.ts +53 -0
  396. package/src/runtime/storage.ts +9 -0
  397. package/src/statusline/colors.ts +31 -0
  398. package/src/statusline/constants.ts +17 -0
  399. package/src/statusline/formatters.ts +54 -0
  400. package/src/statusline/index.ts +4 -0
  401. package/src/statusline/progress-bar.ts +46 -0
  402. package/src/tracking/codex-post-failure.ts +73 -0
  403. package/src/tracking/index.ts +2 -0
  404. package/src/tracking/integrity.ts +99 -0
  405. package/src/tracking/one-shot-dedup.ts +34 -0
  406. package/src/tracking/one-shot-failure.ts +42 -0
  407. package/src/tracking/one-shot-store.ts +95 -0
  408. package/src/tracking/one-shot.ts +93 -0
  409. package/src/tracking/receipts.ts +95 -0
  410. package/src/tracking/session-state.ts +94 -0
  411. package/src/tracking/store.ts +96 -0
  412. package/src/tracking/track-compact.ts +100 -0
  413. package/src/tracking/track-diff.ts +50 -0
  414. package/src/tracking/track-journal.ts +97 -0
  415. package/src/tracking/track-lock-sync.ts +83 -0
  416. package/src/tracking/track-lock.ts +67 -0
  417. package/src/util/compact-json.ts +23 -0
  418. package/src/util/index.ts +2 -0
  419. package/src/util/json-io.ts +47 -0
  420. package/src/util/project-root.ts +41 -0
  421. package/src/util/runtime-io.ts +85 -0
  422. package/src/util/semver.ts +30 -0
@@ -0,0 +1,100 @@
1
+ import { resolveSessions, type AuthEntry } from "../freshness/doc-helpers";
2
+ import { routeReferences } from "../refs/router";
3
+ import { DEFAULT_TTL_SEC, ttlLabel } from "../config/ttl";
4
+ import { CONTEXT7_SOURCE, getSkillSource } from "./skill-source";
5
+ import type { ApexContext, ApexGate } from "./apex";
6
+
7
+ /** Pending doc-credit target written on a Check-1 deny (parity enforce-apex-phases.ts:80 `state.target`). */
8
+ export interface SessionTarget {
9
+ project: string;
10
+ framework: string;
11
+ set_by: string;
12
+ set_at: string;
13
+ }
14
+
15
+ /**
16
+ * Credit a doc consultation onto one framework's auth entry (parity
17
+ * track-doc-consultation.py): the once-per-session Check-2 fields
18
+ * (`doc_sessions` + `sources`) plus the per-framework Check-1 stamp
19
+ * (`sessions` + `doc_consulted`). Pure.
20
+ */
21
+ export function creditDocConsultation(prev: AuthEntry | undefined, sessionId: string, source: string, nowIso: string): AuthEntry {
22
+ const p = prev ?? {};
23
+ const add = (list: string[], v: string): string[] => (v && !list.includes(v) ? [...list, v] : list);
24
+ return {
25
+ ...p,
26
+ sessions: add(resolveSessions(p), sessionId),
27
+ doc_sessions: add(p.doc_sessions ?? [], sessionId),
28
+ sources: add(p.sources ?? (p.source ? [p.source] : []), source),
29
+ doc_consulted: nowIso,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Check 1 (parity enforce-apex-phases.ts isAuthorized): the framework's doc or
35
+ * skill was consulted in THIS session (resolveSessions) AND the stamp is
36
+ * younger than the TTL (FUSE_ENFORCE_TTL_SEC via src/config/ttl.ts).
37
+ */
38
+ export function isAuthorized(auth: AuthEntry | undefined, sessionId: string, now: number, ttlMs: number): boolean {
39
+ if (!auth?.doc_consulted || !resolveSessions(auth).includes(sessionId)) return false;
40
+ const readEpoch = Date.parse(auth.doc_consulted);
41
+ return !Number.isNaN(readEpoch) && now - readEpoch < ttlMs;
42
+ }
43
+
44
+ /**
45
+ * A fresh same-session `skills/**.md` read whose path names the framework also
46
+ * satisfies Check 1 (parity track-doc-consultation.py, which stamps
47
+ * `doc_consulted` on `skills/.*\.md` Reads; the harness records those Reads in
48
+ * `refsRead`/`refsReadAt` instead, and the track file is per-session).
49
+ */
50
+ function skillReadAuthorized(ctx: ApexContext, now: number, ttlMs: number): boolean {
51
+ // Aliases keep every deny-promised path unblockable: laravel skills say "php",
52
+ // and tailwind's SKILL.md lives under "tailwindcss[-v4]" where \btailwind\b
53
+ // never matches (the boundary fails before the "c").
54
+ const alias: Record<string, string> = { laravel: "laravel|php", tailwind: "tailwindcss?" };
55
+ const nameRe = new RegExp(`\\b(${alias[ctx.framework] ?? ctx.framework})\\b`, "i");
56
+ return (ctx.refsRead ?? []).some((p) => {
57
+ if (!/skills\/.*\.md$/i.test(p) || !nameRe.test(p)) return false;
58
+ const at = ctx.refsReadAt?.[p];
59
+ return at !== undefined && now - at < ttlMs;
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Gate (Check 1): deny until the framework's doc/skill is freshly consulted.
65
+ * The deny lists the routed SOLID refs when refs are loaded (parity
66
+ * formatRoutedDeny), else the framework's skill source (parity getSkillSource:
67
+ * a real installed SKILL.md path, or the context7 consigne). The RUNTIME
68
+ * caller persists `state.target` on deny (recordTarget, parity :80-81) so
69
+ * recordDoc can cross-credit this framework on the next doc consultation.
70
+ */
71
+ export const apexAuthorizationGate: ApexGate = (ctx) => {
72
+ const windowMs = ctx.windowMs ?? DEFAULT_TTL_SEC * 1000;
73
+ const now = ctx.now ?? Date.now();
74
+ if (isAuthorized(ctx.authorizations?.[ctx.framework], ctx.sessionId, now, windowMs)) return null;
75
+ if (skillReadAuthorized(ctx, now, windowMs)) return null;
76
+ const ttl = ttlLabel(Math.round(windowMs / 1000));
77
+ const title = `APEX: ${ctx.framework} documentation required`;
78
+ const routed = ctx.refs?.length ? routeReferences(ctx.refs, ctx.filePath, ctx.content) : null;
79
+ if (!routed) {
80
+ // Parity enforce-apex-phases.ts:82-87: getSkillSource (skill-source.ts)
81
+ // resolves the REAL installed SKILL.md, or yields the context7 consigne —
82
+ // never SOLID_REF's phantom `${PLUGINS_DIR}/generic/SKILL.md`.
83
+ const src = getSkillSource(ctx.framework);
84
+ return {
85
+ kind: "block",
86
+ title,
87
+ reason: `APEX: Read doc first (expires every ${ttl}) for ${ctx.framework}! Source: ${src}`,
88
+ actions: [src === CONTEXT7_SOURCE ? `Query ${CONTEXT7_SOURCE} for ${ctx.framework} docs` : `Read ${src}`],
89
+ };
90
+ }
91
+ const optional = routed.optional.map((r) => r.meta.filePath);
92
+ const reason = [
93
+ // H2b: name the full set (required + Optional) and invite the retry.
94
+ `APEX: Read ALL SOLID references below (including Optional), then retry (expires every ${ttl}) for ${ctx.framework}.`,
95
+ `Editing: ${ctx.filePath}`,
96
+ ...(optional.length ? [`Optional: ${optional.join(", ")}`] : []),
97
+ `Full skill: ${routed.skillPath}`,
98
+ ].join("\n");
99
+ return { kind: "block", title, reason, actions: routed.required.map((r) => r.meta.filePath) };
100
+ };
@@ -0,0 +1,91 @@
1
+ import { routeReferences } from "../refs/router";
2
+ import { skillRefKey } from "../refs/ref-key";
3
+ import type { Prompt } from "../prompt/types";
4
+ import { DEFAULT_TTL_SEC, ttlLabel } from "../config/ttl";
5
+ import { PLUGINS_DIR, SOLID_REF } from "./file-size";
6
+ import type { ApexContext, ApexGate } from "./apex";
7
+
8
+ /**
9
+ * True when the SOLID ref read at `path` still counts. Parity require-solid-read.py
10
+ * `_check_solid_read` (FUSE_ENFORCE_TTL_SEC, default 120s): a SOLID read older than
11
+ * the window has expired and must be redone. PARITY NOTE: the TTL applies ONLY to
12
+ * this gate — Python TTL-izes SOLID reads exclusively; the other refsRead consumers
13
+ * (skillTriggerGate, shadcn/tailwind/design gates) stay session-scoped, so this
14
+ * predicate must never leak into them. Backward compat: a path with no `refsReadAt`
15
+ * stamp (tracks recorded before the field existed) or a context without a clock
16
+ * (`now` absent) counts as read.
17
+ */
18
+ function refReadFresh(ctx: ApexContext, path: string, windowMs: number): boolean {
19
+ const at = ctx.refsReadAt?.[path];
20
+ return at === undefined || ctx.now === undefined || ctx.now - at < windowMs;
21
+ }
22
+
23
+ /** Gate: the routed SOLID references for this edit must have been read within the TTL. */
24
+ export const solidReadGate: ApexGate = (ctx: ApexContext) => {
25
+ if (!ctx.refs?.length) return null;
26
+ const routed = routeReferences(ctx.refs, ctx.filePath, ctx.content);
27
+ const windowMs = ctx.windowMs ?? DEFAULT_TTL_SEC * 1000;
28
+ const ttl = ttlLabel(Math.round(windowMs / 1000));
29
+ if (!routed) {
30
+ // Parity require-solid-read.py::_build_reason (the `not routed` branch):
31
+ // refs ARE loaded but none score for this file — Python still denies here,
32
+ // pointing at the framework's SKILL.md. Note: `!ctx.refs?.length` above
33
+ // (no refs installed at all) intentionally stays allow — discoverRefs()'s
34
+ // documented contract ("with no refs the SOLID-read gate stays off").
35
+ const ref = SOLID_REF[ctx.framework] ?? "generic/";
36
+ return {
37
+ kind: "block",
38
+ title: `APEX: no SOLID reference matched ${ctx.filePath}`,
39
+ reason: `${ctx.refs.length} SOLID reference(s) are loaded but none scored for this edit (expires every ${ttl}) — read the framework skill directly instead.`,
40
+ actions: [`Read ${PLUGINS_DIR}/${ref}SKILL.md`],
41
+ };
42
+ }
43
+ // Only reads still inside the TTL window count — a stale read re-blocks
44
+ // (parity require-solid-read.py, which re-validates the TTL on EVERY edit,
45
+ // making the "(expires every …)" wording below actually true).
46
+ const read = new Set((ctx.refsRead ?? []).filter((p) => refReadFresh(ctx, p, windowMs)));
47
+ // A sub-agent/teammate reads the SAME skill material through a different
48
+ // root than the marketplace-first path discoverRefs put in ctx.refs (a
49
+ // version cache, or a standalone .codex/.cursor/.agents dir) — normalize
50
+ // both sides to the `skills/<skill>/...` suffix so that read still credits.
51
+ const readKeys = new Set([...read].map(skillRefKey).filter((k): k is string => k !== null));
52
+ const credited = (path: string): boolean => {
53
+ if (read.has(path)) return true;
54
+ const key = skillRefKey(path);
55
+ return key !== null && readKeys.has(key);
56
+ };
57
+ // Parity with the 3 other refsRead-consuming gates (skillTriggerGate's
58
+ // `skills/${s}/` substring match, shadcnBaseSkillRead, designSkillRead):
59
+ // reading the ref's PARENT SKILL.md counts as proof of consultation too,
60
+ // not just the exact ref file path.
61
+ const skillRead = !!routed.skillPath && credited(routed.skillPath);
62
+ const missing = skillRead ? [] : routed.required.map((r) => r.meta.filePath).filter((p) => !credited(p));
63
+ if (missing.length === 0) return null;
64
+ const optional = routed.optional.map((r) => r.meta.filePath);
65
+ const reason = [
66
+ // H2b: the credit needs the routed refs read — say ALL of them (the
67
+ // Optional line counts too) and that a retry is EXPECTED once read.
68
+ `Read ALL references below (including Optional), then retry — required before editing ${ctx.filePath} (expires every ${ttl}):`,
69
+ ...(optional.length ? [`Optional: ${optional.join(", ")}`] : []),
70
+ `Full skill: ${routed.skillPath}`,
71
+ ].join("\n");
72
+ return {
73
+ kind: "block",
74
+ title: `APEX: read SOLID references for ${ctx.framework}`,
75
+ reason,
76
+ actions: missing,
77
+ };
78
+ };
79
+
80
+ /** Gate: the required prior agents (explore + research) must have run within the window. */
81
+ export const freshnessGate: ApexGate = (ctx: ApexContext): Prompt | null => {
82
+ if (ctx.agentsFresh !== false) return null;
83
+ const missing = ctx.missingAgents?.length ? ctx.missingAgents : ["explore-codebase", "research-expert"];
84
+ const ttl = ttlLabel(Math.round((ctx.windowMs ?? DEFAULT_TTL_SEC * 1000) / 1000));
85
+ return {
86
+ kind: "block",
87
+ title: "APEX: explore + research required",
88
+ reason: `Run ${missing.join(" and ")} within the freshness window (${ttl} TTL) before editing ${ctx.framework}.`,
89
+ actions: missing.map((name) => `Launch the ${name} agent`),
90
+ };
91
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Per-target phrasing tables for the APEX task-tracking step trio/pair
3
+ * (view-pending / mark-in-progress / mark-done) — split out of
4
+ * `apex-target.ts` to keep both files under the SOLID 100-line ceiling.
5
+ * Kimi Code CLI's native tool is `TodoList` (single tool, full-list re-emit
6
+ * per call; status enum is `pending | in_progress | done` — never
7
+ * `completed`, that is Claude's idiom; omitting `todos` queries the current
8
+ * list without mutating it). Kimi has no documented auto-commit tied to a
9
+ * status transition, unlike Claude's `TaskUpdate(completed)` — so these
10
+ * phrasings must NOT promise one. `TaskList` is a distinct Kimi tool for
11
+ * async background tasks and must never be used here.
12
+ */
13
+ import type { HarnessId } from "../detect/interfaces/types";
14
+ import type { ApexTaskSteps, ApexAgentSteps } from "./interfaces/types";
15
+
16
+ /** Codex/Kimi step-trio overrides; any other target falls back to {@link DEFAULT_TASK_STEPS}. */
17
+ const TASK_STEPS: Partial<Record<HarnessId, ApexTaskSteps>> = {
18
+ codex: {
19
+ step3: "update_plan → review the current plan",
20
+ step4: "update_plan → mark the active step in_progress before starting",
21
+ step7: "update_plan → mark the step completed when done",
22
+ },
23
+ kimi: {
24
+ step3: "TodoList → review the current list (omit todos to query)",
25
+ step4: "TodoList → mark the active item in_progress before starting",
26
+ step7: "TodoList → mark the item done when finished",
27
+ },
28
+ };
29
+
30
+ /** Default (Claude Code) step trio — zero-regression fallback for any unlisted target. */
31
+ const DEFAULT_TASK_STEPS: ApexTaskSteps = {
32
+ step3: "TaskList → see pending tasks",
33
+ step4: "TaskUpdate(in_progress) → before starting",
34
+ step7: "TaskUpdate(completed) → triggers auto-commit",
35
+ };
36
+
37
+ /**
38
+ * Resolve the step3/step4/step7 phrasing for a harness target (APEX
39
+ * task-context injection). Replaces the former `isCodex` binary ternary.
40
+ * @param id - Harness id.
41
+ * @returns The {@link ApexTaskSteps} for that target.
42
+ */
43
+ export function apexTaskSteps(id: string): ApexTaskSteps {
44
+ return TASK_STEPS[id as HarnessId] ?? DEFAULT_TASK_STEPS;
45
+ }
46
+
47
+ /** Codex/Kimi before/done overrides; any other target falls back to {@link DEFAULT_AGENT_STEPS}. */
48
+ const AGENT_STEPS: Partial<Record<HarnessId, ApexAgentSteps>> = {
49
+ codex: {
50
+ beforeStart: "Use update_plan → mark the active step in_progress before starting",
51
+ whenDone: "update_plan → mark the step completed when done",
52
+ },
53
+ kimi: {
54
+ beforeStart: "Use TodoList → mark the active item in_progress before starting",
55
+ whenDone: "TodoList → mark the item done when finished",
56
+ },
57
+ };
58
+
59
+ /** Default (Claude Code) before/done pair — zero-regression fallback for any unlisted target. */
60
+ const DEFAULT_AGENT_STEPS: ApexAgentSteps = {
61
+ beforeStart: "Use TaskUpdate(taskId, status: in_progress) before starting",
62
+ whenDone: "TaskUpdate(taskId, status: completed) triggers auto-commit",
63
+ };
64
+
65
+ /**
66
+ * Resolve the beforeStart/whenDone phrasing for a harness target (APEX
67
+ * sub-agent injection). Replaces the former `isCodex` binary ternary.
68
+ * @param id - Harness id.
69
+ * @returns The {@link ApexAgentSteps} for that target.
70
+ */
71
+ export function apexAgentSteps(id: string): ApexAgentSteps {
72
+ return AGENT_STEPS[id as HarnessId] ?? DEFAULT_AGENT_STEPS;
73
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Harness-target helpers for the APEX context builders. Single source of
3
+ * truth for the home-dir segment / root doc name / plan-tool idiom that
4
+ * differ between Claude Code and Codex (and beyond), so the builders in
5
+ * `claude-md-context.ts`, `apex-task-context.ts`, and `inject-apex.ts` never
6
+ * hardcode `.claude`/`CLAUDE.md`/`TaskCreate` again. Runtime `id` values flow
7
+ * through the pipeline as plain `string` (see `handleHook(id: string, ...)`),
8
+ * so these helpers accept `string` and narrow internally — same idiom as the
9
+ * existing `HOME_DIR[id] ?? ".claude"` precedent in `cartographer/detect.ts`.
10
+ */
11
+ import { HOME_DIR } from "../config/dotenv";
12
+ import type { HarnessId } from "../detect/interfaces/types";
13
+
14
+ /**
15
+ * Home-dir segment for a harness target (".claude", ".codex", ...). Falls
16
+ * back to ".claude" for any harness without a {@link HOME_DIR} entry.
17
+ * @param id - Harness id (e.g. "claude-code", "codex").
18
+ * @returns The home-dir segment, e.g. ".claude".
19
+ */
20
+ export function harnessHomeSegment(id: string): string {
21
+ return HOME_DIR[id as HarnessId] ?? ".claude";
22
+ }
23
+
24
+ /**
25
+ * Root instructions doc name for a harness target: "AGENTS.md" for Codex and
26
+ * Kimi Code CLI (both documented conventions), "CLAUDE.md" for every other
27
+ * target (including the default/unknown fallback).
28
+ * @param id - Harness id.
29
+ * @returns The doc file name.
30
+ */
31
+ export function apexDocName(id: string): string {
32
+ return id === "codex" || id === "kimi" ? "AGENTS.md" : "CLAUDE.md";
33
+ }
34
+
35
+ /**
36
+ * Native plan/track tool name for a harness target: Codex's `update_plan`,
37
+ * Kimi Code CLI's `TodoList`, or Claude's `TaskCreate` (default). Covers only
38
+ * the PLAN-step line — other target-specific tool references (`TaskList`,
39
+ * `TaskUpdate`) are resolved via `apexTaskSteps`/`apexAgentSteps` in
40
+ * `apex-target-steps.ts` since their surrounding prose differs structurally,
41
+ * not just by token.
42
+ * @param id - Harness id.
43
+ * @returns The plan-tool name.
44
+ */
45
+ export function apexPlanTool(id: string): string {
46
+ if (id === "codex") return "update_plan";
47
+ if (id === "kimi") return "TodoList";
48
+ return "TaskCreate";
49
+ }
@@ -0,0 +1,81 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveMaxLines } from "../config/limits";
4
+ import { harnessHomeSegment } from "./apex-target";
5
+ import { apexTaskSteps } from "./apex-target-steps";
6
+
7
+ /** Parsed task state injected into a Task sub-agent prompt. */
8
+ export interface ApexTaskState {
9
+ /** Current task id (defaults to "1"). */
10
+ id: string;
11
+ /** Task subject (defaults to ""). */
12
+ subject: string;
13
+ /** Current phase (defaults to "analyze"). */
14
+ phase: string;
15
+ /** Comma-joined consulted doc keys, or "none". */
16
+ docs: string;
17
+ }
18
+
19
+ /**
20
+ * Read the current task state from `task.json`, reproducing the legacy Python
21
+ * logic. Any read/parse error falls back to `("1", "", "analyze", "none")`.
22
+ * @param taskFile - Absolute path to `.claude/apex/task.json`.
23
+ * @returns The parsed {@link ApexTaskState}.
24
+ */
25
+ export function loadApexTaskState(taskFile: string): ApexTaskState {
26
+ try {
27
+ const data = JSON.parse(readFileSync(taskFile, "utf-8")) as Record<string, unknown>;
28
+ const id = String(data.current_task ?? "1");
29
+ const tasks = (data.tasks as Record<string, unknown> | undefined) ?? {};
30
+ const task = (tasks[id] as Record<string, unknown> | undefined) ?? {};
31
+ const subject = typeof task.subject === "string" ? task.subject : "";
32
+ const phase = typeof task.phase === "string" ? task.phase : "analyze";
33
+ const consultedMap = (task.doc_consulted as Record<string, unknown> | undefined) ?? {};
34
+ const consulted = Object.entries(consultedMap)
35
+ .filter(([, v]) => typeof v === "object" && v !== null && (v as Record<string, unknown>).consulted === true)
36
+ .map(([k]) => k);
37
+ return { id, subject, phase, docs: consulted.join(", ") || "none" };
38
+ } catch {
39
+ return { id: "1", subject: "", phase: "analyze", docs: "none" };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Build the APEX context string injected into a Task sub-agent prompt.
45
+ * @param state - The parsed task state.
46
+ * @param maxLines - SOLID per-file line ceiling.
47
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
48
+ * @returns The injection text.
49
+ */
50
+ export function buildApexTaskContext(state: ApexTaskState, maxLines: number, id: string = "claude-code"): string {
51
+ const seg = harnessHomeSegment(id);
52
+ const { step3, step4, step7 } = apexTaskSteps(id);
53
+ return (
54
+ `⚠️ APEX MODE - Read ${seg}/apex/AGENTS.md for rules\n\n` +
55
+ `Current: Task #${state.id} - ${state.subject} (Phase: ${state.phase})\n` +
56
+ `Docs consulted: ${state.docs}\n\n` +
57
+ `Agent must:\n` +
58
+ `1. Read task.json → find last 3 completed tasks\n` +
59
+ `2. Read their notes in docs/ (task-{ID}-{subject}.md)\n` +
60
+ `3. ${step3}\n` +
61
+ `4. ${step4}\n` +
62
+ `5. Apply SOLID (files < ${maxLines} lines)\n` +
63
+ `6. Write notes to docs/task-{ID}-{subject}.md\n` +
64
+ `7. ${step7}`
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Build the PreToolUse Task injection, gated on the existence of the
70
+ * project's target apex dir (`.claude/apex/`, `.codex/apex/`, ...). Returns
71
+ * `null` when APEX is not active (no dir).
72
+ * @param projectRoot - `CLAUDE_PROJECT_DIR` or cwd.
73
+ * @param id - Harness target id (defaults to "claude-code" — zero-regression default).
74
+ * @returns The injection text, or `null` to emit nothing.
75
+ */
76
+ export function buildApexTaskInjection(projectRoot: string, id: string = "claude-code"): string | null {
77
+ const apexDir = join(projectRoot, harnessHomeSegment(id), "apex");
78
+ if (!existsSync(apexDir)) return null;
79
+ const state = loadApexTaskState(join(apexDir, "task.json"));
80
+ return buildApexTaskContext(state, resolveMaxLines(), id);
81
+ }
@@ -0,0 +1,94 @@
1
+ import { formatDocDeny, isDocConsulted, type AuthEntry } from "../freshness/doc-helpers";
2
+ import type { RefMeta } from "../refs/types";
3
+ import type { Prompt } from "../prompt/types";
4
+
5
+ /**
6
+ * Session context for the stateful APEX gates. The harness adapter supplies this
7
+ * (the package owns the gate LOGIC; recording the session activity is the
8
+ * adapter's tracking layer).
9
+ */
10
+ export interface ApexContext {
11
+ sessionId: string;
12
+ framework: string;
13
+ filePath: string;
14
+ content: string;
15
+ /** Doc-consultation authorizations from session state (Context7/Exa). */
16
+ authorizations?: Record<string, AuthEntry>;
17
+ /** Available SOLID references for the framework's skill. */
18
+ refs?: RefMeta[];
19
+ /** Absolute paths of SOLID refs already read this session. */
20
+ refsRead?: string[];
21
+ /**
22
+ * Epoch-ms read timestamp per `refsRead` path (parity track-solid-reads.py
23
+ * `solid_reads[].timestamp`). PARITY: the TTL applies ONLY to solidReadGate —
24
+ * Python TTL-izes SOLID reads exclusively (require-solid-read.py); the
25
+ * skill-trigger/design/shadcn gates stay session-scoped (no TTL). A path in
26
+ * `refsRead` with no stamp here (tracks recorded before this field existed)
27
+ * counts as read — backward compat.
28
+ */
29
+ refsReadAt?: Record<string, number>;
30
+ /** Current epoch ms for the SOLID-read TTL check; absent → reads never expire. */
31
+ now?: number;
32
+ /** Whether the required prior agents (explore + research) ran within the freshness window. */
33
+ agentsFresh?: boolean;
34
+ /** Names of REQUIRED_AGENTS that have NOT run fresh (subset), for a precise freshnessGate message. Absent → generic wording. */
35
+ missingAgents?: string[];
36
+ /** Freshness window in ms: labels the block messages' TTL (e.g. "2min") and bounds the SOLID-read TTL in solidReadGate. */
37
+ windowMs?: number;
38
+ /** Whether brainstorming is required for this edit (creation intent on a new file). */
39
+ brainstormRequired?: boolean;
40
+ /** Whether the brainstorming agent ran within the window. */
41
+ brainstormFresh?: boolean;
42
+ }
43
+
44
+ /** A single APEX gate: returns a blocking {@link Prompt}, or null to pass. */
45
+ export type ApexGate = (ctx: ApexContext) => Prompt | null;
46
+
47
+ /** Gate: BOTH Context7 AND Exa (or a web fallback alone) must have been consulted this session. */
48
+ export const docConsultedGate: ApexGate = (ctx) =>
49
+ isDocConsulted(ctx.authorizations, ctx.sessionId)
50
+ ? null
51
+ : {
52
+ kind: "block",
53
+ title: "APEX: documentation not consulted",
54
+ reason: formatDocDeny(ctx.framework),
55
+ actions: ["Use BOTH mcp__context7__query-docs AND mcp__exa__web_search_exa, or a web fallback alone (WebSearch/WebFetch)"],
56
+ };
57
+
58
+ /** Gate: brainstorming must precede creating new files when flagged. */
59
+ export const brainstormGate: ApexGate = (ctx) =>
60
+ ctx.brainstormRequired && ctx.brainstormFresh === false
61
+ ? {
62
+ kind: "block",
63
+ title: "APEX: brainstorm first",
64
+ reason: `Creation intent detected — brainstorm before creating new ${ctx.framework} files.`,
65
+ actions: ["Launch the brainstorming agent"],
66
+ }
67
+ : null;
68
+
69
+ // solidReadGate/freshnessGate live in ./apex-gates (kept out of this file for
70
+ // the SOLID file-size ceiling — they import ApexContext/ApexGate from here).
71
+ export { solidReadGate, freshnessGate } from "./apex-gates";
72
+ import { solidReadGate, freshnessGate } from "./apex-gates";
73
+
74
+ /** Gates that run BEFORE Check 1 (harness-specific brainstorm + agent freshness, pre-dating enforce-apex-phases). */
75
+ export const PRE_AUTH_GATES: ReadonlyArray<ApexGate> = [brainstormGate, freshnessGate];
76
+
77
+ /** Gates that run AFTER Check 1: Check 2 (doc consulted once per session) then the SOLID refs. */
78
+ export const POST_AUTH_GATES: ReadonlyArray<ApexGate> = [docConsultedGate, solidReadGate];
79
+
80
+ /**
81
+ * Default APEX gate chain (brainstorm, freshness, docs, SOLID refs). Check 1
82
+ * (`apexAuthorizationGate`, policy/apex-authorization.ts) is wired BETWEEN the
83
+ * PRE and POST sub-chains by the runtime (gate-apex.ts), which owns its
84
+ * target-write side effect — parity enforce-apex-phases.ts Check1 -> Check2.
85
+ */
86
+ export const APEX_GATES: ReadonlyArray<ApexGate> = [...PRE_AUTH_GATES, ...POST_AUTH_GATES];
87
+
88
+ /**
89
+ * Run the APEX gates (chain-of-responsibility): the first failing gate's prompt
90
+ * wins; null means every gate passed (allow).
91
+ */
92
+ export function evaluateApex(ctx: ApexContext, gates: ReadonlyArray<ApexGate> = APEX_GATES): Prompt | null {
93
+ return gates.reduce<Prompt | null>((hit, gate) => hit ?? gate(ctx), null);
94
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Indented Unicode tree from scanned plugin items — pure text in, markdown out
3
+ * (no fs). Ports `build_tree.py`.
4
+ */
5
+
6
+ /** A scanned `[type, name, desc]` plugin item (agent/skill/command/hooks). */
7
+ export type ScanRow = readonly [type: string, name: string, desc: string];
8
+
9
+ const SECTION_ORDER: readonly string[] = ["agent", "skill", "command"];
10
+
11
+ /**
12
+ * Format grouped items with tree connectors and optional markdown links.
13
+ * Skill sections link to `./skills/<name>/index.md`; other sections to
14
+ * `./<folder>/<name>.md`; unlinked sections render the bare name.
15
+ * @param prefix - The line prefix (indent + branch glyphs).
16
+ * @param items - The `[name, desc]` pairs to render.
17
+ * @param folder - The link folder ("" disables linking).
18
+ * @param asDirs - Whether items link to a subdirectory `index.md`.
19
+ * @returns The rendered lines.
20
+ */
21
+ function printItems(prefix: string, items: ReadonlyArray<[string, string]>, folder: string, asDirs: boolean): string[] {
22
+ return items.map(([name, desc], i) => {
23
+ const connector = i === items.length - 1 ? "└──" : "├──";
24
+ const safe = name.replace(/^\/+/, "");
25
+ let label = name;
26
+ if (folder && asDirs) label = `[${name}](./${folder}/${safe}/index.md)`;
27
+ else if (folder) label = `[${name}](./${folder}/${safe}.md)`;
28
+ const short = desc && desc !== "(no description)" ? ` — ${desc.slice(0, 80)}` : "";
29
+ return `${prefix}${connector} ${label}${short}`;
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Build an indented tree from scanned items. The `hooks` row renders as a single
35
+ * trailing `└── hooks: …` line; agents/skills/commands render as folder sections.
36
+ * @param items - The scanned `[type, name, desc]` rows.
37
+ * @param linked - When true, leaf names become markdown links.
38
+ * @returns The joined tree text.
39
+ */
40
+ export function buildTree(items: ReadonlyArray<ScanRow>, linked: boolean = false): string {
41
+ const groups: Record<string, [string, string][]> = {};
42
+ let hooksLine = "";
43
+ for (const [typ, name, desc] of items) {
44
+ if (typ === "hooks") hooksLine = name;
45
+ else (groups[typ] ??= []).push([name, desc]);
46
+ }
47
+
48
+ const sections = SECTION_ORDER.filter((s) => s in groups);
49
+ if (hooksLine) sections.push("hooks");
50
+
51
+ const lines: string[] = [];
52
+ const total = sections.length;
53
+ for (let idx = 0; idx < total; idx++) {
54
+ const section = sections[idx] ?? "";
55
+ if (section === "hooks") {
56
+ lines.push(`└── hooks: ${hooksLine}`);
57
+ continue;
58
+ }
59
+ const isLast = idx === total - 1;
60
+ const folder = `${section}s`;
61
+ const prefix = isLast ? "└──" : "├──";
62
+ const subPrefix = isLast ? " " : "│ ";
63
+ lines.push(`${prefix} ${folder}/`);
64
+ const linkFolder = linked ? folder : "";
65
+ const isDirSection = section === "skill";
66
+ lines.push(...printItems(subPrefix, groups[section] ?? [], linkFolder, linked && isDirSection));
67
+ }
68
+ return lines.join("\n");
69
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * File-description heuristics — pure text in, description out (no fs). Ports
3
+ * `describe.py`.
4
+ */
5
+
6
+ const SOURCE_SUFFIXES: ReadonlySet<string> = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".swift"]);
7
+
8
+ /**
9
+ * First `# ` Markdown heading text (sans hashes), sliced to 60. "" when none.
10
+ * @param text - The document text.
11
+ * @returns The heading text, or "".
12
+ */
13
+ export function firstHeading(text: string): string {
14
+ for (const line of text.split("\n")) {
15
+ if (line.startsWith("# ")) return line.replace(/^#+/, "").trim().slice(0, 60);
16
+ }
17
+ return "";
18
+ }
19
+
20
+ /**
21
+ * First leading comment among the first 10 lines (`//`, `#` but not `#!`, or a
22
+ * `"""`/`'''` docstring), sliced to 60. "" when none.
23
+ * @param text - The source text.
24
+ * @returns The comment text, or "".
25
+ */
26
+ export function firstComment(text: string): string {
27
+ const lines = text.split("\n").slice(0, 10);
28
+ for (const raw of lines) {
29
+ const line = raw.trim();
30
+ if ((line.startsWith("//") || line.startsWith("#")) && !line.startsWith("#!")) {
31
+ return line.replace(/^[/#! ]+/, "").slice(0, 60);
32
+ }
33
+ if (line.startsWith('"""') || line.startsWith("'''")) {
34
+ return line.replace(/^['"\s]+|['"\s]+$/g, "").slice(0, 60);
35
+ }
36
+ }
37
+ return "";
38
+ }
39
+
40
+ /**
41
+ * Derive a description from a file's suffix + text. For `.md`, the supplied
42
+ * frontmatter `description` (truncated) wins over the first heading; for known
43
+ * source suffixes, the first comment; else "".
44
+ * @param suffix - The file extension (with dot).
45
+ * @param text - The file text.
46
+ * @param mdField - The pre-parsed frontmatter `description` (md only).
47
+ * @returns The derived description, or "".
48
+ */
49
+ export function descFromText(suffix: string, text: string, mdField: string): string {
50
+ if (suffix === ".md") return mdField.slice(0, 60) || firstHeading(text);
51
+ if (SOURCE_SUFFIXES.has(suffix)) return firstComment(text);
52
+ return "";
53
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tree-entry parsing — pure line regexes. Ports the line parsers of
3
+ * `merge_index.py` and `track-enrichment.py`.
4
+ */
5
+
6
+ /** A parsed `prefix[name](path) — desc` tree line. */
7
+ export interface TreeEntry {
8
+ prefix: string;
9
+ name: string;
10
+ path: string;
11
+ desc: string;
12
+ }
13
+
14
+ const ENTRY_RE = /^(.*?)\[([^\]]+)\]\(([^)]+)\)\s*(?:—|-{1,2})\s*(.*)$/;
15
+ const ENRICH_RE = /^(?:.*?)\[([^\]]+)\]\(([^)]+)\)\s*(?:—|-{1,2})\s*(.+)$/;
16
+
17
+ /**
18
+ * Parse a `merge_index` tree line into its parts. Returns null on no match.
19
+ * @param line - The raw tree line.
20
+ * @returns The parsed entry, or null.
21
+ */
22
+ export function parseEntry(line: string): TreeEntry | null {
23
+ const m = ENTRY_RE.exec(line);
24
+ if (!m || m[1] === undefined || m[2] === undefined || m[3] === undefined || m[4] === undefined) return null;
25
+ return { prefix: m[1], name: m[2], path: m[3], desc: m[4] };
26
+ }
27
+
28
+ /**
29
+ * Parse an enrichment line into `[path, desc]`, requiring a non-empty desc.
30
+ * @param line - The raw index line.
31
+ * @returns The `[path, desc]` pair, or null.
32
+ */
33
+ export function parseEnrichment(line: string): [string, string] | null {
34
+ const m = ENRICH_RE.exec(line);
35
+ if (!m || m[2] === undefined || m[3] === undefined) return null;
36
+ const desc = m[3].trim();
37
+ return desc ? [m[2], desc] : null;
38
+ }