@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
@@ -46,7 +46,12 @@ import { sampleWorkerIdentity, sameWorkerIdentity, createWorkerIdentityCapture,
46
46
  qualifyWorkerCpuTickRate, sampleWorkerCpuTime, isSafeWorkerGroupTarget } from './process-monitor.mjs';
47
47
  import { createWorkerHealthObserver, sampleOwnedWorktreeContent, classifyWorkerHealthFailure } from './lib/worker-health.mjs';
48
48
  import { ScopedFailureBreaker, attributeFailureScope } from './lib/consecutive-failure-circuit-breaker.mjs';
49
+ import { assertManagedWorkerLaunch, validateManagedAttempt } from './lib/managed-worker-attempt.mjs';
50
+ export { assertManagedWorkerLaunch } from './lib/managed-worker-attempt.mjs';
49
51
  import { buildSessionEnvironment } from './lib/session-environment.mjs';
52
+ import { withWorkerPath, boundedPrivateRead } from './lib/worker-inputs.mjs';
53
+ import { collectWorkerFinal } from './lib/worker-final.mjs';
54
+ import { resolveRetryReference } from './lib/worker-launch-cli.mjs';
50
55
  import { launcherRecoveryEvidence, observeLauncherRecoveryOutcome, recordLauncherRecoveryHint } from './lib/launcher-recovery-evidence.mjs';
51
56
  export { observeLauncherRecoveryOutcome } from './lib/launcher-recovery-evidence.mjs';
52
57
  import { workerNeedsNativeNamespaces, probeWorkerNativeSandbox, nativeSandboxFailure, nativeSandboxProbeMessage, NATIVE_SANDBOX_UNAVAILABLE, checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
@@ -94,75 +99,9 @@ const bytesDigest = bytes => `sha256:${createHash('sha256').update(bytes).digest
94
99
  export const beneath = (root, filename) => { const r = relative(root, filename); return r === '' || !r.startsWith('..') && !isAbsolute(r); };
95
100
  const signalNumberToName = Object.fromEntries(Object.entries(osConstants.signals ?? {}).map(([name, number]) => [number, name]));
96
101
 
97
- /** Resolve the deepest existing ancestor's realpath, then reattach any
98
- * not-yet-created remainder. A lexical prefix match is not sufficient: a
99
- * symlinked ancestor of either path can make `beneath()` wrong in either
100
- * direction, so both sides are resolved before the lexical comparison. */
101
- async function resolveRealish(target) {
102
- const parts = target.split('/').filter(Boolean);
103
- for (let i = parts.length; i >= 0; i--) {
104
- const candidate = i === 0 ? '/' : `/${parts.slice(0, i).join('/')}`;
105
- try {
106
- const real = await realpath(candidate);
107
- const remainder = parts.slice(i).join('/');
108
- return remainder ? join(real, remainder) : real;
109
- } catch (error) { if (error.code !== 'ENOENT') throw error; }
110
- }
111
- return target;
112
- }
113
102
  export async function realBeneath(root, filename) {
114
- const [realRoot, realFilename] = await Promise.all([resolveRealish(root), resolveRealish(filename)]);
115
- return beneath(realRoot, realFilename);
116
- }
117
-
118
- // Linux directory handles pin each ancestor. O_NOFOLLOW on a pathname alone
119
- // cannot protect an ancestor that the worker replaces after inspection.
120
- async function withWorkerPath(filename, operation) {
121
- if (!isAbsolute(filename) || filename.includes('\0') || filename.split('/').includes('..')) throw new Error('Invalid worker path');
122
- const components = filename.split('/').filter(Boolean);
123
- const leaf = components.pop();
124
- let directory = await open('/', constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
125
- try {
126
- for (const component of components) {
127
- const next = await open(`/proc/self/fd/${directory.fd}/${component}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
128
- await directory.close(); directory = next;
129
- }
130
- return await operation(`/proc/self/fd/${directory.fd}/${leaf ?? '.'}`);
131
- } finally { await directory.close(); }
132
- }
133
-
134
- // #2915 review 4 (finding 4): `privateOutput: true` REPAIRS a loose mode
135
- // (chmod 0600) rather than rejecting it — correct for the worker's own
136
- // final-output file, which the launcher itself created and is free to
137
- // re-tighten. `rejectInsecureMode: true` additionally REFUSES a mode other
138
- // than exactly 0600 instead of repairing it, for evidence this launcher
139
- // did NOT create moments ago (retry predecessor metadata) — a 0666 file
140
- // there is itself a signal worth refusing on, not silently normalizing
141
- // away.
142
- async function boundedPrivateRead(filename, limit, inspect, privateOutput = false, rejectInsecureMode = false) {
143
- if (!await inspect(filename, 'file')) throw new Error('Required worker input is missing');
144
- const handle = await withWorkerPath(filename, target => open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK));
145
- try {
146
- const stat = await handle.stat();
147
- if (!stat.isFile() || stat.nlink !== 1 || stat.size > limit) throw new Error('Worker input must be a bounded unaliased regular file');
148
- if (privateOutput) {
149
- if (stat.uid !== process.getuid()) throw new Error('Worker final must be conductor-owned');
150
- if (rejectInsecureMode) {
151
- if ((stat.mode & 0o777) !== 0o600) throw new Error('Worker input mode must already be exactly 0600; insecure modes are refused, not repaired');
152
- } else {
153
- await handle.chmod(0o600);
154
- }
155
- }
156
- const buffer = Buffer.alloc(limit + 1);
157
- let length = 0;
158
- while (length < buffer.length) {
159
- const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
160
- if (!bytesRead) break;
161
- length += bytesRead;
162
- }
163
- if (length > limit) throw new Error('Worker input exceeds bound');
164
- return buffer.subarray(0, length);
165
- } finally { await handle.close(); }
103
+ const paths = await importImpl(import.meta.url, 'serve/worker-invocation-paths.js');
104
+ return paths.realBeneath(root, filename);
166
105
  }
167
106
 
168
107
  export async function publishWorkerRecord(runDir, name, value, inspect) {
@@ -330,55 +269,8 @@ function checkDispatchGuard(dispatchGuard) {
330
269
  }
331
270
  }
332
271
 
333
- /** Execution-time files in the launcher's private directory. Content producers
334
- * are pure; preview paths remain ${artifact:name}. Workspace locations are a
335
- * future declaration kind and are refused here. */
336
- export async function materializeWorkerArtifacts(invocation, runDir) {
337
- const artifacts = invocation.artifacts ?? [];
338
- if (!Array.isArray(artifacts) || artifacts.length > 16) throw new Error('Worker artifact count exceeds bound');
339
- const names = new Set();
340
- let total = 0;
341
- for (const artifact of artifacts) {
342
- if (!artifact || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(artifact.name)
343
- || /[^A-Za-z0-9_-]/.test(artifact.name) || names.has(artifact.name) || typeof artifact.content !== 'string'
344
- || artifact.location !== undefined && artifact.location !== 'run-directory') throw new Error('Invalid worker artifact declaration');
345
- names.add(artifact.name);
346
- const size = Buffer.byteLength(artifact.content);
347
- total += size;
348
- if (size > 1048576 || total > 4194304) throw new Error('Worker artifact bytes exceed bound');
349
- }
350
- const paths = new Map(artifacts.map(artifact => [artifact.name, join(runDir, `artifact-${artifact.name}`)]));
351
- const argv = invocation.argv.map(token => {
352
- if (!token.includes('${artifact:')) return token;
353
- const match = /^\$\{artifact:([A-Za-z0-9][A-Za-z0-9_-]{0,63})\}$/.exec(token);
354
- if (!match || !paths.has(match[1])) throw new Error('Undeclared worker artifact placeholder');
355
- return paths.get(match[1]);
356
- });
357
- const { inspectWorkerPath } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
358
- await inspectWorkerPath(runDir, 'directory');
359
- const stat = await lstat(runDir);
360
- if (stat.uid !== process.getuid() || (stat.mode & 0o077)) throw new Error('Worker artifact directory must be private and owned');
361
- const created = [];
362
- const cleanup = async () => {
363
- const results = await Promise.allSettled(created.map(filename =>
364
- withWorkerPath(filename, target => unlink(target)).catch(error => {
365
- if (error.code !== 'ENOENT') throw error;
366
- })));
367
- const failed = results.find(result => result.status === 'rejected');
368
- if (failed) throw failed.reason;
369
- };
370
- try {
371
- for (const artifact of artifacts) {
372
- const filename = paths.get(artifact.name);
373
- const handle = await withWorkerPath(filename, target => open(target,
374
- constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
375
- created.push(filename);
376
- try { await handle.writeFile(artifact.content); await handle.sync(); }
377
- finally { await handle.close(); }
378
- }
379
- return { argv, cleanup };
380
- } catch (error) { await cleanup(); throw error; }
381
- }
272
+ import { materializeWorkerArtifacts, WorkerArtifactLifecycle } from './lib/worker-artifacts.mjs';
273
+ export { materializeWorkerArtifacts } from './lib/worker-artifacts.mjs';
382
274
 
383
275
  const _workerRecoverySubscriptions = new WeakMap();
384
276
  const _workerRecoveryReporters = new WeakMap();
@@ -413,45 +305,6 @@ function subscribeWorkerRecovery(owner, breaker) {
413
305
  _workerRecoverySubscriptions.set(owner, () => reporter.owners.delete(owner));
414
306
  }
415
307
 
416
- /** Reuse the MC authority and its registered one-shot identity at every spawn boundary. */
417
- export async function assertManagedWorkerLaunch(options, invocation, inputs, projectRoot = process.cwd()) {
418
- if (!options.managedAdmission || invocation.mode !== 'managed'
419
- || inputs.snapshot.config.parallelism?.worker_mode !== 'managed') throw new Error('Managed worker admission is required');
420
- const { createMcAdmissionScheduler } = await importImpl(import.meta.url, 'cli/handlers/mc.js');
421
- const { loadLauncherRegistry } = await importImpl(import.meta.url, 'cli/handlers/ralph-launcher.js');
422
- const { workerModelKey } = await importImpl(import.meta.url, 'models/provider-policy.js');
423
- const scheduler = await createMcAdmissionScheduler(projectRoot, inputs.snapshot.config);
424
- const { requestId, generation } = options.managedAdmission;
425
- const snapshot = scheduler.snapshot();
426
- const record = snapshot.records[requestId];
427
- const deadlineAtMs = Math.min(...[record?.accounting?.runKey, record?.accounting?.taskKey]
428
- .map(key => snapshot.allowances?.[key]?.deadlineAt).filter(Boolean).map(Date.parse));
429
- const registered = loadLauncherRegistry(projectRoot).loops[process.env.RALPH_LOOP_ID];
430
- const worker = record?.managed;
431
- const reservation = record?.accounting?.reservation;
432
- if (!record || record.state !== 'admitted' || record.leaseGeneration !== generation
433
- || !record.launchClaimedAt || !record.metadata?.invocationId || Date.parse(record.leaseExpiresAt ?? '') <= Date.now()
434
- || deadlineAtMs <= Date.now()
435
- || worker?.kind !== 'external' || worker.group !== invocation.selected.group
436
- || worker.member !== invocation.selected.member || worker.cli !== invocation.selected.cli
437
- || worker.model !== workerModelKey(invocation.selected.inferenceProvider, invocation.selected.model)
438
- || reservation?.tokens != null && !(invocation.limits.tokens <= reservation.tokens)
439
- || reservation?.costUsd != null && !(invocation.limits.costUsd !== null && invocation.limits.costUsd <= reservation.costUsd)
440
- || registered?.managedAdmission?.requestId !== requestId || registered.managedAdmission.generation !== generation
441
- || registered.managedWorker?.invocationPath !== options.invocationPath
442
- || registered.managedWorker?.runDir !== options.runDir) {
443
- throw new Error('Managed worker launch differs from the current claimed admission');
444
- }
445
- if (record.execution) {
446
- const identity = sampleWorkerIdentity(process.pid);
447
- if (record.execution.pid !== process.pid || !identity?.bootId || !identity.startTicks
448
- || record.execution.startedAt !== `${identity.bootId}:${identity.startTicks}:${identity.uid}`) {
449
- throw new Error('Managed worker supervisor identity differs');
450
- }
451
- }
452
- return { ...record, deadlineAtMs };
453
- }
454
-
455
308
  async function workerDispatchPath(filename) {
456
309
  let candidate = filename;
457
310
  while (dirname(candidate) !== candidate) {
@@ -491,8 +344,25 @@ function workerLaunchRefusal(outcome) {
491
344
  // Neither worker output nor a caller-supplied result can populate this context.
492
345
  const directStartupCorrectionContext = new WeakMap();
493
346
 
347
+ // Each attempt directory is created exclusively; its parent is the conductor's job.
348
+ async function createAttemptRunDirectory(runDir) {
349
+ try { await mkdir(runDir, { mode: 0o700 }); } catch (error) {
350
+ if (error?.code === 'ENOENT') throw new Error(`Worker run directory parent does not exist: ${dirname(runDir)}; create it (mode 0700) before launch`);
351
+ throw error;
352
+ }
353
+ }
354
+
494
355
  async function launchOneWorker(owner, options) {
495
- const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validate } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
356
+ const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validatePlan } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
357
+ // Forward the saved-plan path: invocation identity is verified against it (#2905).
358
+ const validate = async (plan, snapshot, drivers, now, planPath = options.invocationPath) => {
359
+ const validateAt = (p, s, d, n) => validatePlan(p, s, d, n, planPath);
360
+ if (plan.mode !== 'managed' || !options.managedAdmission) return validateAt(plan, snapshot, drivers, now);
361
+ const checked = await validateManagedAttempt(options, plan, { snapshot, drivers }, now, validateAt,
362
+ attempt => publishWorkerRecord(dirname(options.invocationPath), `${basename(options.invocationPath)}.attempt.json`, attempt, inspect));
363
+ options.attempt = checked.attempt;
364
+ return checked.invocation;
365
+ };
496
366
  const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
497
367
  const planBytes = await boundedPrivateRead(options.invocationPath, 1048576, inspect);
498
368
  const saved = JSON.parse(planBytes.toString('utf8'));
@@ -502,9 +372,11 @@ async function launchOneWorker(owner, options) {
502
372
  let inputs = await loadInputs();
503
373
  const clock = () => typeof options.now === 'function' ? options.now() : options.now ?? new Date().toISOString();
504
374
  const now = clock();
505
- const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now);
375
+ const { assertConductorWorkerPlan } = await importImpl(import.meta.url, 'serve/worker-invocation-paths.js');
376
+ await assertConductorWorkerPlan(saved, options.invocationPath, inputs.drivers);
377
+ const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now, options.invocationPath);
506
378
  if (invocation.mode === 'managed' || options.managedAdmission) {
507
- const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
379
+ const admitted = await assertManagedWorkerLaunch(options, invocation, inputs, options.projectRoot ?? process.cwd());
508
380
  options = { ...options, scopedBreaker: null, managedInvocationId: admitted.metadata.invocationId,
509
381
  managedDeadlineAtMs: admitted.deadlineAtMs };
510
382
  }
@@ -536,12 +408,12 @@ async function launchOneWorker(owner, options) {
536
408
  // check too.
537
409
  let verifiedPreviousAttemptId = null;
538
410
  if (options?.retryOf !== undefined) {
539
- const exitRecord = await verifyRetryPredecessor(options.retryOf,
411
+ const exitRecord = await verifyRetryPredecessor(await resolveRetryReference(options.retryOf, { inspect }),
540
412
  { newInvocationPath: options?.invocationPath, inspect, workerWritableRoots: writableRoots });
541
413
  verifiedPreviousAttemptId = exitRecord.invocationId;
542
414
  }
543
- const paths = { stdout: invocation.stdoutPath, stderr: invocation.stderrPath, final: invocation.finalPath };
544
- if (new Set(Object.values(paths)).size !== 3 || Object.values(paths).some(filename =>
415
+ const paths = (await importImpl(import.meta.url, 'serve/worker-invocation-paths.js')).workerOutputPaths(invocation);
416
+ if (new Set(Object.values(paths)).size !== Object.keys(paths).length || Object.values(paths).some(filename =>
545
417
  filename === options.invocationPath || filename === invocation.briefPath
546
418
  || dirname(filename) === runDir && basename(filename).startsWith('artifact-')
547
419
  || ['invocation.json', 'process.json', 'exit.json', 'health.json', 'final-evidence.json', 'worker-start.json', 'worker-identity.json', 'worker-exit.json',
@@ -571,7 +443,7 @@ async function launchOneWorker(owner, options) {
571
443
  const isolation = invocation.isolation === 'native-sandbox' ? { mode: 'native-sandbox' }
572
444
  : options.isolation ?? detectWorkerIsolation();
573
445
  if (isolation.mode === 'systemd-unit' && workerNeedsNativeNamespaces(adapter, permissions)) {
574
- const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ cwd: invocation.cwd,
446
+ const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ adapter, cwd: invocation.cwd,
575
447
  envNames: invocation.envNames, env: Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
576
448
  .map(name => [name, process.env[name]])) });
577
449
  if (!probe.ok) throw new WorkerPolicyError(NATIVE_SANDBOX_UNAVAILABLE, nativeSandboxProbeMessage(probe));
@@ -608,7 +480,7 @@ async function launchOneWorker(owner, options) {
608
480
  // themselves; pre-spawn refusals above never reach this point. Global
609
481
  // dispatch AND per-CLI enablement are checked inside `admitDirectWorker`
610
482
  // before any breaker construction (#2915 final review, finding 1).
611
- const invocationId = randomUUID();
483
+ const invocationId = invocation.invocationId ?? randomUUID();
612
484
  // #2915 review 4 (finding 8): computed ONCE, before admission, so the
613
485
  // SAME digest both gates the allowance reservation here and is published
614
486
  // into invocation.json below — never recomputed from a possibly-stale
@@ -718,13 +590,15 @@ async function launchOneWorker(owner, options) {
718
590
  throw error;
719
591
  }
720
592
  }
593
+ // Created before run-directory preparation so its failure path can report it.
594
+ const artifactLifecycle = new WorkerArtifactLifecycle(invocation, invocationId);
721
595
  // #2915 review 4 (finding 2): this run-directory preparation is
722
596
  // pre-spawn work happening AFTER a successful admission above — any
723
597
  // failure here (mkdir, ownership, the `.launch-owner` write) must settle
724
598
  // the reservation exactly like every other pre-spawn exit, never leak it
725
599
  // by throwing before the big try/catch below is reached.
726
600
  try {
727
- if (!runExists) await mkdir(runDir, { mode: 0o700 }); // exclusive attempt: no recursive creation or reuse
601
+ if (!runExists) await createAttemptRunDirectory(runDir); // exclusive attempt: no recursive creation or reuse
728
602
  const runStat = await lstat(runDir);
729
603
  if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
730
604
  await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
@@ -736,6 +610,7 @@ async function launchOneWorker(owner, options) {
736
610
  // release without leaving a marker even past the canary deadline.
737
611
  await settleLedger({ kind: 'uncertain', confirmedStop: true });
738
612
  if (ledgerDiagnostics.length) error.ledgerDiagnostics = ledgerDiagnostics;
613
+ artifactLifecycle.failure(error);
739
614
  throw error;
740
615
  }
741
616
  const supervisor = sampleWorkerIdentity(process.pid);
@@ -754,6 +629,7 @@ async function launchOneWorker(owner, options) {
754
629
  // own outcome.
755
630
  let latestProcessRecord = null;
756
631
  const publish = (name, value) => {
632
+ artifactLifecycle.observe(name, value);
757
633
  if (name === 'process.json') {
758
634
  value = { ...value, recovery: { ...admission.breaker.recoveryStatus(), observedAt: new Date().toISOString() } };
759
635
  if (options.startupCorrectionRecord) value.startupCorrection = options.startupCorrectionRecord;
@@ -790,6 +666,7 @@ async function launchOneWorker(owner, options) {
790
666
  // task — never from an unverified caller claim.
791
667
  previousAttemptId: verifiedPreviousAttemptId,
792
668
  requestId: invocationId, taskDigest, role: saved.request.role,
669
+ ...(options.managedAdmission ? { attempt: options.attempt } : {}),
793
670
  selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
794
671
  policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
795
672
  expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
@@ -813,7 +690,7 @@ async function launchOneWorker(owner, options) {
813
690
  }
814
691
  // Recheck policy, prompt, installation and exact bindings after file preparation.
815
692
  inputs = await loadInputs();
816
- await validate(saved, inputs.snapshot, inputs.drivers, clock());
693
+ await validate(saved, inputs.snapshot, inputs.drivers, clock(), options.invocationPath);
817
694
  gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
818
695
  paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
819
696
  if (!gate.ok) throw workerDispatchRefusal(gate);
@@ -875,6 +752,7 @@ async function launchOneWorker(owner, options) {
875
752
  // real child's own queued events (exit/close) race ahead of
876
753
  // `captureOneWorker`'s own listener attachment in several tests (and
877
754
  // would in production).
755
+ artifactLifecycle.spawning();
878
756
  const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
879
757
  command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
880
758
  memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation, workerGroup: invocation.selected.group,
@@ -988,6 +866,7 @@ async function launchOneWorker(owner, options) {
988
866
  const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
989
867
  const { confirmedStop } = await resolveStopEvidence({ isolationMode: outcome.mode,
990
868
  unitName: outcome.unit ?? null, child: outcome.child, graceMs });
869
+ artifactLifecycle.confirmedStop = confirmedStop;
991
870
  await settleLedger({ kind: 'uncertain', confirmedStop });
992
871
  // #2915 review 6 (finding 3d): when this bounded stop escalation DOES
993
872
  // confirm termination, also call the explicit reconciliation -- a
@@ -1005,7 +884,7 @@ async function launchOneWorker(owner, options) {
1005
884
  } finally {
1006
885
  admission.breaker.removeListener('transition', onScopeTransition);
1007
886
  await publishing;
1008
- try { await artifacts?.cleanup(); } finally {
887
+ try { await artifactLifecycle.finish(artifacts); } finally {
1009
888
  // Never chmod a worker-replaced pathname; only the inodes we opened.
1010
889
  for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
1011
890
  await inspect(runDir, 'directory');
@@ -1075,7 +954,7 @@ async function captureOneWorker(owner, ctx) {
1075
954
  // For systemd-unit mode the unit's MainPID is not known synchronously; it
1076
955
  // is still resolved once inside observeIdentity.
1077
956
  let nativeSupervisor = hasTransport && outcome.mode !== 'systemd-unit' ? sampleNativeSupervisor(child.pid) : null;
1078
- const streams = [handles[0], handles[1], handles[3], handles[4]].map(handle => createWriteStream('', { fd: handle.fd, autoClose: false }));
957
+ const streams = [handles[0], handles[1], handles[Object.keys(paths).length], handles[Object.keys(paths).length + 1]].map(handle => createWriteStream('', { fd: handle.fd, autoClose: false }));
1079
958
  let identityObservation = 'pending';
1080
959
  let reportedWorker = null;
1081
960
  let output = Buffer.alloc(0);
@@ -1309,26 +1188,10 @@ async function captureOneWorker(owner, ctx) {
1309
1188
  let healthFinalValid = false;
1310
1189
  let healthAttemptFinalValid = false;
1311
1190
  try {
1312
- const finalStat = await lstat(paths.final);
1313
- if (!finalStat.isFile() || finalStat.isSymbolicLink() || finalStat.nlink !== 1 || finalStat.size > 262144) throw new Error('Invalid final output');
1314
- await ctx.inspect(paths.final, 'file');
1315
- let extracted = false;
1316
- if (finalStat.size === 0 && driver.extractFinal) {
1317
- const final = driver.extractFinal(output.toString('utf8'));
1318
- if (final === null || final === undefined || final.success === false) throw new Error('Missing or unsuccessful final extraction');
1319
- const bytes = JSON.stringify({ invocationId, source: 'adapter-extraction', candidate: true, verified: false, result: final });
1320
- if (Buffer.byteLength(bytes) > 262144) throw new Error('Final exceeds bound');
1321
- await handles[2].writeFile(bytes); extracted = true;
1322
- }
1323
- const finalBytes = await boundedPrivateRead(paths.final, 262144, ctx.inspect, true);
1324
- if (!finalBytes.length) throw new Error('Missing final output');
1325
- if (!extracted && driver.extractFinal) {
1326
- const parsed = driver.extractFinal(finalBytes.toString('utf8'));
1327
- if (parsed === null || parsed === undefined || parsed.success === false) throw new Error('Invalid final output');
1328
- }
1329
- await publish('final-evidence.json', { invocationId, path: paths.final, digest: bytesDigest(finalBytes),
1330
- source: extracted ? 'adapter-extraction' : 'worker-output', candidate: true, verified: false,
1331
- terminalSuccess: result.exitCode === 0 && !result.timedOut && !result.errorCode });
1191
+ const { finalBytes, extracted, parseFinal } = await collectWorkerFinal({
1192
+ path: paths.final, workdir: invocation.cwd, driver, output, invocationId, result,
1193
+ inspect: ctx.inspect, handle: handles[2], publish, outputFraming: ctx.profile.health?.outputFraming,
1194
+ });
1332
1195
  // Consume #2890's strict final contract when supplied by the installed
1333
1196
  // driver. Legacy extraction remains a candidate, never health authority.
1334
1197
  if (driver.parseWorkerFinal) {
@@ -1336,7 +1199,7 @@ async function captureOneWorker(owner, ctx) {
1336
1199
  // Strict validation consumes original bytes. A legacy extracted
1337
1200
  // object may already have lost duplicate keys and cannot authorize
1338
1201
  // completion through reserialization.
1339
- const claims = await driver.parseWorkerFinal(extracted ? output : finalBytes, {
1202
+ const claims = await parseFinal(extracted ? output : finalBytes, {
1340
1203
  format: extracted && ctx.profile.health?.outputFraming === 'jsonl' ? 'jsonl' : 'json', observation: { transport: 'shell',
1341
1204
  status: result.exitCode === 0 && !result.errorCode && !result.timedOut ? 'completed' : 'failed',
1342
1205
  termination: result.termination, exitCode: result.exitCode } });
@@ -1601,7 +1464,8 @@ async function captureOneWorker(owner, ctx) {
1601
1464
  //
1602
1465
  // `launchOneWorkerBaseline` and `captureOneWorkerBaseline` reproduce
1603
1466
  // `launchOneWorker`/`captureOneWorker`, apart from the lifecycle fixes above,
1604
- // as they existed at base commit `a4085b86a` (`git show
1467
+ // with shared workerClaims/digest validation (#3675) applied to both paths,
1468
+ // otherwise as they existed at base commit `a4085b86a` (`git show
1605
1469
  // a4085b86a:tools/ralph-external/session-launcher.mjs`) — before any
1606
1470
  // #2915 direct-recovery code existed in this file. `launchOneWorker`
1607
1471
  // above calls `launchOneWorkerBaseline` for every attempt
@@ -1633,6 +1497,7 @@ async function captureOneWorker(owner, ctx) {
1633
1497
  // `a4085b86a` called them — with NO `onDispatchCommit` — which that
1634
1498
  // module's own dual-mode contract (`_runDispatchCommit`) defines as
1635
1499
  // "behaves exactly as before the hook existed";
1500
+ // - shared final validation (`lib/worker-final.mjs`, #3675);
1636
1501
  // - unchanged imports (`process-monitor.mjs`, `lib/worker-health.mjs`,
1637
1502
  // `serve/worker-invocation.js`, `models/qualification-digest.mjs`).
1638
1503
  // Neither function imports, reads, or references the breaker, the
@@ -1647,12 +1512,13 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1647
1512
  loadInputs, validate, inspect, clock, qualificationDigest } = ctx;
1648
1513
  let inputs = ctx.inputs;
1649
1514
  let localGate = gate;
1650
- if (!runExists) await mkdir(runDir, { mode: 0o700 }); // exclusive attempt: no recursive creation or reuse
1515
+ if (!runExists) await createAttemptRunDirectory(runDir); // exclusive attempt: no recursive creation or reuse
1651
1516
  const runStat = await lstat(runDir);
1652
1517
  if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
1653
1518
  await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
1654
1519
  try { await handle.chmod(0o700); } finally { await handle.close(); } });
1655
- const invocationId = options.managedAdmission ? options.managedInvocationId : randomUUID();
1520
+ const invocationId = invocation.invocationId ?? (options.managedAdmission ? options.managedInvocationId : randomUUID());
1521
+ if (options.managedAdmission && invocationId !== options.managedInvocationId) throw new Error('Worker artifact invocation id differs from managed admission');
1656
1522
  const reservation = await withWorkerPath(join(runDir, '.launch-owner'), target => open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
1657
1523
  try { await reservation.writeFile(invocationId); await reservation.sync(); } finally { await reservation.close(); }
1658
1524
  const supervisor = sampleWorkerIdentity(process.pid);
@@ -1664,7 +1530,9 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1664
1530
  const writeRecord = options.publishRecord ?? publishWorkerRecord;
1665
1531
  const publishFailures = [];
1666
1532
  let publishing = Promise.resolve();
1533
+ const artifactLifecycle = new WorkerArtifactLifecycle(invocation, invocationId);
1667
1534
  const publish = (name, value) => {
1535
+ artifactLifecycle.observe(name, value);
1668
1536
  const write = publishing.catch(() => {}).then(() => writeRecord(runDir, name, value, inspect));
1669
1537
  publishing = write.catch(error => { publishFailures.push({ name, error: error?.message ?? String(error) }); });
1670
1538
  return write;
@@ -1678,6 +1546,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1678
1546
  const displayArgv = artifacts.argv.map((token, i) => i === 0 ? token : `<argument:${bytesDigest(token)}>`);
1679
1547
  await publish('invocation.json', { version: 1, origin: 'launcher', invocationId, attemptId: invocationId,
1680
1548
  previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: options.managedAdmission?.requestId ?? invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
1549
+ ...(options.managedAdmission ? { attempt: options.attempt } : {}),
1681
1550
  selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
1682
1551
  policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
1683
1552
  expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
@@ -1701,7 +1570,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1701
1570
  }
1702
1571
  // Recheck policy, prompt, installation and exact bindings after file preparation.
1703
1572
  inputs = await loadInputs();
1704
- await validate(saved, inputs.snapshot, inputs.drivers, clock());
1573
+ await validate(saved, inputs.snapshot, inputs.drivers, clock(), options.invocationPath);
1705
1574
  localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
1706
1575
  paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
1707
1576
  if (!localGate.ok) throw workerDispatchRefusal(localGate);
@@ -1718,13 +1587,14 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1718
1587
  // gated path's own test doubles, not a behavior this branch adds):
1719
1588
  // `await`ing a value that was never a Promise resolves to that exact
1720
1589
  // value with no observable difference.
1590
+ artifactLifecycle.spawning();
1721
1591
  const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
1722
1592
  command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
1723
1593
  memoryMaxGb: options.memoryMaxGb ?? 4, gate: localGate, config: inputs.snapshot.config, isolation,
1724
1594
  workerGroup: invocation.selected.group,
1725
1595
  allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
1726
1596
  ...(options.managedAdmission ? { onDispatchCommit: async () => {
1727
- const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
1597
+ const admitted = await assertManagedWorkerLaunch(options, invocation, inputs, options.projectRoot ?? process.cwd());
1728
1598
  options.managedDeadlineAtMs = Math.min(options.managedDeadlineAtMs, admitted.deadlineAtMs);
1729
1599
  } } : {}),
1730
1600
  ...(options.managedAdmission ? { onSpawnAttempt: () => { managedSpawnAttempted = true; } } : {}),
@@ -1739,9 +1609,10 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1739
1609
  ...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
1740
1610
  endedAt: new Date().toISOString(), isolation: isolation.mode, termination: managedSpawnAttempted ? 'uncertain' : 'confirmed',
1741
1611
  descendants: managedSpawnAttempted ? 'unknown' : 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
1612
+ artifactLifecycle.failure(error);
1742
1613
  throw error;
1743
1614
  } finally {
1744
- try { await artifacts?.cleanup(); } finally {
1615
+ try { await artifactLifecycle.finish(artifacts); } finally {
1745
1616
  // Never chmod a worker-replaced pathname; only the inodes we opened.
1746
1617
  for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
1747
1618
  await inspect(runDir, 'directory');
@@ -1761,7 +1632,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
1761
1632
  let identity = immediate?.pid === child.pid && immediate?.pid === immediate?.pgid
1762
1633
  && immediate?.uid === process.getuid?.() && immediate?.parentPid === process.pid ? immediate : null;
1763
1634
  let nativeSupervisor = hasTransport && outcome.mode !== 'systemd-unit' ? sampleNativeSupervisor(child.pid) : null;
1764
- const streams = [handles[0], handles[1], handles[3], handles[4]].map(handle => createWriteStream('', { fd: handle.fd, autoClose: false }));
1635
+ const streams = [handles[0], handles[1], handles[Object.keys(paths).length], handles[Object.keys(paths).length + 1]].map(handle => createWriteStream('', { fd: handle.fd, autoClose: false }));
1765
1636
  let identityObservation = 'pending';
1766
1637
  let reportedWorker = null;
1767
1638
  let output = Buffer.alloc(0);
@@ -1944,29 +1815,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
1944
1815
  publishFailures: ctx.publishFailures?.length ? ctx.publishFailures.slice() : null };
1945
1816
  let healthFinalValid = false;
1946
1817
  try {
1947
- const finalStat = await lstat(paths.final);
1948
- if (!finalStat.isFile() || finalStat.isSymbolicLink() || finalStat.nlink !== 1 || finalStat.size > 262144) throw new Error('Invalid final output');
1949
- await ctx.inspect(paths.final, 'file');
1950
- let extracted = false;
1951
- if (finalStat.size === 0 && driver.extractFinal) {
1952
- const final = driver.extractFinal(output.toString('utf8'));
1953
- if (final === null || final === undefined || final.success === false) throw new Error('Missing or unsuccessful final extraction');
1954
- const bytes = JSON.stringify({ invocationId, source: 'adapter-extraction', candidate: true, verified: false, result: final });
1955
- if (Buffer.byteLength(bytes) > 262144) throw new Error('Final exceeds bound');
1956
- await handles[2].writeFile(bytes); extracted = true;
1957
- }
1958
- const finalBytes = await boundedPrivateRead(paths.final, 262144, ctx.inspect, true);
1959
- if (!finalBytes.length) throw new Error('Missing final output');
1960
- if (!extracted && driver.extractFinal) {
1961
- const parsed = driver.extractFinal(finalBytes.toString('utf8'));
1962
- if (parsed === null || parsed === undefined || parsed.success === false) throw new Error('Invalid final output');
1963
- }
1964
- await publish('final-evidence.json', { invocationId, path: paths.final, digest: bytesDigest(finalBytes),
1965
- source: extracted ? 'adapter-extraction' : 'worker-output', candidate: true, verified: false,
1966
- terminalSuccess: result.exitCode === 0 && !result.timedOut && !result.errorCode });
1818
+ const { finalBytes, extracted, parseFinal } = await collectWorkerFinal({
1819
+ path: paths.final, workdir: invocation.cwd, driver, output, invocationId, result,
1820
+ inspect: ctx.inspect, handle: handles[2], publish, outputFraming: ctx.profile.health?.outputFraming,
1821
+ });
1967
1822
  if (driver.parseWorkerFinal) {
1968
1823
  try {
1969
- await driver.parseWorkerFinal(extracted ? output : finalBytes, {
1824
+ await parseFinal(extracted ? output : finalBytes, {
1970
1825
  format: extracted && ctx.profile.health?.outputFraming === 'jsonl' ? 'jsonl' : 'json', observation: { transport: 'shell',
1971
1826
  status: result.exitCode === 0 && !result.errorCode && !result.timedOut ? 'completed' : 'failed',
1972
1827
  termination: result.termination, exitCode: result.exitCode } });
@@ -4272,6 +4127,7 @@ export class SessionLauncher extends EventEmitter {
4272
4127
  prompt: options.prompt,
4273
4128
  // AIWG's tracking UUID is not an OMP native session to resume.
4274
4129
  sessionId: this.providerAdapter.getName() === 'omp' ? options.resumeSession : options.sessionId,
4130
+ resumeSession: options.resumeSession,
4275
4131
  model: options.model,
4276
4132
  budget: options.budget,
4277
4133
  maxTurns: options.maxTurns,