@aiwg/cli 2026.10.5 → 2026.10.6

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 (212) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +3 -1
  6. package/dist/src/artifacts/browser-export.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +8 -1
  8. package/dist/src/artifacts/cli.js +57 -6
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  10. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  11. package/dist/src/artifacts/graph-query.js +18 -8
  12. package/dist/src/artifacts/index-adapters.js +339 -0
  13. package/dist/src/artifacts/index-builder.js +143 -14
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-reader.js +44 -0
  16. package/dist/src/artifacts/index-refresh.js +114 -55
  17. package/dist/src/artifacts/index-status.js +20 -5
  18. package/dist/src/artifacts/memory-maintenance.js +503 -0
  19. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  20. package/dist/src/artifacts/query-engine.js +263 -108
  21. package/dist/src/artifacts/relations-builder.js +2 -2
  22. package/dist/src/artifacts/source-graph.js +2 -2
  23. package/dist/src/artifacts/types.js +9 -1
  24. package/dist/src/cli/handlers/decision.js +19 -1
  25. package/dist/src/cli/handlers/guided-setup.js +470 -0
  26. package/dist/src/cli/handlers/help.js +2 -2
  27. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  28. package/dist/src/cli/handlers/index.js +4 -2
  29. package/dist/src/cli/handlers/init.js +41 -6
  30. package/dist/src/cli/handlers/local-executor.js +5 -3
  31. package/dist/src/cli/handlers/mc.js +509 -53
  32. package/dist/src/cli/handlers/models.js +32 -2
  33. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  34. package/dist/src/cli/handlers/rules.js +1 -1
  35. package/dist/src/cli/handlers/script-runner.js +73 -2
  36. package/dist/src/cli/handlers/serve.js +5 -3
  37. package/dist/src/cli/handlers/sessions.js +1 -0
  38. package/dist/src/cli/handlers/setup.js +428 -1
  39. package/dist/src/cli/handlers/skill-lint.js +1 -1
  40. package/dist/src/cli/handlers/subcommands.js +118 -8
  41. package/dist/src/cli/handlers/use.js +279 -93
  42. package/dist/src/cli/handlers/workspace.js +3 -1
  43. package/dist/src/cli/prompt-utils.js +8 -0
  44. package/dist/src/cli/router.js +16 -1
  45. package/dist/src/config/aiwg-config.js +42 -0
  46. package/dist/src/config/cli.js +7 -3
  47. package/dist/src/config/decision-config.js +14 -1
  48. package/dist/src/config/native-sandbox-warnings.js +25 -0
  49. package/dist/src/contributors/validation.js +6 -5
  50. package/dist/src/decision/admission.js +7 -3
  51. package/dist/src/decision/benchmark.js +469 -0
  52. package/dist/src/decision/ledger.js +22 -3
  53. package/dist/src/decision/pricing.js +147 -0
  54. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  55. package/dist/src/decision/qualification/load-staged.js +539 -0
  56. package/dist/src/decision/qualification/load.js +109 -33
  57. package/dist/src/decision/qualification/runner.js +5 -2
  58. package/dist/src/decision/review/service.js +6 -4
  59. package/dist/src/decision/review/validate.js +23 -2
  60. package/dist/src/decision/savings.js +8 -3
  61. package/dist/src/discovery/index.js +492 -0
  62. package/dist/src/discovery/interview.js +233 -0
  63. package/dist/src/discovery/plan-apply.js +571 -0
  64. package/dist/src/discovery/plan-store.js +171 -0
  65. package/dist/src/discovery/plan-types.js +2 -0
  66. package/dist/src/discovery/plan-validation.js +139 -0
  67. package/dist/src/discovery/types.js +2 -0
  68. package/dist/src/extensions/commands/definitions.js +49 -9
  69. package/dist/src/extensions/manifest.js +71 -44
  70. package/dist/src/extensions/project-local-discovery.js +1 -1
  71. package/dist/src/extensions/project-quickref.js +18 -17
  72. package/dist/src/extensions/validation.js +32 -29
  73. package/dist/src/housekeeper/daemon-config.js +148 -0
  74. package/dist/src/housekeeper/ipc.js +84 -0
  75. package/dist/src/housekeeper/lifecycle.js +148 -0
  76. package/dist/src/housekeeper/platform.js +35 -0
  77. package/dist/src/housekeeper/status.js +70 -0
  78. package/dist/src/housekeeper/units.js +114 -0
  79. package/dist/src/jobs/flow.js +22 -21
  80. package/dist/src/lint/rule-activation.js +20 -5
  81. package/dist/src/lint/runner.js +1 -1
  82. package/dist/src/mcp/cli.mjs +5 -6
  83. package/dist/src/mcp/config-file.mjs +6 -2
  84. package/dist/src/mcp/config-layers.mjs +23 -7
  85. package/dist/src/mcp/profiles.js +18 -31
  86. package/dist/src/mcp/profiles.mjs +18 -32
  87. package/dist/src/mcp/registry.js +7 -27
  88. package/dist/src/mcp/registry.mjs +7 -28
  89. package/dist/src/mcp/server.mjs +34 -1
  90. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  91. package/dist/src/mcp/tools/command-run.mjs +3 -2
  92. package/dist/src/mcp/tools/decision.mjs +49 -48
  93. package/dist/src/mcp/tools/discovery.mjs +46 -21
  94. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  95. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  96. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  97. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  98. package/dist/src/memory/audit.js +719 -0
  99. package/dist/src/memory/canonical-context.js +392 -13
  100. package/dist/src/memory/canonical-maintenance.js +95 -0
  101. package/dist/src/memory/cli-result.js +22 -0
  102. package/dist/src/memory/cli.js +176 -0
  103. package/dist/src/memory/context-pack.js +57 -6
  104. package/dist/src/memory/cost.js +512 -0
  105. package/dist/src/memory/firewall-classification.d.mts +7 -0
  106. package/dist/src/memory/firewall-classification.mjs +32 -0
  107. package/dist/src/memory/index.js +8 -0
  108. package/dist/src/memory/line-maintenance.js +68 -0
  109. package/dist/src/memory/maintenance-cli.js +286 -0
  110. package/dist/src/memory/maintenance-contract.js +136 -0
  111. package/dist/src/memory/maintenance-index.js +18 -0
  112. package/dist/src/memory/maintenance-owner.js +2 -0
  113. package/dist/src/memory/maintenance-record.js +138 -0
  114. package/dist/src/memory/maintenance.js +254 -0
  115. package/dist/src/memory/provider-memory-adapter.js +154 -0
  116. package/dist/src/memory/recommend-cli.js +83 -0
  117. package/dist/src/memory/recommend.js +269 -0
  118. package/dist/src/memory/scheduled.js +81 -0
  119. package/dist/src/memory/taxonomy.js +44 -0
  120. package/dist/src/mission-protocol/codecs.js +19 -9
  121. package/dist/src/models/config-loader.js +11 -1
  122. package/dist/src/models/model-capabilities.v1.json +74 -50
  123. package/dist/src/models/provider-policy.js +125 -62
  124. package/dist/src/models/resolver.js +45 -8
  125. package/dist/src/models/worker-installation.d.mts +7 -0
  126. package/dist/src/models/worker-installation.mjs +169 -37
  127. package/dist/src/models/wrapper-route.js +1 -1
  128. package/dist/src/output-modes/registry.js +8 -7
  129. package/dist/src/plugin/metadata-validator.js +1 -1
  130. package/dist/src/providers/deploy-version.d.mts +1 -0
  131. package/dist/src/providers/deploy-version.mjs +17 -0
  132. package/dist/src/providers/provider-definitions.js +19 -16
  133. package/dist/src/providers/skill-ownership.d.mts +11 -2
  134. package/dist/src/providers/skill-ownership.mjs +35 -17
  135. package/dist/src/rules/activation.d.mts +11 -2
  136. package/dist/src/rules/activation.mjs +82 -6
  137. package/dist/src/rules/inventory.js +1 -1
  138. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  139. package/dist/src/schema/zod-validation.d.mts +6 -0
  140. package/dist/src/schema/zod-validation.mjs +50 -0
  141. package/dist/src/serve/executor-registry.js +297 -20
  142. package/dist/src/serve/executor-token.d.mts +1 -0
  143. package/dist/src/serve/executor-token.mjs +8 -0
  144. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  145. package/dist/src/serve/worker-invocation.js +114 -15
  146. package/dist/src/sessions/adapters/claude.js +19 -18
  147. package/dist/src/sessions/adapters/codex.js +10 -9
  148. package/dist/src/sessions/adapters/copilot.js +14 -13
  149. package/dist/src/sessions/adapters/cursor.js +19 -18
  150. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  151. package/dist/src/sessions/adapters/factory.js +11 -10
  152. package/dist/src/sessions/adapters/generic.js +32 -23
  153. package/dist/src/sessions/adapters/grokbot.js +35 -35
  154. package/dist/src/sessions/adapters/hermes.js +20 -19
  155. package/dist/src/sessions/adapters/muse.js +50 -49
  156. package/dist/src/sessions/adapters/omp.js +5 -4
  157. package/dist/src/sessions/adapters/openclaw.js +30 -29
  158. package/dist/src/sessions/adapters/opencode.js +26 -26
  159. package/dist/src/sessions/adapters/openhuman.js +24 -23
  160. package/dist/src/sessions/adapters/pi.js +5 -4
  161. package/dist/src/sessions/adapters/windsurf.js +8 -8
  162. package/dist/src/sessions/analytics.js +10 -9
  163. package/dist/src/sessions/candidates.js +7 -6
  164. package/dist/src/sessions/contracts.js +63 -53
  165. package/dist/src/sessions/output-registration.js +11 -10
  166. package/dist/src/sessions/workspace-discovery.js +3 -4
  167. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  168. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  169. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  170. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  171. package/dist/src/update/checker.d.mts +4 -0
  172. package/dist/src/update/checker.mjs +14 -0
  173. package/dist/src/writing/exemplar-selection.js +6 -6
  174. package/dist/src/writing/voice-evaluation.js +24 -23
  175. package/dist/src/writing/writer-migration.js +14 -13
  176. package/dist/src/writing/writer-profile.js +19 -18
  177. package/dist/src/writing/writing-brief.js +18 -17
  178. package/dist/src/writing/writing-receipt.js +47 -46
  179. package/package.json +5 -2
  180. package/schemas/decision/DecisionReview.schema.json +78 -1
  181. package/schemas/mission-v1.schema.json +3 -0
  182. package/tools/agents/deploy-agents.mjs +67 -39
  183. package/tools/agents/providers/antigravity.mjs +6 -1
  184. package/tools/agents/providers/base.mjs +19 -13
  185. package/tools/agents/providers/claude.mjs +1 -1
  186. package/tools/agents/providers/codex.mjs +50 -56
  187. package/tools/agents/providers/copilot.mjs +8 -2
  188. package/tools/agents/providers/cursor.mjs +1 -1
  189. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  190. package/tools/agents/providers/factory.mjs +7 -1
  191. package/tools/agents/providers/grok-build.mjs +18 -2
  192. package/tools/agents/providers/grokbot.mjs +1 -1
  193. package/tools/agents/providers/hermes.mjs +7 -13
  194. package/tools/agents/providers/muse-hooks.mjs +21 -14
  195. package/tools/agents/providers/muse.mjs +7 -2
  196. package/tools/agents/providers/omp.mjs +28 -4
  197. package/tools/agents/providers/opencode.mjs +14 -5
  198. package/tools/agents/providers/openhuman.mjs +1 -1
  199. package/tools/agents/providers/pi.mjs +1 -1
  200. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  201. package/tools/agents/providers/rule-delivery.mjs +263 -71
  202. package/tools/agents/providers/windsurf.mjs +7 -0
  203. package/tools/cli/workspace-status.mjs +5 -0
  204. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  205. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  206. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  207. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  208. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  209. package/tools/ralph-external/process-monitor.mjs +7 -4
  210. package/tools/ralph-external/session-launcher.mjs +280 -32
  211. package/tools/skills/deploy-skills-codex.mjs +11 -7
  212. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -34,9 +34,9 @@
34
34
  import { StringDecoder } from 'node:string_decoder';
35
35
  import { spawn } from 'child_process';
36
36
  import { createWriteStream, mkdirSync, existsSync, readFileSync, copyFileSync, constants } from 'fs';
37
- import { open, mkdir, rename, unlink, lstat, readdir, realpath, link } from 'node:fs/promises';
37
+ import { open, mkdir, rename, unlink, lstat, readdir, realpath, link, access } from 'node:fs/promises';
38
38
  import { randomUUID, createHash } from 'node:crypto';
39
- import { fileURLToPath } from 'node:url';
39
+ import { fileURLToPath, pathToFileURL } from 'node:url';
40
40
  import { basename, dirname, join, relative, isAbsolute } from 'path';
41
41
  import { EventEmitter } from 'events';
42
42
  import { homedir, constants as osConstants } from 'os';
@@ -49,7 +49,7 @@ import { ScopedFailureBreaker, attributeFailureScope } from './lib/consecutive-f
49
49
  import { buildSessionEnvironment } from './lib/session-environment.mjs';
50
50
  import { launcherRecoveryEvidence, observeLauncherRecoveryOutcome, recordLauncherRecoveryHint } from './lib/launcher-recovery-evidence.mjs';
51
51
  export { observeLauncherRecoveryOutcome } from './lib/launcher-recovery-evidence.mjs';
52
- import { checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
52
+ import { workerNeedsNativeNamespaces, probeWorkerNativeSandbox, nativeSandboxFailure, nativeSandboxProbeMessage, NATIVE_SANDBOX_UNAVAILABLE, checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
53
53
  const { signalChild } = await importImpl(import.meta.url, 'process/signal-child.mjs');
54
54
 
55
55
  // Resolve the same installation owner as CLI/MCP inputs in source and packaged layouts.
@@ -413,6 +413,80 @@ function subscribeWorkerRecovery(owner, breaker) {
413
413
  _workerRecoverySubscriptions.set(owner, () => reporter.owners.delete(owner));
414
414
  }
415
415
 
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
+ async function workerDispatchPath(filename) {
456
+ let candidate = filename;
457
+ while (dirname(candidate) !== candidate) {
458
+ try {
459
+ if (!(await lstat(candidate)).isDirectory()) return filename;
460
+ await access(candidate, constants.R_OK | constants.X_OK);
461
+ return candidate;
462
+ } catch (error) {
463
+ if (error.code !== 'ENOENT') return filename;
464
+ candidate = dirname(candidate);
465
+ }
466
+ }
467
+ // Keep an unusable path for fail-closed sampling rather than falling back to /.
468
+ return filename;
469
+ }
470
+
471
+ function workerDispatchRefusal(gate) {
472
+ const bounded = value => String(value ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 256);
473
+ const reasons = (gate.failures ?? []).slice(0, 8)
474
+ .map(failure => `${bounded(failure.signal)}: ${bounded(failure.reason)}`).join('; ').slice(0, 1024);
475
+ return new Error(`Worker dispatch resource gate refused launch${reasons ? `: ${reasons}` : ''}`);
476
+ }
477
+
478
+ function workerLaunchRefusal(outcome) {
479
+ const bounded = value => String(value ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 256);
480
+ const reasons = (outcome?.failures ?? []).slice(0, 8)
481
+ .map(failure => `${bounded(failure.signal)}: ${bounded(failure.reason)}`);
482
+ for (const detail of [outcome?.diagnostic, outcome?.error]) {
483
+ if (detail) reasons.push(bounded(detail));
484
+ }
485
+ return new Error(reasons.length
486
+ ? `Worker launch refused: ${reasons.join('; ').slice(0, 1024)}`
487
+ : 'Worker launch did not return an attached child');
488
+ }
489
+
416
490
  async function launchOneWorker(owner, options) {
417
491
  const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validate } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
418
492
  const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
@@ -425,6 +499,11 @@ async function launchOneWorker(owner, options) {
425
499
  const clock = () => typeof options.now === 'function' ? options.now() : options.now ?? new Date().toISOString();
426
500
  const now = clock();
427
501
  const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now);
502
+ if (invocation.mode === 'managed' || options.managedAdmission) {
503
+ const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
504
+ options = { ...options, scopedBreaker: null, managedInvocationId: admitted.metadata.invocationId,
505
+ managedDeadlineAtMs: admitted.deadlineAtMs };
506
+ }
428
507
  if (invocation.transport !== 'shell') throw new Error('One-shot capture requires shell transport; use the qualified MCP descriptor directly');
429
508
  if (process.platform !== 'linux') throw new Error('One-shot worker capture requires qualified Linux process identity');
430
509
  const runDir = options.runDir;
@@ -475,9 +554,24 @@ async function launchOneWorker(owner, options) {
475
554
  // document on the stdin pipe, never brief text in argv (#2911).
476
555
  const { workerStdinBytes } = await importImpl(import.meta.url, 'serve/worker-stdin-frame.mjs');
477
556
  const stdinBytes = workerStdinBytes(invocation, brief, inputs.drivers[invocation.selected.cli]);
478
- let gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config });
479
- if (!gate.ok) throw new Error('Worker dispatch resource gate refused launch');
480
- const isolation = options.isolation ?? detectWorkerIsolation();
557
+ let gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
558
+ paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
559
+ if (!gate.ok) throw workerDispatchRefusal(gate);
560
+ const { WorkerPolicyError, assertWorkerLaunchMode } = await importImpl(import.meta.url, 'models/provider-policy.js');
561
+ const permissions = inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution ?? {};
562
+ const adapter = inputs.drivers[invocation.selected.cli].qualification.adapter;
563
+ assertWorkerLaunchMode(adapter, permissions);
564
+ if (options.isolation?.mode === 'native-sandbox' && invocation.isolation !== 'native-sandbox') {
565
+ throw new WorkerPolicyError('OG_POLICY_RELAXATION', 'native-sandbox requires the pinned project execution policy');
566
+ }
567
+ const isolation = invocation.isolation === 'native-sandbox' ? { mode: 'native-sandbox' }
568
+ : options.isolation ?? detectWorkerIsolation();
569
+ if (isolation.mode === 'systemd-unit' && workerNeedsNativeNamespaces(adapter, permissions)) {
570
+ const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ cwd: invocation.cwd,
571
+ envNames: invocation.envNames, env: Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
572
+ .map(name => [name, process.env[name]])) });
573
+ if (!probe.ok) throw new WorkerPolicyError(NATIVE_SANDBOX_UNAVAILABLE, nativeSandboxProbeMessage(probe));
574
+ }
481
575
  if (isolation.mode === 'degraded' && !options.allowDegraded) {
482
576
  throw new Error('Worker isolation unavailable; explicitly select --allow-degraded-worker or use manual observation');
483
577
  }
@@ -692,7 +786,7 @@ async function launchOneWorker(owner, options) {
692
786
  prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
693
787
  worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
694
788
  health: invocation.health ?? null, transport: invocation.transport,
695
- isolation: { mode: isolation.mode, memoryMaxGb: options.memoryMaxGb ?? 4 },
789
+ isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
696
790
  limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
697
791
  await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
698
792
  launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
@@ -704,8 +798,9 @@ async function launchOneWorker(owner, options) {
704
798
  // Recheck policy, prompt, installation and exact bindings after file preparation.
705
799
  inputs = await loadInputs();
706
800
  await validate(saved, inputs.snapshot, inputs.drivers, clock());
707
- gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config });
708
- if (!gate.ok) throw new Error('Worker dispatch resource gate refused launch');
801
+ gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
802
+ paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
803
+ if (!gate.ok) throw workerDispatchRefusal(gate);
709
804
  if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
710
805
  const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
711
806
  .map(name => [name, process.env[name]]));
@@ -766,12 +861,12 @@ async function launchOneWorker(owner, options) {
766
861
  // would in production).
767
862
  const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
768
863
  command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
769
- memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation,
864
+ memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation, workerGroup: invocation.selected.group,
770
865
  allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
771
866
  env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir,
772
867
  ...(onDispatchCommit ? { onDispatchCommit } : {}) });
773
868
  outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
774
- if (!outcome.child) throw new Error('Worker launch did not return an attached child');
869
+ if (!outcome?.child) throw workerLaunchRefusal(outcome);
775
870
  // #2915 review 4 (finding 3): when THIS attempt is the admitted canary
776
871
  // for one or more covering scopes, execution itself must be clipped to
777
872
  // the canary's own deadline (min(task deadline, admission+300s)) — not
@@ -831,7 +926,7 @@ async function launchOneWorker(owner, options) {
831
926
  // canary deadline.
832
927
  await settleLedger({ kind: 'uncertain', confirmedStop: true });
833
928
  await publish('exit.json', { invocationId, exitCode: null, signal: null,
834
- endedAt: new Date().toISOString(), termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
929
+ endedAt: new Date().toISOString(), isolation: isolation.mode, termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
835
930
  } else if (admission.gated) {
836
931
  // #2915 review 5 (finding 2a, "leaked capacity"), hardened by #2915
837
932
  // review 6 (finding 4, "capture-setup exception after spawn"): a
@@ -900,6 +995,38 @@ async function launchOneWorker(owner, options) {
900
995
  }
901
996
  }
902
997
 
998
+ // #3489: process exit/close notification can be observed before the final
999
+ // pipe bytes are delivered as stream 'data' events (the same class as #3488,
1000
+ // which waited for IPC disconnect as well as exit). The child 'data'
1001
+ // handlers are the ONLY path that feeds the health observer and accumulates
1002
+ // the `output` buffer adapter extraction consumes, so settling health on
1003
+ // the process-level event alone classifies a completed exit-0 run as
1004
+ // `failed` (missing terminal event, FINAL_INVALID) whenever it wins the
1005
+ // race under load. Drain any already-buffered stdout/stderr bytes — bounded
1006
+ // by the termination grace — before destroying the source streams and
1007
+ // snapshotting output/observer state. Streams with no buffered bytes
1008
+ // resolve immediately, so never-ending stdio (timer-faked stand-ins, held
1009
+ // pipes) never delays settle; the bound only covers genuinely in-flight
1010
+ // data. The timer stays referenced so a bounded drain always completes,
1011
+ // mirroring the already-referenced exit-grace `later()` timer.
1012
+ function drainChildStdio(child, timeoutMs) {
1013
+ const pending = [];
1014
+ for (const stream of [child?.stdout, child?.stderr]) {
1015
+ if (!stream || typeof stream.once !== 'function' || stream.readableEnded || stream.destroyed) continue;
1016
+ if (!(stream.readableLength > 0)) continue;
1017
+ try { stream.resume?.(); } catch { continue; }
1018
+ pending.push(new Promise(resolve => {
1019
+ const done = () => { clearTimeout(timer); resolve(); };
1020
+ const timer = setTimeout(done, Math.max(0, timeoutMs ?? 0));
1021
+ stream.once('end', done);
1022
+ stream.once('close', done);
1023
+ stream.once('error', done);
1024
+ }));
1025
+ }
1026
+ if (!pending.length) return Promise.resolve();
1027
+ return Promise.all(pending);
1028
+ }
1029
+
903
1030
  async function captureOneWorker(owner, ctx) {
904
1031
  const { child, outcome, invocation, invocationId, supervisor, launchTime, monotonicStart, publish, paths, handles, options,
905
1032
  canaryDeadlineMs = null, canaryNowMs = () => Date.now(), recoveryGated = false } = ctx;
@@ -968,7 +1095,16 @@ async function captureOneWorker(owner, ctx) {
968
1095
  };
969
1096
  const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
970
1097
  const signal = value => {
971
- if (settled || !signalOwnedWorker(identity, value, { supervisor })) return false;
1098
+ if (settled) return false;
1099
+ let delivered = signalOwnedWorker(identity, value, { supervisor });
1100
+ // Startup can consume the whole deadline before the transport spawns
1101
+ // its worker or publishes an owned group. We still own this attached
1102
+ // ChildProcess: stop it before it can launch work after finalization.
1103
+ // This proves no descendant termination; degraded mode stays uncertain.
1104
+ if (!delivered && !identity && hasTransport && outcome.mode !== 'systemd-unit' && !sawExit) {
1105
+ try { delivered = signalChild(child, value); } catch { /* already exited or no child handle */ }
1106
+ }
1107
+ if (!delivered) return false;
972
1108
  deliveredSignal = value;
973
1109
  return true;
974
1110
  };
@@ -1007,8 +1143,19 @@ async function captureOneWorker(owner, ctx) {
1007
1143
  }
1008
1144
  signal('SIGTERM');
1009
1145
  const remainingGrace = Math.min(graceMs, hardRemaining());
1010
- if (remainingGrace === 0) { signal('SIGKILL'); later(() => finish(), 100); }
1011
- else later(() => { signal('SIGKILL'); later(() => finish(), 100); }, remainingGrace);
1146
+ const escalate = () => {
1147
+ signal('SIGKILL');
1148
+ // Startup may have no worker identity yet. Wait for our transport's
1149
+ // exit status before settling: TERM can have killed it even if KILL
1150
+ // was also delivered before Node observes exit (#3532). Use the unused
1151
+ // grace plus the existing 100ms settle tick, keeping the total
1152
+ // termination budget at graceMs + 100 on the tracked launcher timers.
1153
+ const exitWaitMs = !identity && hasTransport && outcome.mode !== 'systemd-unit'
1154
+ ? graceMs - remainingGrace + 100 : 100;
1155
+ later(() => finish(), exitWaitMs);
1156
+ };
1157
+ if (remainingGrace === 0) escalate();
1158
+ else later(escalate, remainingGrace);
1012
1159
  };
1013
1160
  owner.currentProcess = child;
1014
1161
  owner.workerStop = stop;
@@ -1023,9 +1170,15 @@ async function captureOneWorker(owner, ctx) {
1023
1170
  owner.emit('stdout', chunk);
1024
1171
  });
1025
1172
  let stderrBytes = 0;
1173
+ let sandboxStderrTail = '';
1026
1174
  let recoveryReason = null;
1027
1175
  child.stderr.on('data', chunk => {
1028
1176
  observer.feed(chunk, { kind: 'stderr', id: 'stderr', generation: 0, offset: stderrOffset }); stderrOffset += chunk.length;
1177
+ if (workerNeedsNativeNamespaces(driver.qualification.adapter, ctx.inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution)) {
1178
+ const observedStderr = sandboxStderrTail + chunk.toString('utf8');
1179
+ if (nativeSandboxFailure(observedStderr)) errorCode = NATIVE_SANDBOX_UNAVAILABLE;
1180
+ sandboxStderrTail = observedStderr.slice(-1024);
1181
+ }
1029
1182
  const hint = classifyWorkerHealthFailure(chunk.toString('utf8'), { source: { kind: 'stderr' } })?.reasonCode;
1030
1183
  if (['RATE_LIMIT', 'NETWORK'].includes(hint)) recoveryReason = hint;
1031
1184
  const remaining = Math.max(0, 10485760 - stderrBytes);
@@ -1055,6 +1208,10 @@ async function captureOneWorker(owner, ctx) {
1055
1208
  timers.clear();
1056
1209
  process.off('SIGTERM', onSignal); process.off('SIGINT', onSignal);
1057
1210
  owner.currentProcess = null; owner.workerStop = null;
1211
+ // #3489: drain in-flight stdio before destroying the source streams —
1212
+ // destroying first discards bytes never delivered as 'data', settling
1213
+ // health on a partial stream (exit observed before drain).
1214
+ await drainChildStdio(child, graceMs);
1058
1215
  child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
1059
1216
  for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
1060
1217
  const native = outcome.mode === 'systemd-unit' ? readNative() : null;
@@ -1415,12 +1572,15 @@ async function captureOneWorker(owner, ctx) {
1415
1572
 
1416
1573
  // ===========================================================================
1417
1574
  // BASED ON a4085b86a (#2915 round 8, D6).
1418
- // Exception: finish() snapshots timeout classification before cleanup and
1419
- // respects the winning stop cause, matching captureOneWorker's race fix.
1575
+ // Shared lifecycle fixes: finish() snapshots timeout classification before
1576
+ // cleanup and respects the winning stop cause; stop() can signal the owned
1577
+ // transport before worker identity is available, and then uses the unused
1578
+ // termination grace to observe its exit before falling back to the last
1579
+ // delivered signal (#3532).
1420
1580
  // The test/fixtures frozen reference remains unchanged.
1421
1581
  //
1422
1582
  // `launchOneWorkerBaseline` and `captureOneWorkerBaseline` reproduce
1423
- // `launchOneWorker`/`captureOneWorker`, apart from the timeout race fix above,
1583
+ // `launchOneWorker`/`captureOneWorker`, apart from the lifecycle fixes above,
1424
1584
  // as they existed at base commit `a4085b86a` (`git show
1425
1585
  // a4085b86a:tools/ralph-external/session-launcher.mjs`) — before any
1426
1586
  // #2915 direct-recovery code existed in this file. `launchOneWorker`
@@ -1472,7 +1632,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1472
1632
  if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
1473
1633
  await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
1474
1634
  try { await handle.chmod(0o700); } finally { await handle.close(); } });
1475
- const invocationId = randomUUID();
1635
+ const invocationId = options.managedAdmission ? options.managedInvocationId : randomUUID();
1476
1636
  const reservation = await withWorkerPath(join(runDir, '.launch-owner'), target => open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
1477
1637
  try { await reservation.writeFile(invocationId); await reservation.sync(); } finally { await reservation.close(); }
1478
1638
  const supervisor = sampleWorkerIdentity(process.pid);
@@ -1492,11 +1652,12 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1492
1652
  const handles = [];
1493
1653
  let outcome;
1494
1654
  let artifacts;
1655
+ let managedSpawnAttempted = false;
1495
1656
  try {
1496
1657
  artifacts = await materializeWorkerArtifacts(invocation, runDir);
1497
1658
  const displayArgv = artifacts.argv.map((token, i) => i === 0 ? token : `<argument:${bytesDigest(token)}>`);
1498
1659
  await publish('invocation.json', { version: 1, origin: 'launcher', invocationId, attemptId: invocationId,
1499
- previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
1660
+ previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: options.managedAdmission?.requestId ?? invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
1500
1661
  selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
1501
1662
  policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
1502
1663
  expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
@@ -1509,7 +1670,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1509
1670
  prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
1510
1671
  worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
1511
1672
  health: invocation.health ?? null, transport: invocation.transport,
1512
- isolation: { mode: isolation.mode, memoryMaxGb: options.memoryMaxGb ?? 4 },
1673
+ isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
1513
1674
  limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
1514
1675
  await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
1515
1676
  launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
@@ -1521,8 +1682,9 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1521
1682
  // Recheck policy, prompt, installation and exact bindings after file preparation.
1522
1683
  inputs = await loadInputs();
1523
1684
  await validate(saved, inputs.snapshot, inputs.drivers, clock());
1524
- localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config });
1525
- if (!localGate.ok) throw new Error('Worker dispatch resource gate refused launch');
1685
+ localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
1686
+ paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
1687
+ if (!localGate.ok) throw workerDispatchRefusal(localGate);
1526
1688
  if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
1527
1689
  const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
1528
1690
  .map(name => [name, process.env[name]]));
@@ -1539,16 +1701,24 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
1539
1701
  const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
1540
1702
  command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
1541
1703
  memoryMaxGb: options.memoryMaxGb ?? 4, gate: localGate, config: inputs.snapshot.config, isolation,
1704
+ workerGroup: invocation.selected.group,
1542
1705
  allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
1706
+ ...(options.managedAdmission ? { onDispatchCommit: async () => {
1707
+ const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
1708
+ options.managedDeadlineAtMs = Math.min(options.managedDeadlineAtMs, admitted.deadlineAtMs);
1709
+ } } : {}),
1710
+ ...(options.managedAdmission ? { onSpawnAttempt: () => { managedSpawnAttempted = true; } } : {}),
1543
1711
  env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir });
1544
1712
  outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
1545
- if (!outcome.child) throw new Error('Worker launch did not return an attached child');
1713
+ if (!outcome?.child) throw workerLaunchRefusal(outcome);
1546
1714
  return await captureOneWorkerBaseline(owner, { child: outcome.child, outcome, invocation, invocationId, supervisor,
1547
1715
  launchTime, monotonicStart, publish, publishFailures, paths, handles, inspect, inputs, profile, baselineContent, options,
1548
1716
  brief: ctx.brief, stdinBytes: ctx.stdinBytes, runDir });
1549
1717
  } catch (error) {
1550
1718
  if (!outcome?.child) await publish('exit.json', { invocationId, exitCode: null, signal: null,
1551
- endedAt: new Date().toISOString(), termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
1719
+ ...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
1720
+ endedAt: new Date().toISOString(), isolation: isolation.mode, termination: managedSpawnAttempted ? 'uncertain' : 'confirmed',
1721
+ descendants: managedSpawnAttempted ? 'unknown' : 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
1552
1722
  throw error;
1553
1723
  } finally {
1554
1724
  try { await artifacts?.cleanup(); } finally {
@@ -1588,9 +1758,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
1588
1758
  let sawClose = false;
1589
1759
  const driver = ctx.inputs.drivers[invocation.selected.cli];
1590
1760
  const observationOptions = { provider: driver.qualification.adapter.provider, profile: ctx.profile,
1591
- effectiveHealth: invocation.health, invocationId, startedAt: Number(BigInt(monotonicStart)) / 1e6,
1761
+ effectiveHealth: options.managedAdmission ? { ...invocation.health,
1762
+ hardTimeoutSeconds: Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds,
1763
+ (options.managedDeadlineAtMs - Date.parse(launchTime)) / 1000) } : invocation.health,
1764
+ invocationId, startedAt: Number(BigInt(monotonicStart)) / 1e6,
1592
1765
  launchTime, contentDigest: ctx.baselineContent, sessionId: options.healthSessionLog?.sessionId ?? null,
1593
- runId: options.healthSessionLog?.runId ?? null, maxOutputBytes: invocation.maxOutputBytes };
1766
+ runId: options.healthSessionLog?.runId ?? null, maxOutputBytes: invocation.maxOutputBytes,
1767
+ ...(options.managedAdmission ? { observerGeneration: options.managedAdmission.generation, requestId: options.managedAdmission.requestId } : {}) };
1594
1768
  const observer = driver.createHealthObserver?.(observationOptions) ?? createWorkerHealthObserver(observationOptions);
1595
1769
  const tickRate = qualifyWorkerCpuTickRate();
1596
1770
  let previousCpu = null;
@@ -1611,7 +1785,16 @@ async function captureOneWorkerBaseline(owner, ctx) {
1611
1785
  };
1612
1786
  const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
1613
1787
  const signal = value => {
1614
- if (settled || !signalOwnedWorker(identity, value, { supervisor })) return false;
1788
+ if (settled) return false;
1789
+ let delivered = signalOwnedWorker(identity, value, { supervisor });
1790
+ // Startup can consume the whole deadline before the transport spawns
1791
+ // its worker or publishes an owned group. We still own this attached
1792
+ // ChildProcess: stop it before it can launch work after finalization.
1793
+ // This proves no descendant termination; degraded mode stays uncertain.
1794
+ if (!delivered && !identity && hasTransport && outcome.mode !== 'systemd-unit' && !sawExit) {
1795
+ try { delivered = signalChild(child, value); } catch { /* already exited or no child handle */ }
1796
+ }
1797
+ if (!delivered) return false;
1615
1798
  deliveredSignal = value;
1616
1799
  return true;
1617
1800
  };
@@ -1640,8 +1823,19 @@ async function captureOneWorkerBaseline(owner, ctx) {
1640
1823
  }
1641
1824
  signal('SIGTERM');
1642
1825
  const remainingGrace = Math.min(graceMs, hardRemaining());
1643
- if (remainingGrace === 0) { signal('SIGKILL'); later(() => finish(), 100); }
1644
- else later(() => { signal('SIGKILL'); later(() => finish(), 100); }, remainingGrace);
1826
+ const escalate = () => {
1827
+ signal('SIGKILL');
1828
+ // Startup may have no worker identity yet. Wait for our transport's
1829
+ // exit status before settling: TERM can have killed it even if KILL
1830
+ // was also delivered before Node observes exit (#3532). Use the unused
1831
+ // grace plus the existing 100ms settle tick, keeping the total
1832
+ // termination budget at graceMs + 100 on the tracked launcher timers.
1833
+ const exitWaitMs = !identity && hasTransport && outcome.mode !== 'systemd-unit'
1834
+ ? graceMs - remainingGrace + 100 : 100;
1835
+ later(() => finish(), exitWaitMs);
1836
+ };
1837
+ if (remainingGrace === 0) escalate();
1838
+ else later(escalate, remainingGrace);
1645
1839
  };
1646
1840
  owner.currentProcess = child;
1647
1841
  owner.workerStop = stop;
@@ -1656,8 +1850,14 @@ async function captureOneWorkerBaseline(owner, ctx) {
1656
1850
  owner.emit('stdout', chunk);
1657
1851
  });
1658
1852
  let stderrBytes = 0;
1853
+ let sandboxStderrTail = '';
1659
1854
  child.stderr.on('data', chunk => {
1660
1855
  observer.feed(chunk, { kind: 'stderr', id: 'stderr', generation: 0, offset: stderrOffset }); stderrOffset += chunk.length;
1856
+ if (workerNeedsNativeNamespaces(driver.qualification.adapter, ctx.inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution)) {
1857
+ const observedStderr = sandboxStderrTail + chunk.toString('utf8');
1858
+ if (nativeSandboxFailure(observedStderr)) errorCode = NATIVE_SANDBOX_UNAVAILABLE;
1859
+ sandboxStderrTail = observedStderr.slice(-1024);
1860
+ }
1661
1861
  const remaining = Math.max(0, 10485760 - stderrBytes);
1662
1862
  streams[1].write(chunk.subarray(0, remaining)); streams[3].write(chunk.subarray(0, remaining)); stderrBytes += chunk.length;
1663
1863
  if (stderrBytes > 10485760) { errorCode = 'OUTPUT_LIMIT'; stop(); }
@@ -1685,6 +1885,11 @@ async function captureOneWorkerBaseline(owner, ctx) {
1685
1885
  timers.clear();
1686
1886
  process.off('SIGTERM', onSignal); process.off('SIGINT', onSignal);
1687
1887
  owner.currentProcess = null; owner.workerStop = null;
1888
+ // #3489: drain in-flight stdio before destroying the source streams —
1889
+ // destroying first discards bytes never delivered as 'data', settling
1890
+ // health on a partial stream (exit observed before drain). Ordering-only;
1891
+ // recovery semantics in this frozen baseline are unchanged.
1892
+ await drainChildStdio(child, graceMs);
1688
1893
  child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
1689
1894
  for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
1690
1895
  const native = outcome.mode === 'systemd-unit' ? readNative() : null;
@@ -1707,6 +1912,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
1707
1912
  const exitCode = resultSignal ? null
1708
1913
  : outcome.mode === 'systemd-unit' && native?.execMainCode === 1 ? native.execMainStatus : waitCode;
1709
1914
  const result = { invocationId, exitCode, signal: resultSignal,
1915
+ ...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
1710
1916
  endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
1711
1917
  descendants: confirmed ? 'stopped' : 'unknown', timedOut: resultTimedOut,
1712
1918
  cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
@@ -1772,8 +1978,11 @@ async function captureOneWorkerBaseline(owner, ctx) {
1772
1978
  sawExit = true; waitCode = code; waitSignal = sig; later(() => finish(), graceMs);
1773
1979
  });
1774
1980
  child.once('close', () => { sawClose = true; finish(); });
1775
- const hardRemaining = () => Math.max(0, Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds,
1776
- invocation.limits.wallSeconds) * 1000 - (Number(process.hrtime.bigint() - BigInt(monotonicStart)) / 1e6));
1981
+ const hardRemaining = () => Math.max(0, Math.min(
1982
+ Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds, invocation.limits.wallSeconds,
1983
+ options.managedAdmission ? (options.managedDeadlineAtMs - Date.parse(launchTime)) / 1000 : Infinity) * 1000
1984
+ - (Number(process.hrtime.bigint() - BigInt(monotonicStart)) / 1e6),
1985
+ options.managedAdmission ? options.managedDeadlineAtMs - Date.now() : Infinity));
1777
1986
  later(() => {
1778
1987
  observer.check();
1779
1988
  reportHealth(); stop('HARD_DEADLINE');
@@ -5055,3 +5264,42 @@ export function planDirectWorkerLaunch({
5055
5264
  }
5056
5265
 
5057
5266
  export default SessionLauncher;
5267
+
5268
+ /** The packaged session owner accepts only the parent-bound managed one-shot contract. */
5269
+ export function parseManagedWorkerLaunchArgs(args) {
5270
+ const result = {};
5271
+ const seen = new Set();
5272
+ for (let index = 0; index < args.length; index++) {
5273
+ const flag = args[index];
5274
+ if (seen.has(flag)) throw new Error(`Duplicate managed worker option: ${flag}`);
5275
+ seen.add(flag);
5276
+ if (flag === '--launch-worker') continue;
5277
+ const key = { '--invocation': 'invocationPath', '--run-dir': 'runDir',
5278
+ '--managed-request': 'requestId', '--managed-generation': 'generation' }[flag];
5279
+ if (!key) throw new Error(`Unsupported managed worker option: ${flag}`);
5280
+ const value = args[++index];
5281
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
5282
+ result[key] = value;
5283
+ }
5284
+ if (!seen.has('--launch-worker') || !result.invocationPath || !result.runDir || !result.requestId || !result.generation) {
5285
+ throw new Error('Managed worker launch requires invocation, run directory and fenced admission');
5286
+ }
5287
+ const generation = Number(result.generation);
5288
+ if (!Number.isSafeInteger(generation) || generation < 1) throw new Error('Invalid managed admission generation');
5289
+ return { invocationPath: result.invocationPath, runDir: result.runDir,
5290
+ managedAdmission: { requestId: result.requestId, generation } };
5291
+ }
5292
+
5293
+ async function runManagedWorkerEntry() {
5294
+ const result = await new SessionLauncher().launchWorker(parseManagedWorkerLaunchArgs(process.argv.slice(2)));
5295
+ console.log(JSON.stringify(result));
5296
+ process.exitCode = result.termination !== 'confirmed' ? 2
5297
+ : result.errorCode || result.timedOut || result.exitCode !== 0 ? 1 : 0;
5298
+ }
5299
+
5300
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
5301
+ runManagedWorkerEntry().catch(error => {
5302
+ console.error(error.message);
5303
+ process.exitCode = 1;
5304
+ });
5305
+ }
@@ -36,10 +36,10 @@ const _require = createRequire(import.meta.url);
36
36
  let fs;
37
37
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
38
38
  import path from 'path';
39
+ import { fileURLToPath } from 'node:url';
39
40
  import os from 'os';
40
41
  import { contentHash, writeSidecarManifest, getFrameworksForMode, normalizeDeploymentMode, skillMatchesProvider, isKernelSkill } from '../agents/providers/base.mjs';
41
42
 
42
- const CODEX_SKILLS_DIR = path.join(os.homedir(), '.codex', 'skills');
43
43
  const MAX_NAME_LENGTH = 100;
44
44
  const MAX_DESCRIPTION_LENGTH = 500;
45
45
  const LEGACY_RENAMED_SKILLS = new Set(['aiwg-mcp']);
@@ -104,11 +104,10 @@ export function planCodexListingBudget(planned, kernelDir, cap) {
104
104
  return { demoted, before, after: projected, cap };
105
105
  }
106
106
 
107
- function parseArgs() {
108
- const args = process.argv.slice(2);
107
+ function parseArgs(args, homeDir) {
109
108
  const cfg = {
110
109
  source: null,
111
- target: CODEX_SKILLS_DIR,
110
+ target: path.join(homeDir, '.codex', 'skills'),
112
111
  mode: 'all',
113
112
  dryRun: false,
114
113
  force: false,
@@ -483,8 +482,8 @@ function isFullAiwgSourceRoot(srcRoot) {
483
482
  fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'extensions'));
484
483
  }
485
484
 
486
- (async function main() {
487
- const cfg = parseArgs();
485
+ export async function main(args = process.argv.slice(2), internal = {}) {
486
+ const cfg = parseArgs(args, internal.homeDir || os.homedir());
488
487
  const { source, target, mode, dryRun, force } = cfg;
489
488
 
490
489
  // Resolve source directory
@@ -699,4 +698,9 @@ function isFullAiwgSourceRoot(srcRoot) {
699
698
  if (!dryRun && totalDeployed > 0) {
700
699
  console.log(`\nRestart Codex to load new skills.`);
701
700
  }
702
- })();
701
+ }
702
+
703
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
704
+ try { await main(); }
705
+ catch (error) { console.error(error.message); process.exitCode = 1; }
706
+ }
@@ -21,7 +21,12 @@
21
21
  "type": "object", "additionalProperties": false,
22
22
  "properties": {
23
23
  "llm_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000, "default": 15, "description": "Planning USD per million frontier tokens, blended input/output rate." },
24
- "jev_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000, "default": 0.2, "description": "Planning USD per million Jev tokens, blended input/output rate." }
24
+ "jev_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000, "default": 0.2, "description": "Planning USD per million Jev tokens. Complete split rates with known usage take precedence." },
25
+ "jev_input_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000 },
26
+ "jev_output_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000 },
27
+ "source": { "type": "string", "enum": ["typesafe-docs", "manual", "typesafe-snapshot"] },
28
+ "checked_at": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "ISO calendar date YYYY-MM-DD; calendar validity checked by the loader." },
29
+ "jev_model": { "type": "string", "pattern": "^jev-\\d+\\.\\d+\\.\\d+$" }
25
30
  }
26
31
  },
27
32
  "savings": {
@@ -1497,6 +1502,11 @@
1497
1502
  "type": "object",
1498
1503
  "additionalProperties": false,
1499
1504
  "properties": {
1505
+ "launchMode": {
1506
+ "enum": ["systemd-unit", "native-sandbox"],
1507
+ "default": "systemd-unit",
1508
+ "description": "Policy-selected worker launch boundary. native-sandbox requires a verified enforcing CLI sandbox and has no cgroup resource limit."
1509
+ },
1500
1510
  "isolation": {
1501
1511
  "enum": [
1502
1512
  "worktree",