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,401 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { realpath } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import { stableStringify } from "./canonical.ts";
5
+ import {
6
+ MANAGED_RUN_ACTIONS, managedDigest, managedFail, managedId, managedObject, managedText,
7
+ validateManagedSpec, type ManagedRunState, type ManagedRunSpec, type ManagedStep, type ManagedEvent,
8
+ } from "./managed-run-contract.ts";
9
+ import { ManagedRunStore } from "./managed-run-store.ts";
10
+ import { runCodexWorker, type CodexWorkerInput, type CodexWorkerOutput, codexWorkerEnvironment } from "./codex-sdk-worker.ts";
11
+ import { prepareManagedEnvironment } from "./managed-environment.ts";
12
+ import { prepareFabricRepositoryContext } from "./repository-context.ts";
13
+ import {
14
+ captureManagedBase, prepareManagedWorkspace, captureManagedArtifact, publishManagedArtifacts,
15
+ previewManagedArtifacts, confirmManagedPublication, confirmManagedBaseline, type ManagedArtifact,
16
+ } from "./managed-workspace.ts";
17
+ import { createWorkflow, nextWorkflow, claimWorkflow, completeWorkflow, recoverWorkflow, reconcileWorkflow, replanWorkflow, type WorkflowNode } from "./workflow-engine.ts";
18
+
19
+ interface ServiceOptions { worker?: (input: CodexWorkerInput) => Promise<CodexWorkerOutput>; closeTimeoutMs?: number; publisher?: typeof publishManagedArtifacts }
20
+ interface ActiveWorker { controller: AbortController; promise: Promise<void> }
21
+ const COMMON = ["runId", "requestId", "expectedVersion"];
22
+ const terminal = (state: ManagedRunState) => ["completed", "canceled", "failed"].includes(state.status);
23
+ const processOwners = new Map<string, symbol>();
24
+ const ownerKey = (root: string) => process.platform === "win32" ? root.toLowerCase() : root;
25
+ const errorText = (error: unknown) => (error instanceof Error ? error.message : "Managed execution failed").slice(0, 4096);
26
+ function event(state: ManagedRunState, type: string, summary: string, details: Partial<Pick<ManagedEvent, "nodeId" | "attemptId" | "threadId">> = {}): void {
27
+ state.cursor++;
28
+ state.events.push({ cursor: state.cursor, at: new Date().toISOString(), type, summary: summary.slice(0, 4096), ...details });
29
+ if (state.events.length > 500) state.events.splice(0, state.events.length - 500);
30
+ }
31
+ function boundNodes(spec: ManagedRunSpec): WorkflowNode[] {
32
+ return spec.workflow.nodes.map(node => ({ ...node, inputDigest: managedDigest(stableStringify({ declared: node.inputDigest, executor: spec.executors.find(item => item.nodeId === node.nodeId), environment: spec.environment ?? { mode: "auto", ignoreScripts: true } })) }));
33
+ }
34
+ function applicableSteps(state: ManagedRunState): ManagedStep[] {
35
+ return state.workflow.nodes.flatMap(node => {
36
+ const generation = state.workflow.generations.find(item => item.nodeId === node.nodeId)!.generation;
37
+ const run = state.workflow.runs.find(item => item.nodeId === node.nodeId && item.generation === generation && item.status === "succeeded");
38
+ const step = run && state.steps.find(item => item.attemptId === run.attemptId);
39
+ return step ? [step] : [];
40
+ });
41
+ }
42
+ /** Ordered ancestry makes independent artifacts reusable while dependent changes compose. */
43
+ function dependencyArtifacts(state: ManagedRunState, nodeId?: string): ManagedArtifact[] {
44
+ const applicable = new Map(applicableSteps(state).map(step => [step.nodeId, step]));
45
+ const visited = new Set<string>(), ordered: ManagedArtifact[] = [];
46
+ const visit = (id: string) => {
47
+ if (visited.has(id)) return; visited.add(id);
48
+ const node = state.workflow.nodes.find(item => item.nodeId === id)!;
49
+ for (const parent of node.dependsOn) visit(parent);
50
+ const artifact = applicable.get(id)?.artifact; if (artifact?.files.length) ordered.push(artifact);
51
+ };
52
+ if (nodeId) for (const id of state.workflow.nodes.find(node => node.nodeId === nodeId)!.dependsOn) visit(id);
53
+ else for (const node of state.workflow.nodes) visit(node.nodeId);
54
+ return ordered;
55
+ }
56
+ function publicState(state: ManagedRunState) {
57
+ const next = nextWorkflow(state.workflow);
58
+ return {
59
+ runId: state.runId, version: state.version, status: state.status, goal: state.spec.goal, scope: state.spec.scope,
60
+ workflow: { workflowId: state.workflow.workflowId, revision: state.workflow.revision, complete: next.complete, blockedReasons: next.blockedReasons, nextNodes: next.packets.map(packet => packet.nodeId) },
61
+ steps: state.steps.map(({ artifact, ...step }) => ({ ...step, ...(artifact ? { artifact: { digest: artifact.digest, changedFiles: artifact.files.map(file => file.path) } } : {}) })),
62
+ cursor: state.cursor, instructions: state.instructions, ...(state.published ? { published: state.published } : {}),
63
+ ...(state.publicationIntent ? { publicationIntent: state.publicationIntent } : {}), ...(state.error ? { error: state.error } : {}),
64
+ provenance: "executor_observed", reportProvenance: "agent_reported",
65
+ };
66
+ }
67
+
68
+ /** The original Codex App chat drives this owner through MCP/CLI; workers use SDK threads. */
69
+ export class ManagedRunService {
70
+ private readonly loops = new Map<string, Promise<void>>();
71
+ private readonly workers = new Map<string, Map<string, ActiveWorker>>();
72
+ private readonly waiters = new Map<string, Set<() => void>>();
73
+ private closed = false;
74
+ private publicationsInFlight = 0;
75
+ private constructor(readonly root: string, readonly store: ManagedRunStore, private readonly options: ServiceOptions, private readonly ownerToken: symbol) {}
76
+ private releaseOwner(): void { const key = ownerKey(this.root); if (processOwners.get(key) === this.ownerToken) processOwners.delete(key); }
77
+ static async open(root: string, options: ServiceOptions = {}): Promise<ManagedRunService> {
78
+ root = await realpath(root);
79
+ const key = ownerKey(root);
80
+ if (processOwners.has(key)) managedFail("AF_RUN_OWNER_ACTIVE", "This process already owns managed execution for the repository");
81
+ const token = Symbol("managed-owner"); processOwners.set(key, token);
82
+ try {
83
+ const service = new ManagedRunService(root, await ManagedRunStore.open(root), options, token);
84
+ for (const runId of await service.store.list()) {
85
+ const old = await service.store.read(runId); if (!old || terminal(old)) continue;
86
+ if (old.repositoryRoot !== root) managedFail("AF_RUN_STORE", "Run belongs to another repository");
87
+ if (old.ownerPid !== process.pid) {
88
+ try { process.kill(old.ownerPid, 0); managedFail("AF_RUN_OWNER_ACTIVE", "Another live owner holds this run"); }
89
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; }
90
+ if (old.status === "publishing" && old.base && await confirmManagedPublication(old.base, dependencyArtifacts(old))) {
91
+ await service.update(runId, state => { state.ownerPid = process.pid; state.status = "completed"; state.published = state.publicationIntent; event(state, "publication.reconciled", "Observed complete local publication after owner restart"); });
92
+ } else await service.update(runId, state => {
93
+ state.ownerPid = process.pid; state.workflow = recoverWorkflow(state.workflow, "Owner interrupted; inspect effects before retry");
94
+ for (const step of state.steps.filter(item => item.status === "running")) step.status = "uncertain";
95
+ state.status = "blocked"; state.error = state.publicationIntent ? "Local publication was interrupted; inspect scoped files and confirm the intended result" : "Owner restarted; reconcile interrupted attempts before resuming";
96
+ event(state, "run.recovered", state.error);
97
+ });
98
+ }
99
+ }
100
+ return service;
101
+ } catch (error) { if (processOwners.get(key) === token) processOwners.delete(key); throw error; }
102
+ }
103
+ private wake(runId: string): void { for (const callback of this.waiters.get(runId) ?? []) callback(); }
104
+ private async require(runId: string): Promise<ManagedRunState> {
105
+ const state = await this.store.read(runId); if (!state) managedFail("AF_RUN_NOT_FOUND", "Managed run does not exist"); return state;
106
+ }
107
+ private async update(runId: string, apply: (state: ManagedRunState) => void): Promise<ManagedRunState> {
108
+ const result = await this.store.transact(runId, {}, current => { if (!current) managedFail("AF_RUN_NOT_FOUND", "Managed run does not exist"); if (!terminal(current)) apply(current); return current; });
109
+ this.wake(runId); return result.state;
110
+ }
111
+ async execute(action: string, body: Record<string, unknown>): Promise<unknown> {
112
+ if (!(MANAGED_RUN_ACTIONS as readonly string[]).includes(action)) managedFail("AF_RUN_ACTION", "Unknown managed operation");
113
+ if (this.closed) managedFail("AF_RUN_OWNER_CLOSED", "Managed owner is closing");
114
+ if (action === "run-start") {
115
+ const spec = validateManagedSpec(body); const runId = `run-${managedDigest(spec.requestId).slice(7, 39)}`;
116
+ const created = await this.store.transact(runId, { requestId: spec.requestId, fingerprint: managedDigest(stableStringify(body)) }, old => {
117
+ if (old) managedFail("AF_RUN_REQUEST_CONFLICT", "Run request already exists");
118
+ const now = new Date().toISOString(); const state: ManagedRunState = { schemaVersion: 1, runId, repositoryRoot: this.root, ownerPid: process.pid, version: 1, spec,
119
+ workflow: createWorkflow({ ...spec.workflow, nodes: boundNodes(spec) }), status: "preparing", steps: [], events: [], cursor: 0, instructions: [], createdAt: now, updatedAt: now };
120
+ event(state, "run.started", "Preparing immutable source and isolated workers"); return state;
121
+ });
122
+ this.kick(runId); return created.ack;
123
+ }
124
+ const runId = managedId(body.runId, "runId");
125
+ if (action === "run-status") { managedObject(body, ["runId"]); return publicState(await this.require(runId)); }
126
+ if (action === "run-wait") {
127
+ managedObject(body, ["runId", "cursor", "waitMs"]);
128
+ const cursor = body.cursor ?? 0, waitMs = body.waitMs ?? 30000;
129
+ if (!Number.isSafeInteger(cursor) || (cursor as number) < 0 || !Number.isSafeInteger(waitMs) || (waitMs as number) < 0 || (waitMs as number) > 30000) managedFail("AF_RUN_INPUT", "Invalid event cursor or wait deadline");
130
+ let state = await this.require(runId);
131
+ if ((cursor as number) > state.cursor) managedFail("AF_RUN_CURSOR", "Cursor is ahead of the run");
132
+ if (state.cursor <= (cursor as number) && !terminal(state) && (waitMs as number) > 0) {
133
+ await new Promise<void>(resolve => {
134
+ const callbacks = this.waiters.get(runId) ?? new Set<() => void>(); this.waiters.set(runId, callbacks);
135
+ const finish = () => { clearTimeout(timer); callbacks.delete(finish); resolve(); };
136
+ const timer = setTimeout(finish, waitMs as number); callbacks.add(finish);
137
+ // Register before a second read so completion cannot be missed between read and wait.
138
+ void this.require(runId).then(latest => { if (latest.cursor > (cursor as number) || terminal(latest)) finish(); }, finish);
139
+ });
140
+ state = await this.require(runId);
141
+ }
142
+ return { run: publicState(state), cursor: state.cursor, events: state.events.filter(item => item.cursor > (cursor as number)), cursorExpired: state.events.length > 0 && (cursor as number) < state.events[0].cursor - 1 };
143
+ }
144
+ managedId(body.requestId, "requestId");
145
+ if (!Number.isSafeInteger(body.expectedVersion) || (body.expectedVersion as number) < 1) managedFail("AF_RUN_VERSION", "expectedVersion required");
146
+ const fields = action === "run-steer" ? ["instruction"] : action === "run-resume" ? ["expectedRevision", "nodes", "executors", "environment", "reason", "evidenceRefs"] : action === "run-reconcile" ? ["attemptId", "resolution", "reason", "publication"] : [];
147
+ managedObject(body, [...COMMON, ...fields]);
148
+ if (action === "run-reconcile" && body.publication === "confirm") {
149
+ const current = await this.require(runId);
150
+ if (!current.base || !current.publicationIntent || !(await confirmManagedPublication(current.base, dependencyArtifacts(current)))) managedFail("AF_RUN_PUBLICATION", "Actual files do not match the intended publication");
151
+ }
152
+ if (action === "run-reconcile" && body.publication === "retry") {
153
+ const current = await this.require(runId);
154
+ if (current.publicationIntent && (!current.base || !(await confirmManagedBaseline(current.base)))) managedFail("AF_RUN_PUBLICATION", "Publication retry requires an observed complete immutable baseline; preserve partial writes for inspection");
155
+ }
156
+ const result = await this.store.transact(runId, { requestId: body.requestId as string, fingerprint: managedDigest(stableStringify(body)), expectedVersion: body.expectedVersion as number }, state => {
157
+ if (!state) managedFail("AF_RUN_NOT_FOUND", "Managed run does not exist");
158
+ if (terminal(state)) managedFail("AF_RUN_TERMINAL", "Managed run already ended");
159
+ if (state.status === "publishing") managedFail("AF_RUN_PUBLICATION", "Publication in progress; wait for its observed result");
160
+ if (action === "run-pause") { state.status = "paused"; event(state, "run.paused", "New dispatches paused; existing workers may finish"); }
161
+ if (action === "run-steer") {
162
+ if (state.instructions.length >= 20) managedFail("AF_RUN_LIMIT", "Steering instruction limit reached");
163
+ state.instructions.push(managedText(body.instruction, "instruction", 4096)); state.status = "paused";
164
+ event(state, "run.steered", "Instruction queued for subsequent workers; resume or replan explicitly");
165
+ }
166
+ if (action === "run-cancel") { state.status = "canceling"; event(state, "run.cancel_requested", "Cancel requested; in-flight outcomes must be observed"); }
167
+ if (action === "run-reconcile") {
168
+ if (body.publication === "confirm") { state.published = state.publicationIntent; state.status = "completed"; delete state.error; event(state, "publication.reconciled", "Actual scoped files match the intended publication"); }
169
+ else if (body.publication === "retry") {
170
+ if (!state.base || !state.publicationIntent) managedFail("AF_RUN_PUBLICATION", "No interrupted publication to retry");
171
+ delete state.publicationIntent; delete state.error; state.status = "paused";
172
+ event(state, "publication.retry_authorized", "Observed original baseline; publication intent cleared, explicit resume required");
173
+ }
174
+ else {
175
+ if (body.resolution !== "failed") managedFail("AF_RUN_RECONCILE", "Only explicit abandonment of an uncertain attempt is accepted");
176
+ const attemptId = managedId(body.attemptId, "attemptId"), reason = managedText(body.reason, "reason", 4096);
177
+ state.workflow = reconcileWorkflow(state.workflow, { attemptId, result: { status: "failed", reason } });
178
+ const step = state.steps.find(item => item.attemptId === attemptId)!; step.status = "failed"; step.summary = reason;
179
+ state.status = "paused"; event(state, "attempt.reconciled", reason, { nodeId: step.nodeId, attemptId });
180
+ }
181
+ }
182
+ if (action === "run-resume") {
183
+ if (state.publicationIntent) managedFail("AF_RUN_PUBLICATION", "Interrupted publication requires inspection and confirmation");
184
+ if (state.workflow.runs.some(run => run.status === "uncertain")) managedFail("AF_RUN_UNCERTAIN", "Reconcile uncertain attempts before resuming");
185
+ if (body.nodes !== undefined || body.executors !== undefined || body.environment !== undefined) {
186
+ const proposal = validateManagedSpec({ ...state.spec, workflow: { ...state.spec.workflow, nodes: body.nodes ?? state.spec.workflow.nodes }, executors: body.executors ?? state.spec.executors, ...(body.environment !== undefined ? { environment: body.environment } : {}) });
187
+ state.workflow = replanWorkflow(state.workflow, { nodes: boundNodes(proposal), expectedRevision: body.expectedRevision as number, reason: managedText(body.reason, "reason", 4096), evidenceRefs: body.evidenceRefs as string[] });
188
+ state.spec = proposal; event(state, "workflow.replanned", body.reason as string);
189
+ }
190
+ state.status = state.base ? "running" : "preparing"; delete state.error; event(state, "run.resumed", "Authorized dispatch resumed");
191
+ }
192
+ return state;
193
+ });
194
+ this.wake(runId);
195
+ if (!result.replayed && action === "run-cancel") for (const worker of this.workers.get(runId)?.values() ?? []) worker.controller.abort();
196
+ if (action === "run-resume" || action === "run-cancel") this.kick(runId);
197
+ return result.ack;
198
+ }
199
+ private kick(runId: string): void {
200
+ if (this.closed || this.loops.has(runId)) return;
201
+ const promise = this.loop(runId).catch(async error => {
202
+ if ((error as { code?: string }).code === "AF_RUN_PAUSED") return;
203
+ for (const worker of this.workers.get(runId)?.values() ?? []) worker.controller.abort();
204
+ if (this.closed) return;
205
+ await this.update(runId, state => { state.status = "blocked"; state.error = errorText(error); event(state, "run.blocked", state.error); }).catch(() => {});
206
+ }).finally(async () => {
207
+ this.loops.delete(runId);
208
+ // Resume can arrive while the previous loop is still unwinding.
209
+ if (!this.closed) {
210
+ const latest = await this.require(runId).catch(() => undefined);
211
+ if (latest && ["preparing", "running", "canceling"].includes(latest.status)) this.kick(runId);
212
+ }
213
+ });
214
+ this.loops.set(runId, promise);
215
+ }
216
+ private async loop(runId: string): Promise<void> {
217
+ let state = await this.require(runId);
218
+ if (state.status === "preparing") {
219
+ const base = state.base ?? await captureManagedBase(this.root, runId, state.spec.scope);
220
+ if (this.closed) return;
221
+ state = await this.update(runId, current => { current.base = base; if (current.status === "preparing") current.status = "running"; event(current, "source.prepared", "Immutable baseline captured including scoped local changes"); });
222
+ }
223
+ while (!this.closed) {
224
+ state = await this.require(runId); const active = this.workers.get(runId) ?? new Map<string, ActiveWorker>(); this.workers.set(runId, active);
225
+ if (state.status === "canceling") {
226
+ if (active.size) { await Promise.race([...active.values()].map(item => item.promise)); continue; }
227
+ await this.update(runId, current => {
228
+ const uncertain = current.workflow.runs.some(run => run.status === "uncertain");
229
+ current.status = uncertain ? "blocked" : "canceled";
230
+ if (uncertain) current.error = "Cancellation did not confirm all outcomes; reconcile uncertain workers";
231
+ event(current, uncertain ? "run.blocked" : "run.canceled", uncertain ? current.error! : "All managed attempts settled without publication");
232
+ }); return;
233
+ }
234
+ if (state.status !== "running") return;
235
+ const schedule = nextWorkflow(state.workflow);
236
+ if (schedule.complete && !active.size) {
237
+ const artifacts = dependencyArtifacts(state);
238
+ if (state.spec.publish !== false && artifacts.length) {
239
+ const intent = await previewManagedArtifacts(state.base!, artifacts);
240
+ state = await this.update(runId, current => { if (current.status !== "running") managedFail("AF_RUN_PAUSED", "Run was paused before publication"); current.publicationIntent = intent; current.status = "publishing"; event(current, "publication.started", "Reviewed and verified artifact publication recorded before local writes"); });
241
+ if (this.closed) return;
242
+ this.publicationsInFlight++;
243
+ let published: Awaited<ReturnType<typeof publishManagedArtifacts>>;
244
+ try { published = await (this.options.publisher ?? publishManagedArtifacts)(state.base!, artifacts); }
245
+ finally {
246
+ this.publicationsInFlight--;
247
+ if (this.closed && !this.publicationsInFlight) this.releaseOwner();
248
+ }
249
+ if (this.closed) return;
250
+ if (published.digest !== intent.digest) managedFail("AF_RUN_PUBLICATION", "Publication digest differs from reviewed artifacts");
251
+ await this.update(runId, current => { if (this.closed || current.status !== "publishing" || current.publicationIntent?.digest !== intent.digest) return; current.published = published; current.status = "completed"; event(current, "run.completed", "All obligations passed and scoped changes were published locally"); });
252
+ } else await this.update(runId, current => { if (current.status === "running") { current.status = "completed"; event(current, "run.completed", "All required workflow obligations completed"); } });
253
+ return;
254
+ }
255
+ for (const packet of schedule.packets) {
256
+ const current = await this.require(runId); if (this.closed || current.status !== "running") break;
257
+ if (current.workflow.revision !== packet.revision) break;
258
+ const attemptId = `attempt-${randomUUID()}`, controller = new AbortController();
259
+ await this.update(runId, record => {
260
+ if (this.closed || record.status !== "running") managedFail("AF_RUN_PAUSED", "Run stopped before dispatch");
261
+ record.workflow = claimWorkflow(record.workflow, { nodeId: packet.nodeId, attemptId, executorId: "managed-owner", expectedRevision: packet.revision });
262
+ record.steps.push({ nodeId: packet.nodeId, attemptId, status: "running" }); event(record, "attempt.claimed", "Attempt persisted before worker dispatch", { nodeId: packet.nodeId, attemptId });
263
+ });
264
+ if (this.closed) controller.abort();
265
+ const promise = this.executeStep(runId, packet.nodeId, attemptId, controller).finally(() => { active.delete(attemptId); this.wake(runId); if (!this.loops.has(runId)) this.kick(runId); });
266
+ active.set(attemptId, { controller, promise });
267
+ }
268
+ if (active.size) { await Promise.race([...active.values()].map(item => item.promise)); continue; }
269
+ // A worker can commit and leave `active` after the state read above.
270
+ // Decide blockage from the transaction's current schedule, never that stale read.
271
+ const latest = await this.require(runId);
272
+ if (latest.version !== state.version || active.size) continue;
273
+ await this.update(runId, current => {
274
+ if (current.status !== "running" || active.size) return;
275
+ const currentSchedule = nextWorkflow(current.workflow);
276
+ if (currentSchedule.complete || currentSchedule.packets.length) return;
277
+ current.status = "blocked"; current.error = currentSchedule.blockedReasons.join("; ") || "No executable step remains"; event(current, "run.blocked", current.error);
278
+ }); return;
279
+ }
280
+ }
281
+ private async executeStep(runId: string, nodeId: string, attemptId: string, controller: AbortController): Promise<void> {
282
+ const state = await this.require(runId), executor = state.spec.executors.find(item => item.nodeId === nodeId)!;
283
+ const deadline = setTimeout(() => controller.abort(), executor.timeoutMs ?? 600000);
284
+ try {
285
+ const prepared = await prepareManagedWorkspace(state.base!, attemptId, dependencyArtifacts(state, nodeId));
286
+ if (controller.signal.aborted) managedFail("AF_RUN_ABORTED", "Worker canceled during preparation");
287
+ await this.update(runId, current => {
288
+ const step = current.steps.find(item => item.attemptId === attemptId)!;
289
+ if (this.closed || controller.signal.aborted || step.status !== "running") return;
290
+ Object.assign(step, prepared); event(current, "attempt.prepared", "Worker input snapshot captured", { nodeId, attemptId });
291
+ });
292
+ const environment = await prepareManagedEnvironment(prepared.directory, { ...state.spec.environment, signal: controller.signal });
293
+ await captureManagedArtifact(state.base!, prepared.directory, [], prepared.inputDigest, environment);
294
+ await this.update(runId, current => {
295
+ const step = current.steps.find(item => item.attemptId === attemptId)!;
296
+ if (this.closed || controller.signal.aborted || step.status !== "running") return;
297
+ step.environment = environment; event(current, "environment.prepared", `Prepared ${environment.manager} dependencies${environment.cacheHit ? " from verified cache" : ""}`, { nodeId, attemptId });
298
+ });
299
+ if (this.closed || controller.signal.aborted) managedFail("AF_RUN_ABORTED", "Worker canceled during environment preparation");
300
+ let report: ManagedStep["report"], usage: ManagedStep["usage"], threadId: string | undefined, outputDigest: string;
301
+ if (executor.type === "codex") {
302
+ const repository = await prepareFabricRepositoryContext(state.repositoryRoot, prepared.directory, executor.prompt ?? state.spec.goal, executor.writeScope ?? state.spec.scope);
303
+ if (this.closed || controller.signal.aborted) managedFail("AF_RUN_ABORTED", "Worker canceled during repository analysis");
304
+ if (repository) await this.update(runId, current => {
305
+ const step = current.steps.find(item => item.attemptId === attemptId)!;
306
+ if (this.closed || controller.signal.aborted || step.status !== "running") return;
307
+ step.repositoryContext = repository.metadata;
308
+ event(current, "repository.context", `Repository maps ${repository.metadata.status} for prepared clone`, { nodeId, attemptId });
309
+ });
310
+ const previous = [...state.steps].reverse().find(step => step.nodeId === nodeId && step.threadId);
311
+ const output = await (this.options.worker ?? runCodexWorker)({ cwd: prepared.directory, role: executor.role!, model: executor.model, threadId: previous?.threadId, signal: controller.signal,
312
+ prompt: `${executor.prompt}\n\nTask goal: ${state.spec.goal}\nYour current checkout: ${prepared.directory}\nActual input snapshot: ${prepared.inputDigest}\nWrite scope: ${JSON.stringify(executor.writeScope ?? [])}. Do not change files outside it. Do not commit, publish, deploy, install packages or send external messages. Dependencies already applied to this checkout. Reports from dependencies: ${JSON.stringify(applicableSteps(state).filter(step => state.workflow.nodes.find(node => node.nodeId === nodeId)!.dependsOn.includes(step.nodeId)).map(step => ({ nodeId: step.nodeId, summary: step.summary, report: step.report })))}\nCoordinator instructions for this attempt: ${state.instructions.join("\n")}${repository ? `\n\n${repository.prompt}` : ""}`,
313
+ onEvent: async notification => {
314
+ if (this.closed || controller.signal.aborted) return;
315
+ await this.update(runId, current => {
316
+ const step = current.steps.find(item => item.attemptId === attemptId);
317
+ if (this.closed || controller.signal.aborted || !step || step.status !== "running") return;
318
+ if (notification.threadId) step.threadId = notification.threadId;
319
+ if (notification.usage) step.usage = notification.usage;
320
+ event(current, notification.type, notification.summary ?? notification.type, { nodeId, attemptId, ...(notification.threadId ? { threadId: notification.threadId } : {}) });
321
+ });
322
+ },
323
+ });
324
+ report = output.report; usage = output.usage; threadId = output.threadId; outputDigest = managedDigest(stableStringify(output.report));
325
+ } else {
326
+ const result = await this.command(prepared.directory, executor.argv!, controller.signal);
327
+ report = { summary: `Command exited successfully; observed output digest ${result.outputDigest}` }; outputDigest = result.outputDigest;
328
+ }
329
+ if (controller.signal.aborted) managedFail("AF_RUN_ABORTED", "Interrupted worker result requires reconciliation");
330
+ const artifact = await captureManagedArtifact(state.base!, prepared.directory, executor.writeScope ?? [], prepared.inputDigest, environment);
331
+ if (Buffer.byteLength(stableStringify(artifact)) > 8 * 1024 * 1024) managedFail("AF_RUN_ARTIFACT_LIMIT", "Artifact exceeds the managed record budget");
332
+ await this.update(runId, current => {
333
+ const step = current.steps.find(item => item.attemptId === attemptId)!;
334
+ if (this.closed || controller.signal.aborted || step.status !== "running") return;
335
+ Object.assign(step, { report, ...(usage ? { usage } : {}), ...(threadId ? { threadId } : {}), artifact, summary: report!.summary });
336
+ const node = current.workflow.nodes.find(item => item.nodeId === nodeId)!;
337
+ if (executor.role === "reviewer" && (report!.verdict !== "approved" || report!.findings?.length)) {
338
+ step.status = "failed"; current.workflow = completeWorkflow(current.workflow, { attemptId, result: { status: "failed", reason: "Independent review requested changes" } });
339
+ current.status = "blocked"; current.error = "Independent review requested changes; coordinator must revise the plan"; event(current, "review.changes_requested", current.error, { nodeId, attemptId }); return;
340
+ }
341
+ const references = [`managed-attempt:${attemptId}`, `input:${prepared.inputDigest}`, `artifact:${artifact.digest}`];
342
+ current.workflow = completeWorkflow(current.workflow, { attemptId, result: { status: "succeeded", outputDigest, evidenceRefs: references,
343
+ evidenceKinds: ["executor-observed"], ...(node.kind === "decision" ? { selectedNodeIds: report!.selectedNodeIds ?? [] } : {}) } });
344
+ step.status = "succeeded"; event(current, "attempt.completed", report!.summary, { nodeId, attemptId });
345
+ if (report!.replanProposal) { current.status = "blocked"; current.error = "Worker proposed a plan change; coordinator decision required"; event(current, "workflow.proposal", report!.replanProposal, { nodeId, attemptId }); }
346
+ });
347
+ } catch (error) {
348
+ if (this.closed) return;
349
+ const interrupted = controller.signal.aborted || (error as { code?: string }).code === "AF_CODEX_ABORTED";
350
+ await this.update(runId, current => {
351
+ const step = current.steps.find(item => item.attemptId === attemptId)!;
352
+ if (step.status !== "running") return;
353
+ step.status = interrupted ? "uncertain" : "failed"; step.summary = errorText(error);
354
+ current.workflow = completeWorkflow(current.workflow, { attemptId, result: { status: step.status, reason: step.summary } });
355
+ if (current.status !== "canceling") current.status = "blocked"; current.error = step.summary;
356
+ event(current, interrupted ? "attempt.uncertain" : "attempt.failed", step.summary, { nodeId, attemptId });
357
+ });
358
+ } finally { clearTimeout(deadline); }
359
+ }
360
+ private command(cwd: string, argv: string[], signal: AbortSignal): Promise<{ outputDigest: string }> {
361
+ return new Promise((resolve, reject) => {
362
+ if (signal.aborted) { reject(new Error("Command canceled before dispatch")); return; }
363
+ const child = spawn(argv[0], argv.slice(1), { cwd, env: codexWorkerEnvironment(), shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
364
+ const chunks: Buffer[] = []; let bytes = 0, overflow = false, settled = false, grace: ReturnType<typeof setTimeout> | undefined;
365
+ const finish = (error?: Error) => { if (settled) return; settled = true; if (grace) clearTimeout(grace); signal.removeEventListener("abort", abort); if (error) reject(error); else resolve({ outputDigest: managedDigest(Buffer.concat(chunks).toString("base64")) }); };
366
+ const abort = () => { child.kill(); grace = setTimeout(() => finish(new Error("Command termination is uncertain")), 1000); };
367
+ signal.addEventListener("abort", abort, { once: true });
368
+ const collect = (chunk: Buffer) => { bytes += chunk.length; if (bytes > 1024 * 1024) { overflow = true; child.kill(); } else chunks.push(chunk); };
369
+ child.stdout.on("data", collect); child.stderr.on("data", collect);
370
+ child.on("error", error => finish(error)); child.on("close", code => finish(signal.aborted ? new Error("Command interrupted") : overflow ? new Error("Command output exceeds limit") : code !== 0 ? new Error(`Command failed with exit code ${code}`) : undefined));
371
+ });
372
+ }
373
+ async close(): Promise<void> {
374
+ if (this.closed) return; this.closed = true;
375
+ try {
376
+ for (const active of this.workers.values()) for (const worker of active.values()) worker.controller.abort();
377
+ for (const callbacks of this.waiters.values()) for (const callback of callbacks) callback();
378
+ let grace: ReturnType<typeof setTimeout> | undefined;
379
+ try {
380
+ await Promise.race([
381
+ Promise.allSettled([...this.loops.values(), ...[...this.workers.values()].flatMap(active => [...active.values()].map(worker => worker.promise))]),
382
+ new Promise<void>(resolve => { grace = setTimeout(resolve, this.options.closeTimeoutMs ?? 1000); }),
383
+ ]);
384
+ } finally { if (grace) clearTimeout(grace); }
385
+ for (const runId of await this.store.list()) {
386
+ const state = await this.require(runId); if (!terminal(state) && state.workflow.runs.some(run => run.status === "running")) await this.update(runId, current => {
387
+ current.workflow = recoverWorkflow(current.workflow, "Owner closed; outcome requires reconciliation");
388
+ for (const step of current.steps.filter(item => item.status === "running")) step.status = "uncertain";
389
+ current.status = "blocked"; event(current, "run.recovered", "Owner closed with unresolved attempts");
390
+ });
391
+ else if (state.status === "publishing") await this.update(runId, current => {
392
+ current.status = "blocked"; current.error = "Owner closed during publication; inspect actual scoped files before reconciliation";
393
+ event(current, "publication.uncertain", current.error);
394
+ });
395
+ else if (["running", "preparing", "canceling"].includes(state.status)) await this.update(runId, current => {
396
+ current.status = "paused"; event(current, "run.paused", "Owner closed; resume dispatch explicitly after restart");
397
+ });
398
+ }
399
+ } finally { if (!this.publicationsInFlight) this.releaseOwner(); }
400
+ }
401
+ }
@@ -0,0 +1,165 @@
1
+ import { mkdir, open, readFile, rename, unlink, link, lstat, realpath, readdir } from "node:fs/promises";
2
+ import { readFileSync, unlinkSync, lstatSync } from "node:fs";
3
+ import { join, isAbsolute, resolve } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { managedFail, managedId, managedDigest, validateManagedSpec, type ManagedRunState } from "./managed-run-contract.ts";
6
+ import { validateWorkflowState } from "./workflow-engine.ts";
7
+ import { assertAttachedSafePath } from "./attached-snapshot.ts";
8
+ import { stableStringify } from "./canonical.ts";
9
+
10
+ interface Ack { runId: string; version: number; status: ManagedRunState["status"] }
11
+ interface Receipt { requestId: string; fingerprint: string; ack: Ack }
12
+ interface RecordFile { state: ManagedRunState; receipts: Receipt[] }
13
+ export interface ManagedTransactionOptions { requestId?: string; fingerprint?: string; expectedVersion?: number }
14
+ const MAX_RECORD = 32 * 1024 * 1024;
15
+ const missing = (error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT";
16
+ async function readLock(path: string): Promise<string> {
17
+ const deadline = Date.now() + 1000;
18
+ for (;;) {
19
+ try { return await readFile(path, "utf8"); }
20
+ catch (error) {
21
+ if (!["EPERM", "EACCES", "EBUSY"].includes((error as NodeJS.ErrnoException).code ?? "") || Date.now() >= deadline) throw error;
22
+ await new Promise(resolve => setTimeout(resolve, 20));
23
+ }
24
+ }
25
+ }
26
+ const statuses = ["preparing", "running", "paused", "blocked", "canceling", "canceled", "publishing", "completed", "failed"];
27
+ /** Windows readers/AV may briefly hold the destination; never unlink its atomic predecessor. */
28
+ async function replaceRecord(temp: string, destination: string): Promise<void> {
29
+ const deadline = Date.now() + 1000;
30
+ while (true) {
31
+ try { await rename(temp, destination); return; }
32
+ catch (error) {
33
+ if (!["EPERM", "EACCES", "EBUSY"].includes((error as NodeJS.ErrnoException).code ?? "") || Date.now() >= deadline) throw error;
34
+ await new Promise(resolve => setTimeout(resolve, 20));
35
+ }
36
+ }
37
+ }
38
+ function alive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code !== "ESRCH"; } }
39
+ function assert(value: unknown, message: string): asserts value { if (!value) managedFail("AF_RUN_STORE", message); }
40
+ function validateState(state: ManagedRunState, root: string, runId: string) {
41
+ assert(state && Object.keys(state).every(key => ["schemaVersion", "runId", "repositoryRoot", "ownerPid", "version", "spec", "workflow", "base", "status", "steps", "events", "cursor", "instructions", "createdAt", "updatedAt", "published", "publicationIntent", "error"].includes(key)), "Invalid run fields");
42
+ assert(state.schemaVersion === 1 && state.runId === runId && state.repositoryRoot === root, "Invalid run identity");
43
+ assert(Number.isSafeInteger(state.version) && state.version > 0 && Number.isSafeInteger(state.ownerPid) && state.ownerPid > 0 && statuses.includes(state.status), "Invalid run version, owner or status");
44
+ validateManagedSpec(state.spec); validateWorkflowState(state.workflow);
45
+ assert(state.workflow.workflowId === state.spec.workflow.workflowId, "Workflow identity mismatch");
46
+ assert(Array.isArray(state.steps) && state.steps.length <= 100 && new Set(state.steps.map(step => step.attemptId)).size === state.steps.length, "Invalid steps");
47
+ for (const step of state.steps) {
48
+ managedId(step.attemptId, "attemptId"); managedId(step.nodeId, "nodeId");
49
+ assert(["running", "succeeded", "failed", "uncertain"].includes(step.status) && state.workflow.runs.some(run => run.attemptId === step.attemptId && run.nodeId === step.nodeId && run.status === step.status), "Step attempt mismatch");
50
+ assert(step.directory === undefined || (isAbsolute(step.directory) && resolve(step.directory) === step.directory), "Invalid workspace directory");
51
+ if (step.repositoryContext !== undefined) {
52
+ const context = step.repositoryContext;
53
+ assert(context && Object.keys(context).every(key => ["provider", "phase", "status", "sourceRoot", "cloneRoot", "snapshotId", "diagnostics", "quality", "runtime"].includes(key)), "Invalid repository context fields");
54
+ assert(context.provider === "repository" && context.phase === "prepared-input" && ["ready", "unavailable"].includes(context.status) && context.sourceRoot === root && context.cloneRoot === step.directory, "Repository context belongs to another checkout");
55
+ assert(Array.isArray(context.diagnostics) && context.diagnostics.length <= 20 && context.diagnostics.every(message => typeof message === "string" && message.length <= 512), "Invalid repository context diagnostics");
56
+ assert(context.snapshotId === undefined || (typeof context.snapshotId === "string" && context.snapshotId.length <= 256 && context.snapshotId.length > 0), "Invalid repository context snapshot");
57
+ assert(context.status !== "ready" || context.snapshotId !== undefined, "Ready repository context requires a snapshot");
58
+ if (context.runtime !== undefined) {
59
+ const runtime = context.runtime;
60
+ assert(runtime && Object.keys(runtime).every(key => ["reportId", "environmentId", "phase"].includes(key))
61
+ && context.status === "ready" && runtime.phase === "source-observed-matching-input"
62
+ && typeof runtime.reportId === "string" && runtime.reportId.length > 0 && runtime.reportId.length <= 128
63
+ && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(runtime.environmentId), "Invalid runtime repository evidence metadata");
64
+ }
65
+ if (context.quality !== undefined) {
66
+ const quality = context.quality, counts = ["factsReused", "unresolvedLocalImports", "unresolvedUiReferences", "dynamicHttpCalls", "httpEndpointLinks", "analysisErrors"] as const;
67
+ assert(quality && Object.keys(quality).every(key => ["completeness", "runtimeObserved", ...counts].includes(key))
68
+ && quality.completeness === "partial" && quality.runtimeObserved === false && counts.every(key => Number.isSafeInteger(quality[key]) && quality[key] >= 0 && quality[key] <= 1_000_000), "Invalid static repository quality");
69
+ }
70
+ }
71
+ }
72
+ assert(Number.isSafeInteger(state.cursor) && state.cursor >= 0 && Array.isArray(state.events) && state.events.length <= 10000, "Invalid event history");
73
+ let cursor = 0; for (const event of state.events) { assert(Number.isSafeInteger(event.cursor) && event.cursor > cursor && event.cursor <= state.cursor && typeof event.summary === "string" && typeof event.type === "string" && Number.isFinite(Date.parse(event.at)), "Invalid event"); cursor = event.cursor; }
74
+ assert(Array.isArray(state.instructions) && state.instructions.length <= 1000 && state.instructions.every(item => typeof item === "string" && item.length <= 12000), "Invalid instructions");
75
+ assert(Number.isFinite(Date.parse(state.createdAt)) && Number.isFinite(Date.parse(state.updatedAt)), "Invalid timestamps");
76
+ if (state.base) assert(state.base.root === root && state.base.runId === runId && isAbsolute(state.base.baselineDirectory) && /^sha256:[a-f0-9]{64}$/.test(state.base.digest) && /^[a-f0-9]{40,64}$/.test(state.base.head) && stableStringify(state.base.scope) === stableStringify([...state.spec.scope].sort()), "Invalid managed base");
77
+ }
78
+ function reclaimDeadGuard(path: string) {
79
+ try {
80
+ if (!lstatSync(path).isFile()) managedFail("AF_RUN_LOCK", "Unsupported reclaim guard");
81
+ const raw = readFileSync(path, "utf8"), owner = JSON.parse(raw);
82
+ if (Number.isSafeInteger(owner.pid) && owner.pid > 0 && !alive(owner.pid) && readFileSync(path, "utf8") === raw) unlinkSync(path);
83
+ } catch (error) { if (!missing(error) && !(error instanceof SyntaxError)) throw error; }
84
+ }
85
+ /** Atomic state + compact receipts. Locks serialize independent service processes. */
86
+ export class ManagedRunStore {
87
+ private constructor(readonly root: string, readonly directory: string) {}
88
+ static async open(root: string): Promise<ManagedRunStore> {
89
+ root = await realpath(root); const directory = join(root, ".forge", "local", "agent-fabric", "managed-runs");
90
+ await assertAttachedSafePath(root, directory); await mkdir(directory, { recursive: true }); await assertAttachedSafePath(root, directory);
91
+ return new ManagedRunStore(root, directory);
92
+ }
93
+ private path(runId: string) { managedId(runId, "runId"); return join(this.directory, `${managedDigest(runId).slice(7)}.json`); }
94
+ async read(runId: string): Promise<ManagedRunState | undefined> { const record = await this.load(runId); return record ? structuredClone(record.state) : undefined; }
95
+ async list(): Promise<string[]> {
96
+ await assertAttachedSafePath(this.root, this.directory); const ids: string[] = [];
97
+ for (const name of (await readdir(this.directory)).filter(name => /^[a-f0-9]{64}\.json$/.test(name))) {
98
+ const path = join(this.directory, name); await assertAttachedSafePath(this.root, path);
99
+ assert((await lstat(path)).size <= MAX_RECORD, "Run record exceeds size limit");
100
+ const envelope = JSON.parse(await readFile(path, "utf8")); const runId = managedId(envelope?.record?.state?.runId, "stored runId");
101
+ assert(this.path(runId) === path, "Run filename mismatch"); await this.load(runId); ids.push(runId);
102
+ }
103
+ return ids.sort();
104
+ }
105
+ private async load(runId: string): Promise<RecordFile | undefined> {
106
+ const path = this.path(runId); await assertAttachedSafePath(this.root, path);
107
+ try {
108
+ assert((await lstat(path)).isFile() && (await lstat(path)).size <= MAX_RECORD, "Unsupported or excessive run record");
109
+ const envelope = JSON.parse(await readFile(path, "utf8"));
110
+ assert(envelope && Object.keys(envelope).length === 2 && envelope.record && envelope.digest === managedDigest(stableStringify(envelope.record)), "Run integrity check failed");
111
+ const record = envelope.record as RecordFile; assert(Object.keys(record).every(key => ["state", "receipts"].includes(key)) && Array.isArray(record.receipts) && record.receipts.length <= 10000, "Invalid run record");
112
+ validateState(record.state, this.root, runId); const ids = new Set<string>();
113
+ for (const receipt of record.receipts) {
114
+ managedId(receipt.requestId, "requestId"); assert(!ids.has(receipt.requestId) && /^(?:sha256:)?[a-f0-9]{64}$/.test(receipt.fingerprint), "Invalid request receipt"); ids.add(receipt.requestId);
115
+ assert(receipt.ack.runId === runId && Number.isSafeInteger(receipt.ack.version) && receipt.ack.version > 0 && receipt.ack.version <= record.state.version && statuses.includes(receipt.ack.status), "Invalid acknowledgment");
116
+ }
117
+ return record;
118
+ } catch (error) { if (missing(error)) return undefined; if (error instanceof SyntaxError) managedFail("AF_RUN_STORE", "Malformed run JSON"); throw error; }
119
+ }
120
+ private async lock(runId: string): Promise<() => Promise<void>> {
121
+ const lock = `${this.path(runId)}.lock`, token = randomUUID(), candidate = `${lock}.${token}.candidate`, claim = `${lock}.reclaim`;
122
+ const ownerBytes = JSON.stringify({ pid: process.pid, token }), deadline = Date.now() + 10000;
123
+ await assertAttachedSafePath(this.root, candidate); const handle = await open(candidate, "wx", 0o600);
124
+ try { await handle.writeFile(ownerBytes); await handle.sync(); } finally { await handle.close(); }
125
+ try {
126
+ while (true) {
127
+ await assertAttachedSafePath(this.root, lock);
128
+ try { await link(candidate, lock); return async () => { await assertAttachedSafePath(this.root, lock); if (await readLock(lock) !== ownerBytes) managedFail("AF_RUN_LOCK", "Lock ownership changed"); await unlink(lock); }; }
129
+ catch (error) { if (!["EEXIST", "EPERM", "EACCES", "EBUSY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error; }
130
+ try {
131
+ assert((await lstat(lock)).isFile(), "Unsupported lock"); const raw = await readLock(lock), previous = JSON.parse(raw);
132
+ if (Number.isSafeInteger(previous.pid) && previous.pid > 0 && !alive(previous.pid)) {
133
+ await assertAttachedSafePath(this.root, claim); reclaimDeadGuard(claim); let claimed = false;
134
+ try { await link(candidate, claim); claimed = true; if (readFileSync(lock, "utf8") === raw) unlinkSync(lock); }
135
+ catch (error) { if (!missing(error) && (error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
136
+ finally { if (claimed) await unlink(claim).catch(() => {}); }
137
+ }
138
+ } catch (error) { if (!missing(error) && !(error instanceof SyntaxError)) throw error; }
139
+ if (Date.now() >= deadline) managedFail("AF_RUN_LOCK_BUSY", "Run is locked by another process");
140
+ await new Promise(resolve => setTimeout(resolve, 20));
141
+ }
142
+ } finally { await unlink(candidate).catch(() => {}); }
143
+ }
144
+ async transact(runId: string, options: ManagedTransactionOptions, apply: (current?: ManagedRunState) => ManagedRunState): Promise<{ state: ManagedRunState; ack: Ack; replayed: boolean }> {
145
+ managedId(runId, "runId"); assert(options && Object.keys(options).every(key => ["requestId", "fingerprint", "expectedVersion"].includes(key)), "Invalid transaction options");
146
+ if (options.requestId !== undefined) { managedId(options.requestId, "requestId"); assert(typeof options.fingerprint === "string" && /^(?:sha256:)?[a-f0-9]{64}$/.test(options.fingerprint), "Request fingerprint required"); }
147
+ else assert(options.fingerprint === undefined, "Fingerprint without requestId");
148
+ assert(options.expectedVersion === undefined || (Number.isSafeInteger(options.expectedVersion) && options.expectedVersion >= 0), "Invalid expectedVersion");
149
+ const release = await this.lock(runId);
150
+ try {
151
+ const old = await this.load(runId), receipt = old?.receipts.find(item => item.requestId === options.requestId);
152
+ if (receipt) { if (receipt.fingerprint !== options.fingerprint) managedFail("AF_RUN_REQUEST_CONFLICT", "requestId already used with different input"); return { state: structuredClone(old!.state), ack: structuredClone(receipt.ack), replayed: true }; }
153
+ if (options.expectedVersion !== undefined && options.expectedVersion !== (old?.state.version ?? 0)) managedFail("AF_RUN_VERSION_CONFLICT", `Expected version ${old?.state.version ?? 0}`);
154
+ const state = structuredClone(apply(old ? structuredClone(old.state) : undefined));
155
+ state.version = (old?.state.version ?? 0) + 1; state.updatedAt = new Date().toISOString(); validateState(state, this.root, runId);
156
+ const ack: Ack = { runId, version: state.version, status: state.status };
157
+ const record: RecordFile = { state, receipts: [...(old?.receipts ?? []), ...(options.requestId ? [{ requestId: options.requestId, fingerprint: options.fingerprint!, ack }] : [])] };
158
+ const data = stableStringify({ record, digest: managedDigest(stableStringify(record)) }); assert(Buffer.byteLength(data) <= MAX_RECORD && record.receipts.length <= 10000, "Run record limit exceeded");
159
+ const temp = `${this.path(runId)}.${randomUUID()}.tmp`; await assertAttachedSafePath(this.root, temp); const handle = await open(temp, "wx", 0o600);
160
+ try { await handle.writeFile(data); await handle.sync(); } finally { await handle.close(); }
161
+ try { await assertAttachedSafePath(this.root, this.path(runId)); await replaceRecord(temp, this.path(runId)); } finally { await unlink(temp).catch(() => {}); }
162
+ return { state: structuredClone(state), ack: structuredClone(ack), replayed: false };
163
+ } finally { await release(); }
164
+ }
165
+ }