@zq-silk/yui 0.8.6 → 0.8.7
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 +120 -43
- package/dist/commands/globalRoleCommands.js +8 -4
- 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/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
|
@@ -81,8 +81,8 @@ import { AgentConfigurationCatalogService } from "./executor/agentConfigurationC
|
|
|
81
81
|
import { TmuxWebTerminalService } from "./web/tmuxWebTerminal.js";
|
|
82
82
|
import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
|
|
83
83
|
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";
|
|
84
|
+
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertCompatibleControlPlanePreflight, assertExactControlPlanePreflight, assertExactTaskRuntimeEnvironment, assertExactTaskRuntimeState, exactControlPlaneDigest, extractExactControlArgument, createExactControlPlaneDescriptor, parseExactControlPlaneDescriptor } from "./runtime/exactControlPlane.js";
|
|
85
|
+
import { readSessionBootstrapManifest, refreshManagedSessionCliWrappers } from "./context/sessionBootstrapManifest.js";
|
|
86
86
|
import { builtinAgentDriverRegistry } from "./runtime/builtinAgentDrivers.js";
|
|
87
87
|
import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
|
|
88
88
|
import { prepareTaskFinalReviewContractRebindProof, resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractRebind.js";
|
|
@@ -264,6 +264,16 @@ export async function main() {
|
|
|
264
264
|
return;
|
|
265
265
|
}
|
|
266
266
|
if (args[0] === "internal") {
|
|
267
|
+
if (args[1] === "session-cli-refresh" && args.length === 2) {
|
|
268
|
+
if (process.env.YUI_SESSION_SCOPE === "task"
|
|
269
|
+
|| (process.env.YUI_SESSION_SCOPE === "global" && process.env.YUI_ROLE !== "operator")) {
|
|
270
|
+
throw usageError("Managed Session CLI refresh may be run only by the user or global Operator.");
|
|
271
|
+
}
|
|
272
|
+
const result = refreshManagedSessionCliWrappers(home);
|
|
273
|
+
emit(`Refreshed ${result.refreshed} legacy Session CLI wrapper(s); `
|
|
274
|
+
+ `${result.current} already current, ${result.skipped} skipped.`, false, result);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
267
277
|
if (args[1] === "agent-host" && args.length === 4) {
|
|
268
278
|
process.exitCode = await runAgentHost({
|
|
269
279
|
home,
|
|
@@ -1364,7 +1374,8 @@ async function preflightManagedTaskControlPlane() {
|
|
|
1364
1374
|
throw new Error("Exact control-plane invocation requires both frozen descriptors in a managed Task runtime.");
|
|
1365
1375
|
}
|
|
1366
1376
|
if (!exactRuntime) {
|
|
1367
|
-
if (exactControlInvocation.digest !== undefined
|
|
1377
|
+
if (exactControlInvocation.digest !== undefined
|
|
1378
|
+
|| process.env.YUI_SESSION_SCOPE === "global") {
|
|
1368
1379
|
return await preflightManagedGlobalControlPlane(exactControlInvocation.digest);
|
|
1369
1380
|
}
|
|
1370
1381
|
if (taskFinalReviewInvocation.request !== undefined) {
|
|
@@ -1379,24 +1390,42 @@ async function preflightManagedTaskControlPlane() {
|
|
|
1379
1390
|
throw new Error("Exact control-plane invocation is required for this managed Task runtime.");
|
|
1380
1391
|
}
|
|
1381
1392
|
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
|
-
|
|
1393
|
+
const internalCallback = args[0] === "internal"
|
|
1394
|
+
&& ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
|
|
1395
|
+
const home = resolveYuiHome(process.env);
|
|
1396
|
+
assertManagedSessionManifest(home, "task");
|
|
1397
|
+
if (resolve(control.yuiHome) !== resolve(home)) {
|
|
1398
|
+
throw new Error("Managed Task control-plane descriptor belongs to another YUI_HOME.");
|
|
1399
|
+
}
|
|
1400
|
+
const frozenDigest = exactControlPlaneDigest(control);
|
|
1401
|
+
const exactCommand = internalCallback || exactControlInvocation.digest !== undefined;
|
|
1402
|
+
let commandControl;
|
|
1403
|
+
let digest;
|
|
1404
|
+
if (exactCommand) {
|
|
1405
|
+
digest = exactControlInvocation.digest ?? frozenDigest;
|
|
1406
|
+
await assertExactControlPlanePreflight({
|
|
1407
|
+
serializedDescriptor: serializedControl,
|
|
1408
|
+
digest,
|
|
1409
|
+
actualExecutable: process.execPath,
|
|
1410
|
+
actualCliEntry: fileURLToPath(import.meta.url),
|
|
1411
|
+
actualHome: home
|
|
1412
|
+
}, {
|
|
1413
|
+
// Provider callbacks must remain able to append their immutable inbox fact
|
|
1414
|
+
// while the Controller is offline. They still validate executable, CLI,
|
|
1415
|
+
// Home, schema, and the exact Task runtime envelope first.
|
|
1416
|
+
checkController: !internalCallback
|
|
1417
|
+
});
|
|
1418
|
+
commandControl = control;
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
await assertCompatibleControlPlanePreflight({ actualHome: home });
|
|
1422
|
+
// Package/build identity is deliberately not part of ordinary Session
|
|
1423
|
+
// continuity. Keep contracts on the Session's frozen digest while the
|
|
1424
|
+
// current CLI is proven protocol/storage compatible.
|
|
1425
|
+
commandControl = control;
|
|
1426
|
+
digest = frozenDigest;
|
|
1427
|
+
}
|
|
1428
|
+
const runtime = assertExactTaskRuntimeEnvironment(serializedRuntime, process.env, frozenDigest, control.yuiHome);
|
|
1400
1429
|
const runtimeDriverCallback = args[0] === "internal"
|
|
1401
1430
|
&& args[1] === "runtime-hook"
|
|
1402
1431
|
&& process.env.YUI_DRIVER_ID !== undefined
|
|
@@ -1417,7 +1446,7 @@ async function preflightManagedTaskControlPlane() {
|
|
|
1417
1446
|
if (request === undefined) {
|
|
1418
1447
|
return {
|
|
1419
1448
|
contract: undefined,
|
|
1420
|
-
controlPlane: { digest, descriptor:
|
|
1449
|
+
controlPlane: { digest, descriptor: commandControl },
|
|
1421
1450
|
verifiedStore
|
|
1422
1451
|
};
|
|
1423
1452
|
}
|
|
@@ -1427,13 +1456,22 @@ async function preflightManagedTaskControlPlane() {
|
|
|
1427
1456
|
if (request.taskId !== runtime.taskId) {
|
|
1428
1457
|
throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
|
|
1429
1458
|
}
|
|
1459
|
+
const recordedContract = resolveRecordedTaskFinalReviewContract(runtime.taskId, verifiedStore.listWorkItems(runtime.taskId), verifiedStore.listReviewRounds(runtime.taskId), verifiedStore.listEvents(runtime.taskId))?.effective;
|
|
1460
|
+
if (recordedContract !== undefined
|
|
1461
|
+
&& recordedContract.reviewerRoleName !== request.reviewerRoleName) {
|
|
1462
|
+
throw new Error(`Task final-review Reviewer mismatch: expected ${recordedContract.reviewerRoleName}, `
|
|
1463
|
+
+ `found ${request.reviewerRoleName}.`);
|
|
1464
|
+
}
|
|
1430
1465
|
return {
|
|
1431
1466
|
contract: createTaskFinalReviewContract({
|
|
1432
1467
|
taskId: runtime.taskId,
|
|
1433
1468
|
reviewerRoleName: request.reviewerRoleName,
|
|
1434
|
-
|
|
1469
|
+
// Once Task evidence establishes a contract, a compatible Session or
|
|
1470
|
+
// CLI replacement presents that same capability. Package/build identity
|
|
1471
|
+
// must not force a release rebind or invalidate delivery evidence.
|
|
1472
|
+
controlPlaneDigest: recordedContract?.controlPlaneDigest ?? digest
|
|
1435
1473
|
}),
|
|
1436
|
-
controlPlane: { digest, descriptor:
|
|
1474
|
+
controlPlane: { digest, descriptor: commandControl },
|
|
1437
1475
|
verifiedStore
|
|
1438
1476
|
};
|
|
1439
1477
|
}
|
|
@@ -1445,38 +1483,77 @@ async function preflightManagedGlobalControlPlane(digest) {
|
|
|
1445
1483
|
throw new Error("Task final-review contract establishment requires a verified exact Task control-plane invocation.");
|
|
1446
1484
|
}
|
|
1447
1485
|
const manifestPath = process.env.YUI_SESSION_MANIFEST;
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
throw new Error("Exact global control-plane invocation requires its Session Manifest and CLI.");
|
|
1486
|
+
if (manifestPath === undefined) {
|
|
1487
|
+
throw new Error("Managed global control-plane invocation requires its Session Manifest.");
|
|
1451
1488
|
}
|
|
1452
|
-
const
|
|
1489
|
+
const home = resolveYuiHome(process.env);
|
|
1490
|
+
const manifest = assertManagedSessionManifest(home, "global");
|
|
1453
1491
|
const expectedRoleKind = process.env.YUI_ROLE === "operator" ? "operator" : "global";
|
|
1454
1492
|
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.");
|
|
1493
|
+
|| manifest.roleKind !== expectedRoleKind) {
|
|
1494
|
+
throw new Error("Managed global invocation does not match its Session Manifest.");
|
|
1459
1495
|
}
|
|
1460
1496
|
const serializedDescriptor = readFileSync(manifest.controlPlane.descriptorPath, "utf8");
|
|
1461
1497
|
const descriptor = parseExactControlPlaneDescriptor(serializedDescriptor);
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1498
|
+
if (resolve(descriptor.yuiHome) !== resolve(home)) {
|
|
1499
|
+
throw new Error("Managed global control-plane descriptor belongs to another YUI_HOME.");
|
|
1500
|
+
}
|
|
1501
|
+
const frozenDigest = exactControlPlaneDigest(descriptor);
|
|
1502
|
+
if (frozenDigest !== manifest.controlPlane.digest
|
|
1503
|
+
|| resolve(manifest.controlPlane.descriptorPath) !== resolve(join(descriptor.yuiHome, "runtime", "control-plane", `${frozenDigest}.json`))
|
|
1464
1504
|
|| resolve(manifestPath) !== resolve(join(descriptor.yuiHome, "runtime", "session-manifests", `${manifest.digest}.json`))) {
|
|
1465
1505
|
throw new Error("Exact global Session Manifest does not match its control-plane descriptor.");
|
|
1466
1506
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1507
|
+
let commandControl;
|
|
1508
|
+
let commandDigest;
|
|
1509
|
+
if (digest !== undefined) {
|
|
1510
|
+
await assertExactControlPlanePreflight({
|
|
1511
|
+
serializedDescriptor,
|
|
1512
|
+
digest,
|
|
1513
|
+
actualExecutable: process.execPath,
|
|
1514
|
+
actualCliEntry: fileURLToPath(import.meta.url),
|
|
1515
|
+
actualHome: home
|
|
1516
|
+
});
|
|
1517
|
+
commandControl = descriptor;
|
|
1518
|
+
commandDigest = digest;
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
await assertCompatibleControlPlanePreflight({ actualHome: home });
|
|
1522
|
+
commandControl = currentInvocationControlPlane(home);
|
|
1523
|
+
commandDigest = exactControlPlaneDigest(commandControl);
|
|
1524
|
+
}
|
|
1474
1525
|
return {
|
|
1475
1526
|
contract: undefined,
|
|
1476
|
-
controlPlane: { digest, descriptor },
|
|
1477
|
-
verifiedStore: openCompatibleFileTaskStore(
|
|
1527
|
+
controlPlane: { digest: commandDigest, descriptor: commandControl },
|
|
1528
|
+
verifiedStore: openCompatibleFileTaskStore(home)
|
|
1478
1529
|
};
|
|
1479
1530
|
}
|
|
1531
|
+
function currentInvocationControlPlane(home) {
|
|
1532
|
+
return createExactControlPlaneDescriptor({
|
|
1533
|
+
executable: process.execPath,
|
|
1534
|
+
cliEntry: fileURLToPath(import.meta.url),
|
|
1535
|
+
yuiHome: home
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
function assertManagedSessionManifest(home, scope) {
|
|
1539
|
+
const manifestPath = process.env.YUI_SESSION_MANIFEST;
|
|
1540
|
+
if (manifestPath === undefined) {
|
|
1541
|
+
throw new Error("Managed control-plane invocation requires its Session Manifest.");
|
|
1542
|
+
}
|
|
1543
|
+
const manifest = readSessionBootstrapManifest(manifestPath);
|
|
1544
|
+
if (manifest.owner.scope !== scope) {
|
|
1545
|
+
throw new Error("Managed invocation scope does not match its Session Manifest.");
|
|
1546
|
+
}
|
|
1547
|
+
if (scope === "task" && (manifest.owner.scope !== "task"
|
|
1548
|
+
|| manifest.owner.taskId !== process.env.YUI_TASK_ID)) {
|
|
1549
|
+
throw new Error("Managed Task invocation does not match its Session Manifest owner.");
|
|
1550
|
+
}
|
|
1551
|
+
const expectedPath = resolve(home, "runtime", "session-manifests", `${manifest.digest}.json`);
|
|
1552
|
+
if (resolve(manifestPath) !== expectedPath) {
|
|
1553
|
+
throw new Error("Managed Session Manifest path is outside this YUI_HOME.");
|
|
1554
|
+
}
|
|
1555
|
+
return manifest;
|
|
1556
|
+
}
|
|
1480
1557
|
function cleanupCliError(error, fallbackResource) {
|
|
1481
1558
|
if (error instanceof WorkspaceCleanupBlockedError) {
|
|
1482
1559
|
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`;
|
|
@@ -5,6 +5,7 @@ import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds,
|
|
|
5
5
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
6
6
|
import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
7
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
8
|
+
import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
|
|
8
9
|
import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
9
10
|
import { readCommandText } from "./textInput.js";
|
|
10
11
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
@@ -1348,23 +1349,79 @@ function taskMessageCommand(args, store, options) {
|
|
|
1348
1349
|
}
|
|
1349
1350
|
if (messages.length === 0)
|
|
1350
1351
|
return "No messages found.\n";
|
|
1352
|
+
const retirements = new Map(store.listEvents(task.id).flatMap((event) => {
|
|
1353
|
+
const retirement = taskRecordRetirement(event);
|
|
1354
|
+
return retirement?.recordKind === "message"
|
|
1355
|
+
? [[retirement.recordId, retirement]]
|
|
1356
|
+
: [];
|
|
1357
|
+
}));
|
|
1351
1358
|
const timeZone = store.getConfig().timeZone;
|
|
1352
1359
|
return `${renderTable(`Task messages: ${task.id}`, [
|
|
1353
1360
|
{ header: "Message", minWidth: 7, maxWidth: 18 },
|
|
1361
|
+
{ header: "Status", minWidth: 6, maxWidth: 9 },
|
|
1354
1362
|
{ header: "Author", minWidth: 6, maxWidth: 18 },
|
|
1355
1363
|
{ header: "Created", minWidth: 10, maxWidth: 28 },
|
|
1356
1364
|
{ header: "Body", minWidth: 8, maxWidth: 72 }
|
|
1357
1365
|
], messages.map((message) => [
|
|
1358
1366
|
message.id,
|
|
1367
|
+
retirements.has(message.id) ? "retired" : "active",
|
|
1359
1368
|
taskMessageAuthorLabel(message.author),
|
|
1360
1369
|
presentTime(message.createdAt, timeZone),
|
|
1361
1370
|
message.body
|
|
1362
1371
|
]), defaultTableWidth())}\n`;
|
|
1363
1372
|
}
|
|
1373
|
+
if (command === "retire") {
|
|
1374
|
+
return retireMessage(rest, store, options);
|
|
1375
|
+
}
|
|
1364
1376
|
throw usageError(command === undefined
|
|
1365
1377
|
? "Task message command is required."
|
|
1366
1378
|
: `Unknown command: task message ${command}`);
|
|
1367
1379
|
}
|
|
1380
|
+
function retireMessage(args, store, options) {
|
|
1381
|
+
const usage = "Task message retire usage: yui task message retire <task>/<message> --reason <text>.";
|
|
1382
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
1383
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1384
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
1385
|
+
const reference = taskRecordReference(parsed.positionals[0], "message", "Message reference", options);
|
|
1386
|
+
const now = clock(options);
|
|
1387
|
+
const result = store.transaction((tx) => {
|
|
1388
|
+
const task = requireTask(tx, reference.taskId);
|
|
1389
|
+
assertTaskOpen(task);
|
|
1390
|
+
const actor = taskActor(options, task.id);
|
|
1391
|
+
if (actor === "leader") {
|
|
1392
|
+
throw usageError("Only the user or global Operator may retire a Task Message.");
|
|
1393
|
+
}
|
|
1394
|
+
const message = tx.listMessages(task.id).find(({ id }) => id === reference.localId);
|
|
1395
|
+
if (message === undefined) {
|
|
1396
|
+
throw dataError(`Task Message not found: ${task.id}/${reference.localId}.`);
|
|
1397
|
+
}
|
|
1398
|
+
const events = tx.listEvents(task.id);
|
|
1399
|
+
if (isTaskRecordRetired(events, "message", message.id)) {
|
|
1400
|
+
return { task, message, changed: false };
|
|
1401
|
+
}
|
|
1402
|
+
// Remove an isolated pending wake for this exact directive. A merged batch
|
|
1403
|
+
// is retained because its other signals remain actionable; context and
|
|
1404
|
+
// actionability projections still filter the retired message below.
|
|
1405
|
+
try {
|
|
1406
|
+
settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
// Merged pending work cannot be split without losing unrelated signals.
|
|
1410
|
+
}
|
|
1411
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
1412
|
+
eventId: tx.nextEventId(task.id),
|
|
1413
|
+
taskId: task.id,
|
|
1414
|
+
recordKind: "message",
|
|
1415
|
+
recordId: message.id,
|
|
1416
|
+
reason,
|
|
1417
|
+
retiredBy: actor
|
|
1418
|
+
}, now));
|
|
1419
|
+
return { task, message, changed: true };
|
|
1420
|
+
});
|
|
1421
|
+
if (result.changed)
|
|
1422
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1423
|
+
return `Retired Task Message ${result.task.id}/${result.message.id}\n`;
|
|
1424
|
+
}
|
|
1368
1425
|
/**
|
|
1369
1426
|
* Issue 05: force-wake escape hatch. Bypasses the actionability digest and
|
|
1370
1427
|
* enqueues exactly one Leader wakeup with an auditable reason. The reason is
|
|
@@ -2659,9 +2716,7 @@ function retireWork(args, store, options) {
|
|
|
2659
2716
|
if (task.status !== "active") {
|
|
2660
2717
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2661
2718
|
}
|
|
2662
|
-
|
|
2663
|
-
throw usageError("Only the Task Leader may retire a Work Item.");
|
|
2664
|
-
}
|
|
2719
|
+
const actor = taskActor(options, task.id);
|
|
2665
2720
|
if (replacementWorkItemId !== undefined) {
|
|
2666
2721
|
const replacement = requireWorkItem(tx, replacementWorkItemId, options);
|
|
2667
2722
|
if (replacement.taskId !== task.id) {
|
|
@@ -2701,7 +2756,7 @@ function retireWork(args, store, options) {
|
|
|
2701
2756
|
}
|
|
2702
2757
|
}
|
|
2703
2758
|
const next = retireWorkItem(item, {
|
|
2704
|
-
by:
|
|
2759
|
+
by: actor,
|
|
2705
2760
|
summary,
|
|
2706
2761
|
...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
|
|
2707
2762
|
}, now);
|
|
@@ -2713,8 +2768,16 @@ function retireWork(args, store, options) {
|
|
|
2713
2768
|
...(replacementWorkItemId === undefined
|
|
2714
2769
|
? {}
|
|
2715
2770
|
: { replacementWorkItemId }),
|
|
2716
|
-
...leaderActionEventPayload(tx, task.id, options)
|
|
2771
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : { retiredBy: actor })
|
|
2717
2772
|
}, now);
|
|
2773
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
2774
|
+
eventId: tx.nextEventId(task.id),
|
|
2775
|
+
taskId: task.id,
|
|
2776
|
+
recordKind: "work-item",
|
|
2777
|
+
recordId: next.id,
|
|
2778
|
+
reason: summary,
|
|
2779
|
+
retiredBy: actor
|
|
2780
|
+
}, now));
|
|
2718
2781
|
}
|
|
2719
2782
|
return next;
|
|
2720
2783
|
});
|
|
@@ -3801,10 +3864,64 @@ function taskRunCommand(args, store, options) {
|
|
|
3801
3864
|
return yieldRunStatus(rest, store, options);
|
|
3802
3865
|
if (command === "checkpoint")
|
|
3803
3866
|
return output(checkpointRun(rest, store, options));
|
|
3867
|
+
if (command === "retire")
|
|
3868
|
+
return retireRun(rest, store, options);
|
|
3804
3869
|
throw usageError(command === undefined
|
|
3805
3870
|
? "Task run command is required."
|
|
3806
3871
|
: `Unknown command: task run ${command}`);
|
|
3807
3872
|
}
|
|
3873
|
+
function retireRun(args, store, options) {
|
|
3874
|
+
const usage = "Task run retire usage: yui task run retire <task>/<run> --reason <text>.";
|
|
3875
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
3876
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
3877
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
3878
|
+
const reference = taskRecordReference(parsed.positionals[0], "agentRun", "Agent Run reference", options);
|
|
3879
|
+
const now = clock(options);
|
|
3880
|
+
const result = store.transaction((tx) => {
|
|
3881
|
+
const task = requireTask(tx, reference.taskId);
|
|
3882
|
+
assertTaskOpen(task);
|
|
3883
|
+
const actor = taskActor(options, task.id);
|
|
3884
|
+
if (actor === "leader") {
|
|
3885
|
+
throw usageError("Only the user or global Operator may retire an Agent Run.");
|
|
3886
|
+
}
|
|
3887
|
+
let run = tx.getAgentRun(task.id, reference.localId);
|
|
3888
|
+
if (run === null)
|
|
3889
|
+
throw dataError(`Agent Run not found: ${task.id}/${reference.localId}.`);
|
|
3890
|
+
const events = tx.listEvents(task.id);
|
|
3891
|
+
if (isTaskRecordRetired(events, "agent-run", run.id)) {
|
|
3892
|
+
return { task, run, changed: false };
|
|
3893
|
+
}
|
|
3894
|
+
if (run.status === "active") {
|
|
3895
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
3896
|
+
taskId: task.id,
|
|
3897
|
+
roleName: run.roleName,
|
|
3898
|
+
agentId: run.effective.agentId,
|
|
3899
|
+
runId: run.id,
|
|
3900
|
+
receiptId: agentRunDeliveryReceiptId(run),
|
|
3901
|
+
outcome: { status: "failed", summary: `Agent Run retired: ${reason}` }
|
|
3902
|
+
}, now);
|
|
3903
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
3904
|
+
throw usageError(`Agent Run changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
3905
|
+
}
|
|
3906
|
+
run = terminal.run;
|
|
3907
|
+
}
|
|
3908
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
3909
|
+
eventId: tx.nextEventId(task.id),
|
|
3910
|
+
taskId: task.id,
|
|
3911
|
+
recordKind: "agent-run",
|
|
3912
|
+
recordId: run.id,
|
|
3913
|
+
reason,
|
|
3914
|
+
retiredBy: actor
|
|
3915
|
+
}, now));
|
|
3916
|
+
return { task, run, changed: true };
|
|
3917
|
+
});
|
|
3918
|
+
if (result.changed)
|
|
3919
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
3920
|
+
return output(`Retired Agent Run ${result.task.id}/${result.run.id}\n`, {
|
|
3921
|
+
agentRun: result.run,
|
|
3922
|
+
retired: true
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3808
3925
|
function runContextCommand(args, store, options) {
|
|
3809
3926
|
const [first, ...rest] = args;
|
|
3810
3927
|
if (first === "expand") {
|
|
@@ -3878,6 +3995,7 @@ function listRuns(args, store, options) {
|
|
|
3878
3995
|
const runs = store.listAgentRuns(item.taskId).filter((run) => run.workItemId === item.id);
|
|
3879
3996
|
if (runs.length === 0)
|
|
3880
3997
|
return "No runs found.\n";
|
|
3998
|
+
const events = store.listEvents(item.taskId);
|
|
3881
3999
|
return `${renderTable(`Runs: ${item.id}`, [
|
|
3882
4000
|
{ header: "Run", minWidth: 6, maxWidth: 20 },
|
|
3883
4001
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
@@ -3887,6 +4005,7 @@ function listRuns(args, store, options) {
|
|
|
3887
4005
|
{ header: "Profile", minWidth: 7, maxWidth: 8 },
|
|
3888
4006
|
{ header: "Permission", minWidth: 8, maxWidth: 16 },
|
|
3889
4007
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
4008
|
+
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
3890
4009
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
3891
4010
|
], runs.map((run) => [
|
|
3892
4011
|
run.id,
|
|
@@ -3897,6 +4016,7 @@ function listRuns(args, store, options) {
|
|
|
3897
4016
|
run.effective.profileAccess,
|
|
3898
4017
|
run.effective.permission.strategy,
|
|
3899
4018
|
run.status,
|
|
4019
|
+
isTaskRecordRetired(events, "agent-run", run.id) ? "retired" : "active",
|
|
3900
4020
|
run.summary ?? "-"
|
|
3901
4021
|
]), defaultTableWidth())}\n`;
|
|
3902
4022
|
}
|
|
@@ -4907,14 +5027,21 @@ function showRun(args, store, options) {
|
|
|
4907
5027
|
const facts = readRunRecoveryFacts(tx, run.taskId, run.id);
|
|
4908
5028
|
if (facts === null)
|
|
4909
5029
|
throw usageError(`Agent Run not found: ${run.taskId}/${run.id}.`, usage);
|
|
4910
|
-
|
|
5030
|
+
const retirement = tx.listEvents(run.taskId)
|
|
5031
|
+
.map(taskRecordRetirement)
|
|
5032
|
+
.find((entry) => entry?.recordKind === "agent-run" && entry.recordId === run.id) ?? null;
|
|
5033
|
+
return { run, recovery: projectRunRecovery(facts), retirement };
|
|
4911
5034
|
});
|
|
4912
5035
|
if (asJson) {
|
|
4913
5036
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4914
5037
|
}
|
|
4915
|
-
return {
|
|
5038
|
+
return {
|
|
5039
|
+
kind: "output",
|
|
5040
|
+
output: renderRunShow(data.run, data.recovery, data.retirement),
|
|
5041
|
+
data
|
|
5042
|
+
};
|
|
4916
5043
|
}
|
|
4917
|
-
function renderRunShow(run, recovery) {
|
|
5044
|
+
function renderRunShow(run, recovery, retirement) {
|
|
4918
5045
|
const lines = [
|
|
4919
5046
|
`Run: ${run.id}`,
|
|
4920
5047
|
`Task: ${run.taskId}`,
|
|
@@ -4922,6 +5049,10 @@ function renderRunShow(run, recovery) {
|
|
|
4922
5049
|
`Purpose: ${run.purpose}`,
|
|
4923
5050
|
`Mode: ${run.mode}`,
|
|
4924
5051
|
`Status: ${run.status}`,
|
|
5052
|
+
...(retirement === null ? [] : [
|
|
5053
|
+
`History: retired by ${retirement.retiredBy}`,
|
|
5054
|
+
`Retirement reason: ${retirement.reason}`
|
|
5055
|
+
]),
|
|
4925
5056
|
`Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
|
|
4926
5057
|
`Created: ${run.createdAt}`,
|
|
4927
5058
|
...(run.pushedAt === undefined ? [] : [`Pushed: ${run.pushedAt}`]),
|
|
@@ -6065,7 +6196,8 @@ function taskRecordReference(value, kind, label, options) {
|
|
|
6065
6196
|
function assertWorkItemDependenciesCompleted(store, item) {
|
|
6066
6197
|
for (const dependencyId of item.dependsOn) {
|
|
6067
6198
|
const dependency = store.getWorkItem(item.taskId, dependencyId);
|
|
6068
|
-
if (dependency === null
|
|
6199
|
+
if (dependency === null
|
|
6200
|
+
|| (dependency.status !== "completed" && dependency.status !== "retired")) {
|
|
6069
6201
|
throw usageError(`Work Item dependency is not completed: ${dependencyId}.`);
|
|
6070
6202
|
}
|
|
6071
6203
|
}
|