@zq-silk/yui 0.15.7 → 0.15.9
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 +194 -399
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +306 -1131
- 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/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +165 -74
- 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 +245 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +9 -6
- 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 +34 -9
- 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 +281 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1461 -720
- 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} +158 -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 +481 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +17 -6
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +120 -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 +529 -479
- 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 +255 -12
- package/dist/kernel/capabilityRegistry.js +64 -18
- package/dist/kernel/instanceHost.js +12 -1
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +118 -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 +388 -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 +75 -0
- package/dist/resources/projectResourceService.js +343 -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 +3 -7
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/sqliteSchema.js +467 -7
- package/dist/storage/sqliteStore.js +355 -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 +327 -0
- package/dist/task/taskActivationService.js +408 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/task/taskSubmission.js +236 -0
- 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 +121 -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 +442 -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 +133 -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 +84 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +96 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +93 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +50 -0
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -0
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +224 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +83 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +393 -0
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +165 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +305 -0
- package/docs/release-workflow.zh-CN.md +237 -0
- package/docs/roles-and-configuration.md +115 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +78 -0
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -0
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +105 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +8 -6
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +88 -0
- package/docs/testing/verification-levels.zh-CN.md +69 -0
- package/i18n/README.zh-CN.md +270 -722
- package/package.json +3 -2
- package/skills/yui-leader/SKILL.md +88 -304
- package/skills/yui-leader/references/execution.md +303 -0
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +37 -0
- package/skills/yui-operator/SKILL.md +46 -62
- 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
|
@@ -98,14 +98,14 @@ function collectTaskStatuses(store) {
|
|
|
98
98
|
return statuses;
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
|
-
* Workspace paths claimed by active durable Jobs. An active
|
|
101
|
+
* Workspace paths claimed by active durable Jobs. An active AgentRun still
|
|
102
102
|
* holds its workspace even after the managed workspace record is gone, so
|
|
103
|
-
* those paths stay protected until the
|
|
103
|
+
* those paths stay protected until the AgentRun finishes.
|
|
104
104
|
*/
|
|
105
105
|
function collectActiveWorkspaceOwnerPaths(store) {
|
|
106
106
|
const paths = [];
|
|
107
107
|
for (const task of store.listTasks()) {
|
|
108
|
-
for (const run of store.
|
|
108
|
+
for (const run of store.listRuns(task.id)) {
|
|
109
109
|
if (run.status !== "active")
|
|
110
110
|
continue;
|
|
111
111
|
const workspace = run.workspace;
|
|
@@ -142,7 +142,7 @@ export async function scanLiveReferences(input) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
// 6. Active durable workspace owners (e.g. an active
|
|
145
|
+
// 6. Active durable workspace owners (e.g. an active AgentRun launch).
|
|
146
146
|
const activeOwners = input.ports?.activeWorkspaceOwners !== undefined
|
|
147
147
|
? input.ports.activeWorkspaceOwners()
|
|
148
148
|
: [];
|
|
@@ -232,7 +232,7 @@ function readActiveReleaseClaims(home) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
235
|
-
* Read Session owner records (`runtime/session-owners/<
|
|
235
|
+
* Read Session owner records (`runtime/session-owners/<pid-startIdentity>.json`). A
|
|
236
236
|
* record whose Provider root is physically alive with a matching start
|
|
237
237
|
* identity protects its runtime root. A dead PID is stale (reconciliation
|
|
238
238
|
* removes those records); an identity conflict or unreadable record fails
|
|
@@ -301,7 +301,7 @@ function readSessionOwnerClaims(home) {
|
|
|
301
301
|
if (typeof record.runtimeRoot === "string" && record.runtimeRoot.length > 0) {
|
|
302
302
|
claims.push({
|
|
303
303
|
path: resolve(record.runtimeRoot),
|
|
304
|
-
token: `session-owner:${
|
|
304
|
+
token: `session-owner:${pid}-${startIdentity}`
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
307
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
4
|
+
export function validateExecutionEnvironmentSnapshot(value) {
|
|
5
|
+
if (!value || typeof value !== "object")
|
|
6
|
+
throw new Error("Execution environment must be an object.");
|
|
7
|
+
requireIdentity(value.taskId, "Execution environment Task");
|
|
8
|
+
requireIdentity(value.preparationId, "Execution environment preparation");
|
|
9
|
+
if (value.environmentRef !== `${value.taskId}/${value.preparationId}`) {
|
|
10
|
+
throw new Error("Execution environment reference does not match its preparation.");
|
|
11
|
+
}
|
|
12
|
+
if (!["read", "write"].includes(value.access) || value.isolation !== "trusted-local" || !value.directory) {
|
|
13
|
+
throw new Error("Execution environment requires a trusted-local directory and explicit access.");
|
|
14
|
+
}
|
|
15
|
+
requireText(value.directory.path, "Execution environment path");
|
|
16
|
+
if (!isAbsolute(value.directory.path) || resolve(value.directory.path) !== value.directory.path) {
|
|
17
|
+
throw new Error("Execution environment path must be absolute and normalized.");
|
|
18
|
+
}
|
|
19
|
+
requireText(value.directory.device, "Execution environment device");
|
|
20
|
+
requireText(value.directory.inode, "Execution environment inode");
|
|
21
|
+
if (!["preparation", "user"].includes(value.directory.ownership))
|
|
22
|
+
throw new Error("Invalid execution directory owner.");
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function contentDigest(content) {
|
|
26
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
27
|
+
}
|
|
28
|
+
export function validateLocalResource(resource) {
|
|
29
|
+
if (resource.schemaVersion !== 1 || resource.kind !== "local-directory" || resource.ownership !== "user") {
|
|
30
|
+
throw new Error("Invalid local Resource record.");
|
|
31
|
+
}
|
|
32
|
+
requireIdentity(resource.id, "Resource id");
|
|
33
|
+
requireText(resource.displayName, "Resource name");
|
|
34
|
+
requireText(resource.path, "Resource path");
|
|
35
|
+
requireText(resource.device, "Resource device");
|
|
36
|
+
requireText(resource.inode, "Resource inode");
|
|
37
|
+
requireTimestamp(resource.createdAt, "Resource timestamp");
|
|
38
|
+
return resource;
|
|
39
|
+
}
|
|
40
|
+
export function validateEnvironmentPreparation(record) {
|
|
41
|
+
if (record.schemaVersion !== 1 || !["prepared", "adopted", "released"].includes(record.disposition)) {
|
|
42
|
+
throw new Error("Invalid Environment preparation.");
|
|
43
|
+
}
|
|
44
|
+
requireIdentity(record.id, "Preparation id");
|
|
45
|
+
requireIdentity(record.taskId, "Preparation Task");
|
|
46
|
+
requireText(record.intentDigest, "Preparation intent");
|
|
47
|
+
if (!Array.isArray(record.resourceRefs) || new Set(record.resourceRefs).size !== record.resourceRefs.length) {
|
|
48
|
+
throw new Error("Preparation resources must be unique.");
|
|
49
|
+
}
|
|
50
|
+
record.resourceRefs.forEach((id) => requireIdentity(id, "Preparation resource"));
|
|
51
|
+
if (!["read", "write"].includes(record.access))
|
|
52
|
+
throw new Error("Invalid preparation access.");
|
|
53
|
+
if (!["none", "trusted-local"].includes(record.isolation)
|
|
54
|
+
|| (record.directory === undefined) !== (record.isolation === "none")) {
|
|
55
|
+
throw new Error("Environment isolation must describe the actual local preparation.");
|
|
56
|
+
}
|
|
57
|
+
if ((record.directory === undefined) !== (record.environmentRef === undefined)) {
|
|
58
|
+
throw new Error("Environment reference requires an actual directory.");
|
|
59
|
+
}
|
|
60
|
+
if (record.directory) {
|
|
61
|
+
requireText(record.directory.path, "Environment path");
|
|
62
|
+
requireText(record.directory.device, "Environment device");
|
|
63
|
+
requireText(record.directory.inode, "Environment inode");
|
|
64
|
+
if (!["preparation", "user"].includes(record.directory.ownership))
|
|
65
|
+
throw new Error("Invalid directory owner.");
|
|
66
|
+
}
|
|
67
|
+
requireTimestamp(record.createdAt, "Preparation timestamp");
|
|
68
|
+
requireTimestamp(record.updatedAt, "Preparation update");
|
|
69
|
+
if (record.releaseEvidence !== undefined) {
|
|
70
|
+
if (record.disposition !== "released")
|
|
71
|
+
throw new Error("Release evidence requires a released preparation.");
|
|
72
|
+
requireText(record.releaseEvidence, "Release evidence");
|
|
73
|
+
}
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
4
|
+
import { checkGrant, recordGrantUse } from "../grant/capabilityGrant.js";
|
|
5
|
+
import { requireText } from "../domain/validation.js";
|
|
6
|
+
import { validateProject } from "../repository/project.js";
|
|
7
|
+
import { updateRole } from "../role/role.js";
|
|
8
|
+
import { saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
|
|
9
|
+
import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
|
|
10
|
+
import { assertProviderConversationReplaceable, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
11
|
+
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
12
|
+
import { contentDigest, validateExecutionEnvironmentSnapshot } from "./projectResource.js";
|
|
13
|
+
/** Trusted typed owner; the public caller is authenticated by the existing
|
|
14
|
+
* capability boundary. No new Store, worker, scheduling or Git authority. */
|
|
15
|
+
export function createProjectResources(store, now = () => new Date()) {
|
|
16
|
+
const task = (taskId) => {
|
|
17
|
+
const value = store.getTask(taskId);
|
|
18
|
+
if (!value)
|
|
19
|
+
throw new Error(`Task not found: ${taskId}.`);
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
const intent = (taskId) => {
|
|
23
|
+
const value = task(taskId);
|
|
24
|
+
return contentDigest(JSON.stringify({
|
|
25
|
+
// Git's moving delivery pointers do not change Resource intent.
|
|
26
|
+
projectBindings: value.projectBindings.map(({ projectId, directory, baseRef }) => ({ projectId, directory, baseRef })),
|
|
27
|
+
projects: value.projectBindings.map(({ projectId }) => {
|
|
28
|
+
const project = store.getProject(projectId);
|
|
29
|
+
return { projectId, resources: project?.resourceRefs, providers: project?.defaultCapabilityProviders };
|
|
30
|
+
})
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
const openTask = (taskId) => {
|
|
34
|
+
const value = task(taskId);
|
|
35
|
+
if (value.status !== "draft" && value.status !== "active")
|
|
36
|
+
throw new Error("Task is not open for environment adoption.");
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
const resource = (resourceId) => {
|
|
40
|
+
const value = store.getLocalResource(resourceId);
|
|
41
|
+
if (!value)
|
|
42
|
+
throw new Error(`Local resource not found: ${resourceId}.`);
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
const grantFor = (taskId, resourceId, access, reservation, reservedOnly = false) => {
|
|
46
|
+
const target = resource(resourceId);
|
|
47
|
+
const grant = store.listCapabilityGrants(taskId).find((candidate) => {
|
|
48
|
+
// A Project scope is visibility, not an exhaustive Resource grant. A
|
|
49
|
+
// concrete resource bound is mandatory even for a broadly named action.
|
|
50
|
+
if (!candidate.parameterBounds.resourceId?.includes(resourceId))
|
|
51
|
+
return false;
|
|
52
|
+
if (reservedOnly && (reservation === undefined || !candidate.useReservations.includes(reservation)))
|
|
53
|
+
return false;
|
|
54
|
+
if (candidate.scope.homePath !== undefined && resolve(candidate.scope.homePath) !== target.path)
|
|
55
|
+
return false;
|
|
56
|
+
if (candidate.scope.projectIds?.some((id) => !task(taskId).projectBindings.some((binding) => binding.projectId === id)))
|
|
57
|
+
return false;
|
|
58
|
+
return checkGrant(candidate, { action: `resource.local.${access}`, params: { resourceId } }, now(), { skipUsesCheck: reservation !== undefined && candidate.useReservations.includes(reservation) }).allowed;
|
|
59
|
+
});
|
|
60
|
+
if (!grant)
|
|
61
|
+
throw new Error(`Resource grant unavailable: ${resourceId}/${access}.`);
|
|
62
|
+
return grant;
|
|
63
|
+
};
|
|
64
|
+
const preparation = (taskId, id) => {
|
|
65
|
+
const value = store.getEnvironmentPreparation(taskId, id);
|
|
66
|
+
if (!value)
|
|
67
|
+
throw new Error(`Environment preparation not found: ${taskId}/${id}.`);
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
const assertLocalWriteOwner = (path, access) => {
|
|
71
|
+
if (access !== "write")
|
|
72
|
+
return;
|
|
73
|
+
const protectedPaths = [
|
|
74
|
+
realpathSync(store.rootDirectory()),
|
|
75
|
+
...store.listProjects().map((project) => project.path),
|
|
76
|
+
...store.listTasks().flatMap((value) => store.listManagedWorkspaces(value.id)
|
|
77
|
+
.flatMap((workspace) => [workspace.root, ...workspace.entries.map((entry) => entry.path)]))
|
|
78
|
+
];
|
|
79
|
+
if (protectedPaths.some((protectedPath) => overlaps(path, currentOrAbsentPath(protectedPath)))) {
|
|
80
|
+
throw new Error("Local resource overlaps a Yui Home or managed Git workspace; use its existing owner and Git operations.");
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const resolveExecutionEnvironment = (taskId, id) => {
|
|
84
|
+
openTask(taskId);
|
|
85
|
+
const value = preparation(taskId, id);
|
|
86
|
+
if (value.disposition !== "adopted")
|
|
87
|
+
throw new Error("Execution environment must be adopted and not released.");
|
|
88
|
+
if (value.intentDigest !== intent(taskId))
|
|
89
|
+
throw new Error("Task resource/configuration intent changed; prepare again.");
|
|
90
|
+
if (!value.directory || !value.environmentRef || value.isolation !== "trusted-local") {
|
|
91
|
+
throw new Error("Empty preparation has no native execution directory; select a directory environment or managed workspace.");
|
|
92
|
+
}
|
|
93
|
+
assertDirectory(value.directory);
|
|
94
|
+
if (value.directory.ownership === "user")
|
|
95
|
+
assertLocalWriteOwner(value.directory.path, value.access);
|
|
96
|
+
for (const resourceId of value.resourceRefs) {
|
|
97
|
+
// Adoption already charged the bounded grant. Launch/resume may recheck
|
|
98
|
+
// that reservation, but must neither invent nor consume another use.
|
|
99
|
+
grantFor(taskId, resourceId, value.access, id, true);
|
|
100
|
+
const target = resource(resourceId);
|
|
101
|
+
assertDirectory(target);
|
|
102
|
+
if (target.path !== value.directory.path || target.device !== value.directory.device || target.inode !== value.directory.inode) {
|
|
103
|
+
throw new Error("Execution environment no longer matches its registered Resource.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return validateExecutionEnvironmentSnapshot({
|
|
107
|
+
taskId, preparationId: id, environmentRef: value.environmentRef,
|
|
108
|
+
access: value.access, isolation: value.isolation, directory: { ...value.directory }
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
resolveExecutionEnvironment,
|
|
113
|
+
bindEnvironment(taskId, roleName, preparationId, source = { source: "environment.bind" }) {
|
|
114
|
+
return store.transaction((tx) => {
|
|
115
|
+
openTask(taskId);
|
|
116
|
+
const role = tx.getRole(taskId, roleName);
|
|
117
|
+
if (!role)
|
|
118
|
+
throw new Error(`Role not found: ${taskId}/${roleName}.`);
|
|
119
|
+
assertRoleRuntimeMutationAllowed(tx, { scope: "task", taskId, roleName }, "environment binding");
|
|
120
|
+
const executionEnvironment = preparationId === null ? null : resolveExecutionEnvironment(taskId, preparationId);
|
|
121
|
+
const timestamp = now();
|
|
122
|
+
const updated = updateRole(role, { executionEnvironment }, timestamp);
|
|
123
|
+
// Desired configuration only: active native Sessions retain their
|
|
124
|
+
// immutable actual snapshot until explicitly ended.
|
|
125
|
+
saveTaskRoleUpdate(tx, role, updated, timestamp, source);
|
|
126
|
+
return updated;
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
gitResult(taskId, changeSetId) {
|
|
130
|
+
const changeSet = store.getChangeSet(taskId, changeSetId);
|
|
131
|
+
if (!changeSet)
|
|
132
|
+
throw new Error("ChangeSet not found in this Task.");
|
|
133
|
+
return {
|
|
134
|
+
kind: "external-version", resourceId: changeSet.projectId,
|
|
135
|
+
version: changeSet.headCommit,
|
|
136
|
+
verification: { taskId, changeSetId: changeSet.id, baseCommit: changeSet.baseCommit },
|
|
137
|
+
// This is a projection of the existing fixed Git result, not another
|
|
138
|
+
// mutable repository/worktree/Integration record.
|
|
139
|
+
changeSet
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
registerLocalDirectory(displayName, path) {
|
|
143
|
+
const identity = directoryIdentity(path);
|
|
144
|
+
return store.transaction((tx) => {
|
|
145
|
+
const existing = tx.listLocalResources().find((value) => value.device === identity.device && value.inode === identity.inode);
|
|
146
|
+
if (existing)
|
|
147
|
+
return existing;
|
|
148
|
+
const value = { schemaVersion: 1, id: `resource-${randomUUID()}`,
|
|
149
|
+
kind: "local-directory", displayName: requireText(displayName, "Resource name"),
|
|
150
|
+
...identity, ownership: "user", createdAt: now().toISOString() };
|
|
151
|
+
tx.saveLocalResource(value);
|
|
152
|
+
return value;
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
readLocalResource(taskId, resourceId) {
|
|
156
|
+
grantFor(taskId, resourceId, "read");
|
|
157
|
+
return resource(resourceId);
|
|
158
|
+
},
|
|
159
|
+
projectContext(taskId, projectId) {
|
|
160
|
+
if (!task(taskId).projectBindings.some((binding) => binding.projectId === projectId))
|
|
161
|
+
throw new Error("Project is outside the Task context.");
|
|
162
|
+
const project = store.getProject(projectId);
|
|
163
|
+
if (!project)
|
|
164
|
+
throw new Error("Project not found.");
|
|
165
|
+
return { id: project.id, name: project.name, knowledge: project.knowledge,
|
|
166
|
+
resourceRefs: project.resourceRefs, defaultCapabilityProviders: project.defaultCapabilityProviders };
|
|
167
|
+
},
|
|
168
|
+
configureProject(projectId, resourceRefs, defaultCapabilityProviders) {
|
|
169
|
+
return store.transaction((tx) => {
|
|
170
|
+
const project = tx.getProject(projectId);
|
|
171
|
+
if (!project || project.status !== "active")
|
|
172
|
+
throw new Error("Active Project not found.");
|
|
173
|
+
resourceRefs.forEach(resource);
|
|
174
|
+
const next = validateProject({ ...project, resourceRefs, defaultCapabilityProviders, updatedAt: now().toISOString() });
|
|
175
|
+
tx.saveProject(next);
|
|
176
|
+
return next;
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
prepare(taskId, plan) {
|
|
180
|
+
openTask(taskId);
|
|
181
|
+
let directory;
|
|
182
|
+
const id = `preparation-${randomUUID()}`;
|
|
183
|
+
const access = plan.kind === "local" ? plan.access : "write";
|
|
184
|
+
const resourceRefs = plan.kind === "local" ? [plan.resourceId] : [];
|
|
185
|
+
if (plan.kind === "local") {
|
|
186
|
+
grantFor(taskId, plan.resourceId, access);
|
|
187
|
+
const target = resource(plan.resourceId);
|
|
188
|
+
assertDirectory(target);
|
|
189
|
+
assertLocalWriteOwner(target.path, access);
|
|
190
|
+
directory = { path: target.path, device: target.device, inode: target.inode, ownership: "user" };
|
|
191
|
+
}
|
|
192
|
+
else if (plan.kind === "scratch") {
|
|
193
|
+
const parent = join(store.rootDirectory(), "environments");
|
|
194
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
195
|
+
if (realpathSync(parent) !== resolve(parent))
|
|
196
|
+
throw new Error("Environment root must not be a symlink.");
|
|
197
|
+
directory = { ...directoryIdentity(mkdtempSync(join(parent, `${id}-`))), ownership: "preparation" };
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
return store.transaction((tx) => {
|
|
201
|
+
openTask(taskId);
|
|
202
|
+
const timestamp = now().toISOString();
|
|
203
|
+
const value = { schemaVersion: 1, id, taskId,
|
|
204
|
+
disposition: "prepared", intentDigest: intent(taskId), resourceRefs, access,
|
|
205
|
+
isolation: directory ? "trusted-local" : "none",
|
|
206
|
+
...(directory ? { directory, environmentRef: `${taskId}/${id}` } : {}),
|
|
207
|
+
createdAt: timestamp, updatedAt: timestamp };
|
|
208
|
+
tx.saveEnvironmentPreparation(value);
|
|
209
|
+
return value;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (directory?.ownership === "preparation") {
|
|
214
|
+
// Only the exact newly-created empty directory is eligible. Never
|
|
215
|
+
// recursively remove a partial/unknown external preparation.
|
|
216
|
+
try {
|
|
217
|
+
assertDirectory(directory);
|
|
218
|
+
rmdirSync(directory.path);
|
|
219
|
+
}
|
|
220
|
+
catch (cleanup) {
|
|
221
|
+
throw new AggregateError([error, cleanup], `Unadopted environment retained: ${directory.path}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
adopt(taskId, id) {
|
|
228
|
+
return store.transaction((tx) => {
|
|
229
|
+
openTask(taskId);
|
|
230
|
+
const value = preparation(taskId, id);
|
|
231
|
+
if (value.disposition === "released")
|
|
232
|
+
throw new Error("Environment was released.");
|
|
233
|
+
if (value.intentDigest !== intent(taskId))
|
|
234
|
+
throw new Error("Task resource/configuration intent changed; prepare again.");
|
|
235
|
+
if (value.directory)
|
|
236
|
+
assertDirectory(value.directory);
|
|
237
|
+
if (value.directory?.ownership === "user")
|
|
238
|
+
assertLocalWriteOwner(value.directory.path, value.access);
|
|
239
|
+
const grants = value.resourceRefs.map((resourceId) => grantFor(taskId, resourceId, value.access, id));
|
|
240
|
+
if (value.disposition === "adopted")
|
|
241
|
+
return value;
|
|
242
|
+
if (value.directory) {
|
|
243
|
+
for (const otherTask of tx.listTasks()) {
|
|
244
|
+
const conflict = tx.listEnvironmentPreparations(otherTask.id).some((other) => other.disposition === "adopted" && other.directory
|
|
245
|
+
&& (value.access === "write" || other.access === "write")
|
|
246
|
+
&& overlaps(other.directory.path, value.directory.path));
|
|
247
|
+
if (conflict)
|
|
248
|
+
throw new Error("Resource conflicts with an adopted environment; inspect its use or choose isolated scratch.");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
grants.forEach((grant) => tx.saveCapabilityGrant(taskId, recordGrantUse(grant, now(), id)));
|
|
252
|
+
const adopted = { ...value, disposition: "adopted", updatedAt: now().toISOString() };
|
|
253
|
+
tx.saveEnvironmentPreparation(adopted);
|
|
254
|
+
return adopted;
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
release(taskId, id, evidence) {
|
|
258
|
+
return store.transaction((tx) => {
|
|
259
|
+
const value = preparation(taskId, id);
|
|
260
|
+
if (value.disposition === "released")
|
|
261
|
+
return value;
|
|
262
|
+
if (value.disposition === "adopted" && value.directory) {
|
|
263
|
+
requireText(evidence?.quiescence ?? "", "Actual quiescence evidence");
|
|
264
|
+
const references = (environment) => environment?.taskId === taskId && environment.preparationId === id;
|
|
265
|
+
const sessionSets = tx.listRoleSessionSets(taskId);
|
|
266
|
+
const continuations = projectProviderContinuations(tx.listEvents(taskId));
|
|
267
|
+
for (const set of sessionSets) {
|
|
268
|
+
const users = [...Object.values(set.sessions), ...(set.history ?? [])]
|
|
269
|
+
.filter((session) => references(session.effective.executionEnvironment));
|
|
270
|
+
if (set.providerBinding != null && users.some((session) => session.nativeSessionId === currentProviderConversation(set.providerBinding).conversationId)) {
|
|
271
|
+
// A dead Host cannot prove that the native daemon accepted
|
|
272
|
+
// nothing or that a detached input has finished.
|
|
273
|
+
assertProviderConversationReplaceable(set.providerBinding);
|
|
274
|
+
}
|
|
275
|
+
if (continuations.some((continuation) => continuation.roleName === set.owner.roleName
|
|
276
|
+
&& (continuation.execution !== "quiescent"
|
|
277
|
+
|| continuation.observation !== "exact" || continuation.identityConflict)
|
|
278
|
+
&& (users.some((session) => session.agentId === continuation.identity.accountScope
|
|
279
|
+
&& session.nativeSessionId === continuation.identity.conversationId)
|
|
280
|
+
|| references(tx.getRun(taskId, continuation.runId)?.effective.executionEnvironment)))) {
|
|
281
|
+
throw new Error("Environment still has a live or unknown native continuation.");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (value.directory && (tx.listRuns(taskId).some((run) => run.status === "active"
|
|
285
|
+
&& (references(run.effective.executionEnvironment)
|
|
286
|
+
|| (run.workspace && overlaps(run.workspace.root, value.directory.path))))
|
|
287
|
+
|| sessionSets.some((set) => [...Object.values(set.sessions), ...(set.history ?? [])].some((session) => session.status === "active" && references(session.effective.executionEnvironment)))
|
|
288
|
+
|| tx.listDurableJobs(taskId).some((job) => ["queued", "running", "unknown-needs-attention"].includes(job.status)
|
|
289
|
+
&& overlaps(job.workspace, value.directory.path))))
|
|
290
|
+
throw new Error("Environment still has a live or unknown execution reference.");
|
|
291
|
+
}
|
|
292
|
+
if (value.directory?.ownership === "preparation") {
|
|
293
|
+
assertDirectory(value.directory);
|
|
294
|
+
const parent = realpathSync(join(store.rootDirectory(), "environments"));
|
|
295
|
+
if (!within(parent, value.directory.path) || !value.directory.path.startsWith(join(parent, `${id}-`))) {
|
|
296
|
+
throw new Error("Preparation directory is outside its owned root.");
|
|
297
|
+
}
|
|
298
|
+
// Content must first be saved or explicitly handled by its owner.
|
|
299
|
+
// Nonempty directories are retained with ENOTEMPTY, never force-deleted.
|
|
300
|
+
rmdirSync(value.directory.path);
|
|
301
|
+
}
|
|
302
|
+
const released = { ...value, disposition: "released", updatedAt: now().toISOString(),
|
|
303
|
+
...(evidence === undefined ? {} : { releaseEvidence: requireText(evidence.quiescence, "Actual quiescence evidence") }) };
|
|
304
|
+
tx.saveEnvironmentPreparation(released);
|
|
305
|
+
return released;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function directoryIdentity(path) {
|
|
311
|
+
const canonical = realpathSync(resolve(path));
|
|
312
|
+
const stat = statSync(canonical, { bigint: true });
|
|
313
|
+
if (!stat.isDirectory())
|
|
314
|
+
throw new Error("Resource must be a local directory.");
|
|
315
|
+
return { path: canonical, device: String(stat.dev), inode: String(stat.ino) };
|
|
316
|
+
}
|
|
317
|
+
function assertDirectory(expected) {
|
|
318
|
+
if (lstatSync(expected.path).isSymbolicLink())
|
|
319
|
+
throw new Error("Directory identity changed to a symlink.");
|
|
320
|
+
const current = directoryIdentity(expected.path);
|
|
321
|
+
if (current.path !== expected.path || current.device !== expected.device || current.inode !== expected.inode) {
|
|
322
|
+
throw new Error("Directory identity changed; retained for owner inspection.");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function within(parent, path) {
|
|
326
|
+
const value = relative(parent, path);
|
|
327
|
+
return value !== "" && value !== ".." && !value.startsWith("../") && !isAbsolute(value);
|
|
328
|
+
}
|
|
329
|
+
function overlaps(left, right) {
|
|
330
|
+
return left === right || within(left, right) || within(right, left);
|
|
331
|
+
}
|
|
332
|
+
function currentOrAbsentPath(path) {
|
|
333
|
+
try {
|
|
334
|
+
return realpathSync(path);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
// Historical workspace references may outlive a removed directory. Retain
|
|
338
|
+
// that exact address; never manufacture a replacement or ignore other errors.
|
|
339
|
+
if (error.code === "ENOENT")
|
|
340
|
+
return resolve(path);
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -179,8 +179,8 @@ function discoverRuntimeArtifacts(home, input) {
|
|
|
179
179
|
for (const launchEntry of safeReaddir(join(taskRoot, ownerEntry.name))) {
|
|
180
180
|
if (!launchEntry.isDirectory())
|
|
181
181
|
continue;
|
|
182
|
-
const
|
|
183
|
-
const marker = readTaskRuntimeMarker(
|
|
182
|
+
const runtimeRoot = join(taskRoot, ownerEntry.name, launchEntry.name);
|
|
183
|
+
const marker = readTaskRuntimeMarker(runtimeRoot);
|
|
184
184
|
const owner = marker === undefined
|
|
185
185
|
? { home, taskId, basis: "naming-convention" }
|
|
186
186
|
: {
|
|
@@ -193,9 +193,9 @@ function discoverRuntimeArtifacts(home, input) {
|
|
|
193
193
|
discovered.push({
|
|
194
194
|
record: createResourceRecord({
|
|
195
195
|
kind: "runtime-artifact",
|
|
196
|
-
path: resolve(
|
|
196
|
+
path: resolve(runtimeRoot),
|
|
197
197
|
owner,
|
|
198
|
-
...(sizeOf(
|
|
198
|
+
...(sizeOf(runtimeRoot) === undefined ? {} : { sizeBytes: sizeOf(runtimeRoot) }),
|
|
199
199
|
cleanliness: "n/a",
|
|
200
200
|
activeRefs: [],
|
|
201
201
|
disposition: "active"
|
|
@@ -352,9 +352,9 @@ function readDeploymentGitMetadata(path) {
|
|
|
352
352
|
return { repositoryPath: path };
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
function readTaskRuntimeMarker(
|
|
355
|
+
function readTaskRuntimeMarker(runtimeRoot) {
|
|
356
356
|
try {
|
|
357
|
-
const marker = JSON.parse(readFileSync(join(
|
|
357
|
+
const marker = JSON.parse(readFileSync(join(runtimeRoot, ".yui-task-runtime-owner.json"), "utf8"));
|
|
358
358
|
const owner = marker.descriptor?.workspace?.owner;
|
|
359
359
|
if (owner === undefined || owner === null)
|
|
360
360
|
return {};
|
|
@@ -125,7 +125,7 @@ function classifyDiscoveredResources(discovered, registry, scan, input, home) {
|
|
|
125
125
|
}
|
|
126
126
|
/**
|
|
127
127
|
* Collect active workspace owner path fragments from durable state.
|
|
128
|
-
* These protect resources that an active Session, Job, or
|
|
128
|
+
* These protect resources that an active Session, Job, or AgentRun still
|
|
129
129
|
* references even when the Task itself is terminal.
|
|
130
130
|
*/
|
|
131
131
|
function collectActiveWorkspaceOwners(input) {
|
|
@@ -68,7 +68,7 @@ export class ResourceRegistrar {
|
|
|
68
68
|
#registerTaskRuntimeIsolation(descriptor) {
|
|
69
69
|
const owner = ownerFromManagedWorkspace(this.#home, descriptor.workspace.owner);
|
|
70
70
|
this.#save([
|
|
71
|
-
descriptor.roots.
|
|
71
|
+
descriptor.roots.runtime,
|
|
72
72
|
descriptor.roots.data,
|
|
73
73
|
descriptor.roots.cache,
|
|
74
74
|
descriptor.roots.temporary
|
|
@@ -30,7 +30,7 @@ export function isReleasable(record) {
|
|
|
30
30
|
}
|
|
31
31
|
/** Terminal Task statuses: a terminal owner keeps no active runtime claim. */
|
|
32
32
|
export function isTerminalTaskStatus(status) {
|
|
33
|
-
return status === "completed" || status === "
|
|
33
|
+
return status === "completed" || status === "cancelled" || status === "archived";
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* The immutable release namespace owned by the package installer (Issue 02).
|
|
@@ -7,7 +7,7 @@ import { validateDeltaRecheckRecord } from "./reviewRound.js";
|
|
|
7
7
|
export async function assessDeltaRecheck(input) {
|
|
8
8
|
const { repositoryPaths, previousRound, candidate, git } = input;
|
|
9
9
|
if (previousRound.status !== "completed"
|
|
10
|
-
|| previousRound.
|
|
10
|
+
|| previousRound.reviewerRunId === undefined
|
|
11
11
|
|| (previousRound.scope ?? "work-item") !== "task") {
|
|
12
12
|
return {
|
|
13
13
|
kind: "ineligible",
|
|
@@ -129,8 +129,8 @@ export function buildDeltaRecheckDispatchContext(input) {
|
|
|
129
129
|
" semantic change, evidence doubt). This is the safe default.",
|
|
130
130
|
"Yui verified only the technical delta boundary; a Task-control Agent selected this mode.",
|
|
131
131
|
`Previous accepted ReviewRound: ${previousRound.id}@${record.previousBaseCommit}`,
|
|
132
|
-
`Previous Reviewer
|
|
133
|
-
"Read that exact source
|
|
132
|
+
`Previous Reviewer AgentRun: ${previousRound.reviewerRunId ?? "unavailable"}`,
|
|
133
|
+
"Read that exact source AgentRun's original result from the frozen Context Snapshot.",
|
|
134
134
|
...(round.taskCandidate?.projects.map((project) => {
|
|
135
135
|
const previous = previousRound.taskCandidate?.projects
|
|
136
136
|
.find((entry) => entry.projectId === project.projectId)?.commit;
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
export function
|
|
2
|
-
if (round.status !== "completed" || round.
|
|
1
|
+
export function isCompletedReviewExecutionFromRuns(round, runs) {
|
|
2
|
+
if (round.status !== "completed" || round.reviewerRunId === undefined)
|
|
3
3
|
return false;
|
|
4
|
-
const
|
|
5
|
-
return
|
|
6
|
-
&&
|
|
7
|
-
&&
|
|
8
|
-
&&
|
|
9
|
-
&&
|
|
10
|
-
&&
|
|
11
|
-
&&
|
|
12
|
-
&&
|
|
13
|
-
&&
|
|
4
|
+
const run = runs.find(({ id }) => id === round.reviewerRunId);
|
|
5
|
+
return run !== undefined
|
|
6
|
+
&& run.status === "completed"
|
|
7
|
+
&& run.result !== undefined
|
|
8
|
+
&& run.purpose === "review"
|
|
9
|
+
&& run.taskId === round.taskId
|
|
10
|
+
&& run.reviewRoundId === round.id
|
|
11
|
+
&& run.roleName === round.reviewerRoleName
|
|
12
|
+
&& run.executionGroupId === undefined
|
|
13
|
+
&& run.effective.reviewBaseCommit === round.reviewBaseCommit;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Whether a Task-final ReviewRound has one exact completed main Reviewer
|
|
16
|
+
* Whether a Task-final ReviewRound has one exact completed main Reviewer AgentRun.
|
|
17
17
|
* This is structural evidence only and never means the Leader accepted it.
|
|
18
18
|
*/
|
|
19
19
|
export function isCompletedTaskReviewEvidence(store, round) {
|
|
20
|
-
return
|
|
20
|
+
return isCompletedTaskReviewEvidenceFromRuns(round, store.listRuns(round.taskId));
|
|
21
21
|
}
|
|
22
|
-
export function
|
|
22
|
+
export function isCompletedTaskReviewEvidenceFromRuns(round, runs) {
|
|
23
23
|
if ((round.scope ?? "work-item") !== "task"
|
|
24
24
|
|| round.taskCandidate === undefined
|
|
25
25
|
|| round.taskCandidate.projects.length === 0) {
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
|
-
return
|
|
28
|
+
return isCompletedReviewExecutionFromRuns(round, runs);
|
|
29
29
|
}
|
|
@@ -3,7 +3,7 @@ import { isCompletedTaskReviewEvidence } from "./reviewAcceptance.js";
|
|
|
3
3
|
import { projectReviewerAvailability } from "./reviewerAvailability.js";
|
|
4
4
|
/** Read-only facts for Leader judgment; this projection never blocks an action. */
|
|
5
5
|
export function projectReviewDecision(input) {
|
|
6
|
-
const { store, task, roles,
|
|
6
|
+
const { store, task, roles, runs, rounds, reviewConfig, currentCandidate } = input;
|
|
7
7
|
const taskRounds = rounds.filter((round) => (round.scope ?? "work-item") === "task");
|
|
8
8
|
const accepted = [...taskRounds]
|
|
9
9
|
.filter((round) => isCompletedTaskReviewEvidence(store, round))
|
|
@@ -20,15 +20,15 @@ export function projectReviewDecision(input) {
|
|
|
20
20
|
const activeReviews = taskRounds
|
|
21
21
|
.filter((round) => (round.status === "pending" || round.status === "running"))
|
|
22
22
|
.map((round) => {
|
|
23
|
-
const
|
|
24
|
-
const workspaceRoot = round.workspace?.root ??
|
|
23
|
+
const activeRun = runs.find((run) => (run.status === "active" && run.reviewRoundId === round.id));
|
|
24
|
+
const workspaceRoot = round.workspace?.root ?? activeRun?.workspace?.root;
|
|
25
25
|
return {
|
|
26
26
|
reviewRoundId: round.id,
|
|
27
27
|
reviewerRoleName: round.reviewerRoleName,
|
|
28
28
|
mode: round.deltaRecheck === undefined ? "full" : "delta-recheck",
|
|
29
29
|
status: round.status,
|
|
30
|
-
...(
|
|
31
|
-
startedAt:
|
|
30
|
+
...(activeRun === undefined ? {} : { activeRunId: activeRun.id }),
|
|
31
|
+
startedAt: activeRun?.createdAt ?? round.createdAt,
|
|
32
32
|
frozenCandidate: round.taskCandidate ?? null,
|
|
33
33
|
candidateRelation: candidateRelation(round.taskCandidate ?? null, currentCandidate),
|
|
34
34
|
...(workspaceRoot === undefined ? {} : { workspaceRoot })
|
|
@@ -62,9 +62,9 @@ export function projectReviewDecision(input) {
|
|
|
62
62
|
reviewerRoleName,
|
|
63
63
|
status: "busy",
|
|
64
64
|
phase: availability.phase,
|
|
65
|
-
...(availability.
|
|
65
|
+
...(availability.activeRunId === undefined
|
|
66
66
|
? {}
|
|
67
|
-
: {
|
|
67
|
+
: { activeRunId: availability.activeRunId }),
|
|
68
68
|
...(availability.activeReviewRoundId === undefined
|
|
69
69
|
? {}
|
|
70
70
|
: { activeReviewRoundId: availability.activeReviewRoundId }),
|