@zq-silk/yui 0.6.1 → 0.6.3

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 (67) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +62 -24
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/deliveryGuardPreflight.js +0 -5
  9. package/dist/commands/taskCommands.js +54 -12
  10. package/dist/commands/taskContextCommand.js +10 -1
  11. package/dist/commands/taskNextActionCommand.js +16 -1
  12. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  13. package/dist/config/yuiConfig.js +6 -4
  14. package/dist/controller/agentRuntimeObserver.js +210 -0
  15. package/dist/controller/clientRuntime.js +3 -21
  16. package/dist/controller/controller.js +47 -7
  17. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  18. package/dist/controller/runtime.js +9 -3
  19. package/dist/controller/runtimeEventInbox.js +49 -295
  20. package/dist/controller/runtimeEventProcessor.js +184 -321
  21. package/dist/controller/runtimeHookRunFence.js +226 -0
  22. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  23. package/dist/controller/runtimeObservationHook.js +112 -0
  24. package/dist/core/controllerServer.js +5 -0
  25. package/dist/executor/agentAdapter.js +18 -3
  26. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  27. package/dist/executor/managedClaudeRunner.js +121 -0
  28. package/dist/observability/executionAudit.js +6 -3
  29. package/dist/repository/taskWorkspacePreparer.js +1 -4
  30. package/dist/run/providerRetryConfig.js +8 -3
  31. package/dist/runtime/agentDriver.js +229 -0
  32. package/dist/runtime/agentDriverObservation.js +57 -0
  33. package/dist/runtime/builtinAgentDrivers.js +235 -0
  34. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  35. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  36. package/dist/runtime/exactControlPlane.js +2 -2
  37. package/dist/runtime/index.js +1 -1
  38. package/dist/runtime/ports.js +12 -1
  39. package/dist/runtime/runtimeObservation.js +297 -0
  40. package/dist/runtime/runtimeProjection.js +277 -0
  41. package/dist/runtime/sessionTerminationGuard.js +78 -22
  42. package/dist/runtime/tmuxAdapters.js +35 -0
  43. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  44. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  45. package/dist/scheduler/roleRunLiveness.js +2 -2
  46. package/dist/scheduler/roleRunStall.js +62 -114
  47. package/dist/storage/migration/productionRegistry.js +41 -0
  48. package/dist/storage/sqliteStore.js +4 -3
  49. package/dist/storage/storageVersions.js +1 -1
  50. package/dist/storage/taskStore.js +1 -0
  51. package/dist/task/nextAction.js +411 -16
  52. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  53. package/dist/telemetry/telemetryCompaction.js +1 -0
  54. package/dist/telemetry/telemetryConfig.js +4 -5
  55. package/dist/tmux/tmuxManager.js +136 -22
  56. package/dist/web/assets/client/view.js +1 -1
  57. package/dist/web/tmuxWebTerminal.js +17 -12
  58. package/dist/web/webSnapshot.js +1 -1
  59. package/dist/worktree/managedWorkspace.js +14 -0
  60. package/i18n/README.zh-CN.md +7 -5
  61. package/package.json +1 -1
  62. package/skills/yui-leader/SKILL.md +8 -0
  63. package/dist/controller/claudeLifecycleHook.js +0 -203
  64. package/dist/controller/codexLifecycleHook.js +0 -108
  65. package/dist/controller/providerHookRunFence.js +0 -156
  66. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  67. package/dist/telemetry/telemetryRouter.js +0 -32
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { sameTaskFinalReviewContract, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
2
3
  import { currentWorkItemCandidate } from "../workItem/workItem.js";
3
4
  const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
4
5
  export function projectNextAction(facts) {
@@ -47,6 +48,17 @@ export function projectNextAction(facts) {
47
48
  ]
48
49
  });
49
50
  }
51
+ if (task.status === "draft") {
52
+ return buildAction(facts, {
53
+ kind: "implement-current-work-item",
54
+ reason: `Task ${task.id} is still a Draft; activate it before dispatching, integrating, reviewing, or completing work.`,
55
+ refs: [ref("task", task.id)],
56
+ preconditions: [
57
+ { fact: "Task is active", satisfied: false, ref: ref("task", task.id) }
58
+ ],
59
+ recommendedCommand: `yui task activate ${task.id}`
60
+ });
61
+ }
50
62
  const candidateReady = facts.workItems
51
63
  .find((item) => item.status === "awaiting_acceptance");
52
64
  if (candidateReady !== undefined) {
@@ -64,6 +76,70 @@ export function projectNextAction(facts) {
64
76
  recommendedCommand: `yui task work reject ${task.id}/${candidateReady.id} --summary \"empty base==head candidate\"`
65
77
  });
66
78
  }
79
+ const activeReview = latestActiveWorkItemReview(facts.reviewRounds, candidateReady, candidate);
80
+ if (activeReview !== undefined) {
81
+ const reviewRef = ref("review-round", activeReview.id);
82
+ if (activeReview.status === "running") {
83
+ if (reviewGroupAwaitingResolution(activeReview)) {
84
+ return buildResolveReviewGroupAction(facts, activeReview);
85
+ }
86
+ if (activeReview.reviewerRunId === undefined) {
87
+ return buildAction(facts, {
88
+ kind: "repair-protocol-inconsistency",
89
+ reason: `ReviewRound ${activeReview.id} is running but has no Reviewer Run.`,
90
+ refs: [reviewRef],
91
+ conflicts: [reviewRef],
92
+ preconditions: [
93
+ { fact: "Running ReviewRound has an exact Reviewer Run", satisfied: false, ref: reviewRef }
94
+ ]
95
+ });
96
+ }
97
+ const reviewRun = activeReviewRoundRun(activeReview, facts.activeRuns);
98
+ if (reviewRun === undefined) {
99
+ const runRef = ref("agent-run", activeReview.reviewerRunId);
100
+ return buildAction(facts, {
101
+ kind: "repair-protocol-inconsistency",
102
+ reason: `ReviewRound ${activeReview.id} references Reviewer Run ${activeReview.reviewerRunId}, but that Run is not active.`,
103
+ refs: [reviewRef, runRef],
104
+ conflicts: [reviewRef, runRef],
105
+ preconditions: [
106
+ { fact: "Reviewer Run is active", satisfied: false, ref: runRef }
107
+ ]
108
+ });
109
+ }
110
+ return buildAction(facts, {
111
+ kind: "wait-for-owned-execution",
112
+ reason: `Reviewer Run ${reviewRun.id} is evaluating Candidate ${candidateReady.id}/${candidate?.id ?? "unknown"}.`,
113
+ refs: [reviewRef, ref("agent-run", reviewRun.id)],
114
+ preconditions: [
115
+ { fact: "ReviewRound is running", satisfied: true, ref: reviewRef },
116
+ { fact: "Reviewer Run is active", satisfied: true, ref: ref("agent-run", reviewRun.id) }
117
+ ]
118
+ });
119
+ }
120
+ if (activeReview.reviewerRunId !== undefined) {
121
+ const runRef = ref("agent-run", activeReview.reviewerRunId);
122
+ return buildAction(facts, {
123
+ kind: "repair-protocol-inconsistency",
124
+ reason: `Pending ReviewRound ${activeReview.id} already references Reviewer Run ${activeReview.reviewerRunId}.`,
125
+ refs: [reviewRef, runRef],
126
+ conflicts: [reviewRef, runRef],
127
+ preconditions: [
128
+ { fact: "Pending ReviewRound has no Reviewer Run", satisfied: false, ref: runRef }
129
+ ]
130
+ });
131
+ }
132
+ return buildAction(facts, {
133
+ kind: "resume-review",
134
+ reason: `ReviewRound ${activeReview.id} is pending and never launched; resume it before accepting or rejecting the Candidate.`,
135
+ refs: [reviewRef],
136
+ preconditions: [
137
+ { fact: "ReviewRound is pending", satisfied: true, ref: reviewRef },
138
+ { fact: "Reviewer Run exists", satisfied: false }
139
+ ],
140
+ recommendedCommand: `yui task work review ${task.id}/${candidateReady.id}`
141
+ });
142
+ }
67
143
  const refs = [
68
144
  ref("work-item", candidateReady.id),
69
145
  ...(candidate === undefined ? [] : [ref("candidate", `${candidateReady.id}/${candidate.id}`)])
@@ -78,7 +154,25 @@ export function projectNextAction(facts) {
78
154
  ? [{ fact: "Candidate record exists", satisfied: false }]
79
155
  : [{ fact: "Candidate record exists", satisfied: true, ref: refs[1] }])
80
156
  ],
81
- recommendedCommand: `yui task work accept ${task.id}/${candidateReady.id} --summary \"<decision>\"`
157
+ recommendedCommand: `yui task work accept ${task.id}/${candidateReady.id} --summary \"<decision>\"`,
158
+ alternatives: [
159
+ {
160
+ kind: "reject-candidate",
161
+ reason: "Reject when the Candidate does not satisfy the Task objective or acceptance criteria.",
162
+ recommendedCommand: `yui task work reject ${task.id}/${candidateReady.id} --summary \"<reason>\"`,
163
+ refs
164
+ },
165
+ ...(candidate?.reviewPolicy === undefined
166
+ || candidate.reviewPolicy.trigger === "final"
167
+ ? []
168
+ : [{
169
+ kind: "re-review-candidate",
170
+ reason: "Request another WorkItem Review when the Leader needs independent evidence before disposition.",
171
+ recommendedCommand: `yui task work review ${task.id}/${candidateReady.id}`,
172
+ refs
173
+ }])
174
+ ],
175
+ judgmentRequired: `Leader must judge Candidate ${candidateReady.id}/${candidate?.id ?? "unknown"} against the Task objective, acceptance criteria, and delivery risk.`
82
176
  });
83
177
  }
84
178
  const activeWorkers = facts.activeRuns.filter((run) => run.roleName !== "leader");
@@ -101,7 +195,8 @@ export function projectNextAction(facts) {
101
195
  preconditions: [
102
196
  { fact: "Work Item is failed", satisfied: true, ref: ref("work-item", failedWork.id) },
103
197
  { fact: "Review Round is failed", satisfied: true, ref: ref("review-round", failedReview.id) }
104
- ]
198
+ ],
199
+ recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
105
200
  });
106
201
  }
107
202
  return buildAction(facts, {
@@ -114,17 +209,46 @@ export function projectNextAction(facts) {
114
209
  recommendedCommand: `yui task work update ${task.id}/${failedWork.id} running`
115
210
  });
116
211
  }
117
- const openWork = facts.workItems
118
- .find((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
119
- if (openWork !== undefined) {
212
+ const openWork = selectOpenWorkItem(facts.workItems);
213
+ if (openWork?.kind === "blocked") {
214
+ const refs = [
215
+ ref("work-item", openWork.itemId),
216
+ ref("work-item", openWork.blockedBy)
217
+ ];
218
+ return buildAction(facts, {
219
+ kind: "repair-protocol-inconsistency",
220
+ reason: `Work Item ${openWork.itemId} depends on ${openWork.blockedBy}, which is not completed or available.`,
221
+ refs,
222
+ conflicts: refs,
223
+ preconditions: [
224
+ { fact: `Dependency ${openWork.blockedBy} is completed`, satisfied: false, ref: refs[1] }
225
+ ]
226
+ });
227
+ }
228
+ if (openWork?.kind === "ready") {
229
+ const item = openWork.item;
230
+ const refs = [ref("work-item", item.id)];
120
231
  return buildAction(facts, {
121
232
  kind: "implement-current-work-item",
122
- reason: `Work Item ${openWork.id} is ${openWork.status}; dispatch or continue its implementation.`,
123
- refs: [ref("work-item", openWork.id)],
233
+ reason: `Work Item ${item.id} is ${item.status}; dispatch or continue its implementation.`,
234
+ refs,
124
235
  preconditions: [
125
- { fact: `Work Item is ${openWork.status}`, satisfied: true, ref: ref("work-item", openWork.id) }
236
+ { fact: `Work Item is ${item.status}`, satisfied: true, ref: refs[0] }
237
+ ],
238
+ recommendedCommand: `yui task work dispatch ${task.id}/${item.id}`,
239
+ alternatives: [
240
+ {
241
+ kind: "execute-directly",
242
+ reason: "Execute the bounded Work Item directly when the Leader's current context and authority are sufficient.",
243
+ refs
244
+ },
245
+ {
246
+ kind: "native-subagent",
247
+ reason: "Use one native implementer subagent when one bounded implementation pass benefits from parallel attention.",
248
+ refs
249
+ }
126
250
  ],
127
- recommendedCommand: `yui task work dispatch ${task.id}/${openWork.id}`
251
+ judgmentRequired: "Leader must choose the execution path: direct execution, a native subagent, or managed Task Role dispatch."
128
252
  });
129
253
  }
130
254
  if (facts.workItems.length === 0) {
@@ -136,9 +260,7 @@ export function projectNextAction(facts) {
136
260
  { fact: "At least one Work Item exists", satisfied: false },
137
261
  { fact: "Task is active", satisfied: task.status === "active" }
138
262
  ],
139
- recommendedCommand: task.status === "draft"
140
- ? `yui task activate ${task.id}`
141
- : `yui task work create ${task.id} \"<objective>\"`
263
+ recommendedCommand: `yui task work create ${task.id} \"<objective>\"`
142
264
  });
143
265
  }
144
266
  const uncaptured = facts.workItems.find((item) => needsChangeSetCapture(facts, item));
@@ -176,10 +298,80 @@ export function projectNextAction(facts) {
176
298
  refs: [ref("review-round", failedFinal.id)],
177
299
  preconditions: [
178
300
  { fact: "Task-final Review is failed", satisfied: true, ref: ref("review-round", failedFinal.id) }
179
- ]
301
+ ],
302
+ recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
303
+ });
304
+ }
305
+ const activeFinal = latestTaskFinalReview(facts.reviewRounds);
306
+ if (activeFinal !== undefined
307
+ && (activeFinal.status === "pending" || activeFinal.status === "running")) {
308
+ const reviewRef = ref("review-round", activeFinal.id);
309
+ if (activeFinal.status === "running") {
310
+ if (reviewGroupAwaitingResolution(activeFinal)) {
311
+ return buildResolveReviewGroupAction(facts, activeFinal);
312
+ }
313
+ if (activeFinal.reviewerRunId === undefined) {
314
+ return buildAction(facts, {
315
+ kind: "repair-protocol-inconsistency",
316
+ reason: `Task-final ReviewRound ${activeFinal.id} is running but has no Reviewer Run.`,
317
+ refs: [reviewRef],
318
+ conflicts: [reviewRef],
319
+ preconditions: [
320
+ { fact: "Running Task-final ReviewRound has an exact Reviewer Run", satisfied: false, ref: reviewRef }
321
+ ]
322
+ });
323
+ }
324
+ const reviewRun = activeReviewRoundRun(activeFinal, facts.activeRuns);
325
+ if (reviewRun === undefined) {
326
+ const runRef = ref("agent-run", activeFinal.reviewerRunId);
327
+ return buildAction(facts, {
328
+ kind: "repair-protocol-inconsistency",
329
+ reason: `Task-final ReviewRound ${activeFinal.id} references Reviewer Run ${activeFinal.reviewerRunId}, but that Run is not active.`,
330
+ refs: [reviewRef, runRef],
331
+ conflicts: [reviewRef, runRef],
332
+ preconditions: [
333
+ { fact: "Reviewer Run is active", satisfied: false, ref: runRef }
334
+ ]
335
+ });
336
+ }
337
+ return buildAction(facts, {
338
+ kind: "wait-for-owned-execution",
339
+ reason: `Reviewer Run ${reviewRun.id} is executing Task-final Review ${activeFinal.id}.`,
340
+ refs: [reviewRef, ref("agent-run", reviewRun.id)],
341
+ preconditions: [
342
+ { fact: "Task-final ReviewRound is running", satisfied: true, ref: reviewRef },
343
+ { fact: "Reviewer Run is active", satisfied: true, ref: ref("agent-run", reviewRun.id) }
344
+ ]
345
+ });
346
+ }
347
+ if (activeFinal.reviewerRunId !== undefined) {
348
+ const runRef = ref("agent-run", activeFinal.reviewerRunId);
349
+ return buildAction(facts, {
350
+ kind: "repair-protocol-inconsistency",
351
+ reason: `Pending Task-final ReviewRound ${activeFinal.id} already references Reviewer Run ${activeFinal.reviewerRunId}.`,
352
+ refs: [reviewRef, runRef],
353
+ conflicts: [reviewRef, runRef],
354
+ preconditions: [
355
+ { fact: "Pending Task-final ReviewRound has no Reviewer Run", satisfied: false, ref: runRef }
356
+ ]
357
+ });
358
+ }
359
+ return buildAction(facts, {
360
+ kind: "resume-review",
361
+ reason: `Task-final ReviewRound ${activeFinal.id} is pending and never launched; retry it under the same semantic Round.`,
362
+ refs: [reviewRef],
363
+ preconditions: [
364
+ { fact: "Task-final ReviewRound is pending", satisfied: true, ref: reviewRef },
365
+ { fact: "Reviewer Run exists", satisfied: false }
366
+ ],
367
+ recommendedCommand: `yui task review retry ${task.id}/${activeFinal.id}`
180
368
  });
181
369
  }
182
- if (task.projectBindings.length > 0 && !hasValidFinalReview(facts)) {
370
+ const finalReviewRequired = taskFinalReviewRequired(facts);
371
+ if (task.projectBindings.length > 0
372
+ && finalReviewRequired
373
+ && !hasValidFinalReview(facts)) {
374
+ const reviewerRole = taskFinalReviewRole(facts);
183
375
  return buildAction(facts, {
184
376
  kind: "request-final-review",
185
377
  reason: "All Work Items are delivered but no valid Task-final Review attests the integrated head.",
@@ -189,9 +381,20 @@ export function projectNextAction(facts) {
189
381
  { fact: "Every ChangeSet is committed", satisfied: true },
190
382
  { fact: "Valid Task-final Review at the integrated head", satisfied: false }
191
383
  ],
192
- recommendedCommand: `yui task review request ${task.id} --role <global-reviewer>`
384
+ recommendedCommand: `yui task review request ${task.id} --role ${reviewerRole ?? "<reviewer-role>"}`
193
385
  });
194
386
  }
387
+ const finalReviewOptional = task.projectBindings.length > 0
388
+ && !finalReviewRequired
389
+ && !hasValidFinalReview(facts);
390
+ const finalReviewAlternative = finalReviewOptional && facts.reviewConfig !== null
391
+ ? [{
392
+ kind: "request-final-review",
393
+ reason: "Request an independent Task-final Review when the Leader wants extra assurance before completion.",
394
+ recommendedCommand: `yui task review request ${task.id} --role ${facts.reviewConfig.roleName}`,
395
+ refs: [ref("task", task.id)]
396
+ }]
397
+ : [];
195
398
  return buildAction(facts, {
196
399
  kind: "complete-task",
197
400
  reason: "The delivery chain is complete; converge the Task instead of creating successor work.",
@@ -202,9 +405,18 @@ export function projectNextAction(facts) {
202
405
  ? []
203
406
  : [
204
407
  { fact: "Every ChangeSet is committed", satisfied: true },
205
- { fact: "Valid Task-final Review at the integrated head", satisfied: true }
408
+ {
409
+ fact: "Valid Task-final Review at the integrated head",
410
+ satisfied: hasValidFinalReview(facts)
411
+ }
206
412
  ])
207
413
  ],
414
+ ...(finalReviewAlternative.length === 0 ? {} : { alternatives: finalReviewAlternative }),
415
+ ...(!finalReviewOptional
416
+ ? {}
417
+ : {
418
+ judgmentRequired: "Leader must decide whether the integrated delivery is safe to complete directly or needs an optional Task-final Review."
419
+ }),
208
420
  recommendedCommand: `yui task complete ${task.id} --summary-file -`
209
421
  });
210
422
  }
@@ -237,10 +449,174 @@ function buildAction(facts, input) {
237
449
  ...(input.recommendedCommand === undefined
238
450
  ? {}
239
451
  : { recommendedCommand: input.recommendedCommand }),
452
+ ...(input.alternatives === undefined || input.alternatives.length === 0
453
+ ? {}
454
+ : { alternatives: input.alternatives }),
455
+ ...(input.judgmentRequired === undefined
456
+ ? {}
457
+ : { judgmentRequired: input.judgmentRequired }),
240
458
  ...(input.conflicts === undefined ? {} : { conflicts: input.conflicts }),
241
459
  fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
242
460
  };
243
461
  }
462
+ function latestActiveWorkItemReview(rounds, item, candidate) {
463
+ if (candidate === undefined)
464
+ return undefined;
465
+ return [...rounds]
466
+ .reverse()
467
+ .find((round) => (round.workItemId === item.id
468
+ && round.candidateId === candidate.id
469
+ && (round.status === "pending" || round.status === "running")));
470
+ }
471
+ function activeReviewRoundRun(round, activeRuns) {
472
+ if (round.executionGroup !== undefined) {
473
+ for (const lane of round.executionGroup.lanes) {
474
+ if (lane.status !== "running"
475
+ || lane.reviewRoundId !== round.id
476
+ || lane.runId === undefined) {
477
+ continue;
478
+ }
479
+ const run = activeRuns.find((candidate) => (candidate.id === lane.runId && candidate.roleName === lane.roleName));
480
+ if (run !== undefined)
481
+ return run;
482
+ }
483
+ return undefined;
484
+ }
485
+ if (round.reviewerRunId === undefined)
486
+ return undefined;
487
+ return activeRuns.find((run) => (run.id === round.reviewerRunId && run.roleName === round.reviewerRoleName));
488
+ }
489
+ const TERMINAL_REVIEW_LANE_STATUSES = new Set(["yielded", "completed", "failed"]);
490
+ function isPanelReviewRound(round) {
491
+ return round.executionGroup !== undefined
492
+ && (round.executionGroup.lanes.length > 1
493
+ || round.executionGroup.strategy.mode === "adaptive");
494
+ }
495
+ function reviewGroupAwaitingResolution(round) {
496
+ return round.status === "running"
497
+ && round.executionGroup !== undefined
498
+ && round.executionGroup.resolution === undefined
499
+ && isPanelReviewRound(round)
500
+ && round.executionGroup.lanes.every((lane) => TERMINAL_REVIEW_LANE_STATUSES.has(lane.status));
501
+ }
502
+ function buildResolveReviewGroupAction(facts, round) {
503
+ const group = round.executionGroup;
504
+ const refs = [
505
+ ref("review-round", round.id),
506
+ ref("execution-group", group.id)
507
+ ];
508
+ const resolveCommand = (decision) => `yui task review group resolve ${facts.task.id}/${round.id}`
509
+ + ` --decision ${decision} --summary \"<decision>\"`;
510
+ return buildAction(facts, {
511
+ kind: "resolve-review-group",
512
+ reason: `Reviewer panel ${group.id} has finished every Lane; the Leader must resolve ReviewRound ${round.id}.`,
513
+ refs,
514
+ preconditions: [
515
+ { fact: "ReviewRound is running", satisfied: true, ref: refs[0] },
516
+ { fact: "Every Reviewer Lane is terminal", satisfied: true, ref: refs[1] },
517
+ { fact: "Leader has resolved the Review ExecutionGroup", satisfied: false, ref: refs[1] }
518
+ ],
519
+ recommendedCommand: resolveCommand("<accept|reject|blocked>"),
520
+ alternatives: [
521
+ {
522
+ kind: "accept-review-group",
523
+ reason: "Accept the usable Lane outputs when their evidence satisfies the Task objective.",
524
+ recommendedCommand: resolveCommand("accept"),
525
+ refs
526
+ },
527
+ {
528
+ kind: "reject-review-group",
529
+ reason: "Reject the panel evidence when it does not support the delivery.",
530
+ recommendedCommand: resolveCommand("reject"),
531
+ refs
532
+ },
533
+ {
534
+ kind: "block-review-group",
535
+ reason: "Block the Review when a material dependency or external fact prevents a sound decision.",
536
+ recommendedCommand: resolveCommand("blocked"),
537
+ refs
538
+ }
539
+ ],
540
+ judgmentRequired: `Leader must judge Review panel ${group.id} against the Task objective, acceptance criteria, and delivery risk.`
541
+ });
542
+ }
543
+ function reviewRoundConflict(round, activeRuns) {
544
+ const reviewRef = ref("review-round", round.id);
545
+ if (round.status === "running") {
546
+ if (reviewGroupAwaitingResolution(round)) {
547
+ return null;
548
+ }
549
+ if (round.reviewerRunId === undefined) {
550
+ return {
551
+ reason: `ReviewRound ${round.id} is running but has no Reviewer Run.`,
552
+ conflicts: [reviewRef]
553
+ };
554
+ }
555
+ if (activeReviewRoundRun(round, activeRuns) === undefined) {
556
+ const runRef = ref("agent-run", round.reviewerRunId);
557
+ return {
558
+ reason: `ReviewRound ${round.id} references Reviewer Run ${round.reviewerRunId}, but that Run is not active.`,
559
+ conflicts: [reviewRef, runRef]
560
+ };
561
+ }
562
+ }
563
+ if (round.status === "pending") {
564
+ const launchedRunId = round.reviewerRunId
565
+ ?? round.executionGroup?.lanes.find((lane) => lane.runId !== undefined)?.runId;
566
+ if (launchedRunId !== undefined) {
567
+ const runRef = ref("agent-run", launchedRunId);
568
+ return {
569
+ reason: `Pending ReviewRound ${round.id} already references Reviewer Run ${launchedRunId}.`,
570
+ conflicts: [reviewRef, runRef]
571
+ };
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ function selectOpenWorkItem(workItems) {
577
+ const byId = new Map(workItems.map((item) => [item.id, item]));
578
+ const openItems = workItems.filter((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
579
+ if (openItems.length === 0)
580
+ return { kind: "none" };
581
+ const eligible = openItems.find((item) => (item.dependsOn.every((dependencyId) => byId.get(dependencyId)?.status === "completed")));
582
+ if (eligible !== undefined)
583
+ return { kind: "ready", item: eligible };
584
+ let current = openItems[0];
585
+ const visited = new Set();
586
+ while (current !== undefined) {
587
+ if (visited.has(current.id)) {
588
+ return { kind: "blocked", itemId: current.id, blockedBy: current.id };
589
+ }
590
+ visited.add(current.id);
591
+ const blockedBy = current.dependsOn
592
+ .find((dependencyId) => byId.get(dependencyId)?.status !== "completed");
593
+ if (blockedBy === undefined)
594
+ return { kind: "ready", item: current };
595
+ const dependency = byId.get(blockedBy);
596
+ if (dependency === undefined || !OPEN_WORK_ITEM_STATUSES.has(dependency.status)) {
597
+ return { kind: "blocked", itemId: current.id, blockedBy };
598
+ }
599
+ current = dependency;
600
+ }
601
+ return { kind: "none" };
602
+ }
603
+ function taskFinalReviewContract(facts) {
604
+ const contract = facts.workItems
605
+ .flatMap((item) => item.candidates)
606
+ .map((candidate) => candidate.taskFinalReviewContract)
607
+ .find((contract) => contract !== undefined);
608
+ return contract === undefined ? undefined : validateTaskFinalReviewContract(contract);
609
+ }
610
+ function taskFinalReviewRequired(facts) {
611
+ return taskFinalReviewContract(facts) !== undefined
612
+ || latestTaskFinalReview(facts.reviewRounds) !== undefined
613
+ || facts.reviewConfig?.trigger === "final";
614
+ }
615
+ function taskFinalReviewRole(facts) {
616
+ return taskFinalReviewContract(facts)?.reviewerRoleName
617
+ ?? latestTaskFinalReview(facts.reviewRounds)?.reviewerRoleName
618
+ ?? (facts.reviewConfig?.trigger === "final" ? facts.reviewConfig.roleName : undefined);
619
+ }
244
620
  function ref(kind, id) {
245
621
  return { kind, id };
246
622
  }
@@ -303,6 +679,25 @@ function hasValidFinalReview(facts) {
303
679
  function detectProtocolInconsistency(facts) {
304
680
  const changeSetIds = new Set(facts.changeSets.map((changeSet) => changeSet.id));
305
681
  const workItemById = new Map(facts.workItems.map((item) => [item.id, item]));
682
+ const contractedCandidates = facts.workItems
683
+ .flatMap((item) => item.candidates.map((candidate) => ({ item, candidate })))
684
+ .filter(({ candidate }) => candidate.taskFinalReviewContract !== undefined);
685
+ if (contractedCandidates.length > 1) {
686
+ const first = validateTaskFinalReviewContract(contractedCandidates[0].candidate.taskFinalReviewContract);
687
+ const conflict = contractedCandidates.find(({ candidate }) => (!sameTaskFinalReviewContract(first, candidate.taskFinalReviewContract)));
688
+ if (conflict !== undefined) {
689
+ const refs = contractedCandidates.map(({ item, candidate }) => ref("candidate", `${item.id}/${candidate.id}`));
690
+ return {
691
+ reason: "Task-final Review contracts conflict across Candidates; the completion gate is ambiguous.",
692
+ conflicts: refs
693
+ };
694
+ }
695
+ }
696
+ for (const round of facts.reviewRounds) {
697
+ const reviewConflict = reviewRoundConflict(round, facts.activeRuns);
698
+ if (reviewConflict !== null)
699
+ return reviewConflict;
700
+ }
306
701
  for (const attempt of facts.integrations) {
307
702
  if (attempt.status !== "committed")
308
703
  continue;
@@ -163,34 +163,6 @@ export class SqliteTelemetryStore {
163
163
  revision() {
164
164
  return this.#applied;
165
165
  }
166
- latestProgressEvents(taskId) {
167
- const db = this.#ensureDb();
168
- if (db === null)
169
- return [];
170
- const rows = db.prepare(`SELECT task_id, role_name, run_id, generation, progress_id, sequence, payload, received_at FROM (
171
- SELECT *, ROW_NUMBER() OVER (
172
- PARTITION BY run_id
173
- ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
174
- ) AS rn FROM telemetry WHERE task_id = ?
175
- ) WHERE rn = 1`).all(taskId);
176
- return rows.map((row) => {
177
- const entry = rowToEntry(row);
178
- const payload = { ...entry.payload };
179
- payload.runId = entry.runId;
180
- payload.progressId = entry.progressId;
181
- // The semantic progress event carries progressAt = observation time;
182
- // mirror that so liveness folds see the same timestamp.
183
- payload.progressAt = entry.receivedAt;
184
- return {
185
- schemaVersion: 2,
186
- id: `telemetry-progress-${entry.runId}-${entry.progressId}`,
187
- taskId: entry.taskId,
188
- type: "runtime.provider-turn-progress",
189
- payload,
190
- createdAt: entry.receivedAt
191
- };
192
- });
193
- }
194
166
  // -- retention -----------------------------------------------------------------
195
167
  pruneGeneration(taskId, roleName, runId, generation, keep = this.#terminalKeep) {
196
168
  const db = this.#ensureDb();
@@ -15,6 +15,7 @@ import { DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
15
15
  * copy only after this module validates the result; a validation failure
16
16
  * throws and leaves the original history unchanged.
17
17
  */
18
+ /** Historical pre-Driver event type accepted only by the offline migration. */
18
19
  export const PROGRESS_EVENT_TYPE = "runtime.provider-turn-progress";
19
20
  /**
20
21
  * Scan a store and build the compaction plan. Read-only: nothing is modified.
@@ -2,10 +2,9 @@
2
2
  * Issue 09 — telemetry retention & compaction configuration.
3
3
  *
4
4
  * Telemetry lives in the Home's authoritative `yui.db` (database-only
5
- * direction). Its activation is an explicit per-Home switch so the feature
6
- * can be rolled out (legacy dual → bounded) and rolled back independently
7
- * of every other Issue. Retention defaults are the schema's own constants
8
- * (§4.4); the environment only overrides them.
5
+ * direction). Its activation is an explicit per-Home diagnostic switch and
6
+ * never changes canonical runtime-state persistence. Retention defaults are
7
+ * the schema's own constants (§4.4); the environment only overrides them.
9
8
  */
10
9
  import { TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "../storage/sqliteSchema.js";
11
10
  export const DEFAULT_TELEMETRY_MODE = "legacy";
@@ -23,7 +22,7 @@ const TELEMETRY_MODES = ["legacy", "dual", "bounded"];
23
22
  /**
24
23
  * Resolve `YUI_TELEMETRY_MODE` (default `legacy`). Only the three exact
25
24
  * values (case-insensitive) are accepted; anything else fails closed at
26
- * startup instead of silently changing progress routing.
25
+ * startup instead of silently changing diagnostic retention.
27
26
  */
28
27
  export function resolveTelemetryMode(env = process.env) {
29
28
  const raw = env.YUI_TELEMETRY_MODE?.trim().toLowerCase();