@aiwg/cli 2026.10.1 → 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 (166) 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 +210 -4
  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 +75 -27
  38. package/dist/src/decision/driver.js +3 -1
  39. package/dist/src/decision/entry.js +38 -6
  40. package/dist/src/decision/heldout/collector.js +10 -1
  41. package/dist/src/decision/index.js +1 -0
  42. package/dist/src/decision/interop.js +60 -8
  43. package/dist/src/decision/ledger.js +839 -0
  44. package/dist/src/decision/validate.js +6 -3
  45. package/dist/src/effects/dispatch-verifier.js +252 -0
  46. package/dist/src/effects/index.js +1 -0
  47. package/dist/src/features/installer.js +2 -2
  48. package/dist/src/flow/graph-metadata.js +5 -0
  49. package/dist/src/gates/providers/loader.js +3 -1
  50. package/dist/src/installation/manager-command.mjs +1 -1
  51. package/dist/src/installation/manager.mjs +17 -2
  52. package/dist/src/lint/runner.js +2 -2
  53. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  54. package/dist/src/mcp/cli.mjs +587 -77
  55. package/dist/src/mcp/config-file.mjs +279 -0
  56. package/dist/src/mcp/config-layers.mjs +348 -0
  57. package/dist/src/mcp/credentials.mjs +484 -0
  58. package/dist/src/mcp/grok-build-config.mjs +16 -26
  59. package/dist/src/mcp/helpers.mjs +106 -2
  60. package/dist/src/mcp/omp-config.mjs +50 -29
  61. package/dist/src/mcp/profiles.js +97 -23
  62. package/dist/src/mcp/profiles.mjs +99 -27
  63. package/dist/src/mcp/registry.js +264 -57
  64. package/dist/src/mcp/registry.mjs +198 -45
  65. package/dist/src/mcp/server.mjs +403 -20
  66. package/dist/src/mcp/toml-string.mjs +9 -0
  67. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  68. package/dist/src/mcp/tool-filters.mjs +357 -0
  69. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  70. package/dist/src/mcp/tools/decision.mjs +21 -3
  71. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  72. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  73. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  74. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  75. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  76. package/dist/src/memory/project-registry.js +2 -2
  77. package/dist/src/mission-protocol/codecs.js +639 -1
  78. package/dist/src/mission-protocol/types.js +110 -0
  79. package/dist/src/mission-protocol/worker-status.js +2 -0
  80. package/dist/src/models/config-loader.js +70 -13
  81. package/dist/src/models/grok-build-models.js +93 -41
  82. package/dist/src/models/model-capabilities.v1.json +3091 -24
  83. package/dist/src/models/model-discovery.js +862 -63
  84. package/dist/src/models/provider-policy.js +625 -11
  85. package/dist/src/models/qualification-digest.mjs +17 -0
  86. package/dist/src/models/resolver.js +259 -0
  87. package/dist/src/models/worker-direct-readiness.js +529 -0
  88. package/dist/src/models/worker-dispatch-offer.js +169 -0
  89. package/dist/src/models/worker-health-readiness.js +131 -0
  90. package/dist/src/models/worker-installation.mjs +879 -0
  91. package/dist/src/models/wrapper-route.js +18 -0
  92. package/dist/src/ops/registry.js +9 -14
  93. package/dist/src/plugins/standalone-packager.js +4 -3
  94. package/dist/src/process/bounded-child-process.mjs +387 -0
  95. package/dist/src/process/cli-command.mjs +75 -0
  96. package/dist/src/providers/capability-matrix.js +8 -0
  97. package/dist/src/providers/capability-matrix.yaml +270 -0
  98. package/dist/src/providers/grok-build-headless.js +43 -21
  99. package/dist/src/providers/provider-definitions.js +9 -1
  100. package/dist/src/providers/provider-definitions.mjs +9 -2
  101. package/dist/src/serve/executor-registry.js +16 -24
  102. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  103. package/dist/src/serve/mission-conductor.js +9 -0
  104. package/dist/src/serve/shared-host-scheduler.js +2 -0
  105. package/dist/src/serve/worker-invocation.js +538 -0
  106. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  107. package/dist/src/sessions/adapter-registry.js +45 -0
  108. package/dist/src/sessions/adapters/generic.js +37 -3
  109. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  110. package/dist/src/sessions/adapters/grokbot.js +423 -24
  111. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  112. package/dist/src/sessions/adapters/muse.js +68 -29
  113. package/dist/src/sessions/adapters/pi.js +1 -0
  114. package/dist/src/sessions/batch-contracts.js +13 -1
  115. package/dist/src/sessions/batch-import.js +23 -42
  116. package/dist/src/sessions/candidates.js +47 -11
  117. package/dist/src/sessions/contracts.js +35 -0
  118. package/dist/src/sessions/coverage-preflight.js +134 -0
  119. package/dist/src/sessions/discovery.js +3 -0
  120. package/dist/src/sessions/grok-build-cli.js +67 -5
  121. package/dist/src/sessions/importer.js +24 -8
  122. package/dist/src/sessions/index.js +2 -0
  123. package/dist/src/sessions/repository.js +54 -13
  124. package/dist/src/sessions/workspace-discovery.js +99 -37
  125. package/dist/src/skills/adapters/clawhub.js +10 -8
  126. package/dist/src/skills/adapters/openclaw.js +10 -7
  127. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  128. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  129. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  130. package/dist/src/smiths/mcpsmith/example.js +1 -1
  131. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  132. package/dist/src/storage/backends/logseq.js +68 -7
  133. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  134. package/dist/src/testing/generators/test-code-generator.js +57 -30
  135. package/dist/src/uhp/client.js +11 -1
  136. package/dist/src/uhp/mission.js +5 -0
  137. package/dist/src/update/checker.mjs +5 -5
  138. package/dist/src/update/notifier.mjs +54 -2
  139. package/dist/src/writing/fidelity.js +18 -1
  140. package/package.json +18 -1
  141. package/schemas/mission-v1.schema.json +2429 -0
  142. package/schemas/models/model-catalog.v1.schema.json +301 -0
  143. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  144. package/schemas/models/model-policy.v1.schema.json +14 -0
  145. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  146. package/tools/_resolve-impl.mjs +39 -0
  147. package/tools/agents/deploy-agents.mjs +15 -2
  148. package/tools/agents/providers/base.mjs +49 -32
  149. package/tools/agents/providers/grokbot.mjs +11 -12
  150. package/tools/agents/providers/hermes.mjs +6 -3
  151. package/tools/agents/providers/omp.mjs +5 -2
  152. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  153. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  154. package/tools/providers/omp-transport.mjs +192 -0
  155. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  156. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  157. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  158. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  159. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  160. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  161. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  162. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  163. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  164. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  165. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  166. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -4,9 +4,21 @@
4
4
  * Implements `aiwg init` — the first-run wizard that creates
5
5
  * `.aiwg/aiwg.config` for the current project.
6
6
  *
7
- * Prompts:
8
- * 1. Which AI provider toolchains does this project target?
9
- * 2. Would you like to add any scripts? (optional)
7
+ * Prompts, and the argument that answers each one directly (#3062):
8
+ * 1. Which AI provider toolchains does this project target? → `--providers <list>`
9
+ * 2. Would you like to add default scripts (deploy/doctor/sync)? → `--default-scripts true|false`
10
+ * 3. Add a custom script (name + command)? → `--script <name>=<command>` (repeatable;
11
+ * implies yes) or `--custom-scripts true|false` (answers the question directly
12
+ * without supplying a script; `false` declines it outright)
13
+ * 4. Enable direct worker dispatch? → `--worker-dispatch enable|decline`
14
+ *
15
+ * With a terminal attached, any of these given directly skips that
16
+ * question — only genuinely missing values are still asked. Without one,
17
+ * omitted inputs fall back to their documented defaults (`claude`, default
18
+ * scripts on) rather than erroring, since those defaults are always safe to
19
+ * apply headless. `--non-interactive`/`--yes` force the non-interactive
20
+ * path even with a terminal attached. `--dry-run` never writes anything,
21
+ * including the baseline config itself.
10
22
  *
11
23
  * Runs automatically on first `aiwg use` when no config exists.
12
24
  * Can also be run explicitly to reconfigure.
@@ -16,10 +28,14 @@
16
28
  import readline from 'readline';
17
29
  import { readAiwgConfig, writeAiwgConfig, emptyConfig, populateDeployedTo, VALID_PROVIDERS, getConfigPath, } from '../../config/aiwg-config.js';
18
30
  import * as ui from '../ui.js';
19
- import { askString as sharedAskString, askYesNo as sharedAskYesNo } from '../prompt-utils.js';
31
+ import { askString as sharedAskString, askYesNo as sharedAskYesNo, askConsent } from '../prompt-utils.js';
20
32
  import { normalizeProviderDefinitionId } from '../../providers/provider-definitions.js';
21
33
  import { writeNormalizedAiwgMd } from '../../smiths/context-pipeline/finalization.js';
22
34
  import { ensureWorkspaceContext } from '../../smiths/context-pipeline/workspace-context.js';
35
+ import { detectInstalledWorkerClis, evaluateWorkerDispatchOffer, workerDispatchOfferQuestion, workerDispatchOfferHintWithArg, parseWorkerDispatchArg, } from '../../models/worker-dispatch-offer.js';
36
+ import { EXIT_CODES } from '../errors.js';
37
+ import { isInteractive } from '../env.js';
38
+ import { requireFlagValue, collectFlagValues, parseBooleanFlagValue, parseListFlagValue, parseKeyValueEntry, MissingFlagValueError, } from '../flag-parsing.js';
23
39
  const PROVIDER_LABELS = {
24
40
  antigravity: 'Google Antigravity .agents/',
25
41
  claude: 'Claude Code .claude/',
@@ -76,6 +92,89 @@ async function askProviders(rl, signal) {
76
92
  }
77
93
  return selected.length > 0 ? selected : ['claude'];
78
94
  }
95
+ /**
96
+ * Non-interactive / --yes / no-TTY runs never prompt and never touch the
97
+ * config (#3050 item 2) — they only print one line naming the enable
98
+ * command, when two or more worker CLIs are detected and dispatch is not
99
+ * already on or declined. The hint also names `--worker-dispatch` (#3062):
100
+ * that argument answers the same question headlessly.
101
+ */
102
+ function renderWorkerDispatchHintIfApplicable(parallelism, setup) {
103
+ const offer = evaluateWorkerDispatchOffer(parallelism, setup, detectInstalledWorkerClis());
104
+ if (offer.shouldOffer)
105
+ ui.dim(` ${workerDispatchOfferHintWithArg(offer.detected)}`);
106
+ }
107
+ function parseInitArgInputs(args) {
108
+ // #3062 second review, finding 4: `precedence: 'first'` matches this
109
+ // command's pre-existing (and, for `--providers`/`--default-scripts`/
110
+ // `--script`/`--custom-scripts`, only) convention — see
111
+ // `parseInitArgInputs`'s module-level callers for why `--worker-dispatch`
112
+ // specifically needs `rejectEmpty` (an explicit empty choice is never
113
+ // meaningful and must not silently fall through to "not given").
114
+ const workerDispatchArg = parseWorkerDispatchArg(requireFlagValue(args, { rejectEmpty: true }, '--worker-dispatch'));
115
+ const rawProviders = parseListFlagValue(requireFlagValue(args, {}, '--providers'));
116
+ const providersArg = rawProviders?.map((entry) => {
117
+ const canonical = normalizeProviderDefinitionId(entry);
118
+ if (!canonical || canonical === 'generic' || !VALID_PROVIDERS.includes(canonical)) {
119
+ throw new Error(`--providers: unknown provider '${entry}' (valid: ${VALID_PROVIDERS.join(', ')})`);
120
+ }
121
+ return canonical;
122
+ });
123
+ if (providersArg && providersArg.length === 0) {
124
+ throw new Error('--providers requires at least one provider name');
125
+ }
126
+ const defaultScriptsArg = parseBooleanFlagValue(requireFlagValue(args, {}, '--default-scripts'), '--default-scripts');
127
+ const customScriptsArg = parseBooleanFlagValue(requireFlagValue(args, {}, '--custom-scripts'), '--custom-scripts');
128
+ const scriptPairs = {};
129
+ for (const entry of collectFlagValues(args, {}, '--script')) {
130
+ const [name, command] = parseKeyValueEntry(entry, '--script');
131
+ scriptPairs[name] = command;
132
+ }
133
+ return { workerDispatchArg, providersArg, defaultScriptsArg, customScriptsArg, scriptPairs };
134
+ }
135
+ /** One-line description of what an explicit `--worker-dispatch` choice would write. */
136
+ function describeWorkerDispatchWrite(arg) {
137
+ return arg === 'enable'
138
+ ? 'enable parallelism.worker_dispatch'
139
+ : 'record a worker-dispatch decline (setup.worker_dispatch_offer)';
140
+ }
141
+ /**
142
+ * Resolve an explicit `--worker-dispatch` choice into a decision to apply to
143
+ * a brand-new config (#3062). Under `--dry-run`, writes nothing — reports
144
+ * what would be written instead and returns `undefined` so the caller never
145
+ * applies it to the config it builds.
146
+ */
147
+ function resolveWorkerDispatchDecisionForNewConfig(arg, dryRun) {
148
+ if (dryRun) {
149
+ ui.dim(` Would ${describeWorkerDispatchWrite(arg)} (--dry-run writes nothing).`);
150
+ return undefined;
151
+ }
152
+ return { kind: arg };
153
+ }
154
+ /**
155
+ * Interactive-only: asks the one consent question (default/unanswered is
156
+ * no, strict `y`/`yes` parsing only) when two or more worker CLIs are
157
+ * detected and dispatch is not already on or declined. Returns the
158
+ * decision to apply, or `undefined` when there is nothing to offer, or
159
+ * when the prompt timed out / stdin closed without an answer — only an
160
+ * actually-answered "no" is recorded as a persistent decline (#3050
161
+ * review item 4).
162
+ */
163
+ async function askWorkerDispatchOffer(rl, parallelism, setup, signal) {
164
+ const offer = evaluateWorkerDispatchOffer(parallelism, setup, detectInstalledWorkerClis());
165
+ if (!offer.shouldOffer)
166
+ return undefined;
167
+ console.log('');
168
+ const outcome = await askConsent(rl, workerDispatchOfferQuestion(offer.detected), undefined, signal);
169
+ if (outcome.kind !== 'answered')
170
+ return undefined;
171
+ if (outcome.value) {
172
+ ui.success(' Will enable parallelism.worker_dispatch.');
173
+ return { kind: 'enable' };
174
+ }
175
+ ui.dim(' Noted — enable later with `aiwg config set --project parallelism.worker_dispatch true`.');
176
+ return { kind: 'decline' };
177
+ }
79
178
  export const initHandler = {
80
179
  id: 'init',
81
180
  name: 'Init',
@@ -84,16 +183,34 @@ export const initHandler = {
84
183
  aliases: [],
85
184
  async execute(ctx) {
86
185
  const force = ctx.args.includes('--force');
87
- // Treat a missing stdin TTY as non-interactive so piped/backgrounded
88
- // invocations never attempt to create a readline interface. This is a
89
- // single authoritative check — downstream code MUST NOT re-derive
186
+ // `--non-interactive`/`--yes` are this command's own explicit flags for
187
+ // the one shared interactive decision (#3062 review item 12) — honored
188
+ // ahead of (and regardless of) the environment-only checks `isInteractive`
189
+ // otherwise applies (TTY, TERM, CI). Downstream code MUST NOT re-derive
90
190
  // interactive-ness from process.stdin.isTTY again.
91
- const nonInteractive = ctx.args.includes('--non-interactive') ||
92
- ctx.args.includes('--yes') ||
93
- !process.stdin.isTTY ||
94
- !process.stdout.isTTY ||
95
- process.env['CI'] === 'true';
191
+ const explicitNonInteractive = ctx.args.includes('--non-interactive') || ctx.args.includes('--yes');
192
+ const nonInteractive = !isInteractive(explicitNonInteractive);
96
193
  const projectDir = ctx.cwd;
194
+ // `--worker-dispatch enable|decline` answers the worker-dispatch offer
195
+ // directly (#3062), with or without a terminal, and is honored even when
196
+ // fewer than two worker CLIs are detected — an explicit choice overrides
197
+ // the presence-only detection gate. `--providers`/`--default-scripts`/
198
+ // `--script` answer the remaining wizard inputs the same way (review
199
+ // item 10). Every flag is parsed strictly (review item 11): present
200
+ // without a usable value is a usage error, raised here before any
201
+ // preflight check, read, or write.
202
+ let workerDispatchArg;
203
+ let providersArg;
204
+ let defaultScriptsArg;
205
+ let customScriptsArg;
206
+ let scriptPairs;
207
+ try {
208
+ ({ workerDispatchArg, providersArg, defaultScriptsArg, customScriptsArg, scriptPairs } = parseInitArgInputs(ctx.args));
209
+ }
210
+ catch (err) {
211
+ const message = err instanceof MissingFlagValueError ? `${err.message}.` : err instanceof Error ? err.message : String(err);
212
+ return { exitCode: EXIT_CODES.USAGE, message };
213
+ }
97
214
  ui.blank();
98
215
  console.log(` ${ui.brandMark()} ${ui.bold('AIWG Baseline Config')}`);
99
216
  ui.rule();
@@ -101,7 +218,10 @@ export const initHandler = {
101
218
  const existing = await readAiwgConfig(projectDir);
102
219
  if (existing && !force) {
103
220
  ui.info(`Config already exists: ${getConfigPath(projectDir)}`);
104
- await writeNormalizedAiwgMd(projectDir);
221
+ // #3062 review item 18: `--dry-run` must write nothing at all, not
222
+ // just skip the worker-dispatch increment below.
223
+ if (!ctx.dryRun)
224
+ await writeNormalizedAiwgMd(projectDir);
105
225
  console.log(` Providers: ${existing.providers.join(', ')}`);
106
226
  const scriptCount = Object.keys(existing.scripts).length;
107
227
  if (scriptCount > 0) {
@@ -111,22 +231,71 @@ export const initHandler = {
111
231
  ui.dim(' Ask your AIWG agent to review repo, tracker, delivery, signing, and provider policy.');
112
232
  ui.dim(' Agents may use `aiwg setup project --dry-run` as the CLI helper for that review.');
113
233
  ui.dim(' Run `aiwg init --force` only when you need to recreate the baseline config.');
234
+ // #3050: guide users to direct worker dispatch from the setup step they
235
+ // already run, instead of leaving it undiscoverable. `--dry-run` must
236
+ // never prompt and never write (review item 1) — it degrades to the
237
+ // same hint-only behavior as non-interactive runs.
238
+ // #3062: an explicit `--worker-dispatch` argument always answers the
239
+ // question directly (bypassing the ask/hint entirely) except under
240
+ // `--dry-run`, which still writes nothing — it reports what would be
241
+ // written instead.
242
+ if (workerDispatchArg) {
243
+ if (ctx.dryRun) {
244
+ ui.dim(` Would ${describeWorkerDispatchWrite(workerDispatchArg)} (--dry-run writes nothing).`);
245
+ }
246
+ else if (workerDispatchArg === 'enable') {
247
+ await writeAiwgConfig(projectDir, { ...existing, parallelism: { ...existing.parallelism, worker_dispatch: true } });
248
+ ui.success(' Enabled parallelism.worker_dispatch.');
249
+ }
250
+ else {
251
+ await writeAiwgConfig(projectDir, { ...existing, setup: { ...existing.setup, worker_dispatch_offer: 'declined' } });
252
+ ui.success(' Recorded worker-dispatch decline (setup.worker_dispatch_offer).');
253
+ }
254
+ }
255
+ else if (nonInteractive || ctx.dryRun) {
256
+ renderWorkerDispatchHintIfApplicable(existing.parallelism, existing.setup);
257
+ }
258
+ else {
259
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
260
+ try {
261
+ const decision = await askWorkerDispatchOffer(rl, existing.parallelism, existing.setup, ctx.signal);
262
+ if (decision?.kind === 'enable') {
263
+ await writeAiwgConfig(projectDir, { ...existing, parallelism: { ...existing.parallelism, worker_dispatch: true } });
264
+ }
265
+ else if (decision?.kind === 'decline') {
266
+ await writeAiwgConfig(projectDir, { ...existing, setup: { ...existing.setup, worker_dispatch_offer: 'declined' } });
267
+ }
268
+ }
269
+ finally {
270
+ rl.close();
271
+ }
272
+ }
114
273
  ui.blank();
115
274
  return { exitCode: 0 };
116
275
  }
117
276
  let providers;
118
277
  let scripts = {};
278
+ let workerDispatchDecision;
279
+ const DEFAULT_SCRIPTS = { deploy: 'aiwg use all', doctor: 'aiwg doctor', sync: 'aiwg sync' };
119
280
  if (nonInteractive) {
120
- // Non-interactive: use defaults
121
- providers = ['claude'];
122
- scripts = {
123
- deploy: 'aiwg use all',
124
- doctor: 'aiwg doctor',
125
- sync: 'aiwg sync',
126
- };
281
+ // Non-interactive: an explicit `--providers`/`--default-scripts`
282
+ // answers the corresponding wizard input directly (review item 10);
283
+ // anything not supplied keeps its prior default.
284
+ providers = providersArg ?? ['claude'];
285
+ const includeDefaultScripts = defaultScriptsArg ?? true;
286
+ scripts = { ...(includeDefaultScripts ? DEFAULT_SCRIPTS : {}), ...scriptPairs };
287
+ if (workerDispatchArg) {
288
+ workerDispatchDecision = resolveWorkerDispatchDecisionForNewConfig(workerDispatchArg, ctx.dryRun);
289
+ }
290
+ else {
291
+ renderWorkerDispatchHintIfApplicable(undefined, undefined);
292
+ }
127
293
  }
128
294
  else {
129
- // Interactive wizard
295
+ // Interactive wizard — skip a prompt whenever its argument already
296
+ // answered it (review item 10/12: "with a terminal we can skip steps
297
+ // for any params provided and only provide interaction when we are
298
+ // missing a value").
130
299
  const rl = readline.createInterface({
131
300
  input: process.stdin,
132
301
  output: process.stdout,
@@ -134,16 +303,29 @@ export const initHandler = {
134
303
  try {
135
304
  // Thread ctx.signal through every prompt so Ctrl-C during the wizard
136
305
  // cancels cleanly instead of being absorbed by readline (POC #926).
137
- providers = await askProviders(rl, ctx.signal);
306
+ providers = providersArg ?? await askProviders(rl, ctx.signal);
138
307
  console.log('');
139
- const addScripts = await askYesNo(rl, ' Add default scripts (deploy, doctor, sync)? [Y/n]: ', true, ctx.signal);
140
- if (addScripts) {
141
- scripts = {
142
- deploy: 'aiwg use all',
143
- doctor: 'aiwg doctor',
144
- sync: 'aiwg sync',
145
- };
146
- const addCustom = await askYesNo(rl, ' Add a custom script? [y/N]: ', false, ctx.signal);
308
+ const includeDefaultScripts = defaultScriptsArg !== undefined
309
+ ? defaultScriptsArg
310
+ : await askYesNo(rl, ' Add default scripts (deploy, doctor, sync)? [Y/n]: ', true, ctx.signal);
311
+ scripts = includeDefaultScripts ? { ...DEFAULT_SCRIPTS } : {};
312
+ if (Object.keys(scriptPairs).length > 0) {
313
+ // `--script` already answered "add a custom script?" (implicit
314
+ // yes) and its name/command sub-prompts — never ask any of the
315
+ // three (#3062 second review, finding 5a).
316
+ scripts = { ...scripts, ...scriptPairs };
317
+ }
318
+ else {
319
+ // `--custom-scripts true|false` answers "add a custom script?"
320
+ // directly when `--script` itself was not given — an explicit
321
+ // decline (`false`) skips the question entirely, same as every
322
+ // other already-supplied input; `true` still asks for the
323
+ // name/command, since those were not supplied either.
324
+ const addCustom = customScriptsArg !== undefined
325
+ ? customScriptsArg
326
+ : includeDefaultScripts
327
+ ? await askYesNo(rl, ' Add a custom script? [y/N]: ', false, ctx.signal)
328
+ : false;
147
329
  if (addCustom) {
148
330
  const name = await askString(rl, ' Script name: ', '', ctx.signal);
149
331
  const cmd = await askString(rl, ' Command: ', '', ctx.signal);
@@ -152,6 +334,21 @@ export const initHandler = {
152
334
  }
153
335
  }
154
336
  }
337
+ // #3050: offer direct worker dispatch inside the same wizard — but
338
+ // `--dry-run` must never prompt or write anything from the offer
339
+ // (review item 1), so it degrades to the hint instead. An explicit
340
+ // `--worker-dispatch` argument (#3062) always answers the question
341
+ // directly instead of asking or hinting, still subject to `--dry-run`
342
+ // writing nothing.
343
+ if (workerDispatchArg) {
344
+ workerDispatchDecision = resolveWorkerDispatchDecisionForNewConfig(workerDispatchArg, ctx.dryRun);
345
+ }
346
+ else if (ctx.dryRun) {
347
+ renderWorkerDispatchHintIfApplicable(undefined, undefined);
348
+ }
349
+ else {
350
+ workerDispatchDecision = await askWorkerDispatchOffer(rl, undefined, undefined, ctx.signal);
351
+ }
155
352
  }
156
353
  finally {
157
354
  rl.close();
@@ -160,6 +357,24 @@ export const initHandler = {
160
357
  // Build config
161
358
  let config = emptyConfig(providers);
162
359
  config.scripts = scripts;
360
+ if (workerDispatchDecision?.kind === 'enable') {
361
+ config.parallelism = { ...config.parallelism, worker_dispatch: true };
362
+ }
363
+ else if (workerDispatchDecision?.kind === 'decline') {
364
+ config.setup = { ...config.setup, worker_dispatch_offer: 'declined' };
365
+ }
366
+ // #3062 review item 18: `--dry-run` writes nothing at all — not even
367
+ // the baseline config — rather than only skipping the worker-dispatch
368
+ // increment. Report what would be written instead.
369
+ if (ctx.dryRun) {
370
+ ui.blank();
371
+ ui.dim(` Would create ${getConfigPath(projectDir)} (--dry-run writes nothing).`);
372
+ ui.dim(` Providers: ${providers.join(', ')}`);
373
+ if (Object.keys(scripts).length > 0)
374
+ ui.dim(` Scripts: ${Object.keys(scripts).join(', ')}`);
375
+ ui.blank();
376
+ return { exitCode: 0 };
377
+ }
163
378
  // Populate deployedTo from disk for entries with empty deployedTo (#721)
164
379
  config = await populateDeployedTo(config, projectDir);
165
380
  // Write
@@ -15,6 +15,7 @@ import * as ui from '../ui.js';
15
15
  import { promises as fs } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { createHash, randomBytes } from 'node:crypto';
18
+ import { assertMissionWorkerHandoff } from '../../mission-protocol/codecs.js';
18
19
  // ── Constants ────────────────────────────────────────────────
19
20
  const MC_ROOT = '.aiwg/ralph-external/mc';
20
21
  const SESSIONS_DIR = join(MC_ROOT, 'sessions');
@@ -378,6 +379,18 @@ async function mcDispatch(ctx) {
378
379
  ui.error(sessionId ? `Session not found: ${sessionId}` : 'No active session. Run `aiwg mc start` first.');
379
380
  return { exitCode: 1 };
380
381
  }
382
+ // mission-dispatch accepts only the frozen objective envelope. Worker
383
+ // bindings can arrive through its existing session, so inspect that payload.
384
+ try {
385
+ assertMissionWorkerHandoff(selected, 'mc-cli');
386
+ for (const mission of selected.missions)
387
+ assertMissionWorkerHandoff(mission, 'mc-cli');
388
+ }
389
+ catch (error) {
390
+ const message = error instanceof Error ? error.message : String(error);
391
+ ui.error(message);
392
+ return { exitCode: 1, message };
393
+ }
381
394
  // #1361: Check project-level parallelism cap. Active missions = running or
382
395
  // queued; if at cap, warn but still queue (FIFO behavior — the mission goes
383
396
  // into the session as 'queued' and will run when a slot frees up).
@@ -560,6 +573,16 @@ async function mcRun(ctx) {
560
573
  return { exitCode: 1 };
561
574
  }
562
575
  const queued = session.missions.filter((m) => m.status === 'queued');
576
+ try {
577
+ assertMissionWorkerHandoff(session, 'mc-cli');
578
+ for (const mission of queued)
579
+ assertMissionWorkerHandoff(mission, 'mc-cli');
580
+ }
581
+ catch (error) {
582
+ const message = error instanceof Error ? error.message : String(error);
583
+ ui.error(message);
584
+ return { exitCode: 1, message };
585
+ }
563
586
  if (queued.length === 0) {
564
587
  ui.info(`No queued missions in session ${session.id}. Run \`aiwg mc dispatch ${session.id} "<objective>"\` to add one.`);
565
588
  return { exitCode: 0 };
@@ -7,13 +7,63 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { glob } from 'glob';
9
9
  import { load as loadYaml } from 'js-yaml';
10
- import { compileModelPolicy, validateCanonicalModelPolicy, validateUserProjectModelConfig, } from '../../models/provider-policy.js';
10
+ import { compileModelPolicy, validateCanonicalModelPolicy, validateUserProjectModelConfig, loadProviderModelCapabilities, WorkerPolicyError, } from '../../models/provider-policy.js';
11
11
  import { resolveGrokBuildRoleModel } from '../../models/grok-build-models.js';
12
+ import { readAiwgConfig, emptyConfig } from '../../config/aiwg-config.js';
13
+ import { ConfigLoader } from '../../models/config-loader.js';
14
+ import { loadRecordedWorkerInstallations } from '../../models/worker-installation.mjs';
15
+ import { parseWorkerCommandArgs, resolveWorkerInvocation, workerInvocationDiagnostic } from '../../serve/worker-invocation.js';
16
+ /**
17
+ * #2892: exported so the MCP worker-invoke bridge can resolve a direct worker
18
+ * plan in-process (same snapshot/driver construction as `aiwg models resolve`),
19
+ * without shelling out to the CLI. No new loader is introduced; this is the
20
+ * CLI's own existing default `ModelsWorkerServices.load`.
21
+ */
22
+ export async function loadWorkerInputs(ctx) {
23
+ const config = await readAiwgConfig(ctx.cwd) ?? emptyConfig();
24
+ const loader = new ConfigLoader(ctx.cwd);
25
+ const models = await loader.load(config.parallelism);
26
+ const catalog = await effectiveCatalog(ctx, ctx.cwd, false, parseArgs(['resolve']));
27
+ const registry = loadProviderModelCapabilities();
28
+ // Let the resolver explain the dispatch gate before requiring host qualification (#3070).
29
+ if (!config.parallelism?.worker_dispatch) {
30
+ return { snapshot: { config, models, modelDigests: loader.getInputDigests(), catalog,
31
+ registry, installations: {} }, drivers: {} };
32
+ }
33
+ const { registry: qualifiedRegistry, installations, drivers } = await loadRecordedWorkerInstallations({
34
+ registry, cwd: ctx.cwd, policy: models.defaults.workerSelection,
35
+ });
36
+ return { snapshot: { config, models, modelDigests: loader.getInputDigests(), catalog,
37
+ registry: qualifiedRegistry, installations }, drivers };
38
+ }
39
+ async function resolveDirect(ctx, services) {
40
+ let shell = false;
41
+ try {
42
+ const request = parseWorkerCommandArgs(ctx.args.slice(1));
43
+ shell = request.format === 'shell';
44
+ const { snapshot, drivers } = await services.load(ctx);
45
+ const plan = await resolveWorkerInvocation(snapshot, request, drivers, services.now?.());
46
+ const runnable = plan.invocation?.readiness === 'launchable';
47
+ if (shell && !runnable)
48
+ console.error(JSON.stringify({ status: 'blocked', diagnostics: plan.diagnostics }, null, 2));
49
+ else if (shell && plan.invocation?.transport === 'shell')
50
+ print(plan.invocation.shellCommand, false);
51
+ else
52
+ print(plan, true);
53
+ return { exitCode: runnable ? 0 : 2 };
54
+ }
55
+ catch (error) {
56
+ const diagnostic = { status: 'blocked', diagnostics: [workerInvocationDiagnostic(error, error instanceof WorkerPolicyError
57
+ && error.code === 'OG_ADAPTER_UNQUALIFIED' ? '/parallelism/clis' : '/request')] };
58
+ if (shell)
59
+ console.error(JSON.stringify(diagnostic, null, 2));
60
+ else
61
+ print(diagnostic, true);
62
+ return { exitCode: 2 };
63
+ }
64
+ }
12
65
  const TIERS = new Set(['economy', 'standard', 'premium', 'max-quality']);
13
- const PROVIDERS = new Set([
14
- 'antigravity', 'claude', 'codex', 'copilot', 'cursor', 'deepseek-harness', 'factory', 'grok-build', 'grokbot', 'hermes',
15
- 'opencode', 'openclaw', 'openhuman', 'omp', 'pi', 'warp', 'windsurf',
16
- ]);
66
+ const PROVIDERS = new Set(Object.keys(loadProviderModelCapabilities().providers));
17
67
  function parseArgs(args) {
18
68
  const [subcommand = 'help', ...rest] = args;
19
69
  const flags = new Map();
@@ -222,8 +272,18 @@ function replaceFrontmatter(raw, item, tier) {
222
272
  }
223
273
  return `---\n${frontmatter}\n---\n${match[2]}`;
224
274
  }
225
- async function execute(ctx) {
275
+ async function execute(ctx, services = { load: loadWorkerInputs }) {
276
+ // Preserve artifact/native resolve when direct-only flags are absent.
277
+ if (ctx.args[0] === 'resolve' && ctx.args.slice(1).some(arg => /^--(?:task|group|brief|workdir|final-output|transport|format)(?:=|$)/.test(arg))) {
278
+ return resolveDirect(ctx, services);
279
+ }
226
280
  const parsed = parseArgs(ctx.args);
281
+ if (parsed.subcommand === 'resolve') {
282
+ const supported = new Set(['agent', 'skill', 'glob', 'role', 'tier', 'framework', 'all', 'target', 'provider', 'model', 'json', 'help', 'dry-run']);
283
+ for (const key of parsed.flags.keys())
284
+ if (!supported.has(key))
285
+ return { exitCode: 2, message: `Unsupported models resolve flag: --${key}` };
286
+ }
227
287
  const json = parsed.flags.has('json');
228
288
  const dryRun = parsed.flags.has('dry-run') || ctx.dryRun === true;
229
289
  const target = path.resolve(flagString(parsed, 'target') ?? ctx.cwd);
@@ -380,4 +440,7 @@ export const modelsHandler = {
380
440
  aliases: [],
381
441
  execute,
382
442
  };
443
+ export function createModelsHandler(services) {
444
+ return { ...modelsHandler, execute: ctx => execute(ctx, services) };
445
+ }
383
446
  //# sourceMappingURL=models.js.map
@@ -336,6 +336,9 @@ export class RalphStatusHandler {
336
336
  category = 'ralph';
337
337
  aliases = ['agent-loop-status', 'ralph-status'];
338
338
  async execute(ctx) {
339
+ if (ctx.args.includes('--workers')) {
340
+ return createScriptRunner(ctx.frameworkRoot).run('tools/ralph-external/index.mjs', ['--status', ...ctx.args]);
341
+ }
339
342
  const showAll = ctx.args.includes('--all') || ctx.args.includes('-a');
340
343
  const statuses = getLoopStatuses(process.cwd());
341
344
  if (statuses.length === 0) {
@@ -24,6 +24,7 @@ import { refreshAllPackages } from '../../packages/registry.js';
24
24
  import { resolveActiveProvider } from '../provider-resolution.js';
25
25
  import { readAiwgConfig, writeAiwgConfig, hashManifest, getProviderParallelismDefaults, } from '../../config/aiwg-config.js';
26
26
  import { discoverProjectLocalBundles } from '../../extensions/project-local-discovery.js';
27
+ import { detectInstalledWorkerClis, evaluateWorkerDispatchOffer, workerDispatchOfferHint } from '../../models/worker-dispatch-offer.js';
27
28
  import { getProviderArtifactPathStrings } from '../../providers/provider-definitions.js';
28
29
  import { collectPackagedAgentInventory, normalizeAgentArtifactName, parseManagedArtifactMarker, } from '../../agents/packaged-agent-inventory.js';
29
30
  import * as ui from '../ui.js';
@@ -786,6 +787,21 @@ export const refreshHandler = {
786
787
  // Non-fatal — refresh continues even if migration fails
787
788
  }
788
789
  }
790
+ // Step 4.65: Direct worker dispatch opt-in hint (#3050). Refresh never
791
+ // prompts; this only names the enable command when dispatch is off and
792
+ // two or more worker CLIs are detected (presence-only PATH scan, no
793
+ // spawn). Silent otherwise, including after a previous decline.
794
+ if (!quiet) {
795
+ try {
796
+ const config = await readAiwgConfig(process.cwd());
797
+ const offer = evaluateWorkerDispatchOffer(config?.parallelism, config?.setup, detectInstalledWorkerClis());
798
+ if (offer.shouldOffer)
799
+ ui.info(workerDispatchOfferHint(offer.detected));
800
+ }
801
+ catch {
802
+ // Non-fatal — refresh continues even if the hint check fails
803
+ }
804
+ }
789
805
  // Step 5: Health check
790
806
  if (!quiet)
791
807
  ui.info(dryRun ? 'Would run health check...' : 'Running health check...');
@@ -150,6 +150,20 @@ async function handleRegenerate(args, cwd) {
150
150
  console.log(` Selected: ${selection.explicit ? 'explicit' : 'inferred'} — ${selection.reason}`);
151
151
  if (selection.evidence.length > 0)
152
152
  console.log(` Evidence: ${selection.evidence.join(', ')}`);
153
+ // #3050: regenerate never prompts. Point at direct worker dispatch when
154
+ // it is off and two or more worker CLIs are detected (presence-only PATH
155
+ // scan, no spawn); silent otherwise, including after a decline.
156
+ try {
157
+ const { readAiwgConfig } = await import('../../config/aiwg-config.js');
158
+ const { detectInstalledWorkerClis, evaluateWorkerDispatchOffer, workerDispatchOfferHint } = await import('../../models/worker-dispatch-offer.js');
159
+ const dispatchConfig = await readAiwgConfig(target);
160
+ const offer = evaluateWorkerDispatchOffer(dispatchConfig?.parallelism, dispatchConfig?.setup, detectInstalledWorkerClis());
161
+ if (offer.shouldOffer)
162
+ console.log(` ${workerDispatchOfferHint(offer.detected)}`);
163
+ }
164
+ catch {
165
+ // Non-fatal — this is an informational nudge, never a regenerate gate.
166
+ }
153
167
  if (existingProject) {
154
168
  const preflight = await migrateWorkspaceContext(target, {
155
169
  dryRun: true,
@@ -12,6 +12,7 @@ import path from 'path';
12
12
  import { AiwgError, EXIT_CODES, handlerResultFromError } from '../errors.js';
13
13
  import { getPackageRoot } from '../../channel/manager.mjs';
14
14
  import { inspectInstallation } from '../../installation/manager.mjs';
15
+ import { isSessionProviderResolution, resolveActiveProvider } from '../provider-resolution.js';
15
16
  function isMissingRuntimeCatalogError(error) {
16
17
  return error instanceof Error && error.message.includes('No catalog found');
17
18
  }
@@ -304,21 +305,35 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
304
305
  console.log(` Run mode: ${installation.identity?.runMode ?? '(unrecorded)'}`);
305
306
  console.log(` State: ${installation.state}`);
306
307
  // Scheduler backend detection
307
- const { execSync } = await import('child_process');
308
+ const { boundedExecFileSync } = await import('../../process/bounded-child-process.mjs');
308
309
  let schedulerBackend = 'external trigger required (system cron/systemd/CI)';
309
310
  let chronyInstalled = false;
310
- try {
311
- execSync('which chronyc 2>/dev/null || which chronyd 2>/dev/null', { stdio: 'pipe' });
312
- chronyInstalled = true;
313
- }
314
- catch {
315
- // chrony not found
311
+ for (const tool of ['chronyc', 'chronyd']) {
312
+ try {
313
+ boundedExecFileSync('which', [tool], 'quick', { stdio: ['pipe', 'pipe', 'ignore'] });
314
+ chronyInstalled = true;
315
+ break;
316
+ }
317
+ catch {
318
+ // Try the next chrony binary.
319
+ }
316
320
  }
317
321
  // Native CronCreate is only available when running inside a Claude Code agent context.
318
322
  // The CLI itself cannot call agent tools, so we report based on platform heuristics.
319
323
  // When invoked from within an agent, the schedule skill performs live detection.
320
- const isClaudeCodeContext = process.env.CLAUDE_CODE_VERSION !== undefined ||
321
- process.env.ANTHROPIC_API_KEY !== undefined;
324
+ //
325
+ // Resolved through the same shared provider-resolution path `steward
326
+ // capabilities` uses (`resolveActiveProvider` -> the `claude` provider's
327
+ // `detection.env` markers in provider-definitions.ts) so the two never
328
+ // disagree about which provider the current session is running under
329
+ // (#2623, #2627). The ad-hoc `CLAUDE_CODE_VERSION`/`ANTHROPIC_API_KEY`
330
+ // check this replaced was stale (neither is set by current Claude Code)
331
+ // and `ANTHROPIC_API_KEY` was a false positive for any non-Claude
332
+ // session with that key set.
333
+ const activeProviderResolution = await resolveActiveProvider({ cwd });
334
+ // Only a session signal counts: a project merely configured or deployed
335
+ // for Claude is not a Claude Code agent context.
336
+ const isClaudeCodeContext = activeProviderResolution.provider === 'claude' && isSessionProviderResolution(activeProviderResolution);
322
337
  if (isClaudeCodeContext) {
323
338
  schedulerBackend = 'native-cron (CronCreate); external trigger outside agent sessions';
324
339
  }