forgeos 0.1.0-alpha.7 → 0.1.0-alpha.71

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 (598) hide show
  1. package/.agents/skills/forge-agent-fabric/SKILL.md +178 -0
  2. package/.agents/skills/forge-agent-fabric/references/managed-workflows.md +95 -0
  3. package/.agents/skills/forge-agent-fabric/references/repository-analysis.md +102 -0
  4. package/.agents/skills/forge-agent-fabric/scripts/fabric.mjs +46 -0
  5. package/.agents/skills/forge-agent-fabric/scripts/repository.mjs +44 -0
  6. package/.npmignore +10 -0
  7. package/AGENTS.md +142 -90
  8. package/CHANGELOG.md +870 -9
  9. package/LICENSE +21 -0
  10. package/README.md +121 -6
  11. package/adapters/go/README.md +23 -0
  12. package/adapters/go/go.mod +3 -0
  13. package/adapters/go/http.go +149 -0
  14. package/adapters/go/registry.go +234 -0
  15. package/adapters/go/types.go +136 -0
  16. package/adapters/java/README.md +68 -0
  17. package/adapters/java/pom.xml +47 -0
  18. package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
  19. package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
  20. package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
  21. package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
  22. package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
  23. package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
  24. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
  25. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
  26. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
  27. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
  28. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
  29. package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
  30. package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
  31. package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
  32. package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
  33. package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
  34. package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
  35. package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
  36. package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
  37. package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
  38. package/adapters/java-spring-boot-starter/README.md +32 -0
  39. package/adapters/java-spring-boot-starter/pom.xml +49 -0
  40. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
  41. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
  42. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
  43. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
  44. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
  45. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
  46. package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  47. package/bin/forge.mjs +18 -0
  48. package/docs/agent-fabric-programs.md +151 -0
  49. package/docs/agent-fabric.md +582 -0
  50. package/docs/architecture/agent-fabric/PORTABLE_PROJECTS_PLAN.md +136 -0
  51. package/docs/architecture/agent-fabric/README.md +180 -0
  52. package/docs/architecture/agent-fabric/REPOSITORY_MANIFEST_PLAN.md +284 -0
  53. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -0
  54. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
  55. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
  56. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -0
  57. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +590 -0
  58. package/docs/cair-protocol.md +103 -0
  59. package/docs/changelog.md +1070 -0
  60. package/docs/forge-protocol.md +189 -0
  61. package/docs/repository-agent-benchmark.md +179 -0
  62. package/docs/repository-analysis.md +139 -0
  63. package/docs/repository-context-quality.md +179 -0
  64. package/docs/repository-runtime-observation.md +82 -0
  65. package/examples/agent-fabric-v2/README.md +70 -0
  66. package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
  67. package/examples/agent-fabric-v2/make-request.mjs +4 -0
  68. package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
  69. package/examples/agent-fabric-v2/migrate.workflow.ts +35 -0
  70. package/examples/agent-fabric-v2/registry.example.json +256 -0
  71. package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
  72. package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
  73. package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
  74. package/examples/agent-fabric-v2/visual-population.json +13 -0
  75. package/examples/agent-fabric-v2/workers/check.mjs +2 -0
  76. package/examples/agent-fabric-v2/workers/common.mjs +7 -0
  77. package/examples/agent-fabric-v2/workers/discover.mjs +2 -0
  78. package/examples/agent-fabric-v2/workers/migrate.mjs +4 -0
  79. package/examples/agent-fabric-v2/workers/review.mjs +3 -0
  80. package/examples/go-billing/go.mod +7 -0
  81. package/examples/go-billing/main.go +120 -0
  82. package/examples/java-billing/pom.xml +60 -0
  83. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
  84. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
  85. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
  86. package/package.json +60 -5
  87. package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
  88. package/schemas/forge-manifest.schema.json +389 -0
  89. package/scripts/field-test-forgeos.mjs +1021 -0
  90. package/scripts/install-agent-fabric-skill.mjs +105 -0
  91. package/src/forge/_generated/releaseManifest.json +1 -2
  92. package/src/forge/_generated/releaseManifest.ts +3 -3
  93. package/src/forge/agent-adapters/index.ts +1592 -125
  94. package/src/forge/agent-adapters/types.ts +225 -1
  95. package/src/forge/agent-fabric/adapter.ts +131 -0
  96. package/src/forge/agent-fabric/attached-snapshot.ts +55 -0
  97. package/src/forge/agent-fabric/attached-task-contract.ts +105 -0
  98. package/src/forge/agent-fabric/attached-task-service.ts +127 -0
  99. package/src/forge/agent-fabric/attached-task-store.ts +109 -0
  100. package/src/forge/agent-fabric/authority.ts +298 -0
  101. package/src/forge/agent-fabric/canonical.ts +135 -0
  102. package/src/forge/agent-fabric/codex-adversarial-review.ts +229 -0
  103. package/src/forge/agent-fabric/codex-sdk-worker.ts +203 -0
  104. package/src/forge/agent-fabric/conductor.ts +754 -0
  105. package/src/forge/agent-fabric/dictionary.ts +12 -0
  106. package/src/forge/agent-fabric/errors.ts +32 -0
  107. package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
  108. package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
  109. package/src/forge/agent-fabric/index.ts +48 -0
  110. package/src/forge/agent-fabric/journal.ts +116 -0
  111. package/src/forge/agent-fabric/local-adaptive-digest-worker.mjs +26 -0
  112. package/src/forge/agent-fabric/local-adaptive-harness.ts +343 -0
  113. package/src/forge/agent-fabric/local-adaptive-service.ts +398 -0
  114. package/src/forge/agent-fabric/local-adaptive-worker.ts +146 -0
  115. package/src/forge/agent-fabric/local-approval-window.ts +211 -0
  116. package/src/forge/agent-fabric/local-change-review-service.ts +374 -0
  117. package/src/forge/agent-fabric/local-coding-worker.ts +285 -0
  118. package/src/forge/agent-fabric/local-control-store.ts +239 -0
  119. package/src/forge/agent-fabric/local-effect-broker.ts +348 -0
  120. package/src/forge/agent-fabric/local-evolution-approval.ts +81 -0
  121. package/src/forge/agent-fabric/local-evolution-profile.ts +87 -0
  122. package/src/forge/agent-fabric/local-evolution-registry.ts +427 -0
  123. package/src/forge/agent-fabric/local-evolution-service.ts +200 -0
  124. package/src/forge/agent-fabric/local-intelligence.ts +281 -0
  125. package/src/forge/agent-fabric/local-paths.ts +18 -0
  126. package/src/forge/agent-fabric/local-task-contract.ts +203 -0
  127. package/src/forge/agent-fabric/local-task-inbox.ts +543 -0
  128. package/src/forge/agent-fabric/local-task-server.ts +374 -0
  129. package/src/forge/agent-fabric/local-task-service.ts +982 -0
  130. package/src/forge/agent-fabric/local-verification.ts +551 -0
  131. package/src/forge/agent-fabric/managed-environment.ts +205 -0
  132. package/src/forge/agent-fabric/managed-run-contract.ts +103 -0
  133. package/src/forge/agent-fabric/managed-run-service.ts +401 -0
  134. package/src/forge/agent-fabric/managed-run-store.ts +165 -0
  135. package/src/forge/agent-fabric/managed-workspace.ts +253 -0
  136. package/src/forge/agent-fabric/p0a.ts +104 -0
  137. package/src/forge/agent-fabric/p0b-model-adapter.ts +434 -0
  138. package/src/forge/agent-fabric/planning.ts +155 -0
  139. package/src/forge/agent-fabric/program-api.ts +37 -0
  140. package/src/forge/agent-fabric/program-contract.ts +283 -0
  141. package/src/forge/agent-fabric/program-dsl.ts +126 -0
  142. package/src/forge/agent-fabric/program-evidence.ts +22 -0
  143. package/src/forge/agent-fabric/program-image.ts +15 -0
  144. package/src/forge/agent-fabric/program-lock.ts +50 -0
  145. package/src/forge/agent-fabric/program-owner.ts +16 -0
  146. package/src/forge/agent-fabric/program-scheduler.ts +38 -0
  147. package/src/forge/agent-fabric/program-service.ts +793 -0
  148. package/src/forge/agent-fabric/program-store.ts +142 -0
  149. package/src/forge/agent-fabric/program-structure.ts +65 -0
  150. package/src/forge/agent-fabric/program-types.ts +110 -0
  151. package/src/forge/agent-fabric/program-view.ts +12 -0
  152. package/src/forge/agent-fabric/program-worker.ts +108 -0
  153. package/src/forge/agent-fabric/project-profile.ts +50 -0
  154. package/src/forge/agent-fabric/project-registry.ts +120 -0
  155. package/src/forge/agent-fabric/project-runtime.ts +85 -0
  156. package/src/forge/agent-fabric/readiness.ts +26 -0
  157. package/src/forge/agent-fabric/reducer.ts +863 -0
  158. package/src/forge/agent-fabric/repository-context.ts +83 -0
  159. package/src/forge/agent-fabric/resource-ledger.ts +266 -0
  160. package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
  161. package/src/forge/agent-fabric/types.ts +424 -0
  162. package/src/forge/agent-fabric/validation.ts +500 -0
  163. package/src/forge/agent-fabric/workflow-engine.ts +218 -0
  164. package/src/forge/agent-fabric/workflow-task-actions.ts +104 -0
  165. package/src/forge/agent-fabric/workflow-templates.ts +11 -0
  166. package/src/forge/agent-memory/bridge.ts +1728 -0
  167. package/src/forge/agent-memory/context-pack.ts +284 -0
  168. package/src/forge/agent-memory/hook-runner.ts +331 -0
  169. package/src/forge/agent-memory/mcp.ts +597 -0
  170. package/src/forge/agent-memory/normalize.ts +498 -0
  171. package/src/forge/agent-memory/redaction.ts +103 -0
  172. package/src/forge/agent-memory/sources/claude-code.ts +51 -0
  173. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +343 -0
  174. package/src/forge/agent-memory/sources/codex.ts +161 -0
  175. package/src/forge/agent-memory/sources/cursor.ts +35 -0
  176. package/src/forge/agent-memory/types.ts +215 -0
  177. package/src/forge/bench.ts +248 -0
  178. package/src/forge/brownfield-import/index.ts +896 -0
  179. package/src/forge/brownfield-import/types.ts +127 -0
  180. package/src/forge/cair/action-journal.ts +61 -0
  181. package/src/forge/cair/action-parser.ts +314 -0
  182. package/src/forge/cair/action-validator.ts +40 -0
  183. package/src/forge/cair/actions.ts +1818 -0
  184. package/src/forge/cair/format.ts +77 -0
  185. package/src/forge/cair/index.ts +118 -0
  186. package/src/forge/cair/query.ts +478 -0
  187. package/src/forge/cair/snapshot.ts +315 -0
  188. package/src/forge/cair/types.ts +255 -0
  189. package/src/forge/cli/adaptive.ts +51 -0
  190. package/src/forge/cli/ai.ts +16 -1
  191. package/src/forge/cli/auth.ts +459 -15
  192. package/src/forge/cli/authmd.ts +441 -0
  193. package/src/forge/cli/baseline.ts +112 -0
  194. package/src/forge/cli/build.ts +20 -4
  195. package/src/forge/cli/changed.ts +418 -0
  196. package/src/forge/cli/codex-app-server.ts +877 -0
  197. package/src/forge/cli/commands.ts +1859 -20
  198. package/src/forge/cli/db.ts +209 -2
  199. package/src/forge/cli/deploy.ts +1339 -0
  200. package/src/forge/cli/deps.ts +150 -23
  201. package/src/forge/cli/dev.ts +1137 -53
  202. package/src/forge/cli/docs.ts +265 -0
  203. package/src/forge/cli/doctor.ts +297 -0
  204. package/src/forge/cli/env-placeholders.ts +62 -0
  205. package/src/forge/cli/evolution.ts +40 -0
  206. package/src/forge/cli/fabric.ts +253 -0
  207. package/src/forge/cli/field-test.ts +568 -0
  208. package/src/forge/cli/golden-path.ts +487 -0
  209. package/src/forge/cli/handoff.ts +336 -0
  210. package/src/forge/cli/index.ts +1 -0
  211. package/src/forge/cli/last-run.ts +84 -0
  212. package/src/forge/cli/main.ts +196 -3
  213. package/src/forge/cli/new.ts +169 -17
  214. package/src/forge/cli/next-actions.ts +23 -0
  215. package/src/forge/cli/output.ts +398 -6
  216. package/src/forge/cli/parse.ts +1377 -38
  217. package/src/forge/cli/progress.ts +51 -0
  218. package/src/forge/cli/query.ts +32 -0
  219. package/src/forge/cli/release.ts +35 -11
  220. package/src/forge/cli/repository.ts +151 -0
  221. package/src/forge/cli/run.ts +42 -0
  222. package/src/forge/cli/secrets.ts +137 -1
  223. package/src/forge/cli/seed.ts +581 -0
  224. package/src/forge/cli/self-host.ts +56 -14
  225. package/src/forge/cli/studio.ts +2282 -0
  226. package/src/forge/cli/verify.ts +1458 -32
  227. package/src/forge/cli/windows.ts +23 -0
  228. package/src/forge/cli/workos.ts +3509 -0
  229. package/src/forge/compiler/agent-contract/build.ts +728 -64
  230. package/src/forge/compiler/agent-contract/types.ts +45 -2
  231. package/src/forge/compiler/ai-registry/parse.ts +71 -2
  232. package/src/forge/compiler/api-surface/build.ts +47 -0
  233. package/src/forge/compiler/app-graph/build.ts +112 -9
  234. package/src/forge/compiler/app-graph/extract.ts +107 -0
  235. package/src/forge/compiler/app-graph/module-graph.ts +73 -78
  236. package/src/forge/compiler/app-graph/parser.ts +24 -24
  237. package/src/forge/compiler/app-graph/profile.ts +26 -0
  238. package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
  239. package/src/forge/compiler/app-graph/versions.ts +1 -1
  240. package/src/forge/compiler/classifier/capabilities.ts +3 -2
  241. package/src/forge/compiler/classifier/classify.ts +32 -8
  242. package/src/forge/compiler/classifier/secrets.ts +3 -2
  243. package/src/forge/compiler/classifier/signals.ts +91 -1
  244. package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
  245. package/src/forge/compiler/client-sdk/render-client.ts +295 -13
  246. package/src/forge/compiler/data-graph/build.ts +29 -1
  247. package/src/forge/compiler/data-graph/parse.ts +20 -4
  248. package/src/forge/compiler/data-graph/sql/ddl.ts +144 -31
  249. package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
  250. package/src/forge/compiler/data-graph/sql/types.ts +1 -0
  251. package/src/forge/compiler/diagnostics/codes.ts +38 -0
  252. package/src/forge/compiler/diagnostics/create.ts +28 -2
  253. package/src/forge/compiler/diagnostics/index.ts +2 -0
  254. package/src/forge/compiler/emitter/barrel.ts +3 -0
  255. package/src/forge/compiler/emitter/render.ts +9 -0
  256. package/src/forge/compiler/external-manifest/registry.ts +217 -0
  257. package/src/forge/compiler/external-manifest/types.ts +91 -0
  258. package/src/forge/compiler/external-manifest/validate.ts +373 -0
  259. package/src/forge/compiler/frontend-graph/build.ts +142 -14
  260. package/src/forge/compiler/integration/add.ts +991 -33
  261. package/src/forge/compiler/integration/plan.ts +43 -12
  262. package/src/forge/compiler/integration/render.ts +29 -0
  263. package/src/forge/compiler/integration/snapshot.ts +2 -0
  264. package/src/forge/compiler/integration/templates/convex.ts +70 -0
  265. package/src/forge/compiler/integration/templates/index.ts +1 -0
  266. package/src/forge/compiler/integration/templates/render.ts +38 -0
  267. package/src/forge/compiler/integration/templates/types.ts +3 -0
  268. package/src/forge/compiler/integration/templates/workos.ts +1439 -0
  269. package/src/forge/compiler/make-registry/build.ts +8 -9
  270. package/src/forge/compiler/orchestrator/fast-check.ts +4 -0
  271. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  272. package/src/forge/compiler/orchestrator/manifest.ts +1 -1
  273. package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
  274. package/src/forge/compiler/orchestrator/plan.ts +249 -10
  275. package/src/forge/compiler/orchestrator/profile.ts +65 -0
  276. package/src/forge/compiler/orchestrator/run.ts +101 -33
  277. package/src/forge/compiler/orchestrator/serialize.ts +33 -8
  278. package/src/forge/compiler/orchestrator/types.ts +1 -1
  279. package/src/forge/compiler/package-graph/compiler.ts +3 -1
  280. package/src/forge/compiler/package-graph/constants.ts +1 -1
  281. package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
  282. package/src/forge/compiler/package-manager/adapter.ts +4 -1
  283. package/src/forge/compiler/package-manager/commands.ts +4 -0
  284. package/src/forge/compiler/package-manager/executor.ts +30 -1
  285. package/src/forge/compiler/package-manager/version.ts +36 -12
  286. package/src/forge/compiler/package-upgrades/planner.ts +155 -18
  287. package/src/forge/compiler/package-upgrades/types.ts +2 -0
  288. package/src/forge/compiler/policy-registry/build.ts +47 -2
  289. package/src/forge/compiler/policy-registry/parse.ts +32 -2
  290. package/src/forge/compiler/recipes/definitions.ts +63 -0
  291. package/src/forge/compiler/recipes/index.ts +2 -0
  292. package/src/forge/compiler/recipes/profiles.ts +81 -0
  293. package/src/forge/compiler/recipes/registry.ts +6 -0
  294. package/src/forge/compiler/test-graph/build.ts +11 -3
  295. package/src/forge/compiler/types/app-graph.ts +8 -2
  296. package/src/forge/compiler/types/cli.ts +76 -1
  297. package/src/forge/compiler/types/dev-manifest.ts +4 -0
  298. package/src/forge/compiler/types/emit.ts +2 -0
  299. package/src/forge/compiler/types/frontend-graph.ts +2 -2
  300. package/src/forge/compiler/types/integration.ts +1 -0
  301. package/src/forge/compiler/types/package-graph.ts +6 -0
  302. package/src/forge/compiler/types/policy-registry.ts +3 -1
  303. package/src/forge/delta/broker-runner.mjs +5 -0
  304. package/src/forge/delta/broker.ts +429 -0
  305. package/src/forge/delta/classifier.ts +52 -0
  306. package/src/forge/delta/explain.ts +238 -0
  307. package/src/forge/delta/git-observer.ts +67 -0
  308. package/src/forge/delta/ids.ts +44 -0
  309. package/src/forge/delta/index.ts +25 -0
  310. package/src/forge/delta/process-identity.ts +37 -0
  311. package/src/forge/delta/queue-status.ts +190 -0
  312. package/src/forge/delta/recorder.ts +540 -0
  313. package/src/forge/delta/redaction.ts +50 -0
  314. package/src/forge/delta/schema.ts +240 -0
  315. package/src/forge/delta/session.ts +142 -0
  316. package/src/forge/delta/status.ts +1139 -0
  317. package/src/forge/delta/store.ts +3329 -0
  318. package/src/forge/delta/timeline.ts +178 -0
  319. package/src/forge/dev/server.ts +1061 -22
  320. package/src/forge/dev/types.ts +15 -1
  321. package/src/forge/dev/watch.ts +17 -7
  322. package/src/forge/dev-console/cycle.ts +285 -33
  323. package/src/forge/dev-console/types.ts +47 -1
  324. package/src/forge/impact/index.ts +222 -30
  325. package/src/forge/impact/types.ts +33 -1
  326. package/src/forge/intent/index.ts +35 -16
  327. package/src/forge/make/fields.ts +26 -0
  328. package/src/forge/make/index.ts +42 -6
  329. package/src/forge/make/templates.ts +211 -4
  330. package/src/forge/make/types.ts +2 -1
  331. package/src/forge/policy.ts +1 -1
  332. package/src/forge/react/index.ts +160 -2
  333. package/src/forge/refactor/index.ts +1 -0
  334. package/src/forge/repair/rules/index.ts +2 -2
  335. package/src/forge/repository-analysis/adapters/common.ts +72 -0
  336. package/src/forge/repository-analysis/adapters/docker.ts +185 -0
  337. package/src/forge/repository-analysis/adapters/java.ts +133 -0
  338. package/src/forge/repository-analysis/adapters/typescript.ts +350 -0
  339. package/src/forge/repository-analysis/adapters/vue.ts +71 -0
  340. package/src/forge/repository-analysis/analyze.ts +434 -0
  341. package/src/forge/repository-analysis/benchmark.ts +336 -0
  342. package/src/forge/repository-analysis/cache-gc.ts +154 -0
  343. package/src/forge/repository-analysis/check-selection.ts +123 -0
  344. package/src/forge/repository-analysis/context-node.ts +17 -0
  345. package/src/forge/repository-analysis/context.ts +456 -0
  346. package/src/forge/repository-analysis/http-resolution.ts +135 -0
  347. package/src/forge/repository-analysis/identity.ts +45 -0
  348. package/src/forge/repository-analysis/index.ts +18 -0
  349. package/src/forge/repository-analysis/nuxt-resolution.ts +188 -0
  350. package/src/forge/repository-analysis/quality.ts +60 -0
  351. package/src/forge/repository-analysis/retrieval.ts +309 -0
  352. package/src/forge/repository-analysis/runtime-artifacts.ts +195 -0
  353. package/src/forge/repository-analysis/runtime-observation.ts +325 -0
  354. package/src/forge/repository-analysis/scanner.ts +174 -0
  355. package/src/forge/repository-analysis/storage.ts +193 -0
  356. package/src/forge/repository-analysis/test-associations.ts +59 -0
  357. package/src/forge/repository-analysis/types.ts +99 -0
  358. package/src/forge/repository-manifest/index.ts +178 -0
  359. package/src/forge/repository-manifest/types.ts +63 -0
  360. package/src/forge/review/index.ts +162 -12
  361. package/src/forge/review/types.ts +15 -0
  362. package/src/forge/runtime/ai/providers.ts +19 -4
  363. package/src/forge/runtime/auth/config.ts +17 -0
  364. package/src/forge/runtime/auth/evaluate.ts +15 -2
  365. package/src/forge/runtime/auth/resolve.ts +61 -6
  366. package/src/forge/runtime/auth/types.ts +1 -0
  367. package/src/forge/runtime/db/factory.ts +1 -3
  368. package/src/forge/runtime/db/generated-client.ts +13 -2
  369. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  370. package/src/forge/runtime/db/pglite-adapter.ts +285 -2
  371. package/src/forge/runtime/executor.ts +129 -16
  372. package/src/forge/runtime/external/bridge.ts +663 -0
  373. package/src/forge/runtime/runner/run-entry.ts +16 -7
  374. package/src/forge/runtime/telemetry/buffer.ts +4 -1
  375. package/src/forge/runtime/telemetry/scrubber.ts +35 -5
  376. package/src/forge/runtime/webhooks/security.ts +12 -7
  377. package/src/forge/server.ts +71 -7
  378. package/src/forge/ui/index.ts +831 -19
  379. package/src/forge/ui/types.ts +3 -1
  380. package/src/forge/version.ts +1 -1
  381. package/src/forge/vue/index.ts +422 -0
  382. package/src/forge/workspace/baseline.ts +112 -0
  383. package/src/forge/workspace/change-summary.ts +292 -0
  384. package/src/forge/workspace/forge-cli.ts +42 -0
  385. package/src/forge/workspace/git-summary.ts +393 -0
  386. package/templates/agent-workroom/AGENTS.md +29 -0
  387. package/templates/agent-workroom/README.md +34 -0
  388. package/templates/agent-workroom/forge.config.ts +3 -0
  389. package/templates/agent-workroom/package.json +34 -0
  390. package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
  391. package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
  392. package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
  393. package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
  394. package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
  395. package/templates/agent-workroom/src/forge/schema.ts +54 -0
  396. package/templates/agent-workroom/src/policies.ts +6 -0
  397. package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
  398. package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
  399. package/templates/agent-workroom/tsconfig.json +16 -0
  400. package/templates/agent-workroom/web/index.html +13 -0
  401. package/templates/agent-workroom/web/package.json +21 -0
  402. package/templates/agent-workroom/web/src/App.tsx +345 -0
  403. package/templates/agent-workroom/web/src/lib/forge.ts +20 -0
  404. package/templates/agent-workroom/web/src/main.tsx +13 -0
  405. package/templates/agent-workroom/web/src/styles.css +545 -0
  406. package/templates/agent-workroom/web/tsconfig.json +27 -0
  407. package/templates/agent-workroom/web/vite.config.ts +36 -0
  408. package/templates/b2b-support-web/package.json +2 -0
  409. package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
  410. package/templates/b2b-support-web/web/app/page.tsx +2 -2
  411. package/templates/b2b-support-web/web/lib/forge.ts +9 -2
  412. package/templates/b2b-support-web/web/next.config.ts +24 -0
  413. package/templates/b2b-support-web/web/package.json +1 -1
  414. package/templates/minimal-web/package.json +2 -1
  415. package/templates/minimal-web/pnpm-workspace.yaml +2 -0
  416. package/templates/minimal-web/web/index.html +1 -0
  417. package/templates/minimal-web/web/src/App.tsx +12 -6
  418. package/templates/minimal-web/web/src/lib/forge.ts +9 -2
  419. package/templates/minimal-web/web/src/styles.css +34 -0
  420. package/templates/minimal-web/web/vite.config.ts +36 -0
  421. package/templates/nuxt-web/.vscode/settings.json +14 -0
  422. package/templates/nuxt-web/.yarnrc.yml +1 -0
  423. package/templates/nuxt-web/README.md +30 -0
  424. package/templates/nuxt-web/forge.config.ts +3 -0
  425. package/templates/nuxt-web/package.json +37 -0
  426. package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
  427. package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
  428. package/templates/nuxt-web/src/commands/createNote.ts +26 -0
  429. package/templates/nuxt-web/src/forge/schema.ts +12 -0
  430. package/templates/nuxt-web/src/policies.ts +6 -0
  431. package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
  432. package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
  433. package/templates/nuxt-web/tsconfig.json +17 -0
  434. package/templates/nuxt-web/web/app.vue +67 -0
  435. package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
  436. package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
  437. package/templates/nuxt-web/web/composables/forge.ts +13 -0
  438. package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
  439. package/templates/nuxt-web/web/nuxt.config.ts +11 -0
  440. package/templates/nuxt-web/web/package.json +22 -0
  441. package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
  442. package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
  443. package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
  444. package/templates/nuxt-web/web/tsconfig.json +6 -0
  445. package/templates/vendor-access/.vscode/settings.json +14 -0
  446. package/templates/vendor-access/README.md +30 -0
  447. package/templates/vendor-access/forge.config.ts +3 -0
  448. package/templates/vendor-access/package.json +35 -0
  449. package/templates/vendor-access/pnpm-workspace.yaml +2 -0
  450. package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
  451. package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
  452. package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
  453. package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
  454. package/templates/vendor-access/src/forge/schema.ts +74 -0
  455. package/templates/vendor-access/src/policies.ts +11 -0
  456. package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
  457. package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
  458. package/templates/vendor-access/tsconfig.json +17 -0
  459. package/templates/vendor-access/web/index.html +13 -0
  460. package/templates/vendor-access/web/package.json +21 -0
  461. package/templates/vendor-access/web/src/App.tsx +573 -0
  462. package/templates/vendor-access/web/src/lib/forge.ts +20 -0
  463. package/templates/vendor-access/web/src/main.tsx +205 -0
  464. package/templates/vendor-access/web/src/styles.css +682 -0
  465. package/templates/vendor-access/web/tsconfig.json +18 -0
  466. package/templates/vendor-access/web/vite.config.ts +36 -0
  467. package/src/forge/_generated/actionSubscriptions.json +0 -2
  468. package/src/forge/_generated/actionSubscriptions.ts +0 -10
  469. package/src/forge/_generated/agentAdapterManifest.json +0 -2
  470. package/src/forge/_generated/agentAdapterManifest.ts +0 -73
  471. package/src/forge/_generated/agentContract.json +0 -2
  472. package/src/forge/_generated/agentContract.ts +0 -7829
  473. package/src/forge/_generated/agentQuickstart.md +0 -34
  474. package/src/forge/_generated/agentTools.json +0 -2
  475. package/src/forge/_generated/agentTools.md +0 -16
  476. package/src/forge/_generated/agentTools.ts +0 -12
  477. package/src/forge/_generated/aiContext.ts +0 -125
  478. package/src/forge/_generated/aiModels.json +0 -2
  479. package/src/forge/_generated/aiModels.ts +0 -51
  480. package/src/forge/_generated/aiProviders.json +0 -2
  481. package/src/forge/_generated/aiProviders.ts +0 -23
  482. package/src/forge/_generated/aiRegistry.json +0 -2
  483. package/src/forge/_generated/aiRegistry.ts +0 -31
  484. package/src/forge/_generated/api.json +0 -2
  485. package/src/forge/_generated/api.ts +0 -8
  486. package/src/forge/_generated/appGraph.json +0 -2
  487. package/src/forge/_generated/appGraph.ts +0 -14937
  488. package/src/forge/_generated/appMap.md +0 -55
  489. package/src/forge/_generated/artifactManifest.json +0 -2
  490. package/src/forge/_generated/artifactManifest.ts +0 -7
  491. package/src/forge/_generated/authClaims.json +0 -2
  492. package/src/forge/_generated/authClaims.ts +0 -13
  493. package/src/forge/_generated/authConfig.json +0 -2
  494. package/src/forge/_generated/authConfig.ts +0 -17
  495. package/src/forge/_generated/authContext.ts +0 -23
  496. package/src/forge/_generated/authRegistry.json +0 -2
  497. package/src/forge/_generated/authRegistry.ts +0 -25
  498. package/src/forge/_generated/buildInfo.json +0 -2
  499. package/src/forge/_generated/buildInfo.ts +0 -9
  500. package/src/forge/_generated/capabilityMap.json +0 -2
  501. package/src/forge/_generated/capabilityMap.md +0 -15
  502. package/src/forge/_generated/capabilityMap.ts +0 -17
  503. package/src/forge/_generated/client.ts +0 -282
  504. package/src/forge/_generated/clientApi.ts +0 -9
  505. package/src/forge/_generated/clientManifest.json +0 -2
  506. package/src/forge/_generated/clientManifest.ts +0 -39
  507. package/src/forge/_generated/clientTypes.ts +0 -78
  508. package/src/forge/_generated/configRegistry.json +0 -2
  509. package/src/forge/_generated/configRegistry.ts +0 -4
  510. package/src/forge/_generated/dataGraph.json +0 -2
  511. package/src/forge/_generated/dataGraph.ts +0 -8
  512. package/src/forge/_generated/db.json +0 -2
  513. package/src/forge/_generated/db.ts +0 -2
  514. package/src/forge/_generated/dbSecurityManifest.json +0 -2
  515. package/src/forge/_generated/dbSecurityManifest.ts +0 -15
  516. package/src/forge/_generated/dbSessionContext.json +0 -2
  517. package/src/forge/_generated/dbSessionContext.ts +0 -39
  518. package/src/forge/_generated/deployManifest.json +0 -2
  519. package/src/forge/_generated/deployManifest.ts +0 -14
  520. package/src/forge/_generated/devManifest.json +0 -2
  521. package/src/forge/_generated/devManifest.ts +0 -62
  522. package/src/forge/_generated/envSchema.json +0 -2
  523. package/src/forge/_generated/envSchema.ts +0 -59
  524. package/src/forge/_generated/frontendGraph.json +0 -2
  525. package/src/forge/_generated/frontendGraph.ts +0 -27
  526. package/src/forge/_generated/importGuards.json +0 -2
  527. package/src/forge/_generated/importGuards.ts +0 -686
  528. package/src/forge/_generated/index.ts +0 -68
  529. package/src/forge/_generated/liveProductionManifest.json +0 -2
  530. package/src/forge/_generated/liveProductionManifest.ts +0 -23
  531. package/src/forge/_generated/liveProtocol.json +0 -2
  532. package/src/forge/_generated/liveProtocol.ts +0 -21
  533. package/src/forge/_generated/liveQueryRegistry.json +0 -2
  534. package/src/forge/_generated/liveQueryRegistry.ts +0 -9
  535. package/src/forge/_generated/liveTransportConfig.json +0 -2
  536. package/src/forge/_generated/liveTransportConfig.ts +0 -19
  537. package/src/forge/_generated/makeRegistry.json +0 -2
  538. package/src/forge/_generated/makeRegistry.ts +0 -177
  539. package/src/forge/_generated/makeTemplates.json +0 -2
  540. package/src/forge/_generated/makeTemplates.ts +0 -66
  541. package/src/forge/_generated/mockMap.json +0 -2
  542. package/src/forge/_generated/mockMap.ts +0 -7
  543. package/src/forge/_generated/operationPlaybooks.md +0 -167
  544. package/src/forge/_generated/packageGraph.json +0 -2
  545. package/src/forge/_generated/packageGraph.ts +0 -249334
  546. package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
  547. package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
  548. package/src/forge/_generated/permissionMatrix.json +0 -2
  549. package/src/forge/_generated/permissionMatrix.ts +0 -7
  550. package/src/forge/_generated/policyRegistry.json +0 -2
  551. package/src/forge/_generated/policyRegistry.ts +0 -11
  552. package/src/forge/_generated/queryRegistry.json +0 -2
  553. package/src/forge/_generated/queryRegistry.ts +0 -9
  554. package/src/forge/_generated/react.d.ts +0 -22
  555. package/src/forge/_generated/react.ts +0 -29
  556. package/src/forge/_generated/reactManifest.json +0 -2
  557. package/src/forge/_generated/reactManifest.ts +0 -19
  558. package/src/forge/_generated/rlsPolicies.json +0 -2
  559. package/src/forge/_generated/rlsPolicies.sql +0 -34
  560. package/src/forge/_generated/rlsPolicies.ts +0 -6
  561. package/src/forge/_generated/runtimeGraph.json +0 -2
  562. package/src/forge/_generated/runtimeGraph.ts +0 -8
  563. package/src/forge/_generated/runtimeMatrix.json +0 -2
  564. package/src/forge/_generated/runtimeMatrix.ts +0 -334130
  565. package/src/forge/_generated/runtimeRegistry.ts +0 -2
  566. package/src/forge/_generated/runtimeRules.md +0 -91
  567. package/src/forge/_generated/secretRegistry.json +0 -2
  568. package/src/forge/_generated/secretRegistry.ts +0 -50
  569. package/src/forge/_generated/secretsContext.ts +0 -11
  570. package/src/forge/_generated/serverApi.ts +0 -10
  571. package/src/forge/_generated/sourceMapManifest.json +0 -2
  572. package/src/forge/_generated/sourceMapManifest.ts +0 -7
  573. package/src/forge/_generated/sqlPlan.json +0 -2
  574. package/src/forge/_generated/sqlPlan.ts +0 -88
  575. package/src/forge/_generated/subscriptionManifest.json +0 -2
  576. package/src/forge/_generated/subscriptionManifest.ts +0 -7
  577. package/src/forge/_generated/symbolicationManifest.json +0 -2
  578. package/src/forge/_generated/symbolicationManifest.ts +0 -17
  579. package/src/forge/_generated/telemetryRegistry.json +0 -2
  580. package/src/forge/_generated/telemetryRegistry.ts +0 -9
  581. package/src/forge/_generated/telemetrySinks.json +0 -2
  582. package/src/forge/_generated/telemetrySinks.ts +0 -11
  583. package/src/forge/_generated/tenantScope.json +0 -2
  584. package/src/forge/_generated/tenantScope.ts +0 -8
  585. package/src/forge/_generated/testGraph.json +0 -2
  586. package/src/forge/_generated/testGraph.ts +0 -3452
  587. package/src/forge/_generated/testPlanRegistry.json +0 -2
  588. package/src/forge/_generated/testPlanRegistry.ts +0 -33
  589. package/src/forge/_generated/uiRoutes.json +0 -2
  590. package/src/forge/_generated/uiRoutes.ts +0 -16
  591. package/src/forge/_generated/uiScenarios.json +0 -2
  592. package/src/forge/_generated/uiScenarios.ts +0 -30
  593. package/src/forge/_generated/uiTestManifest.json +0 -2
  594. package/src/forge/_generated/uiTestManifest.ts +0 -27
  595. package/src/forge/_generated/workflowRegistry.json +0 -2
  596. package/src/forge/_generated/workflowRegistry.ts +0 -9
  597. package/src/forge/_generated/workflowSubscriptions.json +0 -2
  598. package/src/forge/_generated/workflowSubscriptions.ts +0 -10
@@ -0,0 +1,1728 @@
1
+ import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { createDiagnostic } from "../compiler/diagnostics/create.ts";
5
+ import { createDeltaId } from "../delta/ids.ts";
6
+ import { DeltaStore, DeltaStoreBusyError, describeDeltaStoreBusy, summarizeDeltaStoreBusy } from "../delta/store.ts";
7
+ import { pidWasReused } from "../delta/process-identity.ts";
8
+ import { extractAgentEventBindings, normalizeAgentEvent, summarizeAgentEvent } from "./normalize.ts";
9
+ import { redactAgentPayload } from "./redaction.ts";
10
+ import { buildAgentMemoryContext } from "./context-pack.ts";
11
+ import { claudeCodeInstallFiles, claudeCodeInstallResult } from "./sources/claude-code.ts";
12
+ import { codexInstallFiles, codexInstallResult, privacyDefaults } from "./sources/codex.ts";
13
+ import { readCodexHookMeta } from "./hook-runner.ts";
14
+ import { cursorInstallFiles, cursorInstallResult } from "./sources/cursor.ts";
15
+ import type {
16
+ AgentEventEnvelope,
17
+ AgentIngestResult,
18
+ AgentIngestWatchResult,
19
+ AgentInstallResult,
20
+ AgentMemoryUnavailableResult,
21
+ AgentMemoryContextPack,
22
+ AgentMemoryEventRecord,
23
+ AgentMemoryFreshness,
24
+ AgentMemorySourceName,
25
+ } from "./types.ts";
26
+
27
+ export interface AgentMemoryCommandOptions {
28
+ subcommand: "install" | "ingest" | "context" | "memory";
29
+ workspaceRoot: string;
30
+ json: boolean;
31
+ target?: string;
32
+ source?: string;
33
+ eventName?: string;
34
+ input?: unknown;
35
+ entry?: string;
36
+ change?: string;
37
+ proof?: string;
38
+ handoff?: boolean;
39
+ current?: boolean;
40
+ dryRun?: boolean;
41
+ force?: boolean;
42
+ mcpServer?: string;
43
+ limit?: number;
44
+ watch?: boolean;
45
+ file?: string;
46
+ pollIntervalMs?: number;
47
+ }
48
+
49
+ export type AgentMemoryCommandResult =
50
+ | AgentInstallResult
51
+ | AgentIngestResult
52
+ | AgentIngestWatchResult
53
+ | AgentMemoryContextPack
54
+ | { ok: true; events: AgentMemoryEventRecord[]; freshness: AgentMemoryFreshness; exitCode: 0 }
55
+ | AgentMemoryUnavailableResult;
56
+
57
+ export interface AgentMemoryQueueInspectionResult {
58
+ exists: boolean;
59
+ source: string;
60
+ file: string;
61
+ events: number;
62
+ nativeSignals: number;
63
+ canarySignals: number;
64
+ usefulSignals: number;
65
+ ignoredOutOfWorkspaceEvents: number;
66
+ bytesRead: number;
67
+ pendingBytes: number;
68
+ inspectedBytes?: number;
69
+ skippedBytes?: number;
70
+ truncated?: boolean;
71
+ checkpointFile: string;
72
+ errors: string[];
73
+ latestEventAt?: string;
74
+ }
75
+
76
+ function memoryUnavailable(error: unknown, workspaceRoot: string): AgentMemoryUnavailableResult {
77
+ const message = error instanceof Error ? error.message : "agent memory store is unavailable";
78
+ const busy = error instanceof DeltaStoreBusyError;
79
+ const busyInfo = busy ? describeDeltaStoreBusy(error, workspaceRoot) : undefined;
80
+ const busySummary = busyInfo ? summarizeDeltaStoreBusy(busyInfo) : undefined;
81
+ return {
82
+ ok: false,
83
+ error: busySummary ? `${message} (${busySummary})` : message,
84
+ events: [],
85
+ ...(busyInfo ? { busy: busyInfo } : {}),
86
+ diagnostics: [
87
+ createDiagnostic({
88
+ severity: "error",
89
+ code: busy ? "FORGE_DELTA_BUSY" : "FORGE_AGENT_MEMORY_UNAVAILABLE",
90
+ message: busy
91
+ ? `Forge Delta local store is busy: ${message}${busySummary ? ` (${busySummary})` : ""}`
92
+ : message,
93
+ ...(busy
94
+ ? {
95
+ fixHint: busyInfo?.processAlive
96
+ ? `Wait for pid ${busyInfo.pid ?? "shown in the lock file"} to finish, then retry the agent memory command.`
97
+ : `If no Forge/agent process is still running, inspect ${busyInfo?.relativeLockPath ?? ".forge/delta/delta.lock"} and retry.`,
98
+ suggestedCommands: [
99
+ "forge delta status --json",
100
+ "forge agent timeline --json",
101
+ "forge agent hooks status --target codex --json",
102
+ ],
103
+ }
104
+ : {}),
105
+ }),
106
+ ],
107
+ nextActions: [
108
+ "forge delta status --json",
109
+ ...(busyInfo?.processAlive ? [] : ["forge delta repair --dry-run --json"]),
110
+ "forge agent timeline --json",
111
+ "forge agent hooks status --target codex --json",
112
+ ],
113
+ exitCode: 1,
114
+ };
115
+ }
116
+
117
+ async function openMemoryStore(
118
+ workspaceRoot: string,
119
+ access: "read" | "write" = "write",
120
+ ): Promise<DeltaStore | AgentMemoryUnavailableResult> {
121
+ const retryDelays = access === "write" ? [25, 75, 150] : [];
122
+ for (let attempt = 0; ; attempt += 1) {
123
+ try {
124
+ return await DeltaStore.open(workspaceRoot, {
125
+ access,
126
+ ...(access === "write" ? { waitMs: 1_500, retryDelayMs: 50 } : {}),
127
+ });
128
+ } catch (error) {
129
+ if (!(error instanceof DeltaStoreBusyError) || attempt >= retryDelays.length) {
130
+ return memoryUnavailable(error, workspaceRoot);
131
+ }
132
+ await sleep(retryDelays[attempt] ?? 0);
133
+ }
134
+ }
135
+ }
136
+
137
+ function isMemoryUnavailable(result: DeltaStore | AgentMemoryUnavailableResult): result is AgentMemoryUnavailableResult {
138
+ return "ok" in result && result.ok === false;
139
+ }
140
+
141
+ function isExternalPgliteRead(result: AgentMemoryUnavailableResult): boolean {
142
+ return Boolean(
143
+ result.busy?.relativeLockPath.endsWith("postmaster.pid") &&
144
+ result.busy.processAlive === false,
145
+ );
146
+ }
147
+
148
+ function isDeltaBusyIngestResult(result: AgentIngestResult): boolean {
149
+ return result.ok === false && result.busy?.code === "FORGE_DELTA_BUSY";
150
+ }
151
+
152
+ function fallbackMemoryPath(workspaceRoot: string): string {
153
+ return join(workspaceRoot, ".forge", "agent", "events.ndjson");
154
+ }
155
+
156
+ function hasExternalPglitePostmaster(workspaceRoot: string): boolean {
157
+ return existsSync(join(workspaceRoot, ".forge", "delta", "delta.db", "postmaster.pid")) &&
158
+ !existsSync(join(workspaceRoot, ".forge", "delta", "delta.lock"));
159
+ }
160
+
161
+ function shouldUseFallbackMemory(
162
+ result: AgentMemoryUnavailableResult,
163
+ workspaceRoot: string,
164
+ ): boolean {
165
+ return isExternalPgliteRead(result) || (!result.busy && hasExternalPglitePostmaster(workspaceRoot));
166
+ }
167
+
168
+ function eventRecordFromEnvelope(
169
+ envelope: AgentEventEnvelope,
170
+ summary: string | undefined,
171
+ bindings: Record<string, unknown>,
172
+ ): AgentMemoryEventRecord {
173
+ const capturedAt = envelope.event.timestamp || new Date().toISOString();
174
+ return {
175
+ id: createDeltaId("amem"),
176
+ externalEventId: createDeltaId("aevt"),
177
+ sourceName: String(envelope.source.agent),
178
+ integrationKind: String(envelope.source.integration),
179
+ trustLevel: envelope.capture.trustLevel,
180
+ externalSessionId: envelope.session.externalSessionId,
181
+ externalTurnId: envelope.session.turnId,
182
+ eventKind: envelope.event.kind,
183
+ normalizedKind: envelope.event.kind,
184
+ summary,
185
+ confidence: envelope.capture.confidence,
186
+ capturedAt,
187
+ data: { envelope, bindings },
188
+ };
189
+ }
190
+
191
+ function appendFallbackAgentMemoryEvent(
192
+ workspaceRoot: string,
193
+ envelope: AgentEventEnvelope,
194
+ summary: string | undefined,
195
+ bindings: Record<string, unknown>,
196
+ ): AgentMemoryEventRecord {
197
+ const file = fallbackMemoryPath(workspaceRoot);
198
+ mkdirSync(dirname(file), { recursive: true });
199
+ const event = eventRecordFromEnvelope(envelope, summary, bindings);
200
+ appendFileSync(file, `${JSON.stringify(event)}\n`, "utf8");
201
+ return event;
202
+ }
203
+
204
+ function readFallbackAgentMemoryEvents(
205
+ workspaceRoot: string,
206
+ target: string | undefined,
207
+ limit: number | undefined,
208
+ ): AgentMemoryEventRecord[] {
209
+ const file = fallbackMemoryPath(workspaceRoot);
210
+ if (!existsSync(file)) {
211
+ return [];
212
+ }
213
+ const events: AgentMemoryEventRecord[] = [];
214
+ for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
215
+ if (!line.trim()) {
216
+ continue;
217
+ }
218
+ try {
219
+ const parsed = JSON.parse(line) as unknown;
220
+ if (isAgentMemoryEventRecord(parsed) && agentMemoryEventMatchesTarget(parsed, target)) {
221
+ events.push(parsed);
222
+ }
223
+ } catch {
224
+ // Keep fallback recovery best effort; malformed lines should not break hooks.
225
+ }
226
+ }
227
+ return limit ? events.slice(-Math.max(1, Math.min(limit, 200))) : events;
228
+ }
229
+
230
+ function isAgentMemoryEventRecord(value: unknown): value is AgentMemoryEventRecord {
231
+ return Boolean(
232
+ value &&
233
+ typeof value === "object" &&
234
+ !Array.isArray(value) &&
235
+ typeof (value as { id?: unknown }).id === "string" &&
236
+ typeof (value as { sourceName?: unknown }).sourceName === "string" &&
237
+ typeof (value as { eventKind?: unknown }).eventKind === "string" &&
238
+ typeof (value as { capturedAt?: unknown }).capturedAt === "string",
239
+ );
240
+ }
241
+
242
+ function agentMemoryEventMatchesTarget(event: AgentMemoryEventRecord, target: string | undefined): boolean {
243
+ if (!target) {
244
+ return true;
245
+ }
246
+ return event.sourceName === target ||
247
+ event.summary?.includes(target) === true ||
248
+ JSON.stringify(event.data).includes(target);
249
+ }
250
+
251
+ function mergeAgentMemoryEvents(
252
+ primary: AgentMemoryEventRecord[],
253
+ fallback: AgentMemoryEventRecord[],
254
+ limit: number | undefined,
255
+ ): AgentMemoryEventRecord[] {
256
+ const seen = new Set<string>();
257
+ const primaryEnvelopes = new Set(primary.map((event) => JSON.stringify(event.data.envelope)).filter((value) => value !== undefined));
258
+ const merged = [...primary, ...fallback.filter((event) => !primaryEnvelopes.has(JSON.stringify(event.data.envelope)))]
259
+ .filter((event) => {
260
+ if (seen.has(event.id)) {
261
+ return false;
262
+ }
263
+ seen.add(event.id);
264
+ return true;
265
+ })
266
+ .sort((left, right) => {
267
+ const byTime = left.capturedAt.localeCompare(right.capturedAt);
268
+ return byTime === 0 ? left.id.localeCompare(right.id) : byTime;
269
+ });
270
+ return limit ? merged.slice(-Math.max(1, Math.min(limit, 200))) : merged;
271
+ }
272
+
273
+ function isMissingAgentMemorySchema(error: unknown): boolean {
274
+ const message = error instanceof Error ? error.message : String(error);
275
+ return /agent_memory_events/i.test(message) && /does not exist|no such table|missing/i.test(message);
276
+ }
277
+
278
+ async function listAgentMemoryEventsWithSchemaRepair(
279
+ workspaceRoot: string,
280
+ target: string | undefined,
281
+ limit: number | undefined,
282
+ ): Promise<AgentMemoryEventRecord[] | AgentMemoryUnavailableResult> {
283
+ const fallbackEvents = readFallbackAgentMemoryEvents(workspaceRoot, target, limit);
284
+ let store = await openMemoryStore(workspaceRoot, "read");
285
+ if (isMemoryUnavailable(store)) {
286
+ if (shouldUseFallbackMemory(store, workspaceRoot)) {
287
+ return fallbackEvents;
288
+ }
289
+ return store;
290
+ }
291
+ try {
292
+ return mergeAgentMemoryEvents(await store.listAgentMemoryEvents({ target, limit }), fallbackEvents, limit);
293
+ } catch (error) {
294
+ await store.close().catch(() => undefined);
295
+ if (!isMissingAgentMemorySchema(error)) {
296
+ return memoryUnavailable(error, workspaceRoot);
297
+ }
298
+ const repairStore = await openMemoryStore(workspaceRoot, "write");
299
+ if (isMemoryUnavailable(repairStore)) {
300
+ return repairStore;
301
+ }
302
+ try {
303
+ await repairStore.init();
304
+ return await repairStore.listAgentMemoryEvents({ target, limit });
305
+ } catch (repairError) {
306
+ return memoryUnavailable(repairError, workspaceRoot);
307
+ } finally {
308
+ await repairStore.close();
309
+ }
310
+ } finally {
311
+ await store.close().catch(() => undefined);
312
+ }
313
+ }
314
+
315
+ export async function runAgentMemoryCommand(options: AgentMemoryCommandOptions): Promise<AgentMemoryCommandResult> {
316
+ if (options.subcommand === "install") {
317
+ return installAgentMemory(options);
318
+ }
319
+ if (options.subcommand === "ingest") {
320
+ if (options.watch) {
321
+ return watchAgentMemoryIngest(options);
322
+ }
323
+ if (options.file) {
324
+ return ingestAgentMemoryQueueFile(options);
325
+ }
326
+ return ingestAgentMemory(options);
327
+ }
328
+ if (options.subcommand === "context") {
329
+ try {
330
+ const freshness = await refreshAgentMemory(options.workspaceRoot);
331
+ return await buildAgentMemoryContext({
332
+ workspaceRoot: options.workspaceRoot,
333
+ entry: options.entry,
334
+ change: options.change,
335
+ proof: options.proof,
336
+ handoff: options.handoff,
337
+ limit: options.limit,
338
+ freshness,
339
+ });
340
+ } catch (error) {
341
+ return memoryUnavailable(error, options.workspaceRoot);
342
+ }
343
+ }
344
+ const freshness = await refreshAgentMemory(options.workspaceRoot);
345
+ const events = await listAgentMemoryEventsWithSchemaRepair(options.workspaceRoot, options.entry, options.limit);
346
+ if (!Array.isArray(events)) {
347
+ return events;
348
+ }
349
+ return {
350
+ ok: true,
351
+ events,
352
+ freshness: { ...freshness, ...(events.at(-1)?.capturedAt ? { latestMatchingEventAt: events.at(-1)!.capturedAt } : {}) },
353
+ exitCode: 0,
354
+ };
355
+ }
356
+
357
+ /** Drain a short batch before reads; the broker continues draining in the background. */
358
+ export async function refreshAgentMemory(workspaceRoot: string): Promise<AgentMemoryFreshness> {
359
+ const watchFile = join(workspaceRoot, ".forge", "agent", "events.ndjson");
360
+ if (!existsSync(watchFile)) {
361
+ return { status: "current", pendingBytes: 0, queuedEvents: 0, inspectedEventsTruncated: false };
362
+ }
363
+ let drainError: string | undefined;
364
+ try {
365
+ const drained = await drainAgentMemoryQueueFile({
366
+ workspaceRoot,
367
+ watchFile,
368
+ source: "codex",
369
+ maxEvents: 128,
370
+ maxDurationMs: 1_000,
371
+ });
372
+ drainError = drained.busy ? "Agent Memory store is busy" : drained.errors[0];
373
+ } catch (error) {
374
+ drainError = error instanceof Error ? error.message : String(error);
375
+ }
376
+ try {
377
+ const queue = inspectAgentMemoryQueueFile({ workspaceRoot, watchFile, source: "codex" });
378
+ const pendingBytes = Math.max(0, statSync(watchFile).size - queue.bytesRead);
379
+ return {
380
+ status: drainError ? "unavailable" : pendingBytes > 0 ? "pending" : "current",
381
+ pendingBytes,
382
+ queuedEvents: queue.events,
383
+ inspectedEventsTruncated: queue.truncated === true,
384
+ ...(queue.latestEventAt ? { lastQueuedAt: queue.latestEventAt } : {}),
385
+ ...(drainError ? { error: drainError } : {}),
386
+ };
387
+ } catch (error) {
388
+ return {
389
+ status: "unavailable",
390
+ pendingBytes: 0,
391
+ queuedEvents: 0,
392
+ inspectedEventsTruncated: false,
393
+ error: error instanceof Error ? error.message : String(error),
394
+ };
395
+ }
396
+ }
397
+
398
+ export async function ingestEnvelope(workspaceRoot: string, envelope: AgentEventEnvelope): Promise<AgentIngestResult> {
399
+ const store = await openMemoryStore(workspaceRoot, "write");
400
+ if (isMemoryUnavailable(store)) {
401
+ if (shouldUseFallbackMemory(store, workspaceRoot)) {
402
+ const event = appendFallbackAgentMemoryEvent(
403
+ workspaceRoot,
404
+ envelope,
405
+ summarizeAgentEvent(envelope),
406
+ extractAgentEventBindings(envelope),
407
+ );
408
+ return {
409
+ ok: true,
410
+ event,
411
+ envelope,
412
+ exitCode: 0,
413
+ fallback: {
414
+ kind: "agent-events-ndjson",
415
+ path: ".forge/agent/events.ndjson",
416
+ reason: "pglite-active",
417
+ },
418
+ };
419
+ }
420
+ return {
421
+ ...store,
422
+ envelope,
423
+ };
424
+ }
425
+ try {
426
+ return await recordAgentMemoryEnvelope(store, envelope);
427
+ } finally {
428
+ await store.close();
429
+ }
430
+ }
431
+
432
+ async function recordAgentMemoryEnvelope(store: DeltaStore, envelope: AgentEventEnvelope, idempotencyKey?: string): Promise<AgentIngestResult> {
433
+ const event = await store.recordAgentMemoryEvent({
434
+ envelope,
435
+ summary: summarizeAgentEvent(envelope),
436
+ bindings: extractAgentEventBindings(envelope),
437
+ ...(idempotencyKey ? { idempotencyKey } : {}),
438
+ });
439
+ return { ok: true, event, envelope, exitCode: 0 };
440
+ }
441
+
442
+ async function ingestAgentMemory(options: AgentMemoryCommandOptions): Promise<AgentIngestResult> {
443
+ const source = options.source ?? options.target ?? "generic";
444
+ const raw = normalizeRawInput(options.input ?? await readStdinJson({ timeoutMs: 2000 }));
445
+ if (!raw) {
446
+ return { ok: false, exitCode: 1, error: "agent ingest requires JSON input on stdin or --input" };
447
+ }
448
+ const envelope = normalizeAgentEvent({
449
+ workspaceRoot: options.workspaceRoot,
450
+ source,
451
+ eventName: options.eventName,
452
+ raw,
453
+ integration: source === "cursor" ? "mcp" : "native-hook",
454
+ });
455
+ return ingestEnvelope(options.workspaceRoot, envelope);
456
+ }
457
+
458
+ async function ingestAgentMemoryQueueFile(options: AgentMemoryCommandOptions): Promise<AgentIngestWatchResult> {
459
+ const source = options.source ?? options.target ?? "generic";
460
+ const watchFile = isAbsolute(options.file ?? "")
461
+ ? options.file as string
462
+ : resolve(options.workspaceRoot, options.file ?? "");
463
+ if (!options.file) {
464
+ return {
465
+ ok: false,
466
+ watch: false,
467
+ source,
468
+ eventsIngested: 0,
469
+ errors: ["agent ingest --file requires --file <events.jsonl|events.ndjson>"],
470
+ nextActions: [`forge agent ingest ${source} --file .forge/agent/events.ndjson --json`],
471
+ exitCode: 1,
472
+ };
473
+ }
474
+ if (!existsSync(watchFile)) {
475
+ return {
476
+ ok: false,
477
+ watch: false,
478
+ source,
479
+ file: options.file,
480
+ eventsIngested: 0,
481
+ errors: [`queue file does not exist: ${options.file}`],
482
+ nextActions: [`forge agent hooks status --target ${source} --json`],
483
+ exitCode: 1,
484
+ };
485
+ }
486
+ const drained = await drainAgentMemoryQueueFile({
487
+ workspaceRoot: options.workspaceRoot,
488
+ watchFile,
489
+ source,
490
+ eventName: options.eventName,
491
+ });
492
+ const errors = [
493
+ ...drained.errors,
494
+ ...(drained.busy ? ["DeltaDB is busy; queue checkpoint was not advanced"] : []),
495
+ ];
496
+ return {
497
+ ok: errors.length === 0,
498
+ watch: false,
499
+ source,
500
+ file: options.file,
501
+ eventsIngested: drained.eventsIngested,
502
+ errors,
503
+ bytesRead: drained.bytesRead,
504
+ pendingBytes: drained.pendingBytes,
505
+ checkpointFile: drained.checkpointFile,
506
+ compacted: drained.compacted,
507
+ historyFile: drained.historyFile,
508
+ ...(drained.busy ? { busy: drained.busy, pendingDueToBusy: true } : {}),
509
+ nextActions: [
510
+ ...(drained.busy ? ["forge delta status --json"] : []),
511
+ `forge agent memory --entry ${source} --json`,
512
+ `forge agent hooks status --target ${source} --json`,
513
+ ],
514
+ exitCode: errors.length === 0 ? 0 : 1,
515
+ };
516
+ }
517
+
518
+ function queueCheckpointPath(watchFile: string): string {
519
+ return `${watchFile}.checkpoint.json`;
520
+ }
521
+
522
+ function queueHistoryPath(watchFile: string): string {
523
+ return `${watchFile}.history`;
524
+ }
525
+
526
+ function queueAppendLockPath(watchFile: string): string {
527
+ return `${watchFile}.append-lock.json`;
528
+ }
529
+
530
+ function queueDrainLockPath(watchFile: string): string {
531
+ return `${watchFile}.drain-lock.json`;
532
+ }
533
+
534
+ function queueLockHolderAlive(pid: unknown): boolean {
535
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
536
+ return false;
537
+ }
538
+ try {
539
+ process.kill(pid, 0);
540
+ return true;
541
+ } catch (error) {
542
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
543
+ }
544
+ }
545
+
546
+ function clearStaleQueueAppendLock(lockPath: string): void {
547
+ try {
548
+ const stat = statSync(lockPath);
549
+ const holder = JSON.parse(readFileSync(lockPath, "utf8")) as { pid?: unknown; createdAt?: unknown };
550
+ const ageMs = Date.now() - stat.mtimeMs;
551
+ // A large queue can take longer than 30 seconds to compact. Reclaiming a
552
+ // live writer's lock by age would let a hook append to the old queue just
553
+ // before the atomic replacement and silently drop that event.
554
+ if (ageMs < 2_000 || (queueLockHolderAlive(holder.pid) &&
555
+ !(typeof holder.pid === "number" && pidWasReused(holder.pid, holder.createdAt)))) {
556
+ return;
557
+ }
558
+ unlinkSync(lockPath);
559
+ } catch {
560
+ // A concurrent hook may have replaced the lock; retry acquisition instead.
561
+ }
562
+ }
563
+
564
+ async function acquireQueueFileLock(lockPath: string, waitMs: number): Promise<string | null> {
565
+ const token = randomUUID();
566
+ const started = Date.now();
567
+ for (;;) {
568
+ try {
569
+ const fd = openSync(lockPath, "wx");
570
+ try {
571
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, token, createdAt: new Date().toISOString() }));
572
+ } finally {
573
+ closeSync(fd);
574
+ }
575
+ return token;
576
+ } catch (error) {
577
+ if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
578
+ throw error;
579
+ }
580
+ clearStaleQueueAppendLock(lockPath);
581
+ if (Date.now() - started >= waitMs) {
582
+ return null;
583
+ }
584
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 15));
585
+ }
586
+ }
587
+ }
588
+
589
+ async function acquireQueueAppendLock(watchFile: string, waitMs = 500): Promise<string | null> {
590
+ return acquireQueueFileLock(queueAppendLockPath(watchFile), waitMs);
591
+ }
592
+
593
+ function releaseQueueFileLock(lockPath: string, token: string): void {
594
+ try {
595
+ const holder = JSON.parse(readFileSync(lockPath, "utf8")) as { token?: unknown };
596
+ if (holder.token === token) {
597
+ unlinkSync(lockPath);
598
+ }
599
+ } catch {
600
+ // Best effort; stale lock recovery handles interrupted processes.
601
+ }
602
+ }
603
+
604
+ function releaseQueueAppendLock(watchFile: string, token: string): void {
605
+ releaseQueueFileLock(queueAppendLockPath(watchFile), token);
606
+ }
607
+
608
+ function readQueueCheckpoint(watchFile: string, fileSize: number): number {
609
+ const checkpointFile = queueCheckpointPath(watchFile);
610
+ if (!existsSync(checkpointFile)) {
611
+ return 0;
612
+ }
613
+ try {
614
+ const parsed = JSON.parse(readFileSync(checkpointFile, "utf8")) as unknown;
615
+ const checkpoint = parsed && typeof parsed === "object" && !Array.isArray(parsed)
616
+ ? parsed as { offset?: unknown; generation?: unknown }
617
+ : undefined;
618
+ const offset = checkpoint?.offset;
619
+ if (typeof offset !== "number" || !Number.isFinite(offset) || offset < 0) {
620
+ return 0;
621
+ }
622
+ // An atomic queue replacement can land before its checkpoint reset. The
623
+ // generation mismatch makes the new tail replay from byte zero.
624
+ if ((typeof checkpoint?.generation === "string" ? checkpoint.generation : undefined) !== readQueueGeneration(watchFile)) {
625
+ return 0;
626
+ }
627
+ return offset > fileSize ? 0 : Math.floor(offset);
628
+ } catch {
629
+ return 0;
630
+ }
631
+ }
632
+
633
+ function readQueueGeneration(watchFile: string): string | undefined {
634
+ if (!existsSync(watchFile)) return undefined;
635
+ const fd = openSync(watchFile, "r");
636
+ try {
637
+ const prefix = Buffer.alloc(256);
638
+ const bytes = readSync(fd, prefix, 0, prefix.length, 0);
639
+ const newline = prefix.subarray(0, bytes).indexOf(10);
640
+ if (newline < 0) return undefined;
641
+ const value = JSON.parse(prefix.subarray(0, newline).toString("utf8")) as unknown;
642
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
643
+ const generation = (value as { forgeHookQueueGeneration?: unknown }).forgeHookQueueGeneration;
644
+ return typeof generation === "string" && /^[0-9a-f-]{36}$/iu.test(generation) ? generation : undefined;
645
+ } catch {
646
+ return undefined;
647
+ } finally {
648
+ closeSync(fd);
649
+ }
650
+ }
651
+
652
+ function writeQueueCheckpoint(watchFile: string, offset: number, generation = readQueueGeneration(watchFile)): void {
653
+ const checkpointFile = queueCheckpointPath(watchFile);
654
+ mkdirSync(dirname(checkpointFile), { recursive: true });
655
+ const temporary = `${checkpointFile}.${process.pid}.${randomUUID()}.tmp`;
656
+ try {
657
+ const fd = openSync(temporary, "wx");
658
+ try {
659
+ writeFileSync(fd,
660
+ `${JSON.stringify({
661
+ schema: "forge.agent-hook-queue-checkpoint.v1",
662
+ file: watchFile,
663
+ offset,
664
+ ...(generation ? { generation } : {}),
665
+ updatedAt: new Date().toISOString(),
666
+ }, null, 2)}\n`,
667
+ "utf8",
668
+ );
669
+ fsyncSync(fd);
670
+ } finally {
671
+ closeSync(fd);
672
+ }
673
+ renameSync(temporary, checkpointFile);
674
+ syncParentDirectory(checkpointFile);
675
+ } finally {
676
+ if (existsSync(temporary)) unlinkSync(temporary);
677
+ }
678
+ }
679
+
680
+ function syncParentDirectory(path: string): void {
681
+ try {
682
+ const fd = openSync(dirname(path), "r");
683
+ try { fsyncSync(fd); } finally { closeSync(fd); }
684
+ } catch {
685
+ // Windows cannot fsync a directory through Node. The file itself was
686
+ // fsynced before rename, and generation checks recover either rename state.
687
+ }
688
+ }
689
+
690
+ const DEFAULT_QUEUE_COMPACT_AFTER_BYTES = 256 * 1024;
691
+ const DEFAULT_QUEUE_HISTORY_MAX_BYTES = 1024 * 1024;
692
+
693
+ function trimBufferStart(buffer: Buffer, maxBytes: number): Buffer {
694
+ if (buffer.length <= maxBytes) {
695
+ return buffer;
696
+ }
697
+ return buffer.subarray(buffer.length - maxBytes);
698
+ }
699
+
700
+ async function compactAgentMemoryQueueFile(options: {
701
+ watchFile: string;
702
+ originalBuffer: Buffer;
703
+ consumedOffset: number;
704
+ compactAfterBytes: number;
705
+ historyMaxBytes: number;
706
+ }): Promise<{ compacted: boolean; historyFile: string }> {
707
+ const historyFile = queueHistoryPath(options.watchFile);
708
+ if (options.consumedOffset < options.compactAfterBytes) {
709
+ return { compacted: false, historyFile };
710
+ }
711
+ const lock = await acquireQueueAppendLock(options.watchFile);
712
+ if (!lock) {
713
+ return { compacted: false, historyFile };
714
+ }
715
+ try {
716
+ const currentBuffer = readFileSync(options.watchFile);
717
+ const originalConsumed = options.originalBuffer.subarray(0, options.consumedOffset);
718
+ const currentPrefix = currentBuffer.subarray(0, options.consumedOffset);
719
+ if (!currentPrefix.equals(originalConsumed)) {
720
+ return { compacted: false, historyFile };
721
+ }
722
+ mkdirSync(dirname(historyFile), { recursive: true });
723
+ const existingHistory = existsSync(historyFile) ? readFileSync(historyFile) : Buffer.alloc(0);
724
+ const redactedConsumedHistory = redactedQueueHistoryBuffer(originalConsumed);
725
+ writeFileSync(
726
+ historyFile,
727
+ trimBufferStart(Buffer.concat([existingHistory, redactedConsumedHistory]), options.historyMaxBytes),
728
+ );
729
+ const generation = randomUUID();
730
+ const marker = Buffer.from(`${JSON.stringify({ forgeHookQueueGeneration: generation })}\n`, "utf8");
731
+ const tail = currentBuffer.subarray(options.consumedOffset);
732
+ const temporary = `${options.watchFile}.${process.pid}.${generation}.tmp`;
733
+ try {
734
+ const fd = openSync(temporary, "wx");
735
+ try {
736
+ writeFileSync(fd, Buffer.concat([marker, tail]));
737
+ fsyncSync(fd);
738
+ } finally {
739
+ closeSync(fd);
740
+ }
741
+ // The old queue remains intact until this atomic replacement. If the
742
+ // process stops before checkpoint reset, generation mismatch replays tail.
743
+ renameSync(temporary, options.watchFile);
744
+ syncParentDirectory(options.watchFile);
745
+ } finally {
746
+ if (existsSync(temporary)) unlinkSync(temporary);
747
+ }
748
+ writeQueueCheckpoint(options.watchFile, marker.length, generation);
749
+ return { compacted: true, historyFile };
750
+ } finally {
751
+ releaseQueueAppendLock(options.watchFile, lock);
752
+ }
753
+ }
754
+
755
+ function redactedQueueHistoryBuffer(consumedBuffer: Buffer): Buffer {
756
+ const { complete } = splitCompleteJsonLines(consumedBuffer);
757
+ const lines: string[] = [];
758
+ for (const line of complete) {
759
+ if (!line.raw.trim()) {
760
+ continue;
761
+ }
762
+ const parsed = normalizeRawInput(line.raw);
763
+ if (!parsed) {
764
+ lines.push(JSON.stringify({
765
+ forgeHookQueueV1: true,
766
+ historyRedacted: true,
767
+ rawStored: false,
768
+ payloadRedacted: true,
769
+ payload: { _parseError: true },
770
+ }));
771
+ continue;
772
+ }
773
+ lines.push(JSON.stringify(redactedQueueHistoryEntry(parsed)));
774
+ }
775
+ return Buffer.from(lines.length > 0 ? `${lines.join("\n")}\n` : "", "utf8");
776
+ }
777
+
778
+ function redactedQueueHistoryEntry(parsed: Record<string, unknown>): Record<string, unknown> {
779
+ if (parsed.forgeHookQueueV1 !== true) {
780
+ return {
781
+ historyRedacted: true,
782
+ rawStored: false,
783
+ payloadRedacted: true,
784
+ payload: redactAgentPayload(parsed).value,
785
+ };
786
+ }
787
+ const queuedPayload = objectField(parsed, "payload") ?? objectField(parsed, "raw") ?? {};
788
+ return {
789
+ forgeHookQueueV1: true,
790
+ source: typeof parsed.source === "string" ? parsed.source : "codex",
791
+ integration: parsed.integration === "mcp" ? "mcp" : "native-hook",
792
+ eventName: typeof parsed.eventName === "string" ? parsed.eventName : undefined,
793
+ workspaceRoot: typeof parsed.workspaceRoot === "string" ? parsed.workspaceRoot : undefined,
794
+ enqueuedAt: typeof parsed.enqueuedAt === "string" ? parsed.enqueuedAt : undefined,
795
+ historyRedacted: true,
796
+ rawStored: false,
797
+ payloadRedacted: true,
798
+ payload: parsed.payloadRedacted === true ? queuedPayload : redactAgentPayload(queuedPayload).value,
799
+ };
800
+ }
801
+
802
+ function splitCompleteJsonLines(buffer: Buffer): {
803
+ complete: Array<{ raw: string; endOffset: number }>;
804
+ completeBytes: number;
805
+ pendingBytes: number;
806
+ } {
807
+ const lastNewline = buffer.lastIndexOf(10);
808
+ if (lastNewline < 0) {
809
+ return { complete: [], completeBytes: 0, pendingBytes: buffer.length };
810
+ }
811
+ const completeBytes = lastNewline + 1;
812
+ const text = buffer.subarray(0, completeBytes).toString("utf8");
813
+ const lines: Array<{ raw: string; endOffset: number }> = [];
814
+ let offset = 0;
815
+ for (const rawLine of text.split(/(?<=\n)/)) {
816
+ if (!rawLine) {
817
+ continue;
818
+ }
819
+ const byteLength = Buffer.byteLength(rawLine);
820
+ offset += byteLength;
821
+ const normalized = rawLine.replace(/\r?\n$/, "");
822
+ lines.push({ raw: normalized, endOffset: offset });
823
+ }
824
+ return { complete: lines, completeBytes, pendingBytes: buffer.length - completeBytes };
825
+ }
826
+
827
+ const DEFAULT_QUEUE_INSPECT_MAX_BYTES = 1024 * 1024;
828
+
829
+ function inspectionBufferFromCheckpoint(fileBuffer: Buffer, checkpointOffset: number, maxBytes: number): {
830
+ buffer: Buffer;
831
+ offset: number;
832
+ truncated: boolean;
833
+ skippedBytes: number;
834
+ } {
835
+ const availableBytes = Math.max(0, fileBuffer.length - checkpointOffset);
836
+ if (availableBytes <= maxBytes) {
837
+ return {
838
+ buffer: fileBuffer.subarray(checkpointOffset),
839
+ offset: checkpointOffset,
840
+ truncated: false,
841
+ skippedBytes: 0,
842
+ };
843
+ }
844
+ let offset = fileBuffer.length - maxBytes;
845
+ const firstNewline = fileBuffer.subarray(offset).indexOf(10);
846
+ if (firstNewline >= 0) {
847
+ offset += firstNewline + 1;
848
+ }
849
+ return {
850
+ buffer: fileBuffer.subarray(offset),
851
+ offset,
852
+ truncated: true,
853
+ skippedBytes: Math.max(0, offset - checkpointOffset),
854
+ };
855
+ }
856
+
857
+ function shouldSkipQueuedHookEnvelope(
858
+ envelope: AgentEventEnvelope,
859
+ options: { source: string; workspaceRoot: string },
860
+ ): boolean {
861
+ return (
862
+ envelope.source.agent !== options.source ||
863
+ !workspaceRootsMatch(envelope.workspace.root, options.workspaceRoot) ||
864
+ envelope.payload.forgeHookProbe === true ||
865
+ envelope.payload._parseError === true ||
866
+ envelope.payload._invalidPayload === true
867
+ );
868
+ }
869
+
870
+ export interface AgentMemoryQueueDrainOptions {
871
+ workspaceRoot: string;
872
+ watchFile: string;
873
+ source: string;
874
+ eventName?: string;
875
+ startOffset?: number;
876
+ compactAfterBytes?: number;
877
+ historyMaxBytes?: number;
878
+ maxEvents?: number;
879
+ maxDurationMs?: number;
880
+ /** The owner broker can pass its already-open store to avoid a second PGlite. */
881
+ store?: DeltaStore;
882
+ }
883
+
884
+ export interface AgentMemoryQueueDrainResult {
885
+ eventsIngested: number;
886
+ errors: string[];
887
+ bytesRead: number;
888
+ pendingBytes: number;
889
+ checkpointFile: string;
890
+ compacted: boolean;
891
+ historyFile: string;
892
+ busy?: AgentMemoryUnavailableResult["busy"];
893
+ }
894
+
895
+ export async function drainAgentMemoryQueueFile(options: AgentMemoryQueueDrainOptions): Promise<AgentMemoryQueueDrainResult> {
896
+ const lockPath = queueDrainLockPath(options.watchFile);
897
+ const lock = await acquireQueueFileLock(lockPath, options.store ? 0 : options.maxDurationMs ? 1_000 : 30_000);
898
+ if (!lock) {
899
+ const fileSize = existsSync(options.watchFile) ? statSync(options.watchFile).size : 0;
900
+ const checkpoint = readQueueCheckpoint(options.watchFile, fileSize);
901
+ return {
902
+ eventsIngested: 0,
903
+ errors: [],
904
+ bytesRead: checkpoint,
905
+ pendingBytes: Math.max(0, fileSize - checkpoint),
906
+ checkpointFile: queueCheckpointPath(options.watchFile),
907
+ compacted: false,
908
+ historyFile: queueHistoryPath(options.watchFile),
909
+ };
910
+ }
911
+ try {
912
+ return await drainAgentMemoryQueueFileUnlocked(options);
913
+ } finally {
914
+ releaseQueueFileLock(lockPath, lock);
915
+ }
916
+ }
917
+
918
+ async function drainAgentMemoryQueueFileUnlocked(options: AgentMemoryQueueDrainOptions): Promise<AgentMemoryQueueDrainResult> {
919
+ const historyFile = queueHistoryPath(options.watchFile);
920
+ if (!existsSync(options.watchFile)) {
921
+ return {
922
+ eventsIngested: 0,
923
+ errors: [],
924
+ bytesRead: 0,
925
+ pendingBytes: 0,
926
+ checkpointFile: queueCheckpointPath(options.watchFile),
927
+ compacted: false,
928
+ historyFile,
929
+ };
930
+ }
931
+ // Avoid opening PGlite when the queue has no complete lines. Once there is work,
932
+ // acquire its writer lock before reading the checkpoint: another drainer may
933
+ // have consumed these bytes while this process waited for the lock.
934
+ const initialBuffer = readFileSync(options.watchFile);
935
+ const initialOffset = options.startOffset ?? readQueueCheckpoint(options.watchFile, initialBuffer.length);
936
+ if (splitCompleteJsonLines(initialBuffer.subarray(Math.min(initialOffset, initialBuffer.length))).complete.length === 0) {
937
+ return {
938
+ eventsIngested: 0,
939
+ errors: [],
940
+ bytesRead: initialOffset,
941
+ pendingBytes: Math.max(0, initialBuffer.length - initialOffset),
942
+ checkpointFile: queueCheckpointPath(options.watchFile),
943
+ compacted: false,
944
+ historyFile,
945
+ };
946
+ }
947
+
948
+ const opened = options.store ?? await openMemoryStore(options.workspaceRoot, "write");
949
+ if (isMemoryUnavailable(opened)) {
950
+ return {
951
+ eventsIngested: 0,
952
+ errors: opened.busy ? [] : [opened.error],
953
+ bytesRead: initialOffset,
954
+ pendingBytes: Math.max(0, initialBuffer.length - initialOffset),
955
+ checkpointFile: queueCheckpointPath(options.watchFile),
956
+ compacted: false,
957
+ historyFile,
958
+ ...(opened.busy ? { busy: opened.busy } : {}),
959
+ };
960
+ }
961
+
962
+ let fileBuffer: Buffer;
963
+ try {
964
+ fileBuffer = readFileSync(options.watchFile);
965
+ } catch (error) {
966
+ if (!options.store) await opened.close();
967
+ throw error;
968
+ }
969
+ let bytesRead = options.startOffset ?? readQueueCheckpoint(options.watchFile, fileBuffer.length);
970
+ if (bytesRead > fileBuffer.length) {
971
+ bytesRead = 0;
972
+ }
973
+ const { complete } = splitCompleteJsonLines(fileBuffer.subarray(bytesRead));
974
+ const queueGeneration = readQueueGeneration(options.watchFile);
975
+ let eventsIngested = 0;
976
+ const errors: string[] = [];
977
+ let consumedOffset = bytesRead;
978
+ let linesProcessed = 0;
979
+ const startedAt = Date.now();
980
+ const store = opened;
981
+
982
+ try {
983
+ for (const line of complete) {
984
+ // The local owner also serves CLI/MCP IPC. PGlite may resolve work in
985
+ // microtasks, so a long replay must explicitly let the socket poll phase
986
+ // run between records instead of starving health and read requests.
987
+ if (options.store) await new Promise<void>((resolveYield) => setImmediate(resolveYield));
988
+ if ((options.maxEvents && linesProcessed >= options.maxEvents) ||
989
+ (options.maxDurationMs && linesProcessed > 0 && Date.now() - startedAt >= options.maxDurationMs)) {
990
+ break;
991
+ }
992
+ linesProcessed += 1;
993
+ if (!line.raw.trim()) {
994
+ consumedOffset = bytesRead + line.endOffset;
995
+ continue;
996
+ }
997
+ const parsed = normalizeRawInput(line.raw);
998
+ if (!parsed) {
999
+ quarantineQueuedLine(options.watchFile, line.raw, bytesRead + line.endOffset, "invalid-json-object");
1000
+ consumedOffset = bytesRead + line.endOffset;
1001
+ continue;
1002
+ }
1003
+ if (typeof parsed.forgeHookQueueGeneration === "string") {
1004
+ consumedOffset = bytesRead + line.endOffset;
1005
+ continue;
1006
+ }
1007
+ if (isAgentMemoryEventRecord(parsed)) {
1008
+ const legacyEnvelope = legacyFallbackEnvelope(parsed);
1009
+ if (!legacyEnvelope) {
1010
+ quarantineQueuedLine(options.watchFile, line.raw, bytesRead + line.endOffset, "invalid-legacy-memory-record");
1011
+ consumedOffset = bytesRead + line.endOffset;
1012
+ continue;
1013
+ }
1014
+ if (!workspaceRootsMatch(legacyEnvelope.workspace.root, options.workspaceRoot)) {
1015
+ consumedOffset = bytesRead + line.endOffset;
1016
+ continue;
1017
+ }
1018
+ const migrated = await recordAgentMemoryEnvelope(store, legacyEnvelope, `fallback:${parsed.id}`);
1019
+ if (!migrated.ok) {
1020
+ errors.push(migrated.error ?? "legacy Agent Memory migration failed");
1021
+ break;
1022
+ }
1023
+ eventsIngested += 1;
1024
+ consumedOffset = bytesRead + line.endOffset;
1025
+ continue;
1026
+ }
1027
+ const queued = parseQueuedHookLine(parsed);
1028
+ const payload = queued?.payload ?? parsed;
1029
+ const ingestRoot = queued?.workspaceRoot ?? options.workspaceRoot;
1030
+ const ingestSource = queued?.source ?? options.source;
1031
+ const envelope = normalizeAgentEvent({
1032
+ workspaceRoot: ingestRoot,
1033
+ source: ingestSource,
1034
+ eventName: queued?.eventName ?? options.eventName,
1035
+ raw: payload,
1036
+ integration: queued?.integration ?? (ingestSource === "cursor" ? "mcp" : "native-hook"),
1037
+ });
1038
+ if (shouldSkipQueuedHookEnvelope(envelope, { source: options.source, workspaceRoot: options.workspaceRoot })) {
1039
+ consumedOffset = bytesRead + line.endOffset;
1040
+ continue;
1041
+ }
1042
+ const queueEventId = typeof parsed.queueEventId === "string" && /^[0-9a-f-]{36}$/iu.test(parsed.queueEventId)
1043
+ ? parsed.queueEventId
1044
+ : createHash("sha256").update(`${queueGeneration ?? "legacy"}:${bytesRead + line.endOffset}:${line.raw}`).digest("hex");
1045
+ const result = await recordAgentMemoryEnvelope(store, envelope, `hook:${ingestSource}:${queueEventId}`);
1046
+ if (result.ok) {
1047
+ eventsIngested += 1;
1048
+ consumedOffset = bytesRead + line.endOffset;
1049
+ } else if (isDeltaBusyIngestResult(result)) {
1050
+ if (consumedOffset !== bytesRead) writeQueueCheckpoint(options.watchFile, consumedOffset, queueGeneration);
1051
+ return {
1052
+ eventsIngested,
1053
+ errors,
1054
+ bytesRead,
1055
+ pendingBytes: Math.max(0, fileBuffer.length - consumedOffset),
1056
+ checkpointFile: queueCheckpointPath(options.watchFile),
1057
+ compacted: false,
1058
+ historyFile,
1059
+ busy: result.busy,
1060
+ };
1061
+ } else {
1062
+ errors.push(result.error ?? "agent memory ingest failed");
1063
+ break;
1064
+ }
1065
+ }
1066
+
1067
+ // Every persisted event has a stable idempotency key. Committing one
1068
+ // checkpoint per bounded batch avoids thousands of fsyncs during replay;
1069
+ // a crash before this write safely replays the batch without duplicating it.
1070
+ if (consumedOffset !== bytesRead) writeQueueCheckpoint(options.watchFile, consumedOffset, queueGeneration);
1071
+
1072
+ const retention = errors.length === 0 && consumedOffset > 0
1073
+ ? await compactAgentMemoryQueueFile({
1074
+ watchFile: options.watchFile,
1075
+ originalBuffer: fileBuffer,
1076
+ consumedOffset,
1077
+ compactAfterBytes: options.compactAfterBytes ?? DEFAULT_QUEUE_COMPACT_AFTER_BYTES,
1078
+ historyMaxBytes: options.historyMaxBytes ?? DEFAULT_QUEUE_HISTORY_MAX_BYTES,
1079
+ })
1080
+ : { compacted: false, historyFile };
1081
+ const bytesAfterRetention = retention.compacted
1082
+ ? readQueueCheckpoint(options.watchFile, statSync(options.watchFile).size)
1083
+ : consumedOffset;
1084
+
1085
+ return {
1086
+ eventsIngested,
1087
+ errors,
1088
+ bytesRead: bytesAfterRetention,
1089
+ pendingBytes: Math.max(0, statSync(options.watchFile).size - bytesAfterRetention),
1090
+ checkpointFile: queueCheckpointPath(options.watchFile),
1091
+ compacted: retention.compacted,
1092
+ historyFile: retention.historyFile,
1093
+ };
1094
+ } finally {
1095
+ if (!options.store) await store.close();
1096
+ }
1097
+ }
1098
+
1099
+ function quarantineQueuedLine(watchFile: string, raw: string, offset: number, reason: string): void {
1100
+ // Preserve only a digest and position. Legacy queue lines may contain private
1101
+ // fields, so a rejection log must never copy their raw text.
1102
+ appendFileSync(`${watchFile}.rejects.ndjson`, `${JSON.stringify({
1103
+ schema: "forge.agent-hook-rejection.v1",
1104
+ offset,
1105
+ hash: createHash("sha256").update(raw).digest("hex"),
1106
+ reason,
1107
+ rejectedAt: new Date().toISOString(),
1108
+ })}\n`, "utf8");
1109
+ }
1110
+
1111
+ function legacyFallbackEnvelope(record: AgentMemoryEventRecord): AgentEventEnvelope | undefined {
1112
+ const data = record.data;
1113
+ if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
1114
+ const envelope = data.envelope;
1115
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) return undefined;
1116
+ const candidate = envelope as Partial<AgentEventEnvelope>;
1117
+ if (candidate.schema !== "forge.agent-event.v1" ||
1118
+ typeof candidate.source?.agent !== "string" || typeof candidate.source.integration !== "string" ||
1119
+ typeof candidate.workspace?.root !== "string" || typeof candidate.event?.kind !== "string" ||
1120
+ typeof candidate.event.timestamp !== "string" || typeof candidate.capture?.trustLevel !== "string" ||
1121
+ typeof candidate.capture.confidence !== "number" || !candidate.payload ||
1122
+ typeof candidate.payload !== "object" || Array.isArray(candidate.payload)) return undefined;
1123
+ return candidate as AgentEventEnvelope;
1124
+ }
1125
+
1126
+ function queuedEventHasUsefulSignal(envelope: AgentEventEnvelope): boolean {
1127
+ const bindings = extractAgentEventBindings(envelope);
1128
+ const files = bindings.files;
1129
+ const entries = bindings.entries;
1130
+ const proofs = bindings.proofs;
1131
+ return (
1132
+ typeof bindings.toolName === "string" ||
1133
+ typeof bindings.command === "string" ||
1134
+ typeof bindings.status === "string" ||
1135
+ (Array.isArray(files) && files.length > 0) ||
1136
+ (Array.isArray(entries) && entries.length > 0) ||
1137
+ (Array.isArray(proofs) && proofs.length > 0)
1138
+ );
1139
+ }
1140
+
1141
+ function queuedEventTimestamp(raw: Record<string, unknown>, envelope: AgentEventEnvelope): string | undefined {
1142
+ const enqueuedAt = raw.enqueuedAt;
1143
+ return typeof enqueuedAt === "string" && enqueuedAt.length > 0
1144
+ ? enqueuedAt
1145
+ : envelope.event.timestamp;
1146
+ }
1147
+
1148
+ function workspaceRootsMatch(left: string | undefined, right: string): boolean {
1149
+ if (!left) {
1150
+ return true;
1151
+ }
1152
+ return resolve(left) === resolve(right);
1153
+ }
1154
+
1155
+ export function inspectAgentMemoryQueueFile(options: {
1156
+ workspaceRoot: string;
1157
+ watchFile: string;
1158
+ source: string;
1159
+ eventName?: string;
1160
+ }): AgentMemoryQueueInspectionResult {
1161
+ const checkpointFile = queueCheckpointPath(options.watchFile);
1162
+ const base = {
1163
+ exists: existsSync(options.watchFile),
1164
+ source: options.source,
1165
+ file: options.watchFile,
1166
+ events: 0,
1167
+ nativeSignals: 0,
1168
+ canarySignals: 0,
1169
+ usefulSignals: 0,
1170
+ ignoredOutOfWorkspaceEvents: 0,
1171
+ bytesRead: 0,
1172
+ pendingBytes: 0,
1173
+ checkpointFile,
1174
+ errors: [] as string[],
1175
+ };
1176
+ if (!base.exists) {
1177
+ return base;
1178
+ }
1179
+ const fileBuffer = readFileSync(options.watchFile);
1180
+ const bytesRead = readQueueCheckpoint(options.watchFile, fileBuffer.length);
1181
+ const inspected = inspectionBufferFromCheckpoint(fileBuffer, bytesRead, DEFAULT_QUEUE_INSPECT_MAX_BYTES);
1182
+ const { complete } = splitCompleteJsonLines(inspected.buffer);
1183
+ const result: AgentMemoryQueueInspectionResult = {
1184
+ ...base,
1185
+ bytesRead,
1186
+ inspectedBytes: inspected.buffer.length,
1187
+ skippedBytes: inspected.skippedBytes,
1188
+ truncated: inspected.truncated,
1189
+ pendingBytes: Math.max(0, fileBuffer.length - bytesRead),
1190
+ };
1191
+ for (const line of complete) {
1192
+ if (!line.raw.trim()) {
1193
+ continue;
1194
+ }
1195
+ const parsed = normalizeRawInput(line.raw);
1196
+ if (!parsed) {
1197
+ result.errors.push(`could not parse queued hook line at byte ${inspected.offset + line.endOffset}`);
1198
+ continue;
1199
+ }
1200
+ if (typeof parsed.forgeHookQueueGeneration === "string" || isAgentMemoryEventRecord(parsed)) continue;
1201
+ const queued = parseQueuedHookLine(parsed);
1202
+ const payload = queued?.payload ?? parsed;
1203
+ const source = queued?.source ?? options.source;
1204
+ const workspaceRoot = queued?.workspaceRoot ?? options.workspaceRoot;
1205
+ const envelope = normalizeAgentEvent({
1206
+ workspaceRoot,
1207
+ source,
1208
+ eventName: queued?.eventName ?? options.eventName,
1209
+ raw: payload,
1210
+ integration: queued?.integration ?? (source === "cursor" ? "mcp" : "native-hook"),
1211
+ });
1212
+ if (source !== options.source) {
1213
+ continue;
1214
+ }
1215
+ if (!workspaceRootsMatch(workspaceRoot, options.workspaceRoot)) {
1216
+ result.ignoredOutOfWorkspaceEvents += 1;
1217
+ continue;
1218
+ }
1219
+ const canary = envelope.payload.forgeHookCanary === "FORGE_HOOK_SMOKE_CANARY";
1220
+ if (shouldSkipQueuedHookEnvelope(envelope, { source: options.source, workspaceRoot: options.workspaceRoot })) {
1221
+ continue;
1222
+ }
1223
+ result.events += 1;
1224
+ if (canary) {
1225
+ result.canarySignals += 1;
1226
+ } else if (envelope.source.integration === "native-hook" && envelope.capture.trustLevel === "direct-hook" &&
1227
+ typeof envelope.session.externalSessionId === "string" &&
1228
+ envelope.session.externalSessionId !== "forge-hook-probe" &&
1229
+ envelope.payload.forgeHookProbe !== true) {
1230
+ result.nativeSignals += 1;
1231
+ }
1232
+ if (queuedEventHasUsefulSignal(envelope)) {
1233
+ result.usefulSignals += 1;
1234
+ }
1235
+ const timestamp = queuedEventTimestamp(parsed, envelope);
1236
+ if (timestamp && (!result.latestEventAt || timestamp > result.latestEventAt)) {
1237
+ result.latestEventAt = timestamp;
1238
+ }
1239
+ }
1240
+ return result;
1241
+ }
1242
+
1243
+ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promise<AgentIngestWatchResult> {
1244
+ const source = options.source ?? options.target ?? "generic";
1245
+ const file = options.file;
1246
+ if (options.dryRun) {
1247
+ return {
1248
+ ok: true,
1249
+ watch: true,
1250
+ source,
1251
+ ...(file ? { file } : {}),
1252
+ dryRun: true,
1253
+ eventsIngested: 0,
1254
+ errors: [],
1255
+ nextActions: [
1256
+ `forge agent ingest ${source} --watch${file ? ` --file ${file}` : ""} --json`,
1257
+ `forge agent hooks status --target ${source} --json`,
1258
+ ],
1259
+ exitCode: 0,
1260
+ };
1261
+ }
1262
+ if (!file) {
1263
+ return {
1264
+ ok: false,
1265
+ watch: true,
1266
+ source,
1267
+ eventsIngested: 0,
1268
+ errors: ["agent ingest --watch requires --file <events.jsonl|events.ndjson>"],
1269
+ nextActions: [`forge agent ingest ${source} --watch --file .forge/agent/events.ndjson --json`],
1270
+ exitCode: 1,
1271
+ };
1272
+ }
1273
+ const watchFile = isAbsolute(file) ? file : resolve(options.workspaceRoot, file);
1274
+ if (!existsSync(watchFile)) {
1275
+ return {
1276
+ ok: false,
1277
+ watch: true,
1278
+ source,
1279
+ file,
1280
+ eventsIngested: 0,
1281
+ errors: [`watch file does not exist: ${file}`],
1282
+ nextActions: [`New-Item -ItemType File -Path ${file}`, `forge agent ingest ${source} --watch --file ${file} --json`],
1283
+ exitCode: 1,
1284
+ };
1285
+ }
1286
+
1287
+ let eventsIngested = 0;
1288
+ const errors: string[] = [];
1289
+ let busyRetries = 0;
1290
+ let lastBusy: AgentMemoryUnavailableResult["busy"] | undefined;
1291
+ let pendingIngest = Promise.resolve();
1292
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
1293
+ const scheduleBusyRetry = () => {
1294
+ if (retryTimer) {
1295
+ return;
1296
+ }
1297
+ retryTimer = setTimeout(() => {
1298
+ retryTimer = undefined;
1299
+ pendingIngest = pendingIngest.then(ingestNewContent, ingestNewContent);
1300
+ }, 500);
1301
+ };
1302
+ const ingestNewContent = async () => {
1303
+ const result = await drainAgentMemoryQueueFile({
1304
+ workspaceRoot: options.workspaceRoot,
1305
+ watchFile,
1306
+ source,
1307
+ eventName: options.eventName,
1308
+ });
1309
+ eventsIngested += result.eventsIngested;
1310
+ if (result.busy) {
1311
+ busyRetries += 1;
1312
+ lastBusy = result.busy;
1313
+ scheduleBusyRetry();
1314
+ return;
1315
+ }
1316
+ lastBusy = undefined;
1317
+ errors.push(...result.errors);
1318
+ };
1319
+
1320
+ await ingestNewContent();
1321
+ return await new Promise<AgentIngestWatchResult>((resolve) => {
1322
+ const scheduleIngest = () => {
1323
+ pendingIngest = pendingIngest.then(ingestNewContent, ingestNewContent);
1324
+ };
1325
+ // File notifications can be coalesced or lost during queue compaction and
1326
+ // on Windows. Polling also resumes after a transient Delta busy result.
1327
+ const pollTimer = setInterval(scheduleIngest, Math.max(250, options.pollIntervalMs ?? 2_000));
1328
+ let watcher: ReturnType<typeof watch> | undefined;
1329
+ try {
1330
+ watcher = watch(watchFile, { persistent: true }, scheduleIngest);
1331
+ watcher.on("error", () => {
1332
+ watcher?.close();
1333
+ watcher = undefined;
1334
+ });
1335
+ } catch {
1336
+ // The polling timer keeps draining when fs.watch is unavailable.
1337
+ }
1338
+ const shutdown = () => {
1339
+ if (retryTimer) {
1340
+ clearTimeout(retryTimer);
1341
+ retryTimer = undefined;
1342
+ }
1343
+ clearInterval(pollTimer);
1344
+ watcher?.close();
1345
+ void pendingIngest.finally(() => {
1346
+ resolve({
1347
+ ok: errors.length === 0,
1348
+ watch: true,
1349
+ source,
1350
+ file,
1351
+ eventsIngested,
1352
+ errors,
1353
+ ...(lastBusy ? { busy: lastBusy, pendingDueToBusy: true, busyRetries } : {}),
1354
+ nextActions: [
1355
+ ...(lastBusy ? ["forge delta status --json"] : []),
1356
+ `forge agent memory --entry ${source} --json`,
1357
+ `forge agent hooks status --target ${source} --json`,
1358
+ ],
1359
+ exitCode: errors.length === 0 ? 0 : 1,
1360
+ });
1361
+ });
1362
+ };
1363
+ process.once("SIGINT", shutdown);
1364
+ process.once("SIGTERM", shutdown);
1365
+ });
1366
+ }
1367
+
1368
+ function installAgentMemory(options: AgentMemoryCommandOptions): AgentInstallResult {
1369
+ const target = normalizeInstallTarget(options.target ?? options.source ?? "generic");
1370
+ const previousCodexMeta = target === "codex" ? readCodexHookMeta(options.workspaceRoot) : null;
1371
+ const mcpServer = options.mcpServer ?? (previousCodexMeta?.transport === "mcp_tool" ? previousCodexMeta.mcpServer : undefined);
1372
+ const files =
1373
+ target === "codex"
1374
+ ? codexInstallFiles(options.workspaceRoot, mcpServer)
1375
+ : target === "claude-code"
1376
+ ? claudeCodeInstallFiles()
1377
+ : target === "cursor"
1378
+ ? cursorInstallFiles()
1379
+ : [];
1380
+ if (files.length === 0) {
1381
+ return {
1382
+ ok: false,
1383
+ target,
1384
+ filesWritten: [],
1385
+ filesPlanned: [],
1386
+ privacy: privacyDefaults(),
1387
+ warnings: [`unknown agent memory install target: ${target}`],
1388
+ exitCode: 1,
1389
+ };
1390
+ }
1391
+ const filesWritten: string[] = [];
1392
+ for (const file of files) {
1393
+ const absolute = join(options.workspaceRoot, file.path);
1394
+ const content = maybeMergeJson(absolute, file.content);
1395
+ if (options.dryRun) {
1396
+ continue;
1397
+ }
1398
+ if (!options.force && existsSync(absolute) && readFileSync(absolute, "utf8") === content) {
1399
+ continue;
1400
+ }
1401
+ mkdirSync(dirname(absolute), { recursive: true });
1402
+ writeFileSync(absolute, content);
1403
+ filesWritten.push(file.path);
1404
+ }
1405
+ const planned = files.map((file) => file.path);
1406
+ if (target === "codex") {
1407
+ return codexInstallResult(filesWritten, planned);
1408
+ }
1409
+ if (target === "claude-code") {
1410
+ return claudeCodeInstallResult(filesWritten, planned);
1411
+ }
1412
+ return cursorInstallResult(filesWritten, planned);
1413
+ }
1414
+
1415
+ function sleep(ms: number): Promise<void> {
1416
+ return new Promise((resolve) => setTimeout(resolve, ms));
1417
+ }
1418
+
1419
+ export function formatAgentMemoryJson(result: AgentMemoryCommandResult): string {
1420
+ return `${JSON.stringify(result, null, 2)}\n`;
1421
+ }
1422
+
1423
+ export function formatAgentMemoryHuman(result: AgentMemoryCommandResult): string {
1424
+ if ("event" in result || "envelope" in result) {
1425
+ return result.ok
1426
+ ? `agent memory ingested: ${result.event?.normalizedKind ?? "event"}\n`
1427
+ : `agent memory ingest failed: ${result.error ?? "unknown error"}\n`;
1428
+ }
1429
+ if ("watch" in result) {
1430
+ return [
1431
+ `agent memory ${result.watch ? "watch" : "queue ingest"} ${result.ok ? "ready" : "failed"} for ${result.source}`,
1432
+ ...(result.file ? [`file: ${result.file}`] : []),
1433
+ `events ingested: ${result.eventsIngested}`,
1434
+ ...(result.errors.length > 0 ? ["errors:", ...result.errors.map((error) => `- ${error}`)] : []),
1435
+ ].join("\n") + "\n";
1436
+ }
1437
+ if ("filesPlanned" in result) {
1438
+ return [
1439
+ `Forge Agent Memory Bridge ${result.ok ? "installed" : "failed"} for ${result.target}.`,
1440
+ "files written:",
1441
+ ...(result.filesWritten.length > 0 ? result.filesWritten.map((file) => `- ${file}`) : ["- none"]),
1442
+ "privacy:",
1443
+ "- raw prompts: off",
1444
+ "- raw completions: off",
1445
+ "- raw tool args: off",
1446
+ "- transcript import: off",
1447
+ ].join("\n") + "\n";
1448
+ }
1449
+ if ("agentMemory" in result) {
1450
+ return formatAgentMemoryContextHuman(result);
1451
+ }
1452
+ if (!result.ok) {
1453
+ const nextActions = "nextActions" in result && Array.isArray(result.nextActions) ? result.nextActions : [];
1454
+ return [
1455
+ "Forge Agent Memory unavailable",
1456
+ "",
1457
+ result.error ?? "agent memory command failed",
1458
+ ...(nextActions.length > 0 ? ["", "Next:", ...nextActions.map((action) => ` ${action}`)] : []),
1459
+ ].join("\n") + "\n";
1460
+ }
1461
+ const events = formatAgentMemoryEventsHuman("events" in result ? result.events : []);
1462
+ return "freshness" in result && result.freshness
1463
+ ? `${events.trimEnd()}\n${formatFreshnessHuman(result.freshness)}\n`
1464
+ : events;
1465
+ }
1466
+
1467
+ function formatFreshnessHuman(freshness: AgentMemoryFreshness): string {
1468
+ return `freshness: ${freshness.status} (${freshness.pendingBytes} queued bytes${freshness.inspectedEventsTruncated ? ", recent queue sample only" : ""})`;
1469
+ }
1470
+
1471
+ function formatAgentMemoryContextHuman(result: AgentMemoryContextPack): string {
1472
+ const summary = result.agentMemory.summary;
1473
+ const lines = [
1474
+ `Forge Agent Context (${result.scope}${result.entry ? `: ${result.entry}` : ""})`,
1475
+ "",
1476
+ `target: ${formatAgentContextTarget(result)}`,
1477
+ `events: ${summary.events}`,
1478
+ `sources: ${summary.sources.length > 0 ? summary.sources.join(", ") : "none"}`,
1479
+ `tools: ${summary.tools.length > 0 ? summary.tools.join(", ") : "none"}`,
1480
+ `files: ${summary.files}`,
1481
+ `entries: ${summary.entries}`,
1482
+ `proofs: ${summary.proofs}`,
1483
+ ...(summary.latestEventAt ? [`latest: ${summary.latestEventAt}`] : []),
1484
+ ...(result.freshness ? [formatFreshnessHuman(result.freshness)] : []),
1485
+ ];
1486
+ if (Object.keys(result.currentState).length > 0) {
1487
+ lines.push("", "Current:");
1488
+ for (const [key, value] of Object.entries(result.currentState)) {
1489
+ if (value !== undefined) {
1490
+ lines.push(` ${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
1491
+ }
1492
+ }
1493
+ }
1494
+ const recent = result.agentMemory.events.slice(-5);
1495
+ if (recent.length > 0) {
1496
+ lines.push("", "Recent:");
1497
+ for (const event of recent) {
1498
+ const parts = [
1499
+ event.capturedAt,
1500
+ event.source,
1501
+ event.kind,
1502
+ event.tool,
1503
+ event.status,
1504
+ event.summary,
1505
+ ].filter(Boolean);
1506
+ lines.push(` - ${parts.join(" | ")}`);
1507
+ }
1508
+ }
1509
+ if (result.agentMemory.openQuestions.length > 0) {
1510
+ lines.push("", "Open questions:");
1511
+ for (const question of result.agentMemory.openQuestions.slice(0, 5)) {
1512
+ lines.push(` - ${question}`);
1513
+ }
1514
+ }
1515
+ if (result.recommendedCommands.length > 0) {
1516
+ lines.push("", "Next:");
1517
+ for (const command of result.recommendedCommands.slice(0, 6)) {
1518
+ lines.push(` ${command}`);
1519
+ }
1520
+ }
1521
+ return `${lines.join("\n")}\n`;
1522
+ }
1523
+
1524
+ function formatAgentContextTarget(result: AgentMemoryContextPack): string {
1525
+ const target = result.scopeTarget;
1526
+ const parts: string[] = [target.kind];
1527
+ if (target.value) {
1528
+ parts.push(target.value);
1529
+ }
1530
+ if (target.semanticTarget && target.semanticTarget !== target.value) {
1531
+ parts.push(`semantic=${target.semanticTarget}`);
1532
+ }
1533
+ if (target.currentSessionId) {
1534
+ parts.push(`session=${target.currentSessionId}`);
1535
+ }
1536
+ return parts.join(" ");
1537
+ }
1538
+
1539
+ function formatAgentMemoryEventsHuman(events: AgentMemoryEventRecord[]): string {
1540
+ const sources = uniqueStrings(events.map((event) => event.sourceName));
1541
+ const tools = uniqueStrings(events.flatMap((event) => {
1542
+ const tool = agentMemoryEventBindings(event).toolName;
1543
+ return tool ? [tool] : [];
1544
+ }));
1545
+ const latest = events.at(-1)?.capturedAt;
1546
+ const lines = [
1547
+ "Forge Agent Memory",
1548
+ "",
1549
+ `events: ${events.length}`,
1550
+ `sources: ${sources.length > 0 ? sources.join(", ") : "none"}`,
1551
+ `tools: ${tools.length > 0 ? tools.join(", ") : "none"}`,
1552
+ ...(latest ? [`latest: ${latest}`] : []),
1553
+ ];
1554
+ if (events.length === 0) {
1555
+ lines.push("", "no agent memory events recorded");
1556
+ return `${lines.join("\n")}\n`;
1557
+ }
1558
+ lines.push("", "Recent:");
1559
+ for (const event of events.slice(-12)) {
1560
+ const bindings = agentMemoryEventBindings(event);
1561
+ const parts = [
1562
+ event.capturedAt,
1563
+ event.sourceName,
1564
+ event.normalizedKind,
1565
+ bindings.toolName,
1566
+ bindings.status,
1567
+ event.summary,
1568
+ ].filter(Boolean);
1569
+ lines.push(` - ${parts.join(" | ")}`);
1570
+ const details = [
1571
+ bindings.command ? `command: ${bindings.command}` : undefined,
1572
+ bindings.files.length > 0 ? `files: ${bindings.files.slice(0, 4).join(", ")}` : undefined,
1573
+ bindings.entries.length > 0 ? `entries: ${bindings.entries.slice(0, 4).join(", ")}` : undefined,
1574
+ bindings.proofs.length > 0 ? `proofs: ${bindings.proofs.slice(0, 4).join(", ")}` : undefined,
1575
+ ].filter(Boolean);
1576
+ if (details.length > 0) {
1577
+ lines.push(` ${details.join(" | ")}`);
1578
+ }
1579
+ }
1580
+ return `${lines.join("\n")}\n`;
1581
+ }
1582
+
1583
+ function agentMemoryEventBindings(event: AgentMemoryEventRecord): {
1584
+ toolName?: string;
1585
+ command?: string;
1586
+ status?: string;
1587
+ files: string[];
1588
+ entries: string[];
1589
+ proofs: string[];
1590
+ } {
1591
+ const raw = event.data.bindings;
1592
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1593
+ return { files: [], entries: [], proofs: [] };
1594
+ }
1595
+ const record = raw as Record<string, unknown>;
1596
+ return {
1597
+ toolName: typeof record.toolName === "string" ? record.toolName : undefined,
1598
+ command: typeof record.command === "string" ? record.command : undefined,
1599
+ status: typeof record.status === "string" ? record.status : undefined,
1600
+ files: arrayOfStrings(record.files),
1601
+ entries: arrayOfStrings(record.entries),
1602
+ proofs: arrayOfStrings(record.proofs),
1603
+ };
1604
+ }
1605
+
1606
+ function arrayOfStrings(value: unknown): string[] {
1607
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
1608
+ }
1609
+
1610
+ function uniqueStrings(values: string[]): string[] {
1611
+ return [...new Set(values)].sort();
1612
+ }
1613
+
1614
+ function normalizeInstallTarget(target: string): AgentMemorySourceName | string {
1615
+ if (target === "claude") {
1616
+ return "claude-code";
1617
+ }
1618
+ return target;
1619
+ }
1620
+
1621
+ function normalizeRawInput(input: unknown): Record<string, unknown> | null {
1622
+ if (input && typeof input === "object" && !Array.isArray(input)) {
1623
+ return input as Record<string, unknown>;
1624
+ }
1625
+ if (typeof input === "string" && input.trim()) {
1626
+ try {
1627
+ const parsed = JSON.parse(input) as unknown;
1628
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
1629
+ } catch {
1630
+ return null;
1631
+ }
1632
+ }
1633
+ return null;
1634
+ }
1635
+
1636
+ function objectField(value: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
1637
+ const child = value[key];
1638
+ return child && typeof child === "object" && !Array.isArray(child) ? child as Record<string, unknown> : undefined;
1639
+ }
1640
+
1641
+ export async function readStdinJson(options?: { timeoutMs?: number }): Promise<unknown> {
1642
+ if (process.stdin.isTTY) {
1643
+ return undefined;
1644
+ }
1645
+ const timeoutMs = options?.timeoutMs ?? 2000;
1646
+ const chunks: Buffer[] = [];
1647
+ let settled = false;
1648
+
1649
+ return await new Promise<unknown>((resolve) => {
1650
+ const finish = () => {
1651
+ if (settled) {
1652
+ return;
1653
+ }
1654
+ settled = true;
1655
+ clearTimeout(timer);
1656
+ process.stdin.removeListener("data", onData);
1657
+ process.stdin.removeListener("end", finish);
1658
+ process.stdin.removeListener("close", finish);
1659
+ process.stdin.removeListener("error", finish);
1660
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
1661
+ resolve(raw ? raw : undefined);
1662
+ };
1663
+ const onData = (chunk: Buffer | string) => {
1664
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1665
+ };
1666
+ const timer = setTimeout(() => {
1667
+ process.stdin.destroy();
1668
+ finish();
1669
+ }, timeoutMs);
1670
+ process.stdin.on("data", onData);
1671
+ process.stdin.on("end", finish);
1672
+ process.stdin.on("close", finish);
1673
+ process.stdin.on("error", finish);
1674
+ process.stdin.resume();
1675
+ });
1676
+ }
1677
+
1678
+ function parseQueuedHookLine(raw: Record<string, unknown>): {
1679
+ source: string;
1680
+ integration: "mcp" | "native-hook";
1681
+ eventName?: string;
1682
+ workspaceRoot?: string;
1683
+ payload: Record<string, unknown>;
1684
+ } | null {
1685
+ if (raw.forgeHookQueueV1 !== true) {
1686
+ return null;
1687
+ }
1688
+ const payload = objectField(raw, "payload") ?? objectField(raw, "raw");
1689
+ if (!payload) {
1690
+ return null;
1691
+ }
1692
+ return {
1693
+ source: typeof raw.source === "string" ? raw.source : "codex",
1694
+ integration: raw.integration === "mcp" ? "mcp" : "native-hook",
1695
+ eventName: typeof raw.eventName === "string" ? raw.eventName : undefined,
1696
+ workspaceRoot: typeof raw.workspaceRoot === "string" ? raw.workspaceRoot : undefined,
1697
+ payload,
1698
+ };
1699
+ }
1700
+
1701
+ function maybeMergeJson(path: string, generated: string): string {
1702
+ if (!existsSync(path) || !path.endsWith(".json")) {
1703
+ return generated;
1704
+ }
1705
+ try {
1706
+ const current = JSON.parse(readFileSync(path, "utf8")) as unknown;
1707
+ const next = JSON.parse(generated) as unknown;
1708
+ if (!current || typeof current !== "object" || Array.isArray(current) || !next || typeof next !== "object" || Array.isArray(next)) {
1709
+ return generated;
1710
+ }
1711
+ return `${JSON.stringify(deepMerge(current as Record<string, unknown>, next as Record<string, unknown>), null, 2)}\n`;
1712
+ } catch {
1713
+ return generated;
1714
+ }
1715
+ }
1716
+
1717
+ function deepMerge(left: Record<string, unknown>, right: Record<string, unknown>): Record<string, unknown> {
1718
+ const output: Record<string, unknown> = { ...left };
1719
+ for (const [key, value] of Object.entries(right)) {
1720
+ const existing = output[key];
1721
+ output[key] =
1722
+ existing && typeof existing === "object" && !Array.isArray(existing) &&
1723
+ value && typeof value === "object" && !Array.isArray(value)
1724
+ ? deepMerge(existing as Record<string, unknown>, value as Record<string, unknown>)
1725
+ : value;
1726
+ }
1727
+ return output;
1728
+ }