@zq-silk/yui 0.15.9 → 0.15.11

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 (100) hide show
  1. package/dist/agent/launchEnvironment.js +7 -0
  2. package/dist/cli/commandCatalog.js +26 -6
  3. package/dist/cli/interactionPolicy.js +3 -3
  4. package/dist/cli/updateOrchestrator.js +24 -1
  5. package/dist/cli/updatePorts.js +7 -3
  6. package/dist/cli/upgradeCommand.js +42 -2
  7. package/dist/cli.js +336 -93
  8. package/dist/commands/globalRoleCommands.js +314 -4
  9. package/dist/commands/projectCommands.js +6 -7
  10. package/dist/commands/releaseCommands.js +18 -0
  11. package/dist/commands/taskActor.js +25 -0
  12. package/dist/commands/taskCommands.js +511 -83
  13. package/dist/commands/taskIntegrationCommands.js +16 -38
  14. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  15. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  16. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  17. package/dist/context/taskContext.js +38 -2
  18. package/dist/controller/agentHostObservation.js +155 -0
  19. package/dist/controller/clientRuntime.js +17 -2
  20. package/dist/controller/controller.js +3 -0
  21. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  22. package/dist/controller/globalInputDelivery.js +119 -0
  23. package/dist/controller/jobControl.js +6 -2
  24. package/dist/controller/resourceInventory.js +14 -4
  25. package/dist/controller/resourceInventoryLinux.js +2 -6
  26. package/dist/controller/runtime.js +81 -6
  27. package/dist/controller/runtimeEventInbox.js +32 -3
  28. package/dist/controller/runtimeEventProcessor.js +26 -6
  29. package/dist/controller/runtimeHookRunFence.js +75 -19
  30. package/dist/controller/structuredProviderObservation.js +133 -70
  31. package/dist/coordination/workMailboxQueue.js +5 -0
  32. package/dist/execution/workItemExecutionProjection.js +1 -1
  33. package/dist/executor/agentExecutor.js +64 -4
  34. package/dist/executor/executorRegistry.js +3 -0
  35. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  36. package/dist/integration/deliveryObligation.js +2 -1
  37. package/dist/integration/gitIntegrationService.js +312 -382
  38. package/dist/integration/integrationAttempt.js +30 -4
  39. package/dist/integration/integrationQueueService.js +7 -7
  40. package/dist/integration/integrationSourceApplication.js +323 -0
  41. package/dist/message/globalInterrupt.js +33 -0
  42. package/dist/message/inputControlResolution.js +106 -0
  43. package/dist/message/message.js +367 -0
  44. package/dist/message/messageContinuation.js +126 -3
  45. package/dist/message/taskInterrupt.js +34 -0
  46. package/dist/observability/orchestrationMetrics.js +1 -1
  47. package/dist/release/releaseHandover.js +22 -0
  48. package/dist/release/releaseWorkflowPorts.js +15 -7
  49. package/dist/repository/gitWorkspace.js +72 -15
  50. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  51. package/dist/repository/taskWorkspacePreparer.js +120 -49
  52. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  53. package/dist/resources/resourceDiscovery.js +3 -2
  54. package/dist/runtime/agentHost.js +152 -72
  55. package/dist/runtime/agentHostCompatibility.js +127 -0
  56. package/dist/runtime/agentHostProtocol.js +53 -0
  57. package/dist/runtime/executionEnvironment.js +0 -19
  58. package/dist/runtime/launchBroker.js +6 -0
  59. package/dist/runtime/sessionReconciliation.js +4 -4
  60. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  61. package/dist/runtime/tmuxAdapters.js +5 -3
  62. package/dist/scheduler/operatorEvent.js +4 -0
  63. package/dist/scheduler/taskExecutionProjection.js +12 -1
  64. package/dist/scheduler/wakeReason.js +7 -1
  65. package/dist/scheduler/wakeupQueue.js +2 -0
  66. package/dist/setup/setupCommand.js +26 -8
  67. package/dist/storage/homeLayout.js +130 -0
  68. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  69. package/dist/storage/migrations/integrationContinuation.js +104 -0
  70. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  71. package/dist/storage/sqliteSchema.js +136 -4
  72. package/dist/storage/sqliteStore.js +40 -1
  73. package/dist/storage/storageVersions.js +1 -1
  74. package/dist/storage/storeRpc.js +1 -0
  75. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  76. package/dist/task/archiveDiagnostics.js +128 -0
  77. package/dist/task/nextAction.js +44 -11
  78. package/dist/web/assets/client/app.js +55 -0
  79. package/dist/web/assets/client/components.js +1 -0
  80. package/dist/web/assets/client/i18n.js +6 -0
  81. package/dist/web/assets/client/taskSurface.js +106 -0
  82. package/dist/web/assets/client/view.js +7 -4
  83. package/dist/web/assets/shell.js +23 -0
  84. package/dist/web/assets/styles/layout.js +1 -1
  85. package/dist/web/assets/styles/widgets.js +12 -0
  86. package/dist/web/webServer.js +117 -1
  87. package/dist/web/webSnapshot.js +4 -3
  88. package/dist/web/webTaskSurface.js +222 -5
  89. package/dist/workspace/workItemChangeSetManager.js +18 -2
  90. package/docs/release-workflow.md +39 -0
  91. package/docs/release-workflow.zh-CN.md +29 -0
  92. package/docs/sqlite-control-plane-design.md +223 -1
  93. package/docs/testing/verification-levels.md +24 -0
  94. package/docs/testing/verification-levels.zh-CN.md +11 -0
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +151 -49
  97. package/skills/yui-leader/references/integration.md +52 -2
  98. package/skills/yui-operator/SKILL.md +6 -1
  99. package/skills/yui-runtime/SKILL.md +27 -0
  100. package/skills/yui-runtime/references/publication.md +20 -0
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { recordTaskInterruptResult } from "./message/taskInterrupt.js";
4
5
  import { agentAdapterLabel as adapterLabel } from "./agent/adapterCatalog.js";
5
6
  import { readFileSync } from "node:fs";
6
7
  import { resolve } from "node:path";
@@ -12,6 +13,7 @@ import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
12
13
  import { routeInvocation } from "./cli/invocationRouter.js";
13
14
  import { renderCompletion } from "./cli/completion.js";
14
15
  import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
16
+ import { recordGlobalInterruptResult, recordGlobalSteerResult } from "./message/globalInterrupt.js";
15
17
  import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
16
18
  import { runCompletionWizard } from "./cli/completionWizard.js";
17
19
  import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
@@ -40,8 +42,9 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
40
42
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
41
43
  import { runProjectCommand } from "./commands/projectCommands.js";
42
44
  import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
43
- import { assertWorkItemDependenciesCompletedForCommand, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest } from "./commands/taskCommands.js";
45
+ import { assertWorkItemDependenciesCompletedForCommand, requireWorkItemAssignee, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest } from "./commands/taskCommands.js";
44
46
  import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./commands/taskRemoteDeliveryCommand.js";
47
+ import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
45
48
  import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
46
49
  import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
47
50
  import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
@@ -75,10 +78,11 @@ import { runRuntimeObservationHookCommand } from "./controller/runtimeObservatio
75
78
  import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
76
79
  import { agentNotFound, CliError, runtimeError, usageError } from "./errors/cliError.js";
77
80
  import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
78
- import { AGENT_HOST_CONTROL_PROTOCOL, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl } from "./runtime/agentHost.js";
81
+ import { AGENT_HOST_CONTROL_PROTOCOL, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl, sendAgentHostSteerControl, sendAgentHostCancelControl, foldSteerLiveReceipt, foldInterruptLiveReceipt } from "./runtime/agentHost.js";
79
82
  import { unknownAgentRunConfiguration } from "./runtime/agentRunConfiguration.js";
80
83
  import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
81
84
  import { FileTaskWorkspacePreparer } from "./repository/taskWorkspacePreparer.js";
85
+ import { snapshotWorkItemCandidate } from "./repository/workItemCandidateSnapshot.js";
82
86
  import { inspectStorageSchema } from "./storage/storageSchema.js";
83
87
  import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth, resolveStatusIdentityEnabled } from "./observability/runtimeIdentity.js";
84
88
  import { resolveYuiHome } from "./storage/taskStore.js";
@@ -748,11 +752,146 @@ export async function main() {
748
752
  emit(result);
749
753
  return;
750
754
  }
755
+ // The session surface (record/replace/enter/context) only ever yields the
756
+ // enter control; the live input actions live under the top-level `role`
757
+ // command below, never here.
758
+ if (result.kind !== "enter") {
759
+ throw new Error("Session commands cannot perform a live input control.");
760
+ }
751
761
  await ensureFileTaskController(home, { environment: process.env });
752
762
  await runtime.prepareGlobalRoleEnter(result.role.name);
753
763
  tmux.attachRole("operator", result.role.name, "auto");
754
764
  return;
755
765
  }
766
+ if (resolved[0] === "role") {
767
+ // decision-3 §7 CLI grammar: the durable Global input actions are top-level
768
+ // `role message queue|steer <global-role> …` and `role interrupt
769
+ // <global-role> …`, distinct from `config role …` (desired configuration)
770
+ // and `session …` (native session lifecycle). Core persists the durable
771
+ // Global-owned Message, proves owner/target/capability/writer-fence from
772
+ // durable state, and returns either a string disposition (queued,
773
+ // idempotent-replay, or an explicit not-steered/not-interrupted failure with
774
+ // its exact code) or a resolved live intent. The CLI performs at most one
775
+ // native edge with scope "global" and taskId omitted — the same shared
776
+ // resolver and transport as a Task Role, never a fabricated Task.
777
+ let globalInputFailure;
778
+ const roleOptions = {
779
+ yuiHome: home,
780
+ env: process.env,
781
+ jsonOutput,
782
+ onInputFailure: failure => { globalInputFailure = failure; }
783
+ };
784
+ const result = runGlobalRoleCommand(resolved.slice(1), store, roleOptions);
785
+ if (resolved[1] === "message" && resolved[2] === "queue" && resolved[3] !== undefined) {
786
+ await callController(home, "scheduler.signal", {
787
+ key: `global-role:${encodeURIComponent(resolved[3])}`
788
+ }).catch(() => { });
789
+ }
790
+ if (typeof result === "string") {
791
+ if (globalInputFailure !== undefined) {
792
+ emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
793
+ return;
794
+ }
795
+ emit(result);
796
+ return;
797
+ }
798
+ if (result.kind === "input-steer") {
799
+ // Core already persisted the durable Global Message and proved target +
800
+ // capability + writer fence from the Global Role's own Session set. This
801
+ // is the single live edge: one native steer of the exact current Turn,
802
+ // scope "global", no retarget and no fallback to interrupt or queue.
803
+ await ensureFileTaskController(home, { environment: process.env });
804
+ let control;
805
+ try {
806
+ control = await sendAgentHostSteerControl({
807
+ home,
808
+ scope: "global",
809
+ roleName: result.roleName,
810
+ control: {
811
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
812
+ type: "steer-turn",
813
+ nativeSessionId: result.target.nativeSessionId,
814
+ nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
815
+ authority: result.target.authority,
816
+ run: { attemptId: result.receiptId, boundedText: result.text }
817
+ }
818
+ });
819
+ }
820
+ catch (error) {
821
+ recordGlobalSteerResult(store, result.roleName, result.messageId, {
822
+ state: "steer-unknown", outcome: "pending",
823
+ detail: error instanceof Error ? error.message : String(error)
824
+ });
825
+ throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
826
+ + `${error instanceof Error ? error.message : String(error)}. `
827
+ + "The Message is retained and its outcome is recorded from the Host; whether the "
828
+ + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
829
+ + "do not reissue the same input under a new requestId or a different action.");
830
+ }
831
+ const steer = foldSteerLiveReceipt(control);
832
+ recordGlobalSteerResult(store, result.roleName, result.messageId, steer);
833
+ if (steer.state !== "steered") {
834
+ emitControlFailure(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_NOT_DELIVERED", { roleName: result.roleName, messageId: result.messageId, steer });
835
+ return;
836
+ }
837
+ emit(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), false, {
838
+ roleName: result.roleName,
839
+ messageId: result.messageId, steer
840
+ });
841
+ return;
842
+ }
843
+ if (result.kind === "input-interrupt") {
844
+ // The single live edge for a Global interrupt: one native cancel of the
845
+ // exact current Turn, scope "global". Never a kill/restart/detach. Any
846
+ // then-handoff was already claimed durably by Core (an existing Global
847
+ // Message owned by this Role) and is delivered once by the ordinary
848
+ // continuation path after this Turn reaches a proven terminal.
849
+ await ensureFileTaskController(home, { environment: process.env });
850
+ let control;
851
+ try {
852
+ control = await sendAgentHostCancelControl({
853
+ home,
854
+ scope: "global",
855
+ roleName: result.roleName,
856
+ control: {
857
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
858
+ type: "cancel",
859
+ nativeOnly: true,
860
+ nativeSessionId: result.target.nativeSessionId,
861
+ attemptId: result.target.attemptId,
862
+ authority: result.target.authority
863
+ }
864
+ });
865
+ }
866
+ catch (error) {
867
+ recordGlobalInterruptResult(store, result.roleName, result.receiptId, {
868
+ state: "interrupt-unknown", outcome: "cancel-requested",
869
+ detail: error instanceof Error ? error.message : String(error)
870
+ });
871
+ throw runtimeError(`Interrupt of global role ${result.roleName} did not complete: `
872
+ + `${error instanceof Error ? error.message : String(error)}. `
873
+ + "No process was killed; re-read the Session before retrying.");
874
+ }
875
+ const interrupt = foldInterruptLiveReceipt(control);
876
+ recordGlobalInterruptResult(store, result.roleName, result.receiptId, interrupt);
877
+ if (interrupt.state !== "interrupt-requested") {
878
+ emitControlFailure(interruptReceiptOutput(result.output, result.roleName, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN" : "INTERRUPT_NOT_DELIVERED", { roleName: result.roleName, interrupt });
879
+ return;
880
+ }
881
+ emit(interruptReceiptOutput(result.output, result.roleName, interrupt), false, {
882
+ roleName: result.roleName,
883
+ ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
884
+ interrupt
885
+ });
886
+ return;
887
+ }
888
+ if (result.kind !== "enter") {
889
+ throw new Error("Role command returned an invalid control result.");
890
+ }
891
+ // A top-level `role` command never enters a runtime Session; that is
892
+ // `session enter`.
893
+ throw usageError("Use 'yui session enter <role>' to attach to a Global Role.");
894
+ }
756
895
  if (resolved[0] === "operator") {
757
896
  if (resolved[1] === "enter") {
758
897
  if (resolved.length !== 2)
@@ -1067,7 +1206,7 @@ export async function main() {
1067
1206
  let archiveRemoteDeliveryProof;
1068
1207
  let archiveTaskReviewCandidate;
1069
1208
  if (resolved[1] === "archive") {
1070
- const { taskId, disposition, forceUnverified } = validateTaskArchiveRequest(resolved.slice(2), store, {
1209
+ const { taskId, disposition, force } = validateTaskArchiveRequest(resolved.slice(2), store, {
1071
1210
  runtime,
1072
1211
  environment: process.env,
1073
1212
  yuiHome: home
@@ -1075,11 +1214,26 @@ export async function main() {
1075
1214
  const task = store.getTask(taskId);
1076
1215
  if (task === null)
1077
1216
  throw new Error(`Task disappeared after archive validation: ${taskId}.`);
1078
- if (task.status !== "archived") {
1217
+ if (force || task.status === "archived") {
1218
+ // Archive admission and mandatory audit commit before any fallible
1219
+ // filesystem/provider work. Repeats report facts, never replay cleanup.
1220
+ const admitted = runTaskCommand(resolved.slice(1), store, {
1221
+ runtime, environment: process.env, yuiHome: home
1222
+ });
1223
+ if (force && admitted.kind === "output"
1224
+ && admitted.data.changed) {
1225
+ await workspaceCoordinator.cleanupArchivedTask(taskId, disposition);
1226
+ }
1227
+ const current = store.getTask(taskId);
1228
+ const archive = taskArchiveDiagnostics(store, current);
1229
+ emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
1230
+ return;
1231
+ }
1232
+ {
1079
1233
  if (disposition === "integrated") {
1080
1234
  archiveTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env);
1081
1235
  archiveRemoteDeliveryProof = createTaskRemoteDeliveryProof(store, task, archiveTaskReviewCandidate ?? null);
1082
- assertTaskRemoteDeliveryIntegrated(archiveRemoteDeliveryProof.delivery, { forceUnverified });
1236
+ assertTaskRemoteDeliveryIntegrated(archiveRemoteDeliveryProof.delivery);
1083
1237
  }
1084
1238
  const workItemIds = store.listManagedWorkspaces(task.id)
1085
1239
  .flatMap(({ owner }) => owner.type === "work-item" ? [owner.workItemId] : []);
@@ -1133,9 +1287,8 @@ export async function main() {
1133
1287
  // Authority and pure Lane-shape checks precede every physical or
1134
1288
  // durable workspace preparation performed for dispatch.
1135
1289
  assertTaskDeliveryAuthority(store, process.env, task.id);
1136
- if (item.assignee !== undefined) {
1137
- workItemDispatchLanePlan(resolved, store, item);
1138
- }
1290
+ requireWorkItemAssignee(item);
1291
+ workItemDispatchLanePlan(resolved, store, item);
1139
1292
  }
1140
1293
  // A rejected Candidate starts a new execution iteration. Release every
1141
1294
  // terminal Lane Role runtime before preparing the new Lane workspaces;
@@ -1259,17 +1412,17 @@ export async function main() {
1259
1412
  laneDispatchRelease = preparedLanes.release;
1260
1413
  laneDispatchProjectPaths = preparedLanes.projectPaths;
1261
1414
  }
1262
- const candidateGitSnapshot = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
1263
- const directTaskMainSnapshot = await directTaskMainSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
1415
+ const candidateSnapshots = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
1264
1416
  const actualTaskReviewCandidate = archiveRemoteDeliveryProof === undefined
1265
1417
  ? await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env)
1266
1418
  : archiveTaskReviewCandidate;
1267
1419
  const deltaRecheckPreflight = await deltaRecheckPreflightForTaskCommand(resolved.slice(1), store, actualTaskReviewCandidate);
1268
- // Read-only, and only for a Session inspect. The command itself stays
1420
+ // Read-only Host evidence for Session inspect and Role status/list. The command stays
1269
1421
  // synchronous over persisted state; this is the live reading it prints
1270
1422
  // beside those facts, prepared here because the Host is reached over a
1271
1423
  // socket.
1272
- const liveRunConfiguration = await liveRunConfigurationForTaskCommand(resolved, store, home);
1424
+ const liveHostObservations = await liveHostObservationsForTaskCommand(resolved, store, home);
1425
+ const liveRunConfiguration = runConfigurationForHostObservation(liveHostObservations?.[resolved[5] ?? ""]);
1273
1426
  // Physical preparation may precede the durable write, but Task status,
1274
1427
  // workspace identity/cwd, and ManagedWorkspace ownership are adopted by
1275
1428
  // one transaction. A failed attempt therefore leaves the Task Draft and
@@ -1295,11 +1448,10 @@ export async function main() {
1295
1448
  ? {}
1296
1449
  : { completionPublishedTreeProof }),
1297
1450
  ...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
1298
- ...(candidateGitSnapshot === undefined ? {} : { candidateGitSnapshot }),
1451
+ ...candidateSnapshots,
1299
1452
  ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1300
1453
  ...(taskWorkspaceActivation === undefined ? {} : { taskWorkspaceActivation }),
1301
1454
  ...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
1302
- ...(directTaskMainSnapshot === undefined ? {} : { directTaskMainSnapshot }),
1303
1455
  ...(actualTaskReviewCandidate === undefined
1304
1456
  ? {}
1305
1457
  : { actualTaskReviewCandidate }),
@@ -1312,6 +1464,7 @@ export async function main() {
1312
1464
  ...(liveRunConfiguration === undefined
1313
1465
  ? {}
1314
1466
  : { liveRunConfiguration }),
1467
+ ...(liveHostObservations === undefined ? {} : { liveHostObservations }),
1315
1468
  ...(taskRetirementProof === undefined ? {} : { taskRetirementProof }),
1316
1469
  ...(validateAgentConfiguration === undefined
1317
1470
  ? {}
@@ -1425,12 +1578,19 @@ export async function main() {
1425
1578
  await workspacePreparer.prepareTaskWorkspace(task.id);
1426
1579
  }
1427
1580
  }
1581
+ const controlData = result.data;
1582
+ const failureCode = controlData?.steer?.code ?? controlData?.interrupt?.code;
1583
+ if (failureCode !== undefined) {
1584
+ emitControlFailure(result.output, failureCode, result.data);
1585
+ return;
1586
+ }
1428
1587
  emit(`${result.output}${reviewOutput}`, false, reviewData === undefined
1429
1588
  ? result.data
1430
1589
  : { command: result.data, ...reviewData });
1431
1590
  return;
1432
1591
  }
1433
- if (jsonOutput && result.kind !== "session-stop") {
1592
+ if (jsonOutput && result.kind !== "session-stop"
1593
+ && result.kind !== "input-steer" && result.kind !== "input-interrupt") {
1434
1594
  throw usageError("Task Role view/takeover requires an interactive terminal.");
1435
1595
  }
1436
1596
  if (result.kind === "session-stop") {
@@ -1462,6 +1622,94 @@ export async function main() {
1462
1622
  tmux.attachRole(result.taskId, result.roleName, "read-only");
1463
1623
  return;
1464
1624
  }
1625
+ if (result.kind === "input-steer") {
1626
+ // Core already persisted the Message and proved target + capability +
1627
+ // writer fence. This is the single live edge: one native steer of the
1628
+ // exact current Turn, with no retarget and no fallback to interrupt or
1629
+ // queue. Its durable settlement flows through the steer receipt fold.
1630
+ await ensureFileTaskController(home, { environment: process.env });
1631
+ let control;
1632
+ try {
1633
+ control = await sendAgentHostSteerControl({
1634
+ home,
1635
+ scope: "task",
1636
+ taskId: result.taskId,
1637
+ roleName: result.roleName,
1638
+ control: {
1639
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1640
+ type: "steer-turn",
1641
+ nativeSessionId: result.target.nativeSessionId,
1642
+ nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
1643
+ authority: result.target.authority,
1644
+ run: { attemptId: result.receiptId, boundedText: result.text }
1645
+ }
1646
+ });
1647
+ }
1648
+ catch (error) {
1649
+ throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
1650
+ + `${error instanceof Error ? error.message : String(error)}. `
1651
+ + "The Message is retained and its outcome is recorded from the Host; whether the "
1652
+ + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
1653
+ + "do not reissue the same input under a new requestId or a different action.");
1654
+ }
1655
+ const steer = foldSteerLiveReceipt(control);
1656
+ if (steer.state !== "steered") {
1657
+ emitControlFailure(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_REJECTED", { taskId: result.taskId, roleName: result.roleName, messageId: result.messageId, steer });
1658
+ return;
1659
+ }
1660
+ emit(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), false, {
1661
+ taskId: result.taskId, roleName: result.roleName,
1662
+ messageId: result.messageId, steer
1663
+ });
1664
+ return;
1665
+ }
1666
+ if (result.kind === "input-interrupt") {
1667
+ // The single live edge for interrupt: one native cancel of the exact
1668
+ // current Turn. Never a kill/restart/detach. Any then-handoff was
1669
+ // already claimed durably by Core and is delivered once by the ordinary
1670
+ // continuation path after this Turn reaches a proven terminal.
1671
+ await ensureFileTaskController(home, { environment: process.env });
1672
+ let control;
1673
+ try {
1674
+ control = await sendAgentHostCancelControl({
1675
+ home,
1676
+ scope: "task",
1677
+ taskId: result.taskId,
1678
+ roleName: result.roleName,
1679
+ control: {
1680
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1681
+ type: "cancel",
1682
+ nativeOnly: true,
1683
+ nativeSessionId: result.target.nativeSessionId,
1684
+ // Native cancel names the exact original execution attempt it stops
1685
+ // (Host matches request.attemptId === activeRunAttemptId). That is
1686
+ // distinct from receiptId, the durable identity of this interrupt
1687
+ // control operation — never send the operation id as the turn id.
1688
+ attemptId: result.target.attemptId,
1689
+ authority: result.target.authority
1690
+ }
1691
+ });
1692
+ }
1693
+ catch (error) {
1694
+ recordTaskInterruptResult(store, result.taskId, result.receiptId, { state: "interrupt-unknown", outcome: "cancel-requested" });
1695
+ throw runtimeError(`Interrupt ${result.receiptId} of ${result.taskId}/${result.roleName} did not complete: `
1696
+ + `${error instanceof Error ? error.message : String(error)}. `
1697
+ + "No process was killed; re-read the Session before retrying.");
1698
+ }
1699
+ const interrupt = foldInterruptLiveReceipt(control);
1700
+ recordTaskInterruptResult(store, result.taskId, result.receiptId, interrupt);
1701
+ if (interrupt.state !== "interrupt-requested") {
1702
+ emitControlFailure(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN"
1703
+ : interrupt.state === "interrupt-not-active" ? "NO_ACTIVE_TURN" : "INTERRUPT_REJECTED", { taskId: result.taskId, roleName: result.roleName, receiptId: result.receiptId, interrupt });
1704
+ return;
1705
+ }
1706
+ emit(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), false, {
1707
+ taskId: result.taskId, roleName: result.roleName,
1708
+ ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
1709
+ interrupt
1710
+ });
1711
+ return;
1712
+ }
1465
1713
  const syncAuthority = async (authorityResult) => {
1466
1714
  let control;
1467
1715
  try {
@@ -1766,34 +2014,11 @@ function assertWorkItemExecutionDependenciesForCommand(args, store, environment)
1766
2014
  assertWorkItemDependenciesCompletedForCommand(store, item);
1767
2015
  }
1768
2016
  async function candidateSnapshotForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
1769
- if (args[0] !== "task")
1770
- return undefined;
1771
- const reviewableCandidateCommand = (args[1] === "work" && args[2] === "update"
1772
- && args[3] !== undefined && args[4] === "done") || (args[1] === "work" && args[2] === "group" && args[3] === "resolve"
1773
- && args[4] !== undefined);
1774
- // Explicit Task-final review requests must remain independent of the
1775
- // mutable global review trigger. Candidate snapshots are a delivery
1776
- // boundary for every writable WorkItem, not only review-configured Tasks.
1777
- const groupResolve = args[1] === "work" && args[2] === "group" && args[3] === "resolve";
1778
- if (!reviewableCandidateCommand
1779
- || (groupResolve && args.includes("--decision") && args[args.indexOf("--decision") + 1] !== "accept")) {
1780
- return undefined;
1781
- }
1782
- if (args[1] === "work" && args[2] === "update"
1783
- && args[3] !== undefined && args[4] === "done") {
1784
- const reference = cliWorkItemReference(args[3], environment);
1785
- const workspace = store.getWorkItemWorkspace(reference.taskId, reference.localId);
1786
- if (workspace === null) {
1787
- // The exact Task-final contract intentionally supports a Leader-direct,
1788
- // metadata-only Project Candidate. The command layer performs the full
1789
- // Task/WorkItem/source/contract validation before any aggregate write.
1790
- if (taskFinalReviewContract !== undefined)
1791
- return undefined;
1792
- throw usageError(`Reviewable direct WorkItem has no managed Candidate workspace: ${reference.localId}.`);
1793
- }
1794
- return preparer.snapshotCandidateWorkspace(workspace);
1795
- }
1796
- return undefined;
2017
+ if (args[0] !== "task" || args[1] !== "work" || args[2] !== "update"
2018
+ || args[3] === undefined || args[4] !== "done")
2019
+ return {};
2020
+ const reference = cliWorkItemReference(args[3], environment);
2021
+ return snapshotWorkItemCandidate(store, preparer, reference.taskId, reference.localId, taskFinalReviewContract);
1797
2022
  }
1798
2023
  async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, environment) {
1799
2024
  const isDispatch = args[0] === "task"
@@ -1878,37 +2103,6 @@ async function prepareReviewLaneWorkspaces(taskId, reviewRoundId, store, prepare
1878
2103
  }
1879
2104
  return map;
1880
2105
  }
1881
- async function directTaskMainSnapshotForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
1882
- if (taskFinalReviewContract === undefined
1883
- || args[0] !== "task"
1884
- || args[1] !== "work"
1885
- || args[2] !== "update"
1886
- || args[3] === undefined
1887
- || args[4] !== "done") {
1888
- return undefined;
1889
- }
1890
- const reference = cliWorkItemReference(args[3], environment);
1891
- const item = store.getWorkItem(reference.taskId, reference.localId);
1892
- if (item === null || item.writeProjectIds.length === 0
1893
- || store.getWorkItemWorkspace(reference.taskId, reference.localId) !== null) {
1894
- return undefined;
1895
- }
1896
- const workspace = store.getTaskWorkspace(reference.taskId);
1897
- // Exact Task-final Candidates may intentionally be metadata-only when no
1898
- // Task main exists. They remain review anchors, but are not eligible for the
1899
- // direct ChangeSet capture path.
1900
- if (workspace === null)
1901
- return undefined;
1902
- if (workspace.owner.type !== "task") {
1903
- throw usageError(`Task has no authoritative main workspace: ${reference.taskId}.`);
1904
- }
1905
- try {
1906
- return await preparer.snapshotDirectTaskMain(workspace, item.writeProjectIds);
1907
- }
1908
- catch (error) {
1909
- throw usageError(error instanceof Error ? error.message : String(error));
1910
- }
1911
- }
1912
2106
  async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, environment) {
1913
2107
  if (args[0] !== "task")
1914
2108
  return undefined;
@@ -2368,18 +2562,22 @@ async function warmLegacyRoleConfigurationMutation(commandArgs, store, catalogs)
2368
2562
  * no Host is running for most Sessions ever inspected, and the persisted facts
2369
2563
  * the command prints are worth showing regardless of whether a live one answered.
2370
2564
  */
2371
- async function liveRunConfigurationForTaskCommand(args, store, home) {
2372
- if (args[0] !== "task" || args[1] !== "role" || args[2] !== "session"
2373
- || args[3] !== "inspect" || args.length !== 6) {
2565
+ async function liveHostObservationsForTaskCommand(args, store, home) {
2566
+ if (args[0] !== "task" || args[1] !== "role")
2374
2567
  return undefined;
2375
- }
2376
- const taskId = args[4];
2377
- const roleName = args[5];
2378
- if (taskId === undefined || roleName === undefined)
2568
+ const inspect = args[2] === "session" && args[3] === "inspect" && args.length === 6;
2569
+ const status = args[2] === "status" && args.length === 5;
2570
+ const list = args[2] === "list" && args.length === 4;
2571
+ if (!inspect && !status && !list)
2379
2572
  return undefined;
2380
- // A Session must be recorded as active before asking. Probing a socket for a
2381
- // Role that never ran would report a reach failure as if it were a fact about
2382
- // that Role's Agent.
2573
+ const taskId = args[inspect ? 4 : 3];
2574
+ const roles = list ? store.listRoles(taskId).map(role => role.name) : [args[inspect ? 5 : 4]];
2575
+ const entries = await Promise.all(roles.map(async (roleName) => [
2576
+ roleName, await readLiveHostObservation(store, home, taskId, roleName)
2577
+ ]));
2578
+ return Object.fromEntries(entries.filter((entry) => entry[1] !== undefined));
2579
+ }
2580
+ async function readLiveHostObservation(store, home, taskId, roleName) {
2383
2581
  const sessions = store.getTaskRoleSessionSet(taskId, roleName);
2384
2582
  const active = sessions === null
2385
2583
  ? undefined
@@ -2387,8 +2585,7 @@ async function liveRunConfigurationForTaskCommand(args, store, home) {
2387
2585
  if (active === undefined)
2388
2586
  return undefined;
2389
2587
  if (active.status !== "active") {
2390
- return unknownAgentRunConfiguration(`This Session is ${active.status}, so there is no live Agent to report what it `
2391
- + "is running under.");
2588
+ return { detail: `This Session is ${active.status}; no live Host reading was requested.` };
2392
2589
  }
2393
2590
  try {
2394
2591
  const snapshot = await inspectAgentHost({
@@ -2398,18 +2595,20 @@ async function liveRunConfigurationForTaskCommand(args, store, home) {
2398
2595
  roleName
2399
2596
  });
2400
2597
  if (snapshot.nativeSessionId !== active.nativeSessionId
2401
- || snapshot.adapterId !== active.adapterId
2402
- || snapshot.state === "exited" || snapshot.state === "failed") {
2403
- return unknownAgentRunConfiguration("The Agent Host has no live connection matching the recorded Session.");
2598
+ || snapshot.adapterId !== active.adapterId) {
2599
+ return { detail: "The Agent Host does not match the recorded Session." };
2404
2600
  }
2405
- return snapshot.runConfiguration ?? unknownAgentRunConfiguration("The Agent Host is running but has no Provider Session open, so no Agent "
2406
- + "has reported a configuration yet.");
2601
+ return { snapshot };
2407
2602
  }
2408
2603
  catch (error) {
2409
- return unknownAgentRunConfiguration("The Agent Host for this Session could not be reached, so its live "
2410
- + `configuration is unavailable: ${error instanceof Error ? error.message : String(error)}`);
2604
+ return { detail: `The Agent Host could not be reached: ${error instanceof Error ? error.message : String(error)}` };
2411
2605
  }
2412
2606
  }
2607
+ function runConfigurationForHostObservation(host) {
2608
+ if (host === undefined)
2609
+ return undefined;
2610
+ return host.snapshot?.runConfiguration ?? unknownAgentRunConfiguration(host.detail ?? `Host=${host.snapshot?.state ?? "unknown"}; no live Agent configuration was reported.`);
2611
+ }
2413
2612
  function hasModelOrEffortMutation(args) {
2414
2613
  const operation = (args[0] === "config" && args[1] === "role")
2415
2614
  || (args[0] === "task" && args[1] === "role");
@@ -2547,6 +2746,50 @@ function emit(output, literal = false, data) {
2547
2746
  : { ok: true, data })
2548
2747
  : normalized}\n`);
2549
2748
  }
2749
+ function emitControlFailure(message, code, details) {
2750
+ process.exitCode = 2;
2751
+ process.stdout.write(`${jsonOutput
2752
+ ? JSON.stringify({ ok: false, code, message: message.trim(), details })
2753
+ : message.trim()}\n`);
2754
+ }
2755
+ /**
2756
+ * The human line for a live steer, corrected to the *actual* live acceptance
2757
+ * (decision-3 §7). The store command's `base` line is written optimistically
2758
+ * ("Steering …"); only a proven `steered` keeps it. `steer-unknown` (pending) is
2759
+ * delivery-unknown, and a rejected/unavailable steer did not deliver — in both
2760
+ * cases the Message is retained and the operator must not reissue. There is no
2761
+ * retarget, queue, or interrupt fallback here; this only reports.
2762
+ */
2763
+ function steerReceiptOutput(base, target, messageId, receipt) {
2764
+ if (receipt.state === "steered")
2765
+ return base;
2766
+ const detail = receipt.detail === undefined ? "" : ` ${receipt.detail}`;
2767
+ const head = receipt.state === "steer-unknown"
2768
+ ? `Steer message ${messageId} to ${target} is delivery-unknown (${receipt.outcome}): the Host `
2769
+ + "holds it but the Provider has not yet proven acceptance."
2770
+ : `Steer message ${messageId} to ${target} did not deliver (${receipt.outcome}).`;
2771
+ return `${head}${detail} The Message is retained; re-read the Session before acting, and do not `
2772
+ + "reissue the same input under a new requestId or a different action.\n";
2773
+ }
2774
+ /**
2775
+ * The human line for a live interrupt, corrected to `control.cancellation`
2776
+ * (decision-3 §7). Only a proven stop-request keeps the optimistic `base` line.
2777
+ * `not-active`/`unknown`/unavailable each report that nothing was proven stopped;
2778
+ * no process is ever killed. A then-handoff, if any, was already claimed durably
2779
+ * and is delivered once by the ordinary continuation path after a proven terminal.
2780
+ */
2781
+ function interruptReceiptOutput(base, target, receipt) {
2782
+ if (receipt.state === "interrupt-requested")
2783
+ return `${base.trim()}\nNative cancel requested; Turn termination is not yet proven.\n`;
2784
+ const detail = receipt.detail === undefined ? "" : ` ${receipt.detail}`;
2785
+ const reason = receipt.state === "interrupt-not-active"
2786
+ ? "found no active Turn to stop (not-active)"
2787
+ : receipt.state === "interrupt-unknown"
2788
+ ? "could not prove a stop (unknown)"
2789
+ : `did not complete (${receipt.outcome})`;
2790
+ return `Interrupt of ${target} ${reason}.${detail} No process was killed; re-read the Session `
2791
+ + "before retrying.\n";
2792
+ }
2550
2793
  function withControllerRefreshWarning(output, refresh, label) {
2551
2794
  if (refresh.status !== "failed")
2552
2795
  return output;