@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.
- 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 +210 -4
- 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 +75 -27
- package/dist/src/decision/driver.js +3 -1
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- 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 +21 -3
- 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 +49 -32
- 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
package/dist/src/config/cli.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* validate — Validate all config files
|
|
9
9
|
* reset [<key>] — Reset key or all config to defaults
|
|
10
10
|
* path — Print the active config directory path
|
|
11
|
-
* edit
|
|
11
|
+
* edit [--print-path] — Open config in $EDITOR/$VISUAL, or print its path headlessly
|
|
12
12
|
* gitignore — Show/check/fix .gitignore for AIWG runtime dirs
|
|
13
13
|
*
|
|
14
14
|
* Global flags:
|
|
@@ -21,6 +21,8 @@ import { fileURLToPath } from 'url';
|
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import { UserConfig } from './user-config.js';
|
|
23
23
|
import { AiwgError, EXIT_CODES } from '../cli/errors.js';
|
|
24
|
+
import { isInteractive } from '../cli/env.js';
|
|
25
|
+
import { exitCodeForSpawnResult } from '../cli/child-process-result.js';
|
|
24
26
|
import { projectControlPath, resolveProjectAiwgDir } from './project-artifacts.js';
|
|
25
27
|
const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
28
|
/**
|
|
@@ -63,7 +65,7 @@ export async function main(args) {
|
|
|
63
65
|
handlePath(config);
|
|
64
66
|
break;
|
|
65
67
|
case 'edit':
|
|
66
|
-
await handleEdit(config);
|
|
68
|
+
await handleEdit(config, subArgs);
|
|
67
69
|
break;
|
|
68
70
|
case 'gitignore':
|
|
69
71
|
await handleGitignore(subArgs);
|
|
@@ -86,7 +88,7 @@ export async function main(args) {
|
|
|
86
88
|
}
|
|
87
89
|
async function handleGet(config, args) {
|
|
88
90
|
const isProject = args.includes('--project');
|
|
89
|
-
const positional = args
|
|
91
|
+
const positional = configPositionals(args);
|
|
90
92
|
const key = positional[0];
|
|
91
93
|
if (!key) {
|
|
92
94
|
throw new AiwgError({
|
|
@@ -113,7 +115,7 @@ async function handleGet(config, args) {
|
|
|
113
115
|
}
|
|
114
116
|
async function handleSet(config, args) {
|
|
115
117
|
const isProject = args.includes('--project');
|
|
116
|
-
const positional = args
|
|
118
|
+
const positional = configPositionals(args);
|
|
117
119
|
const key = positional[0];
|
|
118
120
|
const value = positional[1];
|
|
119
121
|
if (!key || value === undefined) {
|
|
@@ -167,6 +169,11 @@ const ENUM_RULES = {
|
|
|
167
169
|
'security.threatAssessment.mode': ['off', 'audit', 'enforce'],
|
|
168
170
|
'artifact_outputs.canonical': ['aiwg'],
|
|
169
171
|
'artifact_outputs.provider_native': ['disabled', 'explicit-only', 'project-default'],
|
|
172
|
+
// #3050: a setup/onboarding preference, not worker policy — kept out of
|
|
173
|
+
// the frozen `parallelism` contract fragment (B-OG-29). String enum (not
|
|
174
|
+
// boolean) so a future state (e.g. "snoozed") can be added without a
|
|
175
|
+
// breaking schema change.
|
|
176
|
+
'setup.worker_dispatch_offer': ['declined'],
|
|
170
177
|
};
|
|
171
178
|
const BOOLEAN_FIELDS = new Set([
|
|
172
179
|
'delivery.delete_branch_on_merge',
|
|
@@ -178,6 +185,7 @@ const BOOLEAN_FIELDS = new Set([
|
|
|
178
185
|
'telemetry.skill_usage.enabled',
|
|
179
186
|
]);
|
|
180
187
|
const STRING_ARRAY_FIELDS = new Set([
|
|
188
|
+
'providers',
|
|
181
189
|
'remotes.tracker_actor.forbid_actors',
|
|
182
190
|
'remotes.customer_tracker_actor.forbid_actors',
|
|
183
191
|
'command_log.scopes',
|
|
@@ -193,6 +201,7 @@ const INTEGER_FIELDS = {
|
|
|
193
201
|
'telemetry.skill_usage.max_bytes': { min: 1024, max: 104_857_600 },
|
|
194
202
|
};
|
|
195
203
|
async function projectConfigGet(key, args) {
|
|
204
|
+
assertSafeConfigPath(key);
|
|
196
205
|
const { readAiwgConfig, getProjectDir } = await import('./aiwg-config.js');
|
|
197
206
|
const projectDir = getProjectDir(undefined, args);
|
|
198
207
|
const cfg = await readAiwgConfig(projectDir);
|
|
@@ -204,6 +213,21 @@ async function projectConfigGet(key, args) {
|
|
|
204
213
|
exitCode: EXIT_CODES.CONFIG,
|
|
205
214
|
});
|
|
206
215
|
}
|
|
216
|
+
// Model preferences keep their existing file owner. Inspection uses the same
|
|
217
|
+
// merged policy and pre-merge origins as worker resolution, never aiwg.config.
|
|
218
|
+
if (key === 'defaults.workerSelection' || key.startsWith('defaults.workerSelection.')) {
|
|
219
|
+
const { ConfigLoader } = await import('../models/config-loader.js');
|
|
220
|
+
const loader = new ConfigLoader(projectDir);
|
|
221
|
+
const models = await loader.load(cfg.parallelism);
|
|
222
|
+
const value = getDottedPath(models, key);
|
|
223
|
+
const pointer = `/${key.split('.').join('/')}`;
|
|
224
|
+
const origins = Object.fromEntries(Object.entries(models.workerPolicyOrigins ?? {})
|
|
225
|
+
.filter(([field]) => field === pointer || field.startsWith(`${pointer}/`)));
|
|
226
|
+
console.log(args.includes('--origins')
|
|
227
|
+
? JSON.stringify({ value: value ?? null, origins, digests: loader.getInputDigests() }, null, 2)
|
|
228
|
+
: value === undefined ? '(not set)' : JSON.stringify(value, null, 2));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
207
231
|
const value = getDottedPath(cfg, key);
|
|
208
232
|
if (value === undefined) {
|
|
209
233
|
console.log('(not set)');
|
|
@@ -216,6 +240,15 @@ async function projectConfigGet(key, args) {
|
|
|
216
240
|
}
|
|
217
241
|
}
|
|
218
242
|
async function projectConfigSet(key, raw, args) {
|
|
243
|
+
assertSafeConfigPath(key);
|
|
244
|
+
if (key === 'defaults' || key.startsWith('defaults.')) {
|
|
245
|
+
throw new AiwgError({
|
|
246
|
+
code: 'ERR_INVALID_KEY',
|
|
247
|
+
message: `Project model preferences '${key}' belong in models.json, not aiwg.config.`,
|
|
248
|
+
hint: 'Edit project-root models.json to change defaults, including defaults.workerSelection.',
|
|
249
|
+
exitCode: EXIT_CODES.USAGE,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
219
252
|
const { readAiwgConfig, writeAiwgConfig, getProjectDir, emptyConfig, validateExternalLinks, } = await import('./aiwg-config.js');
|
|
220
253
|
const projectDir = getProjectDir(undefined, args);
|
|
221
254
|
// Validate enum fields before writing. Deprecated spellings normalize forward with a
|
|
@@ -237,6 +270,12 @@ async function projectConfigSet(key, raw, args) {
|
|
|
237
270
|
}
|
|
238
271
|
// Coerce booleans for known boolean fields
|
|
239
272
|
let value = raw;
|
|
273
|
+
if (key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
|
|
274
|
+
try {
|
|
275
|
+
value = JSON.parse(raw);
|
|
276
|
+
}
|
|
277
|
+
catch { /* String leaves are validated by the whole-object writer. */ }
|
|
278
|
+
}
|
|
240
279
|
if (/^externalLinks\.[^.]+$/.test(key) || key === 'security.threatAssessment') {
|
|
241
280
|
try {
|
|
242
281
|
value = JSON.parse(raw);
|
|
@@ -267,10 +306,24 @@ async function projectConfigSet(key, raw, args) {
|
|
|
267
306
|
}
|
|
268
307
|
}
|
|
269
308
|
if (STRING_ARRAY_FIELDS.has(key)) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
309
|
+
let items = raw.split(',');
|
|
310
|
+
if (raw.trimStart().startsWith('[')) {
|
|
311
|
+
try {
|
|
312
|
+
items = JSON.parse(raw);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
items = undefined;
|
|
316
|
+
}
|
|
317
|
+
if (!Array.isArray(items) || items.some(item => typeof item !== 'string')) {
|
|
318
|
+
throw new AiwgError({
|
|
319
|
+
code: 'ERR_INVALID_VALUE',
|
|
320
|
+
message: `${key} must be a comma-separated list or a JSON array of strings`,
|
|
321
|
+
hint: `Try: aiwg config set --project ${key} '["a","b"]' (or a,b)`,
|
|
322
|
+
exitCode: EXIT_CODES.USAGE,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
value = items.map(item => item.trim()).filter(Boolean);
|
|
274
327
|
if (key === 'command_log.scopes' || key === 'telemetry.skill_usage.scopes') {
|
|
275
328
|
const invalid = value.filter(item => item !== 'project' && item !== 'global');
|
|
276
329
|
if (invalid.length > 0) {
|
|
@@ -336,6 +389,7 @@ function getDottedPath(obj, key) {
|
|
|
336
389
|
}
|
|
337
390
|
/** Write a dotted path into a nested record, creating intermediate objects. */
|
|
338
391
|
function setDottedPath(obj, key, value) {
|
|
392
|
+
assertSafeConfigPath(key);
|
|
339
393
|
const parts = key.split('.');
|
|
340
394
|
const last = parts.pop();
|
|
341
395
|
let cur = obj;
|
|
@@ -348,6 +402,25 @@ function setDottedPath(obj, key, value) {
|
|
|
348
402
|
}
|
|
349
403
|
cur[last] = value;
|
|
350
404
|
}
|
|
405
|
+
function assertSafeConfigPath(key) {
|
|
406
|
+
if (key.split('.').some(part => !part || ['__proto__', 'prototype', 'constructor'].includes(part))) {
|
|
407
|
+
throw new AiwgError({ code: 'ERR_INVALID_KEY', message: 'Unsafe configuration path', exitCode: EXIT_CODES.USAGE });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** Scope/inspection flags are not dotted keys, irrespective of flag order. */
|
|
411
|
+
function configPositionals(args) {
|
|
412
|
+
const result = [];
|
|
413
|
+
for (let i = 0; i < args.length; i++) {
|
|
414
|
+
if (['--target', '--prefix'].includes(args[i])) {
|
|
415
|
+
i++;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (['--project', '--user', '--origins'].includes(args[i]))
|
|
419
|
+
continue;
|
|
420
|
+
result.push(args[i]);
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
351
424
|
async function handleList(config) {
|
|
352
425
|
const allConfig = await config.list();
|
|
353
426
|
console.log(`Config directory: ${config.getPath()}\n`);
|
|
@@ -392,7 +465,7 @@ async function handleValidate(config, args) {
|
|
|
392
465
|
}
|
|
393
466
|
}
|
|
394
467
|
async function handleProjectValidate(args) {
|
|
395
|
-
const { getProjectDir, readAiwgConfig, resolveIssueLabels, validateExternalLinks, validateIndexConfig, validateIssueLabels, } = await import('./aiwg-config.js');
|
|
468
|
+
const { getProjectDir, readAiwgConfig, resolveIssueLabels, validateExternalLinks, validateIndexConfig, validateIssueLabels, validateWorkerConfigCandidate, } = await import('./aiwg-config.js');
|
|
396
469
|
const projectDir = getProjectDir(undefined, args);
|
|
397
470
|
const cfg = await readAiwgConfig(projectDir);
|
|
398
471
|
if (!cfg) {
|
|
@@ -403,6 +476,7 @@ async function handleProjectValidate(args) {
|
|
|
403
476
|
exitCode: EXIT_CODES.CONFIG,
|
|
404
477
|
});
|
|
405
478
|
}
|
|
479
|
+
await validateWorkerConfigCandidate(projectDir, cfg);
|
|
406
480
|
const providerIndex = args.indexOf('--provider');
|
|
407
481
|
const providerArg = providerIndex >= 0 ? args[providerIndex + 1] : undefined;
|
|
408
482
|
const allowedProviders = ['gitea', 'github', 'local'];
|
|
@@ -463,7 +537,7 @@ async function handleProjectValidate(args) {
|
|
|
463
537
|
}
|
|
464
538
|
async function handleReset(config, args) {
|
|
465
539
|
const isProject = args.includes('--project');
|
|
466
|
-
const positional = args
|
|
540
|
+
const positional = configPositionals(args);
|
|
467
541
|
const key = positional[0];
|
|
468
542
|
if (isProject) {
|
|
469
543
|
await projectConfigReset(key, args);
|
|
@@ -496,6 +570,7 @@ async function projectConfigReset(key, args) {
|
|
|
496
570
|
exitCode: EXIT_CODES.USAGE,
|
|
497
571
|
});
|
|
498
572
|
}
|
|
573
|
+
assertSafeConfigPath(key);
|
|
499
574
|
const projectDir = getProjectDir(undefined, args);
|
|
500
575
|
const cfg = await readAiwgConfig(projectDir);
|
|
501
576
|
if (!cfg) {
|
|
@@ -520,6 +595,19 @@ async function projectConfigReset(key, args) {
|
|
|
520
595
|
return;
|
|
521
596
|
}
|
|
522
597
|
if (key.startsWith('parallelism.')) {
|
|
598
|
+
const workerField = key.split('.')[1];
|
|
599
|
+
if (['worker_dispatch', 'worker_mode', 'clis', 'groups', 'admission', 'budgets', 'retry', 'health', 'activation'].includes(workerField)) {
|
|
600
|
+
const parts = key.split('.').slice(1);
|
|
601
|
+
const last = parts.pop();
|
|
602
|
+
let current = cfg.parallelism;
|
|
603
|
+
for (const part of parts)
|
|
604
|
+
current = current?.[part];
|
|
605
|
+
if (current)
|
|
606
|
+
delete current[last];
|
|
607
|
+
await writeAiwgConfig(projectDir, cfg);
|
|
608
|
+
console.log(`Reset --project ${key}`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
523
611
|
const field = key.slice('parallelism.'.length);
|
|
524
612
|
if (field in defaults) {
|
|
525
613
|
cfg.parallelism = cfg.parallelism ?? {};
|
|
@@ -539,13 +627,40 @@ async function projectConfigReset(key, args) {
|
|
|
539
627
|
function handlePath(config) {
|
|
540
628
|
console.log(config.getPath());
|
|
541
629
|
}
|
|
542
|
-
|
|
543
|
-
|
|
630
|
+
/**
|
|
631
|
+
* `aiwg config edit [--print-path]` (#3062). `--print-path` is the headless
|
|
632
|
+
* equivalent: it prints the resolved config file path and exits, so scripts
|
|
633
|
+
* and agents can read/edit the file themselves (or compose with
|
|
634
|
+
* `aiwg config get`/`set`) instead of needing $EDITOR. Without a terminal
|
|
635
|
+
* and without `--print-path`, this refuses instead of spawning an editor
|
|
636
|
+
* that would hang waiting for input nobody can supply.
|
|
637
|
+
*/
|
|
638
|
+
async function handleEdit(config, args = []) {
|
|
544
639
|
const configPath = `${config.getPath()}/config.yaml`;
|
|
640
|
+
if (args.includes('--print-path')) {
|
|
641
|
+
// Ensure the config file exists before pointing a caller at it.
|
|
642
|
+
await config.ensureDir();
|
|
643
|
+
console.log(configPath);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (!isInteractive()) {
|
|
647
|
+
throw new AiwgError({
|
|
648
|
+
code: 'ERR_NON_INTERACTIVE_EDIT',
|
|
649
|
+
message: 'aiwg config edit requires an interactive terminal to open $EDITOR/$VISUAL.',
|
|
650
|
+
hint: 'Use `aiwg config set <key> <value>` to change a value headlessly, or `aiwg config edit --print-path` to get the file path and edit it yourself.',
|
|
651
|
+
exitCode: EXIT_CODES.USAGE,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
545
655
|
// Ensure the config file exists before opening
|
|
546
656
|
await config.ensureDir();
|
|
547
657
|
const { execSync } = await import('child_process');
|
|
548
658
|
try {
|
|
659
|
+
// sync-spawn: interactive editor-launch — launches $EDITOR/$VISUAL on the
|
|
660
|
+
// config file; the user interacts with it directly (see tools/testing/
|
|
661
|
+
// sync-spawn-allowlist.json). Reached only when isInteractive() is true
|
|
662
|
+
// (#3062) — the no-terminal path above refuses before spawning anything.
|
|
663
|
+
// Not a candidate for a bounded timeout.
|
|
549
664
|
execSync(`${editor} "${configPath}"`, { stdio: 'inherit' });
|
|
550
665
|
}
|
|
551
666
|
catch (err) {
|
|
@@ -584,7 +699,7 @@ For project-level config: aiwg config show --project [--json]
|
|
|
584
699
|
}
|
|
585
700
|
// Lazy-import to keep startup cost low for unrelated subcommands.
|
|
586
701
|
const { readAiwgConfig, resolveRemotes, getProjectDir } = await import('./aiwg-config.js');
|
|
587
|
-
const {
|
|
702
|
+
const { boundedSpawnSync } = await import('../process/bounded-child-process.mjs');
|
|
588
703
|
const projectDir = getProjectDir(undefined, args);
|
|
589
704
|
const cfg = await readAiwgConfig(projectDir);
|
|
590
705
|
if (!cfg) {
|
|
@@ -598,7 +713,7 @@ For project-level config: aiwg config show --project [--json]
|
|
|
598
713
|
const resolvedRemotes = resolveRemotes(cfg.remotes);
|
|
599
714
|
// Resolve each declared remote name to its URL via git. Best-effort.
|
|
600
715
|
function getUrl(name) {
|
|
601
|
-
const r =
|
|
716
|
+
const r = boundedSpawnSync('git', ['-C', projectDir, 'remote', 'get-url', name], 'quick', {
|
|
602
717
|
encoding: 'utf-8',
|
|
603
718
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
604
719
|
});
|
|
@@ -691,14 +806,24 @@ For project-level config: aiwg config show --project [--json]
|
|
|
691
806
|
console.log('');
|
|
692
807
|
}
|
|
693
808
|
async function handleGitignore(args) {
|
|
694
|
-
const {
|
|
809
|
+
const { boundedSpawnSync } = await import('../process/bounded-child-process.mjs');
|
|
695
810
|
// Locate the gitignore CLI script relative to this compiled module
|
|
696
811
|
// (#1228). At runtime this module lives at `dist/src/config/cli.js`,
|
|
697
812
|
// so we need three `..` segments to reach the package root where
|
|
698
813
|
// `tools/cli/` is shipped (see package.json `files`).
|
|
699
814
|
const scriptPath = path.resolve(_scriptDir, '../../../tools/cli/config-gitignore.mjs');
|
|
700
|
-
|
|
815
|
+
// Local script editing .gitignore blocks; quick tier (#3062).
|
|
816
|
+
const result = boundedSpawnSync(process.execPath, [scriptPath, ...args], 'quick', { stdio: 'inherit' });
|
|
701
817
|
if (result.status !== 0) {
|
|
818
|
+
// #3062 review item 7: already correctly treats a null status
|
|
819
|
+
// (timeout/signal) as failure (`process.exit(result.status ?? 1)`
|
|
820
|
+
// below); surface the bounded helper's diagnostic too — `stdio:
|
|
821
|
+
// 'inherit'` means a normal script failure already printed its own
|
|
822
|
+
// output, but a bounded-helper timeout/kill would otherwise never be
|
|
823
|
+
// reported anywhere.
|
|
824
|
+
const { message } = exitCodeForSpawnResult(result, 'aiwg config gitignore');
|
|
825
|
+
if (message)
|
|
826
|
+
console.error(message);
|
|
702
827
|
process.exit(result.status ?? 1);
|
|
703
828
|
}
|
|
704
829
|
}
|
|
@@ -707,8 +832,8 @@ function printUsage() {
|
|
|
707
832
|
|
|
708
833
|
Subcommands:
|
|
709
834
|
get <key> Read a user config value
|
|
710
|
-
get --project <key> Read a project config value
|
|
711
|
-
|
|
835
|
+
get --project <key> Read a project config value or merged defaults.workerSelection
|
|
836
|
+
[--origins] Include model policy sources, pointers and input digests
|
|
712
837
|
set <key> <value> Write a user config value
|
|
713
838
|
set --project <key> <value> Write a project config value (validates enums)
|
|
714
839
|
list Show all user config
|
|
@@ -718,7 +843,8 @@ Subcommands:
|
|
|
718
843
|
[--provider gitea|github|local] [--available-label NAME ...]
|
|
719
844
|
reset [<key>] Reset key or all config to defaults
|
|
720
845
|
path Print config directory path
|
|
721
|
-
edit Open config in $EDITOR
|
|
846
|
+
edit Open config in $EDITOR/$VISUAL (requires a terminal)
|
|
847
|
+
edit --print-path Print the config file path instead of opening an editor
|
|
722
848
|
gitignore Show/check/fix .gitignore AIWG entries
|
|
723
849
|
|
|
724
850
|
Global flags:
|
|
@@ -735,6 +861,7 @@ Examples:
|
|
|
735
861
|
aiwg config set --project delivery.mode pr-required
|
|
736
862
|
aiwg config set --project delivery.merge_style squash
|
|
737
863
|
aiwg config get --project parallelism
|
|
864
|
+
aiwg config get --project defaults.workerSelection --origins
|
|
738
865
|
aiwg config set --project parallelism.max_parallel_subagents 6
|
|
739
866
|
aiwg config reset --project parallelism
|
|
740
867
|
aiwg config validate
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Shared runtime resolver for config consumers and direct preflight tools. */
|
|
2
|
+
export const DEFAULT_DISPATCH_HOST_THRESHOLDS = Object.freeze({
|
|
3
|
+
min_avail_mem_gb: 8,
|
|
4
|
+
max_load_ratio: 1.0,
|
|
5
|
+
max_mem_psi_avg10: 30,
|
|
6
|
+
max_mem_psi_avg60: 10,
|
|
7
|
+
// 08 brief/evidence retention: stop new admissions when less than 2 GiB free remains.
|
|
8
|
+
min_free_disk_gb: 2,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configured layers can only tighten one another and the conservative defaults;
|
|
13
|
+
* minimums take MAX and maximums take MIN across build, admission and task.
|
|
14
|
+
* Validate each layer before merging so an invalid value cannot be hidden.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveDispatchHostThresholds(buildDispatch, admissionHostResources, overlay) {
|
|
17
|
+
const configured = { ...DEFAULT_DISPATCH_HOST_THRESHOLDS };
|
|
18
|
+
const keys = [...Object.keys(DEFAULT_DISPATCH_HOST_THRESHOLDS), 'max_workers'];
|
|
19
|
+
for (const layer of [buildDispatch, admissionHostResources, overlay]) {
|
|
20
|
+
if (layer === undefined) continue;
|
|
21
|
+
if (layer === null || typeof layer !== 'object' || Array.isArray(layer)) {
|
|
22
|
+
throw new Error('OG_CONFIG_REFERENCE dispatch thresholds: must be an object');
|
|
23
|
+
}
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
const value = layer[key];
|
|
26
|
+
if (value === undefined) continue;
|
|
27
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
28
|
+
throw new Error(`OG_CONFIG_REFERENCE dispatch threshold ${key}: must be a non-negative number`);
|
|
29
|
+
}
|
|
30
|
+
if (key === 'max_workers' && (!Number.isInteger(value) || value < 1)) {
|
|
31
|
+
throw new Error('OG_CONFIG_REFERENCE dispatch threshold max_workers: must be an integer >= 1 when set');
|
|
32
|
+
}
|
|
33
|
+
configured[key] = configured[key] === undefined ? value
|
|
34
|
+
: key.startsWith('min_') ? Math.max(configured[key], value) : Math.min(configured[key], value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { ...DEFAULT_DISPATCH_HOST_THRESHOLDS, ...configured };
|
|
38
|
+
}
|
|
@@ -10,6 +10,7 @@ import { join, relative, resolve, sep } from 'node:path';
|
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_PROJECT_AIWG_DIR,
|
|
12
12
|
PROJECT_AIWG_LOCATION_FILE,
|
|
13
|
+
isProjectArtifactRootExternal,
|
|
13
14
|
readProjectArtifactLocation,
|
|
14
15
|
resolveProjectAiwgDir,
|
|
15
16
|
} from './project-artifacts-runtime.mjs';
|
|
@@ -57,8 +58,16 @@ export function auditProjectArtifactHealth(projectDir, env = process.env) {
|
|
|
57
58
|
const localRoot = join(root, DEFAULT_PROJECT_AIWG_DIR);
|
|
58
59
|
const artifactRoot = resolveProjectAiwgDir(root, env);
|
|
59
60
|
const source = configuredSource(root, env);
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
// Cheap, read-only check for whether an external root is configured at all
|
|
62
|
+
// — reused so repair's early refusal (src/artifacts/repair.ts) and this
|
|
63
|
+
// audit agree on the same definition of "local mode" (#274).
|
|
64
|
+
const externalConfigured = isProjectArtifactRootExternal(root, env);
|
|
65
|
+
// In local mode (no external root configured) artifactRoot === localRoot:
|
|
66
|
+
// there is nothing external to reach or compare against. Skip the
|
|
67
|
+
// reachability stat and the full-corpus listing/byte-compare entirely —
|
|
68
|
+
// previously this read and byte-compared every local payload file with
|
|
69
|
+
// itself on every `aiwg status`/`doctor`/`repair` call (#274).
|
|
70
|
+
const externalReachable = externalConfigured && existsSync(artifactRoot) && statSync(artifactRoot).isDirectory();
|
|
62
71
|
|
|
63
72
|
const controls = PROJECT_CONTROL_PLANE_FILES.map((file) => {
|
|
64
73
|
const localPath = join(localRoot, file);
|
|
@@ -69,7 +78,9 @@ export function auditProjectArtifactHealth(projectDir, env = process.env) {
|
|
|
69
78
|
});
|
|
70
79
|
const missingLocal = controls.filter((item) => !item.local).map((item) => item.file);
|
|
71
80
|
const divergentControl = controls.filter((item) => item.identical === false).map((item) => item.file);
|
|
72
|
-
const localPayload =
|
|
81
|
+
const localPayload = externalConfigured
|
|
82
|
+
? listFiles(localRoot).filter((file) => !PROJECT_CONTROL_PLANE_FILES.includes(file))
|
|
83
|
+
: [];
|
|
73
84
|
const divergentPayload = externalReachable
|
|
74
85
|
? localPayload.filter((file) => !sameFile(join(localRoot, file), join(artifactRoot, file)))
|
|
75
86
|
: [];
|
|
@@ -245,10 +245,8 @@ export class UserConfig {
|
|
|
245
245
|
if (this.configCache) {
|
|
246
246
|
return this.configCache;
|
|
247
247
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
const content = await readFile(configPath, 'utf-8');
|
|
251
|
-
const parsed = parseYamlSimple(content);
|
|
248
|
+
const parsed = await this.readConfig();
|
|
249
|
+
if (parsed) {
|
|
252
250
|
this.configCache = {
|
|
253
251
|
...DEFAULT_USER_CONFIG,
|
|
254
252
|
...parsed,
|
|
@@ -259,7 +257,7 @@ export class UserConfig {
|
|
|
259
257
|
};
|
|
260
258
|
return this.configCache;
|
|
261
259
|
}
|
|
262
|
-
|
|
260
|
+
else {
|
|
263
261
|
// File doesn't exist — return defaults (don't create file yet)
|
|
264
262
|
this.configCache = {
|
|
265
263
|
...DEFAULT_USER_CONFIG,
|
|
@@ -271,10 +269,24 @@ export class UserConfig {
|
|
|
271
269
|
return this.configCache;
|
|
272
270
|
}
|
|
273
271
|
}
|
|
272
|
+
async readConfig() {
|
|
273
|
+
const configPath = resolve(this.configDir, 'config.yaml');
|
|
274
|
+
try {
|
|
275
|
+
const content = await readFile(configPath, 'utf-8');
|
|
276
|
+
return parseYamlSimple(content);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (err.code === 'ENOENT')
|
|
280
|
+
return null;
|
|
281
|
+
throw new Error(`Failed to load ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
274
284
|
/**
|
|
275
285
|
* Save config.yaml
|
|
276
286
|
*/
|
|
277
287
|
async saveConfig(config) {
|
|
288
|
+
// Recheck the existing file even when defaults or cached values are being saved.
|
|
289
|
+
await this.readConfig();
|
|
278
290
|
await this.ensureDir();
|
|
279
291
|
const configPath = resolve(this.configDir, 'config.yaml');
|
|
280
292
|
const yaml = serializeYamlSimple(config);
|
|
@@ -304,11 +316,15 @@ export function parseYamlSimple(content) {
|
|
|
304
316
|
continue;
|
|
305
317
|
const key = match[1].trim();
|
|
306
318
|
const rawValue = match[2].trim();
|
|
319
|
+
// Config keys have no legitimate use for prototype-related names at any level.
|
|
320
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
321
|
+
throw new Error('Unsafe config key: prototype-related names are not allowed');
|
|
322
|
+
}
|
|
307
323
|
if (indent === 0) {
|
|
308
324
|
if (rawValue === '' || rawValue === undefined) {
|
|
309
325
|
// Section header
|
|
310
326
|
currentSection = key;
|
|
311
|
-
if (!(key
|
|
327
|
+
if (!Object.hasOwn(result, key)) {
|
|
312
328
|
result[key] = {};
|
|
313
329
|
}
|
|
314
330
|
}
|
|
@@ -375,7 +391,7 @@ function getNestedValue(obj, path) {
|
|
|
375
391
|
const parts = path.split('.');
|
|
376
392
|
let current = obj;
|
|
377
393
|
for (const part of parts) {
|
|
378
|
-
if (current === null || current === undefined || typeof current !== 'object') {
|
|
394
|
+
if (current === null || current === undefined || typeof current !== 'object' || !Object.hasOwn(current, part)) {
|
|
379
395
|
return undefined;
|
|
380
396
|
}
|
|
381
397
|
current = current[part];
|
|
@@ -384,15 +400,25 @@ function getNestedValue(obj, path) {
|
|
|
384
400
|
}
|
|
385
401
|
function setNestedValue(obj, path, value) {
|
|
386
402
|
const parts = path.split('.');
|
|
403
|
+
// Reject the entire path before mutating config; key uses below also guard each segment.
|
|
404
|
+
if (parts.some(part => part === '__proto__' || part === 'constructor' || part === 'prototype')) {
|
|
405
|
+
throw new Error('Unsafe config key path: prototype-related segments are not allowed');
|
|
406
|
+
}
|
|
387
407
|
let current = obj;
|
|
388
|
-
for (let i = 0; i < parts.length
|
|
408
|
+
for (let i = 0; i < parts.length; i++) {
|
|
389
409
|
const part = parts[i];
|
|
390
|
-
if (
|
|
410
|
+
if (part === '__proto__' || part === 'constructor' || part === 'prototype') {
|
|
411
|
+
throw new Error('Unsafe config key path: prototype-related segments are not allowed');
|
|
412
|
+
}
|
|
413
|
+
if (i === parts.length - 1) {
|
|
414
|
+
current[part] = value;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!Object.hasOwn(current, part) || typeof current[part] !== 'object' || current[part] === null) {
|
|
391
418
|
current[part] = {};
|
|
392
419
|
}
|
|
393
420
|
current = current[part];
|
|
394
421
|
}
|
|
395
|
-
current[parts[parts.length - 1]] = value;
|
|
396
422
|
}
|
|
397
423
|
function parseValue(value) {
|
|
398
424
|
if (value === 'true')
|