@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.
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- 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
|
|
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
|