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