@zq-silk/yui 2.0.0 → 2.2.0
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/README.md +4 -0
- package/dist/cli/commandCatalog.js +318 -289
- package/dist/cli/commandDiscovery.js +72 -0
- package/dist/cli/completion.js +22 -227
- package/dist/cli/dynamicCompletion.js +12 -6
- package/dist/cli/invocationAuthority.js +2 -2
- package/dist/cli/invocationRouter.js +18 -7
- package/dist/cli.js +23 -3030
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/globalRoleCommands.js +2 -1
- package/dist/commands/taskCommands.js +90 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +12 -3
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +28 -1
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +275 -0
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +40 -0
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +16 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +293 -0
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +95 -0
- package/dist/plugins/pluginService.js +98 -8
- package/dist/resources/projectResourceService.js +23 -1
- package/dist/runtime/managedIdentity.js +6 -0
- package/dist/surface/surfaceContributions.js +3 -0
- package/dist/task/taskAuthority.js +61 -10
- package/dist/web/assets/assetManifest.js +38 -22
- package/dist/web/assets/client/api.js +115 -0
- package/dist/web/assets/client/app.js +615 -862
- package/dist/web/assets/client/components.js +323 -897
- package/dist/web/assets/client/detail.js +333 -0
- package/dist/web/assets/client/dock.js +225 -0
- package/dist/web/assets/client/dom.js +55 -3
- package/dist/web/assets/client/evidence.js +172 -0
- package/dist/web/assets/client/format.js +25 -14
- package/dist/web/assets/client/forms.js +230 -0
- package/dist/web/assets/client/i18n.js +1024 -791
- package/dist/web/assets/client/overview.js +118 -0
- package/dist/web/assets/client/records.js +63 -0
- package/dist/web/assets/client/sections.js +313 -0
- package/dist/web/assets/client/sidebar.js +146 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +125 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +169 -0
- package/dist/web/assets/styles/layout.js +56 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +41 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +292 -0
- package/dist/web/webServer.js +2 -1
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +61 -0
- package/docs/release-workflow.zh-CN.md +46 -0
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +33 -0
- package/dist/web/assets/client/taskSummary.js +0 -350
- package/dist/web/assets/client/taskSurface.js +0 -616
- package/dist/web/assets/client/view.js +0 -608
- package/dist/web/assets/styles/cards.js +0 -247
- package/dist/web/assets/styles/widgets.js +0 -168
|
@@ -0,0 +1,3092 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
import { agentAdapterLabel as adapterLabel } from "./agent/adapterCatalog.js";
|
|
10
|
+
import { recordTaskInterruptResult } from "./message/taskInterrupt.js";
|
|
11
|
+
import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
|
|
12
|
+
import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability } from "./artifacts/artifactCapability.js";
|
|
13
|
+
import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
|
|
14
|
+
import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
|
|
15
|
+
import { discoveryCommandTree } from "./cli/commandDiscovery.js";
|
|
16
|
+
import { contextRecordReader } from "./context/taskContext.js";
|
|
17
|
+
import { renderCompletion } from "./cli/completion.js";
|
|
18
|
+
import { runCompletionWizard } from "./cli/completionWizard.js";
|
|
19
|
+
import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
|
|
20
|
+
import { renderCommandHelp } from "./cli/helpRenderer.js";
|
|
21
|
+
import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
|
|
22
|
+
import { routeInvocation } from "./cli/invocationRouter.js";
|
|
23
|
+
import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
|
|
24
|
+
import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
|
|
25
|
+
import { requireManagedGlobalCaller, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
|
|
26
|
+
import { hasManagedIdentity } from "./runtime/managedIdentity.js";
|
|
27
|
+
import { resolveOperatorWizardArguments } from "./cli/operatorWizard.js";
|
|
28
|
+
import { resolveGlobalRoleAgentConfigurationArguments, resolveRoleWizardArguments } from "./cli/roleWizard.js";
|
|
29
|
+
import { runUpdateCommand } from "./cli/updateCommand.js";
|
|
30
|
+
import { createUpdatePorts } from "./cli/updatePorts.js";
|
|
31
|
+
import { runUpgradeCommand } from "./cli/upgradeCommand.js";
|
|
32
|
+
import { runAgentCommand } from "./commands/agentCommands.js";
|
|
33
|
+
import { runCapabilityCommand } from "./commands/capabilityCommands.js";
|
|
34
|
+
import { runConfigCommand } from "./commands/configCommands.js";
|
|
35
|
+
import { runConfigOverview } from "./commands/configOverview.js";
|
|
36
|
+
import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
|
|
37
|
+
import { runDurableJobCommand } from "./commands/durableJobCommands.js";
|
|
38
|
+
import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
|
|
39
|
+
import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
|
|
40
|
+
import { runJobCommand } from "./commands/jobCommands.js";
|
|
41
|
+
import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
|
|
42
|
+
import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
|
|
43
|
+
import { runProjectCommand } from "./commands/projectCommands.js";
|
|
44
|
+
import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
|
|
45
|
+
import { runResourcesCommand } from "./commands/resourcesCommands.js";
|
|
46
|
+
import { parseSessionReconcileOptions, parseSessionStopOptions, runSessionReconcileCommand, runSessionStopCommand } from "./commands/sessionCommands.js";
|
|
47
|
+
import { assertTaskDeliveryAuthority, taskLocalActor, resolveJobCaller } from "./task/taskAuthority.js";
|
|
48
|
+
import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
|
|
49
|
+
import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
|
|
50
|
+
import { assertWorkItemDependenciesCompletedForCommand, dispatchPreparedReviewRound, failPendingReviewRound, parseTaskArchiveArguments, parseTaskCompletionRequest, planReplicatedWorkItemLanes, preflightTaskCompletion, preserveReviewRoundWorkspace, previewTaskRoleAgentConfigurationMutation, requireWorkItemAssignee, readDispatchContextRefs, runTaskCommand, validateTaskArchiveRequest } from "./commands/taskCommands.js";
|
|
51
|
+
import { verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
|
|
52
|
+
import { finalizeStoppedTaskExecution, parseTaskExecutionStartRequest, parseTaskExecutionStopRequest, startTaskExecutionCommand, stopTaskExecutionCommand } from "./commands/taskExecutionCommands.js";
|
|
53
|
+
import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
|
|
54
|
+
import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
|
|
55
|
+
import { runTaskPublicationAdoptCommand } from "./commands/taskPublicationAdoptCommand.js";
|
|
56
|
+
import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
|
|
57
|
+
import { runTaskUpstreamCommand } from "./commands/taskUpstreamCommands.js";
|
|
58
|
+
import { runTaskWorkspaceCommand } from "./commands/taskWorkspaceCommands.js";
|
|
59
|
+
import { runTelemetryCommand } from "./commands/telemetryCommands.js";
|
|
60
|
+
import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
|
|
61
|
+
import { FileCompletionManager } from "./completion/fileCompletionManager.js";
|
|
62
|
+
import { CONFIG_DOMAINS } from "./config/configCatalog.js";
|
|
63
|
+
import { resolveTmuxBin, resolveTmuxHistoryLimit } from "./config/yuiConfig.js";
|
|
64
|
+
import { readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
|
|
65
|
+
import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
|
|
66
|
+
import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
|
|
67
|
+
import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
|
|
68
|
+
import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
|
|
69
|
+
import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
|
|
70
|
+
import { runRuntimeObservationHookCommand } from "./controller/runtimeObservationHook.js";
|
|
71
|
+
import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
|
|
72
|
+
import { SessionOwnerReconciliation } from "./controller/sessionOwnerReconciliation.js";
|
|
73
|
+
import { callController, ControllerClientError } from "./core/controllerClient.js";
|
|
74
|
+
import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
|
|
75
|
+
import { agentNotFound, CliError, runtimeError, usageError } from "./errors/cliError.js";
|
|
76
|
+
import { describeCliFailure, renderCliFailure, sanitizeFailureText, sanitizeFailureDetails } from "./errors/cliFailure.js";
|
|
77
|
+
import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "./executor/agentConfigurationCatalog.js";
|
|
78
|
+
import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
|
|
79
|
+
import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
|
|
80
|
+
import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
|
|
81
|
+
import { recordGlobalInterruptResult, recordGlobalSteerResult } from "./message/globalInterrupt.js";
|
|
82
|
+
import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth } from "./observability/runtimeIdentity.js";
|
|
83
|
+
import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
|
|
84
|
+
import { renderAgentConfigurationCatalog } from "./output/agentConfigurationPresentation.js";
|
|
85
|
+
import { parseTaskAgentCapabilityQuery } from "./commands/taskAgentCapabilities.js";
|
|
86
|
+
import { capabilityReaderIdentity } from "./controller/agentCapabilities.js";
|
|
87
|
+
import { formatTimestamp } from "./output/timePresentation.js";
|
|
88
|
+
import { resolveAgentProfileView } from "./profile/agentProfileRuntime.js";
|
|
89
|
+
import { assertCliHomeReleaseFence } from "./release/cliHomeReleaseFence.js";
|
|
90
|
+
import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
|
|
91
|
+
import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
|
|
92
|
+
import { NodeGitWorkspace } from "./repository/gitWorkspace.js";
|
|
93
|
+
import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
|
|
94
|
+
import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
|
|
95
|
+
import { FileTaskWorkspacePreparer } from "./repository/taskWorkspacePreparer.js";
|
|
96
|
+
import { snapshotWorkItemCandidate } from "./repository/workItemCandidateSnapshot.js";
|
|
97
|
+
import { assessDeltaRecheck } from "./review/deltaRecheck.js";
|
|
98
|
+
import { isCompletedTaskReviewEvidence } from "./review/reviewAcceptance.js";
|
|
99
|
+
import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
|
|
100
|
+
import { resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractResolution.js";
|
|
101
|
+
import { AGENT_HOST_CONTROL_PROTOCOL, foldInterruptLiveReceipt, foldSteerLiveReceipt, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl, sendAgentHostCancelControl, sendAgentHostSteerControl } from "./runtime/agentHost.js";
|
|
102
|
+
import { unknownAgentRunConfiguration } from "./runtime/agentRunConfiguration.js";
|
|
103
|
+
import { isCodexInspectionPermissionError } from "./runtime/codexSharedDaemon.js";
|
|
104
|
+
import { prepareLocalCodexStartup } from "./runtime/localStartup.js";
|
|
105
|
+
import { requireManagedTaskCaller, resolveManagedTaskReader } from "./runtime/managedCaller.js";
|
|
106
|
+
import { assertRuntimeCoherence } from "./runtime/runtimeCoherence.js";
|
|
107
|
+
import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
|
|
108
|
+
import { openCurrentTaskStore, validateCurrentTaskStore } from "./storage/currentTaskStore.js";
|
|
109
|
+
import { inspectStorageSchema } from "./storage/storageSchema.js";
|
|
110
|
+
import { resolveYuiHome } from "./storage/taskStore.js";
|
|
111
|
+
import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
|
|
112
|
+
import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
|
|
113
|
+
import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./task/remoteDeliveryService.js";
|
|
114
|
+
import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
|
|
115
|
+
import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
|
|
116
|
+
import { NodeCommandExecutor } from "./tmux/commandExecutor.js";
|
|
117
|
+
import { TmuxManager } from "./tmux/tmuxManager.js";
|
|
118
|
+
import { YUI_VERSION, yuiVersionIdentity } from "./version.js";
|
|
119
|
+
import { parseWebCommandOptions } from "./web/webServer.js";
|
|
120
|
+
import { currentWorkItemExecutionGroup } from "./workItem/workItem.js";
|
|
121
|
+
import { CleanupInspectionError } from "./workspace/cleanupInspection.js";
|
|
122
|
+
import { WorkItemChangeSetManager } from "./workspace/workItemChangeSetManager.js";
|
|
123
|
+
import { workspaceProjectEntry } from "./worktree/managedWorkspace.js";
|
|
124
|
+
const VERSION = YUI_VERSION;
|
|
125
|
+
// Preserve the public executable identity after the bootstrap split.
|
|
126
|
+
const cliEntryUrl = new URL("./cli.js", import.meta.url);
|
|
127
|
+
const taskFinalReviewInvocation = extractTaskFinalReviewRequest(process.argv.slice(2));
|
|
128
|
+
const rawArgs = [...taskFinalReviewInvocation.args];
|
|
129
|
+
const jsonOutput = rawArgs.includes("--json");
|
|
130
|
+
const args = normalizeAliases(jsonOutput ? rawArgs.filter((argument) => argument !== "--json") : rawArgs);
|
|
131
|
+
let invocationHome;
|
|
132
|
+
void main().catch((error) => {
|
|
133
|
+
if (error instanceof CleanupInspectionError) {
|
|
134
|
+
error = cleanupCliError(error, error.checks[0]?.resource ?? "workspace");
|
|
135
|
+
}
|
|
136
|
+
const failure = describeCliFailure(error, failureContext());
|
|
137
|
+
process.stderr.write(`${renderCliFailure(failure, jsonOutput)}\n`);
|
|
138
|
+
process.exitCode = failure.exitCode;
|
|
139
|
+
});
|
|
140
|
+
function failureContext() {
|
|
141
|
+
const route = routeInvocation(args);
|
|
142
|
+
const node = "node" in route ? route.node : route.helpNode;
|
|
143
|
+
const operands = args.slice(node.path.length - 1);
|
|
144
|
+
const publicValues = new Set([
|
|
145
|
+
...Object.values(node.argumentValues).flat(),
|
|
146
|
+
...Object.values(node.optionValues).flat()
|
|
147
|
+
]);
|
|
148
|
+
const recordId = /^(?:task|project|message|run|work|wake|operation|review|input|job|integration|publication)-\d+(?:\/(?:message|run|work|wake|review|input|job|integration|publication)-\d+)?$/;
|
|
149
|
+
return {
|
|
150
|
+
operation: node.path.join(" "),
|
|
151
|
+
targets: operands.filter(value => recordId.test(value)),
|
|
152
|
+
// Do not replay argv: operands include Message bodies, config values and credentials.
|
|
153
|
+
privateValues: operands.filter(value => !/^--[a-z][a-z-]*$/.test(value) && !recordId.test(value) && !publicValues.has(value)),
|
|
154
|
+
home: invocationHome ?? process.env.YUI_HOME ?? "(default Home unresolved)",
|
|
155
|
+
cli: fileURLToPath(cliEntryUrl),
|
|
156
|
+
environment: process.env
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export async function main() {
|
|
160
|
+
const home = resolveYuiHome(process.env);
|
|
161
|
+
const discovery = discoveryCommandTree(process.env);
|
|
162
|
+
invocationHome = home;
|
|
163
|
+
const routedForFence = args.length === 0 ? undefined : routeInvocation(args);
|
|
164
|
+
const managedInvocation = process.env.YUI_SESSION_SCOPE === "task"
|
|
165
|
+
|| process.env.YUI_SESSION_SCOPE === "global";
|
|
166
|
+
const homeFreeInvocation = args.length === 0
|
|
167
|
+
|| (args[0] === "config" && (args[1] === "describe"
|
|
168
|
+
|| (args[1] === "completion" && args[2] === "candidates")))
|
|
169
|
+
|| (args[0] === "version" && args.length === 1)
|
|
170
|
+
|| routedForFence?.kind === "help"
|
|
171
|
+
|| routedForFence?.kind === "path-error"
|
|
172
|
+
|| routedForFence?.kind === "incomplete";
|
|
173
|
+
if (!homeFreeInvocation) {
|
|
174
|
+
assertCliHomeReleaseFence({
|
|
175
|
+
home,
|
|
176
|
+
packageRoot: fileURLToPath(new URL("../", import.meta.url)),
|
|
177
|
+
entryPath: fileURLToPath(cliEntryUrl),
|
|
178
|
+
args
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const delegatedDriver = explicitReleaseActivationDriver();
|
|
182
|
+
if (delegatedDriver !== null) {
|
|
183
|
+
const delegated = spawnSync(process.execPath, [delegatedDriver, ...process.argv.slice(2)], {
|
|
184
|
+
cwd: process.cwd(),
|
|
185
|
+
env: process.env,
|
|
186
|
+
stdio: "inherit"
|
|
187
|
+
});
|
|
188
|
+
if (delegated.error !== undefined) {
|
|
189
|
+
throw runtimeError("Target release activation driver could not start.", { cause: delegated.error });
|
|
190
|
+
}
|
|
191
|
+
if (delegated.signal !== null) {
|
|
192
|
+
throw runtimeError(`Target release activation driver stopped by signal ${delegated.signal}.`);
|
|
193
|
+
}
|
|
194
|
+
process.exitCode = delegated.status ?? 5;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (args.length === 0) {
|
|
198
|
+
emit(renderCommandHelp(discovery, VERSION), false, jsonOutput ? describeCommandTree(discovery) : undefined);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (args[0] === "version" && args.length === 1) {
|
|
202
|
+
emit(VERSION, true, yuiVersionIdentity());
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const invocation = routeInvocation(args, discovery);
|
|
206
|
+
if (invocation.kind === "help") {
|
|
207
|
+
emit(renderCommandHelp(invocation.node, VERSION), true, jsonOutput ? describeCommandTree(invocation.node) : undefined);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (invocation.kind === "path-error") {
|
|
211
|
+
throw usageError(`Unknown command: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
|
|
212
|
+
}
|
|
213
|
+
if (invocation.kind === "incomplete") {
|
|
214
|
+
throw usageError(`Command required after: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
|
|
215
|
+
}
|
|
216
|
+
if (args[0] === "version")
|
|
217
|
+
throw usageError("Version usage: yui version");
|
|
218
|
+
// Offline discovery is not an execution credential. Target selectors enter
|
|
219
|
+
// the original authenticated path lazily, only when stored data is needed.
|
|
220
|
+
if (args[0] === "config" && args[1] === "completion" && args[2] === "candidates") {
|
|
221
|
+
await completionCommand(home, invocation.node);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (args[0] === "config" && args[1] === "describe") {
|
|
225
|
+
const target = findCommandNode(args[2] === undefined ? ["config"] : ["config", args[2]], discovery);
|
|
226
|
+
if (args.length > 3 || !target)
|
|
227
|
+
throw usageError("Config describe usage: yui config describe [visible-domain].");
|
|
228
|
+
emit(renderCommandHelp(target, VERSION), false, describeCommandTree(target));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const { contract: taskFinalReviewContract, verifiedStore } = await preflightManagedTaskControlPlane();
|
|
232
|
+
assertTaskInvocationScope(args, process.env);
|
|
233
|
+
if (["config", "resources"].includes(args[0] ?? "") && hasManagedIdentity(process.env)) {
|
|
234
|
+
const ownedStore = verifiedStore === undefined ? openCurrentTaskStore(home) : undefined;
|
|
235
|
+
try {
|
|
236
|
+
assertConfigurationAuthority(args, verifiedStore ?? ownedStore, process.env);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
ownedStore?.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (args[0] === "update") {
|
|
243
|
+
if (jsonOutput)
|
|
244
|
+
throw usageError("Update does not support --json.");
|
|
245
|
+
process.exitCode = runUpdateCommand(args.slice(1));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (args[0] === "release") {
|
|
249
|
+
const subcommand = args[1];
|
|
250
|
+
if (subcommand === "install" && args.length === 3) {
|
|
251
|
+
const result = runReleaseInstall(home, args[2]);
|
|
252
|
+
if (result.outcome === "aborted")
|
|
253
|
+
emitFailureResult(renderReleaseInstallResult(result), result);
|
|
254
|
+
else
|
|
255
|
+
emit(renderReleaseInstallResult(result), false, result);
|
|
256
|
+
if (result.outcome === "aborted")
|
|
257
|
+
process.exitCode = 5;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (subcommand === "list" && args.length === 2) {
|
|
261
|
+
const result = runReleaseList(home);
|
|
262
|
+
emit(renderReleaseList(result), false, result);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (subcommand === "activate" && (args.length === 2 || args.length === 3)) {
|
|
266
|
+
const releaseId = args.length === 3
|
|
267
|
+
? args[2]
|
|
268
|
+
: runReleaseList(home).active ?? undefined;
|
|
269
|
+
if (releaseId === undefined) {
|
|
270
|
+
throw usageError("Release activate usage: yui release activate <release-id> "
|
|
271
|
+
+ "(or activate the active release when exactly one is installed).");
|
|
272
|
+
}
|
|
273
|
+
const result = await runReleaseActivate(home, releaseId);
|
|
274
|
+
if (result.outcome === "aborted" || result.outcome === "dual-owner") {
|
|
275
|
+
emitFailureResult(renderReleaseActivateResult(result), result);
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
emit(renderReleaseActivateResult(result), false, result);
|
|
279
|
+
if (result.outcome === "aborted" || result.outcome === "dual-owner") {
|
|
280
|
+
process.exitCode = 5;
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
throw usageError("Release usage: yui release install <source-dir> | list | activate [release-id].");
|
|
285
|
+
}
|
|
286
|
+
if (args[0] === "config" && args[1] === "completion") {
|
|
287
|
+
await completionCommand(home, invocation.node);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (args[0] === "capability") {
|
|
291
|
+
const result = await runCapabilityCommand(args.slice(1), home, process.env);
|
|
292
|
+
emit(JSON.stringify(result, null, 2), false, result);
|
|
293
|
+
const kind = result?.kind;
|
|
294
|
+
if (typeof result === "object" && result !== null && !Array.isArray(result)
|
|
295
|
+
&& typeof kind === "string" && !["value", "operation"].includes(kind))
|
|
296
|
+
process.exitCode = 5;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (args[0] === "setup") {
|
|
300
|
+
if (jsonOutput)
|
|
301
|
+
throw usageError("Setup does not support --json.");
|
|
302
|
+
await assertFileTaskControllerStorageCompatible(home);
|
|
303
|
+
const setupIo = {
|
|
304
|
+
input: process.stdin,
|
|
305
|
+
output: process.stdout,
|
|
306
|
+
forceInteractive: process.env.YUI_SETUP_INTERACTIVE === "1"
|
|
307
|
+
};
|
|
308
|
+
validateSetupInvocation(args.slice(1), setupIo);
|
|
309
|
+
const output = await runSetupCommand(args.slice(1), process.env, new NodeCommandExecutor(), setupIo);
|
|
310
|
+
// A successful setup leaves the Home ready for normal Yui work. Start the
|
|
311
|
+
// detached per-Home Controller even when setup began with no Controller;
|
|
312
|
+
// read-only commands and failed setup still remain non-starting paths.
|
|
313
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
314
|
+
const setupStore = openCurrentTaskStore(home);
|
|
315
|
+
try {
|
|
316
|
+
await prepareCodexForCurrentRoles(setupStore, home);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
setupStore.close();
|
|
320
|
+
}
|
|
321
|
+
const refresh = await refreshRunningFileTaskControllerEnvironment(home, openCurrentTaskStore(home), process.env);
|
|
322
|
+
emit(withControllerRefreshWarning(output, refresh, "Agent environment"));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (args[0] === "doctor") {
|
|
326
|
+
const doctorArgs = args.slice(1);
|
|
327
|
+
if (doctorArgs.length !== 0) {
|
|
328
|
+
// Preserve the usage error for stray operands (parity with text mode).
|
|
329
|
+
await runDoctorCommand(doctorArgs, process.env, new NodeCommandExecutor());
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const report = await buildDoctorReport(process.env, new NodeCommandExecutor());
|
|
333
|
+
if (jsonOutput) {
|
|
334
|
+
// Machine-readable result: the full checks array + a storage-health verdict
|
|
335
|
+
// the update post-verify parses (P1-3). Exit non-zero when storage is not
|
|
336
|
+
// healthy so even a naive exit-code check fails closed. This exit-code
|
|
337
|
+
// signal is scoped to the --json path; text-mode doctor keeps its existing
|
|
338
|
+
// presentation and exit 0 (the WorkItem allows doctor's presentation to stay).
|
|
339
|
+
if (!report.storage.healthy)
|
|
340
|
+
process.exitCode = 5;
|
|
341
|
+
emit("", false, report);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
emit(renderDoctor(report.checks, report.review));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (args[0] === "start") {
|
|
348
|
+
if (args.length !== 1)
|
|
349
|
+
throw usageError("Start usage: yui start.");
|
|
350
|
+
validateCurrentTaskStore(home);
|
|
351
|
+
const startupStore = openCurrentTaskStore(home);
|
|
352
|
+
try {
|
|
353
|
+
assertHomeStartupAuthority(startupStore);
|
|
354
|
+
const controller = await ensureFileTaskController(home, { environment: process.env });
|
|
355
|
+
let codex;
|
|
356
|
+
try {
|
|
357
|
+
codex = await prepareLocalCodexStartup(startupStore, home, process.env);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
throw runtimeError("Controller is ready, but Codex shared startup failed.", { cause: error });
|
|
361
|
+
}
|
|
362
|
+
const lines = ["Yui Controller is ready."];
|
|
363
|
+
if (codex.daemons.length === 0)
|
|
364
|
+
lines.push("No active Role needs Codex; no Codex daemon was started.");
|
|
365
|
+
else
|
|
366
|
+
for (const daemon of codex.daemons) {
|
|
367
|
+
lines.push(`Codex shared daemon ${daemon.started ? "started" : "ready"}: ${daemon.socketPath}`);
|
|
368
|
+
}
|
|
369
|
+
if (codex.app === "restart-required") {
|
|
370
|
+
lines.push("Codex App is running with an older connection. Quit and reopen it to use Yui sessions.");
|
|
371
|
+
}
|
|
372
|
+
else if (codex.app === "ready") {
|
|
373
|
+
lines.push("Codex App launch is configured for the shared daemon. Use yui doctor to verify its live connection.");
|
|
374
|
+
}
|
|
375
|
+
else if (codex.app === "multiple-homes") {
|
|
376
|
+
lines.push("Active Codex Roles use multiple Codex homes; App sharing needs one selected Home.");
|
|
377
|
+
}
|
|
378
|
+
else if (codex.app === "not-detected") {
|
|
379
|
+
lines.push("Codex App was not detected; the shared daemon is ready for Codex CLI sessions.");
|
|
380
|
+
}
|
|
381
|
+
emit(lines.join("\n"), false, { controller, codex });
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
startupStore.close();
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (args[0] === "execution") {
|
|
389
|
+
// Issue 11 read-only audit: opens the Home read-only, never writes state,
|
|
390
|
+
// never wakes a Leader.
|
|
391
|
+
if (args[1] !== "audit") {
|
|
392
|
+
throw usageError("Execution usage: yui execution audit [--task <id>] [--since <iso>] [--until <iso>].");
|
|
393
|
+
}
|
|
394
|
+
const options = parseExecutionAuditOptions(args.slice(2));
|
|
395
|
+
const result = runExecutionAuditCommand(home, options);
|
|
396
|
+
emit(result.output, false, result.report);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (args[0] === "upgrade") {
|
|
400
|
+
const result = await runUpgradeCommand(args.slice(1), home);
|
|
401
|
+
process.exitCode = result.exitCode;
|
|
402
|
+
if (result.exitCode !== 0)
|
|
403
|
+
emitFailureResult(result.output, result.data);
|
|
404
|
+
else
|
|
405
|
+
emit(result.output, false, result.data);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (args[0] === "internal") {
|
|
409
|
+
if (args[1] === "agent-host" && args.length === 3) {
|
|
410
|
+
process.exitCode = await runAgentHost({
|
|
411
|
+
home,
|
|
412
|
+
ticket: args[2]
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (args[1] === "session-notify" && args.length === 3) {
|
|
417
|
+
await runSessionNotifyCommand(args[2], process.env);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (args[1] === "runtime-hook" && args.length === 2) {
|
|
421
|
+
// Provider lifecycle Hooks are observation channels, never execution
|
|
422
|
+
// gates. A late/stale Hook must not make Claude reject an otherwise
|
|
423
|
+
// valid Session or AgentRun; its exact fence is revalidated before any
|
|
424
|
+
// inbox fact is written, so dropping an invalid observation is safe.
|
|
425
|
+
try {
|
|
426
|
+
await runRuntimeObservationHookCommand(readFileSync(0, "utf8"), process.env);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
throw usageError("Internal lifecycle callback usage is invalid.");
|
|
434
|
+
}
|
|
435
|
+
if (args[0] === "session" && args[1] === "reconcile") {
|
|
436
|
+
const options = parseSessionReconcileOptions(args.slice(2));
|
|
437
|
+
const store = openCurrentTaskStore(home);
|
|
438
|
+
const tmux = new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), new NodeCommandExecutor(), {
|
|
439
|
+
yuiHome: home,
|
|
440
|
+
historyLimit: resolveTmuxHistoryLimit(store.getConfig().tmuxHistoryLimit)
|
|
441
|
+
});
|
|
442
|
+
const reconciliation = new SessionOwnerReconciliation({
|
|
443
|
+
home,
|
|
444
|
+
store,
|
|
445
|
+
environment: process.env,
|
|
446
|
+
tmux
|
|
447
|
+
});
|
|
448
|
+
const result = await runSessionReconcileCommand({
|
|
449
|
+
reconciliation,
|
|
450
|
+
options,
|
|
451
|
+
environment: process.env
|
|
452
|
+
});
|
|
453
|
+
process.exitCode = result.exitCode;
|
|
454
|
+
emit(result.output, false, result.data);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (args[0] === "controller") {
|
|
458
|
+
const method = args[1];
|
|
459
|
+
if (method === "start" && args.length === 2) {
|
|
460
|
+
validateCurrentTaskStore(home);
|
|
461
|
+
const startupStore = openCurrentTaskStore(home);
|
|
462
|
+
try {
|
|
463
|
+
assertHomeStartupAuthority(startupStore);
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
startupStore.close();
|
|
467
|
+
}
|
|
468
|
+
const result = await ensureFileTaskController(home, { environment: process.env });
|
|
469
|
+
emit("Controller is ready.", false, result);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (method === "live-identity" && args.length === 2) {
|
|
473
|
+
try {
|
|
474
|
+
const identity = await callController(home, "controller.identity", {});
|
|
475
|
+
emit("", false, identity);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
if (!(error instanceof ControllerClientError))
|
|
479
|
+
throw error;
|
|
480
|
+
process.stderr.write(`${renderCliFailure(describeCliFailure(error, failureContext()), jsonOutput, error.code)}\n`);
|
|
481
|
+
process.exitCode = 5;
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (method === "identity" && args.length === 2) {
|
|
486
|
+
// The receipt survives a Controller stop. If it has never been written,
|
|
487
|
+
// ask the authenticated live Controller for the same current identity.
|
|
488
|
+
const receipt = readRuntimeIdentity(home);
|
|
489
|
+
if (receipt !== null) {
|
|
490
|
+
emit("", false, receipt);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const identity = await callController(home, "controller.identity", {});
|
|
495
|
+
emit("", false, identity);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
// Preserve the Controller protocol code for the synchronous update
|
|
499
|
+
// lifecycle owner. A generic RUNTIME_ERROR would erase the only
|
|
500
|
+
// definitive CONTROLLER_NOT_RUNNING proof and force an unnecessary
|
|
501
|
+
// unknown-active block.
|
|
502
|
+
if (!(error instanceof ControllerClientError))
|
|
503
|
+
throw error;
|
|
504
|
+
process.stderr.write(`${renderCliFailure(describeCliFailure(error, failureContext()), jsonOutput, error.code)}\n`);
|
|
505
|
+
process.exitCode = 5;
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (method === "status") {
|
|
510
|
+
const options = parseControllerStatusOptions(args.slice(2));
|
|
511
|
+
const snapshot = await scanControllerResourceInventory({
|
|
512
|
+
currentHome: home,
|
|
513
|
+
scope: options.scope,
|
|
514
|
+
environment: process.env
|
|
515
|
+
});
|
|
516
|
+
// Status always includes identity/health. Missing producers stay
|
|
517
|
+
// `unsupported`; storage contradictions retain the nonzero health exit.
|
|
518
|
+
const cliEntry = fileURLToPath(cliEntryUrl);
|
|
519
|
+
const packageRoot = resolve(cliEntry, "..", "..");
|
|
520
|
+
const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(packageRoot, cliEntry, process.env));
|
|
521
|
+
const storage = collectStorageIdentity(home);
|
|
522
|
+
const droppedEvents = countDroppedInboxEvents(home);
|
|
523
|
+
let runtime;
|
|
524
|
+
try {
|
|
525
|
+
const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
|
|
526
|
+
runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
runtime = { source: "unsupported", droppedEvents };
|
|
530
|
+
}
|
|
531
|
+
const mismatch = summarizeDurablePhysicalMismatch(snapshot);
|
|
532
|
+
const identitySection = renderRuntimeIdentitySection({
|
|
533
|
+
build,
|
|
534
|
+
storage,
|
|
535
|
+
runtime,
|
|
536
|
+
mismatch,
|
|
537
|
+
inventoryRssBytes: snapshot.summary.rssBytes
|
|
538
|
+
});
|
|
539
|
+
emit(`${renderControllerResourceStatus(snapshot, options.verbose)}\n\n${identitySection}`, false, { ...snapshot, identity: { build, storage, runtime, mismatch } });
|
|
540
|
+
if (evaluateStorageHealth(storage).status === "fail")
|
|
541
|
+
process.exitCode = 5;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (method === "cleanup") {
|
|
545
|
+
if (jsonOutput)
|
|
546
|
+
throw usageError("Controller cleanup does not support --json.");
|
|
547
|
+
const options = parseControllerCleanupOptions(args.slice(2));
|
|
548
|
+
const readline = createInterface({
|
|
549
|
+
input: process.stdin,
|
|
550
|
+
output: process.stdout
|
|
551
|
+
});
|
|
552
|
+
try {
|
|
553
|
+
const result = await runInteractiveControllerCleanup({
|
|
554
|
+
io: {
|
|
555
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
556
|
+
write: (value) => process.stdout.write(value),
|
|
557
|
+
question: async (prompt) => readline.question(prompt)
|
|
558
|
+
},
|
|
559
|
+
scan: () => scanControllerResourceInventory({
|
|
560
|
+
currentHome: home,
|
|
561
|
+
scope: options.scope,
|
|
562
|
+
environment: process.env
|
|
563
|
+
}),
|
|
564
|
+
clean: (resource) => cleanControllerResource(resource, {
|
|
565
|
+
environment: process.env
|
|
566
|
+
})
|
|
567
|
+
});
|
|
568
|
+
if (result.data.failed.length > 0 || result.data.skipped.length > 0) {
|
|
569
|
+
process.exitCode = 5;
|
|
570
|
+
}
|
|
571
|
+
emit(result.output, false, result.data);
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
readline.close();
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if ((method !== "stop" && method !== "restart") || args.length !== 2) {
|
|
579
|
+
throw usageError("Controller usage: yui controller status [--all] [--verbose] | "
|
|
580
|
+
+ "cleanup [--all] | start | stop | restart.");
|
|
581
|
+
}
|
|
582
|
+
// Stopping an exactly identified Controller is a recovery operation:
|
|
583
|
+
// malformed domain records must not prevent quiescing its process.
|
|
584
|
+
if (method === "restart")
|
|
585
|
+
validateCurrentTaskStore(home);
|
|
586
|
+
const controllerMethod = method;
|
|
587
|
+
const updateHandoverOwner = process.env.YUI_UPDATE_HANDOVER_OWNER_PID;
|
|
588
|
+
// Maintenance children must explicitly name their exact lock owner.
|
|
589
|
+
const updateHandoverOwnerPid = updateHandoverOwner === undefined
|
|
590
|
+
? undefined
|
|
591
|
+
: Number(updateHandoverOwner);
|
|
592
|
+
if (updateHandoverOwnerPid !== undefined
|
|
593
|
+
&& (!Number.isSafeInteger(updateHandoverOwnerPid) || updateHandoverOwnerPid < 1)) {
|
|
594
|
+
throw runtimeError("Update Controller handover owner PID is invalid.");
|
|
595
|
+
}
|
|
596
|
+
const controllerOptions = {
|
|
597
|
+
environment: process.env,
|
|
598
|
+
...(updateHandoverOwnerPid === undefined ? {} : { handoverOwnerPid: updateHandoverOwnerPid })
|
|
599
|
+
};
|
|
600
|
+
const result = controllerMethod === "restart"
|
|
601
|
+
? await restartFileTaskController(home, controllerOptions)
|
|
602
|
+
: await stopFileTaskController(home, controllerOptions);
|
|
603
|
+
// The update lifecycle needs the authenticated replacement PID returned by
|
|
604
|
+
// restart/readiness. Keep stop's long-standing text envelope, while
|
|
605
|
+
// exposing restart's structured result alongside its human output.
|
|
606
|
+
emit(renderControllerResult(controllerMethod, result), false, controllerMethod === "restart" ? result : undefined);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (args[0] === "resources") {
|
|
610
|
+
await assertFileTaskControllerStorageCompatible(home);
|
|
611
|
+
const resourcesStore = openCurrentTaskStore(home);
|
|
612
|
+
const result = await runResourcesCommand(args.slice(1), resourcesStore);
|
|
613
|
+
emit(result.output, false, result.data);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (!operatorOfflineCommand(args))
|
|
617
|
+
await assertFileTaskControllerStorageCompatible(home);
|
|
618
|
+
// Reuse the store the exact runtime preflight already opened and read for
|
|
619
|
+
// this same Home. Opening a second store would parse the unchanged large
|
|
620
|
+
// state a second time; the per-instance fingerprint cache still invalidates
|
|
621
|
+
// on an external writer, and the storage lock + revision CAS are unchanged.
|
|
622
|
+
const store = verifiedStore !== undefined
|
|
623
|
+
&& resolve(verifiedStore.rootDirectory()) === resolve(home)
|
|
624
|
+
? verifiedStore
|
|
625
|
+
: openCurrentTaskStore(home);
|
|
626
|
+
const catalogs = new AgentConfigurationCatalogService(home, {
|
|
627
|
+
environment: process.env
|
|
628
|
+
});
|
|
629
|
+
const resolved = await resolveTerminalArguments(args, invocation.node, store, catalogs);
|
|
630
|
+
if (resolved === null) {
|
|
631
|
+
emit("Cancelled.");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
assertTaskInvocationScope(resolved, process.env);
|
|
635
|
+
if (resolved[0] === "task" && resolved[1] === "role" && resolved[2] === "capabilities") {
|
|
636
|
+
const query = parseTaskAgentCapabilityQuery(resolved.slice(3));
|
|
637
|
+
const result = await callController(home, "task.role-capabilities", {
|
|
638
|
+
...query, reader: capabilityReaderIdentity(process.env)
|
|
639
|
+
}, { timeoutMs: 15_000 });
|
|
640
|
+
emit(`Configuration: ${result.context.selection} ${result.context.taskId}/${result.context.roleName}\n`
|
|
641
|
+
+ "Discovery uses the Controller's current native account environment; settings files and secret values are not historical snapshots.\n"
|
|
642
|
+
+ renderAgentConfigurationCatalog(result), false, result);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const validateAgentConfiguration = await preflightAgentConfigurationMutation(resolved, store, catalogs);
|
|
646
|
+
const executor = new NodeCommandExecutor();
|
|
647
|
+
const tmux = new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), executor, {
|
|
648
|
+
yuiHome: home,
|
|
649
|
+
historyLimit: resolveTmuxHistoryLimit(store.getConfig().tmuxHistoryLimit),
|
|
650
|
+
terminalInput: process.stdin,
|
|
651
|
+
onWarning: (message) => writeCliWarning(message)
|
|
652
|
+
});
|
|
653
|
+
const telemetry = openSchedulerTelemetry(home, store.getConfig());
|
|
654
|
+
try {
|
|
655
|
+
const schedulerStore = new FileSchedulerStoreAdapter(store, telemetry);
|
|
656
|
+
const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
|
|
657
|
+
const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
|
|
658
|
+
const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
|
|
659
|
+
environment: process.env,
|
|
660
|
+
onError: (error) => {
|
|
661
|
+
// Advisory async failure: preserve the foreground command's exit status.
|
|
662
|
+
const failure = describeCliFailure(error, failureContext());
|
|
663
|
+
process.stderr.write(`${renderCliFailure({
|
|
664
|
+
...failure, details: { ...failure.details, advisory: true }
|
|
665
|
+
}, jsonOutput)}\n`);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
const workspaceCoordinator = new TaskWorkspaceCoordinator(store, workspacePreparer, runtime);
|
|
669
|
+
if (resolved[0] === "web") {
|
|
670
|
+
if (managedInvocation || process.env.YUI_ROLE || process.env.YUI_NATIVE_SESSION_ID) {
|
|
671
|
+
throw usageError("The Web user ingress must be started from a local user terminal, not a managed Session.");
|
|
672
|
+
}
|
|
673
|
+
if (resolved.length === 2 && (resolved[1] === "--status" || resolved[1] === "--stop")) {
|
|
674
|
+
const status = await callController(home, "web.status", {});
|
|
675
|
+
if (resolved[1] === "--status")
|
|
676
|
+
emit(status ? `Yui web control room: ${status.url}\n` : "Web is not running.\n", false, status);
|
|
677
|
+
else {
|
|
678
|
+
if (status)
|
|
679
|
+
await callController(home, "web.stop", { id: status.id });
|
|
680
|
+
emit("Web listener stopped; Controller and Agents are unchanged.\n");
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (jsonOutput)
|
|
685
|
+
throw usageError("Web start does not support --json; use --status.");
|
|
686
|
+
const options = parseWebCommandOptions(resolved.slice(1));
|
|
687
|
+
const id = randomUUID();
|
|
688
|
+
await callController(home, "web.start", { ...options, id });
|
|
689
|
+
const displayHost = options.host === "::1" ? "[::1]" : options.host;
|
|
690
|
+
process.stdout.write(`Yui web control room: http://${displayHost}:${options.port}\n`);
|
|
691
|
+
await new Promise((resolve) => {
|
|
692
|
+
const stop = () => {
|
|
693
|
+
process.off("SIGINT", stop);
|
|
694
|
+
process.off("SIGTERM", stop);
|
|
695
|
+
resolve();
|
|
696
|
+
};
|
|
697
|
+
process.once("SIGINT", stop);
|
|
698
|
+
process.once("SIGTERM", stop);
|
|
699
|
+
});
|
|
700
|
+
await callController(home, "web.stop", { id });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (resolved[0] === "config") {
|
|
704
|
+
const domain = resolved[1];
|
|
705
|
+
const roleOptions = {
|
|
706
|
+
yuiHome: home,
|
|
707
|
+
env: process.env
|
|
708
|
+
};
|
|
709
|
+
if (domain === "show") {
|
|
710
|
+
const result = runConfigOverview(resolved.slice(2), store, process.env, roleOptions);
|
|
711
|
+
emit(result.output, false, result.data);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (CONFIG_DOMAINS.includes(domain ?? "")) {
|
|
715
|
+
const configDomain = domain;
|
|
716
|
+
const domainArgs = resolved.slice(2);
|
|
717
|
+
const result = runConfigCommand(configDomain, domainArgs, store);
|
|
718
|
+
if (domainArgs[0] === "set"
|
|
719
|
+
&& domainArgs[1] === "reconciliation-interval-seconds") {
|
|
720
|
+
const refresh = await refreshRunningFileTaskControllerConfiguration(home, { environment: process.env });
|
|
721
|
+
emit(withControllerRefreshWarning(result.output, refresh, "Controller configuration"));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
emit(result.output, false, result.data);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (domain === "agent") {
|
|
728
|
+
const agentArgs = resolved.slice(2);
|
|
729
|
+
if (agentArgs[0] === "capabilities") {
|
|
730
|
+
if (agentArgs.length !== 2 && !(agentArgs.length === 3 && agentArgs[2] === "--refresh")) {
|
|
731
|
+
throw usageError("Agent capabilities usage: yui config agent capabilities <agent-id> [--refresh]");
|
|
732
|
+
}
|
|
733
|
+
const agent = store.getConfiguredAgent(agentArgs[1] ?? "");
|
|
734
|
+
if (agent === null)
|
|
735
|
+
throw agentNotFound(agentArgs[1] ?? "");
|
|
736
|
+
const result = await catalogs.resolve({
|
|
737
|
+
agent,
|
|
738
|
+
cwd: store.getConfig().defaultWorkspace ?? process.cwd(),
|
|
739
|
+
refresh: agentArgs[2] === "--refresh"
|
|
740
|
+
});
|
|
741
|
+
emit(renderAgentConfigurationCatalog(result), false, result);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const affectedAgentId = agentArgs[1];
|
|
745
|
+
const previousAgent = typeof affectedAgentId === "string"
|
|
746
|
+
? store.getConfiguredAgent(affectedAgentId)
|
|
747
|
+
: null;
|
|
748
|
+
const output = runAgentCommand(agentArgs, store);
|
|
749
|
+
if (agentArgs[0] === "add"
|
|
750
|
+
|| agentArgs[0] === "update"
|
|
751
|
+
|| agentArgs[0] === "remove") {
|
|
752
|
+
const currentAgent = typeof affectedAgentId === "string"
|
|
753
|
+
? store.getConfiguredAgent(affectedAgentId)
|
|
754
|
+
: null;
|
|
755
|
+
const capabilityNotice = currentAgent !== null
|
|
756
|
+
&& (agentArgs[0] === "add" || agentArgs[0] === "update")
|
|
757
|
+
? renderAgentConfigurationResolutionNotice(await catalogs.resolve({
|
|
758
|
+
agent: currentAgent,
|
|
759
|
+
cwd: store.getConfig().defaultWorkspace ?? process.cwd()
|
|
760
|
+
}))
|
|
761
|
+
: "";
|
|
762
|
+
const scope = agentEnvironmentRefreshScope(previousAgent, currentAgent, store.listConfiguredAgents());
|
|
763
|
+
const refresh = await refreshRunningFileTaskControllerEnvironment(home, store, process.env, scope);
|
|
764
|
+
emit(withControllerRefreshWarning(`${output.trimEnd()}${capabilityNotice.length === 0 ? "\n" : `\n${capabilityNotice}`}`, refresh, "Agent environment"));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
emit(output);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (domain === "profile") {
|
|
771
|
+
const result = runProfileCommand(resolved.slice(2), store, () => new Date(), validateAgentConfiguration === undefined ? {} : { validateAgentConfiguration });
|
|
772
|
+
emit(result.output, false, result.data);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (domain === "role") {
|
|
776
|
+
const result = runGlobalRoleCommand(resolved.slice(2), store, roleOptions);
|
|
777
|
+
if (typeof result !== "string") {
|
|
778
|
+
throw new Error("Config Role commands cannot enter a runtime Session.");
|
|
779
|
+
}
|
|
780
|
+
emit(result);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
throw usageError(`Unknown configuration domain: ${domain ?? ""}.`);
|
|
784
|
+
}
|
|
785
|
+
if (resolved[0] === "project") {
|
|
786
|
+
const result = await runProjectCommand(resolved.slice(1), store, { environment: process.env });
|
|
787
|
+
emit(result.output, false, result.data);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (resolved[0] === "session") {
|
|
791
|
+
if (resolved[1] === "stop") {
|
|
792
|
+
const options = parseSessionStopOptions(resolved.slice(2));
|
|
793
|
+
const result = await runSessionStopCommand({
|
|
794
|
+
options,
|
|
795
|
+
runtime: {
|
|
796
|
+
beginMaintenance: () => acquireHandoverLock(home),
|
|
797
|
+
snapshot: () => ({
|
|
798
|
+
candidates: schedulerStore.listRuntimeSessionCandidates(),
|
|
799
|
+
dormant: schedulerStore.listDormantRuntimeOwners()
|
|
800
|
+
}),
|
|
801
|
+
drainController: () => runtime.drainController(),
|
|
802
|
+
stopController: () => stopFileTaskController(home, {
|
|
803
|
+
environment: process.env
|
|
804
|
+
}),
|
|
805
|
+
startController: async () => {
|
|
806
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
807
|
+
},
|
|
808
|
+
stopDormantSession: (candidate) => runtime.stopDormantSession(candidate)
|
|
809
|
+
},
|
|
810
|
+
environment: process.env
|
|
811
|
+
});
|
|
812
|
+
process.exitCode = result.exitCode;
|
|
813
|
+
emit(result.output, false, result.data);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const roleOptions = {
|
|
817
|
+
yuiHome: home,
|
|
818
|
+
env: process.env,
|
|
819
|
+
jsonOutput
|
|
820
|
+
};
|
|
821
|
+
const sessionArgs = resolved[1] === "enter" || resolved[1] === "context"
|
|
822
|
+
? [resolved[1], ...resolved.slice(2)]
|
|
823
|
+
: ["session", resolved[1] ?? "", ...resolved.slice(2)];
|
|
824
|
+
const result = runGlobalRoleCommand(sessionArgs, store, roleOptions);
|
|
825
|
+
if (typeof result !== "string" && result.kind === "output") {
|
|
826
|
+
emit(result.output, false, result.data);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (typeof result === "string") {
|
|
830
|
+
emit(result);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
// The session surface (record/replace/enter/context) only ever yields the
|
|
834
|
+
// enter control; the live input actions live under the top-level `role`
|
|
835
|
+
// command below, never here.
|
|
836
|
+
if (result.kind !== "enter") {
|
|
837
|
+
throw new Error("Session commands cannot perform a live input control.");
|
|
838
|
+
}
|
|
839
|
+
await prepareInteractiveCodexApp(result.role.activeAgentId, store, home);
|
|
840
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
841
|
+
await runtime.prepareGlobalRoleEnter(result.role.name);
|
|
842
|
+
tmux.attachRole("operator", result.role.name, "auto");
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (resolved[0] === "role") {
|
|
846
|
+
// decision-3 §7 CLI grammar: the durable Global input actions are top-level
|
|
847
|
+
// `role message queue|steer <global-role> …` and `role interrupt
|
|
848
|
+
// <global-role> …`, distinct from `config role …` (desired configuration)
|
|
849
|
+
// and `session …` (native session lifecycle). Core persists the durable
|
|
850
|
+
// Global-owned Message, proves owner/target/capability/writer-fence from
|
|
851
|
+
// durable state, and returns either a string disposition (queued,
|
|
852
|
+
// idempotent-replay, or an explicit not-steered/not-interrupted failure with
|
|
853
|
+
// its exact code) or a resolved live intent. The CLI performs at most one
|
|
854
|
+
// native edge with scope "global" and taskId omitted — the same shared
|
|
855
|
+
// resolver and transport as a Task Role, never a fabricated Task.
|
|
856
|
+
let globalInputFailure;
|
|
857
|
+
const roleOptions = {
|
|
858
|
+
yuiHome: home,
|
|
859
|
+
env: process.env,
|
|
860
|
+
jsonOutput,
|
|
861
|
+
onInputFailure: failure => { globalInputFailure = failure; }
|
|
862
|
+
};
|
|
863
|
+
const result = runGlobalRoleCommand(resolved.slice(1), store, roleOptions);
|
|
864
|
+
if (resolved[1] === "message" && resolved[2] === "queue" && resolved[3] !== undefined) {
|
|
865
|
+
await callController(home, "scheduler.signal", {
|
|
866
|
+
key: `global-role:${encodeURIComponent(resolved[3])}`
|
|
867
|
+
}).catch(() => { });
|
|
868
|
+
}
|
|
869
|
+
if (typeof result === "string") {
|
|
870
|
+
if (globalInputFailure !== undefined) {
|
|
871
|
+
emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
emit(result);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (result.kind === "output") {
|
|
878
|
+
if (globalInputFailure !== undefined) {
|
|
879
|
+
emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
|
|
880
|
+
}
|
|
881
|
+
else
|
|
882
|
+
emit(result.output, false, result.data);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (result.kind === "input-steer") {
|
|
886
|
+
// Core already persisted the durable Global Message and proved target +
|
|
887
|
+
// capability + writer fence from the Global Role's own Session set. This
|
|
888
|
+
// is the single live edge: one native steer of the exact current Turn,
|
|
889
|
+
// scope "global", no retarget and no fallback to interrupt or queue.
|
|
890
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
891
|
+
let control;
|
|
892
|
+
try {
|
|
893
|
+
control = await sendAgentHostSteerControl({
|
|
894
|
+
home,
|
|
895
|
+
scope: "global",
|
|
896
|
+
roleName: result.roleName,
|
|
897
|
+
control: {
|
|
898
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
899
|
+
type: "steer-turn",
|
|
900
|
+
nativeSessionId: result.target.nativeSessionId,
|
|
901
|
+
nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
|
|
902
|
+
authority: result.target.authority,
|
|
903
|
+
run: { attemptId: result.receiptId, boundedText: result.text }
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
recordGlobalSteerResult(store, result.roleName, result.messageId, {
|
|
909
|
+
state: "steer-unknown", outcome: "pending",
|
|
910
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
911
|
+
});
|
|
912
|
+
throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
|
|
913
|
+
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
914
|
+
+ "The Message is retained and its outcome is recorded from the Host; whether the "
|
|
915
|
+
+ "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
|
|
916
|
+
+ "do not reissue the same input under a new requestId or a different action.");
|
|
917
|
+
}
|
|
918
|
+
const steer = foldSteerLiveReceipt(control);
|
|
919
|
+
recordGlobalSteerResult(store, result.roleName, result.messageId, steer);
|
|
920
|
+
if (steer.state !== "steered") {
|
|
921
|
+
emitControlFailure(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_NOT_DELIVERED", { roleName: result.roleName, messageId: result.messageId, steer });
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
emit(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), false, {
|
|
925
|
+
roleName: result.roleName,
|
|
926
|
+
messageId: result.messageId, steer
|
|
927
|
+
});
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (result.kind === "input-interrupt") {
|
|
931
|
+
// The single live edge for a Global interrupt: one native cancel of the
|
|
932
|
+
// exact current Turn, scope "global". Never a kill/restart/detach. Any
|
|
933
|
+
// then-handoff was already claimed durably by Core (an existing Global
|
|
934
|
+
// Message owned by this Role) and is delivered once by the ordinary
|
|
935
|
+
// continuation path after this Turn reaches a proven terminal.
|
|
936
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
937
|
+
let control;
|
|
938
|
+
try {
|
|
939
|
+
control = await sendAgentHostCancelControl({
|
|
940
|
+
home,
|
|
941
|
+
scope: "global",
|
|
942
|
+
roleName: result.roleName,
|
|
943
|
+
control: {
|
|
944
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
945
|
+
type: "cancel",
|
|
946
|
+
nativeOnly: true,
|
|
947
|
+
nativeSessionId: result.target.nativeSessionId,
|
|
948
|
+
attemptId: result.target.attemptId,
|
|
949
|
+
authority: result.target.authority
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
recordGlobalInterruptResult(store, result.roleName, result.receiptId, {
|
|
955
|
+
state: "interrupt-unknown", outcome: "cancel-requested",
|
|
956
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
957
|
+
});
|
|
958
|
+
throw runtimeError(`Interrupt of global role ${result.roleName} did not complete: `
|
|
959
|
+
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
960
|
+
+ "No process was killed; re-read the Session before retrying.");
|
|
961
|
+
}
|
|
962
|
+
const interrupt = foldInterruptLiveReceipt(control);
|
|
963
|
+
recordGlobalInterruptResult(store, result.roleName, result.receiptId, interrupt);
|
|
964
|
+
if (interrupt.state !== "interrupt-requested") {
|
|
965
|
+
emitControlFailure(interruptReceiptOutput(result.output, result.roleName, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN" : "INTERRUPT_NOT_DELIVERED", { roleName: result.roleName, interrupt });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
emit(interruptReceiptOutput(result.output, result.roleName, interrupt), false, {
|
|
969
|
+
roleName: result.roleName,
|
|
970
|
+
...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
|
|
971
|
+
interrupt
|
|
972
|
+
});
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (result.kind !== "enter") {
|
|
976
|
+
throw new Error("Role command returned an invalid control result.");
|
|
977
|
+
}
|
|
978
|
+
// A top-level `role` command never enters a runtime Session; that is
|
|
979
|
+
// `session enter`.
|
|
980
|
+
throw usageError("Use 'yui session enter <role>' to attach to a Global Role.");
|
|
981
|
+
}
|
|
982
|
+
if (resolved[0] === "operator") {
|
|
983
|
+
if (resolved[1] === "enter") {
|
|
984
|
+
if (resolved.length !== 2)
|
|
985
|
+
throw usageError("Operator enter usage: yui operator enter.");
|
|
986
|
+
const role = store.getGlobalRole("operator");
|
|
987
|
+
if (role !== null)
|
|
988
|
+
await prepareInteractiveCodexApp(role.activeAgentId, store, home);
|
|
989
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
990
|
+
await runtime.prepareGlobalRoleEnter("operator");
|
|
991
|
+
tmux.attachRole("operator", "operator", "auto");
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
|
|
995
|
+
if (result.kind === "output") {
|
|
996
|
+
emit(result.output, false, result.data);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (result.kind !== "session") {
|
|
1000
|
+
throw new Error("Operator command returned an invalid control result.");
|
|
1001
|
+
}
|
|
1002
|
+
await executeOperatorSessionControl(result, home, store, runtime, tmux, catalogs);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (resolved[0] === "task") {
|
|
1006
|
+
if (resolved[1] === "archive-preflight") {
|
|
1007
|
+
const request = parseTaskArchiveArguments(resolved.slice(2), "archive-preflight");
|
|
1008
|
+
if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== request.taskId) {
|
|
1009
|
+
throw usageError("Archive inspection must remain within this Session's Task.");
|
|
1010
|
+
}
|
|
1011
|
+
const data = await inspectTaskArchive(workspaceCoordinator, request);
|
|
1012
|
+
emit(renderTaskArchivePreflight(data), false, data);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (resolved[1] === "artifact") {
|
|
1016
|
+
// File/directory artifacts live in the Task's local Git repository, so
|
|
1017
|
+
// their save/read/list are asynchronous and handled here rather than in
|
|
1018
|
+
// the synchronous runTaskCommand chain. Save commits exactly one path and
|
|
1019
|
+
// returns a self-certifying commit-pinned reference; read pins to a commit
|
|
1020
|
+
// for frozen evidence; list is an ordinary current read.
|
|
1021
|
+
const action = resolved[2];
|
|
1022
|
+
const taskId = resolved[3];
|
|
1023
|
+
const usage = "Usage: yui task artifact list <task> | read <task> <relative-path> [<commit>] | "
|
|
1024
|
+
+ "save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]";
|
|
1025
|
+
if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
|
|
1026
|
+
throw usageError(usage);
|
|
1027
|
+
}
|
|
1028
|
+
if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
|
|
1029
|
+
throw usageError("Artifact is outside the managed Task scope.");
|
|
1030
|
+
}
|
|
1031
|
+
if (store.getTask(taskId) === null)
|
|
1032
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
1033
|
+
let data;
|
|
1034
|
+
if (action === "list") {
|
|
1035
|
+
if (resolved.length !== 4)
|
|
1036
|
+
throw usageError(usage);
|
|
1037
|
+
data = await listArtifactsCapability(home, taskId);
|
|
1038
|
+
}
|
|
1039
|
+
else if (action === "read") {
|
|
1040
|
+
const relativePath = resolved[4];
|
|
1041
|
+
const commit = resolved[5];
|
|
1042
|
+
if (relativePath === undefined || resolved.length > 6)
|
|
1043
|
+
throw usageError(usage);
|
|
1044
|
+
data = await readArtifactCapability(home, taskId, {
|
|
1045
|
+
relativePath, ...(commit === undefined ? {} : { commit })
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
// save: a delivery-authoritative action; a managed Task caller must be the current Leader.
|
|
1050
|
+
taskLocalActor(store, process.env, taskId);
|
|
1051
|
+
const relativePath = resolved[4];
|
|
1052
|
+
const content = resolved[5];
|
|
1053
|
+
if (relativePath === undefined || content === undefined)
|
|
1054
|
+
throw usageError(usage);
|
|
1055
|
+
const rest = resolved.slice(6);
|
|
1056
|
+
let message;
|
|
1057
|
+
let expectedHead;
|
|
1058
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1059
|
+
const value = rest[index + 1];
|
|
1060
|
+
if (rest[index] === "--message" && value !== undefined) {
|
|
1061
|
+
message = value;
|
|
1062
|
+
index += 1;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (rest[index] === "--expected-head" && value !== undefined) {
|
|
1066
|
+
expectedHead = value;
|
|
1067
|
+
index += 1;
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
throw usageError(usage);
|
|
1071
|
+
}
|
|
1072
|
+
data = await saveArtifactCapability(home, taskId, {
|
|
1073
|
+
relativePath, content,
|
|
1074
|
+
...(message === undefined ? {} : { message }),
|
|
1075
|
+
...(expectedHead === undefined ? {} : { expectedHead })
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
emit(JSON.stringify(data, null, 2), false, data);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (resolved[1] === "execution") {
|
|
1082
|
+
if (resolved[2] === "stop") {
|
|
1083
|
+
const request = parseTaskExecutionStopRequest(resolved.slice(3));
|
|
1084
|
+
const result = stopTaskExecutionCommand(request, store, { environment: process.env });
|
|
1085
|
+
try {
|
|
1086
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
1087
|
+
await runtime.stopTaskDurableJobs(result.taskId);
|
|
1088
|
+
await runtime.stopTaskRoleSessions(result.taskId, result.roleNames);
|
|
1089
|
+
await runtime.assertTaskPhysicalResourcesReleased(result.taskId);
|
|
1090
|
+
finalizeStoppedTaskExecution(result.taskId, store);
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
throw runtimeError("Task execution is stopped and durable progress is preserved, but physical runtime cleanup failed.", { cause: error });
|
|
1094
|
+
}
|
|
1095
|
+
emit(result.output, false, result);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (resolved[2] === "start") {
|
|
1099
|
+
const taskId = parseTaskExecutionStartRequest(resolved.slice(3));
|
|
1100
|
+
const task = store.getTask(taskId);
|
|
1101
|
+
if (task === null)
|
|
1102
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
1103
|
+
// Reject managed Task callers before inspecting or starting runtime resources.
|
|
1104
|
+
if (taskLocalActor(store, process.env, taskId) === "leader") {
|
|
1105
|
+
throw usageError("Task execution stop/start requires the global Operator or a human user.");
|
|
1106
|
+
}
|
|
1107
|
+
if (task.executionGate.state === "stopped") {
|
|
1108
|
+
await runtime.assertTaskPhysicalResourcesReleased(taskId);
|
|
1109
|
+
}
|
|
1110
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
1111
|
+
const result = startTaskExecutionCommand(taskId, store, { environment: process.env });
|
|
1112
|
+
// Idempotent start is also a reliable kick: if an earlier caller
|
|
1113
|
+
// committed the gate but lost its Controller acknowledgement, retrying
|
|
1114
|
+
// start re-signals the same durable wake without creating another one.
|
|
1115
|
+
await runtime.notifyMailboxChanged({ kind: "role", taskId, roleName: "leader" });
|
|
1116
|
+
emit(result.output, false, result);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
throw usageError("Task execution usage: yui task execution <stop|start> ...");
|
|
1120
|
+
}
|
|
1121
|
+
if (resolved[1] === "integration") {
|
|
1122
|
+
const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
|
|
1123
|
+
environment: process.env,
|
|
1124
|
+
jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
|
|
1125
|
+
});
|
|
1126
|
+
emit(result.output, false, result.data);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (resolved[1] === "change-set") {
|
|
1130
|
+
const result = await runTaskChangeSetCommand(resolved.slice(2), store);
|
|
1131
|
+
emit(result.output, false, result.data);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (resolved[1] === "publication" && (resolved[2] === "diff" || resolved[2] === "adopt")) {
|
|
1135
|
+
const result = await runTaskPublicationAdoptCommand(resolved.slice(2), store, {
|
|
1136
|
+
environment: process.env
|
|
1137
|
+
});
|
|
1138
|
+
emit(result.output, false, result.data);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (resolved[1] === "publication" && resolved[2] === "verify") {
|
|
1142
|
+
const result = await runTaskPublicationVerifyCommand(resolved.slice(3), store, {
|
|
1143
|
+
verifiers: {
|
|
1144
|
+
github: createGitHubCliPublicationVerifier({
|
|
1145
|
+
environmentPath: process.env.PATH
|
|
1146
|
+
}),
|
|
1147
|
+
gitlab: createGitLabCliPublicationVerifier({
|
|
1148
|
+
environmentPath: process.env.PATH
|
|
1149
|
+
})
|
|
1150
|
+
},
|
|
1151
|
+
environment: process.env
|
|
1152
|
+
});
|
|
1153
|
+
emit(result.output, false, result.data);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (resolved[1] === "overlap") {
|
|
1157
|
+
const result = await runTaskOverlapCommand(resolved.slice(2), store);
|
|
1158
|
+
emit(result.output, false, result.data);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (resolved[1] === "workflow"
|
|
1162
|
+
&& (resolved[2] === "run" || resolved[2] === "resume")) {
|
|
1163
|
+
const result = await runWorkflowCommandAsync(resolved.slice(2), store, {
|
|
1164
|
+
environment: process.env,
|
|
1165
|
+
yuiHome: home,
|
|
1166
|
+
ports: createReleaseWorkflowPorts({
|
|
1167
|
+
home,
|
|
1168
|
+
updatePorts: createUpdatePorts(process.env),
|
|
1169
|
+
projectStore: store
|
|
1170
|
+
})
|
|
1171
|
+
});
|
|
1172
|
+
if (result.kind !== "output") {
|
|
1173
|
+
throw new Error(`Task workflow ${resolved[2]} returned an invalid control result.`);
|
|
1174
|
+
}
|
|
1175
|
+
emit(result.output, false, result.data);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (resolved[1] === "replace") {
|
|
1179
|
+
const result = await runTaskWorkspaceCommand(resolved.slice(1), store, workspacePreparer);
|
|
1180
|
+
emit(result.output, false, result.data);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (resolved[1] === "work" && resolved[2] === "isolate") {
|
|
1184
|
+
const workItemId = resolved[3];
|
|
1185
|
+
if (workItemId === undefined || resolved.length !== 4) {
|
|
1186
|
+
throw usageError("Task work isolate usage: yui task work isolate <task>/<work>.");
|
|
1187
|
+
}
|
|
1188
|
+
const reference = cliWorkItemReference(workItemId, process.env);
|
|
1189
|
+
const workspace = await workspaceCoordinator.isolateWorkItem(reference.taskId, reference.localId);
|
|
1190
|
+
emit(`Created WorkItem workspace for ${reference.taskId}/${reference.localId}\nWorkspace: ${workspace.root}\n`, false, { workItemRef: reference, workspace });
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (resolved[1] === "work" && resolved[2] === "review"
|
|
1194
|
+
&& resolved[3] === "cleanup") {
|
|
1195
|
+
const reviewRoundId = resolved[4];
|
|
1196
|
+
if (reviewRoundId === undefined || resolved.length !== 5) {
|
|
1197
|
+
throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
|
|
1198
|
+
}
|
|
1199
|
+
const reference = cliTaskRecordReference(reviewRoundId, "reviewRound", process.env);
|
|
1200
|
+
const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
|
|
1201
|
+
if (removal === "dirty") {
|
|
1202
|
+
throw usageError(`ReviewRound workspace is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
|
|
1203
|
+
}
|
|
1204
|
+
emit(`Cleaned ReviewRound workspace ${reference.taskId}/${reference.localId} (${removal})\n`, false, { reviewRoundRef: reference, workspace: { removal } });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (resolved[1] === "work" && resolved[2] === "capture") {
|
|
1208
|
+
const workItemId = resolved[3];
|
|
1209
|
+
if (workItemId === undefined || resolved.length !== 4) {
|
|
1210
|
+
throw usageError("Task work capture usage: yui task work capture <task>/<work>.");
|
|
1211
|
+
}
|
|
1212
|
+
const reference = cliWorkItemReference(workItemId, process.env);
|
|
1213
|
+
assertTaskDeliveryAuthority(store, process.env, reference.taskId);
|
|
1214
|
+
const changeSets = await new WorkItemChangeSetManager(store).capture(reference.taskId, reference.localId, taskFinalReviewContract === undefined
|
|
1215
|
+
? {}
|
|
1216
|
+
: { taskFinalReviewContract });
|
|
1217
|
+
const qualified = `${reference.taskId}/${reference.localId}`;
|
|
1218
|
+
emit(changeSets.length === 0
|
|
1219
|
+
? `WorkItem workspace has no changes to capture: ${qualified}\n`
|
|
1220
|
+
: `Captured ChangeSets ${changeSets.map(({ id }) => id).join(", ")} from ${qualified}\n`, false, { workItemRef: reference, changeSets });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (resolved[1] === "work" && resolved[2] === "cleanup") {
|
|
1224
|
+
const workItemId = resolved[3];
|
|
1225
|
+
const disposition = resolved[4];
|
|
1226
|
+
if (workItemId === undefined
|
|
1227
|
+
|| !["--runtime-only", "--integrated", "--abandon"].includes(disposition ?? "")
|
|
1228
|
+
|| resolved.length !== 5) {
|
|
1229
|
+
throw usageError("Task work cleanup usage: yui task work cleanup <task>/<work> "
|
|
1230
|
+
+ "(--runtime-only|--integrated|--abandon).");
|
|
1231
|
+
}
|
|
1232
|
+
const reference = cliWorkItemReference(workItemId, process.env);
|
|
1233
|
+
const qualified = `${reference.taskId}/${reference.localId}`;
|
|
1234
|
+
assertTaskDeliveryAuthority(store, process.env, reference.taskId);
|
|
1235
|
+
if (disposition === "--runtime-only") {
|
|
1236
|
+
let runtimeCleanup;
|
|
1237
|
+
try {
|
|
1238
|
+
runtimeCleanup = await workspaceCoordinator.cleanupWorkItemRuntime(reference.taskId, reference.localId);
|
|
1239
|
+
}
|
|
1240
|
+
catch (error) {
|
|
1241
|
+
throw cleanupCliError(error, `work-item:${qualified}`);
|
|
1242
|
+
}
|
|
1243
|
+
emit(`Released WorkItem runtime ${qualified}; retained its Session and worktree\n`, false, {
|
|
1244
|
+
workItem: store.getWorkItem(reference.taskId, reference.localId),
|
|
1245
|
+
runtime: { cleanup: runtimeCleanup },
|
|
1246
|
+
worktree: { retained: true }
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const cleanedAs = disposition === "--integrated" ? "integrated" : "abandoned";
|
|
1251
|
+
if (cleanedAs === "integrated") {
|
|
1252
|
+
try {
|
|
1253
|
+
await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
throw cleanupCliError(error, `work-item:${qualified}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
let removal;
|
|
1260
|
+
try {
|
|
1261
|
+
removal = await workspaceCoordinator.cleanupWorkItem(reference.taskId, reference.localId, cleanedAs);
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
throw cleanupCliError(error, `work-item:${qualified}`);
|
|
1265
|
+
}
|
|
1266
|
+
if (removal === "dirty") {
|
|
1267
|
+
throw usageError(`WorkItem worktree is dirty and was retained: ${qualified}.`, undefined, cleanupBlockedDetails("dirty-worktree", `work-item:${qualified}`, true));
|
|
1268
|
+
}
|
|
1269
|
+
emit(`Cleaned WorkItem worktree ${qualified} (${cleanedAs})\n`, false, {
|
|
1270
|
+
workItem: store.getWorkItem(reference.taskId, reference.localId),
|
|
1271
|
+
worktree: { removal, disposition: cleanedAs }
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (resolved[1] === "work" && resolved[2] === "review"
|
|
1276
|
+
&& resolved[3] === "cleanup") {
|
|
1277
|
+
const reviewRef = resolved[4];
|
|
1278
|
+
if (reviewRef === undefined || resolved.length !== 5) {
|
|
1279
|
+
throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
|
|
1280
|
+
}
|
|
1281
|
+
const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
|
|
1282
|
+
const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
|
|
1283
|
+
if (removal === "dirty") {
|
|
1284
|
+
throw usageError(`ReviewRound worktree is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
|
|
1285
|
+
}
|
|
1286
|
+
emit(`Cleaned ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
|
|
1287
|
+
reviewRound: store.getReviewRound(reference.taskId, reference.localId),
|
|
1288
|
+
worktree: { removal }
|
|
1289
|
+
});
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (resolved[1] === "work" && resolved[2] === "review"
|
|
1293
|
+
&& resolved[3] === "preserve") {
|
|
1294
|
+
const reviewRef = resolved[4];
|
|
1295
|
+
if (reviewRef === undefined || resolved.length !== 5) {
|
|
1296
|
+
throw usageError("Task work review preserve usage: yui task work review preserve <task>/<review-round>.");
|
|
1297
|
+
}
|
|
1298
|
+
const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
|
|
1299
|
+
const round = preserveReviewRoundWorkspace(reference.taskId, reference.localId, store, { runtime, environment: process.env, yuiHome: home });
|
|
1300
|
+
emit(`Preserved ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
|
|
1301
|
+
reviewRound: round
|
|
1302
|
+
});
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
let archiveRemoteDeliveryProof;
|
|
1306
|
+
let archiveTaskReviewCandidate;
|
|
1307
|
+
if (resolved[1] === "archive") {
|
|
1308
|
+
const request = parseTaskArchiveArguments(resolved.slice(2));
|
|
1309
|
+
if (request.sourceMessage !== undefined) {
|
|
1310
|
+
if (request.force || request.disposition !== "integrated") {
|
|
1311
|
+
throw usageError("Leader ordinary archive does not authorize --force or --abandon.");
|
|
1312
|
+
}
|
|
1313
|
+
const result = await callController(home, "capability.call", {
|
|
1314
|
+
taskId: request.taskId, caller: resolveJobCaller(process.env, request.taskId),
|
|
1315
|
+
request: { name: "task.archive", requestId: request.requestId,
|
|
1316
|
+
input: { taskId: request.taskId, sourceMessage: request.sourceMessage, purpose: request.purpose } }
|
|
1317
|
+
});
|
|
1318
|
+
emit("Controller archive result; inspect task events if this Session ends before the receipt.\n", false, result);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const { taskId, disposition, force } = validateTaskArchiveRequest(resolved.slice(2), store, {
|
|
1322
|
+
runtime,
|
|
1323
|
+
environment: process.env,
|
|
1324
|
+
yuiHome: home
|
|
1325
|
+
});
|
|
1326
|
+
const task = store.getTask(taskId);
|
|
1327
|
+
if (task === null)
|
|
1328
|
+
throw new Error(`Task disappeared after archive validation: ${taskId}.`);
|
|
1329
|
+
if (force || task.status === "archived") {
|
|
1330
|
+
// Archive admission and mandatory audit commit before any fallible
|
|
1331
|
+
// filesystem/provider work. Repeats report facts, never replay cleanup.
|
|
1332
|
+
const admitted = runTaskCommand(resolved.slice(1), store, {
|
|
1333
|
+
runtime, environment: process.env, yuiHome: home
|
|
1334
|
+
});
|
|
1335
|
+
if (force && admitted.kind === "output"
|
|
1336
|
+
&& admitted.data.changed) {
|
|
1337
|
+
await workspaceCoordinator.cleanupArchivedTask(taskId, disposition);
|
|
1338
|
+
}
|
|
1339
|
+
const current = store.getTask(taskId);
|
|
1340
|
+
const archive = taskArchiveDiagnostics(store, current);
|
|
1341
|
+
emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
{
|
|
1345
|
+
if (disposition === "integrated") {
|
|
1346
|
+
archiveTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env);
|
|
1347
|
+
archiveRemoteDeliveryProof = createTaskRemoteDeliveryProof(store, task, archiveTaskReviewCandidate ?? null);
|
|
1348
|
+
assertTaskRemoteDeliveryIntegrated(archiveRemoteDeliveryProof.delivery);
|
|
1349
|
+
}
|
|
1350
|
+
const workItemIds = store.listManagedWorkspaces(task.id)
|
|
1351
|
+
.flatMap(({ owner }) => owner.type === "work-item" ? [owner.workItemId] : []);
|
|
1352
|
+
for (const workItemId of workItemIds) {
|
|
1353
|
+
const item = store.getWorkItem(task.id, workItemId);
|
|
1354
|
+
if (item?.status !== "accepted" || disposition !== "integrated")
|
|
1355
|
+
continue;
|
|
1356
|
+
try {
|
|
1357
|
+
await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
|
|
1364
|
+
if (cleanup.status === "retained-dirty") {
|
|
1365
|
+
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, cleanup.checks));
|
|
1366
|
+
}
|
|
1367
|
+
if (cleanup.status === "failed") {
|
|
1368
|
+
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, cleanup.checks));
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
let taskRetirementProof;
|
|
1373
|
+
if (resolved[1] === "retire") {
|
|
1374
|
+
const taskId = resolved[2];
|
|
1375
|
+
if (taskId !== undefined && !taskId.startsWith("--")) {
|
|
1376
|
+
const task = store.getTask(taskId);
|
|
1377
|
+
if (task?.status === "active" || task?.status === "draft") {
|
|
1378
|
+
try {
|
|
1379
|
+
taskRetirementProof = await new WorkItemChangeSetManager(store)
|
|
1380
|
+
.assertRetirable(taskId);
|
|
1381
|
+
}
|
|
1382
|
+
catch (error) {
|
|
1383
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
assertWorkItemExecutionDependenciesForCommand(resolved, store, process.env);
|
|
1389
|
+
if (resolved[1] === "work" && resolved[2] === "dispatch") {
|
|
1390
|
+
const workItemId = resolved[3];
|
|
1391
|
+
const reference = workItemId === undefined
|
|
1392
|
+
? null
|
|
1393
|
+
: cliWorkItemReference(workItemId, process.env);
|
|
1394
|
+
const item = reference === null
|
|
1395
|
+
? null
|
|
1396
|
+
: store.getWorkItem(reference.taskId, reference.localId);
|
|
1397
|
+
const task = item === null ? null : store.getTask(item.taskId);
|
|
1398
|
+
if (item !== null && task !== null) {
|
|
1399
|
+
// Authority and pure Lane-shape checks precede every physical or
|
|
1400
|
+
// durable workspace preparation performed for dispatch.
|
|
1401
|
+
assertTaskDeliveryAuthority(store, process.env, task.id);
|
|
1402
|
+
readDispatchContextRefs(store, task.id, resolved.flatMap((arg, index) => arg === "--context-ref" ? [resolved[index + 1] ?? ""] : []), process.env);
|
|
1403
|
+
requireWorkItemAssignee(item);
|
|
1404
|
+
workItemDispatchLanePlan(resolved, store, item);
|
|
1405
|
+
}
|
|
1406
|
+
// A rejected Candidate starts a new execution iteration. Release every
|
|
1407
|
+
// terminal Lane Role runtime before preparing the new Lane workspaces;
|
|
1408
|
+
// durable AgentRuns, Groups, Candidates, and workspace owners remain intact.
|
|
1409
|
+
if (item?.status === "open"
|
|
1410
|
+
&& currentWorkItemExecutionGroup(item)?.lanes.every(({ disposition }) => disposition !== "open")) {
|
|
1411
|
+
await workspaceCoordinator.cleanupWorkItemRuntime(item.taskId, item.id);
|
|
1412
|
+
}
|
|
1413
|
+
// Every Task needs an authoritative runtime owner before dispatch. A
|
|
1414
|
+
// Gitless Task uses an empty Task-owned view; Project-backed WorkItems
|
|
1415
|
+
// additionally receive their isolated Develop owner below.
|
|
1416
|
+
if (item !== null && task !== null) {
|
|
1417
|
+
await workspacePreparer.prepareTaskWorkspace(task.id);
|
|
1418
|
+
}
|
|
1419
|
+
// A Project-backed Worker WorkItem gets an isolated Develop owner before
|
|
1420
|
+
// its Lane is prepared. A Leader-owned WorkItem intentionally executes
|
|
1421
|
+
// in the Task main worktree and must not enter this isolation path.
|
|
1422
|
+
if (item !== null
|
|
1423
|
+
&& task !== null
|
|
1424
|
+
&& task.projectBindings.length > 0
|
|
1425
|
+
&& item.assignee !== "leader"
|
|
1426
|
+
&& store.getWorkItemWorkspace(task.id, item.id) === null) {
|
|
1427
|
+
await workspaceCoordinator.isolateWorkItem(item.taskId, item.id);
|
|
1428
|
+
}
|
|
1429
|
+
if (item !== null && task !== null) {
|
|
1430
|
+
// For a new Group the preparer has already created deterministic
|
|
1431
|
+
// worktrees, but the owner record is adopted by dispatch's aggregate
|
|
1432
|
+
// transaction once its exact Lane ids exist.
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
let executionLaneWorkspaces;
|
|
1436
|
+
// Held only for a new Group's dispatch: the per-Project maintenance fence
|
|
1437
|
+
// spans Lane preparation and the adoption transaction, and projectPaths is
|
|
1438
|
+
// the under-fence snapshot the adoption CAS revalidates.
|
|
1439
|
+
let laneDispatchRelease;
|
|
1440
|
+
let laneDispatchProjectPaths;
|
|
1441
|
+
let workItemIntegrationProof;
|
|
1442
|
+
if (resolved[1] === "work" && resolved[2] === "accept") {
|
|
1443
|
+
const workItemId = resolved[3];
|
|
1444
|
+
if (workItemId !== undefined && !workItemId.startsWith("--")) {
|
|
1445
|
+
try {
|
|
1446
|
+
const reference = cliWorkItemReference(workItemId, process.env);
|
|
1447
|
+
workItemIntegrationProof = await new WorkItemChangeSetManager(store)
|
|
1448
|
+
.assertIntegrated(reference.taskId, reference.localId, optionValue(resolved, "--candidate")) ?? undefined;
|
|
1449
|
+
}
|
|
1450
|
+
catch (error) {
|
|
1451
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
let completionSummary;
|
|
1456
|
+
let completionPublishedTreeProof;
|
|
1457
|
+
let completionBaseFreshness;
|
|
1458
|
+
let completionWarnings = [];
|
|
1459
|
+
if (resolved[1] === "base" && resolved[2] === "status") {
|
|
1460
|
+
const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
|
|
1461
|
+
emit(result.output, false, result.data);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (resolved[1] === "upstream") {
|
|
1465
|
+
const result = await runTaskUpstreamCommand(resolved.slice(2), store, home, {
|
|
1466
|
+
environment: process.env,
|
|
1467
|
+
jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
|
|
1468
|
+
});
|
|
1469
|
+
emit(result.output, false, result.data);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (resolved[1] === "complete" && resolved[2] !== undefined) {
|
|
1473
|
+
const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
|
|
1474
|
+
completionSummary = completionRequest.summary;
|
|
1475
|
+
const refreshRemote = completionRequest.refreshRemote;
|
|
1476
|
+
const completion = preflightTaskCompletion(completionRequest.taskId, store, {
|
|
1477
|
+
environment: process.env,
|
|
1478
|
+
...(taskFinalReviewContract === undefined
|
|
1479
|
+
? {}
|
|
1480
|
+
: { taskFinalReviewContract })
|
|
1481
|
+
}, completionRequest);
|
|
1482
|
+
if (!completion.completed && !completion.activeTaskReview) {
|
|
1483
|
+
// An explicit refresh must fetch the remote object graph before the
|
|
1484
|
+
// Publication proof resolves its exact commit. Without the flag the
|
|
1485
|
+
// command remains offline and preserves the existing proof-first path.
|
|
1486
|
+
const refreshedFreshness = refreshRemote
|
|
1487
|
+
? await inspectTaskBaseFreshness(completionRequest.taskId, store, { refresh: true })
|
|
1488
|
+
: undefined;
|
|
1489
|
+
if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
|
|
1490
|
+
completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
|
|
1491
|
+
}
|
|
1492
|
+
completionBaseFreshness = refreshedFreshness
|
|
1493
|
+
?? await inspectTaskBaseFreshness(completionRequest.taskId, store);
|
|
1494
|
+
completionWarnings = assertTaskBaseFreshnessForCompletion(completionBaseFreshness, {
|
|
1495
|
+
...(completionPublishedTreeProof === undefined
|
|
1496
|
+
? {}
|
|
1497
|
+
: {
|
|
1498
|
+
acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
|
|
1499
|
+
})
|
|
1500
|
+
});
|
|
1501
|
+
for (const warning of completionWarnings) {
|
|
1502
|
+
writeCliWarning(warning);
|
|
1503
|
+
}
|
|
1504
|
+
// Refresh observes remote objects only. Choosing and executing an
|
|
1505
|
+
// upstream Integration belongs to its explicit command, never completion.
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
let releaseReviewHandoverLock;
|
|
1509
|
+
if ((resolved[1] === "review"
|
|
1510
|
+
&& ["request", "retry"].includes(resolved[2] ?? ""))
|
|
1511
|
+
|| resolved[1] === "complete") {
|
|
1512
|
+
const handoverLock = acquireHandoverLock(home);
|
|
1513
|
+
releaseReviewHandoverLock = handoverLock.release;
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
const preparedLanes = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
|
|
1517
|
+
if (preparedLanes !== undefined) {
|
|
1518
|
+
executionLaneWorkspaces = preparedLanes.workspaces;
|
|
1519
|
+
laneDispatchRelease = preparedLanes.release;
|
|
1520
|
+
laneDispatchProjectPaths = preparedLanes.projectPaths;
|
|
1521
|
+
}
|
|
1522
|
+
const candidateSnapshots = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
|
|
1523
|
+
const actualTaskReviewCandidate = archiveRemoteDeliveryProof === undefined
|
|
1524
|
+
? await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env)
|
|
1525
|
+
: archiveTaskReviewCandidate;
|
|
1526
|
+
const deltaRecheckPreflight = await deltaRecheckPreflightForTaskCommand(resolved.slice(1), store, actualTaskReviewCandidate);
|
|
1527
|
+
// Read-only Host evidence for Session inspect and Role status/list. The command stays
|
|
1528
|
+
// synchronous over persisted state; this is the live reading it prints
|
|
1529
|
+
// beside those facts, prepared here because the Host is reached over a
|
|
1530
|
+
// socket.
|
|
1531
|
+
const liveHostObservations = await liveHostObservationsForTaskCommand(resolved, store, home);
|
|
1532
|
+
const liveRunConfiguration = runConfigurationForHostObservation(liveHostObservations?.[resolved[5] ?? ""]);
|
|
1533
|
+
// Physical preparation may precede the durable write, but Task status,
|
|
1534
|
+
// workspace identity/cwd, and ManagedWorkspace ownership are adopted by
|
|
1535
|
+
// one transaction. A failed attempt therefore leaves the Task Draft and
|
|
1536
|
+
// owning no writable workspace.
|
|
1537
|
+
let taskWorkspaceActivation;
|
|
1538
|
+
if (resolved[1] === "activate" && resolved.length === 3) {
|
|
1539
|
+
const taskId = resolved[2];
|
|
1540
|
+
const task = taskId === undefined ? null : store.getTask(taskId);
|
|
1541
|
+
if (task !== null && task.status === "draft") {
|
|
1542
|
+
taskLocalActor(store, process.env, task.id);
|
|
1543
|
+
taskWorkspaceActivation = await workspacePreparer.activateTaskWorkspace(task.id, process.env);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const result = runTaskCommand(resolved.slice(1), store, {
|
|
1547
|
+
runtime,
|
|
1548
|
+
environment: process.env,
|
|
1549
|
+
yuiHome: home,
|
|
1550
|
+
...(taskFinalReviewContract === undefined
|
|
1551
|
+
? {}
|
|
1552
|
+
: { taskFinalReviewContract }),
|
|
1553
|
+
...(completionSummary === undefined ? {} : { completionSummary }),
|
|
1554
|
+
...(completionPublishedTreeProof === undefined
|
|
1555
|
+
? {}
|
|
1556
|
+
: { completionPublishedTreeProof }),
|
|
1557
|
+
...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
|
|
1558
|
+
...candidateSnapshots,
|
|
1559
|
+
...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
|
|
1560
|
+
...(taskWorkspaceActivation === undefined ? {} : { taskWorkspaceActivation }),
|
|
1561
|
+
...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
|
|
1562
|
+
...(actualTaskReviewCandidate === undefined
|
|
1563
|
+
? {}
|
|
1564
|
+
: { actualTaskReviewCandidate }),
|
|
1565
|
+
...(archiveRemoteDeliveryProof === undefined
|
|
1566
|
+
? {}
|
|
1567
|
+
: { archiveRemoteDeliveryProof }),
|
|
1568
|
+
...(deltaRecheckPreflight === undefined
|
|
1569
|
+
? {}
|
|
1570
|
+
: { deltaRecheckPreflight }),
|
|
1571
|
+
...(liveRunConfiguration === undefined
|
|
1572
|
+
? {}
|
|
1573
|
+
: { liveRunConfiguration }),
|
|
1574
|
+
...(liveHostObservations === undefined ? {} : { liveHostObservations }),
|
|
1575
|
+
...(taskRetirementProof === undefined ? {} : { taskRetirementProof }),
|
|
1576
|
+
...(validateAgentConfiguration === undefined
|
|
1577
|
+
? {}
|
|
1578
|
+
: { validateAgentConfiguration })
|
|
1579
|
+
});
|
|
1580
|
+
// The dispatch transaction has now adopted (or rejected) the prepared
|
|
1581
|
+
// Lane workspaces. Release the held fence so later output/review
|
|
1582
|
+
// handling can take the per-Project fence itself.
|
|
1583
|
+
if (laneDispatchRelease !== undefined) {
|
|
1584
|
+
laneDispatchRelease();
|
|
1585
|
+
laneDispatchRelease = undefined;
|
|
1586
|
+
}
|
|
1587
|
+
if (result.kind === "output") {
|
|
1588
|
+
const requestedRound = reviewRoundFromCommandData(result.data);
|
|
1589
|
+
const persistedRequestedRound = requestedRound === undefined
|
|
1590
|
+
? null
|
|
1591
|
+
: store.getReviewRound(requestedRound.taskId, requestedRound.id);
|
|
1592
|
+
let reviewOutput = "";
|
|
1593
|
+
let reviewData;
|
|
1594
|
+
const resumesReviewDispatch = (resolved[1] === "review" && resolved[2] === "request")
|
|
1595
|
+
|| (resolved[1] === "work" && resolved[2] === "review")
|
|
1596
|
+
|| (resolved[1] === "run" && resolved[2] === "retry");
|
|
1597
|
+
const reviewDispatchNeeded = requestedRound?.status === "pending"
|
|
1598
|
+
|| (requestedRound?.status === "running"
|
|
1599
|
+
&& resumesReviewDispatch
|
|
1600
|
+
&& persistedRequestedRound?.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
|
|
1601
|
+
&& (lane.currentRunId === undefined
|
|
1602
|
+
|| store.getRun(requestedRound.taskId, lane.currentRunId)?.status === "failed"))) === true);
|
|
1603
|
+
if (reviewDispatchNeeded) {
|
|
1604
|
+
try {
|
|
1605
|
+
const workspace = requestedRound.status === "running"
|
|
1606
|
+
? store.getReviewRoundWorkspace(requestedRound.taskId, requestedRound.id)
|
|
1607
|
+
: await workspacePreparer.prepareReviewRoundWorkspace(requestedRound.taskId, requestedRound.id);
|
|
1608
|
+
if (workspace === null) {
|
|
1609
|
+
throw new Error(`ReviewRound workspace is not ready: ${requestedRound.id}.`);
|
|
1610
|
+
}
|
|
1611
|
+
const reviewLaneWorkspaces = await prepareReviewLaneWorkspaces(requestedRound.taskId, requestedRound.id, store, workspacePreparer);
|
|
1612
|
+
if (reviewLaneWorkspaces !== undefined) {
|
|
1613
|
+
executionLaneWorkspaces = reviewLaneWorkspaces;
|
|
1614
|
+
}
|
|
1615
|
+
const storedRound = store.getReviewRound(requestedRound.taskId, requestedRound.id);
|
|
1616
|
+
const freshTaskCandidate = storedRound?.scope === "task"
|
|
1617
|
+
? await snapshotActualTaskReviewCandidate(requestedRound.taskId, store, workspacePreparer)
|
|
1618
|
+
: undefined;
|
|
1619
|
+
const run = dispatchPreparedReviewRound(requestedRound.taskId, requestedRound.id, store, {
|
|
1620
|
+
runtime,
|
|
1621
|
+
environment: process.env,
|
|
1622
|
+
yuiHome: home,
|
|
1623
|
+
...(taskFinalReviewContract === undefined
|
|
1624
|
+
? {}
|
|
1625
|
+
: { taskFinalReviewContract }),
|
|
1626
|
+
...(freshTaskCandidate === undefined
|
|
1627
|
+
? {}
|
|
1628
|
+
: { actualTaskReviewCandidate: freshTaskCandidate }),
|
|
1629
|
+
...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
|
|
1630
|
+
...(storedRound?.deltaRecheck === undefined
|
|
1631
|
+
|| deltaRecheckPreflight === undefined
|
|
1632
|
+
? {}
|
|
1633
|
+
: {
|
|
1634
|
+
deltaRecheckDiff: deltaRecheckPreflight.diffByProject
|
|
1635
|
+
})
|
|
1636
|
+
});
|
|
1637
|
+
reviewOutput = run === null
|
|
1638
|
+
? `Review ${requestedRound.id} remains running\n`
|
|
1639
|
+
: `Review queued as ${requestedRound.id} (${run.id})\n`;
|
|
1640
|
+
reviewData = {
|
|
1641
|
+
reviewRequest: run === null
|
|
1642
|
+
? {
|
|
1643
|
+
kind: "running",
|
|
1644
|
+
reviewerRoleName: requestedRound.reviewerRoleName,
|
|
1645
|
+
activeReviewRoundId: requestedRound.id,
|
|
1646
|
+
retryable: false
|
|
1647
|
+
}
|
|
1648
|
+
: {
|
|
1649
|
+
kind: "started",
|
|
1650
|
+
reviewerRoleName: requestedRound.reviewerRoleName,
|
|
1651
|
+
reviewRoundId: requestedRound.id,
|
|
1652
|
+
runId: run.id
|
|
1653
|
+
},
|
|
1654
|
+
reviewRound: store.getReviewRound(requestedRound.taskId, requestedRound.id),
|
|
1655
|
+
...(run === null ? {} : { reviewRun: run }),
|
|
1656
|
+
workspace
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
catch (error) {
|
|
1660
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1661
|
+
await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
|
|
1662
|
+
const failed = failPendingReviewRound(requestedRound.taskId, requestedRound.id, message, store, {
|
|
1663
|
+
runtime,
|
|
1664
|
+
environment: process.env,
|
|
1665
|
+
yuiHome: home
|
|
1666
|
+
});
|
|
1667
|
+
reviewOutput = `Review could not start: ${message}\n`
|
|
1668
|
+
+ "The failed ReviewRound was retained for Leader routing.\n";
|
|
1669
|
+
reviewData = {
|
|
1670
|
+
reviewRequest: {
|
|
1671
|
+
kind: "unavailable",
|
|
1672
|
+
reviewerRoleName: requestedRound.reviewerRoleName,
|
|
1673
|
+
reviewRoundId: failed.id,
|
|
1674
|
+
reason: message,
|
|
1675
|
+
retryable: true
|
|
1676
|
+
},
|
|
1677
|
+
reviewRound: failed
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (resolved[1] === "project" && resolved[2] === "add") {
|
|
1682
|
+
const taskId = resolved[3];
|
|
1683
|
+
const task = taskId === undefined ? null : store.getTask(taskId);
|
|
1684
|
+
if (task?.status === "active") {
|
|
1685
|
+
await workspacePreparer.prepareTaskWorkspace(task.id);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
const controlData = result.data;
|
|
1689
|
+
const failureCode = controlData?.steer?.code ?? controlData?.interrupt?.code;
|
|
1690
|
+
if (failureCode !== undefined) {
|
|
1691
|
+
emitControlFailure(result.output, failureCode, result.data);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const commandData = reviewData === undefined
|
|
1695
|
+
? result.data
|
|
1696
|
+
: { command: result.data, ...reviewData };
|
|
1697
|
+
const completionData = resolved[1] === "complete" ? {
|
|
1698
|
+
// Review dispatch may have advanced or failed after the command
|
|
1699
|
+
// prepared its Round. Report that observed stage, not preparation.
|
|
1700
|
+
...(reviewData === undefined ? {} : {
|
|
1701
|
+
stage: reviewRoundFromCommandData(reviewData)?.status === "running"
|
|
1702
|
+
? "review-running"
|
|
1703
|
+
: "review-blocked",
|
|
1704
|
+
projectHeads: result.data.projectHeads
|
|
1705
|
+
}),
|
|
1706
|
+
baseFreshness: completionBaseFreshness ?? null,
|
|
1707
|
+
warnings: completionWarnings
|
|
1708
|
+
} : undefined;
|
|
1709
|
+
emit(`${result.output}${reviewOutput}`, false, completionData === undefined
|
|
1710
|
+
? commandData
|
|
1711
|
+
: { ...commandData, ...completionData });
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (jsonOutput && result.kind !== "session-stop"
|
|
1715
|
+
&& result.kind !== "input-steer" && result.kind !== "input-interrupt") {
|
|
1716
|
+
throw usageError("Task Role view/takeover requires an interactive terminal.");
|
|
1717
|
+
}
|
|
1718
|
+
if (result.kind === "session-stop") {
|
|
1719
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
1720
|
+
try {
|
|
1721
|
+
await runtime.stopExactTaskRoleSession({
|
|
1722
|
+
taskId: result.taskId,
|
|
1723
|
+
roleName: result.roleName,
|
|
1724
|
+
agentId: result.agentId,
|
|
1725
|
+
adapterId: result.adapterId,
|
|
1726
|
+
nativeSessionId: result.nativeSessionId,
|
|
1727
|
+
sessionUpdatedAt: result.sessionUpdatedAt
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
catch (error) {
|
|
1731
|
+
throw runtimeError(`Session stop was requested but physical Host cleanup did not complete: ${error instanceof Error ? error.message : String(error)}`);
|
|
1732
|
+
}
|
|
1733
|
+
emit(result.output, false, {
|
|
1734
|
+
taskId: result.taskId,
|
|
1735
|
+
roleName: result.roleName,
|
|
1736
|
+
stopped: true,
|
|
1737
|
+
reason: result.reason
|
|
1738
|
+
});
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (result.kind === "view") {
|
|
1742
|
+
if (result.output !== undefined)
|
|
1743
|
+
emit(result.output);
|
|
1744
|
+
tmux.attachRole(result.taskId, result.roleName, "read-only");
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (result.kind === "input-steer") {
|
|
1748
|
+
// Core already persisted the Message and proved target + capability +
|
|
1749
|
+
// writer fence. This is the single live edge: one native steer of the
|
|
1750
|
+
// exact current Turn, with no retarget and no fallback to interrupt or
|
|
1751
|
+
// queue. Its durable settlement flows through the steer receipt fold.
|
|
1752
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
1753
|
+
let control;
|
|
1754
|
+
try {
|
|
1755
|
+
control = await sendAgentHostSteerControl({
|
|
1756
|
+
home,
|
|
1757
|
+
scope: "task",
|
|
1758
|
+
taskId: result.taskId,
|
|
1759
|
+
roleName: result.roleName,
|
|
1760
|
+
control: {
|
|
1761
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
1762
|
+
type: "steer-turn",
|
|
1763
|
+
nativeSessionId: result.target.nativeSessionId,
|
|
1764
|
+
nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
|
|
1765
|
+
authority: result.target.authority,
|
|
1766
|
+
run: { attemptId: result.receiptId, boundedText: result.text }
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
catch (error) {
|
|
1771
|
+
throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
|
|
1772
|
+
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
1773
|
+
+ "The Message is retained and its outcome is recorded from the Host; whether the "
|
|
1774
|
+
+ "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
|
|
1775
|
+
+ "do not reissue the same input under a new requestId or a different action.");
|
|
1776
|
+
}
|
|
1777
|
+
const steer = foldSteerLiveReceipt(control);
|
|
1778
|
+
if (steer.state !== "steered") {
|
|
1779
|
+
emitControlFailure(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_REJECTED", { taskId: result.taskId, roleName: result.roleName, messageId: result.messageId, steer });
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
emit(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), false, {
|
|
1783
|
+
taskId: result.taskId, roleName: result.roleName,
|
|
1784
|
+
messageId: result.messageId, steer
|
|
1785
|
+
});
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (result.kind === "input-interrupt") {
|
|
1789
|
+
// The single live edge for interrupt: one native cancel of the exact
|
|
1790
|
+
// current Turn. Never a kill/restart/detach. Any then-handoff was
|
|
1791
|
+
// already claimed durably by Core and is delivered once by the ordinary
|
|
1792
|
+
// continuation path after this Turn reaches a proven terminal.
|
|
1793
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
1794
|
+
let control;
|
|
1795
|
+
try {
|
|
1796
|
+
control = await sendAgentHostCancelControl({
|
|
1797
|
+
home,
|
|
1798
|
+
scope: "task",
|
|
1799
|
+
taskId: result.taskId,
|
|
1800
|
+
roleName: result.roleName,
|
|
1801
|
+
control: {
|
|
1802
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
1803
|
+
type: "cancel",
|
|
1804
|
+
nativeOnly: true,
|
|
1805
|
+
nativeSessionId: result.target.nativeSessionId,
|
|
1806
|
+
// Native cancel names the exact original execution attempt it stops
|
|
1807
|
+
// (Host matches request.attemptId === activeRunAttemptId). That is
|
|
1808
|
+
// distinct from receiptId, the durable identity of this interrupt
|
|
1809
|
+
// control operation — never send the operation id as the turn id.
|
|
1810
|
+
attemptId: result.target.attemptId,
|
|
1811
|
+
authority: result.target.authority
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
catch (error) {
|
|
1816
|
+
recordTaskInterruptResult(store, result.taskId, result.receiptId, { state: "interrupt-unknown", outcome: "cancel-requested" });
|
|
1817
|
+
throw runtimeError(`Interrupt ${result.receiptId} of ${result.taskId}/${result.roleName} did not complete: `
|
|
1818
|
+
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
1819
|
+
+ "No process was killed; re-read the Session before retrying.");
|
|
1820
|
+
}
|
|
1821
|
+
const interrupt = foldInterruptLiveReceipt(control);
|
|
1822
|
+
recordTaskInterruptResult(store, result.taskId, result.receiptId, interrupt);
|
|
1823
|
+
if (interrupt.state !== "interrupt-requested") {
|
|
1824
|
+
emitControlFailure(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN"
|
|
1825
|
+
: interrupt.state === "interrupt-not-active" ? "NO_ACTIVE_TURN" : "INTERRUPT_REJECTED", { taskId: result.taskId, roleName: result.roleName, receiptId: result.receiptId, interrupt });
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
emit(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), false, {
|
|
1829
|
+
taskId: result.taskId, roleName: result.roleName,
|
|
1830
|
+
...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
|
|
1831
|
+
interrupt
|
|
1832
|
+
});
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
const syncAuthority = async (authorityResult) => {
|
|
1836
|
+
let control;
|
|
1837
|
+
try {
|
|
1838
|
+
control = await sendAgentHostAuthorityControl({
|
|
1839
|
+
home,
|
|
1840
|
+
scope: "task",
|
|
1841
|
+
taskId: authorityResult.taskId,
|
|
1842
|
+
roleName: authorityResult.roleName,
|
|
1843
|
+
control: {
|
|
1844
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
1845
|
+
type: "set-authority",
|
|
1846
|
+
nativeSessionId: authorityResult.nativeSessionId,
|
|
1847
|
+
authority: authorityResult.authority
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
catch (error) {
|
|
1852
|
+
throw runtimeError(`Agent Host authority synchronization failed at epoch ${authorityResult.authority.epoch}: `
|
|
1853
|
+
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
1854
|
+
+ `Durable authority is ${authorityResult.authority.owner}-owned; retry `
|
|
1855
|
+
+ `'yui task role release ${authorityResult.taskId} ${authorityResult.roleName}' `
|
|
1856
|
+
+ "to reconcile the Host.");
|
|
1857
|
+
}
|
|
1858
|
+
if (control.outcome !== "accepted"
|
|
1859
|
+
|| control.snapshot.nativeSessionId !== authorityResult.nativeSessionId
|
|
1860
|
+
|| control.snapshot.authorityEpoch !== authorityResult.authority.epoch
|
|
1861
|
+
|| control.snapshot.authorityOwner !== authorityResult.authority.owner
|
|
1862
|
+
|| control.snapshot.authorityHolderId !== authorityResult.authority.holderId) {
|
|
1863
|
+
throw runtimeError(`Agent Host did not accept Provider authority epoch ${authorityResult.authority.epoch}: `
|
|
1864
|
+
+ (control.snapshot.detail ?? control.outcome)
|
|
1865
|
+
+ `. Durable authority is ${authorityResult.authority.owner}-owned; `
|
|
1866
|
+
+ "retry 'yui task role release "
|
|
1867
|
+
+ `${authorityResult.taskId} ${authorityResult.roleName}' to reconcile the Host.`);
|
|
1868
|
+
}
|
|
1869
|
+
return control;
|
|
1870
|
+
};
|
|
1871
|
+
await syncAuthority(result);
|
|
1872
|
+
emit(result.output);
|
|
1873
|
+
if (result.action === "release") {
|
|
1874
|
+
runtime.notifyMailboxChanged({
|
|
1875
|
+
kind: "role",
|
|
1876
|
+
taskId: result.taskId,
|
|
1877
|
+
roleName: result.roleName
|
|
1878
|
+
});
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
process.stdout.write("Provider input is now routed through the Agent Host PTY gateway. "
|
|
1882
|
+
+ "Use tmux detach (Ctrl-b d) to return authority to the Controller.\n");
|
|
1883
|
+
try {
|
|
1884
|
+
tmux.attachRole(result.taskId, result.roleName, "read-write");
|
|
1885
|
+
}
|
|
1886
|
+
finally {
|
|
1887
|
+
const currentTask = store.getTask(result.taskId);
|
|
1888
|
+
// Completing or retiring the Task from inside the takeover AgentRun owns
|
|
1889
|
+
// Provider shutdown and clears the live binding. Do not turn that
|
|
1890
|
+
// successful terminal transition into a failing best-effort release.
|
|
1891
|
+
if (currentTask?.status === "active") {
|
|
1892
|
+
const released = runTaskCommand(["role", "release", result.taskId, result.roleName], store, { runtime, environment: process.env, yuiHome: home });
|
|
1893
|
+
if (released.kind !== "authority" || released.action !== "release") {
|
|
1894
|
+
throw runtimeError("Provider authority release returned an invalid result.");
|
|
1895
|
+
}
|
|
1896
|
+
await syncAuthority(released);
|
|
1897
|
+
emit(released.output);
|
|
1898
|
+
runtime.notifyMailboxChanged({
|
|
1899
|
+
kind: "role",
|
|
1900
|
+
taskId: released.taskId,
|
|
1901
|
+
roleName: released.roleName
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
catch (error) {
|
|
1908
|
+
await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
|
|
1909
|
+
if (laneDispatchRelease !== undefined) {
|
|
1910
|
+
laneDispatchRelease();
|
|
1911
|
+
laneDispatchRelease = undefined;
|
|
1912
|
+
}
|
|
1913
|
+
throw error;
|
|
1914
|
+
}
|
|
1915
|
+
finally {
|
|
1916
|
+
if (releaseReviewHandoverLock !== undefined) {
|
|
1917
|
+
releaseReviewHandoverLock();
|
|
1918
|
+
releaseReviewHandoverLock = undefined;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (resolved[0] === "jobs") {
|
|
1923
|
+
emit(runJobCommand(resolved.slice(1), store, { runtime }));
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (resolved[0] === "job") {
|
|
1927
|
+
emit(await runDurableJobCommand(resolved.slice(1), {
|
|
1928
|
+
home,
|
|
1929
|
+
json: jsonOutput,
|
|
1930
|
+
environment: process.env,
|
|
1931
|
+
store
|
|
1932
|
+
}));
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (resolved[0] === "telemetry") {
|
|
1936
|
+
emit(await runTelemetryCommand(resolved.slice(1), {
|
|
1937
|
+
home,
|
|
1938
|
+
json: jsonOutput,
|
|
1939
|
+
environment: process.env,
|
|
1940
|
+
store
|
|
1941
|
+
}));
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
throw usageError(`Command is not connected to the current TaskStore command routing: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
|
|
1945
|
+
}
|
|
1946
|
+
finally {
|
|
1947
|
+
await telemetry?.sink.close();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function explicitReleaseActivationDriver() {
|
|
1951
|
+
if (args.length !== 3
|
|
1952
|
+
|| args[0] !== "release"
|
|
1953
|
+
|| args[1] !== "activate"
|
|
1954
|
+
|| args[2].startsWith("-")) {
|
|
1955
|
+
return null;
|
|
1956
|
+
}
|
|
1957
|
+
return resolveReleaseActivationDriver(resolveYuiHome(process.env), args[2], fileURLToPath(cliEntryUrl));
|
|
1958
|
+
}
|
|
1959
|
+
async function preflightManagedTaskControlPlane() {
|
|
1960
|
+
if (taskFinalReviewInvocation.error !== undefined) {
|
|
1961
|
+
throw new Error(taskFinalReviewInvocation.error);
|
|
1962
|
+
}
|
|
1963
|
+
if (process.env.YUI_SESSION_SCOPE !== "task") {
|
|
1964
|
+
if (process.env.YUI_SESSION_SCOPE === "global") {
|
|
1965
|
+
return await preflightManagedGlobalControlPlane();
|
|
1966
|
+
}
|
|
1967
|
+
if (hasManagedIdentity(process.env)) {
|
|
1968
|
+
throw usageError("Managed Agent identity is incomplete; refusing to infer user authority.");
|
|
1969
|
+
}
|
|
1970
|
+
if (taskFinalReviewInvocation.request !== undefined) {
|
|
1971
|
+
throw new Error("Task final-review contract may only be established from the Task Leader's managed Session.");
|
|
1972
|
+
}
|
|
1973
|
+
return { contract: undefined, verifiedStore: undefined };
|
|
1974
|
+
}
|
|
1975
|
+
const internalCallback = args[0] === "internal"
|
|
1976
|
+
&& ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
|
|
1977
|
+
const home = resolveYuiHome(process.env);
|
|
1978
|
+
assertManagedSessionManifest(home, "task");
|
|
1979
|
+
const diagnosticTarget = taskDiagnosticTarget(args);
|
|
1980
|
+
const diagnostic = diagnosticTarget !== undefined
|
|
1981
|
+
&& diagnosticTarget === process.env.YUI_TASK_ID;
|
|
1982
|
+
if (diagnosticTarget !== undefined && !diagnostic) {
|
|
1983
|
+
throw usageError("Diagnostics must remain within this Session's Task.");
|
|
1984
|
+
}
|
|
1985
|
+
// One gate for every managed command: the current CLI, Home, and Controller
|
|
1986
|
+
// must agree. Internal callbacks must still be able to append their immutable
|
|
1987
|
+
// fact while the Controller is offline.
|
|
1988
|
+
await assertRuntimeCoherence({ actualHome: home }, { checkController: !internalCallback && !diagnostic });
|
|
1989
|
+
const verifiedStore = openCurrentTaskStore(home);
|
|
1990
|
+
// One authority for "may this process act as this Task Role?". Yui's own
|
|
1991
|
+
// internal callbacks run inside the Host process Yui itself launched; an Agent
|
|
1992
|
+
// command proves it is the Role's current runtime with its per-Session caller
|
|
1993
|
+
// key. Nothing here
|
|
1994
|
+
// gates on the current AgentRun: which AgentRun is active is durable state that the
|
|
1995
|
+
// command needing it reads, never a fact frozen into a process environment.
|
|
1996
|
+
const runtime = internalCallback
|
|
1997
|
+
? undefined
|
|
1998
|
+
: diagnostic
|
|
1999
|
+
? resolveManagedTaskReader(verifiedStore, process.env)
|
|
2000
|
+
: requireManagedTaskCaller(verifiedStore, process.env);
|
|
2001
|
+
if (diagnostic && runtime?.roleName !== "leader"
|
|
2002
|
+
&& !(args[1] === "show"
|
|
2003
|
+
|| (args[1] === "role" && args[2] === "session" && args[3] === "inspect"
|
|
2004
|
+
&& args[5] === runtime?.roleName))) {
|
|
2005
|
+
// Non-Leaders keep their Assignment-scoped read authorization.
|
|
2006
|
+
requireManagedTaskCaller(verifiedStore, process.env);
|
|
2007
|
+
}
|
|
2008
|
+
const request = taskFinalReviewInvocation.request;
|
|
2009
|
+
if (request !== undefined && diagnostic)
|
|
2010
|
+
requireManagedTaskCaller(verifiedStore, process.env);
|
|
2011
|
+
if (request === undefined) {
|
|
2012
|
+
return { contract: undefined, verifiedStore };
|
|
2013
|
+
}
|
|
2014
|
+
if (runtime === undefined || runtime.roleName !== "leader") {
|
|
2015
|
+
throw new Error("Only the Task Leader's managed Session may establish a final-review contract.");
|
|
2016
|
+
}
|
|
2017
|
+
if (request.taskId !== runtime.taskId) {
|
|
2018
|
+
throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
|
|
2019
|
+
}
|
|
2020
|
+
const recordedContract = resolveRecordedTaskFinalReviewContract(runtime.taskId, verifiedStore.listWorkItems(runtime.taskId), verifiedStore.listReviewRounds(runtime.taskId))?.effective;
|
|
2021
|
+
if (recordedContract !== undefined
|
|
2022
|
+
&& recordedContract.reviewerRoleName !== request.reviewerRoleName) {
|
|
2023
|
+
throw new Error(`Task final-review Reviewer mismatch: expected ${recordedContract.reviewerRoleName}, `
|
|
2024
|
+
+ `found ${request.reviewerRoleName}.`);
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
contract: createTaskFinalReviewContract({
|
|
2028
|
+
taskId: runtime.taskId,
|
|
2029
|
+
reviewerRoleName: request.reviewerRoleName
|
|
2030
|
+
}),
|
|
2031
|
+
verifiedStore
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
async function preflightManagedGlobalControlPlane() {
|
|
2035
|
+
if (taskFinalReviewInvocation.request !== undefined) {
|
|
2036
|
+
throw new Error("Task final-review contract may only be established from the Task Leader's managed Session.");
|
|
2037
|
+
}
|
|
2038
|
+
const home = resolveYuiHome(process.env);
|
|
2039
|
+
const manifest = assertManagedSessionManifest(home, "global");
|
|
2040
|
+
const expectedRoleKind = process.env.YUI_ROLE === "operator" ? "operator" : "global";
|
|
2041
|
+
if (manifest.owner.scope !== "global"
|
|
2042
|
+
|| manifest.roleKind !== expectedRoleKind) {
|
|
2043
|
+
throw new ManagedRuntimeDriftError("Managed global invocation does not match its Session Manifest.");
|
|
2044
|
+
}
|
|
2045
|
+
if (expectedRoleKind === "operator" && ["doctor", "upgrade", "update"].includes(args[0] ?? "")) {
|
|
2046
|
+
// These commands inspect/adopt the storage contract themselves. Requiring
|
|
2047
|
+
// current storage before reaching upgrade would make recovery impossible.
|
|
2048
|
+
return { contract: undefined, verifiedStore: undefined };
|
|
2049
|
+
}
|
|
2050
|
+
await assertRuntimeCoherence({ actualHome: home }, {
|
|
2051
|
+
checkController: !(expectedRoleKind === "operator" && operatorOfflineCommand(args))
|
|
2052
|
+
});
|
|
2053
|
+
const verifiedStore = openCurrentTaskStore(home);
|
|
2054
|
+
const internalCallback = args[0] === "internal"
|
|
2055
|
+
&& ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
|
|
2056
|
+
if (!internalCallback && !operatorOfflineCommand(args)) {
|
|
2057
|
+
try {
|
|
2058
|
+
requireManagedGlobalCaller(verifiedStore, process.env);
|
|
2059
|
+
}
|
|
2060
|
+
catch (error) {
|
|
2061
|
+
verifiedStore.close();
|
|
2062
|
+
throw error;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return { contract: undefined, verifiedStore };
|
|
2066
|
+
}
|
|
2067
|
+
function assertManagedSessionManifest(home, scope) {
|
|
2068
|
+
const manifestPath = process.env.YUI_SESSION_MANIFEST;
|
|
2069
|
+
if (manifestPath === undefined) {
|
|
2070
|
+
throw new ManagedRuntimeDriftError("Managed control-plane invocation requires its Session Manifest.");
|
|
2071
|
+
}
|
|
2072
|
+
let manifest;
|
|
2073
|
+
try {
|
|
2074
|
+
manifest = readSessionBootstrapManifest(manifestPath);
|
|
2075
|
+
}
|
|
2076
|
+
catch (error) {
|
|
2077
|
+
throw new ManagedRuntimeDriftError("Managed Session Manifest could not be verified.", { cause: error });
|
|
2078
|
+
}
|
|
2079
|
+
if (manifest.owner.scope !== scope) {
|
|
2080
|
+
throw new ManagedRuntimeDriftError("Managed invocation scope does not match its Session Manifest.");
|
|
2081
|
+
}
|
|
2082
|
+
if (scope === "task" && (manifest.owner.scope !== "task"
|
|
2083
|
+
|| manifest.owner.taskId !== process.env.YUI_TASK_ID)) {
|
|
2084
|
+
throw new ManagedRuntimeDriftError("Managed Task invocation does not match its Session Manifest owner.");
|
|
2085
|
+
}
|
|
2086
|
+
const expectedPath = resolve(home, "runtime", "session-manifests", `${manifest.digest}.json`);
|
|
2087
|
+
// YUI_HOME is canonicalized (its macOS /var -> /private/var prefix is a
|
|
2088
|
+
// system symlink), so compare the manifest in the same canonical space.
|
|
2089
|
+
let canonicalManifestPath;
|
|
2090
|
+
try {
|
|
2091
|
+
canonicalManifestPath = realpathSync(resolve(manifestPath));
|
|
2092
|
+
}
|
|
2093
|
+
catch {
|
|
2094
|
+
canonicalManifestPath = resolve(manifestPath);
|
|
2095
|
+
}
|
|
2096
|
+
if (canonicalManifestPath !== expectedPath) {
|
|
2097
|
+
throw new ManagedRuntimeDriftError("Managed Session Manifest path is outside this YUI_HOME.");
|
|
2098
|
+
}
|
|
2099
|
+
return manifest;
|
|
2100
|
+
}
|
|
2101
|
+
function cleanupCliError(error, fallbackResource) {
|
|
2102
|
+
if (error instanceof CleanupInspectionError) {
|
|
2103
|
+
return usageError(error.message, undefined, cleanupBlockedDetails(error.checks[0]?.reason ?? "cleanup-failed", fallbackResource, true, error.checks));
|
|
2104
|
+
}
|
|
2105
|
+
if (error instanceof WorkspaceCleanupBlockedError) {
|
|
2106
|
+
return usageError(error.message, undefined, cleanupBlockedDetails(error.reason, error.resource, error.retryable));
|
|
2107
|
+
}
|
|
2108
|
+
return new CliError("RUNTIME_ERROR", error instanceof Error ? error.message : String(error), undefined, cleanupBlockedDetails("cleanup-failed", fallbackResource, true), { cause: error });
|
|
2109
|
+
}
|
|
2110
|
+
function cleanupBlockedDetails(reason, resource, retryable, checks) {
|
|
2111
|
+
return {
|
|
2112
|
+
status: "blocked",
|
|
2113
|
+
blockedBy: [{ resource, reason, retryable }],
|
|
2114
|
+
remainingResources: [resource],
|
|
2115
|
+
retryable,
|
|
2116
|
+
...(checks === undefined ? {} : { checks })
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function cliWorkItemReference(value, environment) {
|
|
2120
|
+
try {
|
|
2121
|
+
return resolveTaskRecordReference(value, {
|
|
2122
|
+
kind: "workItem",
|
|
2123
|
+
label: "Work Item reference",
|
|
2124
|
+
...(environment.YUI_TASK_ID === undefined
|
|
2125
|
+
? {}
|
|
2126
|
+
: { contextTaskId: environment.YUI_TASK_ID })
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
catch (error) {
|
|
2130
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
function cliTaskRecordReference(value, kind, environment) {
|
|
2134
|
+
try {
|
|
2135
|
+
return resolveTaskRecordReference(value, {
|
|
2136
|
+
kind,
|
|
2137
|
+
label: kind === "run" ? "AgentRun reference" : "ReviewRound reference",
|
|
2138
|
+
...(environment.YUI_TASK_ID === undefined
|
|
2139
|
+
? {}
|
|
2140
|
+
: { contextTaskId: environment.YUI_TASK_ID })
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
catch (error) {
|
|
2144
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function assertWorkItemExecutionDependenciesForCommand(args, store, environment) {
|
|
2148
|
+
let item;
|
|
2149
|
+
if (args[0] === "task" && args[1] === "work" && args[2] === "dispatch"
|
|
2150
|
+
&& args[3] !== undefined) {
|
|
2151
|
+
const reference = cliWorkItemReference(args[3], environment);
|
|
2152
|
+
item = store.getWorkItem(reference.taskId, reference.localId) ?? undefined;
|
|
2153
|
+
}
|
|
2154
|
+
else if (args[0] === "task" && args[1] === "run" && args[2] === "retry"
|
|
2155
|
+
&& args[3] !== undefined) {
|
|
2156
|
+
const reference = cliTaskRecordReference(args[3], "run", environment);
|
|
2157
|
+
const run = store.getRun(reference.taskId, reference.localId);
|
|
2158
|
+
item = run?.purpose === "execution" && run.workItemId !== undefined
|
|
2159
|
+
? store.getWorkItem(run.taskId, run.workItemId) ?? undefined
|
|
2160
|
+
: undefined;
|
|
2161
|
+
}
|
|
2162
|
+
if (item === undefined)
|
|
2163
|
+
return;
|
|
2164
|
+
assertWorkItemDependenciesCompletedForCommand(store, item);
|
|
2165
|
+
}
|
|
2166
|
+
async function candidateSnapshotForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
|
|
2167
|
+
if (args[0] !== "task" || args[1] !== "work" || args[2] !== "update"
|
|
2168
|
+
|| args[3] === undefined || args[4] !== "done")
|
|
2169
|
+
return {};
|
|
2170
|
+
const reference = cliWorkItemReference(args[3], environment);
|
|
2171
|
+
return snapshotWorkItemCandidate(store, preparer, reference.taskId, reference.localId, taskFinalReviewContract);
|
|
2172
|
+
}
|
|
2173
|
+
async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, environment) {
|
|
2174
|
+
const isDispatch = args[0] === "task"
|
|
2175
|
+
&& args[1] === "work"
|
|
2176
|
+
&& args[2] === "dispatch"
|
|
2177
|
+
&& args[3] !== undefined;
|
|
2178
|
+
if (!isDispatch)
|
|
2179
|
+
return undefined;
|
|
2180
|
+
const itemRef = cliWorkItemReference(args[3], environment);
|
|
2181
|
+
const item = store.getWorkItem(itemRef.taskId, itemRef.localId);
|
|
2182
|
+
if (item === null || item.assignee === undefined)
|
|
2183
|
+
return undefined;
|
|
2184
|
+
const plan = workItemDispatchLanePlan(args, store, item);
|
|
2185
|
+
if (plan.roles.length === 0)
|
|
2186
|
+
return undefined;
|
|
2187
|
+
for (const roleName of plan.roles) {
|
|
2188
|
+
if (store.getRole(item.taskId, roleName) === null) {
|
|
2189
|
+
throw usageError(`Task Role not found: ${item.taskId}/${roleName}.`);
|
|
2190
|
+
}
|
|
2191
|
+
if (store.getActiveRun(item.taskId, roleName) !== null) {
|
|
2192
|
+
throw usageError(`${item.taskId}/${roleName} already has an active turn.`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const held = await preparer.acquireTaskProjectMaintenanceLocks(item.taskId);
|
|
2196
|
+
const map = new Map();
|
|
2197
|
+
try {
|
|
2198
|
+
assertTaskDeliveryAuthority(store, environment, item.taskId);
|
|
2199
|
+
const currentItem = store.getWorkItem(item.taskId, item.id);
|
|
2200
|
+
if (currentItem?.revision !== item.revision
|
|
2201
|
+
|| JSON.stringify(workItemDispatchLanePlan(args, store, currentItem)) !== JSON.stringify(plan)) {
|
|
2202
|
+
throw new Error(`Work item dispatch changed while waiting for Project maintenance: ${item.id}.`);
|
|
2203
|
+
}
|
|
2204
|
+
if (held.current.status !== "active" || held.current.executionGate.state !== "enabled"
|
|
2205
|
+
|| plan.roles.some(roleName => store.getRole(item.taskId, roleName) === null
|
|
2206
|
+
|| store.getActiveRun(item.taskId, roleName) !== null)) {
|
|
2207
|
+
throw new Error(`Task/Role dispatch state changed while waiting for Project maintenance: ${item.taskId}.`);
|
|
2208
|
+
}
|
|
2209
|
+
const projectPaths = new Map();
|
|
2210
|
+
for (const { projectId } of held.current.projectBindings) {
|
|
2211
|
+
const project = store.getProject(projectId);
|
|
2212
|
+
if (project === null)
|
|
2213
|
+
throw new Error(`Project not found: ${projectId}.`);
|
|
2214
|
+
projectPaths.set(projectId, project.path);
|
|
2215
|
+
}
|
|
2216
|
+
const source = item.assignee === "leader"
|
|
2217
|
+
? store.getTaskWorkspace(item.taskId)
|
|
2218
|
+
: store.getWorkItemWorkspace(item.taskId, item.id);
|
|
2219
|
+
if (source === null) {
|
|
2220
|
+
throw usageError(`Execution Lane source workspace is not ready: ${item.id}.`);
|
|
2221
|
+
}
|
|
2222
|
+
const inputHeads = await preparer.snapshotExecutionLaneInputHeads(source, item.writeProjectIds);
|
|
2223
|
+
for (const laneId of plan.laneIds) {
|
|
2224
|
+
map.set(laneId, await preparer.prepareExecutionLaneWorkspace(item.taskId, plan.groupId, laneId, { purpose: "execution", workItemId: item.id, inputHeads }, { current: held.current }));
|
|
2225
|
+
}
|
|
2226
|
+
return { workspaces: map, release: held.release, projectPaths };
|
|
2227
|
+
}
|
|
2228
|
+
catch (error) {
|
|
2229
|
+
await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
|
|
2230
|
+
held.release();
|
|
2231
|
+
throw error;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function workItemDispatchLanePlan(args, store, item) {
|
|
2235
|
+
if (item.assignee === undefined) {
|
|
2236
|
+
throw usageError(`Work Item has no Task Role assignee: ${item.id}.`);
|
|
2237
|
+
}
|
|
2238
|
+
const roles = args.flatMap((value, index) => (value === "--lane-role" && args[index + 1] !== undefined
|
|
2239
|
+
? [args[index + 1]]
|
|
2240
|
+
: []));
|
|
2241
|
+
const groupId = `execution-group-${store.peekNextRunId(item.taskId)}`;
|
|
2242
|
+
return {
|
|
2243
|
+
groupId,
|
|
2244
|
+
...planReplicatedWorkItemLanes(item.assignee, roles, groupId)
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
async function prepareReviewLaneWorkspaces(taskId, reviewRoundId, store, preparer) {
|
|
2248
|
+
const round = store.getReviewRound(taskId, reviewRoundId);
|
|
2249
|
+
const group = round?.executionGroup;
|
|
2250
|
+
if (round === null || round === undefined || group === undefined)
|
|
2251
|
+
return undefined;
|
|
2252
|
+
const map = new Map();
|
|
2253
|
+
try {
|
|
2254
|
+
for (const lane of group.lanes.filter(({ disposition }) => disposition === "open")) {
|
|
2255
|
+
map.set(lane.id, await preparer.prepareExecutionLaneWorkspace(taskId, group.id, lane.id, {
|
|
2256
|
+
purpose: "review",
|
|
2257
|
+
reviewRoundId
|
|
2258
|
+
}));
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch (error) {
|
|
2262
|
+
await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
|
|
2263
|
+
throw error;
|
|
2264
|
+
}
|
|
2265
|
+
return map;
|
|
2266
|
+
}
|
|
2267
|
+
async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, environment) {
|
|
2268
|
+
if (args[0] !== "task")
|
|
2269
|
+
return undefined;
|
|
2270
|
+
let taskId;
|
|
2271
|
+
let decisionSupportRead = false;
|
|
2272
|
+
if (args[1] === "complete" && args[2] !== undefined) {
|
|
2273
|
+
const task = store.getTask(args[2]);
|
|
2274
|
+
if (task === null || task.status !== "active" || task.projectBindings.length === 0) {
|
|
2275
|
+
return undefined;
|
|
2276
|
+
}
|
|
2277
|
+
// Every Project-backed completion must freeze
|
|
2278
|
+
// a clean committed Task-main snapshot. Review policy only decides whether
|
|
2279
|
+
// that head also needs an independent ReviewRound.
|
|
2280
|
+
taskId = task.id;
|
|
2281
|
+
}
|
|
2282
|
+
else if (args[1] === "review"
|
|
2283
|
+
&& args[2] === "request"
|
|
2284
|
+
&& args[3] !== undefined) {
|
|
2285
|
+
taskId = store.getTask(args[3])?.id;
|
|
2286
|
+
}
|
|
2287
|
+
else if (args[1] === "review"
|
|
2288
|
+
&& args[2] === "retry"
|
|
2289
|
+
&& args[3] !== undefined) {
|
|
2290
|
+
const reference = cliTaskRecordReference(args[3], "reviewRound", environment);
|
|
2291
|
+
const round = store.getReviewRound(reference.taskId, reference.localId);
|
|
2292
|
+
if (round !== null && round.scope === "task") {
|
|
2293
|
+
taskId = reference.taskId;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
else if (args[1] === "work"
|
|
2297
|
+
&& args[2] === "review"
|
|
2298
|
+
&& args[3] === "retry"
|
|
2299
|
+
&& args[4] !== undefined) {
|
|
2300
|
+
const reference = cliTaskRecordReference(args[4], "reviewRound", environment);
|
|
2301
|
+
const round = store.getReviewRound(reference.taskId, reference.localId);
|
|
2302
|
+
if (round !== null && round.scope === "task") {
|
|
2303
|
+
taskId = reference.taskId;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
else if (args[1] === "run"
|
|
2307
|
+
&& (args[2] === "retry" || args[2] === "settle")
|
|
2308
|
+
&& args[3] !== undefined) {
|
|
2309
|
+
const reference = cliTaskRecordReference(args[3], "run", environment);
|
|
2310
|
+
const run = store.getRun(reference.taskId, reference.localId);
|
|
2311
|
+
const round = run?.reviewRoundId === undefined
|
|
2312
|
+
? null
|
|
2313
|
+
: store.getReviewRound(reference.taskId, run.reviewRoundId);
|
|
2314
|
+
if (run?.purpose === "review"
|
|
2315
|
+
&& round !== null
|
|
2316
|
+
&& round.scope === "task") {
|
|
2317
|
+
taskId = reference.taskId;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
else if (args[1] === "archive"
|
|
2321
|
+
&& args[2] !== undefined
|
|
2322
|
+
&& args.includes("--integrated")) {
|
|
2323
|
+
taskId = store.getTask(args[2])?.id;
|
|
2324
|
+
}
|
|
2325
|
+
else if ((args[1] === "show"
|
|
2326
|
+
|| args[1] === "next-action"
|
|
2327
|
+
|| args[1] === "remote-delivery")
|
|
2328
|
+
&& args[2] !== undefined) {
|
|
2329
|
+
taskId = store.getTask(args[2])?.id;
|
|
2330
|
+
decisionSupportRead = true;
|
|
2331
|
+
}
|
|
2332
|
+
const status = taskId === undefined ? undefined : store.getTask(taskId)?.status;
|
|
2333
|
+
if (taskId === undefined || (status !== "active" && status !== "cancelled"))
|
|
2334
|
+
return undefined;
|
|
2335
|
+
try {
|
|
2336
|
+
return await snapshotActualTaskReviewCandidate(taskId, store, preparer, !decisionSupportRead);
|
|
2337
|
+
}
|
|
2338
|
+
catch (error) {
|
|
2339
|
+
if (decisionSupportRead && error instanceof CliError)
|
|
2340
|
+
return undefined;
|
|
2341
|
+
throw error;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
async function snapshotActualTaskReviewCandidate(taskId, store, preparer, prepareWorkspace = true) {
|
|
2345
|
+
const task = store.getTask(taskId);
|
|
2346
|
+
if (task === null)
|
|
2347
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
2348
|
+
if (task.projectBindings.length === 0) {
|
|
2349
|
+
throw usageError(`Final Task Review requires a Project-backed Task: ${task.id}.`);
|
|
2350
|
+
}
|
|
2351
|
+
// Mutation preflights reconcile before freezing a candidate. A decision-
|
|
2352
|
+
// support read only observes existing heads; it must not migrate Sessions,
|
|
2353
|
+
// create worktrees or acquire workspace ownership as a side effect.
|
|
2354
|
+
if (prepareWorkspace)
|
|
2355
|
+
await preparer.prepareTaskWorkspace(task.id);
|
|
2356
|
+
const workspace = store.getTaskWorkspace(task.id);
|
|
2357
|
+
if (workspace === null
|
|
2358
|
+
|| workspace.owner.type !== "task"
|
|
2359
|
+
|| workspace.owner.taskId !== task.id) {
|
|
2360
|
+
throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
|
|
2361
|
+
}
|
|
2362
|
+
try {
|
|
2363
|
+
const snapshot = await preparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(({ projectId }) => projectId));
|
|
2364
|
+
const heads = new Map(snapshot.projects.map(({ projectId, headCommit }) => ([projectId, headCommit])));
|
|
2365
|
+
return {
|
|
2366
|
+
schemaVersion: 1,
|
|
2367
|
+
projects: task.projectBindings.map(({ projectId }) => {
|
|
2368
|
+
const commit = heads.get(projectId);
|
|
2369
|
+
if (commit === undefined) {
|
|
2370
|
+
throw new Error(`Task main snapshot omitted Project ${projectId}.`);
|
|
2371
|
+
}
|
|
2372
|
+
return { projectId, commit };
|
|
2373
|
+
})
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
catch (error) {
|
|
2377
|
+
throw usageError(`Actual Task Project head verification failed for ${task.id}: `
|
|
2378
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Issue 07: computes the delta-recheck assessment for `task review request
|
|
2383
|
+
* --delta-recheck`. Runs only for that exact command; every other command
|
|
2384
|
+
* returns undefined. Technical evidence boundaries fail closed here; semantic
|
|
2385
|
+
* risk remains a Leader and Project-policy decision.
|
|
2386
|
+
*/
|
|
2387
|
+
async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReviewCandidate) {
|
|
2388
|
+
if (args[0] !== "task" || args[1] !== "review" || args[2] !== "request") {
|
|
2389
|
+
return undefined;
|
|
2390
|
+
}
|
|
2391
|
+
if (!args.includes("--delta-recheck"))
|
|
2392
|
+
return undefined;
|
|
2393
|
+
const taskId = args[3];
|
|
2394
|
+
if (taskId === undefined)
|
|
2395
|
+
return undefined;
|
|
2396
|
+
const task = store.getTask(taskId);
|
|
2397
|
+
if (task === null || task.status !== "active")
|
|
2398
|
+
return undefined;
|
|
2399
|
+
if (actualTaskReviewCandidate === undefined)
|
|
2400
|
+
return undefined;
|
|
2401
|
+
// The previous Round is the latest completed Task-final Round that accepted
|
|
2402
|
+
// a head (a full Review or an equivalent-and-accepted delta). A
|
|
2403
|
+
// non-accepted delta cannot be the base for a new delta.
|
|
2404
|
+
const previous = [...store.listReviewRounds(task.id)]
|
|
2405
|
+
.filter((round) => isCompletedTaskReviewEvidence(store, round))
|
|
2406
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))
|
|
2407
|
+
.at(-1);
|
|
2408
|
+
if (previous === undefined) {
|
|
2409
|
+
throw usageError("Delta-recheck requires a previous completed Task-final Review that accepted a head.");
|
|
2410
|
+
}
|
|
2411
|
+
const repositoryPaths = {};
|
|
2412
|
+
const taskWorkspace = store.getTaskWorkspace(task.id);
|
|
2413
|
+
if (taskWorkspace === null || taskWorkspace.owner.type !== "task") {
|
|
2414
|
+
throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
|
|
2415
|
+
}
|
|
2416
|
+
for (const candidateProject of actualTaskReviewCandidate.projects) {
|
|
2417
|
+
const entry = workspaceProjectEntry(taskWorkspace, candidateProject.projectId);
|
|
2418
|
+
if (entry === undefined) {
|
|
2419
|
+
throw usageError(`Delta-recheck Task workspace Project not found: ${candidateProject.projectId}.`);
|
|
2420
|
+
}
|
|
2421
|
+
repositoryPaths[candidateProject.projectId] = entry.path;
|
|
2422
|
+
}
|
|
2423
|
+
const assessment = await assessDeltaRecheck({
|
|
2424
|
+
repositoryPaths,
|
|
2425
|
+
previousRound: previous,
|
|
2426
|
+
candidate: actualTaskReviewCandidate,
|
|
2427
|
+
git: new NodeGitWorkspace()
|
|
2428
|
+
});
|
|
2429
|
+
if (assessment.kind === "ineligible") {
|
|
2430
|
+
throw usageError(`Delta-recheck is technically unavailable: ${assessment.reason}`);
|
|
2431
|
+
}
|
|
2432
|
+
return assessment.preflight;
|
|
2433
|
+
}
|
|
2434
|
+
function reviewRoundFromCommandData(data) {
|
|
2435
|
+
if (typeof data !== "object" || data === null || !("reviewRound" in data))
|
|
2436
|
+
return undefined;
|
|
2437
|
+
const round = data.reviewRound;
|
|
2438
|
+
if (typeof round !== "object" || round === null)
|
|
2439
|
+
return undefined;
|
|
2440
|
+
const value = round;
|
|
2441
|
+
return typeof value.id === "string"
|
|
2442
|
+
&& typeof value.taskId === "string"
|
|
2443
|
+
&& typeof value.reviewerRoleName === "string"
|
|
2444
|
+
&& typeof value.status === "string"
|
|
2445
|
+
? {
|
|
2446
|
+
id: value.id,
|
|
2447
|
+
taskId: value.taskId,
|
|
2448
|
+
reviewerRoleName: value.reviewerRoleName,
|
|
2449
|
+
status: value.status
|
|
2450
|
+
}
|
|
2451
|
+
: undefined;
|
|
2452
|
+
}
|
|
2453
|
+
async function executeOperatorSessionControl(control, home, store, runtime, tmux, catalogs) {
|
|
2454
|
+
if (jsonOutput)
|
|
2455
|
+
throw usageError("Operator new and resume do not support --json.");
|
|
2456
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
2457
|
+
throw usageError("Operator new and resume require an interactive terminal.");
|
|
2458
|
+
}
|
|
2459
|
+
const role = store.getGlobalRole("operator");
|
|
2460
|
+
if (role === null)
|
|
2461
|
+
throw usageError("Operator is not configured. Run yui setup first.");
|
|
2462
|
+
const sessionSet = store.getGlobalRoleSessionSet(role.name);
|
|
2463
|
+
const active = sessionSet?.sessions[sessionSet.activeAgentId];
|
|
2464
|
+
const paneRunning = tmux.probeRoleStatus("operator", "operator") === "running";
|
|
2465
|
+
if (paneRunning && active === undefined) {
|
|
2466
|
+
throw usageError("Operator is running but its native session has not been recorded yet. "
|
|
2467
|
+
+ "Record the exact native session before switching sessions.");
|
|
2468
|
+
}
|
|
2469
|
+
if (control.action === "resume"
|
|
2470
|
+
&& paneRunning
|
|
2471
|
+
&& control.targetAgentId === active?.agentId
|
|
2472
|
+
&& active !== undefined
|
|
2473
|
+
&& operatorSessionRef(active) === control.ref) {
|
|
2474
|
+
await prepareInteractiveCodexApp(control.targetAgentId, store, home);
|
|
2475
|
+
tmux.attachRole("operator", "operator", "auto");
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const handle = terminalIo();
|
|
2479
|
+
try {
|
|
2480
|
+
if (control.targetAgentId !== role.activeAgentId) {
|
|
2481
|
+
const binding = role.agentBindings[control.targetAgentId];
|
|
2482
|
+
if (binding === undefined) {
|
|
2483
|
+
throw usageError(`Operator Agent is not bound: ${control.targetAgentId}.`);
|
|
2484
|
+
}
|
|
2485
|
+
handle.io.write([
|
|
2486
|
+
`Switching to ${adapterLabel(binding.adapterId)} (${binding.agentId})`,
|
|
2487
|
+
"",
|
|
2488
|
+
"Saved configuration",
|
|
2489
|
+
` Model ${binding.config.model ?? "CLI default"}`,
|
|
2490
|
+
` Effort ${binding.config.effort ?? "CLI default"}`,
|
|
2491
|
+
""
|
|
2492
|
+
].join("\n"));
|
|
2493
|
+
const update = (await handle.io.question("Update this configuration? [y/N]: "))?.trim().toLowerCase();
|
|
2494
|
+
if (update === "y" || update === "yes") {
|
|
2495
|
+
const resolution = await resolveGlobalRoleAgentConfigurationArguments(role.name, binding.agentId, selectionPorts(store, catalogs), handle.io);
|
|
2496
|
+
if (resolution.kind !== "resolved") {
|
|
2497
|
+
process.stdout.write("Cancelled.\n");
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
const updated = runGlobalRoleCommand(resolution.args.slice(2), store, { yuiHome: home, env: process.env });
|
|
2501
|
+
if (typeof updated !== "string") {
|
|
2502
|
+
throw new Error("Operator Agent configuration returned an invalid control result.");
|
|
2503
|
+
}
|
|
2504
|
+
handle.io.write(`\nUpdated ${adapterLabel(binding.adapterId)} configuration.\n`);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
if (paneRunning) {
|
|
2508
|
+
const answer = (await handle.io.question("Operator is running. Switch session? [y/N]: "))?.trim().toLowerCase();
|
|
2509
|
+
if (answer !== "y" && answer !== "yes") {
|
|
2510
|
+
process.stdout.write("Cancelled.\n");
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
finally {
|
|
2516
|
+
handle.close();
|
|
2517
|
+
}
|
|
2518
|
+
await prepareInteractiveCodexApp(control.targetAgentId, store, home);
|
|
2519
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
2520
|
+
if (paneRunning
|
|
2521
|
+
|| (active !== undefined
|
|
2522
|
+
&& active.status === "active")
|
|
2523
|
+
|| (sessionSet?.providerBinding?.run != null
|
|
2524
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(sessionSet.providerBinding.run.status))
|
|
2525
|
+
|| (sessionSet?.providerBinding?.goal != null && sessionSet.providerBinding.goal.status !== "complete")) {
|
|
2526
|
+
await runtime.stopGlobalRoleSession(role.name);
|
|
2527
|
+
}
|
|
2528
|
+
applyOperatorSessionControl(control, store);
|
|
2529
|
+
await runtime.prepareGlobalRoleEnter(role.name);
|
|
2530
|
+
tmux.attachRole("operator", role.name, "auto");
|
|
2531
|
+
}
|
|
2532
|
+
async function prepareInteractiveCodexApp(agentId, store, home) {
|
|
2533
|
+
await prepareCodexForCurrentRoles(store, home, agentId);
|
|
2534
|
+
}
|
|
2535
|
+
async function prepareCodexForCurrentRoles(store, home, targetAgentId) {
|
|
2536
|
+
try {
|
|
2537
|
+
const result = await prepareLocalCodexStartup(store, home, process.env, targetAgentId);
|
|
2538
|
+
if (result.app === "restart-required") {
|
|
2539
|
+
codexAppNotice("Codex App is already running without the current Yui shared app-server. "
|
|
2540
|
+
+ "Quit and reopen Codex App to see and continue Yui sessions.", "Reopen Codex App to connect to Yui sessions.");
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
catch (error) {
|
|
2544
|
+
const detail = describeCliFailure(error, failureContext()).message;
|
|
2545
|
+
const permissionDenied = isCodexInspectionPermissionError(error);
|
|
2546
|
+
codexAppNotice(`${permissionDenied
|
|
2547
|
+
? "Codex sharing could not be prepared or verified from this process"
|
|
2548
|
+
: "Codex shared runtime could not be prepared"}: ${detail}`, permissionDenied
|
|
2549
|
+
? "macOS denied access while preparing Codex sharing; its state is unverified. See the Yui terminal."
|
|
2550
|
+
: "Codex shared runtime could not be prepared; see the Yui terminal.");
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
function assertHomeStartupAuthority(store) {
|
|
2554
|
+
if (process.env.YUI_SESSION_SCOPE === undefined && process.env.YUI_ROLE === undefined
|
|
2555
|
+
&& process.env.YUI_AGENT_ID === undefined && process.env.YUI_NATIVE_SESSION_ID === undefined)
|
|
2556
|
+
return;
|
|
2557
|
+
if (process.env.YUI_SESSION_SCOPE !== "global" || process.env.YUI_ROLE !== "operator") {
|
|
2558
|
+
throw usageError("Starting Home services requires the user or current Operator.");
|
|
2559
|
+
}
|
|
2560
|
+
requireManagedGlobalCaller(store, process.env);
|
|
2561
|
+
}
|
|
2562
|
+
function codexAppNotice(message, notification) {
|
|
2563
|
+
writeCliWarning(message);
|
|
2564
|
+
if (process.platform !== "darwin")
|
|
2565
|
+
return;
|
|
2566
|
+
// The native TUI clears terminal output on attach, so keep the actionable
|
|
2567
|
+
// warning visible in Notification Center without interrupting Yui startup.
|
|
2568
|
+
spawnSync("/usr/bin/osascript", [
|
|
2569
|
+
"-e",
|
|
2570
|
+
`display notification "${notification}" with title "Yui"`
|
|
2571
|
+
], { stdio: "ignore", timeout: 3_000 });
|
|
2572
|
+
}
|
|
2573
|
+
function writeCliWarning(message) {
|
|
2574
|
+
const safe = sanitizeFailureText(message, failureContext());
|
|
2575
|
+
process.stderr.write(`${jsonOutput
|
|
2576
|
+
? JSON.stringify({ ok: false, code: "RUNTIME_ERROR", message: safe, details: { advisory: true } })
|
|
2577
|
+
: `Warning: ${safe}`}\n`);
|
|
2578
|
+
}
|
|
2579
|
+
function renderControllerResult(method, value) {
|
|
2580
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2581
|
+
return JSON.stringify(value);
|
|
2582
|
+
}
|
|
2583
|
+
const result = value;
|
|
2584
|
+
if (method === "restart") {
|
|
2585
|
+
const previousPid = Number.isSafeInteger(result.previousPid) ? String(result.previousPid) : undefined;
|
|
2586
|
+
const pid = Number.isSafeInteger(result.pid) ? String(result.pid) : undefined;
|
|
2587
|
+
if (previousPid !== undefined && pid !== undefined) {
|
|
2588
|
+
return `Controller restarted (PID ${previousPid} -> ${pid}). tmux sessions were not stopped.`;
|
|
2589
|
+
}
|
|
2590
|
+
return pid === undefined
|
|
2591
|
+
? "Controller restarted. tmux sessions were not stopped."
|
|
2592
|
+
: `Controller started (PID ${pid}). tmux sessions were not stopped.`;
|
|
2593
|
+
}
|
|
2594
|
+
return result.stopped === true
|
|
2595
|
+
? "Controller stopped."
|
|
2596
|
+
: "Controller was already stopped.";
|
|
2597
|
+
}
|
|
2598
|
+
async function completionCommand(home, node) {
|
|
2599
|
+
if (args[2] === "candidates") {
|
|
2600
|
+
const separator = args.indexOf("--", 4);
|
|
2601
|
+
const prefix = args[3];
|
|
2602
|
+
if (prefix === undefined || separator !== 4) {
|
|
2603
|
+
throw usageError("Completion candidates usage: yui config completion candidates <prefix> -- <words...>");
|
|
2604
|
+
}
|
|
2605
|
+
let selection;
|
|
2606
|
+
const candidates = await resolveCompletionCandidates({
|
|
2607
|
+
current: prefix,
|
|
2608
|
+
words: args.slice(separator + 1),
|
|
2609
|
+
root: discoveryCommandTree(process.env),
|
|
2610
|
+
ports: { call: async (method, params) => {
|
|
2611
|
+
if (!selection) {
|
|
2612
|
+
assertCliHomeReleaseFence({
|
|
2613
|
+
home, packageRoot: fileURLToPath(new URL("../", import.meta.url)),
|
|
2614
|
+
entryPath: fileURLToPath(cliEntryUrl), args
|
|
2615
|
+
});
|
|
2616
|
+
await preflightManagedTaskControlPlane();
|
|
2617
|
+
assertTaskInvocationScope(args.slice(separator + 1), process.env);
|
|
2618
|
+
selection = completionSelectionPorts(home);
|
|
2619
|
+
}
|
|
2620
|
+
return selection.call(method, params);
|
|
2621
|
+
} }
|
|
2622
|
+
});
|
|
2623
|
+
process.stdout.write(candidates.length === 0 ? "" : `${candidates.join("\n")}\n`);
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
if (jsonOutput)
|
|
2627
|
+
throw usageError("Completion configuration does not support --json.");
|
|
2628
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
2629
|
+
throw usageError("Completion configuration requires an interactive terminal.");
|
|
2630
|
+
}
|
|
2631
|
+
const shell = completionShell(args[2]);
|
|
2632
|
+
if (args.length > (shell === undefined ? 2 : 3)) {
|
|
2633
|
+
throw usageError("Completion usage: yui config completion [bash|zsh|fish]");
|
|
2634
|
+
}
|
|
2635
|
+
const store = openCurrentTaskStore(home);
|
|
2636
|
+
const ioHandle = terminalIo();
|
|
2637
|
+
try {
|
|
2638
|
+
const manager = new FileCompletionManager(store, process.env);
|
|
2639
|
+
emit(await runCompletionWizard(manager, ioHandle.io, shell === undefined ? {} : { shell }));
|
|
2640
|
+
}
|
|
2641
|
+
finally {
|
|
2642
|
+
ioHandle.close();
|
|
2643
|
+
}
|
|
2644
|
+
void node;
|
|
2645
|
+
}
|
|
2646
|
+
function completionShell(value) {
|
|
2647
|
+
if (value === undefined)
|
|
2648
|
+
return undefined;
|
|
2649
|
+
if (value === "bash" || value === "zsh" || value === "fish")
|
|
2650
|
+
return value;
|
|
2651
|
+
throw usageError("Completion shell must be one of bash, zsh, fish.");
|
|
2652
|
+
}
|
|
2653
|
+
async function resolveTerminalArguments(commandArgs, node, store, catalogs) {
|
|
2654
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2655
|
+
if (!interactive || !allowsInteractiveSelection(commandArgs, jsonOutput)) {
|
|
2656
|
+
return [...commandArgs];
|
|
2657
|
+
}
|
|
2658
|
+
const handle = terminalIo();
|
|
2659
|
+
try {
|
|
2660
|
+
const ports = selectionPorts(store, catalogs);
|
|
2661
|
+
const operatorWizard = await resolveOperatorWizardArguments(commandArgs, store.getGlobalRole("operator"), listOperatorSessions(store.getGlobalRoleSessionSet("operator")), handle.io);
|
|
2662
|
+
if (operatorWizard.kind === "cancelled")
|
|
2663
|
+
return null;
|
|
2664
|
+
const operatorArgs = operatorWizard.args;
|
|
2665
|
+
// Global Role add owns its Agent choice so the configured default can be
|
|
2666
|
+
// shown explicitly. Other commands first resolve missing positional
|
|
2667
|
+
// targets through the generic selector, then enter the focused Role UI.
|
|
2668
|
+
if ((operatorArgs[0] === "config" && operatorArgs[1] === "role" && operatorArgs[2] === "add")
|
|
2669
|
+
|| (operatorArgs[0] === "task" && operatorArgs[1] === "role" && operatorArgs[2] === "add")) {
|
|
2670
|
+
const wizard = await resolveRoleWizardArguments(operatorArgs, ports, handle.io);
|
|
2671
|
+
if (wizard.kind === "cancelled")
|
|
2672
|
+
return null;
|
|
2673
|
+
const selected = await resolveInteractiveArguments(wizard.args, node, ports, handle.io);
|
|
2674
|
+
return selected.kind === "cancelled" ? null : selected.args;
|
|
2675
|
+
}
|
|
2676
|
+
const selected = await resolveInteractiveArguments(operatorArgs, node, ports, handle.io);
|
|
2677
|
+
if (selected.kind === "cancelled")
|
|
2678
|
+
return null;
|
|
2679
|
+
const roleWizard = await resolveRoleWizardArguments(selected.args, ports, handle.io);
|
|
2680
|
+
return roleWizard.kind === "cancelled" ? null : roleWizard.args;
|
|
2681
|
+
}
|
|
2682
|
+
finally {
|
|
2683
|
+
// The Agent process must be the only reader of stdin after Role enter.
|
|
2684
|
+
handle.close();
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
function terminalIo() {
|
|
2688
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
2689
|
+
return {
|
|
2690
|
+
io: {
|
|
2691
|
+
interactive: true,
|
|
2692
|
+
json: jsonOutput,
|
|
2693
|
+
width: process.stdout.columns ?? 100,
|
|
2694
|
+
write: (value) => { process.stdout.write(value); },
|
|
2695
|
+
question: async (prompt) => {
|
|
2696
|
+
try {
|
|
2697
|
+
return await readline.question(prompt);
|
|
2698
|
+
}
|
|
2699
|
+
catch (error) {
|
|
2700
|
+
if (error instanceof Error && (error.name === "AbortError"
|
|
2701
|
+
|| ("code" in error && error.code === "ERR_USE_AFTER_CLOSE")))
|
|
2702
|
+
return undefined;
|
|
2703
|
+
throw error;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
},
|
|
2707
|
+
close: () => { readline.close(); }
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
function selectionPorts(store, catalogs) {
|
|
2711
|
+
return {
|
|
2712
|
+
call: (method, params) => selectionCall(store, catalogs, method, params)
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
async function preflightAgentConfigurationMutation(commandArgs, store, catalogs) {
|
|
2716
|
+
const mutation = profileAgentConfigurationMutation(commandArgs, store)
|
|
2717
|
+
?? taskRoleAgentConfigurationMutation(commandArgs, store);
|
|
2718
|
+
if (mutation === undefined) {
|
|
2719
|
+
await warmConfigurationMutationCatalog(commandArgs, store, catalogs);
|
|
2720
|
+
return undefined;
|
|
2721
|
+
}
|
|
2722
|
+
const agent = store.getConfiguredAgent(mutation.agentId);
|
|
2723
|
+
if (agent === null)
|
|
2724
|
+
throw agentNotFound(mutation.agentId);
|
|
2725
|
+
const resolved = await catalogs.resolve({
|
|
2726
|
+
agent,
|
|
2727
|
+
cwd: mutation.cwd,
|
|
2728
|
+
config: mutation.config
|
|
2729
|
+
});
|
|
2730
|
+
try {
|
|
2731
|
+
validateAgentLaunchConfiguration(resolved.catalog, mutation.config);
|
|
2732
|
+
}
|
|
2733
|
+
catch (error) {
|
|
2734
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
2735
|
+
}
|
|
2736
|
+
return (candidate) => {
|
|
2737
|
+
if (candidate.agentId !== mutation.agentId
|
|
2738
|
+
|| resolve(candidate.cwd) !== resolve(mutation.cwd)
|
|
2739
|
+
|| !isDeepStrictEqual(candidate.config, mutation.config)) {
|
|
2740
|
+
throw usageError("Agent configuration changed after capability preflight; retry the command.");
|
|
2741
|
+
}
|
|
2742
|
+
try {
|
|
2743
|
+
validateAgentLaunchConfiguration(resolved.catalog, candidate.config);
|
|
2744
|
+
}
|
|
2745
|
+
catch (error) {
|
|
2746
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
function profileAgentConfigurationMutation(args, store) {
|
|
2751
|
+
return args[0] === "config" && args[1] === "profile"
|
|
2752
|
+
? previewProfileAgentConfigurationMutation(args.slice(2), store)
|
|
2753
|
+
: undefined;
|
|
2754
|
+
}
|
|
2755
|
+
function taskRoleAgentConfigurationMutation(args, store) {
|
|
2756
|
+
return args[0] === "task"
|
|
2757
|
+
? previewTaskRoleAgentConfigurationMutation(args.slice(1), store)
|
|
2758
|
+
: undefined;
|
|
2759
|
+
}
|
|
2760
|
+
async function warmConfigurationMutationCatalog(commandArgs, store, catalogs) {
|
|
2761
|
+
if (!hasModelOrEffortMutation(commandArgs))
|
|
2762
|
+
return;
|
|
2763
|
+
const agentId = configurationMutationAgentId(commandArgs, store);
|
|
2764
|
+
if (agentId === undefined)
|
|
2765
|
+
return;
|
|
2766
|
+
const agent = store.getConfiguredAgent(agentId);
|
|
2767
|
+
if (agent === null)
|
|
2768
|
+
return;
|
|
2769
|
+
await catalogs.resolve({
|
|
2770
|
+
agent,
|
|
2771
|
+
cwd: store.getConfig().defaultWorkspace ?? process.cwd()
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Ask the live Agent Host what the Session is running under, for a Session
|
|
2776
|
+
* inspect.
|
|
2777
|
+
*
|
|
2778
|
+
* Only for `task role session inspect`. Every other command reads persisted
|
|
2779
|
+
* state and must keep doing so: a query that reached into a running Session
|
|
2780
|
+
* would make reading a Task's records depend on whether an Agent happened to be
|
|
2781
|
+
* up.
|
|
2782
|
+
*
|
|
2783
|
+
* The request is the Host's existing `status` control, which reads state the
|
|
2784
|
+
* Host already holds. It creates no Session, sends no prompt and changes nothing
|
|
2785
|
+
* — which is what lets a read-only inspect use it at all.
|
|
2786
|
+
*
|
|
2787
|
+
* Every failure resolves to `unknown` with the reason rather than propagating:
|
|
2788
|
+
* no Host is running for most Sessions ever inspected, and the persisted facts
|
|
2789
|
+
* the command prints are worth showing regardless of whether a live one answered.
|
|
2790
|
+
*/
|
|
2791
|
+
async function liveHostObservationsForTaskCommand(args, store, home) {
|
|
2792
|
+
if (args[0] !== "task" || args[1] !== "role")
|
|
2793
|
+
return undefined;
|
|
2794
|
+
const inspect = args[2] === "session" && args[3] === "inspect" && args.length >= 6;
|
|
2795
|
+
const status = args[2] === "status" && args.length >= 5;
|
|
2796
|
+
if (!inspect && !status)
|
|
2797
|
+
return undefined;
|
|
2798
|
+
const taskId = args[inspect ? 4 : 3];
|
|
2799
|
+
const roles = [args[inspect ? 5 : 4]];
|
|
2800
|
+
const entries = await Promise.all(roles.map(async (roleName) => [
|
|
2801
|
+
roleName, await readLiveHostObservation(store, home, taskId, roleName)
|
|
2802
|
+
]));
|
|
2803
|
+
return Object.fromEntries(entries.filter((entry) => entry[1] !== undefined));
|
|
2804
|
+
}
|
|
2805
|
+
async function readLiveHostObservation(store, home, taskId, roleName) {
|
|
2806
|
+
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
2807
|
+
const active = sessions === null
|
|
2808
|
+
? undefined
|
|
2809
|
+
: sessions.sessions[sessions.activeAgentId];
|
|
2810
|
+
if (active === undefined)
|
|
2811
|
+
return undefined;
|
|
2812
|
+
if (active.status !== "active") {
|
|
2813
|
+
return { detail: `This Session is ${active.status}; no live Host reading was requested.` };
|
|
2814
|
+
}
|
|
2815
|
+
try {
|
|
2816
|
+
const snapshot = await inspectAgentHost({
|
|
2817
|
+
home,
|
|
2818
|
+
scope: "task",
|
|
2819
|
+
taskId,
|
|
2820
|
+
roleName
|
|
2821
|
+
});
|
|
2822
|
+
if (snapshot.nativeSessionId !== active.nativeSessionId
|
|
2823
|
+
|| snapshot.adapterId !== active.adapterId) {
|
|
2824
|
+
return { detail: "The Agent Host does not match the recorded Session." };
|
|
2825
|
+
}
|
|
2826
|
+
return { snapshot };
|
|
2827
|
+
}
|
|
2828
|
+
catch (error) {
|
|
2829
|
+
return { detail: `The Agent Host could not be reached: ${error instanceof Error ? error.message : String(error)}` };
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
function runConfigurationForHostObservation(host) {
|
|
2833
|
+
if (host === undefined)
|
|
2834
|
+
return undefined;
|
|
2835
|
+
return host.snapshot?.runConfiguration ?? unknownAgentRunConfiguration(host.detail ?? `Host=${host.snapshot?.state ?? "unknown"}; no live Agent configuration was reported.`);
|
|
2836
|
+
}
|
|
2837
|
+
function hasModelOrEffortMutation(args) {
|
|
2838
|
+
const operation = (args[0] === "config" && args[1] === "role")
|
|
2839
|
+
|| (args[0] === "task" && args[1] === "role");
|
|
2840
|
+
return operation && [
|
|
2841
|
+
"--model", "--effort", "--clear-model", "--clear-effort"
|
|
2842
|
+
].some((option) => args.includes(option));
|
|
2843
|
+
}
|
|
2844
|
+
function configurationMutationAgentId(args, store) {
|
|
2845
|
+
const explicit = optionValue(args, "--agent");
|
|
2846
|
+
if (explicit !== undefined)
|
|
2847
|
+
return explicit;
|
|
2848
|
+
if (args[0] === "config" && args[1] === "role" && args[2] === "update") {
|
|
2849
|
+
return store.getGlobalRole(args[3] ?? "")?.activeAgentId;
|
|
2850
|
+
}
|
|
2851
|
+
if (args[0] === "task" && args[1] === "role") {
|
|
2852
|
+
if (args[2] === "add")
|
|
2853
|
+
return store.getConfig().defaultAgent;
|
|
2854
|
+
if (args[2] === "update") {
|
|
2855
|
+
return store.getRole(args[3] ?? "", args[4] ?? "")?.activeAgentId;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return undefined;
|
|
2859
|
+
}
|
|
2860
|
+
function optionValue(args, option) {
|
|
2861
|
+
const index = args.lastIndexOf(option);
|
|
2862
|
+
const value = index < 0 ? undefined : args[index + 1];
|
|
2863
|
+
return typeof value === "string" && !value.startsWith("--") ? value : undefined;
|
|
2864
|
+
}
|
|
2865
|
+
function selectionCall(store, catalogs, method, params) {
|
|
2866
|
+
const reader = store;
|
|
2867
|
+
switch (method) {
|
|
2868
|
+
case "agent.list": return store.listConfiguredAgents();
|
|
2869
|
+
case "agent.capabilities": {
|
|
2870
|
+
const agent = store.getConfiguredAgent(String(params.agentId ?? ""));
|
|
2871
|
+
if (agent === null)
|
|
2872
|
+
return null;
|
|
2873
|
+
const configuredWorkspace = store.getConfig().defaultWorkspace;
|
|
2874
|
+
const cwd = typeof params.cwd === "string" && params.cwd.length > 0
|
|
2875
|
+
? params.cwd
|
|
2876
|
+
: configuredWorkspace ?? process.cwd();
|
|
2877
|
+
const config = typeof params.config === "object" && params.config !== null
|
|
2878
|
+
? params.config
|
|
2879
|
+
: undefined;
|
|
2880
|
+
return catalogs.resolve({
|
|
2881
|
+
agent,
|
|
2882
|
+
cwd,
|
|
2883
|
+
...(config === undefined ? {} : { config })
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
case "config.get": return store.getConfig();
|
|
2887
|
+
case "profile.list": return store.listAgentProfiles().map((profile) => profileSelectionRecord(profile, store));
|
|
2888
|
+
case "profile.show": {
|
|
2889
|
+
const profile = store.getAgentProfile(String(params.id ?? ""));
|
|
2890
|
+
return profile === null ? null : profileSelectionRecord(profile, store);
|
|
2891
|
+
}
|
|
2892
|
+
case "role.list": return store.listGlobalRoles();
|
|
2893
|
+
case "role.show": return store.getGlobalRole(String(params.name ?? ""));
|
|
2894
|
+
case "project.list": return callOptional(reader, "listProjects");
|
|
2895
|
+
case "task.list": return store.listTaskChoices();
|
|
2896
|
+
case "task.integration.list": return store.listIntegrationAttempts(String(params.taskId ?? ""));
|
|
2897
|
+
case "task.change-set.list": return store.listChangeSets(String(params.taskId ?? ""));
|
|
2898
|
+
case "task.role.list": return callOptional(reader, "listRoles", [params.taskId]);
|
|
2899
|
+
case "task.role.show": return callOptional(reader, "getRole", [params.taskId, params.roleName]);
|
|
2900
|
+
case "task.work.list": return callOptional(reader, "listWorkItems", [params.taskId]);
|
|
2901
|
+
case "task.message.list": return callOptional(reader, "listMessages", [params.taskId]);
|
|
2902
|
+
case "task.input.list": {
|
|
2903
|
+
const taskId = typeof params.taskId === "string" ? params.taskId : undefined;
|
|
2904
|
+
const requests = taskId === undefined
|
|
2905
|
+
? store.listAllInputRequests()
|
|
2906
|
+
: store.listInputRequests(taskId);
|
|
2907
|
+
return params.all === true ? requests : requests.filter((request) => request.status === "open");
|
|
2908
|
+
}
|
|
2909
|
+
case "task.turn.list": return callOptional(reader, "listRuns", [params.taskId]);
|
|
2910
|
+
case "task.decision.list": return callOptional(reader, "listDecisions", [params.taskId]);
|
|
2911
|
+
case "task.milestone.list": return presentSelectionTimes(callOptional(reader, "listMilestones", [params.taskId]), store);
|
|
2912
|
+
case "task.event.list": return presentSelectionTimes(callOptional(reader, "listEvents", [params.taskId]), store);
|
|
2913
|
+
case "jobs.list": return callOptional(reader, "listJobs");
|
|
2914
|
+
default: return [];
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
function profileSelectionRecord(profile, store) {
|
|
2918
|
+
const view = resolveAgentProfileView(profile, store);
|
|
2919
|
+
return {
|
|
2920
|
+
...view.profile,
|
|
2921
|
+
runtimeSource: view.runtime.source,
|
|
2922
|
+
...(view.runtime.source === "global-worker"
|
|
2923
|
+
? { workerRevision: view.runtime.workerRevision }
|
|
2924
|
+
: {}),
|
|
2925
|
+
effectiveRuntime: view.runtime,
|
|
2926
|
+
...(view.runtime.status === "resolved"
|
|
2927
|
+
? {
|
|
2928
|
+
agentId: view.runtime.binding.agentId,
|
|
2929
|
+
adapterId: view.runtime.binding.adapterId,
|
|
2930
|
+
model: view.runtime.binding.config.model,
|
|
2931
|
+
effort: view.runtime.binding.config.effort
|
|
2932
|
+
}
|
|
2933
|
+
: {})
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
function presentSelectionTimes(value, store) {
|
|
2937
|
+
if (!Array.isArray(value))
|
|
2938
|
+
return value;
|
|
2939
|
+
const timeZone = store.getConfig().timeZone;
|
|
2940
|
+
return value.map((record) => {
|
|
2941
|
+
if (typeof record !== "object" || record === null || Array.isArray(record))
|
|
2942
|
+
return record;
|
|
2943
|
+
const candidate = record;
|
|
2944
|
+
return typeof candidate.createdAt === "string"
|
|
2945
|
+
? {
|
|
2946
|
+
...candidate,
|
|
2947
|
+
createdAt: formatTimestamp(candidate.createdAt, timeZone)
|
|
2948
|
+
}
|
|
2949
|
+
: candidate;
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
function callOptional(reader, method, args = []) {
|
|
2953
|
+
const operation = reader[method];
|
|
2954
|
+
return operation === undefined ? [] : Reflect.apply(operation, reader, args);
|
|
2955
|
+
}
|
|
2956
|
+
function readableStore(home) {
|
|
2957
|
+
return openCurrentTaskStore(home);
|
|
2958
|
+
}
|
|
2959
|
+
function completionSelectionPorts(home) {
|
|
2960
|
+
if (inspectStorageSchema(home).status === "uninitialized") {
|
|
2961
|
+
return { call: () => [] };
|
|
2962
|
+
}
|
|
2963
|
+
const store = readableStore(home);
|
|
2964
|
+
const ports = selectionPorts(store, new AgentConfigurationCatalogService(home, { environment: process.env }));
|
|
2965
|
+
if (process.env.YUI_SESSION_SCOPE !== "task")
|
|
2966
|
+
return ports;
|
|
2967
|
+
const taskId = process.env.YUI_TASK_ID;
|
|
2968
|
+
const readable = contextRecordReader(store, taskId, process.env);
|
|
2969
|
+
const families = {
|
|
2970
|
+
"task.list": "task", "project.list": "project-policy", "task.role.list": "role",
|
|
2971
|
+
"task.work.list": "work-item", "task.message.list": "task-message", "task.input.list": "input-request",
|
|
2972
|
+
"task.turn.list": "run", "task.decision.list": "task-decision", "task.milestone.list": "task-milestone",
|
|
2973
|
+
"task.event.list": "task-event", "task.change-set.list": "change-set",
|
|
2974
|
+
"task.integration.list": "integration-attempt"
|
|
2975
|
+
};
|
|
2976
|
+
return { call: async (method, params) => {
|
|
2977
|
+
if (params.taskId !== undefined && params.taskId !== taskId)
|
|
2978
|
+
return [];
|
|
2979
|
+
const family = families[method];
|
|
2980
|
+
if (!family)
|
|
2981
|
+
return []; // Completion never needs broader authority than Context.
|
|
2982
|
+
const scopedParams = method === "task.input.list" ? { ...params, taskId } : params;
|
|
2983
|
+
const values = await ports.call(method, scopedParams);
|
|
2984
|
+
if (!Array.isArray(values))
|
|
2985
|
+
return [];
|
|
2986
|
+
return values.filter(value => {
|
|
2987
|
+
const id = String(value.id ?? value.name ?? "");
|
|
2988
|
+
if (method === "task.list" && id !== taskId)
|
|
2989
|
+
return false;
|
|
2990
|
+
if (method === "project.list" && !store.getTask(taskId)?.projectBindings.some(binding => binding.projectId === id))
|
|
2991
|
+
return false;
|
|
2992
|
+
return readable(family, id);
|
|
2993
|
+
});
|
|
2994
|
+
} };
|
|
2995
|
+
}
|
|
2996
|
+
function emit(output, literal = false, data) {
|
|
2997
|
+
const normalized = literal ? output.trimEnd() : output.trimEnd();
|
|
2998
|
+
process.stdout.write(`${jsonOutput
|
|
2999
|
+
? JSON.stringify(data === undefined
|
|
3000
|
+
? { ok: true, output: normalized }
|
|
3001
|
+
: { ok: true, data })
|
|
3002
|
+
: normalized}\n`);
|
|
3003
|
+
}
|
|
3004
|
+
function emitControlFailure(message, code, details) {
|
|
3005
|
+
process.exitCode = 2;
|
|
3006
|
+
const context = failureContext();
|
|
3007
|
+
const safeMessage = sanitizeFailureText(message.trim(), context);
|
|
3008
|
+
const safeDetails = sanitizeFailureDetails({ value: details }, context).value;
|
|
3009
|
+
process.stdout.write(`${jsonOutput
|
|
3010
|
+
? JSON.stringify({ ok: false, code, message: safeMessage, details: safeDetails })
|
|
3011
|
+
: safeMessage}\n`);
|
|
3012
|
+
}
|
|
3013
|
+
/** Domain-owned failure reports retain their output envelope and effect receipts. */
|
|
3014
|
+
function emitFailureResult(output, data) {
|
|
3015
|
+
const context = failureContext();
|
|
3016
|
+
emit(sanitizeFailureText(output, context), false, sanitizeFailureDetails({ value: data }, context).value);
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* The human line for a live steer, corrected to the *actual* live acceptance
|
|
3020
|
+
* (decision-3 §7). The store command's `base` line is written optimistically
|
|
3021
|
+
* ("Steering …"); only a proven `steered` keeps it. `steer-unknown` (pending) is
|
|
3022
|
+
* delivery-unknown, and a rejected/unavailable steer did not deliver — in both
|
|
3023
|
+
* cases the Message is retained and the operator must not reissue. There is no
|
|
3024
|
+
* retarget, queue, or interrupt fallback here; this only reports.
|
|
3025
|
+
*/
|
|
3026
|
+
function steerReceiptOutput(base, target, messageId, receipt) {
|
|
3027
|
+
if (receipt.state === "steered")
|
|
3028
|
+
return base;
|
|
3029
|
+
const detail = receipt.detail === undefined ? "" : ` ${receipt.detail}`;
|
|
3030
|
+
const head = receipt.state === "steer-unknown"
|
|
3031
|
+
? `Steer message ${messageId} to ${target} is delivery-unknown (${receipt.outcome}): the Host `
|
|
3032
|
+
+ "holds it but the Provider has not yet proven acceptance."
|
|
3033
|
+
: `Steer message ${messageId} to ${target} did not deliver (${receipt.outcome}).`;
|
|
3034
|
+
return `${head}${detail} The Message is retained; re-read the Session before acting, and do not `
|
|
3035
|
+
+ "reissue the same input under a new requestId or a different action.\n";
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* The human line for a live interrupt, corrected to `control.cancellation`
|
|
3039
|
+
* (decision-3 §7). Only a proven stop-request keeps the optimistic `base` line.
|
|
3040
|
+
* `not-active`/`unknown`/unavailable each report that nothing was proven stopped;
|
|
3041
|
+
* no process is ever killed. A then-handoff, if any, was already claimed durably
|
|
3042
|
+
* and is delivered once by the ordinary continuation path after a proven terminal.
|
|
3043
|
+
*/
|
|
3044
|
+
function interruptReceiptOutput(base, target, receipt) {
|
|
3045
|
+
if (receipt.state === "interrupt-requested")
|
|
3046
|
+
return `${base.trim()}\nNative cancel requested; Turn termination is not yet proven.\n`;
|
|
3047
|
+
const detail = receipt.detail === undefined ? "" : ` ${receipt.detail}`;
|
|
3048
|
+
const reason = receipt.state === "interrupt-not-active"
|
|
3049
|
+
? "found no active Turn to stop (not-active)"
|
|
3050
|
+
: receipt.state === "interrupt-unknown"
|
|
3051
|
+
? "could not prove a stop (unknown)"
|
|
3052
|
+
: `did not complete (${receipt.outcome})`;
|
|
3053
|
+
return `Interrupt of ${target} ${reason}.${detail} No process was killed; re-read the Session `
|
|
3054
|
+
+ "before retrying.\n";
|
|
3055
|
+
}
|
|
3056
|
+
function withControllerRefreshWarning(output, refresh, label) {
|
|
3057
|
+
if (refresh.status !== "failed")
|
|
3058
|
+
return output;
|
|
3059
|
+
if (label === "Agent environment") {
|
|
3060
|
+
return `${output.trimEnd()}\nWarning: Agent configuration was saved, but its current `
|
|
3061
|
+
+ `environment values were not applied or persisted (${refresh.message}). Retry the `
|
|
3062
|
+
+ "Agent command with those variables present, or restart the Controller from an "
|
|
3063
|
+
+ "environment that provides them.\n";
|
|
3064
|
+
}
|
|
3065
|
+
return `${output.trimEnd()}\nWarning: ${label} was saved, but the running Controller `
|
|
3066
|
+
+ `could not be refreshed (${refresh.message}). Restart the Controller to apply it.\n`;
|
|
3067
|
+
}
|
|
3068
|
+
function agentEnvironmentRefreshScope(previous, current, configured) {
|
|
3069
|
+
const retainedSources = new Set(configured.flatMap((agent) => (agent.environment.map((binding) => binding.sourceName))));
|
|
3070
|
+
const retainedNative = new Set(configured.flatMap((agent) => (nativeAgentEnvironmentNames(agent.adapterId))));
|
|
3071
|
+
const currentSources = current?.environment.map((binding) => binding.sourceName) ?? [];
|
|
3072
|
+
const previousOnlySources = previous?.environment
|
|
3073
|
+
.map((binding) => binding.sourceName)
|
|
3074
|
+
.filter((name) => !retainedSources.has(name)) ?? [];
|
|
3075
|
+
const currentNative = current === null ? [] : nativeAgentEnvironmentNames(current.adapterId);
|
|
3076
|
+
const previousOnlyNative = previous === null
|
|
3077
|
+
? []
|
|
3078
|
+
: nativeAgentEnvironmentNames(previous.adapterId).filter((name) => !retainedNative.has(name));
|
|
3079
|
+
return {
|
|
3080
|
+
sourceNames: [...new Set([...currentSources, ...previousOnlySources])],
|
|
3081
|
+
nativeNames: [...new Set([...currentNative, ...previousOnlyNative])]
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
function normalizeAliases(input) {
|
|
3085
|
+
const normalized = [...input];
|
|
3086
|
+
if (normalized.length === 1 && (normalized[0] === "-v" || normalized[0] === "--version")) {
|
|
3087
|
+
return ["version"];
|
|
3088
|
+
}
|
|
3089
|
+
const help = normalized.findIndex((argument) => argument === "-h" || argument === "--help");
|
|
3090
|
+
return help === normalized.length - 1 ? ["help", ...normalized.slice(0, help)] : normalized;
|
|
3091
|
+
}
|
|
3092
|
+
export { renderCompletion };
|