@aiwg/cli 2026.10.1 → 2026.10.4

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 (361) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/capability-matrix.yaml +270 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +3183 -24
  4. package/bin/aiwg.mjs +135 -47
  5. package/dist/src/a2a/codecs.js +9 -0
  6. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  7. package/dist/src/artifacts/browser-export.js +6 -0
  8. package/dist/src/artifacts/checksum-manifest.js +1 -0
  9. package/dist/src/artifacts/citation-parser.js +173 -92
  10. package/dist/src/artifacts/citation-table-schema.js +59 -0
  11. package/dist/src/artifacts/cli.js +57 -18
  12. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  13. package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
  14. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  15. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  16. package/dist/src/artifacts/discovery-eval.js +6 -4
  17. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  18. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  19. package/dist/src/artifacts/framework-capabilities.js +171 -0
  20. package/dist/src/artifacts/framework-index-paths.js +73 -0
  21. package/dist/src/artifacts/framework-source-digest.js +75 -0
  22. package/dist/src/artifacts/index-builder.js +285 -131
  23. package/dist/src/artifacts/index-files.js +132 -18
  24. package/dist/src/artifacts/index-preparation.js +65 -0
  25. package/dist/src/artifacts/index-refresh.js +576 -0
  26. package/dist/src/artifacts/index-worker.mjs +9 -0
  27. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  28. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  29. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  30. package/dist/src/artifacts/query-engine.js +410 -88
  31. package/dist/src/artifacts/relations-builder.js +168 -0
  32. package/dist/src/artifacts/relations.js +148 -0
  33. package/dist/src/artifacts/repair.js +9 -4
  34. package/dist/src/artifacts/stats.js +5 -2
  35. package/dist/src/artifacts/types.js +30 -10
  36. package/dist/src/artifacts/watcher.js +1 -1
  37. package/dist/src/auth/credential-store.js +2 -1
  38. package/dist/src/catalog/cli.d.mts +5 -0
  39. package/dist/src/channel/manager.d.mts +20 -0
  40. package/dist/src/channel/manager.mjs +78 -12
  41. package/dist/src/cli/child-process-result.js +40 -0
  42. package/dist/src/cli/command-log.js +2 -3
  43. package/dist/src/cli/env.js +38 -16
  44. package/dist/src/cli/flag-parsing.js +236 -0
  45. package/dist/src/cli/handlers/artifacts.js +3 -0
  46. package/dist/src/cli/handlers/auth.js +3 -0
  47. package/dist/src/cli/handlers/decision.js +218 -5
  48. package/dist/src/cli/handlers/diagnose.js +13 -5
  49. package/dist/src/cli/handlers/effect.js +4 -2
  50. package/dist/src/cli/handlers/evidence.js +3 -0
  51. package/dist/src/cli/handlers/feedback.js +14 -5
  52. package/dist/src/cli/handlers/help.js +1 -0
  53. package/dist/src/cli/handlers/index.js +3 -1
  54. package/dist/src/cli/handlers/init.js +248 -30
  55. package/dist/src/cli/handlers/lint.js +5 -0
  56. package/dist/src/cli/handlers/mc.js +23 -0
  57. package/dist/src/cli/handlers/models.js +169 -10
  58. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  59. package/dist/src/cli/handlers/ralph.js +3 -0
  60. package/dist/src/cli/handlers/refresh.js +28 -0
  61. package/dist/src/cli/handlers/regenerate.js +14 -0
  62. package/dist/src/cli/handlers/resource-versions.js +3 -0
  63. package/dist/src/cli/handlers/rules.js +195 -0
  64. package/dist/src/cli/handlers/run.js +4 -0
  65. package/dist/src/cli/handlers/runtime-info.js +25 -11
  66. package/dist/src/cli/handlers/script-runner.js +19 -8
  67. package/dist/src/cli/handlers/session.js +406 -62
  68. package/dist/src/cli/handlers/sessions.js +243 -76
  69. package/dist/src/cli/handlers/setup-manifest.js +474 -79
  70. package/dist/src/cli/handlers/setup.js +180 -39
  71. package/dist/src/cli/handlers/steward.js +11 -0
  72. package/dist/src/cli/handlers/subcommands.js +60 -3
  73. package/dist/src/cli/handlers/team.js +18 -8
  74. package/dist/src/cli/handlers/uhp.js +3 -0
  75. package/dist/src/cli/handlers/use.js +307 -55
  76. package/dist/src/cli/handlers/utilities.js +125 -26
  77. package/dist/src/cli/handlers/version.js +8 -0
  78. package/dist/src/cli/prompt-utils.js +72 -0
  79. package/dist/src/cli/provider-resolution.js +11 -0
  80. package/dist/src/cli/router.js +15 -1
  81. package/dist/src/cli/services/deployment-verification.js +19 -9
  82. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  83. package/dist/src/cli/skill-usage.js +2 -3
  84. package/dist/src/config/aiwg-config.js +349 -35
  85. package/dist/src/config/cli.js +256 -23
  86. package/dist/src/config/decision-config.js +106 -0
  87. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  88. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  89. package/dist/src/config/gitignore.js +50 -0
  90. package/dist/src/config/index-graph-overrides.js +36 -0
  91. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  92. package/dist/src/config/project-artifacts-health.mjs +14 -3
  93. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  94. package/dist/src/config/user-config-dir.d.mts +9 -0
  95. package/dist/src/config/user-config.js +38 -11
  96. package/dist/src/config/user-registry.js +23 -15
  97. package/dist/src/config/user-state.js +7 -0
  98. package/dist/src/decision/adapters/jev.js +75 -27
  99. package/dist/src/decision/ask.js +203 -43
  100. package/dist/src/decision/context-live-qualification.js +2 -2
  101. package/dist/src/decision/driver.js +14 -7
  102. package/dist/src/decision/ensemble/aggregate.js +23 -10
  103. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  104. package/dist/src/decision/entry.js +25 -6
  105. package/dist/src/decision/graph-live-qualification.js +1 -1
  106. package/dist/src/decision/graph-run.js +9 -0
  107. package/dist/src/decision/heldout/collector.js +15 -14
  108. package/dist/src/decision/heldout/contract.js +30 -7
  109. package/dist/src/decision/index.js +1 -0
  110. package/dist/src/decision/interop.js +60 -8
  111. package/dist/src/decision/jev-credentials.js +15 -1
  112. package/dist/src/decision/ledger.js +942 -0
  113. package/dist/src/decision/offload-activation.js +137 -0
  114. package/dist/src/decision/offload.js +550 -0
  115. package/dist/src/decision/qualification/load.js +4 -6
  116. package/dist/src/decision/savings.js +28 -0
  117. package/dist/src/decision/validate.js +6 -3
  118. package/dist/src/effects/dispatch-verifier.js +252 -0
  119. package/dist/src/effects/index.js +1 -0
  120. package/dist/src/effects/types.js +5 -1
  121. package/dist/src/effects/verifiers/git.js +2 -1
  122. package/dist/src/effects/verifiers/tracker.js +26 -14
  123. package/dist/src/extensions/commands/definitions.js +19 -0
  124. package/dist/src/extensions/project-local-remove.js +36 -1
  125. package/dist/src/features/installer.js +2 -2
  126. package/dist/src/flow/graph-metadata.js +5 -0
  127. package/dist/src/gates/providers/loader.js +64 -18
  128. package/dist/src/installation/manager-command.d.mts +18 -0
  129. package/dist/src/installation/manager-command.mjs +15 -3
  130. package/dist/src/installation/manager.d.mts +49 -0
  131. package/dist/src/installation/manager.mjs +95 -5
  132. package/dist/src/issues/cli.js +4 -4
  133. package/dist/src/issues/local.js +24 -5
  134. package/dist/src/jobs/runner.js +1 -1
  135. package/dist/src/lint/cli.js +27 -1
  136. package/dist/src/lint/loader.js +2 -1
  137. package/dist/src/lint/reporters.js +25 -1
  138. package/dist/src/lint/rule-activation.js +43 -0
  139. package/dist/src/lint/runner.js +137 -62
  140. package/dist/src/mcp/adapters/codex-runtime.js +366 -56
  141. package/dist/src/mcp/claude-install.d.mts +4 -0
  142. package/dist/src/mcp/claude-install.mjs +41 -0
  143. package/dist/src/mcp/cli.d.mts +1 -0
  144. package/dist/src/mcp/cli.mjs +620 -85
  145. package/dist/src/mcp/config-file.d.mts +28 -0
  146. package/dist/src/mcp/config-file.mjs +279 -0
  147. package/dist/src/mcp/config-layers.d.mts +36 -0
  148. package/dist/src/mcp/config-layers.mjs +348 -0
  149. package/dist/src/mcp/credentials.d.mts +51 -0
  150. package/dist/src/mcp/credentials.mjs +484 -0
  151. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  152. package/dist/src/mcp/grok-build-config.mjs +17 -27
  153. package/dist/src/mcp/helpers.mjs +109 -4
  154. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  155. package/dist/src/mcp/muse-native-config.mjs +1 -0
  156. package/dist/src/mcp/omp-config.d.mts +4 -0
  157. package/dist/src/mcp/omp-config.mjs +50 -29
  158. package/dist/src/mcp/profiles.js +97 -23
  159. package/dist/src/mcp/profiles.mjs +99 -27
  160. package/dist/src/mcp/registry.js +248 -131
  161. package/dist/src/mcp/registry.mjs +169 -100
  162. package/dist/src/mcp/server-config.d.mts +3 -0
  163. package/dist/src/mcp/server-config.mjs +89 -0
  164. package/dist/src/mcp/server.mjs +127 -82
  165. package/dist/src/mcp/test-simple.mjs +2 -1
  166. package/dist/src/mcp/toml-editor.d.mts +4 -0
  167. package/dist/src/mcp/toml-string.d.mts +1 -0
  168. package/dist/src/mcp/toml-string.mjs +9 -0
  169. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  170. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  171. package/dist/src/mcp/tool-filters.d.mts +46 -0
  172. package/dist/src/mcp/tool-filters.mjs +357 -0
  173. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  174. package/dist/src/mcp/tools/decision.mjs +22 -3
  175. package/dist/src/mcp/tools/discovery.mjs +62 -55
  176. package/dist/src/mcp/tools/orchestration.mjs +50 -2
  177. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  178. package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
  179. package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
  180. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  181. package/dist/src/memory/project-registry.js +2 -2
  182. package/dist/src/mission-protocol/codecs.js +647 -1
  183. package/dist/src/mission-protocol/types.js +110 -0
  184. package/dist/src/mission-protocol/worker-status.js +2 -0
  185. package/dist/src/models/config-loader.js +76 -14
  186. package/dist/src/models/grok-build-models.js +93 -41
  187. package/dist/src/models/model-capabilities.v1.json +3183 -24
  188. package/dist/src/models/model-discovery.js +931 -67
  189. package/dist/src/models/provider-policy.js +719 -11
  190. package/dist/src/models/qualification-digest.d.mts +2 -0
  191. package/dist/src/models/qualification-digest.mjs +17 -0
  192. package/dist/src/models/resolver.js +261 -0
  193. package/dist/src/models/worker-direct-readiness.js +550 -0
  194. package/dist/src/models/worker-dispatch-offer.js +169 -0
  195. package/dist/src/models/worker-health-readiness.js +144 -0
  196. package/dist/src/models/worker-installation.d.mts +39 -0
  197. package/dist/src/models/worker-installation.mjs +986 -0
  198. package/dist/src/models/wrapper-route.js +18 -0
  199. package/dist/src/ops/registry.js +9 -14
  200. package/dist/src/plugins/standalone-packager.js +4 -3
  201. package/dist/src/process/bounded-child-process.d.mts +84 -0
  202. package/dist/src/process/bounded-child-process.mjs +430 -0
  203. package/dist/src/process/cli-command.d.mts +16 -0
  204. package/dist/src/process/cli-command.mjs +75 -0
  205. package/dist/src/process/signal-child.d.mts +6 -0
  206. package/dist/src/process/signal-child.mjs +7 -0
  207. package/dist/src/providers/capability-matrix.js +8 -0
  208. package/dist/src/providers/capability-matrix.yaml +270 -0
  209. package/dist/src/providers/deployed-skill-roots.js +20 -0
  210. package/dist/src/providers/grok-build-acp.js +3 -2
  211. package/dist/src/providers/grok-build-headless.js +53 -23
  212. package/dist/src/providers/grok-build-inspect.js +1 -0
  213. package/dist/src/providers/omp-agent.d.mts +8 -0
  214. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  215. package/dist/src/providers/omp-paths.d.mts +13 -0
  216. package/dist/src/providers/provider-definitions.js +9 -1
  217. package/dist/src/providers/provider-definitions.mjs +9 -2
  218. package/dist/src/providers/skill-ownership.d.mts +9 -0
  219. package/dist/src/providers/skill-ownership.mjs +88 -0
  220. package/dist/src/rules/activation.d.mts +21 -0
  221. package/dist/src/rules/activation.mjs +62 -0
  222. package/dist/src/rules/inline-budget.d.mts +26 -0
  223. package/dist/src/rules/inline-budget.mjs +66 -0
  224. package/dist/src/rules/inventory.js +129 -0
  225. package/dist/src/rules/pin-state.js +73 -0
  226. package/dist/src/rules/pins.d.mts +14 -0
  227. package/dist/src/rules/pins.mjs +112 -0
  228. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  229. package/dist/src/serve/executor-registry.js +16 -24
  230. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  231. package/dist/src/serve/mission-conductor.js +47 -12
  232. package/dist/src/serve/shared-host-scheduler.js +2 -0
  233. package/dist/src/serve/worker-invocation.js +558 -0
  234. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  235. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  236. package/dist/src/sessions/adapter-registry.js +49 -0
  237. package/dist/src/sessions/adapters/claude.js +39 -54
  238. package/dist/src/sessions/adapters/codex.js +82 -67
  239. package/dist/src/sessions/adapters/copilot.js +4 -4
  240. package/dist/src/sessions/adapters/cursor.js +40 -57
  241. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  242. package/dist/src/sessions/adapters/factory.js +31 -27
  243. package/dist/src/sessions/adapters/generic.js +43 -9
  244. package/dist/src/sessions/adapters/grok-build.js +11 -11
  245. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  246. package/dist/src/sessions/adapters/grokbot.js +434 -24
  247. package/dist/src/sessions/adapters/hermes.js +27 -29
  248. package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
  249. package/dist/src/sessions/adapters/muse.js +200 -104
  250. package/dist/src/sessions/adapters/omp.js +28 -14
  251. package/dist/src/sessions/adapters/openclaw.js +89 -79
  252. package/dist/src/sessions/adapters/opencode.js +27 -24
  253. package/dist/src/sessions/adapters/openhuman.js +18 -60
  254. package/dist/src/sessions/adapters/pi.js +30 -14
  255. package/dist/src/sessions/adapters/warp.js +3 -3
  256. package/dist/src/sessions/adapters/windsurf.js +19 -48
  257. package/dist/src/sessions/batch-contracts.js +25 -5
  258. package/dist/src/sessions/batch-import.js +72 -51
  259. package/dist/src/sessions/candidates.js +47 -11
  260. package/dist/src/sessions/contracts.js +75 -2
  261. package/dist/src/sessions/coverage-preflight.js +134 -0
  262. package/dist/src/sessions/discovery.js +3 -0
  263. package/dist/src/sessions/export-plan.js +4 -3
  264. package/dist/src/sessions/grok-build-cli.js +67 -5
  265. package/dist/src/sessions/import-exclusions.js +72 -0
  266. package/dist/src/sessions/import-lease.js +34 -18
  267. package/dist/src/sessions/import-limits.js +37 -0
  268. package/dist/src/sessions/importer.js +84 -46
  269. package/dist/src/sessions/index.js +2 -0
  270. package/dist/src/sessions/lifecycle.js +45 -0
  271. package/dist/src/sessions/policy.js +133 -25
  272. package/dist/src/sessions/readers.js +106 -53
  273. package/dist/src/sessions/refresh.js +106 -0
  274. package/dist/src/sessions/repository.js +199 -63
  275. package/dist/src/sessions/workspace-discovery.js +315 -88
  276. package/dist/src/sessions/workspace-matching.js +107 -0
  277. package/dist/src/skills/adapters/clawhub.js +10 -8
  278. package/dist/src/skills/adapters/openclaw.js +10 -7
  279. package/dist/src/skills/importer.js +1 -0
  280. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  281. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  282. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  283. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  284. package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
  285. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  286. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  287. package/dist/src/smiths/mcpsmith/example.js +1 -1
  288. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  289. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  290. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
  291. package/dist/src/storage/backends/fs.js +18 -76
  292. package/dist/src/storage/backends/logseq.js +68 -7
  293. package/dist/src/storage/backends/obsidian.js +1 -0
  294. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  295. package/dist/src/testing/generators/test-code-generator.js +57 -30
  296. package/dist/src/tracker/forge-http.js +2 -1
  297. package/dist/src/uhp/client.js +11 -1
  298. package/dist/src/uhp/mission.js +5 -0
  299. package/dist/src/update/checker.d.mts +2 -0
  300. package/dist/src/update/checker.mjs +5 -5
  301. package/dist/src/update/notifier.mjs +54 -2
  302. package/dist/src/update/service.d.mts +22 -0
  303. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  304. package/dist/src/utils/contained-filesystem.mjs +683 -0
  305. package/dist/src/utils/temporary-directory.js +36 -0
  306. package/dist/src/writing/example-generator.js +1 -1
  307. package/dist/src/writing/fidelity.js +18 -1
  308. package/package.json +32 -2
  309. package/schemas/mission-v1.schema.json +2429 -0
  310. package/schemas/models/model-catalog.v1.schema.json +301 -0
  311. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  312. package/schemas/models/model-policy.v1.schema.json +14 -0
  313. package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
  314. package/tools/_resolve-impl.mjs +39 -2
  315. package/tools/agents/deploy-agents.mjs +64 -23
  316. package/tools/agents/providers/base.mjs +153 -166
  317. package/tools/agents/providers/claude.mjs +8 -24
  318. package/tools/agents/providers/codex.mjs +18 -11
  319. package/tools/agents/providers/cursor.mjs +19 -18
  320. package/tools/agents/providers/grok-build.mjs +3 -1
  321. package/tools/agents/providers/grokbot.mjs +11 -12
  322. package/tools/agents/providers/hermes.mjs +62 -49
  323. package/tools/agents/providers/omp.mjs +5 -2
  324. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  325. package/tools/agents/providers/openclaw.mjs +9 -4
  326. package/tools/agents/providers/pi.mjs +5 -0
  327. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  328. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  329. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  330. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  331. package/tools/agents/providers/warp.mjs +20 -26
  332. package/tools/cli/decision-offload-report.mjs +40 -0
  333. package/tools/cli/workspace-status.mjs +862 -0
  334. package/tools/plugin/package-plugins.mjs +186 -8
  335. package/tools/plugin/plugin-links.mjs +114 -0
  336. package/tools/providers/antigravity-transport.mjs +3 -1
  337. package/tools/providers/deepseek-harness-transport.mjs +360 -0
  338. package/tools/providers/omp-transport.mjs +194 -0
  339. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  340. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  341. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  342. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  343. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  344. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  345. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  346. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  347. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  348. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  349. package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
  350. package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
  351. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  352. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  353. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  354. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  355. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  356. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  357. package/tools/ralph-external/process-monitor.mjs +1017 -0
  358. package/tools/ralph-external/session-launcher.mjs +5057 -0
  359. package/tools/scripts/resource-preflight.mjs +541 -0
  360. package/tools/skills/deploy-skills-codex.mjs +2 -1
  361. package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
@@ -1,6 +1,7 @@
1
1
  import { compileModelPolicy, } from './provider-policy.js';
2
2
  import { MODEL_WRAPPERS } from './provider-models.js';
3
3
  import { routeModelTier } from './router.js';
4
+ import { resolveWorkerPlan } from './resolver.js';
4
5
  function roleForTier(tier) {
5
6
  if (tier === 'economy')
6
7
  return 'efficiency';
@@ -14,6 +15,23 @@ export function buildWrapperRouteEnvelope(request) {
14
15
  if (!request.assignment.trim())
15
16
  throw new Error('Wrapper routing requires a bounded assignment.');
16
17
  const decision = routeModelTier(request);
18
+ if (request.worker?.snapshot.config.parallelism?.worker_dispatch && !request.deterministic) {
19
+ const worker = resolveWorkerPlan(request.worker.snapshot, {
20
+ ...request.worker.request,
21
+ authority: request,
22
+ });
23
+ const role = worker.role;
24
+ const wrapper = role ? MODEL_WRAPPERS[role] : null;
25
+ return {
26
+ version: 1, provider: request.provider, decision, role,
27
+ tier: role ? worker.policy?.roleGuidance?.[role]?.tier ?? null : null,
28
+ wrapper, capability: request.capability, assignment: request.assignment.trim(), model: null, worker,
29
+ launch: { mechanism: request.launchMechanism, target: worker.selected ? wrapper : null,
30
+ prompt: worker.selected
31
+ ? `Run this bounded assignment through ${wrapper}.\nSelected worker: ${worker.selected.group}/${worker.selected.member}; model ${worker.selected.model}; effort ${worker.selected.effort ?? 'not-applicable'}.\nAssignment: ${request.assignment.trim()}\nRevalidate current input digests before invocation; return claims for conductor verification.`
32
+ : 'Worker resolution is blocked; inspect the plan diagnostics and exclusions.' },
33
+ };
34
+ }
17
35
  const tier = decision.modelTier;
18
36
  const role = roleForTier(tier);
19
37
  const wrapper = role ? MODEL_WRAPPERS[role] : null;
@@ -10,7 +10,7 @@ import { readFile, writeFile, mkdir, readdir } from 'fs/promises';
10
10
  import { resolve, dirname, basename, sep } from 'path';
11
11
  import { homedir } from 'os';
12
12
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
13
- import { execSync } from 'child_process';
13
+ import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
14
14
  import { resolveConfigDir } from '../config/user-config.js';
15
15
  import { resolveWorkspace } from '../config/workspace.js';
16
16
  import { projectControlPath } from '../config/project-artifacts.js';
@@ -495,7 +495,10 @@ export class OpsRegistry {
495
495
  if (remote) {
496
496
  console.log(` Pushing ${repoName} to ${remote}/${branch}...`);
497
497
  try {
498
- execSync(`git push ${shellEscape(remote)} ${shellEscape(branch)}`, {
498
+ if (remote.startsWith('-') || branch.startsWith('-')) {
499
+ throw new Error('Remote and branch must not start with "-"');
500
+ }
501
+ boundedExecFileSync('git', ['push', '--end-of-options', remote, branch], 'network', {
499
502
  cwd: repo.path,
500
503
  stdio: 'pipe',
501
504
  });
@@ -541,23 +544,15 @@ async function provisionRepo(repoPath, fromUrl, silent) {
541
544
  }
542
545
  }
543
546
  await mkdir(dirname(repoPath), { recursive: true });
544
- execSync(`git clone ${shellEscape(fromUrl)} ${shellEscape(repoPath)}`, { stdio: silent ? 'pipe' : 'inherit' });
547
+ // Cloning a full repository; long tier (#3062).
548
+ boundedExecFileSync('git', ['clone', '--', fromUrl, repoPath], 'long', { stdio: silent ? 'pipe' : 'inherit' });
545
549
  return { action: 'Cloned', remote: readGitRemote(repoPath) };
546
550
  }
547
551
  // Case 3: fresh init.
548
552
  await mkdir(repoPath, { recursive: true });
549
- execSync('git init', { cwd: repoPath, stdio: 'pipe' });
553
+ boundedExecFileSync('git', ['init'], 'quick', { cwd: repoPath, stdio: 'pipe' });
550
554
  return { action: 'Created', remote: undefined };
551
555
  }
552
- /**
553
- * Minimal shell escaping for git URLs and paths passed via execSync.
554
- * URLs are URL-encoded already, but paths may contain spaces.
555
- */
556
- function shellEscape(s) {
557
- if (/^[A-Za-z0-9_@:./~+,=-]+$/.test(s))
558
- return s;
559
- return `'${s.replace(/'/g, "'\\''")}'`;
560
- }
561
556
  /**
562
557
  * Return the `origin` remote URL for a git repo rooted at `dirPath`, or
563
558
  * undefined if not a git repo or no origin configured.
@@ -566,7 +561,7 @@ function readGitRemote(dirPath) {
566
561
  if (!existsSync(resolve(dirPath, '.git')))
567
562
  return undefined;
568
563
  try {
569
- const out = execSync('git config --get remote.origin.url', {
564
+ const out = boundedExecFileSync('git', ['config', '--get', 'remote.origin.url'], 'quick', {
570
565
  cwd: dirPath,
571
566
  stdio: ['ignore', 'pipe', 'ignore'],
572
567
  })
@@ -1,5 +1,5 @@
1
1
  // @ts-nocheck
2
- import { spawnSync } from 'node:child_process';
2
+ import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
@@ -65,7 +65,8 @@ function writeProviderMetadata(root, provider, manifest) {
65
65
  }
66
66
  }
67
67
  function createDeterministicArchive(sourceDir, archivePath) {
68
- const result = spawnSync('tar', [
68
+ // Packaging a local plugin directory; quick tier (#3062).
69
+ const result = boundedSpawnSync('tar', [
69
70
  '--sort=name',
70
71
  '--mtime=@0',
71
72
  '--owner=0',
@@ -76,7 +77,7 @@ function createDeterministicArchive(sourceDir, archivePath) {
76
77
  '-C',
77
78
  path.dirname(sourceDir),
78
79
  path.basename(sourceDir),
79
- ], { encoding: 'utf8' });
80
+ ], 'quick', { encoding: 'utf8' });
80
81
  if (result.status !== 0)
81
82
  throw new Error(`Could not create deterministic archive: ${result.stderr || 'tar failed'}`);
82
83
  }
@@ -0,0 +1,84 @@
1
+ import type {
2
+ ExecFileSyncOptions,
3
+ ExecFileSyncOptionsWithBufferEncoding,
4
+ ExecFileSyncOptionsWithStringEncoding,
5
+ ExecSyncOptions,
6
+ ExecSyncOptionsWithBufferEncoding,
7
+ ExecSyncOptionsWithStringEncoding,
8
+ SpawnSyncOptions,
9
+ SpawnSyncOptionsWithBufferEncoding,
10
+ SpawnSyncOptionsWithStringEncoding,
11
+ SpawnSyncReturns,
12
+ } from 'node:child_process';
13
+
14
+ export type SyncProcessTier = 'quick' | 'network' | 'long';
15
+
16
+ export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV: 'AIWG_SYNC_LONG_TIMEOUT_MS';
17
+
18
+ export const SYNC_PROCESS_TIER_BOUNDS_MS: Readonly<{
19
+ quick: number;
20
+ network: number;
21
+ long: number;
22
+ }>;
23
+
24
+ export function resolveSyncProcessTimeoutMs(tier: SyncProcessTier): number;
25
+
26
+ export function positiveTimeoutMs(value: unknown, fallbackMs: number): number;
27
+
28
+ // Overloads mirror Node's own execSync/execFileSync/spawnSync overload set
29
+ // (string vs Buffer return type keyed on `options.encoding`), with the
30
+ // required `tier` inserted right before `options` — same narrowing behavior
31
+ // callers get from the native APIs, e.g. `.trim()` on the result when
32
+ // `encoding: 'utf8'` is set.
33
+
34
+ export function boundedExecSync(command: string, tier: SyncProcessTier): Buffer;
35
+ export function boundedExecSync(command: string, tier: SyncProcessTier, options: ExecSyncOptionsWithStringEncoding): string;
36
+ export function boundedExecSync(command: string, tier: SyncProcessTier, options: ExecSyncOptionsWithBufferEncoding): Buffer;
37
+ export function boundedExecSync(command: string, tier: SyncProcessTier, options?: ExecSyncOptions): string | Buffer;
38
+
39
+ export function boundedExecFileSync(file: string, args: readonly string[], tier: SyncProcessTier): Buffer;
40
+ export function boundedExecFileSync(
41
+ file: string,
42
+ args: readonly string[],
43
+ tier: SyncProcessTier,
44
+ options: ExecFileSyncOptionsWithStringEncoding,
45
+ ): string;
46
+ export function boundedExecFileSync(
47
+ file: string,
48
+ args: readonly string[],
49
+ tier: SyncProcessTier,
50
+ options: ExecFileSyncOptionsWithBufferEncoding,
51
+ ): Buffer;
52
+ export function boundedExecFileSync(
53
+ file: string,
54
+ args: readonly string[],
55
+ tier: SyncProcessTier,
56
+ options?: ExecFileSyncOptions,
57
+ ): string | Buffer;
58
+
59
+ export function boundedSpawnSync(command: string, args: readonly string[], tier: SyncProcessTier): SpawnSyncReturns<Buffer>;
60
+ export function boundedSpawnSync(
61
+ command: string,
62
+ args: readonly string[],
63
+ tier: SyncProcessTier,
64
+ options: SpawnSyncOptionsWithStringEncoding,
65
+ ): SpawnSyncReturns<string>;
66
+ export function boundedSpawnSync(
67
+ command: string,
68
+ args: readonly string[],
69
+ tier: SyncProcessTier,
70
+ options: SpawnSyncOptionsWithBufferEncoding,
71
+ ): SpawnSyncReturns<Buffer>;
72
+ export function boundedSpawnSync(
73
+ command: string,
74
+ args: readonly string[],
75
+ tier: SyncProcessTier,
76
+ options?: SpawnSyncOptions,
77
+ ): SpawnSyncReturns<string | Buffer>;
78
+
79
+ /** Async launch with direct-child timeout and cleanup before termination. */
80
+ export function boundedSpawn(
81
+ command: string, args: readonly string[], tier: SyncProcessTier,
82
+ options: import('node:child_process').SpawnOptions,
83
+ onTermination?: () => void,
84
+ ): Promise<{ status: number | null; signal: NodeJS.Signals | null; error?: Error; pid?: number }>;
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Bounded synchronous child-process wrappers (#3062).
3
+ *
4
+ * `execSync`/`execFileSync`/`spawnSync` block the calling thread for as long
5
+ * as the child — or any grandchild still holding an inherited pipe — lives.
6
+ * With no timeout that is unbounded: it hung the test suite once and can
7
+ * hang the CLI when it runs without a terminal. These wrappers make every
8
+ * call bounded by construction: a required `tier` selects the wall-clock
9
+ * cap, the call never inherits stdin, and a timeout always surfaces as the
10
+ * normal Node `ETIMEDOUT` error/result with a message that names the
11
+ * command, tier and bound.
12
+ *
13
+ * Tiers (see `SYNC_PROCESS_TIER_BOUNDS_MS`):
14
+ * - `quick` 30s — local queries: git status/rev-parse/config, version
15
+ * probes, `which`/`where`, `df`, tmux queries.
16
+ * - `network` 2m — git fetch/pull/ls-remote/push, `gh`/API calls.
17
+ * - `long` 10m — installs, clones, docker, builds, test runs launched
18
+ * by scripts. Overridable via `AIWG_SYNC_LONG_TIMEOUT_MS`
19
+ * (see `resolveSyncProcessTimeoutMs`); `quick`/`network`
20
+ * are not overridable.
21
+ *
22
+ * Choose the tier by what the command does, not by where the call site
23
+ * lives.
24
+ *
25
+ * Interactive-by-design calls (an editor, a login flow, launching a harness
26
+ * binary the user is meant to talk to) do not belong here — bounding them
27
+ * would just make the interactive session time out. Leave those sites on
28
+ * the plain Node APIs.
29
+ *
30
+ * ## Why timeout kills with SIGKILL, not SIGTERM-then-SIGKILL
31
+ *
32
+ * Node's own `timeout`/`killSignal` option sends `killSignal` exactly once
33
+ * when the bound elapses, then keeps blocking until the child actually
34
+ * exits. If the child ignores that signal — trivial to do for SIGTERM, and
35
+ * exactly the "grandchild holding an inherited pipe" scenario this gate
36
+ * exists to catch — the call hangs forever regardless of the configured
37
+ * timeout; killSignal only picks *which* signal is sent once, not whether
38
+ * the wait ever ends. There is no portable way to schedule a second signal
39
+ * from inside a single blocking native call: the calling thread is blocked
40
+ * in a C++ synchronous spawn, so no JS timer can run to escalate.
41
+ *
42
+ * These wrappers therefore always kill with `killSignal: 'SIGKILL'` and
43
+ * ignore any `killSignal` a caller passes in `options` — honoring a
44
+ * caller-supplied `'SIGTERM'` would silently reopen exactly the hang this
45
+ * module exists to close for a child that traps and ignores it. SIGKILL
46
+ * cannot be caught, blocked or ignored, so the *direct child* reliably dies
47
+ * at the bound — confirmed with a real child that traps and ignores SIGTERM
48
+ * (see `test/unit/process/bounded-child-process.test.mjs`): with
49
+ * `killSignal: 'SIGTERM'` the call hangs indefinitely; with `'SIGKILL'` it
50
+ * returns at the bound, every time.
51
+ *
52
+ * ## What SIGKILL does *not* guarantee: descendant cleanup
53
+ *
54
+ * These wrappers bound one synchronous call to one direct child process.
55
+ * SIGKILL is sent to that child only — never to a process group or a
56
+ * descendant tree. A direct child that itself spawned a grandchild (for
57
+ * example a backgrounded, detached, or double-forked process) is not
58
+ * supervised here: that grandchild is not signaled, is not waited for, and
59
+ * may keep running after this call returns (see
60
+ * `test/fixtures/process/spawn-detached-pipe-holder.mjs` and its test for a
61
+ * concrete case). A synchronous Node call cannot supervise a process tree —
62
+ * doing so needs an asynchronous process-group/session owner, which is a
63
+ * different mechanism than what `execSync`/`execFileSync`/`spawnSync` give
64
+ * us. Callers that spawn a tool known to background work of its own are
65
+ * responsible for that tool's own cleanup contract; this module only
66
+ * guarantees the direct child is gone and the calling thread is unblocked
67
+ * by the stated bound.
68
+ *
69
+ * @module src/process/bounded-child-process
70
+ */
71
+
72
+ import {
73
+ spawn,
74
+ execSync as nodeExecSync,
75
+ execFileSync as nodeExecFileSync,
76
+ spawnSync as nodeSpawnSync,
77
+ } from 'node:child_process';
78
+ import { signalChild } from './signal-child.mjs';
79
+
80
+ /** Env var that raises or disables the `long` tier bound. `0` disables it (no timeout). */
81
+ export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV = 'AIWG_SYNC_LONG_TIMEOUT_MS';
82
+
83
+ /**
84
+ * Default wall-clock bounds per tier, in milliseconds. `quick` and `network`
85
+ * are fixed; `long` is the default absent `AIWG_SYNC_LONG_TIMEOUT_MS`.
86
+ */
87
+ export const SYNC_PROCESS_TIER_BOUNDS_MS = Object.freeze({
88
+ quick: 30_000,
89
+ network: 120_000,
90
+ long: 600_000,
91
+ });
92
+
93
+ const VALID_TIERS = new Set(Object.keys(SYNC_PROCESS_TIER_BOUNDS_MS));
94
+
95
+ let warnedInvalidLongTimeoutEnv = false;
96
+
97
+ /**
98
+ * Resolve the `long` tier bound, honoring `AIWG_SYNC_LONG_TIMEOUT_MS`.
99
+ *
100
+ * - Unset/empty → the default (`SYNC_PROCESS_TIER_BOUNDS_MS.long`).
101
+ * - `"0"` → disabled: no bound is applied to the `long` tier at all.
102
+ * - A positive integer → that many milliseconds.
103
+ * - Anything else → the default, with one warning to stderr per process.
104
+ *
105
+ * @returns {number} Milliseconds, or `0` meaning "no bound".
106
+ */
107
+ function resolveLongTierTimeoutMs() {
108
+ const raw = process.env[AIWG_SYNC_LONG_TIMEOUT_MS_ENV];
109
+ if (raw === undefined || raw.trim() === '') return SYNC_PROCESS_TIER_BOUNDS_MS.long;
110
+ const trimmed = raw.trim();
111
+ if (trimmed === '0') return 0;
112
+ const parsed = Number(trimmed);
113
+ if (!Number.isInteger(parsed) || parsed <= 0) {
114
+ if (!warnedInvalidLongTimeoutEnv) {
115
+ warnedInvalidLongTimeoutEnv = true;
116
+ console.warn(
117
+ `[aiwg] ${AIWG_SYNC_LONG_TIMEOUT_MS_ENV}=${JSON.stringify(raw)} is not a positive integer or "0"; ` +
118
+ `using the default long-tier bound (${SYNC_PROCESS_TIER_BOUNDS_MS.long}ms).`,
119
+ );
120
+ }
121
+ return SYNC_PROCESS_TIER_BOUNDS_MS.long;
122
+ }
123
+ return parsed;
124
+ }
125
+
126
+ /**
127
+ * Exposed for tests and for the gate/report tooling: resolve the effective
128
+ * bound for a tier right now (reads the env override for `long` each call,
129
+ * same as the wrappers do).
130
+ *
131
+ * @param {'quick'|'network'|'long'} tier
132
+ * @returns {number} Milliseconds; `0` means "no bound" (long tier disabled).
133
+ */
134
+ export function resolveSyncProcessTimeoutMs(tier) {
135
+ assertValidTier(tier);
136
+ if (tier === 'long') return resolveLongTierTimeoutMs();
137
+ return SYNC_PROCESS_TIER_BOUNDS_MS[tier];
138
+ }
139
+
140
+ function assertValidTier(tier) {
141
+ if (!VALID_TIERS.has(tier)) {
142
+ throw new TypeError(
143
+ `Sync process tier must be one of ${[...VALID_TIERS].map((t) => `'${t}'`).join(', ')}; got ${JSON.stringify(tier)}.`,
144
+ );
145
+ }
146
+ }
147
+
148
+ /**
149
+ * The bound actually applied to one call: the tier bound, unless the
150
+ * caller's own `options.timeout` is a smaller positive number (an explicit
151
+ * smaller timeout always wins; a zero/negative/non-numeric one is ignored —
152
+ * a call site cannot silently opt out of the tier bound by passing
153
+ * `timeout: 0` itself; only the `long`-tier env var can disable a bound).
154
+ *
155
+ * @param {'quick'|'network'|'long'} tier
156
+ * @param {{timeout?: number}} [options]
157
+ * @returns {number}
158
+ */
159
+ function effectiveTimeoutMs(tier, options) {
160
+ const bound = resolveSyncProcessTimeoutMs(tier);
161
+ const explicit = options?.timeout;
162
+ if (typeof explicit === 'number' && Number.isFinite(explicit) && explicit > 0 && (bound === 0 || explicit < bound)) {
163
+ return explicit;
164
+ }
165
+ return bound;
166
+ }
167
+
168
+ /**
169
+ * Clamp a caller-controlled value to a guaranteed-positive millisecond
170
+ * bound, for the one case `boundedExecSync`/`boundedExecFileSync`/
171
+ * `boundedSpawnSync` don't cover by themselves: a *raw* `execSync`/
172
+ * `execFileSync`/`spawnSync` call site whose `timeout` option is computed
173
+ * from caller input (a CLI flag, a config value, a converted unit) rather
174
+ * than a literal — `timeout: options.timeout` or `timeout: timeoutSec *
175
+ * 1000` with an untrusted `timeoutSec`, for example. `tools/testing/
176
+ * check-sync-child-process-timeouts.mjs` (#3062 B/C finding 8) recognizes
177
+ * a call to this function, by binding, as a provably positive timeout —
178
+ * so wrap the dynamic value here instead of passing it through directly.
179
+ *
180
+ * `value` passes through only when it is itself a finite number `> 0`;
181
+ * anything else (`0`, a negative number, `NaN`, `undefined`, a string,
182
+ * ...) falls back to `fallbackMs` — so a caller-controlled `timeoutSec: 0`
183
+ * cannot disable the bound. `fallbackMs` itself falls back to the `quick`
184
+ * tier bound if it is not a valid positive number, so this function can
185
+ * never itself return a non-positive or non-finite value.
186
+ *
187
+ * @param {unknown} value
188
+ * @param {number} fallbackMs
189
+ * @returns {number}
190
+ */
191
+ export function positiveTimeoutMs(value, fallbackMs) {
192
+ const resolvedFallback = typeof fallbackMs === 'number' && Number.isFinite(fallbackMs) && fallbackMs > 0
193
+ ? fallbackMs
194
+ : SYNC_PROCESS_TIER_BOUNDS_MS.quick;
195
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : resolvedFallback;
196
+ }
197
+
198
+ /**
199
+ * Whether a `stdio[0]` (stdin slot) value describes the parent's own stdin —
200
+ * the form Node would otherwise inherit from. Covers the string shorthand,
201
+ * the literal fd `0`, and the `process.stdin` stream object itself; leaves
202
+ * `'pipe'`, `'ignore'`, other fds, and unrelated stream objects alone.
203
+ *
204
+ * @param {unknown} value
205
+ * @returns {boolean}
206
+ */
207
+ function isParentStdinDescriptor(value) {
208
+ return value === 'inherit' || value === 0 || value === process.stdin;
209
+ }
210
+
211
+ /**
212
+ * A bounded call never inherits stdin; stdout/stderr may still be inherited
213
+ * for progress output. When the caller supplies `input`, an inherited
214
+ * stdin form is normalized to `'pipe'` instead of `'ignore'` — `input`
215
+ * needs a real pipe to deliver its payload through, and `'ignore'` would
216
+ * silently drop it (#3062 B/C finding 9). `options` is otherwise left
217
+ * untouched when stdin was not going to be inherited anyway.
218
+ *
219
+ * @param {Record<string, unknown>} options
220
+ * @returns {Record<string, unknown>}
221
+ */
222
+ function withoutInheritedStdin(options) {
223
+ if (options.input !== undefined) {
224
+ // `input` needs stdin to actually be a pipe to deliver its payload —
225
+ // a caller-specified inherited-stdin form in `stdio[0]` must not win
226
+ // over that (#3062 B/C finding 9: an earlier version returned
227
+ // `options` completely untouched here, so `input` combined with
228
+ // `stdio: 'inherit'`/fd `0`/`process.stdin` could still leave stdin
229
+ // connected to the parent's real stdin instead of the pipe `input`
230
+ // is written to). stdout/stderr and the payload itself are untouched.
231
+ if (options.stdio === 'inherit') {
232
+ return { ...options, stdio: ['pipe', 'inherit', 'inherit'] };
233
+ }
234
+ if (Array.isArray(options.stdio) && isParentStdinDescriptor(options.stdio[0])) {
235
+ const stdio = options.stdio.slice();
236
+ stdio[0] = 'pipe';
237
+ return { ...options, stdio };
238
+ }
239
+ return options;
240
+ }
241
+ if (options.stdio === 'inherit') {
242
+ return { ...options, stdio: ['ignore', 'inherit', 'inherit'] };
243
+ }
244
+ if (Array.isArray(options.stdio) && isParentStdinDescriptor(options.stdio[0])) {
245
+ const stdio = options.stdio.slice();
246
+ stdio[0] = 'ignore';
247
+ return { ...options, stdio };
248
+ }
249
+ return options;
250
+ }
251
+
252
+ /**
253
+ * Build the final Node options: bounded stdin, resolved timeout, and a
254
+ * forced `killSignal: 'SIGKILL'`. Unlike `timeout`, `killSignal` is not
255
+ * negotiable: a caller-supplied `killSignal` (e.g. `'SIGTERM'`) is
256
+ * overwritten, not honored, because a child that ignores it would defeat
257
+ * the bound this module exists to guarantee (see the module header).
258
+ *
259
+ * @param {Record<string, unknown> | undefined} options
260
+ * @param {number} boundMs
261
+ * @returns {Record<string, unknown>}
262
+ */
263
+ function toNodeOptions(options, boundMs) {
264
+ const base = withoutInheritedStdin(options ?? {});
265
+ return {
266
+ ...base,
267
+ timeout: boundMs,
268
+ killSignal: 'SIGKILL',
269
+ };
270
+ }
271
+
272
+ function describeCommand(command, args) {
273
+ if (args === undefined) return command;
274
+ return [command, ...args].join(' ');
275
+ }
276
+
277
+ /**
278
+ * On timeout (`error.code === 'ETIMEDOUT'`), prepend a message naming the
279
+ * command, tier and bound. Leaves every other field (`code`, `signal`,
280
+ * `status`, `killed`) exactly as Node set them, and leaves non-timeout
281
+ * errors untouched, so existing catch blocks that inspect those fields (or
282
+ * just catch-and-ignore) keep working.
283
+ *
284
+ * `signal` disambiguates *why* `ETIMEDOUT` fired: when it is `'SIGKILL'` the
285
+ * direct child itself was still running and got killed at the bound: the
286
+ * common case. When it is anything else (including `null`), the direct
287
+ * child had already exited — `status`/`signal` on the result reflect its
288
+ * real exit — but this call was still blocked waiting for its stdout/stderr
289
+ * pipe to reach EOF, which only happens once every process holding that
290
+ * pipe's write end (the child *and* any surviving descendant it handed the
291
+ * fd to) has closed it. A surviving, pipe-holding descendant reproduces
292
+ * that even though the child it came from is long gone (see
293
+ * `test/fixtures/process/spawn-detached-pipe-holder.mjs`).
294
+ *
295
+ * @template {Error & {code?: string, signal?: string | null}} E
296
+ * @param {E | null | undefined} error
297
+ * @param {{description: string, tier: string, boundMs: number, signal?: string | null}} context
298
+ * @returns {E | null | undefined}
299
+ */
300
+ function enrichTimeoutError(error, { description, tier, boundMs, signal }) {
301
+ if (!error || error.code !== 'ETIMEDOUT') return error;
302
+ const effectiveSignal = signal !== undefined ? signal : error.signal;
303
+ const killClause = effectiveSignal === 'SIGKILL'
304
+ ? 'the direct child was killed (SIGKILL)'
305
+ : 'the direct child had already exited, but this call was still waiting for its stdout/stderr pipe to close';
306
+ error.message =
307
+ `\`${description}\` exceeded the '${tier}' tier bound of ${boundMs}ms; ${killClause}. ` +
308
+ `Any descendant the child spawned (detached, backgrounded, or otherwise ignoring signals) is not supervised ` +
309
+ `by this call and may still be running — holding that pipe open is enough to trigger this timeout even after ` +
310
+ `the child itself has exited. ` +
311
+ (tier === 'long' ? `Set ${AIWG_SYNC_LONG_TIMEOUT_MS_ENV} to a larger number of milliseconds, or 0 to remove the bound. ` : '') +
312
+ error.message;
313
+ return error;
314
+ }
315
+
316
+ /**
317
+ * Bounded `child_process.execSync`. Same arguments and return value as
318
+ * Node's `execSync(command, options)`, with a required `tier` inserted
319
+ * before `options`.
320
+ *
321
+ * @param {string} command
322
+ * @param {'quick'|'network'|'long'} tier
323
+ * @param {Record<string, unknown>} [options]
324
+ * @returns {Buffer | string}
325
+ */
326
+ export function boundedExecSync(command, tier, options) {
327
+ assertValidTier(tier);
328
+ const boundMs = effectiveTimeoutMs(tier, options);
329
+ const nodeOptions = toNodeOptions(options, boundMs);
330
+ try {
331
+ return nodeExecSync(command, nodeOptions);
332
+ } catch (error) {
333
+ throw enrichTimeoutError(error, { description: command, tier, boundMs });
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Bounded `child_process.execFileSync`. Same arguments and return value as
339
+ * Node's `execFileSync(file, args, options)`, with a required `tier`
340
+ * inserted before `options`. Unlike the native API, `args` is always
341
+ * required (pass `[]` for none) so tier can occupy a fixed position.
342
+ *
343
+ * @param {string} file
344
+ * @param {readonly string[]} args
345
+ * @param {'quick'|'network'|'long'} tier
346
+ * @param {Record<string, unknown>} [options]
347
+ * @returns {Buffer | string}
348
+ */
349
+ export function boundedExecFileSync(file, args, tier, options) {
350
+ assertValidTier(tier);
351
+ const boundMs = effectiveTimeoutMs(tier, options);
352
+ const nodeOptions = toNodeOptions(options, boundMs);
353
+ try {
354
+ return nodeExecFileSync(file, args, nodeOptions);
355
+ } catch (error) {
356
+ throw enrichTimeoutError(error, { description: describeCommand(file, args), tier, boundMs });
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Bounded `child_process.spawnSync`. Same arguments and return shape as
362
+ * Node's `spawnSync(command, args, options)`, with a required `tier`
363
+ * inserted before `options`. Unlike the native API, `args` is always
364
+ * required (pass `[]` for none) so tier can occupy a fixed position.
365
+ *
366
+ * `spawnSync` never throws; on timeout the returned object has an `error`
367
+ * with `code: 'ETIMEDOUT'` whose message has been enriched in place. In the
368
+ * common case the direct child was still running and gets killed at the
369
+ * bound, so `status: null` and `signal: 'SIGKILL'`. If the direct child had
370
+ * already exited but a surviving descendant is still holding its
371
+ * stdout/stderr pipe open, `status`/`signal` instead reflect that real exit
372
+ * (e.g. `status: 0`, `signal: null`) while `error.code` is still
373
+ * `'ETIMEDOUT'` — check `error`, not just `status`/`signal`, to detect the
374
+ * timeout in that case.
375
+ *
376
+ * @param {string} command
377
+ * @param {readonly string[]} args
378
+ * @param {'quick'|'network'|'long'} tier
379
+ * @param {Record<string, unknown>} [options]
380
+ * @returns {import('node:child_process').SpawnSyncReturns<Buffer | string>}
381
+ */
382
+ export function boundedSpawnSync(command, args, tier, options) {
383
+ assertValidTier(tier);
384
+ const boundMs = effectiveTimeoutMs(tier, options);
385
+ const nodeOptions = toNodeOptions(options, boundMs);
386
+ const result = nodeSpawnSync(command, args, nodeOptions);
387
+ enrichTimeoutError(result.error, { description: describeCommand(command, args), tier, boundMs, signal: result.signal });
388
+ return result;
389
+ }
390
+
391
+ /** Async counterpart for launches that must clean owned files on termination.
392
+ * Like the sync wrappers, this supervises the direct child only. */
393
+ export async function boundedSpawn(command, args, tier, options, onTermination = () => {}) {
394
+ assertValidTier(tier);
395
+ const boundMs = effectiveTimeoutMs(tier, options);
396
+ const { timeout: _timeout, killSignal: _killSignal, ...nodeOptions } = toNodeOptions(options, boundMs);
397
+ let child;
398
+ let timer;
399
+ let timedOut = false;
400
+ const handlers = new Map();
401
+ try {
402
+ child = spawn(command, args, nodeOptions);
403
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
404
+ const handler = () => {
405
+ try { onTermination(); }
406
+ finally {
407
+ signalChild(child, 'SIGKILL');
408
+ // Restore the signal's normal exit semantics after cleanup.
409
+ process.removeListener(signal, handler);
410
+ process.kill(process.pid, signal);
411
+ }
412
+ };
413
+ handlers.set(signal, handler);
414
+ process.once(signal, handler);
415
+ }
416
+ if (boundMs > 0) timer = setTimeout(() => { timedOut = true; signalChild(child, 'SIGKILL'); }, boundMs);
417
+ return await new Promise(resolve => {
418
+ let spawnError;
419
+ child.once('error', error => { spawnError = error; });
420
+ child.once('close', (status, signal) => {
421
+ const error = timedOut ? Object.assign(new Error(`Command timed out after ${boundMs}ms`), { code: 'ETIMEDOUT' }) : spawnError;
422
+ enrichTimeoutError(error, { description: describeCommand(command, args), tier, boundMs, signal });
423
+ resolve({ status, signal, error, pid: child.pid, stdout: null, stderr: null, output: [] });
424
+ });
425
+ });
426
+ } finally {
427
+ clearTimeout(timer);
428
+ for (const [signal, handler] of handlers) process.removeListener(signal, handler);
429
+ }
430
+ }
@@ -0,0 +1,16 @@
1
+ export function resolveExecutable(
2
+ command: string,
3
+ platform?: NodeJS.Platform,
4
+ searchPath?: string,
5
+ pathExt?: string,
6
+ cwd?: string,
7
+ ): string;
8
+
9
+ export function cliCommand(
10
+ command: string,
11
+ args: string[],
12
+ platform?: NodeJS.Platform,
13
+ searchPath?: string,
14
+ pathExt?: string,
15
+ cwd?: string,
16
+ ): [string, string[]];