@zq-silk/yui 0.15.9 → 0.15.12

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 (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -3,7 +3,7 @@ import { assertProjectActive, resolveProject } from "../repository/project.js";
3
3
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
4
4
  import { usageError } from "../errors/cliError.js";
5
5
  import { defaultTableWidth, renderTable } from "../output/table.js";
6
- import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration, updateIntegrationAttempt } from "../integration/integrationAttempt.js";
6
+ import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration } from "../integration/integrationAttempt.js";
7
7
  import { GitIntegrationService } from "../integration/gitIntegrationService.js";
8
8
  import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
9
9
  import { runTaskIntegrationQueueCommand } from "./taskIntegrationQueueCommands.js";
@@ -64,7 +64,9 @@ async function cleanupIntegration(args, store, home, environment) {
64
64
  + `${activeIntegrationJob.id}/${activeIntegrationJob.status}. `
65
65
  + "Cancel or acknowledge it before cleanup.");
66
66
  }
67
- const result = await new GitIntegrationService(home, store).cleanup(integration);
67
+ const result = await new GitIntegrationService(home, store).cleanup(integration, {
68
+ authorize: () => taskLocalActor(store, environment, integration.taskId)
69
+ });
68
70
  if (result === "dirty") {
69
71
  throw usageError(`Integration worktree contains unresolved or uncommitted changes: ${integration.id}. Inspect it before cleanup.`);
70
72
  }
@@ -171,6 +173,7 @@ async function continueIntegration(args, store, home, now, options) {
171
173
  taskLocalActor(store, options.environment, integration.taskId);
172
174
  if (integration.status !== "validating"
173
175
  && integration.status !== "running"
176
+ && integration.status !== "conflicted"
174
177
  && (integration.status !== "blocked"
175
178
  || integration.resolution?.action !== "manual-resolution")) {
176
179
  throw usageError(`Integration is not ready to continue: ${integration.id}/${integration.status}.`);
@@ -181,11 +184,13 @@ async function runIntegration(store, home, integration, now, options) {
181
184
  const result = await new GitIntegrationService(home, store, undefined, now, options.environment, undefined, options.jobPort).integrate(integration.taskId, integration.id);
182
185
  const output = result.status === "committed"
183
186
  ? `Integrated ${integrationSourceLabel(result.attempt)} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
184
- : result.status === "blocked"
185
- ? `Integration ${result.attempt.id} requires a Task Agent resolution decision in ${result.workspace.path}\n`
186
- : result.status === "checks-running"
187
- ? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
188
- : `Integration ${result.attempt.id} failed; target ref was not advanced\n`;
187
+ : result.status === "conflicted"
188
+ ? `Integration ${result.attempt.id} is conflicted in ${result.workspace.path}; resolve files, then run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' (no prior resolve required)\n`
189
+ : result.status === "blocked"
190
+ ? `Integration ${result.attempt.id} needs Leader attention in ${result.workspace.path}: ${result.attempt.summary ?? result.attempt.conflict?.summary ?? "Inspect the current attempt"}\n`
191
+ : result.status === "checks-running"
192
+ ? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
193
+ : `Integration ${result.attempt.id} failed; inspect its target and preserved evidence\n`;
189
194
  return { output, data: result };
190
195
  }
191
196
  function resolveDecision(args, store, now, environment, home) {
@@ -223,41 +228,16 @@ async function abortIntegration(args, store, now, options, home) {
223
228
  const integration = requireIntegration(store, parsed.positionals[0], options.environment);
224
229
  requireActiveIntegrationTask(store, integration);
225
230
  taskLocalActor(store, options.environment, integration.taskId);
226
- if (integration.status !== "running" && integration.status !== "blocked") {
227
- throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
228
- }
229
231
  const reason = parsed.one.get("--reason");
230
232
  if (reason === undefined)
231
233
  throw usageError(usage);
232
- if (integration.jobId !== undefined && options.jobPort !== undefined) {
233
- await options.jobPort.cancelJob(integration.taskId, integration.jobId);
234
- }
235
- return store.transaction((tx) => {
236
- // Re-read inside the transaction: a concurrent `continue` can advance the
237
- // Attempt to validating (and then commit the target) after the initial
238
- // read. Aborting a validating or committed Attempt would leave the
239
- // target advanced while the Attempt is failed.
240
- const current = tx.getIntegrationAttempt(integration.taskId, integration.id);
241
- if (current === null) {
242
- throw usageError(`Integration Attempt not found: ${integration.taskId}/${integration.id}.`);
243
- }
244
- if (current.status !== "running" && current.status !== "blocked") {
245
- throw usageError(`Integration cannot be aborted from ${current.status}: ${current.id}.`);
246
- }
247
- taskLocalActor(tx, options.environment, current.taskId);
248
- const aborted = updateIntegrationAttempt(current, {
249
- status: "failed",
250
- checks: [
251
- ...(current.checks ?? []),
252
- { name: "aborted", outcome: "failed", details: reason }
253
- ]
254
- }, now);
255
- tx.saveIntegrationAttempt(aborted.taskId, aborted);
256
- return {
257
- output: `Aborted Integration ${aborted.id}; start a new Integration Attempt to retry\n`,
258
- data: { integration: aborted }
259
- };
260
- });
234
+ const settled = await new GitIntegrationService(home, store, undefined, () => now, options.environment, undefined, options.jobPort).abort(integration.taskId, integration.id, reason, (tx, taskId) => taskLocalActor(tx, options.environment, taskId));
235
+ return {
236
+ output: settled.status === "committed"
237
+ ? `Integration ${settled.id} had already advanced its target; recorded committed, not aborted.\n`
238
+ : `Aborted Integration ${settled.id}; candidate, workspace and history are preserved. This is not Git abort or proof of Job quiescence. Choose an authorized replacement delivery path.\n`,
239
+ data: { integration: settled }
240
+ };
261
241
  }
262
242
  function supersedeIntegrationCommand(args, store, now, environment, home) {
263
243
  const usage = "Task Integration supersede usage: yui task integration supersede <task>/<integration> --reason <text>.";
@@ -138,7 +138,7 @@ async function process(args, store, home, options) {
138
138
  if (result.status === "committed") {
139
139
  return `${entry.id} committed ${entry.changeSetId} -> ${entry.targetAfter ?? "-"}`;
140
140
  }
141
- if (result.status === "blocked") {
141
+ if (result.status === "blocked" || result.status === "conflicted") {
142
142
  return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "blocked"}`;
143
143
  }
144
144
  return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "gate failure"}`;
@@ -7,6 +7,7 @@ import { projectTaskExecutionFromFacts } from "../scheduler/taskExecutionProject
7
7
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
8
8
  import { projectNextAction } from "../task/nextAction.js";
9
9
  import { projectTaskRemoteDeliveryFromStore } from "./taskRemoteDeliveryCommand.js";
10
+ import { formatUsageMetric } from "../runtime/taskUsageMetrics.js";
10
11
  export function parseTaskListOptions(args) {
11
12
  const allowed = new Set(["--all", "--verbose"]);
12
13
  if (args.some((argument) => !allowed.has(argument))
@@ -18,9 +19,9 @@ export function parseTaskListOptions(args) {
18
19
  verbose: args.includes("--verbose")
19
20
  };
20
21
  }
21
- export function buildTaskOverview(store, options, now = new Date()) {
22
+ export function buildTaskOverview(store, options, now = new Date(), taskId) {
22
23
  const runtimeHealthPolicy = resolveRuntimeHealth(store.getConfig().runtimeHealth);
23
- const tasks = store.listTasks()
24
+ const tasks = (taskId === undefined ? store.listTasks() : [store.getTask(taskId)].filter((task) => task !== null))
24
25
  .filter((task) => options.all || task.status !== "archived")
25
26
  .map((task) => buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy));
26
27
  return { tasks };
@@ -355,7 +356,7 @@ function renderVerboseDetails(tasks, timeZone) {
355
356
  ` Remote delivery: ${deliveryCell(task.remoteDelivery)}; source=${task.remoteDelivery.source}${task.remoteDelivery.provisional ? " (provisional)" : ""}`,
356
357
  ` Monitoring: ${task.execution.monitoring}; attention: ${task.execution.attention.length}`,
357
358
  ` DAG: ${task.execution.observability.dag.nodes.length} nodes, ${task.execution.observability.dag.edges.length} edges; ready=${task.execution.observability.dag.readyIds.join(", ") || "none"}; blocked=${task.execution.observability.dag.blockedIds.join(", ") || "none"}`,
358
- ` Cost: tokens=${task.execution.observability.cost.tokens}${task.execution.observability.cost.tokensObservable ? "" : " (partial)"}; tools=${task.execution.observability.cost.toolCalls}${task.execution.observability.cost.toolCallsObservable ? "" : " (partial)"}; wall=${task.execution.observability.cost.wallClockSeconds}s; retries=${task.execution.observability.cost.retryCount}`,
359
+ ` Observed usage (Task lifetime, observed sources only): tokens=${formatUsageMetric(task.execution.observability.cost.tokens)}; tools=${formatUsageMetric(task.execution.observability.cost.toolCalls)}; elapsed=${formatUsageMetric(task.execution.observability.cost.elapsedSeconds, "s")}; native execution sum=${formatUsageMetric(task.execution.observability.cost.executionSeconds, "s")}; retries=${task.execution.observability.cost.retryCount}`,
359
360
  ` Context: snapshots=${task.execution.observability.context.snapshotCount}; bytes=${task.execution.observability.context.totalBytes ?? "partial"}; compression=unavailable`,
360
361
  ` Session tokens: ${task.execution.observability.sessionTokens.length === 0
361
362
  ? "unobserved"
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { contextContentDigest } from "../context/contextSnapshot.js";
4
+ import { createTaskEvent } from "../event/taskEvent.js";
5
+ import { usageError } from "../errors/cliError.js";
6
+ import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
7
+ import { PUBLICATION_ADOPTED_EVENT, publicationAdoption } from "../task/publicationAdoption.js";
8
+ import { publicationExternalKey } from "../task/publicationReference.js";
9
+ import { commitMap, completionEvent } from "../task/remoteDelivery.js";
10
+ import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
11
+ import { assertTaskDeliveryAuthority, taskActor } from "./taskActor.js";
12
+ import { parseRepeatable } from "./taskIntegrationCommands.js";
13
+ /** Local read -> explicit semantic adoption of those exact bytes. No Git writes,
14
+ * provider reads, completion rewrite or implicit acceptance of Integration. */
15
+ export async function runTaskPublicationAdoptCommand(args, store, options = {}) {
16
+ const [command, ...rest] = args;
17
+ const adopt = command === "adopt";
18
+ if (!adopt && command !== "diff")
19
+ throw usageError("Expected publication diff or adopt.");
20
+ const usage = adopt
21
+ ? "yui task publication adopt <task>/<publication> --reviewed-diff <sha256> --acceptance <text> [--integration <id>]"
22
+ : "yui task publication diff <task>/<publication> [--integration <id>]";
23
+ const parsed = parseRepeatable(rest, new Set(), new Set(adopt ? ["--reviewed-diff", "--acceptance", "--integration"] : ["--integration"]), usage);
24
+ if (parsed.positionals.length !== 1)
25
+ throw usageError(usage);
26
+ const ref = resolveTaskRecordReference(parsed.positionals[0], {
27
+ kind: "publicationReference", label: "Publication reference"
28
+ });
29
+ const acceptance = parsed.one.get("--acceptance")?.trim();
30
+ const reviewedDiff = parsed.one.get("--reviewed-diff");
31
+ if (adopt && (!acceptance || !/^[0-9a-f]{64}$/u.test(reviewedDiff ?? ""))) {
32
+ throw usageError(`Adoption requires a reviewed diff digest and explicit acceptance of the full delta. ${usage}`);
33
+ }
34
+ const integrationId = parsed.one.get("--integration");
35
+ const read = (reader) => {
36
+ // The reader addresses only this Task's managed Git objects. Workers and
37
+ // planning Sessions do not acquire publication-adoption authority.
38
+ const actor = adopt
39
+ ? assertTaskDeliveryAuthority(reader, options.environment, ref.taskId)
40
+ : taskActor(options.environment, ref.taskId);
41
+ const task = reader.getTask(ref.taskId);
42
+ if (task === null || task.status !== "completed") {
43
+ throw usageError("Publication candidate adoption/diff requires a completed, unarchived Task.");
44
+ }
45
+ const publication = reader.getPublicationReference(task.id, ref.localId);
46
+ if (publication === null || publication.localCommit === undefined) {
47
+ throw usageError("Publication must record an exact local candidate.");
48
+ }
49
+ const current = reader.findPublicationReferenceByExternalKey(publicationExternalKey(publication));
50
+ if (current?.taskId !== task.id || current.id !== publication.id) {
51
+ throw usageError("Publication is not the current record for its external identity.");
52
+ }
53
+ const completion = task.completedAt === undefined ? undefined
54
+ : completionEvent(reader.listEvents(task.id), task.completedAt);
55
+ const acceptedCommit = commitMap(completion?.payload.projectHeads).get(publication.projectId);
56
+ if (completion === undefined || acceptedCommit === undefined) {
57
+ throw usageError("Exact completion evidence is unavailable; it cannot be inferred or repaired by adoption.");
58
+ }
59
+ const workspace = reader.getTaskWorkspace(task.id);
60
+ const entry = workspace?.entries.find(e => e.projectId === publication.projectId);
61
+ if (workspace?.owner.type !== "task" || workspace.owner.taskId !== task.id
62
+ || entry === undefined
63
+ || !task.projectBindings.some(b => b.projectId === publication.projectId)) {
64
+ throw usageError("The Publication Project has no Task-owned workspace for local Git evidence.");
65
+ }
66
+ const integration = integrationId === undefined ? null
67
+ : reader.getIntegrationAttempt(task.id, integrationId);
68
+ if (integrationId !== undefined && (integration === null
69
+ || integration.status !== "committed"
70
+ || integration.projectId !== publication.projectId
71
+ || integration.afterCommit !== publication.localCommit)) {
72
+ throw usageError("Integration must be this Task/Project's committed exact publication candidate.");
73
+ }
74
+ return { task, publication, completion, acceptedCommit, workspace, entry, integration, actor };
75
+ };
76
+ const facts = store.transaction(read);
77
+ const git = options.git ?? new NodeGitWorkspace();
78
+ const localCommit = facts.publication.localCommit;
79
+ const [acceptedTree, candidateTree, diff] = await Promise.all([
80
+ git.resolveTree(facts.entry.path, facts.acceptedCommit),
81
+ git.resolveTree(facts.entry.path, localCommit),
82
+ git.diffTextBetween({ repositoryPath: facts.entry.path,
83
+ fromCommit: facts.acceptedCommit, toCommit: localCommit, exact: true })
84
+ ]);
85
+ // Bind both endpoints even when the text diff is empty (equal trees).
86
+ const diffDigest = createHash("sha256").update(JSON.stringify({
87
+ acceptedCommit: facts.acceptedCommit, localCommit, acceptedTree, candidateTree, diff
88
+ })).digest("hex");
89
+ const evidence = {
90
+ projectId: facts.publication.projectId,
91
+ publicationId: facts.publication.id,
92
+ completionEventId: facts.completion.id,
93
+ acceptedCommit: facts.acceptedCommit,
94
+ localCommit, acceptedTree, candidateTree, diffDigest,
95
+ ...(facts.integration === null ? {} : {
96
+ integrationId: facts.integration.id, integrationDigest: contextContentDigest(facts.integration)
97
+ })
98
+ };
99
+ // No async Git/provider work under the transaction; fixed objects cannot move.
100
+ const event = store.transaction(tx => {
101
+ if (!isDeepStrictEqual(read(tx), facts)) {
102
+ throw usageError("Task, Publication or Integration changed while reading candidate evidence.");
103
+ }
104
+ if (!adopt)
105
+ return null;
106
+ if (reviewedDiff !== diffDigest) {
107
+ throw usageError(`Reviewed diff does not match this candidate. Read publication diff ${ref.taskId}/${ref.localId}.`);
108
+ }
109
+ const existing = publicationAdoption(facts.publication, facts.completion, facts.acceptedCommit, tx.listEvents(facts.task.id), tx.listPublicationReferences(facts.task.id), tx.listIntegrationAttempts(facts.task.id)).event;
110
+ if (existing !== null && existing.payload.diffDigest === diffDigest
111
+ && existing.payload.acceptance === acceptance
112
+ && existing.payload.integrationId === integrationId)
113
+ return existing;
114
+ const saved = createTaskEvent(tx.nextEventId(facts.task.id), facts.task.id, PUBLICATION_ADOPTED_EVENT, { ...evidence, acceptance: acceptance, by: facts.actor }, options.now?.() ?? new Date());
115
+ tx.saveEvent(facts.task.id, saved);
116
+ return saved;
117
+ });
118
+ return {
119
+ output: event === null
120
+ ? `Publication candidate: ${ref.taskId}/${ref.localId}\nAccepted: ${facts.acceptedCommit}\n`
121
+ + `Candidate: ${localCommit}\nReviewed diff digest: ${diffDigest}\n`
122
+ + "Review all removals, additions and conflict resolutions against the original acceptance before adoption.\n"
123
+ + `${diff}\n`
124
+ : `Adopted Publication candidate ${ref.taskId}/${ref.localId} (${event.id}); remote verification remains separate.\n`,
125
+ data: event === null ? { ...evidence, diff } : event
126
+ };
127
+ }
@@ -17,6 +17,7 @@ const INHERITED_METADATA_FIELDS = [
17
17
  "targetBranch"
18
18
  ];
19
19
  const INHERITED_EVIDENCE_FIELDS = [
20
+ "headCommit",
20
21
  "remoteCommit",
21
22
  "evidence",
22
23
  "mergedAt"
@@ -34,7 +35,7 @@ export function runPublicationCommand(args, store, options) {
34
35
  }
35
36
  }
36
37
  function upsertPublication(args, store, options) {
37
- const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>].";
38
+ const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>].";
38
39
  const parsed = parseUpsertArgs(args, usage);
39
40
  exactPositionals(parsed.positionals, 1, usage);
40
41
  const task = requireTask(store, parsed.positionals[0]);
@@ -72,6 +73,9 @@ function upsertPublication(args, store, options) {
72
73
  ...(parsed.options.has("--remote-commit")
73
74
  ? { remoteCommit: requiredOption(parsed.options, "--remote-commit", usage) }
74
75
  : {}),
76
+ ...(parsed.options.has("--head-commit")
77
+ ? { headCommit: requiredOption(parsed.options, "--head-commit", usage) }
78
+ : {}),
75
79
  ...(state === undefined ? {} : { state }),
76
80
  ...(verification === undefined ? {} : { verification }),
77
81
  ...(parsed.options.has("--evidence")
@@ -181,6 +185,7 @@ function renderPublication(reference, timeZone) {
181
185
  : [`Branches: ${reference.sourceBranch ?? "unknown"} -> ${reference.targetBranch ?? "unknown"}`]),
182
186
  `State: ${reference.state}`,
183
187
  `Verification: ${reference.verification}`,
188
+ ...(reference.headCommit === undefined ? [] : [`Remote head: ${reference.headCommit}`]),
184
189
  `Lineage: ${reference.localCommit ?? "unknown"} -> ${reference.externalId} -> ${reference.remoteCommit ?? "unknown"}`,
185
190
  ...(reference.mergedAt === undefined
186
191
  ? []
@@ -234,6 +239,10 @@ function inheritPublicationInput(existing, input) {
234
239
  const mergedAtChanged = input.mergedAt !== undefined
235
240
  && input.mergedAt !== existing.mergedAt;
236
241
  const evidenceContextChanged = localCommitChanged
242
+ || (input.headCommit !== undefined
243
+ && normalizeCommitForComparison(input.headCommit) !== existing.headCommit)
244
+ || input.projectId !== existing.projectId
245
+ || input.externalKind !== existing.externalKind
237
246
  || stateChanged
238
247
  || remoteCommitChanged
239
248
  || evidenceChanged
@@ -321,7 +330,7 @@ function parseUpsertArgs(args, usage) {
321
330
  const valueOptions = new Set([
322
331
  "--project", "--provider", "--repository", "--kind", "--id", "--url",
323
332
  "--title", "--source-branch", "--target-branch", "--local-commit",
324
- "--remote-commit", "--state", "--evidence", "--merged-at"
333
+ "--remote-commit", "--head-commit", "--state", "--evidence", "--merged-at"
325
334
  ]);
326
335
  const flags = new Set(["--reported", "--verified"]);
327
336
  const positionals = [];
@@ -1,7 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { taskActor } from "./taskActor.js";
3
3
  import { upsertTaskPublication } from "./taskPublicationCommands.js";
4
- import { projectTaskRemoteDeliveryFromStore } from "./taskRemoteDeliveryCommand.js";
5
4
  import { dataError, runtimeError, taskNotFound, usageError, CliError } from "../errors/cliError.js";
6
5
  import { publicationExternalKey } from "../task/publicationReference.js";
7
6
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
@@ -14,8 +13,7 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
14
13
  throw usageError(`Archived Task Publication cannot be verified: ${task.id}.`);
15
14
  }
16
15
  taskActor(options.environment, task.id);
17
- const initialCandidate = await options.candidateForTask?.(reference.taskId) ?? null;
18
- const request = preparePublicationVerification(store, reference.taskId, reference.localId, initialCandidate, options.environment);
16
+ const request = preparePublicationVerification(store, reference.taskId, reference.localId, options.environment);
19
17
  const verifier = options.verifiers[request.publication.provider];
20
18
  if (verifier === undefined) {
21
19
  throw usageError(`Publication verification is not supported for provider `
@@ -42,19 +40,22 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
42
40
  + `${error instanceof Error ? error.message : String(error)}`);
43
41
  }
44
42
  assertVerificationObservation(request, observation);
45
- const finalCandidate = await options.candidateForTask?.(reference.taskId) ?? null;
46
- const result = commitPublicationVerification(store, request, observation, finalCandidate, options.now?.() ?? new Date());
43
+ const result = commitPublicationVerification(store, request, observation, options.now?.() ?? new Date());
47
44
  return {
48
45
  kind: "output",
49
- output: result.idempotent
50
- ? `Publication ${result.reference.id} is already verified for `
51
- + `${result.reference.repository}#${result.reference.externalId}.\n`
52
- : `Verified publication ${result.reference.id} for `
53
- + `${result.reference.repository}#${result.reference.externalId}.\n`,
46
+ output: result.reference.verification !== "verified"
47
+ ? `Recorded Publication ${result.reference.id}: remote state=${observation.state}; `
48
+ + `head=${observation.headCommit}; local=${request.expectedLocalCommit}. `
49
+ + "Not verified; prior verification is no longer current.\n"
50
+ : result.idempotent
51
+ ? `Publication ${result.reference.id} is already verified for `
52
+ + `${result.reference.repository}#${result.reference.externalId}.\n`
53
+ : `Verified publication ${result.reference.id} for `
54
+ + `${result.reference.repository}#${result.reference.externalId}.\n`,
54
55
  data: { publication: result.reference, observation }
55
56
  };
56
57
  }
57
- function preparePublicationVerification(store, taskId, publicationId, currentCandidate, environment) {
58
+ function preparePublicationVerification(store, taskId, publicationId, environment) {
58
59
  return store.transaction((reader) => {
59
60
  const task = reader.getTask(taskId);
60
61
  if (task === null)
@@ -72,27 +73,19 @@ function preparePublicationVerification(store, taskId, publicationId, currentCan
72
73
  throw usageError(`Publication ${publication.id} is not the current unsuperseded record `
73
74
  + `for ${publicationExternalKey(publication)}.`);
74
75
  }
75
- const delivery = projectTaskRemoteDeliveryFromStore(reader, task, currentCandidate);
76
- const project = delivery.projects.find(({ projectId }) => (projectId === publication.projectId));
76
+ const project = task.projectBindings.find(({ projectId }) => (projectId === publication.projectId));
77
77
  if (project === undefined) {
78
78
  throw usageError(`Publication ${publication.id} Project is not bound to Task ${task.id}: `
79
79
  + `${publication.projectId}.`);
80
80
  }
81
- if (project.expectedLocalCommit === null) {
82
- throw usageError(`Task delivery head is unavailable for Publication ${publication.id}.`);
83
- }
84
81
  if (publication.localCommit === undefined) {
85
82
  throw usageError(`Publication ${publication.id} must record a local commit before verification.`);
86
83
  }
87
- if (publication.localCommit !== project.expectedLocalCommit) {
88
- throw usageError(`Publication ${publication.id} local commit ${publication.localCommit} `
89
- + `does not match Task delivery head ${project.expectedLocalCommit}.`);
90
- }
91
84
  return {
92
85
  task: taskVerificationInvariant(task),
93
86
  actor,
94
87
  publication,
95
- expectedLocalCommit: project.expectedLocalCommit
88
+ expectedLocalCommit: publication.localCommit
96
89
  };
97
90
  });
98
91
  }
@@ -104,22 +97,17 @@ function assertVerificationObservation(request, observation) {
104
97
  || observation.externalId !== publication.externalId) {
105
98
  throw usageError(`Provider returned mismatched Publication identity for ${publication.id}.`);
106
99
  }
107
- if (observation.state !== "merged") {
108
- throw usageError(`Remote ${publication.externalKind} ${publication.repository}#`
109
- + `${publication.externalId} is ${observation.state}, not merged.`);
100
+ if (!/^[0-9a-f]{40}$/u.test(observation.headCommit)) {
101
+ throw usageError("Provider observation requires an exact remote head.");
110
102
  }
111
- if (observation.headCommit !== request.expectedLocalCommit) {
112
- throw usageError(`Remote ${publication.externalKind} head ${observation.headCommit} `
113
- + `does not match Task delivery head ${request.expectedLocalCommit}.`);
114
- }
115
- if (observation.remoteCommit === undefined) {
103
+ if (observation.state === "merged" && observation.remoteCommit === undefined) {
116
104
  throw usageError(`Merged remote ${publication.externalKind} did not expose a remote commit.`);
117
105
  }
118
106
  if (observation.evidence.trim().length === 0) {
119
107
  throw usageError("Provider verification evidence is required.");
120
108
  }
121
109
  }
122
- function commitPublicationVerification(store, request, observation, currentCandidate, now) {
110
+ function commitPublicationVerification(store, request, observation, now) {
123
111
  return store.transaction((tx) => {
124
112
  const task = tx.getTask(request.task.id);
125
113
  if (task === null)
@@ -134,12 +122,6 @@ function commitPublicationVerification(store, request, observation, currentCandi
134
122
  throw usageError(`Publication evidence changed during verification: `
135
123
  + `${task.id}/${request.publication.id}.`);
136
124
  }
137
- const delivery = projectTaskRemoteDeliveryFromStore(tx, task, currentCandidate);
138
- const expectedLocalCommit = delivery.projects.find(({ projectId }) => (projectId === request.publication.projectId))?.expectedLocalCommit;
139
- if (expectedLocalCommit !== request.expectedLocalCommit) {
140
- throw usageError(`Task delivery head changed during Publication verification: `
141
- + `${request.publication.projectId}@${expectedLocalCommit ?? "unavailable"}.`);
142
- }
143
125
  return upsertTaskPublication(tx, task, {
144
126
  projectId: request.publication.projectId,
145
127
  provider: request.publication.provider,
@@ -150,9 +132,11 @@ function commitPublicationVerification(store, request, observation, currentCandi
150
132
  ? {}
151
133
  : { externalUrl: observation.externalUrl }),
152
134
  localCommit: request.expectedLocalCommit,
153
- remoteCommit: observation.remoteCommit,
154
- state: "merged",
155
- verification: "verified",
135
+ headCommit: observation.headCommit,
136
+ ...(observation.remoteCommit === undefined ? {} : { remoteCommit: observation.remoteCommit }),
137
+ state: observation.state,
138
+ verification: observation.state === "merged"
139
+ && observation.headCommit === request.expectedLocalCommit ? "verified" : "reported",
156
140
  evidence: observation.evidence,
157
141
  ...(observation.mergedAt === undefined
158
142
  ? {}
@@ -30,6 +30,7 @@ export function projectTaskRemoteDeliveryFromStore(store, task, currentCandidate
30
30
  publications: store.listPublicationReferences(task.id),
31
31
  managedWorkspaces: store.listManagedWorkspaces(task.id),
32
32
  runs: store.listRuns(task.id),
33
+ integrations: store.listIntegrationAttempts(task.id),
33
34
  currentCandidate
34
35
  });
35
36
  }
@@ -41,6 +42,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
41
42
  publications,
42
43
  managedWorkspaces: store.listManagedWorkspaces(task.id),
43
44
  runs: store.listRuns(task.id),
45
+ integrations: store.listIntegrationAttempts(task.id),
44
46
  currentCandidate
45
47
  });
46
48
  return {
@@ -50,7 +52,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
50
52
  delivery
51
53
  };
52
54
  }
53
- export function assertTaskRemoteDeliveryProof(store, task, proof, options = {}) {
55
+ export function assertTaskRemoteDeliveryProof(store, task, proof) {
54
56
  if (proof === undefined || proof.schemaVersion !== 1) {
55
57
  throw usageError(`Task remote-delivery preflight proof is required: ${task.id}.`);
56
58
  }
@@ -60,26 +62,34 @@ export function assertTaskRemoteDeliveryProof(store, task, proof, options = {})
60
62
  if (!isDeepStrictEqual(proof.publications, store.listPublicationReferences(task.id))) {
61
63
  throw usageError(`Task Publication evidence changed after remote-delivery preflight: ${task.id}.`);
62
64
  }
63
- assertTaskRemoteDeliveryIntegrated(proof.delivery, options);
65
+ // Recheck semantic adoption/Integration facts as well as Publication versions.
66
+ // For provisional cancelled Tasks retain the frozen candidate used at preflight.
67
+ const current = projectTaskRemoteDeliveryFromStore(store, task, {
68
+ projects: proof.delivery.projects.flatMap(p => p.expectedLocalCommit === null
69
+ ? [] : [{ projectId: p.projectId, commit: p.expectedLocalCommit }])
70
+ });
71
+ if (!isDeepStrictEqual(current, proof.delivery)) {
72
+ throw usageError(`Task delivery evidence changed after remote-delivery preflight: ${task.id}.`);
73
+ }
74
+ assertTaskRemoteDeliveryIntegrated(proof.delivery);
64
75
  return proof.delivery;
65
76
  }
66
- export function assertTaskRemoteDeliveryIntegrated(delivery, options = {}) {
77
+ export function assertTaskRemoteDeliveryIntegrated(delivery) {
67
78
  if (!delivery.allMerged) {
68
79
  const headUnavailable = delivery.projects.filter((project) => (project.codeDelivery !== "none"
69
80
  && project.coverage === "head-unavailable"));
70
81
  if (headUnavailable.length > 0 && delivery.source === "task-completed") {
71
- throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}. Reopen and complete it again to record exact heads before archiving; `
72
- + "--force cannot override missing head evidence.");
82
+ throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}. Historical acceptance cannot be inferred from a merge or archive.`);
73
83
  }
74
84
  const uncovered = delivery.projects
75
85
  .filter((project) => project.codeDelivery !== "none" && !project.merged)
76
86
  .map((project) => (`${project.projectId}/${project.coverage}`
77
87
  + (project.publication === null ? "" : `/${project.publication.id}`)))
78
88
  .join(", ");
79
- throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}. Record confirmed current-head evidence with task publication upsert, `
80
- + "or use task archive --abandon for an intentional non-merge.");
89
+ throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}. Inspect each Project reason with task remote-delivery. Record confirmed Publication facts; `
90
+ + "for a changed post-completion candidate, review publication diff and explicitly adopt its acceptance.");
81
91
  }
82
- if (delivery.allVerified || options.forceUnverified === true)
92
+ if (delivery.allVerified)
83
93
  return;
84
94
  const unverified = delivery.projects
85
95
  .filter((project) => (project.codeDelivery !== "none"
@@ -90,8 +100,7 @@ export function assertTaskRemoteDeliveryIntegrated(delivery, options = {}) {
90
100
  .join(", ");
91
101
  throw usageError(`Task ${delivery.taskId} has merged Publication evidence that is not verified: `
92
102
  + `${unverified || "verification evidence is unavailable"}. Run task publication verify `
93
- + "for each Publication, or repeat task archive --integrated --force to explicitly "
94
- + "override verification. --force never overrides missing, stale, open, or closed merge evidence.");
103
+ + "for each Publication only when the provider read is authorized.");
95
104
  }
96
105
  export function renderTaskRemoteDelivery(delivery, indent = "") {
97
106
  const source = `${delivery.source}${delivery.provisional ? " (provisional)" : ""}`;
@@ -108,12 +117,14 @@ export function renderTaskRemoteDelivery(delivery, indent = "") {
108
117
  `${indent}Projects:${delivery.projects.length === 0 ? " none" : ""}`,
109
118
  ...delivery.projects.map((project) => (`${indent}- ${project.directory} (${project.projectId}): `
110
119
  + `expected=${shortCommit(project.expectedLocalCommit)}; `
120
+ + `candidate=${shortCommit(project.deliveryLocalCommit)}; `
121
+ + `adoption=${project.adoption?.id ?? "none"}; `
111
122
  + `base=${shortCommit(project.baseCommit)}; `
112
123
  + `publication=${project.publication?.id ?? "none"}; `
113
124
  + `state=${project.state ?? "none"}; `
114
125
  + `verification=${project.verification ?? "none"}; `
115
126
  + `remote=${shortCommit(project.remoteCommit)}; `
116
- + `coverage=${project.coverage}`))
127
+ + `coverage=${project.coverage}\n${indent} ${project.reason}`))
117
128
  ];
118
129
  return `${lines.join("\n")}\n`;
119
130
  }
@@ -11,6 +11,40 @@ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
11
11
  import { formatRunReceiptId } from "../task/taskRecordReference.js";
12
12
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
13
13
  import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
14
+ export function taskRoleHostDiagnostic(host) {
15
+ const snapshot = host.snapshot;
16
+ if (snapshot === undefined)
17
+ return `unavailable: ${host.detail ?? "Host could not be read"}`;
18
+ const delivery = snapshot.eventDelivery;
19
+ return [
20
+ `Provider Host=${snapshot.state}`,
21
+ delivery === undefined ? "event delivery=unreported (legacy Host)" :
22
+ `pending facts=${delivery.pending}; native terminals awaiting acknowledgement=${delivery.pendingTerminals}`,
23
+ delivery?.failure === undefined ? undefined :
24
+ `reporting failure (${delivery.failure.stage}, ${delivery.failure.observedAt}): ${delivery.failure.detail}`,
25
+ snapshot.detail
26
+ ].filter(Boolean).join("; ");
27
+ }
28
+ /** Overlay live reporting evidence without rewriting Run/Provider/business truth. */
29
+ export function withTaskRoleHostObservation(status, host) {
30
+ if (host === undefined)
31
+ return status;
32
+ const snapshot = host.snapshot;
33
+ if (snapshot !== undefined && (snapshot.nativeSessionId !== status.nativeSession?.nativeSessionId
34
+ || snapshot.adapterId !== status.nativeSession?.adapterId)) {
35
+ return { ...status, host: { detail: "Host does not match the recorded native Session." } };
36
+ }
37
+ const reportingNeedsAttention = snapshot !== undefined && (snapshot.state === "failed" || snapshot.state === "exited"
38
+ || snapshot.eventDelivery?.failure !== undefined
39
+ || (snapshot.eventDelivery?.pendingTerminals ?? 0) > 0);
40
+ return {
41
+ ...status, host,
42
+ ...(reportingNeedsAttention ? {
43
+ health: "needs-attention",
44
+ healthReason: `${taskRoleHostDiagnostic(host)}. Formal Run status and business acceptance are unchanged.`
45
+ } : {})
46
+ };
47
+ }
14
48
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
15
49
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
16
50
  .filter((request) => request.status === "open").length;
@@ -99,6 +133,7 @@ export function renderTaskRoleRuntimeStatus(status) {
99
133
  : "none"}`,
100
134
  ` Native session ${nativeSession}`,
101
135
  ` AgentRuntime ${runtime}`,
136
+ ...(status.host === undefined ? [] : [` Host reporting ${taskRoleHostDiagnostic(status.host)}`]),
102
137
  ` Session tokens ${sessionTokens}`,
103
138
  ` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
104
139
  ` tmux pane ${tmux}`,
@@ -1,3 +1,4 @@
1
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
1
2
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
3
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
3
4
  import { RUN_INPUT_MAX_DELTAS } from "./runInputContract.js";
@@ -295,6 +296,8 @@ function readLiveTaskState(store, taskId) {
295
296
  status: round.status
296
297
  }));
297
298
  return Object.freeze({
299
+ providerRetries: Object.freeze(store.listRoleSessionSets(taskId).flatMap(set => set.providerBinding?.retry === undefined
300
+ ? [] : [{ roleName: set.owner.roleName, retry: providerRetryProjection(set.providerBinding) }])),
298
301
  activeRuns: Object.freeze(activeRuns),
299
302
  activeTaskReviews: Object.freeze(activeTaskReviews)
300
303
  });