@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.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- 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 {
|
|
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 {
|
|
17
|
-
import {
|
|
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 {
|
|
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 {
|
|
23
|
-
import {
|
|
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 {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
153
|
+
emit(renderDoctor(report.checks, report.review));
|
|
106
154
|
return;
|
|
107
155
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
124
|
-
|
|
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, {
|
|
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
|
-
|
|
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] === "
|
|
144
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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"
|
|
455
|
+
if (task?.status === "active") {
|
|
185
456
|
await workspacePreparer.prepareTaskWorkspace(task.id);
|
|
186
457
|
}
|
|
187
458
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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 ((
|
|
285
|
-
|| (
|
|
286
|
-
const wizard = await resolveRoleWizardArguments(
|
|
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(
|
|
1530
|
+
const selected = await resolveInteractiveArguments(operatorArgs, node, ports, handle.io);
|
|
293
1531
|
if (selected.kind === "cancelled")
|
|
294
1532
|
return null;
|
|
295
|
-
const
|
|
296
|
-
return
|
|
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
|
|
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 "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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";
|