@zq-silk/yui 0.6.0 → 0.6.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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -35,7 +35,7 @@
35
35
  */
36
36
  import { spawn } from "node:child_process";
37
37
  import { existsSync, readFileSync, readdirSync } from "node:fs";
38
- import { join } from "node:path";
38
+ import { dirname, join } from "node:path";
39
39
  import { describeReport, runMigration } from "../migration/index.js";
40
40
  import { validateCompatibleFileTaskStore } from "../compatibleTaskStore.js";
41
41
  import { stopFileTaskController, ensureFileTaskController, ensureFileTaskControllerIdentity } from "../../controller/clientRuntime.js";
@@ -47,8 +47,11 @@ import { withUpgradeCoordinationLock } from "../upgradeCoordination.js";
47
47
  import { clearUpgradeReceipt, writeUpgradeReceipt, upgradeReceiptPath } from "./upgradeReceipt.js";
48
48
  import { switchProgressPath } from "./switchProgress.js";
49
49
  import { classifyHome } from "./homeClassification.js";
50
- import { createHomeMigrationTarget, describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime } from "./homeMigrationTarget.js";
50
+ import { createHomeMigrationTarget, describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState } from "./homeMigrationTarget.js";
51
+ import { repairPseudoLayout7 } from "./pseudoLayoutRepair.js";
51
52
  import { createSqliteMigrationTarget } from "./sqliteMigrationTarget.js";
53
+ import { createSqliteRecordMigrationTarget } from "./sqliteRecordMigrationTarget.js";
54
+ import { COMMITTED_DATABASE_FILENAME } from "./sqliteStateMigration.js";
52
55
  import { inspectOfflineUpgradeInventory } from "./offlineUpgradeInventory.js";
53
56
  /**
54
57
  * Run the storage upgrade for one Home. Never throws for an expected blocker;
@@ -92,6 +95,64 @@ export async function runStorageUpgrade(options) {
92
95
  action: blocker.action
93
96
  }, classification);
94
97
  }
98
+ // A pseudo-layout-7 Home (manifest 7, no yui.db, readable state.json) needs
99
+ // the deterministic staged state.json→SQLite repair, not the version
100
+ // migration engine (Issue 01). The update preflight reports it as
101
+ // migration-required so a parent update can stop the old Controller first;
102
+ // dry-run stages and verifies without promoting; execute runs the full
103
+ // fenced repair with Controller lifecycle management.
104
+ if (verdict === "NEEDS_STORAGE_REPAIR") {
105
+ if (mode === "update-preflight") {
106
+ return {
107
+ outcome: "update-preflight",
108
+ status: "migration-required",
109
+ stepCount: 1,
110
+ classification
111
+ };
112
+ }
113
+ if (mode === "dry-run") {
114
+ const repairResult = repairPseudoLayout7({
115
+ home,
116
+ latest,
117
+ mode: "dry-run",
118
+ ...(options.now === undefined ? {} : { now: options.now })
119
+ });
120
+ if (repairResult.outcome === "blocked") {
121
+ return withClassification({
122
+ outcome: "blocked",
123
+ stage: repairResult.stage,
124
+ message: repairResult.message,
125
+ action: repairResult.action
126
+ }, classification);
127
+ }
128
+ const inspected = inspectSourceVersionState(home, latest);
129
+ const source = "corruption" in inspected ? latest : inspected.source;
130
+ return {
131
+ outcome: "dry-run",
132
+ classification,
133
+ report: {
134
+ outcome: "dry-run",
135
+ mode: "dry-run",
136
+ source,
137
+ target: latest,
138
+ steps: [],
139
+ effects: [],
140
+ derived: { rebuiltEffects: [] },
141
+ validation: {
142
+ checks: [
143
+ {
144
+ name: "pseudo-layout-7 repair verification",
145
+ outcome: "passed",
146
+ detail: `verified ${repairResult.verifiedFamilies} record families against an `
147
+ + "independent state.json re-read; staged database discarded"
148
+ }
149
+ ]
150
+ }
151
+ }
152
+ };
153
+ }
154
+ return executePseudoLayout7Repair(options, classification, now);
155
+ }
95
156
  // An all-compatible chain is an online-load contract, not a migration plan.
96
157
  // Validate the declared old source shape and its in-memory normalization before
97
158
  // ANY mode reports compatible, including the internal update preflight. This is
@@ -162,20 +223,36 @@ export async function runStorageUpgrade(options) {
162
223
  return withClassification(offlineInventoryBlocker(inventory, true), classification);
163
224
  }
164
225
  }
165
- // Select the migration target. A layout-6 Home migrates to the SQLite
166
- // control-plane layout (7) via the staged state.json→SQLite target; all
167
- // other plans (record-only on layout 7) use the file-document target.
168
- const usesSqliteTarget = classification.layoutVersion === 6 && latest.layout === 7;
169
- const target = usesSqliteTarget
226
+ // Select the migration target.
227
+ // - A layout-6 Home migrates to the SQLite control-plane layout (7) via the
228
+ // staged state.json→SQLite target.
229
+ // - A layout-7 Home whose authoritative store is `yui.db` migrates
230
+ // record/aggregate versions via the SQLite record target (state.json may
231
+ // have been archived by the pseudo-layout-7 repair).
232
+ // - A layout-6 Home with record-only migrations (no layout step) uses the
233
+ // file-document target.
234
+ const usesSqliteLayoutTarget = classification.layoutVersion === 6 && latest.layout === 7;
235
+ const usesSqliteRecordTarget = classification.layoutVersion === latest.layout
236
+ && latest.layout >= 7;
237
+ const target = usesSqliteLayoutTarget
170
238
  ? createSqliteMigrationTarget({ home, latest, registry, now, callerPid })
171
- : createHomeMigrationTarget({
172
- home,
173
- latest,
174
- now,
175
- callerPid,
176
- ...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl }),
177
- ...(options.switchFaultHook === undefined ? {} : { switchFaultHook: options.switchFaultHook })
178
- });
239
+ : usesSqliteRecordTarget
240
+ ? createSqliteRecordMigrationTarget({
241
+ home,
242
+ latest,
243
+ registry,
244
+ now,
245
+ callerPid,
246
+ ...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl })
247
+ })
248
+ : createHomeMigrationTarget({
249
+ home,
250
+ latest,
251
+ now,
252
+ callerPid,
253
+ ...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl }),
254
+ ...(options.switchFaultHook === undefined ? {} : { switchFaultHook: options.switchFaultHook })
255
+ });
179
256
  // 2) A USABLE (already-current) Home has nothing to migrate; the engine
180
257
  // confirms with a no-op and we never fence, drain, or switch.
181
258
  if (verdict === "USABLE") {
@@ -499,6 +576,375 @@ async function execute(options, classification, target, callerPid, now) {
499
576
  }
500
577
  return result;
501
578
  }
579
+ /**
580
+ * Execute the pseudo-layout-7 repair with Controller lifecycle management
581
+ * (Issue 01). This mirrors the lifecycle of {@link execute} — fence, quiesce,
582
+ * repair, post-verify, restart — but runs the dedicated staged state.json→SQLite
583
+ * repair instead of the version-migration engine.
584
+ */
585
+ async function executePseudoLayout7Repair(options, classification, now) {
586
+ const { home, latest } = options;
587
+ const callerPid = options.callerPid ?? process.pid;
588
+ let releaseFence;
589
+ try {
590
+ releaseFence = placeUpgradeFence(home, {
591
+ reason: "storage repair in progress",
592
+ createdAt: now().toISOString(),
593
+ ownerPid: callerPid
594
+ });
595
+ }
596
+ catch (error) {
597
+ if (!(error instanceof UpgradeFenceError))
598
+ throw error;
599
+ return withClassification({
600
+ outcome: "blocked",
601
+ stage: "coordination",
602
+ message: `Repair coordination could not be acquired: ${error.message}`,
603
+ action: "Another upgrade is already coordinating this Home. Wait for it to finish, then retry."
604
+ }, classification);
605
+ }
606
+ const externallyQuiesced = options.controllerLifecycle === "externally-quiesced";
607
+ let controllerWasRunning = false;
608
+ let controllerStopConfirmed = false;
609
+ let controllerIdentity;
610
+ let result;
611
+ let unexpected;
612
+ const switchState = { committed: false };
613
+ try {
614
+ if (!externallyQuiesced) {
615
+ const controllerStatus = options.controllerStatus
616
+ ?? ((h) => defaultControllerStatus(h, options.controllerOptions));
617
+ let status;
618
+ try {
619
+ status = await controllerStatus(home);
620
+ }
621
+ catch (error) {
622
+ result = withClassification(controllerLifecycleBlocker("Controller status could not be verified", error), classification);
623
+ }
624
+ if (result === undefined && !isControllerLifecycleStatus(status)) {
625
+ result = withClassification(controllerLifecycleBlocker("Controller status was malformed", new Error("expected a boolean running field")), classification);
626
+ }
627
+ if (result === undefined && status.running && !isControllerLaunchIdentity(status.identity)) {
628
+ result = withClassification(controllerLifecycleBlocker("Controller launch identity could not be authenticated", new Error("executable/argv/version identity is unavailable")), classification);
629
+ }
630
+ if (result === undefined && status.running && !isPositivePid(status.pid)) {
631
+ result = withClassification(controllerLifecycleBlocker("Controller PID could not be authenticated", new Error("a positive status PID is unavailable for fenced stop")), classification);
632
+ }
633
+ if (result === undefined) {
634
+ controllerWasRunning = status.running;
635
+ controllerIdentity = status.running ? status.identity : undefined;
636
+ controllerStopConfirmed = !controllerWasRunning;
637
+ }
638
+ const stopController = options.stopController
639
+ ?? ((h, expectedPid) => defaultStopController(h, expectedPid, options.controllerOptions));
640
+ if (result === undefined && controllerWasRunning) {
641
+ try {
642
+ const expectedPid = status.pid;
643
+ const stopped = await stopController(home, expectedPid);
644
+ if (!confirmedControllerStopped(stopped, expectedPid)) {
645
+ result = withClassification(controllerLifecycleBlocker("Controller stop did not confirm a drained process", new Error(`stop did not confirm captured PID ${expectedPid} with stopped:true`)), classification);
646
+ }
647
+ else {
648
+ controllerStopConfirmed = true;
649
+ }
650
+ }
651
+ catch (error) {
652
+ result = withClassification(controllerLifecycleBlocker("Controller stop/drain failed", error), classification);
653
+ }
654
+ }
655
+ }
656
+ else {
657
+ controllerStopConfirmed = true;
658
+ }
659
+ if (result === undefined) {
660
+ try {
661
+ repinRevision(home);
662
+ }
663
+ catch (error) {
664
+ result = withClassification({
665
+ outcome: "blocked",
666
+ stage: "active-runtime",
667
+ message: `The pre-fence writer window is undeterminable and could not be closed safely: ` +
668
+ `${messageOf(error)}.`,
669
+ action: "The Home was not repaired. Inspect the exact storage-lock owner and retry only " +
670
+ "after it is settled."
671
+ }, classification);
672
+ }
673
+ }
674
+ if (result === undefined) {
675
+ const inventory = await readOfflineInventory(options, home);
676
+ if (inventory.total > 0) {
677
+ result = withClassification(offlineInventoryBlocker(inventory, false), classification);
678
+ }
679
+ }
680
+ if (result === undefined) {
681
+ try {
682
+ result = executePseudoLayout7RepairFenced(options, classification, now, switchState);
683
+ }
684
+ catch (error) {
685
+ if (switchState.committed) {
686
+ result = withClassification(postSwitchAmbiguity(home, switchState.backupPath, error), classification);
687
+ }
688
+ else {
689
+ unexpected = error;
690
+ }
691
+ }
692
+ }
693
+ }
694
+ finally {
695
+ try {
696
+ releaseFence();
697
+ }
698
+ catch (error) {
699
+ if (switchState.committed && result === undefined) {
700
+ result = withClassification(postSwitchAmbiguity(home, switchState.backupPath, error), classification);
701
+ }
702
+ else if (unexpected === undefined) {
703
+ unexpected = error;
704
+ }
705
+ }
706
+ }
707
+ if (unexpected !== undefined) {
708
+ if (switchState.committed) {
709
+ return withClassification(postSwitchAmbiguity(home, switchState.backupPath, unexpected), classification);
710
+ }
711
+ if (controllerWasRunning && controllerStopConfirmed) {
712
+ await restoreController(home, options, unexpected, controllerIdentity);
713
+ }
714
+ throw unexpected;
715
+ }
716
+ if (result === undefined) {
717
+ throw new Error("Storage repair did not produce a result.");
718
+ }
719
+ if (result.outcome === "upgraded") {
720
+ const startController = options.startController
721
+ ?? ((h) => defaultStartController(h, options.controllerOptions));
722
+ if (!externallyQuiesced && controllerWasRunning) {
723
+ try {
724
+ await startController(home);
725
+ }
726
+ catch (error) {
727
+ return {
728
+ ...withClassification(postSwitchAmbiguity(home, result.backupPath, new Error(`The replacement Controller could not start after the committed repair: ${messageOf(error)}`)), classification),
729
+ report: result.report
730
+ };
731
+ }
732
+ }
733
+ try {
734
+ options.postSwitchFaultHook?.("receipt-clear");
735
+ clearUpgradeReceipt(home);
736
+ }
737
+ catch (error) {
738
+ return {
739
+ ...withClassification(postSwitchAmbiguity(home, result.backupPath, error), classification),
740
+ report: result.report
741
+ };
742
+ }
743
+ return result;
744
+ }
745
+ if (!externallyQuiesced
746
+ && controllerWasRunning
747
+ && controllerStopConfirmed
748
+ && result.outcome === "blocked"
749
+ && result.stage !== "post-verify"
750
+ && result.stage !== "switch-ambiguous") {
751
+ try {
752
+ await restoreController(home, options, new Error(result.message), controllerIdentity);
753
+ }
754
+ catch (error) {
755
+ return {
756
+ ...withClassification({
757
+ outcome: "blocked",
758
+ stage: "active-runtime",
759
+ message: `${result.message} The previously running Controller could not be restored: ` +
760
+ `${messageOf(error)}.`,
761
+ action: "Keep the old Home quiesced and resolve the Controller startup failure; " +
762
+ "do not resume writes until the Controller and Home are verified."
763
+ }, classification),
764
+ ...(result.report === undefined ? {} : { report: result.report })
765
+ };
766
+ }
767
+ }
768
+ return result;
769
+ }
770
+ /** Run the staged state.json→SQLite repair inside the upgrade fence. */
771
+ function executePseudoLayout7RepairFenced(options, classification, now, switchState) {
772
+ const { home, latest } = options;
773
+ const repairResult = repairPseudoLayout7({
774
+ home,
775
+ latest,
776
+ mode: "execute",
777
+ ...(options.now === undefined ? {} : { now: options.now })
778
+ });
779
+ if (repairResult.outcome === "blocked") {
780
+ return withClassification({
781
+ outcome: "blocked",
782
+ stage: repairResult.stage,
783
+ message: repairResult.message,
784
+ action: repairResult.action
785
+ }, classification);
786
+ }
787
+ if (repairResult.outcome !== "repaired") {
788
+ // Execute mode never returns the dry-run variant; fail closed if it does.
789
+ return withClassification({
790
+ outcome: "blocked",
791
+ stage: "validate",
792
+ message: `Unexpected repair outcome: ${repairResult.outcome}`,
793
+ action: "Retry the repair; the Home was not changed."
794
+ }, classification);
795
+ }
796
+ // The database was promoted (atomic rename committed). From this point the
797
+ // old Home is no longer authoritative; a fault is a post-switch ambiguity.
798
+ switchState.committed = true;
799
+ switchState.backupPath = repairResult.stateBackupPath;
800
+ // Post-switch health check: open the promoted database through a fresh
801
+ // SqliteTaskStore and verify the core families (the repair already did a
802
+ // read-back, but this is the orchestrator's independent verification).
803
+ const postVerify = postSwitchHealthCheck(home);
804
+ if (postVerify !== null) {
805
+ return withClassification(postSwitchAmbiguity(home, repairResult.stateBackupPath, new Error(postVerify.message)), classification);
806
+ }
807
+ // Multi-phase (Issue 01 cross-issue handoff): the repair promoted yui.db but
808
+ // the database may still carry older record/aggregate versions. Re-classify
809
+ // the Home and, when MIGRATABLE, run the record-family migration in the same
810
+ // fenced window so one upgrade attempt reaches the current version. The
811
+ // Controller is already stopped and the fence is held, so no writer can
812
+ // observe the intermediate state.
813
+ const postRepairClassification = classifyHome({
814
+ home,
815
+ registry: options.registry,
816
+ latest
817
+ });
818
+ if (postRepairClassification.classification.verdict === "MIGRATABLE") {
819
+ const callerPid = options.callerPid ?? process.pid;
820
+ const target = createSqliteRecordMigrationTarget({
821
+ home,
822
+ latest,
823
+ registry: options.registry,
824
+ now,
825
+ callerPid,
826
+ ...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl })
827
+ });
828
+ const report = runMigration({
829
+ registry: options.registry,
830
+ target,
831
+ latest,
832
+ mode: "execute"
833
+ });
834
+ if (report.outcome === "failed") {
835
+ target.discardFreshOutput();
836
+ return {
837
+ ...blockedFromFailedReport(report, postRepairClassification),
838
+ report
839
+ };
840
+ }
841
+ if (report.outcome === "switch-ambiguous") {
842
+ return {
843
+ ...blockedFromSwitchAmbiguous(report, postRepairClassification),
844
+ report
845
+ };
846
+ }
847
+ if (report.outcome !== "migrated") {
848
+ target.discardFreshOutput();
849
+ return {
850
+ ...blockedFromEngineReport(report, postRepairClassification),
851
+ report
852
+ };
853
+ }
854
+ // The record migration committed its own atomic switch. Verify the fully
855
+ // migrated database before writing the completion receipt.
856
+ switchState.backupPath = report.switch.backupPath ?? repairResult.stateBackupPath;
857
+ const postRecordVerify = postSwitchHealthCheck(home);
858
+ if (postRecordVerify !== null) {
859
+ return withClassification(postSwitchAmbiguity(home, report.switch.backupPath, new Error(postRecordVerify.message)), postRepairClassification);
860
+ }
861
+ options.postSwitchFaultHook?.("receipt-write");
862
+ writeUpgradeReceipt(home, {
863
+ switched: true,
864
+ homePath: home,
865
+ completedAt: now().toISOString(),
866
+ targetLayoutVersion: latest.layout,
867
+ targetAggregateVersion: latest.aggregate,
868
+ ...(report.switch.backupPath === undefined
869
+ ? {}
870
+ : { backupPath: report.switch.backupPath })
871
+ });
872
+ return {
873
+ outcome: "upgraded",
874
+ classification: postRepairClassification,
875
+ ...(report.switch.backupPath === undefined
876
+ ? {}
877
+ : { backupPath: report.switch.backupPath }),
878
+ report
879
+ };
880
+ }
881
+ // Fail closed: the repair promoted a healthy yui.db, but the pure version
882
+ // classifier still fences this Home (a future/pre-baseline record axis, or
883
+ // structural damage). Reporting "upgraded" would hide a blocker the old
884
+ // pure-classifier route surfaced before the physical-backend check moved
885
+ // ahead of it; the Controller's strict schema gate would then fail at
886
+ // startup with a less precise diagnosis. The repaired database is healthy,
887
+ // so a newer build (or a restored backup) can still proceed from here.
888
+ const postRepairVerdict = postRepairClassification.classification.verdict;
889
+ if (postRepairVerdict === "NEEDS_NEW_VERSION"
890
+ || postRepairVerdict === "CORRUPTED") {
891
+ const detail = postRepairVerdict === "CORRUPTED"
892
+ ? postRepairClassification.classification.detail
893
+ : postRepairClassification.classification.blocker.message;
894
+ return withClassification({
895
+ outcome: "blocked",
896
+ stage: postRepairVerdict === "CORRUPTED" ? "post-verify" : "missing-step",
897
+ message: postRepairVerdict === "CORRUPTED"
898
+ ? `The repaired database is structurally damaged: ${detail}`
899
+ : `The repaired database still cannot be migrated by this build: ${detail}`,
900
+ action: postRepairVerdict === "CORRUPTED"
901
+ ? "Restore the Home from a backup; do not start a Controller against the repaired database."
902
+ : "Install a newer Yui release and re-run `yui upgrade`; the repaired database is healthy and ready for the record migration."
903
+ }, postRepairClassification);
904
+ }
905
+ // Write the temporary upgrade receipt (for the update flow's ambiguity
906
+ // window); it is cleared by the caller after the Controller restarts.
907
+ options.postSwitchFaultHook?.("receipt-write");
908
+ writeUpgradeReceipt(home, {
909
+ switched: true,
910
+ homePath: home,
911
+ completedAt: now().toISOString(),
912
+ targetLayoutVersion: latest.layout,
913
+ targetAggregateVersion: latest.aggregate,
914
+ backupPath: repairResult.stateBackupPath
915
+ });
916
+ const inspected = inspectSourceVersionState(home, latest);
917
+ const source = "corruption" in inspected ? latest : inspected.source;
918
+ return {
919
+ outcome: "upgraded",
920
+ classification,
921
+ backupPath: repairResult.stateBackupPath,
922
+ report: {
923
+ outcome: "migrated",
924
+ mode: "execute",
925
+ source,
926
+ target: latest,
927
+ steps: [],
928
+ effects: [],
929
+ derived: { rebuiltEffects: [] },
930
+ validation: {
931
+ checks: [
932
+ {
933
+ name: "SQLite staged-database checksum verification",
934
+ outcome: "passed",
935
+ detail: `verified ${repairResult.verifiedFamilies} record families against an independent state.json re-read`
936
+ }
937
+ ]
938
+ },
939
+ switch: {
940
+ status: "switched",
941
+ backupPath: repairResult.stateBackupPath,
942
+ detail: `SQLite database promoted and state.json backed up to ${repairResult.stateBackupPath}.`
943
+ },
944
+ completedAt: now().toISOString()
945
+ }
946
+ };
947
+ }
502
948
  /** Execute the fenced, coordinated migration after Controller quiesce. */
503
949
  function executeFenced(options, classification, target, callerPid, now, switchState) {
504
950
  const { home, registry, latest } = options;
@@ -792,11 +1238,31 @@ function blockedFromSwitchAmbiguous(report, classification) {
792
1238
  stage: "switch-ambiguous",
793
1239
  message: `Storage switch is AMBIGUOUS and partially applied: ${report.error}`,
794
1240
  action: `Do NOT assume the Home is unchanged. The original Home is at ${report.backupPath}; `
795
- + `restore it to recover: mv "${report.backupPath}" "${report.homePath}". The interrupted `
1241
+ + `restore it to recover: ${switchAmbiguousRestoreCommand(report)}. The interrupted `
796
1242
  + `switch marker "${switchProgressPath(report.homePath)}" records this ambiguity; verify `
797
1243
  + `with "yui doctor" after restoring.`
798
1244
  }, classification);
799
1245
  }
1246
+ /**
1247
+ * The exact restore command for an ambiguous switch. A file-layout backup is a
1248
+ * sibling directory moved back over the Home; a SQLite backup is the committed
1249
+ * `yui.db` file moved aside *inside* the Home and must be renamed back onto the
1250
+ * database path, not onto the Home directory (which would nest the file).
1251
+ */
1252
+ function switchAmbiguousRestoreCommand(report) {
1253
+ const committedDbPath = join(report.homePath, COMMITTED_DATABASE_FILENAME);
1254
+ // The degenerate SQLite case: no prior database existed, so there is no
1255
+ // backup to move back; the promoted database is already in place and the
1256
+ // recovery is to advance schema.json (or restore externally).
1257
+ if (report.backupPath === committedDbPath) {
1258
+ return `the database is already at ${committedDbPath}; advance schema.json `
1259
+ + "record-family versions or restore from an external backup";
1260
+ }
1261
+ const restoreTarget = dirname(report.backupPath) === report.homePath
1262
+ ? committedDbPath
1263
+ : report.homePath;
1264
+ return `mv "${report.backupPath}" "${restoreTarget}"`;
1265
+ }
800
1266
  /**
801
1267
  * A structured blocker for failures after the atomic Home switch. The receipt
802
1268
  * and progress marker are named explicitly so an operator can reconcile the