@zq-silk/yui 0.8.6 → 0.8.8

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 (36) hide show
  1. package/README.md +20 -3
  2. package/dist/cli/commandCatalog.js +20 -3
  3. package/dist/cli/updatePorts.js +6 -0
  4. package/dist/cli.js +147 -44
  5. package/dist/commands/globalRoleCommands.js +8 -4
  6. package/dist/commands/releaseCommands.js +44 -8
  7. package/dist/commands/taskCommands.js +141 -9
  8. package/dist/commands/taskContextCommand.js +5 -3
  9. package/dist/commands/taskNextActionCommand.js +4 -2
  10. package/dist/commands/taskOverviewCommand.js +2 -1
  11. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  12. package/dist/context/runContextPack.js +9 -5
  13. package/dist/context/sessionBootstrapManifest.js +77 -11
  14. package/dist/context/wakeNotification.js +5 -3
  15. package/dist/controller/clientRuntime.js +8 -8
  16. package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
  17. package/dist/executor/agentExecutor.js +4 -4
  18. package/dist/executor/fileRoleLaunchPlanner.js +21 -34
  19. package/dist/release/releaseHandover.js +2 -2
  20. package/dist/release/runtimeRelease.js +4 -3
  21. package/dist/review/reviewOutcomeClassifier.js +15 -4
  22. package/dist/review/taskFinalReviewContractRebind.js +28 -11
  23. package/dist/runtime/exactControlPlane.js +47 -37
  24. package/dist/runtime/firstProgressStopLoss.js +3 -1
  25. package/dist/scheduler/actionability.js +4 -2
  26. package/dist/scheduler/activeTaskProgress.js +2 -1
  27. package/dist/scheduler/taskExecutionProjection.js +13 -4
  28. package/dist/storage/sqliteStore.js +12 -4
  29. package/dist/storage/taskStore.js +6 -4
  30. package/dist/task/nextAction.js +8 -3
  31. package/dist/task/taskRecordRetirement.js +72 -0
  32. package/dist/workItem/workItem.js +6 -4
  33. package/i18n/README.zh-CN.md +20 -1
  34. package/package.json +1 -1
  35. package/skills/yui-operator/SKILL.md +7 -0
  36. package/skills/yui-runtime/SKILL.md +6 -6
package/README.md CHANGED
@@ -633,6 +633,21 @@ and `task work retire <task>/<work> --summary "..."` to retire obsolete work,
633
633
  optionally naming a replacement. WorkItem and Integration
634
634
  worktrees and check logs remain available as evidence until explicit cleanup.
635
635
 
636
+ Incorrect historical directives and execution attempts can be removed from
637
+ operational projections without deleting their audit records:
638
+
639
+ ```sh
640
+ yui task message retire <task>/<message> --reason "Superseded instruction"
641
+ yui task run retire <task>/<agent-run> --reason "Invalid launch record"
642
+ ```
643
+
644
+ These commands append a retirement fact. Lists and audit views retain the
645
+ original Message, WorkItem, or AgentRun and mark it retired; managed Run context,
646
+ actionability, recovery, review evidence, and scheduling ignore it. Retiring
647
+ an active AgentRun first terminalizes that exact Run, and retirement is
648
+ idempotent. Only the user or global Operator may retire Messages or AgentRuns;
649
+ WorkItems may also be retired by their Task Leader.
650
+
636
651
  For long-running Tasks, the Leader keeps Yui—not a native transcript—as the
637
652
  recovery authority. The Task Brief owns the overall technical approach,
638
653
  including how coordinated Project changes fit together. WorkItems own the
@@ -766,9 +781,11 @@ running AgentRun and its native Session continue under their immutable
766
781
  effective snapshot even if the Role is edited or switched. Resume is allowed
767
782
  only when the complete effective snapshot and workspace remain compatible;
768
783
  otherwise Yui starts a new Session after the old process has stopped and keeps
769
- the terminal Session's immutable effective snapshot in history. Until that
770
- process terminates, exact control-plane wakes continue through its actual
771
- snapshot instead of applying desired drift as a hot change.
784
+ the terminal Session's immutable effective snapshot in history. Managed
785
+ Sessions invoke the ordinary `yui` command; their Manifest and durable
786
+ Role/Run fences authenticate scope while protocol and storage compatibility
787
+ allow a CLI package or Controller upgrade in place. Exact internal callbacks
788
+ remain fenced to their originating runtime snapshot.
772
789
 
773
790
  Use `yui config role unbind <global-role> <agent-id>` or `yui task role unbind <task-id> <role> <agent-id>` to retire a dormant binding. The active binding and any non-stopped native session are rejected; a stopped session record is removed atomically with the binding.
774
791
 
@@ -363,7 +363,7 @@ const taskChildren = [
363
363
  {
364
364
  name: "message",
365
365
  summary: "Manage durable Task messages.",
366
- sections: [{ id: "manage", title: "Commands", entries: ["send", "list"] }],
366
+ sections: [{ id: "manage", title: "Commands", entries: ["send", "list", "retire"] }],
367
367
  children: [
368
368
  {
369
369
  name: "send",
@@ -381,6 +381,12 @@ const taskChildren = [
381
381
  summary: "List Task messages.",
382
382
  usage: "yui task message list <id> [--after <timestamp>] [--limit <n>]",
383
383
  options: ["--after", "--limit"]
384
+ },
385
+ {
386
+ name: "retire",
387
+ summary: "Retire an incorrect historical Task Message without deleting its audit record.",
388
+ usage: "yui task message retire <task>/<message> --reason <text>",
389
+ options: ["--reason"]
384
390
  }
385
391
  ]
386
392
  },
@@ -689,7 +695,7 @@ const taskChildren = [
689
695
  {
690
696
  name: "run",
691
697
  summary: "Inspect and control Task Role Agent Runs.",
692
- sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "settle", "recover", "yield", "context", "checkpoint"] }],
698
+ sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "settle", "recover", "yield", "context", "checkpoint", "retire"] }],
693
699
  children: [
694
700
  { name: "list", summary: "List Runs for a work item.", usage: "yui task run list <task>/<work>" },
695
701
  {
@@ -749,6 +755,12 @@ const taskChildren = [
749
755
  options: ["--note", "--note-file"],
750
756
  fileOptions: ["--note-file"],
751
757
  hidden: true
758
+ },
759
+ {
760
+ name: "retire",
761
+ summary: "Retire an incorrect historical Agent Run without deleting its audit record.",
762
+ usage: "yui task run retire <task>/<run> --reason <text>",
763
+ options: ["--reason"]
752
764
  }
753
765
  ]
754
766
  },
@@ -1405,7 +1417,7 @@ export const ROOT_COMMAND = buildNode({
1405
1417
  sections: [{
1406
1418
  id: "callbacks",
1407
1419
  title: "Callbacks",
1408
- entries: ["session-notify", "runtime-hook", "agent-host"]
1420
+ entries: ["session-notify", "runtime-hook", "agent-host", "session-cli-refresh"]
1409
1421
  }],
1410
1422
  children: [
1411
1423
  {
@@ -1422,6 +1434,11 @@ export const ROOT_COMMAND = buildNode({
1422
1434
  name: "runtime-hook",
1423
1435
  summary: "Record a managed Agent Driver observation from stdin.",
1424
1436
  usage: "yui internal runtime-hook"
1437
+ },
1438
+ {
1439
+ name: "session-cli-refresh",
1440
+ summary: "Refresh legacy managed Session CLI wrappers after an update.",
1441
+ usage: "yui internal session-cli-refresh"
1425
1442
  }
1426
1443
  ]
1427
1444
  }
@@ -199,6 +199,12 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
199
199
  + `but the staged/verified artifact was ${staged.version}. Refusing to trust a `
200
200
  + "different build than the one that passed preflight.");
201
201
  }
202
+ // Existing managed Sessions may have been created by a release that
203
+ // embedded an exact CLI path/version in its wrapper. Convert those
204
+ // authenticated, Manifest-referenced wrappers before the replacement
205
+ // Controller starts so the update cannot strand a live Session.
206
+ const sessionCliRefresh = run(activeBinary, ["--json", "internal", "session-cli-refresh"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
207
+ assertSpawnOk(sessionCliRefresh, "refresh managed Session CLI wrappers");
202
208
  // Retain the exact path used by both doctor and version verification. The
203
209
  // replacement start must not resolve UPDATE_CLI_PATH or npm again.
204
210
  verifiedActivatedBinary = activeBinary;
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
4
5
  import { createInterface } from "node:readline/promises";
@@ -48,7 +49,7 @@ import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
48
49
  import { createUpdatePorts } from "./cli/updatePorts.js";
49
50
  import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
50
51
  import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
51
- import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
52
+ import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
52
53
  import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
53
54
  import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
54
55
  import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
@@ -81,8 +82,8 @@ import { AgentConfigurationCatalogService } from "./executor/agentConfigurationC
81
82
  import { TmuxWebTerminalService } from "./web/tmuxWebTerminal.js";
82
83
  import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
83
84
  import { YUI_VERSION, yuiVersionIdentity } from "./version.js";
84
- import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactControlPlanePreflight, assertExactTaskRuntimeEnvironment, assertExactTaskRuntimeState, exactControlPlaneDigest, extractExactControlArgument, parseExactControlPlaneDescriptor } from "./runtime/exactControlPlane.js";
85
- import { readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
85
+ import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertCompatibleControlPlanePreflight, assertExactControlPlanePreflight, assertExactTaskRuntimeEnvironment, assertExactTaskRuntimeState, exactControlPlaneDigest, extractExactControlArgument, createExactControlPlaneDescriptor, parseExactControlPlaneDescriptor } from "./runtime/exactControlPlane.js";
86
+ import { readSessionBootstrapManifest, refreshManagedSessionCliWrappers } from "./context/sessionBootstrapManifest.js";
86
87
  import { builtinAgentDriverRegistry } from "./runtime/builtinAgentDrivers.js";
87
88
  import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
88
89
  import { prepareTaskFinalReviewContractRebindProof, resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractRebind.js";
@@ -112,6 +113,22 @@ void main().catch((error) => {
112
113
  process.exitCode = 5;
113
114
  });
114
115
  export async function main() {
116
+ const delegatedDriver = explicitReleaseActivationDriver();
117
+ if (delegatedDriver !== null) {
118
+ const delegated = spawnSync(process.execPath, [delegatedDriver, ...process.argv.slice(2)], {
119
+ cwd: process.cwd(),
120
+ env: process.env,
121
+ stdio: "inherit"
122
+ });
123
+ if (delegated.error !== undefined) {
124
+ throw runtimeError(`Target release activation driver could not start: ${delegated.error.message}`);
125
+ }
126
+ if (delegated.signal !== null) {
127
+ throw runtimeError(`Target release activation driver stopped by signal ${delegated.signal}.`);
128
+ }
129
+ process.exitCode = delegated.status ?? 5;
130
+ return;
131
+ }
115
132
  const { contract: taskFinalReviewContract, verifiedStore, controlPlane: exactCurrentControlPlane } = await preflightManagedTaskControlPlane();
116
133
  if (args.length === 0) {
117
134
  emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
@@ -264,6 +281,16 @@ export async function main() {
264
281
  return;
265
282
  }
266
283
  if (args[0] === "internal") {
284
+ if (args[1] === "session-cli-refresh" && args.length === 2) {
285
+ if (process.env.YUI_SESSION_SCOPE === "task"
286
+ || (process.env.YUI_SESSION_SCOPE === "global" && process.env.YUI_ROLE !== "operator")) {
287
+ throw usageError("Managed Session CLI refresh may be run only by the user or global Operator.");
288
+ }
289
+ const result = refreshManagedSessionCliWrappers(home);
290
+ emit(`Refreshed ${result.refreshed} legacy Session CLI wrapper(s); `
291
+ + `${result.current} already current, ${result.skipped} skipped.`, false, result);
292
+ return;
293
+ }
267
294
  if (args[1] === "agent-host" && args.length === 4) {
268
295
  process.exitCode = await runAgentHost({
269
296
  home,
@@ -1350,6 +1377,15 @@ export async function main() {
1350
1377
  }
1351
1378
  throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
1352
1379
  }
1380
+ function explicitReleaseActivationDriver() {
1381
+ if (args.length !== 3
1382
+ || args[0] !== "release"
1383
+ || args[1] !== "activate"
1384
+ || args[2].startsWith("-")) {
1385
+ return null;
1386
+ }
1387
+ return resolveReleaseActivationDriver(resolveYuiHome(process.env), args[2], fileURLToPath(import.meta.url));
1388
+ }
1353
1389
  async function preflightManagedTaskControlPlane() {
1354
1390
  if (exactControlInvocation.error !== undefined) {
1355
1391
  throw new Error(exactControlInvocation.error);
@@ -1364,7 +1400,8 @@ async function preflightManagedTaskControlPlane() {
1364
1400
  throw new Error("Exact control-plane invocation requires both frozen descriptors in a managed Task runtime.");
1365
1401
  }
1366
1402
  if (!exactRuntime) {
1367
- if (exactControlInvocation.digest !== undefined) {
1403
+ if (exactControlInvocation.digest !== undefined
1404
+ || process.env.YUI_SESSION_SCOPE === "global") {
1368
1405
  return await preflightManagedGlobalControlPlane(exactControlInvocation.digest);
1369
1406
  }
1370
1407
  if (taskFinalReviewInvocation.request !== undefined) {
@@ -1379,24 +1416,42 @@ async function preflightManagedTaskControlPlane() {
1379
1416
  throw new Error("Exact control-plane invocation is required for this managed Task runtime.");
1380
1417
  }
1381
1418
  const control = parseExactControlPlaneDescriptor(serializedControl);
1382
- const internalCallback = args[0] === "internal";
1383
- if (!internalCallback && exactControlInvocation.digest === undefined) {
1384
- throw new Error("Exact control-plane invocation is required; bare `yui` and PATH launchers are not valid in a managed Task runtime.");
1385
- }
1386
- const digest = exactControlInvocation.digest ?? exactControlPlaneDigest(control);
1387
- await assertExactControlPlanePreflight({
1388
- serializedDescriptor: serializedControl,
1389
- digest,
1390
- actualExecutable: process.execPath,
1391
- actualCliEntry: fileURLToPath(import.meta.url),
1392
- actualHome: resolveYuiHome(process.env)
1393
- }, {
1394
- // Provider callbacks must remain able to append their immutable inbox fact
1395
- // while the Controller is offline. They still validate executable, CLI,
1396
- // Home, build, schema, and the exact Task runtime envelope first.
1397
- checkController: !internalCallback
1398
- });
1399
- const runtime = assertExactTaskRuntimeEnvironment(serializedRuntime, process.env, digest, control.yuiHome);
1419
+ const internalCallback = args[0] === "internal"
1420
+ && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
1421
+ const home = resolveYuiHome(process.env);
1422
+ assertManagedSessionManifest(home, "task");
1423
+ if (resolve(control.yuiHome) !== resolve(home)) {
1424
+ throw new Error("Managed Task control-plane descriptor belongs to another YUI_HOME.");
1425
+ }
1426
+ const frozenDigest = exactControlPlaneDigest(control);
1427
+ const exactCommand = internalCallback || exactControlInvocation.digest !== undefined;
1428
+ let commandControl;
1429
+ let digest;
1430
+ if (exactCommand) {
1431
+ digest = exactControlInvocation.digest ?? frozenDigest;
1432
+ await assertExactControlPlanePreflight({
1433
+ serializedDescriptor: serializedControl,
1434
+ digest,
1435
+ actualExecutable: process.execPath,
1436
+ actualCliEntry: fileURLToPath(import.meta.url),
1437
+ actualHome: home
1438
+ }, {
1439
+ // Provider callbacks must remain able to append their immutable inbox fact
1440
+ // while the Controller is offline. They still validate executable, CLI,
1441
+ // Home, schema, and the exact Task runtime envelope first.
1442
+ checkController: !internalCallback
1443
+ });
1444
+ commandControl = control;
1445
+ }
1446
+ else {
1447
+ await assertCompatibleControlPlanePreflight({ actualHome: home });
1448
+ // Package/build identity is deliberately not part of ordinary Session
1449
+ // continuity. Keep contracts on the Session's frozen digest while the
1450
+ // current CLI is proven protocol/storage compatible.
1451
+ commandControl = control;
1452
+ digest = frozenDigest;
1453
+ }
1454
+ const runtime = assertExactTaskRuntimeEnvironment(serializedRuntime, process.env, frozenDigest, control.yuiHome);
1400
1455
  const runtimeDriverCallback = args[0] === "internal"
1401
1456
  && args[1] === "runtime-hook"
1402
1457
  && process.env.YUI_DRIVER_ID !== undefined
@@ -1417,7 +1472,7 @@ async function preflightManagedTaskControlPlane() {
1417
1472
  if (request === undefined) {
1418
1473
  return {
1419
1474
  contract: undefined,
1420
- controlPlane: { digest, descriptor: control },
1475
+ controlPlane: { digest, descriptor: commandControl },
1421
1476
  verifiedStore
1422
1477
  };
1423
1478
  }
@@ -1427,13 +1482,22 @@ async function preflightManagedTaskControlPlane() {
1427
1482
  if (request.taskId !== runtime.taskId) {
1428
1483
  throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
1429
1484
  }
1485
+ const recordedContract = resolveRecordedTaskFinalReviewContract(runtime.taskId, verifiedStore.listWorkItems(runtime.taskId), verifiedStore.listReviewRounds(runtime.taskId), verifiedStore.listEvents(runtime.taskId))?.effective;
1486
+ if (recordedContract !== undefined
1487
+ && recordedContract.reviewerRoleName !== request.reviewerRoleName) {
1488
+ throw new Error(`Task final-review Reviewer mismatch: expected ${recordedContract.reviewerRoleName}, `
1489
+ + `found ${request.reviewerRoleName}.`);
1490
+ }
1430
1491
  return {
1431
1492
  contract: createTaskFinalReviewContract({
1432
1493
  taskId: runtime.taskId,
1433
1494
  reviewerRoleName: request.reviewerRoleName,
1434
- controlPlaneDigest: digest
1495
+ // Once Task evidence establishes a contract, a compatible Session or
1496
+ // CLI replacement presents that same capability. Package/build identity
1497
+ // must not force a release rebind or invalidate delivery evidence.
1498
+ controlPlaneDigest: recordedContract?.controlPlaneDigest ?? digest
1435
1499
  }),
1436
- controlPlane: { digest, descriptor: control },
1500
+ controlPlane: { digest, descriptor: commandControl },
1437
1501
  verifiedStore
1438
1502
  };
1439
1503
  }
@@ -1445,38 +1509,77 @@ async function preflightManagedGlobalControlPlane(digest) {
1445
1509
  throw new Error("Task final-review contract establishment requires a verified exact Task control-plane invocation.");
1446
1510
  }
1447
1511
  const manifestPath = process.env.YUI_SESSION_MANIFEST;
1448
- const sessionCliPath = process.env.YUI_SESSION_CLI;
1449
- if (manifestPath === undefined || sessionCliPath === undefined) {
1450
- throw new Error("Exact global control-plane invocation requires its Session Manifest and CLI.");
1512
+ if (manifestPath === undefined) {
1513
+ throw new Error("Managed global control-plane invocation requires its Session Manifest.");
1451
1514
  }
1452
- const manifest = readSessionBootstrapManifest(manifestPath);
1515
+ const home = resolveYuiHome(process.env);
1516
+ const manifest = assertManagedSessionManifest(home, "global");
1453
1517
  const expectedRoleKind = process.env.YUI_ROLE === "operator" ? "operator" : "global";
1454
1518
  if (manifest.owner.scope !== "global"
1455
- || manifest.roleKind !== expectedRoleKind
1456
- || resolve(manifest.controlPlane.sessionCliPath) !== resolve(sessionCliPath)
1457
- || manifest.controlPlane.digest !== digest) {
1458
- throw new Error("Exact global control-plane invocation does not match its Session Manifest.");
1519
+ || manifest.roleKind !== expectedRoleKind) {
1520
+ throw new Error("Managed global invocation does not match its Session Manifest.");
1459
1521
  }
1460
1522
  const serializedDescriptor = readFileSync(manifest.controlPlane.descriptorPath, "utf8");
1461
1523
  const descriptor = parseExactControlPlaneDescriptor(serializedDescriptor);
1462
- if (exactControlPlaneDigest(descriptor) !== digest
1463
- || resolve(manifest.controlPlane.descriptorPath) !== resolve(join(descriptor.yuiHome, "runtime", "control-plane", `${digest}.json`))
1524
+ if (resolve(descriptor.yuiHome) !== resolve(home)) {
1525
+ throw new Error("Managed global control-plane descriptor belongs to another YUI_HOME.");
1526
+ }
1527
+ const frozenDigest = exactControlPlaneDigest(descriptor);
1528
+ if (frozenDigest !== manifest.controlPlane.digest
1529
+ || resolve(manifest.controlPlane.descriptorPath) !== resolve(join(descriptor.yuiHome, "runtime", "control-plane", `${frozenDigest}.json`))
1464
1530
  || resolve(manifestPath) !== resolve(join(descriptor.yuiHome, "runtime", "session-manifests", `${manifest.digest}.json`))) {
1465
1531
  throw new Error("Exact global Session Manifest does not match its control-plane descriptor.");
1466
1532
  }
1467
- await assertExactControlPlanePreflight({
1468
- serializedDescriptor,
1469
- digest,
1470
- actualExecutable: process.execPath,
1471
- actualCliEntry: fileURLToPath(import.meta.url),
1472
- actualHome: resolveYuiHome(process.env)
1473
- });
1533
+ let commandControl;
1534
+ let commandDigest;
1535
+ if (digest !== undefined) {
1536
+ await assertExactControlPlanePreflight({
1537
+ serializedDescriptor,
1538
+ digest,
1539
+ actualExecutable: process.execPath,
1540
+ actualCliEntry: fileURLToPath(import.meta.url),
1541
+ actualHome: home
1542
+ });
1543
+ commandControl = descriptor;
1544
+ commandDigest = digest;
1545
+ }
1546
+ else {
1547
+ await assertCompatibleControlPlanePreflight({ actualHome: home });
1548
+ commandControl = currentInvocationControlPlane(home);
1549
+ commandDigest = exactControlPlaneDigest(commandControl);
1550
+ }
1474
1551
  return {
1475
1552
  contract: undefined,
1476
- controlPlane: { digest, descriptor },
1477
- verifiedStore: openCompatibleFileTaskStore(descriptor.yuiHome)
1553
+ controlPlane: { digest: commandDigest, descriptor: commandControl },
1554
+ verifiedStore: openCompatibleFileTaskStore(home)
1478
1555
  };
1479
1556
  }
1557
+ function currentInvocationControlPlane(home) {
1558
+ return createExactControlPlaneDescriptor({
1559
+ executable: process.execPath,
1560
+ cliEntry: fileURLToPath(import.meta.url),
1561
+ yuiHome: home
1562
+ });
1563
+ }
1564
+ function assertManagedSessionManifest(home, scope) {
1565
+ const manifestPath = process.env.YUI_SESSION_MANIFEST;
1566
+ if (manifestPath === undefined) {
1567
+ throw new Error("Managed control-plane invocation requires its Session Manifest.");
1568
+ }
1569
+ const manifest = readSessionBootstrapManifest(manifestPath);
1570
+ if (manifest.owner.scope !== scope) {
1571
+ throw new Error("Managed invocation scope does not match its Session Manifest.");
1572
+ }
1573
+ if (scope === "task" && (manifest.owner.scope !== "task"
1574
+ || manifest.owner.taskId !== process.env.YUI_TASK_ID)) {
1575
+ throw new Error("Managed Task invocation does not match its Session Manifest owner.");
1576
+ }
1577
+ const expectedPath = resolve(home, "runtime", "session-manifests", `${manifest.digest}.json`);
1578
+ if (resolve(manifestPath) !== expectedPath) {
1579
+ throw new Error("Managed Session Manifest path is outside this YUI_HOME.");
1580
+ }
1581
+ return manifest;
1582
+ }
1480
1583
  function cleanupCliError(error, fallbackResource) {
1481
1584
  if (error instanceof WorkspaceCleanupBlockedError) {
1482
1585
  return usageError(error.message, undefined, cleanupBlockedDetails(error.reason, error.resource, error.retryable));
@@ -36,9 +36,8 @@ function roleContext(args, store, options) {
36
36
  if (environment.YUI_SESSION_SCOPE !== "global" || environment.YUI_ROLE !== name) {
37
37
  throw usageError("Managed GlobalRole context is outside the exact Session authority.");
38
38
  }
39
- if (environment.YUI_SESSION_MANIFEST === undefined
40
- || environment.YUI_SESSION_CLI === undefined) {
41
- throw usageError("Managed GlobalRole context requires the exact Session Manifest and CLI.");
39
+ if (environment.YUI_SESSION_MANIFEST === undefined) {
40
+ throw usageError("Managed GlobalRole context requires its Session Manifest.");
42
41
  }
43
42
  }
44
43
  const role = requireRole(name, store);
@@ -81,7 +80,12 @@ function roleContext(args, store, options) {
81
80
  : ["perform-global-role-request"]
82
81
  },
83
82
  sessionManifestPath: environment.YUI_SESSION_MANIFEST,
84
- sessionCliPath: environment.YUI_SESSION_CLI
83
+ cliCommand: "yui",
84
+ // Compatibility hint for already-running Sessions. New instructions use
85
+ // the stable command name and do not bind behavior to this wrapper path.
86
+ ...(environment.YUI_SESSION_CLI === undefined
87
+ ? {}
88
+ : { legacySessionCliPath: environment.YUI_SESSION_CLI })
85
89
  });
86
90
  if (options.jsonOutput === true)
87
91
  return `${JSON.stringify(context)}\n`;
@@ -8,7 +8,7 @@ import { join, resolve } from "node:path";
8
8
  import { callController, ControllerClientError } from "../core/controllerClient.js";
9
9
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
10
10
  import { activateRelease } from "../release/releaseHandover.js";
11
- import { assertReleaseIsNotWorktreeOrLinked, readActiveReleasePointer, readReleaseManifest, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
11
+ import { assertReleaseIsNotWorktreeOrLinked, detectRunningRelease, readActiveReleasePointer, readReleaseManifest, readSmokeReceipt, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
12
12
  /**
13
13
  * Installs a runtime package directory into the Home's immutable release
14
14
  * tree. The source must carry a release manifest, must not be a Git worktree
@@ -123,7 +123,7 @@ export function runReleaseList(home) {
123
123
  version: manifest.version,
124
124
  buildId: manifest.buildId,
125
125
  packageDigest: manifest.packageDigest,
126
- smoke: existsSync(join(releaseDir, "smoke-receipt.json"))
126
+ smoke: hasMatchingSmokeReceipt(releaseDir, manifest)
127
127
  };
128
128
  }
129
129
  catch {
@@ -142,6 +142,8 @@ export function runReleaseList(home) {
142
142
  /** Resolves an installed release by directory name or build ID. */
143
143
  export function resolveInstalledRelease(home, releaseId) {
144
144
  const directory = releasesDirectory(home);
145
+ if (!existsSync(directory))
146
+ return null;
145
147
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
146
148
  if (!entry.isDirectory())
147
149
  continue;
@@ -156,18 +158,45 @@ export function resolveInstalledRelease(home, releaseId) {
156
158
  + "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
157
159
  }
158
160
  }
161
+ const releaseDir = join(directory, entry.name);
162
+ let manifest;
159
163
  try {
160
- const manifest = readReleaseManifest(join(directory, entry.name));
161
- if (manifest.buildId === releaseId) {
162
- return { releaseDir: join(directory, entry.name), manifest };
163
- }
164
+ manifest = readReleaseManifest(releaseDir);
164
165
  }
165
166
  catch {
166
167
  continue;
167
168
  }
169
+ if (manifest.buildId !== releaseId)
170
+ continue;
171
+ try {
172
+ return { releaseDir, manifest: verifyReleaseIntegrity(releaseDir) };
173
+ }
174
+ catch (error) {
175
+ throw new Error(`Release ${releaseId} failed integrity verification and may have drifted: `
176
+ + `${error instanceof Error ? error.message : String(error)}. `
177
+ + "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
178
+ }
168
179
  }
169
180
  return null;
170
181
  }
182
+ /**
183
+ * Selects the verified target release CLI as the activation driver. Ordinary
184
+ * commands remain on the globally installed CLI; only an explicit target
185
+ * activation crosses into the target package so its handover deadlines and
186
+ * protocol own the complete operation.
187
+ */
188
+ export function resolveReleaseActivationDriver(home, releaseId, currentCliPath) {
189
+ const resolved = resolveInstalledRelease(home, releaseId);
190
+ if (resolved === null)
191
+ return null;
192
+ if (!hasMatchingSmokeReceipt(resolved.releaseDir, resolved.manifest))
193
+ return null;
194
+ const running = detectRunningRelease(currentCliPath);
195
+ if (running !== null && resolve(running.releaseDir) === resolve(resolved.releaseDir)) {
196
+ return null;
197
+ }
198
+ return join(resolved.releaseDir, "dist", "cli.js");
199
+ }
171
200
  /** Runs the atomic Controller handover for one installed release. */
172
201
  export async function runReleaseActivate(home, releaseId, options = {}) {
173
202
  let resolved;
@@ -192,11 +221,11 @@ export async function runReleaseActivate(home, releaseId, options = {}) {
192
221
  recoverable: true
193
222
  };
194
223
  }
195
- if (!existsSync(join(resolved.releaseDir, "smoke-receipt.json"))) {
224
+ if (!hasMatchingSmokeReceipt(resolved.releaseDir, resolved.manifest)) {
196
225
  return {
197
226
  outcome: "aborted",
198
227
  phase: "resolve",
199
- message: `Release has no smoke receipt: ${releaseId}.`,
228
+ message: `Release has no matching smoke receipt: ${releaseId}.`,
200
229
  action: "Reinstall the release; the smoke receipt is written at install time.",
201
230
  recoverable: true
202
231
  };
@@ -217,6 +246,13 @@ export async function runReleaseActivate(home, releaseId, options = {}) {
217
246
  : { dualOwnerGraceMs: options.dualOwnerGraceMs })
218
247
  });
219
248
  }
249
+ function hasMatchingSmokeReceipt(releaseDir, manifest) {
250
+ const receipt = readSmokeReceipt(releaseDir);
251
+ return receipt !== null
252
+ && receipt.version === manifest.version
253
+ && receipt.buildId === manifest.buildId
254
+ && receipt.packageDigest === manifest.packageDigest;
255
+ }
220
256
  /** Renders the install result as concise CLI text. */
221
257
  export function renderReleaseInstallResult(result) {
222
258
  switch (result.outcome) {