@zq-silk/yui 0.16.1 → 1.0.0-alpha

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 (193) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +9 -4
  4. package/dist/agent/agent.js +4 -9
  5. package/dist/agent/executionComponents.js +4 -4
  6. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  7. package/dist/agentRun/agentRun.js +13 -23
  8. package/dist/artifacts/managedGit.js +3 -56
  9. package/dist/brief/taskBrief.js +3 -3
  10. package/dist/cli/agentConfigurationPicker.js +24 -21
  11. package/dist/cli/commandCatalog.js +18 -10
  12. package/dist/cli/roleWizard.js +41 -30
  13. package/dist/cli/updateCommand.js +35 -13
  14. package/dist/cli/updateOrchestrator.js +73 -44
  15. package/dist/cli/updatePorts.js +76 -86
  16. package/dist/cli/upgradeCommand.js +4 -8
  17. package/dist/cli.js +39 -33
  18. package/dist/commands/controllerCommands.js +2 -2
  19. package/dist/commands/executionAuditCommands.js +2 -2
  20. package/dist/commands/projectCommands.js +1 -1
  21. package/dist/commands/releaseCommands.js +9 -38
  22. package/dist/commands/taskCommands.js +91 -81
  23. package/dist/commands/taskCompletionGate.js +0 -117
  24. package/dist/commands/taskRoleRuntimeStatus.js +2 -2
  25. package/dist/commands/taskUpstreamCommands.js +93 -54
  26. package/dist/context/runContextPack.js +30 -54
  27. package/dist/context/runInputContract.js +9 -0
  28. package/dist/context/sessionBootstrapManifest.js +11 -19
  29. package/dist/controller/agentHostObservation.js +4 -2
  30. package/dist/controller/agentRuntimeObserver.js +4 -4
  31. package/dist/controller/clientRuntime.js +19 -44
  32. package/dist/controller/controller.js +24 -10
  33. package/dist/controller/fileSchedulerStoreAdapter.js +91 -24
  34. package/dist/controller/globalInputDelivery.js +31 -8
  35. package/dist/controller/globalRuntimeAttention.js +34 -0
  36. package/dist/controller/jobSupervisor.js +3 -3
  37. package/dist/controller/operatorNotification.js +31 -0
  38. package/dist/controller/providerRetryAdmission.js +3 -1
  39. package/dist/controller/providerRetryDelivery.js +136 -119
  40. package/dist/controller/runtime.js +2 -28
  41. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  42. package/dist/controller/structuredProviderObservation.js +28 -1
  43. package/dist/controller/updateReconciliation.js +72 -20
  44. package/dist/coordination/workMailbox.js +4 -4
  45. package/dist/core/controllerIdentity.js +25 -0
  46. package/dist/core/controllerProcessIdentity.js +2 -2
  47. package/dist/core/controllerServer.js +4 -2
  48. package/dist/core/protocol.js +1 -1
  49. package/dist/doctor/doctor.js +2 -1
  50. package/dist/domain/validation.js +6 -0
  51. package/dist/event/taskEvent.js +3 -3
  52. package/dist/execution/workItemExecution.js +2 -2
  53. package/dist/executor/agentAdapter.js +28 -96
  54. package/dist/executor/agentConfigurationCatalog.js +13 -68
  55. package/dist/executor/agentConfigurationFields.js +120 -0
  56. package/dist/executor/agentConfigurationProbe.js +55 -80
  57. package/dist/executor/agentExecutor.js +11 -8
  58. package/dist/executor/effectiveLaunch.js +3 -3
  59. package/dist/executor/executorRegistry.js +0 -9
  60. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  61. package/dist/grant/capabilityGrant.js +2 -2
  62. package/dist/input/inputRequest.js +4 -4
  63. package/dist/integration/changeSet.js +3 -3
  64. package/dist/integration/integrationAttempt.js +3 -3
  65. package/dist/integration/integrationSourceApplication.js +1 -1
  66. package/dist/interaction/operatorPresentation.js +2 -1
  67. package/dist/job/durableJob.js +1 -1
  68. package/dist/job/jobRunner.js +2 -2
  69. package/dist/message/message.js +8 -6
  70. package/dist/message/messageContinuation.js +8 -9
  71. package/dist/milestone/milestone.js +3 -3
  72. package/dist/observability/executionAudit.js +1 -10
  73. package/dist/observability/runtimeIdentity.js +0 -23
  74. package/dist/output/agentConfigurationPresentation.js +4 -2
  75. package/dist/profile/agentProfile.js +3 -3
  76. package/dist/release/releaseHandover.js +2 -2
  77. package/dist/release/releaseIdempotencyStore.js +0 -23
  78. package/dist/release/releaseWorkflowPorts.js +8 -4
  79. package/dist/release/runtimeRelease.js +9 -7
  80. package/dist/repository/gitWorkspace.js +66 -19
  81. package/dist/repository/project.js +3 -3
  82. package/dist/repository/taskWorkspacePreparer.js +75 -68
  83. package/dist/resources/liveReferences.js +1 -1
  84. package/dist/resources/resourceRegistry.js +90 -44
  85. package/dist/resources/sqliteResourceRegistry.js +5 -7
  86. package/dist/review/reviewRound.js +5 -5
  87. package/dist/role/role.js +7 -8
  88. package/dist/runtime/acpProtocol.js +2 -3
  89. package/dist/runtime/acpSession.js +21 -5
  90. package/dist/runtime/agentDriverObservation.js +1 -1
  91. package/dist/runtime/agentEndpoint.js +4 -1
  92. package/dist/runtime/agentHost.js +25 -65
  93. package/dist/runtime/agentHostCleanup.js +85 -0
  94. package/dist/runtime/agentHostProtocol.js +3 -3
  95. package/dist/runtime/builtinAgentDrivers.js +46 -119
  96. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  97. package/dist/runtime/codexAppServerRuntime.js +55 -89
  98. package/dist/runtime/codexInteractiveHost.js +1 -1
  99. package/dist/runtime/jsonLineChannel.js +35 -7
  100. package/dist/runtime/launchBroker.js +1 -1
  101. package/dist/runtime/processExitObservation.js +1 -1
  102. package/dist/runtime/providerContinuationReconciliationService.js +86 -38
  103. package/dist/runtime/providerRetry.js +15 -0
  104. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  105. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  106. package/dist/runtime/runtimeCoherence.js +7 -3
  107. package/dist/runtime/runtimeObservation.js +3 -3
  108. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  109. package/dist/runtime/structuredProviderHost.js +106 -81
  110. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  111. package/dist/runtime/tmuxAdapters.js +3 -3
  112. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  113. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  114. package/dist/scheduler/taskWake.js +1 -1
  115. package/dist/storage/baselineSchema.js +606 -0
  116. package/dist/storage/homeLayout.js +5 -16
  117. package/dist/storage/recordValidation.js +16 -4
  118. package/dist/storage/sqliteSchema.js +106 -1741
  119. package/dist/storage/sqliteStore.js +16 -14
  120. package/dist/storage/storageSchema.js +8 -7
  121. package/dist/storage/storageVersions.js +23 -16
  122. package/dist/storage/taskStore.js +3 -29
  123. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -112
  124. package/dist/task/nextAction.js +33 -23
  125. package/dist/task/task.js +17 -8
  126. package/dist/task/taskActivation.js +5 -4
  127. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  128. package/dist/verification/gateArtifact.js +5 -3
  129. package/dist/verification/verificationPlan.js +6 -7
  130. package/dist/web/assets/assetManifest.js +2 -0
  131. package/dist/web/assets/client/app.js +72 -3
  132. package/dist/web/assets/client/components.js +2 -1
  133. package/dist/web/assets/client/i18n.js +2 -0
  134. package/dist/web/assets/client/taskSummary.js +345 -0
  135. package/dist/web/assets/client/taskSurface.js +44 -26
  136. package/dist/web/assets/client/view.js +33 -0
  137. package/dist/web/assets/shell.js +1 -0
  138. package/dist/web/assets/styles/cards.js +21 -0
  139. package/dist/web/webServer.js +39 -5
  140. package/dist/web/webSessions.js +165 -0
  141. package/dist/web/webSnapshot.js +23 -0
  142. package/dist/web/webTaskSurface.js +23 -0
  143. package/dist/workItem/workItem.js +6 -6
  144. package/dist/workspace/cleanupInspection.js +1 -9
  145. package/dist/worktree/managedWorkspace.js +3 -3
  146. package/docs/managed-turn-and-session-runtime.md +50 -7
  147. package/docs/managed-turn-and-session-runtime.zh-CN.md +36 -5
  148. package/docs/observability/README.md +47 -0
  149. package/docs/observability/README.zh-CN.md +37 -0
  150. package/docs/project-refresh.md +9 -0
  151. package/docs/project-refresh.zh-CN.md +8 -0
  152. package/docs/provider-retry.md +35 -0
  153. package/docs/release-workflow.md +70 -214
  154. package/docs/release-workflow.zh-CN.md +53 -157
  155. package/docs/roles-and-configuration.md +30 -0
  156. package/docs/roles-and-configuration.zh-CN.md +20 -0
  157. package/docs/sqlite-control-plane-design.md +48 -289
  158. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  159. package/docs/storage-baseline.md +132 -0
  160. package/docs/storage-baseline.zh-CN.md +106 -0
  161. package/docs/task-delivery.md +110 -2
  162. package/docs/task-delivery.zh-CN.md +82 -2
  163. package/docs/task-discovery.md +9 -0
  164. package/docs/task-discovery.zh-CN.md +6 -0
  165. package/docs/testing/verification-levels.md +42 -164
  166. package/docs/testing/verification-levels.zh-CN.md +29 -115
  167. package/i18n/README.zh-CN.md +7 -4
  168. package/package.json +1 -1
  169. package/skills/yui-leader/SKILL.md +11 -0
  170. package/skills/yui-leader/references/execution.md +22 -0
  171. package/skills/yui-leader/references/planning.md +6 -2
  172. package/skills/yui-operator/SKILL.md +45 -13
  173. package/skills/yui-operator/references/task-delivery.md +99 -0
  174. package/skills/yui-runtime/SKILL.md +5 -3
  175. package/skills/yui-runtime/references/publication.md +56 -5
  176. package/skills/yui-runtime/references/recovery.md +12 -2
  177. package/dist/runtime/agentHostCompatibility.js +0 -127
  178. package/dist/storage/migrations/agentFailureContext.js +0 -22
  179. package/dist/storage/migrations/agentRunContract.js +0 -159
  180. package/dist/storage/migrations/artifactsToGit.js +0 -338
  181. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  182. package/dist/storage/migrations/currentInputContract.js +0 -86
  183. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  184. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  185. package/dist/storage/migrations/integrationContinuation.js +0 -105
  186. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  187. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  188. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  189. package/dist/storage/migrations/submitIntent.js +0 -126
  190. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  191. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  192. package/dist/storage/migrations/verificationPolicy.js +0 -74
  193. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -2,7 +2,7 @@ import { renderTable } from "../output/table.js";
2
2
  import { displayExecutionComponent } from "../agent/executionComponents.js";
3
3
  import { isAgentAdapterId } from "../agent/adapterCatalog.js";
4
4
  import { defaultRoleAgentConfig } from "../executor/agentAdapter.js";
5
- import { selectAgentEffort, selectAgentModelAndEffort } from "./agentConfigurationPicker.js";
5
+ import { selectAgentEffort, selectAgentModelAndEffort, renderAgentConfigurationResolutionNotice } from "./agentConfigurationPicker.js";
6
6
  const TEXT_COLUMN = { header: "Selection", minWidth: 12, maxWidth: 34 };
7
7
  export async function resolveRoleWizardArguments(commandArgs, ports, io) {
8
8
  const args = [...commandArgs];
@@ -231,6 +231,8 @@ async function configureNewAgentField(args, agent, ports, io) {
231
231
  args: selection.effort === undefined ? args : [...args, "--effort", selection.effort]
232
232
  };
233
233
  }
234
+ if (resolved !== undefined)
235
+ io.write(renderAgentConfigurationResolutionNotice(resolved));
234
236
  if (selected.value === "permission-strategy") {
235
237
  const strategy = await promptAgentFieldValue(selected, io);
236
238
  if (strategy === undefined || strategy.length === 0)
@@ -407,6 +409,8 @@ async function updateAgentBindingSettings(args, role, binding, ports, io) {
407
409
  ]
408
410
  };
409
411
  }
412
+ if (resolved !== undefined)
413
+ io.write(renderAgentConfigurationResolutionNotice(resolved));
410
414
  if (selectedField.value === "permission-strategy") {
411
415
  const strategy = await promptAgentFieldValue(selectedField, io);
412
416
  if (strategy === undefined || strategy.length === 0)
@@ -446,23 +450,17 @@ function agentFields(binding, catalog) {
446
450
  return [
447
451
  agentField("model", "Model", config.model, "--model", ["--clear-model"]),
448
452
  agentField("effort", "Effort", config.effort, "--effort", ["--clear-effort"]),
449
- agentField("permission-strategy", "Permission strategy", permission.strategy, "--permission-strategy", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.strategy", ["default", "bypass", "configured"])),
453
+ agentField("permission-strategy", "Permission strategy", permission.strategy, "--permission-strategy", ["--permission-strategy", "default"], catalogField(catalog, "permission.strategy")),
450
454
  ...(binding.adapterId === "codex" && permission.strategy === "configured" ? [
451
- agentField("sandbox", "Sandbox", permission.sandbox, "--sandbox", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.sandbox", [
452
- "read-only", "workspace-write", "danger-full-access"
453
- ])),
454
- agentField("approval", "Approval", permission.approval, "--approval", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.approval", [
455
- "untrusted", "on-request", "never"
456
- ]))
455
+ agentField("sandbox", "Sandbox", permission.sandbox, "--sandbox", ["--permission-strategy", "default"], catalogField(catalog, "permission.sandbox")),
456
+ agentField("approval", "Approval", permission.approval, "--approval", ["--permission-strategy", "default"], catalogField(catalog, "permission.approval"))
457
457
  ] : []),
458
458
  ...(binding.adapterId === "codex" ? [
459
- agentField("search", "Web search", config.search, "--search", ["--clear-search"], [
460
- ...catalogChoices(catalog, "search", ["true"])
461
- ])
459
+ agentField("search", "Web search", config.search, "--search", ["--clear-search"], catalogField(catalog, "search"))
462
460
  ] : []),
463
461
  ...((binding.adapterId === "claude" || binding.adapterId === "acp")
464
462
  && permission.strategy === "configured" ? [
465
- agentField("permission-mode", "Permission mode", permission.mode, "--permission-mode", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.mode"))
463
+ agentField("permission-mode", "Permission mode", permission.mode, "--permission-mode", ["--permission-strategy", "default"], catalogField(catalog, "permission.mode"))
466
464
  ] : [])
467
465
  ];
468
466
  }
@@ -479,9 +477,8 @@ async function loadAgentCatalog(ports, binding, cwd) {
479
477
  ? value
480
478
  : undefined;
481
479
  }
482
- function catalogChoices(catalog, key, fallback = []) {
483
- const field = catalog?.fields.find((candidate) => candidate.key === key);
484
- return field === undefined ? [...fallback] : field.choices.map(({ value }) => value);
480
+ function catalogField(catalog, key) {
481
+ return catalog?.fields.find((candidate) => candidate.key === key);
485
482
  }
486
483
  function appendModelEffortPatch(args, selection, update) {
487
484
  return [
@@ -494,8 +491,19 @@ function appendModelEffortPatch(args, selection, update) {
494
491
  : ["--effort", selection.effort])
495
492
  ];
496
493
  }
497
- function agentField(value, label, current, set, clear, choices) {
498
- return { value, label, current: display(current), set, clear, ...(choices === undefined ? {} : { choices }) };
494
+ function agentField(value, label, current, set, clear, capability) {
495
+ const reported = capability?.available === false ? [] : capability?.choices.map(choice => choice.value) ?? [];
496
+ const retained = (typeof current === "string" || typeof current === "boolean")
497
+ && !reported.includes(String(current)) ? [String(current)] : [];
498
+ return {
499
+ value, label, current: display(current), set, clear,
500
+ choices: [...reported, ...retained], allowCustom: capability?.allowCustom === true,
501
+ reason: [
502
+ capability?.reason ?? (capability === undefined
503
+ ? "No catalog field was reported; no choices inferred." : ""),
504
+ ...(retained.length === 0 ? [] : ["Current value is retained but not reported by this catalog."])
505
+ ].filter(Boolean).join(" ")
506
+ };
499
507
  }
500
508
  async function configuredPermissionArgs(binding, catalog, strategy, io) {
501
509
  if (strategy !== "configured")
@@ -509,12 +517,8 @@ async function configuredPermissionArgs(binding, catalog, strategy, io) {
509
517
  return undefined;
510
518
  const option = field === "sandbox" ? "--sandbox" : "--approval";
511
519
  const value = await promptAgentFieldValue(agentField(field, field === "sandbox" ? "Sandbox" : "Approval", undefined, option, ["--permission-strategy", "default"], field === "sandbox"
512
- ? catalogChoices(catalog, "permission.sandbox", [
513
- "read-only", "workspace-write", "danger-full-access"
514
- ])
515
- : catalogChoices(catalog, "permission.approval", [
516
- "untrusted", "on-request", "never"
517
- ])), io);
520
+ ? catalogField(catalog, "permission.sandbox")
521
+ : catalogField(catalog, "permission.approval")), io);
518
522
  return value === undefined
519
523
  ? undefined
520
524
  : ["--permission-strategy", "configured", option, value];
@@ -523,9 +527,9 @@ async function configuredPermissionArgs(binding, catalog, strategy, io) {
523
527
  // An ACP Session's configured permission is one mode the Agent enumerated,
524
528
  // so there is nothing to choose between: ask for the mode directly rather
525
529
  // than offering tool rules the protocol has no place for. The choices come
526
- // from the capability catalog, so a Session that offers no modes presents
527
- // none instead of inviting a value the Agent would reject.
528
- const value = await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, "--permission-mode", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.mode")), io);
530
+ // from the catalog. An explicitly open field permits a custom mode before
531
+ // Session creation; it is checked against the native Session at launch.
532
+ const value = await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, "--permission-mode", ["--permission-strategy", "default"], catalogField(catalog, "permission.mode")), io);
529
533
  return value === undefined || value.length === 0
530
534
  ? undefined
531
535
  : ["--permission-strategy", "configured", "--permission-mode", value];
@@ -541,16 +545,23 @@ async function configuredPermissionArgs(binding, catalog, strategy, io) {
541
545
  ? "--permission-mode"
542
546
  : field === "allowed" ? "--allowed-tool" : "--disallowed-tool";
543
547
  const value = field === "mode"
544
- ? await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, option, ["--permission-strategy", "default"], catalogChoices(catalog, "permission.mode")), io)
548
+ ? await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, option, ["--permission-strategy", "default"], catalogField(catalog, "permission.mode")), io)
545
549
  : (await io.question(`${field === "allowed" ? "Allowed" : "Disallowed"} tool: `))?.trim();
546
550
  return value === undefined || value.length === 0
547
551
  ? undefined
548
552
  : ["--permission-strategy", "configured", option, value];
549
553
  }
550
554
  async function promptAgentFieldValue(field, io) {
551
- return field.choices === undefined
552
- ? (await io.question(`${field.label}: `))?.trim()
553
- : choose(`Set ${field.label}`, field.choices.map((choice) => ({ value: choice, cells: [choice] })), [TEXT_COLUMN], io, field.choices[0], "value");
555
+ if (field.reason)
556
+ io.write(`${field.label}: ${field.reason}\n`);
557
+ if (field.choices === undefined)
558
+ return (await io.question(`${field.label}: `))?.trim();
559
+ const custom = "\0yui:custom";
560
+ const selected = await choose(`Set ${field.label}`, [
561
+ ...field.choices.map(choice => ({ value: choice, cells: [choice] })),
562
+ ...(field.allowCustom ? [{ value: custom, cells: ["Custom value (native acceptance unverified)"] }] : [])
563
+ ], [TEXT_COLUMN], io, field.choices.includes(field.current) ? field.current : field.choices[0], "value");
564
+ return selected === custom ? (await io.question(`Custom ${field.label}: `))?.trim() : selected;
554
565
  }
555
566
  async function selectActiveAgent(role, ports, io) {
556
567
  const configured = await configuredAgents(ports);
@@ -1,6 +1,8 @@
1
1
  import { resolveYuiHome } from "../storage/taskStore.js";
2
2
  import { createUpdatePorts } from "./updatePorts.js";
3
3
  import { runUpdate } from "./updateOrchestrator.js";
4
+ import { isConcreteVersion } from "../domain/validation.js";
5
+ import { usageError } from "../errors/cliError.js";
4
6
  /**
5
7
  * Run `yui update` as a side-by-side, recoverable orchestration.
6
8
  *
@@ -11,15 +13,24 @@ import { runUpdate } from "./updateOrchestrator.js";
11
13
  *
12
14
  * Returns a process exit code: 0 on success or already-current, 5 on abort.
13
15
  */
14
- export function runUpdateCommand(environment = process.env, spawn, write = (text) => process.stdout.write(text), ports) {
16
+ export function runUpdateCommand(args = [], environment = process.env, spawn, write = (text) => process.stdout.write(text), ports) {
17
+ const version = parseUpdateVersion(args);
15
18
  const home = resolveYuiHome(environment);
16
19
  const resolvedPorts = ports ?? createUpdatePorts(environment, spawn);
17
- const result = runUpdate(resolvedPorts, { home });
20
+ const result = runUpdate(resolvedPorts, { home, version });
18
21
  write(`${renderUpdateResult(result)}\n`);
19
22
  if (result.outcome === "aborted")
20
23
  return 5;
21
24
  return 0;
22
25
  }
26
+ function parseUpdateVersion(args) {
27
+ if (args.length === 0)
28
+ return undefined;
29
+ if (args.length === 2 && args[0] === "--version" && isConcreteVersion(args[1])) {
30
+ return args[1].trim();
31
+ }
32
+ throw usageError("Update usage: yui update [--version <exact-version>]");
33
+ }
23
34
  /** Render an {@link UpdateResult} as concise, CLI-style text. */
24
35
  export function renderUpdateResult(result) {
25
36
  const rendered = (() => {
@@ -33,14 +44,18 @@ export function renderUpdateResult(result) {
33
44
  case "aborted":
34
45
  return [
35
46
  `Update aborted during ${result.phase}: ${result.message}`,
36
- result.phase === "migrate-storage"
37
- || (result.phase === "post-verify" && result.recoverable)
38
- ? "The target binary is installed; the Home remains quiesced pending successful verification."
39
- : result.recoverable
40
- ? "The current install and Home remain usable."
41
- : result.phase === "activate-binary"
42
- ? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
43
- : "Manual recovery is required (see below).",
47
+ result.controllerOwnershipUnknown === true || result.controllerRestore?.outcome === "unknown"
48
+ || result.controllerReconciliation?.attempts.some(attempt => attempt.outcome === "unknown")
49
+ || result.controllerReconciliation?.observationError !== undefined
50
+ ? "Controller resource effects are uncertain; do not assume the Home is quiesced or usable."
51
+ : result.phase === "migrate-storage"
52
+ || (result.phase === "post-verify" && result.recoverable)
53
+ ? "The target binary is installed; the Home remains quiesced pending successful verification."
54
+ : result.recoverable
55
+ ? "The current install and Home remain usable."
56
+ : result.phase === "activate-binary"
57
+ ? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
58
+ : "Manual recovery is required (see below).",
44
59
  ...(result.backupPath === undefined
45
60
  ? []
46
61
  : [`Storage backup: ${result.backupPath}`]),
@@ -48,7 +63,14 @@ export function renderUpdateResult(result) {
48
63
  ].join("\n");
49
64
  }
50
65
  })();
51
- return result.cleanupWarning === undefined
52
- ? rendered
53
- : `${rendered}\nWarning: ${result.cleanupWarning}`;
66
+ return [
67
+ rendered,
68
+ ...(result.controllerReconciliation === undefined ? [] : [
69
+ `Controller reconciliation: ${JSON.stringify(result.controllerReconciliation)}`
70
+ ]),
71
+ ...(result.controllerRestore === undefined ? [] : [
72
+ `Controller restore: ${JSON.stringify(result.controllerRestore)}`
73
+ ]),
74
+ ...(result.cleanupWarning === undefined ? [] : [`Warning: ${result.cleanupWarning}`])
75
+ ].join("\n");
54
76
  }
@@ -6,10 +6,12 @@
6
6
  * then activated, any required storage migration is applied, and the result is
7
7
  * verified before the Controller is restarted.
8
8
  */
9
+ import { UpdateControllerReconciliationError } from "../controller/updateReconciliation.js";
10
+ import { isControllerIdentity } from "../core/controllerIdentity.js";
9
11
  export function runUpdate(ports, options) {
10
12
  let staged;
11
13
  try {
12
- staged = ports.stage();
14
+ staged = ports.stage(options.version);
13
15
  }
14
16
  catch (error) {
15
17
  return {
@@ -33,7 +35,9 @@ export function runUpdate(ports, options) {
33
35
  cleanupWarning = `Staging cleanup could not be completed: ${messageOf(error)}`;
34
36
  }
35
37
  }
36
- return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
38
+ return cleanupWarning === undefined ? result : {
39
+ ...result, cleanupWarning: [result.cleanupWarning, cleanupWarning].filter(Boolean).join(" ")
40
+ };
37
41
  }
38
42
  function runStagedUpdate(ports, staged, home) {
39
43
  let preflight;
@@ -58,8 +62,6 @@ function runStagedUpdate(ports, staged, home) {
58
62
  action: preflight.action,
59
63
  recoverable: true,
60
64
  version: staged.version,
61
- ...(preflight.blockers === undefined ? {} : { blockers: preflight.blockers }),
62
- ...(preflight.retryCommand === undefined ? {} : { retryCommand: preflight.retryCommand }),
63
65
  ...(preflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
64
66
  };
65
67
  }
@@ -97,38 +99,69 @@ function runStagedUpdate(ports, staged, home) {
97
99
  version: staged.version
98
100
  };
99
101
  }
102
+ let result;
103
+ let cleanupWarning;
100
104
  try {
101
- const captured = captureControllerLifecycle(ports, staged.version, home);
102
- if ("outcome" in captured)
103
- return captured;
104
- // An older Controller can finish a launch between preflight and drain.
105
- // Recheck after its exact stop, before changing the install or storage.
106
- let fencedPreflight;
105
+ result = reconcileAndUpdate(ports, staged, home);
106
+ }
107
+ finally {
107
108
  try {
108
- fencedPreflight = ports.preflight(staged, home);
109
+ releaseHandover?.();
109
110
  }
110
111
  catch (error) {
111
- return restoreControllerOrReport(ports, home, captured.lifecycle, {
112
- outcome: "aborted", phase: "preflight",
113
- message: `Quiesced compatibility preflight failed: ${messageOf(error)}`,
114
- action: "The install and storage are unchanged; inspect the compatibility failure before retrying.",
115
- recoverable: true, version: staged.version
116
- });
112
+ cleanupWarning = `Controller handover lock release failed for ${home}: ${messageOf(error)}`;
117
113
  }
118
- if (fencedPreflight.status === "blocked") {
119
- return restoreControllerOrReport(ports, home, captured.lifecycle, {
120
- outcome: "aborted", phase: "preflight",
121
- message: fencedPreflight.message, action: fencedPreflight.action,
122
- recoverable: true, version: staged.version,
123
- ...(fencedPreflight.blockers === undefined ? {} : { blockers: fencedPreflight.blockers }),
124
- ...(fencedPreflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
125
- });
126
- }
127
- return activateAndVerify(ports, staged, home, captured.lifecycle, fencedPreflight);
128
114
  }
129
- finally {
130
- releaseHandover?.();
115
+ return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
116
+ }
117
+ function reconcileAndUpdate(ports, staged, home) {
118
+ let reconciliation;
119
+ try {
120
+ reconciliation = ports.reconcileController?.(home);
121
+ }
122
+ catch (error) {
123
+ const evidence = error instanceof UpdateControllerReconciliationError ? error.result : undefined;
124
+ const unknown = evidence === undefined || evidence.observationError !== undefined
125
+ || evidence.lockReleaseError !== undefined || evidence.attempts.some(attempt => attempt.outcome === "unknown");
126
+ return {
127
+ outcome: "aborted", phase: "coordination", version: staged.version,
128
+ message: `Controller reconciliation failed: ${messageOf(error)}`,
129
+ action: "Inspect the reported current-Home targets and partial effects; binary activation was not attempted.",
130
+ recoverable: !unknown,
131
+ ...(evidence === undefined ? {} : { controllerReconciliation: evidence }),
132
+ ...(unknown ? { controllerOwnershipUnknown: true } : {})
133
+ };
131
134
  }
135
+ const coordinated = runCoordinatedUpdate(ports, staged, home);
136
+ return reconciliation === undefined ? coordinated : { ...coordinated, controllerReconciliation: reconciliation };
137
+ }
138
+ function runCoordinatedUpdate(ports, staged, home) {
139
+ const captured = captureControllerLifecycle(ports, staged.version, home);
140
+ if ("outcome" in captured)
141
+ return captured;
142
+ // Storage can change before the Controller drains. Recheck after its
143
+ // exact stop, before changing the install or storage. Its sceneUnchanged
144
+ // verdict does not describe the preceding runtime cleanup/stop/restore.
145
+ let fencedPreflight;
146
+ try {
147
+ fencedPreflight = ports.preflight(staged, home);
148
+ }
149
+ catch (error) {
150
+ return restoreControllerOrReport(ports, home, captured.lifecycle, {
151
+ outcome: "aborted", phase: "preflight",
152
+ message: `Quiesced storage preflight failed: ${messageOf(error)}`,
153
+ action: "The install and storage are unchanged; inspect the storage failure before retrying.",
154
+ recoverable: true, version: staged.version
155
+ });
156
+ }
157
+ if (fencedPreflight.status === "blocked") {
158
+ return restoreControllerOrReport(ports, home, captured.lifecycle, {
159
+ outcome: "aborted", phase: "preflight",
160
+ message: fencedPreflight.message, action: fencedPreflight.action,
161
+ recoverable: true, version: staged.version
162
+ });
163
+ }
164
+ return activateAndVerify(ports, staged, home, captured.lifecycle, fencedPreflight);
132
165
  }
133
166
  function activateAndVerify(ports, staged, home, lifecycle, preflight) {
134
167
  try {
@@ -256,7 +289,8 @@ function captureControllerLifecycle(ports, version, home) {
256
289
  phase: "preflight",
257
290
  message: `Controller status could not be verified: ${messageOf(error)}`,
258
291
  action: "Inspect Controller ownership and retry after its status is known.",
259
- recoverable: true,
292
+ recoverable: false,
293
+ controllerOwnershipUnknown: true,
260
294
  version
261
295
  };
262
296
  }
@@ -279,7 +313,8 @@ function captureControllerLifecycle(ports, version, home) {
279
313
  phase: "preflight",
280
314
  message: `Controller stop/drain failed: ${messageOf(error)}`,
281
315
  action: "Inspect the captured Controller before retrying; binary activation was not attempted.",
282
- recoverable: true,
316
+ recoverable: false,
317
+ controllerOwnershipUnknown: true,
283
318
  version
284
319
  };
285
320
  }
@@ -300,7 +335,8 @@ function malformedControllerResult(version, message) {
300
335
  phase: "preflight",
301
336
  message,
302
337
  action: "Refusing an unfenced Controller handoff; inspect ownership and retry.",
303
- recoverable: true,
338
+ recoverable: false,
339
+ controllerOwnershipUnknown: true,
304
340
  version
305
341
  };
306
342
  }
@@ -309,14 +345,16 @@ function restoreControllerOrReport(ports, home, lifecycle, failure) {
309
345
  return failure;
310
346
  try {
311
347
  ports.restoreController(home, lifecycle.identity);
312
- return failure;
348
+ return { ...failure, controllerRestore: { identity: lifecycle.identity, outcome: "restored" } };
313
349
  }
314
350
  catch (error) {
315
351
  return {
316
352
  ...failure,
317
353
  message: `${failure.message} Captured Controller restore failed: ${messageOf(error)}.`,
318
- action: `${failure.action} Keep the Home quiesced and resolve the restore failure.`,
319
- recoverable: false
354
+ action: `${failure.action} Controller liveness after the failed restore is unknown; inspect its exact identity before further action.`,
355
+ recoverable: false,
356
+ controllerOwnershipUnknown: true,
357
+ controllerRestore: { identity: lifecycle.identity, outcome: "unknown", error: messageOf(error) }
320
358
  };
321
359
  }
322
360
  }
@@ -351,15 +389,6 @@ function isStorageMigrationResult(value) {
351
389
  function isPositivePid(value) {
352
390
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
353
391
  }
354
- function isControllerIdentity(value) {
355
- return isRecord(value)
356
- && typeof value.executablePath === "string"
357
- && value.executablePath.length > 0
358
- && Array.isArray(value.args)
359
- && value.args.every((arg) => typeof arg === "string")
360
- && typeof value.version === "string"
361
- && value.version.length > 0;
362
- }
363
392
  function isRecord(value) {
364
393
  return typeof value === "object" && value !== null && !Array.isArray(value);
365
394
  }