@aiwg/cli 0.0.0-bootstrap.0 → 2026.7.18

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 (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -1
  3. package/bin/aiwg.mjs +349 -0
  4. package/dist/src/a2a/agent-card.js +145 -0
  5. package/dist/src/a2a/client.js +411 -0
  6. package/dist/src/a2a/hitl-cli.js +171 -0
  7. package/dist/src/a2a/hitl-driver.js +370 -0
  8. package/dist/src/a2a/hitl.js +181 -0
  9. package/dist/src/a2a/http.js +193 -0
  10. package/dist/src/a2a/jcs.js +128 -0
  11. package/dist/src/a2a/jws.js +187 -0
  12. package/dist/src/a2a/types.js +24 -0
  13. package/dist/src/a2a/webhook.js +240 -0
  14. package/dist/src/activity-log/cli.js +348 -0
  15. package/dist/src/activity-log/parser.js +81 -0
  16. package/dist/src/activity-log/types.js +50 -0
  17. package/dist/src/agents/agent-deployer.js +367 -0
  18. package/dist/src/agents/agent-packager.js +271 -0
  19. package/dist/src/agents/agent-validator.js +271 -0
  20. package/dist/src/agents/grounding/knowledge-base.js +161 -0
  21. package/dist/src/agents/grounding/types.js +12 -0
  22. package/dist/src/agents/index.js +10 -0
  23. package/dist/src/agents/packaged-agent-inventory.js +75 -0
  24. package/dist/src/agents/types.js +7 -0
  25. package/dist/src/api/index.d.ts +10 -0
  26. package/dist/src/api/index.js +10 -0
  27. package/dist/src/artifacts/address-parser.js +73 -0
  28. package/dist/src/artifacts/audit/cli.js +183 -0
  29. package/dist/src/artifacts/audit/drift.js +122 -0
  30. package/dist/src/artifacts/audit/types.js +12 -0
  31. package/dist/src/artifacts/backends/graphology-backend.js +160 -0
  32. package/dist/src/artifacts/backends/json-backend.js +129 -0
  33. package/dist/src/artifacts/backends/sqlite-backend.js +199 -0
  34. package/dist/src/artifacts/browser-export.js +622 -0
  35. package/dist/src/artifacts/capability-resolver.js +94 -0
  36. package/dist/src/artifacts/checksum-manifest.js +91 -0
  37. package/dist/src/artifacts/citation-parser.js +162 -0
  38. package/dist/src/artifacts/cli.js +1486 -0
  39. package/dist/src/artifacts/corpus-tools/citation-densify.js +311 -0
  40. package/dist/src/artifacts/corpus-tools/cli.js +393 -0
  41. package/dist/src/artifacts/corpus-tools/corpus-graph.js +225 -0
  42. package/dist/src/artifacts/corpus-tools/curator.js +186 -0
  43. package/dist/src/artifacts/corpus-tools/discovery-log.js +70 -0
  44. package/dist/src/artifacts/corpus-tools/funder-network.js +141 -0
  45. package/dist/src/artifacts/corpus-tools/induction-audit.js +233 -0
  46. package/dist/src/artifacts/corpus-tools/integrity-scan.js +202 -0
  47. package/dist/src/artifacts/corpus-tools/profile-communities.js +166 -0
  48. package/dist/src/artifacts/corpus-tools/profile-edges.js +72 -0
  49. package/dist/src/artifacts/corpus-tools/profile-embed.js +113 -0
  50. package/dist/src/artifacts/corpus-tools/profile-generate-fm.js +167 -0
  51. package/dist/src/artifacts/corpus-tools/profile-generate.js +181 -0
  52. package/dist/src/artifacts/corpus-tools/profile-metrics.js +98 -0
  53. package/dist/src/artifacts/corpus-tools/profile-status.js +86 -0
  54. package/dist/src/artifacts/corpus-tools/profile-temporal.js +139 -0
  55. package/dist/src/artifacts/corpus-tools/radar-init.js +93 -0
  56. package/dist/src/artifacts/corpus-tools/radar-report.js +118 -0
  57. package/dist/src/artifacts/corpus-tools/radar-shared.js +189 -0
  58. package/dist/src/artifacts/corpus-tools/radar-status.js +68 -0
  59. package/dist/src/artifacts/corpus-tools/sidecar-lint.js +233 -0
  60. package/dist/src/artifacts/corpus-tools/sidecar-repair.js +277 -0
  61. package/dist/src/artifacts/corpus-tools/snapshot.js +591 -0
  62. package/dist/src/artifacts/corpus-tools/source-types.js +164 -0
  63. package/dist/src/artifacts/corpus-tools/vision-extract.js +243 -0
  64. package/dist/src/artifacts/corpus-views/build.js +126 -0
  65. package/dist/src/artifacts/corpus-views/corpus-config.js +124 -0
  66. package/dist/src/artifacts/corpus-views/ref-parser.js +437 -0
  67. package/dist/src/artifacts/corpus-views/renderers.js +353 -0
  68. package/dist/src/artifacts/corpus-views/taxonomies.js +131 -0
  69. package/dist/src/artifacts/dep-graph.js +173 -0
  70. package/dist/src/artifacts/discover-facets.js +375 -0
  71. package/dist/src/artifacts/embedding-index.js +354 -0
  72. package/dist/src/artifacts/enrichment/cli.js +240 -0
  73. package/dist/src/artifacts/enrichment/prompt.js +47 -0
  74. package/dist/src/artifacts/enrichment/store.js +90 -0
  75. package/dist/src/artifacts/enrichment/types.js +12 -0
  76. package/dist/src/artifacts/fortemi-core-query-adapter.js +475 -0
  77. package/dist/src/artifacts/fortemi-core-sync.js +251 -0
  78. package/dist/src/artifacts/fortemi-shard-export.js +44 -0
  79. package/dist/src/artifacts/fulltext.js +108 -0
  80. package/dist/src/artifacts/graph-backend.js +51 -0
  81. package/dist/src/artifacts/graph-query.js +247 -0
  82. package/dist/src/artifacts/hybrid-query.js +225 -0
  83. package/dist/src/artifacts/index-builder.js +1089 -0
  84. package/dist/src/artifacts/index-reader.js +112 -0
  85. package/dist/src/artifacts/index-status.js +193 -0
  86. package/dist/src/artifacts/legacy-index-migration.js +169 -0
  87. package/dist/src/artifacts/move.js +154 -0
  88. package/dist/src/artifacts/operational-state.js +239 -0
  89. package/dist/src/artifacts/prebuilt-build-lock.js +49 -0
  90. package/dist/src/artifacts/query-engine.js +1955 -0
  91. package/dist/src/artifacts/source-graph.js +529 -0
  92. package/dist/src/artifacts/stats.js +169 -0
  93. package/dist/src/artifacts/types.js +606 -0
  94. package/dist/src/artifacts/views/cli.js +293 -0
  95. package/dist/src/artifacts/views/definition.js +170 -0
  96. package/dist/src/artifacts/views/store.js +149 -0
  97. package/dist/src/artifacts/views/types.js +8 -0
  98. package/dist/src/artifacts/watcher.js +255 -0
  99. package/dist/src/catalog/builtin-models.json +636 -0
  100. package/dist/src/catalog/cli.js +228 -0
  101. package/dist/src/catalog/cli.mjs +289 -0
  102. package/dist/src/catalog/index.js +15 -0
  103. package/dist/src/catalog/loader.js +217 -0
  104. package/dist/src/catalog/loader.mjs +251 -0
  105. package/dist/src/catalog/sources.json +41 -0
  106. package/dist/src/catalog/types.js +8 -0
  107. package/dist/src/channel/manager.mjs +504 -0
  108. package/dist/src/cli/agent-spawn.js +178 -0
  109. package/dist/src/cli/cli-extension-loader.js +245 -0
  110. package/dist/src/cli/command-log.js +275 -0
  111. package/dist/src/cli/config-loader.js +232 -0
  112. package/dist/src/cli/env.js +82 -0
  113. package/dist/src/cli/errors.js +150 -0
  114. package/dist/src/cli/find-package-root.js +40 -0
  115. package/dist/src/cli/git-hooks.js +250 -0
  116. package/dist/src/cli/handlers/agentcard.js +206 -0
  117. package/dist/src/cli/handlers/artifacts.js +76 -0
  118. package/dist/src/cli/handlers/best-practices-audit.js +280 -0
  119. package/dist/src/cli/handlers/cockpit.js +137 -0
  120. package/dist/src/cli/handlers/command-log.js +37 -0
  121. package/dist/src/cli/handlers/daemon.js +59 -0
  122. package/dist/src/cli/handlers/diagnose.js +300 -0
  123. package/dist/src/cli/handlers/execution-mode.js +105 -0
  124. package/dist/src/cli/handlers/feedback.js +341 -0
  125. package/dist/src/cli/handlers/help.js +147 -0
  126. package/dist/src/cli/handlers/index.js +302 -0
  127. package/dist/src/cli/handlers/init.js +179 -0
  128. package/dist/src/cli/handlers/install.js +129 -0
  129. package/dist/src/cli/handlers/issues.js +46 -0
  130. package/dist/src/cli/handlers/lint.js +42 -0
  131. package/dist/src/cli/handlers/local-executor.js +315 -0
  132. package/dist/src/cli/handlers/marketplace.js +168 -0
  133. package/dist/src/cli/handlers/mc.js +1010 -0
  134. package/dist/src/cli/handlers/models.js +352 -0
  135. package/dist/src/cli/handlers/packages.js +155 -0
  136. package/dist/src/cli/handlers/ralph-launcher.js +717 -0
  137. package/dist/src/cli/handlers/ralph.js +575 -0
  138. package/dist/src/cli/handlers/refresh.js +432 -0
  139. package/dist/src/cli/handlers/regenerate.js +336 -0
  140. package/dist/src/cli/handlers/repo-access.js +109 -0
  141. package/dist/src/cli/handlers/run.js +165 -0
  142. package/dist/src/cli/handlers/runtime-info.js +301 -0
  143. package/dist/src/cli/handlers/sandbox.js +197 -0
  144. package/dist/src/cli/handlers/scaffolding.js +149 -0
  145. package/dist/src/cli/handlers/script-runner.js +121 -0
  146. package/dist/src/cli/handlers/sdlc-accelerate.js +148 -0
  147. package/dist/src/cli/handlers/serve.js +1835 -0
  148. package/dist/src/cli/handlers/session.js +348 -0
  149. package/dist/src/cli/handlers/setup.js +440 -0
  150. package/dist/src/cli/handlers/skill-lint.js +367 -0
  151. package/dist/src/cli/handlers/skill-usage.js +74 -0
  152. package/dist/src/cli/handlers/steward.js +567 -0
  153. package/dist/src/cli/handlers/subcommands.js +1702 -0
  154. package/dist/src/cli/handlers/team.js +337 -0
  155. package/dist/src/cli/handlers/types.js +11 -0
  156. package/dist/src/cli/handlers/use.js +2772 -0
  157. package/dist/src/cli/handlers/utilities.js +703 -0
  158. package/dist/src/cli/handlers/version.js +134 -0
  159. package/dist/src/cli/handlers/workspace-context.js +93 -0
  160. package/dist/src/cli/handlers/workspace.js +126 -0
  161. package/dist/src/cli/help-generator.js +178 -0
  162. package/dist/src/cli/hooks/builtin/activity-log-hook.js +126 -0
  163. package/dist/src/cli/hooks/executor.js +96 -0
  164. package/dist/src/cli/hooks/index.js +15 -0
  165. package/dist/src/cli/hooks/registry.js +143 -0
  166. package/dist/src/cli/hooks/types.js +13 -0
  167. package/dist/src/cli/log.js +492 -0
  168. package/dist/src/cli/project-isolation/detect.js +70 -0
  169. package/dist/src/cli/project-isolation/index.js +7 -0
  170. package/dist/src/cli/project-isolation/signals.js +17 -0
  171. package/dist/src/cli/project-isolation/warning.js +102 -0
  172. package/dist/src/cli/prompt-utils.js +155 -0
  173. package/dist/src/cli/provider-deployment-plan.js +126 -0
  174. package/dist/src/cli/provider-resolution.js +119 -0
  175. package/dist/src/cli/router.js +305 -0
  176. package/dist/src/cli/scope-resolver.js +392 -0
  177. package/dist/src/cli/skill-usage.js +616 -0
  178. package/dist/src/cli/ui.js +224 -0
  179. package/dist/src/cli/watch-service.js +203 -0
  180. package/dist/src/cli/workflow-orchestrator.js +478 -0
  181. package/dist/src/cli/workspace-signals.js +321 -0
  182. package/dist/src/community/footer.js +14 -0
  183. package/dist/src/community/links.js +123 -0
  184. package/dist/src/community/milestones.js +46 -0
  185. package/dist/src/community/nudge-policy.js +93 -0
  186. package/dist/src/config/aiwg-config.js +883 -0
  187. package/dist/src/config/cli.js +703 -0
  188. package/dist/src/config/gitignore.js +156 -0
  189. package/dist/src/config/project-artifacts.js +76 -0
  190. package/dist/src/config/user-config.js +428 -0
  191. package/dist/src/config/user-registry.js +127 -0
  192. package/dist/src/config/workspace.js +276 -0
  193. package/dist/src/contributors/detect.js +39 -0
  194. package/dist/src/contributors/discover.js +172 -0
  195. package/dist/src/contributors/heuristic.js +186 -0
  196. package/dist/src/contributors/index.js +11 -0
  197. package/dist/src/contributors/types.js +13 -0
  198. package/dist/src/contributors/validation.js +116 -0
  199. package/dist/src/data/community.schema.json +43 -0
  200. package/dist/src/data/community.yaml +16 -0
  201. package/dist/src/extensions/capability-index.js +270 -0
  202. package/dist/src/extensions/claude-hooks-installer.js +268 -0
  203. package/dist/src/extensions/commands/definitions.js +3485 -0
  204. package/dist/src/extensions/deployment-registration.js +463 -0
  205. package/dist/src/extensions/index.js +22 -0
  206. package/dist/src/extensions/loader.js +132 -0
  207. package/dist/src/extensions/managed-marker.js +100 -0
  208. package/dist/src/extensions/manifest.js +196 -0
  209. package/dist/src/extensions/project-local-activity.js +114 -0
  210. package/dist/src/extensions/project-local-discovery.js +261 -0
  211. package/dist/src/extensions/project-local-doctor.js +281 -0
  212. package/dist/src/extensions/project-local-gitignore.js +176 -0
  213. package/dist/src/extensions/project-local-paths.js +121 -0
  214. package/dist/src/extensions/project-local-promote.js +218 -0
  215. package/dist/src/extensions/project-local-remove.js +373 -0
  216. package/dist/src/extensions/project-local-scaffold.js +252 -0
  217. package/dist/src/extensions/project-quickref.js +291 -0
  218. package/dist/src/extensions/registry.js +368 -0
  219. package/dist/src/extensions/shadow-resolver.js +271 -0
  220. package/dist/src/extensions/types.js +86 -0
  221. package/dist/src/extensions/upstream-registry.js +195 -0
  222. package/dist/src/extensions/validation.js +631 -0
  223. package/dist/src/features/catalog.js +88 -0
  224. package/dist/src/features/cli.js +197 -0
  225. package/dist/src/features/installer.js +57 -0
  226. package/dist/src/features/paths.js +14 -0
  227. package/dist/src/features/runtime.js +21 -0
  228. package/dist/src/features/status.js +134 -0
  229. package/dist/src/hooks/laziness-detection.js +532 -0
  230. package/dist/src/intake/git-history-analyzer.js +486 -0
  231. package/dist/src/intake/index.js +9 -0
  232. package/dist/src/issues/cli.js +327 -0
  233. package/dist/src/issues/index.js +5 -0
  234. package/dist/src/issues/live.js +154 -0
  235. package/dist/src/issues/local.js +666 -0
  236. package/dist/src/issues/sync.js +143 -0
  237. package/dist/src/issues/types.js +2 -0
  238. package/dist/src/issues/workflows.js +164 -0
  239. package/dist/src/kb/cli.js +173 -0
  240. package/dist/src/lint/cli.js +171 -0
  241. package/dist/src/lint/loader.js +225 -0
  242. package/dist/src/lint/reporters.js +141 -0
  243. package/dist/src/lint/runner.js +364 -0
  244. package/dist/src/lint/types.js +10 -0
  245. package/dist/src/marketplace/cache.js +110 -0
  246. package/dist/src/marketplace/identifier.js +63 -0
  247. package/dist/src/marketplace/registry.js +36 -0
  248. package/dist/src/marketplace/sources/clawhub.js +53 -0
  249. package/dist/src/marketplace/sources/git.js +45 -0
  250. package/dist/src/marketplace/types.js +9 -0
  251. package/dist/src/mcp/adapters/codex-runtime.js +224 -0
  252. package/dist/src/mcp/cli.mjs +1197 -0
  253. package/dist/src/mcp/elicitation.mjs +149 -0
  254. package/dist/src/mcp/helpers.mjs +287 -0
  255. package/dist/src/mcp/index.mjs +11 -0
  256. package/dist/src/mcp/profiles.js +263 -0
  257. package/dist/src/mcp/profiles.mjs +360 -0
  258. package/dist/src/mcp/registry.js +376 -0
  259. package/dist/src/mcp/registry.mjs +387 -0
  260. package/dist/src/mcp/server.mjs +633 -0
  261. package/dist/src/mcp/test-simple.mjs +271 -0
  262. package/dist/src/mcp/tools/command-run.mjs +101 -0
  263. package/dist/src/mcp/tools/discovery.mjs +291 -0
  264. package/dist/src/mcp/tools/interaction.mjs +87 -0
  265. package/dist/src/mcp/tools/orchestration.mjs +320 -0
  266. package/dist/src/mcp/tools/subsystems.mjs +640 -0
  267. package/dist/src/memory/cli.js +166 -0
  268. package/dist/src/memory/project-registry.js +282 -0
  269. package/dist/src/metrics/artifact-metrics.js +184 -0
  270. package/dist/src/metrics/context-budget.js +174 -0
  271. package/dist/src/metrics/token-counter.js +133 -0
  272. package/dist/src/models/config-loader.js +228 -0
  273. package/dist/src/models/index.js +22 -0
  274. package/dist/src/models/model-capabilities.v1.json +120 -0
  275. package/dist/src/models/model-catalog.v1.json +96 -0
  276. package/dist/src/models/model-discovery.js +521 -0
  277. package/dist/src/models/provider-models.js +73 -0
  278. package/dist/src/models/provider-policy.js +273 -0
  279. package/dist/src/models/resolver.js +245 -0
  280. package/dist/src/models/router.js +70 -0
  281. package/dist/src/models/types.js +8 -0
  282. package/dist/src/models/wrapper-deployment.js +178 -0
  283. package/dist/src/models/wrapper-route.js +54 -0
  284. package/dist/src/ops/cli.js +268 -0
  285. package/dist/src/ops/registry.js +623 -0
  286. package/dist/src/packages/adapters/clawhub.js +113 -0
  287. package/dist/src/packages/adapters/git.js +153 -0
  288. package/dist/src/packages/adapters/gitea.js +93 -0
  289. package/dist/src/packages/adapters/github.js +69 -0
  290. package/dist/src/packages/adapters/local-cache.js +89 -0
  291. package/dist/src/packages/package-registry.js +143 -0
  292. package/dist/src/packages/registry.js +238 -0
  293. package/dist/src/packages/types.js +11 -0
  294. package/dist/src/plugin/cross-framework-ops.js +475 -0
  295. package/dist/src/plugin/framework-config-loader.js +235 -0
  296. package/dist/src/plugin/framework-detector.js +186 -0
  297. package/dist/src/plugin/framework-isolator.js +341 -0
  298. package/dist/src/plugin/framework-migration.js +371 -0
  299. package/dist/src/plugin/metadata-validator.js +906 -0
  300. package/dist/src/plugin/plugin-installer.js +436 -0
  301. package/dist/src/plugin/plugin-status.js +369 -0
  302. package/dist/src/plugin/plugin-uninstaller.js +473 -0
  303. package/dist/src/plugin/registry-validator.js +344 -0
  304. package/dist/src/plugin/skill-command-translator.js +415 -0
  305. package/dist/src/plugin/workspace-creator.js +189 -0
  306. package/dist/src/plugin/workspace-migrator.js +821 -0
  307. package/dist/src/policy/authorization.js +390 -0
  308. package/dist/src/policy/repo-access.js +215 -0
  309. package/dist/src/provenance/cli.js +16 -0
  310. package/dist/src/providers/capability-matrix.js +300 -0
  311. package/dist/src/providers/capability-matrix.yaml +514 -0
  312. package/dist/src/providers/provider-definitions.js +977 -0
  313. package/dist/src/providers/provider-definitions.mjs +159 -0
  314. package/dist/src/providers/provider-inventory.js +169 -0
  315. package/dist/src/quality/feedback-ab.js +174 -0
  316. package/dist/src/quality/feedback-collector.js +176 -0
  317. package/dist/src/quality/feedback-tracker.js +123 -0
  318. package/dist/src/quality/patterns/adr.json +24 -0
  319. package/dist/src/quality/patterns/sad.json +24 -0
  320. package/dist/src/quality/patterns/test-plan.json +24 -0
  321. package/dist/src/quality/patterns/use-case.json +24 -0
  322. package/dist/src/quality/scoring.js +157 -0
  323. package/dist/src/reflections/cli.js +15 -0
  324. package/dist/src/release/plan.js +139 -0
  325. package/dist/src/research/cache/manager.js +227 -0
  326. package/dist/src/research/clients/arxiv.js +156 -0
  327. package/dist/src/research/clients/base.js +197 -0
  328. package/dist/src/research/clients/crossref.js +112 -0
  329. package/dist/src/research/clients/semantic-scholar.js +126 -0
  330. package/dist/src/research/clients/unpaywall.js +87 -0
  331. package/dist/src/research/index.js +14 -0
  332. package/dist/src/research/query-cli.js +336 -0
  333. package/dist/src/research/services/acquisition.js +313 -0
  334. package/dist/src/research/services/archival.js +225 -0
  335. package/dist/src/research/services/citation.js +279 -0
  336. package/dist/src/research/services/discovery.js +243 -0
  337. package/dist/src/research/services/documentation.js +269 -0
  338. package/dist/src/research/services/index.js +14 -0
  339. package/dist/src/research/services/provenance.js +279 -0
  340. package/dist/src/research/services/quality.js +370 -0
  341. package/dist/src/research/services/types.js +7 -0
  342. package/dist/src/research/storage-cli.js +20 -0
  343. package/dist/src/research/types.js +47 -0
  344. package/dist/src/resources/index.d.ts +2 -0
  345. package/dist/src/resources/index.js +2 -0
  346. package/dist/src/resources/web-release.d.ts +89 -0
  347. package/dist/src/resources/web-release.js +886 -0
  348. package/dist/src/rlm/cache/cli.js +162 -0
  349. package/dist/src/rlm/cache/hash.js +35 -0
  350. package/dist/src/rlm/cache/store.js +170 -0
  351. package/dist/src/rlm/cache/types.js +8 -0
  352. package/dist/src/rlm/cli.js +650 -0
  353. package/dist/src/serve/a2a-terminal-observer.js +120 -0
  354. package/dist/src/serve/agent-router.js +205 -0
  355. package/dist/src/serve/dispatch-router.js +171 -0
  356. package/dist/src/serve/executor-registry.js +715 -0
  357. package/dist/src/serve/mission-conductor.js +177 -0
  358. package/dist/src/serve/orchestrator-adapter.js +113 -0
  359. package/dist/src/serve/orchestrator-override.js +94 -0
  360. package/dist/src/serve/orchestrator-pty.js +133 -0
  361. package/dist/src/serve/pty-bridge.js +799 -0
  362. package/dist/src/serve/sandbox-registry.js +832 -0
  363. package/dist/src/serve/screen-reader.js +228 -0
  364. package/dist/src/serve/stack-adapters.js +68 -0
  365. package/dist/src/serve/telemetry.js +143 -0
  366. package/dist/src/skills/adapters/clawhub.js +250 -0
  367. package/dist/src/skills/adapters/local.js +335 -0
  368. package/dist/src/skills/adapters/openclaw.js +316 -0
  369. package/dist/src/skills/cli.js +334 -0
  370. package/dist/src/skills/registry.js +117 -0
  371. package/dist/src/skills/run.js +259 -0
  372. package/dist/src/skills/runtime.js +94 -0
  373. package/dist/src/skills/types.js +10 -0
  374. package/dist/src/smiths/agentsmith/demo.js +116 -0
  375. package/dist/src/smiths/agentsmith/examples.js +300 -0
  376. package/dist/src/smiths/agentsmith/generator.js +503 -0
  377. package/dist/src/smiths/agentsmith/index.js +11 -0
  378. package/dist/src/smiths/agentsmith/types.js +8 -0
  379. package/dist/src/smiths/commandsmith/example.js +220 -0
  380. package/dist/src/smiths/commandsmith/generator.js +288 -0
  381. package/dist/src/smiths/commandsmith/index.js +41 -0
  382. package/dist/src/smiths/commandsmith/templates.js +296 -0
  383. package/dist/src/smiths/commandsmith/types.js +7 -0
  384. package/dist/src/smiths/context-pipeline/aiwg-md.js +104 -0
  385. package/dist/src/smiths/context-pipeline/allowlist.js +195 -0
  386. package/dist/src/smiths/context-pipeline/claude-hook.js +127 -0
  387. package/dist/src/smiths/context-pipeline/discovery.js +187 -0
  388. package/dist/src/smiths/context-pipeline/external-links-section.js +49 -0
  389. package/dist/src/smiths/context-pipeline/finalization.js +147 -0
  390. package/dist/src/smiths/context-pipeline/generator.js +477 -0
  391. package/dist/src/smiths/context-pipeline/index.js +30 -0
  392. package/dist/src/smiths/context-pipeline/legacy-inject.js +103 -0
  393. package/dist/src/smiths/context-pipeline/managed-hook.js +95 -0
  394. package/dist/src/smiths/context-pipeline/overflow.js +212 -0
  395. package/dist/src/smiths/context-pipeline/parallelism-section.js +94 -0
  396. package/dist/src/smiths/context-pipeline/provider-policy.js +64 -0
  397. package/dist/src/smiths/context-pipeline/sanitizer.js +93 -0
  398. package/dist/src/smiths/context-pipeline/types.js +23 -0
  399. package/dist/src/smiths/context-pipeline/workspace-context.js +988 -0
  400. package/dist/src/smiths/hook-bridge/codex-translator.js +129 -0
  401. package/dist/src/smiths/hook-bridge/copilot-translator.js +64 -0
  402. package/dist/src/smiths/hook-bridge/factory-translator.js +43 -0
  403. package/dist/src/smiths/hook-bridge/hermes-translator.js +88 -0
  404. package/dist/src/smiths/hook-bridge/index.js +79 -0
  405. package/dist/src/smiths/hook-bridge/loader.js +116 -0
  406. package/dist/src/smiths/hook-bridge/shim.js +89 -0
  407. package/dist/src/smiths/hook-bridge/types.js +31 -0
  408. package/dist/src/smiths/index.js +39 -0
  409. package/dist/src/smiths/mcpsmith/analyzers/cli-analyzer.js +390 -0
  410. package/dist/src/smiths/mcpsmith/example.js +119 -0
  411. package/dist/src/smiths/mcpsmith/generator.js +236 -0
  412. package/dist/src/smiths/mcpsmith/index.js +17 -0
  413. package/dist/src/smiths/mcpsmith/types.js +10 -0
  414. package/dist/src/smiths/platform-paths.js +101 -0
  415. package/dist/src/smiths/skillsmith/codex-sidecar.js +85 -0
  416. package/dist/src/smiths/skillsmith/collision-detector.js +249 -0
  417. package/dist/src/smiths/skillsmith/demo.js +83 -0
  418. package/dist/src/smiths/skillsmith/examples.js +161 -0
  419. package/dist/src/smiths/skillsmith/generator.js +316 -0
  420. package/dist/src/smiths/skillsmith/index.js +12 -0
  421. package/dist/src/smiths/skillsmith/namespace-adapter.js +180 -0
  422. package/dist/src/smiths/skillsmith/platform-resolver.js +157 -0
  423. package/dist/src/smiths/skillsmith/types.js +9 -0
  424. package/dist/src/smiths/toolsmith/index.js +10 -0
  425. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +710 -0
  426. package/dist/src/smiths/toolsmith/types.js +8 -0
  427. package/dist/src/storage/backends/fortemi.js +227 -0
  428. package/dist/src/storage/backends/fs.js +134 -0
  429. package/dist/src/storage/backends/logseq.js +309 -0
  430. package/dist/src/storage/backends/obsidian.js +233 -0
  431. package/dist/src/storage/cli.js +468 -0
  432. package/dist/src/storage/config.js +272 -0
  433. package/dist/src/storage/index.js +108 -0
  434. package/dist/src/storage/subsystem-cli.js +201 -0
  435. package/dist/src/storage/types.js +33 -0
  436. package/dist/src/testing/corpus/corpus-builder.js +383 -0
  437. package/dist/src/testing/corpus/ground-truth-manager.js +460 -0
  438. package/dist/src/testing/corpus/index.js +10 -0
  439. package/dist/src/testing/fixtures/index.js +9 -0
  440. package/dist/src/testing/fixtures/test-data-factory.js +472 -0
  441. package/dist/src/testing/generators/index.js +11 -0
  442. package/dist/src/testing/generators/test-case-generator.js +393 -0
  443. package/dist/src/testing/generators/test-code-generator.js +384 -0
  444. package/dist/src/testing/generators/use-case-parser.js +325 -0
  445. package/dist/src/testing/index.js +11 -0
  446. package/dist/src/tracker/capability-protocol.js +118 -0
  447. package/dist/src/update/checker.mjs +396 -0
  448. package/dist/src/update/notifier.mjs +235 -0
  449. package/dist/src/writing/content-diversifier.js +804 -0
  450. package/dist/src/writing/example-generator.js +959 -0
  451. package/dist/src/writing/example-templates.js +99 -0
  452. package/dist/src/writing/pattern-library.js +486 -0
  453. package/dist/src/writing/patterns/banned-phrases.json +4413 -0
  454. package/dist/src/writing/patterns/formulaic-structures.json +1300 -0
  455. package/dist/src/writing/patterns/generic-adjectives.json +1510 -0
  456. package/dist/src/writing/patterns/hedging-language.json +2096 -0
  457. package/dist/src/writing/patterns/transition-words.json +1285 -0
  458. package/dist/src/writing/patterns/weak-verbs.json +1112 -0
  459. package/dist/src/writing/prompt-optimizer.js +660 -0
  460. package/dist/src/writing/prompt-templates.js +583 -0
  461. package/dist/src/writing/scoring-config-loader.js +168 -0
  462. package/dist/src/writing/validation-engine.js +595 -0
  463. package/dist/src/writing/validation-rules.js +380 -0
  464. package/dist/src/writing/voice-analyzer.js +439 -0
  465. package/dist/src/writing/voice-calibration.js +661 -0
  466. package/dist/src/writing/voice-profiles.json +588 -0
  467. package/package.json +65 -9
@@ -0,0 +1,1835 @@
1
+ /**
2
+ * Serve Command Handler
3
+ *
4
+ * Starts a local HTTP + WebSocket server and opens the browser dashboard.
5
+ * Server stack: Hono serving static files, WebSocket PTY bridge, REST API.
6
+ *
7
+ * @issue #711
8
+ * @see #712 — WebSocket PTY bridge
9
+ * @see #714 — React app scaffold
10
+ */
11
+ import path from 'path';
12
+ import { existsSync, readFileSync } from 'fs';
13
+ import { spawnSync } from 'child_process';
14
+ import { createPtyWsHandler, registry as ptyRegistry } from '../../serve/pty-bridge.js';
15
+ import { telemetryStore, createEvent } from '../../serve/telemetry.js';
16
+ import { sandboxRegistry, normalizeSandboxEvent, } from '../../serve/sandbox-registry.js';
17
+ import { routeTask } from '../../serve/agent-router.js';
18
+ import { routeDispatch } from '../../serve/dispatch-router.js';
19
+ import { observeA2ATerminalState } from '../../serve/a2a-terminal-observer.js';
20
+ import { executorRegistry, validateRegisterPayload, validateDispatchPayload, validateEventEnvelope, } from '../../serve/executor-registry.js';
21
+ import { handleWebhook, IdempotencyCache, PushSecretRegistry, } from '../../a2a/webhook.js';
22
+ import { AiwgError, EXIT_CODES } from '../errors.js';
23
+ import { projectAiwgPath } from '../../config/project-artifacts.js';
24
+ // A2A push-notification state — module-scoped so the test harness can
25
+ // monkey-patch them in if needed. One process serves one set of secrets.
26
+ const pushSecretRegistry = new PushSecretRegistry();
27
+ const webhookIdempotency = new IdempotencyCache();
28
+ const DEFAULT_PORT = 7337;
29
+ const DEFAULT_HOST = '127.0.0.1';
30
+ /**
31
+ * Parse --port, --bind, --no-open, --read-only flags from args
32
+ */
33
+ function parseServeArgs(args) {
34
+ let port = DEFAULT_PORT;
35
+ let host = DEFAULT_HOST;
36
+ let open = true;
37
+ let readOnly = false;
38
+ let sandbox = null;
39
+ for (let i = 0; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === '--port' && args[i + 1]) {
42
+ const parsed = parseInt(args[i + 1], 10);
43
+ if (!isNaN(parsed))
44
+ port = parsed;
45
+ i++;
46
+ }
47
+ else if (arg === '--bind' && args[i + 1]) {
48
+ host = args[i + 1];
49
+ i++;
50
+ }
51
+ else if (arg === '--sandbox' && args[i + 1]) {
52
+ sandbox = args[i + 1];
53
+ i++;
54
+ }
55
+ else if (arg === '--no-open') {
56
+ open = false;
57
+ }
58
+ else if (arg === '--read-only') {
59
+ readOnly = true;
60
+ }
61
+ }
62
+ return { port, host, open, readOnly, sandbox };
63
+ }
64
+ // ============================================================
65
+ // WebSocket routing (#851)
66
+ //
67
+ // @hono/node-server v1.x does not export createNodeWebSocket.
68
+ // We wire WebSocket routes directly via the Node.js HTTP server's
69
+ // 'upgrade' event and the `ws` npm package instead.
70
+ // ============================================================
71
+ /**
72
+ * Verbose logging gate. Set `AIWG_SERVE_DEBUG=1` to print per-event
73
+ * traces (sandbox WS message flow, management/orchestrate proxy open/close).
74
+ * Errors and warnings log unconditionally.
75
+ */
76
+ const SERVE_DEBUG = process.env.AIWG_SERVE_DEBUG === '1';
77
+ function logServeWarn(tag, message, meta) {
78
+ if (meta !== undefined) {
79
+ console.warn(`[serve:${tag}] ${message}`, meta);
80
+ }
81
+ else {
82
+ console.warn(`[serve:${tag}] ${message}`);
83
+ }
84
+ }
85
+ function logServeDebug(tag, message, meta) {
86
+ if (!SERVE_DEBUG)
87
+ return;
88
+ if (meta !== undefined) {
89
+ console.log(`[serve:${tag}] ${message}`, meta);
90
+ }
91
+ else {
92
+ console.log(`[serve:${tag}] ${message}`);
93
+ }
94
+ }
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ function handleSandboxWs(ws, sandboxId, token) {
97
+ if (!sandboxRegistry.authenticate(sandboxId, token)) {
98
+ ws.close(4001, 'Unauthorized');
99
+ return;
100
+ }
101
+ sandboxRegistry.setConnected(sandboxId, true);
102
+ ws.on('message', (data) => {
103
+ if (!sandboxRegistry.authenticate(sandboxId, token))
104
+ return;
105
+ try {
106
+ // #933: agentic-sandbox serializes SandboxEvent with
107
+ // rename_all = "snake_case", so the wire tag and field names are
108
+ // snake_case. normalizeSandboxEvent translates to the dot-notation
109
+ // + camelCase shape handleEvent expects. Prior to this, every
110
+ // agent_*/hitl_* event was silently dropped and the dashboard
111
+ // reported "0 agents".
112
+ const raw = JSON.parse(data.toString());
113
+ const event = normalizeSandboxEvent(raw);
114
+ event.sandboxId = sandboxId;
115
+ sandboxRegistry.handleEvent(event);
116
+ }
117
+ catch { /* ignore malformed events */ }
118
+ });
119
+ ws.on('close', () => {
120
+ sandboxRegistry.setConnected(sandboxId, false);
121
+ });
122
+ ws.on('error', (err) => {
123
+ console.error(`[sandbox-registry] WebSocket error for ${sandboxId}:`, err);
124
+ });
125
+ }
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ function handleExecutorWs(ws, executorId, token) {
128
+ if (!executorRegistry.authenticate(executorId, token)) {
129
+ ws.close(4001, 'Unauthorized');
130
+ return;
131
+ }
132
+ // Duck-typed WS connection handle for registry.pushToExecutor()
133
+ executorRegistry.setConnected(executorId, true, ws);
134
+ ws.on('message', (data) => {
135
+ if (!executorRegistry.authenticate(executorId, token))
136
+ return;
137
+ try {
138
+ const raw = JSON.parse(data.toString());
139
+ const { valid } = validateEventEnvelope(raw);
140
+ if (!valid) {
141
+ logServeWarn('executor-ws', `invalid event envelope from executor ${executorId} — ignored`);
142
+ return;
143
+ }
144
+ const envelope = raw;
145
+ // Ensure executor_id matches the authenticated connection
146
+ envelope.executor_id = executorId;
147
+ executorRegistry.handleEvent(envelope);
148
+ }
149
+ catch { /* ignore malformed events */ }
150
+ });
151
+ ws.on('close', () => {
152
+ executorRegistry.setConnected(executorId, false);
153
+ logServeDebug('executor-ws', `executor ${executorId} WS disconnected`);
154
+ });
155
+ ws.on('error', (err) => {
156
+ console.error(`[executor-registry] WebSocket error for ${executorId}:`, err);
157
+ });
158
+ }
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ function handlePtyWs(ws, sessionId, command, cmdArgs, cwd, wsEndpoint, agentId) {
161
+ // createPtyWsHandler expects a Hono-context-like object for param/query extraction.
162
+ // We provide a minimal shim since we've already parsed the URL.
163
+ const mockContext = {
164
+ req: {
165
+ param: (key) => key === 'sessionId' ? sessionId : undefined,
166
+ query: () => ({
167
+ command,
168
+ args: cmdArgs.join(','),
169
+ ...(cwd ? { cwd } : {}),
170
+ ...(wsEndpoint ? { wsEndpoint } : {}),
171
+ ...(agentId ? { agentId } : {}),
172
+ }),
173
+ },
174
+ };
175
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
+ const handler = createPtyWsHandler(mockContext);
177
+ handler.onOpen?.(null, ws);
178
+ ws.on('message', (data) => {
179
+ handler.onMessage?.({ data: data.toString() });
180
+ });
181
+ ws.on('close', () => {
182
+ handler.onClose?.();
183
+ });
184
+ ws.on('error', (err) => {
185
+ handler.onError?.(err);
186
+ });
187
+ }
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ async function setupWebSockets(httpServer, readOnly) {
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
+ let wsMod;
192
+ try {
193
+ // @ts-expect-error — ws lacks bundled types; we use the runtime constructor only
194
+ wsMod = await import('ws');
195
+ }
196
+ catch {
197
+ console.warn('[serve] ws package not available — WebSocket routes disabled. Install with: npm install ws');
198
+ return;
199
+ }
200
+ // ws ships as CJS; ESM import may wrap in .default
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ const WebSocketServer = wsMod.WebSocketServer ??
203
+ wsMod.default?.WebSocketServer ??
204
+ wsMod.Server ??
205
+ wsMod.default?.Server;
206
+ if (!WebSocketServer) {
207
+ console.warn('[serve] Could not resolve WebSocketServer from ws package — WebSocket routes disabled.');
208
+ return;
209
+ }
210
+ const wss = new WebSocketServer({ noServer: true });
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ httpServer.on('upgrade', (req, socket, head) => {
213
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
214
+ const pathname = url.pathname;
215
+ // /ws/sandbox/:sandboxId
216
+ const sandboxMatch = pathname.match(/^\/ws\/sandbox\/([^/]+)$/);
217
+ if (sandboxMatch) {
218
+ const sandboxId = sandboxMatch[1];
219
+ const token = url.searchParams.get('token') ?? '';
220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
+ wss.handleUpgrade(req, socket, head, (ws) => {
222
+ handleSandboxWs(ws, sandboxId, token);
223
+ });
224
+ return;
225
+ }
226
+ // /ws/executors/:executorId — executor contract v1 bidirectional event stream (#1179)
227
+ const executorMatch = pathname.match(/^\/ws\/executors\/([^/]+)$/);
228
+ if (executorMatch) {
229
+ const executorId = executorMatch[1];
230
+ const token = url.searchParams.get('token') ?? '';
231
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
+ wss.handleUpgrade(req, socket, head, (ws) => {
233
+ handleExecutorWs(ws, executorId, token);
234
+ });
235
+ return;
236
+ }
237
+ // /ws/pty/:sessionId (disabled in read-only mode)
238
+ // Optional query params:
239
+ // ?sandbox=<sandboxId> — target a specific registered sandbox
240
+ // ?agent=<agentId> — target a specific agent within that sandbox
241
+ // ?wsEndpoint=<url> — explicit management WS URL (overrides sandbox lookup)
242
+ // Without these params the PTY bridge auto-detects the first connected sandbox.
243
+ if (!readOnly) {
244
+ const ptyMatch = pathname.match(/^\/ws\/pty\/([^/]+)$/);
245
+ if (ptyMatch) {
246
+ const sessionId = ptyMatch[1];
247
+ const command = url.searchParams.get('command') ?? 'aiwg';
248
+ const argsParam = url.searchParams.get('args');
249
+ const cmdArgs = argsParam ? argsParam.split(',') : ['mc', 'watch'];
250
+ const cwd = url.searchParams.get('cwd') ?? undefined;
251
+ const agentId = url.searchParams.get('agent') ?? undefined;
252
+ // Resolve wsEndpoint: explicit param takes precedence over sandbox registry lookup
253
+ let wsEndpoint = url.searchParams.get('wsEndpoint') ?? undefined;
254
+ if (!wsEndpoint) {
255
+ const sandboxId = url.searchParams.get('sandbox') ?? undefined;
256
+ if (sandboxId) {
257
+ const sb = sandboxRegistry.get(sandboxId);
258
+ if (sb?.wsEndpoint)
259
+ wsEndpoint = sb.wsEndpoint;
260
+ }
261
+ }
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ wss.handleUpgrade(req, socket, head, (ws) => {
264
+ handlePtyWs(ws, sessionId, command, cmdArgs, cwd, wsEndpoint, agentId);
265
+ });
266
+ return;
267
+ }
268
+ }
269
+ // /ws/sandbox/:sandboxId/sessions/:sessionId/orchestrate
270
+ // Proxies to the sandbox management server's orchestrate WS endpoint.
271
+ // Browser speaks the orchestrate protocol (screen_update frames) directly;
272
+ // this bridge is purely a cross-origin WS relay.
273
+ const orchMatch = pathname.match(/^\/ws\/sandbox\/([^/]+)\/sessions\/([^/]+)\/orchestrate$/);
274
+ if (orchMatch) {
275
+ const sandboxId = orchMatch[1];
276
+ const sessionId = orchMatch[2];
277
+ const sandbox = sandboxRegistry.get(sandboxId);
278
+ if (!sandbox) {
279
+ logServeWarn('orch-proxy', `sandbox ${sandboxId} not found — rejecting orchestrate WS upgrade for session ${sessionId}`);
280
+ socket.destroy();
281
+ return;
282
+ }
283
+ if (!sandbox.connected) {
284
+ logServeWarn('orch-proxy', `sandbox ${sandboxId} not connected — rejecting orchestrate WS upgrade for session ${sessionId}`);
285
+ socket.destroy();
286
+ return;
287
+ }
288
+ // Convert httpEndpoint to ws:// URL for the sandbox orchestrate path
289
+ const orchWsUrl = sandbox.httpEndpoint.replace(/^http/, 'ws') + `/ws/sessions/${sessionId}/orchestrate`;
290
+ logServeDebug('orch-proxy', `upgrading browser → ${orchWsUrl} for session ${sessionId}`);
291
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
+ wss.handleUpgrade(req, socket, head, async (browserWs) => {
293
+ try {
294
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
+ const WS = wsMod.WebSocket ?? wsMod.default?.WebSocket ?? wsMod.default;
296
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
297
+ const sandboxWs = new WS(orchWsUrl);
298
+ sandboxWs.on('open', () => {
299
+ logServeDebug('orch-proxy', `upstream orchestrate WS open for session ${sessionId}`);
300
+ // Relay sandbox → browser
301
+ sandboxWs.on('message', (data) => {
302
+ if (browserWs.readyState === 1) {
303
+ try {
304
+ browserWs.send(typeof data === 'string' ? data : data.toString());
305
+ }
306
+ catch (err) {
307
+ logServeWarn('orch-proxy', `relay sandbox→browser failed for ${sessionId}`, err);
308
+ }
309
+ }
310
+ });
311
+ sandboxWs.on('close', (code, reason) => {
312
+ logServeDebug('orch-proxy', `upstream orchestrate WS closed for ${sessionId}: code=${code} reason=${reason?.toString?.() ?? ''}`);
313
+ try {
314
+ browserWs.close(1001, 'Sandbox closed');
315
+ }
316
+ catch { /* already closed */ }
317
+ });
318
+ sandboxWs.on('error', (err) => {
319
+ logServeWarn('orch-proxy', `upstream orchestrate WS error for ${sessionId}`, err);
320
+ try {
321
+ browserWs.close(1011, 'Sandbox WS error');
322
+ }
323
+ catch { /* already closed */ }
324
+ });
325
+ // Relay browser → sandbox
326
+ browserWs.on('message', (data) => {
327
+ if (sandboxWs.readyState === 1) {
328
+ try {
329
+ sandboxWs.send(typeof data === 'string' ? data : data.toString());
330
+ }
331
+ catch (err) {
332
+ logServeWarn('orch-proxy', `relay browser→sandbox failed for ${sessionId}`, err);
333
+ }
334
+ }
335
+ });
336
+ browserWs.on('close', () => { try {
337
+ sandboxWs.close();
338
+ }
339
+ catch { /* ignore */ } });
340
+ });
341
+ sandboxWs.on('error', (err) => {
342
+ logServeWarn('orch-proxy', `failed to connect to sandbox orchestrate WS at ${orchWsUrl}`, err);
343
+ try {
344
+ browserWs.close(1011, 'Could not connect to sandbox orchestrate WS');
345
+ }
346
+ catch { /* already closed */ }
347
+ });
348
+ }
349
+ catch (err) {
350
+ logServeWarn('orch-proxy', `orchestrate proxy threw for session ${sessionId}`, err);
351
+ try {
352
+ browserWs.close(1011, 'Orchestrate proxy error');
353
+ }
354
+ catch { /* already closed */ }
355
+ }
356
+ });
357
+ return;
358
+ }
359
+ // /ws/sandbox/:sandboxId/management
360
+ // Proxies to the sandbox management WS bus (wsEndpoint).
361
+ // The management WS is a multicast bus — all agents and sessions share one connection.
362
+ // The browser sends attach_session / send_input / pty_resize frames and receives
363
+ // output / session_attached / session_detached frames for all agents.
364
+ const mgmtMatch = pathname.match(/^\/ws\/sandbox\/([^/]+)\/management$/);
365
+ if (mgmtMatch) {
366
+ const sandboxId = mgmtMatch[1];
367
+ const sandbox = sandboxRegistry.get(sandboxId);
368
+ if (!sandbox) {
369
+ logServeWarn('mgmt-proxy', `sandbox ${sandboxId} not found in registry — rejecting WS upgrade`);
370
+ socket.destroy();
371
+ return;
372
+ }
373
+ if (!sandbox.connected) {
374
+ logServeWarn('mgmt-proxy', `sandbox ${sandboxId} (${sandbox.name}) is not connected — rejecting WS upgrade. Last event: ${sandbox.lastEventAt}`);
375
+ socket.destroy();
376
+ return;
377
+ }
378
+ // wsEndpoint is already a full ws:// URL (e.g. ws://localhost:8121)
379
+ const mgmtWsUrl = sandbox.wsEndpoint;
380
+ logServeDebug('mgmt-proxy', `upgrading browser → ${mgmtWsUrl} for sandbox ${sandboxId} (${sandbox.name})`);
381
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
382
+ wss.handleUpgrade(req, socket, head, async (browserWs) => {
383
+ try {
384
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
+ const WS = wsMod.WebSocket ?? wsMod.default?.WebSocket ?? wsMod.default;
386
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
387
+ const sandboxWs = new WS(mgmtWsUrl);
388
+ // Open-race fix (#1151 follow-up): the browser fires its first
389
+ // messages (subscribe + list_sessions) inside its own ws.onopen
390
+ // handler, which fires the moment the upgrade completes —
391
+ // BEFORE the upstream sandbox WS finishes opening. If we register
392
+ // the browser→sandbox listener inside sandboxWs.on('open', …) the
393
+ // browser's first messages have no listener yet and get silently
394
+ // dropped, leaving the pane forever stuck on "Listing sessions…".
395
+ // Two-pane setups hit this asymmetrically — pure timing decides
396
+ // which pane "wins" the race. Workaround: register the browser
397
+ // listener immediately, queue messages while upstream is still
398
+ // connecting, flush on upstream open.
399
+ const pendingFromBrowser = [];
400
+ let upstreamOpen = false;
401
+ browserWs.on('message', (data) => {
402
+ if (upstreamOpen && sandboxWs.readyState === 1) {
403
+ try {
404
+ sandboxWs.send(typeof data === 'string' ? data : data.toString());
405
+ }
406
+ catch (err) {
407
+ logServeWarn('mgmt-proxy', `relay browser→sandbox failed for ${sandboxId}`, err);
408
+ }
409
+ }
410
+ else {
411
+ // Upstream not ready yet — queue and flush on open. Cap the
412
+ // queue at a reasonable size so a hung upstream doesn't grow
413
+ // memory unbounded.
414
+ if (pendingFromBrowser.length < 64) {
415
+ pendingFromBrowser.push(data);
416
+ }
417
+ else {
418
+ logServeWarn('mgmt-proxy', `dropped browser message for ${sandboxId} — upstream still connecting and queue full`);
419
+ }
420
+ }
421
+ });
422
+ browserWs.on('close', () => { try {
423
+ sandboxWs.close();
424
+ }
425
+ catch { /* ignore */ } });
426
+ sandboxWs.on('open', () => {
427
+ logServeDebug('mgmt-proxy', `upstream WS open for sandbox ${sandboxId} (flushing ${pendingFromBrowser.length} queued msg)`);
428
+ upstreamOpen = true;
429
+ // Flush queued browser→sandbox messages in order.
430
+ while (pendingFromBrowser.length) {
431
+ const data = pendingFromBrowser.shift();
432
+ try {
433
+ sandboxWs.send(typeof data === 'string' ? data : data.toString());
434
+ }
435
+ catch (err) {
436
+ logServeWarn('mgmt-proxy', `flush browser→sandbox failed for ${sandboxId}`, err);
437
+ }
438
+ }
439
+ // Relay sandbox → browser
440
+ sandboxWs.on('message', (data) => {
441
+ if (browserWs.readyState === 1) {
442
+ try {
443
+ browserWs.send(typeof data === 'string' ? data : data.toString());
444
+ }
445
+ catch (err) {
446
+ logServeWarn('mgmt-proxy', `relay sandbox→browser failed for ${sandboxId}`, err);
447
+ }
448
+ }
449
+ });
450
+ sandboxWs.on('close', (code, reason) => {
451
+ logServeDebug('mgmt-proxy', `upstream WS closed for sandbox ${sandboxId}: code=${code} reason=${reason?.toString?.() ?? ''}`);
452
+ try {
453
+ browserWs.close(1001, 'Sandbox closed');
454
+ }
455
+ catch { /* already closed */ }
456
+ });
457
+ sandboxWs.on('error', (err) => {
458
+ logServeWarn('mgmt-proxy', `upstream WS error for sandbox ${sandboxId} (${mgmtWsUrl})`, err);
459
+ try {
460
+ browserWs.close(1011, 'Sandbox WS error');
461
+ }
462
+ catch { /* already closed */ }
463
+ });
464
+ });
465
+ sandboxWs.on('error', (err) => {
466
+ logServeWarn('mgmt-proxy', `failed to connect to sandbox management WS at ${mgmtWsUrl} for ${sandboxId}`, err);
467
+ try {
468
+ browserWs.close(1011, 'Could not connect to sandbox management WS');
469
+ }
470
+ catch { /* already closed */ }
471
+ });
472
+ }
473
+ catch (err) {
474
+ logServeWarn('mgmt-proxy', `management proxy threw for sandbox ${sandboxId}`, err);
475
+ try {
476
+ browserWs.close(1011, 'Management proxy error');
477
+ }
478
+ catch { /* already closed */ }
479
+ }
480
+ });
481
+ return;
482
+ }
483
+ // Unknown WS path — reject cleanly
484
+ socket.destroy();
485
+ });
486
+ }
487
+ /**
488
+ * Start the Hono HTTP server
489
+ *
490
+ * Uses dynamic require-style imports to avoid compile-time resolution of
491
+ * optional deps (hono, @hono/node-server) that are not yet in package.json.
492
+ * TypeScript sees only `unknown`-typed module shapes here.
493
+ */
494
+ /**
495
+ * In-process server bootstrap. Exported for tier-3 integration tests
496
+ * (test/integration/serve-*.test.ts) so they can drive serve as a black-box
497
+ * client without spawning a child node process. The CLI handler also calls
498
+ * this internally — same code path, exactly one entry point.
499
+ *
500
+ * @see #1277 — moves the integration suite off spawn-based testing
501
+ */
502
+ export async function startServer(opts) {
503
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
504
+ let honoMod;
505
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
506
+ let nodeMod;
507
+ try {
508
+ // Plain dynamic import — runtime-resolved, no static analysis issue, and
509
+ // works in sandboxed VM contexts like vitest where the Function-constructor
510
+ // import path raises ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING (#1277).
511
+ // hono is an optionalDependency; tsc may not find its types under
512
+ // `npm ci --omit=optional` (e.g. metadata-validation workflow). The
513
+ // try/catch + auto-install fallback below handles that at runtime.
514
+ // @ts-ignore — optional dep; may not be installed at typecheck time
515
+ honoMod = await import('hono');
516
+ // @ts-ignore — optional dep; may not be installed at typecheck time
517
+ nodeMod = await import('@hono/node-server');
518
+ }
519
+ catch {
520
+ // Auto-install optional serve dependencies on first use
521
+ console.log('Installing serve dependencies (hono, @hono/node-server, ws)...');
522
+ const result = spawnSync('npm', ['install', '--save-optional', 'hono', '@hono/node-server', 'ws'], { stdio: 'inherit' });
523
+ if (result.status !== 0) {
524
+ throw new AiwgError({
525
+ code: 'ERR_SERVE_DEPS_INSTALL_FAILED',
526
+ message: 'Failed to install serve dependencies (hono, @hono/node-server, ws)',
527
+ hint: 'Install manually: npm install hono @hono/node-server ws',
528
+ exitCode: EXIT_CODES.GENERAL,
529
+ });
530
+ }
531
+ // Retry imports after install
532
+ try {
533
+ // @ts-ignore — optional dep; may not be installed at typecheck time
534
+ honoMod = await import('hono');
535
+ // @ts-ignore — optional dep; may not be installed at typecheck time
536
+ nodeMod = await import('@hono/node-server');
537
+ }
538
+ catch (err) {
539
+ throw new AiwgError({
540
+ code: 'ERR_SERVE_DEPS_LOAD_FAILED',
541
+ message: 'Serve dependencies installed but could not be loaded',
542
+ hint: 'Try: npm install hono @hono/node-server ws',
543
+ exitCode: EXIT_CODES.GENERAL,
544
+ cause: err,
545
+ });
546
+ }
547
+ }
548
+ const { Hono } = honoMod;
549
+ const { serve } = nodeMod;
550
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
551
+ const app = new Hono();
552
+ // WebSocket routes are handled via Node.js upgrade event below (see setupWebSockets).
553
+ // @hono/node-server v1.x does not export createNodeWebSocket.
554
+ // Health check
555
+ app.get('/api/health', (c) => c.json({ status: 'ok', readOnly: opts.readOnly }));
556
+ // Connection status — server health, PTY sessions, sandboxes, subsystem status (#887)
557
+ const serverStartTime = Date.now();
558
+ const COUNTS_TTL_MS = 5000;
559
+ const countsCache = new Map();
560
+ const FETCH_TIMEOUT_MS = 1500;
561
+ async function fetchWithTimeout(url) {
562
+ const ctrl = new AbortController();
563
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
564
+ try {
565
+ const resp = await fetch(url, { signal: ctrl.signal });
566
+ if (!resp.ok)
567
+ return null;
568
+ return await resp.json();
569
+ }
570
+ finally {
571
+ clearTimeout(timer);
572
+ }
573
+ }
574
+ async function getInventoryCounts(s) {
575
+ if (!s.connected)
576
+ return { vmCount: null, containerCount: null };
577
+ const cached = countsCache.get(s.id);
578
+ if (cached && Date.now() - cached.at < COUNTS_TTL_MS)
579
+ return cached.counts;
580
+ let vmCount = null;
581
+ let containerCount = null;
582
+ try {
583
+ const [vms, containers] = await Promise.all([
584
+ fetchWithTimeout(`${s.httpEndpoint}/api/v1/vms`).catch(() => null),
585
+ fetchWithTimeout(`${s.httpEndpoint}/api/v1/containers`).catch(() => null),
586
+ ]);
587
+ if (vms && typeof vms.total === 'number') {
588
+ vmCount = vms.total;
589
+ }
590
+ if (containers && typeof containers.total === 'number') {
591
+ containerCount = containers.total;
592
+ }
593
+ }
594
+ catch { /* ignore */ }
595
+ const counts = { vmCount, containerCount };
596
+ countsCache.set(s.id, { at: Date.now(), counts });
597
+ return counts;
598
+ }
599
+ app.get('/api/connections', async (c) => {
600
+ const uptime = Date.now() - serverStartTime;
601
+ // PTY sessions
602
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
603
+ const sessions = [...ptyRegistry['sessions'].keys()];
604
+ // Sandboxes — augmented with VM/container counts (#1157).
605
+ // null counts indicate the sandbox is offline or didn't respond; the UI
606
+ // renders these as `?` rather than `0` so missing data is distinguishable
607
+ // from a confirmed empty inventory.
608
+ const sandboxList = sandboxRegistry.list();
609
+ const allSandboxes = await Promise.all(sandboxList.map(async (s) => {
610
+ const counts = await getInventoryCounts({ id: s.id, httpEndpoint: s.httpEndpoint, connected: s.connected });
611
+ return {
612
+ id: s.id,
613
+ name: s.name,
614
+ connected: s.connected,
615
+ agentCount: s.agentCount,
616
+ vmCount: counts.vmCount,
617
+ containerCount: counts.containerCount,
618
+ };
619
+ }));
620
+ // Ralph subsystem — read .aiwg/ralph/registry.json if present
621
+ let ralphStatus = 'unknown';
622
+ let activeLoops = 0;
623
+ try {
624
+ const ralphPath = projectAiwgPath(process.cwd(), 'ralph', 'registry.json');
625
+ if (existsSync(ralphPath)) {
626
+ const data = JSON.parse(readFileSync(ralphPath, 'utf-8'));
627
+ activeLoops = (data.active_loops ?? []).filter((l) => l.status === 'running').length;
628
+ ralphStatus = activeLoops > 0 ? 'active' : 'idle';
629
+ }
630
+ }
631
+ catch { /* ignore */ }
632
+ // Missions subsystem — check for mc session directory
633
+ let missionsStatus = 'unknown';
634
+ let missionsCount = 0;
635
+ try {
636
+ const mcPath = projectAiwgPath(process.cwd(), 'mc');
637
+ if (existsSync(mcPath)) {
638
+ missionsStatus = 'idle';
639
+ const registryPath = path.join(mcPath, 'registry.json');
640
+ if (existsSync(registryPath)) {
641
+ const data = JSON.parse(readFileSync(registryPath, 'utf-8'));
642
+ const activeSessions = (data.sessions ?? []).filter((s) => s.status === 'running');
643
+ missionsCount = activeSessions.length;
644
+ if (missionsCount > 0)
645
+ missionsStatus = 'active';
646
+ }
647
+ }
648
+ }
649
+ catch { /* ignore */ }
650
+ // Daemon subsystem — check for daemon PID file
651
+ let daemonStatus = 'unknown';
652
+ try {
653
+ const daemonPid = projectAiwgPath(process.cwd(), 'daemon', 'daemon.pid');
654
+ daemonStatus = existsSync(daemonPid) ? 'running' : 'stopped';
655
+ }
656
+ catch { /* ignore */ }
657
+ // RLM subsystem — check for rlm state
658
+ let rlmStatus = 'unknown';
659
+ try {
660
+ const rlmPath = projectAiwgPath(process.cwd(), 'rlm');
661
+ rlmStatus = existsSync(rlmPath) ? 'idle' : 'stopped';
662
+ }
663
+ catch { /* ignore */ }
664
+ // Semantic memory — check for memory index
665
+ let memoryStatus = 'unknown';
666
+ try {
667
+ const memPath = projectAiwgPath(process.cwd(), 'memory');
668
+ memoryStatus = existsSync(memPath) ? 'active' : 'stopped';
669
+ }
670
+ catch { /* ignore */ }
671
+ return c.json({
672
+ server: { status: 'ok', readOnly: opts.readOnly, uptime },
673
+ ptySessions: sessions,
674
+ sandboxes: allSandboxes,
675
+ mcpServers: [],
676
+ subsystems: {
677
+ ralph: { status: ralphStatus, activeLoops },
678
+ missions: { status: missionsStatus, count: missionsCount },
679
+ daemon: { status: daemonStatus },
680
+ rlm: { status: rlmStatus },
681
+ memory: { status: memoryStatus },
682
+ },
683
+ });
684
+ });
685
+ // REST stubs — filled in by #715 / #716
686
+ app.get('/api/sessions', (c) => {
687
+ const sessions = [...ptyRegistry['sessions'].keys()];
688
+ return c.json({ sessions });
689
+ });
690
+ // Mission Control API stubs (#715)
691
+ app.get('/api/missions', (c) => c.json({ missions: [], sessions: [] }));
692
+ app.get('/api/sessions/:id/missions', (c) => c.json({ missions: [] }));
693
+ // Legacy dispatch route — stub preserved for backward compat
694
+ // TODO: remove after v1.x once all callers migrate to /api/v1/sessions/:id/dispatch
695
+ app.post('/api/sessions/:id/dispatch', async (c) => {
696
+ // 301 → /api/v1/sessions/:id/dispatch so existing callers get a clear redirect signal
697
+ const sessionId = c.req.param('id');
698
+ return c.redirect(`/api/v1/sessions/${sessionId}/dispatch`, 301);
699
+ });
700
+ app.put('/api/missions/:id/pause', (c) => c.json({ ok: true }));
701
+ app.put('/api/missions/:id/resume', (c) => c.json({ ok: true }));
702
+ app.delete('/api/missions/:id', (c) => c.json({ ok: true }));
703
+ // ── Executor Registry API (#1179) ──────────────────────────────────────────
704
+ // POST /api/v1/executors/register → 201 with executor_id + token
705
+ app.post('/api/v1/executors/register', async (c) => {
706
+ let body;
707
+ try {
708
+ body = await c.req.json();
709
+ }
710
+ catch {
711
+ return c.json({ error: 'Invalid JSON body' }, 400);
712
+ }
713
+ const { valid, errors } = validateRegisterPayload(body);
714
+ if (!valid) {
715
+ return c.json({ error: `Invalid register payload: ${errors}` }, 400);
716
+ }
717
+ const result = executorRegistry.register(body);
718
+ if ('status' in result && result.status === 400) {
719
+ return c.json({ error: result.error }, 400);
720
+ }
721
+ // Emit telemetry (reuse agent.spawn as closest match for executor registration)
722
+ const resp = result;
723
+ telemetryStore.ingest(createEvent('agent.spawn', resp.executor_id, { name: body.name }));
724
+ return c.json(resp, 201);
725
+ });
726
+ // DELETE /api/v1/executors/:id → 204; auth required
727
+ app.delete('/api/v1/executors/:id', (c) => {
728
+ const executorId = c.req.param('id');
729
+ const authHeader = c.req.header('authorization') ?? '';
730
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
731
+ if (!executorRegistry.authenticate(executorId, token)) {
732
+ return c.json({ error: 'Unauthorized' }, 401);
733
+ }
734
+ const ok = executorRegistry.deregister(executorId, 'operator_deleted');
735
+ if (!ok)
736
+ return c.json({ error: 'Executor not found' }, 404);
737
+ return new Response(null, { status: 204 });
738
+ });
739
+ // GET /api/v1/executors → list
740
+ app.get('/api/v1/executors', (c) => {
741
+ return c.json({ executors: executorRegistry.list() });
742
+ });
743
+ // GET /api/v1/executors/:id → single executor status
744
+ app.get('/api/v1/executors/:id', (c) => {
745
+ const summary = executorRegistry.get(c.req.param('id'));
746
+ if (!summary)
747
+ return c.json({ error: 'Executor not found' }, 404);
748
+ return c.json(summary);
749
+ });
750
+ // POST /api/v1/sessions/:id/dispatch → 202; replaces the #715 stub
751
+ app.post('/api/v1/sessions/:id/dispatch', async (c) => {
752
+ const sessionId = c.req.param('id');
753
+ let body;
754
+ try {
755
+ body = await c.req.json();
756
+ }
757
+ catch {
758
+ return c.json({ error: 'Invalid JSON body' }, 400);
759
+ }
760
+ // 1. Validate payload against schema
761
+ const { valid, errors } = validateDispatchPayload(body);
762
+ if (!valid) {
763
+ return c.json({ error: `Invalid dispatch payload: ${errors}` }, 400);
764
+ }
765
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
766
+ const payload = body;
767
+ const missionId = payload.mission_id;
768
+ const longRunning = payload.long_running === true;
769
+ const executorFilter = payload.executor_filter ?? {};
770
+ // 2. Resolve executor via registry's pickByFilter
771
+ const pickResult = executorRegistry.pickByFilter(executorFilter, longRunning);
772
+ if (!pickResult) {
773
+ if (longRunning) {
774
+ return c.json({ error: 'no_resumable_executor_available' }, 503);
775
+ }
776
+ return c.json({ error: 'no_executor_available' }, 503);
777
+ }
778
+ const { executor } = pickResult;
779
+ // 3. Forward dispatch via the dispatch router (#1252): try A2A v2 first,
780
+ // fall back to v1 /dispatch on 404 with a structured warning.
781
+ let estimatedStart;
782
+ let a2aInstanceId;
783
+ let dispatchPath = 'v2';
784
+ let a2aTask = undefined;
785
+ try {
786
+ const result = await routeDispatch(executor, payload, {
787
+ onV1Fallback: (info) => {
788
+ logServeWarn('dispatch', `v1 fallback for executor ${info.executorId}: ${info.reason}`);
789
+ telemetryStore.ingest(createEvent('v1.dispatch.fallback', sessionId, info, missionId));
790
+ },
791
+ onDeprecation: (info) => {
792
+ logServeWarn('dispatch', `v1 deprecation observed at ${info.path}` +
793
+ (info.sunset ? ` (sunset: ${info.sunset})` : ''));
794
+ telemetryStore.ingest(createEvent('v1.deprecation.observed', sessionId, { ...info }, missionId));
795
+ },
796
+ });
797
+ dispatchPath = result.dispatchPath;
798
+ a2aInstanceId = result.a2aInstanceId;
799
+ a2aTask = result.task;
800
+ if (result.estimatedStart)
801
+ estimatedStart = result.estimatedStart;
802
+ }
803
+ catch (err) {
804
+ const msg = err.message ?? String(err);
805
+ logServeWarn('dispatch', `executor ${executor.executorId} dispatch failed: ${msg}`);
806
+ executorRegistry.assignMission(missionId, executor.executorId);
807
+ executorRegistry.failMission(missionId, msg);
808
+ telemetryStore.ingest(createEvent('mission.abort', sessionId, {
809
+ missionId,
810
+ executorId: executor.executorId,
811
+ error: msg,
812
+ }, missionId));
813
+ // Distinguish unreachable (network/timeout) from forward errors.
814
+ const status = /v1 dispatch failed: \d/.test(msg) ? 502 : 502;
815
+ const errorTag = /unreachable|ECONN|fetch failed/i.test(msg)
816
+ ? 'executor_unreachable'
817
+ : 'executor_forward_failed';
818
+ return c.json({ error: errorTag, detail: msg }, status);
819
+ }
820
+ // 4. Record the mission and emit telemetry
821
+ executorRegistry.assignMission(missionId, executor.executorId);
822
+ if (dispatchPath === 'v2' && a2aTask && a2aInstanceId) {
823
+ void observeA2ATerminalState(executorRegistry, executor, missionId, a2aInstanceId, a2aTask, {
824
+ onError: (err) => {
825
+ logServeWarn('dispatch', `A2A terminal observer failed for mission ${missionId}: ${err.message ?? String(err)}`);
826
+ },
827
+ });
828
+ }
829
+ telemetryStore.ingest(createEvent('mission.dispatch', sessionId, {
830
+ missionId,
831
+ executorId: executor.executorId,
832
+ objective: payload.objective,
833
+ completion: payload.completion,
834
+ }, missionId));
835
+ // 5. Return 202 Accepted
836
+ const dispatchResp = {
837
+ mission_id: missionId,
838
+ executor_id: executor.executorId,
839
+ status: 'assigned',
840
+ dispatch_path: dispatchPath,
841
+ };
842
+ if (a2aInstanceId)
843
+ dispatchResp.a2a_instance_id = a2aInstanceId;
844
+ if (estimatedStart)
845
+ dispatchResp.estimated_start = estimatedStart;
846
+ return c.json(dispatchResp, 202);
847
+ });
848
+ // GET /api/v1/missions/:id → mission status snapshot
849
+ app.get('/api/v1/missions/:id', (c) => {
850
+ const mission = executorRegistry.getMission(c.req.param('id'));
851
+ if (!mission)
852
+ return c.json({ error: 'Mission not found' }, 404);
853
+ return c.json({
854
+ mission_id: mission.missionId,
855
+ executor_id: mission.executorId,
856
+ state: mission.state,
857
+ created_at: mission.createdAt,
858
+ updated_at: mission.updatedAt,
859
+ completed_at: mission.completedAt,
860
+ recent_events: mission.recentEvents,
861
+ pty_session_ref: mission.ptySessionRef,
862
+ exit_code: mission.exitCode,
863
+ error: mission.error,
864
+ });
865
+ });
866
+ // POST /api/v1/missions/:id/hitl_response → 200; forwards to owning executor over WS
867
+ app.post('/api/v1/missions/:id/hitl_response', async (c) => {
868
+ const missionId = c.req.param('id');
869
+ const mission = executorRegistry.getMission(missionId);
870
+ if (!mission)
871
+ return c.json({ error: 'Mission not found' }, 404);
872
+ let body;
873
+ try {
874
+ body = await c.req.json();
875
+ }
876
+ catch {
877
+ return c.json({ error: 'Invalid JSON body' }, 400);
878
+ }
879
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
880
+ const payload = body;
881
+ if (!payload.hitl_id || !payload.response) {
882
+ return c.json({ error: 'hitl_id and response are required' }, 400);
883
+ }
884
+ // Push hitl_responded event to the executor over WS
885
+ const envelope = {
886
+ event: 'mission.hitl_responded',
887
+ executor_id: mission.executorId,
888
+ mission_id: missionId,
889
+ ts: new Date().toISOString(),
890
+ data: {
891
+ hitl_id: payload.hitl_id,
892
+ response: payload.response,
893
+ responded_at: new Date().toISOString(),
894
+ },
895
+ };
896
+ const pushed = executorRegistry.pushToExecutor(mission.executorId, envelope);
897
+ if (!pushed) {
898
+ return c.json({ error: 'executor_not_connected' }, 503);
899
+ }
900
+ return c.json({ ok: true });
901
+ });
902
+ // POST /api/v1/missions/:id/pause → 200
903
+ app.post('/api/v1/missions/:id/pause', (c) => {
904
+ const missionId = c.req.param('id');
905
+ const mission = executorRegistry.getMission(missionId);
906
+ if (!mission)
907
+ return c.json({ error: 'Mission not found' }, 404);
908
+ // Forward pause command to executor over WS
909
+ executorRegistry.pushToExecutor(mission.executorId, {
910
+ event: 'mission.paused',
911
+ executor_id: mission.executorId,
912
+ mission_id: missionId,
913
+ ts: new Date().toISOString(),
914
+ data: { state: 'paused', reason: 'operator_request' },
915
+ });
916
+ executorRegistry.transitionMission(missionId, 'paused');
917
+ return c.json({ ok: true });
918
+ });
919
+ // POST /api/v1/missions/:id/resume → 200
920
+ app.post('/api/v1/missions/:id/resume', (c) => {
921
+ const missionId = c.req.param('id');
922
+ const mission = executorRegistry.getMission(missionId);
923
+ if (!mission)
924
+ return c.json({ error: 'Mission not found' }, 404);
925
+ executorRegistry.pushToExecutor(mission.executorId, {
926
+ event: 'mission.resumed',
927
+ executor_id: mission.executorId,
928
+ mission_id: missionId,
929
+ ts: new Date().toISOString(),
930
+ data: { state: 'running', resumed_from: 'paused' },
931
+ });
932
+ executorRegistry.transitionMission(missionId, 'running');
933
+ return c.json({ ok: true });
934
+ });
935
+ // POST /api/v1/missions/:id/abort → 200
936
+ app.post('/api/v1/missions/:id/abort', (c) => {
937
+ const missionId = c.req.param('id');
938
+ const mission = executorRegistry.getMission(missionId);
939
+ if (!mission)
940
+ return c.json({ error: 'Mission not found' }, 404);
941
+ executorRegistry.pushToExecutor(mission.executorId, {
942
+ event: 'mission.aborted',
943
+ executor_id: mission.executorId,
944
+ mission_id: missionId,
945
+ ts: new Date().toISOString(),
946
+ data: { state: 'aborted', aborted_by: 'operator', reason: 'operator_request' },
947
+ });
948
+ executorRegistry.transitionMission(missionId, 'aborted');
949
+ return c.json({ ok: true });
950
+ });
951
+ // ── A2A push notification webhook receiver (#1256) ────────────────────
952
+ //
953
+ // Receives StreamResponse-shape payloads pushed by an agentic-sandbox
954
+ // executor when SSE isn't available (e.g. serverless missions). HMAC
955
+ // verification is on the raw body, replay window is 5 minutes,
956
+ // event-id idempotency is in-process. Per-mission secrets are
957
+ // registered via POST /api/v1/push-configs.
958
+ //
959
+ // The endpoint path is configurable via env so deployments behind a
960
+ // load-balancer with path rewriting can match upstream.
961
+ const webhookPath = process.env['AIWG_A2A_WEBHOOK_PATH'] ?? '/aiwg/webhooks/a2a';
962
+ app.post(webhookPath, async (c) => {
963
+ const configId = c.req.query('configId') ?? c.req.query('config_id') ?? '';
964
+ const signature = c.req.header('x-aiwg-signature') ?? c.req.header('X-AIWG-Signature') ?? undefined;
965
+ const eventId = c.req.header('x-aiwg-event-id') ?? c.req.header('X-AIWG-Event-Id') ?? undefined;
966
+ // Read raw body bytes — signature is computed over UTF-8 bytes.
967
+ const rawText = await c.req.text();
968
+ const bodyBuf = Buffer.from(rawText, 'utf8');
969
+ const result = await handleWebhook(configId, bodyBuf, signature, eventId, {
970
+ registry: pushSecretRegistry,
971
+ idempotency: webhookIdempotency,
972
+ route: async (entry, event) => {
973
+ // Append to mission recentEvents via a synthesized envelope.
974
+ // The 'mission.webhook' event type falls through the registry's
975
+ // default case (no state mutation), but timestamp + telemetry
976
+ // still record the delivery for operator visibility.
977
+ if (entry.missionId) {
978
+ const mission = executorRegistry.getMission(entry.missionId);
979
+ if (mission) {
980
+ executorRegistry.handleEvent({
981
+ event: 'mission.webhook',
982
+ executor_id: mission.executorId,
983
+ mission_id: entry.missionId,
984
+ ts: new Date().toISOString(),
985
+ data: { stream_event: event },
986
+ });
987
+ }
988
+ }
989
+ telemetryStore.ingest(createEvent('a2a.webhook.received', entry.missionId ?? entry.configId, {
990
+ configId: entry.configId,
991
+ ...(entry.taskId ? { taskId: entry.taskId } : {}),
992
+ }));
993
+ },
994
+ });
995
+ return c.json(result.body, result.status);
996
+ });
997
+ // POST /api/v1/push-configs → register a per-mission webhook secret.
998
+ // Returns { configId } so the caller can pass it to
999
+ // A2AClient.createPushNotificationConfig with `metadata.aiwg.config_id`.
1000
+ app.post('/api/v1/push-configs', async (c) => {
1001
+ let body;
1002
+ try {
1003
+ body = await c.req.json();
1004
+ }
1005
+ catch {
1006
+ return c.json({ error: 'Invalid JSON body' }, 400);
1007
+ }
1008
+ const p = body;
1009
+ if (!p.configId || typeof p.configId !== 'string') {
1010
+ return c.json({ error: 'configId is required' }, 400);
1011
+ }
1012
+ if (!p.secret || typeof p.secret !== 'string' || p.secret.length < 16) {
1013
+ return c.json({ error: 'secret is required and must be ≥16 chars' }, 400);
1014
+ }
1015
+ pushSecretRegistry.register({
1016
+ configId: p.configId,
1017
+ secret: p.secret,
1018
+ ...(p.missionId ? { missionId: p.missionId } : {}),
1019
+ ...(p.taskId ? { taskId: p.taskId } : {}),
1020
+ ...(p.metadata ? { metadata: p.metadata } : {}),
1021
+ });
1022
+ return c.json({ ok: true, configId: p.configId }, 201);
1023
+ });
1024
+ // DELETE /api/v1/push-configs/:configId → unregister on mission complete.
1025
+ app.delete('/api/v1/push-configs/:configId', (c) => {
1026
+ const configId = c.req.param('configId');
1027
+ const removed = pushSecretRegistry.unregister(configId);
1028
+ if (!removed)
1029
+ return c.json({ error: 'configId not found' }, 404);
1030
+ return new Response(null, { status: 204 });
1031
+ });
1032
+ // GET /api/v1/push-configs → debug surface; lists registered configIds.
1033
+ app.get('/api/v1/push-configs', (_c) => {
1034
+ return _c.json({ count: pushSecretRegistry.size() });
1035
+ });
1036
+ // Telemetry API (#716)
1037
+ app.get('/api/telemetry', (c) => {
1038
+ const sid = c.req.query('sessionId');
1039
+ const limit = parseInt(c.req.query('limit') ?? '100', 10);
1040
+ const events = telemetryStore.query(sid || 'default', { limit });
1041
+ return c.json({ events });
1042
+ });
1043
+ app.get('/api/telemetry/metrics', (c) => {
1044
+ const sid = c.req.query('sessionId') || 'default';
1045
+ return c.json(telemetryStore.metrics(sid));
1046
+ });
1047
+ app.post('/api/telemetry', async (c) => {
1048
+ try {
1049
+ const body = await c.req.json();
1050
+ telemetryStore.ingest(body);
1051
+ return c.json({ ok: true }, 201);
1052
+ }
1053
+ catch {
1054
+ return c.json({ error: 'Invalid event' }, 400);
1055
+ }
1056
+ });
1057
+ if (!opts.readOnly) {
1058
+ app.post('/api/sessions', (c) => c.json({ id: null, error: 'Use /ws/pty/:sessionId to start a PTY session' }, 501));
1059
+ }
1060
+ // ---- Sandbox Registration API (#731) ----
1061
+ // Register a sandbox instance
1062
+ app.post('/api/sandboxes/register', async (c) => {
1063
+ try {
1064
+ const body = await c.req.json();
1065
+ if (!body.name || !body.grpc_endpoint || !body.ws_endpoint || !body.http_endpoint) {
1066
+ return c.json({ error: 'Missing required fields: name, grpc_endpoint, ws_endpoint, http_endpoint' }, 400);
1067
+ }
1068
+ const result = sandboxRegistry.register(body);
1069
+ return c.json(result, 201);
1070
+ }
1071
+ catch {
1072
+ return c.json({ error: 'Invalid request body' }, 400);
1073
+ }
1074
+ });
1075
+ // Clear all disconnected sandboxes (forces re-registration)
1076
+ app.delete('/api/sandboxes/offline', (c) => {
1077
+ const removed = sandboxRegistry.clearOffline();
1078
+ return c.json({ ok: true, removed });
1079
+ });
1080
+ // Admin-force deregister any sandbox by id (no sandbox token required — local dashboard only).
1081
+ // Idempotent: returns ok even if the sandbox is already gone (e.g. server restarted since last poll).
1082
+ app.delete('/api/sandboxes/:id/forget', (c) => {
1083
+ sandboxRegistry.deregister(c.req.param('id'));
1084
+ return c.json({ ok: true });
1085
+ });
1086
+ // Deregister a sandbox
1087
+ app.delete('/api/sandboxes/:id', (c) => {
1088
+ const id = c.req.param('id');
1089
+ const token = (c.req.header('authorization') ?? '').replace(/^Bearer\s+/i, '');
1090
+ if (!sandboxRegistry.authenticate(id, token)) {
1091
+ return c.json({ error: 'Unauthorized' }, 401);
1092
+ }
1093
+ sandboxRegistry.deregister(id);
1094
+ return c.json({ ok: true });
1095
+ });
1096
+ // List all registered sandboxes
1097
+ app.get('/api/sandboxes', (c) => {
1098
+ return c.json({ sandboxes: sandboxRegistry.list() });
1099
+ });
1100
+ // Get a single sandbox
1101
+ app.get('/api/sandboxes/:id', (c) => {
1102
+ const summary = sandboxRegistry.getSummary(c.req.param('id'));
1103
+ if (!summary)
1104
+ return c.json({ error: 'Sandbox not found' }, 404);
1105
+ return c.json(summary);
1106
+ });
1107
+ // List agents for a specific sandbox
1108
+ app.get('/api/sandboxes/:id/agents', (c) => {
1109
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1110
+ if (!sandbox)
1111
+ return c.json({ error: 'Sandbox not found' }, 404);
1112
+ return c.json({ agents: [...sandbox.agents.values()] });
1113
+ });
1114
+ // List all agents across all sandboxes
1115
+ app.get('/api/agents', (c) => {
1116
+ return c.json({ agents: sandboxRegistry.allAgents() });
1117
+ });
1118
+ // Routing candidates — returns all agents ranked by filter match + load score (#916)
1119
+ // Query params map directly to AgentFilter fields (JSON-encoded or comma-separated).
1120
+ app.get('/api/agents/candidates', (c) => {
1121
+ const q = c.req.query();
1122
+ const filter = {};
1123
+ if (q.sandbox_id)
1124
+ filter.sandbox_id = q.sandbox_id;
1125
+ if (q.agent_id)
1126
+ filter.agent_id = q.agent_id;
1127
+ if (q.agent_name)
1128
+ filter.agent_name = q.agent_name;
1129
+ if (q.frameworks)
1130
+ filter.frameworks = q.frameworks.split(',').filter(Boolean);
1131
+ if (q.agents)
1132
+ filter.agents = q.agents.split(',').filter(Boolean);
1133
+ if (q.skills)
1134
+ filter.skills = q.skills.split(',').filter(Boolean);
1135
+ if (q.max_cpu_percent)
1136
+ filter.max_cpu_percent = parseFloat(q.max_cpu_percent);
1137
+ if (q.min_memory_gb)
1138
+ filter.min_memory_gb = parseFloat(q.min_memory_gb);
1139
+ const allAgents = sandboxRegistry.allAgents();
1140
+ const result = routeTask(allAgents, filter);
1141
+ return c.json(result);
1142
+ });
1143
+ // Proxy endpoints for sandbox lifecycle (#733)
1144
+ // These forward to the registered sandbox's HTTP endpoint
1145
+ app.get('/api/sandboxes/:id/loadouts', async (c) => {
1146
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1147
+ if (!sandbox)
1148
+ return c.json({ error: 'Sandbox not found' }, 404);
1149
+ try {
1150
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/loadouts`);
1151
+ return c.json(await resp.json(), resp.status);
1152
+ }
1153
+ catch (err) {
1154
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1155
+ }
1156
+ });
1157
+ // List VMs running inside a sandbox (#930)
1158
+ // Proxies GET /api/v1/vms on the sandbox HTTP endpoint.
1159
+ // Supports `state` and `prefix` query params per sandbox API contract.
1160
+ app.get('/api/sandboxes/:id/vms', async (c) => {
1161
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1162
+ if (!sandbox)
1163
+ return c.json({ error: 'Sandbox not found' }, 404);
1164
+ try {
1165
+ const url = new URL(`${sandbox.httpEndpoint}/api/v1/vms`);
1166
+ const state = c.req.query('state');
1167
+ const prefix = c.req.query('prefix');
1168
+ if (state)
1169
+ url.searchParams.set('state', state);
1170
+ if (prefix)
1171
+ url.searchParams.set('prefix', prefix);
1172
+ const resp = await fetch(url.toString());
1173
+ return c.json(await resp.json(), resp.status);
1174
+ }
1175
+ catch (err) {
1176
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1177
+ }
1178
+ });
1179
+ // Get a single VM's details — resource allocation, IP, attached agent (#930)
1180
+ app.get('/api/sandboxes/:id/vms/:name', async (c) => {
1181
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1182
+ if (!sandbox)
1183
+ return c.json({ error: 'Sandbox not found' }, 404);
1184
+ try {
1185
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/vms/${encodeURIComponent(c.req.param('name'))}`);
1186
+ return c.json(await resp.json(), resp.status);
1187
+ }
1188
+ catch (err) {
1189
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1190
+ }
1191
+ });
1192
+ // Authoritative agent list from sandbox HTTP API (#930).
1193
+ // Complements the event-cached view at /api/sandboxes/:id/agents by calling
1194
+ // the sandbox's own /api/v1/agents — useful when events have been missed or
1195
+ // when the dashboard needs full system_info / metrics not captured in events.
1196
+ app.get('/api/sandboxes/:id/agents/full', async (c) => {
1197
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1198
+ if (!sandbox)
1199
+ return c.json({ error: 'Sandbox not found' }, 404);
1200
+ try {
1201
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents`);
1202
+ return c.json(await resp.json(), resp.status);
1203
+ }
1204
+ catch (err) {
1205
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1206
+ }
1207
+ });
1208
+ // Proxy the loadout registry (frameworks/providers/init scripts/presets)
1209
+ // used by the UI's compose builder.
1210
+ app.get('/api/sandboxes/:id/loadout-registry', async (c) => {
1211
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1212
+ if (!sandbox)
1213
+ return c.json({ error: 'Sandbox not found' }, 404);
1214
+ try {
1215
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/loadout/registry`);
1216
+ return c.json(await resp.json(), resp.status);
1217
+ }
1218
+ catch (err) {
1219
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1220
+ }
1221
+ });
1222
+ // Pass-through proxy for VM creation. Forwards the request body verbatim to
1223
+ // the sandbox /api/v1/vms endpoint so the UI can use either preset
1224
+ // (profile/loadout) or custom (composition) shapes without translation loss.
1225
+ app.post('/api/sandboxes/:id/provision', async (c) => {
1226
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1227
+ if (!sandbox)
1228
+ return c.json({ error: 'Sandbox not found' }, 404);
1229
+ try {
1230
+ const body = await c.req.json();
1231
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/vms`, {
1232
+ method: 'POST',
1233
+ headers: { 'Content-Type': 'application/json' },
1234
+ body: JSON.stringify(body),
1235
+ });
1236
+ return c.json(await resp.json(), resp.status);
1237
+ }
1238
+ catch (err) {
1239
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1240
+ }
1241
+ });
1242
+ // Proxy agent lifecycle actions to sandbox
1243
+ for (const action of ['start', 'stop', 'destroy', 'reprovision']) {
1244
+ app.post(`/api/sandboxes/:id/agents/:aid/${action}`, async (c) => {
1245
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1246
+ if (!sandbox)
1247
+ return c.json({ error: 'Sandbox not found' }, 404);
1248
+ try {
1249
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/${action}`, {
1250
+ method: 'POST',
1251
+ });
1252
+ return c.json(await resp.json(), resp.status);
1253
+ }
1254
+ catch (err) {
1255
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1256
+ }
1257
+ });
1258
+ }
1259
+ app.delete('/api/sandboxes/:id/agents/:aid', async (c) => {
1260
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1261
+ if (!sandbox)
1262
+ return c.json({ error: 'Sandbox not found' }, 404);
1263
+ try {
1264
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}`, {
1265
+ method: 'DELETE',
1266
+ });
1267
+ return c.json(await resp.json(), resp.status);
1268
+ }
1269
+ catch (err) {
1270
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1271
+ }
1272
+ });
1273
+ // ---- VM lifecycle proxies (#1146) ----
1274
+ // Mirror /api/v1/vms/{name}/{start,stop,restart,destroy,deploy-agent} and
1275
+ // DELETE /api/v1/vms/{name}. Bodies and query strings are forwarded as-is so
1276
+ // callers can pass libvirt-specific options (mode, timeout_seconds, force,
1277
+ // delete_disk, etc.) without AIWG having to know about them.
1278
+ for (const action of ['start', 'stop', 'restart', 'destroy', 'deploy-agent']) {
1279
+ app.post(`/api/sandboxes/:id/vms/:name/${action}`, async (c) => {
1280
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1281
+ if (!sandbox)
1282
+ return c.json({ error: 'Sandbox not found' }, 404);
1283
+ try {
1284
+ const body = await c.req.text().catch(() => '');
1285
+ const headers = {};
1286
+ const ct = c.req.header('content-type');
1287
+ if (body && ct)
1288
+ headers['Content-Type'] = ct;
1289
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/vms/${encodeURIComponent(c.req.param('name'))}/${action}`, { method: 'POST', headers, body: body || undefined });
1290
+ const text = await resp.text();
1291
+ return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
1292
+ }
1293
+ catch (err) {
1294
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1295
+ }
1296
+ });
1297
+ }
1298
+ app.delete('/api/sandboxes/:id/vms/:name', async (c) => {
1299
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1300
+ if (!sandbox)
1301
+ return c.json({ error: 'Sandbox not found' }, 404);
1302
+ try {
1303
+ const url = new URL(`${sandbox.httpEndpoint}/api/v1/vms/${encodeURIComponent(c.req.param('name'))}`);
1304
+ const force = c.req.query('force');
1305
+ const deleteDisk = c.req.query('delete_disk');
1306
+ if (force)
1307
+ url.searchParams.set('force', force);
1308
+ if (deleteDisk)
1309
+ url.searchParams.set('delete_disk', deleteDisk);
1310
+ const resp = await fetch(url.toString(), { method: 'DELETE' });
1311
+ const text = await resp.text();
1312
+ return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
1313
+ }
1314
+ catch (err) {
1315
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1316
+ }
1317
+ });
1318
+ // ---- Containers proxies (#1146) ----
1319
+ // Mirror /api/v1/containers (list/get/create/delete/start/stop) and
1320
+ // /api/v1/container-images. Same pattern as the /vms proxy block — thin
1321
+ // pass-through so the dashboard's container surface lights up identically
1322
+ // to the agentic-sandbox dashboard's Instances panel.
1323
+ app.get('/api/sandboxes/:id/containers', async (c) => {
1324
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1325
+ if (!sandbox)
1326
+ return c.json({ error: 'Sandbox not found' }, 404);
1327
+ try {
1328
+ const url = new URL(`${sandbox.httpEndpoint}/api/v1/containers`);
1329
+ const status = c.req.query('status');
1330
+ if (status)
1331
+ url.searchParams.set('status', status);
1332
+ const resp = await fetch(url.toString());
1333
+ return c.json(await resp.json(), resp.status);
1334
+ }
1335
+ catch (err) {
1336
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1337
+ }
1338
+ });
1339
+ app.get('/api/sandboxes/:id/containers/:name', async (c) => {
1340
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1341
+ if (!sandbox)
1342
+ return c.json({ error: 'Sandbox not found' }, 404);
1343
+ try {
1344
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers/${encodeURIComponent(c.req.param('name'))}`);
1345
+ return c.json(await resp.json(), resp.status);
1346
+ }
1347
+ catch (err) {
1348
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1349
+ }
1350
+ });
1351
+ app.post('/api/sandboxes/:id/containers', async (c) => {
1352
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1353
+ if (!sandbox)
1354
+ return c.json({ error: 'Sandbox not found' }, 404);
1355
+ try {
1356
+ const body = await c.req.json();
1357
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers`, {
1358
+ method: 'POST',
1359
+ headers: { 'Content-Type': 'application/json' },
1360
+ body: JSON.stringify(body),
1361
+ });
1362
+ return c.json(await resp.json(), resp.status);
1363
+ }
1364
+ catch (err) {
1365
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1366
+ }
1367
+ });
1368
+ for (const action of ['start', 'stop']) {
1369
+ app.post(`/api/sandboxes/:id/containers/:name/${action}`, async (c) => {
1370
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1371
+ if (!sandbox)
1372
+ return c.json({ error: 'Sandbox not found' }, 404);
1373
+ try {
1374
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers/${encodeURIComponent(c.req.param('name'))}/${action}`, { method: 'POST' });
1375
+ const text = await resp.text();
1376
+ return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
1377
+ }
1378
+ catch (err) {
1379
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1380
+ }
1381
+ });
1382
+ }
1383
+ app.delete('/api/sandboxes/:id/containers/:name', async (c) => {
1384
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1385
+ if (!sandbox)
1386
+ return c.json({ error: 'Sandbox not found' }, 404);
1387
+ try {
1388
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers/${encodeURIComponent(c.req.param('name'))}`, { method: 'DELETE' });
1389
+ const text = await resp.text();
1390
+ return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
1391
+ }
1392
+ catch (err) {
1393
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1394
+ }
1395
+ });
1396
+ app.get('/api/sandboxes/:id/container-images', async (c) => {
1397
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1398
+ if (!sandbox)
1399
+ return c.json({ error: 'Sandbox not found' }, 404);
1400
+ try {
1401
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/container-images`);
1402
+ return c.json(await resp.json(), resp.status);
1403
+ }
1404
+ catch (err) {
1405
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1406
+ }
1407
+ });
1408
+ // Agent identity alias endpoint (#917)
1409
+ // Assigns a stable human-readable logical name to an agent.
1410
+ // Persists in ~/.config/aiwg/sandbox-agents.json so name survives AIWG restarts.
1411
+ app.post('/api/sandboxes/:id/agents/:aid/alias', async (c) => {
1412
+ const sandboxId = c.req.param('id');
1413
+ const agentId = c.req.param('aid');
1414
+ const body = await c.req.json().catch(() => ({}));
1415
+ const { name } = body;
1416
+ if (!name || typeof name !== 'string' || !name.trim()) {
1417
+ return c.json({ error: 'name is required' }, 400);
1418
+ }
1419
+ const ok = sandboxRegistry.aliasAgent(sandboxId, agentId, name.trim());
1420
+ if (!ok)
1421
+ return c.json({ error: 'Agent or sandbox not found' }, 404);
1422
+ return c.json({ ok: true, logicalName: name.trim() });
1423
+ });
1424
+ // Resolve agent by agentId, instanceId, or logicalName (#917)
1425
+ app.get('/api/agents/resolve/:ref', (c) => {
1426
+ const ref = c.req.param('ref');
1427
+ const result = sandboxRegistry.resolveAgent(ref);
1428
+ if (!result)
1429
+ return c.json({ error: `Agent '${ref}' not found` }, 404);
1430
+ return c.json({
1431
+ sandboxId: result.sandbox.id,
1432
+ sandboxName: result.sandbox.name,
1433
+ agent: result.agent,
1434
+ });
1435
+ });
1436
+ // List known agent identities from persistent store (#917)
1437
+ app.get('/api/agents/identities', (c) => {
1438
+ return c.json({ identities: sandboxRegistry.knownAgentIdentities() });
1439
+ });
1440
+ // Session management proxy endpoints (#896)
1441
+ // Forwards to sandbox management HTTP API: GET/POST /api/v1/agents/:aid/sessions
1442
+ // and DELETE /api/v1/agents/:aid/sessions/:session
1443
+ // Live since agentic-sandbox v2026.7.x (#140 sessions API, #611 host-runtime
1444
+ // session listing); the 502 path is now only a genuine-unreachability fallback.
1445
+ app.get('/api/sandboxes/:id/agents/:aid/sessions', async (c) => {
1446
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1447
+ if (!sandbox)
1448
+ return c.json({ error: 'Sandbox not found' }, 404);
1449
+ try {
1450
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/sessions`);
1451
+ return c.json(await resp.json(), resp.status);
1452
+ }
1453
+ catch (err) {
1454
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1455
+ }
1456
+ });
1457
+ app.post('/api/sandboxes/:id/agents/:aid/sessions', async (c) => {
1458
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1459
+ if (!sandbox)
1460
+ return c.json({ error: 'Sandbox not found' }, 404);
1461
+ try {
1462
+ const body = await c.req.json().catch(() => ({}));
1463
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/sessions`, {
1464
+ method: 'POST',
1465
+ headers: { 'Content-Type': 'application/json' },
1466
+ body: JSON.stringify(body),
1467
+ });
1468
+ return c.json(await resp.json(), resp.status);
1469
+ }
1470
+ catch (err) {
1471
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1472
+ }
1473
+ });
1474
+ // DELETE /api/sandboxes/:id/agents/:aid/sessions/:session
1475
+ // Path key `:session` is the session_name (not session_id) — sandbox returns 204 No Content.
1476
+ app.delete('/api/sandboxes/:id/agents/:aid/sessions/:session', async (c) => {
1477
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1478
+ if (!sandbox)
1479
+ return c.json({ error: 'Sandbox not found' }, 404);
1480
+ try {
1481
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/sessions/${c.req.param('session')}`, { method: 'DELETE' });
1482
+ if (resp.status === 204)
1483
+ return new Response(null, { status: 204 });
1484
+ return c.json(await resp.json(), resp.status);
1485
+ }
1486
+ catch (err) {
1487
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1488
+ }
1489
+ });
1490
+ // HITL endpoints (#732)
1491
+ app.get('/api/hitl', (c) => {
1492
+ return c.json({ requests: sandboxRegistry.pendingHitl() });
1493
+ });
1494
+ app.post('/api/hitl/:id/respond', async (c) => {
1495
+ const hitlId = c.req.param('id');
1496
+ // Use getHitl (not resolveHitl) — don't remove from queue until sandbox confirms (#908)
1497
+ const hitl = sandboxRegistry.getHitl(hitlId);
1498
+ if (!hitl)
1499
+ return c.json({ error: 'HITL request not found or already resolved' }, 404);
1500
+ const sandbox = sandboxRegistry.get(hitl.sandboxId);
1501
+ if (!sandbox)
1502
+ return c.json({ error: 'Sandbox no longer registered' }, 410);
1503
+ try {
1504
+ const { text } = await c.req.json();
1505
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/hitl/${hitlId}/respond`, {
1506
+ method: 'POST',
1507
+ headers: { 'Content-Type': 'application/json' },
1508
+ body: JSON.stringify({ text }),
1509
+ });
1510
+ if (resp.ok || resp.status === 204) {
1511
+ // Remove from queue only after sandbox confirms receipt (#908)
1512
+ sandboxRegistry.resolveHitl(hitlId);
1513
+ // Notify browser subscribers that the HITL was responded to (#908)
1514
+ sandboxRegistry.emit({
1515
+ type: 'hitl.responded',
1516
+ sandboxId: hitl.sandboxId,
1517
+ agentId: hitl.agentId,
1518
+ timestamp: new Date().toISOString(),
1519
+ hitlId,
1520
+ sessionId: hitl.sessionId,
1521
+ });
1522
+ }
1523
+ // Return 200 regardless of sandbox's 204 (body-less responses cause issues on some clients)
1524
+ return c.json({ ok: resp.ok }, resp.ok ? 200 : resp.status);
1525
+ }
1526
+ catch (err) {
1527
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1528
+ }
1529
+ });
1530
+ app.post('/api/hitl/:id/dismiss', (c) => {
1531
+ const hitl = sandboxRegistry.resolveHitl(c.req.param('id'));
1532
+ if (!hitl)
1533
+ return c.json({ error: 'HITL request not found or already resolved' }, 404);
1534
+ return c.json({ ok: true });
1535
+ });
1536
+ // Task proxy endpoints (#907)
1537
+ // Forward to sandbox /api/v1/tasks — enables the serve dashboard task submission UI
1538
+ app.get('/api/sandboxes/:id/tasks', async (c) => {
1539
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1540
+ if (!sandbox)
1541
+ return c.json({ error: 'Sandbox not found' }, 404);
1542
+ try {
1543
+ const url = new URL(`${sandbox.httpEndpoint}/api/v1/tasks`);
1544
+ const qs = c.req.query();
1545
+ for (const [k, v] of Object.entries(qs))
1546
+ url.searchParams.set(k, v);
1547
+ const resp = await fetch(url.toString());
1548
+ return c.json(await resp.json(), resp.status);
1549
+ }
1550
+ catch (err) {
1551
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1552
+ }
1553
+ });
1554
+ app.post('/api/sandboxes/:id/tasks', async (c) => {
1555
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1556
+ if (!sandbox)
1557
+ return c.json({ error: 'Sandbox not found' }, 404);
1558
+ try {
1559
+ const body = await c.req.json().catch(() => ({}));
1560
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/tasks`, {
1561
+ method: 'POST',
1562
+ headers: { 'Content-Type': 'application/json' },
1563
+ body: JSON.stringify(body),
1564
+ });
1565
+ return c.json(await resp.json(), resp.status);
1566
+ }
1567
+ catch (err) {
1568
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1569
+ }
1570
+ });
1571
+ app.get('/api/sandboxes/:id/tasks/:taskId', async (c) => {
1572
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1573
+ if (!sandbox)
1574
+ return c.json({ error: 'Sandbox not found' }, 404);
1575
+ try {
1576
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/tasks/${c.req.param('taskId')}`);
1577
+ return c.json(await resp.json(), resp.status);
1578
+ }
1579
+ catch (err) {
1580
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1581
+ }
1582
+ });
1583
+ app.delete('/api/sandboxes/:id/tasks/:taskId', async (c) => {
1584
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1585
+ if (!sandbox)
1586
+ return c.json({ error: 'Sandbox not found' }, 404);
1587
+ try {
1588
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/tasks/${c.req.param('taskId')}`, {
1589
+ method: 'DELETE',
1590
+ });
1591
+ if (resp.status === 204)
1592
+ return new Response(null, { status: 204 });
1593
+ return c.json(await resp.json(), resp.status);
1594
+ }
1595
+ catch (err) {
1596
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1597
+ }
1598
+ });
1599
+ // Screen state proxy (#913)
1600
+ // Forwards to sandbox GET /api/v1/sessions/:sessionId/screen — already exists in sandbox.
1601
+ app.get('/api/sandboxes/:id/sessions/:sessionId/screen', async (c) => {
1602
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1603
+ if (!sandbox)
1604
+ return c.json({ error: 'Sandbox not found' }, 404);
1605
+ try {
1606
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/sessions/${c.req.param('sessionId')}/screen`);
1607
+ return c.json(await resp.json(), resp.status);
1608
+ }
1609
+ catch (err) {
1610
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1611
+ }
1612
+ });
1613
+ // Remote AIWG exec proxy (#914)
1614
+ // Forwards to sandbox POST /api/v1/aiwg/exec — requires sandbox companion issue.
1615
+ // Will 502 gracefully until that endpoint is implemented.
1616
+ app.post('/api/sandboxes/:id/agents/:aid/aiwg/exec', async (c) => {
1617
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1618
+ if (!sandbox)
1619
+ return c.json({ error: 'Sandbox not found' }, 404);
1620
+ try {
1621
+ const body = await c.req.json().catch(() => ({}));
1622
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/aiwg/exec`, {
1623
+ method: 'POST',
1624
+ headers: { 'Content-Type': 'application/json' },
1625
+ body: JSON.stringify(body),
1626
+ });
1627
+ return c.json(await resp.json(), resp.status);
1628
+ }
1629
+ catch (err) {
1630
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1631
+ }
1632
+ });
1633
+ // Framework update proxy (#910)
1634
+ // Instructs a sandbox agent to update a named framework.
1635
+ // Requires sandbox companion issue (PATCH /api/v1/agents/:id/frameworks/:name).
1636
+ app.patch('/api/sandboxes/:id/agents/:aid/frameworks/:name', async (c) => {
1637
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1638
+ if (!sandbox)
1639
+ return c.json({ error: 'Sandbox not found' }, 404);
1640
+ try {
1641
+ const body = await c.req.json().catch(() => ({}));
1642
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/frameworks/${c.req.param('name')}`, {
1643
+ method: 'PATCH',
1644
+ headers: { 'Content-Type': 'application/json' },
1645
+ body: JSON.stringify(body),
1646
+ });
1647
+ if (resp.status === 204)
1648
+ return new Response(null, { status: 204 });
1649
+ return c.json(await resp.json(), resp.status);
1650
+ }
1651
+ catch (err) {
1652
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1653
+ }
1654
+ });
1655
+ // Agent manifest discovery and push (#909)
1656
+ // GET — list deployed manifests for a platform (with content hashes for drift detection)
1657
+ // POST — push/replace a single agent manifest
1658
+ // Requires sandbox companion issue (GET/POST /api/v1/agents/:id/manifests/:platform).
1659
+ app.get('/api/sandboxes/:id/agents/:aid/manifests/:platform', async (c) => {
1660
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1661
+ if (!sandbox)
1662
+ return c.json({ error: 'Sandbox not found' }, 404);
1663
+ try {
1664
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/manifests/${c.req.param('platform')}`);
1665
+ return c.json(await resp.json(), resp.status);
1666
+ }
1667
+ catch (err) {
1668
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1669
+ }
1670
+ });
1671
+ app.post('/api/sandboxes/:id/agents/:aid/manifests/:platform', async (c) => {
1672
+ const sandbox = sandboxRegistry.get(c.req.param('id'));
1673
+ if (!sandbox)
1674
+ return c.json({ error: 'Sandbox not found' }, 404);
1675
+ try {
1676
+ const body = await c.req.json().catch(() => ({}));
1677
+ const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/manifests/${c.req.param('platform')}`, {
1678
+ method: 'POST',
1679
+ headers: { 'Content-Type': 'application/json' },
1680
+ body: JSON.stringify(body),
1681
+ });
1682
+ return c.json(await resp.json(), resp.status);
1683
+ }
1684
+ catch (err) {
1685
+ return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
1686
+ }
1687
+ });
1688
+ // Static file serving — apps/web/dist/ (#714)
1689
+ // Only register serveStatic when the dist directory actually exists.
1690
+ // When it doesn't, fall back to a helpful HTML placeholder so the
1691
+ // browser gets a 503 with context rather than a bare "Not found".
1692
+ const webDistDir = path.join(opts.frameworkRoot, 'apps', 'web', 'dist');
1693
+ if (existsSync(webDistDir)) {
1694
+ try {
1695
+ const { serveStatic } = await (new Function('m', 'return import(m)'))('@hono/node-server/serve-static');
1696
+ app.use('/*', serveStatic({ root: webDistDir }));
1697
+ }
1698
+ catch {
1699
+ // serve-static import failed — fall through to placeholder below
1700
+ }
1701
+ }
1702
+ if (!existsSync(webDistDir)) {
1703
+ app.get('/*', (c) => c.html(`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
1704
+ <title>AIWG Dashboard</title>
1705
+ <style>body{font-family:system-ui,sans-serif;max-width:600px;margin:80px auto;padding:0 20px;color:#333}
1706
+ code{background:#f4f4f4;padding:2px 6px;border-radius:3px}
1707
+ a{color:#0070f3}</style></head><body>
1708
+ <h1>AIWG Dashboard</h1>
1709
+ <p>The dashboard UI has not been built yet.</p>
1710
+ <p>The <strong>API is fully operational</strong> — try
1711
+ <a href="/api/health">/api/health</a> or
1712
+ <a href="/api/sandboxes">/api/sandboxes</a>.</p>
1713
+ <hr>
1714
+ <p>To build the UI, run:<br><code>pnpm --filter @aiwg/web build</code></p>
1715
+ </body></html>`, 503));
1716
+ }
1717
+ // Fallback: 404 for API routes not matched above
1718
+ app.notFound((c) => {
1719
+ if (c.req.path.startsWith('/api/') || c.req.path.startsWith('/ws/')) {
1720
+ return c.json({ error: 'Not found' }, 404);
1721
+ }
1722
+ // SPA fallback — serve index.html for client-side routing
1723
+ return c.text('Not found', 404);
1724
+ });
1725
+ // Resolve the actual bound port (matters for opts.port === 0 which lets the
1726
+ // OS pick an ephemeral port). @hono/node-server's serve() accepts a
1727
+ // listen-complete callback that receives the AddressInfo for the bound socket.
1728
+ // We await that callback so the URL we return reflects the real port (#1275).
1729
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1730
+ const server = await new Promise((resolve, reject) => {
1731
+ try {
1732
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1733
+ const s = serve({ fetch: app.fetch, port: opts.port, hostname: opts.host }, () => resolve(s));
1734
+ // Fallback: if serve does not invoke the callback for some reason, resolve
1735
+ // after a short tick so we don't hang indefinitely.
1736
+ setTimeout(() => resolve(s), 100).unref?.();
1737
+ }
1738
+ catch (err) {
1739
+ reject(err);
1740
+ }
1741
+ });
1742
+ // Discover the actual bound port. @hono/node-server returns the underlying
1743
+ // http.Server or a wrapper exposing .address() / .server.address().
1744
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1745
+ const addressSrc = server?.server ?? server;
1746
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1747
+ const addr = typeof addressSrc?.address === 'function' ? addressSrc.address() : null;
1748
+ const actualPort = typeof addr === 'object' && addr && typeof addr.port === 'number' ? addr.port : opts.port;
1749
+ const url = `http://${opts.host}:${actualPort}`;
1750
+ // Wire up WebSocket routes via Node.js upgrade event (#851)
1751
+ await setupWebSockets(server, opts.readOnly);
1752
+ return {
1753
+ url,
1754
+ close: () => {
1755
+ ptyRegistry.shutdown();
1756
+ sandboxRegistry.shutdown();
1757
+ if (typeof server.close === 'function') {
1758
+ server.close();
1759
+ }
1760
+ },
1761
+ };
1762
+ }
1763
+ /**
1764
+ * Serve command handler
1765
+ */
1766
+ export const serveHandler = {
1767
+ id: 'serve',
1768
+ name: 'Serve',
1769
+ description: 'Start local HTTP dashboard server',
1770
+ category: 'project',
1771
+ aliases: [],
1772
+ async execute(ctx) {
1773
+ const { port, host, open, readOnly } = parseServeArgs(ctx.args);
1774
+ let server;
1775
+ try {
1776
+ server = await startServer({ port, host, readOnly, frameworkRoot: ctx.frameworkRoot });
1777
+ }
1778
+ catch (error) {
1779
+ const { handlerResultFromError } = await import('../errors.js');
1780
+ return handlerResultFromError(error);
1781
+ }
1782
+ const { url } = server;
1783
+ console.log(`Dashboard: ${url}`);
1784
+ if (readOnly)
1785
+ console.log(' (read-only mode)');
1786
+ console.log('Press Ctrl+C to stop.');
1787
+ // Auto-open browser
1788
+ if (open) {
1789
+ try {
1790
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1791
+ const openMod = await (new Function('m', 'return import(m)'))('open');
1792
+ const openBrowser = openMod.default ?? openMod;
1793
+ await openBrowser(url);
1794
+ }
1795
+ catch {
1796
+ // open is optional — not a fatal error
1797
+ }
1798
+ }
1799
+ // Keep process alive; shut down cleanly on SIGINT/SIGTERM.
1800
+ //
1801
+ // Registered listeners are removed on the resolve path so that the only
1802
+ // handler we install does not outlive this command (important for tests
1803
+ // or future in-process CLI flows). A 5-second hard deadline after signal
1804
+ // receipt forces process.exit() in case server.close() hangs draining
1805
+ // websocket connections.
1806
+ await new Promise((resolve) => {
1807
+ let settled = false;
1808
+ const shutdown = () => {
1809
+ if (settled)
1810
+ return;
1811
+ settled = true;
1812
+ // Immediately remove both handlers so a second signal doesn't
1813
+ // re-enter shutdown().
1814
+ process.removeListener('SIGINT', shutdown);
1815
+ process.removeListener('SIGTERM', shutdown);
1816
+ try {
1817
+ server.close();
1818
+ }
1819
+ catch {
1820
+ // Server may already be closing; safe to ignore.
1821
+ }
1822
+ // Hard deadline: if server.close() doesn't drain in 5s, force exit.
1823
+ const forceExit = setTimeout(() => {
1824
+ process.exit(143); // SIGTERM convention
1825
+ }, 5_000);
1826
+ forceExit.unref?.();
1827
+ resolve();
1828
+ };
1829
+ process.once('SIGINT', shutdown);
1830
+ process.once('SIGTERM', shutdown);
1831
+ });
1832
+ return { exitCode: 0 };
1833
+ },
1834
+ };
1835
+ //# sourceMappingURL=serve.js.map