@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
@@ -12,14 +12,244 @@
12
12
  * @issue #483
13
13
  */
14
14
  import * as ui from '../ui.js';
15
- import { promises as fs } from 'node:fs';
16
- import { join } from 'node:path';
15
+ import { constants as fsConstants, promises as fs } from 'node:fs';
16
+ import { isAbsolute, join, relative } from 'node:path';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
17
18
  import { createHash, randomBytes } from 'node:crypto';
18
- import { assertMissionWorkerHandoff } from '../../mission-protocol/codecs.js';
19
+ import { assertMissionWorkerHandoff, assertValidWorkerPayload, validateWorkerPayload, workerConsumerSupport } from '../../mission-protocol/codecs.js';
20
+ import { WORKER_BASELINE_FEATURES } from '../../mission-protocol/types.js';
19
21
  // ── Constants ────────────────────────────────────────────────
20
22
  const MC_ROOT = '.aiwg/ralph-external/mc';
21
23
  const SESSIONS_DIR = join(MC_ROOT, 'sessions');
22
24
  const CONTROL_ID_RE = /^[a-zA-Z0-9._-]+$/;
25
+ /** Managed callers fail closed on invalid configuration; legacy reads keep their
26
+ * historical best-effort behavior. No activation is installed by a caller. */
27
+ async function readAdmissionConfig(projectRoot) {
28
+ const { readAiwgConfig, readAiwgConfigCandidate } = await import('../../config/aiwg-config.js');
29
+ try {
30
+ return await readAiwgConfig(projectRoot);
31
+ }
32
+ catch (error) {
33
+ const candidate = await readAiwgConfigCandidate(projectRoot);
34
+ if (candidate?.parallelism?.worker_mode === 'managed')
35
+ throw error;
36
+ return null;
37
+ }
38
+ }
39
+ /** Shared binding used by MC and the managed Mission/Flow/Ralph caller paths. */
40
+ export async function createMcAdmissionScheduler(projectRoot, cfg, hostRegistry) {
41
+ const { resolveParallelism, workerActivationDigests, workerConfigDigest } = await import('../../config/aiwg-config.js');
42
+ const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
43
+ const provider = cfg?.providers[0] ?? 'unknown';
44
+ const parallelism = resolveParallelism(cfg?.parallelism, provider);
45
+ if (parallelism.worker_mode === 'managed') {
46
+ const { managedProjectAdmissionPolicy, projectAdmissionAuthority } = await import('../../serve/shared-host-scheduler.js');
47
+ const activation = parallelism.activation;
48
+ if (!cfg || !activation || activation.configDigest !== workerActivationDigests(cfg).configDigest) {
49
+ throw new Error('managed admission requires a current activated policy');
50
+ }
51
+ const authority = projectAdmissionAuthority(projectRoot);
52
+ const authorityRef = parallelism.admission?.authorityRef ?? 'project';
53
+ if (authorityRef.startsWith('host:') && process.env['AIWG_EXECUTOR_IDENTITY_STORE']) {
54
+ ui.warn('AIWG_EXECUTOR_IDENTITY_STORE is ignored for host admission authority lookup.');
55
+ }
56
+ const policy = managedProjectAdmissionPolicy(projectRoot, { ...parallelism,
57
+ admission: { ...parallelism.admission, authorityRef: 'project' },
58
+ }, workerConfigDigest(activation), parallelism.max_parallel_subagents);
59
+ const binding = authorityRef === 'project' ? { store: new FileAdmissionStore(authority.path), policy }
60
+ : (hostRegistry ?? (await import('../../serve/executor-registry.js')).executorRegistry).resolveHostAdmissionAuthority(authorityRef, policy);
61
+ const scheduler = new SharedHostScheduler(binding.store, binding.policy);
62
+ scheduler.reconcileNow();
63
+ return scheduler;
64
+ }
65
+ const maxConcurrent = parallelism.max_parallel_mc_missions;
66
+ return new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), {
67
+ maxConcurrent, leaseTtlMs: 5 * 60_000, agingIntervalMs: 30_000, allowPreemption: false, defaultHostQuota: maxConcurrent,
68
+ });
69
+ }
70
+ const mcWorkerSupport = () => workerConsumerSupport([...WORKER_BASELINE_FEATURES, 'worker-health.v1']);
71
+ function workerDispatch(mission) {
72
+ const bindings = new Set();
73
+ const seen = new Set();
74
+ function visit(value) {
75
+ if (!value || typeof value !== 'object' || seen.has(value))
76
+ return;
77
+ seen.add(value);
78
+ const record = value;
79
+ if (record['aiwg.worker-dispatch'])
80
+ bindings.add(record['aiwg.worker-dispatch']);
81
+ visit(record.extensions);
82
+ visit(record.metadata);
83
+ for (const [key, child] of Object.entries(record))
84
+ if (key.startsWith('aiwg.source.'))
85
+ visit(child);
86
+ }
87
+ visit(mission);
88
+ if (bindings.size > 1)
89
+ throw new Error('Managed task contains multiple dispatch bindings');
90
+ return [...bindings][0];
91
+ }
92
+ async function readWorkerArtifact(artifact, projectRoot) {
93
+ const filename = artifact.ref.startsWith('file:') ? fileURLToPath(artifact.ref) : artifact.ref;
94
+ const inside = relative(projectRoot, filename);
95
+ if (!isAbsolute(filename) || inside.startsWith('..') || isAbsolute(inside))
96
+ throw new Error('Managed worker artifacts must be inside the project');
97
+ const bytes = await readWorkerBytes(filename);
98
+ if (`sha256:${createHash('sha256').update(bytes).digest('hex')}` !== artifact.digest)
99
+ throw new Error('Managed worker artifact digest changed');
100
+ return JSON.parse(bytes.toString('utf8'));
101
+ }
102
+ async function readWorkerBytes(filename) {
103
+ const { inspectWorkerPath } = await import('../../serve/worker-invocation.js');
104
+ if (!await inspectWorkerPath(filename, 'file'))
105
+ throw Object.assign(new Error('Managed worker artifact is missing'), { code: 'ENOENT' });
106
+ const handle = await fs.open(filename, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
107
+ try {
108
+ const stat = await handle.stat();
109
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1048576)
110
+ throw new Error('Invalid managed worker artifact');
111
+ const bytes = Buffer.alloc(1048577);
112
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
113
+ if (bytesRead > 1048576)
114
+ throw new Error('Managed worker artifact exceeds its size bound');
115
+ return bytes.subarray(0, bytesRead);
116
+ }
117
+ finally {
118
+ await handle.close();
119
+ }
120
+ }
121
+ async function readWorkerJson(filename) {
122
+ return JSON.parse((await readWorkerBytes(filename)).toString('utf8'));
123
+ }
124
+ async function resolveMcWorker(ctx, session, mission, parentKind = 'mission') {
125
+ const dispatch = workerDispatch(mission);
126
+ if (!dispatch)
127
+ return undefined;
128
+ const projectRoot = ctx.cwd || process.cwd();
129
+ const intent = await readWorkerArtifact(dispatch.intent, projectRoot);
130
+ assertValidWorkerPayload('workerSelectionIntent', intent);
131
+ if (!dispatch.brief)
132
+ throw new Error('Managed worker requires a prepared brief');
133
+ const brief = await readWorkerArtifact(dispatch.brief, projectRoot);
134
+ assertValidWorkerPayload('workerBrief', brief);
135
+ const { workerConfigDigest } = await import('../../config/aiwg-config.js');
136
+ if (intent.parent.kind !== parentKind || intent.parent.id !== session.id || intent.objective !== mission.objective
137
+ || intent.taskId !== dispatch.taskId || intent.requestId !== dispatch.requestId
138
+ || brief.invocationId !== dispatch.invocationId || workerConfigDigest(brief.intent) !== workerConfigDigest(intent)
139
+ || brief.intentDigest !== workerConfigDigest(intent))
140
+ throw new Error('Managed worker task authority differs from its parent');
141
+ const { loadWorkerInputs } = await import('./models.js');
142
+ const { resolveWorkerInvocation } = await import('../../serve/worker-invocation.js');
143
+ const inputs = await loadWorkerInputs(ctx);
144
+ const plan = await resolveWorkerInvocation(inputs.snapshot, { role: intent.role, task: intent.taskClass, group: intent.group,
145
+ brief: dispatch.brief.ref.startsWith('file:') ? fileURLToPath(dispatch.brief.ref) : dispatch.brief.ref,
146
+ workdir: brief.workspace.path, final_output: brief.output.resultPath, managedIntent: intent }, inputs.drivers);
147
+ if (!plan.invocation || plan.status !== 'eligible' || plan.invocation.readiness !== 'launchable') {
148
+ throw new Error(`Managed worker is ineligible: ${plan.diagnostics.map(item => item.message).join('; ')}`);
149
+ }
150
+ if (parentKind === 'mission' && plan.invocation.transport !== 'shell')
151
+ throw new Error('Managed Ralph capture requires a qualified shell transport');
152
+ return { dispatch, intent, brief, plan };
153
+ }
154
+ /** Flow adapters use the same eligibility, project authority and allowance owner. */
155
+ export async function createMcFlowAdapter(ctx, bindings) {
156
+ const cfg = await readAdmissionConfig(ctx.cwd);
157
+ if (!cfg?.parallelism?.worker_dispatch || cfg.parallelism.worker_mode !== 'managed') {
158
+ throw new Error('OG_CONTRACT_UNSUPPORTED: Flow shared dispatch requires managed mode');
159
+ }
160
+ const scheduler = await createMcAdmissionScheduler(ctx.cwd, cfg);
161
+ const { managedAllowanceFromOwner } = await import('../../serve/shared-host-scheduler.js');
162
+ const { createManagedCompositionAdapter } = await import(join(ctx.frameworkRoot, 'agentic/code/addons/composition-engine/lib/runtime.mjs'));
163
+ return createManagedCompositionAdapter({ scheduler, allowanceFromOwner: managedAllowanceFromOwner,
164
+ invokeNode: bindings.invokeNode,
165
+ prepareNode: async (request) => {
166
+ const binding = await bindings.prepareNode(request);
167
+ const worker = await resolveMcWorker(ctx, { id: request.runId }, {
168
+ id: request.node.id, objective: binding.objective, status: 'queued', loop: 0,
169
+ maxIterations: 1, priority: 'normal', mode: 'direct', extensions: { 'aiwg.worker-dispatch': binding.dispatch },
170
+ }, 'flow');
171
+ if (!worker || worker.intent.taskId !== request.node.id)
172
+ throw new Error('Flow task authority differs from node identity');
173
+ const selected = worker.plan.selected;
174
+ return { invocation: worker.plan.invocation, plan: worker.plan, brief: worker.brief,
175
+ admission: { requestId: worker.dispatch.requestId, orchestratorId: request.runId,
176
+ environment: process.env.AIWG_ENVIRONMENT ?? 'default', provider: selected.inferenceProvider, runtimeKind: 'host',
177
+ priority: 10, submittedAt: new Date().toISOString(), queueTimeoutMs: (cfg.parallelism.admission?.queueTimeoutSeconds ?? 300) * 1000,
178
+ preemptible: false, metadata: { nodeId: request.node.id, invocationId: worker.dispatch.invocationId },
179
+ managed: { kind: 'external', group: selected.group, member: selected.member, cli: selected.cli,
180
+ model: selected.modelKey, quotaScope: cfg.parallelism.clis[selected.cli].quotaScope },
181
+ allowance: { runId: request.runId, taskId: request.node.id,
182
+ taskBudgets: { maxAttempts: worker.intent.limits.maxAttempts, maxTokens: worker.intent.limits.tokens,
183
+ maxCostUsd: worker.intent.limits.costUsd ?? undefined, timeoutSeconds: worker.intent.limits.wallSeconds },
184
+ reservation: { tokens: worker.plan.invocation.limits.tokens, costUsd: worker.plan.invocation.limits.costUsd } },
185
+ } };
186
+ },
187
+ });
188
+ }
189
+ /** Capture completion is evidence for the parent, never an independent gate. */
190
+ async function readManagedWorkerState(mission, scheduler) {
191
+ const binding = mission.managedWorker;
192
+ const observation = await readWorkerJson(join(binding.runDir, 'health.json'));
193
+ assertValidWorkerPayload('workerHealthSummary', observation.health);
194
+ const dispatch = workerDispatch(mission);
195
+ if (observation.invocationId !== dispatch.invocationId || observation.health.observerGeneration !== mission.admissionGeneration) {
196
+ throw new Error('Stale managed worker observation');
197
+ }
198
+ let exit;
199
+ try {
200
+ exit = await readWorkerJson(join(binding.runDir, 'exit.json'));
201
+ }
202
+ catch (error) {
203
+ if (error.code !== 'ENOENT')
204
+ throw error;
205
+ }
206
+ if (!exit)
207
+ return { status: 'running', health: observation.health };
208
+ if (exit.invocationId !== dispatch.invocationId || exit.managedAdmission?.requestId !== mission.admissionRequestId
209
+ || exit.managedAdmission?.generation !== mission.admissionGeneration)
210
+ throw new Error('Stale managed worker exit evidence');
211
+ const record = scheduler.snapshot().records[mission.admissionRequestId];
212
+ const usage = { inputTokens: null, outputTokens: null, cacheTokens: null, reasoningTokens: null,
213
+ costUsd: null, quality: 'unknown', reservedTokens: record.accounting.reservation.tokens,
214
+ reservedUsd: record.accounting.reservation.costUsd };
215
+ const plan = await readWorkerJson(binding.invocationPath);
216
+ const succeeded = exit.exitCode === 0 && !exit.errorCode && !exit.timedOut && !exit.cancelled;
217
+ const result = { version: 1, taskId: dispatch.taskId, invocationId: dispatch.invocationId,
218
+ briefDigest: plan.invocation.briefDigest, worker: null,
219
+ execution: { status: succeeded ? 'completed' : 'failed', exitCode: exit.exitCode,
220
+ requested: plan.invocation.selected, observedModel: null, observedFamily: null, observedEffort: null,
221
+ identityEvidence: [], identityQuality: 'unavailable', startedAt: mission.startedAt, endedAt: exit.endedAt,
222
+ termination: exit.termination, usage, failure: null, health: observation.health },
223
+ verification: { status: 'pending', verifierId: null, runnerRef: null, artifactDigest: null, evidence: [], findings: [] } };
224
+ try {
225
+ const claims = await readWorkerJson(plan.invocation.finalPath);
226
+ assertValidWorkerPayload('workerClaims', claims);
227
+ result.worker = claims;
228
+ }
229
+ catch { /* Invalid or absent claims remain separate from execution evidence. */ }
230
+ assertValidWorkerPayload('workerResult', result);
231
+ // Use the checkpoint's own directory; request IDs need not contain a session ID.
232
+ const destination = `${binding.invocationPath}.result.json`;
233
+ const bytes = JSON.stringify(result);
234
+ await fs.writeFile(destination, bytes, { mode: 0o600 });
235
+ dispatch.result = { ref: pathToFileURL(destination).href, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` };
236
+ scheduler.acceptFinal(mission.admissionRequestId, mission.admissionGeneration, observation.health, usage);
237
+ // A surviving or unobservable outer supervisor cannot prove its own stop.
238
+ let supervisorGone = false;
239
+ try {
240
+ process.kill(mission.admissionExecution.pid, 0);
241
+ }
242
+ catch (error) {
243
+ supervisorGone = error.code === 'ESRCH';
244
+ }
245
+ if (supervisorGone && exit.termination === 'confirmed' && exit.descendants === 'stopped') {
246
+ const settled = scheduler.settle(mission.admissionRequestId, mission.admissionGeneration, observation.health, usage, undefined, mission.admissionExecution);
247
+ mission.admissionState = settled.state;
248
+ }
249
+ dispatch.state = mission.admissionState === 'settled' ? succeeded ? 'completed' : 'failed' : 'uncertain';
250
+ // Verification and original completion criteria remain with the parent.
251
+ return { status: succeeded && result.worker?.status === 'completed' ? 'paused' : 'failed', health: observation.health };
252
+ }
23
253
  // ── Helpers ──────────────────────────────────────────────────
24
254
  function genId(prefix) {
25
255
  return `${prefix}-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`;
@@ -221,6 +451,7 @@ Queue a mission onto a session. Does NOT execute — use 'aiwg mc run' to launch
221
451
  --budget-stop-policy P completion-wins (default) | budget-wins
222
452
  --mode pty-orchestrator PTY-orchestrator mode (requires --target-agent)
223
453
  --target-agent <id> Required for --mode pty-orchestrator
454
+ --worker-dispatch <path> Prepared managed worker dispatch JSON (queues only)
224
455
  --expected-updated-at <time> Reject stale state with exit 3
225
456
  --request-id <id> Idempotent dispatch key`,
226
457
  run: `Usage: aiwg mc run [<session-id>] [--accept-cost]
@@ -382,9 +613,11 @@ async function mcDispatch(ctx) {
382
613
  // mission-dispatch accepts only the frozen objective envelope. Worker
383
614
  // bindings can arrive through its existing session, so inspect that payload.
384
615
  try {
385
- assertMissionWorkerHandoff(selected, 'mc-cli');
616
+ const cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
617
+ const support = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed' ? mcWorkerSupport() : undefined;
618
+ assertMissionWorkerHandoff(selected, 'mc-cli', support);
386
619
  for (const mission of selected.missions)
387
- assertMissionWorkerHandoff(mission, 'mc-cli');
620
+ assertMissionWorkerHandoff(mission, 'mc-cli', support);
388
621
  }
389
622
  catch (error) {
390
623
  const message = error instanceof Error ? error.message : String(error);
@@ -430,6 +663,24 @@ async function mcDispatch(ctx) {
430
663
  mode,
431
664
  targetAgent: targetAgent || undefined,
432
665
  };
666
+ const dispatchPath = parseFlag(ctx.args, '--worker-dispatch');
667
+ if (dispatchPath) {
668
+ try {
669
+ const cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
670
+ if (!cfg?.parallelism?.worker_dispatch || cfg.parallelism.worker_mode !== 'managed' || mode !== 'direct') {
671
+ throw new Error('OG_CONTRACT_UNSUPPORTED: worker dispatch requires the managed MC caller');
672
+ }
673
+ if (!isAbsolute(dispatchPath) || relative(ctx.cwd, dispatchPath).startsWith('..'))
674
+ throw new Error('Worker dispatch path must be inside the project');
675
+ const dispatch = await readWorkerJson(dispatchPath);
676
+ mission.extensions = { 'aiwg.worker-dispatch': dispatch };
677
+ assertMissionWorkerHandoff(mission, 'mc-cli', mcWorkerSupport());
678
+ await resolveMcWorker(ctx, selected, mission);
679
+ }
680
+ catch (error) {
681
+ return managedControlFailure(error);
682
+ }
683
+ }
433
684
  const result = await mutateSession(selected.id, 'dispatch', mission.id, flags.expectedUpdatedAt, flags.requestId, session => {
434
685
  if (session.state !== 'active')
435
686
  return 'invalid_state';
@@ -573,10 +824,13 @@ async function mcRun(ctx) {
573
824
  return { exitCode: 1 };
574
825
  }
575
826
  const queued = session.missions.filter((m) => m.status === 'queued');
827
+ let cfg;
576
828
  try {
577
- assertMissionWorkerHandoff(session, 'mc-cli');
829
+ cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
830
+ const support = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed' ? mcWorkerSupport() : undefined;
831
+ assertMissionWorkerHandoff(session, 'mc-cli', support);
578
832
  for (const mission of queued)
579
- assertMissionWorkerHandoff(mission, 'mc-cli');
833
+ assertMissionWorkerHandoff(mission, 'mc-cli', support);
580
834
  }
581
835
  catch (error) {
582
836
  const message = error instanceof Error ? error.message : String(error);
@@ -596,8 +850,15 @@ async function mcRun(ctx) {
596
850
  const eligible = queued.filter(m => m.mode !== 'pty-orchestrator' && !!m.completion);
597
851
  const projectRoot = ctx.cwd || process.cwd();
598
852
  const frameworkRoot = ctx.frameworkRoot;
599
- const { readAiwgConfig, resolveParallelism } = await import('../../config/aiwg-config.js');
600
- const cfg = await readAiwgConfig(projectRoot).catch(() => null);
853
+ let scheduler;
854
+ try {
855
+ scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
856
+ }
857
+ catch (error) {
858
+ const message = error instanceof Error ? error.message : String(error);
859
+ ui.error(message);
860
+ return { exitCode: 1, message };
861
+ }
601
862
  const provider = cfg?.providers[0] ?? 'unknown';
602
863
  const estimate = estimateMissionRunCost(eligible, provider);
603
864
  if (estimate.estimateUsd >= COST_WARNING_THRESHOLD_USD && !acceptCost) {
@@ -629,16 +890,20 @@ async function mcRun(ctx) {
629
890
  let launched = 0;
630
891
  let skipped = 0;
631
892
  let failed = 0;
632
- const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
633
- const maxConcurrent = resolveParallelism(cfg?.parallelism, provider).max_parallel_mc_missions;
634
- const scheduler = new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), {
635
- maxConcurrent,
636
- leaseTtlMs: 5 * 60_000,
637
- agingIntervalMs: 30_000,
638
- allowPreemption: false,
639
- defaultHostQuota: maxConcurrent,
640
- });
893
+ const managed = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed';
894
+ const allowanceOwner = managed ? await import('../../serve/shared-host-scheduler.js') : null;
895
+ // Qualify every worker before admission; one parent retains completion gates.
896
+ const workers = new Map();
897
+ try {
898
+ for (const mission of queued)
899
+ if (workerDispatch(mission))
900
+ workers.set(mission.id, await resolveMcWorker(ctx, session, mission));
901
+ }
902
+ catch (error) {
903
+ return managedControlFailure(error);
904
+ }
641
905
  for (const mission of queued) {
906
+ const worker = workers.get(mission.id);
642
907
  if (mission.mode === 'pty-orchestrator') {
643
908
  ui.warn(`Mission ${mission.id} mode=pty-orchestrator is not yet wired to mc run; skipping. Use 'aiwg ralph' directly for PTY-orchestrator workflows.`);
644
909
  skipped += 1;
@@ -651,7 +916,7 @@ async function mcRun(ctx) {
651
916
  }
652
917
  const admissionRequestId = mission.admissionState === 'released'
653
918
  ? `${session.id}.${mission.id}.${Date.now().toString(36)}`
654
- : mission.admissionRequestId ?? `${session.id}.${mission.id}`;
919
+ : mission.admissionRequestId ?? worker?.dispatch.requestId ?? `${session.id}.${mission.id}`;
655
920
  const admissionSubmittedAt = mission.admissionState === 'released'
656
921
  ? new Date().toISOString()
657
922
  : mission.admissionSubmittedAt ?? new Date().toISOString();
@@ -659,18 +924,35 @@ async function mcRun(ctx) {
659
924
  requestId: admissionRequestId,
660
925
  orchestratorId: session.id,
661
926
  environment: process.env.AIWG_ENVIRONMENT ?? 'default',
662
- provider,
927
+ provider: worker?.plan.invocation?.selected.inferenceProvider ?? provider,
663
928
  runtimeKind: 'host',
664
929
  priority: mission.priority === 'critical' ? 100 : mission.priority === 'high' ? 50 : mission.priority === 'low' ? 0 : 10,
665
930
  submittedAt: admissionSubmittedAt,
666
- queueTimeoutMs: 24 * 60 * 60_000,
931
+ queueTimeoutMs: managed ? (cfg?.parallelism?.admission?.queueTimeoutSeconds ?? 300) * 1000 : 24 * 60 * 60_000,
667
932
  preemptible: false,
668
- metadata: { missionId: mission.id },
933
+ metadata: { missionId: mission.id, ...(worker ? { invocationId: worker.dispatch.invocationId } : {}) },
934
+ ...(managed ? { managed: worker ? { kind: 'external', group: worker.plan.selected.group,
935
+ member: worker.plan.selected.member, cli: worker.plan.selected.cli, model: worker.plan.selected.modelKey,
936
+ quotaScope: cfg.parallelism.clis[worker.plan.selected.cli].quotaScope } : { kind: 'native' },
937
+ allowance: worker ? { runId: session.id, taskId: worker.intent.taskId,
938
+ taskBudgets: { maxAttempts: Math.min(worker.intent.limits.maxAttempts, mission.maxIterations),
939
+ maxTokens: Math.min(worker.intent.limits.tokens, mission.maxTotalTokens ?? Infinity),
940
+ maxCostUsd: worker.intent.limits.costUsd === null ? mission.maxTotalCost
941
+ : Math.min(worker.intent.limits.costUsd, mission.maxTotalCost ?? Infinity),
942
+ timeoutSeconds: Math.min(worker.intent.limits.wallSeconds, (mission.maxWallClockMinutes ?? Infinity) * 60) },
943
+ reservation: { tokens: worker.plan.invocation.limits.tokens, costUsd: worker.plan.invocation.limits.costUsd },
944
+ } : allowanceOwner.managedAllowanceFromOwner(session.id, mission.id, {
945
+ kind: 'ralph', maxIterations: mission.maxIterations, maxTotalTokens: mission.maxTotalTokens,
946
+ maxTotalCost: mission.maxTotalCost, maxWallClockMinutes: mission.maxWallClockMinutes,
947
+ }),
948
+ } : {}),
669
949
  });
670
950
  mission.admissionRequestId = admissionRequestId;
671
951
  mission.admissionSubmittedAt = admissionSubmittedAt;
672
952
  mission.admissionState = admission.state;
673
953
  mission.admissionLeaseExpiresAt = admission.leaseExpiresAt;
954
+ if (managed)
955
+ mission.admissionGeneration = admission.leaseGeneration;
674
956
  await writeSession(session);
675
957
  if (admission.state !== 'admitted') {
676
958
  await appendLog(session.id, {
@@ -684,21 +966,62 @@ async function mcRun(ctx) {
684
966
  continue;
685
967
  }
686
968
  try {
969
+ let maxWallClockMinutes = mission.maxWallClockMinutes;
970
+ if (managed) {
971
+ const scopes = scheduler.snapshot().allowances;
972
+ const deadlines = [scopes[admission.accounting.runKey].deadlineAt, scopes[admission.accounting.taskKey].deadlineAt]
973
+ .filter((value) => value !== undefined).map(Date.parse);
974
+ const remainingMs = Math.min(...deadlines) - Date.now();
975
+ if (remainingMs <= 0)
976
+ throw new Error('cumulative allowance deadline elapsed before launch');
977
+ maxWallClockMinutes = deadlines.length ? remainingMs / 60_000 : undefined;
978
+ }
979
+ if (worker) {
980
+ if (worker.brief.lease.generation !== mission.admissionGeneration || worker.brief.lease.reservationId !== admissionRequestId
981
+ || worker.brief.lease.authorityRef !== 'project')
982
+ throw new Error('Managed brief lease differs from current admission');
983
+ const invocationPath = join(projectRoot, SESSIONS_DIR, session.id, `${mission.id}.invocation.json`);
984
+ const runDir = join(projectRoot, SESSIONS_DIR, session.id, `${mission.id}.run`);
985
+ await fs.writeFile(invocationPath, JSON.stringify(worker.plan), { mode: 0o600 });
986
+ mission.managedWorker = { invocationPath, runDir };
987
+ }
988
+ if (managed) {
989
+ scheduler.claimLaunch(admissionRequestId, mission.admissionGeneration);
990
+ mission.admissionLaunchClaimed = true;
991
+ await writeSession(session);
992
+ }
687
993
  const result = await launchExternalRalph(frameworkRoot, projectRoot, {
688
994
  objective: mission.objective,
689
995
  completionCriteria: mission.completion,
690
- maxIterations: mission.maxIterations,
691
- maxTotalTokens: mission.maxTotalTokens,
996
+ maxIterations: managed ? admission.accounting.attemptCount : mission.maxIterations,
997
+ maxTotalTokens: managed ? admission.accounting.reservation.tokens ?? undefined : mission.maxTotalTokens,
692
998
  maxOutputTokens: mission.maxOutputTokens,
693
999
  maxToolCalls: mission.maxToolCalls,
694
- maxTotalCost: mission.maxTotalCost,
695
- maxWallClockMinutes: mission.maxWallClockMinutes,
1000
+ maxTotalCost: managed ? admission.accounting.reservation.costUsd ?? undefined : mission.maxTotalCost,
1001
+ maxWallClockMinutes,
696
1002
  explorationQuota: mission.explorationQuota,
697
1003
  budgetStopPolicy: mission.budgetStopPolicy,
698
1004
  // Defaults for cycle 1; advanced options can be added per-mission later.
699
1005
  verbose: false,
1006
+ ...(managed ? { managedAdmission: { requestId: admissionRequestId, generation: mission.admissionGeneration } } : {}),
1007
+ ...(worker ? { managedWorker: mission.managedWorker } : {}),
700
1008
  });
1009
+ if (managed) {
1010
+ if (!result.execution || result.execution.pid !== result.pid)
1011
+ throw new Error('managed launch requires its OS process birth identity');
1012
+ scheduler.bindExecution(admissionRequestId, mission.admissionGeneration, result.execution);
1013
+ mission.admissionExecution = result.execution;
1014
+ mission.ralphLoopId = result.loopId;
1015
+ mission.ralphPid = result.pid;
1016
+ const current = scheduler.renew(admissionRequestId, mission.admissionGeneration);
1017
+ if (current.state !== 'admitted') {
1018
+ await cancelManagedMission(ctx, scheduler, mission);
1019
+ throw new Error('managed launch lost admission during execution checkpoint; reconcile uncertain work');
1020
+ }
1021
+ }
701
1022
  mission.status = 'running';
1023
+ if (worker)
1024
+ worker.dispatch.state = 'running';
702
1025
  mission.startedAt = new Date().toISOString();
703
1026
  mission.ralphLoopId = result.loopId;
704
1027
  mission.ralphPid = result.pid;
@@ -717,8 +1040,12 @@ async function mcRun(ctx) {
717
1040
  mission.status = 'failed';
718
1041
  mission.error = msg;
719
1042
  mission.completedAt = new Date().toISOString();
720
- scheduler.release(admissionRequestId);
721
- mission.admissionState = 'released';
1043
+ if (managed)
1044
+ mission.admissionState = scheduler.stop(admissionRequestId, mission.admissionGeneration, 'observer-loss').state;
1045
+ else {
1046
+ scheduler.release(admissionRequestId);
1047
+ mission.admissionState = 'released';
1048
+ }
722
1049
  mission.admissionLeaseExpiresAt = undefined;
723
1050
  await writeSession(session);
724
1051
  await appendLog(session.id, {
@@ -752,22 +1079,33 @@ async function mcRun(ctx) {
752
1079
  */
753
1080
  async function syncMissionsFromRalph(session, projectRoot) {
754
1081
  let mutated = false;
755
- const { readAiwgConfig, resolveParallelism } = await import('../../config/aiwg-config.js');
756
- const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
757
- const cfg = await readAiwgConfig(projectRoot).catch(() => null);
758
- const provider = cfg?.providers[0] ?? 'unknown';
759
- const maxConcurrent = resolveParallelism(cfg?.parallelism, provider).max_parallel_mc_missions;
760
- const scheduler = new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), { maxConcurrent, leaseTtlMs: 5 * 60_000, agingIntervalMs: 30_000, allowPreemption: false, defaultHostQuota: maxConcurrent });
1082
+ const cfg = await readAdmissionConfig(projectRoot);
1083
+ const scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
1084
+ const managed = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed';
761
1085
  for (const mission of session.missions) {
762
- if (mission.status !== 'running')
1086
+ const wasRunning = mission.status === 'running';
1087
+ if (!wasRunning && !(managed && mission.managedWorker && mission.admissionState === 'uncertain'))
763
1088
  continue;
764
1089
  if (!mission.ralphLoopId)
765
1090
  continue;
766
1091
  const ralphStatePath = join(projectRoot, '.aiwg', 'ralph-external', 'loops', mission.ralphLoopId, 'session-state.json');
1092
+ if (managed) {
1093
+ const current = mission.admissionRequestId ? scheduler.snapshot().records[mission.admissionRequestId] : undefined;
1094
+ if (!current || !mission.admissionGeneration || current.leaseGeneration !== mission.admissionGeneration)
1095
+ continue;
1096
+ }
767
1097
  try {
768
- const raw = await fs.readFile(ralphStatePath, 'utf-8');
769
- const state = JSON.parse(raw);
1098
+ const state = managed && mission.managedWorker
1099
+ ? await readManagedWorkerState(mission, scheduler)
1100
+ : JSON.parse(await fs.readFile(ralphStatePath, 'utf-8'));
770
1101
  const ralphStatus = String(state.status || '').toLowerCase();
1102
+ if (managed && mission.admissionRequestId && state.health !== undefined) {
1103
+ if (!validateWorkerPayload('workerHealthSummary', state.health).valid)
1104
+ throw new Error('invalid Ralph worker health');
1105
+ const observed = scheduler.observeHealth(mission.admissionRequestId, mission.admissionGeneration, state.health);
1106
+ mission.admissionState = observed.state;
1107
+ mutated = true;
1108
+ }
771
1109
  // Map ralph status → mc mission status
772
1110
  let nextStatus = null;
773
1111
  if (ralphStatus === 'completed')
@@ -778,6 +1116,8 @@ async function syncMissionsFromRalph(session, projectRoot) {
778
1116
  nextStatus = 'aborted';
779
1117
  else if (ralphStatus === 'paused')
780
1118
  nextStatus = 'paused';
1119
+ if (!wasRunning)
1120
+ nextStatus = null;
781
1121
  // 'running' stays running
782
1122
  // Reflect iteration count if available
783
1123
  const iter = typeof state.iteration === 'number' ? state.iteration : (typeof state.currentIteration === 'number' ? state.currentIteration : null);
@@ -797,17 +1137,29 @@ async function syncMissionsFromRalph(session, projectRoot) {
797
1137
  }
798
1138
  mutated = true;
799
1139
  if (mission.admissionRequestId) {
800
- try {
801
- scheduler.release(mission.admissionRequestId);
1140
+ if (managed) {
1141
+ if (!mission.managedWorker) {
1142
+ if (state.health)
1143
+ scheduler.acceptFinal(mission.admissionRequestId, mission.admissionGeneration, state.health);
1144
+ }
1145
+ if (mission.admissionState !== 'settled')
1146
+ mission.admissionState = scheduler.stop(mission.admissionRequestId, mission.admissionGeneration, 'observer-loss').state;
1147
+ }
1148
+ else {
1149
+ try {
1150
+ scheduler.release(mission.admissionRequestId);
1151
+ }
1152
+ catch { /* already expired or released */ }
1153
+ mission.admissionState = 'released';
802
1154
  }
803
- catch { /* already expired or released */ }
804
- mission.admissionState = 'released';
805
1155
  mission.admissionLeaseExpiresAt = undefined;
806
1156
  }
807
1157
  }
808
1158
  else if (mission.admissionRequestId) {
809
1159
  try {
810
- const renewed = scheduler.renew(mission.admissionRequestId);
1160
+ const renewed = managed
1161
+ ? scheduler.renew(mission.admissionRequestId, mission.admissionGeneration)
1162
+ : scheduler.renew(mission.admissionRequestId);
811
1163
  mission.admissionState = renewed.state;
812
1164
  mission.admissionLeaseExpiresAt = renewed.leaseExpiresAt;
813
1165
  mutated = true;
@@ -818,7 +1170,12 @@ async function syncMissionsFromRalph(session, projectRoot) {
818
1170
  }
819
1171
  }
820
1172
  catch {
821
- // State file missing/unreadable — leave mission status unchanged.
1173
+ if (managed && mission.admissionRequestId) {
1174
+ mission.admissionState = scheduler.disconnect(mission.admissionRequestId, mission.admissionGeneration).state;
1175
+ mission.admissionLeaseExpiresAt = undefined;
1176
+ mutated = true;
1177
+ }
1178
+ // Missing supervision preserves the last task status and all artifacts.
822
1179
  }
823
1180
  }
824
1181
  if (mutated) {
@@ -909,6 +1266,46 @@ async function mcStatus(ctx) {
909
1266
  ui.blank();
910
1267
  return { exitCode: 0 };
911
1268
  }
1269
+ async function managedControlScheduler(ctx, missions) {
1270
+ const projectRoot = ctx.cwd || process.cwd();
1271
+ const cfg = await readAdmissionConfig(projectRoot);
1272
+ if (!cfg?.parallelism?.worker_dispatch || cfg.parallelism.worker_mode !== 'managed')
1273
+ return undefined;
1274
+ const scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
1275
+ const snapshot = scheduler.snapshot();
1276
+ for (const mission of missions) {
1277
+ if (!mission.admissionRequestId)
1278
+ continue;
1279
+ const record = snapshot.records[mission.admissionRequestId];
1280
+ if (!record || !mission.admissionGeneration || record.leaseGeneration !== mission.admissionGeneration) {
1281
+ throw new Error('stale managed Mission checkpoint generation');
1282
+ }
1283
+ }
1284
+ return scheduler;
1285
+ }
1286
+ function managedControlFailure(error) {
1287
+ const message = error instanceof Error ? error.message : String(error);
1288
+ ui.error(message);
1289
+ return { exitCode: 1, message };
1290
+ }
1291
+ async function cancelManagedMission(ctx, scheduler, mission) {
1292
+ if (!mission.admissionRequestId)
1293
+ return;
1294
+ const cancelled = scheduler.cancel(mission.admissionRequestId, mission.admissionGeneration);
1295
+ mission.admissionState = cancelled.state;
1296
+ mission.admissionLeaseExpiresAt = undefined;
1297
+ if (cancelled.state !== 'uncertain' || !mission.ralphLoopId || !mission.admissionExecution)
1298
+ return;
1299
+ try {
1300
+ const { cancelManagedRalph } = await import('./ralph-launcher.js');
1301
+ await cancelManagedRalph(ctx.frameworkRoot, ctx.cwd || process.cwd(), mission.ralphLoopId, {
1302
+ requestId: mission.admissionRequestId, generation: mission.admissionGeneration, execution: mission.admissionExecution,
1303
+ });
1304
+ }
1305
+ catch {
1306
+ // Reservation is already uncertain; unavailable signalling cannot release it.
1307
+ }
1308
+ }
912
1309
  async function mcAbort(ctx) {
913
1310
  const positional = getPositionalArgs(ctx.args);
914
1311
  const sessionId = positional[0];
@@ -917,6 +1314,14 @@ async function mcAbort(ctx) {
917
1314
  ui.error('Usage: aiwg mc abort <session-id> <mission-id>');
918
1315
  return { exitCode: 1 };
919
1316
  }
1317
+ let managedScheduler;
1318
+ try {
1319
+ const selected = await readSession(sessionId);
1320
+ managedScheduler = await managedControlScheduler(ctx, selected?.missions.filter(mission => mission.id === missionId) ?? []);
1321
+ }
1322
+ catch (error) {
1323
+ return managedControlFailure(error);
1324
+ }
920
1325
  const flags = mutationFlags(ctx.args);
921
1326
  const result = await mutateSession(sessionId, 'cancel', missionId, flags.expectedUpdatedAt, flags.requestId, session => {
922
1327
  const mission = session.missions.find(m => m.id === missionId);
@@ -934,17 +1339,21 @@ async function mcAbort(ctx) {
934
1339
  const mission = result.session.missions.find(candidate => candidate.id === missionId);
935
1340
  if (mission?.admissionRequestId) {
936
1341
  try {
937
- const projectRoot = ctx.cwd || process.cwd();
938
- const { readAiwgConfig, resolveParallelism } = await import('../../config/aiwg-config.js');
939
- const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
940
- const cfg = await readAiwgConfig(projectRoot).catch(() => null);
941
- const provider = cfg?.providers[0] ?? 'unknown';
942
- const maxConcurrent = resolveParallelism(cfg?.parallelism, provider).max_parallel_mc_missions;
943
- const scheduler = new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), { maxConcurrent, leaseTtlMs: 5 * 60_000, agingIntervalMs: 30_000, allowPreemption: false, defaultHostQuota: maxConcurrent });
944
- scheduler.cancel(mission.admissionRequestId);
1342
+ if (managedScheduler) {
1343
+ await cancelManagedMission(ctx, managedScheduler, mission);
1344
+ await writeSession(result.session);
1345
+ }
1346
+ else {
1347
+ const projectRoot = ctx.cwd || process.cwd();
1348
+ const cfg = await readAdmissionConfig(projectRoot);
1349
+ const scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
1350
+ scheduler.cancel(mission.admissionRequestId);
1351
+ }
945
1352
  }
946
- catch {
947
- // The mission state is authoritative; lease expiry provides recovery.
1353
+ catch (error) {
1354
+ if (managedScheduler)
1355
+ return managedControlFailure(error);
1356
+ // Legacy leases keep their existing best-effort cancellation behavior.
948
1357
  }
949
1358
  }
950
1359
  }
@@ -959,6 +1368,13 @@ async function mcPause(ctx) {
959
1368
  ui.error('No active session to pause.');
960
1369
  return { exitCode: 1 };
961
1370
  }
1371
+ let managedScheduler;
1372
+ try {
1373
+ managedScheduler = await managedControlScheduler(ctx, selected.missions.filter(mission => mission.status === 'running'));
1374
+ }
1375
+ catch (error) {
1376
+ return managedControlFailure(error);
1377
+ }
962
1378
  const flags = mutationFlags(ctx.args);
963
1379
  const result = await mutateSession(selected.id, 'pause', selected.id, flags.expectedUpdatedAt, flags.requestId, session => {
964
1380
  if (session.state !== 'active')
@@ -971,6 +1387,13 @@ async function mcPause(ctx) {
971
1387
  });
972
1388
  if (!result.ok)
973
1389
  return mutationFailure(result);
1390
+ if (!result.replayed && managedScheduler) {
1391
+ for (const mission of result.session.missions) {
1392
+ if (mission.status === 'paused')
1393
+ await cancelManagedMission(ctx, managedScheduler, mission);
1394
+ }
1395
+ await writeSession(result.session);
1396
+ }
974
1397
  ui.success(`${result.replayed ? 'Replayed pause for' : 'Paused'} session: ${selected.id}`);
975
1398
  return { exitCode: 0, message: JSON.stringify({ ok: true, replayed: result.replayed, updated_at: result.session.updatedAt }) };
976
1399
  }
@@ -982,6 +1405,26 @@ async function mcResume(ctx) {
982
1405
  ui.error('No paused session to resume.');
983
1406
  return { exitCode: 1 };
984
1407
  }
1408
+ try {
1409
+ const cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
1410
+ if (cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed') {
1411
+ const scheduler = await createMcAdmissionScheduler(ctx.cwd || process.cwd(), cfg);
1412
+ const snapshot = scheduler.snapshot();
1413
+ for (const mission of selected.missions.filter(candidate => candidate.status === 'paused')) {
1414
+ if (!mission.admissionRequestId)
1415
+ continue;
1416
+ const record = snapshot.records[mission.admissionRequestId];
1417
+ if (!record || record.leaseGeneration !== mission.admissionGeneration || record.state !== 'admitted') {
1418
+ throw new Error('managed resume requires current admission and reconciliation of uncertain work');
1419
+ }
1420
+ }
1421
+ }
1422
+ }
1423
+ catch (error) {
1424
+ const message = error instanceof Error ? error.message : String(error);
1425
+ ui.error(message);
1426
+ return { exitCode: 1, message };
1427
+ }
985
1428
  const flags = mutationFlags(ctx.args);
986
1429
  const result = await mutateSession(selected.id, 'resume', selected.id, flags.expectedUpdatedAt, flags.requestId, session => {
987
1430
  if (session.state !== 'paused')
@@ -1006,6 +1449,13 @@ async function mcStop(ctx) {
1006
1449
  ui.error('No active session to stop.');
1007
1450
  return { exitCode: 1 };
1008
1451
  }
1452
+ let managedScheduler;
1453
+ try {
1454
+ managedScheduler = await managedControlScheduler(ctx, session.missions.filter(mission => mission.status === 'queued' || (!drain && (mission.status === 'running' || mission.status === 'paused'))));
1455
+ }
1456
+ catch (error) {
1457
+ return managedControlFailure(error);
1458
+ }
1009
1459
  if (drain) {
1010
1460
  // Mark queued missions as aborted, let running finish
1011
1461
  for (const m of session.missions) {
@@ -1025,6 +1475,12 @@ async function mcStop(ctx) {
1025
1475
  }
1026
1476
  }
1027
1477
  }
1478
+ if (managedScheduler) {
1479
+ for (const mission of session.missions) {
1480
+ if (mission.status === 'aborted')
1481
+ await cancelManagedMission(ctx, managedScheduler, mission);
1482
+ }
1483
+ }
1028
1484
  session.state = 'stopped';
1029
1485
  await writeSession(session);
1030
1486
  await appendLog(session.id, { event: 'session_stopped', drain });