@aiwg/cli 2026.10.8 → 2026.10.10

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 (104) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +197 -3
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/artifacts/cli.js +9 -5
  4. package/dist/src/auth/config.js +3 -8
  5. package/dist/src/auth/resource-credentials.js +35 -2
  6. package/dist/src/cli/handlers/diagnose.js +13 -0
  7. package/dist/src/cli/handlers/mc-admission.js +39 -0
  8. package/dist/src/cli/handlers/mc.js +2 -31
  9. package/dist/src/cli/handlers/models-help.js +52 -0
  10. package/dist/src/cli/handlers/models.js +11 -81
  11. package/dist/src/cli/handlers/resource-versions.js +8 -5
  12. package/dist/src/cli/handlers/script-runner.js +13 -1
  13. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  14. package/dist/src/cli/handlers/use.js +209 -233
  15. package/dist/src/cli/ui.js +1 -0
  16. package/dist/src/config/aiwg-config.js +38 -184
  17. package/dist/src/config/cli-help.js +54 -0
  18. package/dist/src/config/cli.js +5 -69
  19. package/dist/src/config/config-normalization.js +10 -0
  20. package/dist/src/config/worker-activation-gate.js +26 -0
  21. package/dist/src/config/worker-adoption-cli.js +102 -0
  22. package/dist/src/config/worker-adoption.js +368 -0
  23. package/dist/src/config/worker-config-digest.js +14 -0
  24. package/dist/src/config/worker-group-templates.js +150 -0
  25. package/dist/src/config/worker-guidance-warning.js +28 -0
  26. package/dist/src/config/worker-owner-lock.js +152 -0
  27. package/dist/src/config/worker-parallelism.js +154 -0
  28. package/dist/src/config/worker-write.js +75 -0
  29. package/dist/src/decision/telemetry/exporter.js +2 -1
  30. package/dist/src/decision/telemetry/index.js +1 -0
  31. package/dist/src/decision/telemetry/mapping.js +43 -0
  32. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  33. package/dist/src/decision/telemetry/redaction.js +6 -1
  34. package/dist/src/decision/telemetry/types.js +3 -0
  35. package/dist/src/decision/telemetry/worker.js +69 -0
  36. package/dist/src/lint/rule-activation.js +15 -7
  37. package/dist/src/lint/runner.js +2 -2
  38. package/dist/src/mcp/server.mjs +2 -11
  39. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  40. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  41. package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
  42. package/dist/src/mission-protocol/codecs.js +8 -38
  43. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  44. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  45. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  46. package/dist/src/models/conductor-data-policy.mjs +190 -0
  47. package/dist/src/models/config-loader.js +19 -3
  48. package/dist/src/models/config-writer.js +27 -0
  49. package/dist/src/models/model-capabilities.v1.json +197 -3
  50. package/dist/src/models/provider-policy.js +7 -155
  51. package/dist/src/models/resolver.js +2 -293
  52. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  53. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  54. package/dist/src/models/worker-direct-readiness.js +9 -218
  55. package/dist/src/models/worker-health-readiness.js +105 -26
  56. package/dist/src/models/worker-installation.mjs +12 -14
  57. package/dist/src/models/worker-member-references.js +64 -0
  58. package/dist/src/models/worker-plan.js +317 -0
  59. package/dist/src/models/worker-selection-policy.js +217 -0
  60. package/dist/src/models/wrapper-deployment.js +1 -1
  61. package/dist/src/resources/web-release.js +76 -65
  62. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  63. package/dist/src/serve/shared-host-scheduler.js +25 -107
  64. package/dist/src/serve/shared-host-starvation.js +73 -0
  65. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  66. package/dist/src/serve/worker-artifacts.mjs +56 -0
  67. package/dist/src/serve/worker-command-request.js +68 -0
  68. package/dist/src/serve/worker-invocation-paths.js +144 -0
  69. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  70. package/dist/src/serve/worker-invocation.js +24 -291
  71. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  72. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  73. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  74. package/dist/src/storage/backends/fortemi.js +33 -29
  75. package/dist/src/storage/backends/logseq.js +16 -3
  76. package/dist/src/storage/backends/postgres.js +74 -33
  77. package/dist/src/storage/backends/postgrest.js +37 -13
  78. package/dist/src/storage/config.js +6 -1
  79. package/dist/src/uhp/config.js +4 -0
  80. package/package.json +9 -1
  81. package/schemas/mission-v1.schema.json +20 -19
  82. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  83. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  84. package/tools/agents/providers/claude.mjs +3 -30
  85. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  86. package/tools/agents/providers/codex.mjs +2 -8
  87. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  88. package/tools/agents/providers/rule-delivery.mjs +3 -16
  89. package/tools/agents/providers/rule-presentation.mjs +38 -0
  90. package/tools/plugin/package-plugins.mjs +1 -1
  91. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  92. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  93. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  94. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  95. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  96. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  97. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  98. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  99. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  100. package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
  101. package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
  102. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  103. package/tools/ralph-external/session-launcher.mjs +73 -217
  104. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -1,3 +1,10 @@
1
+ import { normalizeAiwgConfigCandidate } from './config-normalization.js';
2
+ import { atomicConfigWrite, withWorkerWriteLock } from './worker-write.js';
3
+ import { workerConfigDigest } from './worker-config-digest.js';
4
+ export { workerConfigDigest } from './worker-config-digest.js';
5
+ import { validateParallelismConfig } from './worker-parallelism.js';
6
+ export { validateParallelismConfig, validateWorkerHealthConfig, resolveWorkerGroupInstallation } from './worker-parallelism.js';
7
+ import { expandWorkerGroupTemplates } from './worker-group-templates.js';
1
8
  import { validateDecisionConfig } from './decision-config.js';
2
9
  /**
3
10
  * Project-Level AIWG Config
@@ -10,20 +17,15 @@ import { validateDecisionConfig } from './decision-config.js';
10
17
  * @implements #621
11
18
  */
12
19
  import { validateImportExclusions } from '../sessions/import-exclusions.js';
13
- import { resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
14
20
  export { DEFAULT_DISPATCH_HOST_THRESHOLDS, resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
15
21
  import { validateIndexGraphOverrides } from './index-graph-overrides.js';
16
22
  import { DEFAULT_RULES_INLINE_BUDGET_TOKENS } from '../rules/inline-budget.mjs';
17
23
  export { validateRulesConfig, resolveRulesInlineBudget } from '../rules/inline-budget.mjs';
18
24
  import { loadRulePinConfigs, installedRuleCapabilities, validateRulePinCapabilities } from '../rules/pins.mjs';
19
- import Ajv from 'ajv';
20
- import addFormats from 'ajv-formats';
21
25
  import { readFileSync, existsSync } from 'node:fs';
22
- import { fileURLToPath } from 'node:url';
23
- import { isCanonicalWorkerModelKey, loadProviderModelCapabilities, resolveWorkerInstallation } from '../models/provider-policy.js';
24
- import { readFile, writeFile, mkdir, access, readdir, rename, unlink } from 'fs/promises';
25
- import { createHash, randomBytes } from 'crypto';
26
- import { resolve, join, dirname, isAbsolute } from 'path';
26
+ import { readFile, access, readdir } from 'fs/promises';
27
+ import { createHash } from 'crypto';
28
+ import { resolve, join, isAbsolute } from 'path';
27
29
  import { normalizeNamedCaptures } from '../artifacts/index-builder.js';
28
30
  import { getProviderDefinition, getProviderKernelSkillPath, PROVIDER_IDS, resolveProviderPathValue, } from '../providers/provider-definitions.js';
29
31
  import { validateAuthorization, } from '../policy/authorization.js';
@@ -175,142 +177,22 @@ export function resolveDelivery(delivery) {
175
177
  issue_comment_on_cycle: delivery?.issue_comment_on_cycle ?? true,
176
178
  };
177
179
  }
178
- let parallelismValidator;
179
- /** Offline shape and declaration validation. Qualification is checked at use time. */
180
- export function validateParallelismConfig(value) {
181
- if (value === undefined)
182
- return [];
183
- if (!parallelismValidator) {
184
- const here = dirname(fileURLToPath(import.meta.url));
185
- const candidates = [resolve(here, '../../vscode-extension/schemas/aiwg.config.v1.json'), resolve(here, '../../../vscode-extension/schemas/aiwg.config.v1.json')];
186
- const file = candidates.find(existsSync);
187
- if (!file)
188
- throw new Error('Parallelism schema is unavailable');
189
- const schema = JSON.parse(readFileSync(file, 'utf8'));
190
- const ajv = addFormats(new Ajv({ strict: true, allErrors: true }));
191
- parallelismValidator = ajv.compile({ ...schema.properties.parallelism, definitions: schema.definitions });
192
- }
193
- if (!parallelismValidator(value))
194
- return (parallelismValidator.errors ?? []).map(error => `OG_CONFIG_REFERENCE /parallelism${error.instancePath}: ${error.message}`);
195
- const policy = value;
196
- const errors = [];
197
- const error = (pointer, message) => { errors.push(`OG_CONFIG_REFERENCE /parallelism/${pointer}: ${message}`); };
198
- const builtinIds = new Set(PROVIDER_IDS.filter(id => id !== 'generic'));
199
- for (const [id, installation] of Object.entries(policy.clis ?? {})) {
200
- if (installation.adapter.startsWith('providers:') && !builtinIds.has(installation.adapter.slice(10)))
201
- error(`clis/${id}/adapter`, 'Unknown provider binding');
202
- }
203
- for (const [id, group] of Object.entries(policy.groups ?? {})) {
204
- if (group.execution?.launchMode === 'native-sandbox'
205
- && (!['read-only', 'workspace-write'].includes(group.execution.sandbox ?? '')
206
- || (group.execution.transport ?? 'shell') !== 'shell')) {
207
- error(`groups/${id}/execution/launchMode`, 'native-sandbox requires an enforcing read-only or workspace-write shell sandbox');
208
- }
209
- if (group.execution?.launchMode === 'native-sandbox') {
210
- if (group.execution.approval === 'on-request')
211
- error(`groups/${id}/execution/approval`, 'native-sandbox requires approval="never"');
212
- if (group.execution.network === 'allowlisted')
213
- error(`groups/${id}/execution/network`, 'native-sandbox requires network="deny"');
214
- }
215
- const ids = new Set();
216
- for (const member of group.members) {
217
- if (ids.has(member.id))
218
- error(`groups/${id}/members`, 'Duplicate member ID');
219
- ids.add(member.id);
220
- if (!policy.clis?.[member.cli] && !builtinIds.has(member.cli))
221
- error(`groups/${id}/members/${member.id}/cli`, 'Unknown installation');
222
- if (group.execution?.launchMode === 'native-sandbox'
223
- && (policy.clis?.[member.cli]?.adapter ?? `providers:${member.cli}`) !== 'providers:codex') {
224
- error(`groups/${id}/members/${member.id}/cli`, 'native-sandbox requires a codex adapter');
225
- }
226
- }
227
- for (const member of group.fallback ?? [])
228
- if (!ids.has(member))
229
- error(`groups/${id}/fallback`, 'Unknown fallback member');
230
- for (const role of group.requiredRoles ?? []) {
231
- if (!group.members.some(m => m.enabled !== false && policy.clis?.[m.cli]?.enabled !== false && m.roles.includes(role)))
232
- error(`groups/${id}/requiredRoles`, `No enabled member covers ${role}`);
233
- }
234
- }
235
- const admission = policy.admission;
236
- for (const group of Object.keys(admission?.groupCaps ?? {}))
237
- if (!policy.groups?.[group])
238
- error('admission/groupCaps', 'Unknown group');
239
- for (const cli of Object.keys(admission?.cliCaps ?? {}))
240
- if (!policy.clis?.[cli] && !builtinIds.has(cli))
241
- error('admission/cliCaps', 'Unknown installation');
242
- for (const key of Object.keys(admission?.memberCaps ?? {})) {
243
- const [group, member, extra] = key.split('/');
244
- if (extra !== undefined || !policy.groups?.[group]?.members.some(m => m.id === member))
245
- error('admission/memberCaps', 'Unknown group/member');
246
- }
247
- for (const key of Object.keys(admission?.modelCaps ?? {}))
248
- if (!isCanonicalWorkerModelKey(key))
249
- error('admission/modelCaps', 'Noncanonical inference model key');
250
- if (admission?.hostResources !== undefined) {
251
- try {
252
- resolveDispatchHostThresholds(undefined, admission.hostResources);
253
- }
254
- catch (validation) {
255
- error('admission/hostResources', validation instanceof Error ? validation.message : String(validation));
256
- }
257
- }
258
- errors.push(...validateWorkerHealthConfig(policy.health));
259
- return errors;
260
- }
261
- /** Cross-field health preferences are validated offline; controls are qualified at dispatch. */
262
- export function validateWorkerHealthConfig(health) {
263
- if (health === undefined)
264
- return [];
265
- const errors = [];
266
- const error = (field, message) => { errors.push(`OG_CONFIG_REFERENCE /parallelism/health/${field}: ${message}`); };
267
- const checkSeconds = health.checkSeconds ?? 15;
268
- if (checkSeconds > (health.startupSeconds ?? 60))
269
- error('checkSeconds', 'Must not exceed startupSeconds');
270
- if ((health.quietSeconds ?? 90) >= (health.stallSeconds ?? 300))
271
- error('quietSeconds', 'Must be less than stallSeconds');
272
- if ((health.cooldownSeconds ?? 120) > (health.maxCooldownSeconds ?? 900))
273
- error('cooldownSeconds', 'Must not exceed maxCooldownSeconds');
274
- for (const role of Object.keys(health.roleScale ?? {})) {
275
- if (!['efficiency', 'coding', 'reasoning'].includes(role))
276
- error('roleScale', `Unknown role ${role}`);
277
- }
278
- for (const field of ['modelScale', 'modelStartupSeconds']) {
279
- for (const [key, value] of Object.entries(health[field] ?? {})) {
280
- if (!isCanonicalWorkerModelKey(key))
281
- error(field, 'Noncanonical inference model key');
282
- if (field === 'modelStartupSeconds' && checkSeconds > value)
283
- error(field, 'Model startup must cover checkSeconds');
284
- }
285
- }
286
- return errors;
287
- }
288
- /** Resolve one enabled installation through trusted metadata, without probes.
289
- * Implicit canonical IDs inherit current-account auth; no account/quota identity
290
- * is invented. Profile qualification remains the invocation owner's obligation.
180
+ /**
181
+ * Provider-scoped parallelism cap — limits how many concurrent subagents,
182
+ * Ralph loops, and Mission Control missions agents may spawn. Designed to
183
+ * keep AIWG within the rate-limit envelope of the underlying model provider
184
+ * (Anthropic per-key TPM/RPM caps are the most-reported trigger).
185
+ *
186
+ * Composes with (effective limit = MIN of):
187
+ * - `parallelism.max_parallel_subagents` (this config)
188
+ * - `context-budget` rule's `AIWG_CONTEXT_WINDOW`-derived cap, if set
189
+ * - `rlm-context-management` Rule 8's 7-agent hard cap (RLM dispatches only)
190
+ * - The natural task decomposition (no point spawning 4 when only 2 subtasks exist)
191
+ *
192
+ * Every field is optional. Defaults applied via {@link resolveParallelism}.
193
+ *
194
+ * @implements #1359
291
195
  */
292
- export function resolveWorkerGroupInstallation(parallelism, installationId, registry = loadProviderModelCapabilities()) {
293
- const errors = validateParallelismConfig(parallelism);
294
- if (errors.length)
295
- throw new Error(errors.join('; '));
296
- if (!parallelism.worker_dispatch)
297
- throw new Error('OG_CONFIG_REFERENCE /parallelism/worker_dispatch: Worker dispatch is disabled');
298
- const explicit = parallelism.clis?.[installationId];
299
- const installation = explicit
300
- ? structuredClone(explicit)
301
- : { adapter: `providers:${installationId}`, enabled: true };
302
- if (!explicit && (!PROVIDER_IDS.includes(installationId) || installationId === 'generic')) {
303
- throw new Error(`OG_CONFIG_REFERENCE /parallelism/clis/${installationId}: Unknown canonical installation`);
304
- }
305
- if (installation.enabled === false)
306
- throw new Error(`OG_CONFIG_REFERENCE /parallelism/clis/${installationId}: Installation is disabled`);
307
- try {
308
- return { installation: { ...installation, enabled: true }, ...resolveWorkerInstallation(registry, installation) };
309
- }
310
- catch (error) {
311
- throw new Error(`OG_ADAPTER_UNQUALIFIED /parallelism/clis/${installationId}: ${error}`);
312
- }
313
- }
314
196
  /** Validate current model restrictions and group references before any write.
315
197
  * Disabled/unbound projects retain their existing config-only edit behavior.
316
198
  * This is shared validation, not Tier 1 coherent activation.
@@ -329,17 +211,6 @@ export async function validateWorkerConfigCandidate(projectDir, config, options)
329
211
  const { ConfigLoader } = await import('../models/config-loader.js');
330
212
  await new ConfigLoader(projectDir, options).load(config.parallelism);
331
213
  }
332
- /** Hash canonical JSON, treating absent optional model files as null. */
333
- export function workerConfigDigest(value) {
334
- const canonical = (node) => {
335
- if (Array.isArray(node))
336
- return node.map(canonical);
337
- if (node !== null && typeof node === 'object')
338
- return Object.fromEntries(Object.keys(node).sort().filter(key => node[key] !== undefined).map(key => [key, canonical(node[key])]));
339
- return node ?? null;
340
- };
341
- return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`;
342
- }
343
214
  export function workerActivationDigests(config, projectModels, userModels, builtinModels) {
344
215
  const candidate = structuredClone(config);
345
216
  if (candidate.parallelism)
@@ -415,6 +286,10 @@ export function getProviderParallelismDefaults(provider) {
415
286
  * @implements #1359
416
287
  */
417
288
  export function resolveParallelism(parallelism, primaryProvider) {
289
+ const expansion = expandWorkerGroupTemplates(parallelism);
290
+ if (expansion.errors.length)
291
+ throw new Error(expansion.errors.join('; '));
292
+ parallelism = expansion.parallelism;
418
293
  const defaults = getProviderParallelismDefaults(primaryProvider);
419
294
  const resolved = {
420
295
  max_parallel_subagents: parallelism?.max_parallel_subagents ?? defaults.max_parallel_subagents,
@@ -441,7 +316,7 @@ export function resolveParallelism(parallelism, primaryProvider) {
441
316
  for (const cli of Object.values(resolved.clis ?? {}))
442
317
  cli.enabled ??= true;
443
318
  if (resolved.admission)
444
- resolved.admission = { maxQueued: 128, queueTimeoutSeconds: 300, leaseSeconds: 60, authorityRef: 'project', ...resolved.admission };
319
+ resolved.admission = { maxQueued: 128, queueTimeoutSeconds: 300, leaseSeconds: 60, starvationSeconds: 120, authorityRef: 'project', ...resolved.admission };
445
320
  if (resolved.budgets)
446
321
  resolved.budgets = { enforcement: 'observed', unknownCost: 'deny', ...resolved.budgets };
447
322
  if (resolved.retry)
@@ -1071,12 +946,7 @@ export async function readAiwgConfigCandidate(projectDir) {
1071
946
  const content = await readFile(filePath, 'utf-8');
1072
947
  const parsed = JSON.parse(content);
1073
948
  // Ensure required fields exist (forward-compat)
1074
- if (!parsed.providers)
1075
- parsed.providers = ['claude'];
1076
- if (!parsed.installed)
1077
- parsed.installed = {};
1078
- if (!parsed.scripts)
1079
- parsed.scripts = {};
949
+ normalizeAiwgConfigCandidate(parsed);
1080
950
  if (parsed.sessions !== undefined && (parsed.sessions === null || typeof parsed.sessions !== 'object' || Array.isArray(parsed.sessions)
1081
951
  || (parsed.sessions.codex_roots !== undefined && (!Array.isArray(parsed.sessions.codex_roots)
1082
952
  || !parsed.sessions.codex_roots.every((root) => typeof root === 'string' && root.trim().length > 0))))) {
@@ -1164,6 +1034,9 @@ export async function readAiwgConfig(projectDir, options) {
1164
1034
  * sync so split-root health remains deterministic.
1165
1035
  */
1166
1036
  export async function writeAiwgConfig(projectDir, config) {
1037
+ return withWorkerWriteLock(projectDir, () => writeAiwgConfigUnlocked(projectDir, config));
1038
+ }
1039
+ async function writeAiwgConfigUnlocked(projectDir, config) {
1167
1040
  // Uninstall is an explicit capability change. Retire project selectors for
1168
1041
  // removed capabilities before strict validation, and tell the operator.
1169
1042
  const previousFile = getConfigPath(projectDir);
@@ -1222,28 +1095,7 @@ export async function writeAiwgConfig(projectDir, config) {
1222
1095
  const artifactDir = resolveProjectAiwgDir(projectDir);
1223
1096
  const artifactPath = join(artifactDir, CONFIG_FILENAME);
1224
1097
  const content = JSON.stringify(config, null, 2) + '\n';
1225
- const writeAtomic = async (filePath) => {
1226
- await mkdir(dirname(filePath), { recursive: true });
1227
- // Atomic write: emit to a temp sibling, fsync-ish via rename. Prevents a
1228
- // crash or kill mid-write from corrupting the config file. Rename is
1229
- // atomic on POSIX and on NTFS when both paths are on the same volume.
1230
- // The random suffix avoids collisions if two concurrent writers run.
1231
- const tmpPath = `${filePath}.${randomBytes(6).toString('hex')}.tmp`;
1232
- try {
1233
- await writeFile(tmpPath, content, 'utf-8');
1234
- await rename(tmpPath, filePath);
1235
- }
1236
- catch (err) {
1237
- // Best-effort cleanup of the temp file on failure, ignoring ENOENT.
1238
- try {
1239
- await unlink(tmpPath);
1240
- }
1241
- catch {
1242
- /* ignore */
1243
- }
1244
- throw err;
1245
- }
1246
- };
1098
+ const writeAtomic = (file) => atomicConfigWrite(file, content);
1247
1099
  await writeAtomic(localPath);
1248
1100
  if (artifactPath !== localPath) {
1249
1101
  try {
@@ -1454,6 +1306,7 @@ export function validateWorkerGroupReferences(parallelism, selection, qualifiedR
1454
1306
  const errors = validateParallelismConfig(parallelism);
1455
1307
  if (errors.length)
1456
1308
  return errors;
1309
+ parallelism = expandWorkerGroupTemplates(parallelism).parallelism;
1457
1310
  for (const [role, guidance] of Object.entries(selection.roleGuidance ?? {})) {
1458
1311
  for (const group of guidance.groups)
1459
1312
  if (!parallelism.groups?.[group])
@@ -1467,9 +1320,10 @@ export function validateWorkerGroupReferences(parallelism, selection, qualifiedR
1467
1320
  /** Structured qualification gaps retain role scope for readiness projections. */
1468
1321
  export function validateWorkerGroupQualifications(parallelism, qualifiedRoles) {
1469
1322
  const diagnostics = [];
1323
+ parallelism = expandWorkerGroupTemplates(parallelism).parallelism;
1470
1324
  for (const [id, group] of Object.entries(parallelism.groups ?? {})) {
1471
1325
  for (const role of group.requiredRoles ?? []) {
1472
- if (!group.members.some(m => m.enabled !== false && parallelism.clis?.[m.cli]?.enabled !== false
1326
+ if (!(group.members ?? []).some(m => m.enabled !== false && parallelism.clis?.[m.cli]?.enabled !== false
1473
1327
  && m.roles.includes(role) && qualifiedRoles[`${id}/${m.id}`]?.includes(role))) {
1474
1328
  diagnostics.push({ code: 'OG_ADAPTER_UNQUALIFIED', pointer: `/parallelism/groups/${id}/requiredRoles`,
1475
1329
  message: `No qualified route covers ${role}`, roles: [role] });
@@ -0,0 +1,54 @@
1
+ // Documents the existing config commands and reviewed worker maintenance flags.
2
+ export function printUsage() {
3
+ console.log(`Usage: aiwg config <subcommand> [options]
4
+
5
+ Subcommands:
6
+ get <key> Read a user config value
7
+ get --project <key> Read a project config value or merged defaults.workerSelection
8
+ [--origins] Include model policy sources, pointers and input digests
9
+ set <key> <value> Write a user config value
10
+ set --project <key> <value> Write a project config value (validates enums)
11
+ list Show all user config
12
+ show --project Show resolved project config and artifact root
13
+ validate Validate user config files
14
+ validate --project Validate resolved project config taxonomy/index semantics
15
+ [--provider gitea|github|local] [--available-label NAME ...]
16
+ reset [<key>] Reset key or all config to defaults
17
+ path Print config directory path
18
+ edit Open config in $EDITOR/$VISUAL (requires a terminal)
19
+ edit --print-path Print the config file path instead of opening an editor
20
+ gitignore Show/check/fix .gitignore AIWG entries
21
+
22
+ Reviewed worker policy pair (project scope, no prompts):
23
+ set --project --parallelism FILE --worker-selection FILE --propose FILE
24
+ set --project --apply FILE --authorize DIGEST
25
+ set --project --apply FILE --standing-authorization FILE
26
+ set --project --recover --authorize DIGEST
27
+ --target DIRECTORY selects the project. Proposal inputs are JSON files.
28
+
29
+ Global flags:
30
+ --config-dir <path> Override config directory
31
+
32
+ Examples:
33
+ aiwg config get defaults.provider
34
+ aiwg config set defaults.verbosity quiet
35
+ aiwg config set updates.channel next
36
+ aiwg config list
37
+ aiwg config show --project
38
+ aiwg config show --project --json
39
+ aiwg config get --project delivery.mode
40
+ aiwg config set --project delivery.mode pr-required
41
+ aiwg config set --project delivery.merge_style squash
42
+ aiwg config get --project parallelism
43
+ aiwg config get --project defaults.workerSelection --origins
44
+ aiwg config set --project parallelism.max_parallel_subagents 6
45
+ aiwg config reset --project parallelism
46
+ aiwg config validate
47
+ aiwg config path
48
+ aiwg config reset defaults.provider
49
+ aiwg config --config-dir /custom/path list
50
+ aiwg config gitignore
51
+ aiwg config gitignore --fix
52
+ aiwg config gitignore --check`);
53
+ }
54
+ //# sourceMappingURL=cli-help.js.map
@@ -1,3 +1,4 @@
1
+ import { printUsage } from './cli-help.js';
1
2
  /**
2
3
  * Config CLI — Subcommand router for `aiwg config`
3
4
  *
@@ -19,8 +20,8 @@
19
20
  */
20
21
  import { fileURLToPath } from 'url';
21
22
  import path from 'path';
22
- import { isDeepStrictEqual } from 'node:util';
23
- import { normalizeProviderDefinitionId } from '../providers/provider-definitions.js';
23
+ import { warnStaleParallelism } from './worker-guidance-warning.js';
24
+ import { handleWorkerAdoptionArgs } from './worker-adoption-cli.js';
24
25
  import { UserConfig } from './user-config.js';
25
26
  import { AiwgError, EXIT_CODES } from '../cli/errors.js';
26
27
  import { isInteractive } from '../cli/env.js';
@@ -28,32 +29,12 @@ import { splitParams } from '../cli/agent-spawn.js';
28
29
  import { exitCodeForSpawnResult } from '../cli/child-process-result.js';
29
30
  import { projectControlPath, resolveProjectAiwgDir } from './project-artifacts.js';
30
31
  const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
31
- function warnStaleParallelism(cfg, projectDir, key, before) {
32
- if (key !== 'parallelism' && !key.startsWith('parallelism.'))
33
- return;
34
- if (isDeepStrictEqual(before, getDottedPath(cfg, key)))
35
- return;
36
- const configured = (cfg.providers ?? []).map(normalizeProviderDefinitionId).filter((id) => !!id);
37
- const recorded = Object.values(cfg.installed ?? {}).flatMap(entry => Object.keys(entry.deployedTo ?? {}))
38
- .map(normalizeProviderDefinitionId).filter((id) => !!id).sort();
39
- const providers = [...new Set([...configured, ...recorded])];
40
- const targets = providers.length ? providers : ['<provider>'];
41
- const lines = [
42
- `WARNING: ${key} changed project configuration; generated workspace/provider guidance may now be stale.`,
43
- `From project root ${path.resolve(projectDir)}, preview and refresh every configured or recorded provider context:`,
44
- ...targets.flatMap(provider => [
45
- ` aiwg regenerate --workspace --provider ${provider} --dry-run`,
46
- ` aiwg regenerate --workspace --provider ${provider}`,
47
- ]),
48
- ' aiwg workspace-context doctor',
49
- `Shared parallelism defaults and labels use primary provider '${cfg.providers?.[0] ?? 'unknown'}' (providers[0]); --provider selects the adapter/context target.`,
50
- ];
51
- console.error(lines.join('\n'));
52
- }
53
32
  /**
54
33
  * Main CLI entry point for `aiwg config <subcommand> [args]`
55
34
  */
56
35
  export async function main(args) {
36
+ if (await handleWorkerAdoptionArgs(args, process.cwd(), 'config'))
37
+ return;
57
38
  // Extract --config-dir flag before routing
58
39
  let configDir;
59
40
  const filteredArgs = [];
@@ -910,51 +891,6 @@ async function handleGitignore(args) {
910
891
  process.exit(result.status ?? 1);
911
892
  }
912
893
  }
913
- function printUsage() {
914
- console.log(`Usage: aiwg config <subcommand> [options]
915
-
916
- Subcommands:
917
- get <key> Read a user config value
918
- get --project <key> Read a project config value or merged defaults.workerSelection
919
- [--origins] Include model policy sources, pointers and input digests
920
- set <key> <value> Write a user config value
921
- set --project <key> <value> Write a project config value (validates enums)
922
- list Show all user config
923
- show --project Show resolved project config and artifact root
924
- validate Validate user config files
925
- validate --project Validate resolved project config taxonomy/index semantics
926
- [--provider gitea|github|local] [--available-label NAME ...]
927
- reset [<key>] Reset key or all config to defaults
928
- path Print config directory path
929
- edit Open config in $EDITOR/$VISUAL (requires a terminal)
930
- edit --print-path Print the config file path instead of opening an editor
931
- gitignore Show/check/fix .gitignore AIWG entries
932
-
933
- Global flags:
934
- --config-dir <path> Override config directory
935
-
936
- Examples:
937
- aiwg config get defaults.provider
938
- aiwg config set defaults.verbosity quiet
939
- aiwg config set updates.channel next
940
- aiwg config list
941
- aiwg config show --project
942
- aiwg config show --project --json
943
- aiwg config get --project delivery.mode
944
- aiwg config set --project delivery.mode pr-required
945
- aiwg config set --project delivery.merge_style squash
946
- aiwg config get --project parallelism
947
- aiwg config get --project defaults.workerSelection --origins
948
- aiwg config set --project parallelism.max_parallel_subagents 6
949
- aiwg config reset --project parallelism
950
- aiwg config validate
951
- aiwg config path
952
- aiwg config reset defaults.provider
953
- aiwg config --config-dir /custom/path list
954
- aiwg config gitignore
955
- aiwg config gitignore --fix
956
- aiwg config gitignore --check`);
957
- }
958
894
  /** Same configuration owner and validation used by the rules command wrappers. */
959
895
  export async function setRuleConfigKey(cwd, source, key, raw, userConfigDir) {
960
896
  assertSafeConfigPath(key);
@@ -0,0 +1,10 @@
1
+ export function normalizeAiwgConfigCandidate(parsed) {
2
+ if (!parsed.providers)
3
+ parsed.providers = ['claude'];
4
+ if (!parsed.installed)
5
+ parsed.installed = {};
6
+ if (!parsed.scripts)
7
+ parsed.scripts = {};
8
+ return parsed;
9
+ }
10
+ //# sourceMappingURL=config-normalization.js.map
@@ -0,0 +1,26 @@
1
+ // Rechecks the persistent apply block at managed admission, including already-created schedulers.
2
+ import { normalizeAiwgConfigCandidate } from './config-normalization.js';
3
+ import { readFileSync } from 'node:fs';
4
+ import { getConfigPath, workerActivationDigests, workerConfigDigest } from './aiwg-config.js';
5
+ import { workerDispatchBlocked } from './worker-write.js';
6
+ export function assertWorkerActivationCurrent(project, expected) {
7
+ if (workerDispatchBlocked(project))
8
+ throw new Error('Managed dispatch is closed for worker policy apply/recovery');
9
+ if (expected) {
10
+ let raw;
11
+ try {
12
+ raw = readFileSync(getConfigPath(project), 'utf8');
13
+ }
14
+ catch (error) {
15
+ // Fail closed: an admission bound to an activation cannot be verified without the project config.
16
+ if (error.code === 'ENOENT')
17
+ throw new Error(`Managed activation cannot be verified: ${getConfigPath(project)} is missing`);
18
+ throw error;
19
+ }
20
+ const config = normalizeAiwgConfigCandidate(JSON.parse(raw));
21
+ if (workerConfigDigest(config.parallelism?.activation) !== workerConfigDigest(expected)
22
+ || workerActivationDigests(config).configDigest !== expected.configDigest)
23
+ throw new Error('Managed activation changed; reload the current config and admission binding');
24
+ }
25
+ }
26
+ //# sourceMappingURL=worker-activation-gate.js.map
@@ -0,0 +1,102 @@
1
+ // Adds headless proposal/review/apply flags to the existing config and models setters.
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { atomicConfigWrite, workerWriteLock } from './worker-write.js';
5
+ export async function handleWorkerAdoptionArgs(args, cwd, owner, signal, dryRun = false) {
6
+ args = args.flatMap(arg => {
7
+ const equals = arg.startsWith('--') ? arg.indexOf('=') : -1;
8
+ return equals > 0 ? [arg.slice(0, equals), arg.slice(equals + 1)] : [arg];
9
+ });
10
+ if (!args.some(arg => ['--propose', '--apply', '--authorize', '--standing-authorization', '--parallelism', '--worker-selection', '--recover'].includes(arg)))
11
+ return false;
12
+ if (dryRun)
13
+ throw new Error('Reviewed maintenance cannot write in dry-run mode; use --propose explicitly to create a review');
14
+ if (!['set', 'set-default'].includes(args[0]) || (owner === 'config' && args[0] !== 'set'))
15
+ throw new Error('Reviewed adoption flags are supported only on existing config/models setters; refresh cannot authorize or apply preferences');
16
+ const { applyWorkerAdoption, proposeWorkerAdoption, recoverWorkerAdoption } = await import('./worker-adoption.js');
17
+ const flags = new Map();
18
+ const positionals = [];
19
+ const values = new Set(['--target', '--propose', '--apply', '--authorize', '--standing-authorization', '--parallelism', '--worker-selection']);
20
+ for (let i = 1; i < args.length; i++) {
21
+ const arg = args[i];
22
+ if (values.has(arg)) {
23
+ if (!args[i + 1] || args[i + 1].startsWith('--') || flags.has(arg))
24
+ throw new Error(`A single value is required for ${arg}`);
25
+ flags.set(arg, args[++i]);
26
+ }
27
+ else if (arg === '--project' || arg === '--json' || arg === '--recover')
28
+ continue;
29
+ else if (arg.startsWith('--'))
30
+ throw new Error(`Unsupported reviewed adoption flag: ${arg}`);
31
+ else
32
+ positionals.push(arg);
33
+ }
34
+ const project = resolve(flags.get('--target') ?? cwd);
35
+ const readJson = async (file) => JSON.parse(await readFile(resolve(cwd, file), 'utf8'));
36
+ if (args.includes('--recover')) {
37
+ if (flags.has('--apply') || flags.has('--propose') || flags.has('--parallelism') || flags.has('--worker-selection') || positionals.length)
38
+ throw new Error('Recovery accepts only authorization flags');
39
+ if (flags.has('--authorize') === flags.has('--standing-authorization'))
40
+ throw new Error('Recovery requires exactly one authorization');
41
+ const authority = flags.has('--authorize') ? { digest: flags.get('--authorize') } : await readJson(flags.get('--standing-authorization'));
42
+ await recoverWorkerAdoption(project, authority, { signal });
43
+ console.log('Previous validated policy pair restored; guidance regenerated.');
44
+ return true;
45
+ }
46
+ if (flags.has('--apply')) {
47
+ if (flags.has('--propose') || flags.has('--parallelism') || flags.has('--worker-selection') || positionals.length)
48
+ throw new Error('Apply accepts only a reviewed proposal and authorization');
49
+ if (flags.has('--authorize') === flags.has('--standing-authorization'))
50
+ throw new Error('Supply exactly one of --authorize DIGEST or --standing-authorization FILE');
51
+ const proposal = await readJson(flags.get('--apply'));
52
+ const authority = flags.has('--authorize') ? { digest: flags.get('--authorize') } : await readJson(flags.get('--standing-authorization'));
53
+ await applyWorkerAdoption(project, proposal, authority, { signal });
54
+ console.log(JSON.stringify({ applied: proposal.digest, revision: proposal.after.parallelism?.worker_mode === 'managed' ? proposal.baseRevision + 1 : null }));
55
+ return true;
56
+ }
57
+ if (!flags.has('--propose') || flags.has('--authorize') || flags.has('--standing-authorization'))
58
+ throw new Error('Use --propose FILE to review first, then --apply FILE with separate authorization');
59
+ const changes = {};
60
+ if (flags.has('--parallelism'))
61
+ changes.parallelism = await readJson(flags.get('--parallelism'));
62
+ if (flags.has('--worker-selection'))
63
+ changes.workerSelection = await readJson(flags.get('--worker-selection'));
64
+ if (positionals.length) {
65
+ const [key, raw] = positionals;
66
+ const parts = key.split('.');
67
+ if (owner !== 'config' || parts[0] !== 'parallelism' || positionals.length !== 2 || flags.has('--parallelism')
68
+ || parts.some(part => !part || ['__proto__', 'prototype', 'constructor', 'activation'].includes(part)))
69
+ throw new Error('Reviewed edits support safe parallelism fields; model preferences use --worker-selection FILE');
70
+ let value;
71
+ try {
72
+ value = JSON.parse(raw);
73
+ }
74
+ catch {
75
+ value = raw;
76
+ }
77
+ if (parts.length === 1)
78
+ changes.parallelism = value;
79
+ else {
80
+ const base = await proposeWorkerAdoption(project, {});
81
+ const parallelism = structuredClone(base.before.parallelism ?? {});
82
+ let node = parallelism;
83
+ for (const part of parts.slice(1, -1)) {
84
+ if (!Object.hasOwn(node, part) || !node[part] || typeof node[part] !== 'object')
85
+ node[part] = {};
86
+ node = node[part];
87
+ }
88
+ node[parts.at(-1)] = value;
89
+ changes.parallelism = parallelism;
90
+ }
91
+ }
92
+ if (!Object.keys(changes).length)
93
+ throw new Error('Proposal requires --parallelism FILE or --worker-selection FILE');
94
+ const proposal = await proposeWorkerAdoption(project, changes, { signal });
95
+ const file = resolve(cwd, flags.get('--propose'));
96
+ if (proposal.inputs.some(input => input.file === file) || file.startsWith(workerWriteLock(project)))
97
+ throw new Error('Proposal output must be separate from activation inputs and recovery files');
98
+ await atomicConfigWrite(file, JSON.stringify(proposal, null, 2) + '\n');
99
+ console.log(JSON.stringify(proposal, null, 2));
100
+ return true;
101
+ }
102
+ //# sourceMappingURL=worker-adoption-cli.js.map