@zq-silk/yui 0.15.6 → 0.15.8
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 +192 -399
- package/README.md +127 -1149
- package/dist/agent/adapterCatalog.js +15 -2
- package/dist/agent/agent.js +23 -3
- package/dist/agent/argumentPolicy.js +7 -1
- package/dist/agent/connectionPlan.js +62 -0
- package/dist/agent/executionComponents.js +158 -0
- package/dist/agent/launchEnvironment.js +31 -3
- package/dist/agent/managedRuntimeEnvironment.js +3 -5
- package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
- package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +167 -70
- package/dist/cli/interactionCandidates.js +5 -5
- package/dist/cli/interactionPolicy.js +38 -8
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/managedDiagnostics.js +28 -0
- package/dist/cli/operatorWizard.js +1 -7
- package/dist/cli/roleOptionOrder.js +27 -0
- package/dist/cli/roleWizard.js +50 -14
- package/dist/cli/updateOrchestrator.js +1 -1
- package/dist/cli/updatePorts.js +3 -4
- package/dist/cli.js +188 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +57 -0
- package/dist/commands/configCommands.js +20 -20
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +24 -24
- package/dist/commands/globalRoleCommands.js +1 -1
- package/dist/commands/grantCommands.js +4 -4
- package/dist/commands/operatorCommands.js +1 -7
- package/dist/commands/projectCommands.js +4 -4
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/roleConfiguration.js +25 -5
- package/dist/commands/roleRuntimeGuard.js +4 -5
- package/dist/commands/sessionCommands.js +3 -7
- package/dist/commands/taskActivationCommands.js +259 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1216 -730
- package/dist/commands/taskContextCommand.js +39 -583
- package/dist/commands/taskExecutionCommands.js +32 -32
- package/dist/commands/taskInputCommands.js +40 -104
- package/dist/commands/taskIntegrationCommands.js +3 -2
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -8
- package/dist/commands/taskOverviewCommand.js +33 -45
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
- package/dist/commands/taskRoleRuntimeStatus.js +133 -102
- package/dist/commands/telemetryCommands.js +36 -38
- package/dist/config/configCatalog.js +4 -4
- package/dist/config/yuiConfig.js +8 -8
- package/dist/context/contextSnapshot.js +10 -10
- package/dist/context/dispatchContext.js +11 -11
- package/dist/context/roleSessionContext.js +6 -3
- package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
- package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
- package/dist/context/sessionBootstrapManifest.js +21 -2
- package/dist/context/sourceRunContext.js +30 -0
- package/dist/context/taskContext.js +458 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +77 -0
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +127 -188
- package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
- package/dist/controller/jobControl.js +54 -85
- package/dist/controller/resourceInventory.js +8 -27
- package/dist/controller/resourceInventoryLinux.js +12 -13
- package/dist/controller/runtime.js +530 -476
- package/dist/controller/runtimeEventInbox.js +55 -25
- package/dist/controller/runtimeEventProcessor.js +22 -31
- package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
- package/dist/controller/runtimeLaunchCoordinator.js +80 -426
- package/dist/controller/runtimeObservationHook.js +14 -18
- package/dist/controller/sessionNotify.js +16 -24
- package/dist/controller/sessionOwnerReconciliation.js +168 -50
- package/dist/controller/structuredProviderObservation.js +138 -99
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/coordination/workMailboxQueue.js +36 -33
- package/dist/core/boundedRpc.js +8 -1
- package/dist/core/controllerClient.js +20 -1
- package/dist/core/controllerServer.js +4 -4
- package/dist/doctor/doctor.js +13 -2
- package/dist/domain/agentResultTransport.js +9 -9
- package/dist/execution/codexThreadNaming.js +2 -8
- package/dist/execution/executionHealth.js +51 -63
- package/dist/execution/reviewMainRun.js +137 -0
- package/dist/execution/workItemExecution.js +28 -29
- package/dist/execution/workItemExecutionProjection.js +99 -107
- package/dist/execution/workItemMainRun.js +141 -0
- package/dist/executor/agentAdapter.js +227 -20
- package/dist/executor/agentConfigurationCatalog.js +126 -4
- package/dist/executor/agentConfigurationProbe.js +162 -4
- package/dist/executor/agentExecutor.js +79 -78
- package/dist/executor/effectiveLaunch.js +105 -18
- package/dist/executor/executorRegistry.js +29 -44
- package/dist/executor/fileRoleLaunchPlanner.js +229 -154
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +6 -3
- package/dist/input/inputRequest.js +12 -10
- package/dist/integration/gitIntegrationService.js +4 -11
- package/dist/integration/integrationQueueService.js +4 -4
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +423 -0
- package/dist/kernel/capabilityRegistry.js +314 -0
- package/dist/kernel/capabilitySchema.js +91 -0
- package/dist/kernel/instanceHost.js +18 -1
- package/dist/kernel/kernelPorts.js +4 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +62 -6
- package/dist/message/messageContinuation.js +204 -0
- package/dist/observability/executionAudit.js +70 -72
- package/dist/observability/faultClassification.js +2 -2
- package/dist/observability/orchestrationMetrics.js +8 -8
- package/dist/operator/operatorSessionHistory.js +1 -7
- package/dist/output/agentConfigurationPresentation.js +8 -3
- package/dist/output/agentRunConfigurationPresentation.js +128 -0
- package/dist/output/rolePresentation.js +54 -3
- package/dist/plugins/pluginChild.js +104 -0
- package/dist/plugins/pluginIntent.js +26 -0
- package/dist/plugins/pluginInterpreter.js +43 -0
- package/dist/plugins/pluginPackage.js +101 -0
- package/dist/plugins/pluginProcess.js +112 -0
- package/dist/plugins/pluginService.js +380 -0
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/gitWorkspace.js +26 -4
- package/dist/repository/project.js +19 -4
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +20 -27
- package/dist/repository/taskWorkspacePreparer.js +344 -83
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +3 -3
- package/dist/resources/projectResource.js +123 -0
- package/dist/resources/projectResourceService.js +421 -0
- package/dist/resources/resourceDiscovery.js +6 -6
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/resourceRegistrar.js +1 -1
- package/dist/resources/resourceTypes.js +1 -1
- package/dist/review/deltaRecheck.js +3 -3
- package/dist/review/reviewAcceptance.js +16 -16
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewRound.js +21 -20
- package/dist/review/reviewerAvailability.js +2 -2
- package/dist/role/role.js +51 -7
- package/dist/role/taskRoleUpdate.js +30 -0
- package/dist/runtime/acpProtocol.js +425 -0
- package/dist/runtime/acpSession.js +731 -0
- package/dist/runtime/acpSessionConfiguration.js +260 -0
- package/dist/runtime/agentDriver.js +30 -11
- package/dist/runtime/agentEndpoint.js +278 -0
- package/dist/runtime/agentEndpointIdentity.js +86 -0
- package/dist/runtime/agentEndpointOwnership.js +239 -0
- package/dist/runtime/agentError.js +2 -10
- package/dist/runtime/agentHost.js +565 -314
- package/dist/runtime/agentRunConfiguration.js +258 -0
- package/dist/runtime/builtinAgentDrivers.js +134 -18
- package/dist/runtime/builtinAgentErrorMappers.js +55 -3
- package/dist/runtime/builtinTranscriptUsage.js +1 -1
- package/dist/runtime/claude-process-owner +0 -0
- package/dist/runtime/codexAppServerRuntime.js +38 -30
- package/dist/runtime/codexInteractiveHost.js +41 -6
- package/dist/runtime/continuationManager.js +2 -6
- package/dist/runtime/executionEnvironment.js +30 -0
- package/dist/runtime/firstProgressAdvisory.js +11 -11
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/jsonLineChannel.js +109 -0
- package/dist/runtime/launchBroker.js +91 -16
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +10 -18
- package/dist/runtime/managedCaller.js +61 -17
- package/dist/runtime/nativeSessionControl.js +102 -0
- package/dist/runtime/ports.js +6 -21
- package/dist/runtime/processExitObservation.js +8 -7
- package/dist/runtime/promptEnvelope.js +17 -6
- package/dist/runtime/providerContinuation.js +3 -9
- package/dist/runtime/providerContinuationReconciliationService.js +4 -13
- package/dist/runtime/providerControl.js +2 -7
- package/dist/runtime/providerRuntimeIdentity.js +110 -222
- package/dist/runtime/providerRuntimeReconciler.js +5 -9
- package/dist/runtime/runtimeBinding.js +0 -1
- package/dist/runtime/runtimeContinuationProjection.js +4 -7
- package/dist/runtime/runtimeDeadlines.js +9 -0
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +29 -65
- package/dist/runtime/runtimeProjection.js +43 -51
- package/dist/runtime/runtimeSessionCandidate.js +1 -3
- package/dist/runtime/sessionLaunchRequest.js +3 -7
- package/dist/runtime/sessionOwnerIdentity.js +7 -54
- package/dist/runtime/sessionOwnerRegistry.js +22 -17
- package/dist/runtime/sessionReconciliation.js +4 -8
- package/dist/runtime/sessionTerminationGuard.js +70 -259
- package/dist/runtime/sessionTokenMetrics.js +5 -16
- package/dist/runtime/structuredProviderHost.js +237 -117
- package/dist/runtime/taskRuntimeIsolation.js +39 -122
- package/dist/runtime/tmuxAdapters.js +39 -86
- package/dist/scheduler/activeRoleRunDelivery.js +354 -0
- package/dist/scheduler/leaderWakeupProcessor.js +75 -266
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +80 -9
- package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
- package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
- package/dist/scheduler/taskExecutionProjection.js +120 -124
- package/dist/scheduler/taskObservabilityProjection.js +29 -29
- package/dist/scheduler/taskWake.js +11 -4
- package/dist/scheduler/wakeReason.js +9 -1
- package/dist/setup/setupCommand.js +1 -0
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/sqliteSchema.js +431 -5
- package/dist/storage/sqliteStore.js +375 -220
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +10 -5
- package/dist/storage/taskStore.js +13 -11
- package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
- package/dist/surface/surfaceContributions.js +102 -0
- package/dist/task/completionReadiness.js +32 -6
- package/dist/task/deliveryGuard.js +16 -16
- package/dist/task/draftPlan.js +72 -12
- package/dist/task/nextAction.js +144 -128
- package/dist/task/remoteDelivery.js +6 -6
- package/dist/task/task.js +184 -18
- package/dist/task/taskActivation.js +301 -0
- package/dist/task/taskActivationService.js +392 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +55 -68
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +2 -2
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +120 -20
- package/dist/web/assets/client/components.js +87 -54
- package/dist/web/assets/client/i18n.js +83 -41
- package/dist/web/assets/client/markdown.js +1 -1
- package/dist/web/assets/client/taskSurface.js +353 -0
- package/dist/web/assets/client/view.js +49 -44
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +22 -4
- package/dist/web/controllerWeb.js +60 -0
- package/dist/web/webMutation.js +28 -0
- package/dist/web/webServer.js +118 -8
- package/dist/web/webSnapshot.js +81 -74
- package/dist/web/webTaskSurface.js +64 -0
- package/dist/workItem/dependencyGate.js +1 -1
- package/dist/workItem/workItem.js +80 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +94 -0
- package/docs/agent-runtime-drivers.md +91 -0
- package/docs/architecture/README.md +38 -0
- package/docs/architecture/capabilities-and-resources.md +79 -0
- package/docs/managed-turn-and-session-runtime.md +222 -0
- package/docs/observability/README.md +81 -0
- package/docs/plugin-sdk.md +290 -0
- package/docs/provider-runtime.md +163 -0
- package/docs/release-workflow.md +303 -0
- package/docs/roles-and-configuration.md +113 -0
- package/docs/sqlite-control-plane-design.md +76 -0
- package/docs/task-dag-semantics.md +57 -0
- package/docs/task-delivery.md +103 -0
- package/docs/task-local-identity.md +6 -6
- package/docs/testing/verification-levels.md +86 -0
- package/i18n/README.zh-CN.md +98 -739
- package/package.json +5 -5
- package/skills/yui-leader/SKILL.md +130 -103
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +33 -0
- package/skills/yui-operator/SKILL.md +30 -59
- package/skills/yui-reviewer/SKILL.md +35 -36
- package/skills/yui-runtime/SKILL.md +88 -24
- package/skills/yui-runtime/references/publication.md +22 -0
- package/skills/yui-runtime/references/recovery.md +64 -0
- package/skills/yui-worker/SKILL.md +37 -39
- package/dist/cli/roleOptionCatalog.js +0 -68
- package/dist/context/sourceTurnContext.js +0 -30
- package/dist/execution/reviewMainTurn.js +0 -161
- package/dist/execution/workItemMainTurn.js +0 -164
- package/dist/lifecycle/exactTurnTerminalization.js +0 -407
- package/dist/runtime/preallocatedNativeSession.js +0 -13
- package/dist/runtime/runtimeStopReceipt.js +0 -42
- package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
package/dist/task/task.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateTaskWorkspaceIdentity } from "../repository/taskWorkspaceIdentity.js";
|
|
2
|
+
import { cancelTaskActivationRequest, recordTaskActivationEvidence, validateTaskActivationRequest } from "./taskActivation.js";
|
|
2
3
|
export function createTask(id, title, now, metadata = {}) {
|
|
3
4
|
const timestamp = now.toISOString();
|
|
4
5
|
return {
|
|
@@ -38,13 +39,106 @@ export function bindTaskWorkspaceIdentity(task, identity, now) {
|
|
|
38
39
|
updatedAt: now.toISOString()
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* How many terminal activation requests the Task payload *displays* beside its
|
|
44
|
+
* latest one.
|
|
45
|
+
*
|
|
46
|
+
* This is a display bound, not an authority. Whether a requestId was already
|
|
47
|
+
* cancelled or adopted is answered from the durable activation event ledger,
|
|
48
|
+
* which is never compacted, so overflow trims only what the payload shows, never
|
|
49
|
+
* what replay enforcement can see: an evicted entry is still refused. The oldest
|
|
50
|
+
* request is not "least likely to be replayed" — it is simply the oldest, and it
|
|
51
|
+
* keeps its authority exactly like the newest.
|
|
52
|
+
*/
|
|
53
|
+
export const MAX_SETTLED_ACTIVATION_REQUESTS = 16;
|
|
54
|
+
/** Terminal dispositions: the request will never adopt again on its own. */
|
|
55
|
+
function isSettledActivationRequest(request) {
|
|
56
|
+
return request.disposition === "cancelled" || request.disposition === "adopted";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Persists the Task's latest activation request.
|
|
60
|
+
*
|
|
61
|
+
* The request is intent only, so this never touches `status`: a Draft stays a
|
|
62
|
+
* continuable Draft after a request is recorded, cancelled, or failed. Only
|
|
63
|
+
* `activateTask` inside the adoption transaction moves the lifecycle.
|
|
64
|
+
*
|
|
65
|
+
* A request being displaced from the slot is projected into the bounded settled
|
|
66
|
+
* history when its outcome was terminal, so the recent decisions stay visible on
|
|
67
|
+
* the Task payload. This history is a display projection, not the authority:
|
|
68
|
+
* whether a cancelled or adopted id may be replayed is decided from the durable
|
|
69
|
+
* activation event ledger, so trimming the oldest entries here never lets a
|
|
70
|
+
* decided outcome be replayed away. A `failed` request is deliberately not
|
|
71
|
+
* projected: it stays replayable by contract, and it is still in the slot until
|
|
72
|
+
* something replaces it.
|
|
73
|
+
*/
|
|
74
|
+
export function setTaskActivationRequest(task, request, now) {
|
|
75
|
+
validateTask(task);
|
|
76
|
+
const valid = validateTaskActivationRequest(request);
|
|
77
|
+
if (valid.operation.targetId !== task.id) {
|
|
78
|
+
throw new Error(`Activation request belongs to another Task: ${valid.operation.targetId}.`);
|
|
79
|
+
}
|
|
80
|
+
const existing = task.activationRequest;
|
|
81
|
+
if (existing !== undefined
|
|
82
|
+
&& existing.operation.requestId === valid.operation.requestId
|
|
83
|
+
&& existing.operation.inputDigest !== valid.operation.inputDigest) {
|
|
84
|
+
throw new Error(`Activation requestId ${valid.operation.requestId} was already used for different inputs.`);
|
|
85
|
+
}
|
|
86
|
+
// Replacing the slot: keep the outgoing terminal outcome as evidence. The
|
|
87
|
+
// incoming request's own id is dropped from the history because it is now the
|
|
88
|
+
// slot's occupant, so exactly one record per id exists.
|
|
89
|
+
const displaced = existing !== undefined
|
|
90
|
+
&& existing.operation.requestId !== valid.operation.requestId
|
|
91
|
+
&& isSettledActivationRequest(existing)
|
|
92
|
+
? [existing]
|
|
93
|
+
: [];
|
|
94
|
+
const retained = [
|
|
95
|
+
...(task.settledActivationRequests ?? []),
|
|
96
|
+
...displaced
|
|
97
|
+
].filter(({ operation }) => operation.requestId !== valid.operation.requestId)
|
|
98
|
+
.slice(-MAX_SETTLED_ACTIVATION_REQUESTS);
|
|
99
|
+
return validateTask({
|
|
100
|
+
...task,
|
|
101
|
+
activationRequest: valid,
|
|
102
|
+
...(retained.length === 0 ? {} : { settledActivationRequests: retained }),
|
|
103
|
+
updatedAt: now.toISOString()
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Records operation evidence against one activation request by id, wherever the
|
|
108
|
+
* Task still holds it — the live slot or the settled history.
|
|
109
|
+
*
|
|
110
|
+
* Evidence has to survive the request losing the slot: resources adopted for A
|
|
111
|
+
* remain adopted after A is cancelled and B is requested, and that effect must
|
|
112
|
+
* stay attached to A rather than following the slot to B. The underlying ratchet
|
|
113
|
+
* only raises `effect` and adds refs, so this never rewrites an outcome.
|
|
114
|
+
*/
|
|
115
|
+
export function recordTaskActivationRequestEvidence(task, requestId, evidence, now) {
|
|
116
|
+
const current = task.activationRequest;
|
|
117
|
+
if (current?.operation.requestId === requestId) {
|
|
118
|
+
const updated = recordTaskActivationEvidence(current, evidence, now);
|
|
119
|
+
if (updated === current)
|
|
120
|
+
return task;
|
|
121
|
+
return validateTask({ ...task, activationRequest: updated, updatedAt: now.toISOString() });
|
|
122
|
+
}
|
|
123
|
+
const settled = task.settledActivationRequests;
|
|
124
|
+
if (settled?.some(({ operation }) => operation.requestId === requestId) !== true) {
|
|
125
|
+
throw new Error(`Activation request not found on Task ${task.id}: ${requestId}.`);
|
|
126
|
+
}
|
|
127
|
+
return validateTask({
|
|
128
|
+
...task,
|
|
129
|
+
settledActivationRequests: settled.map((request) => (request.operation.requestId === requestId
|
|
130
|
+
? recordTaskActivationEvidence(request, evidence, now)
|
|
131
|
+
: request)),
|
|
132
|
+
updatedAt: now.toISOString()
|
|
133
|
+
});
|
|
134
|
+
}
|
|
41
135
|
export function activateTask(task, now) {
|
|
42
136
|
if (task.status === "archived")
|
|
43
137
|
throw new Error(`Cannot activate archived Task: ${task.id}.`);
|
|
44
138
|
if (task.status === "completed")
|
|
45
139
|
throw new Error(`Cannot activate completed Task ${task.id}; reopen it instead.`);
|
|
46
|
-
if (task.status === "
|
|
47
|
-
throw new Error(`Cannot activate
|
|
140
|
+
if (task.status === "cancelled")
|
|
141
|
+
throw new Error(`Cannot activate cancelled Task ${task.id}; reopen it with user or Operator authority.`);
|
|
48
142
|
if (task.status === "active")
|
|
49
143
|
return task;
|
|
50
144
|
return { ...task, status: "active", updatedAt: now.toISOString() };
|
|
@@ -146,7 +240,7 @@ export function retireTask(task, input, now) {
|
|
|
146
240
|
throw new Error("A Task cannot replace itself.");
|
|
147
241
|
}
|
|
148
242
|
}
|
|
149
|
-
if (task.status === "
|
|
243
|
+
if (task.status === "cancelled") {
|
|
150
244
|
if (task.retiredBy === by
|
|
151
245
|
&& task.retirementSummary === summary
|
|
152
246
|
&& task.replacementTaskId === input.replacementTaskId) {
|
|
@@ -160,10 +254,11 @@ export function retireTask(task, input, now) {
|
|
|
160
254
|
const timestamp = now.toISOString();
|
|
161
255
|
return validateTask({
|
|
162
256
|
...task,
|
|
163
|
-
status: "
|
|
257
|
+
status: "cancelled",
|
|
164
258
|
retiredAt: timestamp,
|
|
165
259
|
retiredBy: by,
|
|
166
260
|
retirementSummary: summary,
|
|
261
|
+
...(input.isolated === true ? { retirementIsolation: true } : {}),
|
|
167
262
|
...(input.replacementTaskId === undefined
|
|
168
263
|
? {}
|
|
169
264
|
: { replacementTaskId: input.replacementTaskId }),
|
|
@@ -183,24 +278,42 @@ export function completeTask(task, now, completion) {
|
|
|
183
278
|
completedAt: timestamp,
|
|
184
279
|
completedBy: completion.by,
|
|
185
280
|
completionSummary: requireText(completion.summary, "Task completion summary"),
|
|
281
|
+
...(completion.artifactRefs === undefined ? {} : {
|
|
282
|
+
completionArtifactRefs: completion.artifactRefs.map((ref) => requireText(ref, "Artifact ref"))
|
|
283
|
+
}),
|
|
186
284
|
updatedAt: timestamp
|
|
187
285
|
};
|
|
188
286
|
}
|
|
189
287
|
export function reopenTask(task, now) {
|
|
190
288
|
if (task.status === "archived")
|
|
191
289
|
throw new Error(`Cannot reopen archived Task: ${task.id}.`);
|
|
192
|
-
if (task.status !== "completed") {
|
|
193
|
-
throw new Error(`Only a completed Task can be reopened: ${task.id}.`);
|
|
194
|
-
}
|
|
195
|
-
const { completedAt: _completedAt, completedBy: _completedBy, completionSummary: _completionSummary, ...reopened } = task;
|
|
196
|
-
|
|
290
|
+
if (task.status !== "completed" && task.status !== "cancelled") {
|
|
291
|
+
throw new Error(`Only a completed or cancelled Task can be reopened: ${task.id}.`);
|
|
292
|
+
}
|
|
293
|
+
const { completedAt: _completedAt, completedBy: _completedBy, completionSummary: _completionSummary, completionArtifactRefs: _completionArtifactRefs, retiredAt: _retiredAt, retiredBy: _retiredBy, retirementSummary: _retirementSummary, replacementTaskId: _replacementTaskId, retirementIsolation: _retirementIsolation, ...reopened } = task;
|
|
294
|
+
// A request that never adopted is superseded by the completion it predates:
|
|
295
|
+
// it is cancelled rather than deleted, so it cannot be replayed into the new
|
|
296
|
+
// delivery cycle and the intent stays visible. An adopted request records a
|
|
297
|
+
// real environment and status change and is left exactly as it is.
|
|
298
|
+
const request = reopened.activationRequest;
|
|
299
|
+
return validateTask({
|
|
300
|
+
...reopened,
|
|
301
|
+
...(request?.disposition === "pending"
|
|
302
|
+
? {
|
|
303
|
+
activationRequest: cancelTaskActivationRequest(request, `Task was completed and reopened at ${now.toISOString()}; request superseded.`, now)
|
|
304
|
+
}
|
|
305
|
+
: {}),
|
|
306
|
+
status: "active",
|
|
307
|
+
executionGate: { state: "enabled" },
|
|
308
|
+
updatedAt: now.toISOString()
|
|
309
|
+
});
|
|
197
310
|
}
|
|
198
311
|
export function archiveTask(task, now, archive) {
|
|
199
312
|
if (task.status === "archived")
|
|
200
313
|
return task;
|
|
201
314
|
if (task.status !== "completed"
|
|
202
|
-
&& task.status !== "
|
|
203
|
-
throw new Error(`Only a completed or
|
|
315
|
+
&& task.status !== "cancelled") {
|
|
316
|
+
throw new Error(`Only a completed or cancelled Task can be archived: ${task.id}.`);
|
|
204
317
|
}
|
|
205
318
|
const timestamp = now.toISOString();
|
|
206
319
|
return validateTask({
|
|
@@ -256,10 +369,22 @@ export function isTaskArchived(task) {
|
|
|
256
369
|
export function isTaskExecutionEnabled(task) {
|
|
257
370
|
return task.executionGate.state === "enabled";
|
|
258
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Whether this Task owns a managed Git workspace at all.
|
|
374
|
+
*
|
|
375
|
+
* A Task activated with an empty environment plan legitimately owns none: it
|
|
376
|
+
* binds no Project and adopted no directory, so there is no worktree to be
|
|
377
|
+
* ready and no cwd to verify. Workspace fences use this to distinguish "not
|
|
378
|
+
* prepared yet" from "correctly owns nothing", instead of creating a workspace
|
|
379
|
+
* only to satisfy the fence.
|
|
380
|
+
*/
|
|
381
|
+
export function taskOwnsManagedWorkspace(task) {
|
|
382
|
+
return task.projectBindings.length > 0 || task.cwd !== undefined;
|
|
383
|
+
}
|
|
259
384
|
export function stopTaskExecution(task, now) {
|
|
260
385
|
validateTask(task);
|
|
261
|
-
if (task.status !== "active") {
|
|
262
|
-
throw new Error(`Only an
|
|
386
|
+
if (task.status !== "active" && task.status !== "draft") {
|
|
387
|
+
throw new Error(`Only an open Task can be stopped: ${task.id}.`);
|
|
263
388
|
}
|
|
264
389
|
if (task.executionGate.state === "stopped")
|
|
265
390
|
return task;
|
|
@@ -271,8 +396,8 @@ export function stopTaskExecution(task, now) {
|
|
|
271
396
|
}
|
|
272
397
|
export function startTaskExecution(task, now) {
|
|
273
398
|
validateTask(task);
|
|
274
|
-
if (task.status !== "active") {
|
|
275
|
-
throw new Error(`Only an
|
|
399
|
+
if (task.status !== "active" && task.status !== "draft") {
|
|
400
|
+
throw new Error(`Only an open Task can be started: ${task.id}.`);
|
|
276
401
|
}
|
|
277
402
|
if (task.executionGate.state === "enabled")
|
|
278
403
|
return task;
|
|
@@ -289,7 +414,7 @@ export function validateTask(task) {
|
|
|
289
414
|
requireText(task.title, "Task title");
|
|
290
415
|
if (task.type !== undefined)
|
|
291
416
|
requireSafeIdentity(task.type, "Task type");
|
|
292
|
-
if (!["draft", "active", "completed", "
|
|
417
|
+
if (!["draft", "active", "completed", "cancelled", "archived"].includes(task.status)) {
|
|
293
418
|
throw new Error(`Task status is invalid: ${String(task.status)}.`);
|
|
294
419
|
}
|
|
295
420
|
if (task.executionGate === null
|
|
@@ -299,6 +424,14 @@ export function validateTask(task) {
|
|
|
299
424
|
}
|
|
300
425
|
requireTimestamp(task.createdAt, "Task createdAt");
|
|
301
426
|
requireTimestamp(task.updatedAt, "Task updatedAt");
|
|
427
|
+
if (task.retirementIsolation !== undefined && task.retirementIsolation !== true) {
|
|
428
|
+
throw new Error("Task retirement isolation must represent explicit established evidence.");
|
|
429
|
+
}
|
|
430
|
+
if (task.completionArtifactRefs !== undefined && !Array.isArray(task.completionArtifactRefs)) {
|
|
431
|
+
throw new Error("Task completion artifact refs must be an array.");
|
|
432
|
+
}
|
|
433
|
+
for (const ref of task.completionArtifactRefs ?? [])
|
|
434
|
+
requireText(ref, "Task completion artifact ref");
|
|
302
435
|
if (Date.parse(task.updatedAt) < Date.parse(task.createdAt)) {
|
|
303
436
|
throw new Error("Task updatedAt cannot precede createdAt.");
|
|
304
437
|
}
|
|
@@ -308,6 +441,39 @@ export function validateTask(task) {
|
|
|
308
441
|
throw new Error(`Task workspace identity belongs to another Task: ${identity.taskId}.`);
|
|
309
442
|
}
|
|
310
443
|
}
|
|
444
|
+
if (task.activationRequest !== undefined) {
|
|
445
|
+
const request = validateTaskActivationRequest(task.activationRequest);
|
|
446
|
+
if (request.operation.targetId !== task.id) {
|
|
447
|
+
throw new Error(`Task activation request belongs to another Task: ${request.operation.targetId}.`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (task.settledActivationRequests !== undefined) {
|
|
451
|
+
const settled = task.settledActivationRequests;
|
|
452
|
+
if (!Array.isArray(settled)) {
|
|
453
|
+
throw new Error("Task settled activation requests are invalid.");
|
|
454
|
+
}
|
|
455
|
+
if (settled.length > MAX_SETTLED_ACTIVATION_REQUESTS) {
|
|
456
|
+
throw new Error("Task settled activation requests exceed the bounded limit.");
|
|
457
|
+
}
|
|
458
|
+
const seen = new Set();
|
|
459
|
+
for (const entry of settled) {
|
|
460
|
+
const request = validateTaskActivationRequest(entry);
|
|
461
|
+
if (request.operation.targetId !== task.id) {
|
|
462
|
+
throw new Error(`Task settled activation request belongs to another Task: ${request.operation.targetId}.`);
|
|
463
|
+
}
|
|
464
|
+
if (!isSettledActivationRequest(request)) {
|
|
465
|
+
throw new Error("Task settled activation history retains only terminal requests: "
|
|
466
|
+
+ `${request.operation.requestId}/${request.disposition}.`);
|
|
467
|
+
}
|
|
468
|
+
// One record per id, and never a stale copy of the live slot: the slot is
|
|
469
|
+
// the only place a request that can still change is allowed to live.
|
|
470
|
+
if (seen.has(request.operation.requestId)
|
|
471
|
+
|| request.operation.requestId === task.activationRequest?.operation.requestId) {
|
|
472
|
+
throw new Error(`Task activation request is duplicated: ${request.operation.requestId}.`);
|
|
473
|
+
}
|
|
474
|
+
seen.add(request.operation.requestId);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
311
477
|
if (task.priority !== undefined
|
|
312
478
|
&& !["low", "medium", "high", "urgent"].includes(task.priority)) {
|
|
313
479
|
throw new Error(`Task priority is invalid: ${String(task.priority)}.`);
|
|
@@ -344,7 +510,7 @@ export function validateTask(task) {
|
|
|
344
510
|
if (task.status === "completed" && !hasAllCompletion) {
|
|
345
511
|
throw new Error("A completed Task requires completedAt, completedBy, and completionSummary.");
|
|
346
512
|
}
|
|
347
|
-
if (["draft", "active", "
|
|
513
|
+
if (["draft", "active", "cancelled"].includes(task.status)
|
|
348
514
|
&& hasAnyCompletion) {
|
|
349
515
|
throw new Error(`Task completion metadata is invalid for ${task.status} status.`);
|
|
350
516
|
}
|
|
@@ -355,7 +521,7 @@ export function validateTask(task) {
|
|
|
355
521
|
task.replacementTaskId
|
|
356
522
|
];
|
|
357
523
|
const hasAnyRetirement = retirementFields.some((value) => value !== undefined);
|
|
358
|
-
const retired = task.status === "
|
|
524
|
+
const retired = task.status === "cancelled";
|
|
359
525
|
const archivedRetirement = task.status === "archived" && hasAnyRetirement;
|
|
360
526
|
if (retired || archivedRetirement) {
|
|
361
527
|
if (task.retiredAt === undefined
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { requireIdentity, requireTimestamp } from "../domain/validation.js";
|
|
4
|
+
import { recordOperationEvidence, validateOperationFacts } from "../kernel/operationFacts.js";
|
|
5
|
+
export const TASK_ACTIVATION_IMPLEMENTATION = Object.freeze({
|
|
6
|
+
id: "yui:task-activation",
|
|
7
|
+
generation: "1"
|
|
8
|
+
});
|
|
9
|
+
export const TASK_ACTIVATION_CAPABILITY = "task.activate";
|
|
10
|
+
/**
|
|
11
|
+
* The durable activation event vocabulary.
|
|
12
|
+
*
|
|
13
|
+
* These events are the never-compacted authority for a request's terminal
|
|
14
|
+
* outcome, so the type strings are named constants shared by every emitter and
|
|
15
|
+
* every reader. A rename cannot silently desynchronise what adoption/cancel
|
|
16
|
+
* record from what replay enforcement reads back — which is exactly how the
|
|
17
|
+
* bounded display list used to lose authority once it overflowed.
|
|
18
|
+
*/
|
|
19
|
+
export const TASK_ACTIVATION_EVENT = Object.freeze({
|
|
20
|
+
requested: "task.activation-requested",
|
|
21
|
+
cancelled: "task.activation-cancelled",
|
|
22
|
+
adopted: "task.activation-adopted",
|
|
23
|
+
failed: "task.activation-failed"
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The digest that decides whether a repeated requestId is the same request.
|
|
27
|
+
*
|
|
28
|
+
* It covers only what adoption actually consumes: the Task, the start mode and
|
|
29
|
+
* the resource configuration. Ordinary planning progress — a Brief edit, a new
|
|
30
|
+
* Decision, a changed current focus — deliberately does not appear here, so
|
|
31
|
+
* continuing to plan never invalidates an already-issued request.
|
|
32
|
+
*/
|
|
33
|
+
export function taskActivationInputDigest(taskId, input) {
|
|
34
|
+
return createHash("sha256").update(JSON.stringify([
|
|
35
|
+
taskId,
|
|
36
|
+
input.startMode,
|
|
37
|
+
input.afterPlanningRun ?? null,
|
|
38
|
+
canonicalEnvironmentPlan(input.environmentPlan)
|
|
39
|
+
])).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
export function createTaskActivationRequest(taskId, input, now) {
|
|
42
|
+
if (input.startMode === "after-planning-turn") {
|
|
43
|
+
if (input.afterPlanningRun === undefined) {
|
|
44
|
+
throw new Error("A deferred activation request must name its planning Turn.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (input.afterPlanningRun !== undefined) {
|
|
48
|
+
throw new Error("An immediate activation request cannot defer to a planning Turn.");
|
|
49
|
+
}
|
|
50
|
+
const timestamp = now.toISOString();
|
|
51
|
+
return validateTaskActivationRequest({
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
operation: {
|
|
54
|
+
requestId: input.requestId,
|
|
55
|
+
inputDigest: taskActivationInputDigest(taskId, input),
|
|
56
|
+
actorId: input.actorId,
|
|
57
|
+
authorityRef: input.authorityRef,
|
|
58
|
+
targetId: taskId,
|
|
59
|
+
capability: TASK_ACTIVATION_CAPABILITY,
|
|
60
|
+
implementation: TASK_ACTIVATION_IMPLEMENTATION,
|
|
61
|
+
effect: "none",
|
|
62
|
+
receiptRefs: [],
|
|
63
|
+
partialResultRefs: []
|
|
64
|
+
},
|
|
65
|
+
startMode: input.startMode,
|
|
66
|
+
...(input.afterPlanningRun === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { afterPlanningRun: input.afterPlanningRun }),
|
|
69
|
+
environmentPlan: canonicalEnvironmentPlan(input.environmentPlan),
|
|
70
|
+
disposition: "pending",
|
|
71
|
+
requestedAt: timestamp,
|
|
72
|
+
updatedAt: timestamp
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Records that adoption completed. `effect` escalates to `confirmed` and never
|
|
77
|
+
* returns: a later launch failure is reported on the Task, not by erasing the
|
|
78
|
+
* fact that the environment and the active status were adopted.
|
|
79
|
+
*/
|
|
80
|
+
export function adoptTaskActivationRequest(request, evidence, now) {
|
|
81
|
+
if (request.disposition === "adopted")
|
|
82
|
+
return request;
|
|
83
|
+
if (request.disposition !== "pending" && request.disposition !== "failed") {
|
|
84
|
+
throw new Error(`Activation request is not adoptable: ${request.operation.requestId}.`);
|
|
85
|
+
}
|
|
86
|
+
return validateTaskActivationRequest({
|
|
87
|
+
...request,
|
|
88
|
+
operation: recordOperationEvidence(request.operation, {
|
|
89
|
+
effect: "confirmed",
|
|
90
|
+
...(evidence.receiptRefs === undefined ? {} : { receiptRefs: evidence.receiptRefs })
|
|
91
|
+
}),
|
|
92
|
+
disposition: "adopted",
|
|
93
|
+
...(evidence.preparationId === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { preparationId: evidence.preparationId }),
|
|
96
|
+
updatedAt: now.toISOString()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Records an attempt that did not adopt. The request stays replayable and the
|
|
101
|
+
* Task stays a continuable Draft; `effect` rises to `possible` only when the
|
|
102
|
+
* caller could not prove the resources were left untouched.
|
|
103
|
+
*/
|
|
104
|
+
export function failTaskActivationRequest(request, outcome, now, evidence = {}) {
|
|
105
|
+
if (request.disposition === "adopted") {
|
|
106
|
+
throw new Error(`Adopted activation cannot be reported as failed: ${request.operation.requestId}.`);
|
|
107
|
+
}
|
|
108
|
+
if (request.disposition === "cancelled")
|
|
109
|
+
return request;
|
|
110
|
+
return validateTaskActivationRequest({
|
|
111
|
+
...request,
|
|
112
|
+
operation: recordOperationEvidence(request.operation, evidence),
|
|
113
|
+
disposition: "failed",
|
|
114
|
+
outcome: requireOutcome(outcome),
|
|
115
|
+
updatedAt: now.toISOString()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Records what an attempt really did, without changing its disposition.
|
|
120
|
+
*
|
|
121
|
+
* Adoption of resources and adoption of the request are separate moments, so a
|
|
122
|
+
* request that was cancelled or replaced after its environment was already
|
|
123
|
+
* adopted still has to carry that effect. This is the same monotonic evidence
|
|
124
|
+
* ratchet the other transitions use — it can only add refs and raise `effect`,
|
|
125
|
+
* never lower it and never rewrite the outcome.
|
|
126
|
+
*/
|
|
127
|
+
export function recordTaskActivationEvidence(request, evidence, now) {
|
|
128
|
+
const operation = recordOperationEvidence(request.operation, evidence);
|
|
129
|
+
if (isDeepStrictEqual(operation, request.operation))
|
|
130
|
+
return request;
|
|
131
|
+
return validateTaskActivationRequest({
|
|
132
|
+
...request,
|
|
133
|
+
operation,
|
|
134
|
+
updatedAt: now.toISOString()
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** An explicitly cancelled request is never replayed, even if it is retried. */
|
|
138
|
+
export function cancelTaskActivationRequest(request, outcome, now) {
|
|
139
|
+
if (request.disposition === "adopted") {
|
|
140
|
+
throw new Error(`Adopted activation cannot be cancelled: ${request.operation.requestId}.`);
|
|
141
|
+
}
|
|
142
|
+
if (request.disposition === "cancelled")
|
|
143
|
+
return request;
|
|
144
|
+
return validateTaskActivationRequest({
|
|
145
|
+
...request,
|
|
146
|
+
disposition: "cancelled",
|
|
147
|
+
outcome: requireOutcome(outcome),
|
|
148
|
+
updatedAt: now.toISOString()
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Re-checks a stored request against current facts immediately before adoption.
|
|
153
|
+
*
|
|
154
|
+
* Everything decided at request time is deliberately re-decided here: the Task
|
|
155
|
+
* may have been retired, execution may have been stopped, the request may have
|
|
156
|
+
* been cancelled while the planning Turn was still running. Resource-level
|
|
157
|
+
* rechecks stay with `prepare`/`adopt`, which own the grant and directory
|
|
158
|
+
* identity fences.
|
|
159
|
+
*/
|
|
160
|
+
export function admitTaskActivationRequest(task, request, planningRunIsActive) {
|
|
161
|
+
if (request === undefined)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (request.disposition === "adopted" || request.disposition === "cancelled") {
|
|
164
|
+
return { disposition: "settled", request };
|
|
165
|
+
}
|
|
166
|
+
if (request.operation.targetId !== task.id) {
|
|
167
|
+
return {
|
|
168
|
+
disposition: "unavailable",
|
|
169
|
+
request,
|
|
170
|
+
reason: `Activation request targets another Task: ${request.operation.targetId}.`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (task.status !== "draft") {
|
|
174
|
+
return {
|
|
175
|
+
disposition: "unavailable",
|
|
176
|
+
request,
|
|
177
|
+
reason: `Task is no longer a Draft: ${task.id}/${task.status}.`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (task.executionGate.state !== "enabled") {
|
|
181
|
+
return {
|
|
182
|
+
disposition: "unavailable",
|
|
183
|
+
request,
|
|
184
|
+
reason: `Task execution is stopped: ${task.id}.`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (request.afterPlanningRun !== undefined
|
|
188
|
+
&& planningRunIsActive(request.afterPlanningRun)) {
|
|
189
|
+
return {
|
|
190
|
+
disposition: "deferred",
|
|
191
|
+
request,
|
|
192
|
+
afterPlanningRun: request.afterPlanningRun
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { disposition: "ready", request };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The durable terminal outcome for a requestId, read from the activation event
|
|
199
|
+
* ledger.
|
|
200
|
+
*
|
|
201
|
+
* `task.activation-cancelled` and `task.activation-adopted` are never compacted
|
|
202
|
+
* — only runtime-observation events are pruned — so this answers "was this id
|
|
203
|
+
* already settled?" for the entire life of the Task, no matter how many later
|
|
204
|
+
* requests displaced it from the bounded display payload. A `failed` request is
|
|
205
|
+
* intentionally absent here: it stays replayable by contract, so replaying it is
|
|
206
|
+
* not a resurrection of a decided outcome.
|
|
207
|
+
*
|
|
208
|
+
* A requestId reaches at most one terminal disposition — adoption and
|
|
209
|
+
* cancellation exclude each other by construction — so the first matching
|
|
210
|
+
* terminal event is authoritative.
|
|
211
|
+
*/
|
|
212
|
+
export function settledActivationFromEvents(events, requestId) {
|
|
213
|
+
for (const event of events) {
|
|
214
|
+
if (event.payload.requestId !== requestId)
|
|
215
|
+
continue;
|
|
216
|
+
if (event.type === TASK_ACTIVATION_EVENT.cancelled) {
|
|
217
|
+
return {
|
|
218
|
+
disposition: "cancelled",
|
|
219
|
+
...(event.payload.reason === undefined ? {} : { outcome: event.payload.reason })
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (event.type === TASK_ACTIVATION_EVENT.adopted) {
|
|
223
|
+
return { disposition: "adopted" };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
export function validateTaskActivationRequest(request) {
|
|
229
|
+
if (request.schemaVersion !== 1) {
|
|
230
|
+
throw new Error("Activation request must use schemaVersion 1.");
|
|
231
|
+
}
|
|
232
|
+
validateOperationFacts(request.operation);
|
|
233
|
+
if (request.operation.capability !== TASK_ACTIVATION_CAPABILITY) {
|
|
234
|
+
throw new Error("Activation request capability is invalid.");
|
|
235
|
+
}
|
|
236
|
+
if (!["immediate", "after-planning-turn"].includes(request.startMode)) {
|
|
237
|
+
throw new Error(`Activation start mode is invalid: ${String(request.startMode)}.`);
|
|
238
|
+
}
|
|
239
|
+
if ((request.startMode === "after-planning-turn")
|
|
240
|
+
!== (request.afterPlanningRun !== undefined)) {
|
|
241
|
+
throw new Error("Activation deferral must name exactly the planning Turn it waits for.");
|
|
242
|
+
}
|
|
243
|
+
if (request.afterPlanningRun !== undefined) {
|
|
244
|
+
requireIdentity(request.afterPlanningRun, "Activation planning Turn");
|
|
245
|
+
}
|
|
246
|
+
if (!["pending", "adopted", "cancelled", "failed"].includes(request.disposition)) {
|
|
247
|
+
throw new Error(`Activation disposition is invalid: ${String(request.disposition)}.`);
|
|
248
|
+
}
|
|
249
|
+
if (request.disposition === "adopted" && request.operation.effect !== "confirmed") {
|
|
250
|
+
throw new Error("An adopted activation must record a confirmed effect.");
|
|
251
|
+
}
|
|
252
|
+
if (request.preparationId !== undefined) {
|
|
253
|
+
requireIdentity(request.preparationId, "Activation preparation");
|
|
254
|
+
if (request.disposition !== "adopted") {
|
|
255
|
+
throw new Error("Only an adopted activation records the preparation it consumed.");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (request.outcome !== undefined)
|
|
259
|
+
requireOutcome(request.outcome);
|
|
260
|
+
canonicalEnvironmentPlan(request.environmentPlan);
|
|
261
|
+
requireTimestamp(request.requestedAt, "Activation requestedAt");
|
|
262
|
+
requireTimestamp(request.updatedAt, "Activation updatedAt");
|
|
263
|
+
if (Date.parse(request.updatedAt) < Date.parse(request.requestedAt)) {
|
|
264
|
+
throw new Error("Activation updatedAt cannot precede requestedAt.");
|
|
265
|
+
}
|
|
266
|
+
return request;
|
|
267
|
+
}
|
|
268
|
+
/** Normalizes the plan so the digest never depends on key order. */
|
|
269
|
+
export function canonicalEnvironmentPlan(plan) {
|
|
270
|
+
const kind = plan?.kind;
|
|
271
|
+
if (kind === "empty")
|
|
272
|
+
return { kind: "empty" };
|
|
273
|
+
if (kind === "scratch")
|
|
274
|
+
return { kind: "scratch" };
|
|
275
|
+
if (kind === "local") {
|
|
276
|
+
const local = plan;
|
|
277
|
+
if (!["read", "write"].includes(local.access)) {
|
|
278
|
+
throw new Error(`Environment plan access is invalid: ${String(local.access)}.`);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
kind: "local",
|
|
282
|
+
resourceId: requireIdentity(local.resourceId, "Environment plan resource"),
|
|
283
|
+
access: local.access
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
throw new Error(`Environment plan kind is invalid: ${String(kind)}.`);
|
|
287
|
+
}
|
|
288
|
+
export function describeEnvironmentPlan(plan) {
|
|
289
|
+
return plan.kind === "local"
|
|
290
|
+
? `local ${plan.resourceId} (${plan.access})`
|
|
291
|
+
: plan.kind;
|
|
292
|
+
}
|
|
293
|
+
function requireOutcome(value) {
|
|
294
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
295
|
+
throw new Error("Activation outcome is invalid.");
|
|
296
|
+
}
|
|
297
|
+
const normalized = value.trim();
|
|
298
|
+
if (normalized.length === 0)
|
|
299
|
+
throw new Error("Activation outcome is required.");
|
|
300
|
+
return normalized.slice(0, 2000);
|
|
301
|
+
}
|