@zq-silk/yui 0.8.2 → 0.8.6
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 +40 -22
- package/README.md +61 -9
- package/dist/cli/commandCatalog.js +31 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +187 -22
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +648 -74
- package/dist/commands/taskCompletionGate.js +166 -2
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/runContextPack.js +19 -4
- package/dist/context/sessionBootstrapManifest.js +83 -2
- package/dist/controller/clientRuntime.js +7 -7
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +25 -5
- package/dist/executor/fileRoleLaunchPlanner.js +16 -11
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/gitWorkspace.js +7 -4
- package/dist/repository/taskBaseFreshness.js +4 -2
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +252 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +350 -0
- package/dist/run/agentRun.js +2 -2
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/firstProgressStopLoss.js +52 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/storage/sqliteStore.js +10 -1
- package/dist/storage/taskStore.js +12 -1
- package/dist/task/completionReadiness.js +91 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +146 -51
- package/dist/task/publicationReference.js +1 -0
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +12 -0
- package/dist/workspace/workItemChangeSetManager.js +2 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +79 -32
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +7 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isReviewFindingBlocking } from "../review/reviewFinding.js";
|
|
2
2
|
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
|
+
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
3
4
|
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
4
5
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
5
6
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
@@ -16,6 +17,7 @@ const TERMINAL_REVIEW_STATUSES = new Set(["completed", "failed"]);
|
|
|
16
17
|
const TERMINAL_LANE_STATUSES = new Set(["completed", "failed", "yielded"]);
|
|
17
18
|
export function projectCompletionReadiness(facts, options = {}) {
|
|
18
19
|
const blockers = [];
|
|
20
|
+
const advisories = [];
|
|
19
21
|
const { task } = facts;
|
|
20
22
|
const findingsGate = options.findingsGate ?? true;
|
|
21
23
|
// A pending/running Task-final Review must be resumed or blocked first.
|
|
@@ -38,7 +40,11 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
38
40
|
// escalations remain audit evidence, but a later completed full Review (or
|
|
39
41
|
// accepted delta) supersedes them instead of blocking completion forever.
|
|
40
42
|
const latestCompletedTaskReview = facts.reviewRounds
|
|
41
|
-
.filter((round) => ((round.scope ?? "work-item") === "task" && round
|
|
43
|
+
.filter((round) => ((round.scope ?? "work-item") === "task" && isSemanticReviewRound(round, {
|
|
44
|
+
listAgentRuns: () => facts.agentRuns,
|
|
45
|
+
listReviewFindings: () => facts.reviewFindings,
|
|
46
|
+
listEvents: () => facts.events
|
|
47
|
+
})))
|
|
42
48
|
.slice()
|
|
43
49
|
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
44
50
|
|| left.id.localeCompare(right.id, undefined, { numeric: true })))
|
|
@@ -142,6 +148,8 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
142
148
|
}
|
|
143
149
|
// Provider continuations that may still write the Workspace.
|
|
144
150
|
for (const continuation of blockingProviderContinuations(facts.events)) {
|
|
151
|
+
if (!providerContinuationBlocksCompletion(continuation, facts))
|
|
152
|
+
continue;
|
|
145
153
|
const identity = continuation.identity;
|
|
146
154
|
blockers.push({
|
|
147
155
|
code: "blocking-provider-continuation",
|
|
@@ -151,12 +159,15 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
151
159
|
fix: `wait for or recover the Provider turn on run ${continuation.runId}`
|
|
152
160
|
});
|
|
153
161
|
}
|
|
154
|
-
//
|
|
155
|
-
//
|
|
162
|
+
// Terminal child workspaces are cleanup advisories: Task completion is the
|
|
163
|
+
// semantic delivery boundary, while archive remains the fail-closed resource
|
|
164
|
+
// reclamation boundary. Missing/non-terminal ownership stays conservative.
|
|
156
165
|
for (const workspace of facts.managedWorkspaces) {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
159
|
-
blockers.push(
|
|
166
|
+
const disposition = workspaceCompletionDisposition(facts, task.id, workspace);
|
|
167
|
+
if (disposition?.kind === "blocker")
|
|
168
|
+
blockers.push(disposition.value);
|
|
169
|
+
if (disposition?.kind === "advisory")
|
|
170
|
+
advisories.push(disposition.value);
|
|
160
171
|
}
|
|
161
172
|
// Active non-Leader Runs must finish. The Leader's own Run is terminalized
|
|
162
173
|
// by the completion transaction itself, so it is not a readiness blocker.
|
|
@@ -207,63 +218,124 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
207
218
|
return kindOrder;
|
|
208
219
|
return left.ref.id.localeCompare(right.ref.id, undefined, { numeric: true });
|
|
209
220
|
});
|
|
210
|
-
|
|
221
|
+
const sortedAdvisories = [...advisories].sort((left, right) => {
|
|
222
|
+
const codeOrder = left.code.localeCompare(right.code);
|
|
223
|
+
if (codeOrder !== 0)
|
|
224
|
+
return codeOrder;
|
|
225
|
+
const kindOrder = left.ref.kind.localeCompare(right.ref.kind);
|
|
226
|
+
if (kindOrder !== 0)
|
|
227
|
+
return kindOrder;
|
|
228
|
+
return left.ref.id.localeCompare(right.ref.id, undefined, { numeric: true });
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
taskId: task.id,
|
|
232
|
+
ready: sorted.length === 0,
|
|
233
|
+
blockers: sorted,
|
|
234
|
+
advisories: sortedAdvisories
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* A terminal Run releases only its completion blocker, never its immutable
|
|
239
|
+
* continuation audit. Missing or inconsistent ownership remains ambiguous and
|
|
240
|
+
* therefore fail-closed. A live exact native Session can still deliver or
|
|
241
|
+
* write for a terminal Run, so it retains the blocker until that Session is
|
|
242
|
+
* stopped, broken, or replaced by another Conversation identity.
|
|
243
|
+
*/
|
|
244
|
+
function providerContinuationBlocksCompletion(continuation, facts) {
|
|
245
|
+
if (continuation.identityConflict)
|
|
246
|
+
return true;
|
|
247
|
+
const ownerRun = facts.agentRuns.find(({ id }) => id === continuation.runId);
|
|
248
|
+
if (ownerRun === undefined
|
|
249
|
+
|| ownerRun.taskId !== continuation.taskId
|
|
250
|
+
|| ownerRun.roleName !== continuation.roleName
|
|
251
|
+
|| ownerRun.effective.agentId !== continuation.identity.accountScope) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (ownerRun.status !== "failed" && ownerRun.status !== "yielded")
|
|
255
|
+
return true;
|
|
256
|
+
const sessions = facts.roleSessionSets.find(({ owner }) => (owner.taskId === continuation.taskId && owner.roleName === continuation.roleName));
|
|
257
|
+
const session = sessions?.sessions[continuation.identity.accountScope];
|
|
258
|
+
if (session !== undefined
|
|
259
|
+
&& session.status !== "stopped"
|
|
260
|
+
&& session.status !== "broken"
|
|
261
|
+
&& session.nativeSessionId === continuation.identity.conversationId) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
const binding = sessions?.providerBinding;
|
|
265
|
+
if (binding === undefined || binding === null
|
|
266
|
+
|| binding.providerNamespace !== continuation.identity.providerNamespace
|
|
267
|
+
|| binding.accountScope !== continuation.identity.accountScope) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const conversationIsCurrent = binding.conversations.some((conversation) => (conversation.conversationId === continuation.identity.conversationId
|
|
271
|
+
&& conversation.epoch === binding.currentConversationEpoch
|
|
272
|
+
&& conversation.status === "current"));
|
|
273
|
+
const activationIsLive = binding.activations.some((activation) => (activation.activationId === continuation.identity.activationId
|
|
274
|
+
&& activation.conversationId === continuation.identity.conversationId
|
|
275
|
+
&& activation.status === "active"));
|
|
276
|
+
return conversationIsCurrent && activationIsLive;
|
|
211
277
|
}
|
|
212
|
-
function
|
|
278
|
+
function workspaceCompletionDisposition(facts, taskId, workspace) {
|
|
213
279
|
const owner = workspace.owner;
|
|
214
280
|
switch (owner.type) {
|
|
215
281
|
case "task":
|
|
216
282
|
// The Task main workspace is cleaned at archive, not completion.
|
|
217
283
|
return null;
|
|
218
284
|
case "work-item": {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// the incomplete-work-item blocker fires too, and once it is terminal
|
|
222
|
-
// the workspace is the exact cleanup target.
|
|
223
|
-
return {
|
|
285
|
+
const item = facts.workItems.find((entry) => entry.id === owner.workItemId);
|
|
286
|
+
const value = {
|
|
224
287
|
code: "work-item-workspace-undisposed",
|
|
225
288
|
ref: ref("work-item", owner.workItemId),
|
|
226
289
|
reason: `Work Item ${owner.workItemId} has an isolated workspace that is not disposed.`,
|
|
227
290
|
fix: `yui task work cleanup ${taskId}/${owner.workItemId} --integrated|--abandon`
|
|
228
291
|
};
|
|
292
|
+
return item !== undefined && (item.status === "completed" || item.status === "retired")
|
|
293
|
+
? { kind: "advisory", value }
|
|
294
|
+
: { kind: "blocker", value };
|
|
229
295
|
}
|
|
230
296
|
case "review-round": {
|
|
231
297
|
const round = facts.reviewRounds.find((entry) => entry.id === owner.reviewRoundId);
|
|
232
|
-
// A workspace for a still-active Review is covered by the
|
|
233
|
-
// active-task-review / wait-for-owned-execution path; only project the
|
|
234
|
-
// cleanup blocker once the Round is terminal.
|
|
235
298
|
if (round !== undefined && !TERMINAL_REVIEW_STATUSES.has(round.status))
|
|
236
299
|
return null;
|
|
237
|
-
|
|
300
|
+
const value = {
|
|
238
301
|
code: "review-workspace-undisposed",
|
|
239
302
|
ref: ref("review-round", owner.reviewRoundId),
|
|
240
303
|
reason: `ReviewRound ${owner.reviewRoundId} is terminal but its workspace is not cleaned up.`,
|
|
241
304
|
fix: `yui task work review cleanup ${taskId}/${owner.reviewRoundId}`
|
|
242
305
|
};
|
|
306
|
+
return round === undefined
|
|
307
|
+
? { kind: "blocker", value }
|
|
308
|
+
: { kind: "advisory", value };
|
|
243
309
|
}
|
|
244
310
|
case "integration-attempt": {
|
|
245
311
|
const integration = facts.integrations.find((entry) => entry.id === owner.integrationAttemptId);
|
|
246
312
|
if (integration !== undefined && UNRESOLVED_INTEGRATION_STATUSES.has(integration.status)) {
|
|
247
313
|
return null;
|
|
248
314
|
}
|
|
249
|
-
|
|
315
|
+
const value = {
|
|
250
316
|
code: "integration-workspace-undisposed",
|
|
251
317
|
ref: ref("integration-attempt", owner.integrationAttemptId),
|
|
252
318
|
reason: `Integration Attempt ${owner.integrationAttemptId} is terminal but its workspace is not cleaned up.`,
|
|
253
319
|
fix: `retry or continue Integration ${owner.integrationAttemptId} so its workspace is reclaimed`
|
|
254
320
|
};
|
|
321
|
+
return integration === undefined
|
|
322
|
+
? { kind: "blocker", value }
|
|
323
|
+
: { kind: "advisory", value };
|
|
255
324
|
}
|
|
256
325
|
case "execution-lane": {
|
|
257
326
|
const lane = findExecutionLane(facts, owner.executionGroupId, owner.executionLaneId);
|
|
258
327
|
if (lane !== undefined && !TERMINAL_LANE_STATUSES.has(lane.status))
|
|
259
328
|
return null;
|
|
260
|
-
|
|
329
|
+
const value = {
|
|
261
330
|
code: "execution-lane-workspace-undisposed",
|
|
262
331
|
ref: ref("execution-lane", `${owner.executionGroupId}/${owner.executionLaneId}`),
|
|
263
332
|
reason: `Execution Lane ${owner.executionGroupId}/${owner.executionLaneId} `
|
|
264
333
|
+ "is terminal but its workspace is not cleaned up.",
|
|
265
334
|
fix: `clean up Execution Lane ${owner.executionGroupId}/${owner.executionLaneId}`
|
|
266
335
|
};
|
|
336
|
+
return lane === undefined
|
|
337
|
+
? { kind: "blocker", value }
|
|
338
|
+
: { kind: "advisory", value };
|
|
267
339
|
}
|
|
268
340
|
}
|
|
269
341
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nextActionReviewOutcomeEvidence } from "./nextAction.js";
|
|
2
|
+
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
1
3
|
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
2
4
|
export function detectDeliveryDuplicates(facts, intent) {
|
|
3
5
|
switch (intent.kind) {
|
|
@@ -127,7 +129,7 @@ function detectReviewDuplicates(facts, intent) {
|
|
|
127
129
|
if (!sameCandidate)
|
|
128
130
|
continue;
|
|
129
131
|
const ref = { kind: "review-round", id: round.id };
|
|
130
|
-
if (round
|
|
132
|
+
if (isSemanticReviewRound(round, nextActionReviewOutcomeEvidence(facts))) {
|
|
131
133
|
duplicates.push({
|
|
132
134
|
severity: "exact",
|
|
133
135
|
reason: `Task-final Review ${round.id} already attests this exact head`,
|
package/dist/task/nextAction.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
|
+
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
4
|
+
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
5
|
+
import { taskDeliveryPath } from "./task.js";
|
|
6
|
+
import { currentWorkItemCandidate, governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
4
7
|
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
5
8
|
export function projectNextAction(facts) {
|
|
6
9
|
const { task } = facts;
|
|
@@ -252,16 +255,41 @@ export function projectNextAction(facts) {
|
|
|
252
255
|
});
|
|
253
256
|
}
|
|
254
257
|
if (facts.workItems.length === 0) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
if (taskDeliveryPath(task) === "direct" && !taskFinalReviewRequired(facts)) {
|
|
259
|
+
return buildAction(facts, {
|
|
260
|
+
kind: "complete-task",
|
|
261
|
+
reason: `Task ${task.id} uses direct delivery; implement and verify the managed Task main, then complete without creating a WorkItem.`,
|
|
262
|
+
refs: [ref("task", task.id)],
|
|
263
|
+
preconditions: [
|
|
264
|
+
{ fact: "Task is active", satisfied: task.status === "active", ref: ref("task", task.id) },
|
|
265
|
+
{ fact: "Task main is clean, committed, and verified", satisfied: false }
|
|
266
|
+
],
|
|
267
|
+
recommendedCommand: `yui task complete ${task.id} --summary-file -`,
|
|
268
|
+
...(facts.reviewConfig === null
|
|
269
|
+
? {}
|
|
270
|
+
: {
|
|
271
|
+
alternatives: [{
|
|
272
|
+
kind: "promote-to-integrated-delivery",
|
|
273
|
+
reason: "Before Task main advances, promote to integrated delivery when risk warrants an independently managed ReviewRound.",
|
|
274
|
+
recommendedCommand: `yui task update ${task.id} --delivery integrated`,
|
|
275
|
+
refs: [ref("task", task.id)]
|
|
276
|
+
}],
|
|
277
|
+
judgmentRequired: "Leader must decide whether the work still fits direct delivery or should be promoted before completing it."
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (taskDeliveryPath(task) === "integrated") {
|
|
282
|
+
return buildAction(facts, {
|
|
283
|
+
kind: "implement-current-work-item",
|
|
284
|
+
reason: `Task ${task.id} has no Work Item; create the first unit of work.`,
|
|
285
|
+
refs: [],
|
|
286
|
+
preconditions: [
|
|
287
|
+
{ fact: "At least one Work Item exists", satisfied: false },
|
|
288
|
+
{ fact: "Task is active", satisfied: task.status === "active" }
|
|
289
|
+
],
|
|
290
|
+
recommendedCommand: `yui task work create ${task.id} \"<objective>\"`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
265
293
|
}
|
|
266
294
|
const uncaptured = facts.workItems.find((item) => needsChangeSetCapture(facts, item));
|
|
267
295
|
if (uncaptured !== undefined) {
|
|
@@ -291,10 +319,38 @@ export function projectNextAction(facts) {
|
|
|
291
319
|
});
|
|
292
320
|
}
|
|
293
321
|
const failedFinal = latestTaskFinalReview(facts.reviewRounds);
|
|
294
|
-
|
|
322
|
+
const failedFinalOutcome = failedFinal === undefined
|
|
323
|
+
? null
|
|
324
|
+
: classifyReviewRoundOutcome(failedFinal, nextActionReviewOutcomeEvidence(facts));
|
|
325
|
+
if (failedFinal !== undefined && failedFinalOutcome?.kind === "non-semantic") {
|
|
326
|
+
return buildAction(facts, {
|
|
327
|
+
kind: "resume-review",
|
|
328
|
+
reason: `Task-final Review ${failedFinal.id} ended before a semantic review was proven.`,
|
|
329
|
+
refs: [ref("review-round", failedFinal.id)],
|
|
330
|
+
preconditions: [
|
|
331
|
+
{ fact: "Task-final Review has semantic evidence", satisfied: false, ref: ref("review-round", failedFinal.id) }
|
|
332
|
+
],
|
|
333
|
+
recommendedCommand: `yui task review force-fresh ${task.id}/${failedFinal.id}`
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (failedFinal !== undefined && failedFinalOutcome?.kind === "ambiguous") {
|
|
337
|
+
return buildAction(facts, {
|
|
338
|
+
kind: "repair-protocol-inconsistency",
|
|
339
|
+
reason: `Task-final Review ${failedFinal.id} has ambiguous semantic and infrastructure evidence: ${failedFinalOutcome.reason}`,
|
|
340
|
+
refs: [ref("review-round", failedFinal.id)],
|
|
341
|
+
conflicts: [ref("review-round", failedFinal.id)],
|
|
342
|
+
preconditions: [
|
|
343
|
+
{ fact: "Review outcome is unambiguously semantic or non-semantic", satisfied: false, ref: ref("review-round", failedFinal.id) }
|
|
344
|
+
]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (failedFinal !== undefined
|
|
348
|
+
&& failedFinalOutcome?.kind === "semantic"
|
|
349
|
+
&& ((failedFinal.checks ?? []).some(({ outcome }) => outcome === "failed")
|
|
350
|
+
|| deltaRecheckBlocksAcceptance(failedFinal))) {
|
|
295
351
|
return buildAction(facts, {
|
|
296
352
|
kind: "route-review-findings",
|
|
297
|
-
reason: `Task-final Review ${failedFinal.id}
|
|
353
|
+
reason: `Task-final Review ${failedFinal.id} delivered semantic negative evidence; route its open findings into a repair wave on one frozen head.`,
|
|
298
354
|
refs: [ref("review-round", failedFinal.id)],
|
|
299
355
|
preconditions: [
|
|
300
356
|
{ fact: "Task-final Review is failed", satisfied: true, ref: ref("review-round", failedFinal.id) }
|
|
@@ -372,19 +428,27 @@ export function projectNextAction(facts) {
|
|
|
372
428
|
&& finalReviewRequired
|
|
373
429
|
&& !hasValidFinalReview(facts)) {
|
|
374
430
|
const reviewerRole = taskFinalReviewRole(facts);
|
|
431
|
+
const directWithoutWorkItems = taskDeliveryPath(task) === "direct"
|
|
432
|
+
&& facts.workItems.length === 0;
|
|
375
433
|
return buildAction(facts, {
|
|
376
434
|
kind: "request-final-review",
|
|
377
|
-
reason:
|
|
435
|
+
reason: directWithoutWorkItems
|
|
436
|
+
? "This direct Task already owns a final-Review obligation; completion must prepare or resume a Review of its frozen Task head."
|
|
437
|
+
: "All Work Items are delivered but no valid Task-final Review attests the integrated head.",
|
|
378
438
|
refs: [ref("task", task.id)],
|
|
379
|
-
preconditions:
|
|
380
|
-
{ fact: "
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
439
|
+
preconditions: directWithoutWorkItems
|
|
440
|
+
? [{ fact: "Valid established Task-final Review at the direct head", satisfied: false }]
|
|
441
|
+
: [
|
|
442
|
+
{ fact: "All Work Items are terminal", satisfied: true },
|
|
443
|
+
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
444
|
+
{ fact: "Valid Task-final Review at the integrated head", satisfied: false }
|
|
445
|
+
],
|
|
446
|
+
recommendedCommand: directWithoutWorkItems
|
|
447
|
+
? `yui task complete ${task.id} --summary-file -`
|
|
448
|
+
: `yui task review request ${task.id} --role ${reviewerRole ?? "<reviewer-role>"}`
|
|
385
449
|
});
|
|
386
450
|
}
|
|
387
|
-
const finalReviewOptional = task
|
|
451
|
+
const finalReviewOptional = taskDeliveryPath(task) === "integrated"
|
|
388
452
|
&& !finalReviewRequired
|
|
389
453
|
&& !hasValidFinalReview(facts);
|
|
390
454
|
const finalReviewAlternative = finalReviewOptional && facts.reviewConfig !== null
|
|
@@ -397,19 +461,26 @@ export function projectNextAction(facts) {
|
|
|
397
461
|
: [];
|
|
398
462
|
return buildAction(facts, {
|
|
399
463
|
kind: "complete-task",
|
|
400
|
-
reason:
|
|
464
|
+
reason: taskDeliveryPath(task) === "direct"
|
|
465
|
+
? "The direct Task head and its established obligations are ready; converge the Task without creating successor work."
|
|
466
|
+
: "The delivery chain is complete; converge the Task instead of creating successor work.",
|
|
401
467
|
refs: [ref("task", task.id)],
|
|
402
468
|
preconditions: [
|
|
403
469
|
{ fact: "All Work Items are terminal", satisfied: true },
|
|
404
470
|
...(task.projectBindings.length === 0
|
|
405
471
|
? []
|
|
406
|
-
:
|
|
407
|
-
{
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
472
|
+
: taskDeliveryPath(task) === "direct"
|
|
473
|
+
? [{
|
|
474
|
+
fact: "Valid established Task-final Review at the direct head",
|
|
475
|
+
satisfied: hasValidFinalReview(facts)
|
|
476
|
+
}]
|
|
477
|
+
: [
|
|
478
|
+
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
479
|
+
{
|
|
480
|
+
fact: "Valid Task-final Review at the integrated head",
|
|
481
|
+
satisfied: hasValidFinalReview(facts)
|
|
482
|
+
}
|
|
483
|
+
])
|
|
413
484
|
],
|
|
414
485
|
...(finalReviewAlternative.length === 0 ? {} : { alternatives: finalReviewAlternative }),
|
|
415
486
|
...(!finalReviewOptional
|
|
@@ -431,7 +502,8 @@ export function durableStateFingerprint(facts) {
|
|
|
431
502
|
...facts.workItems.map((item) => `work:${item.id}:${item.status}:${item.revision}:${item.updatedAt}`),
|
|
432
503
|
...facts.changeSets.map((changeSet) => `change-set:${changeSet.id}:${changeSet.headCommit}`),
|
|
433
504
|
...facts.integrations.map((attempt) => `integration:${attempt.id}:${attempt.status}:${attempt.updatedAt}`),
|
|
434
|
-
...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`)
|
|
505
|
+
...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`),
|
|
506
|
+
...facts.taskFinalReviewContractEvents.map((event) => `task-final-review-event:${event.id}:${event.createdAt}`)
|
|
435
507
|
];
|
|
436
508
|
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
437
509
|
}
|
|
@@ -601,16 +673,16 @@ function selectOpenWorkItem(workItems) {
|
|
|
601
673
|
return { kind: "none" };
|
|
602
674
|
}
|
|
603
675
|
function taskFinalReviewContract(facts) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
return contract === undefined ? undefined : validateTaskFinalReviewContract(contract);
|
|
676
|
+
return taskFinalReviewContractResolution(facts)?.effective;
|
|
677
|
+
}
|
|
678
|
+
function taskFinalReviewContractResolution(facts) {
|
|
679
|
+
return resolveRecordedTaskFinalReviewContract(facts.task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents);
|
|
609
680
|
}
|
|
610
681
|
function taskFinalReviewRequired(facts) {
|
|
611
682
|
return taskFinalReviewContract(facts) !== undefined
|
|
612
683
|
|| latestTaskFinalReview(facts.reviewRounds) !== undefined
|
|
613
|
-
|| facts.
|
|
684
|
+
|| (taskDeliveryPath(facts.task) === "integrated"
|
|
685
|
+
&& facts.reviewConfig?.trigger === "final");
|
|
614
686
|
}
|
|
615
687
|
function taskFinalReviewRole(facts) {
|
|
616
688
|
return taskFinalReviewContract(facts)?.reviewerRoleName
|
|
@@ -658,7 +730,11 @@ function needsChangeSetCapture(facts, item) {
|
|
|
658
730
|
}
|
|
659
731
|
function hasValidFinalReview(facts) {
|
|
660
732
|
const final = latestTaskFinalReview(facts.reviewRounds);
|
|
661
|
-
if (final === undefined
|
|
733
|
+
if (final === undefined
|
|
734
|
+
|| !isSemanticReviewRound(final, nextActionReviewOutcomeEvidence(facts)))
|
|
735
|
+
return false;
|
|
736
|
+
if ((final.checks ?? []).some(({ outcome }) => outcome === "failed")
|
|
737
|
+
|| deltaRecheckBlocksAcceptance(final))
|
|
662
738
|
return false;
|
|
663
739
|
const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
|
|
664
740
|
if (reviewedCommits.size === 0)
|
|
@@ -676,22 +752,41 @@ function hasValidFinalReview(facts) {
|
|
|
676
752
|
}
|
|
677
753
|
return true;
|
|
678
754
|
}
|
|
755
|
+
/** Adapts the serializable next-action evidence bundle to the shared classifier. */
|
|
756
|
+
export function nextActionReviewOutcomeEvidence(facts) {
|
|
757
|
+
const evidence = facts.reviewOutcomeEvidence;
|
|
758
|
+
if (evidence === undefined)
|
|
759
|
+
return undefined;
|
|
760
|
+
return {
|
|
761
|
+
listAgentRuns: () => evidence.agentRuns,
|
|
762
|
+
listReviewFindings: () => evidence.reviewFindings,
|
|
763
|
+
listEvents: () => evidence.events
|
|
764
|
+
};
|
|
765
|
+
}
|
|
679
766
|
function detectProtocolInconsistency(facts) {
|
|
680
767
|
const changeSetIds = new Set(facts.changeSets.map((changeSet) => changeSet.id));
|
|
681
768
|
const workItemById = new Map(facts.workItems.map((item) => [item.id, item]));
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
769
|
+
try {
|
|
770
|
+
taskFinalReviewContractResolution(facts);
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
const candidateRefs = facts.workItems.flatMap((item) => {
|
|
774
|
+
const candidate = governingWorkItemCandidate(item);
|
|
775
|
+
return candidate?.taskFinalReviewContract === undefined
|
|
776
|
+
? []
|
|
777
|
+
: [ref("candidate", `${item.id}/${candidate.id}`)];
|
|
778
|
+
});
|
|
779
|
+
const reviewRefs = facts.reviewRounds
|
|
780
|
+
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
781
|
+
&& round.taskFinalReviewContract !== undefined))
|
|
782
|
+
.map((round) => ref("review-round", round.id));
|
|
783
|
+
const eventRefs = facts.taskFinalReviewContractEvents
|
|
784
|
+
.map((event) => ref("task-event", event.id));
|
|
785
|
+
return {
|
|
786
|
+
reason: "Task-final Review contract history is inconsistent: "
|
|
787
|
+
+ (error instanceof Error ? error.message : String(error)),
|
|
788
|
+
conflicts: [...candidateRefs, ...reviewRefs, ...eventRefs]
|
|
789
|
+
};
|
|
695
790
|
}
|
|
696
791
|
for (const round of facts.reviewRounds) {
|
|
697
792
|
const reviewConflict = reviewRoundConflict(round, facts.activeRuns);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
2
|
import { validateTaskRecordReference } from "./taskRecordReference.js";
|
|
3
3
|
export const PUBLICATION_REFERENCE_SCHEMA_VERSION = 1;
|
|
4
|
+
export const TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT = "task.completion-published-tree-authorized";
|
|
4
5
|
const PROVIDERS = new Set(["github", "gitlab"]);
|
|
5
6
|
const EXTERNAL_KINDS = new Set(["pull-request", "merge-request"]);
|
|
6
7
|
const STATES = new Set(["open", "merged", "closed"]);
|
package/dist/task/repairWave.js
CHANGED
|
@@ -27,10 +27,23 @@ export function extractReviewFindings(round) {
|
|
|
27
27
|
}
|
|
28
28
|
return parseReportFindings(round.report ?? "");
|
|
29
29
|
}
|
|
30
|
-
export function planRepairWave(reviewRoundId, findings) {
|
|
30
|
+
export function planRepairWave(reviewRoundId, findings, strategy = "consolidated") {
|
|
31
31
|
if (findings.length === 0) {
|
|
32
32
|
return { reviewRoundId, openFindingCount: 0, groups: [] };
|
|
33
33
|
}
|
|
34
|
+
if (strategy === "consolidated") {
|
|
35
|
+
const ordered = [...findings].sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })));
|
|
36
|
+
return {
|
|
37
|
+
reviewRoundId,
|
|
38
|
+
openFindingCount: findings.length,
|
|
39
|
+
groups: [{
|
|
40
|
+
id: "repair-1",
|
|
41
|
+
findingIds: ordered.map(({ id }) => id),
|
|
42
|
+
paths: [...new Set(ordered.flatMap(({ paths }) => paths))].sort(),
|
|
43
|
+
reason: "Consolidated by default to minimize repair, integration, and review churn"
|
|
44
|
+
}]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
34
47
|
const parent = new Array(findings.length).fill(0).map((_, index) => index);
|
|
35
48
|
const find = (index) => {
|
|
36
49
|
let current = index;
|
package/dist/task/task.js
CHANGED
|
@@ -319,6 +319,16 @@ export function taskProjectBinding(task, projectId) {
|
|
|
319
319
|
export function taskHasProjects(task) {
|
|
320
320
|
return task.projectBindings.length > 0;
|
|
321
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Delivery is a projection of the existing Task contract, not a second source
|
|
324
|
+
* of truth. Keeping it derived preserves every stored Task schema while giving
|
|
325
|
+
* callers one product-level name for the three supported paths.
|
|
326
|
+
*/
|
|
327
|
+
export function taskDeliveryPath(task) {
|
|
328
|
+
if (task.projectBindings.length === 0)
|
|
329
|
+
return "no-project";
|
|
330
|
+
return task.requireIntegration === true ? "integrated" : "direct";
|
|
331
|
+
}
|
|
322
332
|
export function taskProjectIds(task) {
|
|
323
333
|
return task.projectBindings.map(({ projectId }) => projectId);
|
|
324
334
|
}
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { taskDeliveryPath } from "../task/task.js";
|
|
1
2
|
import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
2
3
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
3
4
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -40,6 +41,7 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
|
|
|
40
41
|
});
|
|
41
42
|
return {
|
|
42
43
|
...task,
|
|
44
|
+
deliveryPath: taskDeliveryPath(task),
|
|
43
45
|
...(names.length === 0 ? {} : { projectNames: names }),
|
|
44
46
|
workItems: countWorkItems(reader.listWorkItems(task.id)),
|
|
45
47
|
roleCount: reader.listRoles(task.id).length,
|
|
@@ -106,7 +108,11 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
|
|
|
106
108
|
};
|
|
107
109
|
});
|
|
108
110
|
return {
|
|
109
|
-
task:
|
|
111
|
+
task: {
|
|
112
|
+
...task,
|
|
113
|
+
deliveryPath: taskDeliveryPath(task),
|
|
114
|
+
...(projectNames.length === 0 ? {} : { projectNames })
|
|
115
|
+
},
|
|
110
116
|
execution: buildTaskExecutionProjection(reader, taskId),
|
|
111
117
|
brief: reader.getTaskBrief(taskId),
|
|
112
118
|
roles,
|
|
@@ -567,6 +567,18 @@ export function currentWorkItemCandidate(workItem) {
|
|
|
567
567
|
? workItem.candidates.at(-1)
|
|
568
568
|
: undefined;
|
|
569
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Resolve the one Candidate that still governs Task delivery semantics.
|
|
572
|
+
* Awaiting Candidates remain under Leader disposition, while completed
|
|
573
|
+
* Candidates freeze accepted delivery evidence. Failed, retired, pending,
|
|
574
|
+
* and running WorkItems retain Candidate history only for audit; older
|
|
575
|
+
* Candidates on the same WorkItem are superseded by its latest Candidate.
|
|
576
|
+
*/
|
|
577
|
+
export function governingWorkItemCandidate(workItem) {
|
|
578
|
+
return workItem.status === "awaiting_acceptance" || workItem.status === "completed"
|
|
579
|
+
? workItem.candidates.at(-1)
|
|
580
|
+
: undefined;
|
|
581
|
+
}
|
|
570
582
|
export function updateWorkItemWriteProjects(workItem, writeProjectIds, now) {
|
|
571
583
|
validateWorkItem(workItem);
|
|
572
584
|
if (isTerminalStatus(workItem.status)) {
|
|
@@ -4,6 +4,7 @@ import { createChangeSetManifest } from "../integration/changeSetManifest.js";
|
|
|
4
4
|
import { deriveManifestTags } from "../integration/manifestTags.js";
|
|
5
5
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
6
6
|
import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
7
|
+
import { governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
7
8
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
8
9
|
import { captureManagedGitChanges } from "./gitChangeSetCapture.js";
|
|
9
10
|
const CAPTURABLE_WORK_ITEM_STATUSES = new Set([
|
|
@@ -427,7 +428,7 @@ function latestExactDirectAnchorId(store, taskId, contract) {
|
|
|
427
428
|
return undefined;
|
|
428
429
|
return store.listWorkItems(taskId)
|
|
429
430
|
.filter((item) => {
|
|
430
|
-
const candidate = item
|
|
431
|
+
const candidate = governingWorkItemCandidate(item);
|
|
431
432
|
return candidate?.source.type === "direct"
|
|
432
433
|
&& sameTaskFinalReviewContract(candidate.taskFinalReviewContract, contract);
|
|
433
434
|
})
|