@zq-silk/yui 0.15.8 → 0.15.11

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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -380,12 +380,12 @@ const taskChildren = [
380
380
  { name: "show", summary: "Show a Task.", usage: "yui task show <id>" },
381
381
  {
382
382
  name: "artifact",
383
- summary: "Save fixed Task results and read history without the original Runtime.",
384
- sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "save"] }],
383
+ summary: "Save Task files in local Git and read current or commit-pinned content.",
384
+ sections: [{ id: "manage", title: "Commands", entries: ["list", "read", "save"] }],
385
385
  children: [
386
386
  { name: "list", summary: "List saved Task artifacts.", usage: "yui task artifact list <task>" },
387
- { name: "show", summary: "Read one saved artifact.", usage: "yui task artifact show <task> <artifact-id>" },
388
- { name: "save", summary: "Save content, a version, receipt, or reference.", usage: "yui task artifact save <task> <artifact-json>" }
387
+ { name: "read", summary: "Read a file at HEAD or an exact commit.", usage: "yui task artifact read <task> <relative-path> [<commit>]" },
388
+ { name: "save", summary: "Save and locally commit one file.", usage: "yui task artifact save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]", options: ["--message", "--expected-head"] }
389
389
  ]
390
390
  },
391
391
  {
@@ -408,8 +408,8 @@ const taskChildren = [
408
408
  },
409
409
  {
410
410
  name: "archive",
411
- summary: "Archive a Task after confirming the main worktree outcome.",
412
- usage: "yui task archive <id> (--integrated [--force]|--abandon)",
411
+ summary: "Archive a terminal Task; explicit --force commits despite delivery/cleanup warnings, retaining unsafe resources.",
412
+ usage: "yui task archive <id> (--integrated|--abandon) [--force]",
413
413
  options: ["--integrated", "--abandon", "--force"]
414
414
  },
415
415
  { name: "reconcile", summary: "Run one immediate Controller reconciliation.", usage: "yui task reconcile <id>" },
@@ -435,7 +435,7 @@ const taskChildren = [
435
435
  {
436
436
  name: "message",
437
437
  summary: "Manage durable Task messages.",
438
- sections: [{ id: "manage", title: "Commands", entries: ["send", "handoff", "list", "show", "update", "retire"] }],
438
+ sections: [{ id: "manage", title: "Commands", entries: ["send", "queue", "steer", "handoff", "list", "show", "update", "retire"] }],
439
439
  children: [
440
440
  {
441
441
  name: "handoff",
@@ -445,14 +445,29 @@ const taskChildren = [
445
445
  },
446
446
  {
447
447
  name: "send",
448
- summary: "Send a Task message.",
449
- usage: "yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none] [--to <role> --work-item <id>|--review-round <id>]",
450
- options: ["--body-file", "--wake-policy", "--to", "--work-item", "--review-round"],
448
+ summary: "Send a Task message. An unaddressed user/operator message carries a submission intent (record|discuss|develop) and an optional idempotency key.",
449
+ usage: "yui task message send <id> (<body>|--body-file <path|->) [--intent record|discuss|develop] [--request-id <key>] [--wake-policy leader|none] [--to <role> --work-item <id>|--review-round <id>]",
450
+ options: ["--body-file", "--intent", "--request-id", "--wake-policy", "--to", "--work-item", "--review-round"],
451
451
  optionValues: {
452
+ "--intent": ["record", "discuss", "develop"],
452
453
  "--wake-policy": ["leader", "none"]
453
454
  },
454
455
  fileOptions: ["--body-file"]
455
456
  },
457
+ {
458
+ name: "queue",
459
+ summary: "Queue an input for delivery at the recipient's next legal opportunity (idempotent by request id).",
460
+ usage: "yui task message queue <id> (<body>|--body-file <path|->) --request-id <id> [--to <role> --work-item <id>|--review-round <id>]",
461
+ options: ["--body-file", "--request-id", "--to", "--work-item", "--review-round"],
462
+ fileOptions: ["--body-file"]
463
+ },
464
+ {
465
+ name: "steer",
466
+ summary: "Steer only a Role's exact current native Turn; saved and reported without fallback when unsupported.",
467
+ usage: "yui task message steer <id> (<body>|--body-file <path|->) --request-id <id> --expected-target <turn> --to <role> [--work-item <id>|--review-round <id>]",
468
+ options: ["--body-file", "--request-id", "--expected-target", "--to", "--work-item", "--review-round"],
469
+ fileOptions: ["--body-file"]
470
+ },
456
471
  {
457
472
  name: "list",
458
473
  summary: "List Task messages.",
@@ -621,7 +636,7 @@ const taskChildren = [
621
636
  summary: "Manage Roles within a Task.",
622
637
  sections: [{ id: "manage", title: "Commands", entries: [
623
638
  "add", "list", "status", "show", "update", "remove", "bind", "unbind",
624
- "session", "view", "takeover", "release"
639
+ "session", "interrupt", "view", "takeover", "release"
625
640
  ] }],
626
641
  children: [
627
642
  {
@@ -679,6 +694,12 @@ const taskChildren = [
679
694
  summary: "Attach read-only to an independent Provider presentation surface.",
680
695
  usage: "yui task role view <task> <role>"
681
696
  },
697
+ {
698
+ name: "interrupt",
699
+ summary: "Interrupt a Role's exact current native Turn via native cancel; optionally deliver a saved Message once after a proven terminal.",
700
+ usage: "yui task role interrupt <task> <role> --expected-target <turn> [--then-message <task/message>] [--request-id <id>]",
701
+ options: ["--expected-target", "--then-message", "--request-id"]
702
+ },
682
703
  {
683
704
  name: "takeover",
684
705
  summary: "Enter the PTY input gateway for an independent Provider process.",
@@ -705,7 +726,7 @@ const taskChildren = [
705
726
  children: [
706
727
  {
707
728
  name: "create",
708
- summary: "Create a work item.",
729
+ summary: "Create a separately managed result; a coherent Task may use zero WorkItems. Omit --role for Leader-direct execution; --role (including leader) selects a managed AgentRun executor. Isolate writable direct work before editing, then submit, integrate and accept its Candidate.",
709
730
  usage: "yui task work create <task> <title> [--project <project> ...] [--base-ref <project>=<ref> ...] [--objective <text>] [--accept <criterion> ...] [--after <work> ...] [--role <name>]",
710
731
  options: ["--project", "--base-ref", "--objective", "--accept", "--after", "--role"]
711
732
  },
@@ -921,7 +942,7 @@ const taskChildren = [
921
942
  },
922
943
  {
923
944
  name: "abort",
924
- summary: "Abandon a running or blocked Integration Attempt.",
945
+ summary: "Abandon an unadvanced Integration while preserving evidence; reconcile an already-applied target.",
925
946
  usage: "yui task integration abort <task>/<integration> --reason <text>",
926
947
  options: ["--reason"]
927
948
  },
@@ -1286,9 +1307,12 @@ export const ROOT_COMMAND = buildNode({
1286
1307
  },
1287
1308
  {
1288
1309
  name: "submit",
1289
- summary: "Submit work through the Operator.",
1290
- usage: "yui operator submit (<body>|--body-file <path|->) [--task <id>]",
1291
- options: ["--task", "--body-file"],
1310
+ summary: "Submit work through the Operator with a submission intent (record|discuss|develop); a task-less submit opens a Draft. An optional idempotency key makes a retry safe.",
1311
+ usage: "yui operator submit (<body>|--body-file <path|->) [--task <id>] [--intent record|discuss|develop] [--request-id <key>]",
1312
+ options: ["--task", "--body-file", "--intent", "--request-id"],
1313
+ optionValues: {
1314
+ "--intent": ["record", "discuss", "develop"]
1315
+ },
1292
1316
  fileOptions: ["--body-file"]
1293
1317
  }
1294
1318
  ]
@@ -574,9 +574,9 @@ export const INTERACTION_POLICIES = Object.freeze([
574
574
  entity: "integration-attempt",
575
575
  provider: "integration-attempts",
576
576
  actionTarget: true,
577
- ...(command === "continue" ? { statuses: ["blocked", "validating"] } : {}),
578
- ...(command === "resolve" ? { statuses: ["blocked"] } : {}),
579
- ...(command === "abort" ? { statuses: ["running", "blocked"] } : {})
577
+ ...(command === "continue" ? { statuses: ["running", "conflicted", "blocked", "validating"] } : {}),
578
+ ...(command === "resolve" ? { statuses: ["blocked", "conflicted"] } : {}),
579
+ ...(command === "abort" ? { statuses: ["running", "conflicted", "blocked", "validating"] } : {})
580
580
  }],
581
581
  ...(command === "resolve"
582
582
  ? { trailingOptions: { "--option": "value", "--rationale": "value" } }
@@ -101,7 +101,30 @@ function runStagedUpdate(ports, staged, home) {
101
101
  const captured = captureControllerLifecycle(ports, staged.version, home);
102
102
  if ("outcome" in captured)
103
103
  return captured;
104
- return activateAndVerify(ports, staged, home, captured.lifecycle, preflight);
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;
107
+ try {
108
+ fencedPreflight = ports.preflight(staged, home);
109
+ }
110
+ 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
+ });
117
+ }
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);
105
128
  }
106
129
  finally {
107
130
  releaseHandover?.();
@@ -25,14 +25,15 @@
25
25
  * mismatch fails closed.
26
26
  */
27
27
  import { spawnSync } from "node:child_process";
28
- import { accessSync, constants, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
29
- import { tmpdir } from "node:os";
28
+ import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
30
29
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
31
30
  import { fileURLToPath } from "node:url";
32
31
  import { runtimeError } from "../errors/cliError.js";
33
32
  import { isConcreteVersion } from "../domain/validation.js";
34
33
  import { STORAGE_DOCTOR_CHECK_NAMES } from "../doctor/doctor.js";
35
34
  import { acquireHandoverLock } from "../release/runtimeRelease.js";
35
+ import { updateStagingRoot } from "../storage/homeLayout.js";
36
+ import { resolveYuiHome } from "../storage/taskStore.js";
36
37
  const PACKAGE_NAME = "@zq-silk/yui";
37
38
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
38
39
  function resolveExecutable(command, environmentPath) {
@@ -64,7 +65,7 @@ function failedSpawnResult(message) {
64
65
  };
65
66
  }
66
67
  /** Build the real ports. `spawn` is injectable so tests avoid real installs. */
67
- export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot = tmpdir()) {
68
+ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot = updateStagingRoot(resolveYuiHome(environment))) {
68
69
  // The production adapter must not let a later PATH change select a
69
70
  // different npm during staging, activation, or recovery. Test doubles are
70
71
  // intentionally left untouched so deterministic tests can dispatch on the
@@ -105,6 +106,9 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
105
106
  throw runtimeError(`Refusing to stage a non-concrete version (${String(version)}): only an exact `
106
107
  + "major.minor.patch version can be pinned for an update.");
107
108
  }
109
+ // The Home staging parent may not exist yet on a Home that has never
110
+ // run an update; create it (private) before minting a throwaway prefix.
111
+ mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
108
112
  const stagingPath = mkdtempSync(join(stagingRoot, "yui-update-stage-"));
109
113
  let ownsStaging = true;
110
114
  try {
@@ -1,6 +1,8 @@
1
1
  /** `yui upgrade` plans or applies the supported linear storage migration chain. */
2
2
  import { runStorageUpgrade } from "../storage/upgrade/upgradeOrchestrator.js";
3
- import { ensureFileTaskController, stopFileTaskController } from "../controller/clientRuntime.js";
3
+ import { ensureFileTaskController, ensureFileTaskControllerIdentity, stopFileTaskController } from "../controller/clientRuntime.js";
4
+ import { callController, ControllerClientError } from "../core/controllerClient.js";
5
+ import { spawn } from "node:child_process";
4
6
  import { runtimeError, usageError } from "../errors/cliError.js";
5
7
  import { acquireHandoverLock, isForeignHandoverLockHeld, isHandoverLockHeld } from "../release/runtimeRelease.js";
6
8
  /**
@@ -43,12 +45,29 @@ async function runInteractiveUpgrade(home, environment) {
43
45
  const handover = acquireHandoverLock(home);
44
46
  let controllerWasRunning = false;
45
47
  try {
48
+ const previous = await captureUpgradeController(home);
46
49
  const stopped = await stopFileTaskController(home, {
47
50
  environment,
48
- handoverOwnerPid: process.pid
51
+ handoverOwnerPid: process.pid,
52
+ ...(previous === undefined ? {} : { expectedPid: previous.pid })
49
53
  });
50
54
  controllerWasRunning = stopped.stopped;
51
55
  const result = await runStorageUpgrade({ home, mode: "execute" });
56
+ if (controllerWasRunning && result.outcome === "blocked") {
57
+ if (previous === undefined) {
58
+ throw runtimeError("Upgrade was blocked without changing storage; the stopped Controller identity is unknown. Keep the Home quiesced.");
59
+ }
60
+ await ensureFileTaskControllerIdentity(home, previous.identity, {
61
+ environment, handoverOwnerPid: process.pid,
62
+ spawnController: (_home, launchEnvironment) => {
63
+ const child = spawn(previous.identity.executablePath, [...previous.identity.args], {
64
+ env: launchEnvironment, detached: true, stdio: "ignore"
65
+ });
66
+ child.unref();
67
+ return child.pid;
68
+ }
69
+ });
70
+ }
52
71
  if (controllerWasRunning
53
72
  && (result.outcome === "upgraded" || result.outcome === "already-current")) {
54
73
  try {
@@ -70,6 +89,27 @@ async function runInteractiveUpgrade(home, environment) {
70
89
  handover.release();
71
90
  }
72
91
  }
92
+ async function captureUpgradeController(home) {
93
+ let value;
94
+ try {
95
+ value = await callController(home, "controller.identity", {});
96
+ }
97
+ catch (error) {
98
+ if (error instanceof ControllerClientError && error.code === "CONTROLLER_NOT_RUNNING")
99
+ return undefined;
100
+ throw error;
101
+ }
102
+ const identity = value;
103
+ if (!Number.isSafeInteger(identity.pid) || identity.pid < 1
104
+ || typeof identity.executablePath !== "string" || !identity.executablePath
105
+ || !Array.isArray(identity.args) || identity.args.some(arg => typeof arg !== "string")
106
+ || typeof identity.version !== "string" || !identity.version) {
107
+ throw runtimeError("Cannot capture the exact Controller for a reversible upgrade preflight.");
108
+ }
109
+ return { pid: identity.pid, identity: {
110
+ executablePath: identity.executablePath, args: identity.args, version: identity.version
111
+ } };
112
+ }
73
113
  async function runUpdateOwnedUpgrade(home, environment) {
74
114
  const ownerText = environment.YUI_UPDATE_HANDOVER_OWNER_PID;
75
115
  const ownerPid = ownerText === undefined ? Number.NaN : Number(ownerText);