@zq-silk/yui 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export function createProject(id, name, path, branches, now, metadata = {}) {
|
|
3
|
+
const timestamp = now.toISOString();
|
|
4
|
+
return validateProject({
|
|
5
|
+
schemaVersion: 2,
|
|
6
|
+
id: requireIdentity(id, "Project id"),
|
|
7
|
+
name: validateProjectName(name),
|
|
8
|
+
aliases: normalizeAliases(metadata.aliases ?? [], name),
|
|
9
|
+
path: resolve(requireText(path, "Project path")),
|
|
10
|
+
...(metadata.remoteUrl === undefined
|
|
11
|
+
? {}
|
|
12
|
+
: { remoteUrl: requireText(metadata.remoteUrl, "Project remote URL") }),
|
|
13
|
+
stableBranch: requireGitRef(branches.stable, "Project stable branch"),
|
|
14
|
+
developmentBranch: requireGitRef(branches.development, "Project development branch"),
|
|
15
|
+
knowledge: [],
|
|
16
|
+
createdAt: timestamp,
|
|
17
|
+
updatedAt: timestamp
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function validateProjectName(value) {
|
|
21
|
+
return requireIdentity(value, "Project name");
|
|
22
|
+
}
|
|
23
|
+
export function addProjectKnowledge(project, id, title, body, now) {
|
|
24
|
+
const timestamp = now.toISOString();
|
|
25
|
+
const knowledge = {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
id: requireIdentity(id, "Project knowledge id"),
|
|
28
|
+
title: requireText(title, "Project knowledge title"),
|
|
29
|
+
body: requireText(body, "Project knowledge body"),
|
|
30
|
+
status: "active",
|
|
31
|
+
createdAt: timestamp,
|
|
32
|
+
updatedAt: timestamp
|
|
33
|
+
};
|
|
34
|
+
if (project.knowledge.some((entry) => entry.id === knowledge.id)) {
|
|
35
|
+
throw new Error(`Project knowledge already exists: ${knowledge.id}.`);
|
|
36
|
+
}
|
|
37
|
+
return validateProject({
|
|
38
|
+
...project,
|
|
39
|
+
knowledge: [...project.knowledge, knowledge],
|
|
40
|
+
updatedAt: timestamp
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function updateProjectMetadata(project, patch, now) {
|
|
44
|
+
const remoteUrl = patch.remoteUrl === undefined
|
|
45
|
+
? project.remoteUrl
|
|
46
|
+
: patch.remoteUrl === null
|
|
47
|
+
? undefined
|
|
48
|
+
: requireText(patch.remoteUrl, "Project remote URL");
|
|
49
|
+
const { remoteUrl: _remoteUrl, ...withoutRemote } = project;
|
|
50
|
+
return validateProject({
|
|
51
|
+
...withoutRemote,
|
|
52
|
+
aliases: normalizeAliases(patch.aliases ?? project.aliases, project.name),
|
|
53
|
+
...(remoteUrl === undefined ? {} : { remoteUrl }),
|
|
54
|
+
stableBranch: patch.stableBranch === undefined
|
|
55
|
+
? project.stableBranch
|
|
56
|
+
: requireGitRef(patch.stableBranch, "Project stable branch"),
|
|
57
|
+
developmentBranch: patch.developmentBranch === undefined
|
|
58
|
+
? project.developmentBranch
|
|
59
|
+
: requireGitRef(patch.developmentBranch, "Project development branch"),
|
|
60
|
+
updatedAt: now.toISOString()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function updateProjectKnowledge(project, id, patch, now) {
|
|
64
|
+
const knowledgeId = requireIdentity(id, "Project knowledge id");
|
|
65
|
+
let found = false;
|
|
66
|
+
const knowledge = project.knowledge.map((entry) => {
|
|
67
|
+
if (entry.id !== knowledgeId)
|
|
68
|
+
return entry;
|
|
69
|
+
found = true;
|
|
70
|
+
if (entry.status === "retired") {
|
|
71
|
+
throw new Error(`Project knowledge is retired: ${knowledgeId}.`);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...entry,
|
|
75
|
+
title: patch.title === undefined
|
|
76
|
+
? entry.title
|
|
77
|
+
: requireText(patch.title, "Project knowledge title"),
|
|
78
|
+
body: patch.body === undefined
|
|
79
|
+
? entry.body
|
|
80
|
+
: requireText(patch.body, "Project knowledge body"),
|
|
81
|
+
updatedAt: now.toISOString()
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
if (!found)
|
|
85
|
+
throw new Error(`Project knowledge not found: ${knowledgeId}.`);
|
|
86
|
+
return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
|
|
87
|
+
}
|
|
88
|
+
export function retireProjectKnowledge(project, id, now) {
|
|
89
|
+
const knowledgeId = requireIdentity(id, "Project knowledge id");
|
|
90
|
+
let found = false;
|
|
91
|
+
const knowledge = project.knowledge.map((entry) => {
|
|
92
|
+
if (entry.id !== knowledgeId)
|
|
93
|
+
return entry;
|
|
94
|
+
found = true;
|
|
95
|
+
return {
|
|
96
|
+
...entry,
|
|
97
|
+
status: "retired",
|
|
98
|
+
updatedAt: now.toISOString()
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
if (!found)
|
|
102
|
+
throw new Error(`Project knowledge not found: ${knowledgeId}.`);
|
|
103
|
+
return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
|
|
104
|
+
}
|
|
105
|
+
export function resolveProject(projects, reference) {
|
|
106
|
+
const normalized = requireText(reference, "Project reference").toLocaleLowerCase();
|
|
107
|
+
const matches = projects.filter((project) => (project.id.toLocaleLowerCase() === normalized
|
|
108
|
+
|| project.name.toLocaleLowerCase() === normalized
|
|
109
|
+
|| project.aliases.some((alias) => alias.toLocaleLowerCase() === normalized)));
|
|
110
|
+
if (matches.length > 1)
|
|
111
|
+
throw new Error(`Project reference is ambiguous: ${reference}.`);
|
|
112
|
+
return matches[0] ?? null;
|
|
113
|
+
}
|
|
114
|
+
export function assertProjectCatalog(projects) {
|
|
115
|
+
const paths = new Map();
|
|
116
|
+
const references = new Map();
|
|
117
|
+
for (const project of projects) {
|
|
118
|
+
validateProject(project);
|
|
119
|
+
const pathOwner = paths.get(project.path);
|
|
120
|
+
if (pathOwner !== undefined && pathOwner !== project.id) {
|
|
121
|
+
throw new Error(`Project path is already registered: ${project.path}.`);
|
|
122
|
+
}
|
|
123
|
+
paths.set(project.path, project.id);
|
|
124
|
+
for (const reference of [project.id, project.name, ...project.aliases]) {
|
|
125
|
+
const key = reference.toLocaleLowerCase();
|
|
126
|
+
const owner = references.get(key);
|
|
127
|
+
if (owner !== undefined && owner !== project.id) {
|
|
128
|
+
throw new Error(`Project reference is already registered: ${reference}.`);
|
|
129
|
+
}
|
|
130
|
+
references.set(key, project.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function validateProject(project) {
|
|
135
|
+
if (project.schemaVersion !== 2) {
|
|
136
|
+
throw new Error("Project must use schemaVersion 2.");
|
|
137
|
+
}
|
|
138
|
+
requireIdentity(project.id, "Project id");
|
|
139
|
+
requireIdentity(project.name, "Project name");
|
|
140
|
+
normalizeAliases(project.aliases, project.name);
|
|
141
|
+
const references = new Set();
|
|
142
|
+
for (const reference of [project.id, project.name, ...project.aliases]) {
|
|
143
|
+
const folded = reference.toLocaleLowerCase();
|
|
144
|
+
if (references.has(folded)) {
|
|
145
|
+
throw new Error(`Duplicate Project reference: ${reference}.`);
|
|
146
|
+
}
|
|
147
|
+
references.add(folded);
|
|
148
|
+
}
|
|
149
|
+
if (resolve(requireText(project.path, "Project path")) !== project.path) {
|
|
150
|
+
throw new Error("Project path must be absolute and normalized.");
|
|
151
|
+
}
|
|
152
|
+
if (project.remoteUrl !== undefined)
|
|
153
|
+
requireText(project.remoteUrl, "Project remote URL");
|
|
154
|
+
requireGitRef(project.stableBranch, "Project stable branch");
|
|
155
|
+
requireGitRef(project.developmentBranch, "Project development branch");
|
|
156
|
+
const knowledgeIds = new Set();
|
|
157
|
+
for (const entry of project.knowledge) {
|
|
158
|
+
if (entry.schemaVersion !== 1)
|
|
159
|
+
throw new Error("Project knowledge must use schemaVersion 1.");
|
|
160
|
+
requireIdentity(entry.id, "Project knowledge id");
|
|
161
|
+
requireText(entry.title, "Project knowledge title");
|
|
162
|
+
requireText(entry.body, "Project knowledge body");
|
|
163
|
+
if (!["active", "retired"].includes(entry.status)) {
|
|
164
|
+
throw new Error("Project knowledge status is invalid.");
|
|
165
|
+
}
|
|
166
|
+
requireTimestamp(entry.createdAt, "Project knowledge createdAt");
|
|
167
|
+
requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
|
|
168
|
+
if (knowledgeIds.has(entry.id))
|
|
169
|
+
throw new Error(`Duplicate Project knowledge id: ${entry.id}.`);
|
|
170
|
+
knowledgeIds.add(entry.id);
|
|
171
|
+
}
|
|
172
|
+
requireTimestamp(project.createdAt, "Project createdAt");
|
|
173
|
+
requireTimestamp(project.updatedAt, "Project updatedAt");
|
|
174
|
+
return project;
|
|
175
|
+
}
|
|
176
|
+
function normalizeAliases(values, name) {
|
|
177
|
+
const aliases = values.map((value) => requireIdentity(value, "Project alias"));
|
|
178
|
+
const folded = new Set();
|
|
179
|
+
for (const value of [name, ...aliases]) {
|
|
180
|
+
const key = value.toLocaleLowerCase();
|
|
181
|
+
if (folded.has(key))
|
|
182
|
+
throw new Error(`Duplicate Project name or alias: ${value}.`);
|
|
183
|
+
folded.add(key);
|
|
184
|
+
}
|
|
185
|
+
return Object.freeze([...aliases]);
|
|
186
|
+
}
|
|
187
|
+
function requireIdentity(value, label) {
|
|
188
|
+
const normalized = requireText(value, label);
|
|
189
|
+
if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
|
|
190
|
+
|| /[\/\\\0]/.test(normalized)) {
|
|
191
|
+
throw new Error(`${label} is invalid.`);
|
|
192
|
+
}
|
|
193
|
+
return normalized;
|
|
194
|
+
}
|
|
195
|
+
function requireGitRef(value, label) {
|
|
196
|
+
const ref = requireText(value, label);
|
|
197
|
+
if (ref.startsWith("-") || /[\r\n]/.test(ref))
|
|
198
|
+
throw new Error(`${label} is invalid.`);
|
|
199
|
+
return ref;
|
|
200
|
+
}
|
|
201
|
+
function requireText(value, label) {
|
|
202
|
+
if (typeof value !== "string" || value.includes("\0"))
|
|
203
|
+
throw new Error(`${label} is invalid.`);
|
|
204
|
+
const normalized = value.trim();
|
|
205
|
+
if (normalized.length === 0)
|
|
206
|
+
throw new Error(`${label} is required.`);
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
function requireTimestamp(value, label) {
|
|
210
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
211
|
+
throw new Error(`${label} is invalid.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { retireConfirmedAbsentInactiveTaskRolePlaceholders } from "../executor/agentExecutor.js";
|
|
3
|
+
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
|
+
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
5
|
+
import { WorkspaceCleanupBlockedError } from "./taskWorkspacePreparer.js";
|
|
6
|
+
export { WorkspaceCleanupBlockedError } from "./taskWorkspacePreparer.js";
|
|
7
|
+
/**
|
|
8
|
+
* Coordinates persisted Role cwd changes with the runtime that may still be
|
|
9
|
+
* executing in the previous directory.
|
|
10
|
+
*/
|
|
11
|
+
export class TaskWorkspaceCoordinator {
|
|
12
|
+
store;
|
|
13
|
+
preparer;
|
|
14
|
+
runtime;
|
|
15
|
+
constructor(store, preparer, runtime) {
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.preparer = preparer;
|
|
18
|
+
this.runtime = runtime;
|
|
19
|
+
}
|
|
20
|
+
async isolateWorkItem(taskId, workItemId) {
|
|
21
|
+
const item = this.store.getWorkItem(taskId, workItemId);
|
|
22
|
+
if (item === null)
|
|
23
|
+
throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
|
|
24
|
+
const assignee = this.#workItemIsolationAssignee(item);
|
|
25
|
+
const activeDevelopRun = this.store.listAgentRuns(item.taskId)
|
|
26
|
+
.find((run) => run.status === "active" && run.workItemId === item.id);
|
|
27
|
+
if (activeDevelopRun !== undefined) {
|
|
28
|
+
throw new Error(`Work Item already has an active Develop Run: ${activeDevelopRun.id}.`);
|
|
29
|
+
}
|
|
30
|
+
const task = this.store.getTask(item.taskId);
|
|
31
|
+
const taskProjectIds = task.projectBindings.map(({ projectId }) => projectId);
|
|
32
|
+
const existing = this.store.getWorkItemWorkspace(item.taskId, item.id);
|
|
33
|
+
if (existing !== null
|
|
34
|
+
&& existing.owner.type === "work-item"
|
|
35
|
+
&& existing.owner.workItemId === item.id
|
|
36
|
+
&& sameProjectScope(existing, taskProjectIds, item.writeProjectIds)
|
|
37
|
+
&& item.baseRefs === undefined)
|
|
38
|
+
return existing;
|
|
39
|
+
if (existing !== null) {
|
|
40
|
+
if (existing.owner.type !== "work-item" || existing.owner.workItemId !== item.id) {
|
|
41
|
+
throw new Error(`WorkItem already has another managed workspace: ${item.taskId}/${item.id}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await this.preparer.prepareTaskWorkspace(item.taskId);
|
|
45
|
+
const prepared = this.store.getWorkItemWorkspace(item.taskId, item.id);
|
|
46
|
+
if (prepared !== null
|
|
47
|
+
&& prepared.owner.type === "work-item"
|
|
48
|
+
&& prepared.owner.workItemId === item.id
|
|
49
|
+
&& sameProjectScope(prepared, taskProjectIds, item.writeProjectIds)
|
|
50
|
+
&& item.baseRefs === undefined)
|
|
51
|
+
return prepared;
|
|
52
|
+
if (prepared !== null) {
|
|
53
|
+
if (prepared.owner.type !== "work-item" || prepared.owner.workItemId !== item.id) {
|
|
54
|
+
throw new Error(`WorkItem already has another managed workspace: ${item.taskId}/${item.id}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (assignee !== undefined)
|
|
58
|
+
await this.#stopLiveRoles(item.taskId, [assignee]);
|
|
59
|
+
return this.preparer.prepareWorkItemWorkspace(item.taskId, item.id);
|
|
60
|
+
}
|
|
61
|
+
async cleanupWorkItem(taskId, workItemId, disposition) {
|
|
62
|
+
const item = this.store.getWorkItem(taskId, workItemId);
|
|
63
|
+
if (item === null)
|
|
64
|
+
throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
|
|
65
|
+
if (!isTerminalWorkItem(item)) {
|
|
66
|
+
throw new Error(`Work item must be terminal before cleanup: ${item.id}.`);
|
|
67
|
+
}
|
|
68
|
+
if (item.workspaceDisposition !== undefined
|
|
69
|
+
&& item.workspaceDisposition !== disposition) {
|
|
70
|
+
throw new Error(`Work item workspace is already recorded as ${item.workspaceDisposition}.`);
|
|
71
|
+
}
|
|
72
|
+
if (item.workspaceDisposition === disposition)
|
|
73
|
+
return "missing";
|
|
74
|
+
const state = await this.preparer.inspectWorkItemWorkspace(item.taskId, item.id);
|
|
75
|
+
if (state === "dirty")
|
|
76
|
+
return "dirty";
|
|
77
|
+
this.#assertWorkItemRuntimeQuiescent(item);
|
|
78
|
+
await this.#stopLiveRoles(item.taskId, this.#workItemRoleNames(item));
|
|
79
|
+
const laneCleanup = await this.preparer.cleanupExecutionLaneWorkspacesForWorkItem(item.taskId, item.id);
|
|
80
|
+
if (laneCleanup === "dirty")
|
|
81
|
+
return "dirty";
|
|
82
|
+
return this.preparer.cleanupWorkItemWorkspace(item.taskId, item.id, disposition);
|
|
83
|
+
}
|
|
84
|
+
async cleanupWorkItemRuntime(taskId, workItemId) {
|
|
85
|
+
const item = this.store.getWorkItem(taskId, workItemId);
|
|
86
|
+
if (item === null)
|
|
87
|
+
throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
|
|
88
|
+
this.#assertWorkItemRuntimeQuiescent(item);
|
|
89
|
+
await this.#stopLiveRoles(item.taskId, this.#workItemRoleNames(item));
|
|
90
|
+
return "released";
|
|
91
|
+
}
|
|
92
|
+
async cleanupReviewRound(taskId, reviewRoundId) {
|
|
93
|
+
const round = this.store.getReviewRound(taskId, reviewRoundId);
|
|
94
|
+
if (round === null)
|
|
95
|
+
throw new Error(`ReviewRound not found: ${taskId}/${reviewRoundId}.`);
|
|
96
|
+
if (round.status !== "completed" && round.status !== "failed") {
|
|
97
|
+
throw new Error(`ReviewRound must be terminal before cleanup: ${round.id}.`);
|
|
98
|
+
}
|
|
99
|
+
const state = await this.preparer.inspectReviewRoundWorkspace(taskId, reviewRoundId);
|
|
100
|
+
if (state === "dirty")
|
|
101
|
+
return "dirty";
|
|
102
|
+
await this.#stopLiveRoles(taskId, this.#reviewRoundRoleNames(round));
|
|
103
|
+
const laneCleanup = await this.preparer.cleanupExecutionLaneWorkspacesForReviewRound(taskId, reviewRoundId);
|
|
104
|
+
if (laneCleanup === "dirty")
|
|
105
|
+
return "dirty";
|
|
106
|
+
return this.preparer.cleanupReviewRoundWorkspace(taskId, reviewRoundId);
|
|
107
|
+
}
|
|
108
|
+
async cleanupTaskForArchive(taskId, disposition) {
|
|
109
|
+
try {
|
|
110
|
+
const task = this.store.getTask(taskId);
|
|
111
|
+
if (task === null)
|
|
112
|
+
throw new Error(`Task not found: ${taskId}.`);
|
|
113
|
+
if (task.status !== "completed" && task.status !== "retired") {
|
|
114
|
+
throw new Error(`Task must be completed or retired before archive cleanup: ${task.id}.`);
|
|
115
|
+
}
|
|
116
|
+
const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
|
|
117
|
+
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
118
|
+
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
119
|
+
const laneWorkspaces = managedWorkspaces.filter(({ owner }) => owner.type === "execution-lane");
|
|
120
|
+
const workspaces = managedWorkspaces
|
|
121
|
+
.filter(({ owner }) => owner.type === "work-item");
|
|
122
|
+
const workItems = workspaces.map((workspace) => {
|
|
123
|
+
const workItemId = workspace.owner.type === "work-item"
|
|
124
|
+
? workspace.owner.workItemId
|
|
125
|
+
: "";
|
|
126
|
+
const item = this.store.getWorkItem(task.id, workItemId);
|
|
127
|
+
if (item === null)
|
|
128
|
+
throw new Error(`Work item not found: ${task.id}/${workItemId}.`);
|
|
129
|
+
if (item.status !== "completed" && item.status !== "retired") {
|
|
130
|
+
throw new Error(`Work item must be completed or retired before archive cleanup: ${item.id}.`);
|
|
131
|
+
}
|
|
132
|
+
return item;
|
|
133
|
+
});
|
|
134
|
+
const allWorkItems = [...this.store.listWorkItems(task.id)]
|
|
135
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
136
|
+
const allReviewRounds = [...this.store.listReviewRounds(task.id)]
|
|
137
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
138
|
+
const reviewRounds = allReviewRounds.filter((round) => (round.workspace !== undefined && round.workspaceDisposition?.kind !== "removed"));
|
|
139
|
+
const snapshot = {
|
|
140
|
+
task,
|
|
141
|
+
managedWorkspaces,
|
|
142
|
+
workItems: allWorkItems,
|
|
143
|
+
reviewRounds: allReviewRounds
|
|
144
|
+
};
|
|
145
|
+
for (const round of reviewRounds) {
|
|
146
|
+
if (round.status !== "completed" && round.status !== "failed") {
|
|
147
|
+
throw new Error(`ReviewRound must be terminal before archive cleanup: ${round.id}.`);
|
|
148
|
+
}
|
|
149
|
+
if (await this.preparer.inspectReviewRoundWorkspace(task.id, round.id) === "dirty") {
|
|
150
|
+
return {
|
|
151
|
+
taskId,
|
|
152
|
+
status: "retained-dirty",
|
|
153
|
+
...(task.cwd === undefined ? {} : { path: task.cwd }),
|
|
154
|
+
error: `ReviewRound worktree is dirty: ${round.id}.`,
|
|
155
|
+
reason: "dirty-worktree",
|
|
156
|
+
resource: `review-round:${task.id}/${round.id}`,
|
|
157
|
+
retryable: true
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const item of workItems) {
|
|
162
|
+
if (await this.preparer.inspectWorkItemWorkspace(task.id, item.id) === "dirty") {
|
|
163
|
+
return {
|
|
164
|
+
taskId,
|
|
165
|
+
status: "retained-dirty",
|
|
166
|
+
...(task.cwd === undefined ? {} : { path: task.cwd }),
|
|
167
|
+
error: `WorkItem worktree is dirty: ${item.id}.`,
|
|
168
|
+
reason: "dirty-worktree",
|
|
169
|
+
resource: `work-item:${task.id}/${item.id}`,
|
|
170
|
+
retryable: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const workspace of laneWorkspaces) {
|
|
175
|
+
if (workspace.owner.type !== "execution-lane")
|
|
176
|
+
continue;
|
|
177
|
+
if (await this.preparer.inspectExecutionLaneWorkspace(task.id, workspace.owner.executionGroupId, workspace.owner.executionLaneId) === "dirty") {
|
|
178
|
+
return {
|
|
179
|
+
taskId,
|
|
180
|
+
status: "retained-dirty",
|
|
181
|
+
...(task.cwd === undefined ? {} : { path: task.cwd }),
|
|
182
|
+
error: `Execution Lane worktree is dirty: ${managedWorkspaceKey(workspace.owner)}.`,
|
|
183
|
+
reason: "dirty-worktree",
|
|
184
|
+
resource: managedWorkspaceKey(workspace.owner),
|
|
185
|
+
retryable: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const state = await this.preparer.inspectTaskMainWorkspace(taskId);
|
|
190
|
+
if (state === "dirty") {
|
|
191
|
+
return {
|
|
192
|
+
taskId,
|
|
193
|
+
status: "retained-dirty",
|
|
194
|
+
...(task.cwd === undefined ? {} : { path: task.cwd }),
|
|
195
|
+
error: `Task main worktree is dirty: ${task.id}.`,
|
|
196
|
+
reason: "dirty-worktree",
|
|
197
|
+
resource: `task-worktree:${task.id}`,
|
|
198
|
+
retryable: true
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const activeRole = this.store.listRoles(taskId)
|
|
202
|
+
.find((role) => this.store.getActiveAgentRun(taskId, role.name) !== null);
|
|
203
|
+
if (activeRole !== undefined) {
|
|
204
|
+
throw new WorkspaceCleanupBlockedError("active-run", `role:${task.id}/${activeRole.name}`, true, `Task Role still has an active Run: ${task.id}/${activeRole.name}.`);
|
|
205
|
+
}
|
|
206
|
+
const roleNames = this.store.listRoles(taskId).map(({ name }) => name);
|
|
207
|
+
if (roleNames.length > 0) {
|
|
208
|
+
await this.runtime.stopTaskRoleSessions(taskId, roleNames);
|
|
209
|
+
}
|
|
210
|
+
this.#assertTaskArchiveSnapshot(snapshot);
|
|
211
|
+
for (const workspace of laneWorkspaces) {
|
|
212
|
+
this.#assertTaskArchiveLifecycle(task);
|
|
213
|
+
if (workspace.owner.type === "execution-lane") {
|
|
214
|
+
await this.preparer.cleanupExecutionLaneWorkspace(task.id, workspace.owner.executionGroupId, workspace.owner.executionLaneId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const round of reviewRounds) {
|
|
218
|
+
this.#assertTaskArchiveLifecycle(task);
|
|
219
|
+
await this.preparer.cleanupReviewRoundWorkspace(task.id, round.id);
|
|
220
|
+
}
|
|
221
|
+
for (const item of workItems) {
|
|
222
|
+
this.#assertTaskArchiveLifecycle(task);
|
|
223
|
+
await this.preparer.cleanupWorkItemWorkspace(task.id, item.id, item.status === "completed" ? disposition : "abandoned");
|
|
224
|
+
}
|
|
225
|
+
this.#assertTaskArchiveLifecycle(task);
|
|
226
|
+
return this.preparer.cleanupTaskForArchive(taskId);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
const task = this.store.getTask(taskId);
|
|
230
|
+
return {
|
|
231
|
+
taskId,
|
|
232
|
+
status: "failed",
|
|
233
|
+
...(task?.cwd === undefined ? {} : { path: task.cwd }),
|
|
234
|
+
error: error instanceof Error ? error.message : String(error),
|
|
235
|
+
...(error instanceof WorkspaceCleanupBlockedError
|
|
236
|
+
? {
|
|
237
|
+
reason: error.reason,
|
|
238
|
+
resource: error.resource,
|
|
239
|
+
retryable: error.retryable
|
|
240
|
+
}
|
|
241
|
+
: {
|
|
242
|
+
reason: "cleanup-failed",
|
|
243
|
+
resource: `task:${taskId}`,
|
|
244
|
+
retryable: true
|
|
245
|
+
})
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
#assertTaskArchiveSnapshot(snapshot) {
|
|
250
|
+
this.#assertTaskArchiveLifecycle(snapshot.task);
|
|
251
|
+
const managedWorkspaces = [...this.store.listManagedWorkspaces(snapshot.task.id)]
|
|
252
|
+
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
253
|
+
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
254
|
+
const workItems = [...this.store.listWorkItems(snapshot.task.id)]
|
|
255
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
256
|
+
const reviewRounds = [...this.store.listReviewRounds(snapshot.task.id)]
|
|
257
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
258
|
+
if (!isDeepStrictEqual(managedWorkspaces, snapshot.managedWorkspaces)
|
|
259
|
+
|| !isDeepStrictEqual(workItems, snapshot.workItems)
|
|
260
|
+
|| !isDeepStrictEqual(reviewRounds, snapshot.reviewRounds)) {
|
|
261
|
+
throw new WorkspaceCleanupBlockedError("task-changed", `task:${snapshot.task.id}`, true, `Task resources changed while archive cleanup stopped runtimes: ${snapshot.task.id}.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
#assertTaskArchiveLifecycle(expected) {
|
|
265
|
+
const current = this.store.getTask(expected.id);
|
|
266
|
+
if (current === null || !isDeepStrictEqual(current, expected)) {
|
|
267
|
+
throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
#assertWorkItemRuntimeQuiescent(item) {
|
|
271
|
+
const activeRun = this.store.listAgentRuns(item.taskId)
|
|
272
|
+
.find((run) => run.status === "active" && run.workItemId === item.id);
|
|
273
|
+
if (activeRun !== undefined) {
|
|
274
|
+
throw new WorkspaceCleanupBlockedError("active-run", `work-item:${item.taskId}/${item.id}`, true, `Work item still has an active Run: ${item.taskId}/${item.id}.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
#workItemRoleNames(item) {
|
|
278
|
+
return [
|
|
279
|
+
...(item.assignee === undefined ? [] : [item.assignee]),
|
|
280
|
+
...(item.executionGroups.flatMap((group) => group.lanes.map(({ roleName }) => roleName)))
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
#reviewRoundRoleNames(round) {
|
|
284
|
+
return [
|
|
285
|
+
round.reviewerRoleName,
|
|
286
|
+
...(round.executionGroup?.lanes.map(({ roleName }) => roleName) ?? [])
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
async #stopLiveRoles(taskId, roleNames) {
|
|
290
|
+
const targets = [...new Set(roleNames)];
|
|
291
|
+
const getActiveRun = this.store.getActiveAgentRun?.bind(this.store);
|
|
292
|
+
for (const roleName of targets) {
|
|
293
|
+
if (getActiveRun !== undefined && getActiveRun(taskId, roleName) !== null) {
|
|
294
|
+
throw new Error(`Role has an active Run: ${taskId}/${roleName}.`);
|
|
295
|
+
}
|
|
296
|
+
const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
297
|
+
if (sessions?.inFlight !== null && sessions?.inFlight !== undefined) {
|
|
298
|
+
throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
|
|
299
|
+
}
|
|
300
|
+
if (sessions?.pendingTurnCompletion !== null
|
|
301
|
+
&& sessions?.pendingTurnCompletion !== undefined) {
|
|
302
|
+
throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
|
|
303
|
+
}
|
|
304
|
+
if (this.store.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
305
|
+
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const inspect = this.runtime.inspectTaskRolePanes?.bind(this.runtime);
|
|
309
|
+
const observedPanes = inspect?.(taskId);
|
|
310
|
+
const live = targets.filter((roleName) => {
|
|
311
|
+
const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
312
|
+
const activeSession = sessions === null
|
|
313
|
+
? undefined
|
|
314
|
+
: sessions.activeAgentId === undefined
|
|
315
|
+
// Narrow test doubles and restored callers predating activeAgentId
|
|
316
|
+
// still conservatively represent any nonterminal record as live.
|
|
317
|
+
? Object.values(sessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken")
|
|
318
|
+
: sessions.sessions[sessions.activeAgentId];
|
|
319
|
+
return observedPanes?.some((pane) => pane.roleName === roleName && !pane.dead) === true
|
|
320
|
+
|| (sessions !== null && activeSession !== undefined
|
|
321
|
+
&& activeSession.status !== "stopped"
|
|
322
|
+
&& activeSession.status !== "broken");
|
|
323
|
+
});
|
|
324
|
+
if (live.length > 0)
|
|
325
|
+
await this.runtime.stopTaskRoleSessions(taskId, live);
|
|
326
|
+
// The aggregate-16 dormant Claude placeholder is the sole exception to
|
|
327
|
+
// strict workspace-session retirement. The synchronous pane inspection is
|
|
328
|
+
// performed while holding the Task store transaction so a normal launch
|
|
329
|
+
// cannot reserve a Run/Session between absence proof and terminalization.
|
|
330
|
+
if (inspect === undefined || this.store.transaction === undefined)
|
|
331
|
+
return;
|
|
332
|
+
this.store.transaction((tx) => {
|
|
333
|
+
const panes = inspect(taskId);
|
|
334
|
+
for (const roleName of targets) {
|
|
335
|
+
if (panes.some((pane) => pane.roleName === roleName && !pane.dead)) {
|
|
336
|
+
throw new Error(`Task Role native pane must stop before workspace migration: ${roleName}.`);
|
|
337
|
+
}
|
|
338
|
+
if (tx.getActiveAgentRun(taskId, roleName) !== null) {
|
|
339
|
+
throw new Error(`Role has an active Run: ${taskId}/${roleName}.`);
|
|
340
|
+
}
|
|
341
|
+
if (tx.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
342
|
+
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
343
|
+
}
|
|
344
|
+
const sessions = tx.getTaskRoleSessionSet(taskId, roleName);
|
|
345
|
+
if (sessions === null)
|
|
346
|
+
continue;
|
|
347
|
+
const retired = retireConfirmedAbsentInactiveTaskRolePlaceholders(sessions, this.preparer.now());
|
|
348
|
+
if (retired !== sessions)
|
|
349
|
+
tx.saveTaskRoleSessionSet(retired);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
#workItemIsolationAssignee(item) {
|
|
354
|
+
const task = this.store.getTask(item.taskId);
|
|
355
|
+
if (task === null)
|
|
356
|
+
throw new Error(`Task not found: ${item.taskId}.`);
|
|
357
|
+
if (task.status !== "active")
|
|
358
|
+
throw new Error(`Task is not active: ${task.id}.`);
|
|
359
|
+
if (task.projectBindings.length === 0) {
|
|
360
|
+
throw new Error(`WorkItem isolation requires a Project-backed Task: ${task.id}.`);
|
|
361
|
+
}
|
|
362
|
+
if (item.assignee === "leader") {
|
|
363
|
+
throw new Error("The Leader must remain in the Task main worktree.");
|
|
364
|
+
}
|
|
365
|
+
if (isTerminalWorkItem(item)) {
|
|
366
|
+
throw new Error(`Work item is already terminal: ${item.id}.`);
|
|
367
|
+
}
|
|
368
|
+
if (item.assignee !== undefined && this.store.getActiveAgentRun(task.id, item.assignee) !== null) {
|
|
369
|
+
throw new Error(`Role has an active Run: ${task.id}/${item.assignee}.`);
|
|
370
|
+
}
|
|
371
|
+
if (item.assignee !== undefined && this.store.getRole(task.id, item.assignee) === null) {
|
|
372
|
+
throw new Error(`Role not found: ${task.id}/${item.assignee}.`);
|
|
373
|
+
}
|
|
374
|
+
return item.assignee;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function sameProjectScope(workspace, taskProjectIds, writeProjectIds) {
|
|
378
|
+
const actualProjects = workspace.entries.map(({ projectId }) => projectId).sort();
|
|
379
|
+
const expectedProjects = [...taskProjectIds].sort();
|
|
380
|
+
const actual = workspace.entries
|
|
381
|
+
.filter(({ access }) => access === "write")
|
|
382
|
+
.map(({ projectId }) => projectId)
|
|
383
|
+
.sort();
|
|
384
|
+
const expected = [...writeProjectIds].sort();
|
|
385
|
+
return actualProjects.length === expectedProjects.length
|
|
386
|
+
&& actualProjects.every((projectId, index) => projectId === expectedProjects[index])
|
|
387
|
+
&& actual.length === expected.length
|
|
388
|
+
&& actual.every((projectId, index) => projectId === expected[index]);
|
|
389
|
+
}
|
|
390
|
+
function isTerminalWorkItem(item) {
|
|
391
|
+
return ["completed", "failed", "retired"].includes(item.status);
|
|
392
|
+
}
|