@zq-silk/yui 0.11.3 → 0.12.1

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 (108) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +44 -22
  3. package/dist/cli/commandCatalog.js +13 -10
  4. package/dist/cli/interactionPolicy.js +5 -6
  5. package/dist/cli/updateCommand.js +3 -1
  6. package/dist/cli/updateOrchestrator.js +173 -28
  7. package/dist/cli/updatePorts.js +137 -8
  8. package/dist/cli/upgradeCommand.js +19 -9
  9. package/dist/cli.js +156 -57
  10. package/dist/commands/configCommands.js +13 -46
  11. package/dist/commands/executionAuditCommands.js +3 -2
  12. package/dist/commands/jobCommands.js +4 -10
  13. package/dist/commands/taskCommands.js +234 -200
  14. package/dist/commands/taskCompletionGate.js +29 -54
  15. package/dist/commands/taskContextCommand.js +61 -13
  16. package/dist/commands/taskInputCommands.js +8 -5
  17. package/dist/commands/taskNextActionCommand.js +56 -6
  18. package/dist/commands/taskOverviewCommand.js +17 -31
  19. package/dist/commands/taskRoleRuntimeStatus.js +55 -7
  20. package/dist/commands/taskWorkspaceCommands.js +5 -2
  21. package/dist/config/configCatalog.js +3 -3
  22. package/dist/config/yuiConfig.js +3 -7
  23. package/dist/context/wakeNotification.js +20 -5
  24. package/dist/controller/agentRuntimeObserver.js +247 -51
  25. package/dist/controller/clientRuntime.js +39 -3
  26. package/dist/controller/controller.js +31 -27
  27. package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
  28. package/dist/controller/runtime.js +58 -3
  29. package/dist/controller/runtimeEventInbox.js +17 -7
  30. package/dist/controller/runtimeEventProcessor.js +12 -19
  31. package/dist/controller/runtimeHookRunFence.js +19 -4
  32. package/dist/controller/runtimeObservationHook.js +45 -0
  33. package/dist/controller/structuredProviderObservation.js +20 -3
  34. package/dist/core/controllerClient.js +20 -2
  35. package/dist/core/controllerServer.js +1 -0
  36. package/dist/execution/resourceBroker.js +5 -4
  37. package/dist/executor/agentExecutor.js +52 -50
  38. package/dist/executor/effectiveLaunch.js +8 -48
  39. package/dist/executor/fileRoleLaunchPlanner.js +129 -45
  40. package/dist/executor/workspacePreflightClassification.js +23 -2
  41. package/dist/interaction/operatorPresentation.js +33 -89
  42. package/dist/lifecycle/exactRunTerminalization.js +69 -5
  43. package/dist/observability/executionAudit.js +5 -0
  44. package/dist/observability/orchestrationMetrics.js +8 -3
  45. package/dist/profile/agentProfile.js +1 -1
  46. package/dist/release/cliHomeReleaseFence.js +123 -0
  47. package/dist/release/runtimeRelease.js +20 -0
  48. package/dist/repository/taskWorkspacePreparer.js +176 -60
  49. package/dist/resources/sqliteResourceRegistry.js +1 -1
  50. package/dist/review/deltaRecheck.js +12 -51
  51. package/dist/review/reviewAcceptance.js +26 -0
  52. package/dist/review/reviewConfig.js +0 -31
  53. package/dist/review/reviewDecision.js +113 -0
  54. package/dist/review/reviewOutcomeClassifier.js +1 -1
  55. package/dist/review/reviewRound.js +1 -1
  56. package/dist/review/reviewerAvailability.js +69 -0
  57. package/dist/run/recoveryProjection.js +45 -6
  58. package/dist/runtime/agentDriverObservation.js +24 -10
  59. package/dist/runtime/agentHost.js +159 -85
  60. package/dist/runtime/builtinAgentDrivers.js +4 -3
  61. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  62. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  63. package/dist/runtime/conversationSwitch.js +277 -0
  64. package/dist/runtime/index.js +2 -1
  65. package/dist/runtime/launchBroker.js +12 -0
  66. package/dist/runtime/processExitOutbox.js +88 -0
  67. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  68. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  69. package/dist/runtime/runtimeObservation.js +28 -0
  70. package/dist/runtime/runtimeProjection.js +22 -32
  71. package/dist/runtime/sessionTokenMetrics.js +181 -0
  72. package/dist/runtime/structuredProviderHost.js +7 -1
  73. package/dist/runtime/tmuxAdapters.js +4 -1
  74. package/dist/scheduler/activeRoleRunDelivery.js +34 -199
  75. package/dist/scheduler/activeTaskProgress.js +7 -6
  76. package/dist/scheduler/leaderWakeupProcessor.js +42 -138
  77. package/dist/scheduler/operatorEvent.js +34 -0
  78. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  79. package/dist/scheduler/roleRunStall.js +53 -31
  80. package/dist/scheduler/taskExecutionProjection.js +97 -76
  81. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  82. package/dist/storage/migration/productionRegistry.js +379 -0
  83. package/dist/storage/sqliteSchema.js +66 -23
  84. package/dist/storage/sqliteStore.js +46 -23
  85. package/dist/storage/storeRpc.js +0 -1
  86. package/dist/storage/taskStore.js +15 -30
  87. package/dist/storage/upgrade/homeClassification.js +52 -0
  88. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  89. package/dist/storage/upgrade/recordVersions.js +3 -2
  90. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  91. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  92. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  94. package/dist/task/completionReadiness.js +10 -6
  95. package/dist/task/nextAction.js +82 -58
  96. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  97. package/dist/web/assets/client/components.js +13 -3
  98. package/dist/web/assets/client/i18n.js +6 -2
  99. package/dist/web/webSnapshot.js +7 -1
  100. package/i18n/README.zh-CN.md +2 -2
  101. package/package.json +1 -1
  102. package/skills/yui-leader/SKILL.md +60 -20
  103. package/skills/yui-operator/SKILL.md +13 -4
  104. package/skills/yui-reviewer/SKILL.md +35 -11
  105. package/dist/context/sessionContextBudget.js +0 -71
  106. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  107. package/dist/lifecycle/taskRoleSessionReset.js +0 -126
  108. package/dist/scheduler/operatorNotification.js +0 -59
@@ -3,7 +3,7 @@ import { GitIntegrationService } from "../integration/gitIntegrationService.js";
3
3
  import { createIntegrationAttempt } from "../integration/integrationAttempt.js";
4
4
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
5
5
  import { publicationExternalKey } from "../task/publicationReference.js";
6
- import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
6
+ import { isAcceptedTaskReviewBaseline } from "../review/reviewAcceptance.js";
7
7
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
8
8
  /**
9
9
  * Verify the one supported ancestry waiver before `task complete` mutates any
@@ -29,15 +29,6 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
29
29
  throw usageError(`Publication ${publication.id} must record exact local and remote commits.`);
30
30
  }
31
31
  const workspace = requireTaskWorkspace(store, task);
32
- const latestRecordedReview = latestTaskFinalReview(store, task.id);
33
- const latestReview = latestRecordedReview !== undefined
34
- && isSemanticReviewRound(latestRecordedReview, store)
35
- && latestRecordedReview.taskCandidate !== undefined
36
- ? latestRecordedReview
37
- : undefined;
38
- if (latestRecordedReview !== undefined && latestReview === undefined) {
39
- throw usageError(`Task ${task.id} has an unsettled Task-final Review obligation before accepting a published tree.`);
40
- }
41
32
  const git = options.git ?? new NodeGitWorkspace();
42
33
  const actualHeads = new Map();
43
34
  for (const taskBinding of task.projectBindings) {
@@ -46,22 +37,18 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
46
37
  throw usageError(`Task ${task.id} has no writable managed main workspace for Project ${taskBinding.projectId}.`);
47
38
  }
48
39
  const actualCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
49
- if (latestReview !== undefined) {
50
- const reviewedCommit = latestReview.taskCandidate.projects.find(({ projectId }) => (projectId === taskBinding.projectId))?.commit;
51
- if (reviewedCommit === undefined) {
52
- throw usageError(`Task-final Review ${latestReview.id} omitted Project ${taskBinding.projectId}.`);
53
- }
54
- if (actualCommit !== reviewedCommit) {
55
- throw usageError(`Task-final Review ${latestReview.id} does not match Task head `
56
- + `${taskBinding.projectId}@${actualCommit}.`);
57
- }
58
- }
59
40
  actualHeads.set(taskBinding.projectId, actualCommit);
60
41
  }
61
- if (latestReview !== undefined
62
- && actualHeads.size !== latestReview.taskCandidate.projects.length) {
63
- throw usageError(`Task-final Review ${latestReview.id} Project set does not match Task ${task.id}.`);
64
- }
42
+ const actualCandidate = {
43
+ schemaVersion: 1,
44
+ projects: task.projectBindings.map(({ projectId }) => ({
45
+ projectId,
46
+ commit: actualHeads.get(projectId)
47
+ }))
48
+ };
49
+ const latestReview = acceptedTaskFinalReviews(store, task.id)
50
+ .find((round) => (round.taskCandidate !== undefined
51
+ && sameTaskCandidate(round.taskCandidate, actualCandidate)));
65
52
  const entry = workspaceProjectEntry(workspace, publication.projectId);
66
53
  const localCommit = actualHeads.get(publication.projectId);
67
54
  if (publication.localCommit !== localCommit) {
@@ -124,18 +111,14 @@ export function assertTaskCompletionPublishedTreeProof(store, task, publicationI
124
111
  throw usageError(`Publication evidence changed before Task completion: ${publication.id}.`);
125
112
  }
126
113
  if (proof.reviewRoundId !== undefined) {
127
- const latestReview = latestTaskFinalReview(store, task.id);
128
- if (latestReview === undefined
129
- || latestReview.id !== proof.reviewRoundId
130
- || !isSemanticReviewRound(latestReview, store)
131
- || latestReview.taskCandidate === undefined
132
- || !sameTaskCandidate(latestReview.taskCandidate, actualCandidate)) {
114
+ const review = store.getReviewRound(task.id, proof.reviewRoundId);
115
+ if (review === null
116
+ || !isAcceptedTaskReviewBaseline(store, review)
117
+ || review.taskCandidate === undefined
118
+ || !sameTaskCandidate(review.taskCandidate, actualCandidate)) {
133
119
  throw usageError(`Task-final Review evidence changed before published-tree completion: ${task.id}.`);
134
120
  }
135
121
  }
136
- else if (latestTaskFinalReview(store, task.id) !== undefined) {
137
- throw usageError(`Published-tree proof omitted the Task-final Review: ${task.id}.`);
138
- }
139
122
  const actualCommit = actualCandidate.projects.find(({ projectId }) => (projectId === proof.projectId))?.commit;
140
123
  if (actualCommit !== proof.localCommit) {
141
124
  throw usageError(`Task head changed before published-tree completion: `
@@ -157,11 +140,10 @@ function requireCurrentVerifiedPublication(store, taskId, publicationId) {
157
140
  }
158
141
  return publication;
159
142
  }
160
- function latestTaskFinalReview(store, taskId) {
143
+ function acceptedTaskFinalReviews(store, taskId) {
161
144
  return store.listReviewRounds(taskId)
162
- .filter((round) => (round.scope ?? "work-item") === "task")
163
- .sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })))
164
- .at(-1);
145
+ .filter((round) => isAcceptedTaskReviewBaseline(store, round))
146
+ .sort((left, right) => (right.id.localeCompare(left.id, undefined, { numeric: true })));
165
147
  }
166
148
  function sameTaskCandidate(left, right) {
167
149
  return left.projects.length === right.projects.length
@@ -326,33 +308,26 @@ function requireTaskWorkspace(store, task) {
326
308
  return workspace;
327
309
  }
328
310
  /**
329
- * Check whether the latest Task-final ReviewRound is completed and its
311
+ * Check whether an accepted Task-final ReviewRound is completed and its
330
312
  * immutable candidate exactly matches the current actual heads for every
331
313
  * Project in the Task. When this holds, the reviewed head is the final
332
314
  * head and remote reconciliation must not change it.
333
315
  */
334
316
  async function hasCompletedTaskFinalReviewForCurrentCandidate(store, task, workspace, git) {
335
- const rounds = store.listReviewRounds(task.id)
336
- .filter((round) => (round.scope ?? "work-item") === "task")
337
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
338
- const latest = rounds.at(-1);
339
- if (latest === undefined || !isSemanticReviewRound(latest, store))
340
- return false;
341
- if (latest.taskCandidate === undefined)
342
- return false;
343
- const candidateProjects = new Map(latest.taskCandidate.projects.map((entry) => [entry.projectId, entry.commit]));
317
+ const actualProjects = [];
344
318
  for (const binding of task.projectBindings) {
345
- const reviewedCommit = candidateProjects.get(binding.projectId);
346
- if (reviewedCommit === undefined)
347
- return false;
348
319
  const entry = workspaceProjectEntry(workspace, binding.projectId);
349
320
  if (entry === undefined || entry.access !== "write")
350
321
  return false;
351
322
  const actualHead = (await git.inspect(entry.path, "HEAD")).baseCommit;
352
- if (actualHead !== reviewedCommit)
353
- return false;
323
+ actualProjects.push({ projectId: binding.projectId, commit: actualHead });
354
324
  }
355
- return true;
325
+ const actualCandidate = {
326
+ schemaVersion: 1,
327
+ projects: actualProjects
328
+ };
329
+ return acceptedTaskFinalReviews(store, task.id).some((round) => (round.taskCandidate !== undefined
330
+ && sameTaskCandidate(round.taskCandidate, actualCandidate)));
356
331
  }
357
332
  function latestCommittedIntegration(store, taskId, projectId) {
358
333
  return store.listIntegrationAttempts(taskId)
@@ -369,7 +344,7 @@ function latestCommittedIntegration(store, taskId, projectId) {
369
344
  function hasFrozenTaskBaseline(store, taskId, projectId, currentCommit) {
370
345
  return store.listReviewRounds(taskId)
371
346
  .some((round) => ((round.scope ?? "work-item") === "task"
372
- && isSemanticReviewRound(round, store)
347
+ && isAcceptedTaskReviewBaseline(store, round)
373
348
  && round.taskCandidate !== undefined
374
349
  && round.taskCandidate.projects.some((entry) => (entry.projectId === projectId && entry.commit === currentCommit))));
375
350
  }
@@ -9,6 +9,7 @@ import { summarizeExecutionGroup } from "../execution/executionGroup.js";
9
9
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
10
10
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
11
11
  import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js";
12
+ import { projectReviewDecision } from "../review/reviewDecision.js";
12
13
  const RECENT_RECORD_LIMIT = 5;
13
14
  const RELATED_RECORD_LIMIT = 5;
14
15
  const SUMMARY_TEXT_LIMIT = 400;
@@ -17,7 +18,7 @@ const TERMINAL_WORK_ITEM_STATUSES = new Set([
17
18
  "failed",
18
19
  "retired"
19
20
  ]);
20
- export function runTaskContextCommand(args, store) {
21
+ export function runTaskContextCommand(args, store, currentTaskReviewCandidate = null) {
21
22
  if (args.length !== 1 || args[0]?.trim().length === 0) {
22
23
  throw usageError("Task context usage: yui task context <task>.");
23
24
  }
@@ -46,6 +47,7 @@ export function runTaskContextCommand(args, store) {
46
47
  const events = reader.listEvents(task.id);
47
48
  const agentRuns = chronological(operationalTaskRecords(reader.listAgentRuns(task.id), events, "agent-run"));
48
49
  const reviewRounds = chronological(reader.listReviewRounds(task.id));
50
+ const reviewConfig = reader.getReviewConfig();
49
51
  const changeSets = chronological(reader.listChangeSets(task.id));
50
52
  const integrations = chronological(reader.listIntegrationAttempts(task.id));
51
53
  const publications = chronological(reader.listPublicationReferences(task.id));
@@ -56,7 +58,16 @@ export function runTaskContextCommand(args, store) {
56
58
  return {
57
59
  task,
58
60
  execution,
59
- reviewConfig: reader.getReviewConfig(),
61
+ reviewConfig,
62
+ reviewDecision: projectReviewDecision({
63
+ store: reader,
64
+ task,
65
+ roles,
66
+ runs: agentRuns,
67
+ rounds: reviewRounds,
68
+ reviewConfig,
69
+ currentCandidate: currentTaskReviewCandidate
70
+ }),
60
71
  brief: reader.getTaskBrief(task.id),
61
72
  activeDecisions: reader.listDecisions(task.id)
62
73
  .filter((decision) => decision.status === "active"),
@@ -78,20 +89,19 @@ export function runTaskContextCommand(args, store) {
78
89
  events,
79
90
  nextAction: projectNextAction({
80
91
  ...nextActionFacts,
92
+ currentTaskReviewCandidate,
81
93
  executionGroups: execution.executionGroups,
82
94
  runRecoveries: projectExecutionLaneRunRecoveries(reader, task.id, execution.executionGroups)
83
95
  })
84
96
  };
85
97
  });
86
- const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
98
+ const { task, execution, reviewConfig, reviewDecision, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
87
99
  const timeZone = store.getConfig().timeZone;
88
100
  const displayedActiveDecisions = activeDecisions.slice(-RECENT_RECORD_LIMIT);
89
101
  const displayedWorkItems = currentAndRecentWorkItems(workItems);
90
102
  const displayedOpenInputRequests = openInputRequests.slice(-RECENT_RECORD_LIMIT);
91
103
  const displayedResolvedInputRequests = resolvedInputRequests.slice(-RECENT_RECORD_LIMIT);
92
- const displayedExecutionCarriers = execution.monitoring === "active"
93
- ? execution.activeRuns
94
- : [];
104
+ const displayedActiveRuns = execution.activeRuns;
95
105
  const executionGroupsById = new Map(execution.executionGroups.map((group) => [
96
106
  group.groupId,
97
107
  group
@@ -135,14 +145,26 @@ export function runTaskContextCommand(args, store) {
135
145
  ` Attention: ${execution.attention.length === 0
136
146
  ? "none"
137
147
  : execution.attention.map(({ kind, owner }) => `${kind}/${owner}`).join(", ")}`,
138
- ` Active execution carriers (${displayedExecutionCarriers.length}):`,
139
- ...(displayedExecutionCarriers.length === 0
148
+ ` Active AgentRuns (${displayedActiveRuns.length}):`,
149
+ ...(displayedActiveRuns.length === 0
140
150
  ? [" None."]
141
- : displayedExecutionCarriers.map((run) => (` ${run.roleName}: ${run.id} [${run.status}; ${run.delivered ? "accepted" : "delivery-pending"}]`))),
151
+ : displayedActiveRuns.map((run) => (` ${run.roleName}: ${run.id} [${run.status}/${run.purpose}; ${run.delivered ? "accepted" : "delivery-pending"}${run.workItemId === undefined ? "" : `; work-item=${run.workItemId}`}${run.reviewRoundId === undefined ? "" : `; review-round=${run.reviewRoundId}`}${run.executionGroupId === undefined ? "" : `; group=${run.executionGroupId}`}${run.executionLaneId === undefined ? "" : `; lane=${run.executionLaneId}`}]`))),
142
152
  "Observability:",
143
153
  ` DAG: ${observability.dag.nodes.length} node(s), ${observability.dag.edges.length} edge(s); ready=${observability.dag.readyIds.join(", ") || "none"}; blocked=${observability.dag.blockedIds.join(", ") || "none"}`,
144
154
  ` Cost: tokens=${resourceUsageLabel(observability.cost.tokens, undefined, observability.cost.tokensObservable)}; tools=${resourceUsageLabel(observability.cost.toolCalls, undefined, observability.cost.toolCallsObservable)}; wall=${observability.cost.wallClockSeconds}s; lanes=${observability.cost.laneCount}; groups=${observability.cost.groupCount}; retries=${observability.cost.retryCount}; marginal-value=unavailable`,
145
- ` Context: snapshots=${observability.context.snapshotCount}; bytes=${observability.context.totalBytes === null ? "partial" : observability.context.totalBytes}; peak-input=${observability.context.observedInputPeakTokens}; compression=unavailable`,
155
+ ` Context: snapshots=${observability.context.snapshotCount}; bytes=${observability.context.totalBytes === null ? "partial" : observability.context.totalBytes}; compression=unavailable`,
156
+ " Session tokens:",
157
+ ...(observability.sessionTokens.length === 0
158
+ ? [" unobserved"]
159
+ : observability.sessionTokens.map(({ roleName, agentId, metrics }) => {
160
+ const total = metrics.cumulativeTotal.status === "observed"
161
+ ? metrics.cumulativeTotal.totalTokens
162
+ : "unobserved";
163
+ const maximum = metrics.maximumRequestInput.status === "observed"
164
+ ? metrics.maximumRequestInput.inputTokens
165
+ : "unobserved";
166
+ return ` ${roleName}/${agentId}: total=${total}; max-request-input=${maximum}`;
167
+ })),
146
168
  ...(task.projectBindings.length === 0
147
169
  ? []
148
170
  : [
@@ -166,6 +188,28 @@ export function runTaskContextCommand(args, store) {
166
188
  `Global review: ${reviewConfig === null
167
189
  ? "disabled"
168
190
  : `${reviewConfig.roleName} (${reviewConfig.trigger})`}`,
191
+ "Review decision support:",
192
+ ` Current durable heads: ${reviewDecision.currentCandidate?.projects
193
+ .map(({ projectId, commit }) => `${projectId}@${commit.slice(0, 12)}`).join(", ")
194
+ ?? "unavailable"}`,
195
+ " Active Reviews:",
196
+ ...(reviewDecision.activeReviews.length === 0
197
+ ? [" - none"]
198
+ : reviewDecision.activeReviews.map((review) => (` - ${review.reviewRoundId}/${review.reviewerRoleName}/${review.mode}`
199
+ + ` [${review.status}; current=${review.candidateRelation}]`
200
+ + ` heads=${review.frozenCandidate?.projects.map(({ projectId, commit }) => (`${projectId}@${commit.slice(0, 12)}`)).join(", ") ?? "unavailable"}`
201
+ + `${review.workspaceRoot === undefined
202
+ ? ""
203
+ : ` workspace=${review.workspaceRoot}`}`))),
204
+ ` Reviewer slots: ${reviewDecision.reviewers.length === 0
205
+ ? "none configured"
206
+ : reviewDecision.reviewers.map((reviewer) => (`${reviewer.reviewerRoleName}=${reviewer.status}`
207
+ + `${reviewer.activeRunId === undefined ? "" : `(${reviewer.activeRunId})`}`)).join(", ")}`,
208
+ ` Accepted baseline: ${reviewDecision.latestAcceptedBaseline === null
209
+ ? "none"
210
+ : `${reviewDecision.latestAcceptedBaseline.reviewRoundId}`
211
+ + ` [${reviewDecision.latestAcceptedBaseline.relationToCurrent}]`}`,
212
+ ` Delta: ${reviewDecision.delta.technicalAvailability}; ${reviewDecision.delta.reason}`,
169
213
  "",
170
214
  "Brief:",
171
215
  ...(brief === null
@@ -225,7 +269,9 @@ export function runTaskContextCommand(args, store) {
225
269
  : [` Runtime source at creation: ${creation.payload.runtimeSource}`]),
226
270
  ...(recovery === undefined ? [] : [
227
271
  ` Runtime cleanup: ${recovery.runtimeCleanupPending ? "pending" : "none"}`,
228
- ` Fresh launch: ${recovery.freshLaunchAllowed ? "allowed" : "blocked"}`
272
+ ` Fresh launch: ${recovery.freshLaunchAllowed
273
+ ? "allowed"
274
+ : `blocked (${recovery.freshLaunchBlockers.join(", ")})`}`
229
275
  ])
230
276
  ];
231
277
  })),
@@ -376,7 +422,9 @@ function renderCoordinationMailbox(mailbox) {
376
422
  }
377
423
  function latestStallKind(events, runId) {
378
424
  const event = [...events]
379
- .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
425
+ .filter((candidate) => candidate.type === "run.stalled"
426
+ && candidate.payload.runId === runId
427
+ && candidate.payload.status !== "diagnostic-only")
380
428
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
381
429
  return event?.payload.kind ?? "workflow-not-progressing";
382
430
  }
@@ -577,5 +625,5 @@ function resourceUsageLabel(used, remaining, observable) {
577
625
  }
578
626
  function renderWorkItemObservability(item) {
579
627
  const stages = item.stages.map((stage) => (`${stage.stage ?? "single"}${stage.round === undefined ? "" : `#${stage.round}`}${stage.stageAttempt === undefined ? "" : `/a${stage.stageAttempt}`}`)).join(", ");
580
- return ` Observability: stages=${stages || "none"}; tokens=${item.cost.tokens}; tools=${item.cost.toolCalls}; wall=${item.cost.wallClockSeconds}s; retries=${item.cost.retryCount}; snapshots=${item.context.snapshotCount}; peak-input=${item.context.observedInputPeakTokens}; evidence=${item.evidenceCount}; open-findings=${item.openFindingCount}; compression=${item.context.compressionStatus}`;
628
+ return ` Observability: stages=${stages || "none"}; tokens=${item.cost.tokens}; tools=${item.cost.toolCalls}; wall=${item.cost.wallClockSeconds}s; retries=${item.cost.retryCount}; snapshots=${item.context.snapshotCount}; evidence=${item.evidenceCount}; open-findings=${item.openFindingCount}; compression=${item.context.compressionStatus}`;
581
629
  }
@@ -6,7 +6,7 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
6
6
  import { formatTimestamp } from "../output/timePresentation.js";
7
7
  import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
8
8
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
9
- import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
9
+ import { isRoleRunStalled, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
10
10
  import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
11
11
  import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
12
12
  import { isLinuxProcessLive, listOwnedProcessTree } from "../runtime/sessionOwnerIdentity.js";
@@ -66,9 +66,13 @@ function createRequest(args, store, options) {
66
66
  const wasStalled = isRoleRunStalled(tx.listEvents(task.id), origin.run.id);
67
67
  tx.saveInputRequest(task.id, created);
68
68
  enqueueWork(tx, { kind: "operator" }, "input-requested", now, [
69
- { type: "input", taskId: task.id, id: created.id },
70
- { type: "run", taskId: task.id, id: origin.run.id }
71
- ]);
69
+ { type: "input", taskId: task.id, id: created.id }
70
+ ], {
71
+ source: "input-request",
72
+ dedupeKey: `operator-input:${task.id}:${created.id}`,
73
+ deliveryMode: "followup",
74
+ lane: "normal"
75
+ });
72
76
  const terminal = terminalizeExactTaskRun(tx, {
73
77
  taskId: task.id,
74
78
  roleName: LEADER_ROLE,
@@ -94,7 +98,6 @@ function createRequest(args, store, options) {
94
98
  kind: "input-request"
95
99
  }, now);
96
100
  }
97
- clearMatchingLeaderStallAttention(tx, task.id, origin.run.id);
98
101
  recordTaskEvent(tx, task.id, "input.requested", {
99
102
  requestId: created.id,
100
103
  requesterRunId: created.requester.runId,
@@ -6,6 +6,7 @@ import { projectTaskOrchestration } from "../observability/orchestrationMetrics.
6
6
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
7
7
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
8
8
  import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js";
9
+ import { projectReviewDecision } from "../review/reviewDecision.js";
9
10
  /**
10
11
  * Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
11
12
  * Folds the existing durable records into exactly one protocol-level next
@@ -18,7 +19,7 @@ import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js"
18
19
  * carries the full `completionReadiness` blocker list so the Leader sees every
19
20
  * terminalization precondition before attempting `yui task complete`.
20
21
  */
21
- export function runTaskNextActionCommand(args, store) {
22
+ export function runTaskNextActionCommand(args, store, currentTaskReviewCandidate = null) {
22
23
  const usage = "Task next-action usage: yui task next-action <task> [--json].";
23
24
  const positionals = args.filter((arg) => arg !== "--json");
24
25
  const asJson = args.includes("--json");
@@ -40,6 +41,7 @@ export function runTaskNextActionCommand(args, store) {
40
41
  throw taskNotFound(taskId);
41
42
  const actionFacts = {
42
43
  ...facts,
44
+ currentTaskReviewCandidate,
43
45
  executionGroups: execution.executionGroups,
44
46
  runRecoveries: projectExecutionLaneRunRecoveries(reader, taskId, execution.executionGroups)
45
47
  };
@@ -69,13 +71,25 @@ export function runTaskNextActionCommand(args, store) {
69
71
  completionReadiness = projectCompletionReadiness(readinessFacts);
70
72
  }
71
73
  const events = reader.listEvents(taskId);
74
+ const task = reader.getTask(taskId);
75
+ const operationalRuns = operationalTaskRecords(reader.listAgentRuns(taskId), events, "agent-run");
76
+ const reviewRounds = reader.listReviewRounds(taskId);
77
+ const reviewDecision = projectReviewDecision({
78
+ store: reader,
79
+ task,
80
+ roles: reader.listRoles(taskId),
81
+ runs: operationalRuns,
82
+ rounds: reviewRounds,
83
+ reviewConfig: reader.getReviewConfig(),
84
+ currentCandidate: currentTaskReviewCandidate
85
+ });
72
86
  const orchestration = projectTaskOrchestration({
73
- task: reader.getTask(taskId),
74
- runs: operationalTaskRecords(reader.listAgentRuns(taskId), events, "agent-run"),
87
+ task,
88
+ runs: operationalRuns,
75
89
  roleSessionSets: reader.listRoleSessionSets(taskId),
76
90
  workItems: reader.listWorkItems(taskId),
77
91
  changeSets: reader.listChangeSets(taskId),
78
- reviewRounds: reader.listReviewRounds(taskId),
92
+ reviewRounds,
79
93
  reviewFindings: reader.listReviewFindings(taskId),
80
94
  integrations: reader.listIntegrationAttempts(taskId),
81
95
  durableJobs: reader.listDurableJobs(taskId),
@@ -90,6 +104,8 @@ export function runTaskNextActionCommand(args, store) {
90
104
  repairWave,
91
105
  completionReadiness,
92
106
  knowledgeProposals,
107
+ reviewDecision,
108
+ execution: nextActionExecutionView(execution),
93
109
  orchestration
94
110
  };
95
111
  });
@@ -102,7 +118,7 @@ export function runTaskNextActionCommand(args, store) {
102
118
  }
103
119
  return {
104
120
  kind: "output",
105
- output: renderNextAction(data.action, data.taskType, data.repairWave, data.completionReadiness, data.knowledgeProposals, data.orchestration.advisories),
121
+ output: renderNextAction(data.action, data.taskType, data.repairWave, data.completionReadiness, data.knowledgeProposals, data.reviewDecision, data.execution, data.orchestration.advisories),
106
122
  data
107
123
  };
108
124
  }
@@ -120,12 +136,17 @@ function repairWaveFor(action, facts) {
120
136
  return null;
121
137
  return planRepairWave(round.id, findings);
122
138
  }
123
- function renderNextAction(action, taskType, repairWave, completionReadiness, knowledgeProposals, orchestrationAdvisories) {
139
+ function renderNextAction(action, taskType, repairWave, completionReadiness, knowledgeProposals, reviewDecision, execution, orchestrationAdvisories) {
124
140
  const lines = [
125
141
  `Task: ${action.taskId}`,
126
142
  `Type: ${taskType ?? "unspecified"}`,
127
143
  `Next action: ${action.kind}`,
128
144
  `Reason: ${action.reason}`,
145
+ `Execution view: ${execution.status}; owner/action=${execution.owner}/${execution.action}; reason=${execution.reason}; ${execution.summary}`,
146
+ `Active AgentRuns (${execution.activeRuns.length}):`,
147
+ ...(execution.activeRuns.length === 0
148
+ ? [" none"]
149
+ : execution.activeRuns.map((run) => ` ${renderActiveRun(run)}`)),
129
150
  ...(action.refs.length === 0
130
151
  ? ["Refs: none"]
131
152
  : ["Refs:", ...action.refs.map((ref) => ` ${ref.kind}: ${ref.id}`)]),
@@ -142,6 +163,16 @@ function renderNextAction(action, taskType, repairWave, completionReadiness, kno
142
163
  ...(action.judgmentRequired === undefined
143
164
  ? []
144
165
  : [`Judgment: ${action.judgmentRequired}`]),
166
+ `Review state: active=${reviewDecision.activeReviews.length}; reviewers=${reviewDecision.reviewers
167
+ .map(({ reviewerRoleName, status }) => `${reviewerRoleName}:${status}`).join(", ") || "none"}`,
168
+ ...(reviewDecision.activeReviews.length === 0
169
+ ? []
170
+ : [
171
+ "Frozen Reviews: " + reviewDecision.activeReviews.map((review) => (`${review.reviewRoundId}/${review.reviewerRoleName}`
172
+ + `[${review.status}; current=${review.candidateRelation}; `
173
+ + `${review.frozenCandidate?.projects.map(({ projectId, commit }) => (`${projectId}@${commit.slice(0, 12)}`)).join(", ") ?? "heads-unavailable"}]`)).join("; ")
174
+ ]),
175
+ `Delta support: ${reviewDecision.delta.technicalAvailability}; ${reviewDecision.delta.reason}`,
145
176
  ...(action.alternatives === undefined || action.alternatives.length === 0
146
177
  ? []
147
178
  : [
@@ -188,3 +219,22 @@ function renderNextAction(action, taskType, repairWave, completionReadiness, kno
188
219
  }
189
220
  return `${lines.join("\n")}\n`;
190
221
  }
222
+ function renderActiveRun(run) {
223
+ const subject = run.reviewRoundId === undefined
224
+ ? run.workItemId === undefined ? "task" : `work-item=${run.workItemId}`
225
+ : `review-round=${run.reviewRoundId}`;
226
+ const lane = run.executionGroupId === undefined
227
+ ? ""
228
+ : `; group=${run.executionGroupId}${run.executionLaneId === undefined ? "" : `; lane=${run.executionLaneId}`}`;
229
+ return `${run.id} [${run.purpose}/${run.status}; role=${run.roleName}; ${run.delivered ? "accepted" : "delivery-pending"}; ${subject}${lane}]`;
230
+ }
231
+ function nextActionExecutionView(execution) {
232
+ return {
233
+ status: execution.status,
234
+ owner: execution.owner,
235
+ action: execution.action,
236
+ reason: execution.reason,
237
+ summary: execution.summary,
238
+ activeRuns: execution.activeRuns
239
+ };
240
+ }
@@ -64,7 +64,6 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
64
64
  const agentRuns = store.listAgentRuns(task.id);
65
65
  const events = store.listEvents(task.id);
66
66
  const leaderFailure = store.getLeaderFailure(task.id);
67
- const operatorNotification = store.getOperatorNotification(task.id);
68
67
  const leaderMailbox = store.getWorkMailbox({ kind: "role", taskId: task.id, roleName: "leader" });
69
68
  const pendingWakeup = pendingWakeupProjection(leaderMailbox);
70
69
  const reviewRounds = store.listReviewRounds(task.id);
@@ -74,7 +73,7 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
74
73
  const session = store.getRoleSession(task.id, role.name);
75
74
  return session === null ? [] : [{ roleName: role.name, ...session }];
76
75
  });
77
- const attention = collectAttention(task, agentRuns, events, leaderFailure, operatorNotification);
76
+ const attention = collectAttention(task, agentRuns, events, leaderFailure);
78
77
  const blockers = collectBlockers(workItems, openInputRequests, attention);
79
78
  const legacyNext = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
80
79
  const leader = {
@@ -119,7 +118,6 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
119
118
  pendingWakeup,
120
119
  leaderMailbox,
121
120
  leaderFailure,
122
- operatorNotification,
123
121
  roleSessions,
124
122
  contextSnapshots: store.listContextSnapshots(task.id),
125
123
  now,
@@ -154,37 +152,16 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
154
152
  execution
155
153
  };
156
154
  }
157
- function collectAttention(task, runs, events, failure, notification) {
155
+ function collectAttention(task, runs, events, failure) {
158
156
  const items = [];
159
- if (failure !== null || notification?.type === "leader-recovery-failed") {
157
+ if (failure !== null) {
160
158
  items.push({
161
159
  kind: "leader-recovery",
162
160
  id: `leader-recovery:${task.id}`,
163
161
  status: "failed",
164
162
  owner: "operator",
165
- summary: failure?.message
166
- ?? (notification?.type === "leader-recovery-failed"
167
- ? notification.message
168
- : "Leader recovery failed."),
169
- updatedAt: failure?.lastFailedAt
170
- ?? (notification?.type === "leader-recovery-failed"
171
- ? notification.updatedAt
172
- : task.updatedAt)
173
- });
174
- }
175
- const notificationStall = notification?.type === "leader-stalled"
176
- ? notification
177
- : null;
178
- if (notificationStall !== null) {
179
- items.push({
180
- kind: "leader-stalled",
181
- id: `leader-stall:${notificationStall.runId}:${notificationStall.progressAt}`,
182
- status: "needs-attention",
183
- owner: "operator",
184
- summary: notificationStall.message,
185
- updatedAt: notificationStall.updatedAt,
186
- runId: notificationStall.runId,
187
- roleName: "leader"
163
+ summary: failure.message,
164
+ updatedAt: failure.lastFailedAt
188
165
  });
189
166
  }
190
167
  for (const run of runs) {
@@ -192,8 +169,6 @@ function collectAttention(task, runs, events, failure, notification) {
192
169
  || run.status !== "active"
193
170
  || !isRoleRunStalled(events, run.id))
194
171
  continue;
195
- if (notificationStall?.runId === run.id)
196
- continue;
197
172
  const progressAt = latestStallProgressAt(events, run.id) ?? run.updatedAt;
198
173
  items.push({
199
174
  kind: "leader-stalled",
@@ -399,7 +374,18 @@ function renderVerboseDetails(tasks, timeZone) {
399
374
  ` Monitoring: ${task.execution.monitoring}; attention: ${task.execution.attention.length}`,
400
375
  ` 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"}`,
401
376
  ` 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}`,
402
- ` Context: snapshots=${task.execution.observability.context.snapshotCount}; bytes=${task.execution.observability.context.totalBytes ?? "partial"}; peak-input=${task.execution.observability.context.observedInputPeakTokens}; compression=unavailable`,
377
+ ` Context: snapshots=${task.execution.observability.context.snapshotCount}; bytes=${task.execution.observability.context.totalBytes ?? "partial"}; compression=unavailable`,
378
+ ` Session tokens: ${task.execution.observability.sessionTokens.length === 0
379
+ ? "unobserved"
380
+ : task.execution.observability.sessionTokens.map(({ roleName, metrics }) => {
381
+ const total = metrics.cumulativeTotal.status === "observed"
382
+ ? metrics.cumulativeTotal.totalTokens
383
+ : "unobserved";
384
+ const maximum = metrics.maximumRequestInput.status === "observed"
385
+ ? metrics.maximumRequestInput.inputTokens
386
+ : "unobserved";
387
+ return `${roleName}: total=${total}, max-request-input=${maximum}`;
388
+ }).join("; ")}`,
403
389
  ` Projects: ${task.projectBindings.length === 0
404
390
  ? "none"
405
391
  : task.projectBindings.map(({ directory, projectId, baseRef }) => (`${directory} (${projectId} @ ${baseRef})`)).join(", ")}`