@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.
- package/ARCHITECTURE.md +28 -4
- package/README.md +62 -24
- package/dist/agent/argumentPolicy.js +1 -1
- package/dist/agent/managedRuntimeEnvironment.js +1 -0
- package/dist/cli/commandCatalog.js +19 -9
- package/dist/cli/interactionPolicy.js +4 -2
- package/dist/cli.js +77 -32
- package/dist/commands/deliveryGuardPreflight.js +0 -5
- package/dist/commands/taskCommands.js +54 -12
- package/dist/commands/taskContextCommand.js +10 -1
- package/dist/commands/taskNextActionCommand.js +16 -1
- package/dist/commands/taskRoleRuntimeStatus.js +170 -10
- package/dist/config/yuiConfig.js +6 -4
- package/dist/controller/agentRuntimeObserver.js +210 -0
- package/dist/controller/clientRuntime.js +3 -21
- package/dist/controller/controller.js +47 -7
- package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
- package/dist/controller/runtime.js +9 -3
- package/dist/controller/runtimeEventInbox.js +49 -295
- package/dist/controller/runtimeEventProcessor.js +184 -321
- package/dist/controller/runtimeHookRunFence.js +226 -0
- package/dist/controller/runtimeLaunchCoordinator.js +91 -26
- package/dist/controller/runtimeObservationHook.js +112 -0
- package/dist/core/controllerServer.js +5 -0
- package/dist/executor/agentAdapter.js +18 -3
- package/dist/executor/fileRoleLaunchPlanner.js +64 -15
- package/dist/executor/managedClaudeRunner.js +121 -0
- package/dist/observability/executionAudit.js +6 -3
- package/dist/repository/taskWorkspacePreparer.js +1 -4
- package/dist/run/providerRetryConfig.js +8 -3
- package/dist/runtime/agentDriver.js +229 -0
- package/dist/runtime/agentDriverObservation.js +57 -0
- package/dist/runtime/builtinAgentDrivers.js +235 -0
- package/dist/runtime/builtinTranscriptObserver.js +290 -0
- package/dist/runtime/builtinTranscriptUsage.js +97 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +12 -1
- package/dist/runtime/runtimeObservation.js +297 -0
- package/dist/runtime/runtimeProjection.js +277 -0
- package/dist/runtime/sessionTerminationGuard.js +78 -22
- package/dist/runtime/tmuxAdapters.js +35 -0
- package/dist/scheduler/activeRoleRunDelivery.js +28 -13
- package/dist/scheduler/leaderWakeupProcessor.js +21 -2
- package/dist/scheduler/roleRunLiveness.js +2 -2
- package/dist/scheduler/roleRunStall.js +62 -114
- package/dist/storage/migration/productionRegistry.js +41 -0
- package/dist/storage/sqliteStore.js +4 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +1 -0
- package/dist/task/nextAction.js +411 -16
- package/dist/telemetry/sqliteTelemetryStore.js +0 -28
- package/dist/telemetry/telemetryCompaction.js +1 -0
- package/dist/telemetry/telemetryConfig.js +4 -5
- package/dist/tmux/tmuxManager.js +136 -22
- package/dist/web/assets/client/view.js +1 -1
- package/dist/web/tmuxWebTerminal.js +17 -12
- package/dist/web/webSnapshot.js +1 -1
- package/dist/worktree/managedWorkspace.js +14 -0
- package/i18n/README.zh-CN.md +7 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
- package/dist/controller/claudeLifecycleHook.js +0 -203
- package/dist/controller/codexLifecycleHook.js +0 -108
- package/dist/controller/providerHookRunFence.js +0 -156
- package/dist/lifecycle/providerLifecycleMapping.js +0 -190
- package/dist/telemetry/telemetryRouter.js +0 -32
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;
|
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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();
|