@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.
Files changed (60) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +46 -16
  3. package/dist/cli/commandCatalog.js +23 -11
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli.js +154 -16
  6. package/dist/commands/executionAuditCommands.js +30 -0
  7. package/dist/commands/operatorCommands.js +42 -1
  8. package/dist/commands/taskCommands.js +386 -138
  9. package/dist/commands/taskCompletionGate.js +36 -24
  10. package/dist/commands/taskContextCommand.js +6 -1
  11. package/dist/commands/taskInputCommands.js +48 -10
  12. package/dist/commands/taskNextActionCommand.js +36 -3
  13. package/dist/context/sessionBootstrapManifest.js +82 -1
  14. package/dist/controller/clientRuntime.js +7 -7
  15. package/dist/controller/controller.js +16 -8
  16. package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
  17. package/dist/controller/handoverCandidate.js +10 -3
  18. package/dist/controller/sessionNotify.js +4 -22
  19. package/dist/executor/agentAdapter.js +2 -2
  20. package/dist/executor/agentExecutor.js +25 -5
  21. package/dist/executor/fileRoleLaunchPlanner.js +16 -11
  22. package/dist/integration/gitIntegrationService.js +50 -2
  23. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  24. package/dist/observability/executionAudit.js +47 -1
  25. package/dist/observability/faultClassification.js +6 -4
  26. package/dist/observability/orchestrationMetrics.js +196 -0
  27. package/dist/operator/operatorSessionHistory.js +36 -0
  28. package/dist/release/releaseHandover.js +7 -5
  29. package/dist/release/runtimeRelease.js +15 -0
  30. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  31. package/dist/review/deltaRecheck.js +3 -2
  32. package/dist/review/reviewFindingLedger.js +5 -4
  33. package/dist/review/reviewOutcomeClassifier.js +252 -54
  34. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  35. package/dist/review/taskFinalReviewContractRebind.js +350 -0
  36. package/dist/run/runIdentity.js +10 -70
  37. package/dist/runtime/agentHost.js +3 -4
  38. package/dist/runtime/codexAppServerRuntime.js +6 -0
  39. package/dist/runtime/firstProgressStopLoss.js +52 -0
  40. package/dist/runtime/launchBroker.js +10 -2
  41. package/dist/runtime/runtimeDeadlines.js +14 -0
  42. package/dist/runtime/sessionTitle.js +24 -12
  43. package/dist/runtime/structuredProviderHost.js +7 -1
  44. package/dist/runtime/tmuxAdapters.js +10 -3
  45. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  46. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  47. package/dist/scheduler/wakeReason.js +1 -0
  48. package/dist/storage/sqliteStore.js +8 -1
  49. package/dist/storage/taskStore.js +10 -1
  50. package/dist/task/completionReadiness.js +48 -19
  51. package/dist/task/deliveryGuard.js +3 -1
  52. package/dist/task/nextAction.js +145 -52
  53. package/dist/task/repairWave.js +14 -1
  54. package/dist/task/task.js +10 -0
  55. package/dist/web/webSnapshot.js +7 -1
  56. package/i18n/README.zh-CN.md +28 -8
  57. package/package.json +1 -1
  58. package/skills/yui-leader/SKILL.md +73 -31
  59. package/skills/yui-operator/SKILL.md +51 -10
  60. 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.status === "completed"))
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
- // Undisposed managed workspaces. The Task-owned main workspace is excluded
157
- // (it is cleaned at archive, not at completion).
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 blocker = workspaceBlocker(facts, task.id, workspace);
160
- if (blocker !== null)
161
- blockers.push(blocker);
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
- return { taskId: task.id, ready: sorted.length === 0, blockers: sorted };
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 workspaceBlocker(facts, taskId, workspace) {
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
- // The preflight has always blocked on WorkItem workspaces. Project the
263
- // blocker regardless of the Work Item status: if the item is still open
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
- return {
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
- return {
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
- return {
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.status === "completed") {
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`,
@@ -1,5 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { sameTaskFinalReviewContract, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
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
- return buildAction(facts, {
256
- kind: "implement-current-work-item",
257
- reason: `Task ${task.id} has no Work Item; create the first unit of work.`,
258
- refs: [],
259
- preconditions: [
260
- { fact: "At least one Work Item exists", satisfied: false },
261
- { fact: "Task is active", satisfied: task.status === "active" }
262
- ],
263
- recommendedCommand: `yui task work create ${task.id} \"<objective>\"`
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
- if (failedFinal !== undefined && failedFinal.status === "failed") {
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} failed; route its open findings into a repair wave on one frozen head.`,
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: "All Work Items are delivered but no valid Task-final Review attests the integrated head.",
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: "All Work Items are terminal", satisfied: true },
381
- { fact: "Every ChangeSet is committed", satisfied: true },
382
- { fact: "Valid Task-final Review at the integrated head", satisfied: false }
383
- ],
384
- recommendedCommand: `yui task review request ${task.id} --role ${reviewerRole ?? "<reviewer-role>"}`
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.projectBindings.length > 0
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: "The delivery chain is complete; converge the Task instead of creating successor work.",
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
- { fact: "Every ChangeSet is committed", satisfied: true },
408
- {
409
- fact: "Valid Task-final Review at the integrated head",
410
- satisfied: hasValidFinalReview(facts)
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
- const contract = facts.workItems
605
- .map((item) => governingWorkItemCandidate(item)?.taskFinalReviewContract)
606
- .find((contract) => contract !== undefined);
607
- 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);
608
680
  }
609
681
  function taskFinalReviewRequired(facts) {
610
682
  return taskFinalReviewContract(facts) !== undefined
611
683
  || latestTaskFinalReview(facts.reviewRounds) !== undefined
612
- || facts.reviewConfig?.trigger === "final";
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 || final.status !== "completed")
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
- const contractedCandidates = facts.workItems
682
- .flatMap((item) => {
683
- const candidate = governingWorkItemCandidate(item);
684
- return candidate === undefined ? [] : [{ item, candidate }];
685
- })
686
- .filter(({ candidate }) => candidate.taskFinalReviewContract !== undefined);
687
- if (contractedCandidates.length > 1) {
688
- const first = validateTaskFinalReviewContract(contractedCandidates[0].candidate.taskFinalReviewContract);
689
- const conflict = contractedCandidates.find(({ candidate }) => (!sameTaskFinalReviewContract(first, candidate.taskFinalReviewContract)));
690
- if (conflict !== undefined) {
691
- const refs = contractedCandidates.map(({ item, candidate }) => ref("candidate", `${item.id}/${candidate.id}`));
692
- return {
693
- reason: "Task-final Review contracts conflict across Candidates; the completion gate is ambiguous.",
694
- conflicts: refs
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);
@@ -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
  }
@@ -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: projectNames.length === 0 ? task : { ...task, projectNames },
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,
@@ -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 "交付 CSV 导出" --project app
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;`task complete` 会在每个绑定
154
- Project 都有 committed Integration 后排队一次 Task 级 Review。冻结的集成头
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 list` 按固定的最近更新时间倒序展示历史对话,并显示 Agent
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、重试和迟到的 yield。每个隔离 WorkItem worktree 必须先显式清理,清理时也会删除其受管分支;archive 还必须通过 `--integrated` 或 `--abandon` 明确 Task main 的处理结果,之后才会停止 session 并清理干净的 Task main。Task 与 WorkItem 记录都会保留,Task main 分支作为恢复信息保留,不会被静默删除。
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.8.3",
3
+ "version": "0.8.6",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,