@zq-silk/yui 0.11.3 → 0.12.0

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 (86) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +36 -13
  3. package/dist/cli/commandCatalog.js +2 -4
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/taskCommands.js +154 -171
  9. package/dist/commands/taskCompletionGate.js +29 -54
  10. package/dist/commands/taskContextCommand.js +55 -11
  11. package/dist/commands/taskInputCommands.js +8 -5
  12. package/dist/commands/taskNextActionCommand.js +56 -6
  13. package/dist/commands/taskOverviewCommand.js +17 -31
  14. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  15. package/dist/commands/taskWorkspaceCommands.js +5 -2
  16. package/dist/config/configCatalog.js +2 -2
  17. package/dist/config/yuiConfig.js +3 -7
  18. package/dist/context/wakeNotification.js +20 -5
  19. package/dist/controller/agentRuntimeObserver.js +247 -51
  20. package/dist/controller/clientRuntime.js +1 -1
  21. package/dist/controller/controller.js +8 -12
  22. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  23. package/dist/controller/runtime.js +3 -3
  24. package/dist/controller/runtimeEventInbox.js +17 -7
  25. package/dist/controller/runtimeEventProcessor.js +12 -19
  26. package/dist/controller/runtimeObservationHook.js +45 -0
  27. package/dist/execution/resourceBroker.js +5 -4
  28. package/dist/executor/agentExecutor.js +4 -4
  29. package/dist/executor/effectiveLaunch.js +8 -48
  30. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  31. package/dist/executor/workspacePreflightClassification.js +23 -2
  32. package/dist/interaction/operatorPresentation.js +33 -89
  33. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  34. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  35. package/dist/observability/orchestrationMetrics.js +8 -3
  36. package/dist/profile/agentProfile.js +1 -1
  37. package/dist/release/cliHomeReleaseFence.js +123 -0
  38. package/dist/repository/taskWorkspacePreparer.js +176 -60
  39. package/dist/resources/sqliteResourceRegistry.js +1 -1
  40. package/dist/review/deltaRecheck.js +12 -51
  41. package/dist/review/reviewAcceptance.js +26 -0
  42. package/dist/review/reviewConfig.js +0 -31
  43. package/dist/review/reviewDecision.js +113 -0
  44. package/dist/review/reviewOutcomeClassifier.js +1 -1
  45. package/dist/review/reviewRound.js +1 -1
  46. package/dist/review/reviewerAvailability.js +69 -0
  47. package/dist/runtime/agentDriverObservation.js +24 -10
  48. package/dist/runtime/builtinAgentDrivers.js +4 -3
  49. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  50. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  51. package/dist/runtime/index.js +1 -0
  52. package/dist/runtime/runtimeObservation.js +13 -0
  53. package/dist/runtime/runtimeProjection.js +16 -25
  54. package/dist/runtime/sessionTokenMetrics.js +181 -0
  55. package/dist/runtime/structuredProviderHost.js +7 -1
  56. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  57. package/dist/scheduler/activeTaskProgress.js +7 -6
  58. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  59. package/dist/scheduler/operatorEvent.js +34 -0
  60. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  61. package/dist/scheduler/roleRunStall.js +0 -14
  62. package/dist/scheduler/taskExecutionProjection.js +97 -76
  63. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  64. package/dist/storage/migration/productionRegistry.js +379 -0
  65. package/dist/storage/sqliteSchema.js +18 -8
  66. package/dist/storage/sqliteStore.js +24 -20
  67. package/dist/storage/storeRpc.js +0 -1
  68. package/dist/storage/taskStore.js +15 -30
  69. package/dist/storage/upgrade/recordVersions.js +3 -2
  70. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  71. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  72. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  73. package/dist/task/completionReadiness.js +10 -6
  74. package/dist/task/nextAction.js +82 -24
  75. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  76. package/dist/web/assets/client/components.js +13 -3
  77. package/dist/web/assets/client/i18n.js +6 -2
  78. package/dist/web/webSnapshot.js +4 -0
  79. package/i18n/README.zh-CN.md +2 -2
  80. package/package.json +1 -1
  81. package/skills/yui-leader/SKILL.md +53 -16
  82. package/skills/yui-operator/SKILL.md +9 -0
  83. package/skills/yui-reviewer/SKILL.md +28 -7
  84. package/dist/context/sessionContextBudget.js +0 -71
  85. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  86. 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
@@ -577,5 +621,5 @@ function resourceUsageLabel(used, remaining, observable) {
577
621
  }
578
622
  function renderWorkItemObservability(item) {
579
623
  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}`;
624
+ 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
625
  }
@@ -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(", ")}`
@@ -8,6 +8,7 @@ import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
8
8
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
9
9
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
10
10
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
11
+ import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
11
12
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
12
13
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
13
14
  .filter((request) => request.status === "open").length;
@@ -65,6 +66,12 @@ export function renderTaskRoleRuntimeStatus(status) {
65
66
  ? ""
66
67
  : ` (${status.runtime.observerDetail})`}`
67
68
  ].filter((value) => value !== undefined).join("; ");
69
+ const sessionTokens = [
70
+ `total=${sessionCumulativeTokenLabel(status.sessionTokens)}`,
71
+ `max-request-input=${status.sessionTokens.maximumRequestInput.status === "observed"
72
+ ? status.sessionTokens.maximumRequestInput.inputTokens
73
+ : "unobserved"}`
74
+ ].join("; ");
68
75
  return [
69
76
  `Task Role status: ${status.taskId}/${status.roleName}`,
70
77
  "",
@@ -87,6 +94,7 @@ export function renderTaskRoleRuntimeStatus(status) {
87
94
  : "none"}`,
88
95
  ` Native session ${nativeSession}`,
89
96
  ` Agent runtime ${runtime}`,
97
+ ` Session tokens ${sessionTokens}`,
90
98
  ` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
91
99
  ` Fresh launch ${status.freshLaunchAllowed ? "allowed" : "blocked"}`,
92
100
  ` tmux pane ${tmux}`,
@@ -178,6 +186,9 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
178
186
  ? { managed: false, path: role.workspace }
179
187
  : { ...managedWorkspace, managed: true };
180
188
  const events = store.listEvents(taskId);
189
+ const sessionTokens = projectSessionTokenMetrics(events, resolveSessionTokenIdentity(nativeSession === null
190
+ ? null
191
+ : { taskId, roleName: role.name, ...nativeSession }));
181
192
  const runtime = projectTaskRoleRuntime(activeRun, nativeSession, tmux, events, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }), store, taskId, role.name, now);
182
193
  const stalled = activeRun !== null && isRoleRunStalled(events, activeRun.id);
183
194
  const stallProgressAt = activeRun === null
@@ -213,10 +224,23 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
213
224
  nativeSession,
214
225
  tmux,
215
226
  workspace,
227
+ sessionTokens,
216
228
  runtime,
217
229
  stall
218
230
  };
219
231
  }
232
+ function sessionCumulativeTokenLabel(metrics) {
233
+ const total = metrics.cumulativeTotal;
234
+ if (total.status === "unobserved")
235
+ return "unobserved";
236
+ const breakdown = [
237
+ `input=${total.inputTokens}`,
238
+ `output=${total.outputTokens}`,
239
+ ...(total.cachedInputTokens === undefined ? [] : [`cached-input=${total.cachedInputTokens}`]),
240
+ ...(total.reasoningTokens === undefined ? [] : [`reasoning=${total.reasoningTokens}`])
241
+ ];
242
+ return `${total.totalTokens} (${breakdown.join(", ")})`;
243
+ }
220
244
  function latestStallKind(events, runId) {
221
245
  const event = [...events]
222
246
  .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
@@ -99,8 +99,11 @@ function replaceTaskCommand(args, store, options) {
99
99
  const old = store.getTask(oldId);
100
100
  if (old === null)
101
101
  throw usageError(`Task not found: ${oldId}.`);
102
- if (old.status === "draft" || old.status === "active") {
103
- throw usageError(`Task is open and can be rebuilt instead of replaced: ${old.id}.`);
102
+ if (old.status === "draft") {
103
+ throw usageError(`Draft Task ${old.id} owns no workspace to replace or rebuild; activate it or retire it and create a new Draft.`);
104
+ }
105
+ if (old.status === "active") {
106
+ throw usageError(`Active Task can be rebuilt instead of replaced: ${old.id}.`);
104
107
  }
105
108
  let title = `Replaces ${old.id}: ${old.title}`;
106
109
  for (let index = 1; index < args.length; index += 1) {
@@ -22,8 +22,8 @@ export const CONFIG_DEFINITIONS = Object.freeze([
22
22
  { key: "provider-retry-max-window-seconds", domain: "runtime", property: "providerRetryMaxWindowSeconds", label: "Provider retry max window", summary: "Positive-integer total retry budget per Run lineage in seconds (default: 600).", takesEffect: "The next retry episode; an active episode keeps its existing deadline." },
23
23
  { key: "leader-next-action", domain: "workflow", property: "leaderNextActionMode", label: "Leader next-action mode", summary: "Leader next-action mode: display, warn, or enforce (default: display).", takesEffect: "The next Leader next-action projection or gate." },
24
24
  { key: "leader-semantic-budget-turns", domain: "workflow", property: "leaderSemanticBudgetTurns", label: "Leader semantic budget", summary: "Consecutive yielded Leader turns without durable delivery progress before warning, 1-20 (default: 3).", takesEffect: "The next Leader delivery guard evaluation." },
25
- { key: "context-budget", domain: "workflow", property: "contextBudget", label: "Context budget", summary: `Per-Session context token budget; soft and hard are ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}, with soft below hard (defaults: 100000/120000).`, takesEffect: "The next context-budget evaluation; retained conversation content is unchanged." },
26
- { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger; optional finding-ledger and delta-recheck controls. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
25
+ { key: "context-budget", domain: "workflow", property: "contextBudget", label: "Legacy context budget (inactive)", summary: `Compatibility-only soft and hard values in ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}; they are parsed but never control Session or workflow behavior.`, takesEffect: "No runtime effect; Session Token metrics are read-only observations." },
26
+ { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger and optional finding-ledger behavior. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
27
27
  { key: "resources-gc-mode", domain: "resources", property: "resourcesGcMode", label: "Resources GC mode", summary: "Resource GC mode: report or quarantine (default: report).", takesEffect: "The next resource GC operation." },
28
28
  { key: "resources-gc-auto-quarantine", domain: "resources", property: "resourcesGcAutoQuarantine", label: "Resources GC auto-quarantine", summary: "Automatically quarantine eligible terminal Task resources (default: false).", takesEffect: "The next eligible automatic resource GC operation." },
29
29
  { key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },