@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.
Files changed (33) 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 +120 -43
  5. package/dist/commands/globalRoleCommands.js +8 -4
  6. package/dist/commands/taskCommands.js +141 -9
  7. package/dist/commands/taskContextCommand.js +5 -3
  8. package/dist/commands/taskNextActionCommand.js +4 -2
  9. package/dist/commands/taskOverviewCommand.js +2 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  11. package/dist/context/runContextPack.js +9 -5
  12. package/dist/context/sessionBootstrapManifest.js +77 -11
  13. package/dist/context/wakeNotification.js +5 -3
  14. package/dist/controller/clientRuntime.js +8 -8
  15. package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
  16. package/dist/executor/agentExecutor.js +4 -4
  17. package/dist/executor/fileRoleLaunchPlanner.js +21 -34
  18. package/dist/review/reviewOutcomeClassifier.js +15 -4
  19. package/dist/review/taskFinalReviewContractRebind.js +28 -11
  20. package/dist/runtime/exactControlPlane.js +47 -37
  21. package/dist/runtime/firstProgressStopLoss.js +3 -1
  22. package/dist/scheduler/actionability.js +4 -2
  23. package/dist/scheduler/activeTaskProgress.js +2 -1
  24. package/dist/scheduler/taskExecutionProjection.js +13 -4
  25. package/dist/storage/sqliteStore.js +12 -4
  26. package/dist/storage/taskStore.js +6 -4
  27. package/dist/task/nextAction.js +8 -3
  28. package/dist/task/taskRecordRetirement.js +72 -0
  29. package/dist/workItem/workItem.js +6 -4
  30. package/i18n/README.zh-CN.md +20 -1
  31. package/package.json +1 -1
  32. package/skills/yui-operator/SKILL.md +7 -0
  33. 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
@@ -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
- 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);
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: control },
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
- controlPlaneDigest: digest
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: control },
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
- 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.");
1486
+ if (manifestPath === undefined) {
1487
+ throw new Error("Managed global control-plane invocation requires its Session Manifest.");
1451
1488
  }
1452
- const manifest = readSessionBootstrapManifest(manifestPath);
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
- || 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.");
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 (exactControlPlaneDigest(descriptor) !== digest
1463
- || resolve(manifest.controlPlane.descriptorPath) !== resolve(join(descriptor.yuiHome, "runtime", "control-plane", `${digest}.json`))
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
- await assertExactControlPlanePreflight({
1468
- serializedDescriptor,
1469
- digest,
1470
- actualExecutable: process.execPath,
1471
- actualCliEntry: fileURLToPath(import.meta.url),
1472
- actualHome: resolveYuiHome(process.env)
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(descriptor.yuiHome)
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
- || 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`;
@@ -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
- if (taskActor(options, task.id) !== "leader") {
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: "leader",
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
- return { run, recovery: projectRunRecovery(facts) };
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 { kind: "output", output: renderRunShow(data.run, data.recovery), data };
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 || dependency.status !== "completed") {
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
  }