@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.
Files changed (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. 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
+ }