@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,86 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const executeFile = promisify(execFile);
4
+ export async function captureManagedGitChanges(input) {
5
+ await assertManagedHead(input);
6
+ const status = await git([
7
+ "-C", input.path, "status", "--porcelain=v1", "--untracked-files=all"
8
+ ]);
9
+ if (status.trim().length > 0) {
10
+ if (input.requireClean === true) {
11
+ throw new Error(`Managed workspace must be clean before capture: ${input.identity}.`);
12
+ }
13
+ await git(["-C", input.path, "add", "--all"]);
14
+ await git([
15
+ "-C", input.path,
16
+ "-c", "user.name=Yui",
17
+ "-c", "user.email=yui@local",
18
+ "commit", "-m", input.commitMessage
19
+ ]);
20
+ }
21
+ const headCommit = await assertManagedHead(input);
22
+ if (headCommit === input.baseCommit)
23
+ return null;
24
+ const changedPaths = (await git([
25
+ "-C", input.path,
26
+ "diff", "--name-only", "-z",
27
+ input.baseCommit,
28
+ headCommit
29
+ ])).split("\0").filter(Boolean);
30
+ return { headCommit, changedPaths };
31
+ }
32
+ async function assertManagedHead(input) {
33
+ const currentBranch = (await git([
34
+ "-C", input.path, "symbolic-ref", "--quiet", "--short", "HEAD"
35
+ ])).trim();
36
+ if (currentBranch !== input.branch) {
37
+ throw new Error(`Managed workspace left its managed branch: expected ${input.branch}, found ${currentBranch} (${input.identity}).`);
38
+ }
39
+ const headCommit = await gitLine(["-C", input.path, "rev-parse", "HEAD^{commit}"]);
40
+ if (input.expectedHead !== undefined && headCommit !== input.expectedHead) {
41
+ throw new Error(`Managed workspace HEAD no longer matches its Candidate snapshot: ${input.identity}.`);
42
+ }
43
+ if (!await gitSucceeds([
44
+ "-C", input.path,
45
+ "merge-base", "--is-ancestor",
46
+ input.baseCommit,
47
+ headCommit
48
+ ])) {
49
+ throw new Error(`Managed workspace HEAD does not descend from its recorded base: ${input.identity}.`);
50
+ }
51
+ return headCommit;
52
+ }
53
+ async function git(args) {
54
+ try {
55
+ const result = await executeFile("git", [...args], {
56
+ encoding: "utf8",
57
+ maxBuffer: 16 * 1024 * 1024,
58
+ timeout: 120_000
59
+ });
60
+ return result.stdout;
61
+ }
62
+ catch (error) {
63
+ const stderr = typeof error === "object" && error !== null && "stderr" in error
64
+ ? String(error.stderr).trim()
65
+ : "";
66
+ throw new Error(stderr.length === 0 ? "Git command failed." : `Git command failed: ${stderr}`, {
67
+ cause: error
68
+ });
69
+ }
70
+ }
71
+ async function gitLine(args) {
72
+ const value = (await git(args)).trim();
73
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value)) {
74
+ throw new Error("Git returned an invalid commit.");
75
+ }
76
+ return value;
77
+ }
78
+ async function gitSucceeds(args) {
79
+ try {
80
+ await git(args);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
@@ -0,0 +1,445 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { createWorkItemChangeSet } from "../integration/changeSet.js";
3
+ import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
4
+ import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
5
+ import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
6
+ import { captureManagedGitChanges } from "./gitChangeSetCapture.js";
7
+ const CAPTURABLE_WORK_ITEM_STATUSES = new Set([
8
+ "awaiting_acceptance",
9
+ "completed",
10
+ "failed",
11
+ "retired"
12
+ ]);
13
+ export class WorkItemChangeSetManager {
14
+ store;
15
+ now;
16
+ constructor(store, now = () => new Date()) {
17
+ this.store = store;
18
+ this.now = now;
19
+ }
20
+ async capture(taskId, workItemId, options = {}) {
21
+ const context = requireCapturableContext(this.store, taskId, workItemId, options.taskFinalReviewContract);
22
+ const entries = capturableEntries(context);
23
+ if (context.source === "task-main") {
24
+ const git = new NodeGitWorkspace();
25
+ for (const { entry, expectedHead } of entries) {
26
+ if (!await git.isClean(entry.path)) {
27
+ throw new Error(`Exact direct Candidate Task main must be clean: ${context.workItemId}/${entry.projectId}.`);
28
+ }
29
+ const branch = await git.headRef(entry.path);
30
+ const headCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
31
+ if (branch !== entry.branch || headCommit !== expectedHead) {
32
+ throw new Error(`Exact direct Candidate Task main no longer matches its frozen snapshot: ${context.workItemId}/${entry.projectId}.`);
33
+ }
34
+ }
35
+ }
36
+ const captured = [];
37
+ for (const entry of entries) {
38
+ const changeSet = await this.#captureProject(context, entry);
39
+ if (changeSet !== null)
40
+ captured.push(changeSet);
41
+ }
42
+ return captured;
43
+ }
44
+ async assertIntegrated(taskId, workItemId) {
45
+ const item = this.store.getWorkItem(taskId, workItemId);
46
+ if (item === null)
47
+ throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
48
+ const workspace = this.store.getWorkItemWorkspace(item.taskId, item.id);
49
+ if (workspace === null
50
+ || workspace.owner.type !== "work-item"
51
+ || workspace.owner.workItemId !== item.id)
52
+ return null;
53
+ const git = new NodeGitWorkspace();
54
+ const projects = [];
55
+ for (const entry of writableEntries(workspace)) {
56
+ if (!await git.isClean(entry.path)) {
57
+ throw new Error(`WorkItem Project workspace is not clean: ${item.id}/${entry.projectId}.`);
58
+ }
59
+ const headCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
60
+ const latest = latestWorkItemChangeSet(this.store, item.taskId, item.id, entry.projectId);
61
+ if (headCommit === entry.baseCommit) {
62
+ if (latest !== undefined) {
63
+ throw new Error(`WorkItem Project no longer matches its latest ChangeSet: ${item.id}/${entry.projectId}.`);
64
+ }
65
+ projects.push({
66
+ projectId: entry.projectId,
67
+ baseCommit: entry.baseCommit,
68
+ headCommit
69
+ });
70
+ continue;
71
+ }
72
+ if (latest === undefined
73
+ || latest.baseCommit !== entry.baseCommit
74
+ || latest.headCommit !== headCommit
75
+ || latest.branch !== entry.branch) {
76
+ throw new Error(`WorkItem Project has uncaptured commits: ${item.id}/${entry.projectId}.`);
77
+ }
78
+ const integrated = this.store.listIntegrationAttempts(item.taskId).some((integration) => (integration.status === "committed"
79
+ && integration.projectId === entry.projectId
80
+ && integration.changeSetIds.includes(latest.id)));
81
+ if (!integrated) {
82
+ throw new Error(`WorkItem ChangeSet is not integrated: ${latest.id}.`);
83
+ }
84
+ projects.push({
85
+ projectId: entry.projectId,
86
+ baseCommit: entry.baseCommit,
87
+ headCommit,
88
+ changeSetId: latest.id
89
+ });
90
+ }
91
+ return {
92
+ workItemId: item.id,
93
+ ...(item.assignee === undefined ? {} : { assignee: item.assignee }),
94
+ workspace,
95
+ projects
96
+ };
97
+ }
98
+ /**
99
+ * Fail-closed proof that retiring the aggregate will not hide unrecorded Git
100
+ * state. It never removes or modifies a worktree.
101
+ */
102
+ async assertRetirable(taskId) {
103
+ const task = this.store.getTask(taskId);
104
+ if (task === null)
105
+ throw new Error(`Task not found: ${taskId}.`);
106
+ if (task.status !== "active" && task.status !== "draft") {
107
+ throw new Error(`Task cannot be retired from ${task.status}: ${task.id}.`);
108
+ }
109
+ const unresolved = this.store.listIntegrationAttempts(task.id).find((attempt) => (attempt.status === "running"
110
+ || attempt.status === "blocked"
111
+ || attempt.status === "validating"));
112
+ if (unresolved !== undefined) {
113
+ throw new Error(`Task has an unresolved Integration Attempt: ${task.id}/${unresolved.id}.`);
114
+ }
115
+ const git = new NodeGitWorkspace();
116
+ const remoteHeads = new Map();
117
+ const findPublishedCommit = async (entry, headCommit) => {
118
+ const project = this.store.getProject(entry.projectId);
119
+ if (project?.remoteUrl === undefined)
120
+ return null;
121
+ let remoteHead = remoteHeads.get(project.id);
122
+ if (remoteHead === undefined) {
123
+ remoteHead = (await git.fetchRemoteHeadIntoWorktree({
124
+ repositoryPath: entry.path,
125
+ remoteUrl: project.remoteUrl,
126
+ branch: project.developmentBranch
127
+ })).commit;
128
+ remoteHeads.set(project.id, remoteHead);
129
+ }
130
+ return git.findCommitWithSameTreeInHistory({
131
+ repositoryPath: entry.path,
132
+ sourceCommit: headCommit,
133
+ historyHead: remoteHead
134
+ });
135
+ };
136
+ const workspaces = [];
137
+ for (const workspace of this.store.listManagedWorkspaces(task.id)) {
138
+ const projects = [];
139
+ for (const entry of workspace.entries) {
140
+ if (!await git.isClean(entry.path)) {
141
+ throw new Error(`Task retirement workspace is not clean: ${task.id}/${entry.projectId}.`);
142
+ }
143
+ const headCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
144
+ if (headCommit !== entry.baseCommit) {
145
+ if (workspace.owner.type === "work-item") {
146
+ const latest = latestWorkItemChangeSet(this.store, task.id, workspace.owner.workItemId, entry.projectId);
147
+ if (latest === undefined
148
+ || latest.baseCommit !== entry.baseCommit
149
+ || latest.headCommit !== headCommit
150
+ || latest.branch !== entry.branch) {
151
+ const publishedCommit = await findPublishedCommit(entry, headCommit);
152
+ if (publishedCommit === null) {
153
+ throw new Error(`Task retirement would hide uncaptured WorkItem commits: `
154
+ + `${workspace.owner.workItemId}/${entry.projectId}.`);
155
+ }
156
+ projects.push({
157
+ projectId: entry.projectId,
158
+ baseCommit: entry.baseCommit,
159
+ headCommit,
160
+ publishedCommit
161
+ });
162
+ continue;
163
+ }
164
+ projects.push({
165
+ projectId: entry.projectId,
166
+ baseCommit: entry.baseCommit,
167
+ headCommit,
168
+ changeSetId: latest.id
169
+ });
170
+ continue;
171
+ }
172
+ const committed = this.store.listIntegrationAttempts(task.id).find((attempt) => (attempt.status === "committed"
173
+ && attempt.projectId === entry.projectId
174
+ && attempt.candidateCommit === headCommit));
175
+ if (committed === undefined) {
176
+ const publishedCommit = await findPublishedCommit(entry, headCommit);
177
+ if (publishedCommit === null) {
178
+ throw new Error(`Task retirement would hide uncaptured Task workspace commits: `
179
+ + `${task.id}/${entry.projectId}.`);
180
+ }
181
+ projects.push({
182
+ projectId: entry.projectId,
183
+ baseCommit: entry.baseCommit,
184
+ headCommit,
185
+ publishedCommit
186
+ });
187
+ continue;
188
+ }
189
+ }
190
+ projects.push({
191
+ projectId: entry.projectId,
192
+ baseCommit: entry.baseCommit,
193
+ headCommit
194
+ });
195
+ }
196
+ workspaces.push({ ownerKey: managedWorkspaceKey(workspace.owner), workspace, projects });
197
+ }
198
+ return {
199
+ taskId: task.id,
200
+ taskUpdatedAt: task.updatedAt,
201
+ workspaces
202
+ };
203
+ }
204
+ async #captureProject(context, project) {
205
+ const { entry, expectedHead } = project;
206
+ const result = await captureManagedGitChanges({
207
+ path: entry.path,
208
+ branch: entry.branch,
209
+ baseCommit: entry.baseCommit,
210
+ commitMessage: `yui: work item ${context.workItemId} (${entry.directory})`,
211
+ identity: `${context.workItemId}/${entry.projectId}`,
212
+ ...(context.source === "task-main" ? { requireClean: true, expectedHead } : {})
213
+ });
214
+ if (result === null)
215
+ return null;
216
+ const existing = findWorkItemChangeSet(this.store, context.taskId, context.workItemId, entry.projectId, entry.baseCommit, result.headCommit);
217
+ if (existing !== undefined)
218
+ return existing;
219
+ return this.store.transaction((tx) => {
220
+ assertCaptureStillCurrent(tx, context);
221
+ const concurrent = findWorkItemChangeSet(tx, context.taskId, context.workItemId, entry.projectId, entry.baseCommit, result.headCommit);
222
+ if (concurrent !== undefined) {
223
+ if (!sameCapturedResult(concurrent, {
224
+ ...concurrent,
225
+ projectId: entry.projectId,
226
+ branch: entry.branch,
227
+ changedPaths: result.changedPaths
228
+ })) {
229
+ throw new Error(`Work item ChangeSet is immutable: ${context.workItemId}/${entry.projectId}.`);
230
+ }
231
+ return concurrent;
232
+ }
233
+ const changeSet = createWorkItemChangeSet({
234
+ id: tx.nextChangeSetId(context.taskId),
235
+ taskId: context.taskId,
236
+ workItemId: context.workItemId,
237
+ projectId: entry.projectId,
238
+ baseCommit: entry.baseCommit,
239
+ headCommit: result.headCommit,
240
+ branch: entry.branch,
241
+ changedPaths: result.changedPaths
242
+ }, nextCaptureTime(tx, context.taskId, context.workItemId, entry.projectId, this.now()));
243
+ tx.saveChangeSet(context.taskId, changeSet);
244
+ return changeSet;
245
+ });
246
+ }
247
+ }
248
+ function requireCapturableContext(store, taskId, workItemId, taskFinalReviewContract) {
249
+ const item = store.getWorkItem(taskId, workItemId);
250
+ if (item === null)
251
+ throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
252
+ if (!CAPTURABLE_WORK_ITEM_STATUSES.has(item.status)) {
253
+ throw new Error(`Work item must be awaiting acceptance or terminal before capture: ${item.id}.`);
254
+ }
255
+ if (item.workspaceDisposition !== undefined) {
256
+ throw new Error(`Work item workspace is already recorded as ${item.workspaceDisposition}: ${item.id}.`);
257
+ }
258
+ const task = store.getTask(item.taskId);
259
+ if (task === null)
260
+ throw new Error(`Task not found: ${item.taskId}.`);
261
+ if (task.status !== "active") {
262
+ throw new Error(`Task is not active: ${task.id}/${task.status}.`);
263
+ }
264
+ const workspace = store.getWorkItemWorkspace(task.id, item.id);
265
+ if (workspace?.owner.type === "review-round") {
266
+ throw new Error(`ReviewRound-owned workspace cannot be captured as WorkItem Develop: ${item.id}.`);
267
+ }
268
+ const exactWorkItemWorkspace = workspace !== null
269
+ && workspace.owner.type === "work-item"
270
+ && workspace.owner.workItemId === item.id;
271
+ let selectedWorkspace;
272
+ let source;
273
+ let taskMainSnapshot;
274
+ if (exactWorkItemWorkspace) {
275
+ selectedWorkspace = workspace;
276
+ source = "work-item";
277
+ }
278
+ else {
279
+ if (taskFinalReviewContract === undefined) {
280
+ throw new Error(`Work item has no managed workspace: ${item.id}.`);
281
+ }
282
+ const candidate = item.candidates.at(-1);
283
+ if (candidate?.source.type !== "direct"
284
+ || !sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalReviewContract)) {
285
+ throw new Error(`Work item has no matching exact Task-final direct Candidate: ${item.id}.`);
286
+ }
287
+ if (candidate.taskMainSnapshot === undefined) {
288
+ throw new Error(`Exact Task-final direct Candidate has no frozen Task-main snapshot: ${item.id}.`);
289
+ }
290
+ if (latestExactDirectAnchorId(store, task.id, taskFinalReviewContract) !== item.id) {
291
+ throw new Error(`Only the latest exact Task-final direct Candidate may capture Task main: ${item.id}.`);
292
+ }
293
+ const taskWorkspace = store.getTaskWorkspace(task.id);
294
+ if (taskWorkspace === null
295
+ || taskWorkspace.owner.type !== "task"
296
+ || taskWorkspace.owner.taskId !== task.id) {
297
+ throw new Error(`Task has no authoritative main workspace: ${task.id}.`);
298
+ }
299
+ selectedWorkspace = taskWorkspace;
300
+ source = "task-main";
301
+ taskMainSnapshot = candidate.taskMainSnapshot;
302
+ }
303
+ const expectedWriteProjects = [...item.writeProjectIds].sort();
304
+ const availableWriteProjects = writableEntries(selectedWorkspace)
305
+ .map(({ projectId }) => projectId);
306
+ const actualWriteProjects = availableWriteProjects
307
+ .filter((projectId) => expectedWriteProjects.includes(projectId))
308
+ .sort();
309
+ if (!isDeepStrictEqual(actualWriteProjects, expectedWriteProjects)) {
310
+ throw new Error(`Work item workspace scope is stale: ${item.id}.`);
311
+ }
312
+ if (source === "task-main") {
313
+ const snapshotProjects = taskMainSnapshot.projects
314
+ .map(({ projectId }) => projectId)
315
+ .sort();
316
+ if (!isDeepStrictEqual(snapshotProjects, expectedWriteProjects)) {
317
+ throw new Error(`Direct Candidate snapshot scope is stale: ${item.id}.`);
318
+ }
319
+ for (const snapshot of taskMainSnapshot.projects) {
320
+ const current = selectedWorkspace.entries.find(({ projectId }) => projectId === snapshot.projectId);
321
+ if (current === undefined
322
+ || current.access !== "write"
323
+ || current.directory !== snapshot.directory
324
+ || current.branch !== snapshot.branch) {
325
+ throw new Error(`Direct Candidate Task-main identity is stale: ${item.id}/${snapshot.projectId}.`);
326
+ }
327
+ }
328
+ }
329
+ return {
330
+ taskId: task.id,
331
+ workItemId: item.id,
332
+ ...(item.assignee === undefined ? {} : { assignee: item.assignee }),
333
+ expectedRevision: item.revision,
334
+ source,
335
+ writeProjectIds: expectedWriteProjects,
336
+ ...(source === "task-main" ? { taskFinalReviewContract, taskMainSnapshot } : {}),
337
+ workspace: selectedWorkspace
338
+ };
339
+ }
340
+ function assertCaptureStillCurrent(store, expected) {
341
+ const task = store.getTask(expected.taskId);
342
+ const item = store.getWorkItem(expected.taskId, expected.workItemId);
343
+ const workspace = expected.source === "task-main"
344
+ ? store.getTaskWorkspace(expected.taskId)
345
+ : store.getWorkItemWorkspace(expected.taskId, expected.workItemId);
346
+ const candidate = item?.candidates.at(-1);
347
+ if (task?.status !== "active"
348
+ || item === null
349
+ || item.revision !== expected.expectedRevision
350
+ || !CAPTURABLE_WORK_ITEM_STATUSES.has(item.status)
351
+ || item.workspaceDisposition !== undefined
352
+ || workspace === null
353
+ || (expected.source === "task-main"
354
+ ? !sameTaskMainWorkspaceIdentity(workspace, expected.workspace)
355
+ : !isDeepStrictEqual(workspace, expected.workspace))
356
+ || (expected.source === "task-main" && (candidate?.source.type !== "direct"
357
+ || !isDeepStrictEqual(candidate.taskMainSnapshot, expected.taskMainSnapshot)
358
+ || !sameTaskFinalReviewContract(candidate.taskFinalReviewContract, expected.taskFinalReviewContract)
359
+ || latestExactDirectAnchorId(store, expected.taskId, expected.taskFinalReviewContract) !== expected.workItemId))) {
360
+ throw new Error(`WorkItem changed while its ChangeSets were being captured: ${expected.workItemId}. Retry capture.`);
361
+ }
362
+ }
363
+ function sameTaskMainWorkspaceIdentity(current, expected) {
364
+ return current.owner.type === "task"
365
+ && expected.owner.type === "task"
366
+ && current.owner.taskId === expected.owner.taskId
367
+ && current.root === expected.root
368
+ && current.entries.length === expected.entries.length
369
+ && current.entries.every((entry, index) => {
370
+ const frozen = expected.entries[index];
371
+ return frozen !== undefined
372
+ && entry.projectId === frozen.projectId
373
+ && entry.directory === frozen.directory
374
+ && entry.access === frozen.access
375
+ && entry.path === frozen.path
376
+ && entry.branch === frozen.branch;
377
+ });
378
+ }
379
+ function latestExactDirectAnchorId(store, taskId, contract) {
380
+ if (contract === undefined)
381
+ return undefined;
382
+ return store.listWorkItems(taskId)
383
+ .filter((item) => {
384
+ const candidate = item.candidates.at(-1);
385
+ return candidate?.source.type === "direct"
386
+ && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, contract);
387
+ })
388
+ .sort((left, right) => (left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id)))
389
+ .at(-1)?.id;
390
+ }
391
+ function capturableEntries(context) {
392
+ const allowed = new Set(context.writeProjectIds);
393
+ return writableEntries(context.workspace)
394
+ .filter(({ projectId }) => allowed.has(projectId))
395
+ .map((entry) => {
396
+ if (context.source !== "task-main")
397
+ return { entry };
398
+ const snapshot = context.taskMainSnapshot.projects.find(({ projectId }) => projectId === entry.projectId);
399
+ return {
400
+ entry: {
401
+ ...entry,
402
+ branch: snapshot.branch,
403
+ baseCommit: snapshot.baseCommit
404
+ },
405
+ expectedHead: snapshot.headCommit
406
+ };
407
+ });
408
+ }
409
+ function writableEntries(workspace) {
410
+ return workspace.entries.filter(({ access }) => access === "write");
411
+ }
412
+ function latestWorkItemChangeSet(store, taskId, workItemId, projectId, commits) {
413
+ return store.listChangeSets(taskId).filter((changeSet) => (changeSet.schemaVersion === 2
414
+ && changeSet.workItemId === workItemId
415
+ && changeSet.projectId === projectId
416
+ && (commits === undefined
417
+ || (changeSet.baseCommit === commits.baseCommit
418
+ && changeSet.headCommit === commits.headCommit)))).sort(compareNewestFirst)[0];
419
+ }
420
+ function findWorkItemChangeSet(store, taskId, workItemId, projectId, baseCommit, headCommit) {
421
+ return latestWorkItemChangeSet(store, taskId, workItemId, projectId, {
422
+ baseCommit,
423
+ headCommit
424
+ });
425
+ }
426
+ function compareNewestFirst(left, right) {
427
+ return right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id);
428
+ }
429
+ function nextCaptureTime(store, taskId, workItemId, projectId, candidate) {
430
+ const latest = latestWorkItemChangeSet(store, taskId, workItemId, projectId);
431
+ if (latest === undefined)
432
+ return candidate;
433
+ const minimum = Date.parse(latest.createdAt) + 1;
434
+ return candidate.getTime() >= minimum ? candidate : new Date(minimum);
435
+ }
436
+ function sameCapturedResult(left, right) {
437
+ return left.taskId === right.taskId
438
+ && left.workItemId === right.workItemId
439
+ && left.projectId === right.projectId
440
+ && left.baseCommit === right.baseCommit
441
+ && left.headCommit === right.headCommit
442
+ && left.branch === right.branch
443
+ && left.changedPaths.length === right.changedPaths.length
444
+ && left.changedPaths.every((path, index) => path === right.changedPaths[index]);
445
+ }