@zq-silk/yui 0.2.0 → 0.4.2

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
package/dist/cli.js CHANGED
@@ -1,41 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
3
  import { createInterface } from "node:readline/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import { isDeepStrictEqual } from "node:util";
4
6
  import { renderCommandHelp } from "./cli/helpRenderer.js";
5
7
  import { routeInvocation } from "./cli/invocationRouter.js";
6
8
  import { renderCompletion } from "./cli/completion.js";
7
9
  import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
8
10
  import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
9
11
  import { runCompletionWizard } from "./cli/completionWizard.js";
10
- import { resolveRoleWizardArguments } from "./cli/roleWizard.js";
12
+ import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
13
+ import { resolveGlobalRoleAgentConfigurationArguments, resolveRoleWizardArguments } from "./cli/roleWizard.js";
14
+ import { resolveOperatorWizardArguments } from "./cli/operatorWizard.js";
11
15
  import { runUpdateCommand } from "./cli/updateCommand.js";
16
+ import { runUpgradeCommand } from "./cli/upgradeCommand.js";
17
+ import { formatTimestamp } from "./output/timePresentation.js";
18
+ import { renderAgentConfigurationCatalog } from "./output/agentConfigurationPresentation.js";
19
+ import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
12
20
  import { runAgentCommand } from "./commands/agentCommands.js";
13
21
  import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
22
+ import { runConfigCommand } from "./commands/configCommands.js";
23
+ import { parseControllerCleanupOptions, parseControllerStatusOptions, renderControllerResourceStatus, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
14
24
  import { runJobCommand } from "./commands/jobCommands.js";
15
- import { runOperatorCommand } from "./commands/operatorCommands.js";
16
- import { runRepositoryCommand } from "./commands/repositoryCommands.js";
17
- import { runTaskCommand } from "./commands/taskCommands.js";
25
+ import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
26
+ import { runProjectCommand } from "./commands/projectCommands.js";
27
+ import { runProfileCommand } from "./commands/profileCommands.js";
28
+ import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
29
+ import { taskActor } from "./commands/taskActor.js";
30
+ import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
31
+ import { reconcileTaskRemoteBaselines } from "./commands/taskCompletionGate.js";
18
32
  import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
19
- import { callFileTaskController, ensureFileTaskController, FileTaskWorkflowRuntime, restartFileTaskController } from "./controller/clientRuntime.js";
33
+ import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
34
+ import { callController, ControllerClientError } from "./core/controllerClient.js";
20
35
  import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
36
+ import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
37
+ import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
21
38
  import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
22
- import { runDoctorCommand } from "./doctor/doctor.js";
23
- import { CliError, usageError } from "./errors/cliError.js";
39
+ import { runClaudeLifecycleHookCommand } from "./controller/claudeLifecycleHook.js";
40
+ import { runCodexLifecycleHookCommand } from "./controller/codexLifecycleHook.js";
41
+ import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
42
+ import { agentNotFound, CliError, usageError } from "./errors/cliError.js";
24
43
  import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
25
- import { FileTaskWorkspacePreparer } from "./repository/taskWorkspacePreparer.js";
26
- import { inspectStorageSchema, requireStorageSchema } from "./storage/storageSchema.js";
27
- import { FileTaskStore, resolveYuiHome } from "./storage/taskStore.js";
44
+ import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
45
+ import { FileTaskWorkspacePreparer, ReviewRoundWorkspaceEvidenceError } from "./repository/taskWorkspacePreparer.js";
46
+ import { inspectStorageSchema } from "./storage/storageSchema.js";
47
+ import { resolveYuiHome } from "./storage/taskStore.js";
48
+ import { openCompatibleFileTaskStore, validateCompatibleFileTaskStore } from "./storage/compatibleTaskStore.js";
49
+ import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
28
50
  import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
29
51
  import { NodeCommandExecutor } from "./tmux/commandExecutor.js";
30
52
  import { TmuxManager } from "./tmux/tmuxManager.js";
31
- const VERSION = readPackageVersion();
32
- const rawArgs = process.argv.slice(2);
53
+ import { WorkItemChangeSetManager } from "./workspace/workItemChangeSetManager.js";
54
+ import { parseWebCommandOptions, startYuiWebServer } from "./web/webServer.js";
55
+ import { AgentConfigurationCatalogService } from "./executor/agentConfigurationCatalog.js";
56
+ import { TmuxWebTerminalService } from "./web/tmuxWebTerminal.js";
57
+ import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
58
+ import { YUI_VERSION, yuiVersionIdentity } from "./version.js";
59
+ import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactControlPlanePreflight, assertExactTaskRuntimeEnvironment, assertExactTaskRuntimeState, exactControlPlaneDigest, extractExactControlArgument, parseExactControlPlaneDescriptor } from "./runtime/exactControlPlane.js";
60
+ import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
61
+ import { currentWorkItemExecutionGroup, workItemExecutionGroupById } from "./workItem/workItem.js";
62
+ const VERSION = YUI_VERSION;
63
+ const exactControlInvocation = extractExactControlArgument(process.argv.slice(2));
64
+ const taskFinalReviewInvocation = extractTaskFinalReviewRequest(exactControlInvocation.args);
65
+ const rawArgs = [...taskFinalReviewInvocation.args];
33
66
  const jsonOutput = rawArgs.includes("--json");
34
67
  const args = normalizeAliases(jsonOutput ? rawArgs.filter((argument) => argument !== "--json") : rawArgs);
35
68
  void main().catch((error) => {
36
69
  if (error instanceof CliError) {
37
70
  const rendered = jsonOutput
38
- ? JSON.stringify({ ok: false, code: error.code, message: error.message, details: {} })
71
+ ? JSON.stringify({ ok: false, code: error.code, message: error.message, details: error.details })
39
72
  : `${error.code}: ${error.message}${error.helpText === undefined ? "" : `\n\n${error.helpText.trimEnd()}`}`;
40
73
  process.stderr.write(`${rendered}\n`);
41
74
  process.exitCode = error.exitCode;
@@ -48,12 +81,13 @@ void main().catch((error) => {
48
81
  process.exitCode = 5;
49
82
  });
50
83
  export async function main() {
84
+ const taskFinalReviewContract = await preflightManagedTaskControlPlane();
51
85
  if (args.length === 0) {
52
86
  emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
53
87
  return;
54
88
  }
55
89
  if (args[0] === "version" && args.length === 1) {
56
- emit(VERSION, true);
90
+ emit(VERSION, true, yuiVersionIdentity());
57
91
  return;
58
92
  }
59
93
  const invocation = routeInvocation(args);
@@ -85,47 +119,173 @@ export async function main() {
85
119
  if (args[0] === "setup") {
86
120
  if (jsonOutput)
87
121
  throw usageError("Setup does not support --json.");
122
+ await assertFileTaskControllerStorageCompatible(home);
88
123
  const setupIo = {
89
124
  input: process.stdin,
90
125
  output: process.stdout,
91
126
  forceInteractive: process.env.YUI_SETUP_INTERACTIVE === "1"
92
127
  };
93
128
  validateSetupInvocation(args.slice(1), setupIo);
94
- emit(await runSetupCommand(args.slice(1), process.env, new NodeCommandExecutor(), setupIo));
129
+ const output = await runSetupCommand(args.slice(1), process.env, new NodeCommandExecutor(), setupIo);
130
+ const refresh = await refreshRunningFileTaskControllerEnvironment(home, openCompatibleFileTaskStore(home), process.env);
131
+ emit(withControllerRefreshWarning(output, refresh, "Agent environment"));
95
132
  return;
96
133
  }
97
134
  if (args[0] === "doctor") {
98
- emit(runDoctorCommand(args.slice(1), process.env, new NodeCommandExecutor()));
99
- return;
100
- }
101
- if (args[0] === "internal") {
102
- if (args[1] !== "session-notify" || args.length !== 3) {
103
- throw usageError("Internal session notify usage is invalid.");
135
+ const doctorArgs = args.slice(1);
136
+ if (doctorArgs.length !== 0) {
137
+ // Preserve the usage error for stray operands (parity with text mode).
138
+ runDoctorCommand(doctorArgs, process.env, new NodeCommandExecutor());
139
+ return;
140
+ }
141
+ const report = buildDoctorReport(process.env, new NodeCommandExecutor());
142
+ if (jsonOutput) {
143
+ // Machine-readable result: the full checks array + a storage-health verdict
144
+ // the update post-verify parses (P1-3). Exit non-zero when storage is not
145
+ // healthy so even a naive exit-code check fails closed. This exit-code
146
+ // signal is scoped to the --json path; text-mode doctor keeps its existing
147
+ // presentation and exit 0 (the WorkItem allows doctor's presentation to stay).
148
+ if (!report.storage.healthy)
149
+ process.exitCode = 5;
150
+ emit("", false, report);
151
+ return;
104
152
  }
105
- await runSessionNotifyCommand(args[2], process.env);
153
+ emit(renderDoctor(report.checks, report.review));
106
154
  return;
107
155
  }
108
- requireStorageSchema(home);
109
- const store = new FileTaskStore(home);
110
- const resolved = await resolveTerminalArguments(args, invocation.node, store);
111
- if (resolved === null) {
112
- emit("Cancelled.");
156
+ if (args[0] === "upgrade") {
157
+ // Mirror doctor/controller: needs a Home but self-manages the schema check,
158
+ // because upgrade must run against a non-current Home.
159
+ const result = await runUpgradeCommand(args.slice(1), home, process.env.YUI_UPDATE_EXTERNALLY_QUIESCED === "1"
160
+ ? { controllerLifecycle: "externally-quiesced" }
161
+ : {});
162
+ process.exitCode = result.exitCode;
163
+ emit(result.output, false, result.data);
113
164
  return;
114
165
  }
115
- if (resolved[0] === "controller") {
116
- const method = resolved[1];
117
- if ((method !== "status" && method !== "stop" && method !== "restart") || resolved.length !== 2) {
118
- throw usageError("Controller usage: yui controller status|stop|restart.");
166
+ if (args[0] === "internal") {
167
+ if (args[1] === "session-notify" && args.length === 3) {
168
+ await runSessionNotifyCommand(args[2], process.env);
169
+ return;
119
170
  }
171
+ if (args[1] === "claude-hook" && args.length === 2) {
172
+ await runClaudeLifecycleHookCommand(readFileSync(0, "utf8"), process.env);
173
+ return;
174
+ }
175
+ if (args[1] === "codex-hook" && args.length === 2) {
176
+ await runCodexLifecycleHookCommand(readFileSync(0, "utf8"), process.env);
177
+ return;
178
+ }
179
+ throw usageError("Internal lifecycle callback usage is invalid.");
180
+ }
181
+ if (args[0] === "controller") {
182
+ const method = args[1];
183
+ if (method === "identity" && args.length === 2) {
184
+ // Internal lifecycle seam used by update/upgrade. The Controller socket
185
+ // authenticates this exact launch identity; public `controller status`
186
+ // intentionally redacts argv in its resource inventory.
187
+ try {
188
+ const identity = await callController(home, "controller.identity", {});
189
+ emit("", false, identity);
190
+ }
191
+ catch (error) {
192
+ // Preserve the Controller protocol code for the synchronous update
193
+ // lifecycle owner. A generic RUNTIME_ERROR would erase the only
194
+ // definitive CONTROLLER_NOT_RUNNING proof and force an unnecessary
195
+ // unknown-active block.
196
+ if (!(error instanceof ControllerClientError))
197
+ throw error;
198
+ if (jsonOutput) {
199
+ process.stderr.write(`${JSON.stringify({
200
+ ok: false,
201
+ code: error.code,
202
+ message: error.message,
203
+ details: {}
204
+ })}\n`);
205
+ }
206
+ else {
207
+ process.stderr.write(`RUNTIME_ERROR: ${error.message}\n`);
208
+ }
209
+ process.exitCode = 5;
210
+ }
211
+ return;
212
+ }
213
+ if (method === "status") {
214
+ const options = parseControllerStatusOptions(args.slice(2));
215
+ const snapshot = await scanControllerResourceInventory({
216
+ currentHome: home,
217
+ scope: options.scope,
218
+ environment: process.env
219
+ });
220
+ emit(renderControllerResourceStatus(snapshot, options.verbose), false, snapshot);
221
+ return;
222
+ }
223
+ if (method === "cleanup") {
224
+ if (jsonOutput)
225
+ throw usageError("Controller cleanup does not support --json.");
226
+ const options = parseControllerCleanupOptions(args.slice(2));
227
+ const readline = createInterface({
228
+ input: process.stdin,
229
+ output: process.stdout
230
+ });
231
+ try {
232
+ const result = await runInteractiveControllerCleanup({
233
+ io: {
234
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
235
+ write: (value) => process.stdout.write(value),
236
+ question: async (prompt) => readline.question(prompt)
237
+ },
238
+ scan: () => scanControllerResourceInventory({
239
+ currentHome: home,
240
+ scope: options.scope,
241
+ environment: process.env
242
+ }),
243
+ clean: (resource) => cleanControllerResource(resource, {
244
+ environment: process.env
245
+ })
246
+ });
247
+ if (result.data.failed.length > 0 || result.data.skipped.length > 0) {
248
+ process.exitCode = 5;
249
+ }
250
+ emit(result.output, false, result.data);
251
+ }
252
+ finally {
253
+ readline.close();
254
+ }
255
+ return;
256
+ }
257
+ if ((method !== "stop" && method !== "restart") || args.length !== 2) {
258
+ throw usageError("Controller usage: yui controller status [--all] [--verbose] | "
259
+ + "cleanup [--all] | stop | restart.");
260
+ }
261
+ validateCompatibleFileTaskStore(home);
120
262
  const controllerMethod = method;
121
263
  const result = controllerMethod === "restart"
122
264
  ? await restartFileTaskController(home, { environment: process.env })
123
- : await callFileTaskController(home, `controller.${controllerMethod}`);
124
- emit(renderControllerResult(controllerMethod, result));
265
+ : await stopFileTaskController(home, { environment: process.env });
266
+ // The update lifecycle needs the authenticated replacement PID returned by
267
+ // restart/readiness. Keep stop's long-standing text envelope, while
268
+ // exposing restart's structured result alongside its human output.
269
+ emit(renderControllerResult(controllerMethod, result), false, controllerMethod === "restart" ? result : undefined);
125
270
  return;
126
271
  }
272
+ await assertFileTaskControllerStorageCompatible(home);
273
+ const store = openCompatibleFileTaskStore(home);
274
+ const catalogs = new AgentConfigurationCatalogService(home, {
275
+ environment: process.env
276
+ });
277
+ const resolved = await resolveTerminalArguments(args, invocation.node, store, catalogs);
278
+ if (resolved === null) {
279
+ emit("Cancelled.");
280
+ return;
281
+ }
282
+ await preflightAgentConfigurationMutation(resolved, store, catalogs);
127
283
  const executor = new NodeCommandExecutor();
128
- const tmux = new TmuxManager(process.env.YUI_TMUX_BIN ?? "tmux", executor, { yuiHome: home, terminalInput: process.stdin });
284
+ const tmux = new TmuxManager(process.env.YUI_TMUX_BIN ?? "tmux", executor, {
285
+ yuiHome: home,
286
+ terminalInput: process.stdin,
287
+ onWarning: (message) => process.stderr.write(`Warning: ${message}\n`)
288
+ });
129
289
  const schedulerStore = new FileSchedulerStoreAdapter(store);
130
290
  const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
131
291
  const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
@@ -136,12 +296,113 @@ export async function main() {
136
296
  process.stderr.write(`Controller runtime error: ${message}\n`);
137
297
  }
138
298
  });
299
+ const workspaceCoordinator = new TaskWorkspaceCoordinator(store, workspacePreparer, runtime);
300
+ if (resolved[0] === "web") {
301
+ if (jsonOutput)
302
+ throw usageError("Web does not support --json.");
303
+ const options = parseWebCommandOptions(resolved.slice(1));
304
+ const terminal = new TmuxWebTerminalService({
305
+ yuiHome: home,
306
+ tmuxBin: process.env.YUI_TMUX_BIN ?? "tmux",
307
+ tmux,
308
+ prepareTaskRole: (input) => runtime.prepareTaskRoleEnter(input),
309
+ prepareGlobalRole: (roleName) => runtime.prepareGlobalRoleEnter(roleName),
310
+ environment: process.env,
311
+ onError: (error) => {
312
+ const message = error instanceof Error ? error.message : String(error);
313
+ process.stderr.write(`Web terminal cleanup error: ${message}\n`);
314
+ }
315
+ });
316
+ await startYuiWebServer(store, options, {
317
+ terminal,
318
+ answerInput: async ({ taskId, inputId, answer }) => {
319
+ const command = [
320
+ "input", "answer", inputId, "--task", taskId,
321
+ ...("choiceKey" in answer
322
+ ? ["--choice", answer.choiceKey]
323
+ : ["--text", answer.text])
324
+ ];
325
+ const result = runTaskCommand(command, store, {
326
+ runtime,
327
+ environment: {},
328
+ yuiHome: home
329
+ });
330
+ if (result.kind !== "output") {
331
+ throw new Error("Input answer returned an invalid control result.");
332
+ }
333
+ const data = result.data;
334
+ if (data?.request === undefined) {
335
+ throw new Error("Input answer did not return the updated request.");
336
+ }
337
+ return data.request;
338
+ }
339
+ });
340
+ const displayHost = options.host === "::1" ? "[::1]" : options.host;
341
+ process.stdout.write(`Yui web control room: http://${displayHost}:${options.port}\n`);
342
+ return;
343
+ }
139
344
  if (resolved[0] === "agent") {
140
- emit(runAgentCommand(resolved.slice(1), store));
345
+ const agentArgs = resolved.slice(1);
346
+ if (agentArgs[0] === "capabilities") {
347
+ if (agentArgs.length !== 2) {
348
+ throw usageError("Agent capabilities usage: yui agent capabilities <agent-id>");
349
+ }
350
+ const agent = store.getConfiguredAgent(agentArgs[1] ?? "");
351
+ if (agent === null)
352
+ throw agentNotFound(agentArgs[1] ?? "");
353
+ const result = await catalogs.resolve({
354
+ agent,
355
+ cwd: store.getConfig().defaultWorkspace ?? process.cwd()
356
+ });
357
+ emit(renderAgentConfigurationCatalog(result), false, result);
358
+ return;
359
+ }
360
+ const affectedAgentId = agentArgs[1];
361
+ const previousAgent = typeof affectedAgentId === "string"
362
+ ? store.getConfiguredAgent(affectedAgentId)
363
+ : null;
364
+ const output = runAgentCommand(agentArgs, store);
365
+ if (agentArgs[0] === "add"
366
+ || agentArgs[0] === "update"
367
+ || agentArgs[0] === "remove") {
368
+ const currentAgent = typeof affectedAgentId === "string"
369
+ ? store.getConfiguredAgent(affectedAgentId)
370
+ : null;
371
+ const capabilityNotice = currentAgent !== null
372
+ && (agentArgs[0] === "add" || agentArgs[0] === "update")
373
+ ? renderAgentConfigurationResolutionNotice(await catalogs.resolve({
374
+ agent: currentAgent,
375
+ cwd: store.getConfig().defaultWorkspace ?? process.cwd()
376
+ }))
377
+ : "";
378
+ const scope = agentEnvironmentRefreshScope(previousAgent, currentAgent, store.listConfiguredAgents());
379
+ const refresh = await refreshRunningFileTaskControllerEnvironment(home, store, process.env, scope);
380
+ emit(withControllerRefreshWarning(`${output.trimEnd()}${capabilityNotice.length === 0 ? "\n" : `\n${capabilityNotice}`}`, refresh, "Agent environment"));
381
+ return;
382
+ }
383
+ emit(output);
141
384
  return;
142
385
  }
143
- if (resolved[0] === "repository") {
144
- emit(await runRepositoryCommand(resolved.slice(1), store));
386
+ if (resolved[0] === "config") {
387
+ const configArgs = resolved.slice(1);
388
+ const output = runConfigCommand(configArgs, store);
389
+ if (configArgs[0] === "set"
390
+ && configArgs[1] === "--reconciliation-interval-seconds") {
391
+ const refresh = await refreshRunningFileTaskControllerConfiguration(home, { environment: process.env });
392
+ emit(withControllerRefreshWarning(output, refresh, "Controller configuration"));
393
+ return;
394
+ }
395
+ emit(output);
396
+ return;
397
+ }
398
+ if (resolved[0] === "project") {
399
+ const result = await runProjectCommand(resolved.slice(1), store);
400
+ emit(result.output, false, result.data);
401
+ return;
402
+ }
403
+ if (resolved[0] === "profile") {
404
+ const result = runProfileCommand(resolved.slice(1), store);
405
+ emit(result.output, false, result.data);
145
406
  return;
146
407
  }
147
408
  if (resolved[0] === "role") {
@@ -155,7 +416,7 @@ export async function main() {
155
416
  return;
156
417
  }
157
418
  await ensureFileTaskController(home, { environment: process.env });
158
- runtime.prepareGlobalRoleEnter(result.role.name);
419
+ await runtime.prepareGlobalRoleEnter(result.role.name);
159
420
  tmux.attachRole("operator", result.role.name);
160
421
  return;
161
422
  }
@@ -164,36 +425,442 @@ export async function main() {
164
425
  if (resolved.length !== 2)
165
426
  throw usageError("Operator enter usage: yui operator enter.");
166
427
  await ensureFileTaskController(home, { environment: process.env });
167
- runtime.prepareGlobalRoleEnter("operator");
428
+ await runtime.prepareGlobalRoleEnter("operator");
168
429
  tmux.attachRole("operator", "operator");
169
430
  return;
170
431
  }
171
432
  const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
172
- if (result.kind !== "output")
173
- throw new Error("Operator submit returned an invalid control result.");
174
- emit(result.output);
433
+ if (result.kind === "output") {
434
+ emit(result.output, false, result.data);
435
+ return;
436
+ }
437
+ if (result.kind !== "session") {
438
+ throw new Error("Operator command returned an invalid control result.");
439
+ }
440
+ await executeOperatorSessionControl(result, home, store, runtime, tmux, catalogs);
175
441
  return;
176
442
  }
177
443
  if (resolved[0] === "task") {
444
+ if (resolved[1] === "integration") {
445
+ const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, { environment: process.env });
446
+ emit(result.output, false, result.data);
447
+ return;
448
+ }
178
449
  const enteringTask = (resolved[1] === "enter")
179
450
  || (resolved[1] === "role" && resolved[2] === "enter");
180
451
  if (enteringTask) {
181
452
  await ensureFileTaskController(home, { environment: process.env });
182
453
  const taskId = resolved[1] === "enter" ? resolved[2] : resolved[3];
183
454
  const task = taskId === undefined ? null : store.getTask(taskId);
184
- if (task?.status === "active" && task.repositoryId !== undefined) {
455
+ if (task?.status === "active") {
185
456
  await workspacePreparer.prepareTaskWorkspace(task.id);
186
457
  }
187
458
  }
188
- const result = runTaskCommand(resolved.slice(1), store, { runtime, environment: process.env });
189
- if (result.kind === "output") {
190
- emit(result.output, false, result.data);
459
+ if (resolved[1] === "work" && resolved[2] === "isolate") {
460
+ const workItemId = resolved[3];
461
+ if (workItemId === undefined || resolved.length !== 4) {
462
+ throw usageError("Task work isolate usage: yui task work isolate <task>/<work>.");
463
+ }
464
+ const reference = cliWorkItemReference(workItemId, process.env);
465
+ const workspace = await workspaceCoordinator.isolateWorkItem(reference.taskId, reference.localId);
466
+ emit(`Created WorkItem workspace for ${reference.taskId}/${reference.localId}\nWorkspace: ${workspace.root}\n`, false, { workItemRef: reference, workspace });
191
467
  return;
192
468
  }
193
- if (result.output !== undefined)
194
- emit(result.output);
195
- tmux.attachRole(result.taskId, result.roleName);
196
- return;
469
+ if (resolved[1] === "work" && resolved[2] === "review"
470
+ && resolved[3] === "cleanup") {
471
+ const reviewRoundId = resolved[4];
472
+ if (reviewRoundId === undefined || resolved.length !== 5) {
473
+ throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
474
+ }
475
+ const reference = cliTaskRecordReference(reviewRoundId, "reviewRound", process.env);
476
+ const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
477
+ if (removal === "dirty") {
478
+ throw usageError(`ReviewRound workspace is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
479
+ }
480
+ emit(`Cleaned ReviewRound workspace ${reference.taskId}/${reference.localId} (${removal})\n`, false, { reviewRoundRef: reference, workspace: { removal } });
481
+ return;
482
+ }
483
+ if (resolved[1] === "work" && resolved[2] === "capture") {
484
+ const workItemId = resolved[3];
485
+ if (workItemId === undefined || resolved.length !== 4) {
486
+ throw usageError("Task work capture usage: yui task work capture <task>/<work>.");
487
+ }
488
+ const reference = cliWorkItemReference(workItemId, process.env);
489
+ const changeSets = await new WorkItemChangeSetManager(store).capture(reference.taskId, reference.localId, taskFinalReviewContract === undefined
490
+ ? {}
491
+ : { taskFinalReviewContract });
492
+ const qualified = `${reference.taskId}/${reference.localId}`;
493
+ emit(changeSets.length === 0
494
+ ? `WorkItem workspace has no changes to capture: ${qualified}\n`
495
+ : `Captured ChangeSets ${changeSets.map(({ id }) => id).join(", ")} from ${qualified}\n`, false, { workItemRef: reference, changeSets });
496
+ return;
497
+ }
498
+ if (resolved[1] === "work" && resolved[2] === "cleanup") {
499
+ const workItemId = resolved[3];
500
+ const disposition = resolved[4];
501
+ if (workItemId === undefined
502
+ || !["--runtime-only", "--integrated", "--abandon"].includes(disposition ?? "")
503
+ || resolved.length !== 5) {
504
+ throw usageError("Task work cleanup usage: yui task work cleanup <task>/<work> "
505
+ + "(--runtime-only|--integrated|--abandon).");
506
+ }
507
+ const reference = cliWorkItemReference(workItemId, process.env);
508
+ const qualified = `${reference.taskId}/${reference.localId}`;
509
+ const actor = taskActor(process.env, reference.taskId);
510
+ if (actor === "operator") {
511
+ throw usageError("Only the Task Leader may clean a WorkItem from a managed Session.");
512
+ }
513
+ if (disposition === "--runtime-only") {
514
+ let runtimeCleanup;
515
+ try {
516
+ runtimeCleanup = await workspaceCoordinator.cleanupWorkItemRuntime(reference.taskId, reference.localId);
517
+ }
518
+ catch (error) {
519
+ throw cleanupCliError(error, `work-item:${qualified}`);
520
+ }
521
+ emit(`Released WorkItem runtime ${qualified}; retained its Session and worktree\n`, false, {
522
+ workItem: store.getWorkItem(reference.taskId, reference.localId),
523
+ runtime: { cleanup: runtimeCleanup },
524
+ worktree: { retained: true }
525
+ });
526
+ return;
527
+ }
528
+ const cleanedAs = disposition === "--integrated" ? "integrated" : "abandoned";
529
+ if (cleanedAs === "integrated") {
530
+ try {
531
+ await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
532
+ }
533
+ catch (error) {
534
+ throw usageError(error instanceof Error ? error.message : String(error));
535
+ }
536
+ }
537
+ let removal;
538
+ try {
539
+ removal = await workspaceCoordinator.cleanupWorkItem(reference.taskId, reference.localId, cleanedAs);
540
+ }
541
+ catch (error) {
542
+ throw cleanupCliError(error, `work-item:${qualified}`);
543
+ }
544
+ if (removal === "dirty") {
545
+ throw usageError(`WorkItem worktree is dirty and was retained: ${qualified}.`, undefined, cleanupBlockedDetails("dirty-worktree", `work-item:${qualified}`, true));
546
+ }
547
+ emit(`Cleaned WorkItem worktree ${qualified} (${cleanedAs})\n`, false, {
548
+ workItem: store.getWorkItem(reference.taskId, reference.localId),
549
+ worktree: { removal, disposition: cleanedAs }
550
+ });
551
+ return;
552
+ }
553
+ if (resolved[1] === "work" && resolved[2] === "review"
554
+ && resolved[3] === "cleanup") {
555
+ const reviewRef = resolved[4];
556
+ if (reviewRef === undefined || resolved.length !== 5) {
557
+ throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
558
+ }
559
+ const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
560
+ const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
561
+ if (removal === "dirty") {
562
+ throw usageError(`ReviewRound worktree is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
563
+ }
564
+ emit(`Cleaned ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
565
+ reviewRound: store.getReviewRound(reference.taskId, reference.localId),
566
+ worktree: { removal }
567
+ });
568
+ return;
569
+ }
570
+ if (resolved[1] === "work" && resolved[2] === "review"
571
+ && resolved[3] === "preserve") {
572
+ const reviewRef = resolved[4];
573
+ if (reviewRef === undefined || resolved.length !== 5) {
574
+ throw usageError("Task work review preserve usage: yui task work review preserve <task>/<review-round>.");
575
+ }
576
+ const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
577
+ const round = preserveReviewRoundWorkspace(reference.taskId, reference.localId, store, { runtime, environment: process.env, yuiHome: home });
578
+ emit(`Preserved ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
579
+ reviewRound: round
580
+ });
581
+ return;
582
+ }
583
+ if (resolved[1] === "archive") {
584
+ const { taskId, disposition } = validateTaskArchiveRequest(resolved.slice(2), store, { runtime, environment: process.env, yuiHome: home });
585
+ const task = store.getTask(taskId);
586
+ if (task === null)
587
+ throw new Error(`Task disappeared after archive validation: ${taskId}.`);
588
+ if (task.status !== "archived") {
589
+ const workItemIds = store.listManagedWorkspaces(task.id)
590
+ .flatMap(({ owner }) => owner.type === "work-item" ? [owner.workItemId] : []);
591
+ for (const workItemId of workItemIds) {
592
+ const item = store.getWorkItem(task.id, workItemId);
593
+ if (item?.status !== "completed" || disposition !== "integrated")
594
+ continue;
595
+ try {
596
+ await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
597
+ }
598
+ catch (error) {
599
+ throw usageError(error instanceof Error ? error.message : String(error));
600
+ }
601
+ }
602
+ const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
603
+ if (cleanup.status === "retained-dirty") {
604
+ throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true));
605
+ }
606
+ if (cleanup.status === "failed") {
607
+ throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true));
608
+ }
609
+ }
610
+ }
611
+ let taskRetirementProof;
612
+ if (resolved[1] === "retire") {
613
+ const taskId = resolved[2];
614
+ if (taskId !== undefined && !taskId.startsWith("--")) {
615
+ const task = store.getTask(taskId);
616
+ if (task?.status === "active" || task?.status === "draft") {
617
+ try {
618
+ taskRetirementProof = await new WorkItemChangeSetManager(store)
619
+ .assertRetirable(taskId);
620
+ }
621
+ catch (error) {
622
+ throw usageError(error instanceof Error ? error.message : String(error));
623
+ }
624
+ }
625
+ }
626
+ }
627
+ if (resolved[1] === "work" && resolved[2] === "dispatch") {
628
+ const workItemId = resolved[3];
629
+ const reference = workItemId === undefined
630
+ ? null
631
+ : cliWorkItemReference(workItemId, process.env);
632
+ const item = reference === null
633
+ ? null
634
+ : store.getWorkItem(reference.taskId, reference.localId);
635
+ const task = item === null ? null : store.getTask(item.taskId);
636
+ // A rejected Candidate starts a new execution iteration. Release every
637
+ // terminal Lane Role runtime before preparing the new Lane workspaces;
638
+ // durable Runs, Groups, Candidates, and workspace owners remain intact.
639
+ if (item?.status === "failed"
640
+ && currentWorkItemExecutionGroup(item)?.resolution !== undefined) {
641
+ await workspaceCoordinator.cleanupWorkItemRuntime(item.taskId, item.id);
642
+ }
643
+ // Every Task needs an authoritative runtime owner before dispatch. A
644
+ // Gitless Task uses an empty Task-owned view; Project-backed WorkItems
645
+ // additionally receive their isolated Develop owner below.
646
+ if (item !== null && task !== null) {
647
+ await workspacePreparer.prepareTaskWorkspace(task.id);
648
+ }
649
+ // Every Project-backed WorkItem needs its own Develop owner before a
650
+ // Lane can be prepared. The physical preparer creates the symlink view
651
+ // and stores the exact WorkItem owner before dispatch creates the Run.
652
+ if (item !== null
653
+ && task !== null
654
+ && task.projectBindings.length > 0
655
+ && store.getWorkItemWorkspace(task.id, item.id) === null) {
656
+ await workspaceCoordinator.isolateWorkItem(item.taskId, item.id);
657
+ }
658
+ if (item !== null && task !== null) {
659
+ // For a new Group the preparer has already created deterministic
660
+ // worktrees, but the owner record is adopted by dispatch's aggregate
661
+ // transaction once its exact Lane ids exist.
662
+ }
663
+ }
664
+ let executionLaneWorkspaces = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
665
+ let workItemIntegrationProof;
666
+ if (resolved[1] === "work" && resolved[2] === "accept") {
667
+ const workItemId = resolved[3];
668
+ if (workItemId !== undefined && !workItemId.startsWith("--")) {
669
+ try {
670
+ const reference = cliWorkItemReference(workItemId, process.env);
671
+ workItemIntegrationProof = await new WorkItemChangeSetManager(store)
672
+ .assertIntegrated(reference.taskId, reference.localId) ?? undefined;
673
+ }
674
+ catch (error) {
675
+ throw usageError(error instanceof Error ? error.message : String(error));
676
+ }
677
+ }
678
+ }
679
+ let completionSummary;
680
+ if (resolved[1] === "complete" && resolved[2] !== undefined) {
681
+ const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
682
+ completionSummary = completionRequest.summary;
683
+ const completion = preflightTaskCompletion(resolved[2], store, {
684
+ environment: process.env,
685
+ ...(taskFinalReviewContract === undefined
686
+ ? {}
687
+ : { taskFinalReviewContract })
688
+ });
689
+ if (!completion.completed && !completion.activeTaskReview) {
690
+ await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env });
691
+ }
692
+ }
693
+ let candidateMaterialization;
694
+ let candidateMaterializationCommitted = false;
695
+ try {
696
+ candidateMaterialization = await candidateMaterializationForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
697
+ const candidateGitSnapshot = candidateMaterialization === undefined
698
+ ? await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract)
699
+ : candidateMaterialization.snapshot;
700
+ const directTaskMainSnapshot = await directTaskMainSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
701
+ const actualTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
702
+ const reviewWorkspaceResult = await reviewWorkspaceResultForTaskCommand(resolved, store, workspacePreparer, process.env);
703
+ const executionLaneGitSnapshot = await executionLaneGitSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env);
704
+ const laneSnapshotPreflight = executionLaneGitSnapshot === undefined
705
+ ? undefined
706
+ : executionLaneGitSnapshot;
707
+ const result = runTaskCommand(resolved.slice(1), store, {
708
+ runtime,
709
+ environment: process.env,
710
+ yuiHome: home,
711
+ ...(taskFinalReviewContract === undefined
712
+ ? {}
713
+ : { taskFinalReviewContract }),
714
+ ...(completionSummary === undefined ? {} : { completionSummary }),
715
+ ...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
716
+ ...(candidateGitSnapshot === undefined ? {} : { candidateGitSnapshot }),
717
+ ...(candidateMaterialization === undefined
718
+ ? {}
719
+ : { candidateWorkspace: candidateMaterialization.workspace ?? null }),
720
+ ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
721
+ ...(directTaskMainSnapshot === undefined ? {} : { directTaskMainSnapshot }),
722
+ ...(actualTaskReviewCandidate === undefined
723
+ ? {}
724
+ : { actualTaskReviewCandidate }),
725
+ ...(reviewWorkspaceResult === undefined ? {} : { reviewWorkspaceResult }),
726
+ ...(laneSnapshotPreflight === undefined ? {} : { executionLaneGitSnapshot: laneSnapshotPreflight }),
727
+ ...(taskRetirementProof === undefined ? {} : { taskRetirementProof })
728
+ });
729
+ // The command transaction has now durably submitted the Candidate. Any
730
+ // later output/review handling must not roll back its Git snapshot.
731
+ candidateMaterializationCommitted = candidateMaterialization !== undefined;
732
+ if (result.kind === "output") {
733
+ const requestedRound = reviewRoundFromCommandData(result.data);
734
+ const persistedRequestedRound = requestedRound === undefined
735
+ ? null
736
+ : store.getReviewRound(requestedRound.taskId, requestedRound.id);
737
+ let reviewOutput = "";
738
+ let reviewData;
739
+ const reviewDispatchNeeded = requestedRound?.status === "pending"
740
+ || (requestedRound?.status === "running"
741
+ && resolved[1] === "review"
742
+ && resolved[2] === "request"
743
+ && persistedRequestedRound?.executionGroup?.lanes.some((lane) => (lane.status === "pending" && lane.runId === undefined)) === true);
744
+ if (reviewDispatchNeeded) {
745
+ try {
746
+ const workspace = requestedRound.status === "running"
747
+ ? store.getReviewRoundWorkspace(requestedRound.taskId, requestedRound.id)
748
+ : await workspacePreparer.prepareReviewRoundWorkspace(requestedRound.taskId, requestedRound.id);
749
+ if (workspace === null) {
750
+ throw new Error(`ReviewRound workspace is not ready: ${requestedRound.id}.`);
751
+ }
752
+ const reviewLaneWorkspaces = await prepareReviewLaneWorkspaces(requestedRound.taskId, requestedRound.id, store, workspacePreparer);
753
+ if (reviewLaneWorkspaces !== undefined) {
754
+ executionLaneWorkspaces = reviewLaneWorkspaces;
755
+ }
756
+ const storedRound = store.getReviewRound(requestedRound.taskId, requestedRound.id);
757
+ const freshTaskCandidate = (storedRound?.scope ?? "work-item") === "task"
758
+ ? await snapshotActualTaskReviewCandidate(requestedRound.taskId, store, workspacePreparer)
759
+ : undefined;
760
+ const run = dispatchPreparedReviewRound(requestedRound.taskId, requestedRound.id, store, {
761
+ runtime,
762
+ environment: process.env,
763
+ yuiHome: home,
764
+ ...(taskFinalReviewContract === undefined
765
+ ? {}
766
+ : { taskFinalReviewContract }),
767
+ ...(freshTaskCandidate === undefined
768
+ ? {}
769
+ : { actualTaskReviewCandidate: freshTaskCandidate }),
770
+ ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces })
771
+ });
772
+ reviewOutput = `Review queued as ${requestedRound.id} (${run.id})\n`;
773
+ reviewData = {
774
+ reviewRound: store.getReviewRound(requestedRound.taskId, requestedRound.id),
775
+ reviewRun: run,
776
+ workspace
777
+ };
778
+ }
779
+ catch (error) {
780
+ const currentRound = store.getReviewRound(requestedRound.taskId, requestedRound.id);
781
+ if (requestedRound.resumedPendingFinalReview
782
+ && !requestedRound.terminalizedLeaderRun
783
+ && (error instanceof ReviewRoundWorkspaceEvidenceError
784
+ || error instanceof TaskFinalReviewDispatchDriftError)
785
+ && (currentRound?.scope ?? "work-item") === "task") {
786
+ throw error;
787
+ }
788
+ const message = error instanceof Error ? error.message : String(error);
789
+ await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
790
+ const failed = failPendingReviewRound(requestedRound.taskId, requestedRound.id, message, store, { runtime, environment: process.env, yuiHome: home });
791
+ reviewOutput = `Review could not start: ${message}\n`;
792
+ reviewData = { reviewRound: failed };
793
+ }
794
+ }
795
+ if (resolved[1] === "create") {
796
+ const created = result.data;
797
+ if (created?.task?.id !== undefined) {
798
+ let workspace;
799
+ try {
800
+ workspace = await workspacePreparer.prepareTaskWorkspace(created.task.id);
801
+ }
802
+ catch (error) {
803
+ const message = error instanceof Error ? error.message : String(error);
804
+ workspace = {
805
+ taskId: created.task.id,
806
+ status: "failed",
807
+ error: message
808
+ };
809
+ }
810
+ const latest = store.getTask(created.task.id);
811
+ const leader = store.getRole(created.task.id, "leader");
812
+ if (latest !== null && leader !== null) {
813
+ const warning = workspace.status === "failed"
814
+ ? `Main worktree is not ready: ${workspace.error ?? "unknown error"}.\n`
815
+ + `After correcting the Git problem, run yui task reconcile ${created.task.id}.\n`
816
+ : "";
817
+ emit(`${result.output}${warning}`, false, {
818
+ ...created,
819
+ task: latest,
820
+ leader,
821
+ workspace
822
+ });
823
+ return;
824
+ }
825
+ }
826
+ }
827
+ if (resolved[1] === "activate") {
828
+ const taskId = resolved[2];
829
+ const task = taskId === undefined ? null : store.getTask(taskId);
830
+ if (task?.status === "active") {
831
+ await workspacePreparer.prepareTaskWorkspace(task.id);
832
+ }
833
+ }
834
+ if (resolved[1] === "project" && resolved[2] === "add") {
835
+ const taskId = resolved[3];
836
+ const task = taskId === undefined ? null : store.getTask(taskId);
837
+ if (task?.status === "active") {
838
+ await workspacePreparer.prepareTaskWorkspace(task.id);
839
+ }
840
+ }
841
+ emit(`${result.output}${reviewOutput}`, false, reviewData === undefined
842
+ ? result.data
843
+ : { command: result.data, ...reviewData });
844
+ return;
845
+ }
846
+ await runtime.prepareTaskRoleEnter({
847
+ taskId: result.taskId,
848
+ roleName: result.roleName
849
+ });
850
+ if (result.output !== undefined)
851
+ emit(result.output);
852
+ tmux.attachRole(result.taskId, result.roleName);
853
+ return;
854
+ }
855
+ catch (error) {
856
+ if (candidateMaterialization !== undefined && !candidateMaterializationCommitted) {
857
+ await workspacePreparer.restoreExecutionGroupCandidateMaterialization(candidateMaterialization);
858
+ }
859
+ if (!candidateMaterializationCommitted) {
860
+ await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
861
+ }
862
+ throw error;
863
+ }
197
864
  }
198
865
  if (resolved[0] === "jobs") {
199
866
  emit(runJobCommand(resolved.slice(1), store, { runtime }));
@@ -201,18 +868,586 @@ export async function main() {
201
868
  }
202
869
  throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
203
870
  }
871
+ async function preflightManagedTaskControlPlane() {
872
+ if (exactControlInvocation.error !== undefined) {
873
+ throw new Error(exactControlInvocation.error);
874
+ }
875
+ if (taskFinalReviewInvocation.error !== undefined) {
876
+ throw new Error(taskFinalReviewInvocation.error);
877
+ }
878
+ const serializedControl = process.env[YUI_CONTROL_PLANE_DESCRIPTOR];
879
+ const serializedRuntime = process.env[YUI_TASK_RUNTIME_DESCRIPTOR];
880
+ const exactRuntime = serializedControl !== undefined || serializedRuntime !== undefined;
881
+ if (process.env.YUI_SESSION_SCOPE === "task" && !exactRuntime) {
882
+ throw new Error("Exact control-plane invocation requires both frozen descriptors in a managed Task runtime.");
883
+ }
884
+ if (!exactRuntime) {
885
+ if (exactControlInvocation.digest !== undefined) {
886
+ throw new Error("Exact Task control-plane invocation requires its frozen runtime descriptors.");
887
+ }
888
+ if (taskFinalReviewInvocation.request !== undefined) {
889
+ throw new Error("Task final-review contract requires a verified exact Task control-plane invocation.");
890
+ }
891
+ return undefined;
892
+ }
893
+ if (process.env.YUI_SESSION_SCOPE !== "task") {
894
+ throw new Error("Exact Task control-plane invocation requires a managed Task runtime.");
895
+ }
896
+ if (serializedControl === undefined || serializedRuntime === undefined) {
897
+ throw new Error("Exact control-plane invocation is required for this managed Task runtime.");
898
+ }
899
+ const control = parseExactControlPlaneDescriptor(serializedControl);
900
+ const internalCallback = args[0] === "internal";
901
+ if (!internalCallback && exactControlInvocation.digest === undefined) {
902
+ throw new Error("Exact control-plane invocation is required; bare `yui` and PATH launchers are not valid in a managed Task runtime.");
903
+ }
904
+ const digest = exactControlInvocation.digest ?? exactControlPlaneDigest(control);
905
+ await assertExactControlPlanePreflight({
906
+ serializedDescriptor: serializedControl,
907
+ digest,
908
+ actualExecutable: process.execPath,
909
+ actualCliEntry: fileURLToPath(import.meta.url),
910
+ actualHome: resolveYuiHome(process.env)
911
+ }, {
912
+ // Provider callbacks must remain able to append their immutable inbox fact
913
+ // while the Controller is offline. They still validate executable, CLI,
914
+ // Home, build, schema, and the exact Task runtime envelope first.
915
+ checkController: !internalCallback
916
+ });
917
+ const runtime = assertExactTaskRuntimeEnvironment(serializedRuntime, process.env, digest, control.yuiHome);
918
+ const preallocatedClaudeCallback = args.length === 2
919
+ && args[0] === "internal"
920
+ && args[1] === "claude-hook";
921
+ assertExactTaskRuntimeState(runtime, openCompatibleFileTaskStore(control.yuiHome), preallocatedClaudeCallback
922
+ ? { preallocatedNativeSessionReservation: { yuiHome: control.yuiHome } }
923
+ : {});
924
+ const request = taskFinalReviewInvocation.request;
925
+ if (request === undefined)
926
+ return undefined;
927
+ if (runtime.roleName !== "leader") {
928
+ throw new Error("Only the exact Task Leader invocation may establish a final-review contract.");
929
+ }
930
+ if (request.taskId !== runtime.taskId) {
931
+ throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
932
+ }
933
+ return createTaskFinalReviewContract({
934
+ taskId: runtime.taskId,
935
+ reviewerRoleName: request.reviewerRoleName,
936
+ controlPlaneDigest: digest
937
+ });
938
+ }
939
+ function cleanupCliError(error, fallbackResource) {
940
+ if (error instanceof WorkspaceCleanupBlockedError) {
941
+ return usageError(error.message, undefined, cleanupBlockedDetails(error.reason, error.resource, error.retryable));
942
+ }
943
+ return new CliError("RUNTIME_ERROR", error instanceof Error ? error.message : String(error), undefined, cleanupBlockedDetails("cleanup-failed", fallbackResource, true));
944
+ }
945
+ function cleanupBlockedDetails(reason, resource, retryable) {
946
+ return {
947
+ status: "blocked",
948
+ blockedBy: [{ resource, reason, retryable }],
949
+ remainingResources: [resource],
950
+ retryable
951
+ };
952
+ }
953
+ function cliWorkItemReference(value, environment) {
954
+ try {
955
+ return resolveTaskRecordReference(value, {
956
+ kind: "workItem",
957
+ label: "Work Item reference",
958
+ ...(environment.YUI_TASK_ID === undefined
959
+ ? {}
960
+ : { contextTaskId: environment.YUI_TASK_ID })
961
+ });
962
+ }
963
+ catch (error) {
964
+ throw usageError(error instanceof Error ? error.message : String(error));
965
+ }
966
+ }
967
+ function cliTaskRecordReference(value, kind, environment) {
968
+ try {
969
+ return resolveTaskRecordReference(value, {
970
+ kind,
971
+ label: kind === "agentRun" ? "Agent Run reference" : "ReviewRound reference",
972
+ ...(environment.YUI_TASK_ID === undefined
973
+ ? {}
974
+ : { contextTaskId: environment.YUI_TASK_ID })
975
+ });
976
+ }
977
+ catch (error) {
978
+ throw usageError(error instanceof Error ? error.message : String(error));
979
+ }
980
+ }
981
+ async function candidateSnapshotForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
982
+ if (args[0] !== "task")
983
+ return undefined;
984
+ const reviewableCandidateCommand = (args[1] === "run" && args[2] === "yield" && args[3] !== undefined) || (args[1] === "work" && args[2] === "update"
985
+ && args[3] !== undefined && args[4] === "done") || (args[1] === "work" && args[2] === "group" && args[3] === "resolve"
986
+ && args[4] !== undefined);
987
+ // Explicit Task-final review requests must remain independent of the
988
+ // mutable global review trigger. Candidate snapshots are a delivery
989
+ // boundary for every writable WorkItem, not only review-configured Tasks.
990
+ const groupResolve = args[1] === "work" && args[2] === "group" && args[3] === "resolve";
991
+ if (!reviewableCandidateCommand
992
+ || (groupResolve && args.includes("--decision") && args[args.indexOf("--decision") + 1] !== "accept")) {
993
+ return undefined;
994
+ }
995
+ if (args[1] === "run" && args[2] === "yield" && args[3] !== undefined) {
996
+ const reference = cliTaskRecordReference(args[3], "agentRun", environment);
997
+ const run = store.getAgentRun(reference.taskId, reference.localId);
998
+ if (run === null || run.purpose !== "execution" || run.workItemId === undefined) {
999
+ return undefined;
1000
+ }
1001
+ if (run.workspace === undefined) {
1002
+ // Gitless execution has no workspace or Git snapshot to capture. The
1003
+ // command layer still records the yielded Lane/Candidate evidence.
1004
+ return undefined;
1005
+ }
1006
+ // Group-backed Runs yield Lane evidence first; the Leader's later group
1007
+ // resolution performs the one Candidate snapshot after selected Lane
1008
+ // outputs have been materialized into the WorkItem workspace.
1009
+ if (run.executionGroupId !== undefined || run.workspace.owner.type === "execution-lane") {
1010
+ const item = store.getWorkItem(run.taskId, run.workItemId);
1011
+ const group = item === null || run.executionGroupId === undefined
1012
+ ? undefined
1013
+ : workItemExecutionGroupById(item, run.executionGroupId);
1014
+ const fixedSingleLane = group?.strategy.mode === "fixed"
1015
+ && group.strategy.count === 1
1016
+ && group.lanes.length === 1
1017
+ && run.workspace.owner.type === "work-item";
1018
+ if (fixedSingleLane)
1019
+ return preparer.snapshotCandidateWorkspace(run.workspace);
1020
+ return undefined;
1021
+ }
1022
+ return preparer.snapshotCandidateWorkspace(run.workspace);
1023
+ }
1024
+ if (args[1] === "work" && args[2] === "update"
1025
+ && args[3] !== undefined && args[4] === "done") {
1026
+ const reference = cliWorkItemReference(args[3], environment);
1027
+ const workspace = store.getWorkItemWorkspace(reference.taskId, reference.localId);
1028
+ if (workspace === null) {
1029
+ // The exact Task-final contract intentionally supports a Leader-direct,
1030
+ // metadata-only Project Candidate. The command layer performs the full
1031
+ // Task/WorkItem/source/contract validation before any aggregate write.
1032
+ if (taskFinalReviewContract !== undefined)
1033
+ return undefined;
1034
+ throw usageError(`Reviewable direct WorkItem has no managed Candidate workspace: ${reference.localId}.`);
1035
+ }
1036
+ return preparer.snapshotCandidateWorkspace(workspace);
1037
+ }
1038
+ if (args[1] === "work" && args[2] === "group" && args[3] === "resolve"
1039
+ && args[4] !== undefined) {
1040
+ // The grouped accept path snapshots only after all selected Lane outputs
1041
+ // have been merged by candidateMaterializationForTaskCommand.
1042
+ return undefined;
1043
+ }
1044
+ return undefined;
1045
+ }
1046
+ async function candidateMaterializationForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
1047
+ if (args[0] !== "task" || args[1] !== "work" || args[2] !== "group"
1048
+ || args[3] !== "resolve" || args[4] === undefined
1049
+ || !args.includes("--decision")
1050
+ || args[args.indexOf("--decision") + 1] !== "accept")
1051
+ return undefined;
1052
+ const reference = cliWorkItemReference(args[4], environment);
1053
+ const item = store.getWorkItem(reference.taskId, reference.localId);
1054
+ const group = item === null || item === undefined
1055
+ ? undefined
1056
+ : currentWorkItemExecutionGroup(item);
1057
+ if (item === null || item === undefined || group === undefined)
1058
+ return undefined;
1059
+ const selected = args.flatMap((value, index) => value === "--lane" && args[index + 1] !== undefined ? [args[index + 1]] : []);
1060
+ try {
1061
+ return await preparer.materializeExecutionGroupCandidate(item.taskId, item.id, group.id, selected);
1062
+ }
1063
+ catch (error) {
1064
+ throw usageError(error instanceof Error ? error.message : String(error));
1065
+ }
1066
+ }
1067
+ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, environment) {
1068
+ const isDispatch = args[0] === "task" && args[1] === "work" && args[2] === "dispatch" && args[3] !== undefined;
1069
+ const isRetry = args[0] === "task" && args[1] === "run" && args[2] === "retry" && args[3] !== undefined;
1070
+ if (!isDispatch && !isRetry)
1071
+ return undefined;
1072
+ const itemRef = isDispatch
1073
+ ? cliWorkItemReference(args[3], environment)
1074
+ : null;
1075
+ const item = itemRef === null
1076
+ ? (() => {
1077
+ const runRef = cliTaskRecordReference(args[3], "agentRun", environment);
1078
+ const run = store.getAgentRun(runRef.taskId, runRef.localId);
1079
+ return run?.workItemId === undefined ? null : store.getWorkItem(run.taskId, run.workItemId);
1080
+ })()
1081
+ : store.getWorkItem(itemRef.taskId, itemRef.localId);
1082
+ if (item === null)
1083
+ return undefined;
1084
+ const retryRun = isRetry
1085
+ ? store.getAgentRun(item.taskId, cliTaskRecordReference(args[3], "agentRun", environment).localId)
1086
+ : null;
1087
+ const currentGroup = currentWorkItemExecutionGroup(item);
1088
+ const group = retryRun?.executionGroupId === undefined
1089
+ ? (isDispatch && currentGroup?.resolution !== undefined ? undefined : currentGroup)
1090
+ : workItemExecutionGroupById(item, retryRun.executionGroupId);
1091
+ const roles = args.flatMap((value, index) => (value === "--lane-role" && args[index + 1] !== undefined
1092
+ ? [args[index + 1]]
1093
+ : []));
1094
+ const requestedStrategy = (() => {
1095
+ const index = args.indexOf("--strategy");
1096
+ const value = index < 0 ? undefined : args[index + 1];
1097
+ if (value === undefined)
1098
+ return undefined;
1099
+ const fixed = /^fixed:([1-9]\d*)$/u.exec(value);
1100
+ if (fixed !== null)
1101
+ return { mode: "fixed", count: Number(fixed[1]) };
1102
+ const adaptive = /^adaptive:([1-9]\d*)$/u.exec(value);
1103
+ if (adaptive !== null)
1104
+ return { mode: "adaptive", max: Number(adaptive[1]) };
1105
+ throw usageError(`Invalid execution strategy: ${value}.`);
1106
+ })();
1107
+ const retryLaneId = isRetry
1108
+ ? store.getAgentRun(item.taskId, cliTaskRecordReference(args[3], "agentRun", environment).localId)?.executionLaneId
1109
+ : undefined;
1110
+ const plan = normalizedExecutionLanePlan({
1111
+ assignee: item.assignee ?? "",
1112
+ requestedRoles: roles,
1113
+ requestedStrategy,
1114
+ existingGroup: group === undefined ? undefined : group,
1115
+ status: item.status,
1116
+ nextGroupId: `execution-group-${store.peekNextAgentRunId(item.taskId)}`,
1117
+ retryLaneId,
1118
+ phase: isRetry ? "retry" : "dispatch"
1119
+ });
1120
+ const laneRoles = plan.roles;
1121
+ if (!isRetry && laneRoles.length === 0) {
1122
+ throw usageError("At least one --lane-role is required when expanding an ExecutionGroup.");
1123
+ }
1124
+ if (group !== undefined && requestedStrategy !== undefined) {
1125
+ const same = group.strategy.mode === requestedStrategy.mode
1126
+ && (group.strategy.mode === "fixed"
1127
+ ? requestedStrategy.mode === "fixed" && group.strategy.count === requestedStrategy.count
1128
+ : requestedStrategy.mode === "adaptive" && group.strategy.max === requestedStrategy.max);
1129
+ if (!same)
1130
+ throw usageError(`ExecutionGroup strategy is frozen: ${group.id}.`);
1131
+ }
1132
+ const laneCount = plan.requestedCount;
1133
+ const strategyArg = args.find((value) => value.startsWith("adaptive:") || value.startsWith("fixed:"));
1134
+ const adaptive = strategyArg?.startsWith("adaptive:") === true || group?.strategy.mode === "adaptive";
1135
+ const needsIsolation = adaptive || laneCount > 1 || (group?.lanes.length ?? 0) > 1;
1136
+ if (!needsIsolation)
1137
+ return undefined;
1138
+ const groupId = group?.id ?? `execution-group-${store.peekNextAgentRunId(item.taskId)}`;
1139
+ const laneIds = plan.laneIds;
1140
+ const map = new Map();
1141
+ try {
1142
+ for (const laneId of laneIds.filter((value) => value.length > 0)) {
1143
+ map.set(laneId, await preparer.prepareExecutionLaneWorkspace(item.taskId, groupId, laneId, {
1144
+ purpose: "execution",
1145
+ workItemId: item.id
1146
+ }));
1147
+ }
1148
+ }
1149
+ catch (error) {
1150
+ await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
1151
+ throw error;
1152
+ }
1153
+ return map;
1154
+ }
1155
+ async function prepareReviewLaneWorkspaces(taskId, reviewRoundId, store, preparer) {
1156
+ const round = store.getReviewRound(taskId, reviewRoundId);
1157
+ const group = round?.executionGroup;
1158
+ if (round === null || round === undefined || group === undefined)
1159
+ return undefined;
1160
+ if (group.lanes.length < 2 && group.strategy.mode !== "adaptive")
1161
+ return undefined;
1162
+ const map = new Map();
1163
+ try {
1164
+ for (const lane of group.lanes.filter((candidate) => candidate.status === "pending" || candidate.status === "running")) {
1165
+ map.set(lane.id, await preparer.prepareExecutionLaneWorkspace(taskId, group.id, lane.id, {
1166
+ purpose: "review",
1167
+ reviewRoundId
1168
+ }));
1169
+ }
1170
+ }
1171
+ catch (error) {
1172
+ await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
1173
+ throw error;
1174
+ }
1175
+ return map;
1176
+ }
1177
+ async function directTaskMainSnapshotForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
1178
+ if (taskFinalReviewContract === undefined
1179
+ || args[0] !== "task"
1180
+ || args[1] !== "work"
1181
+ || args[2] !== "update"
1182
+ || args[3] === undefined
1183
+ || args[4] !== "done") {
1184
+ return undefined;
1185
+ }
1186
+ const reference = cliWorkItemReference(args[3], environment);
1187
+ const item = store.getWorkItem(reference.taskId, reference.localId);
1188
+ if (item === null || item.writeProjectIds.length === 0
1189
+ || store.getWorkItemWorkspace(reference.taskId, reference.localId) !== null) {
1190
+ return undefined;
1191
+ }
1192
+ const workspace = store.getTaskWorkspace(reference.taskId);
1193
+ // Exact Task-final Candidates may intentionally be metadata-only when no
1194
+ // Task main exists. They remain review anchors, but are not eligible for the
1195
+ // direct ChangeSet capture path.
1196
+ if (workspace === null)
1197
+ return undefined;
1198
+ if (workspace.owner.type !== "task") {
1199
+ throw usageError(`Task has no authoritative main workspace: ${reference.taskId}.`);
1200
+ }
1201
+ try {
1202
+ return await preparer.snapshotDirectTaskMain(workspace, item.writeProjectIds);
1203
+ }
1204
+ catch (error) {
1205
+ throw usageError(error instanceof Error ? error.message : String(error));
1206
+ }
1207
+ }
1208
+ async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
1209
+ if (args[0] !== "task")
1210
+ return undefined;
1211
+ let taskId;
1212
+ if (args[1] === "complete" && args[2] !== undefined) {
1213
+ const task = store.getTask(args[2]);
1214
+ if (task === null || task.status !== "active" || task.projectBindings.length === 0) {
1215
+ return undefined;
1216
+ }
1217
+ const establishedFinalRound = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"));
1218
+ if (taskFinalReviewContract === undefined
1219
+ && store.getReviewConfig()?.trigger !== "final"
1220
+ && !establishedFinalRound) {
1221
+ return undefined;
1222
+ }
1223
+ taskId = task.id;
1224
+ }
1225
+ else if (args[1] === "review"
1226
+ && args[2] === "request"
1227
+ && args[3] !== undefined) {
1228
+ taskId = store.getTask(args[3])?.id;
1229
+ }
1230
+ else if (args[1] === "review"
1231
+ && args[2] === "retry"
1232
+ && args[3] !== undefined) {
1233
+ const reference = cliTaskRecordReference(args[3], "reviewRound", environment);
1234
+ const round = store.getReviewRound(reference.taskId, reference.localId);
1235
+ if (round !== null && (round.scope ?? "work-item") === "task") {
1236
+ taskId = reference.taskId;
1237
+ }
1238
+ }
1239
+ else if (args[1] === "work"
1240
+ && args[2] === "review"
1241
+ && args[3] === "retry"
1242
+ && args[4] !== undefined) {
1243
+ const reference = cliTaskRecordReference(args[4], "reviewRound", environment);
1244
+ const round = store.getReviewRound(reference.taskId, reference.localId);
1245
+ if (round !== null && (round.scope ?? "work-item") === "task") {
1246
+ taskId = reference.taskId;
1247
+ }
1248
+ }
1249
+ else if (args[1] === "run"
1250
+ && (args[2] === "retry" || args[2] === "settle")
1251
+ && args[3] !== undefined) {
1252
+ const reference = cliTaskRecordReference(args[3], "agentRun", environment);
1253
+ const run = store.getAgentRun(reference.taskId, reference.localId);
1254
+ const round = run?.reviewRoundId === undefined
1255
+ ? null
1256
+ : store.getReviewRound(reference.taskId, run.reviewRoundId);
1257
+ if (run?.purpose === "review"
1258
+ && round !== null
1259
+ && (round.scope ?? "work-item") === "task") {
1260
+ taskId = reference.taskId;
1261
+ }
1262
+ }
1263
+ if (taskId === undefined || store.getTask(taskId)?.status !== "active")
1264
+ return undefined;
1265
+ return snapshotActualTaskReviewCandidate(taskId, store, preparer);
1266
+ }
1267
+ async function snapshotActualTaskReviewCandidate(taskId, store, preparer) {
1268
+ const task = store.getTask(taskId);
1269
+ if (task === null)
1270
+ throw usageError(`Task not found: ${taskId}.`);
1271
+ if (task.projectBindings.length === 0) {
1272
+ throw usageError(`Final Task Review requires a Project-backed Task: ${task.id}.`);
1273
+ }
1274
+ const workspace = store.getTaskWorkspace(task.id);
1275
+ if (workspace === null
1276
+ || workspace.owner.type !== "task"
1277
+ || workspace.owner.taskId !== task.id) {
1278
+ throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
1279
+ }
1280
+ try {
1281
+ const snapshot = await preparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(({ projectId }) => projectId));
1282
+ const heads = new Map(snapshot.projects.map(({ projectId, headCommit }) => ([projectId, headCommit])));
1283
+ return {
1284
+ schemaVersion: 1,
1285
+ projects: task.projectBindings.map(({ projectId }) => {
1286
+ const commit = heads.get(projectId);
1287
+ if (commit === undefined) {
1288
+ throw new Error(`Task main snapshot omitted Project ${projectId}.`);
1289
+ }
1290
+ return { projectId, commit };
1291
+ })
1292
+ };
1293
+ }
1294
+ catch (error) {
1295
+ throw usageError(`Actual Task Project head verification failed for ${task.id}: `
1296
+ + `${error instanceof Error ? error.message : String(error)}`);
1297
+ }
1298
+ }
1299
+ async function reviewWorkspaceResultForTaskCommand(args, store, preparer, environment) {
1300
+ if (args[0] !== "task" || args[1] !== "run" || args[2] !== "yield"
1301
+ || args[3] === undefined)
1302
+ return undefined;
1303
+ const reference = cliTaskRecordReference(args[3], "agentRun", environment);
1304
+ const run = store.getAgentRun(reference.taskId, reference.localId);
1305
+ if (run === null || run.purpose !== "review" || run.reviewRoundId === undefined) {
1306
+ return undefined;
1307
+ }
1308
+ return preparer.snapshotReviewRunResult(reference.taskId, run);
1309
+ }
1310
+ async function executionLaneGitSnapshotForTaskCommand(args, store, preparer, environment) {
1311
+ if (args[0] !== "task" || args[1] !== "run" || args[2] !== "yield"
1312
+ || args[3] === undefined)
1313
+ return undefined;
1314
+ const reference = cliTaskRecordReference(args[3], "agentRun", environment);
1315
+ const run = store.getAgentRun(reference.taskId, reference.localId);
1316
+ if (run === null || run.executionGroupId === undefined
1317
+ || run.executionLaneId === undefined
1318
+ // Review output has a distinct evidence contract: diagnostic work may
1319
+ // remain uncommitted, and snapshotReviewRunResult validates its exact
1320
+ // ReviewRound/Lane owner. It must not also pass the Develop Candidate
1321
+ // snapshot preflight, which requires a clean committed worktree.
1322
+ || run.purpose === "review") {
1323
+ return undefined;
1324
+ }
1325
+ if (run.workspace === undefined)
1326
+ return null;
1327
+ const stored = store.getManagedWorkspace(run.workspace.owner);
1328
+ if (stored === null || !isDeepStrictEqual(stored, run.workspace)) {
1329
+ throw usageError(`Execution Lane managed workspace changed before yield: ${run.id}.`);
1330
+ }
1331
+ // A Gitless fixed(1) Lane runs from the durable Task-owned empty view. It
1332
+ // has no writable Project boundary, so there is no Lane Git snapshot to
1333
+ // freeze; keep the normal Candidate path metadata-only.
1334
+ if (run.workspace.owner.type === "task" && run.workspace.entries.length === 0) {
1335
+ return null;
1336
+ }
1337
+ try {
1338
+ return (await preparer.snapshotExecutionLaneWorkspace(run.workspace)) ?? null;
1339
+ }
1340
+ catch (error) {
1341
+ throw usageError(`Execution Lane Git snapshot preflight failed for ${run.id}: `
1342
+ + `${error instanceof Error ? error.message : String(error)}`);
1343
+ }
1344
+ }
1345
+ function reviewRoundFromCommandData(data) {
1346
+ if (typeof data !== "object" || data === null || !("reviewRound" in data))
1347
+ return undefined;
1348
+ const round = data.reviewRound;
1349
+ if (typeof round !== "object" || round === null)
1350
+ return undefined;
1351
+ const value = round;
1352
+ return typeof value.id === "string"
1353
+ && typeof value.taskId === "string"
1354
+ && typeof value.status === "string"
1355
+ ? {
1356
+ id: value.id,
1357
+ taskId: value.taskId,
1358
+ status: value.status,
1359
+ resumedPendingFinalReview: data[RESUMED_PENDING_FINAL_REVIEW] === true,
1360
+ terminalizedLeaderRun: data[TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW] === true
1361
+ }
1362
+ : undefined;
1363
+ }
1364
+ async function executeOperatorSessionControl(control, home, store, runtime, tmux, catalogs) {
1365
+ if (jsonOutput)
1366
+ throw usageError("Operator new and resume do not support --json.");
1367
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
1368
+ throw usageError("Operator new and resume require an interactive terminal.");
1369
+ }
1370
+ const role = store.getGlobalRole("operator");
1371
+ if (role === null)
1372
+ throw usageError("Operator is not configured. Run yui setup first.");
1373
+ const sessionSet = store.getGlobalRoleSessionSet(role.name);
1374
+ const active = sessionSet?.sessions[sessionSet.activeAgentId];
1375
+ const paneRunning = tmux.detectRoleStatus("operator", "operator") === "running";
1376
+ if (paneRunning && active === undefined) {
1377
+ throw usageError("Operator is running but its native session has not been recorded yet. "
1378
+ + "Wait for the first turn to settle before switching sessions.");
1379
+ }
1380
+ if (control.action === "resume"
1381
+ && paneRunning
1382
+ && control.targetAgentId === active?.agentId
1383
+ && active !== undefined
1384
+ && operatorSessionRef(active) === control.ref) {
1385
+ tmux.attachRole("operator", "operator");
1386
+ return;
1387
+ }
1388
+ const handle = terminalIo();
1389
+ try {
1390
+ if (control.targetAgentId !== role.activeAgentId) {
1391
+ const binding = role.agentBindings[control.targetAgentId];
1392
+ if (binding === undefined) {
1393
+ throw usageError(`Operator Agent is not bound: ${control.targetAgentId}.`);
1394
+ }
1395
+ handle.io.write([
1396
+ `Switching to ${adapterLabel(binding.adapterId)} (${binding.agentId})`,
1397
+ "",
1398
+ "Saved configuration",
1399
+ ` Model ${binding.config.model ?? "CLI default"}`,
1400
+ ` Effort ${binding.config.effort ?? "CLI default"}`,
1401
+ ""
1402
+ ].join("\n"));
1403
+ const update = (await handle.io.question("Update this configuration? [y/N]: "))?.trim().toLowerCase();
1404
+ if (update === "y" || update === "yes") {
1405
+ const resolution = await resolveGlobalRoleAgentConfigurationArguments(role.name, binding.agentId, selectionPorts(store, catalogs), handle.io);
1406
+ if (resolution.kind !== "resolved") {
1407
+ process.stdout.write("Cancelled.\n");
1408
+ return;
1409
+ }
1410
+ const updated = runGlobalRoleCommand(resolution.args.slice(1), store, { yuiHome: home, env: process.env });
1411
+ if (typeof updated !== "string") {
1412
+ throw new Error("Operator Agent configuration returned an invalid control result.");
1413
+ }
1414
+ handle.io.write(`\nUpdated ${adapterLabel(binding.adapterId)} configuration.\n`);
1415
+ }
1416
+ }
1417
+ if (paneRunning) {
1418
+ const answer = (await handle.io.question("Operator is running. Switch session? [y/N]: "))?.trim().toLowerCase();
1419
+ if (answer !== "y" && answer !== "yes") {
1420
+ process.stdout.write("Cancelled.\n");
1421
+ return;
1422
+ }
1423
+ }
1424
+ }
1425
+ finally {
1426
+ handle.close();
1427
+ }
1428
+ await ensureFileTaskController(home, { environment: process.env });
1429
+ if (paneRunning
1430
+ || (active !== undefined
1431
+ && active.status !== "stopped"
1432
+ && active.status !== "broken")) {
1433
+ await runtime.stopGlobalRoleSession(role.name);
1434
+ }
1435
+ applyOperatorSessionControl(control, store);
1436
+ await runtime.prepareGlobalRoleEnter(role.name);
1437
+ tmux.attachRole("operator", role.name);
1438
+ }
1439
+ function adapterLabel(adapterId) {
1440
+ return adapterId === "codex"
1441
+ ? "Codex"
1442
+ : adapterId === "claude"
1443
+ ? "Claude"
1444
+ : adapterId;
1445
+ }
204
1446
  function renderControllerResult(method, value) {
205
1447
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
206
1448
  return JSON.stringify(value);
207
1449
  }
208
1450
  const result = value;
209
- if (method === "status") {
210
- if (result.running !== true)
211
- return "Controller is not running.";
212
- return result.pid === undefined
213
- ? "Controller is running."
214
- : `Controller is running (PID ${String(result.pid)}).`;
215
- }
216
1451
  if (method === "restart") {
217
1452
  const previousPid = Number.isSafeInteger(result.previousPid) ? String(result.previousPid) : undefined;
218
1453
  const pid = Number.isSafeInteger(result.pid) ? String(result.pid) : undefined;
@@ -251,8 +1486,7 @@ async function completionCommand(home, node) {
251
1486
  if (args.length > (shell === undefined ? 1 : 2)) {
252
1487
  throw usageError("Completion usage: yui completion [bash|zsh|fish]");
253
1488
  }
254
- requireStorageSchema(home);
255
- const store = new FileTaskStore(home);
1489
+ const store = openCompatibleFileTaskStore(home);
256
1490
  const ioHandle = terminalIo();
257
1491
  try {
258
1492
  const manager = new FileCompletionManager(store, process.env, resolveCliIdentity(process.env));
@@ -270,30 +1504,34 @@ function completionShell(value) {
270
1504
  return value;
271
1505
  throw usageError("Completion shell must be one of bash, zsh, fish.");
272
1506
  }
273
- async function resolveTerminalArguments(commandArgs, node, store) {
1507
+ async function resolveTerminalArguments(commandArgs, node, store, catalogs) {
274
1508
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
275
1509
  if (!interactive || !allowsInteractiveSelection(commandArgs, jsonOutput)) {
276
1510
  return [...commandArgs];
277
1511
  }
278
1512
  const handle = terminalIo();
279
1513
  try {
280
- const ports = selectionPorts(store);
1514
+ const ports = selectionPorts(store, catalogs);
1515
+ const operatorWizard = await resolveOperatorWizardArguments(commandArgs, store.getGlobalRole("operator"), listOperatorSessions(store.getGlobalRoleSessionSet("operator")), handle.io);
1516
+ if (operatorWizard.kind === "cancelled")
1517
+ return null;
1518
+ const operatorArgs = operatorWizard.args;
281
1519
  // Global Role add owns its Agent choice so the configured default can be
282
1520
  // shown explicitly. Other commands first resolve missing positional
283
1521
  // targets through the generic selector, then enter the focused Role UI.
284
- if ((commandArgs[0] === "role" && commandArgs[1] === "add")
285
- || (commandArgs[0] === "task" && commandArgs[1] === "role" && commandArgs[2] === "add")) {
286
- const wizard = await resolveRoleWizardArguments(commandArgs, ports, handle.io);
1522
+ if ((operatorArgs[0] === "role" && operatorArgs[1] === "add")
1523
+ || (operatorArgs[0] === "task" && operatorArgs[1] === "role" && operatorArgs[2] === "add")) {
1524
+ const wizard = await resolveRoleWizardArguments(operatorArgs, ports, handle.io);
287
1525
  if (wizard.kind === "cancelled")
288
1526
  return null;
289
1527
  const selected = await resolveInteractiveArguments(wizard.args, node, ports, handle.io);
290
1528
  return selected.kind === "cancelled" ? null : selected.args;
291
1529
  }
292
- const selected = await resolveInteractiveArguments(commandArgs, node, ports, handle.io);
1530
+ const selected = await resolveInteractiveArguments(operatorArgs, node, ports, handle.io);
293
1531
  if (selected.kind === "cancelled")
294
1532
  return null;
295
- const wizard = await resolveRoleWizardArguments(selected.args, ports, handle.io);
296
- return wizard.kind === "cancelled" ? null : wizard.args;
1533
+ const roleWizard = await resolveRoleWizardArguments(selected.args, ports, handle.io);
1534
+ return roleWizard.kind === "cancelled" ? null : roleWizard.args;
297
1535
  }
298
1536
  finally {
299
1537
  // The Agent process must be the only reader of stdin after Role enter.
@@ -323,20 +1561,83 @@ function terminalIo() {
323
1561
  close: () => { readline.close(); }
324
1562
  };
325
1563
  }
326
- function selectionPorts(store) {
1564
+ function selectionPorts(store, catalogs) {
327
1565
  return {
328
- call: (method, params) => selectionCall(store, method, params)
1566
+ call: (method, params) => selectionCall(store, catalogs, method, params)
329
1567
  };
330
1568
  }
331
- function selectionCall(store, method, params) {
1569
+ async function preflightAgentConfigurationMutation(commandArgs, store, catalogs) {
1570
+ if (!hasModelOrEffortMutation(commandArgs))
1571
+ return;
1572
+ const agentId = configurationMutationAgentId(commandArgs, store);
1573
+ if (agentId === undefined)
1574
+ return;
1575
+ const agent = store.getConfiguredAgent(agentId);
1576
+ if (agent === null)
1577
+ return;
1578
+ await catalogs.resolve({
1579
+ agent,
1580
+ cwd: store.getConfig().defaultWorkspace ?? process.cwd()
1581
+ });
1582
+ }
1583
+ function hasModelOrEffortMutation(args) {
1584
+ const operation = args[0] === "role"
1585
+ || (args[0] === "task" && args[1] === "role");
1586
+ return operation && [
1587
+ "--model", "--effort", "--clear-model", "--clear-effort"
1588
+ ].some((option) => args.includes(option));
1589
+ }
1590
+ function configurationMutationAgentId(args, store) {
1591
+ const explicit = optionValue(args, "--agent");
1592
+ if (explicit !== undefined)
1593
+ return explicit;
1594
+ if (args[0] === "role" && args[1] === "update") {
1595
+ return store.getGlobalRole(args[2] ?? "")?.activeAgentId;
1596
+ }
1597
+ if (args[0] === "task" && args[1] === "role") {
1598
+ if (args[2] === "add")
1599
+ return store.getConfig().defaultAgent;
1600
+ if (args[2] === "update") {
1601
+ return store.getRole(args[3] ?? "", args[4] ?? "")?.activeAgentId;
1602
+ }
1603
+ }
1604
+ return undefined;
1605
+ }
1606
+ function optionValue(args, option) {
1607
+ const index = args.lastIndexOf(option);
1608
+ const value = index < 0 ? undefined : args[index + 1];
1609
+ return typeof value === "string" && !value.startsWith("--") ? value : undefined;
1610
+ }
1611
+ function selectionCall(store, catalogs, method, params) {
332
1612
  const reader = store;
333
1613
  switch (method) {
334
1614
  case "agent.list": return store.listConfiguredAgents();
1615
+ case "agent.capabilities": {
1616
+ const agent = store.getConfiguredAgent(String(params.agentId ?? ""));
1617
+ if (agent === null)
1618
+ return null;
1619
+ const configuredWorkspace = store.getConfig().defaultWorkspace;
1620
+ const cwd = typeof params.cwd === "string" && params.cwd.length > 0
1621
+ ? params.cwd
1622
+ : configuredWorkspace ?? process.cwd();
1623
+ const config = typeof params.config === "object" && params.config !== null
1624
+ ? params.config
1625
+ : undefined;
1626
+ return catalogs.resolve({
1627
+ agent,
1628
+ cwd,
1629
+ ...(config === undefined ? {} : { config })
1630
+ });
1631
+ }
335
1632
  case "config.get": return store.getConfig();
1633
+ case "profile.list": return store.listAgentProfiles();
1634
+ case "profile.show": return store.getAgentProfile(String(params.id ?? ""));
336
1635
  case "role.list": return store.listGlobalRoles();
337
1636
  case "role.show": return store.getGlobalRole(String(params.name ?? ""));
338
- case "repository.list": return callOptional(reader, "listRepositories");
1637
+ case "project.list": return callOptional(reader, "listProjects");
339
1638
  case "task.list": return callOptional(reader, "listTasks");
1639
+ case "task.integration.list": return store.listIntegrationAttempts(String(params.taskId ?? ""));
1640
+ case "task.change-set.list": return store.listChangeSets(String(params.taskId ?? ""));
340
1641
  case "task.role.list": return callOptional(reader, "listRoles", [params.taskId]);
341
1642
  case "task.role.show": return callOptional(reader, "getRole", [params.taskId, params.roleName]);
342
1643
  case "task.work.list": return callOptional(reader, "listWorkItems", [params.taskId]);
@@ -347,27 +1648,43 @@ function selectionCall(store, method, params) {
347
1648
  : store.listInputRequests(taskId);
348
1649
  return params.all === true ? requests : requests.filter((request) => request.status === "open");
349
1650
  }
350
- case "task.run.list": return callOptional(reader, "listAgentRuns", [params.workItemId]);
1651
+ case "task.run.list": return callOptional(reader, "listAgentRuns", [params.taskId]);
351
1652
  case "task.decision.list": return callOptional(reader, "listDecisions", [params.taskId]);
352
- case "task.milestone.list": return callOptional(reader, "listMilestones", [params.taskId]);
353
- case "task.event.list": return callOptional(reader, "listEvents", [params.taskId]);
1653
+ case "task.milestone.list": return presentSelectionTimes(callOptional(reader, "listMilestones", [params.taskId]), store);
1654
+ case "task.event.list": return presentSelectionTimes(callOptional(reader, "listEvents", [params.taskId]), store);
354
1655
  case "jobs.list": return callOptional(reader, "listJobs");
355
1656
  default: return [];
356
1657
  }
357
1658
  }
1659
+ function presentSelectionTimes(value, store) {
1660
+ if (!Array.isArray(value))
1661
+ return value;
1662
+ const timeZone = store.getConfig().timeZone;
1663
+ return value.map((record) => {
1664
+ if (typeof record !== "object" || record === null || Array.isArray(record))
1665
+ return record;
1666
+ const candidate = record;
1667
+ return typeof candidate.createdAt === "string"
1668
+ ? {
1669
+ ...candidate,
1670
+ createdAt: formatTimestamp(candidate.createdAt, timeZone)
1671
+ }
1672
+ : candidate;
1673
+ });
1674
+ }
358
1675
  function callOptional(reader, method, args = []) {
359
1676
  const operation = reader[method];
360
1677
  return operation === undefined ? [] : Reflect.apply(operation, reader, args);
361
1678
  }
362
1679
  function readableStore(home) {
363
- requireStorageSchema(home);
364
- return new FileTaskStore(home);
1680
+ return openCompatibleFileTaskStore(home);
365
1681
  }
366
1682
  function completionSelectionPorts(home) {
367
1683
  if (inspectStorageSchema(home).status === "uninitialized") {
368
1684
  return { call: () => [] };
369
1685
  }
370
- return selectionPorts(readableStore(home));
1686
+ const store = readableStore(home);
1687
+ return selectionPorts(store, new AgentConfigurationCatalogService(home, { environment: process.env }));
371
1688
  }
372
1689
  function emit(output, literal = false, data) {
373
1690
  const normalized = literal ? output.trimEnd() : output.trimEnd();
@@ -377,16 +1694,33 @@ function emit(output, literal = false, data) {
377
1694
  : { ok: true, data })
378
1695
  : normalized}\n`);
379
1696
  }
380
- function readPackageVersion() {
381
- try {
382
- const value = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
383
- if (typeof value.version === "string" && value.version.length > 0)
384
- return value.version;
1697
+ function withControllerRefreshWarning(output, refresh, label) {
1698
+ if (refresh.status !== "failed")
1699
+ return output;
1700
+ if (label === "Agent environment") {
1701
+ return `${output.trimEnd()}\nWarning: Agent configuration was saved, but its current `
1702
+ + `environment values were not applied or persisted (${refresh.message}). Retry the `
1703
+ + "Agent command with those variables present, or restart the Controller from an "
1704
+ + "environment that provides them.\n";
385
1705
  }
386
- catch {
387
- // Keep help/version available if package metadata is damaged.
388
- }
389
- return "0.0.0";
1706
+ return `${output.trimEnd()}\nWarning: ${label} was saved, but the running Controller `
1707
+ + `could not be refreshed (${refresh.message}). Restart the Controller to apply it.\n`;
1708
+ }
1709
+ function agentEnvironmentRefreshScope(previous, current, configured) {
1710
+ const retainedSources = new Set(configured.flatMap((agent) => (agent.environment.map((binding) => binding.sourceName))));
1711
+ const retainedNative = new Set(configured.flatMap((agent) => (nativeAgentEnvironmentNames(agent.adapterId))));
1712
+ const currentSources = current?.environment.map((binding) => binding.sourceName) ?? [];
1713
+ const previousOnlySources = previous?.environment
1714
+ .map((binding) => binding.sourceName)
1715
+ .filter((name) => !retainedSources.has(name)) ?? [];
1716
+ const currentNative = current === null ? [] : nativeAgentEnvironmentNames(current.adapterId);
1717
+ const previousOnlyNative = previous === null
1718
+ ? []
1719
+ : nativeAgentEnvironmentNames(previous.adapterId).filter((name) => !retainedNative.has(name));
1720
+ return {
1721
+ sourceNames: [...new Set([...currentSources, ...previousOnlySources])],
1722
+ nativeNames: [...new Set([...currentNative, ...previousOnlyNative])]
1723
+ };
390
1724
  }
391
1725
  export function cliIdentity(env) {
392
1726
  return env.YUI_CLI_NAME === "yui-dev" ? "yui-dev" : "yui";