@aiwg/cli 2026.10.9 → 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 (91) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +197 -3
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/cli/handlers/mc-admission.js +39 -0
  4. package/dist/src/cli/handlers/mc.js +2 -31
  5. package/dist/src/cli/handlers/models-help.js +52 -0
  6. package/dist/src/cli/handlers/models.js +11 -81
  7. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  8. package/dist/src/cli/handlers/use.js +191 -230
  9. package/dist/src/cli/ui.js +1 -0
  10. package/dist/src/config/aiwg-config.js +38 -184
  11. package/dist/src/config/cli-help.js +54 -0
  12. package/dist/src/config/cli.js +5 -69
  13. package/dist/src/config/config-normalization.js +10 -0
  14. package/dist/src/config/worker-activation-gate.js +26 -0
  15. package/dist/src/config/worker-adoption-cli.js +102 -0
  16. package/dist/src/config/worker-adoption.js +368 -0
  17. package/dist/src/config/worker-config-digest.js +14 -0
  18. package/dist/src/config/worker-group-templates.js +150 -0
  19. package/dist/src/config/worker-guidance-warning.js +28 -0
  20. package/dist/src/config/worker-owner-lock.js +152 -0
  21. package/dist/src/config/worker-parallelism.js +154 -0
  22. package/dist/src/config/worker-write.js +75 -0
  23. package/dist/src/decision/telemetry/exporter.js +2 -1
  24. package/dist/src/decision/telemetry/index.js +1 -0
  25. package/dist/src/decision/telemetry/mapping.js +43 -0
  26. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  27. package/dist/src/decision/telemetry/redaction.js +6 -1
  28. package/dist/src/decision/telemetry/types.js +3 -0
  29. package/dist/src/decision/telemetry/worker.js +69 -0
  30. package/dist/src/lint/rule-activation.js +15 -7
  31. package/dist/src/lint/runner.js +2 -2
  32. package/dist/src/mcp/server.mjs +2 -11
  33. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  34. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  35. package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
  36. package/dist/src/mission-protocol/codecs.js +8 -38
  37. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  38. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  39. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  40. package/dist/src/models/conductor-data-policy.mjs +190 -0
  41. package/dist/src/models/config-loader.js +19 -3
  42. package/dist/src/models/config-writer.js +27 -0
  43. package/dist/src/models/model-capabilities.v1.json +197 -3
  44. package/dist/src/models/provider-policy.js +7 -155
  45. package/dist/src/models/resolver.js +2 -293
  46. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  47. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  48. package/dist/src/models/worker-direct-readiness.js +9 -218
  49. package/dist/src/models/worker-health-readiness.js +105 -26
  50. package/dist/src/models/worker-installation.mjs +12 -14
  51. package/dist/src/models/worker-member-references.js +64 -0
  52. package/dist/src/models/worker-plan.js +317 -0
  53. package/dist/src/models/worker-selection-policy.js +217 -0
  54. package/dist/src/models/wrapper-deployment.js +1 -1
  55. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  56. package/dist/src/serve/shared-host-scheduler.js +25 -107
  57. package/dist/src/serve/shared-host-starvation.js +73 -0
  58. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  59. package/dist/src/serve/worker-artifacts.mjs +56 -0
  60. package/dist/src/serve/worker-command-request.js +68 -0
  61. package/dist/src/serve/worker-invocation-paths.js +144 -0
  62. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  63. package/dist/src/serve/worker-invocation.js +24 -291
  64. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  65. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  66. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  67. package/package.json +9 -1
  68. package/schemas/mission-v1.schema.json +20 -19
  69. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  70. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  71. package/tools/agents/providers/claude.mjs +3 -30
  72. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  73. package/tools/agents/providers/codex.mjs +2 -8
  74. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  75. package/tools/agents/providers/rule-delivery.mjs +3 -16
  76. package/tools/agents/providers/rule-presentation.mjs +38 -0
  77. package/tools/plugin/package-plugins.mjs +1 -1
  78. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  79. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  80. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  81. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  82. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  83. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  84. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  85. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  86. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  87. package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
  88. package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
  89. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  90. package/tools/ralph-external/session-launcher.mjs +73 -217
  91. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -1,14 +1,17 @@
1
+ import { WorkerRequestArgumentError } from './worker-command-request.js';
2
+ export { parseWorkerCommandArgs } from './worker-command-request.js';
1
3
  /** One nonlaunching typed renderer shared by models resolve and Ralph. */
2
4
  import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { constants } from 'node:fs';
5
- import { lstat, realpath, open } from 'node:fs/promises';
6
- import { fileURLToPath } from 'node:url';
5
+ import { inspectWorkerPath as inspectPath, workerOutputPaths, readBrief, assertConductorWorkerPlan } from './worker-invocation-paths.js';
6
+ export { inspectWorkerPath } from './worker-invocation-paths.js';
7
+ import { createHash, randomUUID } from 'node:crypto';
7
8
  import { resolveWorkerPlan } from '../models/resolver.js';
8
9
  import { workerConfigDigest, validateWorkerHealthConfig } from '../config/aiwg-config.js';
9
10
  import { assertValidWorkerPayload } from '../mission-protocol/codecs.js';
10
11
  import { qualificationDigest } from '../models/qualification-digest.mjs';
11
- import { qualifyWorkerInvocationProfile, isOperatorQualifiedWorkerAdapter, assertOperatorWorkerNativeArgv, assertWorkerSelectionAllowed, assertWorkerLaunchMode, WorkerRoleEffortError, requiredWorkerControls, requiresWorkerExternalBoundary, isCanonicalWorkerModelKey, assertWorkerHealthQualification, WorkerPolicyError, DEFAULT_WORKER_SELECTION } from '../models/provider-policy.js';
12
+ import { prepareWorkerInvocation } from './worker-invocation-preparation.js';
13
+ export { prepareWorkerInvocation, mcpToolSchemaDigest } from './worker-invocation-preparation.js';
14
+ import { isOperatorQualifiedWorkerAdapter, assertOperatorWorkerNativeArgv, assertWorkerLaunchMode, WorkerRoleEffortError, requiredWorkerControls, isCanonicalWorkerModelKey, WorkerPolicyError, DEFAULT_WORKER_SELECTION } from '../models/provider-policy.js';
12
15
  /** Frozen per-attempt windows; parent/transport ceilings include time already spent. */
13
16
  export function resolveWorkerEffectiveHealth(config, input) {
14
17
  const roleDefaults = { efficiency: 1, coding: 1, reasoning: 2 };
@@ -70,165 +73,6 @@ export function resolveWorkerEffectiveHealth(config, input) {
70
73
  assertValidWorkerPayload('workerEffectiveHealth', health);
71
74
  return health;
72
75
  }
73
- /**
74
- * The exact digest formula an MCP worker binding's `inputSchemaDigest`/
75
- * `outputSchemaDigest` are pinned against (used below and reused verbatim by
76
- * the #2892 MCP worker-invoke bridge's own live `tools/list` schema-drift
77
- * check — see `src/mcp/tools/worker-invoke.mjs` M3). Deliberately the raw
78
- * `JSON.stringify` of the schema object, NOT `workerConfigDigest`'s
79
- * canonicalized (sorted-key) form — a schema's own declared key order is
80
- * part of what gets pinned at qualification time.
81
- */
82
- export function mcpToolSchemaDigest(schema) {
83
- return `sha256:${createHash('sha256').update(JSON.stringify(schema)).digest('hex')}`;
84
- }
85
- export async function prepareWorkerInvocation(invocation, qualification) {
86
- const result = structuredClone(invocation);
87
- // shellCommand is derived syntax, never authority supplied by a brief.
88
- if (result.transport === 'shell')
89
- result.shellCommand = '';
90
- // Validate the established invocation shape first. Health is optional and
91
- // is checked after policy, installation and transport qualification below.
92
- const contract = { ...result };
93
- delete contract.health;
94
- assertValidWorkerPayload('workerResolvedInvocation', contract);
95
- if ((qualification.platform ?? process.platform) !== 'linux') {
96
- throw new Error('Unsupported worker invocation platform; Linux is required');
97
- }
98
- if (!Number.isFinite(qualification.now) || Date.parse(result.expiresAt) <= qualification.now) {
99
- throw new Error('Worker invocation is expired or has no validation clock');
100
- }
101
- if (qualification.adapter.id !== qualification.adapterId) {
102
- throw new Error('Worker invocation adapter identity mismatch');
103
- }
104
- assertWorkerLaunchMode(qualification.adapter, qualification.permissions);
105
- if (result.isolation !== undefined && result.isolation !== (qualification.permissions?.launchMode ?? 'systemd-unit')) {
106
- throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker launch mode differs from the execution policy');
107
- }
108
- for (const [key, value] of Object.entries(qualification.expectedDigests ?? {})) {
109
- if (result[key] !== value)
110
- throw new WorkerPolicyError('OG_PIN_MISMATCH', `Worker invocation ${key} changed`);
111
- }
112
- const verified = Date.parse(qualification.adapter.qualification.verifiedAt ?? '');
113
- const policyDays = qualification.policy?.freshness?.policyDays ?? DEFAULT_WORKER_SELECTION.freshness.policyDays;
114
- if (!Number.isFinite(verified) || verified > qualification.now || qualification.now - verified >= policyDays * 86400000) {
115
- throw new Error('Worker installation qualification is stale or invalid');
116
- }
117
- assertWorkerSelectionAllowed(result.selected, qualification);
118
- const profile = qualifyWorkerInvocationProfile(qualification.adapter, {
119
- profile: result.selected.profileRef, transport: result.transport,
120
- cliVersion: qualification.cliVersion,
121
- requiredControls: qualification.requiredControls, policy: qualification.policy,
122
- permissions: qualification.permissions, effort: result.selected.effort.value, boundary: qualification.boundary,
123
- mcp: qualification.mcp,
124
- });
125
- if (requiresWorkerExternalBoundary(qualification.adapter, { ...qualification,
126
- profile: result.selected.profileRef, transport: result.transport, effort: result.selected.effort.value })) {
127
- const installed = qualification.installation;
128
- const evidence = profile.installationQualification;
129
- if (!installed || !evidence || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef']
130
- .some(key => installed.evidence?.[key] !== evidence[key])
131
- || !qualification.boundary || qualificationDigest(qualification.boundary) !== qualificationDigest(installed.boundary)) {
132
- throw new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Worker installation evidence and qualified boundary must match');
133
- }
134
- }
135
- if (profile.installationQualification) {
136
- const installedAt = Date.parse(profile.installationQualification.verifiedAt);
137
- if (!Number.isFinite(installedAt) || installedAt > qualification.now
138
- || qualification.now - installedAt >= policyDays * 86400000) {
139
- throw new Error('Worker installation qualification is stale or invalid');
140
- }
141
- }
142
- const scopeDigest = profile.installationQualification?.scopeDigest ?? qualification.adapter.qualification.fixtureDigest;
143
- if (result.selected.qualificationDigest !== scopeDigest)
144
- throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker qualification digest changed');
145
- const absolute = (value) => typeof value === 'string' && path.posix.isAbsolute(value) && !value.includes('\0');
146
- for (const key of ['cwd', 'briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
147
- if (!absolute(result[key]))
148
- throw new Error(`Worker invocation requires absolute ${key}`);
149
- }
150
- const briefPath = path.posix.normalize(result.briefPath);
151
- if ([result.stdoutPath, result.stderrPath, result.finalPath].some(value => path.posix.normalize(value) === briefPath)
152
- || path.posix.normalize(result.stdoutPath) === path.posix.normalize(result.stderrPath)) {
153
- throw new Error('Worker invocation input/output paths must be distinct');
154
- }
155
- if (result.transport === 'shell') {
156
- assertOperatorWorkerNativeArgv(qualification.adapter, result.argv, result.selected, qualification.permissions);
157
- if (!absolute(result.argv[0]) || result.argv[0].includes('=') || result.argv.some(value => value.includes('\0'))) {
158
- throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Worker invocation requires an absolute binary without equals and NUL-free argv');
159
- }
160
- if (result.envNames.some(name => !profile.envAllowlist.includes(name))) {
161
- throw new Error('Worker invocation environment is outside the qualified allowlist');
162
- }
163
- const quote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
164
- // Only validated environment names appear as shell syntax; their values
165
- // remain in the conductor environment and are never printed in the plan.
166
- const environment = result.envNames.map(name => `${name}="\${${name}-}"`).join(' ');
167
- // The frozen 'closed' value means empty stdin (immediate EOF). 'brief-json'
168
- // pipes a conductor-built params document framed from the brief file
169
- // (worker-stdin-frame.mjs), so brief text never enters any argv.
170
- const worker = `env -i${environment ? ` ${environment}` : ''} ${result.argv.map(quote).join(' ')}`;
171
- const outputs = `> ${quote(result.stdoutPath)} 2> ${quote(result.stderrPath)}`;
172
- if (result.stdin === 'brief-json') {
173
- const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
174
- const frame = [process.execPath, framer, result.briefPath, result.selected.model, result.cwd].map(quote).join(' ');
175
- result.shellCommand = `cd ${quote(result.cwd)} && env -i ${frame} | ${worker} ${outputs}`;
176
- }
177
- else if (result.stdin === 'adapter-frame') {
178
- const binding = qualification.stdinFramerBinding;
179
- if (!binding || !absolute(binding.storePath) || !/^sha256:[a-f0-9]{64}$/.test(binding.storeDigest)) {
180
- throw new Error('Adapter stdin framer requires a trusted installation binding');
181
- }
182
- const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
183
- const frame = [process.execPath, framer, '--adapter-frame', result.briefPath, result.cwd,
184
- result.selected.cli, qualification.adapterId, result.selected.qualificationDigest, result.briefDigest,
185
- binding.storePath, binding.storeDigest, JSON.stringify(qualification.policy ?? null)].map(quote).join(' ');
186
- result.shellCommand = `cd ${quote(result.cwd)} && env -i${environment ? ` ${environment}` : ''} ${frame} | ${worker} ${outputs}`;
187
- }
188
- else if (result.stdin === 'closed' || result.stdin === 'brief-text') {
189
- const stdin = result.stdin === 'closed' ? '< /dev/null' : `< ${quote(result.briefPath)}`;
190
- result.shellCommand = `cd ${quote(result.cwd)} && ${worker} ${stdin} ${outputs}`;
191
- }
192
- else {
193
- throw new Error(`Unsupported worker stdin mode: ${result.stdin}`);
194
- }
195
- }
196
- else {
197
- const binding = result.mcp;
198
- for (const key of ['serverRef', 'tool', 'inputSchemaDigest', 'outputSchemaDigest', 'sessionIsolation']) {
199
- if (binding[key] !== profile.mcp[key])
200
- throw new WorkerPolicyError('OG_PIN_MISMATCH', `MCP worker binding changed: ${key}`);
201
- }
202
- if (binding.timeoutSeconds > result.limits.wallSeconds) {
203
- throw new Error('MCP worker timeout exceeds the invocation deadline');
204
- }
205
- for (const key of ['input', 'output']) {
206
- const schema = qualification.mcpSchemas?.[key];
207
- if (!schema || mcpToolSchemaDigest(schema) !== binding[`${key}SchemaDigest`]) {
208
- throw new WorkerPolicyError('OG_PIN_MISMATCH', `MCP worker ${key} schema digest changed`);
209
- }
210
- }
211
- const { default: Ajv2020 } = await import('ajv/dist/2020.js');
212
- const { default: addFormats } = await import('ajv-formats');
213
- const validate = addFormats(new Ajv2020({ strict: true, allErrors: true })).compile(qualification.mcpSchemas.input);
214
- if (!validate(binding.arguments))
215
- throw new Error('MCP worker arguments violate the pinned input schema');
216
- }
217
- assertValidWorkerPayload('workerResolvedInvocation', result);
218
- if (result.health !== undefined) {
219
- if (result.health.profileRef !== result.selected.profileRef
220
- || result.health.hardTimeoutSeconds > Math.min(result.limits.wallSeconds, result.mcp?.timeoutSeconds ?? Infinity)) {
221
- throw new Error('Worker health profile or wall ceiling mismatch');
222
- }
223
- if (['native', 'external'].includes(result.health.stepEnforcement)
224
- && profile.controls.stepCap?.support !== result.health.stepEnforcement)
225
- throw new Error('Unqualified worker step enforcement');
226
- }
227
- assertWorkerHealthQualification(profile, { profileRef: result.selected.profileRef, cliVersion: qualification.cliVersion,
228
- accountScope: qualification.installation?.accountScope ?? '', inferenceProvider: result.selected.inferenceProvider,
229
- model: result.selected.model, effort: result.selected.effort.value });
230
- return result;
231
- }
232
76
  const invocationChecks = {
233
77
  invocation_build_failed: { pointer: '/invocation/buildInvocation',
234
78
  message: 'Worker adapter could not build the invocation; check the selected model, effort, permissions and request paths.' },
@@ -269,127 +113,7 @@ export async function buildWorkerInvocationTransport(driver, input) {
269
113
  }
270
114
  return transport;
271
115
  }
272
- /** Only fixed argument guidance is safe to expose in public request diagnostics. */
273
- class WorkerRequestArgumentError extends WorkerPolicyError {
274
- pointer;
275
- constructor(key) {
276
- super('OG_CONFIG_REFERENCE', key === 'task'
277
- ? `Missing required --task. Valid task classes: ${Object.keys(DEFAULT_WORKER_SELECTION.taskClasses).join(', ')}, plus classes configured in defaults.workerSelection.taskClasses.`
278
- : `--${key.replace('_', '-')} must be an absolute path; relative paths are not resolved against cwd or --workdir.`);
279
- this.pointer = `/request/${key}`;
280
- }
281
- }
282
- /** Reject unsupported syntax before discovery or any path reads. */
283
- export function parseWorkerCommandArgs(args) {
284
- const names = { role: 'role', task: 'task', group: 'group', brief: 'brief',
285
- workdir: 'workdir', 'final-output': 'final_output', transport: 'transport', format: 'format' };
286
- const result = {};
287
- let json = false;
288
- let premiumAuthorized = false;
289
- for (let i = 0; i < args.length; i++) {
290
- const token = args[i];
291
- if (token === '--authorize-premium') {
292
- if (premiumAuthorized)
293
- throw new Error('Duplicate --authorize-premium');
294
- premiumAuthorized = true;
295
- continue;
296
- }
297
- if (token === '--json') {
298
- if (json)
299
- throw new Error('Duplicate --json');
300
- json = true;
301
- continue;
302
- }
303
- const match = /^--([^=]+)(?:=([\s\S]*))?$/.exec(token);
304
- if (!match || !Object.hasOwn(names, match[1]))
305
- throw new Error(`Unsupported models resolve argument: ${token}`);
306
- const key = names[match[1]];
307
- if (key in result)
308
- throw new Error(`Duplicate --${match[1]}`);
309
- const value = match[2] ?? args[++i];
310
- if (!value || value.startsWith('--') && match[2] === undefined || value.includes('\0')) {
311
- if (key === 'task' && !value?.includes('\0'))
312
- throw new WorkerRequestArgumentError('task');
313
- throw new Error(`--${match[1]} requires a value`);
314
- }
315
- result[key] = value;
316
- }
317
- if (json && result.format && result.format !== 'json')
318
- throw new Error('--json conflicts with --format');
319
- if (json)
320
- result.format = 'json';
321
- for (const key of ['role', 'task', 'group', 'brief', 'workdir', 'final_output']) {
322
- if (key === 'task' && !result[key])
323
- throw new WorkerRequestArgumentError('task');
324
- if (!result[key])
325
- throw new Error(`Missing ${key}`);
326
- }
327
- if (!['efficiency', 'coding', 'reasoning'].includes(result.role))
328
- throw new Error('Unsupported role');
329
- if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.group))
330
- throw new Error('Invalid group');
331
- if (result.transport && !['shell', 'mcp'].includes(result.transport))
332
- throw new Error('Unsupported transport');
333
- if (result.format && !['shell', 'json'].includes(result.format))
334
- throw new Error('Unsupported renderer; use POSIX shell or JSON');
335
- return { ...result, ...(premiumAuthorized ? { authority: { premiumAuthorized: true, maxAutoTier: 3 } } : {}) };
336
- }
337
116
  const bytesDigest = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
338
- export async function inspectWorkerPath(filename, kind) {
339
- if (!path.isAbsolute(filename) || filename.includes('\0'))
340
- throw new Error('Absolute NUL-free paths are required');
341
- if (filename.split('/').includes('..'))
342
- throw new Error('Worker path traversal is not allowed');
343
- const normalized = path.resolve(filename);
344
- let current = normalized;
345
- while (true) {
346
- try {
347
- const canonical = await realpath(current);
348
- if (canonical !== current)
349
- throw new Error(`Symlink path is not allowed: ${filename}`);
350
- if (current !== normalized)
351
- return false;
352
- const stat = await lstat(normalized);
353
- if (kind === 'directory' ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1) {
354
- throw new Error(`Expected an unaliased ${kind}: ${filename}`);
355
- }
356
- return true;
357
- }
358
- catch (error) {
359
- if (error.code !== 'ENOENT')
360
- throw error;
361
- const parent = path.dirname(current);
362
- if (parent === current)
363
- throw error;
364
- current = parent;
365
- }
366
- }
367
- }
368
- const inspectPath = inspectWorkerPath;
369
- async function readBrief(filename) {
370
- if (!await inspectPath(filename, 'file'))
371
- return null;
372
- const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
373
- try {
374
- const stat = await handle.stat();
375
- if (!stat.isFile() || stat.nlink !== 1 || stat.size > 262144)
376
- throw new Error('Brief must be a bounded regular file (256 KiB)');
377
- const buffer = Buffer.alloc(262145);
378
- let length = 0;
379
- while (length < buffer.length) {
380
- const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
381
- if (!bytesRead)
382
- break;
383
- length += bytesRead;
384
- }
385
- if (length > 262144)
386
- throw new Error('Brief exceeds 256 KiB');
387
- return buffer.subarray(0, length);
388
- }
389
- finally {
390
- await handle.close();
391
- }
392
- }
393
117
  /** Structured root-check failure; only these explicit path details are public. */
394
118
  export class WorkerPathScopeError extends WorkerPolicyError {
395
119
  pointer;
@@ -399,7 +123,7 @@ export class WorkerPathScopeError extends WorkerPolicyError {
399
123
  }
400
124
  }
401
125
  /** Re-resolve saved JSON through the same trusted owners; saved argv is never authority. */
402
- export async function revalidateWorkerInvocationPlan(saved, snapshot, drivers, now = new Date().toISOString()) {
126
+ export async function revalidateWorkerInvocationPlan(saved, snapshot, drivers, now = new Date().toISOString(), invocationPath) {
403
127
  const managed = saved.mode === 'managed' && snapshot.config.parallelism?.worker_mode === 'managed';
404
128
  if (!saved.request || (saved.mode !== 'direct' && !managed) || saved.mode !== (snapshot.config.parallelism?.worker_mode ?? 'direct') || saved.status !== 'eligible'
405
129
  || (managed && saved.invocation?.mode !== 'managed')
@@ -407,7 +131,9 @@ export async function revalidateWorkerInvocationPlan(saved, snapshot, drivers, n
407
131
  throw new Error('A fresh launchable models resolve JSON plan is required');
408
132
  }
409
133
  assertValidWorkerPayload('workerResolvedInvocation', saved.invocation);
410
- const current = await resolveWorkerInvocation(snapshot, saved.request, drivers, now, { digests: saved.digests, briefDigest: saved.invocation.briefDigest ?? null });
134
+ if (saved.invocation.invocationId)
135
+ await assertConductorWorkerPlan(saved, invocationPath, drivers);
136
+ const current = await resolveWorkerInvocation(snapshot, saved.request, drivers, now, { digests: saved.digests, briefDigest: saved.invocation.briefDigest ?? null, invocationId: saved.invocation.invocationId });
411
137
  // Expiry is calculated from the resolution clock. All execution bindings,
412
138
  // controls, thresholds and paths must remain byte-for-byte digest equivalent.
413
139
  if (!current.invocation || current.status !== 'eligible'
@@ -449,7 +175,7 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
449
175
  if (managedIntent && workerConfigDigest(brief.intent) !== workerConfigDigest(managedIntent)) {
450
176
  throw new Error('Managed brief differs from parent task authority');
451
177
  }
452
- if (brief.intent.role !== request.role || brief.intent.taskClass !== request.task || brief.intent.group !== request.group
178
+ if (brief.intent.role !== (snapshot.models.defaults.workerSelection?.roleGuidance?.[request.role]?.baseRole ?? request.role) || brief.intent.taskClass !== request.task || brief.intent.group !== request.group
453
179
  || brief.workspace.path !== request.workdir || brief.output.resultPath !== request.final_output) {
454
180
  throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Brief role/task/group/workspace/output differs from request');
455
181
  }
@@ -545,14 +271,20 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
545
271
  requiredControls: healthConfig?.requireStepCap ? ['stepCap'] : [] });
546
272
  pointer = '/parallelism/health';
547
273
  failureCode = 'OG_ADAPTER_UNQUALIFIED';
548
- const healthInput = { role: request.role, effort: tuple.effort,
274
+ const healthInput = { role: plan.policy?.roleGuidance?.[request.role]?.baseRole ?? request.role, effort: tuple.effort,
549
275
  modelKey: tuple.modelKey, profileRef: tuple.profile, wallSeconds: limits.wallSeconds,
550
276
  transportSeconds: adapter.invocation.kind === 'http' ? adapter.invocation.timeoutSeconds : undefined,
551
277
  stepEnforcement: controls.includes('stepCap') ? profile.controls.stepCap.support : 'unavailable' };
552
278
  let effectiveHealth = resolveWorkerEffectiveHealth(healthConfig, healthInput);
553
- const transport = await buildWorkerInvocationTransport(driver, { selected: structuredClone(selected), permissions: structuredClone(tuple.permissions),
279
+ const invocationId = expected?.invocationId ?? randomUUID();
280
+ const transport = await buildWorkerInvocationTransport(driver, { invocationId, selected: structuredClone(selected), permissions: structuredClone(tuple.permissions),
554
281
  cwd: request.workdir, briefPath: request.brief, brief: bytes?.toString('utf8') ?? '', finalPath, limits: structuredClone(limits),
555
- health: structuredClone(effectiveHealth) });
282
+ health: structuredClone(effectiveHealth), role: request.role });
283
+ for (const output of Object.values(workerOutputPaths({ ...transport, selected, transport: tuple.transport, finalPath, stdoutPath, stderrPath }))) {
284
+ await inspectPath(output, 'output');
285
+ if (path.resolve(request.brief) === output)
286
+ throw new Error('Brief and output paths must be distinct');
287
+ }
556
288
  pointer = '/invocation/buildInvocation';
557
289
  if (transport.stdin === 'adapter-frame' && typeof driver.buildWorkerStdinFrame !== 'function')
558
290
  throw new Error('Installed adapter stdin framer is required');
@@ -564,7 +296,8 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
564
296
  isolation: tuple.permissions.launchMode ?? 'systemd-unit',
565
297
  policyDigest, catalogDigest: plan.digests.catalogDigest, profileDigest: plan.digests.profileDigest,
566
298
  expiresAt, cwd: request.workdir, briefPath: request.brief, stdoutPath, stderrPath, finalPath,
567
- ...(transport.artifacts ? { artifacts: transport.artifacts } : {}),
299
+ ...(transport.artifacts ? { artifacts: transport.artifacts,
300
+ ...(transport.artifacts.some(artifact => artifact.location === 'workspace-relative') ? { invocationId } : {}) } : {}),
568
301
  argv: transport.argv, stdin: transport.stdin, envNames: transport.envNames, mcp: transport.mcp,
569
302
  ...(tuple.transport === 'shell' ? { shellCommand: '' } : {}),
570
303
  readiness: bytes !== null && workspaceExists && outputParentsExist ? 'launchable' : 'preparatory', briefDigest,
@@ -3,8 +3,8 @@ export const WORKER_STDIN_FRAME_MAX_BRIEF_BYTES: number;
3
3
  export const WORKER_STDIN_FRAME_MAX_BYTES: number;
4
4
  export function recordedWorkerStdinBytes(input: {
5
5
  filename: string; workspace: string; cli: string; adapterId: string; scopeDigest: string; briefDigest: string;
6
- storePath: string; storeDigest: string; policy?: import('../models/types.js').WorkerSelectionPolicy;
6
+ storePath: string; storeDigest: string; model?: string; policy?: import('../models/types.js').WorkerSelectionPolicy;
7
7
  }): Promise<Buffer | null>;
8
8
  export function workerStdinDocument(input: { brief: string | Buffer; model: string; cwd: string }): string;
9
9
  export function workerStdinBytes(invocation: { stdin: 'closed' | 'brief-text' | 'brief-json' | 'adapter-frame'; cwd?: string;
10
- selected?: { model?: string } }, brief: string | Buffer, adapter?: { buildWorkerStdinFrame?(brief: string | Buffer): string | Uint8Array }): Buffer | null;
10
+ selected?: { model?: string } }, brief: string | Buffer, adapter?: { buildWorkerStdinFrame?(brief: string | Buffer, invocation?: { selected?: { model?: string } }): string | Uint8Array }): Buffer | null;
@@ -39,7 +39,7 @@ export function workerStdinBytes(invocation, brief, adapter) {
39
39
  if (invocation.stdin === 'adapter-frame') {
40
40
  if (typeof adapter?.buildWorkerStdinFrame !== 'function') throw new Error('Installed adapter stdin framer is required');
41
41
  if (Buffer.byteLength(brief) > WORKER_STDIN_FRAME_MAX_BRIEF_BYTES) throw new Error('Brief exceeds the framing bound');
42
- const frame = adapter.buildWorkerStdinFrame(brief);
42
+ const frame = adapter.buildWorkerStdinFrame(brief, invocation);
43
43
  if (typeof frame !== 'string' && !(frame instanceof Uint8Array)) throw new Error('Adapter stdin frame must be bytes or text');
44
44
  if (Buffer.byteLength(frame) === 0) throw new Error('Adapter stdin frame must not be empty');
45
45
  if (Buffer.byteLength(frame) > WORKER_STDIN_FRAME_MAX_BYTES) throw new Error('Adapter stdin frame exceeds byte bound');
@@ -67,7 +67,7 @@ async function readBounded(filename) {
67
67
 
68
68
  /** Bound host authority only; workspace is an execution scope, never a config source. */
69
69
  export async function recordedWorkerStdinBytes({ filename, workspace, cli, adapterId, scopeDigest, briefDigest,
70
- storePath, storeDigest, policy }) {
70
+ storePath, storeDigest, policy, model }) {
71
71
  if (!filename?.startsWith('/') || !storePath?.startsWith('/')
72
72
  || !/^sha256:[a-f0-9]{64}$/.test(storeDigest ?? '')) throw new Error('Invalid adapter stdin framing request');
73
73
  const { loadRecordedWorkerInstallations } = await import('../models/worker-installation.mjs');
@@ -80,7 +80,7 @@ export async function recordedWorkerStdinBytes({ filename, workspace, cli, adapt
80
80
  || driver.qualification.installation?.evidence?.scopeDigest !== scopeDigest) throw new Error('Adapter stdin binding changed');
81
81
  const brief = await readBounded(filename);
82
82
  if (`sha256:${createHash('sha256').update(brief).digest('hex')}` !== briefDigest) throw new Error('Adapter stdin brief changed');
83
- return workerStdinBytes({ stdin: 'adapter-frame' }, brief, driver);
83
+ return workerStdinBytes({ stdin: 'adapter-frame', selected: { model } }, brief, driver);
84
84
  }
85
85
 
86
86
  // Pipeline stage: `worker-stdin-frame.mjs BRIEF_PATH MODEL CWD`. Only the
@@ -89,13 +89,13 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
89
89
  const [briefPath, model, cwd] = process.argv.slice(2);
90
90
  try {
91
91
  if (briefPath === '--adapter-frame') {
92
- const [filename, workspace, cli, adapterId, scopeDigest, briefDigest, storePath, storeDigest, policyJson] = process.argv.slice(3);
93
- if (process.argv.length !== 12) throw new Error('Invalid adapter stdin framing request');
92
+ const [filename, workspace, cli, adapterId, scopeDigest, briefDigest, storePath, storeDigest, policyJson, selectedModel] = process.argv.slice(3);
93
+ if (process.argv.length !== 13) throw new Error('Invalid adapter stdin framing request');
94
94
  // Reuse the recorded-installation loader; no module path or code supplied
95
95
  // by a brief is ever imported. The conductor binds store bytes and policy;
96
96
  // never resolve configuration from the worker-writable workspace.
97
97
  process.stdout.write(await recordedWorkerStdinBytes({ filename, workspace, cli, adapterId, scopeDigest, briefDigest,
98
- storePath, storeDigest, policy: JSON.parse(policyJson) ?? undefined }));
98
+ storePath, storeDigest, policy: JSON.parse(policyJson) ?? undefined, model: selectedModel }));
99
99
  } else {
100
100
  if (process.argv.length !== 5 || !briefPath?.startsWith('/')) throw new Error('usage: worker-stdin-frame.mjs BRIEF_PATH MODEL CWD');
101
101
  process.stdout.write(workerStdinDocument({ brief: await readBounded(briefPath), model, cwd }));
@@ -87,7 +87,7 @@ export const MAX_SHOWN_GROUPS = 8;
87
87
  */
88
88
  function excludedMembers(parallelism, group) {
89
89
  const excluded = [];
90
- for (const member of group.members) {
90
+ for (const member of (group.members ?? [])) {
91
91
  if (member.enabled === false)
92
92
  excluded.push(`${member.id} (member disabled)`);
93
93
  else if (parallelism?.clis?.[member.cli]?.enabled === false)
@@ -97,15 +97,19 @@ function excludedMembers(parallelism, group) {
97
97
  }
98
98
  function groupSummaries(entries, parallelism) {
99
99
  return entries.map(([id, group]) => {
100
- const roles = [...new Set(group.members.flatMap(m => m.roles ?? []))].join('/');
101
- const clis = [...new Set(group.members.map(m => m.cli))].join('/');
102
- const parts = [`${oneLine(id, 64)} (members: ${group.members.length}`];
100
+ const roles = [...new Set((group.members ?? []).flatMap(m => m.roles ?? []))].join('/');
101
+ const clis = [...new Set((group.members ?? []).map(m => m.cli))].join('/');
102
+ const parts = [`${oneLine(id, 64)} (members: ${(group.members ?? []).length}`];
103
103
  if (clis)
104
104
  parts.push(`clis: ${oneLine(clis, 96)}`);
105
105
  if (roles)
106
106
  parts.push(`roles: ${oneLine(roles, 96)}`);
107
107
  if (group.maxParallel !== undefined)
108
108
  parts.push(`maxParallel: ${group.maxParallel}`);
109
+ if (group.efforts)
110
+ parts.push(`efforts: ${oneLine(group.efforts.join('/'), 96)}`);
111
+ if (group.dataPolicy)
112
+ parts.push(`dataPolicy: ${oneLine(JSON.stringify(group.dataPolicy), 240)}`);
109
113
  if (group.requiredRoles?.length)
110
114
  parts.push(`requiredRoles: ${group.requiredRoles.join('/')}`);
111
115
  parts.push('configured');
@@ -147,13 +151,13 @@ async function observationSummaries(entries, deps) {
147
151
  `Cached model observations read at ${readAt.toISOString()}; catalog fetchedAt: ${stamp(catalog?.discovery?.fetchedAt)}. Ages are measured at this read and grow afterward. This cache may differ from the resolver's current installation observations; no eligibility read is performed by context generation. Missing or ambiguous observations have unknown age.`,
148
152
  '',
149
153
  ...entries.map(([id, group]) => {
150
- const observations = group.members.flatMap(member => {
154
+ const observations = (group.members ?? []).flatMap(member => {
151
155
  // Mirror the resolver's exact, unambiguous id/API-id lookup per installation.
152
156
  const rows = catalog?.discovery?.instances?.[member.cli]?.models
153
157
  .filter(row => row.id === member.model || row.apiModelId === member.model) ?? [];
154
158
  return rows.length === 1 && rows[0].worker ? [rows[0].worker] : [];
155
159
  });
156
- return `- ${oneLine(id, 64)} cached observations: ${observations.length}/${group.members.length} members; observedAt: ${range(observations.map(row => row.observedAt))}; verifiedAt: ${range(observations.map(row => row.verifiedAt))}.`;
160
+ return `- ${oneLine(id, 64)} cached observations: ${observations.length}/${(group.members ?? []).length} members; observedAt: ${range(observations.map(row => row.observedAt))}; verifiedAt: ${range(observations.map(row => row.verifiedAt))}.`;
157
161
  }),
158
162
  '',
159
163
  ];
@@ -313,7 +317,7 @@ export async function buildParallelismSection(projectPath, deps = {}) {
313
317
  const hard = healthValue(health, 'hardTimeoutSeconds', 1800);
314
318
  lines.push('### Orchestration groups (group dispatch enabled)');
315
319
  lines.push('');
316
- const entries = Object.entries(cfg.parallelism?.groups ?? {});
320
+ const entries = Object.entries(resolved.groups ?? {});
317
321
  if (entries.length > 0) {
318
322
  // Slice before summarizing so at most MAX_SHOWN_GROUPS summaries are built.
319
323
  const shown = groupSummaries(entries.slice(0, MAX_SHOWN_GROUPS), cfg.parallelism).join(' | ');
@@ -328,6 +332,14 @@ export async function buildParallelismSection(projectPath, deps = {}) {
328
332
  lines.push('');
329
333
  lines.push(...await observationSummaries(entries.slice(0, MAX_SHOWN_GROUPS), deps));
330
334
  const models = await new ConfigLoader(projectPath, { aiwgHome: resolveAiwgHome() }).load(cfg.parallelism);
335
+ for (const [pointer, origins] of Object.entries(models.workerPolicyOrigins ?? {})) {
336
+ if (!pointer.startsWith('/parallelism/groups/') || !pointer.endsWith('/template'))
337
+ continue;
338
+ lines.push(`Template expansion ${oneLine(pointer, 160)}: ${origins.map(origin => `${oneLine(origin.source, 240)}#${oneLine(origin.pointer, 160)} (${origin.digest})`).join(' → ')}. Contributions are retained in workerPolicyOrigins; inherited ceilings remain binding.`);
339
+ }
340
+ const aliases = Object.entries(models.defaults.workerSelection?.roleGuidance ?? {}).filter(([, guidance]) => guidance.baseRole);
341
+ for (const [role, guidance] of aliases)
342
+ lines.push(`Project role ${role} aliases ${guidance.baseRole}; ordered groups: ${guidance.groups.join(', ')}. Resolver eligibility and base-role qualification remain required.`);
331
343
  const freshness = models.defaults.workerSelection.freshness;
332
344
  lines.push(`Resolver freshness limits from merged model policy: observation warning after ${freshness.warnHours} hours; observation blocked after ${freshness.blockHours} hours; verification blocked after ${freshness.policyDays} days. Future-dated evidence is blocked. Re-resolve before launch; cached ages do not establish eligibility.`);
333
345
  lines.push('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.10.9",
3
+ "version": "2026.10.10",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,7 +49,11 @@
49
49
  "tools/ralph-external/session-launcher.mjs",
50
50
  "tools/ralph-external/process-monitor.mjs",
51
51
  "tools/ralph-external/lib/session-environment.mjs",
52
+ "tools/ralph-external/lib/worker-inputs.mjs",
53
+ "tools/ralph-external/lib/worker-final.mjs",
54
+ "tools/ralph-external/lib/worker-launch-cli.mjs",
52
55
  "tools/ralph-external/lib/worker-isolation.mjs",
56
+ "tools/ralph-external/lib/worker-native-sandbox.mjs",
53
57
  "tools/ralph-external/lib/systemd-client.mjs",
54
58
  "tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs",
55
59
  "tools/ralph-external/lib/launcher-recovery-evidence.mjs",
@@ -57,7 +61,11 @@
57
61
  "tools/ralph-external/lib/worker-overflow.mjs",
58
62
  "tools/scripts/resource-preflight.mjs",
59
63
  "tools/ralph-external/lib/provider-adapter.mjs",
64
+ "tools/ralph-external/lib/strict-worker-json.mjs",
65
+ "tools/ralph-external/lib/session-attempt.mjs",
66
+ "tools/ralph-external/lib/managed-worker-attempt.mjs",
60
67
  "tools/ralph-external/lib/worker-health.mjs",
68
+ "tools/ralph-external/lib/worker-artifacts.mjs",
61
69
  "tools/ralph-external/lib/claude-adapter.mjs",
62
70
  "tools/ralph-external/lib/codex-adapter.mjs",
63
71
  "tools/ralph-external/lib/opencode-adapter.mjs",
@@ -1643,6 +1643,7 @@
1643
1643
  "type": "object",
1644
1644
  "additionalProperties": false,
1645
1645
  "properties": {
1646
+ "invocationId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,51}(?![\\s\\S])" },
1646
1647
  "isolation": {
1647
1648
  "enum": ["systemd-unit", "native-sandbox"]
1648
1649
  },
@@ -1703,28 +1704,24 @@
1703
1704
  ]
1704
1705
  },
1705
1706
  "artifacts": {
1706
- "type": "array",
1707
- "maxItems": 16,
1707
+ "type": "array", "maxItems": 16,
1708
+ "contains": { "type": "object", "required": ["location"], "properties": { "location": { "const": "workspace-relative" } } },
1709
+ "minContains": 0, "maxContains": 1,
1708
1710
  "items": {
1709
- "type": "object",
1710
- "additionalProperties": false,
1711
+ "type": "object", "additionalProperties": false,
1711
1712
  "properties": {
1712
- "name": {
1713
- "type": "string",
1714
- "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}(?![\\s\\S])"
1715
- },
1716
- "content": {
1717
- "type": "string",
1718
- "maxLength": 1048576
1719
- },
1720
- "location": {
1721
- "const": "run-directory"
1722
- }
1713
+ "name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}(?![\\s\\S])" },
1714
+ "content": { "type": "string", "maxLength": 1048576 },
1715
+ "location": { "enum": ["run-directory", "workspace-relative"] },
1716
+ "prefix": { "type": "string", "minLength": 1, "maxLength": 256 },
1717
+ "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096 }
1723
1718
  },
1724
- "required": [
1725
- "name",
1726
- "content"
1727
- ]
1719
+ "required": ["name", "content"],
1720
+ "allOf": [{
1721
+ "if": { "required": ["location"], "properties": { "location": { "const": "workspace-relative" } } },
1722
+ "then": { "required": ["prefix", "relativePath"] },
1723
+ "else": { "not": { "anyOf": [{ "required": ["prefix"] }, { "required": ["relativePath"] }] } }
1724
+ }]
1728
1725
  }
1729
1726
  },
1730
1727
  "briefPath": {
@@ -1871,6 +1868,10 @@
1871
1868
  "controlEvidence"
1872
1869
  ],
1873
1870
  "allOf": [
1871
+ {
1872
+ "if": { "required": ["artifacts"], "properties": { "artifacts": { "type": "array", "contains": { "type": "object", "required": ["location"], "properties": { "location": { "const": "workspace-relative" } } } } } },
1873
+ "then": { "required": ["invocationId"], "properties": { "transport": { "const": "shell" } } }
1874
+ },
1874
1875
  {
1875
1876
  "if": {
1876
1877
  "properties": {
@@ -184,6 +184,13 @@
184
184
  "type": "object",
185
185
  "additionalProperties": false,
186
186
  "properties": {
187
+ "baseRole": {
188
+ "enum": [
189
+ "efficiency",
190
+ "coding",
191
+ "reasoning"
192
+ ]
193
+ },
187
194
  "groups": {
188
195
  "type": "array",
189
196
  "items": {
@@ -248,6 +255,15 @@
248
255
  "type": "object",
249
256
  "additionalProperties": false,
250
257
  "properties": {
258
+ "localOnlyPaths": {
259
+ "type": "array",
260
+ "uniqueItems": true,
261
+ "items": {
262
+ "type": "string",
263
+ "pattern": "^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9_.@+,-]+(?:/[A-Za-z0-9_.@+,-]+)*$(?![\\s\\S])"
264
+ },
265
+ "description": "Literal project-relative local-only files or directory subtrees; deployed conductor guards have limited shell coverage."
266
+ },
251
267
  "allowedProcessing": {
252
268
  "type": "array",
253
269
  "items": {