forgeos 0.1.0-alpha.2 → 0.1.0-alpha.21

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