@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
@@ -127,6 +127,78 @@ export async function askChoice(rl, prompt, options, fallback, signal) {
127
127
  return pick;
128
128
  }, pick, label, promptTimeoutMs(), signal);
129
129
  }
130
+ /**
131
+ * Prompt for a strict, distinguishable consent answer. Two differences from
132
+ * {@link askYesNo}, both deliberately scoped to this helper so no existing
133
+ * `askYesNo` call site changes behavior:
134
+ *
135
+ * 1. **Strict parsing**: only a trimmed, case-insensitive `y` or `yes`
136
+ * counts as yes. `askYesNo`'s `startsWith('y')` would also accept
137
+ * `yesterday` or `yolo` — too loose for an irreversible opt-in.
138
+ * 2. **Answered vs. not-answered**: the caller gets a {@link ConsentOutcome}
139
+ * instead of a bare boolean, so a timeout or a closed/EOF'd stdin (the
140
+ * `rl.on('close', ...)` listener settles immediately — it does not wait
141
+ * out the timeout) is distinguishable from an actual "no" answer. A
142
+ * caller that records a persistent decline must only do so for
143
+ * `{ kind: 'answered', value: false }`.
144
+ *
145
+ * Signal abort still rejects (same contract as {@link askWithTimeout}).
146
+ */
147
+ export function askConsent(rl, question, timeoutMs = promptTimeoutMs(), signal) {
148
+ return new Promise((resolve, reject) => {
149
+ let settled = false;
150
+ const cleanup = () => {
151
+ clearTimeout(timer);
152
+ rl.removeListener('close', onClose);
153
+ signal?.removeEventListener('abort', onAbort);
154
+ };
155
+ const timer = setTimeout(() => {
156
+ if (settled)
157
+ return;
158
+ settled = true;
159
+ ui.warn(` No input within ${Math.round(timeoutMs / 1000)}s — leaving the setting unchanged.`);
160
+ cleanup();
161
+ resolve({ kind: 'timeout' });
162
+ }, timeoutMs);
163
+ // See askWithTimeout: unref so a closed event loop never waits on this.
164
+ timer.unref?.();
165
+ // Stdin closing (EOF, piped input exhausted, or the interface being
166
+ // closed elsewhere) must settle immediately, not after the full
167
+ // timeout — a closed stdin must never hang a consent prompt.
168
+ const onClose = () => {
169
+ if (settled)
170
+ return;
171
+ settled = true;
172
+ ui.warn(' Input closed before an answer — leaving the setting unchanged.');
173
+ cleanup();
174
+ resolve({ kind: 'closed' });
175
+ };
176
+ rl.on('close', onClose);
177
+ const onAbort = () => {
178
+ if (settled)
179
+ return;
180
+ settled = true;
181
+ cleanup();
182
+ try {
183
+ rl.close();
184
+ }
185
+ catch { /* already closed */ }
186
+ reject(signal.reason ?? new Error('Aborted'));
187
+ };
188
+ if (signal?.aborted) {
189
+ onAbort();
190
+ return;
191
+ }
192
+ signal?.addEventListener('abort', onAbort, { once: true });
193
+ rl.question(question, (answer) => {
194
+ if (settled)
195
+ return;
196
+ settled = true;
197
+ cleanup();
198
+ resolve({ kind: 'answered', value: ['y', 'yes'].includes(answer.trim().toLowerCase()) });
199
+ });
200
+ });
201
+ }
130
202
  /**
131
203
  * Prompt for a labeled selection from a list of options. Renders each option
132
204
  * as a numbered line before asking. Users can respond by number (1-based) or
@@ -19,6 +19,17 @@ function uniqueProviders(providers) {
19
19
  }
20
20
  return result;
21
21
  }
22
+ /**
23
+ * Sources that say which harness this process is actually running under.
24
+ * `config`, `deployment` and `default` describe the project, not the session:
25
+ * a project deployed for one provider is still not "inside" that provider
26
+ * when a command is run from a plain terminal.
27
+ */
28
+ const SESSION_PROVIDER_SOURCES = new Set(['explicit', 'env', 'runtime-env', 'process']);
29
+ /** True when a resolution came from the running session rather than project state. */
30
+ export function isSessionProviderResolution(resolution) {
31
+ return resolution.provider != null && SESSION_PROVIDER_SOURCES.has(resolution.source);
32
+ }
22
33
  function detectProviderFromRuntimeEnv(env) {
23
34
  const definitions = [...listProviderDefinitions()].sort((a, b) => (a.detection.runtimeEnvPriority ?? Number.MAX_SAFE_INTEGER) - (b.detection.runtimeEnvPriority ?? Number.MAX_SAFE_INTEGER));
24
35
  for (const definition of definitions) {
@@ -8,6 +8,13 @@
8
8
  *
9
9
  * @implements #621
10
10
  */
11
+ import { resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
12
+ export { DEFAULT_DISPATCH_HOST_THRESHOLDS, resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
13
+ import Ajv from 'ajv';
14
+ import addFormats from 'ajv-formats';
15
+ import { readFileSync, existsSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { isCanonicalWorkerModelKey, loadProviderModelCapabilities, resolveWorkerInstallation } from '../models/provider-policy.js';
11
18
  import { readFile, writeFile, mkdir, access, readdir, rename, unlink } from 'fs/promises';
12
19
  import { createHash, randomBytes } from 'crypto';
13
20
  import { resolve, join, dirname, isAbsolute } from 'path';
@@ -162,6 +169,166 @@ export function resolveDelivery(delivery) {
162
169
  issue_comment_on_cycle: delivery?.issue_comment_on_cycle ?? true,
163
170
  };
164
171
  }
172
+ let parallelismValidator;
173
+ /** Offline shape and declaration validation. Qualification is checked at use time. */
174
+ export function validateParallelismConfig(value) {
175
+ if (value === undefined)
176
+ return [];
177
+ if (!parallelismValidator) {
178
+ const here = dirname(fileURLToPath(import.meta.url));
179
+ const candidates = [resolve(here, '../../vscode-extension/schemas/aiwg.config.v1.json'), resolve(here, '../../../vscode-extension/schemas/aiwg.config.v1.json')];
180
+ const file = candidates.find(existsSync);
181
+ if (!file)
182
+ throw new Error('Parallelism schema is unavailable');
183
+ const schema = JSON.parse(readFileSync(file, 'utf8'));
184
+ const ajv = addFormats(new Ajv({ strict: true, allErrors: true }));
185
+ parallelismValidator = ajv.compile({ ...schema.properties.parallelism, definitions: schema.definitions });
186
+ }
187
+ if (!parallelismValidator(value))
188
+ return (parallelismValidator.errors ?? []).map(error => `OG_CONFIG_REFERENCE /parallelism${error.instancePath}: ${error.message}`);
189
+ const policy = value;
190
+ const errors = [];
191
+ const error = (pointer, message) => { errors.push(`OG_CONFIG_REFERENCE /parallelism/${pointer}: ${message}`); };
192
+ const builtinIds = new Set(PROVIDER_IDS.filter(id => id !== 'generic'));
193
+ for (const [id, installation] of Object.entries(policy.clis ?? {})) {
194
+ if (installation.adapter.startsWith('providers:') && !builtinIds.has(installation.adapter.slice(10)))
195
+ error(`clis/${id}/adapter`, 'Unknown provider binding');
196
+ }
197
+ for (const [id, group] of Object.entries(policy.groups ?? {})) {
198
+ const ids = new Set();
199
+ for (const member of group.members) {
200
+ if (ids.has(member.id))
201
+ error(`groups/${id}/members`, 'Duplicate member ID');
202
+ ids.add(member.id);
203
+ if (!policy.clis?.[member.cli] && !builtinIds.has(member.cli))
204
+ error(`groups/${id}/members/${member.id}/cli`, 'Unknown installation');
205
+ }
206
+ for (const member of group.fallback ?? [])
207
+ if (!ids.has(member))
208
+ error(`groups/${id}/fallback`, 'Unknown fallback member');
209
+ for (const role of group.requiredRoles ?? []) {
210
+ if (!group.members.some(m => m.enabled !== false && policy.clis?.[m.cli]?.enabled !== false && m.roles.includes(role)))
211
+ error(`groups/${id}/requiredRoles`, `No enabled member covers ${role}`);
212
+ }
213
+ }
214
+ const admission = policy.admission;
215
+ for (const group of Object.keys(admission?.groupCaps ?? {}))
216
+ if (!policy.groups?.[group])
217
+ error('admission/groupCaps', 'Unknown group');
218
+ for (const cli of Object.keys(admission?.cliCaps ?? {}))
219
+ if (!policy.clis?.[cli] && !builtinIds.has(cli))
220
+ error('admission/cliCaps', 'Unknown installation');
221
+ for (const key of Object.keys(admission?.memberCaps ?? {})) {
222
+ const [group, member, extra] = key.split('/');
223
+ if (extra !== undefined || !policy.groups?.[group]?.members.some(m => m.id === member))
224
+ error('admission/memberCaps', 'Unknown group/member');
225
+ }
226
+ for (const key of Object.keys(admission?.modelCaps ?? {}))
227
+ if (!isCanonicalWorkerModelKey(key))
228
+ error('admission/modelCaps', 'Noncanonical inference model key');
229
+ if (admission?.hostResources !== undefined) {
230
+ try {
231
+ resolveDispatchHostThresholds(undefined, admission.hostResources);
232
+ }
233
+ catch (validation) {
234
+ error('admission/hostResources', validation instanceof Error ? validation.message : String(validation));
235
+ }
236
+ }
237
+ errors.push(...validateWorkerHealthConfig(policy.health));
238
+ return errors;
239
+ }
240
+ /** Cross-field health preferences are validated offline; controls are qualified at dispatch. */
241
+ export function validateWorkerHealthConfig(health) {
242
+ if (health === undefined)
243
+ return [];
244
+ const errors = [];
245
+ const error = (field, message) => { errors.push(`OG_CONFIG_REFERENCE /parallelism/health/${field}: ${message}`); };
246
+ const checkSeconds = health.checkSeconds ?? 15;
247
+ if (checkSeconds > (health.startupSeconds ?? 60))
248
+ error('checkSeconds', 'Must not exceed startupSeconds');
249
+ if ((health.quietSeconds ?? 90) >= (health.stallSeconds ?? 300))
250
+ error('quietSeconds', 'Must be less than stallSeconds');
251
+ if ((health.cooldownSeconds ?? 120) > (health.maxCooldownSeconds ?? 900))
252
+ error('cooldownSeconds', 'Must not exceed maxCooldownSeconds');
253
+ for (const role of Object.keys(health.roleScale ?? {})) {
254
+ if (!['efficiency', 'coding', 'reasoning'].includes(role))
255
+ error('roleScale', `Unknown role ${role}`);
256
+ }
257
+ for (const field of ['modelScale', 'modelStartupSeconds']) {
258
+ for (const [key, value] of Object.entries(health[field] ?? {})) {
259
+ if (!isCanonicalWorkerModelKey(key))
260
+ error(field, 'Noncanonical inference model key');
261
+ if (field === 'modelStartupSeconds' && checkSeconds > value)
262
+ error(field, 'Model startup must cover checkSeconds');
263
+ }
264
+ }
265
+ return errors;
266
+ }
267
+ /** Resolve one enabled installation through trusted metadata, without probes.
268
+ * Implicit canonical IDs inherit current-account auth; no account/quota identity
269
+ * is invented. Profile qualification remains the invocation owner's obligation.
270
+ */
271
+ export function resolveWorkerGroupInstallation(parallelism, installationId, registry = loadProviderModelCapabilities()) {
272
+ const errors = validateParallelismConfig(parallelism);
273
+ if (errors.length)
274
+ throw new Error(errors.join('; '));
275
+ if (!parallelism.worker_dispatch)
276
+ throw new Error('OG_CONFIG_REFERENCE /parallelism/worker_dispatch: Worker dispatch is disabled');
277
+ const explicit = parallelism.clis?.[installationId];
278
+ const installation = explicit
279
+ ? structuredClone(explicit)
280
+ : { adapter: `providers:${installationId}`, enabled: true };
281
+ if (!explicit && (!PROVIDER_IDS.includes(installationId) || installationId === 'generic')) {
282
+ throw new Error(`OG_CONFIG_REFERENCE /parallelism/clis/${installationId}: Unknown canonical installation`);
283
+ }
284
+ if (installation.enabled === false)
285
+ throw new Error(`OG_CONFIG_REFERENCE /parallelism/clis/${installationId}: Installation is disabled`);
286
+ try {
287
+ return { installation: { ...installation, enabled: true }, ...resolveWorkerInstallation(registry, installation) };
288
+ }
289
+ catch (error) {
290
+ throw new Error(`OG_ADAPTER_UNQUALIFIED /parallelism/clis/${installationId}: ${error}`);
291
+ }
292
+ }
293
+ /** Validate current model restrictions and group references before any write.
294
+ * Disabled/unbound projects retain their existing config-only edit behavior.
295
+ * This is shared validation, not Tier 1 coherent activation.
296
+ *
297
+ * `options.aiwgHome`, when provided, is passed through to `ConfigLoader`
298
+ * unchanged — it never reads or writes `process.env.AIWG_HOME`. Omitting it
299
+ * keeps `ConfigLoader`'s own default precedence (`AIWG_HOME` env, else the
300
+ * user-home install).
301
+ */
302
+ export async function validateWorkerConfigCandidate(projectDir, config, options) {
303
+ const errors = validateParallelismConfig(config.parallelism);
304
+ if (errors.length)
305
+ throw new Error(errors.join('; '));
306
+ if (!config.parallelism?.worker_dispatch)
307
+ return;
308
+ const { ConfigLoader } = await import('../models/config-loader.js');
309
+ await new ConfigLoader(projectDir, options).load(config.parallelism);
310
+ }
311
+ /** Hash canonical JSON, treating absent optional model files as null. */
312
+ export function workerConfigDigest(value) {
313
+ const canonical = (node) => {
314
+ if (Array.isArray(node))
315
+ return node.map(canonical);
316
+ if (node !== null && typeof node === 'object')
317
+ return Object.fromEntries(Object.keys(node).sort().filter(key => node[key] !== undefined).map(key => [key, canonical(node[key])]));
318
+ return node ?? null;
319
+ };
320
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`;
321
+ }
322
+ export function workerActivationDigests(config, projectModels, userModels, builtinModels) {
323
+ const candidate = structuredClone(config);
324
+ if (candidate.parallelism)
325
+ delete candidate.parallelism.activation;
326
+ return { configDigest: workerConfigDigest(candidate), projectModelsDigest: workerConfigDigest(projectModels), userModelsDigest: workerConfigDigest(userModels), builtinModelsDigest: workerConfigDigest(builtinModels) };
327
+ }
328
+ /** Pure inventory only: this does not activate, lock, synchronize, or admit work. */
329
+ export function workerActivationCopyInventory(projectDir, compatibilityConfig, userModels, builtinModels) {
330
+ return [...new Set([getConfigPath(projectDir), join(projectDir, 'models.json'), compatibilityConfig, userModels, builtinModels].map(file => resolve(file)))];
331
+ }
165
332
  /**
166
333
  * Per-provider parallelism defaults. Conservative numbers for Anthropic-backed
167
334
  * providers reflect Pro/Team-plan rate limits — operators on Enterprise tiers
@@ -235,6 +402,30 @@ export function resolveParallelism(parallelism, primaryProvider) {
235
402
  };
236
403
  if (parallelism?.rationale !== undefined)
237
404
  resolved.rationale = parallelism.rationale;
405
+ const errors = validateParallelismConfig(parallelism);
406
+ if (errors.length)
407
+ throw new Error(errors.join('; '));
408
+ if (parallelism?.worker_dispatch) {
409
+ Object.assign(resolved, Object.fromEntries(Object.entries(structuredClone(parallelism)).filter(([, value]) => value !== undefined)));
410
+ resolved.worker_mode ??= 'direct';
411
+ for (const group of Object.values(resolved.groups ?? {})) {
412
+ group.fallback ??= [];
413
+ group.requiredRoles ??= [];
414
+ group.execution = { isolation: 'worktree', sandbox: 'workspace-write', approval: 'never', network: 'deny', transport: 'shell', receipts: 'optional', ...group.execution };
415
+ for (const member of group.members) {
416
+ member.enabled ??= true;
417
+ member.priority ??= 50;
418
+ }
419
+ }
420
+ for (const cli of Object.values(resolved.clis ?? {}))
421
+ cli.enabled ??= true;
422
+ if (resolved.admission)
423
+ resolved.admission = { maxQueued: 128, queueTimeoutSeconds: 300, leaseSeconds: 60, authorityRef: 'project', ...resolved.admission };
424
+ if (resolved.budgets)
425
+ resolved.budgets = { enforcement: 'observed', unknownCost: 'deny', ...resolved.budgets };
426
+ if (resolved.retry)
427
+ resolved.retry = { maxAttempts: 3, on: ['rate-limit', 'unavailable', 'timeout'], baseDelaySeconds: 2, maxDelaySeconds: 60, ...resolved.retry };
428
+ }
238
429
  return resolved;
239
430
  }
240
431
  const GRAPH_BACKENDS = ['json', 'graphology', 'sqlite'];
@@ -826,24 +1017,35 @@ export function getProjectDir(ctx, args = []) {
826
1017
  return targetValue || ctx?.cwd || process.cwd();
827
1018
  }
828
1019
  /**
829
- * Read .aiwg/aiwg.config.
1020
+ * Read and independently validate .aiwg/aiwg.config EXCEPT for the
1021
+ * worker-model defaults step (`validateWorkerConfigCandidate`), which needs
1022
+ * its own `aiwgHome` resolution and can fail solely because the bundled
1023
+ * model defaults are unreachable. Callers that want to degrade gracefully
1024
+ * on that one failure mode (e.g. generated-context rendering) read the
1025
+ * candidate here, then call `validateWorkerConfigCandidate` themselves and
1026
+ * handle its failure explicitly. `readAiwgConfig` below is the composition
1027
+ * of both steps for callers that want the original all-or-nothing contract.
830
1028
  * Returns null if the file does not exist.
831
1029
  */
832
- export async function readAiwgConfig(projectDir) {
1030
+ export async function readAiwgConfigCandidate(projectDir) {
833
1031
  const localPath = getConfigPath(projectDir);
834
1032
  const artifactPath = projectAiwgPath(projectDir, CONFIG_FILENAME);
835
1033
  let filePath = localPath;
836
1034
  try {
837
1035
  await access(localPath);
838
1036
  }
839
- catch {
1037
+ catch (error) {
1038
+ if (error.code !== 'ENOENT')
1039
+ throw error;
840
1040
  if (artifactPath === localPath)
841
1041
  return null;
842
1042
  try {
843
1043
  await access(artifactPath);
844
1044
  filePath = artifactPath;
845
1045
  }
846
- catch {
1046
+ catch (error) {
1047
+ if (error.code !== 'ENOENT')
1048
+ throw error;
847
1049
  return null;
848
1050
  }
849
1051
  }
@@ -873,6 +1075,7 @@ export async function readAiwgConfig(projectDir) {
873
1075
  const threatAssessmentErrors = [
874
1076
  ...validateThreatAssessmentConfig(parsed.security?.threatAssessment),
875
1077
  ...validateProjectConfig(parsed.project),
1078
+ ...validateParallelismConfig(parsed.parallelism),
876
1079
  ];
877
1080
  if (threatAssessmentErrors.length > 0) {
878
1081
  throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
@@ -894,6 +1097,22 @@ export async function readAiwgConfig(projectDir) {
894
1097
  }
895
1098
  return parsed;
896
1099
  }
1100
+ /**
1101
+ * Read .aiwg/aiwg.config, including worker-model default validation.
1102
+ * Returns null if the file does not exist.
1103
+ *
1104
+ * `aiwgHome` (optional) resolves the bundled AIWG model-defaults location
1105
+ * passed through to `validateWorkerConfigCandidate` / `ConfigLoader`,
1106
+ * without ever reading or writing `process.env.AIWG_HOME`. Omit it to keep
1107
+ * the default precedence (`AIWG_HOME` env, else the user-home install).
1108
+ */
1109
+ export async function readAiwgConfig(projectDir, options) {
1110
+ const parsed = await readAiwgConfigCandidate(projectDir);
1111
+ if (!parsed)
1112
+ return null;
1113
+ await validateWorkerConfigCandidate(projectDir, parsed, options);
1114
+ return parsed;
1115
+ }
897
1116
  /**
898
1117
  * Write aiwg.config to the repository-local control plane. When a reachable
899
1118
  * external corpus also carries the compatibility control copy, keep it in
@@ -903,6 +1122,7 @@ export async function writeAiwgConfig(projectDir, config) {
903
1122
  const threatAssessmentErrors = [
904
1123
  ...validateThreatAssessmentConfig(config.security?.threatAssessment),
905
1124
  ...validateProjectConfig(config.project),
1125
+ ...validateParallelismConfig(config.parallelism),
906
1126
  ];
907
1127
  if (threatAssessmentErrors.length > 0) {
908
1128
  throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
@@ -916,6 +1136,7 @@ export async function writeAiwgConfig(projectDir, config) {
916
1136
  const gatesErrors = validateGatesConfig(config.gates);
917
1137
  if (gatesErrors.length > 0)
918
1138
  throw new Error(`Invalid .aiwg/aiwg.config:\n${gatesErrors.join('\n')}`);
1139
+ await validateWorkerConfigCandidate(projectDir, config);
919
1140
  const localPath = getConfigPath(projectDir);
920
1141
  const artifactDir = resolveProjectAiwgDir(projectDir);
921
1142
  const artifactPath = join(artifactDir, CONFIG_FILENAME);
@@ -1134,4 +1355,47 @@ export async function populateDeployedTo(config, projectDir) {
1134
1355
  }
1135
1356
  return config;
1136
1357
  }
1358
+ /** Shape validation alone cannot infer process/HTTP metadata or model authority. */
1359
+ export function validateWorkerInstallationBindings(parallelism, adapters) {
1360
+ const errors = [];
1361
+ for (const [id, cli] of Object.entries(parallelism.clis ?? {})) {
1362
+ if (cli.enabled === false)
1363
+ continue;
1364
+ const metadata = adapters[cli.adapter];
1365
+ if (!metadata)
1366
+ errors.push(`OG_ADAPTER_UNQUALIFIED /parallelism/clis/${id}/adapter: Unknown trusted binding`);
1367
+ else if (metadata.transport === 'http' ? (!cli.endpoint || cli.executable !== undefined) : (cli.endpoint !== undefined || !(cli.executable ?? metadata.executable)))
1368
+ errors.push(`OG_CONFIG_REFERENCE /parallelism/clis/${id}: Invalid transport binding`);
1369
+ }
1370
+ return errors;
1371
+ }
1372
+ export function validateWorkerGroupReferences(parallelism, selection, qualifiedRoles) {
1373
+ const errors = validateParallelismConfig(parallelism);
1374
+ if (errors.length)
1375
+ return errors;
1376
+ for (const [role, guidance] of Object.entries(selection.roleGuidance ?? {})) {
1377
+ for (const group of guidance.groups)
1378
+ if (!parallelism.groups?.[group])
1379
+ errors.push(`OG_CONFIG_REFERENCE /defaults/workerSelection/roleGuidance/${role}/groups: Unknown group ${group}`);
1380
+ }
1381
+ if (qualifiedRoles)
1382
+ for (const [id, group] of Object.entries(parallelism.groups ?? {})) {
1383
+ for (const role of group.requiredRoles ?? [])
1384
+ if (!group.members.some(m => m.enabled !== false && parallelism.clis?.[m.cli]?.enabled !== false && m.roles.includes(role) && qualifiedRoles[`${id}/${m.id}`]?.includes(role)))
1385
+ errors.push(`OG_ADAPTER_UNQUALIFIED /parallelism/groups/${id}/requiredRoles: No qualified route covers ${role}`);
1386
+ }
1387
+ return errors;
1388
+ }
1389
+ /** A configured hard ceiling is eligible only where a qualified control enforces it. */
1390
+ export function validateWorkerBudgetEnforcement(budgets, enforceable) {
1391
+ if (budgets?.enforcement !== 'hard')
1392
+ return [];
1393
+ const errors = [];
1394
+ for (const scope of ['perRun', 'perTask'])
1395
+ for (const dimension of ['tokens', 'costUsd', 'wallSeconds']) {
1396
+ if (budgets[scope]?.[dimension] !== undefined && !enforceable.includes(dimension))
1397
+ errors.push(`OG_BUDGET_UNMEASURABLE /parallelism/budgets/${scope}/${dimension}: No qualified enforcing control`);
1398
+ }
1399
+ return errors;
1400
+ }
1137
1401
  //# sourceMappingURL=aiwg-config.js.map