@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -4,8 +4,13 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { parseDocument, stringify } from 'yaml';
6
6
  import { findPackageRoot } from '../find-package-root.js';
7
+ import { isInteractive } from '../env.js';
8
+ import { boundedSpawnSync } from '../../process/bounded-child-process.mjs';
7
9
  import { sha256 } from '../../security/artifact-trust.js';
8
10
  import { artifactVerifyHandler } from './artifact-verify.js';
11
+ import { requireFlagValue, MissingFlagValueError } from '../flag-parsing.js';
12
+ import { exitCodeForSpawnResult } from '../child-process-result.js';
13
+ import { askString, createPromptInterface } from '../prompt-utils.js';
9
14
  const SETUP_SCHEMA_REL = 'agentic/code/addons/agentic-installer/schemas/v1/setup-manifest.schema.json';
10
15
  const GENERATE_HELP = `aiwg setup-generate - generate a starter setup.aiwg.io/v1 SetupManifest
11
16
 
@@ -67,15 +72,29 @@ Safety:
67
72
  Mutating execution refuses to run without explicit confirmation.
68
73
  Provider-orchestrated manifests are never handed to an agent unless their
69
74
  exact bytes have status verified under the explicit trust policy.
75
+
76
+ Headless input steps (#3062):
77
+ A 'type: ask' step resolves its declared 'params' the same way a 'script'
78
+ step's env vars do (--param/--params-file/env/default); if every declared
79
+ param is already resolved it runs headless, otherwise it is refused,
80
+ naming the missing param names — this CLI has no interactive-capture
81
+ mechanism for 'ask' steps, with or without a terminal. A step marked
82
+ 'interactive: true' runs an arbitrary script whose questions cannot be
83
+ enumerated; without a terminal it is refused outright. Every selected
84
+ step (including nested 'platform-route' steps) is preflighted for both
85
+ conditions before any step executes — never partway through.
70
86
  `;
87
+ /**
88
+ * A value-taking flag present without a usable value is a usage error, not
89
+ * "not given" (#3062 review item 11) — delegates to the shared strict
90
+ * parser. Callers (the three handlers below) catch `MissingFlagValueError`
91
+ * and report it before any preflight check, spawn, or write.
92
+ * `precedence: 'first'` matches this command's own pre-#3062 behavior (a
93
+ * hand-rolled loop that returned on the first match), unchanged by the
94
+ * shared parser's second-round fix (#3062 second review, finding 4).
95
+ */
71
96
  function flagValue(args, name) {
72
- for (let i = 0; i < args.length; i += 1) {
73
- if (args[i] === name) {
74
- const value = args[i + 1];
75
- return value && !value.startsWith('--') ? value : undefined;
76
- }
77
- }
78
- return undefined;
97
+ return requireFlagValue(args, {}, name);
79
98
  }
80
99
  function hasFlag(args, ...names) {
81
100
  return args.some((arg) => names.includes(arg));
@@ -238,6 +257,83 @@ function allSteps(manifest) {
238
257
  function existingRelative(manifestDir, rel) {
239
258
  return !!rel && existsSync(path.resolve(manifestDir, rel));
240
259
  }
260
+ /**
261
+ * Detect `on_fail` reference cycles among recovery procedures themselves —
262
+ * go/no-go review blocker 3. A step's `on_fail` can name any recovery
263
+ * procedure, including (structurally, per the shared `SetupStep` type) a
264
+ * step *inside* a recovery procedure's own `steps`. A recovery procedure
265
+ * that (directly, or through another recovery procedure) names itself used
266
+ * to make the old, eager preflight recurse into it forever —
267
+ * `RangeError: Maximum call stack size exceeded`, reproduced in memory by
268
+ * the review.
269
+ *
270
+ * This is a one-time, bounded graph walk over `manifest.spec.recovery[]`
271
+ * only (at most `recovery.length` nodes) — a visited/"on the current path"
272
+ * marker per node means each node is fully explored exactly once, so this
273
+ * terminates and never re-enters a node already being explored; it reports
274
+ * one finding per distinct cycle instead of throwing. An `on_fail` that
275
+ * does not resolve to a known recovery id is reported separately by the
276
+ * existing unresolved-recovery check (`installerConsistencyChecks` above)
277
+ * and is not itself a cycle.
278
+ *
279
+ * This graph check is independent of, and does not change, how `on_fail`
280
+ * references are actually used (`collectPreflightBlockers()`/`runStep()`
281
+ * below): execution never follows an `on_fail` reference more than once
282
+ * (a failing recovery step reports failure directly, it does not itself
283
+ * trigger further recovery), so neither the main-sequence preflight nor a
284
+ * selected recovery's own preflight recurses through `on_fail` at all —
285
+ * see `collectPreflightBlockers()`'s own doc comment. This cycle check
286
+ * exists for the case the type system permits but execution never
287
+ * exercises: a recovery procedure's own steps declaring `on_fail`.
288
+ */
289
+ function detectRecoveryReferenceCycles(manifest) {
290
+ const recoveries = manifest.spec.recovery ?? [];
291
+ if (recoveries.length === 0)
292
+ return [];
293
+ const onFailTargets = new Map();
294
+ for (const recovery of recoveries) {
295
+ const targets = [];
296
+ const collect = (steps) => {
297
+ steps?.forEach((step) => {
298
+ if (step.on_fail && step.on_fail !== 'recover')
299
+ targets.push(step.on_fail);
300
+ step.routes?.forEach((route) => collect(route.steps));
301
+ });
302
+ };
303
+ collect(recovery.steps);
304
+ onFailTargets.set(recovery.id, targets);
305
+ }
306
+ const findings = [];
307
+ const reportedCycles = new Set();
308
+ const state = new Map();
309
+ const visit = (id, path) => {
310
+ if (state.get(id) === 'done')
311
+ return;
312
+ if (state.get(id) === 'on-path') {
313
+ const cycle = [...path.slice(path.indexOf(id)), id];
314
+ const key = [...new Set(cycle)].sort().join('\u0000');
315
+ if (!reportedCycles.has(key)) {
316
+ reportedCycles.add(key);
317
+ findings.push({
318
+ severity: 'error',
319
+ path: '/spec/recovery',
320
+ rule: 'recoveryCycle',
321
+ message: `recovery reference cycle: ${cycle.join(' -> ')}`,
322
+ });
323
+ }
324
+ return;
325
+ }
326
+ state.set(id, 'on-path');
327
+ for (const next of onFailTargets.get(id) ?? []) {
328
+ if (onFailTargets.has(next))
329
+ visit(next, [...path, id]);
330
+ }
331
+ state.set(id, 'done');
332
+ };
333
+ for (const recovery of recoveries)
334
+ visit(recovery.id, []);
335
+ return findings;
336
+ }
241
337
  function installerConsistencyChecks(manifest, manifestDir) {
242
338
  if (!manifest)
243
339
  return [];
@@ -262,6 +358,8 @@ function installerConsistencyChecks(manifest, manifestDir) {
262
358
  findings.push({ severity: 'error', path: `/spec/steps/${index}/on_fail`, rule: 'on_fail', message: `unresolved recovery '${step.on_fail}'` });
263
359
  }
264
360
  }
361
+ // Go/no-go review blocker 3.
362
+ findings.push(...detectRecoveryReferenceCycles(manifest));
265
363
  for (const { step, pointer } of allSteps(manifest)) {
266
364
  if (step.type === 'script' && !existingRelative(manifestDir, step.script)) {
267
365
  findings.push({ severity: 'error', path: `${pointer}/script`, rule: 'scriptExists', message: `script not found: ${step.script ?? '(missing)'}` });
@@ -602,13 +700,41 @@ function planLines(manifest, manifestPath, target, params, steps) {
602
700
  });
603
701
  return lines.join('\n') + '\n';
604
702
  }
605
- function commandResult(status) {
606
- const code = status === null ? 1 : status;
607
- return { ok: code === 0, status: code };
703
+ /**
704
+ * #3062 review item 7: a `null` status already falls through to `code = 1`
705
+ * (not success); this also surfaces the bounded helper's `error`/`signal`
706
+ * diagnostic (e.g. the enriched timeout message naming the bound and
707
+ * override variable) instead of silently dropping it.
708
+ */
709
+ function commandResult(result) {
710
+ const outcome = exitCodeForSpawnResult(result);
711
+ if (outcome.message)
712
+ console.error(`[setup] ${outcome.message}`);
713
+ const code = result.status === null ? 1 : result.status;
714
+ return { ok: code === 0 && !result.error && !result.signal, status: code };
608
715
  }
716
+ /**
717
+ * Run one manifest-author-defined shell command (#3062).
718
+ *
719
+ * Without a terminal, a command that tries to prompt must fail fast instead
720
+ * of hanging forever: bounded (long tier, stdin ignored). With a terminal,
721
+ * unchanged — inherited stdio, unbounded, so a human can actually answer a
722
+ * prompt the command issues.
723
+ */
609
724
  function runShell(command, cwd, env) {
725
+ if (!isInteractive()) {
726
+ const result = boundedSpawnSync(command, [], 'long', {
727
+ cwd, env, shell: true, stdio: ['ignore', 'inherit', 'inherit'],
728
+ });
729
+ return commandResult(result);
730
+ }
731
+ // sync-spawn: interactive manifest-shell-command — runs an arbitrary
732
+ // manifest-author-defined shell command with inherited stdio; it may
733
+ // itself prompt (see tools/testing/sync-spawn-allowlist.json). Reached
734
+ // only when isInteractive() is true (#3062) — the no-terminal path above
735
+ // uses the bounded helper instead. Not a candidate for a bounded timeout.
610
736
  const result = spawnSync(command, { cwd, env, shell: true, stdio: 'inherit' });
611
- return commandResult(result.status);
737
+ return commandResult(result);
612
738
  }
613
739
  function isDestructiveScript(filePath) {
614
740
  const name = path.basename(filePath);
@@ -619,9 +745,180 @@ function isDestructiveScript(filePath) {
619
745
  const text = readFileSync(filePath, 'utf8');
620
746
  return /\brm\s+-rf\b|\b(cp|mv)\s+-f\b|>\s*\S+/.test(text);
621
747
  }
622
- function runStep(step, manifest, manifestDir, params, targetOs, confirmed, recovery = false) {
748
+ /**
749
+ * Param names a `type: ask` step resolves (#3062 review item 10). Reuses
750
+ * the same `params` field `type: script` steps already use to pass env
751
+ * vars — there is no separate "ask step input" field in the schema, and
752
+ * this is the same mechanism, just read by the `ask` branch below instead
753
+ * of passed to a script.
754
+ */
755
+ function askStepParams(step) {
756
+ return step.params ?? [];
757
+ }
758
+ /** Which of an `ask` step's declared params are not yet resolved (missing from `--param`/params-file/env/default). */
759
+ function missingAskParams(step, params) {
760
+ return askStepParams(step).filter((name) => params[name] === undefined);
761
+ }
762
+ /**
763
+ * Preflight every step in `steps` (recursively into `platform-route` nested
764
+ * steps only) for conditions that would block it from running at all, and
765
+ * return one message per blocker — never a partial list halted at the
766
+ * first step (#3062 review item 13, and the second review's finding 7:
767
+ * refusal must happen before ANY step executes).
768
+ *
769
+ * Used for two distinct call sites, both passing only the steps that are
770
+ * actually about to run:
771
+ *
772
+ * 1. The main selected sequence, up front, before any of it runs
773
+ * (`runSetupManifest()`'s own preflight call, below).
774
+ * 2. A *specific* recovery procedure's own steps, called again at the
775
+ * moment a failed step selects it — completely, before that
776
+ * recovery's first step runs (go/no-go review blocker 3).
777
+ *
778
+ * This function never itself follows a step's `on_fail` into another
779
+ * recovery procedure, in either case — go/no-go review blocker 3's
780
+ * "preflight is too eager" finding: the previous version always recursed
781
+ * into every selected step's `on_fail` recovery during the main, up-front
782
+ * preflight, which (a) refused a manifest whose main sequence would
783
+ * succeed outright, solely because its *unused* recovery contained an
784
+ * unsupported step, and (b) had no cycle protection, so a recovery whose
785
+ * own steps' `on_fail` pointed back into a recovery already being
786
+ * preflighted recursed without bound (`RangeError: Maximum call stack
787
+ * size exceeded`, reproduced by the review). Not recursing into `on_fail`
788
+ * here also matches what execution itself does: a failing step triggers
789
+ * its *one* named recovery exactly once; a failing step *within* that
790
+ * recovery reports failure directly rather than following a further
791
+ * `on_fail` — so preflight never needs to follow one either. (A
792
+ * recovery-to-recovery `on_fail` reference cycle in the manifest itself is
793
+ * still caught — as a graph check over `manifest.spec.recovery[]`, not a
794
+ * recursive preflight — by `detectRecoveryReferenceCycles()` during
795
+ * manifest validation.)
796
+ *
797
+ * - `interactive: true` without a terminal AND without every declared
798
+ * `params` entry resolved: with every declared param supplied, the
799
+ * step runs headless instead (review item 10; second review finding
800
+ * 5b — the step itself is trusted not to need to prompt once its
801
+ * declared inputs are present). With no declared params at all
802
+ * ("undeclared interaction" — the step's questions cannot be
803
+ * enumerated or supplied via `--param`) this always requires a
804
+ * terminal.
805
+ * - `type: ask` with no declared `params`: always refused — unlike
806
+ * `interactive: true`, this CLI itself owns the prompting for `ask`
807
+ * steps (there is no separate script to hand a terminal to), and
808
+ * with nothing declared there is no destination to store an answer
809
+ * in. This is one of the two agreed, permanent exceptions (the other
810
+ * is the contributor sync-upstream strategy choice).
811
+ * - `type: ask` with declared params not all resolved, and no
812
+ * terminal: refused, naming the missing names so `--param` can
813
+ * supply them. With a terminal, this is *not* a blocker — the
814
+ * missing ones are prompted for when the step actually runs (second
815
+ * review finding 5b).
816
+ * - `type: agentic`/`type: chain`: `runStep()` refuses both
817
+ * unconditionally; preflighted the same way so an earlier, unrelated
818
+ * step is never executed ahead of this unconditional refusal (second
819
+ * review finding 7).
820
+ */
821
+ function collectPreflightBlockers(steps, manifest, targetOs, params, terminalPresent) {
822
+ const blockers = [];
823
+ for (const step of steps) {
824
+ if (!stepPlatformMatches(targetOs, step))
825
+ continue;
826
+ if (step.interactive) {
827
+ const names = askStepParams(step);
828
+ const missing = missingAskParams(step, params);
829
+ const fullySupplied = names.length > 0 && missing.length === 0;
830
+ if (!fullySupplied && !terminalPresent) {
831
+ blockers.push(names.length === 0
832
+ ? `step '${step.id}' is marked interactive (its prompts cannot be enumerated or supplied via --param) and requires a terminal`
833
+ : `step '${step.id}' is marked interactive and is missing required param(s): ${missing.join(', ')} (supply with --param <name>=<value>, or run with a terminal attached)`);
834
+ }
835
+ }
836
+ if (step.type === 'ask') {
837
+ const names = askStepParams(step);
838
+ if (names.length === 0) {
839
+ blockers.push(`ask step '${step.id}' declares no params to resolve via --param; interactive capture for ask ` +
840
+ "steps is not implemented — declare 'params' on the step and supply them with --param <name>=<value>");
841
+ }
842
+ else {
843
+ const missing = missingAskParams(step, params);
844
+ if (missing.length > 0 && !terminalPresent) {
845
+ blockers.push(`ask step '${step.id}' is missing required param(s): ${missing.join(', ')} (supply with --param ` +
846
+ '<name>=<value>, or run with a terminal attached to be prompted for them)');
847
+ }
848
+ }
849
+ }
850
+ if (step.type === 'agentic' || step.type === 'chain') {
851
+ blockers.push(`${step.type} step '${step.id}' is not supported by deterministic CLI execution and is always refused`);
852
+ }
853
+ if (step.type === 'platform-route') {
854
+ const route = step.routes?.find((item) => item.platform === targetOs);
855
+ if (route)
856
+ blockers.push(...collectPreflightBlockers(route.steps, manifest, targetOs, params, terminalPresent));
857
+ }
858
+ }
859
+ return blockers;
860
+ }
861
+ /**
862
+ * Preflight a *specific* recovery procedure's own steps — go/no-go review
863
+ * blocker 3. Called at the moment a failed step selects it, completely,
864
+ * before any of its steps run. Returns `undefined` when the recovery id
865
+ * does not resolve to a known procedure (the schema/consistency check
866
+ * already reports that as a manifest error; this function's caller treats
867
+ * an unresolved id as "no recovery available", same as before).
868
+ */
869
+ function findRecoveryProcedure(manifest, recoveryId) {
870
+ return manifest.spec.recovery?.find((item) => item.id === recoveryId);
871
+ }
872
+ /**
873
+ * Prompt interactively for each of `missingNames`, one per declared `ask`-
874
+ * step param still unresolved (#3062 second review, finding 5b: declared
875
+ * ask inputs missing AT a terminal are prompted for, not refused — this
876
+ * is reachable only when `isInteractive()` is true, so it is always safe
877
+ * to open a prompt interface here).
878
+ */
879
+ async function promptForAskStep(step, missingNames) {
880
+ const rl = createPromptInterface();
881
+ const answers = {};
882
+ try {
883
+ if (step.message)
884
+ console.log(`[setup] ${step.message}`);
885
+ for (const name of missingNames) {
886
+ const answer = await askString(rl, ` ${name}: `, '', undefined);
887
+ if (answer)
888
+ answers[name] = answer;
889
+ }
890
+ }
891
+ finally {
892
+ rl.close();
893
+ }
894
+ return answers;
895
+ }
896
+ async function runStep(step, manifest, manifestDir, params, targetOs, confirmed, recovery = false) {
623
897
  if (!stepPlatformMatches(targetOs, step))
624
898
  return { ok: true, status: 0 };
899
+ // A step marked `interactive: true` (#3062) declares that it prompts
900
+ // directly. For a top-level selected step this condition is already
901
+ // preflighted before any step runs (review item 13; second review
902
+ // finding 7); this remains the only gate for a recovery step, which
903
+ // isn't known until a step actually fails. With every declared `params`
904
+ // entry resolved, the step is trusted not to need to prompt and runs
905
+ // headless below like any other step (second review finding 5b — the
906
+ // bug this fixes: it used to be refused here even when fully supplied).
907
+ // With no declared params at all ("undeclared interaction") or some
908
+ // still missing, a terminal is required.
909
+ if (step.interactive) {
910
+ const names = askStepParams(step);
911
+ const missing = missingAskParams(step, params);
912
+ const fullySupplied = names.length > 0 && missing.length === 0;
913
+ if (!fullySupplied && !isInteractive()) {
914
+ console.error(names.length === 0
915
+ ? `setup-run: step '${step.id}' is marked interactive (its prompts cannot be enumerated or supplied ` +
916
+ 'via --param) and requires a terminal; re-run from an interactive shell.'
917
+ : `setup-run: step '${step.id}' is marked interactive and is missing required param(s): ` +
918
+ `${missing.join(', ')}; supply with --param <name>=<value>, or re-run from an interactive shell.`);
919
+ return { ok: false, status: 2, failedStep: step.id };
920
+ }
921
+ }
625
922
  const env = { ...process.env, ...params };
626
923
  console.log(`[setup] Step ${step.id} (${step.type})`);
627
924
  if (step.type === 'script') {
@@ -634,8 +931,19 @@ function runStep(step, manifest, manifestDir, params, targetOs, confirmed, recov
634
931
  console.error(`setup-run: refusing destructive step '${step.id}' without --yes/--confirm`);
635
932
  return { ok: false, status: 2, failedStep: step.id };
636
933
  }
637
- const result = spawnSync(scriptPath, [], { cwd: manifestDir, env, stdio: 'inherit' });
638
- const ran = commandResult(result.status);
934
+ // Without a terminal, a script that tries to prompt must fail fast
935
+ // instead of hanging forever: bounded (long tier, stdin ignored). With
936
+ // a terminal, unchanged (#3062).
937
+ const result = isInteractive()
938
+ // sync-spawn: interactive manifest-script-step — runs an arbitrary
939
+ // manifest-author-defined script file with inherited stdio; it may
940
+ // itself prompt (see tools/testing/sync-spawn-allowlist.json). Reached
941
+ // only when isInteractive() is true (#3062) — the no-terminal path
942
+ // uses the bounded helper instead. Not a candidate for a bounded
943
+ // timeout.
944
+ ? spawnSync(scriptPath, [], { cwd: manifestDir, env, stdio: 'inherit' })
945
+ : boundedSpawnSync(scriptPath, [], 'long', { cwd: manifestDir, env, stdio: ['ignore', 'inherit', 'inherit'] });
946
+ const ran = commandResult(result);
639
947
  if (!ran.ok)
640
948
  return { ...ran, failedStep: step.id };
641
949
  const verifies = Array.isArray(step.verify) ? step.verify : step.verify ? [step.verify] : [];
@@ -677,13 +985,51 @@ function runStep(step, manifest, manifestDir, params, targetOs, confirmed, recov
677
985
  return { ok: false, status: 2, failedStep: step.id };
678
986
  }
679
987
  if (step.type === 'ask') {
680
- console.error(`setup-run: ask step '${step.id}' requires interactive input; use params before execution`);
681
- return { ok: false, status: 2, failedStep: step.id };
988
+ // #3062 review item 10: a top-level selected step with all its declared
989
+ // params already resolved is already caught as *not* blocking by
990
+ // `collectPreflightBlockers` and runs headless here; this remains the
991
+ // gate for a recovery-path `ask` step, which isn't preflighted.
992
+ const names = askStepParams(step);
993
+ if (names.length === 0) {
994
+ // Agreed, permanent exception (second review finding 5b): this CLI
995
+ // owns the prompting for `ask` steps directly (there is no separate
996
+ // script to hand a terminal to), and with nothing declared there is
997
+ // no destination to store an answer in — always refused, with or
998
+ // without a terminal.
999
+ console.error(`setup-run: ask step '${step.id}' declares no params to resolve via --param; interactive capture ` +
1000
+ "for ask steps is not implemented — declare 'params' on the step and supply them with --param " +
1001
+ '<name>=<value>.');
1002
+ return { ok: false, status: 2, failedStep: step.id };
1003
+ }
1004
+ const missing = missingAskParams(step, params);
1005
+ if (missing.length === 0) {
1006
+ console.log(` ask: ${step.message ?? step.id} -> answered via --param (${names.join(', ')})`);
1007
+ return { ok: true, status: 0 };
1008
+ }
1009
+ if (!isInteractive()) {
1010
+ console.error(`setup-run: ask step '${step.id}' is missing required param(s): ${missing.join(', ')}; supply with ` +
1011
+ '--param <name>=<value>, or re-run from an interactive shell to be prompted for them.');
1012
+ return { ok: false, status: 2, failedStep: step.id };
1013
+ }
1014
+ // #3062 second review, finding 5b: declared ask inputs missing AT a
1015
+ // terminal are prompted for, not refused.
1016
+ const answers = await promptForAskStep(step, missing);
1017
+ const stillMissing = missing.filter((name) => !answers[name]);
1018
+ if (stillMissing.length > 0) {
1019
+ console.error(`setup-run: ask step '${step.id}' did not receive a value for: ${stillMissing.join(', ')}.`);
1020
+ return { ok: false, status: 2, failedStep: step.id };
1021
+ }
1022
+ // Merged into the same object reference the caller's loop (and every
1023
+ // later step) reads from, so a later step sees the interactively-
1024
+ // captured answer exactly like a `--param`-supplied one.
1025
+ Object.assign(params, answers);
1026
+ console.log(` ask: ${step.message ?? step.id} -> answered interactively (${missing.join(', ')})`);
1027
+ return { ok: true, status: 0 };
682
1028
  }
683
1029
  if (step.type === 'platform-route') {
684
1030
  const route = step.routes?.find((item) => item.platform === targetOs);
685
1031
  for (const nested of route?.steps ?? []) {
686
- const result = runStep(nested, manifest, manifestDir, params, targetOs, confirmed);
1032
+ const result = await runStep(nested, manifest, manifestDir, params, targetOs, confirmed);
687
1033
  if (!result.ok)
688
1034
  return result;
689
1035
  }
@@ -691,7 +1037,7 @@ function runStep(step, manifest, manifestDir, params, targetOs, confirmed, recov
691
1037
  }
692
1038
  return { ok: true, status: 0 };
693
1039
  }
694
- export function runSetupManifest(options) {
1040
+ export async function runSetupManifest(options) {
695
1041
  const validation = validateSetupManifest({
696
1042
  cwd: options.cwd,
697
1043
  frameworkRoot: options.frameworkRoot,
@@ -735,15 +1081,44 @@ export function runSetupManifest(options) {
735
1081
  if (!options.yes) {
736
1082
  return { exitCode: 2, message: 'setup-run: mutating execution requires --yes or --confirm after reviewing the plan with --dry-run' };
737
1083
  }
1084
+ // #3062 review item 13: preflight every selected step (recursively into
1085
+ // `platform-route` nesting) before executing any of them — the previous
1086
+ // behavior checked `interactive`/`ask` conditions inside the per-step
1087
+ // loop below, so an earlier normal step could already perform work
1088
+ // before a later step's refusal fired.
1089
+ const blockers = collectPreflightBlockers(steps, manifest, target.os, params.values, isInteractive());
1090
+ if (blockers.length > 0) {
1091
+ for (const blocker of blockers)
1092
+ console.error(`setup-run: ${blocker}.`);
1093
+ return {
1094
+ exitCode: 2,
1095
+ message: `setup-run: refusing before executing any step:\n${blockers.map((b) => ` - ${b}`).join('\n')}`,
1096
+ };
1097
+ }
738
1098
  for (const step of steps) {
739
- const result = runStep(step, manifest, validation.manifestDir, params.values, target.os, options.yes);
1099
+ const result = await runStep(step, manifest, validation.manifestDir, params.values, target.os, options.yes);
740
1100
  if (!result.ok) {
741
1101
  if (result.failedStep && step.on_fail) {
742
- const recovery = manifest.spec.recovery?.find((item) => item.id === step.on_fail);
1102
+ const recovery = findRecoveryProcedure(manifest, step.on_fail);
743
1103
  if (recovery) {
1104
+ // Go/no-go review blocker 3: preflight the *selected* recovery's
1105
+ // own steps completely, now that a failure has actually chosen
1106
+ // it — not when the manifest was first loaded (that preflighted
1107
+ // the main sequence only, above) — and before any of its steps
1108
+ // run. Refuses with zero recovery steps executed, same
1109
+ // all-blockers-at-once shape as the main-sequence preflight.
1110
+ const recoveryBlockers = collectPreflightBlockers(recovery.steps, manifest, target.os, params.values, isInteractive());
1111
+ if (recoveryBlockers.length > 0) {
1112
+ for (const blocker of recoveryBlockers)
1113
+ console.error(`setup-run: [recovery '${recovery.id}'] ${blocker}.`);
1114
+ return {
1115
+ exitCode: 2,
1116
+ message: `setup-run: recovery '${recovery.id}' (for failed step '${result.failedStep}') refusing before executing any recovery step:\n${recoveryBlockers.map((b) => ` - ${b}`).join('\n')}`,
1117
+ };
1118
+ }
744
1119
  console.error(`[setup] recovery '${recovery.id}' available for failed step '${result.failedStep}'`);
745
1120
  for (const recoveryStep of recovery.steps) {
746
- const recoveryResult = runStep(recoveryStep, manifest, validation.manifestDir, params.values, target.os, options.yes, true);
1121
+ const recoveryResult = await runStep(recoveryStep, manifest, validation.manifestDir, params.values, target.os, options.yes, true);
747
1122
  if (!recoveryResult.ok)
748
1123
  return { exitCode: recoveryResult.status || 1, message: `setup-run: recovery '${recovery.id}' failed` };
749
1124
  }
@@ -771,11 +1146,22 @@ export const setupValidateHandler = {
771
1146
  return { exitCode: 0 };
772
1147
  }
773
1148
  const strict = hasFlag(ctx.args, '--strict');
1149
+ let manifestPath;
1150
+ let schemaPath;
1151
+ try {
1152
+ manifestPath = flagValue(ctx.args, '--manifest') ?? positionalManifest(ctx.args);
1153
+ schemaPath = flagValue(ctx.args, '--schema');
1154
+ }
1155
+ catch (err) {
1156
+ if (err instanceof MissingFlagValueError)
1157
+ return { exitCode: 2, message: `${err.message}.` };
1158
+ throw err;
1159
+ }
774
1160
  const result = validateSetupManifest({
775
1161
  cwd: ctx.cwd,
776
1162
  frameworkRoot: ctx.frameworkRoot,
777
- manifestPath: flagValue(ctx.args, '--manifest') ?? positionalManifest(ctx.args),
778
- schemaPath: flagValue(ctx.args, '--schema'),
1163
+ manifestPath,
1164
+ schemaPath,
779
1165
  strict,
780
1166
  });
781
1167
  const payload = {
@@ -807,8 +1193,9 @@ export const setupGenerateHandler = {
807
1193
  }
808
1194
  catch (error) {
809
1195
  return {
810
- exitCode: 1,
811
- message: error instanceof Error ? error.message : String(error),
1196
+ // #3062 review item 11: a missing flag value is a usage error.
1197
+ exitCode: error instanceof MissingFlagValueError ? 2 : 1,
1198
+ message: error instanceof Error ? `${error.message}.` : String(error),
812
1199
  };
813
1200
  }
814
1201
  },
@@ -824,21 +1211,34 @@ export const setupRunHandler = {
824
1211
  process.stdout.write(RUN_HELP);
825
1212
  return { exitCode: 0 };
826
1213
  }
827
- const options = parseRunOptions(ctx);
828
- const manifestPath = options.manifestPath ?? 'setup.manifest.yaml';
1214
+ let options;
1215
+ let policyFlag;
1216
+ let verificationArgs = [];
1217
+ try {
1218
+ options = parseRunOptions(ctx);
1219
+ policyFlag = flagValue(ctx.args, '--policy');
1220
+ if (policyFlag) {
1221
+ const manifestPathForVerification = options.manifestPath ?? 'setup.manifest.yaml';
1222
+ verificationArgs = [
1223
+ manifestPathForVerification,
1224
+ '--attestation', flagValue(ctx.args, '--attestation') ?? `${manifestPathForVerification}.aiwg-attestation.json`,
1225
+ '--policy', policyFlag,
1226
+ '--asset-type', flagValue(ctx.args, '--asset-type') ?? 'setup-manifest',
1227
+ '--namespace', flagValue(ctx.args, '--namespace') ?? 'aiwg',
1228
+ '--channel', flagValue(ctx.args, '--channel') ?? 'stable',
1229
+ '--json',
1230
+ ...(flagValue(ctx.args, '--state') ? ['--state', flagValue(ctx.args, '--state')] : []),
1231
+ ...(hasFlag(ctx.args, '--offline') ? ['--offline'] : []),
1232
+ ];
1233
+ }
1234
+ }
1235
+ catch (err) {
1236
+ if (err instanceof MissingFlagValueError)
1237
+ return { exitCode: 2, message: `setup-run: ${err.message}.` };
1238
+ throw err;
1239
+ }
829
1240
  let artifactVerification;
830
- if (flagValue(ctx.args, '--policy')) {
831
- const verificationArgs = [
832
- manifestPath,
833
- '--attestation', flagValue(ctx.args, '--attestation') ?? `${manifestPath}.aiwg-attestation.json`,
834
- '--policy', flagValue(ctx.args, '--policy'),
835
- '--asset-type', flagValue(ctx.args, '--asset-type') ?? 'setup-manifest',
836
- '--namespace', flagValue(ctx.args, '--namespace') ?? 'aiwg',
837
- '--channel', flagValue(ctx.args, '--channel') ?? 'stable',
838
- '--json',
839
- ...(flagValue(ctx.args, '--state') ? ['--state', flagValue(ctx.args, '--state')] : []),
840
- ...(hasFlag(ctx.args, '--offline') ? ['--offline'] : []),
841
- ];
1241
+ if (policyFlag) {
842
1242
  const verification = await artifactVerifyHandler.execute({ ...ctx, args: verificationArgs, rawArgs: verificationArgs });
843
1243
  try {
844
1244
  artifactVerification = JSON.parse(verification.message ?? '');