@zq-silk/yui 0.13.9 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -75,7 +75,7 @@ export function builtinAgentProfileInputs() {
|
|
|
75
75
|
{
|
|
76
76
|
id: "reviewer",
|
|
77
77
|
description: "Review one candidate against the user's core outcome, supported behavior, and direct evidence.",
|
|
78
|
-
instructions: "Start from user intent and acceptance criteria. Inspect the complete relevant change and report only reachable, material, actionable problems with direct evidence. Separate defects from verification gaps, and prefer the smallest sufficient correction. Follow the bound Project's Policy and Knowledge for build, test, migration, release, and review expectations; do not import rules from another Project or Task. For normal software delivery, review the frozen Task result as one final ReviewRound rather than inventing a per-WorkItem protocol unless the Project Policy explicitly requires one. A Task-final Round has no synthetic WorkItem anchor, and the Reviewer Session and physical workspace continue across changed-head Rounds without reusing an earlier verdict. The current
|
|
78
|
+
instructions: "Start from user intent and acceptance criteria. Inspect the complete relevant change and report only reachable, material, actionable problems with direct evidence. Separate defects from verification gaps, and prefer the smallest sufficient correction. Follow the bound Project's Policy and Knowledge for build, test, migration, release, and review expectations; do not import rules from another Project or Task. For normal software delivery, review the frozen Task result as one final ReviewRound rather than inventing a per-WorkItem protocol unless the Project Policy explicitly requires one. A Task-final Round has no synthetic WorkItem anchor, and the Reviewer Session and physical workspace continue across changed-head Rounds without reusing an earlier verdict. The current Turn's frozen candidate remains the only scope even if Task main advances. For Delta Recheck, return equivalent-and-accepted, finding, or requires-full-review with explicit reasoning; never create the next Round. Do not turn speculative or extreme edge cases into new state, retries, fallbacks, or protocol. In the Review workspace you may edit source or tests, run local checks, and optionally commit diagnostic evidence. Never push, integrate, mutate Task state, touch another workspace or stable checkout, or write the real Yui control-plane home. End the Provider turn with complete findings, checks actually run, uncertainty, and bounded next actions; Yui preserves the full free-form report automatically. Expose evidence and options to the Leader, who decides.",
|
|
79
79
|
defaultAccess: "write"
|
|
80
80
|
}
|
|
81
81
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { UNSUPPORTED, collectRuntimeBuildIdentity, createProductionRuntimeIdentityPorts } from "../observability/runtimeIdentity.js";
|
|
4
|
-
import {
|
|
4
|
+
import { readCurrentHomeIdentity } from "../storage/currentTaskStore.js";
|
|
5
5
|
import { detectRunningRelease, readActiveReleasePointer, releaseDirectoryName } from "./runtimeRelease.js";
|
|
6
6
|
/**
|
|
7
7
|
* Prevent a checkout or otherwise unverified CLI from entering a Home that is
|
|
@@ -47,7 +47,7 @@ export function describeCliHomeInvocation(input) {
|
|
|
47
47
|
: "unverified-local";
|
|
48
48
|
let homeId = "unknown";
|
|
49
49
|
try {
|
|
50
|
-
homeId =
|
|
50
|
+
homeId = readCurrentHomeIdentity(input.home).homeId;
|
|
51
51
|
}
|
|
52
52
|
catch {
|
|
53
53
|
// The primary migration error remains authoritative when identity is unreadable.
|
|
@@ -102,7 +102,7 @@ function readPackageDistribution(packageRoot) {
|
|
|
102
102
|
function fenceError(input, active, source, release, collected = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(input.packageRoot, input.entryPath))) {
|
|
103
103
|
let homeId = "unknown";
|
|
104
104
|
try {
|
|
105
|
-
homeId =
|
|
105
|
+
homeId = readCurrentHomeIdentity(input.home).homeId;
|
|
106
106
|
}
|
|
107
107
|
catch {
|
|
108
108
|
// The release fence must still diagnose and reject an unreadable Home.
|
|
@@ -30,8 +30,8 @@ const IRREVERSIBILITY_RANK = {
|
|
|
30
30
|
* Concurrency guard: prevents two runs of the same workflow from submitting
|
|
31
31
|
* duplicate external effects. Each CLI invocation is a fresh Node process with
|
|
32
32
|
* its own module instance, so an in-process map cannot coordinate across
|
|
33
|
-
* processes. When the store
|
|
34
|
-
*
|
|
33
|
+
* processes. When the store exposes its root directory, the guard is a file
|
|
34
|
+
* lock under that root; otherwise (in-memory test stores)
|
|
35
35
|
* it falls back to an in-process map. The second run waits for the first to
|
|
36
36
|
* finish, then re-reads the (now-updated) workflow state.
|
|
37
37
|
*/
|
|
@@ -86,8 +86,8 @@ async function acquireWorkflowLock(store, taskId, workflowId) {
|
|
|
86
86
|
* crash between the two is healed on resume as described above.
|
|
87
87
|
*
|
|
88
88
|
* Concurrent runs of the same workflow are serialized by a cross-process file
|
|
89
|
-
* lock when the store
|
|
90
|
-
*
|
|
89
|
+
* lock when the store exposes its durable root, so a second CLI process cannot
|
|
90
|
+
* submit a duplicate effect.
|
|
91
91
|
*/
|
|
92
92
|
export async function runReleaseWorkflow(store, taskId, workflowId, ports, options = {}) {
|
|
93
93
|
// Prevent concurrent runs of the same workflow from submitting duplicate
|
|
@@ -381,7 +381,7 @@ async function runReleaseWorkflowLocked(store, taskId, workflowId, ports, option
|
|
|
381
381
|
// For irreversible steps, mark as unknown and stop so the effect is
|
|
382
382
|
// never re-submitted blindly. For reversible steps, mark as failed.
|
|
383
383
|
// The exception is re-thrown so the caller sees the crash; the workflow
|
|
384
|
-
// state is already persisted for recovery on the next
|
|
384
|
+
// state is already persisted for recovery on the next turn.
|
|
385
385
|
const message = error instanceof Error ? error.message : String(error);
|
|
386
386
|
const crashNow = clock();
|
|
387
387
|
if (effectiveIrreversibility === "irreversible") {
|
|
@@ -461,11 +461,7 @@ function attemptReservationKey(workflowId, stepId, attempt) {
|
|
|
461
461
|
* by starting a second workflow against the same grant.
|
|
462
462
|
*/
|
|
463
463
|
function grantUseRecognized(grant, workflowId, stepId, attempt) {
|
|
464
|
-
|
|
465
|
-
if (Array.isArray(reservations)) {
|
|
466
|
-
return reservations.includes(attemptReservationKey(workflowId, stepId, attempt));
|
|
467
|
-
}
|
|
468
|
-
return false;
|
|
464
|
+
return grant.useReservations.includes(attemptReservationKey(workflowId, stepId, attempt));
|
|
469
465
|
}
|
|
470
466
|
/**
|
|
471
467
|
* Records one grant use after a successful authorization decision. The engine
|
|
@@ -186,7 +186,7 @@ export function createReleaseWorkflowPorts(deps) {
|
|
|
186
186
|
logs: [`cli-update: ${result.outcome} (${result.version})`]
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
|
-
if (result.outcome === "
|
|
189
|
+
if (result.outcome === "aborted" && !result.recoverable) {
|
|
190
190
|
// P1-2 (rr20): An aborted activation with recoverable=false means
|
|
191
191
|
// npm activation began but its outcome is unknowable — the global
|
|
192
192
|
// install may or may not have landed. Treat it as ambiguous:
|
|
@@ -203,7 +203,7 @@ export function createReleaseWorkflowPorts(deps) {
|
|
|
203
203
|
// externalIdentity so the engine marks the step as `unknown`
|
|
204
204
|
// and stops — resume must not continue until the expected
|
|
205
205
|
// Controller lifecycle/identity handoff is proven complete.
|
|
206
|
-
if (result.
|
|
206
|
+
if (result.controllerOwnershipUnknown === true) {
|
|
207
207
|
return {
|
|
208
208
|
outcome: "timeout",
|
|
209
209
|
logs: [`cli-update Controller ownership unknown at phase ${result.phase}: ${result.message}`]
|
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
/**
|
|
5
5
|
* Cross-process mutual exclusion for one workflow's engine runs. Each CLI
|
|
6
6
|
* invocation is a fresh Node process, so an in-process map cannot coordinate
|
|
7
|
-
* across processes; this lock lives under the
|
|
7
|
+
* across processes; this lock lives under the current Home root instead.
|
|
8
8
|
*
|
|
9
9
|
* The lock is a directory created atomically with mkdir(2): its existence is
|
|
10
10
|
* the lock, so a second acquirer fails with EEXIST while the first holds it.
|
|
@@ -25,7 +25,7 @@ export async function healCheckoutSwap(request, ports = realPorts) {
|
|
|
25
25
|
* leaving the registered path without a valid repository. The previous
|
|
26
26
|
* checkout is moved to the backup path first; if the promotion fails it is
|
|
27
27
|
* restored and the staging clone is removed. A crash between the two renames
|
|
28
|
-
* is healed by {@link healCheckoutSwap} on the next
|
|
28
|
+
* is healed by {@link healCheckoutSwap} on the next turn.
|
|
29
29
|
*/
|
|
30
30
|
export async function swapManagedCheckout(request, ports = realPorts) {
|
|
31
31
|
await healCheckoutSwap(request, ports);
|
|
@@ -121,19 +121,6 @@ export class NodeGitWorkspace {
|
|
|
121
121
|
}
|
|
122
122
|
return { addedLines, deletedLines };
|
|
123
123
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Files deleted between two commits. Uses `--diff-filter=D` so a rename
|
|
126
|
-
* reports its source path here (and its destination in `changedFilesBetween`),
|
|
127
|
-
* letting the containment proof verify both sides of a rename.
|
|
128
|
-
*/
|
|
129
|
-
async deletedFilesBetween(input) {
|
|
130
|
-
const output = await git([
|
|
131
|
-
"-C", input.repositoryPath,
|
|
132
|
-
"diff", "--name-only", "--diff-filter=D", "--no-renames",
|
|
133
|
-
input.fromCommit, input.toCommit
|
|
134
|
-
]);
|
|
135
|
-
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
136
|
-
}
|
|
137
124
|
/**
|
|
138
125
|
* Resolve the configured Project branch directly from its remote. This is
|
|
139
126
|
* deliberately read-only: unlike `refresh`, it never advances the stable
|
|
@@ -31,7 +31,7 @@ export function validateProjectName(value) {
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Soft-retire a Project: mark it retired and record the audit trail. The
|
|
34
|
-
* catalog record, checkout, and every historical Task/
|
|
34
|
+
* catalog record, checkout, and every historical Task/Turn/Review/Integration/
|
|
35
35
|
* Publication reference are retained. Retiring an already-retired Project
|
|
36
36
|
* fails closed; a retired Project must be deleted (or kept) as-is.
|
|
37
37
|
*/
|
|
@@ -3,8 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
/**
|
|
4
4
|
* Per-Project maintenance fence.
|
|
5
5
|
*
|
|
6
|
-
* `project migrate
|
|
7
|
-
* archive cleanup are separate CLI processes that all mutate a Project's
|
|
6
|
+
* `project migrate` and Task archive cleanup are separate CLI processes that mutate a Project's
|
|
8
7
|
* Git repository. Without coordination they can interleave with each other
|
|
9
8
|
* and with the Controller's worktree preparation. The fence is an exclusive
|
|
10
9
|
* directory under the Home's `locks/` area (O_EXCL via mkdir), so it works
|
|
@@ -61,10 +61,10 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
|
|
|
61
61
|
const task = requireTask(store, taskId);
|
|
62
62
|
const git = options.git ?? newGitWorkspace();
|
|
63
63
|
const workspace = store.getTaskWorkspace(taskId);
|
|
64
|
-
// Collect active
|
|
65
|
-
// report whether a live
|
|
66
|
-
const
|
|
67
|
-
.filter((
|
|
64
|
+
// Collect active Turn workspace snapshots once so every Project entry can
|
|
65
|
+
// report whether a live Turn is pinned to a different commit.
|
|
66
|
+
const activeTurns = store.listTurns(taskId)
|
|
67
|
+
.filter((turn) => turn.status === "active" && turn.workspace !== undefined);
|
|
68
68
|
const entries = await Promise.all(task.projectBindings.map(async (binding) => {
|
|
69
69
|
const project = requireProject(store, binding.projectId);
|
|
70
70
|
const entry = workspace === null ? undefined : workspaceProjectEntry(workspace, project.id);
|
|
@@ -77,12 +77,12 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
|
|
|
77
77
|
catch {
|
|
78
78
|
physicalHead = undefined;
|
|
79
79
|
}
|
|
80
|
-
// Find the first active
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
80
|
+
// Find the first active Turn whose workspace snapshot covers this Project.
|
|
81
|
+
const turnSnapshot = activeTurns.find((turn) => {
|
|
82
|
+
if (turn.workspace === undefined)
|
|
83
83
|
return false;
|
|
84
|
-
const
|
|
85
|
-
return
|
|
84
|
+
const turnEntry = workspaceProjectEntry(turn.workspace, project.id);
|
|
85
|
+
return turnEntry !== undefined;
|
|
86
86
|
});
|
|
87
87
|
const provenance = latestBaseProvenance(store.listEvents(taskId), project.id);
|
|
88
88
|
const tracked = await resolveTracked(git, project, workspacePath, options.refresh === true);
|
|
@@ -99,11 +99,11 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
|
|
|
99
99
|
workspacePath,
|
|
100
100
|
workspaceClean,
|
|
101
101
|
...(physicalHead === undefined ? {} : { physicalHead }),
|
|
102
|
-
...(
|
|
102
|
+
...(turnSnapshot?.workspace === undefined
|
|
103
103
|
? {}
|
|
104
104
|
: {
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
turnSnapshotCommit: workspaceProjectEntry(turnSnapshot.workspace, project.id)?.baseCommit,
|
|
106
|
+
turnSnapshotId: turnSnapshot.id
|
|
107
107
|
}),
|
|
108
108
|
...(tracked === undefined ? {} : {
|
|
109
109
|
trackedRef: tracked.ref,
|
|
@@ -161,7 +161,7 @@ export function renderTaskBaseFreshnessReport(report) {
|
|
|
161
161
|
`Task base freshness: ${report.taskId} (source: ${report.refreshed ? "remote refresh" : "local tracking"})`
|
|
162
162
|
];
|
|
163
163
|
for (const entry of report.entries) {
|
|
164
|
-
lines.push(`- ${entry.directory}: ${entry.projectId} @ ${entry.baseCommit}`, ` status: ${entry.status}; workspace: ${entry.workspaceClean === null ? "unknown" : entry.workspaceClean ? "clean" : "dirty"}`, ` physical HEAD: ${entry.physicalHead ?? "-"}`, `
|
|
164
|
+
lines.push(`- ${entry.directory}: ${entry.projectId} @ ${entry.baseCommit}`, ` status: ${entry.status}; workspace: ${entry.workspaceClean === null ? "unknown" : entry.workspaceClean ? "clean" : "dirty"}`, ` physical HEAD: ${entry.physicalHead ?? "-"}`, ` Turn snapshot: ${entry.turnSnapshotCommit ?? "-"}${entry.turnSnapshotId === undefined ? "" : ` (${entry.turnSnapshotId})`}`, ` observed remote: ${entry.observedRemoteUrl ?? "-"}`, ` tracked: ${entry.trackedCommit ?? "-"}${entry.trackedRef === undefined ? "" : ` (${entry.trackedRef})`}`, ` observed: ${entry.observedTrackingCommit ?? "-"}${entry.observedAt === undefined ? "" : ` at ${entry.observedAt}`}`);
|
|
165
165
|
if (entry.risk !== undefined)
|
|
166
166
|
lines.push(` risk: ${entry.risk}`);
|
|
167
167
|
if (entry.error !== undefined)
|
|
@@ -23,10 +23,10 @@ export class TaskWorkspaceCoordinator {
|
|
|
23
23
|
if (item === null)
|
|
24
24
|
throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
|
|
25
25
|
const assignee = this.#workItemIsolationAssignee(item);
|
|
26
|
-
const
|
|
26
|
+
const activeDevelopTurn = this.store.listTurns(item.taskId)
|
|
27
27
|
.find((run) => run.status === "active" && run.workItemId === item.id);
|
|
28
|
-
if (
|
|
29
|
-
throw new Error(`Work Item already has an active Develop
|
|
28
|
+
if (activeDevelopTurn !== undefined) {
|
|
29
|
+
throw new Error(`Work Item already has an active Develop Turn: ${activeDevelopTurn.id}.`);
|
|
30
30
|
}
|
|
31
31
|
const task = this.store.getTask(item.taskId);
|
|
32
32
|
const taskProjectIds = task.projectBindings.map(({ projectId }) => projectId);
|
|
@@ -72,8 +72,8 @@ export class TaskWorkspaceCoordinator {
|
|
|
72
72
|
}
|
|
73
73
|
if (item.workspaceDisposition === disposition)
|
|
74
74
|
return "missing";
|
|
75
|
-
// Hold the per-Project maintenance fence so
|
|
76
|
-
// archive cannot interleave with worktree removal.
|
|
75
|
+
// Hold the per-Project maintenance fence so concurrent Project maintenance
|
|
76
|
+
// or Task archive cannot interleave with worktree removal.
|
|
77
77
|
const workspace = this.store.getWorkItemWorkspace(item.taskId, item.id);
|
|
78
78
|
const projectIds = workspace === null
|
|
79
79
|
? []
|
|
@@ -124,8 +124,8 @@ export class TaskWorkspaceCoordinator {
|
|
|
124
124
|
}
|
|
125
125
|
if (round.workspaceDisposition?.kind === "reassigned")
|
|
126
126
|
return "missing";
|
|
127
|
-
// Hold the per-Project maintenance fence so
|
|
128
|
-
// archive cannot interleave with worktree removal.
|
|
127
|
+
// Hold the per-Project maintenance fence so concurrent Project maintenance
|
|
128
|
+
// or Task archive cannot interleave with worktree removal.
|
|
129
129
|
const workspace = this.store.getReviewRoundWorkspace(taskId, reviewRoundId);
|
|
130
130
|
const projectIds = workspace === null
|
|
131
131
|
? []
|
|
@@ -163,7 +163,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
163
163
|
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
164
164
|
// Archive cleanup removes worktrees from every Project the Task uses:
|
|
165
165
|
// hold each Project's maintenance fence so the Controller defers
|
|
166
|
-
// preparation and no
|
|
166
|
+
// preparation and no Project maintenance or Task archive interleaves.
|
|
167
167
|
const projectIds = new Set(task.projectBindings.map(({ projectId }) => projectId));
|
|
168
168
|
for (const workspace of managedWorkspaces) {
|
|
169
169
|
for (const entry of workspace.entries)
|
|
@@ -253,9 +253,9 @@ export class TaskWorkspaceCoordinator {
|
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
const activeRole = this.store.listRoles(taskId)
|
|
256
|
-
.find((role) => this.store.
|
|
256
|
+
.find((role) => this.store.getActiveTurn(taskId, role.name) !== null);
|
|
257
257
|
if (activeRole !== undefined) {
|
|
258
|
-
throw new WorkspaceCleanupBlockedError("active-
|
|
258
|
+
throw new WorkspaceCleanupBlockedError("active-turn", `role:${task.id}/${activeRole.name}`, true, `Task Role still has an active Turn: ${task.id}/${activeRole.name}.`);
|
|
259
259
|
}
|
|
260
260
|
const roleNames = this.store.listRoles(taskId).map(({ name }) => name);
|
|
261
261
|
if (roleNames.length > 0) {
|
|
@@ -326,10 +326,10 @@ export class TaskWorkspaceCoordinator {
|
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
#assertWorkItemRuntimeQuiescent(item) {
|
|
329
|
-
const
|
|
329
|
+
const activeTurn = this.store.listTurns(item.taskId)
|
|
330
330
|
.find((run) => run.status === "active" && run.workItemId === item.id);
|
|
331
|
-
if (
|
|
332
|
-
throw new WorkspaceCleanupBlockedError("active-
|
|
331
|
+
if (activeTurn !== undefined) {
|
|
332
|
+
throw new WorkspaceCleanupBlockedError("active-turn", `work-item:${item.taskId}/${item.id}`, true, `Work item still has an active Turn: ${item.taskId}/${item.id}.`);
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
/**
|
|
@@ -368,14 +368,10 @@ export class TaskWorkspaceCoordinator {
|
|
|
368
368
|
}
|
|
369
369
|
async #stopLiveRoles(taskId, roleNames) {
|
|
370
370
|
const targets = [...new Set(roleNames)];
|
|
371
|
-
const
|
|
371
|
+
const getActiveTurn = this.store.getActiveTurn?.bind(this.store);
|
|
372
372
|
for (const roleName of targets) {
|
|
373
|
-
if (
|
|
374
|
-
throw new Error(`Role has an active
|
|
375
|
-
}
|
|
376
|
-
const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
377
|
-
if (sessions?.inFlight !== null && sessions?.inFlight !== undefined) {
|
|
378
|
-
throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
|
|
373
|
+
if (getActiveTurn !== undefined && getActiveTurn(taskId, roleName) !== null) {
|
|
374
|
+
throw new Error(`Role has an active Turn: ${taskId}/${roleName}.`);
|
|
379
375
|
}
|
|
380
376
|
if (this.store.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
381
377
|
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
@@ -397,7 +393,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
397
393
|
// The aggregate-16 dormant Claude placeholder is the sole exception to
|
|
398
394
|
// strict workspace-session retirement. The synchronous pane inspection is
|
|
399
395
|
// performed while holding the Task store transaction so a normal launch
|
|
400
|
-
// cannot reserve a
|
|
396
|
+
// cannot reserve a Turn/Session between absence proof and terminalization.
|
|
401
397
|
if (inspect === undefined || this.store.transaction === undefined)
|
|
402
398
|
return;
|
|
403
399
|
this.store.transaction((tx) => {
|
|
@@ -406,8 +402,8 @@ export class TaskWorkspaceCoordinator {
|
|
|
406
402
|
if (panes.some((pane) => pane.roleName === roleName && !pane.dead)) {
|
|
407
403
|
throw new Error(`Task Role native pane must stop before workspace migration: ${roleName}.`);
|
|
408
404
|
}
|
|
409
|
-
if (tx.
|
|
410
|
-
throw new Error(`Role has an active
|
|
405
|
+
if (tx.getActiveTurn(taskId, roleName) !== null) {
|
|
406
|
+
throw new Error(`Role has an active Turn: ${taskId}/${roleName}.`);
|
|
411
407
|
}
|
|
412
408
|
if (tx.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
413
409
|
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
@@ -436,8 +432,8 @@ export class TaskWorkspaceCoordinator {
|
|
|
436
432
|
if (isTerminalWorkItem(item)) {
|
|
437
433
|
throw new Error(`Work item is already terminal: ${item.id}.`);
|
|
438
434
|
}
|
|
439
|
-
if (item.assignee !== undefined && this.store.
|
|
440
|
-
throw new Error(`Role has an active
|
|
435
|
+
if (item.assignee !== undefined && this.store.getActiveTurn(task.id, item.assignee) !== null) {
|
|
436
|
+
throw new Error(`Role has an active Turn: ${task.id}/${item.assignee}.`);
|
|
441
437
|
}
|
|
442
438
|
if (item.assignee !== undefined && this.store.getRole(task.id, item.assignee) === null) {
|
|
443
439
|
throw new Error(`Role not found: ${task.id}/${item.assignee}.`);
|
|
@@ -46,20 +46,18 @@ export function taskWorkspaceRefSegmentFromIdentity(identity) {
|
|
|
46
46
|
return `${valid.taskId}-${valid.token}`;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* The durable ref segment for a Task.
|
|
50
|
-
*
|
|
51
|
-
* valid v4 Task that never had a managed Git workspace, or one awaiting the
|
|
52
|
-
* controlled rebuild) keeps its bare Task id so its existing worktrees remain
|
|
53
|
-
* addressable until they are rebuilt.
|
|
49
|
+
* The durable ref segment for a Task. Every managed Git workspace uses the
|
|
50
|
+
* token-bearing identity minted before its first adoption.
|
|
54
51
|
*/
|
|
55
52
|
export function taskWorkspaceRefSegment(task) {
|
|
56
|
-
if (task.workspaceIdentity === undefined)
|
|
57
|
-
|
|
53
|
+
if (task.workspaceIdentity === undefined) {
|
|
54
|
+
throw new Error(`Task workspace identity is required: ${task.id}.`);
|
|
55
|
+
}
|
|
58
56
|
return taskWorkspaceRefSegmentFromIdentity(task.workspaceIdentity);
|
|
59
57
|
}
|
|
60
58
|
/**
|
|
61
59
|
* The strict main-branch ref for a Task workspace. The main branch is always
|
|
62
|
-
* `yui/task-N-<8hex>/main`; any other shape is
|
|
60
|
+
* `yui/task-N-<8hex>/main`; any other shape is foreign to the current contract.
|
|
63
61
|
*/
|
|
64
62
|
export function taskMainBranch(refSegment) {
|
|
65
63
|
if (!TASK_WORKSPACE_REF_SEGMENT_PATTERN.test(refSegment)) {
|
|
@@ -89,11 +87,9 @@ export function taskIntegrationBranch(refSegment, integrationId) {
|
|
|
89
87
|
return taskDerivedBranch(refSegment, `integration/${integrationId}`);
|
|
90
88
|
}
|
|
91
89
|
/**
|
|
92
|
-
* The non-colliding, auditable archive ref for a
|
|
93
|
-
*
|
|
90
|
+
* The non-colliding, auditable archive ref for a current managed Task ref.
|
|
91
|
+
* The Home id keeps two Homes' archives apart when they share a Project
|
|
94
92
|
* repository; the full original ref name is preserved verbatim.
|
|
95
|
-
* Legacy refs live at `refs/heads/yui/task-N/...`; archives at
|
|
96
|
-
* `refs/yui/archive/<homeId>/heads/yui/task-N/...`.
|
|
97
93
|
*/
|
|
98
94
|
export function taskArchiveRef(homeId, sourceRef) {
|
|
99
95
|
const id = requireIdentityPart(homeId, "Home id");
|
|
@@ -105,15 +101,6 @@ export function taskArchiveRef(homeId, sourceRef) {
|
|
|
105
101
|
}
|
|
106
102
|
return `refs/yui/archive/${id}/${ref.slice("refs/".length)}`;
|
|
107
103
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Whether a managed branch belongs to the legacy (pre-identity) layout:
|
|
110
|
-
* `yui/task-N/...` with a bare Task id segment. Identity-bearing branches
|
|
111
|
-
* (`yui/task-N-<8hex>/...`) are never legacy.
|
|
112
|
-
*/
|
|
113
|
-
export function isLegacyTaskRef(refName) {
|
|
114
|
-
const match = /^refs\/heads\/(yui\/(task-[0-9]+)\/.+)$/.exec(refName);
|
|
115
|
-
return match !== null && !TASK_WORKSPACE_REF_SEGMENT_PATTERN.test(match[2]);
|
|
116
|
-
}
|
|
117
104
|
export function validateTaskWorkspaceIdentity(identity) {
|
|
118
105
|
if (identity.schemaVersion !== 1) {
|
|
119
106
|
throw new Error("Task workspace identity must use schemaVersion 1.");
|