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