@zq-silk/yui 0.15.12 → 0.16.0
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 +3 -2
- package/ARCHITECTURE.zh-CN.md +3 -2
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +57 -41
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1280 -1243
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/globalRoleCommands.js +53 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +1 -10
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +271 -710
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +17 -57
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +3 -3
- package/dist/commands/taskPublicationCommands.js +1 -1
- package/dist/commands/taskPublicationVerifyCommand.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +2 -3
- package/dist/context/taskCatalog.js +3 -5
- package/dist/context/taskContext.js +139 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +9 -22
- package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryDelivery.js +7 -7
- package/dist/controller/runtime.js +51 -44
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +60 -84
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +1 -9
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/projectMaintenanceLock.js +23 -64
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +13 -22
- package/dist/repository/taskWorkspacePreparer.js +29 -25
- package/dist/repository/workspaceCleanupInspection.js +5 -5
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +15 -15
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +4 -41
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +7 -7
- package/dist/scheduler/leaderWakeupProcessor.js +19 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +25 -33
- package/dist/scheduler/taskObservabilityProjection.js +0 -1
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +142 -1
- package/dist/storage/sqliteStore.js +156 -162
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +15 -7
- package/dist/storage/taskStore.js +3 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/taskSurface.js +4 -4
- package/dist/web/webServer.js +4 -6
- package/dist/web/webSnapshot.js +3 -83
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/docs/architecture/capabilities-and-resources.md +14 -2
- package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
- package/docs/managed-turn-and-session-runtime.md +83 -10
- package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
- package/docs/plugin-sdk.md +4 -2
- package/docs/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +91 -5
- package/docs/task-delivery.zh-CN.md +72 -4
- package/docs/task-discovery.md +10 -11
- package/docs/task-discovery.zh-CN.md +8 -9
- package/docs/testing/verification-levels.md +145 -9
- package/docs/testing/verification-levels.zh-CN.md +103 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +15 -10
- package/skills/yui-runtime/SKILL.md +6 -6
- package/skills/yui-runtime/references/recovery.md +47 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -377
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { usageError } from "../errors/cliError.js";
|
|
3
|
-
import { installCompletion, uninstallCompletion } from "./completionInstaller.js";
|
|
4
|
-
import { COMPLETION_SHELLS, activationBlock, activationIsAutomatic, currentCompletionShell, inspectCompletionStates, renderCompletionStateTable, shellQuote, suggestedCompletionInstallation } from "./completionState.js";
|
|
5
|
-
export async function runCompletionWizard(operation, store, env, identity, question, options = {}) {
|
|
6
|
-
const states = inspectCompletionStates(store.getConfig(), env, identity);
|
|
7
|
-
const current = currentCompletionShell(env);
|
|
8
|
-
let shell;
|
|
9
|
-
if (options.shell === undefined) {
|
|
10
|
-
const defaultSelection = options.defaultSelection ?? "current-shell";
|
|
11
|
-
const defaultHint = defaultSelection === "skip"
|
|
12
|
-
? " [skip]"
|
|
13
|
-
: current === undefined ? "" : ` [${current}]`;
|
|
14
|
-
const skipHint = defaultSelection === "skip" ? "" : " (or skip)";
|
|
15
|
-
const answer = await question(`${renderCompletionStateTable(states, options.width)}\n`
|
|
16
|
-
+ `Choose shell by number or name${defaultHint}${skipHint}: `);
|
|
17
|
-
const normalized = answer.trim().toLowerCase();
|
|
18
|
-
if (normalized === "skip" || (normalized.length === 0 && defaultSelection === "skip")) {
|
|
19
|
-
return `Completion ${operation} skipped.\n`;
|
|
20
|
-
}
|
|
21
|
-
shell = parseSelection(answer, current);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
shell = options.shell;
|
|
25
|
-
}
|
|
26
|
-
const state = states.find((candidate) => candidate.shell === shell);
|
|
27
|
-
if (state === undefined)
|
|
28
|
-
throw usageError("Completion shell selection is invalid.");
|
|
29
|
-
if (operation === "uninstall") {
|
|
30
|
-
if (state.installation === undefined) {
|
|
31
|
-
return `Completion ${shell} is not installed.\n`;
|
|
32
|
-
}
|
|
33
|
-
const confirm = normalize(await question(`Remove managed ${shell} completion?\n`
|
|
34
|
-
+ `Script: ${state.installation.scriptPath}\n`
|
|
35
|
-
+ `Activation: ${state.installation.activationPath}\n`
|
|
36
|
-
+ "Remove now? [y/N]: "));
|
|
37
|
-
if (confirm !== "y" && confirm !== "yes") {
|
|
38
|
-
return `Completion ${shell} uninstall skipped.\n`;
|
|
39
|
-
}
|
|
40
|
-
uninstallCompletion(store, shell, identity);
|
|
41
|
-
return `Completion ${shell} uninstalled.\n`;
|
|
42
|
-
}
|
|
43
|
-
const suggested = state.installation
|
|
44
|
-
?? suggestedCompletionInstallation(shell, env, identity);
|
|
45
|
-
const automatic = activationIsAutomatic(shell, suggested, env, identity);
|
|
46
|
-
const decision = normalize(await question(`Selected: ${shell} (${state.action})\n`
|
|
47
|
-
+ `Script: ${suggested.scriptPath}\n`
|
|
48
|
-
+ `Activation: ${suggested.activationPath}`
|
|
49
|
-
+ `${automatic ? " (automatic)" : " (managed startup block)"}\n`
|
|
50
|
-
+ "Install using these paths? [Y/n/customize]: "));
|
|
51
|
-
if (decision === "n" || decision === "no") {
|
|
52
|
-
return `Completion ${shell} installation skipped.\n`;
|
|
53
|
-
}
|
|
54
|
-
let installation = suggested;
|
|
55
|
-
let customized = false;
|
|
56
|
-
if (decision === "customize") {
|
|
57
|
-
const scriptPath = resolve((await question(`Completion script path [${suggested.scriptPath}]: `)).trim()
|
|
58
|
-
|| suggested.scriptPath);
|
|
59
|
-
const activationPath = resolve((await question(`Activation file path [${suggested.activationPath}]: `)).trim()
|
|
60
|
-
|| suggested.activationPath);
|
|
61
|
-
installation = { scriptPath, activationPath };
|
|
62
|
-
customized = true;
|
|
63
|
-
}
|
|
64
|
-
else if (decision !== "" && decision !== "y" && decision !== "yes") {
|
|
65
|
-
throw usageError("Choose Y, n, or customize.");
|
|
66
|
-
}
|
|
67
|
-
const needsActivation = !activationIsAutomatic(shell, installation, env, identity);
|
|
68
|
-
let activate = false;
|
|
69
|
-
let activationReady = !needsActivation;
|
|
70
|
-
if (needsActivation) {
|
|
71
|
-
const activationAnswer = normalize(await question(`${activationBlock(shell, installation, identity)}\n`
|
|
72
|
-
+ `Update ${installation.activationPath} with the managed Yui block? [Y/n]: `));
|
|
73
|
-
if (activationAnswer !== ""
|
|
74
|
-
&& activationAnswer !== "y"
|
|
75
|
-
&& activationAnswer !== "yes"
|
|
76
|
-
&& activationAnswer !== "n"
|
|
77
|
-
&& activationAnswer !== "no") {
|
|
78
|
-
throw usageError("Choose Y or n.");
|
|
79
|
-
}
|
|
80
|
-
activate = activationAnswer === ""
|
|
81
|
-
|| activationAnswer === "y"
|
|
82
|
-
|| activationAnswer === "yes";
|
|
83
|
-
activationReady = activate || (!customized && state.status === "Installed");
|
|
84
|
-
}
|
|
85
|
-
installCompletion(store, shell, installation, env, identity, activate);
|
|
86
|
-
if (!activationReady) {
|
|
87
|
-
return `Completion ${shell} script installed; activation still required.\n`;
|
|
88
|
-
}
|
|
89
|
-
const result = `Completion ${shell} ${pastTense(state.action)}.\n`;
|
|
90
|
-
return needsActivation
|
|
91
|
-
? `${result}${activationGuidance(shell, installation, env, identity)}`
|
|
92
|
-
: result;
|
|
93
|
-
}
|
|
94
|
-
function activationGuidance(shell, installation, env, identity) {
|
|
95
|
-
const unchanged = "The current shell is unchanged.\n";
|
|
96
|
-
const defaultActivationPath = suggestedCompletionInstallation(shell, env, identity).activationPath;
|
|
97
|
-
if (installation.activationPath !== defaultActivationPath) {
|
|
98
|
-
return `${unchanged}From a ${shell} session, load the custom activation file: source ${shellQuote(installation.activationPath)}\n`;
|
|
99
|
-
}
|
|
100
|
-
return currentCompletionShell(env) === shell
|
|
101
|
-
? `${unchanged}Restart the current shell to activate completion: exec ${shell}\n`
|
|
102
|
-
: `${unchanged}Switch this terminal to ${shell} to activate completion: exec ${shell}\n`;
|
|
103
|
-
}
|
|
104
|
-
function parseSelection(value, current) {
|
|
105
|
-
const normalized = value.trim().toLowerCase();
|
|
106
|
-
if (normalized.length === 0 && current !== undefined)
|
|
107
|
-
return current;
|
|
108
|
-
if (/^[1-3]$/.test(normalized)) {
|
|
109
|
-
return COMPLETION_SHELLS[Number.parseInt(normalized, 10) - 1];
|
|
110
|
-
}
|
|
111
|
-
if (COMPLETION_SHELLS.includes(normalized)) {
|
|
112
|
-
return normalized;
|
|
113
|
-
}
|
|
114
|
-
throw usageError("Choose Bash, Zsh, or Fish by number or name.");
|
|
115
|
-
}
|
|
116
|
-
function pastTense(action) {
|
|
117
|
-
switch (action) {
|
|
118
|
-
case "Install": return "installed";
|
|
119
|
-
case "Refresh": return "refreshed";
|
|
120
|
-
case "Repair": return "repaired";
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function normalize(value) {
|
|
124
|
-
return value?.trim().toLowerCase() ?? "";
|
|
125
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
export function buildRoleContext(context) {
|
|
2
|
-
return context.role.name === "leader"
|
|
3
|
-
? buildLeaderContext(context)
|
|
4
|
-
: buildWorkerContext(context);
|
|
5
|
-
}
|
|
6
|
-
export function buildLeaderContext(context) {
|
|
7
|
-
return renderDispatchContext("leader", context);
|
|
8
|
-
}
|
|
9
|
-
export function buildWorkerContext(context) {
|
|
10
|
-
return renderDispatchContext("worker", context);
|
|
11
|
-
}
|
|
12
|
-
const WORKER_RUN_COMPLETION_MARKER = "Yui Role AgentRun result requirement:";
|
|
13
|
-
const WORKER_RUN_COMPLETION_REQUIREMENT = [
|
|
14
|
-
WORKER_RUN_COMPLETION_MARKER,
|
|
15
|
-
"End the Provider turn with a truthful final report. Yui records that native turn result "
|
|
16
|
-
+ "for the current AgentRun automatically; no completion command is required.",
|
|
17
|
-
"If you cannot finally determine success, failure, completeness, or the correct "
|
|
18
|
-
+ "disposition, do not guess, silently stop, or hide uncertainty behind a success "
|
|
19
|
-
+ "summary. Label the final report uncertain, incomplete, "
|
|
20
|
-
+ "blocked, or requiring Leader judgment.",
|
|
21
|
-
"Report the most complete truthful evidence available and, when applicable: exact AgentRun, "
|
|
22
|
-
+ "WorkItem, and native Session identity; actions actually performed; changed paths "
|
|
23
|
-
+ "and commit/worktree state; checks actually run and their outcomes; provider, runtime, "
|
|
24
|
-
+ "or permission errors; the last confirmed lifecycle boundary; work not performed; "
|
|
25
|
-
+ "unresolved assumptions or decisions; residual risks; confidence; and bounded next options.",
|
|
26
|
-
"The Provider turn ending closes only this AgentRun. It does not imply Leader acceptance, "
|
|
27
|
-
+ "WorkItem completion, ChangeSet capture, Integration, or Task completion. Review AgentRuns "
|
|
28
|
-
+ "report findings, verification gaps, and limits; the Leader decides disposition."
|
|
29
|
-
].join("\n");
|
|
30
|
-
export function ensureWorkerRunCompletionRequirement(input) {
|
|
31
|
-
return input.endsWith(`\n\n${WORKER_RUN_COMPLETION_REQUIREMENT}`)
|
|
32
|
-
? input
|
|
33
|
-
: `${input}\n\n${WORKER_RUN_COMPLETION_REQUIREMENT}`;
|
|
34
|
-
}
|
|
35
|
-
function renderDispatchContext(kind, context) {
|
|
36
|
-
const profile = context.role;
|
|
37
|
-
const binding = profile.agentBindings[profile.activeAgentId];
|
|
38
|
-
const profileLines = [
|
|
39
|
-
`Task: ${context.taskId}`,
|
|
40
|
-
`Role: ${profile.name}`,
|
|
41
|
-
`Active Agent: ${profile.activeAgentId}`,
|
|
42
|
-
`Runtime: ${binding.adapterId}; model: ${binding.config.model ?? "CLI default"}; effort: ${binding.config.effort ?? "CLI default"}; permission: ${binding.config.permission.strategy}`,
|
|
43
|
-
profile.description === undefined ? null : `Description: ${profile.description}`,
|
|
44
|
-
...(profile.responsibilities ?? []).map((item) => `Responsibility: ${item}`),
|
|
45
|
-
...(profile.constraints ?? []).map((item) => `Constraint: ${item}`),
|
|
46
|
-
profile.expectedOutput === undefined ? null : `Expected output: ${profile.expectedOutput}`
|
|
47
|
-
].filter((line) => line !== null);
|
|
48
|
-
const workLines = kind === "worker"
|
|
49
|
-
? renderWorkerScope(context)
|
|
50
|
-
: [];
|
|
51
|
-
const rendered = [
|
|
52
|
-
`Follow the injected yui-${kind} Skill for this Yui dispatch.`,
|
|
53
|
-
renderContextLayers(context, kind),
|
|
54
|
-
profileLines.join("\n"),
|
|
55
|
-
workLines.length === 0 ? null : workLines.join("\n"),
|
|
56
|
-
"Yui dispatch:",
|
|
57
|
-
requireText(context.input, "dispatch input")
|
|
58
|
-
].filter((section) => section !== null && section.length > 0)
|
|
59
|
-
.join("\n\n");
|
|
60
|
-
return kind === "worker"
|
|
61
|
-
? ensureWorkerRunCompletionRequirement(rendered)
|
|
62
|
-
: rendered;
|
|
63
|
-
}
|
|
64
|
-
function renderContextLayers(context, kind) {
|
|
65
|
-
const projects = context.projectPolicy ?? [];
|
|
66
|
-
const policyLines = projects.length === 0
|
|
67
|
-
? ["- none (this Task is not Project-backed)"]
|
|
68
|
-
: projects.map(({ projectId, directory }) => {
|
|
69
|
-
const label = directory === undefined ? projectId : `${directory} (${projectId})`;
|
|
70
|
-
return `- ${label}: \`yui project show ${projectId}\`; then \`yui project knowledge list ${projectId}\` and show the relevant entries`;
|
|
71
|
-
});
|
|
72
|
-
return [
|
|
73
|
-
"Context layers:",
|
|
74
|
-
"- Yui Core: durable identity, lifecycle, access, workspace, and exact handoff safety.",
|
|
75
|
-
`- Generic ${kind} Skill: reusable role behavior and evidence discipline.`,
|
|
76
|
-
"- Project Policy: project-owned build, test, migration, release, and review rules; it is not a Yui Core default.",
|
|
77
|
-
...["Project Policy references:", ...policyLines],
|
|
78
|
-
"- Task Contract: the current Task brief, WorkItem objective, acceptance criteria, and dispatch input.",
|
|
79
|
-
"Do not import rules from another Project or Task, and do not infer Project Policy from repository files alone."
|
|
80
|
-
].join("\n");
|
|
81
|
-
}
|
|
82
|
-
function renderWorkerScope(context) {
|
|
83
|
-
if (context.workItem === undefined || context.workspace === undefined)
|
|
84
|
-
return [];
|
|
85
|
-
const writable = context.workspace.entries.filter(({ access }) => access === "write");
|
|
86
|
-
const contextOnly = context.workspace.entries.filter(({ access }) => access === "read");
|
|
87
|
-
const projectLines = (label, entries) => [
|
|
88
|
-
`${label}:`,
|
|
89
|
-
...(entries.length === 0
|
|
90
|
-
? ["- none"]
|
|
91
|
-
: entries.map(({ directory, projectId }) => `- ${directory} (${projectId})`))
|
|
92
|
-
];
|
|
93
|
-
return [
|
|
94
|
-
`WorkItem: ${context.workItem.id}`,
|
|
95
|
-
`Workspace root: ${context.workspace.root}`,
|
|
96
|
-
...projectLines("Writable Projects", writable),
|
|
97
|
-
...projectLines("Context-only Projects", contextOnly),
|
|
98
|
-
`Read the full Task state with \`yui task context ${context.taskId}\`.`,
|
|
99
|
-
`Read the current WorkItem scope with \`yui task work list ${context.taskId}\`.`,
|
|
100
|
-
"Modify only Writable Projects. If another Project must change, stop and ask "
|
|
101
|
-
+ "the Task Leader to expand this WorkItem scope before continuing."
|
|
102
|
-
];
|
|
103
|
-
}
|
|
104
|
-
function requireText(value, label) {
|
|
105
|
-
const normalized = value.trim();
|
|
106
|
-
if (normalized.length === 0 || normalized.includes("\0")) {
|
|
107
|
-
throw new Error(`${label} is required.`);
|
|
108
|
-
}
|
|
109
|
-
return normalized;
|
|
110
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { renderWakeReason } from "../scheduler/wakeReason.js";
|
|
2
|
-
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
3
|
-
/**
|
|
4
|
-
* Issue 04 (context token budget) — long-term design:
|
|
5
|
-
*
|
|
6
|
-
* A Leader wake is a NOTIFICATION, not a context dump. The wake envelope
|
|
7
|
-
* carries only what the Agent needs for immediate orientation: the wake id,
|
|
8
|
-
* aggregated reason tags, delta window, and a bounded list of active frozen
|
|
9
|
-
* Task Reviews. The Agent reads delta content on demand with
|
|
10
|
-
* `yui task wake show <wake-id>` and the full projection with
|
|
11
|
-
* `yui task context <task>`.
|
|
12
|
-
*
|
|
13
|
-
* The envelope is mode-agnostic: fresh Sessions and resumed Sessions
|
|
14
|
-
* receive the same minimal text. The native Session is a disposable cache of
|
|
15
|
-
* working context; Yui's durable Task records (including the wake ledger) are
|
|
16
|
-
* the checkpoint.
|
|
17
|
-
*/
|
|
18
|
-
/** Structural guardrail: the envelope stays far below a normal model context window. */
|
|
19
|
-
export const WAKE_ENVELOPE_HARD_BYTES = 2_000;
|
|
20
|
-
/** Maximum reason tags rendered before elision to a count. */
|
|
21
|
-
const REASON_DISPLAY_LIMIT = 6;
|
|
22
|
-
export function buildTaskWakeEnvelope(reader, request) {
|
|
23
|
-
const task = reader.getTask(request.taskId);
|
|
24
|
-
if (task === null)
|
|
25
|
-
throw new Error(`Task not found: ${request.taskId}.`);
|
|
26
|
-
if (request.reasons.length === 0) {
|
|
27
|
-
throw new Error(`Wake envelope ${request.wakeId} must carry at least one reason.`);
|
|
28
|
-
}
|
|
29
|
-
const fromTime = Date.parse(request.fromCursor);
|
|
30
|
-
const events = reader.listEvents(request.taskId);
|
|
31
|
-
const deltaEvents = events.filter((record) => Date.parse(record.createdAt) > fromTime);
|
|
32
|
-
const runs = operationalTaskRecords(reader.listRuns(request.taskId), events, "run");
|
|
33
|
-
const referencedRunIds = referencedWakeRunIds(runs, events, deltaEvents);
|
|
34
|
-
const changedRunIds = new Set([
|
|
35
|
-
...runs
|
|
36
|
-
.filter((record) => Date.parse(record.createdAt) > fromTime)
|
|
37
|
-
.map(({ id }) => id),
|
|
38
|
-
...referencedRunIds
|
|
39
|
-
]);
|
|
40
|
-
const counts = {
|
|
41
|
-
events: deltaEvents.length,
|
|
42
|
-
messages: operationalTaskRecords(reader.listMessages(request.taskId), events, "message")
|
|
43
|
-
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
44
|
-
runs: changedRunIds.size
|
|
45
|
-
};
|
|
46
|
-
const activeReviews = reader.listReviewRounds(request.taskId).filter((round) => ((round.scope ?? "work-item") === "task"
|
|
47
|
-
&& (round.status === "pending" || round.status === "running")));
|
|
48
|
-
const renderReviewOrientation = (limit) => activeReviews.slice(0, limit).map((round) => {
|
|
49
|
-
const projects = round.taskCandidate?.projects ?? [];
|
|
50
|
-
const heads = projects.slice(0, 2).map(({ projectId, commit }) => (`${projectId}@${commit.slice(0, 12)}`)).join("+");
|
|
51
|
-
return `${round.id}/${round.reviewerRoleName}`
|
|
52
|
-
+ `/${round.deltaRecheck === undefined ? "full" : "delta"}`
|
|
53
|
-
+ `[${round.status}]`
|
|
54
|
-
+ `@${heads || round.reviewBaseCommit.slice(0, 12)}`
|
|
55
|
-
+ `${projects.length > 2 ? `+${projects.length - 2}` : ""}`;
|
|
56
|
-
}).join(", ");
|
|
57
|
-
const render = (reasonLimit, resultRunLimit, reviewLimit) => {
|
|
58
|
-
const reviewOrientation = renderReviewOrientation(reviewLimit);
|
|
59
|
-
return [
|
|
60
|
-
`Wake: ${request.wakeId} — delta since ${request.fromCursor}`,
|
|
61
|
-
` Reasons: ${renderReasons(request.reasons, reasonLimit)}`,
|
|
62
|
-
` Changed: ${counts.events} events, ${counts.messages} messages, ${counts.runs} AgentRuns`
|
|
63
|
-
+ ` → yui task wake show ${request.taskId} ${request.wakeId}`,
|
|
64
|
-
` Result AgentRuns: ${renderResultRuns(request.taskId, referencedRunIds, resultRunLimit)}`,
|
|
65
|
-
` Active Task Reviews: ${activeReviews.length === 0
|
|
66
|
-
? "none"
|
|
67
|
-
: reviewLimit === 0
|
|
68
|
-
? `${activeReviews.length} → yui task context ${request.taskId}`
|
|
69
|
-
: `${reviewOrientation}${activeReviews.length > reviewLimit
|
|
70
|
-
? `, … (+${activeReviews.length - reviewLimit})`
|
|
71
|
-
: ""}`}`,
|
|
72
|
-
`Full context: yui task context ${request.taskId}`
|
|
73
|
-
].join("\n");
|
|
74
|
-
};
|
|
75
|
-
let reasonLimit = Math.min(REASON_DISPLAY_LIMIT, request.reasons.length);
|
|
76
|
-
let resultRunLimit = Math.min(4, referencedRunIds.length);
|
|
77
|
-
let reviewLimit = Math.min(3, activeReviews.length);
|
|
78
|
-
let body = render(reasonLimit, resultRunLimit, reviewLimit);
|
|
79
|
-
while (byteLength(body) + 1 > WAKE_ENVELOPE_HARD_BYTES) {
|
|
80
|
-
if (resultRunLimit > 0)
|
|
81
|
-
resultRunLimit -= 1;
|
|
82
|
-
else if (reasonLimit > 0)
|
|
83
|
-
reasonLimit -= 1;
|
|
84
|
-
else if (reviewLimit > 0)
|
|
85
|
-
reviewLimit -= 1;
|
|
86
|
-
else
|
|
87
|
-
break;
|
|
88
|
-
body = render(reasonLimit, resultRunLimit, reviewLimit);
|
|
89
|
-
}
|
|
90
|
-
if (byteLength(body) + 1 > WAKE_ENVELOPE_HARD_BYTES) {
|
|
91
|
-
body = fitUtf8([
|
|
92
|
-
`Wake: ${request.wakeId}`,
|
|
93
|
-
`Inspect: yui task wake show ${request.taskId} ${request.wakeId}`,
|
|
94
|
-
`Full context: yui task context ${request.taskId}`
|
|
95
|
-
].join("\n"), WAKE_ENVELOPE_HARD_BYTES - 1);
|
|
96
|
-
}
|
|
97
|
-
const totalBytes = byteLength(body) + 1;
|
|
98
|
-
return Object.freeze({
|
|
99
|
-
taskId: request.taskId,
|
|
100
|
-
wakeId: request.wakeId,
|
|
101
|
-
fromCursor: request.fromCursor,
|
|
102
|
-
referencedRunIds: Object.freeze(referencedRunIds),
|
|
103
|
-
totalBytes,
|
|
104
|
-
text: `${body}\n`
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
export function referencedWakeRunIds(runs, allEvents, terminalEvents) {
|
|
108
|
-
const turnsById = new Map(operationalTaskRecords(runs, allEvents, "run").map((run) => [run.id, run]));
|
|
109
|
-
return [...new Set(terminalEvents.flatMap((event) => {
|
|
110
|
-
if (!["run.completed", "run.failed", "run.cancelled"].includes(event.type))
|
|
111
|
-
return [];
|
|
112
|
-
const runId = event.payload.runId;
|
|
113
|
-
return runId !== undefined && turnsById.has(runId) ? [runId] : [];
|
|
114
|
-
}))];
|
|
115
|
-
}
|
|
116
|
-
function renderReasons(reasons, limit) {
|
|
117
|
-
if (limit === 0)
|
|
118
|
-
return `${reasons.length} reason tags`;
|
|
119
|
-
const selected = reasons.slice(0, limit);
|
|
120
|
-
const elided = reasons.length - selected.length;
|
|
121
|
-
const rendered = selected.map(renderWakeReason).join(", ");
|
|
122
|
-
return elided === 0 ? rendered : `${rendered}, … (+${elided} more)`;
|
|
123
|
-
}
|
|
124
|
-
function renderResultRuns(taskId, runIds, limit) {
|
|
125
|
-
if (runIds.length === 0)
|
|
126
|
-
return "none";
|
|
127
|
-
if (limit === 0)
|
|
128
|
-
return `${runIds.length} → inspect the wake delta`;
|
|
129
|
-
return `${runIds.slice(0, limit)
|
|
130
|
-
.map((runId) => `${runId} → yui task run show ${taskId}/${runId}`)
|
|
131
|
-
.join(", ")}${runIds.length > limit ? `, … (+${runIds.length - limit})` : ""}`;
|
|
132
|
-
}
|
|
133
|
-
function fitUtf8(value, maxBytes) {
|
|
134
|
-
let fitted = "";
|
|
135
|
-
for (const character of value) {
|
|
136
|
-
if (byteLength(fitted) + byteLength(character) > maxBytes)
|
|
137
|
-
break;
|
|
138
|
-
fitted += character;
|
|
139
|
-
}
|
|
140
|
-
return fitted;
|
|
141
|
-
}
|
|
142
|
-
function byteLength(value) {
|
|
143
|
-
return Buffer.byteLength(value, "utf8");
|
|
144
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/** Internal, Controller-derived release fence carried by Integration jobs. */
|
|
2
|
-
export const INTEGRATION_RUNTIME_RELEASE_ENV = "YUI_INTEGRATION_RUNTIME_RELEASE_ID";
|
|
3
|
-
/**
|
|
4
|
-
* Find exact reusable evidence without creating cache state. Every identity
|
|
5
|
-
* field is read from the existing IntegrationAttempt and DurableJob records.
|
|
6
|
-
*/
|
|
7
|
-
export function findReusableIntegrationCheckEvidence(input) {
|
|
8
|
-
const attempts = [...input.attempts]
|
|
9
|
-
.filter((attempt) => (attempt.taskId === input.taskId
|
|
10
|
-
&& attempt.id !== input.currentAttemptId
|
|
11
|
-
&& attempt.projectId === input.projectId
|
|
12
|
-
&& sameOrderedText(attempt.checkCommands, input.checkCommands)))
|
|
13
|
-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
14
|
-
for (const attempt of attempts) {
|
|
15
|
-
if (attempt.jobId === undefined)
|
|
16
|
-
continue;
|
|
17
|
-
const job = input.jobs.find((candidate) => (candidate.id === attempt.jobId
|
|
18
|
-
&& candidate.taskId === input.taskId
|
|
19
|
-
&& candidate.owner.kind === "integration-attempt"
|
|
20
|
-
&& candidate.owner.integrationAttemptId === attempt.id));
|
|
21
|
-
const resultSteps = job?.result?.steps ?? [];
|
|
22
|
-
const logPaths = resultSteps.map((step) => (input.logPathFor?.(job, step.logPath) ?? step.logPath));
|
|
23
|
-
if (job === undefined
|
|
24
|
-
|| job.projectId !== input.projectId
|
|
25
|
-
|| job.head !== input.candidateCommit
|
|
26
|
-
|| job.status !== "succeeded"
|
|
27
|
-
|| job.result?.outcome !== "succeeded"
|
|
28
|
-
|| job.env[INTEGRATION_RUNTIME_RELEASE_ENV] !== input.runtimeReleaseId
|
|
29
|
-
|| job.steps.length !== input.checkCommands.length
|
|
30
|
-
|| job.steps.some((step, index) => (step.name !== `check-${index + 1}`
|
|
31
|
-
|| step.command !== input.checkCommands[index]))
|
|
32
|
-
|| resultSteps.length !== input.checkCommands.length
|
|
33
|
-
|| resultSteps.some((step, index) => (step.name !== `check-${index + 1}`
|
|
34
|
-
|| step.exitCode !== 0
|
|
35
|
-
|| step.signal !== null
|
|
36
|
-
|| step.timedOut
|
|
37
|
-
|| step.head !== input.candidateCommit
|
|
38
|
-
|| (input.logExists !== undefined && !input.logExists(logPaths[index]))))) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const checks = resultSteps.map((step, index) => ({
|
|
42
|
-
name: input.checkCommands[index],
|
|
43
|
-
outcome: "passed",
|
|
44
|
-
details: `Reused successful check evidence from ${attempt.id}/${job.id}.`,
|
|
45
|
-
logPath: logPaths[index]
|
|
46
|
-
}));
|
|
47
|
-
return Object.freeze({ sourceAttempt: attempt, sourceJob: job, checks });
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
function sameOrderedText(left, right) {
|
|
52
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
53
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
-
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
-
export function createIntegrationQueueEntry(input, now) {
|
|
4
|
-
const timestamp = now.toISOString();
|
|
5
|
-
return validateIntegrationQueueEntry({
|
|
6
|
-
schemaVersion: 1,
|
|
7
|
-
id: input.id,
|
|
8
|
-
taskId: input.taskId,
|
|
9
|
-
projectId: input.projectId,
|
|
10
|
-
changeSetId: input.changeSetId,
|
|
11
|
-
targetRef: input.targetRef,
|
|
12
|
-
status: "queued",
|
|
13
|
-
checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration queue check command"),
|
|
14
|
-
evidenceRefs: normalizedUniqueText(input.evidenceRefs ?? [], "Integration queue evidence"),
|
|
15
|
-
createdAt: timestamp,
|
|
16
|
-
updatedAt: timestamp
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* An entry whose ChangeSet is already represented on the target (an ancestor
|
|
21
|
-
* or a same-tree commit) converges directly to committed with its proof. The
|
|
22
|
-
* entry still names the committed IntegrationAttempt that recorded the
|
|
23
|
-
* convergence, so every acceptance and provenance consumer reads one
|
|
24
|
-
* integration authority.
|
|
25
|
-
*/
|
|
26
|
-
export function createConvergedIntegrationQueueEntry(input, now) {
|
|
27
|
-
const timestamp = now.toISOString();
|
|
28
|
-
return validateIntegrationQueueEntry({
|
|
29
|
-
schemaVersion: 1,
|
|
30
|
-
id: input.id,
|
|
31
|
-
taskId: input.taskId,
|
|
32
|
-
projectId: input.projectId,
|
|
33
|
-
changeSetId: input.changeSetId,
|
|
34
|
-
targetRef: input.targetRef,
|
|
35
|
-
status: "committed",
|
|
36
|
-
checkCommands: [],
|
|
37
|
-
targetBefore: input.targetHead,
|
|
38
|
-
targetAfter: input.targetHead,
|
|
39
|
-
evidenceRefs: [input.proof],
|
|
40
|
-
...(input.integrationAttemptId === undefined
|
|
41
|
-
? {}
|
|
42
|
-
: { integrationAttemptId: input.integrationAttemptId }),
|
|
43
|
-
createdAt: timestamp,
|
|
44
|
-
updatedAt: timestamp,
|
|
45
|
-
endedAt: timestamp
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
export function markIntegrationQueueRunning(entry, targetBefore, now) {
|
|
49
|
-
validateIntegrationQueueEntry(entry);
|
|
50
|
-
if (!["queued", "conflicted"].includes(entry.status)) {
|
|
51
|
-
throw new Error(`Integration queue entry cannot run from ${entry.status}: ${entry.id}.`);
|
|
52
|
-
}
|
|
53
|
-
const { conflictSummary: _previousConflict, ...rest } = entry;
|
|
54
|
-
return validateIntegrationQueueEntry({
|
|
55
|
-
...rest,
|
|
56
|
-
status: "running",
|
|
57
|
-
targetBefore,
|
|
58
|
-
updatedAt: now.toISOString()
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
export function recordIntegrationQueueAttempt(entry, integrationAttemptId, now) {
|
|
62
|
-
validateIntegrationQueueEntry(entry);
|
|
63
|
-
if (entry.status !== "running") {
|
|
64
|
-
throw new Error(`Integration queue entry is not running: ${entry.id}/${entry.status}.`);
|
|
65
|
-
}
|
|
66
|
-
return validateIntegrationQueueEntry({
|
|
67
|
-
...entry,
|
|
68
|
-
integrationAttemptId,
|
|
69
|
-
updatedAt: now.toISOString()
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
export function markIntegrationQueueBlocked(entry, conflictSummary, now) {
|
|
73
|
-
validateIntegrationQueueEntry(entry);
|
|
74
|
-
if (entry.status !== "running") {
|
|
75
|
-
throw new Error(`Integration queue entry is not running: ${entry.id}/${entry.status}.`);
|
|
76
|
-
}
|
|
77
|
-
return validateIntegrationQueueEntry({
|
|
78
|
-
...entry,
|
|
79
|
-
status: "conflicted",
|
|
80
|
-
conflictSummary: requireText(conflictSummary, "Integration queue conflict summary"),
|
|
81
|
-
updatedAt: now.toISOString()
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
export function markIntegrationQueueCommitted(entry, targetAfter, now) {
|
|
85
|
-
validateIntegrationQueueEntry(entry);
|
|
86
|
-
if (entry.status !== "running" && entry.status !== "conflicted") {
|
|
87
|
-
throw new Error(`Integration queue entry cannot commit from ${entry.status}: ${entry.id}.`);
|
|
88
|
-
}
|
|
89
|
-
const timestamp = now.toISOString();
|
|
90
|
-
return validateIntegrationQueueEntry({
|
|
91
|
-
...entry,
|
|
92
|
-
status: "committed",
|
|
93
|
-
targetAfter,
|
|
94
|
-
updatedAt: timestamp,
|
|
95
|
-
endedAt: timestamp
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
/** A conflicted entry is manually retried after its underlying issue was fixed. */
|
|
99
|
-
export function markIntegrationQueueRequeued(entry, now) {
|
|
100
|
-
validateIntegrationQueueEntry(entry);
|
|
101
|
-
if (entry.status !== "conflicted") {
|
|
102
|
-
throw new Error(`Integration queue entry cannot requeue from ${entry.status}: ${entry.id}.`);
|
|
103
|
-
}
|
|
104
|
-
return validateIntegrationQueueEntry({
|
|
105
|
-
...entry,
|
|
106
|
-
status: "queued",
|
|
107
|
-
updatedAt: now.toISOString()
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
export function recordIntegrationQueueAffectedPaths(entry, affectedPaths, now) {
|
|
111
|
-
validateIntegrationQueueEntry(entry);
|
|
112
|
-
if (entry.status !== "queued") {
|
|
113
|
-
throw new Error(`Integration queue entry cannot recompute overlap from ${entry.status}: ${entry.id}.`);
|
|
114
|
-
}
|
|
115
|
-
const previous = new Set(entry.affectedPaths ?? []);
|
|
116
|
-
const merged = [...new Set([...previous, ...affectedPaths])].sort();
|
|
117
|
-
return validateIntegrationQueueEntry({
|
|
118
|
-
...entry,
|
|
119
|
-
affectedPaths: merged,
|
|
120
|
-
updatedAt: now.toISOString()
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
export function markIntegrationQueueSuperseded(entry, reason, now) {
|
|
124
|
-
validateIntegrationQueueEntry(entry);
|
|
125
|
-
if (!["queued", "conflicted"].includes(entry.status)) {
|
|
126
|
-
throw new Error(`Integration queue entry cannot be superseded from ${entry.status}: ${entry.id}.`);
|
|
127
|
-
}
|
|
128
|
-
const timestamp = now.toISOString();
|
|
129
|
-
return validateIntegrationQueueEntry({
|
|
130
|
-
...entry,
|
|
131
|
-
status: "superseded",
|
|
132
|
-
supersedeReason: requireText(reason, "Integration queue supersede reason"),
|
|
133
|
-
updatedAt: timestamp,
|
|
134
|
-
endedAt: timestamp
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
export function validateIntegrationQueueEntry(entry) {
|
|
138
|
-
if (entry.schemaVersion !== 1) {
|
|
139
|
-
throw new Error("IntegrationQueueEntry must use schemaVersion 1.");
|
|
140
|
-
}
|
|
141
|
-
validateTaskRecordReference({ taskId: entry.taskId, localId: entry.id }, "integrationQueue");
|
|
142
|
-
validateTaskRecordReference({ taskId: entry.taskId, localId: entry.changeSetId }, "changeSet");
|
|
143
|
-
requireIdentity(entry.projectId, "Project id");
|
|
144
|
-
requireText(entry.targetRef, "Integration queue target ref");
|
|
145
|
-
normalizedUniqueText(entry.checkCommands, "Integration queue check command");
|
|
146
|
-
normalizedUniqueText(entry.evidenceRefs, "Integration queue evidence");
|
|
147
|
-
if (![
|
|
148
|
-
"queued",
|
|
149
|
-
"running",
|
|
150
|
-
"conflicted",
|
|
151
|
-
"committed",
|
|
152
|
-
"superseded"
|
|
153
|
-
].includes(entry.status)) {
|
|
154
|
-
throw new Error(`Integration queue status is invalid: ${String(entry.status)}.`);
|
|
155
|
-
}
|
|
156
|
-
if (entry.targetBefore !== undefined)
|
|
157
|
-
requireCommit(entry.targetBefore, "Integration queue targetBefore");
|
|
158
|
-
if (entry.targetAfter !== undefined)
|
|
159
|
-
requireCommit(entry.targetAfter, "Integration queue targetAfter");
|
|
160
|
-
if (entry.integrationAttemptId !== undefined) {
|
|
161
|
-
normalizedUniqueIdentities([entry.integrationAttemptId], "Integration attempt id");
|
|
162
|
-
}
|
|
163
|
-
if (entry.conflictSummary !== undefined) {
|
|
164
|
-
requireText(entry.conflictSummary, "Integration queue conflict summary");
|
|
165
|
-
}
|
|
166
|
-
if (entry.affectedPaths !== undefined) {
|
|
167
|
-
normalizedUniqueText(entry.affectedPaths, "Integration queue affected path");
|
|
168
|
-
}
|
|
169
|
-
if (entry.supersedeReason !== undefined) {
|
|
170
|
-
requireText(entry.supersedeReason, "Integration queue supersede reason");
|
|
171
|
-
}
|
|
172
|
-
requireTimestamp(entry.createdAt, "Integration queue entry createdAt");
|
|
173
|
-
requireTimestamp(entry.updatedAt, "Integration queue entry updatedAt");
|
|
174
|
-
if (["committed", "superseded"].includes(entry.status)) {
|
|
175
|
-
requireTimestamp(entry.endedAt ?? "", "Integration queue entry endedAt");
|
|
176
|
-
}
|
|
177
|
-
if (entry.status === "committed") {
|
|
178
|
-
requireCommit(entry.targetAfter ?? "", "Integration queue targetAfter");
|
|
179
|
-
}
|
|
180
|
-
if (entry.status === "running") {
|
|
181
|
-
requireCommit(entry.targetBefore ?? "", "Integration queue targetBefore");
|
|
182
|
-
}
|
|
183
|
-
return entry;
|
|
184
|
-
}
|
|
185
|
-
function requireCommit(value, label) {
|
|
186
|
-
const normalized = requireText(value, label).toLowerCase();
|
|
187
|
-
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(normalized)) {
|
|
188
|
-
throw new Error(`${label} is invalid.`);
|
|
189
|
-
}
|
|
190
|
-
return normalized;
|
|
191
|
-
}
|