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,2282 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createConnection } from "node:net";
4
+ import { basename, join, relative, resolve } from "node:path";
5
+ import { setTimeout as sleep } from "node:timers/promises";
6
+ import { normalizePath } from "../compiler/primitives/paths.ts";
7
+ import { nodeFileSystem } from "../compiler/fs/index.ts";
8
+ import type { Diagnostic } from "../compiler/types/diagnostic.ts";
9
+ import { createDiagnostic } from "../compiler/diagnostics/create.ts";
10
+ import { run as runGenerate } from "../compiler/orchestrator/run.ts";
11
+ import { runAgentPrepare } from "../agent-adapters/index.ts";
12
+ import { runAgentHooksStatus } from "../agent-adapters/index.ts";
13
+ import type { AgentAdapterTarget } from "../agent-adapters/types.ts";
14
+ import type { DevConsoleCycle, DevConsoleDiffPlan, DevConsoleGeneratedSummary } from "../dev-console/types.ts";
15
+ import { runDevConsoleCycle } from "../dev-console/cycle.ts";
16
+ import { runChangedCommand } from "./changed.ts";
17
+ import { runDeltaStatus } from "../delta/index.ts";
18
+ import {
19
+ codexAppServerCommands,
20
+ generateCodexAppServerSchemas,
21
+ inspectCodexAppServer,
22
+ probeCodexAppServerHandshake,
23
+ skippedCodexAppServerHandshake,
24
+ type CodexAppServerCommands,
25
+ type CodexAppServerHandshakeResult,
26
+ type CodexAppServerProof,
27
+ type CodexAppServerSchemaGenerationResult,
28
+ } from "./codex-app-server.ts";
29
+ import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
30
+
31
+ const STUDIO_TARGET_RUNTIME_PORT = 3766;
32
+ const STUDIO_LOCAL_TENANT_ID = "00000000-0000-4000-8000-000000000001";
33
+ const STUDIO_LOCAL_USER_ID = "forge-studio-dev";
34
+ const STUDIO_LOCAL_ROLE = "owner";
35
+
36
+ export interface StudioAttachOptions {
37
+ workspaceRoot: string;
38
+ subcommand?: "attach" | "snapshot" | "watch" | "open" | "doctor" | "bridge" | "codex-server";
39
+ path?: string;
40
+ previewUrl?: string;
41
+ previewPort?: number;
42
+ studioUrl?: string;
43
+ intervalMs?: number;
44
+ once?: boolean;
45
+ workspaceId?: string;
46
+ tenantId?: string;
47
+ userId?: string;
48
+ role?: string;
49
+ targets: string[];
50
+ install?: boolean;
51
+ start?: boolean;
52
+ bridge?: boolean;
53
+ writeSchemas?: boolean;
54
+ probeAppServer?: boolean;
55
+ json: boolean;
56
+ dryRun: boolean;
57
+ force: boolean;
58
+ }
59
+
60
+ export interface StudioAttachResult {
61
+ schemaVersion: "0.1.0";
62
+ ok: boolean;
63
+ action: "attach";
64
+ app: {
65
+ name: string;
66
+ path: string;
67
+ template?: string;
68
+ };
69
+ preview: {
70
+ url: string;
71
+ port?: number;
72
+ requestedUrl?: string;
73
+ requestedPort?: number;
74
+ source: "explicit-url" | "preview-port" | "default" | "studio-avoid-self-preview";
75
+ isStudioSelfPreview: boolean;
76
+ note: string;
77
+ status: {
78
+ state: "reachable" | "not-running" | "not-checked";
79
+ checked: boolean;
80
+ reason: string;
81
+ suggestedCommands: string[];
82
+ };
83
+ };
84
+ posture: {
85
+ checked: boolean;
86
+ state: "ready" | "needs-attention" | "not-checked";
87
+ reason: string;
88
+ safeToEdit?: boolean;
89
+ generated?: DevConsoleGeneratedSummary;
90
+ changedFiles?: number;
91
+ diffPlan?: DevConsoleDiffPlan;
92
+ recommendedCommands: string[];
93
+ };
94
+ targets: string[];
95
+ manifestPath: string;
96
+ filesWritten: string[];
97
+ filesPlanned: string[];
98
+ agentResults: Array<{
99
+ target: string;
100
+ ok: boolean;
101
+ filesWritten: string[];
102
+ filesPlanned: string[];
103
+ diagnostics: Diagnostic[];
104
+ }>;
105
+ commands: {
106
+ startTargetApp: string;
107
+ startTargetAppCwd: string;
108
+ openPreview: string;
109
+ probePreview: string;
110
+ installHooks: string[];
111
+ checkHooks: string[];
112
+ openContext: string;
113
+ codexAppServer?: CodexAppServerCommands;
114
+ };
115
+ diagnostics: Diagnostic[];
116
+ nextActions: string[];
117
+ exitCode: 0 | 1;
118
+ }
119
+
120
+ export interface StudioSnapshotResult {
121
+ schemaVersion: "0.1.0";
122
+ ok: boolean;
123
+ action: "snapshot";
124
+ app: StudioAttachResult["app"];
125
+ preview: StudioAttachResult["preview"];
126
+ posture: StudioAttachResult["posture"];
127
+ targets: string[];
128
+ changed: Record<string, unknown>;
129
+ commands: StudioAttachResult["commands"] & {
130
+ attach: string;
131
+ changed: string;
132
+ handoff: string;
133
+ watch: string;
134
+ bridge: string;
135
+ doctor: string;
136
+ open: string;
137
+ };
138
+ contextPacket: {
139
+ source: "forgeos";
140
+ readFiles: string[];
141
+ commands: string[];
142
+ diffPlan?: DevConsoleDiffPlan;
143
+ };
144
+ handoff: {
145
+ previewUrl: string;
146
+ currentSession?: { id?: string; title?: string; status?: string; confidence?: number };
147
+ changedFiles?: number;
148
+ generatedState?: string;
149
+ deltaHealth?: string;
150
+ agentContextCommand: string;
151
+ recommendedCommands: string[];
152
+ };
153
+ proofs: {
154
+ preview: StudioAttachResult["preview"]["status"];
155
+ generated: StudioAttachResult["posture"]["generated"];
156
+ hooks: Array<{
157
+ target: string;
158
+ ok: boolean;
159
+ installed?: boolean;
160
+ bridgeWritable?: boolean;
161
+ deltaWritable?: boolean;
162
+ visibleInMemory?: boolean;
163
+ recentEvents?: number;
164
+ usefulSignals?: number;
165
+ nativeSignals?: number;
166
+ canarySignals?: number;
167
+ approvalRequired?: boolean;
168
+ approvalStatus?: string;
169
+ workspaceRoot?: string;
170
+ ignoredOutOfWorkspaceEvents?: number;
171
+ lastSignal?: unknown;
172
+ checks?: unknown[];
173
+ diagnostics?: Diagnostic[];
174
+ nextActions?: string[];
175
+ }>;
176
+ codexAppServer?: CodexAppServerProof;
177
+ delta?: unknown;
178
+ };
179
+ diagnostics: Diagnostic[];
180
+ nextActions: string[];
181
+ exitCode: 0 | 1;
182
+ }
183
+
184
+ export interface StudioWatchResult {
185
+ schemaVersion: "0.1.0";
186
+ ok: boolean;
187
+ action: "watch";
188
+ stream: {
189
+ mode: "once" | "watch";
190
+ event: "studio.snapshot";
191
+ note: string;
192
+ followCommand: string;
193
+ intervalMs: number;
194
+ dryRun: boolean;
195
+ emittedAt: string;
196
+ };
197
+ snapshot: StudioSnapshotResult;
198
+ exitCode: 0 | 1;
199
+ }
200
+
201
+ export interface StudioDoctorResult {
202
+ schemaVersion: "0.1.0";
203
+ ok: boolean;
204
+ action: "doctor";
205
+ app: StudioAttachResult["app"];
206
+ checks: Array<{
207
+ name: string;
208
+ ok: boolean;
209
+ status: "ok" | "warning" | "failed";
210
+ message: string;
211
+ suggestedCommands: string[];
212
+ }>;
213
+ snapshot: StudioSnapshotResult;
214
+ diagnostics: Diagnostic[];
215
+ nextActions: string[];
216
+ exitCode: 0 | 1;
217
+ }
218
+
219
+ export interface StudioBridgeResult {
220
+ schemaVersion: "0.1.0";
221
+ ok: boolean;
222
+ action: "bridge";
223
+ mode: "once" | "watch";
224
+ studioUrl: string;
225
+ endpoint: string;
226
+ intervalMs: number;
227
+ provider: string;
228
+ target: string;
229
+ posted: boolean;
230
+ dryRun: boolean;
231
+ snapshot: StudioSnapshotResult;
232
+ response?: unknown;
233
+ diagnostics: Diagnostic[];
234
+ nextActions: string[];
235
+ exitCode: 0 | 1;
236
+ }
237
+
238
+ export interface StudioOpenResult {
239
+ schemaVersion: "0.1.0";
240
+ ok: boolean;
241
+ action: "open";
242
+ app: StudioAttachResult["app"];
243
+ preview: StudioAttachResult["preview"];
244
+ attach: StudioAttachResult;
245
+ previewAutomation: {
246
+ attempted: boolean;
247
+ started: boolean;
248
+ alreadyRunning?: boolean;
249
+ skippedReason?: "already-running" | "dry-run" | "disabled" | "non-local-preview" | "missing-dependencies" | "install-failed";
250
+ command: string;
251
+ cwd: string;
252
+ pid?: number;
253
+ owner?: {
254
+ kind: "forge-managed" | "external-process" | "preexisting-reachable-preview" | "not-owned" | "dry-run";
255
+ pid?: number;
256
+ command?: string;
257
+ evidence: string;
258
+ statePath?: string;
259
+ };
260
+ statusBefore: StudioAttachResult["preview"]["status"];
261
+ statusAfter: StudioAttachResult["preview"]["status"];
262
+ install: {
263
+ required: boolean;
264
+ installed: boolean;
265
+ attempted: boolean;
266
+ command?: string;
267
+ cwd: string;
268
+ ok?: boolean;
269
+ exitCode?: number;
270
+ };
271
+ };
272
+ bridge: {
273
+ attempted: boolean;
274
+ ok: boolean;
275
+ posted: boolean;
276
+ dryRun: boolean;
277
+ mode?: "once" | "watch";
278
+ autoStarted?: boolean;
279
+ alreadyRunning?: boolean;
280
+ command?: string;
281
+ cwd?: string;
282
+ intervalMs?: number;
283
+ pid?: number;
284
+ studioUrl: string;
285
+ endpoint?: string;
286
+ diagnostics: Diagnostic[];
287
+ nextActions: string[];
288
+ };
289
+ commands: StudioAttachResult["commands"] & {
290
+ attach: string;
291
+ bridge: string;
292
+ doctor: string;
293
+ open: string;
294
+ install?: string;
295
+ };
296
+ diagnostics: Diagnostic[];
297
+ nextActions: string[];
298
+ exitCode: 0 | 1;
299
+ }
300
+
301
+ export interface StudioCodexServerResult {
302
+ schemaVersion: "0.1.0";
303
+ ok: boolean;
304
+ action: "codex-server";
305
+ app: StudioAttachResult["app"];
306
+ proof: CodexAppServerProof;
307
+ schemaGeneration: CodexAppServerSchemaGenerationResult;
308
+ handshake: CodexAppServerHandshakeResult;
309
+ commands: CodexAppServerCommands;
310
+ diagnostics: Diagnostic[];
311
+ nextActions: string[];
312
+ exitCode: 0 | 1;
313
+ }
314
+
315
+ function readPackageJson(appRoot: string): Record<string, unknown> {
316
+ const path = join(appRoot, "package.json");
317
+ if (!nodeFileSystem.exists(path)) {
318
+ return {};
319
+ }
320
+ try {
321
+ return JSON.parse(nodeFileSystem.readText(path) ?? "{}") as Record<string, unknown>;
322
+ } catch {
323
+ return {};
324
+ }
325
+ }
326
+
327
+ function packageName(pkg: Record<string, unknown>, appRoot: string): string {
328
+ return typeof pkg.name === "string" && pkg.name.trim()
329
+ ? pkg.name.trim()
330
+ : basename(appRoot);
331
+ }
332
+
333
+ function packageTemplate(pkg: Record<string, unknown>): string | undefined {
334
+ const forge = pkg.forge;
335
+ if (!forge || typeof forge !== "object" || Array.isArray(forge)) {
336
+ return undefined;
337
+ }
338
+ const template = (forge as { template?: unknown }).template;
339
+ return typeof template === "string" ? template : undefined;
340
+ }
341
+
342
+ function normalizeTarget(target: string): string {
343
+ return target === "claude-code" ? "claude" : target;
344
+ }
345
+
346
+ function expandedTargets(targets: string[]): string[] {
347
+ const normalized = targets.map(normalizeTarget);
348
+ return normalized.includes("all")
349
+ ? ["codex", "claude", "cursor"]
350
+ : [...new Set(normalized)];
351
+ }
352
+
353
+ function providerName(target: string): string {
354
+ const normalized = normalizeTarget(target);
355
+ if (normalized === "claude") return "Claude Code";
356
+ if (normalized === "cursor") return "Cursor";
357
+ return "Codex";
358
+ }
359
+
360
+ function hasCodexTarget(targets: string[]): boolean {
361
+ return targets.map(normalizeTarget).includes("codex");
362
+ }
363
+
364
+ function normalizeStudioUrl(value?: string): string {
365
+ return (value?.trim() || process.env.FORGE_STUDIO_URL || "http://127.0.0.1:3765").replace(/\/+$/, "");
366
+ }
367
+
368
+ function localPreviewPort(url: string): number | undefined {
369
+ try {
370
+ const parsed = new URL(url);
371
+ const hostname = parsed.hostname.toLowerCase();
372
+ if (hostname !== "localhost" && hostname !== "127.0.0.1") {
373
+ return undefined;
374
+ }
375
+ const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
376
+ return Number.isInteger(port) && port > 0 ? port : undefined;
377
+ } catch {
378
+ return undefined;
379
+ }
380
+ }
381
+
382
+ function previewStatus(
383
+ state: StudioAttachResult["preview"]["status"]["state"],
384
+ reason: string,
385
+ suggestedCommands: string[] = [],
386
+ ): StudioAttachResult["preview"]["status"] {
387
+ return {
388
+ state,
389
+ checked: state !== "not-checked",
390
+ reason,
391
+ suggestedCommands,
392
+ };
393
+ }
394
+
395
+ function withPreviewStatus(
396
+ preview: Omit<StudioAttachResult["preview"], "status">,
397
+ status: StudioAttachResult["preview"]["status"] = previewStatus("not-checked", "preview reachability has not been checked yet"),
398
+ ): StudioAttachResult["preview"] {
399
+ return { ...preview, status };
400
+ }
401
+
402
+ function previewFor(options: StudioAttachOptions, diagnostics: Diagnostic[]): StudioAttachResult["preview"] {
403
+ const avoidSelfPreview = (requestedUrl: string, requestedPort: number): StudioAttachResult["preview"] => {
404
+ if (requestedPort !== 5173 || options.force) {
405
+ return withPreviewStatus({
406
+ url: requestedUrl,
407
+ port: requestedPort,
408
+ source: options.previewUrl?.trim() ? "explicit-url" : "preview-port",
409
+ isStudioSelfPreview: requestedPort === 5173,
410
+ note: requestedPort === 5173
411
+ ? "Preview points at the conventional Forge Studio port because --force was used."
412
+ : "Preview URL was provided explicitly.",
413
+ });
414
+ }
415
+ diagnostics.push(createDiagnostic({
416
+ severity: "warning",
417
+ code: "FORGE_STUDIO_SELF_PREVIEW_AVOIDED",
418
+ message: "preview pointed at http://127.0.0.1:5173, which is normally Forge Studio itself; using http://127.0.0.1:5174 for the target app preview",
419
+ fixHint: "Start the app under construction on 5174, or pass --force if 5173 is intentionally the target app.",
420
+ suggestedCommands: ["forge studio attach . --preview-port 5174 --target codex --json", targetAppDevCommand(5174)],
421
+ }));
422
+ return withPreviewStatus({
423
+ url: "http://127.0.0.1:5174",
424
+ port: 5174,
425
+ requestedUrl,
426
+ requestedPort,
427
+ source: "studio-avoid-self-preview",
428
+ isStudioSelfPreview: true,
429
+ note: "Avoided rendering Forge Studio inside itself; use 5174 for the app being built.",
430
+ });
431
+ };
432
+
433
+ if (options.previewUrl?.trim()) {
434
+ const requestedUrl = options.previewUrl.trim();
435
+ const requestedPort = localPreviewPort(requestedUrl);
436
+ if (requestedPort) {
437
+ return avoidSelfPreview(requestedUrl, requestedPort);
438
+ }
439
+ return withPreviewStatus({
440
+ url: requestedUrl,
441
+ source: "explicit-url",
442
+ isStudioSelfPreview: false,
443
+ note: "Preview URL was provided explicitly.",
444
+ });
445
+ }
446
+ if (options.previewPort) {
447
+ const requestedUrl = `http://127.0.0.1:${options.previewPort}`;
448
+ if (options.previewPort === 5173) {
449
+ return avoidSelfPreview(requestedUrl, options.previewPort);
450
+ }
451
+ return withPreviewStatus({
452
+ url: requestedUrl,
453
+ port: options.previewPort,
454
+ source: "preview-port",
455
+ isStudioSelfPreview: false,
456
+ note: "Preview port was provided explicitly.",
457
+ });
458
+ }
459
+ return withPreviewStatus({
460
+ url: "http://127.0.0.1:5174",
461
+ port: 5174,
462
+ source: "default",
463
+ isStudioSelfPreview: false,
464
+ note: "Default target app preview URL for Studio observer flows.",
465
+ });
466
+ }
467
+
468
+ function localPreviewHost(url: string): string | undefined {
469
+ try {
470
+ const parsed = new URL(url);
471
+ const hostname = parsed.hostname.toLowerCase();
472
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
473
+ return "127.0.0.1";
474
+ }
475
+ return undefined;
476
+ } catch {
477
+ return undefined;
478
+ }
479
+ }
480
+
481
+ export async function probeStudioPreview(
482
+ preview: Omit<StudioAttachResult["preview"], "status">,
483
+ options: { dryRun: boolean; startCommand: string; timeoutMs?: number },
484
+ ): Promise<StudioAttachResult["preview"]["status"]> {
485
+ if (options.dryRun) {
486
+ return previewStatus("not-checked", "dry-run does not probe the preview URL", [options.startCommand]);
487
+ }
488
+ const host = localPreviewHost(preview.url);
489
+ const port = preview.port ?? localPreviewPort(preview.url);
490
+ if (!host || !port) {
491
+ return previewStatus("not-checked", "preview URL is not a local host:port pair", [options.startCommand]);
492
+ }
493
+ const timeoutMs = options.timeoutMs ?? 500;
494
+ const reachable = await new Promise<boolean>((resolve) => {
495
+ const socket = createConnection({ host, port });
496
+ let settled = false;
497
+ const settle = (value: boolean) => {
498
+ if (settled) return;
499
+ settled = true;
500
+ socket.destroy();
501
+ resolve(value);
502
+ };
503
+ socket.setTimeout(timeoutMs, () => settle(false));
504
+ socket.once("connect", () => settle(true));
505
+ socket.once("error", () => settle(false));
506
+ });
507
+ return reachable
508
+ ? previewStatus("reachable", `preview is reachable at ${preview.url}`, [])
509
+ : previewStatus("not-running", `preview is not reachable at ${preview.url}`, [options.startCommand, "forge dev --once --json"]);
510
+ }
511
+
512
+ function renderManifest(input: {
513
+ app: StudioAttachResult["app"];
514
+ preview: StudioAttachResult["preview"];
515
+ posture: StudioAttachResult["posture"];
516
+ targets: string[];
517
+ commands: StudioAttachResult["commands"];
518
+ }): string {
519
+ return `${JSON.stringify({
520
+ schemaVersion: "0.1.0",
521
+ attachedAt: new Date().toISOString(),
522
+ app: input.app,
523
+ preview: input.preview,
524
+ posture: input.posture,
525
+ targets: input.targets,
526
+ commands: input.commands,
527
+ }, null, 2)}\n`;
528
+ }
529
+
530
+ function readAttachmentManifest(appRoot: string): Partial<StudioAttachResult> | null {
531
+ const absolute = join(appRoot, ".forge", "studio", "attachment.json");
532
+ if (!nodeFileSystem.exists(absolute)) {
533
+ return null;
534
+ }
535
+ try {
536
+ return JSON.parse(nodeFileSystem.readText(absolute) ?? "{}") as Partial<StudioAttachResult>;
537
+ } catch {
538
+ return null;
539
+ }
540
+ }
541
+
542
+ function attachCommandFor(targets: string[], previewPort: number): string {
543
+ const targetArgs = (targets.length > 0 ? targets : ["codex"])
544
+ .map((target) => `--target ${target}`)
545
+ .join(" ");
546
+ return `forge studio attach . --preview-port ${previewPort} ${targetArgs} --json`;
547
+ }
548
+
549
+ function targetAppDevCommand(previewPort: number): string {
550
+ return `forge dev --port ${STUDIO_TARGET_RUNTIME_PORT} --web-port ${previewPort}`;
551
+ }
552
+
553
+ function forgeSourcePresent(appRoot: string): boolean {
554
+ return nodeFileSystem.exists(join(appRoot, "src", "forge"));
555
+ }
556
+
557
+ async function withWorkspaceCwd<T>(workspaceRoot: string, fn: () => Promise<T>): Promise<T> {
558
+ const previous = process.cwd();
559
+ const target = resolve(workspaceRoot);
560
+ if (resolve(previous).toLowerCase() === target.toLowerCase()) {
561
+ return fn();
562
+ }
563
+ process.chdir(target);
564
+ try {
565
+ return await fn();
566
+ } finally {
567
+ process.chdir(previous);
568
+ }
569
+ }
570
+
571
+ function postureFromDevCycle(cycle: DevConsoleCycle): StudioAttachResult["posture"] {
572
+ return {
573
+ checked: true,
574
+ state: cycle.ok ? "ready" : "needs-attention",
575
+ reason: cycle.ok
576
+ ? "forge dev --once completed cleanly for the attached app"
577
+ : "forge dev --once found issues in the attached app",
578
+ safeToEdit: cycle.summary.agentContext.safeToEdit,
579
+ generated: cycle.summary.generated,
580
+ changedFiles: cycle.summary.agentContext.changedFiles,
581
+ ...(cycle.summary.agentContext.diffPlan ? { diffPlan: cycle.summary.agentContext.diffPlan } : {}),
582
+ recommendedCommands: cycle.summary.agentContext.recommendedCommands.length > 0
583
+ ? cycle.summary.agentContext.recommendedCommands
584
+ : ["forge dev --once --json"],
585
+ };
586
+ }
587
+
588
+ async function inspectAttachPosture(
589
+ appRoot: string,
590
+ options: StudioAttachOptions,
591
+ ): Promise<StudioAttachResult["posture"]> {
592
+ if (options.dryRun) {
593
+ return {
594
+ checked: false,
595
+ state: "not-checked",
596
+ reason: "dry-run does not run forge dev --once in the attached app",
597
+ recommendedCommands: ["forge dev --once --json"],
598
+ };
599
+ }
600
+ if (!forgeSourcePresent(appRoot)) {
601
+ return {
602
+ checked: false,
603
+ state: "not-checked",
604
+ reason: "attached path does not contain src/forge; no ForgeOS posture snapshot was collected",
605
+ recommendedCommands: ["forge dev --once --json", "forge status --json"],
606
+ };
607
+ }
608
+ try {
609
+ return postureFromDevCycle(await runDevConsoleCycle({
610
+ workspaceRoot: appRoot,
611
+ mode: "once",
612
+ includeImpact: true,
613
+ }));
614
+ } catch (error) {
615
+ return {
616
+ checked: false,
617
+ state: "not-checked",
618
+ reason: `forge dev --once snapshot failed: ${error instanceof Error ? error.message : String(error)}`,
619
+ recommendedCommands: ["forge dev --once --json", "forge check --json"],
620
+ };
621
+ }
622
+ }
623
+
624
+ async function inspectReadOnlyPosture(
625
+ appRoot: string,
626
+ ): Promise<StudioAttachResult["posture"]> {
627
+ if (!forgeSourcePresent(appRoot)) {
628
+ return {
629
+ checked: false,
630
+ state: "not-checked",
631
+ reason: "attached path does not contain src/forge; no ForgeOS posture snapshot was collected",
632
+ recommendedCommands: ["forge dev --once --json", "forge status --json"],
633
+ };
634
+ }
635
+ try {
636
+ const generated = await withWorkspaceCwd(appRoot, () =>
637
+ runGenerate({
638
+ workspaceRoot: appRoot,
639
+ check: true,
640
+ dryRun: false,
641
+ json: false,
642
+ concurrency: 4,
643
+ })
644
+ );
645
+ const changed = runChangedCommand(appRoot);
646
+ const summary = changed.data.summary as { changedFiles?: number } | undefined;
647
+ const diffPlan = changed.data.diffPlan as DevConsoleDiffPlan | undefined;
648
+ const generatedSummary: DevConsoleGeneratedSummary = {
649
+ ok: generated.exitCode === 0,
650
+ state: generated.exitCode === 0 ? "fresh" : "stale-risk",
651
+ changedFiles: generated.changed.length,
652
+ sampleChanged: generated.changed.slice(0, 12),
653
+ hiddenChanged: Math.max(0, generated.changed.length - 12),
654
+ message: generated.exitCode === 0
655
+ ? "generated artifacts are fresh; snapshot did not write files"
656
+ : "generated artifacts may be stale; snapshot did not regenerate files",
657
+ command: "forge generate",
658
+ checkCommand: "forge generate --check --json",
659
+ };
660
+ return {
661
+ checked: true,
662
+ state: generated.exitCode === 0 ? "ready" : "needs-attention",
663
+ reason: generated.exitCode === 0
664
+ ? "read-only generated check passed"
665
+ : "read-only generated check found stale generated artifacts",
666
+ safeToEdit: generated.exitCode === 0,
667
+ generated: generatedSummary,
668
+ changedFiles: summary?.changedFiles ?? 0,
669
+ ...(diffPlan ? { diffPlan } : {}),
670
+ recommendedCommands: generated.exitCode === 0
671
+ ? ["forge dev --once --json", "forge changed --json"]
672
+ : ["forge generate", "forge check --json"],
673
+ };
674
+ } catch (error) {
675
+ return {
676
+ checked: false,
677
+ state: "not-checked",
678
+ reason: `read-only generated snapshot failed: ${error instanceof Error ? error.message : String(error)}`,
679
+ recommendedCommands: ["forge generate --check --json", "forge check --json"],
680
+ };
681
+ }
682
+ }
683
+
684
+ async function collectHookProofs(appRoot: string, targets: string[]): Promise<StudioSnapshotResult["proofs"]["hooks"]> {
685
+ const proofs: StudioSnapshotResult["proofs"]["hooks"] = [];
686
+ for (const target of targets) {
687
+ try {
688
+ const result = await runAgentHooksStatus({
689
+ subcommand: "hooks",
690
+ hookAction: "status",
691
+ workspaceRoot: appRoot,
692
+ json: true,
693
+ target: target as AgentAdapterTarget,
694
+ dryRun: false,
695
+ force: false,
696
+ preserveUserSections: true,
697
+ skills: true,
698
+ rules: true,
699
+ });
700
+ proofs.push({
701
+ target,
702
+ ok: result.ok,
703
+ installed: result.installed,
704
+ bridgeWritable: result.bridgeWritable,
705
+ deltaWritable: result.deltaWritable,
706
+ visibleInMemory: result.visibleInMemory,
707
+ recentEvents: result.recentEvents,
708
+ usefulSignals: result.usefulSignals,
709
+ nativeSignals: result.nativeSignals,
710
+ canarySignals: result.canarySignals,
711
+ approvalRequired: result.approvalRequired,
712
+ approvalStatus: result.approvalStatus,
713
+ workspaceRoot: result.workspaceRoot,
714
+ ignoredOutOfWorkspaceEvents: result.ignoredOutOfWorkspaceEvents,
715
+ ...(result.lastSignal ? { lastSignal: result.lastSignal } : {}),
716
+ checks: result.checks,
717
+ diagnostics: result.diagnostics,
718
+ nextActions: result.nextActions,
719
+ });
720
+ } catch (error) {
721
+ proofs.push({
722
+ target,
723
+ ok: false,
724
+ nextActions: [`forge agent hooks status --target ${target} --json`],
725
+ lastSignal: {
726
+ error: error instanceof Error ? error.message : String(error),
727
+ },
728
+ });
729
+ }
730
+ }
731
+ return proofs;
732
+ }
733
+
734
+ function contextPacketFor(input: {
735
+ appRoot: string;
736
+ posture: StudioAttachResult["posture"];
737
+ commands: StudioSnapshotResult["commands"];
738
+ }): StudioSnapshotResult["contextPacket"] {
739
+ return {
740
+ source: "forgeos",
741
+ readFiles: [
742
+ "AGENTS.md",
743
+ "src/forge/_generated/agentContract.json",
744
+ "src/forge/_generated/appMap.md",
745
+ "src/forge/_generated/runtimeRules.md",
746
+ "src/forge/_generated/operationPlaybooks.md",
747
+ "src/forge/_generated/frontendGraph.json",
748
+ ].filter((file) => nodeFileSystem.exists(join(input.appRoot, file))),
749
+ commands: [
750
+ input.commands.changed,
751
+ input.commands.handoff,
752
+ input.commands.probePreview,
753
+ input.commands.doctor,
754
+ ...(input.commands.codexAppServer
755
+ ? [
756
+ input.commands.codexAppServer.inspect,
757
+ input.commands.codexAppServer.generateTypes,
758
+ input.commands.codexAppServer.generateJsonSchema,
759
+ input.commands.codexAppServer.probeHandshake,
760
+ ]
761
+ : []),
762
+ ...input.commands.checkHooks,
763
+ ],
764
+ ...(input.posture.diffPlan ? { diffPlan: input.posture.diffPlan } : {}),
765
+ };
766
+ }
767
+
768
+ function mergeCodexAppServerHandshakeProof(
769
+ proof: CodexAppServerProof,
770
+ handshake: CodexAppServerHandshakeResult | undefined,
771
+ ): CodexAppServerProof {
772
+ if (!handshake) {
773
+ return proof;
774
+ }
775
+ const handshakeReady = handshake.ok && handshake.initialized;
776
+ const checks = handshakeReady && !proof.available
777
+ ? proof.checks.map((check) => {
778
+ if (check.name === "codex-cli") {
779
+ return {
780
+ ...check,
781
+ ok: true,
782
+ status: "ok" as const,
783
+ message: "Codex app-server initialized over stdio; CLI version probe is no longer blocking.",
784
+ };
785
+ }
786
+ if (check.name === "codex-app-server") {
787
+ return {
788
+ ...check,
789
+ ok: true,
790
+ status: "ok" as const,
791
+ message: "Codex app-server handshake succeeded over stdio.",
792
+ };
793
+ }
794
+ if (check.name === "codex-app-server-schemas") {
795
+ return {
796
+ ...check,
797
+ ok: true,
798
+ status: "ok" as const,
799
+ message: "Codex app-server is available; generate version-matched schemas when implementing the streaming client.",
800
+ };
801
+ }
802
+ return check;
803
+ })
804
+ : proof.checks;
805
+ const nextActions = Array.from(new Set(handshakeReady
806
+ ? handshake.nextActions
807
+ : [...(proof.nextActions ?? []), ...handshake.nextActions]));
808
+ return {
809
+ ...proof,
810
+ ...(handshakeReady ? { state: "ready" as const, available: true, error: undefined } : {}),
811
+ handshake,
812
+ checks,
813
+ nextActions,
814
+ };
815
+ }
816
+
817
+ export async function runStudioAttachCommand(options: StudioAttachOptions): Promise<StudioAttachResult> {
818
+ const appRoot = resolve(options.workspaceRoot, options.path ?? ".").replace(/\\/g, "/");
819
+ const pkg = readPackageJson(appRoot);
820
+ const diagnostics: Diagnostic[] = [];
821
+ if (!nodeFileSystem.exists(join(appRoot, "package.json"))) {
822
+ diagnostics.push(createDiagnostic({
823
+ severity: "warning",
824
+ code: "FORGE_STUDIO_PACKAGE_JSON_MISSING",
825
+ message: `no package.json found in ${appRoot}; attaching as a filesystem workspace`,
826
+ file: "package.json",
827
+ }));
828
+ }
829
+
830
+ const targets = expandedTargets(options.targets);
831
+ const initialPreview = previewFor(options, diagnostics);
832
+ const app = {
833
+ name: packageName(pkg, appRoot),
834
+ path: appRoot,
835
+ ...(packageTemplate(pkg) ? { template: packageTemplate(pkg) } : {}),
836
+ };
837
+ const commands = {
838
+ startTargetApp: targetAppDevCommand(initialPreview.port ?? 5174),
839
+ startTargetAppCwd: appRoot,
840
+ openPreview: initialPreview.url,
841
+ probePreview: "forge dev --once --json",
842
+ installHooks: targets.map((target) => `forge agent onboard --target ${target} --json`),
843
+ checkHooks: targets.map((target) => `forge agent hooks status --target ${target} --json`),
844
+ openContext: "forge agent context --current --json",
845
+ ...(hasCodexTarget(targets) ? { codexAppServer: codexAppServerCommands() } : {}),
846
+ };
847
+ const preview = {
848
+ ...initialPreview,
849
+ status: await probeStudioPreview(initialPreview, {
850
+ dryRun: options.dryRun,
851
+ startCommand: commands.startTargetApp,
852
+ }),
853
+ };
854
+ const posture = await inspectAttachPosture(appRoot, options);
855
+ const manifestPath = ".forge/studio/attachment.json";
856
+ const filesPlanned = [manifestPath];
857
+ const filesWritten: string[] = [];
858
+
859
+ if (!options.dryRun) {
860
+ const absoluteManifest = join(appRoot, manifestPath);
861
+ nodeFileSystem.mkdirp(join(appRoot, ".forge", "studio"));
862
+ nodeFileSystem.writeText(absoluteManifest, renderManifest({ app, preview, posture, targets, commands }));
863
+ filesWritten.push(manifestPath);
864
+ }
865
+
866
+ const agentResults = [];
867
+ if (!options.dryRun) {
868
+ for (const target of targets) {
869
+ const result = await runAgentPrepare({
870
+ subcommand: "prepare",
871
+ workspaceRoot: appRoot,
872
+ json: options.json,
873
+ target: target as AgentAdapterTarget,
874
+ dryRun: false,
875
+ force: options.force,
876
+ preserveUserSections: true,
877
+ skills: true,
878
+ rules: true,
879
+ });
880
+ diagnostics.push(...result.diagnostics);
881
+ agentResults.push({
882
+ target,
883
+ ok: result.ok,
884
+ filesWritten: result.exportResult.filesWritten,
885
+ filesPlanned: result.exportResult.filesPlanned,
886
+ diagnostics: result.diagnostics,
887
+ });
888
+ }
889
+ }
890
+
891
+ const ok = diagnostics.every((diagnostic) => diagnostic.severity !== "error") &&
892
+ agentResults.every((result) => result.ok);
893
+ const nextActions = ok
894
+ ? [
895
+ commands.startTargetApp,
896
+ commands.probePreview,
897
+ ...commands.checkHooks,
898
+ ]
899
+ : [
900
+ "forge generate",
901
+ "forge agent doctor --target codex --json",
902
+ "forge dev --once --json",
903
+ ];
904
+
905
+ return normalizeForgeCliCommandsInValue(appRoot, {
906
+ schemaVersion: "0.1.0",
907
+ ok,
908
+ action: "attach",
909
+ app,
910
+ preview,
911
+ posture,
912
+ targets,
913
+ manifestPath,
914
+ filesWritten,
915
+ filesPlanned,
916
+ agentResults,
917
+ commands,
918
+ diagnostics,
919
+ nextActions,
920
+ exitCode: ok ? 0 : 1,
921
+ });
922
+ }
923
+
924
+ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Promise<StudioSnapshotResult> {
925
+ const appRoot = resolve(options.workspaceRoot, options.path ?? ".").replace(/\\/g, "/");
926
+ const manifest = readAttachmentManifest(appRoot);
927
+ const pkg = readPackageJson(appRoot);
928
+ const diagnostics: Diagnostic[] = [];
929
+ const manifestPreview = manifest?.preview;
930
+ const manifestTargets = Array.isArray(manifest?.targets)
931
+ ? manifest.targets.filter((target): target is string => typeof target === "string")
932
+ : [];
933
+ const targets = options.targets.length === 1 && options.targets[0] === "codex" && manifestTargets.length > 0
934
+ ? expandedTargets(manifestTargets)
935
+ : expandedTargets(options.targets);
936
+ const effectiveOptions: StudioAttachOptions = {
937
+ ...options,
938
+ targets,
939
+ previewUrl: options.previewUrl ?? (!options.previewPort && manifestPreview?.url ? manifestPreview.url : undefined),
940
+ previewPort: options.previewPort ?? (!options.previewUrl && manifestPreview?.port ? manifestPreview.port : undefined),
941
+ };
942
+ const initialPreview = previewFor(effectiveOptions, diagnostics);
943
+ const app = {
944
+ name: packageName(pkg, appRoot),
945
+ path: appRoot,
946
+ ...(packageTemplate(pkg) ? { template: packageTemplate(pkg) } : {}),
947
+ };
948
+ const baseCommands = {
949
+ startTargetApp: targetAppDevCommand(initialPreview.port ?? 5174),
950
+ startTargetAppCwd: appRoot,
951
+ openPreview: initialPreview.url,
952
+ probePreview: "forge dev --once --json",
953
+ installHooks: targets.map((target) => `forge agent onboard --target ${target} --json`),
954
+ checkHooks: targets.map((target) => `forge agent hooks status --target ${target} --json`),
955
+ openContext: "forge agent context --current --json",
956
+ ...(hasCodexTarget(targets) ? { codexAppServer: codexAppServerCommands() } : {}),
957
+ };
958
+ const preview = {
959
+ ...initialPreview,
960
+ status: await probeStudioPreview(initialPreview, {
961
+ dryRun: effectiveOptions.dryRun,
962
+ startCommand: baseCommands.startTargetApp,
963
+ }),
964
+ };
965
+ const posture = await inspectReadOnlyPosture(appRoot);
966
+ const changed = runChangedCommand(appRoot);
967
+ const attachPreviewPort = preview.port ?? localPreviewPort(preview.url) ?? 5174;
968
+ const commands = {
969
+ ...baseCommands,
970
+ attach: attachCommandFor(targets, attachPreviewPort),
971
+ changed: "forge changed --json",
972
+ handoff: "forge handoff --json",
973
+ watch: `forge studio watch . --preview-port ${attachPreviewPort} ${targets.map((target) => `--target ${target}`).join(" ")}${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
974
+ bridge: `forge studio bridge . --preview-port ${attachPreviewPort} ${targets.map((target) => `--target ${target}`).join(" ")} --studio-url http://127.0.0.1:3765${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
975
+ doctor: `forge studio doctor . --preview-port ${attachPreviewPort} ${targets.map((target) => `--target ${target}`).join(" ")}${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
976
+ open: `forge studio open . --preview-port ${attachPreviewPort} ${targets.map((target) => `--target ${target}`).join(" ")}${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
977
+ };
978
+ const hookProofs = await collectHookProofs(appRoot, targets);
979
+ const codexAppServerBase = hasCodexTarget(targets)
980
+ ? inspectCodexAppServer({ workspaceRoot: appRoot, relevant: true })
981
+ : undefined;
982
+ const codexAppServerHandshake = codexAppServerBase && options.probeAppServer
983
+ ? await probeCodexAppServerHandshake({
984
+ workspaceRoot: appRoot,
985
+ dryRun: options.dryRun,
986
+ available: codexAppServerBase.available ? true : undefined,
987
+ disabled: codexAppServerBase.state === "disabled",
988
+ })
989
+ : undefined;
990
+ const codexAppServer = codexAppServerBase
991
+ ? mergeCodexAppServerHandshakeProof(codexAppServerBase, codexAppServerHandshake)
992
+ : undefined;
993
+ const delta = await runDeltaStatus(appRoot);
994
+ const contextPacket = contextPacketFor({ appRoot, posture, commands });
995
+ const handoff = studioHandoffFor({
996
+ previewUrl: preview.url,
997
+ posture,
998
+ changed: changed.data,
999
+ delta,
1000
+ commands,
1001
+ });
1002
+ const gitState = (changed.data as { git?: { available?: boolean } }).git;
1003
+ const changedReadable = changed.ok || gitState?.available === false;
1004
+ const ok = posture.state !== "needs-attention" && changedReadable &&
1005
+ diagnostics.every((diagnostic) => diagnostic.severity !== "error");
1006
+ const nextActions = [
1007
+ commands.startTargetApp,
1008
+ commands.probePreview,
1009
+ commands.changed,
1010
+ commands.doctor,
1011
+ ...(codexAppServer && !codexAppServer.handshake && commands.codexAppServer?.probeHandshake
1012
+ ? [commands.codexAppServer.probeHandshake]
1013
+ : []),
1014
+ ...commands.checkHooks,
1015
+ ];
1016
+ return normalizeForgeCliCommandsInValue(appRoot, {
1017
+ schemaVersion: "0.1.0",
1018
+ ok,
1019
+ action: "snapshot",
1020
+ app,
1021
+ preview,
1022
+ posture,
1023
+ targets,
1024
+ changed: changed.data,
1025
+ commands,
1026
+ contextPacket,
1027
+ handoff,
1028
+ proofs: {
1029
+ preview: preview.status,
1030
+ generated: posture.generated,
1031
+ hooks: hookProofs,
1032
+ ...(codexAppServer ? { codexAppServer } : {}),
1033
+ delta,
1034
+ },
1035
+ diagnostics,
1036
+ nextActions,
1037
+ exitCode: ok ? 0 : 1,
1038
+ });
1039
+ }
1040
+
1041
+ function studioHandoffFor(input: {
1042
+ previewUrl: string;
1043
+ posture: StudioSnapshotResult["posture"];
1044
+ changed: Record<string, unknown>;
1045
+ delta: unknown;
1046
+ commands: StudioSnapshotResult["commands"];
1047
+ }): StudioSnapshotResult["handoff"] {
1048
+ const changedSummary = input.changed.summary as { changedFiles?: unknown } | undefined;
1049
+ const deltaRecord = input.delta && typeof input.delta === "object" && !Array.isArray(input.delta)
1050
+ ? input.delta as { workSession?: { id?: unknown; title?: unknown; status?: unknown; confidence?: unknown }; details?: { health?: { status?: unknown } } }
1051
+ : undefined;
1052
+ const workSession = deltaRecord?.workSession;
1053
+ return {
1054
+ previewUrl: input.previewUrl,
1055
+ ...(workSession
1056
+ ? {
1057
+ currentSession: {
1058
+ ...(typeof workSession.id === "string" ? { id: workSession.id } : {}),
1059
+ ...(typeof workSession.title === "string" ? { title: workSession.title } : {}),
1060
+ ...(typeof workSession.status === "string" ? { status: workSession.status } : {}),
1061
+ ...(typeof workSession.confidence === "number" ? { confidence: workSession.confidence } : {}),
1062
+ },
1063
+ }
1064
+ : {}),
1065
+ ...(typeof changedSummary?.changedFiles === "number" ? { changedFiles: changedSummary.changedFiles } : {}),
1066
+ generatedState: input.posture.generated?.state,
1067
+ deltaHealth: typeof deltaRecord?.details?.health?.status === "string" ? deltaRecord.details.health.status : undefined,
1068
+ agentContextCommand: "forge agent context --handoff --json",
1069
+ recommendedCommands: [
1070
+ input.commands.handoff,
1071
+ "forge agent context --handoff --json",
1072
+ input.commands.changed,
1073
+ input.commands.doctor,
1074
+ ],
1075
+ };
1076
+ }
1077
+
1078
+ export async function runStudioWatchCommand(options: StudioAttachOptions): Promise<StudioWatchResult> {
1079
+ const snapshot = await runStudioSnapshotCommand(options);
1080
+ const intervalMs = Math.max(1000, Math.floor(options.intervalMs ?? 5000));
1081
+ const single = options.once || options.dryRun;
1082
+ return normalizeForgeCliCommandsInValue(snapshot.app.path, {
1083
+ schemaVersion: "0.1.0",
1084
+ ok: snapshot.ok,
1085
+ action: "watch",
1086
+ stream: {
1087
+ mode: single ? "once" : "watch",
1088
+ event: "studio.snapshot",
1089
+ note: single
1090
+ ? "This command emitted one Studio-compatible snapshot event."
1091
+ : "This command emits Studio-compatible snapshot events until stopped.",
1092
+ followCommand: "forge dev --watch --json",
1093
+ intervalMs,
1094
+ dryRun: options.dryRun,
1095
+ emittedAt: new Date().toISOString(),
1096
+ },
1097
+ snapshot,
1098
+ exitCode: snapshot.exitCode,
1099
+ });
1100
+ }
1101
+
1102
+ export async function runStudioWatchLoop(
1103
+ options: StudioAttachOptions,
1104
+ onResult: (result: StudioWatchResult) => void,
1105
+ ): Promise<0 | 1> {
1106
+ do {
1107
+ const result = await runStudioWatchCommand(options);
1108
+ onResult(result);
1109
+ if (options.once || options.dryRun) {
1110
+ return result.exitCode;
1111
+ }
1112
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1000, Math.floor(options.intervalMs ?? 5000))));
1113
+ } while (true);
1114
+ }
1115
+
1116
+ async function postStudioSnapshot(input: {
1117
+ studioUrl: string;
1118
+ workspaceId?: string;
1119
+ provider: string;
1120
+ snapshot: StudioSnapshotResult;
1121
+ bridge: {
1122
+ mode: "once" | "watch";
1123
+ intervalMs: number;
1124
+ postedAt: string;
1125
+ };
1126
+ tenantId?: string;
1127
+ userId?: string;
1128
+ role?: string;
1129
+ }): Promise<{ ok: boolean; status: number; body: unknown; error?: string }> {
1130
+ const endpoint = `${input.studioUrl}/commands/ingestStudioSnapshot`;
1131
+ try {
1132
+ const response = await fetch(endpoint, {
1133
+ method: "POST",
1134
+ headers: {
1135
+ "content-type": "application/json",
1136
+ "x-forge-tenant-id": input.tenantId ?? process.env.FORGE_TENANT_ID ?? STUDIO_LOCAL_TENANT_ID,
1137
+ "x-forge-user-id": input.userId ?? process.env.FORGE_USER_ID ?? STUDIO_LOCAL_USER_ID,
1138
+ "x-forge-role": input.role ?? process.env.FORGE_ROLE ?? STUDIO_LOCAL_ROLE,
1139
+ },
1140
+ body: JSON.stringify({
1141
+ args: {
1142
+ ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
1143
+ provider: input.provider,
1144
+ snapshot: input.snapshot,
1145
+ bridge: {
1146
+ ...input.bridge,
1147
+ status: "received",
1148
+ },
1149
+ },
1150
+ }),
1151
+ });
1152
+ const body = await response.json().catch(() => ({}));
1153
+ return {
1154
+ ok: response.ok && (body as { ok?: boolean }).ok !== false,
1155
+ status: response.status,
1156
+ body,
1157
+ };
1158
+ } catch (error) {
1159
+ return {
1160
+ ok: false,
1161
+ status: 0,
1162
+ body: {},
1163
+ error: error instanceof Error ? error.message : String(error),
1164
+ };
1165
+ }
1166
+ }
1167
+
1168
+ export async function runStudioBridgeCommand(options: StudioAttachOptions): Promise<StudioBridgeResult> {
1169
+ const targets = expandedTargets(options.targets.length > 0 ? options.targets : ["codex"]);
1170
+ const target = targets[0] ?? "codex";
1171
+ const provider = providerName(target);
1172
+ const studioUrl = normalizeStudioUrl(options.studioUrl);
1173
+ const endpoint = `${studioUrl}/commands/ingestStudioSnapshot`;
1174
+ const intervalMs = Math.max(1000, Math.floor(options.intervalMs ?? 5000));
1175
+ const diagnostics: Diagnostic[] = [];
1176
+ const snapshot = await runStudioSnapshotCommand({
1177
+ ...options,
1178
+ targets,
1179
+ dryRun: options.dryRun,
1180
+ });
1181
+
1182
+ let posted = false;
1183
+ let responseBody: unknown;
1184
+ if (options.dryRun) {
1185
+ diagnostics.push(createDiagnostic({
1186
+ severity: "info",
1187
+ code: "FORGE_STUDIO_BRIDGE_DRY_RUN",
1188
+ message: `dry-run collected a Studio snapshot but did not POST it to ${endpoint}`,
1189
+ suggestedCommands: [`forge studio bridge . --studio-url ${studioUrl} --target ${target} --preview-port ${snapshot.preview.port ?? 5174} --json`],
1190
+ }));
1191
+ } else {
1192
+ const response = await postStudioSnapshot({
1193
+ studioUrl,
1194
+ workspaceId: options.workspaceId,
1195
+ provider,
1196
+ snapshot,
1197
+ bridge: {
1198
+ mode: options.once || options.dryRun ? "once" : "watch",
1199
+ intervalMs,
1200
+ postedAt: new Date().toISOString(),
1201
+ },
1202
+ tenantId: options.tenantId,
1203
+ userId: options.userId,
1204
+ role: options.role,
1205
+ });
1206
+ posted = response.ok;
1207
+ responseBody = response.body;
1208
+ if (!response.ok) {
1209
+ diagnostics.push(createDiagnostic({
1210
+ severity: "error",
1211
+ code: response.status === 0 ? "FORGE_STUDIO_BRIDGE_UNREACHABLE" : "FORGE_STUDIO_BRIDGE_INGEST_FAILED",
1212
+ message: response.status === 0
1213
+ ? `cannot reach Forge Studio runtime at ${studioUrl}: ${response.error ?? "network request failed"}`
1214
+ : `Forge Studio rejected snapshot ingest with HTTP ${response.status}`,
1215
+ fixHint: `Start Forge Studio, then run forge studio bridge . --studio-url ${studioUrl} --target ${target} --preview-port ${snapshot.preview.port ?? 5174} --json`,
1216
+ suggestedCommands: [
1217
+ "npm run dev",
1218
+ `forge studio doctor . --preview-port ${snapshot.preview.port ?? 5174} --target ${target} --json`,
1219
+ ],
1220
+ }));
1221
+ }
1222
+ }
1223
+
1224
+ const ok = diagnostics.every((diagnostic) => diagnostic.severity !== "error");
1225
+ const nextActions = ok
1226
+ ? [
1227
+ `forge studio bridge . --studio-url ${studioUrl} --target ${target} --preview-port ${snapshot.preview.port ?? 5174} --json`,
1228
+ snapshot.commands.doctor,
1229
+ snapshot.commands.changed,
1230
+ ]
1231
+ : [
1232
+ "Start Forge Studio with npm run dev",
1233
+ `forge studio bridge . --studio-url ${studioUrl} --target ${target} --preview-port ${snapshot.preview.port ?? 5174} --json`,
1234
+ snapshot.commands.doctor,
1235
+ ];
1236
+
1237
+ return normalizeForgeCliCommandsInValue(snapshot.app.path, {
1238
+ schemaVersion: "0.1.0",
1239
+ ok,
1240
+ action: "bridge",
1241
+ mode: options.once || options.dryRun ? "once" : "watch",
1242
+ studioUrl,
1243
+ endpoint,
1244
+ intervalMs,
1245
+ provider,
1246
+ target,
1247
+ posted,
1248
+ dryRun: options.dryRun,
1249
+ snapshot,
1250
+ ...(responseBody !== undefined ? { response: responseBody } : {}),
1251
+ diagnostics,
1252
+ nextActions,
1253
+ exitCode: ok ? 0 : 1,
1254
+ });
1255
+ }
1256
+
1257
+ export async function runStudioBridgeLoop(
1258
+ options: StudioAttachOptions,
1259
+ onResult: (result: StudioBridgeResult) => void,
1260
+ ): Promise<0 | 1> {
1261
+ do {
1262
+ const result = await runStudioBridgeCommand(options);
1263
+ const readyNonce = process.env.FORGE_STUDIO_BRIDGE_READY_NONCE;
1264
+ if (readyNonce && /^[0-9a-f-]{36}$/i.test(readyNonce)) {
1265
+ const appRoot = resolve(options.workspaceRoot, options.path ?? ".");
1266
+ const readyPath = join(appRoot, ".forge", "studio", `bridge-ready-${readyNonce}.json`);
1267
+ nodeFileSystem.mkdirp(join(appRoot, ".forge", "studio"));
1268
+ nodeFileSystem.writeText(readyPath, `${JSON.stringify({
1269
+ pid: process.pid,
1270
+ ok: result.ok && result.posted,
1271
+ diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
1272
+ })}\n`);
1273
+ delete process.env.FORGE_STUDIO_BRIDGE_READY_NONCE;
1274
+ }
1275
+ onResult(result);
1276
+ if (options.once || options.dryRun) {
1277
+ return result.exitCode;
1278
+ }
1279
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1000, Math.floor(options.intervalMs ?? 5000))));
1280
+ } while (true);
1281
+ }
1282
+
1283
+ export async function runStudioCodexServerCommand(options: StudioAttachOptions): Promise<StudioCodexServerResult> {
1284
+ const appRoot = resolve(options.workspaceRoot, options.path ?? ".").replace(/\\/g, "/");
1285
+ const pkg = readPackageJson(appRoot);
1286
+ const app = {
1287
+ name: packageName(pkg, appRoot),
1288
+ path: appRoot,
1289
+ ...(packageTemplate(pkg) ? { template: packageTemplate(pkg) } : {}),
1290
+ };
1291
+ const proof = inspectCodexAppServer({
1292
+ workspaceRoot: appRoot,
1293
+ relevant: true,
1294
+ forceRefresh: true,
1295
+ });
1296
+ const schemaGeneration = options.writeSchemas
1297
+ ? generateCodexAppServerSchemas({
1298
+ workspaceRoot: appRoot,
1299
+ dryRun: options.dryRun,
1300
+ })
1301
+ : generateCodexAppServerSchemas({
1302
+ workspaceRoot: appRoot,
1303
+ dryRun: true,
1304
+ });
1305
+ const handshake = options.probeAppServer
1306
+ ? await probeCodexAppServerHandshake({
1307
+ workspaceRoot: appRoot,
1308
+ dryRun: options.dryRun,
1309
+ available: proof.available ? true : undefined,
1310
+ disabled: proof.state === "disabled",
1311
+ })
1312
+ : skippedCodexAppServerHandshake({
1313
+ reason: "not-requested",
1314
+ dryRun: options.dryRun,
1315
+ });
1316
+ const mergedProof = mergeCodexAppServerHandshakeProof(proof, handshake);
1317
+ const ok = (mergedProof.available || mergedProof.state === "disabled") &&
1318
+ (!options.writeSchemas || schemaGeneration.ok) &&
1319
+ (!options.probeAppServer || handshake.ok);
1320
+ const primaryNextActions = options.probeAppServer && mergedProof.available && handshake.ok
1321
+ ? options.writeSchemas && schemaGeneration.ok
1322
+ ? []
1323
+ : schemaGeneration.nextActions
1324
+ : options.writeSchemas
1325
+ ? schemaGeneration.nextActions
1326
+ : proof.nextActions;
1327
+ const nextActions = Array.from(new Set([
1328
+ ...primaryNextActions,
1329
+ ...(!options.probeAppServer ? handshake.nextActions : []),
1330
+ ...(options.probeAppServer && !handshake.ok ? handshake.nextActions : []),
1331
+ ]));
1332
+ return normalizeForgeCliCommandsInValue(appRoot, {
1333
+ schemaVersion: "0.1.0",
1334
+ ok,
1335
+ action: "codex-server",
1336
+ app,
1337
+ proof: mergedProof,
1338
+ schemaGeneration,
1339
+ handshake,
1340
+ commands: codexAppServerCommands(),
1341
+ diagnostics: [],
1342
+ nextActions,
1343
+ exitCode: ok ? 0 : 1,
1344
+ });
1345
+ }
1346
+
1347
+ function installPlanFor(pkg: Record<string, unknown>): { command: string; args: string[]; label: string } {
1348
+ const packageManager = typeof pkg.packageManager === "string" ? pkg.packageManager : "";
1349
+ if (packageManager.startsWith("bun")) {
1350
+ return { command: "bun", args: ["install"], label: "bun install" };
1351
+ }
1352
+ if (packageManager.startsWith("pnpm")) {
1353
+ return { command: "pnpm", args: ["install"], label: "pnpm install" };
1354
+ }
1355
+ if (packageManager.startsWith("yarn")) {
1356
+ return { command: "yarn", args: ["install"], label: "yarn install" };
1357
+ }
1358
+ return { command: "npm", args: ["install"], label: "npm install" };
1359
+ }
1360
+
1361
+ function dependencyStatusFor(appRoot: string, pkg: Record<string, unknown>): {
1362
+ required: boolean;
1363
+ installed: boolean;
1364
+ command?: string;
1365
+ cwd: string;
1366
+ } {
1367
+ const hasRootPackage = nodeFileSystem.exists(join(appRoot, "package.json"));
1368
+ const hasWebPackage = nodeFileSystem.exists(join(appRoot, "web", "package.json"));
1369
+ if (!hasRootPackage && !hasWebPackage) {
1370
+ return {
1371
+ required: false,
1372
+ installed: true,
1373
+ cwd: appRoot,
1374
+ };
1375
+ }
1376
+ const hasRootNodeModules = nodeFileSystem.exists(join(appRoot, "node_modules"));
1377
+ const hasWebNodeModules = nodeFileSystem.exists(join(appRoot, "web", "node_modules"));
1378
+ const plan = installPlanFor(pkg);
1379
+ return {
1380
+ required: true,
1381
+ installed: hasRootNodeModules || hasWebNodeModules,
1382
+ command: plan.label,
1383
+ cwd: appRoot,
1384
+ };
1385
+ }
1386
+
1387
+ function runDependencyInstall(appRoot: string, pkg: Record<string, unknown>): {
1388
+ ok: boolean;
1389
+ exitCode: number;
1390
+ command: string;
1391
+ } {
1392
+ const plan = installPlanFor(pkg);
1393
+ const result = spawnSync(plan.command, plan.args, {
1394
+ cwd: appRoot,
1395
+ encoding: "utf8",
1396
+ stdio: "pipe",
1397
+ });
1398
+ return {
1399
+ ok: result.status === 0,
1400
+ exitCode: result.status ?? 1,
1401
+ command: plan.label,
1402
+ };
1403
+ }
1404
+
1405
+ function previewStatePath(appRoot: string): string {
1406
+ return join(appRoot, ".forge", "studio", "preview.json");
1407
+ }
1408
+
1409
+ function readPreviewState(appRoot: string): { pid?: number; previewPort?: number; listenerPid?: number } | null {
1410
+ const path = previewStatePath(appRoot);
1411
+ if (!nodeFileSystem.exists(path)) return null;
1412
+ try {
1413
+ return JSON.parse(nodeFileSystem.readText(path) ?? "{}") as { pid?: number; previewPort?: number; listenerPid?: number };
1414
+ } catch {
1415
+ return null;
1416
+ }
1417
+ }
1418
+
1419
+ function writePreviewState(input: {
1420
+ appRoot: string;
1421
+ pid?: number;
1422
+ previewPort: number;
1423
+ command: string;
1424
+ listenerPid?: number;
1425
+ }): void {
1426
+ if (!input.pid || !processIsRunning(input.pid)) {
1427
+ return;
1428
+ }
1429
+ nodeFileSystem.mkdirp(join(input.appRoot, ".forge", "studio"));
1430
+ nodeFileSystem.writeText(previewStatePath(input.appRoot), `${JSON.stringify({
1431
+ pid: input.pid,
1432
+ command: input.command,
1433
+ previewPort: input.previewPort,
1434
+ ...(input.listenerPid ? { listenerPid: input.listenerPid } : {}),
1435
+ runtimePort: STUDIO_TARGET_RUNTIME_PORT,
1436
+ startedAt: new Date().toISOString(),
1437
+ readyAt: new Date().toISOString(),
1438
+ }, null, 2)}\n`);
1439
+ }
1440
+
1441
+ async function spawnForgeDev(appRoot: string, previewPort: number): Promise<{ pid?: number; command: string; error?: string }> {
1442
+ // This path is reached only after the preview probe failed. A live PID in an
1443
+ // old state file does not prove that it is still serving this port.
1444
+ nodeFileSystem.remove(previewStatePath(appRoot));
1445
+ const cliEntry = process.argv[1] ? resolve(process.argv[1]) : undefined;
1446
+ const command = cliEntry ? process.execPath : "forge";
1447
+ const args = cliEntry
1448
+ ? [cliEntry, "dev", "--port", String(STUDIO_TARGET_RUNTIME_PORT), "--web-port", String(previewPort)]
1449
+ : ["dev", "--port", String(STUDIO_TARGET_RUNTIME_PORT), "--web-port", String(previewPort)];
1450
+ const label = targetAppDevCommand(previewPort);
1451
+ try {
1452
+ const child = spawn(command, args, {
1453
+ cwd: appRoot,
1454
+ detached: true,
1455
+ stdio: "ignore",
1456
+ windowsHide: true,
1457
+ shell: !cliEntry && process.platform === "win32",
1458
+ });
1459
+ const spawnError = await new Promise<Error | undefined>((resolve) => {
1460
+ child.once("error", (error) => resolve(error));
1461
+ child.once("spawn", () => resolve(undefined));
1462
+ });
1463
+ if (spawnError) {
1464
+ return { command: label, error: spawnError.message };
1465
+ }
1466
+ child.unref();
1467
+ return { pid: child.pid, command: label };
1468
+ } catch (error) {
1469
+ return {
1470
+ command: label,
1471
+ error: error instanceof Error ? error.message : String(error),
1472
+ };
1473
+ }
1474
+ }
1475
+
1476
+ function processIsRunning(pid: number | undefined): boolean {
1477
+ if (!pid || !Number.isInteger(pid) || pid <= 0) {
1478
+ return false;
1479
+ }
1480
+ try {
1481
+ process.kill(pid, 0);
1482
+ return true;
1483
+ } catch {
1484
+ return false;
1485
+ }
1486
+ }
1487
+
1488
+ export function parseWindowsNetstatListener(output: string, port: number): { pid: number; evidence: string } | null {
1489
+ for (const line of output.split(/\r?\n/)) {
1490
+ const match = /^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
1491
+ if (match && Number(match[1]) === port && Number(match[2]) > 0) {
1492
+ return { pid: Number(match[2]), evidence: "netstat reported a listener on the preview port" };
1493
+ }
1494
+ }
1495
+ return null;
1496
+ }
1497
+
1498
+ function detectListeningProcess(port: number): { pid?: number; command?: string; evidence: string } | null {
1499
+ if (process.platform === "win32") {
1500
+ const netstat = spawnSync("netstat", ["-ano", "-p", "TCP"], {
1501
+ encoding: "utf8",
1502
+ stdio: ["ignore", "pipe", "ignore"],
1503
+ windowsHide: true,
1504
+ });
1505
+ return netstat.status === 0 ? parseWindowsNetstatListener(netstat.stdout, port) : null;
1506
+ }
1507
+ const lsof = spawnSync("lsof", [`-iTCP:${port}`, "-sTCP:LISTEN", "-n", "-P", "-Fp", "-Fc"], {
1508
+ encoding: "utf8",
1509
+ stdio: ["ignore", "pipe", "ignore"],
1510
+ windowsHide: true,
1511
+ });
1512
+ if (lsof.status === 0 && lsof.stdout) {
1513
+ const pid = /^p(\d+)$/m.exec(lsof.stdout)?.[1];
1514
+ const command = /^c(.+)$/m.exec(lsof.stdout)?.[1];
1515
+ return {
1516
+ ...(pid ? { pid: Number(pid) } : {}),
1517
+ ...(command ? { command } : {}),
1518
+ evidence: "lsof reported a listener on the preview port",
1519
+ };
1520
+ }
1521
+ const ss = spawnSync("ss", ["-ltnp", `sport = :${port}`], {
1522
+ encoding: "utf8",
1523
+ stdio: ["ignore", "pipe", "ignore"],
1524
+ windowsHide: true,
1525
+ });
1526
+ if (ss.status === 0 && ss.stdout) {
1527
+ const pid = /pid=(\d+)/.exec(ss.stdout)?.[1];
1528
+ const command = /users:\(\("([^"]+)"/.exec(ss.stdout)?.[1];
1529
+ if (pid || command) {
1530
+ return {
1531
+ ...(pid ? { pid: Number(pid) } : {}),
1532
+ ...(command ? { command } : {}),
1533
+ evidence: "ss reported a listener on the preview port",
1534
+ };
1535
+ }
1536
+ }
1537
+ return null;
1538
+ }
1539
+
1540
+ function bridgeStatePath(appRoot: string): string {
1541
+ return join(appRoot, ".forge", "studio", "bridge.json");
1542
+ }
1543
+
1544
+ function readBridgeState(appRoot: string): { pid?: number; command?: string } | null {
1545
+ const path = bridgeStatePath(appRoot);
1546
+ if (!nodeFileSystem.exists(path)) {
1547
+ return null;
1548
+ }
1549
+ try {
1550
+ const parsed = JSON.parse(nodeFileSystem.readText(path) ?? "{}") as { pid?: number; command?: string };
1551
+ return parsed;
1552
+ } catch {
1553
+ return null;
1554
+ }
1555
+ }
1556
+
1557
+ export async function spawnForgeStudioBridge(input: {
1558
+ appRoot: string;
1559
+ previewPort: number;
1560
+ targets: string[];
1561
+ studioUrl: string;
1562
+ intervalMs: number;
1563
+ probeAppServer?: boolean;
1564
+ cliEntry?: string;
1565
+ }): Promise<{ pid?: number; command: string; alreadyRunning: boolean; error?: string }> {
1566
+ const existing = readBridgeState(input.appRoot);
1567
+ if (existing?.pid && processIsRunning(existing.pid)) {
1568
+ return {
1569
+ pid: existing.pid,
1570
+ command: existing.command ?? "forge studio bridge",
1571
+ alreadyRunning: true,
1572
+ };
1573
+ }
1574
+ if (existing) nodeFileSystem.remove(bridgeStatePath(input.appRoot));
1575
+
1576
+ const cliEntry = input.cliEntry
1577
+ ? resolve(input.cliEntry)
1578
+ : process.argv[1] ? resolve(process.argv[1]) : undefined;
1579
+ const command = cliEntry ? process.execPath : "forge";
1580
+ const targetArgs = input.targets.flatMap((target) => ["--target", target]);
1581
+ const args = [
1582
+ ...(cliEntry ? [cliEntry] : []),
1583
+ "studio",
1584
+ "bridge",
1585
+ input.appRoot,
1586
+ "--preview-port",
1587
+ String(input.previewPort),
1588
+ ...targetArgs,
1589
+ "--studio-url",
1590
+ input.studioUrl,
1591
+ "--interval-ms",
1592
+ String(input.intervalMs),
1593
+ "--json",
1594
+ ...(input.probeAppServer ? ["--probe-codex-server"] : []),
1595
+ ];
1596
+ const label = `forge ${args.filter((arg) => arg !== cliEntry).map((arg) =>
1597
+ /\s/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg
1598
+ ).join(" ")}`;
1599
+ const readyNonce = randomUUID();
1600
+ const readyPath = join(input.appRoot, ".forge", "studio", `bridge-ready-${readyNonce}.json`);
1601
+ try {
1602
+ nodeFileSystem.mkdirp(join(input.appRoot, ".forge", "studio"));
1603
+ const child = spawn(command, args, {
1604
+ cwd: input.appRoot,
1605
+ detached: true,
1606
+ stdio: "ignore",
1607
+ windowsHide: true,
1608
+ shell: !cliEntry && process.platform === "win32",
1609
+ env: { ...process.env, FORGE_STUDIO_BRIDGE_READY_NONCE: readyNonce },
1610
+ });
1611
+ const spawnError = await new Promise<Error | undefined>((resolve) => {
1612
+ child.once("error", (error) => resolve(error));
1613
+ child.once("spawn", () => resolve(undefined));
1614
+ });
1615
+ if (spawnError) {
1616
+ return { command: label, alreadyRunning: false, error: spawnError.message };
1617
+ }
1618
+ child.unref();
1619
+ type BridgeReadySignal = { pid?: number; ok?: boolean; diagnostics?: string[] };
1620
+ let ready: BridgeReadySignal | null = null;
1621
+ for (let attempt = 0; attempt < 150; attempt++) {
1622
+ if (nodeFileSystem.exists(readyPath)) {
1623
+ try {
1624
+ ready = JSON.parse(nodeFileSystem.readText(readyPath) ?? "{}") as BridgeReadySignal;
1625
+ break;
1626
+ } catch {
1627
+ // The child may still be writing the marker. Keep waiting for a
1628
+ // complete readiness signal while it is alive.
1629
+ }
1630
+ }
1631
+ if (!processIsRunning(child.pid)) break;
1632
+ await sleep(100);
1633
+ }
1634
+ nodeFileSystem.remove(readyPath);
1635
+ if (!ready?.ok || ready.pid !== child.pid || !processIsRunning(child.pid)) {
1636
+ if (processIsRunning(child.pid)) {
1637
+ try { child.kill(); } catch { /* The child may have just exited. */ }
1638
+ }
1639
+ return {
1640
+ command: label,
1641
+ alreadyRunning: false,
1642
+ error: ready?.diagnostics?.join("; ") ||
1643
+ (ready ? "bridge readiness signal was invalid" : "bridge exited or timed out before its first successful snapshot"),
1644
+ };
1645
+ }
1646
+ const state = {
1647
+ pid: child.pid,
1648
+ command: label,
1649
+ studioUrl: input.studioUrl,
1650
+ previewPort: input.previewPort,
1651
+ intervalMs: input.intervalMs,
1652
+ targets: input.targets,
1653
+ startedAt: new Date().toISOString(),
1654
+ readyAt: new Date().toISOString(),
1655
+ };
1656
+ nodeFileSystem.writeText(bridgeStatePath(input.appRoot), `${JSON.stringify(state, null, 2)}\n`);
1657
+ return { pid: child.pid, command: label, alreadyRunning: false };
1658
+ } catch (error) {
1659
+ return {
1660
+ command: label,
1661
+ alreadyRunning: false,
1662
+ error: error instanceof Error ? error.message : String(error),
1663
+ };
1664
+ }
1665
+ }
1666
+
1667
+ async function waitForPreviewAfterStart(
1668
+ preview: Omit<StudioAttachResult["preview"], "status">,
1669
+ startCommand: string,
1670
+ pid: number | undefined,
1671
+ ): Promise<StudioAttachResult["preview"]["status"]> {
1672
+ let status = previewStatus("not-running", `preview is not reachable at ${preview.url}`, [startCommand, "forge dev --once --json"]);
1673
+ for (let attempt = 0; attempt < 20; attempt++) {
1674
+ if (!processIsRunning(pid)) {
1675
+ return previewStatus("not-running", `preview process exited before ${preview.url} became reachable`, [startCommand, "forge dev --once --json"]);
1676
+ }
1677
+ await sleep(attempt === 0 ? 500 : 1000);
1678
+ status = await probeStudioPreview(preview, {
1679
+ dryRun: false,
1680
+ startCommand,
1681
+ timeoutMs: 750,
1682
+ });
1683
+ if (status.state === "reachable" && processIsRunning(pid)) {
1684
+ return status;
1685
+ }
1686
+ }
1687
+ return status;
1688
+ }
1689
+
1690
+ export async function runStudioDoctorCommand(options: StudioAttachOptions): Promise<StudioDoctorResult> {
1691
+ const snapshot = await runStudioSnapshotCommand(options);
1692
+ const hookProofs = snapshot.proofs.hooks;
1693
+ const delta = snapshot.proofs.delta as { recording?: boolean; diagnostics?: Diagnostic[]; exitCode?: number } | undefined;
1694
+ const hookReady = hookProofs.some((proof) => proof.ok && (proof.nativeSignals ?? proof.usefulSignals ?? 0) > 0);
1695
+ const hookMemoryUnavailable = hookProofs.some((proof) =>
1696
+ proof.deltaWritable === false ||
1697
+ proof.approvalStatus === "memory-unavailable" ||
1698
+ (proof.diagnostics ?? []).some((diag) =>
1699
+ ["FORGE_AGENT_MEMORY_UNAVAILABLE", "FORGE_DELTA_STORE_UNAVAILABLE", "FORGE_DELTA_BUSY"].includes(diag.code)
1700
+ )
1701
+ );
1702
+ const hookWaitingForApproval = hookProofs.some((proof) =>
1703
+ proof.approvalRequired === true || proof.approvalStatus === "waiting-for-user-trust"
1704
+ );
1705
+ const hookInstalled = hookProofs.some((proof) => proof.installed === true);
1706
+ const codexAppServer = snapshot.proofs.codexAppServer;
1707
+ const codexHandshake = codexAppServer?.handshake;
1708
+ const codexHandshakeFailed = codexHandshake?.attempted === true && codexHandshake.ok !== true;
1709
+ const checks: StudioDoctorResult["checks"] = [
1710
+ {
1711
+ name: "preview",
1712
+ ok: snapshot.preview.status.state === "reachable",
1713
+ status: snapshot.preview.status.state === "reachable" ? "ok" : "warning",
1714
+ message: snapshot.preview.status.reason,
1715
+ suggestedCommands: snapshot.preview.status.suggestedCommands,
1716
+ },
1717
+ {
1718
+ name: "generated",
1719
+ ok: snapshot.posture.generated?.ok === true,
1720
+ status: snapshot.posture.generated?.ok === true ? "ok" : "failed",
1721
+ message: snapshot.posture.generated?.message ?? snapshot.posture.reason,
1722
+ suggestedCommands: ["forge generate --check --json", "forge generate"],
1723
+ },
1724
+ {
1725
+ name: "hooks",
1726
+ ok: hookReady,
1727
+ status: hookReady
1728
+ ? "ok"
1729
+ : hookMemoryUnavailable || hookWaitingForApproval || hookInstalled
1730
+ ? "warning"
1731
+ : "failed",
1732
+ message: hookReady
1733
+ ? "hooks are installed and trusted native agent signals are visible"
1734
+ : hookMemoryUnavailable
1735
+ ? "hooks are installed, but Agent Memory/DeltaDB is unavailable so hook trust cannot be verified"
1736
+ : hookWaitingForApproval
1737
+ ? "hooks are installed, but no trusted native Codex hook signal is visible yet"
1738
+ : hookInstalled
1739
+ ? "hooks are installed, but no trusted native agent signal is visible yet"
1740
+ : "no target reported a ready hook bridge",
1741
+ suggestedCommands: hookProofs.flatMap((proof) => proof.nextActions ?? [`forge agent hooks status --target ${proof.target} --json`]),
1742
+ },
1743
+ {
1744
+ name: "deltadb",
1745
+ ok: delta?.exitCode === 0 && delta?.recording !== false,
1746
+ status: delta?.exitCode === 0 && delta?.recording !== false ? "ok" : "warning",
1747
+ message: delta?.exitCode === 0 ? "DeltaDB status is readable" : "DeltaDB status needs attention",
1748
+ suggestedCommands: ["forge delta status --json", "forge agent hooks smoke --target codex --json"],
1749
+ },
1750
+ ...(codexAppServer
1751
+ ? [{
1752
+ name: "codex-app-server",
1753
+ ok: !codexHandshakeFailed,
1754
+ status: codexHandshakeFailed
1755
+ ? "failed" as const
1756
+ : codexAppServer.available
1757
+ ? "ok" as const
1758
+ : "warning" as const,
1759
+ message: codexHandshakeFailed
1760
+ ? `Codex app-server handshake failed: ${codexHandshake?.error ?? "initialize did not complete"}`
1761
+ : codexHandshake?.initialized
1762
+ ? "Codex app-server initialized over stdio for deep Studio integration"
1763
+ : codexAppServer.available
1764
+ ? "Codex app-server is available for deep Studio integration"
1765
+ : "Codex app-server is not available yet; Studio will rely on hooks, MCP, and Forge snapshots",
1766
+ suggestedCommands: codexAppServer.nextActions,
1767
+ }]
1768
+ : []),
1769
+ ];
1770
+ const ok = checks.every((check) => check.ok);
1771
+ return normalizeForgeCliCommandsInValue(snapshot.app.path, {
1772
+ schemaVersion: "0.1.0",
1773
+ ok,
1774
+ action: "doctor",
1775
+ app: snapshot.app,
1776
+ checks,
1777
+ snapshot,
1778
+ diagnostics: snapshot.diagnostics,
1779
+ nextActions: checks.flatMap((check) => check.ok ? [] : check.suggestedCommands).slice(0, 12),
1780
+ exitCode: ok ? 0 : 1,
1781
+ });
1782
+ }
1783
+
1784
+ export async function runStudioOpenCommand(options: StudioAttachOptions): Promise<StudioOpenResult> {
1785
+ const attach = await runStudioAttachCommand(options);
1786
+ const appRoot = attach.app.path;
1787
+ const pkg = readPackageJson(appRoot);
1788
+ const diagnostics: Diagnostic[] = [...attach.diagnostics];
1789
+ const shouldStart = options.start !== false;
1790
+ const shouldBridge = options.bridge !== false;
1791
+ const previewPort = attach.preview.port ?? localPreviewPort(attach.preview.url);
1792
+ const commands = {
1793
+ ...attach.commands,
1794
+ attach: attachCommandFor(attach.targets, previewPort ?? 5174),
1795
+ bridge: `forge studio bridge . --preview-port ${previewPort ?? 5174} ${attach.targets.map((target) => `--target ${target}`).join(" ")} --studio-url ${normalizeStudioUrl(options.studioUrl)}${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
1796
+ doctor: `forge studio doctor . --preview-port ${previewPort ?? 5174} ${attach.targets.map((target) => `--target ${target}`).join(" ")}${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
1797
+ open: `forge studio open . --preview-port ${previewPort ?? 5174} ${attach.targets.map((target) => `--target ${target}`).join(" ")}${options.probeAppServer ? " --probe-codex-server" : ""} --json`,
1798
+ };
1799
+
1800
+ const dependencyStatus = dependencyStatusFor(appRoot, pkg);
1801
+ const install: StudioOpenResult["previewAutomation"]["install"] = {
1802
+ required: dependencyStatus.required,
1803
+ installed: dependencyStatus.installed,
1804
+ attempted: false,
1805
+ ...(dependencyStatus.command ? { command: dependencyStatus.command } : {}),
1806
+ cwd: appRoot,
1807
+ };
1808
+
1809
+ if (dependencyStatus.required && !dependencyStatus.installed) {
1810
+ if (options.install && !options.dryRun) {
1811
+ const installed = runDependencyInstall(appRoot, pkg);
1812
+ install.attempted = true;
1813
+ install.ok = installed.ok;
1814
+ install.exitCode = installed.exitCode;
1815
+ install.command = installed.command;
1816
+ install.installed = installed.ok;
1817
+ if (!installed.ok) {
1818
+ diagnostics.push(createDiagnostic({
1819
+ severity: "error",
1820
+ code: "FORGE_STUDIO_DEPENDENCY_INSTALL_FAILED",
1821
+ message: `dependency install failed in ${appRoot} with exit code ${installed.exitCode}`,
1822
+ fixHint: `Run ${installed.command} in the target app, then retry forge studio open.`,
1823
+ suggestedCommands: [installed.command, commands.open],
1824
+ }));
1825
+ }
1826
+ } else {
1827
+ diagnostics.push(createDiagnostic({
1828
+ severity: "warning",
1829
+ code: "FORGE_STUDIO_DEPENDENCIES_MISSING",
1830
+ message: `target app dependencies are not installed in ${appRoot}`,
1831
+ fixHint: options.dryRun
1832
+ ? "dry-run does not install dependencies; rerun with --install when you want ForgeOS to install them."
1833
+ : "Run the install command yourself, or rerun forge studio open with --install.",
1834
+ suggestedCommands: [
1835
+ dependencyStatus.command ?? "npm install",
1836
+ `${commands.open} --install`,
1837
+ ],
1838
+ }));
1839
+ }
1840
+ }
1841
+
1842
+ let started = false;
1843
+ let startAttempted = false;
1844
+ let skippedReason: StudioOpenResult["previewAutomation"]["skippedReason"];
1845
+ let previewStatusAfter = attach.preview.status;
1846
+ let pid: number | undefined;
1847
+ let previewOwner: StudioOpenResult["previewAutomation"]["owner"];
1848
+
1849
+ if (attach.preview.status.state === "reachable") {
1850
+ skippedReason = "already-running";
1851
+ const listener = previewPort ? detectListeningProcess(previewPort) : null;
1852
+ const state = readPreviewState(appRoot);
1853
+ const managed = Boolean(
1854
+ listener?.pid && state?.listenerPid === listener.pid &&
1855
+ state.previewPort === previewPort && processIsRunning(state.pid),
1856
+ );
1857
+ if (managed) {
1858
+ pid = state?.pid;
1859
+ }
1860
+ previewOwner = managed
1861
+ ? {
1862
+ kind: "forge-managed",
1863
+ ...(pid ? { pid } : {}),
1864
+ evidence: `${attach.preview.url} is reachable and its listener matches the recorded ForgeOS process`,
1865
+ statePath: normalizePath(relative(appRoot, previewStatePath(appRoot))),
1866
+ }
1867
+ : listener
1868
+ ? {
1869
+ kind: "external-process",
1870
+ ...(listener.pid ? { pid: listener.pid } : {}),
1871
+ ...(listener.command ? { command: listener.command } : {}),
1872
+ evidence: `${attach.preview.url} was reachable before ForgeOS attempted startup; ${listener.evidence}`,
1873
+ }
1874
+ : {
1875
+ kind: "preexisting-reachable-preview",
1876
+ evidence: `${attach.preview.url} was reachable before ForgeOS attempted to start the target app`,
1877
+ };
1878
+ } else if (options.dryRun) {
1879
+ skippedReason = "dry-run";
1880
+ previewOwner = {
1881
+ kind: "dry-run",
1882
+ evidence: "dry-run did not inspect or start a preview process",
1883
+ };
1884
+ } else if (!shouldStart) {
1885
+ skippedReason = "disabled";
1886
+ } else if (!previewPort) {
1887
+ skippedReason = "non-local-preview";
1888
+ diagnostics.push(createDiagnostic({
1889
+ severity: "warning",
1890
+ code: "FORGE_STUDIO_PREVIEW_NOT_LOCAL",
1891
+ message: `cannot auto-start non-local preview URL ${attach.preview.url}`,
1892
+ fixHint: "Use --preview-port for a local target app preview, or start the preview manually.",
1893
+ suggestedCommands: [commands.startTargetApp, commands.probePreview],
1894
+ }));
1895
+ } else if (install.required && !install.installed) {
1896
+ skippedReason = install.attempted ? "install-failed" : "missing-dependencies";
1897
+ } else {
1898
+ startAttempted = true;
1899
+ const spawned = await spawnForgeDev(appRoot, previewPort);
1900
+ if (spawned.error) {
1901
+ diagnostics.push(createDiagnostic({
1902
+ severity: "error",
1903
+ code: "FORGE_STUDIO_PREVIEW_START_FAILED",
1904
+ message: `failed to start target app preview: ${spawned.error}`,
1905
+ fixHint: `Run ${commands.startTargetApp} in ${appRoot}.`,
1906
+ suggestedCommands: [commands.startTargetApp, commands.probePreview],
1907
+ }));
1908
+ } else {
1909
+ pid = spawned.pid;
1910
+ previewStatusAfter = await waitForPreviewAfterStart(attach.preview, commands.startTargetApp, pid);
1911
+ if (previewStatusAfter.state === "reachable" && processIsRunning(pid)) {
1912
+ started = true;
1913
+ const listenerPid = detectListeningProcess(previewPort)?.pid;
1914
+ writePreviewState({ appRoot, pid, previewPort, command: spawned.command, listenerPid });
1915
+ previewOwner = {
1916
+ kind: "forge-managed",
1917
+ ...(pid ? { pid } : {}),
1918
+ evidence: "ForgeOS started the target preview and verified its listener",
1919
+ statePath: normalizePath(relative(appRoot, previewStatePath(appRoot))),
1920
+ };
1921
+ } else {
1922
+ diagnostics.push(createDiagnostic({
1923
+ severity: "error",
1924
+ code: "FORGE_STUDIO_PREVIEW_START_FAILED",
1925
+ message: `${commands.startTargetApp} did not make ${attach.preview.url} reachable: ${previewStatusAfter.reason}`,
1926
+ fixHint: `Run ${commands.startTargetApp} in ${appRoot} to inspect its output, then retry.`,
1927
+ suggestedCommands: [commands.probePreview, commands.doctor],
1928
+ }));
1929
+ }
1930
+ }
1931
+ }
1932
+
1933
+ const preview = {
1934
+ ...attach.preview,
1935
+ status: previewStatusAfter,
1936
+ };
1937
+
1938
+ if (!options.dryRun && attach.filesWritten.includes(attach.manifestPath)) {
1939
+ nodeFileSystem.writeText(
1940
+ join(appRoot, attach.manifestPath),
1941
+ renderManifest({
1942
+ app: attach.app,
1943
+ preview,
1944
+ posture: attach.posture,
1945
+ targets: attach.targets,
1946
+ commands: attach.commands,
1947
+ }),
1948
+ );
1949
+ }
1950
+
1951
+ let bridgeResult: StudioBridgeResult | undefined;
1952
+ let autoBridge: Awaited<ReturnType<typeof spawnForgeStudioBridge>> | undefined;
1953
+ if (shouldBridge && !options.dryRun) {
1954
+ const intervalMs = Math.max(1000, Math.floor(options.intervalMs ?? 5000));
1955
+ bridgeResult = await runStudioBridgeCommand({
1956
+ ...options,
1957
+ path: appRoot,
1958
+ previewUrl: preview.url,
1959
+ previewPort,
1960
+ once: true,
1961
+ targets: attach.targets,
1962
+ });
1963
+ diagnostics.push(...bridgeResult.diagnostics);
1964
+ if (!options.dryRun && bridgeResult.ok && preview.status.state === "reachable" && previewPort) {
1965
+ autoBridge = await spawnForgeStudioBridge({
1966
+ appRoot,
1967
+ previewPort,
1968
+ targets: attach.targets,
1969
+ studioUrl: normalizeStudioUrl(options.studioUrl),
1970
+ intervalMs,
1971
+ probeAppServer: options.probeAppServer,
1972
+ });
1973
+ if (autoBridge.error) {
1974
+ diagnostics.push(createDiagnostic({
1975
+ severity: "error",
1976
+ code: "FORGE_STUDIO_BRIDGE_AUTOSTART_FAILED",
1977
+ message: `initial snapshot was delivered, but the live Studio bridge could not be started: ${autoBridge.error}`,
1978
+ fixHint: `Run ${autoBridge.command} in ${appRoot}.`,
1979
+ suggestedCommands: [autoBridge.command, commands.doctor],
1980
+ }));
1981
+ }
1982
+ }
1983
+ }
1984
+
1985
+ const bridge = bridgeResult
1986
+ ? {
1987
+ attempted: true,
1988
+ ok: bridgeResult.ok && !autoBridge?.error,
1989
+ posted: bridgeResult.posted,
1990
+ dryRun: bridgeResult.dryRun,
1991
+ mode: autoBridge && !autoBridge.error ? "watch" as const : bridgeResult.mode,
1992
+ autoStarted: Boolean(autoBridge && !autoBridge.error && !autoBridge.alreadyRunning),
1993
+ alreadyRunning: Boolean(autoBridge?.alreadyRunning),
1994
+ ...(autoBridge?.command ? { command: autoBridge.command } : {}),
1995
+ cwd: appRoot,
1996
+ intervalMs: Math.max(1000, Math.floor(options.intervalMs ?? 5000)),
1997
+ ...(autoBridge?.pid ? { pid: autoBridge.pid } : {}),
1998
+ studioUrl: bridgeResult.studioUrl,
1999
+ endpoint: bridgeResult.endpoint,
2000
+ diagnostics: bridgeResult.diagnostics,
2001
+ nextActions: bridgeResult.nextActions,
2002
+ }
2003
+ : shouldBridge && options.dryRun
2004
+ ? {
2005
+ attempted: true,
2006
+ ok: true,
2007
+ posted: false,
2008
+ dryRun: true,
2009
+ mode: "watch" as const,
2010
+ autoStarted: false,
2011
+ alreadyRunning: false,
2012
+ command: commands.bridge,
2013
+ cwd: appRoot,
2014
+ intervalMs: Math.max(1000, Math.floor(options.intervalMs ?? 5000)),
2015
+ studioUrl: normalizeStudioUrl(options.studioUrl),
2016
+ endpoint: `${normalizeStudioUrl(options.studioUrl)}/commands/ingestStudioSnapshot`,
2017
+ diagnostics: [],
2018
+ nextActions: [commands.bridge, commands.doctor],
2019
+ }
2020
+ : {
2021
+ attempted: false,
2022
+ ok: true,
2023
+ posted: false,
2024
+ dryRun: options.dryRun,
2025
+ mode: "once" as const,
2026
+ autoStarted: false,
2027
+ alreadyRunning: false,
2028
+ studioUrl: normalizeStudioUrl(options.studioUrl),
2029
+ diagnostics: [],
2030
+ nextActions: [`forge studio bridge . --preview-port ${previewPort ?? 5174} --target ${attach.targets[0] ?? "codex"} --json`],
2031
+ };
2032
+
2033
+ const previewReady = options.dryRun || preview.status.state === "reachable";
2034
+ const ok = attach.ok &&
2035
+ previewReady &&
2036
+ bridge.ok &&
2037
+ diagnostics.every((diagnostic) => diagnostic.severity !== "error");
2038
+ const nextActions = Array.from(new Set([
2039
+ ...(install.required && !install.installed && install.command ? [install.command] : []),
2040
+ ...(preview.status.state === "reachable" ? [] : [commands.startTargetApp, commands.probePreview]),
2041
+ ...(bridge.attempted && !bridge.ok ? bridge.nextActions : []),
2042
+ commands.doctor,
2043
+ ...attach.commands.checkHooks,
2044
+ ])).slice(0, 12);
2045
+
2046
+ return normalizeForgeCliCommandsInValue(appRoot, {
2047
+ schemaVersion: "0.1.0",
2048
+ ok,
2049
+ action: "open",
2050
+ app: attach.app,
2051
+ preview,
2052
+ attach,
2053
+ previewAutomation: {
2054
+ attempted: startAttempted,
2055
+ started,
2056
+ alreadyRunning: skippedReason === "already-running",
2057
+ ...(skippedReason ? { skippedReason } : {}),
2058
+ command: commands.startTargetApp,
2059
+ cwd: appRoot,
2060
+ ...(pid ? { pid } : {}),
2061
+ ...(previewOwner ? { owner: previewOwner } : {}),
2062
+ statusBefore: attach.preview.status,
2063
+ statusAfter: preview.status,
2064
+ install,
2065
+ },
2066
+ bridge,
2067
+ commands,
2068
+ diagnostics,
2069
+ nextActions,
2070
+ exitCode: ok ? 0 : 1,
2071
+ });
2072
+ }
2073
+
2074
+ export function formatStudioAttachJson(result: StudioAttachResult): string {
2075
+ return `${JSON.stringify(result, null, 2)}\n`;
2076
+ }
2077
+
2078
+ export function formatStudioOpenJson(result: StudioOpenResult): string {
2079
+ return `${JSON.stringify(result, null, 2)}\n`;
2080
+ }
2081
+
2082
+ export function formatStudioSnapshotJson(result: StudioSnapshotResult): string {
2083
+ return `${JSON.stringify(result, null, 2)}\n`;
2084
+ }
2085
+
2086
+ export function formatStudioWatchJson(result: StudioWatchResult): string {
2087
+ return `${JSON.stringify({
2088
+ schemaVersion: result.schemaVersion,
2089
+ event: result.stream.event,
2090
+ ok: result.ok,
2091
+ stream: result.stream,
2092
+ snapshot: result.snapshot,
2093
+ exitCode: result.exitCode,
2094
+ }, null, 2)}\n`;
2095
+ }
2096
+
2097
+ export function formatStudioBridgeJson(result: StudioBridgeResult): string {
2098
+ return `${JSON.stringify(result, null, 2)}\n`;
2099
+ }
2100
+
2101
+ export function formatStudioCodexServerJson(result: StudioCodexServerResult): string {
2102
+ return `${JSON.stringify(result, null, 2)}\n`;
2103
+ }
2104
+
2105
+ export function formatStudioBridgeEventJson(result: StudioBridgeResult): string {
2106
+ return `${JSON.stringify({
2107
+ schemaVersion: result.schemaVersion,
2108
+ event: "studio.bridge",
2109
+ ok: result.ok,
2110
+ posted: result.posted,
2111
+ dryRun: result.dryRun,
2112
+ studioUrl: result.studioUrl,
2113
+ provider: result.provider,
2114
+ target: result.target,
2115
+ snapshot: result.snapshot,
2116
+ diagnostics: result.diagnostics,
2117
+ exitCode: result.exitCode,
2118
+ })}\n`;
2119
+ }
2120
+
2121
+ export function formatStudioDoctorJson(result: StudioDoctorResult): string {
2122
+ return `${JSON.stringify(result, null, 2)}\n`;
2123
+ }
2124
+
2125
+ export function formatStudioAttachHuman(result: StudioAttachResult): string {
2126
+ const lines = [
2127
+ `Forge Studio attach: ${result.ok ? "ready" : "needs attention"}`,
2128
+ `App: ${result.app.name}`,
2129
+ `Path: ${result.app.path}`,
2130
+ `Preview: ${result.preview.url}`,
2131
+ `Preview status: ${result.preview.status.state} (${result.preview.status.reason})`,
2132
+ `Preview note: ${result.preview.note}`,
2133
+ `Posture: ${result.posture.state} (${result.posture.reason})`,
2134
+ ...(result.posture.generated ? [`Generated: ${result.posture.generated.state} (${result.posture.generated.changedFiles} changed)`] : []),
2135
+ `Start app: ${result.commands.startTargetApp}`,
2136
+ `Start cwd: ${result.commands.startTargetAppCwd}`,
2137
+ `Targets: ${result.targets.join(", ")}`,
2138
+ `Manifest: ${result.manifestPath}`,
2139
+ "",
2140
+ "Next:",
2141
+ ...result.nextActions.map((action) => ` ${action}`),
2142
+ ];
2143
+ if (result.diagnostics.length > 0) {
2144
+ lines.push("", "Diagnostics:");
2145
+ lines.push(...result.diagnostics.slice(0, 8).map((diag) => ` ${diag.severity} ${diag.code}: ${diag.message}`));
2146
+ }
2147
+ return `${lines.join("\n")}\n`;
2148
+ }
2149
+
2150
+ export function formatStudioOpenHuman(result: StudioOpenResult): string {
2151
+ const bridgeStatus = result.bridge.attempted
2152
+ ? result.bridge.posted
2153
+ ? "posted"
2154
+ : result.bridge.dryRun
2155
+ ? "dry-run"
2156
+ : result.bridge.ok
2157
+ ? "ready"
2158
+ : "needs attention"
2159
+ : "skipped";
2160
+ const previewAutomation = result.previewAutomation.started
2161
+ ? `started${result.previewAutomation.pid ? ` (pid ${result.previewAutomation.pid})` : ""}`
2162
+ : result.previewAutomation.skippedReason ?? "not started";
2163
+ const owner = result.previewAutomation.owner
2164
+ ? `${result.previewAutomation.owner.kind}${result.previewAutomation.owner.pid ? ` (pid ${result.previewAutomation.owner.pid})` : ""}`
2165
+ : "unknown";
2166
+ const lines = [
2167
+ `Forge Studio open: ${result.ok ? "ready" : "needs attention"}`,
2168
+ `App: ${result.app.name}`,
2169
+ `Path: ${result.app.path}`,
2170
+ `Preview: ${result.preview.url}`,
2171
+ `Preview status: ${result.preview.status.state} (${result.preview.status.reason})`,
2172
+ `Preview automation: ${previewAutomation}`,
2173
+ `Preview owner: ${owner}`,
2174
+ `Bridge: ${bridgeStatus} (${result.bridge.studioUrl})`,
2175
+ `Start cwd: ${result.previewAutomation.cwd}`,
2176
+ "",
2177
+ "Next:",
2178
+ ...result.nextActions.map((action) => ` ${action}`),
2179
+ ];
2180
+ if (result.diagnostics.length > 0) {
2181
+ lines.push("", "Diagnostics:");
2182
+ lines.push(...result.diagnostics.slice(0, 10).map((diag) => ` ${diag.severity} ${diag.code}: ${diag.message}`));
2183
+ }
2184
+ return `${lines.join("\n")}\n`;
2185
+ }
2186
+
2187
+ export function formatStudioSnapshotHuman(result: StudioSnapshotResult): string {
2188
+ const changedSummary = result.changed.summary as { changedFiles?: number; humanFiles?: number; generatedFiles?: number } | undefined;
2189
+ const lines = [
2190
+ `Forge Studio snapshot: ${result.ok ? "ready" : "needs attention"}`,
2191
+ `App: ${result.app.name}`,
2192
+ `Path: ${result.app.path}`,
2193
+ `Preview: ${result.preview.url}`,
2194
+ `Preview status: ${result.preview.status.state} (${result.preview.status.reason})`,
2195
+ `Posture: ${result.posture.state} (${result.posture.reason})`,
2196
+ ...(result.posture.generated ? [`Generated: ${result.posture.generated.state} (${result.posture.generated.changedFiles} changed)`] : []),
2197
+ `Changed: ${changedSummary?.changedFiles ?? 0} (${changedSummary?.humanFiles ?? 0} authored, ${changedSummary?.generatedFiles ?? 0} generated)`,
2198
+ `Start app: ${result.commands.startTargetApp}`,
2199
+ `Start cwd: ${result.commands.startTargetAppCwd}`,
2200
+ "",
2201
+ "Next:",
2202
+ ...result.nextActions.map((action) => ` ${action}`),
2203
+ ];
2204
+ if (result.diagnostics.length > 0) {
2205
+ lines.push("", "Diagnostics:");
2206
+ lines.push(...result.diagnostics.slice(0, 8).map((diag) => ` ${diag.severity} ${diag.code}: ${diag.message}`));
2207
+ }
2208
+ return `${lines.join("\n")}\n`;
2209
+ }
2210
+
2211
+ export function formatStudioWatchHuman(result: StudioWatchResult): string {
2212
+ return [
2213
+ `Forge Studio watch: ${result.ok ? "ready" : "needs attention"}`,
2214
+ `Event: ${result.stream.event}`,
2215
+ `Mode: ${result.stream.mode}`,
2216
+ `Preview: ${result.snapshot.preview.url}`,
2217
+ `Follow: ${result.stream.followCommand}`,
2218
+ "",
2219
+ ].join("\n");
2220
+ }
2221
+
2222
+ export function formatStudioBridgeHuman(result: StudioBridgeResult): string {
2223
+ const lines = [
2224
+ `Forge Studio bridge: ${result.ok ? "delivered" : "needs attention"}`,
2225
+ `Studio runtime: ${result.studioUrl}`,
2226
+ `Provider: ${result.provider}`,
2227
+ `Preview: ${result.snapshot.preview.url}`,
2228
+ `Posted: ${result.posted ? "yes" : result.dryRun ? "dry-run" : "no"}`,
2229
+ `Snapshot posture: ${result.snapshot.posture.state} (${result.snapshot.posture.reason})`,
2230
+ "",
2231
+ "Next:",
2232
+ ...result.nextActions.map((action) => ` ${action}`),
2233
+ ];
2234
+ if (result.diagnostics.length > 0) {
2235
+ lines.push("", "Diagnostics:");
2236
+ lines.push(...result.diagnostics.slice(0, 8).map((diag) => ` ${diag.severity} ${diag.code}: ${diag.message}`));
2237
+ }
2238
+ return `${lines.join("\n")}\n`;
2239
+ }
2240
+
2241
+ export function formatStudioCodexServerHuman(result: StudioCodexServerResult): string {
2242
+ const lines = [
2243
+ `Forge Studio Codex app-server: ${result.ok ? "ready" : "needs attention"}`,
2244
+ `App: ${result.app.name}`,
2245
+ `Path: ${result.app.path}`,
2246
+ `State: ${result.proof.state}`,
2247
+ `Available: ${result.proof.available ? "yes" : "no"}`,
2248
+ `Inspect: ${result.commands.inspect}`,
2249
+ `Schemas: ${result.commands.generateTypes}`,
2250
+ `Schema generation: ${result.schemaGeneration.attempted ? result.schemaGeneration.ok ? "written" : "failed" : "planned"}`,
2251
+ `Handshake: ${result.handshake.attempted ? result.handshake.ok ? "initialized" : "failed" : result.handshake.skippedReason ?? "not requested"}`,
2252
+ `Connect: ${result.commands.connectStdio}`,
2253
+ "",
2254
+ "Checks:",
2255
+ ...result.proof.checks.map((check) => {
2256
+ const status = check.status === "ok" ? "OK" : check.status.toUpperCase();
2257
+ return ` ${status} ${check.name}: ${check.message}`;
2258
+ }),
2259
+ ];
2260
+ if (result.nextActions.length > 0) {
2261
+ lines.push("", "Next:", ...result.nextActions.map((action) => ` ${action}`));
2262
+ }
2263
+ return `${lines.join("\n")}\n`;
2264
+ }
2265
+
2266
+ export function formatStudioDoctorHuman(result: StudioDoctorResult): string {
2267
+ const lines = [
2268
+ `Forge Studio doctor: ${result.ok ? "ready" : "needs attention"}`,
2269
+ `App: ${result.app.name}`,
2270
+ `Path: ${result.app.path}`,
2271
+ "",
2272
+ "Checks:",
2273
+ ...result.checks.map((check) => {
2274
+ const status = check.status === "ok" ? "OK" : check.status.toUpperCase();
2275
+ return ` ${status} ${check.name}: ${check.message}`;
2276
+ }),
2277
+ ];
2278
+ if (result.nextActions.length > 0) {
2279
+ lines.push("", "Next:", ...result.nextActions.map((action) => ` ${action}`));
2280
+ }
2281
+ return `${lines.join("\n")}\n`;
2282
+ }