@zq-silk/yui 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `yui update` orchestration.
|
|
3
|
+
*
|
|
4
|
+
* The previous implementation ran `npm install --global` *first*, which is
|
|
5
|
+
* self-contradictory with "on failure the old CLI and Home are both unchanged":
|
|
6
|
+
* once the global package is replaced there is nothing to fall back to. This
|
|
7
|
+
* orchestrator replaces that with a side-by-side staging flow:
|
|
8
|
+
*
|
|
9
|
+
* stage (side-by-side, never touches the live install)
|
|
10
|
+
* -> preflight (the STAGED new binary classifies the target Home read-only)
|
|
11
|
+
* -> current/compatible: exact Controller stop -> activate the same binary
|
|
12
|
+
* -> migration-required: authoritative offline inventory -> recoverable
|
|
13
|
+
* storage switch -> activate the same binary
|
|
14
|
+
* -> post-verify through the activated binary, then replacement Controller
|
|
15
|
+
*
|
|
16
|
+
* ## Rollback boundary (NARROWED — no versioned binary pointer is introduced)
|
|
17
|
+
*
|
|
18
|
+
* The managed Session launcher is an in-place forwarder, not a versioned package
|
|
19
|
+
* pointer, so this release does NOT claim binary+Home dual-resource atomicity.
|
|
20
|
+
* The guarantees it DOES make, precisely:
|
|
21
|
+
*
|
|
22
|
+
* - **Staging is isolated.** `stage` installs side-by-side and never mutates the
|
|
23
|
+
* live global install, so a stage/preflight failure leaves the old binary and
|
|
24
|
+
* the Home byte-for-byte unchanged.
|
|
25
|
+
* - **Compatible activation does not switch the Home.** The fast path stops the
|
|
26
|
+
* exact Controller, promotes the staged artifact, and verifies a compatible
|
|
27
|
+
* current-model loader without copy, backup, rename, or Session wait.
|
|
28
|
+
* - **Storage activation is recoverable until writes resume.** The Home switch is
|
|
29
|
+
* atomic with a timestamped backup (see the migration engine). Before the new
|
|
30
|
+
* version resumes writes, recovery is a single restore of that backup.
|
|
31
|
+
* - **No auto-downgrade after writes.** Once the new version has resumed writes
|
|
32
|
+
* (a new Controller/first write against the migrated Home), the tool never
|
|
33
|
+
* auto-reverts; the migrated Home is authoritative.
|
|
34
|
+
*
|
|
35
|
+
* The one window it CANNOT make atomic is: storage already switched to the new
|
|
36
|
+
* schema, but binary promotion then fails. Because the axes are version-gated,
|
|
37
|
+
* the old binary fail-closes on the new Home rather than misreading it; recovery
|
|
38
|
+
* is to restore the timestamped Home backup (the exact command is printed), after
|
|
39
|
+
* which the old binary works again. This is stated in the failure output, not
|
|
40
|
+
* hidden behind a false atomicity claim.
|
|
41
|
+
*/
|
|
42
|
+
import { isAbsolute } from "node:path";
|
|
43
|
+
import { switchProgressPath } from "../storage/upgrade/switchProgress.js";
|
|
44
|
+
import { upgradeReceiptPath } from "../storage/upgrade/upgradeReceipt.js";
|
|
45
|
+
/**
|
|
46
|
+
* Run the update orchestration. Never performs an irreversible step before the
|
|
47
|
+
* read-only preflight has proven the Home is safe, and reports the precise phase
|
|
48
|
+
* plus a recovery action on any failure.
|
|
49
|
+
*/
|
|
50
|
+
export function runUpdate(ports, options) {
|
|
51
|
+
const home = options.home;
|
|
52
|
+
// 1) Stage side-by-side. A failure here leaves the live install untouched.
|
|
53
|
+
let staged;
|
|
54
|
+
try {
|
|
55
|
+
staged = ports.stage();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
outcome: "aborted",
|
|
60
|
+
phase: "stage",
|
|
61
|
+
message: `Failed to stage the new package: ${messageOf(error)}`,
|
|
62
|
+
action: "The current install and Home are unchanged. Fix the staging error and retry.",
|
|
63
|
+
recoverable: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
let result;
|
|
67
|
+
let cleanupWarning;
|
|
68
|
+
try {
|
|
69
|
+
result = runStagedUpdate(ports, staged, home);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
try {
|
|
73
|
+
ports.cleanup(staged);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Cleanup is deliberately best-effort. Once the orchestrator has
|
|
77
|
+
// determined an update/abort/ambiguous result, a staging I/O failure must
|
|
78
|
+
// not replace that authoritative outcome or turn it into a generic error.
|
|
79
|
+
cleanupWarning = `Staging cleanup could not be completed: ${messageOf(error)}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
|
|
83
|
+
}
|
|
84
|
+
/** Run the staged flow; cleanup is owned by the caller so warnings are retained. */
|
|
85
|
+
function runStagedUpdate(ports, staged, home) {
|
|
86
|
+
// 2) Preflight — the staged binary inspects the Home read-only; no switch.
|
|
87
|
+
// A preflight port that throws unexpectedly (e.g. an I/O fault) is not a safe
|
|
88
|
+
// green light: treat it as a blocked preflight (recoverable, no switch) rather
|
|
89
|
+
// than letting the exception escape (R4-F1).
|
|
90
|
+
let preflight;
|
|
91
|
+
try {
|
|
92
|
+
preflight = ports.preflight(staged, home);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
outcome: "aborted",
|
|
97
|
+
phase: "preflight",
|
|
98
|
+
message: `Preflight failed unexpectedly: ${messageOf(error)}`,
|
|
99
|
+
action: "The current install and Home are unchanged. Investigate the staged binary and retry.",
|
|
100
|
+
recoverable: true,
|
|
101
|
+
version: staged.version
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (preflight.status === "blocked") {
|
|
105
|
+
return {
|
|
106
|
+
outcome: "aborted",
|
|
107
|
+
phase: "preflight",
|
|
108
|
+
message: preflight.message,
|
|
109
|
+
action: preflight.action,
|
|
110
|
+
recoverable: true,
|
|
111
|
+
version: staged.version,
|
|
112
|
+
...(preflight.blockers === undefined ? {} : { blockers: preflight.blockers }),
|
|
113
|
+
...(preflight.retryCommand === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { retryCommand: preflight.retryCommand }),
|
|
116
|
+
...(preflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Capture and stop the old Controller exactly once after preflight but
|
|
120
|
+
// before either storage activation or binary promotion. This parent update
|
|
121
|
+
// process remains the sole lifecycle owner for both binary-only and
|
|
122
|
+
// migratable updates.
|
|
123
|
+
const lifecycle = captureControllerLifecycle(ports, staged.version, home);
|
|
124
|
+
if ("outcome" in lifecycle)
|
|
125
|
+
return lifecycle;
|
|
126
|
+
if (preflight.status === "already-current" || preflight.status === "compatible") {
|
|
127
|
+
// Current and all-compatible chains share the lean path: no Home copy,
|
|
128
|
+
// backup, rename, or command replay. The exact Controller handoff prevents
|
|
129
|
+
// old/new writers from overlapping while the same staged artifact activates.
|
|
130
|
+
return activateAndVerify(ports, staged, home, undefined, lifecycle.lifecycle, preflight.status === "compatible" ? "compatible-fast" : "current-fast");
|
|
131
|
+
}
|
|
132
|
+
// 3) Activate storage — recoverable: atomic switch + timestamped backup. An
|
|
133
|
+
// activation port that throws unexpectedly may have committed the switch
|
|
134
|
+
// before failing, so its state is UNKNOWN — resolve it as ambiguous from the
|
|
135
|
+
// durable on-disk evidence, never let the exception escape as a false clean
|
|
136
|
+
// failure (R4-F1 / P1-2).
|
|
137
|
+
let activation;
|
|
138
|
+
try {
|
|
139
|
+
activation = ports.activateStorage(staged, home);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return resolveAmbiguousActivation(ports, staged, home, `the activation step threw unexpectedly: ${messageOf(error)}`);
|
|
143
|
+
}
|
|
144
|
+
if (activation.status === "blocked") {
|
|
145
|
+
const failure = {
|
|
146
|
+
outcome: "aborted",
|
|
147
|
+
phase: "activate-storage",
|
|
148
|
+
message: activation.message,
|
|
149
|
+
action: activation.action,
|
|
150
|
+
// The engine guarantees the source Home is unchanged on a blocked/failed
|
|
151
|
+
// migration, and the binary has not been promoted, so this is recoverable.
|
|
152
|
+
recoverable: true,
|
|
153
|
+
version: staged.version,
|
|
154
|
+
...(activation.blockers === undefined ? {} : { blockers: activation.blockers }),
|
|
155
|
+
...(activation.retryCommand === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { retryCommand: activation.retryCommand }),
|
|
158
|
+
...(activation.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
159
|
+
};
|
|
160
|
+
// The child was externally quiesced by this parent. A clean pre-switch
|
|
161
|
+
// refusal therefore restores the exact captured identity; an ambiguous
|
|
162
|
+
// activation is handled separately and never restores blindly.
|
|
163
|
+
return restoreBeforeSwitchOrReport(ports, home, lifecycle.lifecycle, undefined, failure);
|
|
164
|
+
}
|
|
165
|
+
if (activation.status === "ambiguous") {
|
|
166
|
+
// The activation child left no parseable receipt: the switch may or may not
|
|
167
|
+
// have committed. Resolve the true state from the durable on-disk evidence
|
|
168
|
+
// and report an explicit manual recovery — never a false "recoverable".
|
|
169
|
+
return resolveAmbiguousActivation(ports, staged, home, activation.detail);
|
|
170
|
+
}
|
|
171
|
+
if (activation.status === "migrated" && !isValidBackupPath(activation.backupPath)) {
|
|
172
|
+
// A migrated/upgraded success without a concrete backup path violates
|
|
173
|
+
// the recoverable storage-activation contract. Resolve it through the
|
|
174
|
+
// existing durable receipt/schema probe instead of inferring that the
|
|
175
|
+
// Home was untouched.
|
|
176
|
+
return resolveAmbiguousActivation(ports, staged, home, "the activation reported migrated without a non-empty absolute backupPath");
|
|
177
|
+
}
|
|
178
|
+
const backupPath = activation.status === "migrated" ? activation.backupPath : undefined;
|
|
179
|
+
// 4/5) Promote the binary, then post-verify with the new binary's loader.
|
|
180
|
+
return activateAndVerify(ports, staged, home, backupPath, lifecycle.lifecycle, "offline-migration");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Promote the staged binary and run the new-binary health check. This is the
|
|
184
|
+
* last, non-atomic step: after storage has switched, binary promotion cannot be
|
|
185
|
+
* made atomic with it, so a failure here is reported with the exact backup-based
|
|
186
|
+
* recovery and is NOT auto-reverted once the new version would resume writes.
|
|
187
|
+
*/
|
|
188
|
+
function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, path) {
|
|
189
|
+
try {
|
|
190
|
+
ports.activateBinary(staged);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const failure = {
|
|
194
|
+
outcome: "aborted",
|
|
195
|
+
phase: "activate-binary",
|
|
196
|
+
message: `Failed to activate the new binary: ${messageOf(error)}`,
|
|
197
|
+
action: storageBackupPath === undefined
|
|
198
|
+
? binaryActivationUncertainAction()
|
|
199
|
+
: postSwitchRecoveryAction(home, storageBackupPath),
|
|
200
|
+
// Once binary activation begins, its outcome is not knowable from a
|
|
201
|
+
// failed npm process. Home-not-switched is useful evidence, but it does
|
|
202
|
+
// not prove the current installation remains usable.
|
|
203
|
+
recoverable: false,
|
|
204
|
+
version: staged.version,
|
|
205
|
+
...(storageBackupPath === undefined ? {} : { storageBackupPath })
|
|
206
|
+
};
|
|
207
|
+
return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
ports.verify(staged, home);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
const failure = {
|
|
214
|
+
outcome: "aborted",
|
|
215
|
+
phase: "post-verify",
|
|
216
|
+
message: `Post-update health check failed: ${messageOf(error)}`,
|
|
217
|
+
action: storageBackupPath === undefined
|
|
218
|
+
? binaryHealthUncertainAction()
|
|
219
|
+
: postSwitchRecoveryAction(home, storageBackupPath),
|
|
220
|
+
recoverable: false,
|
|
221
|
+
version: staged.version,
|
|
222
|
+
...(storageBackupPath === undefined ? {} : { storageBackupPath })
|
|
223
|
+
};
|
|
224
|
+
return restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure);
|
|
225
|
+
}
|
|
226
|
+
if (lifecycle?.wasRunning === true) {
|
|
227
|
+
try {
|
|
228
|
+
ports.startController(home);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const unknownActive = isUnknownActiveControllerFailure(error);
|
|
232
|
+
const failure = {
|
|
233
|
+
outcome: "aborted",
|
|
234
|
+
phase: "post-verify",
|
|
235
|
+
message: `${unknownActive ? "Replacement Controller ownership could not be authenticated safely" : "The replacement Controller could not start after activation and health verification"}: `
|
|
236
|
+
+ `${messageOf(error)}.`,
|
|
237
|
+
action: unknownActive
|
|
238
|
+
? unknownActiveControllerAction(home, storageBackupPath)
|
|
239
|
+
: storageBackupPath === undefined
|
|
240
|
+
? "The Home was not migrated. Keep writes quiesced and restore the previously running Controller identity before retrying."
|
|
241
|
+
: postSwitchRecoveryAction(home, storageBackupPath),
|
|
242
|
+
recoverable: false,
|
|
243
|
+
version: staged.version,
|
|
244
|
+
...(storageBackupPath === undefined ? {} : { storageBackupPath })
|
|
245
|
+
};
|
|
246
|
+
// An ownership-unknown mismatch is a live-process blocker, not a
|
|
247
|
+
// recoverable pre-switch failure. Restoring the captured identity here
|
|
248
|
+
// could overwrite or race a foreign process, so leave the Home quiesced
|
|
249
|
+
// and preserve the explicit manual blocker.
|
|
250
|
+
return unknownActive
|
|
251
|
+
? failure
|
|
252
|
+
: restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return storageBackupPath === undefined
|
|
256
|
+
? { outcome: "updated", version: staged.version, path }
|
|
257
|
+
: { outcome: "updated", version: staged.version, path, storageBackupPath };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Capture the old Controller before a binary-only update mutates the install.
|
|
261
|
+
* A partial lifecycle port set is rejected rather than silently selecting a
|
|
262
|
+
* staged/new `ensureFileTaskController` for restoration.
|
|
263
|
+
*/
|
|
264
|
+
function captureControllerLifecycle(ports, version, home) {
|
|
265
|
+
const supplied = [
|
|
266
|
+
ports.controllerStatus,
|
|
267
|
+
ports.stopController,
|
|
268
|
+
ports.startController,
|
|
269
|
+
ports.restoreController
|
|
270
|
+
].some((port) => port !== undefined);
|
|
271
|
+
if (!supplied)
|
|
272
|
+
return { lifecycle: { wasRunning: false, stopped: false } };
|
|
273
|
+
if (ports.controllerStatus === undefined
|
|
274
|
+
|| ports.stopController === undefined
|
|
275
|
+
|| ports.startController === undefined
|
|
276
|
+
|| ports.restoreController === undefined) {
|
|
277
|
+
return {
|
|
278
|
+
outcome: "aborted",
|
|
279
|
+
phase: "preflight",
|
|
280
|
+
message: "Controller lifecycle ownership is incomplete for this update.",
|
|
281
|
+
action: "Provide status, stop, replacement-start, and exact-identity restore seams; refusing to activate a binary without a complete lifecycle owner.",
|
|
282
|
+
recoverable: true,
|
|
283
|
+
version
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
let status;
|
|
287
|
+
try {
|
|
288
|
+
status = ports.controllerStatus(home);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
return {
|
|
292
|
+
outcome: "aborted",
|
|
293
|
+
phase: "preflight",
|
|
294
|
+
message: `Controller status could not be verified: ${messageOf(error)}`,
|
|
295
|
+
action: "Do not activate the binary while Controller ownership is unknown; inspect the Controller and retry once status is verified.",
|
|
296
|
+
recoverable: true,
|
|
297
|
+
version
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (!isControllerLifecycleStatus(status)) {
|
|
301
|
+
return {
|
|
302
|
+
outcome: "aborted",
|
|
303
|
+
phase: "preflight",
|
|
304
|
+
message: "Controller status was malformed; expected a boolean running field.",
|
|
305
|
+
action: "Inspect the Controller status provider and retry; no binary activation was attempted.",
|
|
306
|
+
recoverable: true,
|
|
307
|
+
version
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (!status.running)
|
|
311
|
+
return { lifecycle: { wasRunning: false, stopped: false } };
|
|
312
|
+
if (!isPositivePid(status.pid)) {
|
|
313
|
+
return {
|
|
314
|
+
outcome: "aborted",
|
|
315
|
+
phase: "preflight",
|
|
316
|
+
message: "The Controller is running but its exact PID could not be captured.",
|
|
317
|
+
action: "Refusing an unfenced stop; inspect Controller ownership and retry only after its PID is authenticated.",
|
|
318
|
+
recoverable: true,
|
|
319
|
+
version
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (!isControllerIdentity(status.identity)) {
|
|
323
|
+
return {
|
|
324
|
+
outcome: "aborted",
|
|
325
|
+
phase: "preflight",
|
|
326
|
+
message: "The Controller is running but its executable/version identity could not be captured.",
|
|
327
|
+
action: "Refusing to stop a Controller that cannot be restored exactly; provide its executable path, arguments, and version, then retry.",
|
|
328
|
+
recoverable: true,
|
|
329
|
+
version
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
let stopped;
|
|
333
|
+
try {
|
|
334
|
+
stopped = ports.stopController(home, status.pid);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return {
|
|
338
|
+
outcome: "aborted",
|
|
339
|
+
phase: "preflight",
|
|
340
|
+
message: `Controller stop/drain failed: ${messageOf(error)}`,
|
|
341
|
+
action: "The Controller may still be active or draining. Do not retry stop blindly; inspect it and retry once the Home is quiesced.",
|
|
342
|
+
recoverable: true,
|
|
343
|
+
version
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (!isControllerStopResult(stopped)
|
|
347
|
+
|| stopped.stopped !== true
|
|
348
|
+
|| stopped.pid !== status.pid) {
|
|
349
|
+
return {
|
|
350
|
+
outcome: "aborted",
|
|
351
|
+
phase: "preflight",
|
|
352
|
+
message: `Controller stop did not confirm the captured PID ${status.pid} with stopped:true.`,
|
|
353
|
+
action: "Keep the Home quiesced and inspect the Controller; no binary activation was attempted and no stop retry was issued.",
|
|
354
|
+
recoverable: true,
|
|
355
|
+
version
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
lifecycle: {
|
|
360
|
+
wasRunning: true,
|
|
361
|
+
stopped: true,
|
|
362
|
+
identity: status.identity
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function restoreBeforeSwitchOrReport(ports, home, lifecycle, storageBackupPath, failure) {
|
|
367
|
+
// Once storage switched, the old Controller is never safe to restore. Keep
|
|
368
|
+
// the failure structured and point at all durable recovery evidence instead.
|
|
369
|
+
if (storageBackupPath !== undefined || lifecycle?.wasRunning !== true)
|
|
370
|
+
return failure;
|
|
371
|
+
try {
|
|
372
|
+
ports.restoreController(home, lifecycle.identity);
|
|
373
|
+
return failure;
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
// Preserve the original phase, Home-switch evidence, and binary-health
|
|
377
|
+
// uncertainty guidance. A restore failure is an appended blocker, not a
|
|
378
|
+
// replacement generic post-verify result.
|
|
379
|
+
return {
|
|
380
|
+
...failure,
|
|
381
|
+
message: `${failure.message} The previously running Controller identity could not be restored: `
|
|
382
|
+
+ `${messageOf(error)}.`,
|
|
383
|
+
action: `${failure.action} The old Controller restore also failed: ${messageOf(error)}. `
|
|
384
|
+
+ "Keep the Home quiesced; inspect the restore blocker and resolve it before any bounded retry. ",
|
|
385
|
+
recoverable: false
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function postSwitchRecoveryAction(home, backupPath) {
|
|
390
|
+
return `The storage switch is committed (backup at ${backupPath}); do not restore the old Controller. `
|
|
391
|
+
+ `Inspect the backup, receipt marker "${upgradeReceiptPath(home)}", and switch-progress marker `
|
|
392
|
+
+ `"${switchProgressPath(home)}". Verify the migrated Home, then either finish the update or `
|
|
393
|
+
+ `restore the backup explicitly with mv "${backupPath}" "${home}".`;
|
|
394
|
+
}
|
|
395
|
+
function binaryActivationUncertainAction() {
|
|
396
|
+
return "The Home was not migrated, but binary activation began and its outcome is unknown; "
|
|
397
|
+
+ "do not assume the current install is usable. Reinstall Yui, verify `yui version` and "
|
|
398
|
+
+ "`yui doctor`, then retry `yui update` before resuming writes.";
|
|
399
|
+
}
|
|
400
|
+
function binaryHealthUncertainAction() {
|
|
401
|
+
return "The Home was not migrated, but the activated binary failed health verification; do not "
|
|
402
|
+
+ "assume the current install is usable. Reinstall Yui, verify `yui version` and `yui doctor`, "
|
|
403
|
+
+ "then retry `yui update` before resuming writes.";
|
|
404
|
+
}
|
|
405
|
+
function unknownActiveControllerAction(home, backupPath) {
|
|
406
|
+
const storageEvidence = backupPath === undefined
|
|
407
|
+
? "The Home was not migrated."
|
|
408
|
+
: `The storage switch is committed (backup at ${backupPath}); do not restore the old Controller. `;
|
|
409
|
+
return `${storageEvidence} A replacement Controller may still be active under unknown ownership. `
|
|
410
|
+
+ `Keep writes quiesced and do not claim recovery or resume writes. Inspect the authenticated `
|
|
411
|
+
+ `Controller status for ${home}, stop only the PID proven to belong to this update, then `
|
|
412
|
+
+ "verify `yui version` and `yui doctor` before a bounded retry.";
|
|
413
|
+
}
|
|
414
|
+
function isUnknownActiveControllerFailure(error) {
|
|
415
|
+
return isRecord(error) && error.code === "UPDATE_CONTROLLER_UNKNOWN_ACTIVE";
|
|
416
|
+
}
|
|
417
|
+
function isControllerLifecycleStatus(value) {
|
|
418
|
+
return isRecord(value) && typeof value.running === "boolean";
|
|
419
|
+
}
|
|
420
|
+
function isControllerStopResult(value) {
|
|
421
|
+
return isRecord(value) && typeof value.stopped === "boolean";
|
|
422
|
+
}
|
|
423
|
+
function isPositivePid(value) {
|
|
424
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
425
|
+
}
|
|
426
|
+
function isControllerIdentity(value) {
|
|
427
|
+
return isRecord(value)
|
|
428
|
+
&& typeof value.executablePath === "string"
|
|
429
|
+
&& value.executablePath.length > 0
|
|
430
|
+
&& Array.isArray(value.args)
|
|
431
|
+
&& value.args.every((arg) => typeof arg === "string")
|
|
432
|
+
&& typeof value.version === "string"
|
|
433
|
+
&& value.version.length > 0;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Resolve an ambiguous storage activation (P1-2): the activation child left no
|
|
437
|
+
* parseable receipt, so the switch may or may not have committed. Probe the
|
|
438
|
+
* durable on-disk evidence and report an explicit manual recovery — this is
|
|
439
|
+
* NEVER reported as recoverable-and-unchanged.
|
|
440
|
+
*
|
|
441
|
+
* The verdict is driven by the completion receipt first (the switch writes it the
|
|
442
|
+
* instant it commits), then corroborated by the on-disk schema:
|
|
443
|
+
* - receipt present -> the switch committed; point at the backup and require
|
|
444
|
+
* the operator to verify the migrated Home before resuming (the binary was
|
|
445
|
+
* never promoted, so the old binary fail-closes on the new schema).
|
|
446
|
+
* - no receipt, schema already current -> most likely the switch never ran
|
|
447
|
+
* (or fully reverted); still require an explicit re-run rather than asserting
|
|
448
|
+
* "unchanged", because stdout was lost.
|
|
449
|
+
* - no receipt, schema not current -> genuinely indeterminate; give the
|
|
450
|
+
* operator the exact files to inspect.
|
|
451
|
+
*/
|
|
452
|
+
function resolveAmbiguousActivation(ports, staged, home, detail) {
|
|
453
|
+
let probe;
|
|
454
|
+
try {
|
|
455
|
+
probe = ports.probeStorage(home);
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
// Even the probe failed: report maximum uncertainty with the raw evidence.
|
|
459
|
+
return {
|
|
460
|
+
outcome: "ambiguous",
|
|
461
|
+
phase: "activate-storage",
|
|
462
|
+
message: `Storage activation result is unknown (${detail}); probing the Home also failed: ${messageOf(error)}.`,
|
|
463
|
+
action: `Do NOT assume the update succeeded or was a no-op. Manually inspect ${home} and any `
|
|
464
|
+
+ `"${home}.backup-*" sibling, "${upgradeReceiptPath(home)}", and `
|
|
465
|
+
+ `"${switchProgressPath(home)}"; if a receipt exists the `
|
|
466
|
+
+ `switch committed — verify the migrated Home with "yui doctor" before resuming, otherwise `
|
|
467
|
+
+ `restore the newest backup with mv before re-running "yui update".`,
|
|
468
|
+
version: staged.version,
|
|
469
|
+
schemaCurrent: false,
|
|
470
|
+
switched: false
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (probe.interrupted === true) {
|
|
474
|
+
// A partially-applied, interrupted switch: the original was moved to the
|
|
475
|
+
// backup and neither promotion nor rollback completed, so the Home path may
|
|
476
|
+
// be missing. This is the strongest "restore the backup now" signal — never a
|
|
477
|
+
// "verify the migrated Home" or a "recoverable no-op".
|
|
478
|
+
return {
|
|
479
|
+
outcome: "ambiguous",
|
|
480
|
+
phase: "activate-storage",
|
|
481
|
+
message: `Storage switch was INTERRUPTED mid-rename (${detail}); the Home may be missing and was `
|
|
482
|
+
+ `NOT left intact. The new binary was NOT promoted.`,
|
|
483
|
+
action: `Restore the timestamped backup to recover the original Home: `
|
|
484
|
+
+ `mv "${probe.backupPath ?? "<home>.backup-*"}" "${home}". Do NOT resume writes until it is `
|
|
485
|
+
+ `restored; inspect "${switchProgressPath(home)}" for the interrupted phase, then re-run "yui update".`,
|
|
486
|
+
version: staged.version,
|
|
487
|
+
schemaCurrent: probe.schemaCurrent,
|
|
488
|
+
switched: false,
|
|
489
|
+
...(probe.backupPath === undefined ? {} : { storageBackupPath: probe.backupPath })
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (probe.switched) {
|
|
493
|
+
// The switch provably committed; the binary was not promoted.
|
|
494
|
+
return {
|
|
495
|
+
outcome: "ambiguous",
|
|
496
|
+
phase: "activate-storage",
|
|
497
|
+
message: `Storage was switched (a completion receipt is present) but the activation process `
|
|
498
|
+
+ `did not confirm success (${detail}). The new binary was NOT promoted.`,
|
|
499
|
+
action: `Verify the migrated Home before resuming writes: run "yui doctor"`
|
|
500
|
+
+ `${probe.backupPath === undefined ? "" : ` (its timestamped backup is ${probe.backupPath})`}. `
|
|
501
|
+
+ `Inspect "${upgradeReceiptPath(home)}" and "${switchProgressPath(home)}" before deciding. `
|
|
502
|
+
+ `If it is healthy, finish by re-running "yui update" to promote the binary; if not, restore `
|
|
503
|
+
+ `the backup with mv "${probe.backupPath ?? "<home>.backup-*"}" "${home}". Do NOT resume `
|
|
504
|
+
+ `writes with the old binary against the migrated Home.`,
|
|
505
|
+
version: staged.version,
|
|
506
|
+
schemaCurrent: probe.schemaCurrent,
|
|
507
|
+
switched: true,
|
|
508
|
+
...(probe.backupPath === undefined ? {} : { storageBackupPath: probe.backupPath })
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// No receipt: the switch most likely never committed, but stdout was lost so we
|
|
512
|
+
// cannot assert "unchanged". Require an explicit, verified re-run.
|
|
513
|
+
return {
|
|
514
|
+
outcome: "ambiguous",
|
|
515
|
+
phase: "activate-storage",
|
|
516
|
+
message: `Storage activation did not confirm a result (${detail}) and no completion receipt was found`
|
|
517
|
+
+ `${probe.schemaCurrent ? "; the Home currently loads at the current schema" : ""}.`,
|
|
518
|
+
action: `The switch most likely did not commit, but this was not confirmed. Verify with "yui doctor" `
|
|
519
|
+
+ `and check for any "${home}.backup-*" sibling, "${upgradeReceiptPath(home)}", and `
|
|
520
|
+
+ `"${switchProgressPath(home)}" before retrying; then re-run "yui update". Do `
|
|
521
|
+
+ `NOT assume the update completed.`,
|
|
522
|
+
version: staged.version,
|
|
523
|
+
schemaCurrent: probe.schemaCurrent,
|
|
524
|
+
switched: false
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function messageOf(error) {
|
|
528
|
+
return error instanceof Error ? error.message : String(error);
|
|
529
|
+
}
|
|
530
|
+
function isValidBackupPath(value) {
|
|
531
|
+
return typeof value === "string"
|
|
532
|
+
&& value.length > 0
|
|
533
|
+
&& value.trim() === value
|
|
534
|
+
&& !value.includes("\0")
|
|
535
|
+
&& isAbsolute(value);
|
|
536
|
+
}
|
|
537
|
+
function isRecord(value) {
|
|
538
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
539
|
+
}
|