@zq-silk/yui 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { requireIdentity } from "../domain/validation.js";
|
|
2
|
+
export const REVIEW_TRIGGERS = ["always", "leader", "final"];
|
|
3
|
+
/** The Reviewer Role seeded in a new Home by `yui setup`. */
|
|
4
|
+
export const DEFAULT_REVIEWER_ROLE = "reviewer";
|
|
5
|
+
export function validateReviewConfig(config) {
|
|
6
|
+
requireIdentity(config.roleName, "Review Role");
|
|
7
|
+
if (!REVIEW_TRIGGERS.includes(config.trigger)) {
|
|
8
|
+
throw new Error(`Review trigger is invalid: ${String(config.trigger)}.`);
|
|
9
|
+
}
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
+
import { validateTaskFinalReviewContract } from "./taskFinalReviewContract.js";
|
|
4
|
+
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
5
|
+
import { assertExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
6
|
+
export function createReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, reviewBaseCommit, now, executionGroup) {
|
|
7
|
+
return validateReviewRound({
|
|
8
|
+
schemaVersion: 4,
|
|
9
|
+
id: requireIdentity(id, "ReviewRound id"),
|
|
10
|
+
taskId: requireIdentity(taskId, "Task id"),
|
|
11
|
+
workItemId: requireIdentity(workItemId, "Work Item id"),
|
|
12
|
+
candidateId: requireIdentity(candidateId, "Candidate id"),
|
|
13
|
+
reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
|
|
14
|
+
reviewBaseCommit: requireCommit(reviewBaseCommit, "Review base commit"),
|
|
15
|
+
requestedBy: validateReviewRequestSource(requestedBy),
|
|
16
|
+
status: "pending",
|
|
17
|
+
...(executionGroup === undefined ? {} : { executionGroup }),
|
|
18
|
+
createdAt: now.toISOString()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function createTaskReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, taskCandidate, now, taskFinalReviewContract, executionGroup) {
|
|
22
|
+
const candidate = validateTaskReviewCandidate(taskCandidate);
|
|
23
|
+
return validateReviewRound({
|
|
24
|
+
schemaVersion: 4,
|
|
25
|
+
id: requireIdentity(id, "ReviewRound id"),
|
|
26
|
+
taskId: requireIdentity(taskId, "Task id"),
|
|
27
|
+
workItemId: requireIdentity(workItemId, "Work Item id"),
|
|
28
|
+
candidateId: requireIdentity(candidateId, "Candidate id"),
|
|
29
|
+
reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
|
|
30
|
+
reviewBaseCommit: candidate.projects[0].commit,
|
|
31
|
+
scope: "task",
|
|
32
|
+
taskCandidate: candidate,
|
|
33
|
+
...(taskFinalReviewContract === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { taskFinalReviewContract }),
|
|
36
|
+
requestedBy: validateReviewRequestSource(requestedBy),
|
|
37
|
+
status: "pending",
|
|
38
|
+
...(executionGroup === undefined ? {} : { executionGroup }),
|
|
39
|
+
createdAt: now.toISOString()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function attachReviewRoundWorkspace(round, workspace) {
|
|
43
|
+
validateReviewRound(round);
|
|
44
|
+
validateReviewWorkspace(round, workspace);
|
|
45
|
+
if (round.status !== "pending") {
|
|
46
|
+
throw new Error(`ReviewRound workspace cannot attach from ${round.status}: ${round.id}.`);
|
|
47
|
+
}
|
|
48
|
+
if (round.workspace !== undefined) {
|
|
49
|
+
if (JSON.stringify(round.workspace) !== JSON.stringify(workspace)) {
|
|
50
|
+
throw new Error(`ReviewRound workspace is immutable: ${round.id}.`);
|
|
51
|
+
}
|
|
52
|
+
return round;
|
|
53
|
+
}
|
|
54
|
+
return validateReviewRound({ ...round, workspace });
|
|
55
|
+
}
|
|
56
|
+
export function startReviewRound(round, reviewerRunId) {
|
|
57
|
+
validateReviewRound(round);
|
|
58
|
+
if (round.status !== "pending") {
|
|
59
|
+
throw new Error(`ReviewRound cannot start from ${round.status}: ${round.id}.`);
|
|
60
|
+
}
|
|
61
|
+
if (round.workspace === undefined) {
|
|
62
|
+
throw new Error(`ReviewRound workspace is not ready: ${round.id}.`);
|
|
63
|
+
}
|
|
64
|
+
return validateReviewRound({
|
|
65
|
+
...round,
|
|
66
|
+
reviewerRunId: requireIdentity(reviewerRunId, "Reviewer Run id"),
|
|
67
|
+
status: "running"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function finishReviewRound(round, status, summary, now, result = {}) {
|
|
71
|
+
validateReviewRound(round);
|
|
72
|
+
if (round.status !== "pending" && round.status !== "running") {
|
|
73
|
+
throw new Error(`ReviewRound is already terminal: ${round.id}.`);
|
|
74
|
+
}
|
|
75
|
+
return validateReviewRound({
|
|
76
|
+
...round,
|
|
77
|
+
status,
|
|
78
|
+
summary: requireText(summary, "Review summary"),
|
|
79
|
+
report: requireText(result.report ?? summary, "Review report"),
|
|
80
|
+
checks: validateChecks(result.checks ?? []),
|
|
81
|
+
...(result.evidenceCommit === undefined
|
|
82
|
+
? {}
|
|
83
|
+
: { evidenceCommit: requireCommit(result.evidenceCommit, "Review evidence commit") }),
|
|
84
|
+
endedAt: now.toISOString()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Accepts the Reviewer's complete report and extracts only optional known evidence. */
|
|
88
|
+
export function parseReviewYieldReport(value) {
|
|
89
|
+
const report = requireText(value, "Review report");
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(report);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return { summary: report, report, checks: [] };
|
|
96
|
+
}
|
|
97
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
98
|
+
return { summary: report, report, checks: [] };
|
|
99
|
+
}
|
|
100
|
+
const record = parsed;
|
|
101
|
+
const summary = typeof record.summary === "string" && record.summary.trim().length > 0
|
|
102
|
+
? requireText(record.summary, "Review summary")
|
|
103
|
+
: report;
|
|
104
|
+
const checks = extractChecks(record.checks);
|
|
105
|
+
const findings = extractFindings(record.findings);
|
|
106
|
+
const evidence = extractEvidence(record.evidence);
|
|
107
|
+
return {
|
|
108
|
+
summary,
|
|
109
|
+
report,
|
|
110
|
+
checks,
|
|
111
|
+
...(findings.length === 0 ? {} : { findings }),
|
|
112
|
+
...(evidence.length === 0 ? {} : { evidence }),
|
|
113
|
+
...(typeof record.evidenceCommit !== "string"
|
|
114
|
+
? {}
|
|
115
|
+
: {
|
|
116
|
+
evidenceCommit: requireCommit(record.evidenceCommit, "Review evidence commit")
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function extractFindings(value) {
|
|
121
|
+
if (!Array.isArray(value))
|
|
122
|
+
return [];
|
|
123
|
+
return value.flatMap((entry) => {
|
|
124
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const finding = entry;
|
|
128
|
+
if (typeof finding.id !== "string"
|
|
129
|
+
|| (finding.severity !== "low" && finding.severity !== "medium"
|
|
130
|
+
&& finding.severity !== "high" && finding.severity !== "critical"
|
|
131
|
+
&& finding.severity !== "p1" && finding.severity !== "p2")
|
|
132
|
+
|| (finding.status !== "open" && finding.status !== "resolved")
|
|
133
|
+
|| typeof finding.summary !== "string") {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
return [{
|
|
137
|
+
id: requireIdentity(finding.id, "Review finding id"),
|
|
138
|
+
// Reviewers may use the product-level P1/P2 vocabulary. Execution
|
|
139
|
+
// groups keep one canonical severity scale so resolution gates remain
|
|
140
|
+
// stable across Worker and Reviewer reports.
|
|
141
|
+
severity: finding.severity === "p1"
|
|
142
|
+
? "critical"
|
|
143
|
+
: finding.severity === "p2"
|
|
144
|
+
? "high"
|
|
145
|
+
: finding.severity,
|
|
146
|
+
status: finding.status,
|
|
147
|
+
summary: requireText(finding.summary, "Review finding summary")
|
|
148
|
+
}];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function extractEvidence(value) {
|
|
152
|
+
if (!Array.isArray(value))
|
|
153
|
+
return [];
|
|
154
|
+
return value.flatMap((entry) => (typeof entry === "string" && entry.trim().length > 0 ? [entry.trim()] : []));
|
|
155
|
+
}
|
|
156
|
+
export function recordReviewWorkspaceDisposition(round, disposition, now) {
|
|
157
|
+
validateReviewRound(round);
|
|
158
|
+
if (round.status !== "completed" && round.status !== "failed") {
|
|
159
|
+
throw new Error(`ReviewRound must be terminal before workspace disposition: ${round.id}.`);
|
|
160
|
+
}
|
|
161
|
+
if (round.workspace === undefined) {
|
|
162
|
+
throw new Error(`ReviewRound has no managed workspace: ${round.id}.`);
|
|
163
|
+
}
|
|
164
|
+
if (disposition !== "preserved" && disposition !== "removed") {
|
|
165
|
+
throw new Error(`Review workspace disposition is invalid: ${String(disposition)}.`);
|
|
166
|
+
}
|
|
167
|
+
if (round.workspaceDisposition?.kind === disposition)
|
|
168
|
+
return round;
|
|
169
|
+
if (round.workspaceDisposition?.kind === "removed") {
|
|
170
|
+
throw new Error(`ReviewRound workspace is already removed: ${round.id}.`);
|
|
171
|
+
}
|
|
172
|
+
return validateReviewRound({
|
|
173
|
+
...round,
|
|
174
|
+
workspaceDisposition: { kind: disposition, recordedAt: now.toISOString() }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** Attach the common Reviewer panel Group while preserving the Round target. */
|
|
178
|
+
export function attachReviewExecutionGroup(round, executionGroup) {
|
|
179
|
+
validateReviewRound(round);
|
|
180
|
+
validateReviewExecutionGroup(executionGroup, round);
|
|
181
|
+
if (round.status !== "pending") {
|
|
182
|
+
throw new Error(`ReviewRound ExecutionGroup cannot attach from ${round.status}: ${round.id}.`);
|
|
183
|
+
}
|
|
184
|
+
if (round.executionGroup !== undefined) {
|
|
185
|
+
if (JSON.stringify(round.executionGroup) !== JSON.stringify(executionGroup)) {
|
|
186
|
+
throw new Error(`ReviewRound ExecutionGroup is immutable: ${round.id}.`);
|
|
187
|
+
}
|
|
188
|
+
return round;
|
|
189
|
+
}
|
|
190
|
+
return validateReviewRound({ ...round, executionGroup });
|
|
191
|
+
}
|
|
192
|
+
/** Advance a Reviewer panel Group without changing the Round target. */
|
|
193
|
+
export function updateReviewExecutionGroup(round, executionGroup) {
|
|
194
|
+
validateReviewRound(round);
|
|
195
|
+
validateReviewExecutionGroup(executionGroup, round);
|
|
196
|
+
if (round.executionGroup === undefined) {
|
|
197
|
+
if (round.status !== "pending") {
|
|
198
|
+
throw new Error(`ReviewRound ExecutionGroup cannot attach from ${round.status}: ${round.id}.`);
|
|
199
|
+
}
|
|
200
|
+
return validateReviewRound({ ...round, executionGroup });
|
|
201
|
+
}
|
|
202
|
+
if (round.executionGroup.id !== executionGroup.id
|
|
203
|
+
|| JSON.stringify(round.executionGroup.target) !== JSON.stringify(executionGroup.target)) {
|
|
204
|
+
throw new Error(`ReviewRound ExecutionGroup target is immutable: ${round.id}.`);
|
|
205
|
+
}
|
|
206
|
+
if (JSON.stringify(round.executionGroup) === JSON.stringify(executionGroup))
|
|
207
|
+
return round;
|
|
208
|
+
assertExecutionGroupTransition(round.executionGroup, executionGroup);
|
|
209
|
+
return validateReviewRound({ ...round, executionGroup });
|
|
210
|
+
}
|
|
211
|
+
export function validateReviewRound(round) {
|
|
212
|
+
if (round.schemaVersion !== 4)
|
|
213
|
+
throw new Error("ReviewRound must use schemaVersion 4.");
|
|
214
|
+
validateTaskRecordReference({ taskId: round.taskId, localId: round.id }, "reviewRound");
|
|
215
|
+
validateTaskRecordReference({ taskId: round.taskId, localId: round.workItemId }, "workItem");
|
|
216
|
+
if (!/^candidate-[1-9]\d*$/.test(round.candidateId)) {
|
|
217
|
+
throw new Error(`Candidate local id is invalid: ${round.candidateId}.`);
|
|
218
|
+
}
|
|
219
|
+
requireIdentity(round.reviewerRoleName, "Reviewer Role");
|
|
220
|
+
requireCommit(round.reviewBaseCommit, "Review base commit");
|
|
221
|
+
const scope = round.scope ?? "work-item";
|
|
222
|
+
if (scope !== "work-item" && scope !== "task") {
|
|
223
|
+
throw new Error(`ReviewRound scope is invalid: ${String(round.scope)}.`);
|
|
224
|
+
}
|
|
225
|
+
if (scope === "task") {
|
|
226
|
+
if (round.taskCandidate === undefined) {
|
|
227
|
+
throw new Error(`Task ReviewRound requires a frozen Task candidate: ${round.id}.`);
|
|
228
|
+
}
|
|
229
|
+
const candidate = validateTaskReviewCandidate(round.taskCandidate);
|
|
230
|
+
if (candidate.projects[0].commit !== round.reviewBaseCommit) {
|
|
231
|
+
throw new Error(`Task ReviewRound base does not match its primary Project: ${round.id}.`);
|
|
232
|
+
}
|
|
233
|
+
if (round.taskFinalReviewContract !== undefined) {
|
|
234
|
+
const contract = validateTaskFinalReviewContract(round.taskFinalReviewContract);
|
|
235
|
+
if (contract.taskId !== round.taskId) {
|
|
236
|
+
throw new Error(`Task ReviewRound contract belongs to another Task: ${round.id}.`);
|
|
237
|
+
}
|
|
238
|
+
if (contract.reviewerRoleName !== round.reviewerRoleName) {
|
|
239
|
+
throw new Error(`Task ReviewRound contract uses another Reviewer Role: ${round.id}.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (round.taskCandidate !== undefined
|
|
244
|
+
|| round.taskFinalReviewContract !== undefined) {
|
|
245
|
+
throw new Error(`WorkItem ReviewRound cannot carry a Task candidate or contract: ${round.id}.`);
|
|
246
|
+
}
|
|
247
|
+
if (round.executionGroup !== undefined)
|
|
248
|
+
validateReviewExecutionGroup(round.executionGroup, round);
|
|
249
|
+
validateReviewRequestSource(round.requestedBy);
|
|
250
|
+
if (!["pending", "running", "completed", "failed"].includes(round.status)) {
|
|
251
|
+
throw new Error(`ReviewRound status is invalid: ${String(round.status)}.`);
|
|
252
|
+
}
|
|
253
|
+
if (round.reviewerRunId !== undefined) {
|
|
254
|
+
validateTaskRecordReference({
|
|
255
|
+
taskId: round.taskId,
|
|
256
|
+
localId: round.reviewerRunId
|
|
257
|
+
}, "agentRun");
|
|
258
|
+
}
|
|
259
|
+
if (round.workspace !== undefined)
|
|
260
|
+
validateReviewWorkspace(round, round.workspace);
|
|
261
|
+
requireTimestamp(round.createdAt, "ReviewRound createdAt");
|
|
262
|
+
const terminal = round.status === "completed" || round.status === "failed";
|
|
263
|
+
if (terminal) {
|
|
264
|
+
requireText(round.summary ?? "", "Review summary");
|
|
265
|
+
requireText(round.report ?? "", "Review report");
|
|
266
|
+
validateChecks(round.checks ?? []);
|
|
267
|
+
if (round.evidenceCommit !== undefined) {
|
|
268
|
+
requireCommit(round.evidenceCommit, "Review evidence commit");
|
|
269
|
+
if (round.evidenceCommit === round.reviewBaseCommit) {
|
|
270
|
+
throw new Error("Review evidence commit must differ from its review base.");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
requireTimestamp(round.endedAt ?? "", "ReviewRound endedAt");
|
|
274
|
+
}
|
|
275
|
+
else if (round.summary !== undefined
|
|
276
|
+
|| round.report !== undefined
|
|
277
|
+
|| round.checks !== undefined
|
|
278
|
+
|| round.evidenceCommit !== undefined
|
|
279
|
+
|| round.endedAt !== undefined) {
|
|
280
|
+
throw new Error("An active ReviewRound cannot have terminal metadata.");
|
|
281
|
+
}
|
|
282
|
+
if (round.status === "running"
|
|
283
|
+
&& (round.reviewerRunId === undefined || round.workspace === undefined)) {
|
|
284
|
+
throw new Error("A running ReviewRound requires a Reviewer Run and workspace.");
|
|
285
|
+
}
|
|
286
|
+
if (round.workspaceDisposition !== undefined) {
|
|
287
|
+
if (!terminal || round.workspace === undefined) {
|
|
288
|
+
throw new Error("Only a terminal ReviewRound workspace can have a disposition.");
|
|
289
|
+
}
|
|
290
|
+
if (round.workspaceDisposition.kind !== "preserved"
|
|
291
|
+
&& round.workspaceDisposition.kind !== "removed") {
|
|
292
|
+
throw new Error("Review workspace disposition is invalid.");
|
|
293
|
+
}
|
|
294
|
+
requireTimestamp(round.workspaceDisposition.recordedAt, "Review workspace disposition time");
|
|
295
|
+
}
|
|
296
|
+
return round;
|
|
297
|
+
}
|
|
298
|
+
function validateReviewExecutionGroup(group, round) {
|
|
299
|
+
validateExecutionGroup(group);
|
|
300
|
+
if (group.taskId !== round.taskId || group.purpose !== "review") {
|
|
301
|
+
throw new Error(`ReviewRound ExecutionGroup provenance is invalid: ${round.id}.`);
|
|
302
|
+
}
|
|
303
|
+
const expectedKind = (round.scope ?? "work-item") === "task"
|
|
304
|
+
? "task-final-review"
|
|
305
|
+
: "work-item";
|
|
306
|
+
if (group.target.kind !== expectedKind
|
|
307
|
+
|| group.target.taskId !== round.taskId
|
|
308
|
+
|| group.target.candidateId !== round.candidateId) {
|
|
309
|
+
throw new Error(`ReviewRound ExecutionGroup target is invalid: ${round.id}.`);
|
|
310
|
+
}
|
|
311
|
+
if (expectedKind === "work-item" && group.target.workItemId !== round.workItemId) {
|
|
312
|
+
throw new Error(`ReviewRound ExecutionGroup WorkItem target is invalid: ${round.id}.`);
|
|
313
|
+
}
|
|
314
|
+
return group;
|
|
315
|
+
}
|
|
316
|
+
export function validateTaskReviewCandidate(candidate) {
|
|
317
|
+
if (candidate.schemaVersion !== 1) {
|
|
318
|
+
throw new Error("Task Review candidate must use schemaVersion 1.");
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(candidate.projects) || candidate.projects.length === 0) {
|
|
321
|
+
throw new Error("Task Review candidate requires at least one Project.");
|
|
322
|
+
}
|
|
323
|
+
const projects = candidate.projects.map(({ projectId, commit }) => ({
|
|
324
|
+
projectId: requireIdentity(projectId, "Task Review Project"),
|
|
325
|
+
commit: requireCommit(commit, "Task Review Project commit")
|
|
326
|
+
}));
|
|
327
|
+
if (new Set(projects.map(({ projectId }) => projectId)).size !== projects.length) {
|
|
328
|
+
throw new Error("Task Review candidate Projects must be unique.");
|
|
329
|
+
}
|
|
330
|
+
return { schemaVersion: 1, projects };
|
|
331
|
+
}
|
|
332
|
+
function extractChecks(value) {
|
|
333
|
+
if (!Array.isArray(value))
|
|
334
|
+
return [];
|
|
335
|
+
const extracted = value.flatMap((entry) => {
|
|
336
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
337
|
+
return [];
|
|
338
|
+
const check = entry;
|
|
339
|
+
if (typeof check.name !== "string"
|
|
340
|
+
|| (check.outcome !== "passed"
|
|
341
|
+
&& check.outcome !== "failed"
|
|
342
|
+
&& check.outcome !== "skipped"))
|
|
343
|
+
return [];
|
|
344
|
+
return [{
|
|
345
|
+
name: check.name,
|
|
346
|
+
outcome: check.outcome,
|
|
347
|
+
...(typeof check.details === "string" ? { details: check.details } : {})
|
|
348
|
+
}];
|
|
349
|
+
});
|
|
350
|
+
return validateChecks(extracted);
|
|
351
|
+
}
|
|
352
|
+
function validateReviewWorkspace(round, workspace) {
|
|
353
|
+
validateManagedWorkspace(workspace);
|
|
354
|
+
if (workspace.owner.taskId !== round.taskId) {
|
|
355
|
+
throw new Error(`ReviewRound workspace provenance is invalid: ${round.id}.`);
|
|
356
|
+
}
|
|
357
|
+
if (workspace.owner.type !== "review-round"
|
|
358
|
+
|| workspace.owner.reviewRoundId !== round.id) {
|
|
359
|
+
throw new Error(`ReviewRound does not own its workspace: ${round.id}.`);
|
|
360
|
+
}
|
|
361
|
+
if (workspace.entries.length === 0
|
|
362
|
+
|| workspace.entries.some(({ access }) => access !== "write")) {
|
|
363
|
+
throw new Error(`ReviewRound workspace must contain only writable isolated entries: ${round.id}.`);
|
|
364
|
+
}
|
|
365
|
+
if (!workspace.entries.some(({ baseCommit }) => baseCommit === round.reviewBaseCommit)) {
|
|
366
|
+
throw new Error(`ReviewRound workspace does not contain its review base: ${round.id}.`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function validateChecks(checks) {
|
|
370
|
+
if (!Array.isArray(checks))
|
|
371
|
+
throw new Error("Review checks are invalid.");
|
|
372
|
+
return checks.map((check) => {
|
|
373
|
+
const name = requireText(check.name, "Review check name");
|
|
374
|
+
if (check.outcome !== "passed" && check.outcome !== "failed"
|
|
375
|
+
&& check.outcome !== "skipped") {
|
|
376
|
+
throw new Error(`Review check outcome is invalid: ${String(check.outcome)}.`);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
name,
|
|
380
|
+
outcome: check.outcome,
|
|
381
|
+
...(check.details === undefined
|
|
382
|
+
? {}
|
|
383
|
+
: { details: requireText(check.details, "Review check details") })
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function requireCommit(value, label) {
|
|
388
|
+
const commit = requireText(value, label).toLowerCase();
|
|
389
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
|
|
390
|
+
throw new Error(`${label} is invalid.`);
|
|
391
|
+
}
|
|
392
|
+
return commit;
|
|
393
|
+
}
|
|
394
|
+
function validateReviewRequestSource(source) {
|
|
395
|
+
if (source !== "policy" && source !== "leader") {
|
|
396
|
+
throw new Error(`Review request source is invalid: ${String(source)}.`);
|
|
397
|
+
}
|
|
398
|
+
return source;
|
|
399
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { requireIdentity } from "../domain/validation.js";
|
|
3
|
+
export const TASK_FINAL_REVIEW_ARGUMENT = "--yui-task-final-review";
|
|
4
|
+
export function createTaskFinalReviewContract(input) {
|
|
5
|
+
const taskId = requireIdentity(input.taskId, "Task final-review contract Task id");
|
|
6
|
+
const reviewerRoleName = requireIdentity(input.reviewerRoleName, "Task final-review contract Reviewer Role");
|
|
7
|
+
const controlPlaneDigest = requireDigest(input.controlPlaneDigest, "Task final-review contract control-plane digest");
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
taskId,
|
|
11
|
+
reviewerRoleName,
|
|
12
|
+
controlPlaneDigest,
|
|
13
|
+
digest: contractDigest(taskId, reviewerRoleName, controlPlaneDigest)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function validateTaskFinalReviewContract(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null || value.schemaVersion !== 1) {
|
|
18
|
+
throw new Error("Task final-review contract must use schemaVersion 1.");
|
|
19
|
+
}
|
|
20
|
+
const expected = createTaskFinalReviewContract(value);
|
|
21
|
+
const digest = requireDigest(value.digest, "Task final-review contract digest");
|
|
22
|
+
if (digest !== expected.digest) {
|
|
23
|
+
throw new Error("Task final-review contract digest does not match its immutable fields.");
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function taskFinalReviewConfig(contract) {
|
|
28
|
+
const validated = validateTaskFinalReviewContract(contract);
|
|
29
|
+
return { roleName: validated.reviewerRoleName, trigger: "final" };
|
|
30
|
+
}
|
|
31
|
+
export function sameTaskFinalReviewContract(left, right) {
|
|
32
|
+
if (left === undefined || right === undefined)
|
|
33
|
+
return left === right;
|
|
34
|
+
return validateTaskFinalReviewContract(left).digest
|
|
35
|
+
=== validateTaskFinalReviewContract(right).digest;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The contract switch is an exact CLI prefix, never an environment variable.
|
|
39
|
+
* It must immediately follow `--yui-control <digest>` so the preflight can
|
|
40
|
+
* bind it to the one verified Task runtime before opening mutable storage.
|
|
41
|
+
*/
|
|
42
|
+
export function extractTaskFinalReviewRequest(args) {
|
|
43
|
+
const index = args.indexOf(TASK_FINAL_REVIEW_ARGUMENT);
|
|
44
|
+
if (index < 0)
|
|
45
|
+
return { args: [...args] };
|
|
46
|
+
if (index !== 0) {
|
|
47
|
+
return {
|
|
48
|
+
args: [...args],
|
|
49
|
+
error: `${TASK_FINAL_REVIEW_ARGUMENT} must immediately follow the exact control prefix.`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const taskId = args[1];
|
|
53
|
+
const reviewerRoleName = args[2];
|
|
54
|
+
if (taskId === undefined || reviewerRoleName === undefined) {
|
|
55
|
+
return {
|
|
56
|
+
args: args.slice(3),
|
|
57
|
+
error: `${TASK_FINAL_REVIEW_ARGUMENT} requires <task-id> <reviewer-role>.`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return {
|
|
62
|
+
request: {
|
|
63
|
+
taskId: requireIdentity(taskId, "Task final-review contract Task id"),
|
|
64
|
+
reviewerRoleName: requireIdentity(reviewerRoleName, "Task final-review contract Reviewer Role")
|
|
65
|
+
},
|
|
66
|
+
args: args.slice(3)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
args: args.slice(3),
|
|
72
|
+
error: error instanceof Error ? error.message : String(error)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function contractDigest(taskId, reviewerRoleName, controlPlaneDigest) {
|
|
77
|
+
return createHash("sha256").update(JSON.stringify([
|
|
78
|
+
"yui-task-final-review",
|
|
79
|
+
1,
|
|
80
|
+
taskId,
|
|
81
|
+
reviewerRoleName,
|
|
82
|
+
controlPlaneDigest
|
|
83
|
+
])).digest("hex");
|
|
84
|
+
}
|
|
85
|
+
function requireDigest(value, label) {
|
|
86
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
|
|
87
|
+
throw new Error(`${label} is invalid.`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|