@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
@@ -1,45 +1,664 @@
1
+ import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
2
+ import { validateReviewConfig } from "../review/reviewConfig.js";
3
+ import { taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
4
+ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
5
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
6
+ import { assertExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
7
+ const TERMINAL_STATUSES = [
8
+ "completed",
9
+ "failed",
10
+ "retired"
11
+ ];
1
12
  export function createWorkItem(id, taskId, input, now) {
2
13
  const timestamp = now.toISOString();
3
- return {
4
- schemaVersion: 1,
5
- id: requireSafeIdentity(id, "Work item id"),
6
- taskId: requireSafeIdentity(taskId, "Task id"),
14
+ return validateWorkItem({
15
+ schemaVersion: 9,
16
+ id: requireIdentity(id, "Work Item id"),
17
+ taskId: requireIdentity(taskId, "Task id"),
7
18
  title: requireText(input.title, "Work item title"),
8
- assignee: requireSafeIdentity(input.assignee, "Work item assignee"),
19
+ objective: requireText(input.objective ?? input.title, "Work item objective"),
20
+ acceptance: normalizedUniqueText(input.acceptance ?? [], "Work item acceptance criterion"),
21
+ dependsOn: normalizedUniqueIdentities(input.dependsOn ?? [], "Work item dependency"),
22
+ writeProjectIds: normalizedUniqueIdentities(input.writeProjectIds ?? [], "Work item writable Project"),
23
+ executionGroups: (input.executionGroups ?? []).map((group) => validateWorkItemExecutionGroup(group, taskId, id)),
24
+ ...(input.currentExecutionGroupId === undefined
25
+ ? {}
26
+ : { currentExecutionGroupId: requireIdentity(input.currentExecutionGroupId, "ExecutionGroup id") }),
27
+ ...(input.baseRefs === undefined || input.baseRefs.length === 0
28
+ ? {}
29
+ : { baseRefs: normalizeProjectBaseRefs(input.baseRefs) }),
30
+ revision: 1,
31
+ ...(input.assignee === undefined
32
+ ? {}
33
+ : { assignee: requireIdentity(input.assignee, "Work item assignee") }),
9
34
  status: "pending",
35
+ candidates: [],
10
36
  createdAt: timestamp,
11
37
  updatedAt: timestamp
12
- };
38
+ });
39
+ }
40
+ export function submitWorkItemCandidate(workItem, input, now) {
41
+ validateWorkItem(workItem);
42
+ if (workItem.status !== "running") {
43
+ throw new Error(`Work Item candidate can only be submitted from running: ${workItem.id}/${workItem.status}.`);
44
+ }
45
+ const revision = workItem.revision + 1;
46
+ const sequence = workItem.candidates.length + 1;
47
+ const candidate = validateWorkItemCandidate({
48
+ schemaVersion: 2,
49
+ id: `candidate-${sequence}`,
50
+ taskId: workItem.taskId,
51
+ workItemId: workItem.id,
52
+ sequence,
53
+ workItemRevision: revision,
54
+ summary: input.summary,
55
+ source: input.source,
56
+ ...(input.reviewPolicy === undefined ? {} : { reviewPolicy: input.reviewPolicy }),
57
+ ...(input.taskFinalReviewContract === undefined
58
+ ? {}
59
+ : { taskFinalReviewContract: input.taskFinalReviewContract }),
60
+ ...(input.executionGroupId === undefined
61
+ ? {}
62
+ : { executionGroupId: requireIdentity(input.executionGroupId, "ExecutionGroup id") }),
63
+ ...(input.executionLaneId === undefined
64
+ ? {}
65
+ : { executionLaneId: requireIdentity(input.executionLaneId, "ExecutionLane id") }),
66
+ ...(input.workspace === undefined ? {} : { workspace: input.workspace }),
67
+ ...(input.gitSnapshot === undefined ? {} : { gitSnapshot: input.gitSnapshot }),
68
+ ...(input.taskMainSnapshot === undefined
69
+ ? {}
70
+ : { taskMainSnapshot: input.taskMainSnapshot }),
71
+ createdAt: now.toISOString()
72
+ });
73
+ const { outcome: _outcome, endedAt: _endedAt, ...base } = workItem;
74
+ return validateWorkItem({
75
+ ...base,
76
+ status: "awaiting_acceptance",
77
+ candidates: [...workItem.candidates, candidate],
78
+ revision,
79
+ updatedAt: now.toISOString()
80
+ });
13
81
  }
14
- export function updateWorkItemStatus(workItem, status, outcome, now) {
15
- const terminal = ["completed", "failed", "cancelled", "superseded"].includes(status);
16
- const normalizedOutcome = outcome === undefined ? undefined : requireText(outcome, "Work item outcome");
82
+ export function updateWorkItemStatus(workItem, status, now, outcome) {
83
+ validateWorkItem(workItem);
84
+ validateStatus(status);
85
+ const alreadyTerminal = isTerminalStatus(workItem.status);
86
+ if (workItem.disposition !== undefined && status !== workItem.status) {
87
+ throw new Error(`Retired Work Item status cannot change: ${workItem.id}.`);
88
+ }
89
+ const closingFailedWork = workItem.status === "failed" && status === "retired";
90
+ if (alreadyTerminal && status !== workItem.status && !closingFailedWork) {
91
+ throw new Error(`Terminal Work Item status cannot change: ${workItem.id}.`);
92
+ }
93
+ const terminal = isTerminalStatus(status);
94
+ const normalizedOutcome = outcome === undefined
95
+ ? undefined
96
+ : requireText(outcome, "Work item outcome");
17
97
  if (terminal && normalizedOutcome === undefined) {
18
98
  throw new Error(`Work item outcome is required for ${status}.`);
19
99
  }
20
100
  const timestamp = now.toISOString();
21
- const { outcome: _outcome, endedAt: _endedAt, ...base } = workItem;
22
- return {
101
+ if (alreadyTerminal && status === workItem.status) {
102
+ return validateWorkItem({
103
+ ...workItem,
104
+ outcome: normalizedOutcome,
105
+ revision: workItem.revision + 1,
106
+ updatedAt: timestamp
107
+ });
108
+ }
109
+ const { endedAt: _endedAt, outcome: _outcome, workspaceDisposition, ...base } = workItem;
110
+ return validateWorkItem({
23
111
  ...base,
24
112
  status,
113
+ revision: workItem.revision + 1,
25
114
  ...(normalizedOutcome === undefined ? {} : { outcome: normalizedOutcome }),
115
+ // Isolated workspace cleanup remains durable evidence after retirement.
116
+ ...(closingFailedWork && workspaceDisposition !== undefined
117
+ ? { workspaceDisposition }
118
+ : {}),
26
119
  updatedAt: timestamp,
27
120
  ...(terminal ? { endedAt: timestamp } : {})
121
+ });
122
+ }
123
+ /** Attach the common execution Group without changing the WorkItem identity. */
124
+ export function attachWorkItemExecutionGroup(workItem, executionGroup, now) {
125
+ validateWorkItem(workItem);
126
+ const checked = validateWorkItemExecutionGroup(executionGroup, workItem.taskId, workItem.id);
127
+ if (workItem.status === "completed" || workItem.status === "failed" || workItem.status === "retired") {
128
+ throw new Error(`A terminal Work Item cannot attach an ExecutionGroup: ${workItem.id}.`);
129
+ }
130
+ const existing = workItemExecutionGroupById(workItem, checked.id);
131
+ if (existing !== undefined) {
132
+ if (JSON.stringify(existing) !== JSON.stringify(checked)) {
133
+ throw new Error(`Work Item ExecutionGroup is immutable: ${workItem.id}/${checked.id}.`);
134
+ }
135
+ if (workItem.currentExecutionGroupId === checked.id)
136
+ return workItem;
137
+ throw new Error(`Work Item historical ExecutionGroup is immutable: ${workItem.id}/${checked.id}.`);
138
+ }
139
+ const current = currentWorkItemExecutionGroup(workItem);
140
+ if (current !== undefined && current.resolution === undefined) {
141
+ throw new Error(`Work Item already has an unresolved ExecutionGroup: ${workItem.id}/${current.id}.`);
142
+ }
143
+ return validateWorkItem({
144
+ ...workItem,
145
+ executionGroups: [...workItem.executionGroups, checked],
146
+ currentExecutionGroupId: checked.id,
147
+ revision: workItem.revision + 1,
148
+ updatedAt: now.toISOString()
149
+ });
150
+ }
151
+ /** Advance a Group's lane state while keeping its frozen target immutable. */
152
+ export function updateWorkItemExecutionGroup(workItem, executionGroup, now) {
153
+ validateWorkItem(workItem);
154
+ const checked = validateWorkItemExecutionGroup(executionGroup, workItem.taskId, workItem.id);
155
+ const existing = workItemExecutionGroupById(workItem, checked.id);
156
+ if (existing === undefined) {
157
+ return attachWorkItemExecutionGroup(workItem, checked, now);
158
+ }
159
+ if (workItem.currentExecutionGroupId !== checked.id) {
160
+ throw new Error(`Work Item historical ExecutionGroup is immutable: ${workItem.id}/${checked.id}.`);
161
+ }
162
+ if (JSON.stringify(existing.target) !== JSON.stringify(checked.target)) {
163
+ throw new Error(`Work Item ExecutionGroup target is immutable: ${workItem.id}/${checked.id}.`);
164
+ }
165
+ if (JSON.stringify(existing) === JSON.stringify(checked))
166
+ return workItem;
167
+ assertExecutionGroupTransition(existing, checked);
168
+ return validateWorkItem({
169
+ ...workItem,
170
+ executionGroups: workItem.executionGroups.map((group) => group.id === checked.id ? checked : group),
171
+ revision: workItem.revision + 1,
172
+ updatedAt: now.toISOString()
173
+ });
174
+ }
175
+ /**
176
+ * Records the Leader's explicit disposition as the authoritative terminal
177
+ * fact. Replays of the same decision are idempotent; a different terminal
178
+ * decision can never overwrite the first one.
179
+ */
180
+ export function retireWorkItem(workItem, input, now) {
181
+ validateWorkItem(workItem);
182
+ const disposition = normalizeDisposition(input, now);
183
+ if (workItem.disposition !== undefined) {
184
+ if (workItem.status === "retired"
185
+ && sameDisposition(workItem.disposition, disposition)) {
186
+ return workItem;
187
+ }
188
+ throw new Error(`Work Item already has an explicit disposition: ${workItem.id}.`);
189
+ }
190
+ if (workItem.status === "completed") {
191
+ throw new Error(`Completed Work Item cannot be retired: ${workItem.id}.`);
192
+ }
193
+ if (isTerminalStatus(workItem.status)
194
+ && workItem.status !== "failed"
195
+ && workItem.status !== "retired") {
196
+ throw new Error(`Terminal Work Item status cannot change: ${workItem.id}.`);
197
+ }
198
+ const timestamp = disposition.retiredAt;
199
+ return validateWorkItem({
200
+ ...workItem,
201
+ status: "retired",
202
+ outcome: disposition.summary,
203
+ disposition,
204
+ revision: workItem.revision + 1,
205
+ updatedAt: timestamp,
206
+ endedAt: timestamp
207
+ });
208
+ }
209
+ export function retryFailedWorkItem(workItem, now) {
210
+ validateWorkItem(workItem);
211
+ if (workItem.status !== "failed") {
212
+ throw new Error(`Work item is not retryable from ${workItem.status}: ${workItem.id}.`);
213
+ }
214
+ if (workItem.workspaceDisposition !== undefined) {
215
+ throw new Error(`Work item workspace is already settled: ${workItem.id}.`);
216
+ }
217
+ const { outcome: _outcome, endedAt: _endedAt, ...base } = workItem;
218
+ // Keep every historical Group. A resolved current Group is cleared only as
219
+ // the current pointer; dispatch appends a fresh immutable Group.
220
+ const current = currentWorkItemExecutionGroup(workItem);
221
+ const retryBase = current?.resolution === undefined
222
+ ? base
223
+ : (({ currentExecutionGroupId: _currentExecutionGroupId, ...history }) => history)(base);
224
+ return validateWorkItem({
225
+ ...retryBase,
226
+ status: "running",
227
+ revision: workItem.revision + 1,
228
+ updatedAt: now.toISOString()
229
+ });
230
+ }
231
+ export function recordWorkItemWorkspaceDisposition(workItem, disposition, now) {
232
+ validateWorkItem(workItem);
233
+ if (!isTerminalStatus(workItem.status)) {
234
+ throw new Error("Only a terminal Work Item can record workspace cleanup.");
235
+ }
236
+ if (workItem.workspaceDisposition !== undefined) {
237
+ if (workItem.workspaceDisposition !== disposition) {
238
+ throw new Error(`Work Item workspace is already recorded as ${workItem.workspaceDisposition}.`);
239
+ }
240
+ return workItem;
241
+ }
242
+ return validateWorkItem({
243
+ ...workItem,
244
+ workspaceDisposition: disposition,
245
+ revision: workItem.revision + 1,
246
+ updatedAt: now.toISOString()
247
+ });
248
+ }
249
+ export function validateWorkItem(workItem) {
250
+ if (workItem.schemaVersion !== 9)
251
+ throw new Error("WorkItem must use schemaVersion 9.");
252
+ validateTaskRecordReference({ taskId: workItem.taskId, localId: workItem.id }, "workItem");
253
+ requireIdentity(workItem.taskId, "Task id");
254
+ requireText(workItem.title, "Work item title");
255
+ requireText(workItem.objective, "Work item objective");
256
+ normalizedUniqueText(workItem.acceptance, "Work item acceptance criterion");
257
+ const dependsOn = normalizedUniqueIdentities(workItem.dependsOn, "Work item dependency");
258
+ normalizedUniqueIdentities(workItem.writeProjectIds, "Work item writable Project");
259
+ if (workItem.baseRefs !== undefined) {
260
+ const baseRefs = normalizeProjectBaseRefs(workItem.baseRefs);
261
+ const writableProjects = new Set(workItem.writeProjectIds);
262
+ if (baseRefs.some(({ projectId }) => !writableProjects.has(projectId))) {
263
+ throw new Error("Work Item Project base refs must target writable Projects.");
264
+ }
265
+ }
266
+ if (dependsOn.includes(workItem.id)) {
267
+ throw new Error("A Work Item cannot depend on itself.");
268
+ }
269
+ if (!Number.isSafeInteger(workItem.revision) || workItem.revision < 1) {
270
+ throw new Error("Work Item revision must be a positive integer.");
271
+ }
272
+ if (workItem.assignee !== undefined) {
273
+ requireIdentity(workItem.assignee, "Work item assignee");
274
+ }
275
+ if (!Array.isArray(workItem.executionGroups)) {
276
+ throw new Error("Work Item executionGroups are invalid.");
277
+ }
278
+ const groupIds = new Set();
279
+ for (const group of workItem.executionGroups) {
280
+ validateWorkItemExecutionGroup(group, workItem.taskId, workItem.id);
281
+ if (groupIds.has(group.id)) {
282
+ throw new Error(`Work Item ExecutionGroup is duplicated: ${group.id}.`);
283
+ }
284
+ groupIds.add(group.id);
285
+ }
286
+ if (workItem.currentExecutionGroupId !== undefined) {
287
+ requireIdentity(workItem.currentExecutionGroupId, "ExecutionGroup id");
288
+ if (!groupIds.has(workItem.currentExecutionGroupId)) {
289
+ throw new Error(`Work Item current ExecutionGroup is not in history: ${workItem.id}.`);
290
+ }
291
+ }
292
+ validateStatus(workItem.status);
293
+ if (!Array.isArray(workItem.candidates)) {
294
+ throw new Error("Work Item candidates are invalid.");
295
+ }
296
+ const candidateIds = new Set();
297
+ workItem.candidates.forEach((candidate, index) => {
298
+ validateWorkItemCandidate(candidate);
299
+ if (candidate.taskId !== workItem.taskId || candidate.workItemId !== workItem.id) {
300
+ throw new Error("Work Item candidate provenance is invalid.");
301
+ }
302
+ if (candidate.sequence !== index + 1) {
303
+ throw new Error("Work Item candidate sequence is invalid.");
304
+ }
305
+ if (candidateIds.has(candidate.id)) {
306
+ throw new Error(`Work Item candidate is duplicated: ${candidate.id}.`);
307
+ }
308
+ candidateIds.add(candidate.id);
309
+ if (candidate.workItemRevision > workItem.revision) {
310
+ throw new Error("Work Item candidate revision cannot exceed the Work Item revision.");
311
+ }
312
+ });
313
+ const currentCandidate = currentWorkItemCandidate(workItem);
314
+ if (workItem.status === "awaiting_acceptance" && currentCandidate === undefined) {
315
+ throw new Error("A Work Item awaiting acceptance requires a candidate.");
316
+ }
317
+ if (workItem.outcome !== undefined)
318
+ requireText(workItem.outcome, "Work item outcome");
319
+ requireTimestamp(workItem.createdAt, "Work Item createdAt");
320
+ requireTimestamp(workItem.updatedAt, "Work Item updatedAt");
321
+ const terminal = isTerminalStatus(workItem.status);
322
+ if (terminal) {
323
+ requireText(workItem.outcome ?? "", "Work item outcome");
324
+ requireTimestamp(workItem.endedAt ?? "", "Work Item endedAt");
325
+ }
326
+ else {
327
+ if (workItem.outcome !== undefined) {
328
+ throw new Error("A non-terminal Work Item cannot have an outcome.");
329
+ }
330
+ if (workItem.endedAt !== undefined) {
331
+ throw new Error("A non-terminal Work Item cannot have endedAt.");
332
+ }
333
+ }
334
+ if (workItem.workspaceDisposition !== undefined) {
335
+ if (!["integrated", "abandoned"].includes(workItem.workspaceDisposition)) {
336
+ throw new Error("Work item workspaceDisposition is invalid.");
337
+ }
338
+ if (!terminal) {
339
+ throw new Error("Only a terminal Work Item can record workspace cleanup.");
340
+ }
341
+ }
342
+ if (workItem.disposition !== undefined) {
343
+ validateDisposition(workItem.disposition);
344
+ if (workItem.status !== "retired") {
345
+ throw new Error("Work Item disposition does not match its status.");
346
+ }
347
+ if (workItem.outcome !== workItem.disposition.summary
348
+ || workItem.endedAt !== workItem.disposition.retiredAt) {
349
+ throw new Error("Work Item disposition does not match its terminal metadata.");
350
+ }
351
+ }
352
+ return workItem;
353
+ }
354
+ /** Resolve the WorkItem's current execution iteration. */
355
+ export function currentWorkItemExecutionGroup(workItem) {
356
+ return workItem.currentExecutionGroupId === undefined
357
+ ? undefined
358
+ : workItemExecutionGroupById(workItem, workItem.currentExecutionGroupId);
359
+ }
360
+ /** Resolve any historical or current WorkItem Group by its immutable id. */
361
+ export function workItemExecutionGroupById(workItem, executionGroupId) {
362
+ return workItem.executionGroups.find(({ id }) => id === executionGroupId);
363
+ }
364
+ function validateWorkItemExecutionGroup(group, taskId, workItemId) {
365
+ validateExecutionGroup(group);
366
+ if (group.taskId !== taskId || group.purpose !== "execution") {
367
+ throw new Error(`Work Item ExecutionGroup provenance is invalid: ${workItemId}.`);
368
+ }
369
+ if (group.target.kind !== "work-item"
370
+ || group.target.workItemId !== workItemId
371
+ || group.target.taskId !== taskId) {
372
+ throw new Error(`Work Item ExecutionGroup target is invalid: ${workItemId}.`);
373
+ }
374
+ return group;
375
+ }
376
+ export function validateWorkItemCandidate(candidate) {
377
+ if (typeof candidate !== "object" || candidate === null) {
378
+ throw new Error("Work Item candidate is required.");
379
+ }
380
+ if (candidate.schemaVersion !== 2) {
381
+ throw new Error("Work Item candidate must use schemaVersion 2.");
382
+ }
383
+ requireIdentity(candidate.taskId, "Work Item candidate Task id");
384
+ validateTaskRecordReference({
385
+ taskId: candidate.taskId,
386
+ localId: candidate.workItemId
387
+ }, "workItem");
388
+ if (candidate.id !== `candidate-${candidate.sequence}`) {
389
+ throw new Error("Work Item candidate local id is invalid.");
390
+ }
391
+ if (!Number.isSafeInteger(candidate.sequence) || candidate.sequence < 1) {
392
+ throw new Error("Work Item candidate sequence must be a positive integer.");
393
+ }
394
+ if (!Number.isSafeInteger(candidate.workItemRevision)
395
+ || candidate.workItemRevision < 1) {
396
+ throw new Error("Work Item candidate revision must be a positive integer.");
397
+ }
398
+ requireText(candidate.summary, "Work Item candidate summary");
399
+ if (typeof candidate.source !== "object" || candidate.source === null) {
400
+ throw new Error("Work Item candidate source is required.");
401
+ }
402
+ if (candidate.source.type !== "direct" && candidate.source.type !== "run") {
403
+ throw new Error("Work Item candidate source is invalid.");
404
+ }
405
+ if (candidate.source.type === "run") {
406
+ validateTaskRecordReference({
407
+ taskId: candidate.taskId,
408
+ localId: candidate.source.runId
409
+ }, "agentRun");
410
+ }
411
+ if ((candidate.executionGroupId === undefined) !== (candidate.executionLaneId === undefined)) {
412
+ throw new Error("Work Item candidate execution lineage is incomplete.");
413
+ }
414
+ if (candidate.executionGroupId !== undefined) {
415
+ requireIdentity(candidate.executionGroupId, "ExecutionGroup id");
416
+ requireIdentity(candidate.executionLaneId, "ExecutionLane id");
417
+ }
418
+ if (candidate.reviewPolicy !== undefined)
419
+ validateReviewConfig(candidate.reviewPolicy);
420
+ if (candidate.taskFinalReviewContract !== undefined) {
421
+ const contract = validateTaskFinalReviewContract(candidate.taskFinalReviewContract);
422
+ if (contract.taskId !== candidate.taskId) {
423
+ throw new Error("Task final-review contract belongs to another Task.");
424
+ }
425
+ const policy = taskFinalReviewConfig(contract);
426
+ if (candidate.reviewPolicy?.roleName !== policy.roleName
427
+ || candidate.reviewPolicy.trigger !== policy.trigger) {
428
+ throw new Error("Task final-review contract does not match the Candidate review policy.");
429
+ }
430
+ }
431
+ if (candidate.workspace !== undefined) {
432
+ validateManagedWorkspace(candidate.workspace);
433
+ if (candidate.workspace.owner.type !== "work-item"
434
+ || candidate.workspace.owner.taskId !== candidate.taskId
435
+ || candidate.workspace.owner.workItemId !== candidate.workItemId) {
436
+ throw new Error("Work Item candidate must use its WorkItem-owned workspace.");
437
+ }
438
+ }
439
+ if (candidate.gitSnapshot !== undefined) {
440
+ if (candidate.workspace === undefined) {
441
+ throw new Error("Candidate Git snapshot requires a managed workspace.");
442
+ }
443
+ validateCandidateGitSnapshot(candidate.gitSnapshot, candidate.workspace);
444
+ }
445
+ if (candidate.taskMainSnapshot !== undefined) {
446
+ if (candidate.source.type !== "direct"
447
+ || candidate.workspace !== undefined
448
+ || candidate.gitSnapshot !== undefined
449
+ || candidate.taskFinalReviewContract === undefined) {
450
+ throw new Error("Task-main Candidate snapshot requires an exact direct metadata-only Candidate.");
451
+ }
452
+ validateDirectTaskMainSnapshot(candidate.taskMainSnapshot);
453
+ }
454
+ requireTimestamp(candidate.createdAt, "Work Item candidate createdAt");
455
+ return candidate;
456
+ }
457
+ export function createDirectTaskMainSnapshot(workspace, projectIds, heads) {
458
+ validateManagedWorkspace(workspace);
459
+ if (workspace.owner.type !== "task") {
460
+ throw new Error("Direct Candidate snapshot must come from Task main.");
461
+ }
462
+ const selected = normalizedUniqueIdentities(projectIds, "Direct Candidate snapshot Project");
463
+ const headByProject = new Map(heads.map(({ projectId, headCommit }) => [
464
+ requireIdentity(projectId, "Direct Candidate snapshot Project"),
465
+ requireCommit(headCommit, "Direct Candidate snapshot head")
466
+ ]));
467
+ if (headByProject.size !== heads.length) {
468
+ throw new Error("Direct Candidate snapshot Projects are duplicated.");
469
+ }
470
+ const projects = workspace.entries
471
+ .filter(({ projectId }) => selected.includes(projectId))
472
+ .map((entry) => {
473
+ if (entry.access !== "write") {
474
+ throw new Error(`Direct Candidate snapshot Project is not writable: ${entry.projectId}.`);
475
+ }
476
+ const headCommit = headByProject.get(entry.projectId);
477
+ if (headCommit === undefined) {
478
+ throw new Error(`Direct Candidate snapshot Project head is missing: ${entry.projectId}.`);
479
+ }
480
+ return {
481
+ projectId: entry.projectId,
482
+ directory: entry.directory,
483
+ branch: entry.branch,
484
+ baseCommit: entry.baseCommit,
485
+ headCommit
486
+ };
487
+ });
488
+ if (projects.length !== selected.length || projects.length !== headByProject.size) {
489
+ throw new Error("Direct Candidate snapshot Project scope does not match Task main.");
490
+ }
491
+ return validateDirectTaskMainSnapshot({ schemaVersion: 1, projects });
492
+ }
493
+ function validateDirectTaskMainSnapshot(snapshot) {
494
+ if (typeof snapshot !== "object" || snapshot === null || snapshot.schemaVersion !== 1) {
495
+ throw new Error("Direct Candidate Task-main snapshot must use schemaVersion 1.");
496
+ }
497
+ if (!Array.isArray(snapshot.projects) || snapshot.projects.length === 0) {
498
+ throw new Error("Direct Candidate Task-main snapshot requires a Project.");
499
+ }
500
+ const projectIds = new Set();
501
+ for (const project of snapshot.projects) {
502
+ const projectId = requireIdentity(project.projectId, "Direct Candidate snapshot Project");
503
+ if (projectIds.has(projectId)) {
504
+ throw new Error("Direct Candidate snapshot Projects are duplicated.");
505
+ }
506
+ projectIds.add(projectId);
507
+ requireText(project.directory, "Direct Candidate snapshot directory");
508
+ requireText(project.branch, "Direct Candidate snapshot branch");
509
+ requireCommit(project.baseCommit, "Direct Candidate snapshot base");
510
+ requireCommit(project.headCommit, "Direct Candidate snapshot head");
511
+ }
512
+ return snapshot;
513
+ }
514
+ export function createCandidateGitSnapshot(workspace, projects) {
515
+ validateManagedWorkspace(workspace);
516
+ if (workspace.owner.type !== "work-item") {
517
+ throw new Error("Candidate Git snapshot must come from a WorkItem workspace.");
518
+ }
519
+ if (workspace.entries.length === 0) {
520
+ throw new Error("Candidate Git snapshot requires a Project workspace.");
521
+ }
522
+ const byProject = new Map(projects.map(({ projectId, commit: value }) => [
523
+ requireIdentity(projectId, "Candidate snapshot Project"),
524
+ requireCommit(value, "Candidate snapshot commit")
525
+ ]));
526
+ if (byProject.size !== projects.length) {
527
+ throw new Error("Candidate snapshot Projects are duplicated.");
528
+ }
529
+ const normalized = workspace.entries.map(({ projectId }) => {
530
+ const value = byProject.get(projectId);
531
+ if (value === undefined) {
532
+ throw new Error(`Candidate snapshot Project is missing: ${projectId}.`);
533
+ }
534
+ return { projectId, commit: value };
535
+ });
536
+ if (normalized.length !== byProject.size) {
537
+ throw new Error("Candidate snapshot contains a Project outside its workspace.");
538
+ }
539
+ const primary = workspace.entries.find(({ access }) => access === "write")
540
+ ?? workspace.entries[0];
541
+ return {
542
+ schemaVersion: 1,
543
+ reviewBaseCommit: byProject.get(primary.projectId),
544
+ projects: normalized
28
545
  };
29
546
  }
30
- function requireSafeIdentity(value, label) {
31
- const normalized = requireText(value, label);
32
- if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
33
- || /[\/\\\0]/.test(normalized)) {
34
- throw new Error(`${label} is invalid.`);
547
+ function validateCandidateGitSnapshot(snapshot, workspace) {
548
+ if (snapshot.schemaVersion !== 1) {
549
+ throw new Error("Candidate Git snapshot must use schemaVersion 1.");
550
+ }
551
+ requireCommit(snapshot.reviewBaseCommit, "Candidate review base commit");
552
+ const normalized = createCandidateGitSnapshot(workspace, snapshot.projects);
553
+ if (normalized.reviewBaseCommit !== snapshot.reviewBaseCommit
554
+ || JSON.stringify(normalized.projects) !== JSON.stringify(snapshot.projects)) {
555
+ throw new Error("Candidate Git snapshot does not match its managed workspace.");
35
556
  }
36
- return normalized;
37
557
  }
38
- function requireText(value, label) {
39
- if (typeof value !== "string" || value.includes("\0"))
558
+ function requireCommit(value, label) {
559
+ const commit = requireText(value, label).toLowerCase();
560
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
40
561
  throw new Error(`${label} is invalid.`);
41
- const normalized = value.trim();
42
- if (normalized.length === 0)
43
- throw new Error(`${label} is required.`);
44
- return normalized;
562
+ }
563
+ return commit;
564
+ }
565
+ export function currentWorkItemCandidate(workItem) {
566
+ return workItem.status === "awaiting_acceptance"
567
+ ? workItem.candidates.at(-1)
568
+ : undefined;
569
+ }
570
+ export function updateWorkItemWriteProjects(workItem, writeProjectIds, now) {
571
+ validateWorkItem(workItem);
572
+ if (isTerminalStatus(workItem.status)) {
573
+ throw new Error(`Terminal Work Item write scope cannot change: ${workItem.id}.`);
574
+ }
575
+ const normalized = normalizedUniqueIdentities(writeProjectIds, "Work item writable Project");
576
+ const requested = new Set(normalized);
577
+ const removed = workItem.writeProjectIds.filter((projectId) => !requested.has(projectId));
578
+ if (removed.length > 0) {
579
+ throw new Error(`Work Item write scope cannot remove approved Projects: ${removed.join(", ")}.`);
580
+ }
581
+ if (normalized.length === workItem.writeProjectIds.length
582
+ && workItem.writeProjectIds.every((projectId) => requested.has(projectId))) {
583
+ return workItem;
584
+ }
585
+ return validateWorkItem({
586
+ ...workItem,
587
+ writeProjectIds: normalized,
588
+ revision: workItem.revision + 1,
589
+ updatedAt: now.toISOString()
590
+ });
591
+ }
592
+ function isTerminalStatus(status) {
593
+ return TERMINAL_STATUSES.includes(status);
594
+ }
595
+ function validateStatus(status) {
596
+ if (![
597
+ "pending",
598
+ "running",
599
+ "awaiting_acceptance",
600
+ "completed",
601
+ "failed",
602
+ "retired"
603
+ ].includes(status)) {
604
+ throw new Error(`Work Item status is invalid: ${String(status)}.`);
605
+ }
606
+ }
607
+ function normalizeProjectBaseRefs(baseRefs) {
608
+ if (!Array.isArray(baseRefs)) {
609
+ throw new Error("Work Item Project base refs are invalid.");
610
+ }
611
+ const projectIds = new Set();
612
+ return baseRefs.map((entry) => {
613
+ if (typeof entry !== "object" || entry === null) {
614
+ throw new Error("Work Item Project base ref is invalid.");
615
+ }
616
+ const projectId = requireIdentity(entry.projectId, "Work Item Project id");
617
+ if (projectIds.has(projectId)) {
618
+ throw new Error(`Work Item Project base ref is duplicated: ${projectId}.`);
619
+ }
620
+ projectIds.add(projectId);
621
+ const baseRef = requireText(entry.baseRef, "Work Item Project base ref");
622
+ if (baseRef.startsWith("-") || /[\r\n]/u.test(baseRef)) {
623
+ throw new Error("Work Item Project base ref is invalid.");
624
+ }
625
+ return { projectId, baseRef };
626
+ });
627
+ }
628
+ function normalizeDisposition(input, now) {
629
+ if (input.by !== "leader") {
630
+ throw new Error("Only the Task Leader may retire a Work Item.");
631
+ }
632
+ const summary = requireText(input.summary, "Work item disposition summary");
633
+ const replacementWorkItemId = input.replacementWorkItemId;
634
+ if (replacementWorkItemId !== undefined) {
635
+ requireIdentity(replacementWorkItemId, "Replacement Work Item id");
636
+ }
637
+ const result = {
638
+ schemaVersion: 1,
639
+ by: "leader",
640
+ summary,
641
+ retiredAt: now.toISOString(),
642
+ ...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
643
+ };
644
+ return validateDisposition(result);
645
+ }
646
+ function validateDisposition(disposition) {
647
+ if (disposition.schemaVersion !== 1) {
648
+ throw new Error("Work Item disposition must use schemaVersion 1.");
649
+ }
650
+ if (disposition.by !== "leader") {
651
+ throw new Error("Work Item disposition actor is invalid.");
652
+ }
653
+ requireText(disposition.summary, "Work item disposition summary");
654
+ requireTimestamp(disposition.retiredAt, "Work Item retiredAt");
655
+ if (disposition.replacementWorkItemId !== undefined) {
656
+ requireIdentity(disposition.replacementWorkItemId, "Replacement Work Item id");
657
+ }
658
+ return disposition;
659
+ }
660
+ function sameDisposition(left, right) {
661
+ return left.by === right.by
662
+ && left.summary === right.summary
663
+ && left.replacementWorkItemId === right.replacementWorkItemId;
45
664
  }