@zq-silk/yui 0.2.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -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
+ }