@zq-silk/yui 0.8.3 → 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 +46 -16
- package/dist/cli/commandCatalog.js +23 -11
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +154 -16
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +386 -138
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/sessionBootstrapManifest.js +82 -1
- 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/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/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 +1 -0
- package/dist/storage/sqliteStore.js +8 -1
- package/dist/storage/taskStore.js +10 -1
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +145 -52
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
|
@@ -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 })))
|
|
@@ -153,12 +159,15 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
153
159
|
fix: `wait for or recover the Provider turn on run ${continuation.runId}`
|
|
154
160
|
});
|
|
155
161
|
}
|
|
156
|
-
//
|
|
157
|
-
//
|
|
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.
|
|
158
165
|
for (const workspace of facts.managedWorkspaces) {
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
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);
|
|
162
171
|
}
|
|
163
172
|
// Active non-Leader Runs must finish. The Leader's own Run is terminalized
|
|
164
173
|
// by the completion transaction itself, so it is not a readiness blocker.
|
|
@@ -209,7 +218,21 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
209
218
|
return kindOrder;
|
|
210
219
|
return left.ref.id.localeCompare(right.ref.id, undefined, { numeric: true });
|
|
211
220
|
});
|
|
212
|
-
|
|
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
|
+
};
|
|
213
236
|
}
|
|
214
237
|
/**
|
|
215
238
|
* A terminal Run releases only its completion blocker, never its immutable
|
|
@@ -252,61 +275,67 @@ function providerContinuationBlocksCompletion(continuation, facts) {
|
|
|
252
275
|
&& activation.status === "active"));
|
|
253
276
|
return conversationIsCurrent && activationIsLive;
|
|
254
277
|
}
|
|
255
|
-
function
|
|
278
|
+
function workspaceCompletionDisposition(facts, taskId, workspace) {
|
|
256
279
|
const owner = workspace.owner;
|
|
257
280
|
switch (owner.type) {
|
|
258
281
|
case "task":
|
|
259
282
|
// The Task main workspace is cleaned at archive, not completion.
|
|
260
283
|
return null;
|
|
261
284
|
case "work-item": {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// the incomplete-work-item blocker fires too, and once it is terminal
|
|
265
|
-
// the workspace is the exact cleanup target.
|
|
266
|
-
return {
|
|
285
|
+
const item = facts.workItems.find((entry) => entry.id === owner.workItemId);
|
|
286
|
+
const value = {
|
|
267
287
|
code: "work-item-workspace-undisposed",
|
|
268
288
|
ref: ref("work-item", owner.workItemId),
|
|
269
289
|
reason: `Work Item ${owner.workItemId} has an isolated workspace that is not disposed.`,
|
|
270
290
|
fix: `yui task work cleanup ${taskId}/${owner.workItemId} --integrated|--abandon`
|
|
271
291
|
};
|
|
292
|
+
return item !== undefined && (item.status === "completed" || item.status === "retired")
|
|
293
|
+
? { kind: "advisory", value }
|
|
294
|
+
: { kind: "blocker", value };
|
|
272
295
|
}
|
|
273
296
|
case "review-round": {
|
|
274
297
|
const round = facts.reviewRounds.find((entry) => entry.id === owner.reviewRoundId);
|
|
275
|
-
// A workspace for a still-active Review is covered by the
|
|
276
|
-
// active-task-review / wait-for-owned-execution path; only project the
|
|
277
|
-
// cleanup blocker once the Round is terminal.
|
|
278
298
|
if (round !== undefined && !TERMINAL_REVIEW_STATUSES.has(round.status))
|
|
279
299
|
return null;
|
|
280
|
-
|
|
300
|
+
const value = {
|
|
281
301
|
code: "review-workspace-undisposed",
|
|
282
302
|
ref: ref("review-round", owner.reviewRoundId),
|
|
283
303
|
reason: `ReviewRound ${owner.reviewRoundId} is terminal but its workspace is not cleaned up.`,
|
|
284
304
|
fix: `yui task work review cleanup ${taskId}/${owner.reviewRoundId}`
|
|
285
305
|
};
|
|
306
|
+
return round === undefined
|
|
307
|
+
? { kind: "blocker", value }
|
|
308
|
+
: { kind: "advisory", value };
|
|
286
309
|
}
|
|
287
310
|
case "integration-attempt": {
|
|
288
311
|
const integration = facts.integrations.find((entry) => entry.id === owner.integrationAttemptId);
|
|
289
312
|
if (integration !== undefined && UNRESOLVED_INTEGRATION_STATUSES.has(integration.status)) {
|
|
290
313
|
return null;
|
|
291
314
|
}
|
|
292
|
-
|
|
315
|
+
const value = {
|
|
293
316
|
code: "integration-workspace-undisposed",
|
|
294
317
|
ref: ref("integration-attempt", owner.integrationAttemptId),
|
|
295
318
|
reason: `Integration Attempt ${owner.integrationAttemptId} is terminal but its workspace is not cleaned up.`,
|
|
296
319
|
fix: `retry or continue Integration ${owner.integrationAttemptId} so its workspace is reclaimed`
|
|
297
320
|
};
|
|
321
|
+
return integration === undefined
|
|
322
|
+
? { kind: "blocker", value }
|
|
323
|
+
: { kind: "advisory", value };
|
|
298
324
|
}
|
|
299
325
|
case "execution-lane": {
|
|
300
326
|
const lane = findExecutionLane(facts, owner.executionGroupId, owner.executionLaneId);
|
|
301
327
|
if (lane !== undefined && !TERMINAL_LANE_STATUSES.has(lane.status))
|
|
302
328
|
return null;
|
|
303
|
-
|
|
329
|
+
const value = {
|
|
304
330
|
code: "execution-lane-workspace-undisposed",
|
|
305
331
|
ref: ref("execution-lane", `${owner.executionGroupId}/${owner.executionLaneId}`),
|
|
306
332
|
reason: `Execution Lane ${owner.executionGroupId}/${owner.executionLaneId} `
|
|
307
333
|
+ "is terminal but its workspace is not cleaned up.",
|
|
308
334
|
fix: `clean up Execution Lane ${owner.executionGroupId}/${owner.executionLaneId}`
|
|
309
335
|
};
|
|
336
|
+
return lane === undefined
|
|
337
|
+
? { kind: "blocker", value }
|
|
338
|
+
: { kind: "advisory", value };
|
|
310
339
|
}
|
|
311
340
|
}
|
|
312
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,5 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
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";
|
|
3
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) {
|
|
@@ -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,15 +673,16 @@ function selectOpenWorkItem(workItems) {
|
|
|
601
673
|
return { kind: "none" };
|
|
602
674
|
}
|
|
603
675
|
function taskFinalReviewContract(facts) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
return
|
|
676
|
+
return taskFinalReviewContractResolution(facts)?.effective;
|
|
677
|
+
}
|
|
678
|
+
function taskFinalReviewContractResolution(facts) {
|
|
679
|
+
return resolveRecordedTaskFinalReviewContract(facts.task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents);
|
|
608
680
|
}
|
|
609
681
|
function taskFinalReviewRequired(facts) {
|
|
610
682
|
return taskFinalReviewContract(facts) !== undefined
|
|
611
683
|
|| latestTaskFinalReview(facts.reviewRounds) !== undefined
|
|
612
|
-
|| facts.
|
|
684
|
+
|| (taskDeliveryPath(facts.task) === "integrated"
|
|
685
|
+
&& facts.reviewConfig?.trigger === "final");
|
|
613
686
|
}
|
|
614
687
|
function taskFinalReviewRole(facts) {
|
|
615
688
|
return taskFinalReviewContract(facts)?.reviewerRoleName
|
|
@@ -657,7 +730,11 @@ function needsChangeSetCapture(facts, item) {
|
|
|
657
730
|
}
|
|
658
731
|
function hasValidFinalReview(facts) {
|
|
659
732
|
const final = latestTaskFinalReview(facts.reviewRounds);
|
|
660
|
-
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))
|
|
661
738
|
return false;
|
|
662
739
|
const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
|
|
663
740
|
if (reviewedCommits.size === 0)
|
|
@@ -675,25 +752,41 @@ function hasValidFinalReview(facts) {
|
|
|
675
752
|
}
|
|
676
753
|
return true;
|
|
677
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
|
+
}
|
|
678
766
|
function detectProtocolInconsistency(facts) {
|
|
679
767
|
const changeSetIds = new Set(facts.changeSets.map((changeSet) => changeSet.id));
|
|
680
768
|
const workItemById = new Map(facts.workItems.map((item) => [item.id, item]));
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
+
};
|
|
697
790
|
}
|
|
698
791
|
for (const round of facts.reviewRounds) {
|
|
699
792
|
const reviewConflict = reviewRoundConflict(round, facts.activeRuns);
|
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,
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -106,7 +106,8 @@ yui project add app /absolute/workspace/app \
|
|
|
106
106
|
yui project update app --alias app-cli --development develop
|
|
107
107
|
yui project list
|
|
108
108
|
|
|
109
|
-
yui task create "
|
|
109
|
+
yui task create "修复 CSV 转义" --project app --delivery direct
|
|
110
|
+
yui task create "交付 CSV 导出" --project app --delivery integrated
|
|
110
111
|
yui task update <task-id> --priority high --tags release,csv --due-at 2026-08-01T00:00:00Z
|
|
111
112
|
yui task update <task-id> --clear-priority --clear-tags --clear-due-at
|
|
112
113
|
yui task show <task-id>
|
|
@@ -114,6 +115,16 @@ yui task context <task-id>
|
|
|
114
115
|
yui task activate <task-id>
|
|
115
116
|
```
|
|
116
117
|
|
|
118
|
+
Project 交付路径显式分为两类,但仍复用现有 Task schema。`direct` 用于
|
|
119
|
+
Project Policy 允许的单一低风险修复:Leader 直接在干净且已提交的 Task main
|
|
120
|
+
实现、验证并完成,不要求 WorkItem、IntegrationAttempt 或由全局策略自动创建的
|
|
121
|
+
final ReviewRound。`integrated` 用于受保护、跨 Project、迁移、授权、并发/恢复、
|
|
122
|
+
破坏性或发布改动,并要求 WorkItem、ChangeSet 与 committed Integration 证据。
|
|
123
|
+
全局 final-review 策略只自动约束 integrated Task。direct Task 可使用有边界的原生
|
|
124
|
+
review;若需要独立托管的 final Review,必须在 Task main 前进前提升为 integrated。
|
|
125
|
+
一旦已有提交或交付证据,提升会 fail closed,避免早期修改失去 ChangeSet provenance。
|
|
126
|
+
旧的 `--require-integration` 等价于 `--delivery integrated`,且 integrated 不可降级。
|
|
127
|
+
|
|
117
128
|
面向用户的时间默认按北京时间(`Asia/Shanghai`)显示;持久化记录和
|
|
118
129
|
`--json` 数据仍使用 UTC/RFC 3339。可通过以下命令查看或修改 IANA 时区:
|
|
119
130
|
|
|
@@ -150,8 +161,9 @@ AgentRun 不创建新 WorkItem,也不会递归触发审查。审查以自然
|
|
|
150
161
|
唤醒 Leader;Leader 决定验收、reject 后在原 Role 与原 Session 中修复、
|
|
151
162
|
再次审查,或通过 InputRequest 询问用户。审查失败会保留为可见证据并
|
|
152
163
|
唤醒 Leader,但不会取代 Leader 的最终判断。
|
|
153
|
-
`final` 不为每个 WorkItem 创建完整 ReviewRound
|
|
154
|
-
Project 都有 committed Integration 后排队一次
|
|
164
|
+
`final` 不为每个 WorkItem 创建完整 ReviewRound;integrated Task 的
|
|
165
|
+
`task complete` 会在每个绑定 Project 都有 committed Integration 后排队一次
|
|
166
|
+
Task 级 Review。direct Task 不会因可变的全局 final 配置自动增加 Review。冻结的集成头
|
|
155
167
|
发生变化时才会重新排队,旧报告仍保留为证据。Reviewer 按 Project Policy/Knowledge
|
|
156
168
|
检查整个 Task,并只报告有直接证据的可达、重要、可行动问题或有限验证缺口。
|
|
157
169
|
所有候选、ReviewRound 和 Leader 决策都集中在原 WorkItem 下;reject
|
|
@@ -230,6 +242,7 @@ WorkItem workspace 之间移动时,Yui 会退役已停止的旧 Session;下
|
|
|
230
242
|
```sh
|
|
231
243
|
yui operator submit "比较 CSV 与 JSON 的兼容性" --task <task-id>
|
|
232
244
|
yui operator submit "研究更小的缓存设计"
|
|
245
|
+
yui operator status
|
|
233
246
|
yui operator list
|
|
234
247
|
yui operator resume
|
|
235
248
|
yui operator resume --last
|
|
@@ -254,12 +267,13 @@ Operator 会结合 Project Catalog 和现有 Task context 路由请求。同一
|
|
|
254
267
|
目标的追加需求、修复、审查和咨询继续进入原 Task,即使它涉及多个 Project。
|
|
255
268
|
目标、所有权边界或生命周期独立时才创建新 Task。需求、Bug 和咨询共用同一
|
|
256
269
|
Task/WorkItem 模型,不增加额外任务类型。
|
|
257
|
-
`operator
|
|
270
|
+
`operator status` 将 GlobalRole 选中的唯一 active writer 与保留的历史对话
|
|
271
|
+
分开展示。`operator list` 按固定的最近更新时间倒序展示历史对话,并显示 Agent
|
|
258
272
|
及可读的标题或摘要;底层 provider session ID 始终保持内部实现细节。
|
|
259
273
|
若 adapter 尚未提供这些元数据,Yui 会显示 provider 和稳定的 Yui
|
|
260
|
-
短引用,确保无标题会话仍可区分。`operator resume`
|
|
261
|
-
`--last`
|
|
262
|
-
`operator new
|
|
274
|
+
短引用,确保无标题会话仍可区分。`operator resume` 使用轻量历史编号列表,
|
|
275
|
+
`--last` 可直接恢复最近一条;新建会话不会伪装成 resume 选项,必须显式使用
|
|
276
|
+
`operator new`,并把原对话保留在历史中。
|
|
263
277
|
|
|
264
278
|
从已配置的全局 Worker 创建 Task Role,应用 Profile 并派发 WorkItem:
|
|
265
279
|
|
|
@@ -430,7 +444,13 @@ yui task complete <task-id> --summary "CSV 导出已交付并验证"
|
|
|
430
444
|
yui task reopen <task-id>
|
|
431
445
|
```
|
|
432
446
|
|
|
433
|
-
completed Task 在显式 reopen 前会拒绝消息、派发、进入 session、重试和迟到的
|
|
447
|
+
completed Task 在显式 reopen 前会拒绝消息、派发、进入 session、重试和迟到的
|
|
448
|
+
yield。终态 WorkItem、Review、Integration 与 Lane worktree 会作为非阻塞的
|
|
449
|
+
completion advisory 返回,但必须在 archive 前处理。每个隔离 WorkItem worktree
|
|
450
|
+
仍需显式标记 integrated 或 abandoned,清理时也会删除其受管分支;archive 还必须
|
|
451
|
+
通过 `--integrated` 或 `--abandon` 明确 Task main 的处理结果,之后才会停止
|
|
452
|
+
session 并清理干净的 Task main。Task 与 WorkItem 记录都会保留,Task main 分支
|
|
453
|
+
作为恢复信息保留,不会被静默删除。
|
|
434
454
|
Task 生命周期的交互选择只展示有效来源状态:activate 只展示 Draft,complete 只展示 active,reopen 只展示 completed。
|
|
435
455
|
|
|
436
456
|
## Session 与 tmux
|