@zq-silk/yui 0.6.1 → 0.6.2

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.
@@ -17,11 +17,6 @@ export function runDeliveryGuardPreflight(store, taskId, intent, options = {}) {
17
17
  if (options.budget === true) {
18
18
  const budget = evaluateSemanticBudget(facts);
19
19
  if (budget.exhausted) {
20
- if (mode === "enforce") {
21
- throw usageError(`Semantic progress budget exhausted: ${budget.reason} `
22
- + `Evidence: ${budget.evidence.join(", ")}. `
23
- + "Record a diagnosis/yield and wait for new facts instead of creating more records.");
24
- }
25
20
  warnings.push(`Semantic progress budget: ${budget.reason}`);
26
21
  }
27
22
  }
@@ -2442,13 +2442,20 @@ function reviewWork(args, store, options) {
2442
2442
  && round.candidateId === candidate.id
2443
2443
  && (round.status === "pending" || round.status === "running")))).at(-1);
2444
2444
  if (activeRound !== undefined) {
2445
+ if (activeRound.status === "pending" && activeRound.reviewerRunId === undefined) {
2446
+ return { round: activeRound, run: null, resumed: true };
2447
+ }
2445
2448
  throw usageError(`ReviewRound is already active: ${activeRound.id}/${activeRound.status}.`);
2446
2449
  }
2447
- return queueReviewRound(tx, item, config, "leader", now);
2450
+ const queued = queueReviewRound(tx, item, config, "leader", now);
2451
+ return { ...queued, resumed: false };
2448
2452
  });
2449
2453
  if (result.run !== null) {
2450
2454
  notifyReviewMailbox(options, options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
2451
2455
  }
2456
+ if (result.resumed) {
2457
+ return output(`Review request ${result.round.id} is pending; resuming dispatch.\n`, { reviewRound: result.round });
2458
+ }
2452
2459
  return result.round.status === "failed"
2453
2460
  ? output(`Review could not start for ${result.round.workItemId}: ${result.round.summary}\n`, { reviewRound: result.round })
2454
2461
  : output(`Review requested as ${result.round.id}\n`, { reviewRound: result.round });
@@ -85,6 +85,15 @@ export function runTaskContextCommand(args, store) {
85
85
  ...(nextAction.recommendedCommand === undefined
86
86
  ? []
87
87
  : [` Recommended: ${nextAction.recommendedCommand}`]),
88
+ ...(nextAction.judgmentRequired === undefined
89
+ ? []
90
+ : [` Judgment: ${nextAction.judgmentRequired}`]),
91
+ ...(nextAction.alternatives === undefined || nextAction.alternatives.length === 0
92
+ ? []
93
+ : [
94
+ " Alternatives:",
95
+ ...nextAction.alternatives.map((alternative) => ` ${alternative.kind}: ${alternative.reason}`)
96
+ ]),
88
97
  ...(task.description === undefined ? [] : [`Description: ${compactText(task.description)}`]),
89
98
  ...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
90
99
  ...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
@@ -4,7 +4,8 @@ import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
4
4
  /**
5
5
  * Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
6
6
  * Folds the existing durable records into exactly one protocol-level next
7
- * action with exact refs, preconditions, and the single recommended command.
7
+ * action with exact refs, preconditions, recommended command, alternatives, and
8
+ * the judgment that remains owned by the Leader.
8
9
  * The command never mutates state; when the action is `route-review-findings`
9
10
  * it also prints the minimal repair wave for the failing Review.
10
11
  */
@@ -69,6 +70,20 @@ function renderNextAction(action, repairWave) {
69
70
  ...(action.recommendedCommand === undefined
70
71
  ? []
71
72
  : [`Recommended: ${action.recommendedCommand}`]),
73
+ ...(action.judgmentRequired === undefined
74
+ ? []
75
+ : [`Judgment: ${action.judgmentRequired}`]),
76
+ ...(action.alternatives === undefined || action.alternatives.length === 0
77
+ ? []
78
+ : [
79
+ "Alternatives:",
80
+ ...action.alternatives.map((alternative) => {
81
+ const command = alternative.recommendedCommand === undefined
82
+ ? ""
83
+ : ` — ${alternative.recommendedCommand}`;
84
+ return ` ${alternative.kind}: ${alternative.reason}${command}`;
85
+ })
86
+ ]),
72
87
  ...(action.conflicts === undefined || action.conflicts.length === 0
73
88
  ? []
74
89
  : [
@@ -43,10 +43,12 @@ export function reconciliationIntervalMilliseconds(value) {
43
43
  }
44
44
  /**
45
45
  * Issue 07 (Leader convergence) feature mode. `display` only shows the
46
- * read-only next-action projection; `warn` additionally reports duplicate
47
- * deliveries; `enforce` hard-blocks exact duplicates and exhausted semantic
48
- * budgets. The mode is additive and optional Homes without it keep the
49
- * `display` default, so no config migration is required.
46
+ * read-only next-action decision support; `warn` additionally reports
47
+ * duplicate deliveries and semantic-budget warnings; `enforce` hard-blocks
48
+ * only exact duplicates, while semantic-budget exhaustion remains a warning
49
+ * because it must not override Leader judgment. The mode is additive and
50
+ * optional — Homes without it keep the `display` default, so no config
51
+ * migration is required.
50
52
  */
51
53
  export const LEADER_NEXT_ACTION_MODES = ["display", "warn", "enforce"];
52
54
  export const DEFAULT_LEADER_NEXT_ACTION_MODE = "display";
@@ -656,6 +656,7 @@ export class SqliteTaskStore {
656
656
  changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
657
657
  integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
658
658
  reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
659
+ reviewConfig: this.getReviewConfig(),
659
660
  openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
660
661
  activeRuns: runs.filter((run) => run.status === "active"),
661
662
  leaderRuns: runs.filter((run) => run.roleName === "leader")
@@ -404,6 +404,7 @@ export class FileTaskStore {
404
404
  changeSets: values(aggregate.changeSets, "id"),
405
405
  integrations: values(aggregate.integrationAttempts, "id"),
406
406
  reviewRounds: values(aggregate.reviewRounds, "id"),
407
+ reviewConfig: this.getReviewConfig(),
407
408
  openInputRequests: values(aggregate.inputRequests, "id")
408
409
  .filter((request) => request.status === "open"),
409
410
  activeRuns: agentRuns.filter((run) => run.status === "active"),
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -100,6 +100,14 @@ unavailable external fact that blocks progress.
100
100
 
101
101
  ## Lead with judgment
102
102
 
103
+ - `yui task next-action <task-id>` is decision support, not an autopilot. It
104
+ reads durable Task records and returns one recommended command, exact refs,
105
+ legitimate alternatives, and any `judgmentRequired` explanation. The Leader
106
+ still owns product priority, acceptance, risk, and the choice among legal
107
+ alternatives. Treat protocol inconsistencies, active Run ownership, open
108
+ InputRequests, Draft activation, exact duplicates, and durable final-review
109
+ contracts as hard boundaries; treat semantic-budget and suspected-duplicate
110
+ warnings as evidence rather than commands.
103
111
  - Keep the context layers distinct in every handoff: Yui Core supplies durable
104
112
  identity, lifecycle, access, workspace, and exact-yield safety; this generic
105
113
  role Skill supplies portable collaboration behavior; the bound Project's