@zq-silk/yui 0.16.0 → 0.16.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 (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +139 -20
  95. package/docs/release-workflow.zh-CN.md +115 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -6,10 +6,11 @@
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";
9
10
  export function runUpdate(ports, options) {
10
11
  let staged;
11
12
  try {
12
- staged = ports.stage();
13
+ staged = ports.stage(options.version);
13
14
  }
14
15
  catch (error) {
15
16
  return {
@@ -33,7 +34,9 @@ export function runUpdate(ports, options) {
33
34
  cleanupWarning = `Staging cleanup could not be completed: ${messageOf(error)}`;
34
35
  }
35
36
  }
36
- return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
37
+ return cleanupWarning === undefined ? result : {
38
+ ...result, cleanupWarning: [result.cleanupWarning, cleanupWarning].filter(Boolean).join(" ")
39
+ };
37
40
  }
38
41
  function runStagedUpdate(ports, staged, home) {
39
42
  let preflight;
@@ -97,38 +100,70 @@ function runStagedUpdate(ports, staged, home) {
97
100
  version: staged.version
98
101
  };
99
102
  }
103
+ let result;
104
+ let cleanupWarning;
100
105
  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;
106
+ result = reconcileAndUpdate(ports, staged, home);
107
+ }
108
+ finally {
107
109
  try {
108
- fencedPreflight = ports.preflight(staged, home);
110
+ releaseHandover?.();
109
111
  }
110
112
  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
- });
113
+ cleanupWarning = `Controller handover lock release failed for ${home}: ${messageOf(error)}`;
117
114
  }
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
115
  }
129
- finally {
130
- releaseHandover?.();
116
+ return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
117
+ }
118
+ function reconcileAndUpdate(ports, staged, home) {
119
+ let reconciliation;
120
+ try {
121
+ reconciliation = ports.reconcileController?.(home);
122
+ }
123
+ catch (error) {
124
+ const evidence = error instanceof UpdateControllerReconciliationError ? error.result : undefined;
125
+ const unknown = evidence === undefined || evidence.observationError !== undefined
126
+ || evidence.lockReleaseError !== undefined || evidence.attempts.some(attempt => attempt.outcome === "unknown");
127
+ return {
128
+ outcome: "aborted", phase: "coordination", version: staged.version,
129
+ message: `Controller reconciliation failed: ${messageOf(error)}`,
130
+ action: "Inspect the reported current-Home targets and partial effects; binary activation was not attempted.",
131
+ recoverable: !unknown,
132
+ ...(evidence === undefined ? {} : { controllerReconciliation: evidence }),
133
+ ...(unknown ? { controllerOwnershipUnknown: true } : {})
134
+ };
131
135
  }
136
+ const coordinated = runCoordinatedUpdate(ports, staged, home);
137
+ return reconciliation === undefined ? coordinated : { ...coordinated, controllerReconciliation: reconciliation };
138
+ }
139
+ function runCoordinatedUpdate(ports, staged, home) {
140
+ const captured = captureControllerLifecycle(ports, staged.version, home);
141
+ if ("outcome" in captured)
142
+ return captured;
143
+ // Storage can change before the Controller drains. Recheck after its
144
+ // exact stop, before changing the install or storage. Its sceneUnchanged
145
+ // verdict does not describe the preceding runtime cleanup/stop/restore.
146
+ let fencedPreflight;
147
+ try {
148
+ fencedPreflight = ports.preflight(staged, home);
149
+ }
150
+ catch (error) {
151
+ return restoreControllerOrReport(ports, home, captured.lifecycle, {
152
+ outcome: "aborted", phase: "preflight",
153
+ message: `Quiesced storage preflight failed: ${messageOf(error)}`,
154
+ action: "The install and storage are unchanged; inspect the storage failure before retrying.",
155
+ recoverable: true, version: staged.version
156
+ });
157
+ }
158
+ if (fencedPreflight.status === "blocked") {
159
+ return restoreControllerOrReport(ports, home, captured.lifecycle, {
160
+ outcome: "aborted", phase: "preflight",
161
+ message: fencedPreflight.message, action: fencedPreflight.action,
162
+ recoverable: true, version: staged.version,
163
+ ...(fencedPreflight.blockers === undefined ? {} : { blockers: fencedPreflight.blockers })
164
+ });
165
+ }
166
+ return activateAndVerify(ports, staged, home, captured.lifecycle, fencedPreflight);
132
167
  }
133
168
  function activateAndVerify(ports, staged, home, lifecycle, preflight) {
134
169
  try {
@@ -256,7 +291,8 @@ function captureControllerLifecycle(ports, version, home) {
256
291
  phase: "preflight",
257
292
  message: `Controller status could not be verified: ${messageOf(error)}`,
258
293
  action: "Inspect Controller ownership and retry after its status is known.",
259
- recoverable: true,
294
+ recoverable: false,
295
+ controllerOwnershipUnknown: true,
260
296
  version
261
297
  };
262
298
  }
@@ -279,7 +315,8 @@ function captureControllerLifecycle(ports, version, home) {
279
315
  phase: "preflight",
280
316
  message: `Controller stop/drain failed: ${messageOf(error)}`,
281
317
  action: "Inspect the captured Controller before retrying; binary activation was not attempted.",
282
- recoverable: true,
318
+ recoverable: false,
319
+ controllerOwnershipUnknown: true,
283
320
  version
284
321
  };
285
322
  }
@@ -300,7 +337,8 @@ function malformedControllerResult(version, message) {
300
337
  phase: "preflight",
301
338
  message,
302
339
  action: "Refusing an unfenced Controller handoff; inspect ownership and retry.",
303
- recoverable: true,
340
+ recoverable: false,
341
+ controllerOwnershipUnknown: true,
304
342
  version
305
343
  };
306
344
  }
@@ -309,14 +347,16 @@ function restoreControllerOrReport(ports, home, lifecycle, failure) {
309
347
  return failure;
310
348
  try {
311
349
  ports.restoreController(home, lifecycle.identity);
312
- return failure;
350
+ return { ...failure, controllerRestore: { identity: lifecycle.identity, outcome: "restored" } };
313
351
  }
314
352
  catch (error) {
315
353
  return {
316
354
  ...failure,
317
355
  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
356
+ action: `${failure.action} Controller liveness after the failed restore is unknown; inspect its exact identity before further action.`,
357
+ recoverable: false,
358
+ controllerOwnershipUnknown: true,
359
+ controllerRestore: { identity: lifecycle.identity, outcome: "unknown", error: messageOf(error) }
320
360
  };
321
361
  }
322
362
  }
@@ -34,6 +34,7 @@ import { STORAGE_DOCTOR_CHECK_NAMES } from "../doctor/doctor.js";
34
34
  import { acquireHandoverLock } from "../release/runtimeRelease.js";
35
35
  import { updateStagingRoot } from "../storage/homeLayout.js";
36
36
  import { resolveYuiHome } from "../storage/taskStore.js";
37
+ import { UpdateControllerReconciliationError } from "../controller/updateReconciliation.js";
37
38
  const PACKAGE_NAME = "@zq-silk/yui";
38
39
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
39
40
  function resolveExecutable(command, environmentPath) {
@@ -90,20 +91,20 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
90
91
  beginControllerHandover(home) {
91
92
  return acquireHandoverLock(home).release;
92
93
  },
93
- stage(version) {
94
+ stage(requestedVersion) {
94
95
  // A caller that names a version (the release workflow, which freezes the
95
96
  // exact version in its plan) installs THAT version — never a moving
96
97
  // `latest` that could resolve to a different build than the one the
97
98
  // plan authorized. A non-concrete value fails closed rather than being
98
99
  // interpolated into an install spec. An omitted version keeps the
99
100
  // interactive `yui update` behavior of staging latest.
100
- const spec = version === undefined
101
+ const spec = requestedVersion === undefined
101
102
  ? PACKAGE_SPEC
102
- : isConcreteVersion(version)
103
- ? `${PACKAGE_NAME}@${version.trim()}`
103
+ : isConcreteVersion(requestedVersion)
104
+ ? `${PACKAGE_NAME}@${requestedVersion.trim()}`
104
105
  : null;
105
106
  if (spec === null) {
106
- throw runtimeError(`Refusing to stage a non-concrete version (${String(version)}): only an exact `
107
+ throw runtimeError(`Refusing to stage a non-concrete version (${String(requestedVersion)}): only an exact `
107
108
  + "major.minor.patch version can be pinned for an update.");
108
109
  }
109
110
  // The Home staging parent may not exist yet on a Home that has never
@@ -126,6 +127,10 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
126
127
  + "nor `yui --json version` returned a concrete version). Refusing to proceed with a "
127
128
  + "`@latest` fallback that could promote a different build than the one preflighted.");
128
129
  }
130
+ if (requestedVersion !== undefined && version !== requestedVersion.trim()) {
131
+ throw runtimeError(`Requested Yui ${requestedVersion.trim()}, but staged Yui ${version}. `
132
+ + "Refusing to activate a different release; the install and storage are unchanged.");
133
+ }
129
134
  // Successful staging transfers cleanup ownership to runUpdate's finally
130
135
  // block. Every assertion, spawn, npm/network, or version-resolution
131
136
  // failure before that handoff removes the throwaway prefix here.
@@ -196,10 +201,9 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
196
201
  + `but the staged/verified artifact was ${staged.version}. Refusing to trust a `
197
202
  + "different build than the one that passed preflight.");
198
203
  }
199
- // Existing managed Sessions may have been created by an earlier release.
200
- // Retarget those authenticated, Manifest-referenced wrappers to the
201
- // activated control plane before the replacement Controller starts so
202
- // the update cannot strand a live Session.
204
+ // Retarget current Manifest-referenced wrappers to the activated
205
+ // installation before the replacement Controller starts. This relocates
206
+ // the current shell form; it does not convert historical Session wrappers.
203
207
  const sessionCliRefresh = run(activeBinary, ["--json", "internal", "session-cli-refresh"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
204
208
  assertSpawnOk(sessionCliRefresh, "refresh managed Session CLI wrappers");
205
209
  // Retain the exact path used by both doctor and version verification. The
@@ -214,13 +218,15 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
214
218
  },
215
219
  // `runUpdate` is intentionally synchronous because the npm/staged-binary
216
220
  // ports use spawnSync. A short-lived child owns the async, exactly fenced
217
- // reconciliation first; lifecycle capture then uses structured live
221
+ // reconciliation explicitly; lifecycle capture uses only structured live
218
222
  // Controller commands. Tests can replace these seams with deterministic
219
223
  // fakes without touching a real Controller.
220
224
  controllerStatus(home) {
221
- reconcileControllerResourcesForUpdate(home, environment, spawn);
222
225
  return readControllerLifecycle(home, environment, spawn);
223
226
  },
227
+ reconcileController(home) {
228
+ return reconcileControllerResourcesForUpdate(home, environment, spawn);
229
+ },
224
230
  stopController(home, expectedPid) {
225
231
  return stopControllerForUpdate(home, expectedPid, environment, spawn);
226
232
  },
@@ -355,6 +361,7 @@ function stopReplacementControllerForUpdate(home, expectedPid, environment, spaw
355
361
  function runSchemaIndependentControllerStop(home, environment, spawn, expectedPid) {
356
362
  const helper = [
357
363
  "const values = process.argv.slice(1);",
364
+ "const handoverOwnerPid = Number(values.pop());",
358
365
  "const expectedPid = values.length === 3 ? Number(values.pop()) : undefined;",
359
366
  "const home = values.pop();",
360
367
  "const runtimeModule = values.pop();",
@@ -362,6 +369,7 @@ function runSchemaIndependentControllerStop(home, environment, spawn, expectedPi
362
369
  " const { stopFileTaskController } = await import(runtimeModule);",
363
370
  " const data = await stopFileTaskController(home, {",
364
371
  " environment: process.env,",
372
+ " handoverOwnerPid,",
365
373
  " ...(expectedPid === undefined ? {} : { expectedPid })",
366
374
  " });",
367
375
  " process.stdout.write(JSON.stringify({ ok: true, data }));",
@@ -377,7 +385,10 @@ function runSchemaIndependentControllerStop(home, environment, spawn, expectedPi
377
385
  helper,
378
386
  UPDATE_CLIENT_RUNTIME_PATH,
379
387
  home,
380
- ...(expectedPid === undefined ? [] : [String(expectedPid)])
388
+ ...(expectedPid === undefined ? [] : [String(expectedPid)]),
389
+ // The runtime validates the existing lock owner; the child must identify
390
+ // its owning updater instead of waiting on the parent's lock as foreign.
391
+ String(process.pid)
381
392
  ], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
382
393
  if (result.error !== undefined || result.status !== 0) {
383
394
  const detail = result.stderr.toString("utf8").trim();
@@ -491,10 +502,11 @@ function restoreControllerIdentity(home, identity, environment, spawn) {
491
502
  // Spawn a detached child through a short-lived Node helper so the synchronous
492
503
  // update process can still use the authenticated, bounded readiness handshake
493
504
  // shared by Controller startup. No retry, sleep, or new identity inference is
494
- // hidden here.
505
+ // hidden here. Restoration participates in the same parent-owned lock.
495
506
  const helper = [
496
507
  "const { spawn } = require('node:child_process');",
497
508
  "const values = process.argv.slice(1);",
509
+ "const handoverOwnerPid = Number(values.pop());",
498
510
  "const version = values.pop();",
499
511
  "const args = JSON.parse(values.pop());",
500
512
  "const executable = values.pop();",
@@ -504,6 +516,7 @@ function restoreControllerIdentity(home, identity, environment, spawn) {
504
516
  " const { ensureFileTaskControllerIdentity } = await import(runtimeModule);",
505
517
  " await ensureFileTaskControllerIdentity(home, { executablePath: executable, args, version }, {",
506
518
  " environment: process.env,",
519
+ " handoverOwnerPid,",
507
520
  " spawnController: (_home, launchEnv) => {",
508
521
  " const child = spawn(executable, args, { detached: true, stdio: 'ignore', env: launchEnv });",
509
522
  " child.unref();",
@@ -518,7 +531,8 @@ function restoreControllerIdentity(home, identity, environment, spawn) {
518
531
  home,
519
532
  identity.executablePath,
520
533
  JSON.stringify(identity.args),
521
- identity.version
534
+ identity.version,
535
+ String(process.pid)
522
536
  ], { cwd: process.cwd(), env: launchEnvironment, shell: false, stdio: "pipe" });
523
537
  assertSpawnOk(result, "restore the previously running Controller identity");
524
538
  }
@@ -585,14 +599,23 @@ function reconcileControllerResourcesForUpdate(home, environment, spawn) {
585
599
  " process.stdout.write(JSON.stringify({ ok: true, data }));",
586
600
  "})().catch((error) => {",
587
601
  " const message = error instanceof Error ? error.message : String(error);",
588
- " process.stderr.write(JSON.stringify({ ok: false, code: 'RUNTIME_ERROR', message }));",
602
+ " process.stderr.write(JSON.stringify({ ok: false, code: 'RUNTIME_ERROR', message, result: error.result }));",
589
603
  " process.exitCode = 5;",
590
604
  "});"
591
605
  ].join(" ");
592
606
  const result = spawn(process.execPath, ["-e", helper, UPDATE_CONTROLLER_RECONCILIATION_PATH, home], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
593
607
  if (result.error !== undefined || result.status !== 0) {
594
608
  const detail = structuredErrorMessage(result) ?? result.stderr.toString("utf8").trim();
595
- throw new Error(`Controller reconciliation failed (exit ${result.status ?? "null"})${detail.length === 0 ? "." : `: ${detail}`}`);
609
+ const message = `Controller reconciliation failed (exit ${result.status ?? "null"})${detail.length === 0 ? "." : `: ${detail}`}`;
610
+ let evidence;
611
+ try {
612
+ evidence = JSON.parse(result.stderr.toString("utf8")).result;
613
+ }
614
+ catch { /* Unknown child effects are reported below, never retried here. */ }
615
+ if (isReconciliationResult(evidence, home)) {
616
+ throw new UpdateControllerReconciliationError(message, evidence);
617
+ }
618
+ throw new Error(`${message} Cleanup effects are unknown; inspect the exact Home ${home} before retrying.`);
596
619
  }
597
620
  let parsed;
598
621
  try {
@@ -605,11 +628,19 @@ function reconcileControllerResourcesForUpdate(home, environment, spawn) {
605
628
  }
606
629
  if (!isRecord(parsed)
607
630
  || parsed.ok !== true
608
- || !isRecord(parsed.data)
609
- || !Array.isArray(parsed.data.cleaned)
610
- || parsed.data.cleaned.some((id) => typeof id !== "string")) {
631
+ || !isReconciliationResult(parsed.data, home)) {
611
632
  throw new Error("Controller reconciliation returned an invalid structured result.");
612
633
  }
634
+ return parsed.data;
635
+ }
636
+ function isReconciliationResult(value, home) {
637
+ return isRecord(value) && value.home === resolve(home)
638
+ && Array.isArray(value.cleaned) && value.cleaned.every(id => typeof id === "string")
639
+ && Array.isArray(value.attempts) && value.attempts.every(attempt => (isRecord(attempt) && isRecord(attempt.resource) && typeof attempt.resource.id === "string"
640
+ && ["cleaned", "absent", "unknown"].includes(String(attempt.outcome))
641
+ && (attempt.error === undefined || typeof attempt.error === "string")))
642
+ && Array.isArray(value.remaining) && value.remaining.every(resource => (isRecord(resource) && typeof resource.id === "string"))
643
+ && ["observedAt", "observationError", "lockReleaseError"].every(key => (value[key] === undefined || typeof value[key] === "string"));
613
644
  }
614
645
  function structuredErrorMessage(result) {
615
646
  for (const buffer of [result.stderr, result.stdout]) {
@@ -779,7 +810,7 @@ function interpretPreflight(result) {
779
810
  }
780
811
  const outcome = typeof data.outcome === "string" ? data.outcome : undefined;
781
812
  // EXIT/OUTCOME CONSISTENCY (P1-2): the one success-class internal preflight
782
- // outcome must exit 0. A user dry-run, legacy direct classification outcome,
813
+ // outcome must exit 0. A user dry-run, a direct classification outcome,
783
814
  // or any other spelling is not this contract and is never promoted to green.
784
815
  if (outcome === "update-preflight" && result.status !== 0) {
785
816
  return {
package/dist/cli.js CHANGED
@@ -45,7 +45,7 @@ import { assertTaskDeliveryAuthority, taskLocalActor } from "./task/taskAuthorit
45
45
  import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
46
46
  import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
47
47
  import { assertWorkItemDependenciesCompletedForCommand, dispatchPreparedReviewRound, failPendingReviewRound, parseTaskArchiveArguments, parseTaskCompletionRequest, planReplicatedWorkItemLanes, preflightTaskCompletion, preserveReviewRoundWorkspace, previewTaskRoleAgentConfigurationMutation, requireWorkItemAssignee, runTaskCommand, validateTaskArchiveRequest } from "./commands/taskCommands.js";
48
- import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
48
+ import { verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
49
49
  import { finalizeStoppedTaskExecution, parseTaskExecutionStartRequest, parseTaskExecutionStopRequest, startTaskExecutionCommand, stopTaskExecutionCommand } from "./commands/taskExecutionCommands.js";
50
50
  import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
51
51
  import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
@@ -225,9 +225,7 @@ export async function main() {
225
225
  if (args[0] === "update") {
226
226
  if (jsonOutput)
227
227
  throw usageError("Update does not support --json.");
228
- if (args.length !== 1)
229
- throw usageError("Update usage: yui update");
230
- process.exitCode = runUpdateCommand();
228
+ process.exitCode = runUpdateCommand(args.slice(1));
231
229
  return;
232
230
  }
233
231
  if (args[0] === "release") {
@@ -1384,6 +1382,8 @@ export async function main() {
1384
1382
  }
1385
1383
  let completionSummary;
1386
1384
  let completionPublishedTreeProof;
1385
+ let completionBaseFreshness;
1386
+ let completionWarnings = [];
1387
1387
  if (resolved[1] === "base" && resolved[2] === "status") {
1388
1388
  const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
1389
1389
  emit(result.output, false, result.data);
@@ -1400,8 +1400,8 @@ export async function main() {
1400
1400
  if (resolved[1] === "complete" && resolved[2] !== undefined) {
1401
1401
  const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
1402
1402
  completionSummary = completionRequest.summary;
1403
- const refreshRemote = resolved.includes("--refresh-remote");
1404
- const completion = preflightTaskCompletion(resolved[2], store, {
1403
+ const refreshRemote = completionRequest.refreshRemote;
1404
+ const completion = preflightTaskCompletion(completionRequest.taskId, store, {
1405
1405
  environment: process.env,
1406
1406
  ...(taskFinalReviewContract === undefined
1407
1407
  ? {}
@@ -1412,34 +1412,25 @@ export async function main() {
1412
1412
  // Publication proof resolves its exact commit. Without the flag the
1413
1413
  // command remains offline and preserves the existing proof-first path.
1414
1414
  const refreshedFreshness = refreshRemote
1415
- ? await inspectTaskBaseFreshness(resolved[2], store, { refresh: true })
1415
+ ? await inspectTaskBaseFreshness(completionRequest.taskId, store, { refresh: true })
1416
1416
  : undefined;
1417
1417
  if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
1418
1418
  completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
1419
1419
  }
1420
- const freshness = refreshedFreshness
1421
- ?? await inspectTaskBaseFreshness(resolved[2], store);
1422
- for (const warning of assertTaskBaseFreshnessForCompletion(freshness, {
1420
+ completionBaseFreshness = refreshedFreshness
1421
+ ?? await inspectTaskBaseFreshness(completionRequest.taskId, store);
1422
+ completionWarnings = assertTaskBaseFreshnessForCompletion(completionBaseFreshness, {
1423
1423
  ...(completionPublishedTreeProof === undefined
1424
1424
  ? {}
1425
1425
  : {
1426
1426
  acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
1427
1427
  })
1428
- })) {
1428
+ });
1429
+ for (const warning of completionWarnings) {
1429
1430
  process.stderr.write(`Warning: ${warning}\n`);
1430
1431
  }
1431
- // Keep completion offline by default. An explicit refresh is the only
1432
- // path that may fetch and reconcile a moved remote baseline.
1433
- if (refreshRemote) {
1434
- const reconciled = await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env }) });
1435
- if (reconciled.length > 0) {
1436
- const updates = reconciled.map((entry) => (`${entry.projectId}: ${entry.fromCommit} -> ${entry.toCommit} `
1437
- + `(Integration ${entry.integrationId})`)).join("; ");
1438
- throw usageError(`Remote baseline reconciliation advanced Task ${resolved[2]} (${updates}). `
1439
- + "The Task remains active so the Leader can inspect the new authoritative head, "
1440
- + "decide how prior Review evidence applies, and retry task complete.");
1441
- }
1442
- }
1432
+ // Refresh observes remote objects only. Choosing and executing an
1433
+ // upstream Integration belongs to its explicit command, never completion.
1443
1434
  }
1444
1435
  }
1445
1436
  let releaseReviewHandoverLock;
@@ -1628,9 +1619,24 @@ export async function main() {
1628
1619
  emitControlFailure(result.output, failureCode, result.data);
1629
1620
  return;
1630
1621
  }
1631
- emit(`${result.output}${reviewOutput}`, false, reviewData === undefined
1622
+ const commandData = reviewData === undefined
1632
1623
  ? result.data
1633
- : { command: result.data, ...reviewData });
1624
+ : { command: result.data, ...reviewData };
1625
+ const completionData = resolved[1] === "complete" ? {
1626
+ // Review dispatch may have advanced or failed after the command
1627
+ // prepared its Round. Report that observed stage, not preparation.
1628
+ ...(reviewData === undefined ? {} : {
1629
+ stage: reviewRoundFromCommandData(reviewData)?.status === "running"
1630
+ ? "review-running"
1631
+ : "review-blocked",
1632
+ projectHeads: result.data.projectHeads
1633
+ }),
1634
+ baseFreshness: completionBaseFreshness ?? null,
1635
+ warnings: completionWarnings
1636
+ } : undefined;
1637
+ emit(`${result.output}${reviewOutput}`, false, completionData === undefined
1638
+ ? commandData
1639
+ : { ...commandData, ...completionData });
1634
1640
  return;
1635
1641
  }
1636
1642
  if (jsonOutput && result.kind !== "session-stop"
@@ -2431,7 +2437,10 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2431
2437
  await ensureFileTaskController(home, { environment: process.env });
2432
2438
  if (paneRunning
2433
2439
  || (active !== undefined
2434
- && active.status === "active")) {
2440
+ && active.status === "active")
2441
+ || (sessionSet?.providerBinding?.run != null
2442
+ && ["submitting", "accepted", "delivery-unknown"].includes(sessionSet.providerBinding.run.status))
2443
+ || (sessionSet?.providerBinding?.goal != null && sessionSet.providerBinding.goal.status !== "complete")) {
2435
2444
  await runtime.stopGlobalRoleSession(role.name);
2436
2445
  }
2437
2446
  applyOperatorSessionControl(control, store);
@@ -202,7 +202,7 @@ export function renderRuntimeIdentitySection(input) {
202
202
  ` Source commit ${build.sourceCommit}`,
203
203
  ` Node ${build.nodeVersion} (${build.platform})`,
204
204
  ` Storage version ${storage.storageVersion} (status ${storage.storageStatus}, minimum ${storage.minimumStorageVersion}) · backend ${storage.configuredBackend} · worker ${storage.workerEnabled ? "on" : "off"}`,
205
- ` Store files state.json ${storage.physicalStateJson.present ? "present" : "absent"} · yui.db ${storage.physicalDatabase.present ? "present" : "absent"}${storage.physicalDatabase.wal ? " +WAL" : ""}${storage.physicalDatabase.present && storage.physicalDatabase.health !== UNSUPPORTED ? ` (${storage.physicalDatabase.health})` : ""}`
205
+ ` Store files yui.db ${storage.physicalDatabase.present ? "present" : "absent"}${storage.physicalDatabase.wal ? " +WAL" : ""}${storage.physicalDatabase.present && storage.physicalDatabase.health !== UNSUPPORTED ? ` (${storage.physicalDatabase.health})` : ""}`
206
206
  ];
207
207
  for (const finding of storage.findings) {
208
208
  lines.push(` ! ${finding.severity === "contradiction" ? "CONTRADICTION" : finding.severity === "needs-repair" ? "NEEDS-REPAIR" : "warning"} ${finding.code}: ${finding.message}`, ` remediation: ${finding.remediation}`);
@@ -258,8 +258,8 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
258
258
  }
259
259
  if (report.storage.status === "ok" && report.storage.data !== undefined) {
260
260
  const storage = report.storage.data;
261
- lines.push("", `Storage: backend ${storage.backend} · state.json ${formatBytes(storage.stateJsonBytes)} · yui.db ${formatBytes(storage.databaseBytes)}`
262
- + ` · runtime/ ${formatBytes(storage.runtimeDirBytes)} · deployments ${formatBytes(storage.deploymentsBytes)}`);
261
+ lines.push("", `Storage: backend ${storage.backend} · yui.db ${formatBytes(storage.databaseBytes)}`
262
+ + ` · runtime/ ${formatBytes(storage.runtimeDirBytes)}`);
263
263
  }
264
264
  else {
265
265
  lines.push("", ...sectionError("storage", report));
@@ -238,7 +238,7 @@ async function cloneProject(args, store, options) {
238
238
  // A remote-URL binding is Home-managed by default: its canonical repository
239
239
  // lives below the persistent Home, so the runtime never depends on a
240
240
  // user-controlled checkout path. --external is an explicit opt-in to the
241
- // legacy clone-inside-the-workspace mode.
241
+ // user-owned clone-inside-the-workspace mode.
242
242
  let destination;
243
243
  let ownership;
244
244
  if (parsed.external) {
@@ -341,55 +341,26 @@ export function createReleaseActivatePorts(overrides = {}) {
341
341
  }),
342
342
  runPreflight: overrides.runPreflight ?? ((releaseDir, home) => {
343
343
  const cli = join(releaseDir, "dist", "cli.js");
344
- const result = spawnSync(process.execPath, [cli, "doctor", "--json"], {
344
+ // The target owns current-record and database integrity validation.
345
+ // Activation never migrates; a valid but older Home still blocks here.
346
+ const result = spawnSync(process.execPath, [cli, "--json", "upgrade", "--update-preflight"], {
345
347
  env: { ...process.env, YUI_HOME: home, NO_COLOR: "1" },
346
348
  encoding: "utf8",
347
349
  timeout: 60_000
348
350
  });
349
351
  let report;
350
352
  try {
351
- const envelope = JSON.parse(result.stdout);
352
- report = envelope.data;
353
+ report = JSON.parse(result.stdout);
353
354
  }
354
355
  catch (error) {
355
356
  throw new Error(`Release preflight produced no JSON (exit ${result.status}): `
356
357
  + `${messageOf(error)} ${result.stderr.trim()}`);
357
358
  }
358
- // The handover preflight must prove the exact current Home contract.
359
- // Older, newer, malformed, or incomplete Homes all fail closed and are
360
- // never normalized by release activation.
361
- const checks = report?.checks;
362
- if (!Array.isArray(checks)) {
363
- throw new Error("Release preflight report has no storage checks.");
364
- }
365
- const required = ["storage compatibility"];
366
- const failed = required.filter((name) => {
367
- const check = checks.find((candidate) => candidate !== null
368
- && typeof candidate === "object"
369
- && candidate.name === name);
370
- return check === undefined
371
- || check.status !== "ok";
372
- });
373
- if (failed.length > 0) {
374
- throw new Error(`Release preflight storage compatibility checks failed: ${failed.join(", ")}.`);
375
- }
376
- // Doctor proves the target's storage, not the pinned code in existing
377
- // Hosts. Ask the target's upgrade boundary for its independent Host
378
- // protocol proof as well; release activation itself still never migrates.
379
- const hostCheck = spawnSync(process.execPath, [cli, "--json", "upgrade", "--update-preflight"], {
380
- env: { ...process.env, YUI_HOME: home, NO_COLOR: "1" },
381
- encoding: "utf8", timeout: 60_000
382
- });
383
- let preflight;
384
- try {
385
- preflight = JSON.parse(hostCheck.stdout).data;
386
- }
387
- catch {
388
- throw new Error(`Release Host preflight produced no JSON: ${hostCheck.stderr.trim()}`);
389
- }
390
- if (hostCheck.status !== 0 || preflight?.outcome !== "update-preflight"
391
- || preflight.status !== "already-current") {
392
- throw new Error(`Release Host compatibility preflight failed: ${preflight?.message ?? preflight?.status ?? "unknown"}. ${preflight?.action ?? ""}`);
359
+ if (result.status !== 0 || report?.ok !== true
360
+ || report.data?.outcome !== "update-preflight"
361
+ || report.data.status !== "already-current") {
362
+ throw new Error(`Release storage preflight failed: ${report?.data?.message ?? report?.data?.status ?? "invalid response"}. `
363
+ + `${report?.data?.action ?? result.stderr.trim()}`);
393
364
  }
394
365
  }),
395
366
  killOwnedProcess: overrides.killOwnedProcess ?? ((owner) => {