@aiwg/cli 2026.10.9 → 2026.10.11

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 (116) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +275 -17
  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/sessions.js +198 -15
  8. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  9. package/dist/src/cli/handlers/use.js +203 -234
  10. package/dist/src/cli/ui.js +1 -0
  11. package/dist/src/config/aiwg-config.js +38 -184
  12. package/dist/src/config/cli-help.js +54 -0
  13. package/dist/src/config/cli.js +5 -69
  14. package/dist/src/config/config-normalization.js +10 -0
  15. package/dist/src/config/worker-activation-gate.js +26 -0
  16. package/dist/src/config/worker-adoption-cli.js +102 -0
  17. package/dist/src/config/worker-adoption.js +368 -0
  18. package/dist/src/config/worker-config-digest.js +14 -0
  19. package/dist/src/config/worker-context.js +337 -0
  20. package/dist/src/config/worker-group-templates.js +150 -0
  21. package/dist/src/config/worker-guidance-warning.js +28 -0
  22. package/dist/src/config/worker-owner-lock.js +152 -0
  23. package/dist/src/config/worker-parallelism.js +154 -0
  24. package/dist/src/config/worker-write.js +75 -0
  25. package/dist/src/decision/telemetry/exporter.js +2 -1
  26. package/dist/src/decision/telemetry/index.js +1 -0
  27. package/dist/src/decision/telemetry/mapping.js +43 -0
  28. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  29. package/dist/src/decision/telemetry/redaction.js +6 -1
  30. package/dist/src/decision/telemetry/types.js +3 -0
  31. package/dist/src/decision/telemetry/worker.js +69 -0
  32. package/dist/src/lint/rule-activation.js +15 -7
  33. package/dist/src/lint/runner.js +2 -2
  34. package/dist/src/mcp/server.mjs +2 -11
  35. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  36. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  37. package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
  38. package/dist/src/mission-protocol/codecs.js +8 -38
  39. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  40. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  41. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  42. package/dist/src/models/conductor-data-policy.mjs +190 -0
  43. package/dist/src/models/config-loader.js +19 -3
  44. package/dist/src/models/config-writer.js +27 -0
  45. package/dist/src/models/model-capabilities.v1.json +275 -17
  46. package/dist/src/models/provider-policy.js +14 -155
  47. package/dist/src/models/resolver.js +2 -293
  48. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  49. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  50. package/dist/src/models/worker-direct-readiness.js +9 -218
  51. package/dist/src/models/worker-health-readiness.js +105 -26
  52. package/dist/src/models/worker-installation.mjs +12 -14
  53. package/dist/src/models/worker-member-references.js +64 -0
  54. package/dist/src/models/worker-plan.js +317 -0
  55. package/dist/src/models/worker-selection-policy.js +217 -0
  56. package/dist/src/models/wrapper-deployment.js +1 -1
  57. package/dist/src/policy/authorization.js +29 -1
  58. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  59. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  60. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  61. package/dist/src/serve/shared-host-scheduler.js +25 -107
  62. package/dist/src/serve/shared-host-starvation.js +73 -0
  63. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  64. package/dist/src/serve/worker-artifacts.mjs +56 -0
  65. package/dist/src/serve/worker-command-request.js +68 -0
  66. package/dist/src/serve/worker-invocation-paths.js +144 -0
  67. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  68. package/dist/src/serve/worker-invocation.js +24 -291
  69. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  70. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  71. package/dist/src/sessions/adapters/grokbot.js +7 -0
  72. package/dist/src/sessions/batch-import.js +34 -6
  73. package/dist/src/sessions/contracts.js +19 -0
  74. package/dist/src/sessions/coverage-preflight.js +7 -1
  75. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  76. package/dist/src/sessions/host-catalog.js +157 -0
  77. package/dist/src/sessions/image-blobs.js +141 -0
  78. package/dist/src/sessions/importer.js +23 -3
  79. package/dist/src/sessions/index.js +2 -0
  80. package/dist/src/sessions/refresh.js +25 -2
  81. package/dist/src/sessions/repository.js +164 -22
  82. package/dist/src/sessions/workspace-discovery.js +5 -2
  83. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  84. package/package.json +15 -1
  85. package/schemas/mission-v1.schema.json +20 -19
  86. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  87. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  88. package/tools/agents/providers/claude.mjs +3 -30
  89. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  90. package/tools/agents/providers/codex.mjs +2 -8
  91. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  92. package/tools/agents/providers/rule-delivery.mjs +3 -16
  93. package/tools/agents/providers/rule-presentation.mjs +38 -0
  94. package/tools/plugin/package-plugins.mjs +1 -1
  95. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  96. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  97. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  98. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  99. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  100. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  101. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  102. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  103. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  104. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  105. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  106. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  107. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  108. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  109. package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
  110. package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
  111. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  112. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  113. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  114. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  115. package/tools/ralph-external/session-launcher.mjs +124 -313
  116. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -46,7 +46,15 @@ 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 { verifyRetryPredecessor } from './lib/worker-retry-evidence.mjs';
55
+ import { observeWorkerTermination, terminationRemedy } from './lib/worker-termination.mjs';
56
+ import { buildWorkerInvocationEnvironment, WORKER_INVOCATION_MARKER } from './lib/worker-invocation-processes.mjs';
57
+ import { resolveRetryReference } from './lib/worker-launch-cli.mjs';
50
58
  import { launcherRecoveryEvidence, observeLauncherRecoveryOutcome, recordLauncherRecoveryHint } from './lib/launcher-recovery-evidence.mjs';
51
59
  export { observeLauncherRecoveryOutcome } from './lib/launcher-recovery-evidence.mjs';
52
60
  import { workerNeedsNativeNamespaces, probeWorkerNativeSandbox, nativeSandboxFailure, nativeSandboxProbeMessage, NATIVE_SANDBOX_UNAVAILABLE, checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
@@ -94,75 +102,9 @@ const bytesDigest = bytes => `sha256:${createHash('sha256').update(bytes).digest
94
102
  export const beneath = (root, filename) => { const r = relative(root, filename); return r === '' || !r.startsWith('..') && !isAbsolute(r); };
95
103
  const signalNumberToName = Object.fromEntries(Object.entries(osConstants.signals ?? {}).map(([name, number]) => [number, name]));
96
104
 
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
105
  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(); }
106
+ const paths = await importImpl(import.meta.url, 'serve/worker-invocation-paths.js');
107
+ return paths.realBeneath(root, filename);
166
108
  }
167
109
 
168
110
  export async function publishWorkerRecord(runDir, name, value, inspect) {
@@ -330,55 +272,8 @@ function checkDispatchGuard(dispatchGuard) {
330
272
  }
331
273
  }
332
274
 
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
- }
275
+ import { materializeWorkerArtifacts, WorkerArtifactLifecycle } from './lib/worker-artifacts.mjs';
276
+ export { materializeWorkerArtifacts } from './lib/worker-artifacts.mjs';
382
277
 
383
278
  const _workerRecoverySubscriptions = new WeakMap();
384
279
  const _workerRecoveryReporters = new WeakMap();
@@ -413,45 +308,6 @@ function subscribeWorkerRecovery(owner, breaker) {
413
308
  _workerRecoverySubscriptions.set(owner, () => reporter.owners.delete(owner));
414
309
  }
415
310
 
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
311
  async function workerDispatchPath(filename) {
456
312
  let candidate = filename;
457
313
  while (dirname(candidate) !== candidate) {
@@ -491,8 +347,25 @@ function workerLaunchRefusal(outcome) {
491
347
  // Neither worker output nor a caller-supplied result can populate this context.
492
348
  const directStartupCorrectionContext = new WeakMap();
493
349
 
350
+ // Each attempt directory is created exclusively; its parent is the conductor's job.
351
+ async function createAttemptRunDirectory(runDir) {
352
+ try { await mkdir(runDir, { mode: 0o700 }); } catch (error) {
353
+ if (error?.code === 'ENOENT') throw new Error(`Worker run directory parent does not exist: ${dirname(runDir)}; create it (mode 0700) before launch`);
354
+ throw error;
355
+ }
356
+ }
357
+
494
358
  async function launchOneWorker(owner, options) {
495
- const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validate } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
359
+ const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validatePlan } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
360
+ // Forward the saved-plan path: invocation identity is verified against it (#2905).
361
+ const validate = async (plan, snapshot, drivers, now, planPath = options.invocationPath) => {
362
+ const validateAt = (p, s, d, n) => validatePlan(p, s, d, n, planPath);
363
+ if (plan.mode !== 'managed' || !options.managedAdmission) return validateAt(plan, snapshot, drivers, now);
364
+ const checked = await validateManagedAttempt(options, plan, { snapshot, drivers }, now, validateAt,
365
+ attempt => publishWorkerRecord(dirname(options.invocationPath), `${basename(options.invocationPath)}.attempt.json`, attempt, inspect));
366
+ options.attempt = checked.attempt;
367
+ return checked.invocation;
368
+ };
496
369
  const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
497
370
  const planBytes = await boundedPrivateRead(options.invocationPath, 1048576, inspect);
498
371
  const saved = JSON.parse(planBytes.toString('utf8'));
@@ -502,9 +375,28 @@ async function launchOneWorker(owner, options) {
502
375
  let inputs = await loadInputs();
503
376
  const clock = () => typeof options.now === 'function' ? options.now() : options.now ?? new Date().toISOString();
504
377
  const now = clock();
505
- const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now);
378
+ const { assertConductorWorkerPlan } = await importImpl(import.meta.url, 'serve/worker-invocation-paths.js');
379
+ await assertConductorWorkerPlan(saved, options.invocationPath, inputs.drivers);
380
+ const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now, options.invocationPath);
381
+ // #3685 round 2: the worker CLI's AIWG bootstrap must be present in the
382
+ // worker workdir before any spawn — a context-free worker would run with
383
+ // no project rules/skills/hooks. Opt-in per entry point (the
384
+ // `--launch-worker` CLI enables it via `options.ensureWorkerContext`);
385
+ // absent means "no ensure", never a default deploy, so pinned baselines
386
+ // and synthetic launches are unaffected. A failed ensure throws the
387
+ // typed `OG_WORKER_CONTEXT_UNAVAILABLE` diagnostic (with remedy) and the
388
+ // worker never spawns.
389
+ if (options.ensureWorkerContext) {
390
+ const { ensureWorkerContextForLaunch } = await import('./lib/worker-context.mjs');
391
+ await ensureWorkerContextForLaunch({
392
+ workdir: invocation.cwd,
393
+ cli: invocation.selected?.cli,
394
+ config: inputs.snapshot?.config ?? null,
395
+ ...(typeof options.ensureWorkerContext === 'object' ? options.ensureWorkerContext : {}),
396
+ });
397
+ }
506
398
  if (invocation.mode === 'managed' || options.managedAdmission) {
507
- const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
399
+ const admitted = await assertManagedWorkerLaunch(options, invocation, inputs, options.projectRoot ?? process.cwd());
508
400
  options = { ...options, scopedBreaker: null, managedInvocationId: admitted.metadata.invocationId,
509
401
  managedDeadlineAtMs: admitted.deadlineAtMs };
510
402
  }
@@ -536,12 +428,12 @@ async function launchOneWorker(owner, options) {
536
428
  // check too.
537
429
  let verifiedPreviousAttemptId = null;
538
430
  if (options?.retryOf !== undefined) {
539
- const exitRecord = await verifyRetryPredecessor(options.retryOf,
540
- { newInvocationPath: options?.invocationPath, inspect, workerWritableRoots: writableRoots });
431
+ const exitRecord = await verifyRetryPredecessor(await resolveRetryReference(options.retryOf, { inspect }),
432
+ { newInvocationPath: options?.invocationPath, inspect, workerWritableRoots: writableRoots, checkDescendants: options.checkDescendants });
541
433
  verifiedPreviousAttemptId = exitRecord.invocationId;
542
434
  }
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 =>
435
+ const paths = (await importImpl(import.meta.url, 'serve/worker-invocation-paths.js')).workerOutputPaths(invocation);
436
+ if (new Set(Object.values(paths)).size !== Object.keys(paths).length || Object.values(paths).some(filename =>
545
437
  filename === options.invocationPath || filename === invocation.briefPath
546
438
  || dirname(filename) === runDir && basename(filename).startsWith('artifact-')
547
439
  || ['invocation.json', 'process.json', 'exit.json', 'health.json', 'final-evidence.json', 'worker-start.json', 'worker-identity.json', 'worker-exit.json',
@@ -571,7 +463,7 @@ async function launchOneWorker(owner, options) {
571
463
  const isolation = invocation.isolation === 'native-sandbox' ? { mode: 'native-sandbox' }
572
464
  : options.isolation ?? detectWorkerIsolation();
573
465
  if (isolation.mode === 'systemd-unit' && workerNeedsNativeNamespaces(adapter, permissions)) {
574
- const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ cwd: invocation.cwd,
466
+ const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ adapter, cwd: invocation.cwd,
575
467
  envNames: invocation.envNames, env: Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
576
468
  .map(name => [name, process.env[name]])) });
577
469
  if (!probe.ok) throw new WorkerPolicyError(NATIVE_SANDBOX_UNAVAILABLE, nativeSandboxProbeMessage(probe));
@@ -608,7 +500,7 @@ async function launchOneWorker(owner, options) {
608
500
  // themselves; pre-spawn refusals above never reach this point. Global
609
501
  // dispatch AND per-CLI enablement are checked inside `admitDirectWorker`
610
502
  // before any breaker construction (#2915 final review, finding 1).
611
- const invocationId = randomUUID();
503
+ const invocationId = invocation.invocationId ?? randomUUID();
612
504
  // #2915 review 4 (finding 8): computed ONCE, before admission, so the
613
505
  // SAME digest both gates the allowance reservation here and is published
614
506
  // into invocation.json below — never recomputed from a possibly-stale
@@ -718,13 +610,15 @@ async function launchOneWorker(owner, options) {
718
610
  throw error;
719
611
  }
720
612
  }
613
+ // Created before run-directory preparation so its failure path can report it.
614
+ const artifactLifecycle = new WorkerArtifactLifecycle(invocation, invocationId);
721
615
  // #2915 review 4 (finding 2): this run-directory preparation is
722
616
  // pre-spawn work happening AFTER a successful admission above — any
723
617
  // failure here (mkdir, ownership, the `.launch-owner` write) must settle
724
618
  // the reservation exactly like every other pre-spawn exit, never leak it
725
619
  // by throwing before the big try/catch below is reached.
726
620
  try {
727
- if (!runExists) await mkdir(runDir, { mode: 0o700 }); // exclusive attempt: no recursive creation or reuse
621
+ if (!runExists) await createAttemptRunDirectory(runDir); // exclusive attempt: no recursive creation or reuse
728
622
  const runStat = await lstat(runDir);
729
623
  if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
730
624
  await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
@@ -736,6 +630,7 @@ async function launchOneWorker(owner, options) {
736
630
  // release without leaving a marker even past the canary deadline.
737
631
  await settleLedger({ kind: 'uncertain', confirmedStop: true });
738
632
  if (ledgerDiagnostics.length) error.ledgerDiagnostics = ledgerDiagnostics;
633
+ artifactLifecycle.failure(error);
739
634
  throw error;
740
635
  }
741
636
  const supervisor = sampleWorkerIdentity(process.pid);
@@ -754,6 +649,7 @@ async function launchOneWorker(owner, options) {
754
649
  // own outcome.
755
650
  let latestProcessRecord = null;
756
651
  const publish = (name, value) => {
652
+ artifactLifecycle.observe(name, value);
757
653
  if (name === 'process.json') {
758
654
  value = { ...value, recovery: { ...admission.breaker.recoveryStatus(), observedAt: new Date().toISOString() } };
759
655
  if (options.startupCorrectionRecord) value.startupCorrection = options.startupCorrectionRecord;
@@ -790,6 +686,7 @@ async function launchOneWorker(owner, options) {
790
686
  // task — never from an unverified caller claim.
791
687
  previousAttemptId: verifiedPreviousAttemptId,
792
688
  requestId: invocationId, taskDigest, role: saved.request.role,
689
+ ...(options.managedAdmission ? { attempt: options.attempt } : {}),
793
690
  selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
794
691
  policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
795
692
  expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
@@ -798,13 +695,13 @@ async function launchOneWorker(owner, options) {
798
695
  executedArgvDigest: qualificationDigest(artifacts.argv),
799
696
  artifacts: (invocation.artifacts ?? []).map(artifact => ({ name: artifact.name,
800
697
  byteLength: Buffer.byteLength(artifact.content), sha256: bytesDigest(artifact.content) })),
801
- envNames: invocation.envNames, stdin: invocation.stdin,
698
+ envNames: [...new Set([...invocation.envNames, WORKER_INVOCATION_MARKER])], stdin: invocation.stdin,
802
699
  prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
803
700
  worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
804
701
  health: invocation.health ?? null, transport: invocation.transport,
805
702
  isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
806
703
  limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
807
- await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
704
+ await publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor, worker: null,
808
705
  launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
809
706
  // All captures (including the reserved final) open successfully BEFORE spawn.
810
707
  for (const filename of [...Object.values(paths), ...Object.values(captures)]) {
@@ -813,13 +710,12 @@ async function launchOneWorker(owner, options) {
813
710
  }
814
711
  // Recheck policy, prompt, installation and exact bindings after file preparation.
815
712
  inputs = await loadInputs();
816
- await validate(saved, inputs.snapshot, inputs.drivers, clock());
713
+ await validate(saved, inputs.snapshot, inputs.drivers, clock(), options.invocationPath);
817
714
  gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
818
715
  paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
819
716
  if (!gate.ok) throw workerDispatchRefusal(gate);
820
717
  if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
821
- const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
822
- .map(name => [name, process.env[name]]));
718
+ const environment = buildWorkerInvocationEnvironment(invocation.envNames, invocationId);
823
719
  const baselineContent = await sampleOwnedWorktreeContent(invocation.cwd, { files: options.healthArtifacts });
824
720
  // #2915 round 8 (D2, replaces the former "debit immediately before this
825
721
  // call" comment): the ledger debit now happens INSIDE the dispatch-
@@ -875,11 +771,12 @@ async function launchOneWorker(owner, options) {
875
771
  // real child's own queued events (exit/close) race ahead of
876
772
  // `captureOneWorker`'s own listener attachment in several tests (and
877
773
  // would in production).
774
+ artifactLifecycle.spawning();
878
775
  const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
879
776
  command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
880
777
  memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation, workerGroup: invocation.selected.group,
881
778
  allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
882
- env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir,
779
+ ...environment, stdin: invocation.stdin, runDir,
883
780
  ...(onDispatchCommit ? { onDispatchCommit } : {}) });
884
781
  outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
885
782
  if (!outcome?.child) throw workerLaunchRefusal(outcome);
@@ -988,6 +885,7 @@ async function launchOneWorker(owner, options) {
988
885
  const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
989
886
  const { confirmedStop } = await resolveStopEvidence({ isolationMode: outcome.mode,
990
887
  unitName: outcome.unit ?? null, child: outcome.child, graceMs });
888
+ artifactLifecycle.confirmedStop = confirmedStop;
991
889
  await settleLedger({ kind: 'uncertain', confirmedStop });
992
890
  // #2915 review 6 (finding 3d): when this bounded stop escalation DOES
993
891
  // confirm termination, also call the explicit reconciliation -- a
@@ -1005,7 +903,7 @@ async function launchOneWorker(owner, options) {
1005
903
  } finally {
1006
904
  admission.breaker.removeListener('transition', onScopeTransition);
1007
905
  await publishing;
1008
- try { await artifacts?.cleanup(); } finally {
906
+ try { await artifactLifecycle.finish(artifacts); } finally {
1009
907
  // Never chmod a worker-replaced pathname; only the inodes we opened.
1010
908
  for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
1011
909
  await inspect(runDir, 'directory');
@@ -1075,7 +973,7 @@ async function captureOneWorker(owner, ctx) {
1075
973
  // For systemd-unit mode the unit's MainPID is not known synchronously; it
1076
974
  // is still resolved once inside observeIdentity.
1077
975
  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 }));
976
+ 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
977
  let identityObservation = 'pending';
1080
978
  let reportedWorker = null;
1081
979
  let output = Buffer.alloc(0);
@@ -1209,7 +1107,7 @@ async function captureOneWorker(owner, ctx) {
1209
1107
  for (const stream of streams) stream.on('error', () => { errorCode = 'CAPTURE_WRITE'; stop(); });
1210
1108
  child.stdin?.on('error', error => { if (error.code !== 'EPIPE') { errorCode = 'STDIN_WRITE'; stop(); } });
1211
1109
  child.stdin?.end(ctx.stdinBytes ?? undefined);
1212
- const recordProcess = () => publish('process.json', { invocationId, origin: 'launcher', supervisor,
1110
+ const recordProcess = () => publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor,
1213
1111
  transportProcess: hasTransport ? sampleWorkerIdentity(child.pid) : null,
1214
1112
  nativeSupervisor,
1215
1113
  worker: identity ?? reportedWorker, identityObservation, launchTime, monotonicStart, ownership: identity ? 'owned-group' : 'unknown',
@@ -1234,12 +1132,14 @@ async function captureOneWorker(owner, ctx) {
1234
1132
  await drainChildStdio(child, graceMs);
1235
1133
  child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
1236
1134
  for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
1237
- const native = outcome.mode === 'systemd-unit' ? readNative() : null;
1135
+ // Prefer the unit's retained state whenever a unit name is recorded
1136
+ // (#3674 round 4): launch-mode selection is unchanged, but unit (cgroup)
1137
+ // evidence is used wherever it is available.
1138
+ const native = outcome.unit ? readNative() : null;
1238
1139
  const members = identity ? sampleWorkerGroup(identity.pgid) : null;
1239
- // An unconfined group can escape via setsid. Empty PGID alone never proves
1240
- // descendant termination; keep the explicit degraded stop uncertain.
1241
- const confirmed = outcome.mode === 'systemd-unit' ? native?.active === false && [1, 2, 3].includes(native.execMainCode)
1242
- : child.pid === undefined;
1140
+ // Require positive kernel evidence of owned containment being empty.
1141
+ const confirmed = (outcome.mode !== 'systemd-unit' && child.pid === undefined) || observeWorkerTermination({ mode: outcome.mode, native,
1142
+ workerExited: sawExit, identity, supervisor, invocationId }).confirmed;
1243
1143
  // `worker-exit.json`/`worker-identity.json` are written into a directory
1244
1144
  // the worker itself can reach (same UID, no filesystem confinement), so a
1245
1145
  // worker can plant either file before the trusted transport ever writes
@@ -1275,7 +1175,7 @@ async function captureOneWorker(owner, ctx) {
1275
1175
  : outcome.mode === 'systemd-unit' && native?.execMainCode === 1 ? native.execMainStatus : waitCode;
1276
1176
  const result = { invocationId, exitCode, signal: resultSignal,
1277
1177
  endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
1278
- descendants: confirmed ? 'stopped' : 'unknown', timedOut: resultTimedOut,
1178
+ descendants: confirmed ? 'none' : 'unknown', timedOut: resultTimedOut,
1279
1179
  cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
1280
1180
  // Unauthenticated: reported by the worker-writable transport record, not
1281
1181
  // kernel-observed. Never used above for exitCode/signal/termination.
@@ -1294,9 +1194,8 @@ async function captureOneWorker(owner, ctx) {
1294
1194
  // ExecMainCode/Status when applicable, launcher-detected errors,
1295
1195
  // timeout and explicit cancellation) — never worker stderr/stdout text
1296
1196
  // or worker-writable records. A degraded-isolation attempt can reach
1297
- // 'succeeded' here even though its `termination` stays 'uncertain'
1298
- // forever (no descendant-stop confirmation is possible without a
1299
- // systemd unit) — this is exactly the #2915 final-review fix: "give
1197
+ // 'succeeded' here even when descendant-stop evidence is unavailable.
1198
+ // This preserves the #2915 final-review fix: "give
1300
1199
  // degraded successful execution a valid recovery-success path without
1301
1200
  // falsely declaring descendant termination confirmed."
1302
1201
  const directChildCleanExit = sawExit && waitCode === 0 && waitSignal == null;
@@ -1309,26 +1208,10 @@ async function captureOneWorker(owner, ctx) {
1309
1208
  let healthFinalValid = false;
1310
1209
  let healthAttemptFinalValid = false;
1311
1210
  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 });
1211
+ const { finalBytes, extracted, parseFinal } = await collectWorkerFinal({
1212
+ path: paths.final, workdir: invocation.cwd, driver, output, invocationId, result,
1213
+ inspect: ctx.inspect, handle: handles[2], publish, outputFraming: ctx.profile.health?.outputFraming,
1214
+ });
1332
1215
  // Consume #2890's strict final contract when supplied by the installed
1333
1216
  // driver. Legacy extraction remains a candidate, never health authority.
1334
1217
  if (driver.parseWorkerFinal) {
@@ -1336,7 +1219,7 @@ async function captureOneWorker(owner, ctx) {
1336
1219
  // Strict validation consumes original bytes. A legacy extracted
1337
1220
  // object may already have lost duplicate keys and cannot authorize
1338
1221
  // completion through reserialization.
1339
- const claims = await driver.parseWorkerFinal(extracted ? output : finalBytes, {
1222
+ const claims = await parseFinal(extracted ? output : finalBytes, {
1340
1223
  format: extracted && ctx.profile.health?.outputFraming === 'jsonl' ? 'jsonl' : 'json', observation: { transport: 'shell',
1341
1224
  status: result.exitCode === 0 && !result.errorCode && !result.timedOut ? 'completed' : 'failed',
1342
1225
  termination: result.termination, exitCode: result.exitCode } });
@@ -1601,7 +1484,8 @@ async function captureOneWorker(owner, ctx) {
1601
1484
  //
1602
1485
  // `launchOneWorkerBaseline` and `captureOneWorkerBaseline` reproduce
1603
1486
  // `launchOneWorker`/`captureOneWorker`, apart from the lifecycle fixes above,
1604
- // as they existed at base commit `a4085b86a` (`git show
1487
+ // with shared workerClaims/digest validation (#3675) applied to both paths,
1488
+ // otherwise as they existed at base commit `a4085b86a` (`git show
1605
1489
  // a4085b86a:tools/ralph-external/session-launcher.mjs`) — before any
1606
1490
  // #2915 direct-recovery code existed in this file. `launchOneWorker`
1607
1491
  // above calls `launchOneWorkerBaseline` for every attempt
@@ -1633,6 +1517,7 @@ async function captureOneWorker(owner, ctx) {
1633
1517
  // `a4085b86a` called them — with NO `onDispatchCommit` — which that
1634
1518
  // module's own dual-mode contract (`_runDispatchCommit`) defines as
1635
1519
  // "behaves exactly as before the hook existed";
1520
+ // - shared final validation (`lib/worker-final.mjs`, #3675);
1636
1521
  // - unchanged imports (`process-monitor.mjs`, `lib/worker-health.mjs`,
1637
1522
  // `serve/worker-invocation.js`, `models/qualification-digest.mjs`).
1638
1523
  // Neither function imports, reads, or references the breaker, the
@@ -1647,12 +1532,13 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1647
1532
  loadInputs, validate, inspect, clock, qualificationDigest } = ctx;
1648
1533
  let inputs = ctx.inputs;
1649
1534
  let localGate = gate;
1650
- if (!runExists) await mkdir(runDir, { mode: 0o700 }); // exclusive attempt: no recursive creation or reuse
1535
+ if (!runExists) await createAttemptRunDirectory(runDir); // exclusive attempt: no recursive creation or reuse
1651
1536
  const runStat = await lstat(runDir);
1652
1537
  if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
1653
1538
  await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
1654
1539
  try { await handle.chmod(0o700); } finally { await handle.close(); } });
1655
- const invocationId = options.managedAdmission ? options.managedInvocationId : randomUUID();
1540
+ const invocationId = invocation.invocationId ?? (options.managedAdmission ? options.managedInvocationId : randomUUID());
1541
+ if (options.managedAdmission && invocationId !== options.managedInvocationId) throw new Error('Worker artifact invocation id differs from managed admission');
1656
1542
  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
1543
  try { await reservation.writeFile(invocationId); await reservation.sync(); } finally { await reservation.close(); }
1658
1544
  const supervisor = sampleWorkerIdentity(process.pid);
@@ -1664,7 +1550,9 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1664
1550
  const writeRecord = options.publishRecord ?? publishWorkerRecord;
1665
1551
  const publishFailures = [];
1666
1552
  let publishing = Promise.resolve();
1553
+ const artifactLifecycle = new WorkerArtifactLifecycle(invocation, invocationId);
1667
1554
  const publish = (name, value) => {
1555
+ artifactLifecycle.observe(name, value);
1668
1556
  const write = publishing.catch(() => {}).then(() => writeRecord(runDir, name, value, inspect));
1669
1557
  publishing = write.catch(error => { publishFailures.push({ name, error: error?.message ?? String(error) }); });
1670
1558
  return write;
@@ -1678,6 +1566,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1678
1566
  const displayArgv = artifacts.argv.map((token, i) => i === 0 ? token : `<argument:${bytesDigest(token)}>`);
1679
1567
  await publish('invocation.json', { version: 1, origin: 'launcher', invocationId, attemptId: invocationId,
1680
1568
  previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: options.managedAdmission?.requestId ?? invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
1569
+ ...(options.managedAdmission ? { attempt: options.attempt } : {}),
1681
1570
  selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
1682
1571
  policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
1683
1572
  expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
@@ -1686,13 +1575,13 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1686
1575
  executedArgvDigest: qualificationDigest(artifacts.argv),
1687
1576
  artifacts: (invocation.artifacts ?? []).map(artifact => ({ name: artifact.name,
1688
1577
  byteLength: Buffer.byteLength(artifact.content), sha256: bytesDigest(artifact.content) })),
1689
- envNames: invocation.envNames, stdin: invocation.stdin,
1578
+ envNames: [...new Set([...invocation.envNames, WORKER_INVOCATION_MARKER])], stdin: invocation.stdin,
1690
1579
  prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
1691
1580
  worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
1692
1581
  health: invocation.health ?? null, transport: invocation.transport,
1693
1582
  isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
1694
1583
  limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
1695
- await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
1584
+ await publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor, worker: null,
1696
1585
  launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
1697
1586
  // All captures (including the reserved final) open successfully BEFORE spawn.
1698
1587
  for (const filename of [...Object.values(paths), ...Object.values(captures)]) {
@@ -1701,13 +1590,12 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1701
1590
  }
1702
1591
  // Recheck policy, prompt, installation and exact bindings after file preparation.
1703
1592
  inputs = await loadInputs();
1704
- await validate(saved, inputs.snapshot, inputs.drivers, clock());
1593
+ await validate(saved, inputs.snapshot, inputs.drivers, clock(), options.invocationPath);
1705
1594
  localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
1706
1595
  paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
1707
1596
  if (!localGate.ok) throw workerDispatchRefusal(localGate);
1708
1597
  if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
1709
- const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
1710
- .map(name => [name, process.env[name]]));
1598
+ const environment = buildWorkerInvocationEnvironment(invocation.envNames, invocationId);
1711
1599
  const baselineContent = await sampleOwnedWorktreeContent(invocation.cwd, { files: options.healthArtifacts });
1712
1600
  // The real `launchIsolatedWorker`, called with no `onDispatchCommit`
1713
1601
  // (never passed anywhere in this frozen function), is synchronous —
@@ -1718,17 +1606,18 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1718
1606
  // gated path's own test doubles, not a behavior this branch adds):
1719
1607
  // `await`ing a value that was never a Promise resolves to that exact
1720
1608
  // value with no observable difference.
1609
+ artifactLifecycle.spawning();
1721
1610
  const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
1722
1611
  command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
1723
1612
  memoryMaxGb: options.memoryMaxGb ?? 4, gate: localGate, config: inputs.snapshot.config, isolation,
1724
1613
  workerGroup: invocation.selected.group,
1725
1614
  allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
1726
1615
  ...(options.managedAdmission ? { onDispatchCommit: async () => {
1727
- const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
1616
+ const admitted = await assertManagedWorkerLaunch(options, invocation, inputs, options.projectRoot ?? process.cwd());
1728
1617
  options.managedDeadlineAtMs = Math.min(options.managedDeadlineAtMs, admitted.deadlineAtMs);
1729
1618
  } } : {}),
1730
1619
  ...(options.managedAdmission ? { onSpawnAttempt: () => { managedSpawnAttempted = true; } } : {}),
1731
- env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir });
1620
+ ...environment, stdin: invocation.stdin, runDir });
1732
1621
  outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
1733
1622
  if (!outcome?.child) throw workerLaunchRefusal(outcome);
1734
1623
  return await captureOneWorkerBaseline(owner, { child: outcome.child, outcome, invocation, invocationId, supervisor,
@@ -1739,9 +1628,10 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1739
1628
  ...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
1740
1629
  endedAt: new Date().toISOString(), isolation: isolation.mode, termination: managedSpawnAttempted ? 'uncertain' : 'confirmed',
1741
1630
  descendants: managedSpawnAttempted ? 'unknown' : 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
1631
+ artifactLifecycle.failure(error);
1742
1632
  throw error;
1743
1633
  } finally {
1744
- try { await artifacts?.cleanup(); } finally {
1634
+ try { await artifactLifecycle.finish(artifacts); } finally {
1745
1635
  // Never chmod a worker-replaced pathname; only the inodes we opened.
1746
1636
  for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
1747
1637
  await inspect(runDir, 'directory');
@@ -1761,7 +1651,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
1761
1651
  let identity = immediate?.pid === child.pid && immediate?.pid === immediate?.pgid
1762
1652
  && immediate?.uid === process.getuid?.() && immediate?.parentPid === process.pid ? immediate : null;
1763
1653
  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 }));
1654
+ 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
1655
  let identityObservation = 'pending';
1766
1656
  let reportedWorker = null;
1767
1657
  let output = Buffer.alloc(0);
@@ -1886,7 +1776,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
1886
1776
  for (const stream of streams) stream.on('error', () => { errorCode = 'CAPTURE_WRITE'; stop(); });
1887
1777
  child.stdin?.on('error', error => { if (error.code !== 'EPIPE') { errorCode = 'STDIN_WRITE'; stop(); } });
1888
1778
  child.stdin?.end(ctx.stdinBytes ?? undefined);
1889
- const recordProcess = () => publish('process.json', { invocationId, origin: 'launcher', supervisor,
1779
+ const recordProcess = () => publish('process.json', { invocationId, invocationMarker: invocationId, origin: 'launcher', supervisor,
1890
1780
  transportProcess: hasTransport ? sampleWorkerIdentity(child.pid) : null,
1891
1781
  nativeSupervisor,
1892
1782
  worker: identity ?? reportedWorker, identityObservation, launchTime, monotonicStart, ownership: identity ? 'owned-group' : 'unknown',
@@ -1912,10 +1802,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
1912
1802
  await drainChildStdio(child, graceMs);
1913
1803
  child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
1914
1804
  for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
1915
- const native = outcome.mode === 'systemd-unit' ? readNative() : null;
1805
+ // Prefer the unit's retained state whenever a unit name is recorded
1806
+ // (#3674 round 4): launch-mode selection is unchanged, but unit (cgroup)
1807
+ // evidence is used wherever it is available.
1808
+ const native = outcome.unit ? readNative() : null;
1916
1809
  const members = identity ? sampleWorkerGroup(identity.pgid) : null;
1917
- const confirmed = outcome.mode === 'systemd-unit' ? native?.active === false && [1, 2, 3].includes(native.execMainCode)
1918
- : child.pid === undefined;
1810
+ const confirmed = (outcome.mode !== 'systemd-unit' && child.pid === undefined) || observeWorkerTermination({ mode: outcome.mode, native,
1811
+ workerExited: sawExit, identity, supervisor, invocationId }).confirmed;
1919
1812
  const workerExit = hasTransport ? await transportRecord('worker-exit.json') : null;
1920
1813
  if (!identity && hasTransport && nativeSupervisor) {
1921
1814
  const workerStart = await transportRecord('worker-identity.json');
@@ -1934,7 +1827,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
1934
1827
  const result = { invocationId, exitCode, signal: resultSignal,
1935
1828
  ...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
1936
1829
  endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
1937
- descendants: confirmed ? 'stopped' : 'unknown', timedOut: resultTimedOut,
1830
+ descendants: confirmed ? 'none' : 'unknown', timedOut: resultTimedOut,
1938
1831
  cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
1939
1832
  workerTermination: hasTransport ? workerExit && !workerExit.errorCode ? 'confirmed' : 'uncertain' : sawExit ? 'confirmed' : 'uncertain',
1940
1833
  workerReportedExit: workerExit ? { exitCode: workerExit.exitCode, signal: workerExit.signal, errorCode: workerExit.errorCode ?? null } : null,
@@ -1944,29 +1837,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
1944
1837
  publishFailures: ctx.publishFailures?.length ? ctx.publishFailures.slice() : null };
1945
1838
  let healthFinalValid = false;
1946
1839
  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 });
1840
+ const { finalBytes, extracted, parseFinal } = await collectWorkerFinal({
1841
+ path: paths.final, workdir: invocation.cwd, driver, output, invocationId, result,
1842
+ inspect: ctx.inspect, handle: handles[2], publish, outputFraming: ctx.profile.health?.outputFraming,
1843
+ });
1967
1844
  if (driver.parseWorkerFinal) {
1968
1845
  try {
1969
- await driver.parseWorkerFinal(extracted ? output : finalBytes, {
1846
+ await parseFinal(extracted ? output : finalBytes, {
1970
1847
  format: extracted && ctx.profile.health?.outputFraming === 'jsonl' ? 'jsonl' : 'json', observation: { transport: 'shell',
1971
1848
  status: result.exitCode === 0 && !result.errorCode && !result.timedOut ? 'completed' : 'failed',
1972
1849
  termination: result.termination, exitCode: result.exitCode } });
@@ -2160,13 +2037,13 @@ export const UNBOUND_RETRY_EVIDENCE_CODE = 'UNBOUND_RETRY_EVIDENCE';
2160
2037
  * retry and is surfaced with a code the conductor can act on — it must never
2161
2038
  * silently spawn a duplicate writer.
2162
2039
  */
2163
- export function stopBeforeRetryGate(previousResult, { expectedInvocationId = null } = {}) {
2040
+ export function stopBeforeRetryGate(previousResult, { expectedInvocationId = null, runDir, missing } = {}) {
2164
2041
  if (previousResult?.termination !== 'confirmed') {
2165
2042
  return {
2166
2043
  ok: false,
2167
2044
  code: STOP_BEFORE_RETRY_CODE,
2168
2045
  reason: `Previous attempt termination is '${previousResult?.termination ?? 'unknown'}', not confirmed: `
2169
- + 'retry is blocked until owned stop is proven (no duplicate writer while the old tree may continue).',
2046
+ + 'retry is blocked until owned stop is proven (no duplicate writer while the old tree may continue). ' + (missing ? `Missing: ${missing}. ` : '') + terminationRemedy(previousResult, runDir),
2170
2047
  previousResult: previousResult === undefined ? undefined : (previousResult ?? null),
2171
2048
  };
2172
2049
  }
@@ -3283,74 +3160,7 @@ export async function releaseLedgerPolicy({ captureRoot, taskChainDigest } = {})
3283
3160
  * Returns the predecessor's verified `exit.json` record on success; throws
3284
3161
  * on any refusal.
3285
3162
  */
3286
- export async function verifyRetryPredecessor(retryOf, { newInvocationPath, inspect, workerWritableRoots } = {}) {
3287
- const fail = (reason) => {
3288
- const error = new Error(`Retry predecessor evidence refused: ${reason}`);
3289
- error.code = UNBOUND_RETRY_EVIDENCE_CODE;
3290
- throw error;
3291
- };
3292
- if (!retryOf || typeof retryOf.invocationId !== 'string' || !retryOf.invocationId
3293
- || typeof retryOf.runDir !== 'string' || !retryOf.runDir) {
3294
- fail('retryOf requires both a non-empty invocationId and runDir');
3295
- }
3296
- const { invocationId: prevId, runDir: prevRunDir } = retryOf;
3297
- // #2915 review 4 (finding 4): the minimal directory-authority repair —
3298
- // reject a predecessor location inside any worker-writable root, exactly
3299
- // like `launchOneWorker` already refuses for a NEW run directory. This
3300
- // closes the easiest forgery (planting a complete, well-formed fake
3301
- // predecessor inside the worktree the worker itself can write to) but is
3302
- // explicitly NOT a defense against an unconfined malicious same-UID
3303
- // worker in general: such a worker can still write anywhere this
3304
- // conductor process can (e.g. directly under a configured capture root,
3305
- // if that root is not itself outside every worker-writable path, or by
3306
- // racing a legitimate predecessor's own directory before this check
3307
- // runs). A real guarantee needs an enforced filesystem/process boundary
3308
- // protecting conductor-owned metadata, or a different execution
3309
- // identity — same-UID alone never provides it.
3310
- if (Array.isArray(workerWritableRoots)) {
3311
- for (const root of workerWritableRoots) {
3312
- if (await realBeneath(root, prevRunDir)) fail('predecessor location is inside a worker-writable root');
3313
- }
3314
- }
3315
- if (!await inspect(prevRunDir, 'directory')) fail('predecessor run directory is missing');
3316
- const dirStat = await lstat(prevRunDir);
3317
- if (dirStat.uid !== process.getuid() || (dirStat.mode & 0o077)) fail('predecessor run directory is not privately owned');
3318
- let ownerBytes, invocationBytes, exitBytes;
3319
- try {
3320
- // #2915 review 4 (finding 4): `rejectInsecureMode: true` — an insecure
3321
- // mode on predecessor metadata REFUSES the retry instead of silently
3322
- // chmod-repairing it and proceeding (the former behaviour the review
3323
- // found: "the probe's 0666 files were accepted and changed to 0600").
3324
- ownerBytes = await boundedPrivateRead(join(prevRunDir, '.launch-owner'), 256, inspect, true, true);
3325
- invocationBytes = await boundedPrivateRead(join(prevRunDir, 'invocation.json'), 1048576, inspect, true, true);
3326
- exitBytes = await boundedPrivateRead(join(prevRunDir, 'exit.json'), 1048576, inspect, true, true);
3327
- } catch (error) { fail(`predecessor metadata is missing, insecurely-moded, or not a bounded, privately-owned, unaliased regular file (${error.message})`); }
3328
- if (ownerBytes.toString('utf8') !== prevId) fail('.launch-owner does not match retryOf.invocationId');
3329
- let invocationRecord, exitRecord;
3330
- try {
3331
- invocationRecord = JSON.parse(invocationBytes.toString('utf8'));
3332
- exitRecord = JSON.parse(exitBytes.toString('utf8'));
3333
- } catch { fail('predecessor metadata is not valid JSON'); }
3334
- if (invocationRecord?.invocationId !== prevId || invocationRecord?.origin !== 'launcher') {
3335
- fail('invocation.json identity does not match retryOf (or is not launcher-written)');
3336
- }
3337
- if (exitRecord?.invocationId !== prevId) fail('exit.json identity does not match retryOf');
3338
- if (newInvocationPath !== undefined) {
3339
- // #2915 review 4 (finding 4): the SAME stable task-chain identity
3340
- // `launchOneWorker` publishes — role/task/group plus the brief CONTENT
3341
- // digest, never `final_output`/`brief`/`workdir` PATHS, which legitimately
3342
- // differ between a confirmed predecessor and this fresh attempt.
3343
- let newTaskDigest;
3344
- try {
3345
- const newPlanBytes = await boundedPrivateRead(newInvocationPath, 1048576, inspect);
3346
- const newPlan = JSON.parse(newPlanBytes.toString('utf8'));
3347
- newTaskDigest = await stableTaskChainDigest(newPlan.request, newPlan.invocation?.briefDigest ?? null);
3348
- } catch { fail('new attempt plan is missing or invalid; cannot bind the retry to a task'); }
3349
- if (invocationRecord.taskDigest !== newTaskDigest) fail('predecessor task does not match the new attempt (task-chain binding)');
3350
- }
3351
- assertStopBeforeRetry(exitRecord, { expectedInvocationId: prevId });
3352
- return exitRecord;
3353
- }
3163
+ export { verifyRetryPredecessor } from './lib/worker-retry-evidence.mjs';
3354
3164
 
3355
3165
  /**
3356
3166
  * One corrected startup retry (#2915, 03 §11.5 step 1). A failed start may
@@ -4272,6 +4082,7 @@ export class SessionLauncher extends EventEmitter {
4272
4082
  prompt: options.prompt,
4273
4083
  // AIWG's tracking UUID is not an OMP native session to resume.
4274
4084
  sessionId: this.providerAdapter.getName() === 'omp' ? options.resumeSession : options.sessionId,
4085
+ resumeSession: options.resumeSession,
4275
4086
  model: options.model,
4276
4087
  budget: options.budget,
4277
4088
  maxTurns: options.maxTurns,