@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.
- package/dist/commands/deliveryGuardPreflight.js +0 -5
- package/dist/commands/taskCommands.js +8 -1
- package/dist/commands/taskContextCommand.js +9 -0
- package/dist/commands/taskNextActionCommand.js +16 -1
- package/dist/config/yuiConfig.js +6 -4
- package/dist/storage/sqliteStore.js +1 -0
- package/dist/storage/taskStore.js +1 -0
- package/dist/task/nextAction.js +411 -16
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -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
|
-
|
|
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,
|
|
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
|
: [
|
package/dist/config/yuiConfig.js
CHANGED
|
@@ -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
|
|
47
|
-
* deliveries; `enforce` hard-blocks
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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"),
|
package/dist/task/nextAction.js
CHANGED
|
@@ -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
|
-
|
|
119
|
-
|
|
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 ${
|
|
123
|
-
refs
|
|
233
|
+
reason: `Work Item ${item.id} is ${item.status}; dispatch or continue its implementation.`,
|
|
234
|
+
refs,
|
|
124
235
|
preconditions: [
|
|
125
|
-
{ fact: `Work Item is ${
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 <
|
|
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
|
-
{
|
|
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
|
@@ -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
|