@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.
- package/README.md +20 -3
- package/dist/cli/commandCatalog.js +20 -3
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +147 -44
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/releaseCommands.js +44 -8
- package/dist/commands/taskCommands.js +141 -9
- package/dist/commands/taskContextCommand.js +5 -3
- package/dist/commands/taskNextActionCommand.js +4 -2
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +77 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +8 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/fileRoleLaunchPlanner.js +21 -34
- package/dist/release/releaseHandover.js +2 -2
- package/dist/release/runtimeRelease.js +4 -3
- package/dist/review/reviewOutcomeClassifier.js +15 -4
- package/dist/review/taskFinalReviewContractRebind.js +28 -11
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +3 -1
- package/dist/scheduler/actionability.js +4 -2
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/storage/sqliteStore.js +12 -4
- package/dist/storage/taskStore.js +6 -4
- package/dist/task/nextAction.js +8 -3
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +20 -1
- package/package.json +1 -1
- package/skills/yui-operator/SKILL.md +7 -0
- 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.
|
|
770
|
-
|
|
771
|
-
|
|
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
|
}
|
package/dist/cli/updatePorts.js
CHANGED
|
@@ -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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1449
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1463
|
-
|
|
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
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (!
|
|
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) {
|