@zq-silk/yui 0.15.7 → 0.15.9

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 (308) hide show
  1. package/ARCHITECTURE.md +194 -399
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +306 -1131
  4. package/dist/agent/adapterCatalog.js +15 -2
  5. package/dist/agent/agent.js +23 -3
  6. package/dist/agent/argumentPolicy.js +7 -1
  7. package/dist/agent/connectionPlan.js +62 -0
  8. package/dist/agent/executionComponents.js +158 -0
  9. package/dist/agent/launchEnvironment.js +31 -3
  10. package/dist/agent/managedRuntimeEnvironment.js +3 -5
  11. package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
  12. package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
  13. package/dist/artifacts/artifactCapability.js +74 -0
  14. package/dist/artifacts/artifactCommitLock.js +249 -0
  15. package/dist/artifacts/artifactPaths.js +151 -0
  16. package/dist/artifacts/gitArtifactRef.js +146 -0
  17. package/dist/artifacts/managedGit.js +332 -0
  18. package/dist/artifacts/taskArtifactRepository.js +277 -0
  19. package/dist/brief/taskBrief.js +12 -0
  20. package/dist/cli/agentConfigurationPicker.js +13 -0
  21. package/dist/cli/commandCatalog.js +165 -74
  22. package/dist/cli/interactionCandidates.js +5 -5
  23. package/dist/cli/interactionPolicy.js +38 -8
  24. package/dist/cli/invocationRouter.js +1 -1
  25. package/dist/cli/managedDiagnostics.js +28 -0
  26. package/dist/cli/operatorWizard.js +1 -7
  27. package/dist/cli/roleOptionOrder.js +27 -0
  28. package/dist/cli/roleWizard.js +50 -14
  29. package/dist/cli/updateOrchestrator.js +1 -1
  30. package/dist/cli/updatePorts.js +3 -4
  31. package/dist/cli.js +245 -95
  32. package/dist/commands/agentCommands.js +72 -14
  33. package/dist/commands/capabilityCommands.js +9 -6
  34. package/dist/commands/configCommands.js +20 -20
  35. package/dist/commands/deliveryGuardPreflight.js +2 -2
  36. package/dist/commands/executionAuditCommands.js +24 -24
  37. package/dist/commands/globalRoleCommands.js +1 -1
  38. package/dist/commands/grantCommands.js +4 -4
  39. package/dist/commands/operatorCommands.js +34 -9
  40. package/dist/commands/projectCommands.js +4 -4
  41. package/dist/commands/resourcesCommands.js +2 -2
  42. package/dist/commands/roleConfiguration.js +25 -5
  43. package/dist/commands/roleRuntimeGuard.js +4 -5
  44. package/dist/commands/sessionCommands.js +3 -7
  45. package/dist/commands/taskActivationCommands.js +281 -0
  46. package/dist/commands/taskActor.js +28 -49
  47. package/dist/commands/taskCommands.js +1461 -720
  48. package/dist/commands/taskContextCommand.js +39 -583
  49. package/dist/commands/taskExecutionCommands.js +32 -32
  50. package/dist/commands/taskInputCommands.js +40 -104
  51. package/dist/commands/taskIntegrationCommands.js +3 -2
  52. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  53. package/dist/commands/taskNextActionCommand.js +8 -8
  54. package/dist/commands/taskOverviewCommand.js +33 -45
  55. package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
  56. package/dist/commands/taskRoleRuntimeStatus.js +133 -102
  57. package/dist/commands/telemetryCommands.js +36 -38
  58. package/dist/config/configCatalog.js +4 -4
  59. package/dist/config/yuiConfig.js +8 -8
  60. package/dist/context/contextSnapshot.js +10 -10
  61. package/dist/context/dispatchContext.js +11 -11
  62. package/dist/context/roleSessionContext.js +6 -3
  63. package/dist/context/{turnContextPack.js → runContextPack.js} +158 -81
  64. package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
  65. package/dist/context/sessionBootstrapManifest.js +21 -2
  66. package/dist/context/sourceRunContext.js +30 -0
  67. package/dist/context/taskContext.js +481 -0
  68. package/dist/context/wakeNotification.js +27 -27
  69. package/dist/controller/agentRuntimeObserver.js +21 -24
  70. package/dist/controller/capabilityBridge.js +17 -6
  71. package/dist/controller/clientRuntime.js +65 -92
  72. package/dist/controller/controller.js +120 -188
  73. package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
  74. package/dist/controller/jobControl.js +54 -85
  75. package/dist/controller/resourceInventory.js +8 -27
  76. package/dist/controller/resourceInventoryLinux.js +12 -13
  77. package/dist/controller/runtime.js +529 -479
  78. package/dist/controller/runtimeEventInbox.js +55 -25
  79. package/dist/controller/runtimeEventProcessor.js +22 -31
  80. package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
  81. package/dist/controller/runtimeLaunchCoordinator.js +80 -426
  82. package/dist/controller/runtimeObservationHook.js +14 -18
  83. package/dist/controller/sessionNotify.js +16 -24
  84. package/dist/controller/sessionOwnerReconciliation.js +168 -50
  85. package/dist/controller/structuredProviderObservation.js +138 -99
  86. package/dist/coordination/workMailbox.js +3 -3
  87. package/dist/coordination/workMailboxQueue.js +36 -33
  88. package/dist/core/boundedRpc.js +8 -1
  89. package/dist/core/controllerClient.js +20 -1
  90. package/dist/core/controllerServer.js +4 -4
  91. package/dist/doctor/doctor.js +13 -2
  92. package/dist/domain/agentResultTransport.js +9 -9
  93. package/dist/execution/codexThreadNaming.js +2 -8
  94. package/dist/execution/executionHealth.js +51 -63
  95. package/dist/execution/reviewMainRun.js +137 -0
  96. package/dist/execution/workItemExecution.js +28 -29
  97. package/dist/execution/workItemExecutionProjection.js +99 -107
  98. package/dist/execution/workItemMainRun.js +141 -0
  99. package/dist/executor/agentAdapter.js +227 -20
  100. package/dist/executor/agentConfigurationCatalog.js +126 -4
  101. package/dist/executor/agentConfigurationProbe.js +162 -4
  102. package/dist/executor/agentExecutor.js +79 -78
  103. package/dist/executor/effectiveLaunch.js +105 -18
  104. package/dist/executor/executorRegistry.js +29 -44
  105. package/dist/executor/fileRoleLaunchPlanner.js +229 -154
  106. package/dist/executor/workspacePreflightClassification.js +16 -16
  107. package/dist/grant/capabilityGrant.js +6 -3
  108. package/dist/input/inputRequest.js +12 -10
  109. package/dist/integration/gitIntegrationService.js +4 -11
  110. package/dist/integration/integrationQueueService.js +4 -4
  111. package/dist/interaction/operatorPresentation.js +1 -1
  112. package/dist/kernel/builtinCapabilities.js +255 -12
  113. package/dist/kernel/capabilityRegistry.js +64 -18
  114. package/dist/kernel/instanceHost.js +12 -1
  115. package/dist/kernel/kernelPorts.js +2 -2
  116. package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
  117. package/dist/lifecycle/exactRunTerminalization.js +449 -0
  118. package/dist/message/message.js +118 -6
  119. package/dist/message/messageContinuation.js +204 -0
  120. package/dist/observability/executionAudit.js +70 -72
  121. package/dist/observability/faultClassification.js +2 -2
  122. package/dist/observability/orchestrationMetrics.js +8 -8
  123. package/dist/operator/operatorSessionHistory.js +1 -7
  124. package/dist/output/agentConfigurationPresentation.js +8 -3
  125. package/dist/output/agentRunConfigurationPresentation.js +128 -0
  126. package/dist/output/rolePresentation.js +54 -3
  127. package/dist/plugins/pluginChild.js +104 -0
  128. package/dist/plugins/pluginIntent.js +26 -0
  129. package/dist/plugins/pluginInterpreter.js +43 -0
  130. package/dist/plugins/pluginPackage.js +101 -0
  131. package/dist/plugins/pluginProcess.js +112 -0
  132. package/dist/plugins/pluginService.js +388 -0
  133. package/dist/profile/agentProfile.js +1 -1
  134. package/dist/repository/gitWorkspace.js +26 -4
  135. package/dist/repository/project.js +19 -4
  136. package/dist/repository/taskBaseFreshness.js +13 -13
  137. package/dist/repository/taskWorkspaceCoordinator.js +20 -27
  138. package/dist/repository/taskWorkspacePreparer.js +344 -83
  139. package/dist/resources/autoResourceGc.js +3 -3
  140. package/dist/resources/liveReferences.js +3 -3
  141. package/dist/resources/projectResource.js +75 -0
  142. package/dist/resources/projectResourceService.js +343 -0
  143. package/dist/resources/resourceDiscovery.js +6 -6
  144. package/dist/resources/resourceGc.js +1 -1
  145. package/dist/resources/resourceRegistrar.js +1 -1
  146. package/dist/resources/resourceTypes.js +1 -1
  147. package/dist/review/deltaRecheck.js +3 -3
  148. package/dist/review/reviewAcceptance.js +16 -16
  149. package/dist/review/reviewDecision.js +7 -7
  150. package/dist/review/reviewRound.js +21 -20
  151. package/dist/review/reviewerAvailability.js +2 -2
  152. package/dist/role/role.js +51 -7
  153. package/dist/role/taskRoleUpdate.js +30 -0
  154. package/dist/runtime/acpProtocol.js +425 -0
  155. package/dist/runtime/acpSession.js +731 -0
  156. package/dist/runtime/acpSessionConfiguration.js +260 -0
  157. package/dist/runtime/agentDriver.js +30 -11
  158. package/dist/runtime/agentEndpoint.js +278 -0
  159. package/dist/runtime/agentEndpointIdentity.js +86 -0
  160. package/dist/runtime/agentEndpointOwnership.js +239 -0
  161. package/dist/runtime/agentError.js +2 -10
  162. package/dist/runtime/agentHost.js +565 -314
  163. package/dist/runtime/agentRunConfiguration.js +258 -0
  164. package/dist/runtime/builtinAgentDrivers.js +134 -18
  165. package/dist/runtime/builtinAgentErrorMappers.js +55 -3
  166. package/dist/runtime/builtinTranscriptUsage.js +1 -1
  167. package/dist/runtime/claude-process-owner +0 -0
  168. package/dist/runtime/codexAppServerRuntime.js +38 -30
  169. package/dist/runtime/codexInteractiveHost.js +41 -6
  170. package/dist/runtime/continuationManager.js +2 -6
  171. package/dist/runtime/executionEnvironment.js +30 -0
  172. package/dist/runtime/firstProgressAdvisory.js +11 -11
  173. package/dist/runtime/index.js +4 -3
  174. package/dist/runtime/jsonLineChannel.js +109 -0
  175. package/dist/runtime/launchBroker.js +91 -16
  176. package/dist/runtime/launchDiagnostics.js +2 -2
  177. package/dist/runtime/lifecycleReservation.js +10 -18
  178. package/dist/runtime/managedCaller.js +61 -17
  179. package/dist/runtime/nativeSessionControl.js +102 -0
  180. package/dist/runtime/ports.js +6 -21
  181. package/dist/runtime/processExitObservation.js +8 -7
  182. package/dist/runtime/promptEnvelope.js +17 -6
  183. package/dist/runtime/providerContinuation.js +3 -9
  184. package/dist/runtime/providerContinuationReconciliationService.js +4 -13
  185. package/dist/runtime/providerControl.js +2 -7
  186. package/dist/runtime/providerRuntimeIdentity.js +110 -222
  187. package/dist/runtime/providerRuntimeReconciler.js +5 -9
  188. package/dist/runtime/runtimeBinding.js +0 -1
  189. package/dist/runtime/runtimeContinuationProjection.js +4 -7
  190. package/dist/runtime/runtimeDeadlines.js +9 -0
  191. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  192. package/dist/runtime/runtimeObservation.js +29 -65
  193. package/dist/runtime/runtimeProjection.js +43 -51
  194. package/dist/runtime/runtimeSessionCandidate.js +1 -3
  195. package/dist/runtime/sessionLaunchRequest.js +3 -7
  196. package/dist/runtime/sessionOwnerIdentity.js +7 -54
  197. package/dist/runtime/sessionOwnerRegistry.js +22 -17
  198. package/dist/runtime/sessionReconciliation.js +4 -8
  199. package/dist/runtime/sessionTerminationGuard.js +70 -259
  200. package/dist/runtime/sessionTokenMetrics.js +5 -16
  201. package/dist/runtime/structuredProviderHost.js +237 -117
  202. package/dist/runtime/taskRuntimeIsolation.js +39 -122
  203. package/dist/runtime/tmuxAdapters.js +39 -86
  204. package/dist/scheduler/activeRoleRunDelivery.js +354 -0
  205. package/dist/scheduler/leaderWakeupProcessor.js +75 -266
  206. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  207. package/dist/scheduler/ports.js +80 -9
  208. package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
  209. package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
  210. package/dist/scheduler/taskExecutionProjection.js +120 -124
  211. package/dist/scheduler/taskObservabilityProjection.js +29 -29
  212. package/dist/scheduler/taskWake.js +11 -4
  213. package/dist/scheduler/wakeReason.js +9 -1
  214. package/dist/setup/setupCommand.js +3 -7
  215. package/dist/storage/migrations/agentRunContract.js +159 -0
  216. package/dist/storage/migrations/artifactsToGit.js +338 -0
  217. package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
  218. package/dist/storage/migrations/submitIntent.js +126 -0
  219. package/dist/storage/sqliteSchema.js +467 -7
  220. package/dist/storage/sqliteStore.js +355 -220
  221. package/dist/storage/storageVersions.js +1 -1
  222. package/dist/storage/storeRpc.js +10 -5
  223. package/dist/storage/taskStore.js +13 -11
  224. package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
  225. package/dist/surface/surfaceContributions.js +102 -0
  226. package/dist/task/completionReadiness.js +32 -6
  227. package/dist/task/deliveryGuard.js +16 -16
  228. package/dist/task/draftPlan.js +72 -12
  229. package/dist/task/nextAction.js +144 -128
  230. package/dist/task/remoteDelivery.js +6 -6
  231. package/dist/task/task.js +184 -18
  232. package/dist/task/taskActivation.js +327 -0
  233. package/dist/task/taskActivationService.js +408 -0
  234. package/dist/task/taskRecordReference.js +5 -4
  235. package/dist/task/taskRecordRetirement.js +1 -1
  236. package/dist/task/taskSubmission.js +236 -0
  237. package/dist/telemetry/sqliteTelemetryStore.js +55 -68
  238. package/dist/telemetry/telemetryConfig.js +14 -14
  239. package/dist/telemetry/telemetryWiring.js +2 -2
  240. package/dist/web/assets/assetManifest.js +2 -0
  241. package/dist/web/assets/client/app.js +121 -20
  242. package/dist/web/assets/client/components.js +87 -54
  243. package/dist/web/assets/client/i18n.js +83 -41
  244. package/dist/web/assets/client/markdown.js +1 -1
  245. package/dist/web/assets/client/taskSurface.js +442 -0
  246. package/dist/web/assets/client/view.js +49 -44
  247. package/dist/web/assets/shell.js +1 -1
  248. package/dist/web/assets/styles/cards.js +22 -4
  249. package/dist/web/controllerWeb.js +60 -0
  250. package/dist/web/webMutation.js +28 -0
  251. package/dist/web/webServer.js +133 -8
  252. package/dist/web/webSnapshot.js +81 -74
  253. package/dist/web/webTaskSurface.js +64 -0
  254. package/dist/workItem/dependencyGate.js +1 -1
  255. package/dist/workItem/workItem.js +84 -48
  256. package/dist/workspace/workItemChangeSetManager.js +16 -9
  257. package/docs/agent-result-consumption.md +96 -0
  258. package/docs/agent-result-consumption.zh-CN.md +81 -0
  259. package/docs/agent-runtime-drivers.md +93 -0
  260. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  261. package/docs/architecture/README.md +50 -0
  262. package/docs/architecture/README.zh-CN.md +43 -0
  263. package/docs/architecture/capabilities-and-resources.md +118 -0
  264. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  265. package/docs/managed-turn-and-session-runtime.md +224 -0
  266. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  267. package/docs/observability/README.md +83 -0
  268. package/docs/observability/README.zh-CN.md +71 -0
  269. package/docs/plugin-sdk.md +393 -0
  270. package/docs/plugin-sdk.zh-CN.md +293 -0
  271. package/docs/provider-runtime.md +165 -0
  272. package/docs/provider-runtime.zh-CN.md +132 -0
  273. package/docs/release-workflow.md +305 -0
  274. package/docs/release-workflow.zh-CN.md +237 -0
  275. package/docs/roles-and-configuration.md +115 -0
  276. package/docs/roles-and-configuration.zh-CN.md +96 -0
  277. package/docs/sqlite-control-plane-design.md +78 -0
  278. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  279. package/docs/task-dag-semantics.md +80 -0
  280. package/docs/task-dag-semantics.zh-CN.md +59 -0
  281. package/docs/task-delivery.md +105 -0
  282. package/docs/task-delivery.zh-CN.md +82 -0
  283. package/docs/task-local-identity.md +8 -6
  284. package/docs/task-local-identity.zh-CN.md +58 -0
  285. package/docs/testing/verification-levels.md +88 -0
  286. package/docs/testing/verification-levels.zh-CN.md +69 -0
  287. package/i18n/README.zh-CN.md +270 -722
  288. package/package.json +3 -2
  289. package/skills/yui-leader/SKILL.md +88 -304
  290. package/skills/yui-leader/references/execution.md +303 -0
  291. package/skills/yui-leader/references/integration.md +39 -0
  292. package/skills/yui-leader/references/planning.md +109 -0
  293. package/skills/yui-leader/references/replicated-execution.md +42 -0
  294. package/skills/yui-leader/references/task-plugins.md +37 -0
  295. package/skills/yui-operator/SKILL.md +46 -62
  296. package/skills/yui-reviewer/SKILL.md +35 -36
  297. package/skills/yui-runtime/SKILL.md +88 -24
  298. package/skills/yui-runtime/references/publication.md +22 -0
  299. package/skills/yui-runtime/references/recovery.md +64 -0
  300. package/skills/yui-worker/SKILL.md +37 -39
  301. package/dist/cli/roleOptionCatalog.js +0 -68
  302. package/dist/context/sourceTurnContext.js +0 -30
  303. package/dist/execution/reviewMainTurn.js +0 -161
  304. package/dist/execution/workItemMainTurn.js +0 -164
  305. package/dist/lifecycle/exactTurnTerminalization.js +0 -407
  306. package/dist/runtime/preallocatedNativeSession.js +0 -13
  307. package/dist/runtime/runtimeStopReceipt.js +0 -42
  308. package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
@@ -0,0 +1,141 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { enqueueRoleRunDispatch } from "../coordination/workMailboxQueue.js";
3
+ import { createRunInput } from "../context/runInputContract.js";
4
+ import { contextSnapshotDeltaRefIds, freezeRunContextSnapshot } from "../context/runContextPack.js";
5
+ import { contextSnapshotRef } from "../context/contextSnapshot.js";
6
+ import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
7
+ import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
8
+ import { createTaskEvent } from "../event/taskEvent.js";
9
+ import { createRun } from "../agentRun/agentRun.js";
10
+ import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
11
+ export function selectedWorkItemSynthesisProducers(store, item, group, sourceRunIds) {
12
+ if (sourceRunIds.length === 0 || new Set(sourceRunIds).size !== sourceRunIds.length) {
13
+ throw new Error("Synthesis requires explicit, distinct source AgentRun references.");
14
+ }
15
+ return sourceRunIds.map((runId) => {
16
+ const run = store.getRun(item.taskId, runId);
17
+ const lane = group.lanes.find(({ id }) => id === run?.executionLaneId);
18
+ if (run === null
19
+ || lane === undefined
20
+ || !["completed", "failed"].includes(run.status)
21
+ || run.purpose !== "execution"
22
+ || run.workItemId !== item.id
23
+ || run.executionGroupId !== group.id
24
+ || run.executionLaneId !== lane.id
25
+ || run.roleName !== lane.roleName
26
+ || run.result === undefined) {
27
+ throw new Error(`Synthesis source is not an exact terminal Producer result: ${group.id}/${runId}.`);
28
+ }
29
+ return {
30
+ laneId: lane.id,
31
+ roleName: lane.roleName,
32
+ runId: run.id
33
+ };
34
+ });
35
+ }
36
+ /** Called within the caller's transaction after Task authority is checked. */
37
+ export function dispatchWorkItemSynthesis(store, taskId, workItemId, sourceRunIds, now) {
38
+ const task = store.getTask(taskId);
39
+ if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
40
+ throw new Error(`Task execution is not enabled: ${taskId}.`);
41
+ }
42
+ const item = store.getWorkItem(taskId, workItemId);
43
+ if (item === null || item.status !== "open") {
44
+ throw new Error(`WorkItem is not open: ${taskId}/${workItemId}.`);
45
+ }
46
+ const group = currentWorkItemExecutionGroup(item);
47
+ if (group === undefined)
48
+ throw new Error(`WorkItem has no ExecutionGroup: ${item.id}.`);
49
+ const producers = selectedWorkItemSynthesisProducers(store, item, group, sourceRunIds);
50
+ const existing = store.listRuns(taskId).some((run) => (run.purpose === "execution"
51
+ && run.workItemId === item.id
52
+ && run.sourceExecutionGroupId === group.id));
53
+ if (existing)
54
+ throw new Error(`Synthesis already exists for ${group.id}; retry its AgentRun explicitly.`);
55
+ if (item.assignee === undefined) {
56
+ throw new Error(`Replicated WorkItem has no main assignee: ${item.id}.`);
57
+ }
58
+ const role = store.getRole(taskId, item.assignee);
59
+ if (role === null)
60
+ throw new Error(`WorkItem main Role is missing: ${taskId}/${item.assignee}.`);
61
+ if (store.getActiveRun(taskId, role.name) !== null) {
62
+ throw new Error(`WorkItem main Role already has an active AgentRun: ${role.name}.`);
63
+ }
64
+ const workspace = role.name === "leader"
65
+ ? store.getTaskWorkspace(taskId)
66
+ : store.getWorkItemWorkspace(taskId, item.id);
67
+ if (workspace === null) {
68
+ throw new Error(`WorkItem main workspace is missing: ${taskId}/${item.id}.`);
69
+ }
70
+ const visibleProjectIds = workspace.entries.map(({ projectId }) => projectId).sort();
71
+ const taskProjectIds = task.projectBindings.map(({ projectId }) => projectId).sort();
72
+ const writableProjectIds = workspace.entries
73
+ .filter(({ access }) => access === "write")
74
+ .map(({ projectId }) => projectId)
75
+ .sort();
76
+ if (!isDeepStrictEqual(visibleProjectIds, taskProjectIds)
77
+ || !isDeepStrictEqual(writableProjectIds, [...item.writeProjectIds].sort())) {
78
+ throw new Error(`WorkItem main workspace does not match its approved scope: ${item.id}.`);
79
+ }
80
+ const effective = resolveEffectiveLaunch({
81
+ role,
82
+ purpose: "execution",
83
+ workspace,
84
+ workItemWriteProjectIds: item.writeProjectIds
85
+ });
86
+ const snapshot = freezeRunContextSnapshot(store, {
87
+ taskId,
88
+ roleName: role.name,
89
+ purpose: "execution",
90
+ workItemId: item.id,
91
+ sourceExecutionGroupId: group.id,
92
+ workspace
93
+ }, now, "leader", group.assignment.contextSnapshotRef, sourceRunIds);
94
+ const run = createRun(store.nextRunId(taskId), taskId, role.name, roleAgentSessionResumeMode(store.getTaskRoleSessionSet(taskId, role.name), effective.agentId, effective), createRunInput({
95
+ source: { type: "yui", channel: "workitem-dispatch" },
96
+ directive: synthesisDirective(group, producers),
97
+ contextSnapshotRef: contextSnapshotRef(snapshot),
98
+ deltaRefIds: contextSnapshotDeltaRefIds(store, snapshot)
99
+ }), now, {
100
+ workItemId: item.id,
101
+ sourceExecutionGroupId: group.id,
102
+ workspace,
103
+ effective
104
+ });
105
+ store.saveRun(run);
106
+ store.saveActiveRun(run);
107
+ enqueueRoleRunDispatch(store, {
108
+ taskId,
109
+ roleName: role.name,
110
+ runId: run.id,
111
+ reason: "workitem-synthesis-ready",
112
+ occurredAt: now
113
+ });
114
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "run.dispatched", {
115
+ runId: run.id,
116
+ role: run.roleName,
117
+ purpose: run.purpose,
118
+ mode: run.mode,
119
+ agent: `${run.effective.agentId}/${run.effective.adapterId}`,
120
+ component: run.effective.component,
121
+ effectiveRevision: String(run.effective.sourceDesiredRevision),
122
+ profileAccess: run.effective.profileAccess,
123
+ effectivePermission: run.effective.permission.strategy,
124
+ writeProjectIds: run.effective.writeProjectIds.join(",") || "none",
125
+ workItemId: item.id,
126
+ sourceExecutionGroupId: group.id
127
+ }, now));
128
+ return run;
129
+ }
130
+ function synthesisDirective(group, producers) {
131
+ return [
132
+ "Synthesize the explicitly selected Producer results in the supplied order.",
133
+ "Expand each exact source AgentRun from the frozen Context Snapshot and consume its original result text plus Core-authored system evidence.",
134
+ "Do not rerun, retry, append, or abandon any Lane. Form the final WorkItem result from these records.",
135
+ JSON.stringify({
136
+ schemaVersion: 1,
137
+ sourceExecutionGroupId: group.id,
138
+ producers
139
+ }, null, 2)
140
+ ].join("\n\n");
141
+ }
@@ -1,10 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { realpathSync, statSync } from "node:fs";
3
3
  import { isAbsolute, resolve } from "node:path";
4
- import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
4
+ import { isAgentAdapterId, supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
5
5
  import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgentBaseArguments } from "../agent/argumentPolicy.js";
6
6
  import { writeTextFileAtomically } from "../storage/durableFile.js";
7
- import { discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
7
+ import { discoverAcpConfiguration, discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
8
8
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
9
9
  const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
10
10
  const APPROVALS = ["untrusted", "on-request", "never"];
@@ -61,6 +61,17 @@ class CodexAdapter extends BaseAdapter {
61
61
  discoverConfiguration(input) {
62
62
  return discoverCodexConfiguration(input);
63
63
  }
64
+ compileNew(input) {
65
+ const launch = super.compileNew(input);
66
+ const developerInstructions = codexSessionInstructions(input);
67
+ return {
68
+ ...launch,
69
+ codexThread: {
70
+ ...codexThreadOptions(input, this.canonicalizeConfig(input.config)),
71
+ ...(developerInstructions === undefined ? {} : { developerInstructions })
72
+ }
73
+ };
74
+ }
64
75
  validateStructured(config) {
65
76
  exact(config, ["adapterId", "model", "effort", "permission", "search", "profile",
66
77
  "additionalDirectories", "advanced"], "Codex Agent config");
@@ -125,23 +136,13 @@ class CodexAdapter extends BaseAdapter {
125
136
  "--config",
126
137
  `projects={${JSON.stringify(resolve(input.workspace))}={trust_level="trusted"}}`
127
138
  ];
128
- const instructions = input.sessionManifestPath === undefined ? [
129
- input.developerInstructions,
130
- ...(input.skills === undefined || input.skills.length === 0
131
- ? []
132
- : [
133
- "Yui Role Skills are available at the paths below. Before performing work governed by one, read and follow its SKILL.md on demand; do not treat this list as a user message.",
134
- ...input.skills.map((skill) => `- ${skill.id}: ${skill.path}/SKILL.md`)
135
- ])
136
- ].filter((value) => value !== undefined && value.trim().length > 0) : [
137
- `Yui managed Session. Read and follow the Session Manifest at ${input.sessionManifestPath} (digest ${input.sessionManifestDigest ?? "unknown"}). Load each Skill and Role Profile by its manifest path before acting; Task content is available only through the manifest's exact Context API.`
138
- ];
139
- if (instructions.length === 0)
139
+ const instructions = codexSessionInstructions(input);
140
+ if (instructions === undefined)
140
141
  return workspaceTrust;
141
142
  return [
142
143
  ...workspaceTrust,
143
144
  "--config",
144
- `developer_instructions=${tomlString(instructions.join("\n"))}`
145
+ `developer_instructions=${tomlString(instructions)}`
145
146
  ];
146
147
  }
147
148
  compileResume(input) {
@@ -261,6 +262,7 @@ class ClaudeAdapter extends BaseAdapter {
261
262
  input.developerInstructions,
262
263
  ...(input.skills ?? []).map((skill) => [
263
264
  `# Yui Skill: ${skill.id}`,
265
+ `Source: ${skill.path}/SKILL.md (resolve relative links from this directory).`,
264
266
  skill.content
265
267
  ].join("\n\n"))
266
268
  ].filter((value) => value !== undefined && value.trim().length > 0);
@@ -300,6 +302,150 @@ class ClaudeAdapter extends BaseAdapter {
300
302
  };
301
303
  }
302
304
  }
305
+ /**
306
+ * The Agent Client Protocol adapter.
307
+ *
308
+ * One adapter serves every ACP Agent. It deliberately compiles no arguments of
309
+ * its own: entering ACP mode is a per-product invocation detail that belongs in
310
+ * the Agent descriptor's `baseArgs`, and everything after startup is negotiated
311
+ * over the protocol. Adding a second ACP product therefore requires a new
312
+ * descriptor and no new code here.
313
+ */
314
+ class AcpAdapter extends BaseAdapter {
315
+ id = "acp";
316
+ label = "Agent Client Protocol";
317
+ // The protocol version Yui implements. ACP Agents are versioned
318
+ // independently of their products, and `initialize` negotiates the real
319
+ // version at connect time, so no product's release number belongs here.
320
+ supportedVersion = "0.0.0";
321
+ capabilities = {
322
+ recover: true,
323
+ interrupt: true,
324
+ // ACP assigns the Session id in its `session/new` response.
325
+ nativeSessionDiscovery: "runtime",
326
+ preInputReadiness: driverPreInputReadiness("acp")
327
+ };
328
+ discoverConfiguration(input) {
329
+ return discoverAcpConfiguration(input);
330
+ }
331
+ validateStructured(config) {
332
+ exact(config, ["adapterId", "model", "effort", "permission", "additionalDirectories",
333
+ "settingsFile", "settingsSources", "advanced"], "ACP Agent config");
334
+ if (config.adapterId !== "acp")
335
+ throw new Error("ACP Agent config adapter is invalid.");
336
+ // Model and effort are session config options in ACP, so a value is legal
337
+ // here whatever this build knows about the product. Whether the Agent
338
+ // actually offers it is decided by the live option list at launch: the
339
+ // protocol enumerates the accepted values, and only that enumeration can
340
+ // answer it. Rejecting an unrecognised name here would require Yui to hold
341
+ // a model list per product, which is the fabricated authority this design
342
+ // avoids.
343
+ optionalText(config.model, "ACP model");
344
+ optionalText(config.effort, "ACP effort");
345
+ if (config.settingsFile !== undefined || config.settingsSources !== undefined) {
346
+ throw new Error("Yui's ACP client exposes no client-side settings configuration.");
347
+ }
348
+ // `additionalDirectories` is a real ACP session-lifecycle field, so a
349
+ // Project-backed workspace is configurable here. Whether it is actually
350
+ // sent is decided per connection: ACP requires Clients to send it only to
351
+ // an Agent that advertised `sessionCapabilities.additionalDirectories`, and
352
+ // only the live handshake knows that. Rejecting it here instead would make
353
+ // every multi-root Project launch fail before the Agent is even asked.
354
+ validatePaths(config.additionalDirectories, "ACP additional directory");
355
+ if (config.permission === undefined) {
356
+ throw new Error("ACP permission strategy is required.");
357
+ }
358
+ // `configured` carries the one mode id the user named; the other two
359
+ // strategies carry nothing, so an extra field would mean a caller expected a
360
+ // value this adapter never reads.
361
+ if (config.permission.strategy === "configured") {
362
+ exact(config.permission, ["strategy", "mode"], "ACP permission config");
363
+ // The mode is matched by exact id against what the Agent offers at launch.
364
+ // Yui keeps no per-product mode list, so any non-empty id is accepted here
365
+ // and verified there.
366
+ requireOneText(config.permission.mode, "ACP permission mode");
367
+ }
368
+ else {
369
+ exact(config.permission, ["strategy"], "ACP permission config");
370
+ if (config.permission.strategy !== "default" && config.permission.strategy !== "bypass") {
371
+ throw new Error("ACP permission strategy is invalid.");
372
+ }
373
+ }
374
+ advanced(this.id, config.advanced);
375
+ }
376
+ structuredArgs(_config) {
377
+ return [];
378
+ }
379
+ compileResume(input) {
380
+ // Reattaching is a `session/load` call inside the protocol, not a flag.
381
+ nativeId(input.nativeSessionId);
382
+ return this.compileNew(input);
383
+ }
384
+ compileManagedControl(input, _mode, _nativeSessionId) {
385
+ const config = this.canonicalizeConfig(input.config);
386
+ const bootstrap = acpSessionBootstrap(input);
387
+ const directories = config.additionalDirectories ?? [];
388
+ // What the Role asked for, in the form the Session applies. Only stated
389
+ // fields travel: an absent model is a request for the Agent's own default,
390
+ // and `default` permission deliberately sends no mode at all.
391
+ const desired = {
392
+ ...(config.model === undefined ? {} : { model: config.model }),
393
+ ...(config.effort === undefined ? {} : { effort: config.effort }),
394
+ ...(config.permission.strategy === "configured"
395
+ ? { permissionMode: config.permission.mode }
396
+ : config.permission.strategy === "bypass"
397
+ ? { permissionBypass: true }
398
+ : {})
399
+ };
400
+ const hasDesired = Object.keys(desired).length > 0;
401
+ return {
402
+ ...this.compileNew(input),
403
+ transport: "acp-stdio",
404
+ // ACP accepts no Yui flags, so everything a managed Session needs travels
405
+ // as protocol input. Omit the key entirely when there is nothing to say.
406
+ ...(directories.length === 0 && bootstrap === undefined && !hasDesired ? {} : {
407
+ acpSession: {
408
+ ...(directories.length === 0 ? {} : { additionalDirectories: [...directories] }),
409
+ ...(bootstrap === undefined ? {} : { sessionBootstrap: bootstrap }),
410
+ ...(hasDesired ? { desiredConfiguration: desired } : {})
411
+ }
412
+ })
413
+ };
414
+ }
415
+ }
416
+ /**
417
+ * The instructions a managed ACP Session must read before it acts.
418
+ *
419
+ * ACP defines no system prompt and no equivalent of `--append-system-prompt`,
420
+ * so unlike Codex and Claude this text cannot be delivered at launch. It is
421
+ * carried to the Session and prepended to the first prompt instead. The
422
+ * manifest form stays a pointer rather than an inlined Task: content is read
423
+ * through the manifest's own Context API, exactly as the other adapters do.
424
+ */
425
+ function acpSessionBootstrap(input) {
426
+ const sections = input.sessionManifestPath === undefined
427
+ ? [
428
+ input.developerInstructions,
429
+ ...(input.skills === undefined || input.skills.length === 0 ? [] : [
430
+ [
431
+ "Yui Role Skills are available at the paths below. Before performing work "
432
+ + "governed by one, read and follow its SKILL.md on demand; do not treat "
433
+ + "this list as a user message.",
434
+ ...input.skills.map((skill) => `- ${skill.id}: ${skill.path}/SKILL.md`)
435
+ ].join("\n")
436
+ ])
437
+ ]
438
+ : [
439
+ `Yui managed Session. Read and follow the Session Manifest at `
440
+ + `${input.sessionManifestPath} (digest ${input.sessionManifestDigest ?? "unknown"}). `
441
+ + "Load each Skill and Role Profile by its manifest path before acting; Task "
442
+ + "content is available only through the manifest's exact Context API."
443
+ ];
444
+ const bootstrap = sections
445
+ .filter((value) => value !== undefined && value.trim().length > 0)
446
+ .join("\n\n");
447
+ return bootstrap.length === 0 ? undefined : bootstrap;
448
+ }
303
449
  function driverPreInputReadiness(adapterId) {
304
450
  const capability = builtinAgentDriverRegistry().requireByAdapterId(adapterId)
305
451
  .capabilities.observation.preInputReadiness;
@@ -316,7 +462,7 @@ function driverPreInputReadiness(adapterId) {
316
462
  });
317
463
  }
318
464
  const ADAPTERS = {
319
- codex: new CodexAdapter(), claude: new ClaudeAdapter()
465
+ codex: new CodexAdapter(), claude: new ClaudeAdapter(), acp: new AcpAdapter()
320
466
  };
321
467
  function tomlString(value) {
322
468
  if (value.includes("\0"))
@@ -354,6 +500,19 @@ function codexThreadOptions(input, config) {
354
500
  ...(Object.keys(threadConfig).length === 0 ? {} : { config: threadConfig })
355
501
  };
356
502
  }
503
+ function codexSessionInstructions(input) {
504
+ if (input.sessionManifestPath !== undefined) {
505
+ return `Yui managed Session. Read and follow the Session Manifest at ${input.sessionManifestPath} (digest ${input.sessionManifestDigest ?? "unknown"}). Load each Skill and Role Profile by its manifest path before acting; Task content is available only through the manifest's exact Context API.`;
506
+ }
507
+ const instructions = [
508
+ input.developerInstructions,
509
+ ...(input.skills === undefined || input.skills.length === 0 ? [] : [
510
+ "Yui Role Skills are available at the paths below. Before performing work governed by one, read and follow its SKILL.md on demand; do not treat this list as a user message.",
511
+ ...input.skills.map((skill) => `- ${skill.id}: ${skill.path}/SKILL.md`)
512
+ ])
513
+ ].filter((value) => value !== undefined && value.trim().length > 0);
514
+ return instructions.length === 0 ? undefined : instructions.join("\n");
515
+ }
357
516
  function withoutCodexConfigOverride(argv, key) {
358
517
  const filtered = [];
359
518
  for (let index = 0; index < argv.length; index += 1) {
@@ -379,7 +538,10 @@ function sessionTitle(value) {
379
538
  }
380
539
  export { supportedAgentAdapterIds };
381
540
  export function findAgentAdapter(id) {
382
- return id === "codex" || id === "claude" ? ADAPTERS[id] : null;
541
+ // `ADAPTERS` is keyed by the catalog, so it already answers this question.
542
+ // A second name list here would strand a catalogued adapter that has a real
543
+ // implementation sitting one line above.
544
+ return isAgentAdapterId(id) ? ADAPTERS[id] : null;
383
545
  }
384
546
  export function resolveAgentAdapter(id) {
385
547
  const adapter = findAgentAdapter(id);
@@ -450,6 +612,25 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
450
612
  }, fields, at, warnings);
451
613
  }
452
614
  function baseline(id) {
615
+ // ACP negotiates its whole configurable surface per Session, so a static
616
+ // baseline can only say which axes exist — never which values one Agent
617
+ // accepts. Model, effort and mode are all `degraded` for exactly that reason:
618
+ // they are configurable and the enumeration comes from the live Session, so
619
+ // promising values here would be inventing them.
620
+ if (id === "acp")
621
+ return [
622
+ field("model", "enum", "degraded", true),
623
+ field("effort", "enum", "degraded", true),
624
+ field("permission.strategy", "enum", "available", false, ["default", "bypass", "configured"]),
625
+ // The exact mode ids belong to the Agent, and only a live Session lists
626
+ // them. `configured` carries whichever id the user names.
627
+ field("permission.mode", "enum", "degraded", true),
628
+ // Configurable, but only reaches an Agent that advertises
629
+ // `sessionCapabilities.additionalDirectories` at `initialize`. A static
630
+ // baseline cannot see that handshake, so it reports the field as degraded
631
+ // rather than promising delivery it cannot guarantee.
632
+ field("additionalDirectories", "path-list", "degraded", true)
633
+ ];
453
634
  if (id === "codex")
454
635
  return [
455
636
  field("model", "enum", "degraded", true), field("effort", "enum", "unavailable", true),
@@ -472,6 +653,10 @@ function baseline(id) {
472
653
  }
473
654
  function fromHelp(id, help) {
474
655
  const fields = baseline(id);
656
+ // Nothing in an ACP Agent's `--help` describes the protocol it speaks, so
657
+ // there is nothing here to refine: the baseline is already the whole truth.
658
+ if (id === "acp")
659
+ return fields;
475
660
  const replacements = id === "codex"
476
661
  ? [permissionStrategyField(help, "--dangerously-bypass-approvals-and-sandbox"),
477
662
  choiceField("permission.sandbox", help, "--sandbox", SANDBOXES),
@@ -549,6 +734,12 @@ function compareVersions(leftVersion, rightVersion) {
549
734
  return 0;
550
735
  }
551
736
  function missingRequiredCapabilities(id, help) {
737
+ // An ACP Agent declares its capabilities in the `initialize` handshake, not
738
+ // in `--help`. Demanding Claude's flags of it reported a working Agent as a
739
+ // broken one, so ACP is required to expose no flags at all here; the real
740
+ // check is `discoverAcpConfiguration`, which speaks the protocol.
741
+ if (id === "acp")
742
+ return [];
552
743
  const required = id === "codex"
553
744
  ? [
554
745
  [/(?:^|\s)--config(?:\s|[=<,]|$)/m, "--config"],
@@ -572,6 +763,12 @@ function missingRequiredCapabilities(id, help) {
572
763
  }
573
764
  function cloneConfig(config, paths) {
574
765
  const advancedConfig = config.advanced?.rawArgs === undefined ? config.advanced : { rawArgs: [...config.advanced.rawArgs] };
766
+ if (config.adapterId === "acp") {
767
+ return { ...config,
768
+ permission: { ...config.permission },
769
+ ...(paths === undefined ? {} : { additionalDirectories: [...paths] }),
770
+ ...(advancedConfig === undefined ? {} : { advanced: advancedConfig }) };
771
+ }
575
772
  if (config.adapterId === "codex")
576
773
  return { ...config,
577
774
  permission: { ...config.permission },
@@ -588,9 +785,15 @@ function cloneConfig(config, paths) {
588
785
  ...(advancedConfig === undefined ? {} : { advanced: advancedConfig }) };
589
786
  }
590
787
  export function defaultRoleAgentConfig(adapterId) {
591
- return adapterId === "codex"
592
- ? { adapterId: "codex", permission: { strategy: "bypass" } }
593
- : { adapterId: "claude", permission: { strategy: "bypass" } };
788
+ // Keyed by adapter, not a codex/else ternary: defaulting an unrecognized
789
+ // adapter to Claude's shape produced a config whose `adapterId` disagreed
790
+ // with the binding it was created for.
791
+ //
792
+ // ACP defaults to `default` rather than `bypass` because Yui answers ACP
793
+ // permission requests itself, and the answer it must never invent is "yes".
794
+ return adapterId === "acp"
795
+ ? { adapterId: "acp", permission: { strategy: "default" } }
796
+ : { adapterId, permission: { strategy: "bypass" } };
594
797
  }
595
798
  function validateSimplePermissionStrategy(value, label) {
596
799
  if (value !== "default" && value !== "bypass") {
@@ -638,6 +841,10 @@ function optionalText(value, label) {
638
841
  if (value !== undefined)
639
842
  text(value, label);
640
843
  }
844
+ /** A required non-empty single-line value. */
845
+ function requireOneText(value, label) {
846
+ text(value, label);
847
+ }
641
848
  function optionalTexts(values, label) {
642
849
  if (values === undefined)
643
850
  return;
@@ -104,6 +104,26 @@ export function fallbackAgentConfigurationCatalog(agent) {
104
104
  field("model", [], true),
105
105
  field("effort", [], true)
106
106
  ];
107
+ // The fallback exists for when the probe could not run, so it must still be
108
+ // this Agent's own shape. Falling through to Claude's fields handed the
109
+ // caller a catalog whose `adapterId` contradicted the Agent it described.
110
+ if (agent.adapterId === "acp") {
111
+ return {
112
+ schemaVersion: 1,
113
+ agentId: agent.id,
114
+ adapterId: "acp",
115
+ models: [],
116
+ fields: [
117
+ ...common,
118
+ field("permission.strategy", [choice("default"), choice("bypass"), choice("configured")], false),
119
+ // The mode ids come from a live Session, so an offline catalog can only
120
+ // say the axis exists. Listing candidates here would invent an Agent's
121
+ // vocabulary from a build-time guess.
122
+ field("permission.mode", [], true)
123
+ ],
124
+ warnings: ["Runtime configuration catalog is unavailable."]
125
+ };
126
+ }
107
127
  return agent.adapterId === "codex"
108
128
  ? {
109
129
  schemaVersion: 1,
@@ -183,9 +203,15 @@ function catalogFingerprint(input, environment) {
183
203
  sourceName: binding.sourceName,
184
204
  value: environment[binding.sourceName] ?? null
185
205
  }));
186
- const nativeRoot = input.agent.adapterId === "codex"
187
- ? environment.CODEX_HOME ?? join(environment.HOME ?? homedir(), ".codex")
188
- : environment.CLAUDE_CONFIG_DIR ?? join(environment.HOME ?? homedir(), ".claude");
206
+ // Which directory an ACP Agent keeps its own state in is that product's
207
+ // business, not the protocol's. Fingerprinting it against Claude's config
208
+ // root made unrelated Claude edits invalidate this cache; the executable
209
+ // path below is the honest identity for an Agent Yui only speaks to.
210
+ const nativeRoot = input.agent.adapterId === "acp"
211
+ ? null
212
+ : input.agent.adapterId === "codex"
213
+ ? environment.CODEX_HOME ?? join(environment.HOME ?? homedir(), ".codex")
214
+ : environment.CLAUDE_CONFIG_DIR ?? join(environment.HOME ?? homedir(), ".claude");
189
215
  const context = input.config?.adapterId === "codex"
190
216
  ? { profile: input.config.profile ?? null }
191
217
  : input.config?.adapterId === "claude"
@@ -195,6 +221,10 @@ function catalogFingerprint(input, environment) {
195
221
  }
196
222
  : null;
197
223
  return createHash("sha256").update(JSON.stringify({
224
+ // The component, not just the plan: two ACP products answer the same
225
+ // handshake differently, so a cache keyed on the plan alone would serve one
226
+ // product's capabilities for the other.
227
+ component: input.agent.component,
198
228
  adapterId: input.agent.adapterId,
199
229
  command: input.agent.command,
200
230
  baseArgs: input.agent.baseArgs,
@@ -239,7 +269,6 @@ function validateCatalog(value, agent) {
239
269
  || value.agentId !== agent.id
240
270
  || value.adapterId !== agent.adapterId
241
271
  || !Array.isArray(value.models)
242
- || value.models.length === 0
243
272
  || !Array.isArray(value.fields)
244
273
  || !Array.isArray(value.warnings)) {
245
274
  throw new Error("Agent configuration model catalog is incomplete.");
@@ -248,6 +277,9 @@ function validateCatalog(value, agent) {
248
277
  unique(models.map(({ value: model }) => model), "model");
249
278
  const fields = value.fields.map(validateField);
250
279
  unique(fields.map(({ key }) => key), "configuration field");
280
+ if (models.length === 0 && !modelAxisIsAccountedFor(fields)) {
281
+ throw new Error("Agent configuration model catalog is incomplete.");
282
+ }
251
283
  const warnings = value.warnings.map((warning) => text(warning, "catalog warning"));
252
284
  return {
253
285
  schemaVersion: 1,
@@ -255,11 +287,101 @@ function validateCatalog(value, agent) {
255
287
  adapterId: agent.adapterId,
256
288
  ...(typeof value.cliVersion === "string"
257
289
  ? { cliVersion: text(value.cliVersion, "CLI version") } : {}),
290
+ // Carried through rather than dropped. This is the only record of what the
291
+ // Agent on the other end actually agreed to, and for a plan whose products
292
+ // are interchangeable it is the one fact that tells them apart. Rebuilding
293
+ // the catalog without it silently erased the probe's answer on both the
294
+ // live path and the cache read-back, leaving every consumer unable to
295
+ // distinguish "negotiated nothing" from "never asked".
296
+ ...(value.handshake === undefined
297
+ ? {}
298
+ : { handshake: validateHandshake(value.handshake) }),
258
299
  models,
259
300
  fields,
260
301
  warnings
261
302
  };
262
303
  }
304
+ /**
305
+ * Whether an empty model list is an answer or a failure.
306
+ *
307
+ * Emptiness alone does not distinguish the two, so the `model` field's own
308
+ * contract decides. `available` is the field that carries a probe's explicit
309
+ * statement about an axis, and only a probe that states it has said anything
310
+ * about what an empty list means:
311
+ *
312
+ * - `available: false` — the axis does not exist for this Agent, and `reason`
313
+ * says why. Nothing to enumerate.
314
+ * - `available: true` — the axis exists and its values are deliberately
315
+ * enumerated later, with `allowCustom` letting a value be named before Yui has
316
+ * the list. This is ACP: models live in the `configOptions` a Session returns,
317
+ * so listing them at probe time would mean opening a real, possibly billed
318
+ * Session to populate a menu. The probe reports the axis, defers the values,
319
+ * and the configured model is verified against the Agent's own list at launch.
320
+ * - absent — the probe made no claim, which is the Codex and Claude shape. Those
321
+ * probes enumerate models into the top-level `models` array on success, so an
322
+ * empty array there means `model list` failed or returned nothing usable, and
323
+ * the catalog really is incomplete.
324
+ *
325
+ * So an unstated `available` with no models stays a rejection, which is what
326
+ * keeps a genuinely broken Codex or Claude discovery from passing as an answer.
327
+ *
328
+ * The earlier form of this check accepted only the first case. It therefore
329
+ * rejected a valid ACP catalog and replaced it with the fallback, discarding the
330
+ * live handshake and the probe's own reasons — the failure mode the
331
+ * `available`/`reason` pair exists to prevent.
332
+ */
333
+ function modelAxisIsAccountedFor(fields) {
334
+ const model = fields.find(({ key }) => key === "model");
335
+ if (model === undefined)
336
+ return false;
337
+ if (model.available === undefined)
338
+ return model.choices.length > 0;
339
+ // Stated either way, the field itself explains the empty list.
340
+ return true;
341
+ }
342
+ /**
343
+ * A handshake observation, kept in whichever of its three states it arrived in.
344
+ *
345
+ * Absence is meaningful and is preserved by the caller: it means no live
346
+ * connection produced this catalog. The two present states are distinguished
347
+ * here rather than merged, because `unsupported` ("this plan negotiates
348
+ * nothing") and an `observed` result with empty capabilities ("the Agent
349
+ * answered, and advertised none") are different answers that would otherwise
350
+ * be indistinguishable. An unrecognised status is rejected instead of being
351
+ * coerced into either one.
352
+ */
353
+ function validateHandshake(value) {
354
+ if (!record(value))
355
+ throw new Error("Agent handshake observation is invalid.");
356
+ if (value.status === "unsupported") {
357
+ return {
358
+ status: "unsupported",
359
+ reason: text(value.reason, "handshake reason")
360
+ };
361
+ }
362
+ if (value.status !== "observed") {
363
+ throw new Error(`Agent handshake status is unsupported: ${String(value.status)}.`);
364
+ }
365
+ if (!Number.isSafeInteger(value.protocolVersion)) {
366
+ throw new Error("Agent handshake protocol version is invalid.");
367
+ }
368
+ const capabilities = array(value.capabilities, "handshake capabilities")
369
+ .map((capability) => text(capability, "handshake capability"));
370
+ unique(capabilities, "handshake capability");
371
+ const authMethods = array(value.authMethods, "handshake authentication methods")
372
+ .map((method) => text(method, "handshake authentication method"));
373
+ unique(authMethods, "handshake authentication method");
374
+ return {
375
+ status: "observed",
376
+ protocolVersion: value.protocolVersion,
377
+ // `unknown` is a real answer here — the Agent connected but did not name
378
+ // itself — so it is stored as given and never replaced by a guess.
379
+ agentName: text(value.agentName, "handshake Agent name"),
380
+ agentVersion: text(value.agentVersion, "handshake Agent version"),
381
+ capabilities: [...capabilities].sort(),
382
+ authMethods: [...authMethods].sort()
383
+ };
384
+ }
263
385
  function validateModel(value) {
264
386
  if (!record(value) || typeof value.isDefault !== "boolean" || !Array.isArray(value.efforts)) {
265
387
  throw new Error("Agent configuration model entry is invalid.");