@zq-silk/yui 0.11.2 → 0.12.0

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 (91) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +80 -13
  3. package/dist/cli/commandCatalog.js +30 -5
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/projectCommands.js +581 -2
  9. package/dist/commands/taskCommands.js +159 -172
  10. package/dist/commands/taskCompletionGate.js +29 -54
  11. package/dist/commands/taskContextCommand.js +55 -11
  12. package/dist/commands/taskInputCommands.js +8 -5
  13. package/dist/commands/taskIntegrationCommands.js +2 -1
  14. package/dist/commands/taskNextActionCommand.js +56 -6
  15. package/dist/commands/taskOverviewCommand.js +17 -31
  16. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  17. package/dist/commands/taskWorkspaceCommands.js +5 -2
  18. package/dist/config/configCatalog.js +2 -2
  19. package/dist/config/yuiConfig.js +3 -7
  20. package/dist/context/wakeNotification.js +20 -5
  21. package/dist/controller/agentRuntimeObserver.js +247 -51
  22. package/dist/controller/clientRuntime.js +1 -1
  23. package/dist/controller/controller.js +8 -12
  24. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  25. package/dist/controller/runtime.js +3 -3
  26. package/dist/controller/runtimeEventInbox.js +17 -7
  27. package/dist/controller/runtimeEventProcessor.js +12 -19
  28. package/dist/controller/runtimeObservationHook.js +45 -0
  29. package/dist/execution/resourceBroker.js +5 -4
  30. package/dist/executor/agentExecutor.js +4 -4
  31. package/dist/executor/effectiveLaunch.js +8 -48
  32. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  33. package/dist/executor/workspacePreflightClassification.js +23 -2
  34. package/dist/interaction/operatorPresentation.js +33 -89
  35. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  36. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  37. package/dist/observability/orchestrationMetrics.js +8 -3
  38. package/dist/profile/agentProfile.js +1 -1
  39. package/dist/release/cliHomeReleaseFence.js +123 -0
  40. package/dist/repository/checkoutSwap.js +61 -0
  41. package/dist/repository/gitWorkspace.js +36 -0
  42. package/dist/repository/project.js +53 -3
  43. package/dist/repository/taskWorkspacePreparer.js +176 -60
  44. package/dist/resources/sqliteResourceRegistry.js +1 -1
  45. package/dist/review/deltaRecheck.js +12 -51
  46. package/dist/review/reviewAcceptance.js +26 -0
  47. package/dist/review/reviewConfig.js +0 -31
  48. package/dist/review/reviewDecision.js +113 -0
  49. package/dist/review/reviewOutcomeClassifier.js +1 -1
  50. package/dist/review/reviewRound.js +1 -1
  51. package/dist/review/reviewerAvailability.js +69 -0
  52. package/dist/runtime/agentDriverObservation.js +24 -10
  53. package/dist/runtime/builtinAgentDrivers.js +4 -3
  54. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  55. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  56. package/dist/runtime/index.js +1 -0
  57. package/dist/runtime/runtimeObservation.js +13 -0
  58. package/dist/runtime/runtimeProjection.js +16 -25
  59. package/dist/runtime/sessionTokenMetrics.js +181 -0
  60. package/dist/runtime/structuredProviderHost.js +7 -1
  61. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  62. package/dist/scheduler/activeTaskProgress.js +7 -6
  63. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  64. package/dist/scheduler/operatorEvent.js +34 -0
  65. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  66. package/dist/scheduler/roleRunStall.js +0 -14
  67. package/dist/scheduler/taskExecutionProjection.js +97 -76
  68. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  69. package/dist/storage/migration/productionRegistry.js +467 -0
  70. package/dist/storage/sqliteSchema.js +18 -8
  71. package/dist/storage/sqliteStore.js +61 -21
  72. package/dist/storage/storeRpc.js +0 -1
  73. package/dist/storage/taskStore.js +47 -31
  74. package/dist/storage/upgrade/recordVersions.js +3 -2
  75. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  76. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  77. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  78. package/dist/task/completionReadiness.js +10 -6
  79. package/dist/task/nextAction.js +82 -24
  80. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  81. package/dist/web/assets/client/components.js +13 -3
  82. package/dist/web/assets/client/i18n.js +6 -2
  83. package/dist/web/webSnapshot.js +4 -0
  84. package/i18n/README.zh-CN.md +2 -2
  85. package/package.json +1 -1
  86. package/skills/yui-leader/SKILL.md +53 -16
  87. package/skills/yui-operator/SKILL.md +9 -0
  88. package/skills/yui-reviewer/SKILL.md +28 -7
  89. package/dist/context/sessionContextBudget.js +0 -71
  90. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  91. package/dist/scheduler/operatorNotification.js +0 -59
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
2
3
  import { changeSetDeliverySettled, governingChangeSets } from "../integration/deliveryObligation.js";
3
- import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
4
- import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
4
+ import { isAcceptedTaskReviewBaselineFromEvidence } from "../review/reviewAcceptance.js";
5
+ import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
5
6
  import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
6
7
  import { candidateConvergenceDisagreement, candidateConvergenceEvidenceSufficient, candidateConvergenceStageResultsValid } from "../execution/candidateConvergence.js";
7
8
  import { executionStageSpendClosed, routeExecutionStage } from "../execution/resourceBroker.js";
9
+ import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
8
10
  import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
9
11
  import { currentWorkItemCandidate, currentWorkItemExecutionGroup, governingWorkItemCandidate } from "../workItem/workItem.js";
10
12
  const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
@@ -232,13 +234,15 @@ export function projectNextAction(facts) {
232
234
  judgmentRequired: `Leader must judge Candidate ${candidateReady.id}/${candidate?.id ?? "unknown"} against the Task objective, acceptance criteria, and delivery risk.`
233
235
  });
234
236
  }
235
- const activeWorkers = facts.activeRuns.filter((run) => run.roleName !== "leader");
236
- if (activeWorkers.length > 0) {
237
+ // AgentRun purpose owns routing. Review Runs remain attached to their exact
238
+ // ReviewRound branches below instead of being mistaken for Worker delivery.
239
+ const activeDelegatedExecutions = facts.activeRuns.filter((run) => (run.purpose === "execution" && run.roleName !== "leader"));
240
+ if (activeDelegatedExecutions.length > 0) {
237
241
  return buildAction(facts, {
238
242
  kind: "wait-for-owned-execution",
239
- reason: `${activeWorkers.length} delegated Run(s) are active; wait for their delivery.`,
240
- refs: activeWorkers.map((run) => ref("agent-run", run.id)),
241
- preconditions: activeWorkers.map((run) => ({ fact: `Delegated Run ${run.id} is active`, satisfied: true, ref: ref("agent-run", run.id) }))
243
+ reason: `${activeDelegatedExecutions.length} delegated execution Run(s) are active; wait for their delivery.`,
244
+ refs: activeDelegatedExecutions.map((run) => ref("agent-run", run.id)),
245
+ preconditions: activeDelegatedExecutions.map((run) => ({ fact: `Execution Run ${run.id} is active`, satisfied: true, ref: ref("agent-run", run.id) }))
242
246
  });
243
247
  }
244
248
  const failedWork = facts.workItems.find((item) => item.status === "failed");
@@ -386,7 +390,8 @@ export function projectNextAction(facts) {
386
390
  });
387
391
  }
388
392
  const finalReviewRequired = taskFinalReviewRequired(facts);
389
- const failedFinal = latestTaskFinalReview(facts.reviewRounds);
393
+ const finalReviewContract = taskFinalReviewContract(facts);
394
+ const failedFinal = latestTaskFinalReview(facts.reviewRounds, finalReviewContract);
390
395
  const failedFinalOutcome = failedFinal === undefined
391
396
  ? null
392
397
  : classifyReviewRoundOutcome(failedFinal, nextActionReviewOutcomeEvidence(facts));
@@ -414,11 +419,38 @@ export function projectNextAction(facts) {
414
419
  ]
415
420
  });
416
421
  }
422
+ if (finalReviewRequired
423
+ && failedFinal !== undefined
424
+ && failedFinalOutcome?.kind === "semantic"
425
+ && failedFinal.deltaRecheck?.disposition === "requires-full-review") {
426
+ return buildAction(facts, {
427
+ kind: "request-final-review",
428
+ reason: `Delta Recheck ${failedFinal.id} could not establish equivalence; Yui recorded the result and left the next Review action to the Leader.`,
429
+ refs: [ref("review-round", failedFinal.id)],
430
+ preconditions: [
431
+ { fact: "Current frozen head has accepting final Review evidence", satisfied: false }
432
+ ],
433
+ alternatives: [
434
+ {
435
+ kind: "request-full-review",
436
+ reason: "Request a full Review when independent evidence is still required.",
437
+ recommendedCommand: `yui task review request ${task.id} --role ${failedFinal.reviewerRoleName}`,
438
+ refs: [ref("review-round", failedFinal.id)]
439
+ },
440
+ {
441
+ kind: "continue-leader-work",
442
+ reason: "Inspect directly, change the candidate, or choose another Reviewer as Task risk requires.",
443
+ refs: [ref("review-round", failedFinal.id)]
444
+ }
445
+ ],
446
+ judgmentRequired: "Leader must choose full Review, another Reviewer, direct inspection, or more development; Core will not auto-escalate."
447
+ });
448
+ }
417
449
  if (finalReviewRequired
418
450
  && failedFinal !== undefined
419
451
  && failedFinalOutcome?.kind === "semantic"
420
452
  && ((failedFinal.checks ?? []).some(({ outcome }) => outcome === "failed")
421
- || deltaRecheckBlocksAcceptance(failedFinal))) {
453
+ || failedFinal.deltaRecheck?.disposition === "finding")) {
422
454
  return buildAction(facts, {
423
455
  kind: "route-review-findings",
424
456
  reason: `Task-final Review ${failedFinal.id} delivered semantic negative evidence; route its open findings into a repair wave on one frozen head.`,
@@ -429,7 +461,7 @@ export function projectNextAction(facts) {
429
461
  recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
430
462
  });
431
463
  }
432
- const activeFinal = latestTaskFinalReview(facts.reviewRounds);
464
+ const activeFinal = latestTaskFinalReview(facts.reviewRounds, finalReviewContract);
433
465
  if (activeFinal !== undefined
434
466
  && (activeFinal.status === "pending" || activeFinal.status === "running")) {
435
467
  const reviewRef = ref("review-round", activeFinal.id);
@@ -475,12 +507,26 @@ export function projectNextAction(facts) {
475
507
  }
476
508
  return buildAction(facts, {
477
509
  kind: "wait-for-owned-execution",
478
- reason: `Reviewer Run ${reviewRun.id} is executing Task-final Review ${activeFinal.id}.`,
510
+ reason: `Reviewer Run ${reviewRun.id} is executing frozen Task-final Review ${activeFinal.id}; this Review does not globally pause Leader decisions on newer facts.`,
479
511
  refs: [reviewRef, ref("agent-run", reviewRun.id)],
480
512
  preconditions: [
481
513
  { fact: "Task-final ReviewRound is running", satisfied: true, ref: reviewRef },
482
514
  { fact: "Reviewer Run is active", satisfied: true, ref: ref("agent-run", reviewRun.id) }
483
- ]
515
+ ],
516
+ alternatives: [
517
+ {
518
+ kind: "continue-leader-work",
519
+ reason: "Process new user input or advance a later candidate while preserving this frozen Review.",
520
+ refs: [reviewRef]
521
+ },
522
+ {
523
+ kind: "request-another-reviewer",
524
+ reason: "Use another available Reviewer slot when an independent view adds value.",
525
+ recommendedCommand: `yui task review request ${task.id} --role <other-reviewer-role>`,
526
+ refs: [reviewRef]
527
+ }
528
+ ],
529
+ judgmentRequired: "Leader decides whether the current facts justify waiting, continuing development, direct review, or another Reviewer."
484
530
  });
485
531
  }
486
532
  if (activeFinal.reviewerRunId !== undefined) {
@@ -530,11 +576,16 @@ export function projectNextAction(facts) {
530
576
  }
531
577
  const finalReviewOptional = !finalReviewRequired
532
578
  && !hasValidFinalReview(facts);
533
- const finalReviewAlternative = finalReviewOptional && facts.reviewConfig !== null
579
+ const unresolvedDelta = failedFinal?.deltaRecheck?.disposition === "requires-full-review";
580
+ const optionalReviewer = facts.reviewConfig?.roleName
581
+ ?? (unresolvedDelta ? failedFinal?.reviewerRoleName : undefined);
582
+ const finalReviewAlternative = finalReviewOptional && optionalReviewer !== undefined
534
583
  ? [{
535
584
  kind: "request-final-review",
536
- reason: "Request an independent Task-final Review when the Leader wants extra assurance before completion.",
537
- recommendedCommand: `yui task review request ${task.id} --role ${facts.reviewConfig.roleName}`,
585
+ reason: unresolvedDelta
586
+ ? `Delta Recheck ${failedFinal.id} returned requires-full-review; request full independent evidence when the Leader judges it necessary.`
587
+ : "Request an independent Task-final Review when the Leader wants extra assurance before completion.",
588
+ recommendedCommand: `yui task review request ${task.id} --role ${optionalReviewer}`,
538
589
  refs: [ref("task", task.id)]
539
590
  }]
540
591
  : [];
@@ -564,7 +615,9 @@ export function projectNextAction(facts) {
564
615
  ...(!finalReviewOptional
565
616
  ? {}
566
617
  : {
567
- judgmentRequired: "Leader must decide whether the frozen Task result is safe to complete or needs one optional Task-final Review."
618
+ judgmentRequired: unresolvedDelta
619
+ ? "Leader must route the non-accepting Delta result: full Review, another Reviewer, direct inspection, more development, or completion when policy permits."
620
+ : "Leader must decide whether the frozen Task result is safe to complete or needs one optional Task-final Review."
568
621
  }),
569
622
  recommendedCommand: `yui task complete ${task.id} --summary-file -`
570
623
  });
@@ -1013,10 +1066,11 @@ function latestFailedReviewFor(rounds, workItemId) {
1013
1066
  .reverse()
1014
1067
  .find((round) => round.workItemId === workItemId && round.status === "failed");
1015
1068
  }
1016
- function latestTaskFinalReview(rounds) {
1069
+ function latestTaskFinalReview(rounds, contract) {
1017
1070
  return [...rounds]
1018
1071
  .reverse()
1019
- .find((round) => (round.scope ?? "work-item") === "task");
1072
+ .find((round) => ((round.scope ?? "work-item") === "task"
1073
+ && (contract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, contract))));
1020
1074
  }
1021
1075
  function needsChangeSetCapture(facts, item) {
1022
1076
  if (item.status !== "completed")
@@ -1036,13 +1090,17 @@ function needsChangeSetCapture(facts, item) {
1036
1090
  return candidate.workspace !== undefined || candidate.gitSnapshot !== undefined;
1037
1091
  }
1038
1092
  function hasValidFinalReview(facts) {
1039
- const final = latestTaskFinalReview(facts.reviewRounds);
1040
- if (final === undefined
1041
- || !isSemanticReviewRound(final, nextActionReviewOutcomeEvidence(facts)))
1042
- return false;
1043
- if ((final.checks ?? []).some(({ outcome }) => outcome === "failed")
1044
- || deltaRecheckBlocksAcceptance(final))
1093
+ const contract = taskFinalReviewContract(facts);
1094
+ const final = [...facts.reviewRounds]
1095
+ .reverse()
1096
+ .find((round) => ((round.scope ?? "work-item") === "task"
1097
+ && (contract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, contract))));
1098
+ if (final === undefined || !isAcceptedTaskReviewBaselineFromEvidence(final, nextActionReviewOutcomeEvidence(facts)))
1045
1099
  return false;
1100
+ if (facts.currentTaskReviewCandidate !== undefined) {
1101
+ return facts.currentTaskReviewCandidate !== null
1102
+ && isDeepStrictEqual(final.taskCandidate, facts.currentTaskReviewCandidate);
1103
+ }
1046
1104
  const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
1047
1105
  if (reviewedCommits.size === 0)
1048
1106
  return false;
@@ -246,7 +246,7 @@ export class SqliteTelemetryStore {
246
246
  db.pragma("foreign_keys = ON");
247
247
  db.pragma("busy_timeout = 5000");
248
248
  db.pragma("wal_autocheckpoint = 1000");
249
- migrateSqliteSchema(db);
249
+ migrateSqliteSchema(db, { mode: "validate" });
250
250
  this.#db = db;
251
251
  return db;
252
252
  }
@@ -114,9 +114,9 @@ export function executionBand(projection, t, locale) {
114
114
  head.append(pill(t, "exec.status", projection.status));
115
115
  head.append(node("span", "exec-band-owner",
116
116
  t("exec.owner." + projection.owner) + " · " + t("exec.action." + projection.action)));
117
- if (projection.activeExecutorCount > 0) {
117
+ if (projection.activeRuns && projection.activeRuns.length > 0) {
118
118
  head.append(node("span", "exec-band-executors",
119
- projection.activeExecutorCount + " " + t("exec.executors")));
119
+ projection.activeRuns.length + " " + t("exec.executors")));
120
120
  }
121
121
  if (projection.monitoring === "stopped") {
122
122
  head.append(node("span", "exec-band-stopped", t("exec.monitoring.stopped")));
@@ -242,7 +242,6 @@ export function observabilityMetricCard(observability, t) {
242
242
  card.append(metricTile(t("detail.wallClock"), cost.wallClockSeconds + "s"));
243
243
  card.append(metricTile(t("detail.ready"), (observability.dag?.readyIds || []).length, { hot: true }));
244
244
  card.append(metricTile(t("detail.contextSnapshots"), context.snapshotCount));
245
- card.append(metricTile(t("detail.contextPeak"), context.observedInputPeakTokens));
246
245
  const contextMeta = node("div", "record-meta observability-context-meta");
247
246
  contextMeta.append(node("span", "", t("detail.contextBytes") + " · "
248
247
  + (context.totalBytes === null ? t("detail.partial") : context.totalBytes + " B")));
@@ -775,6 +774,17 @@ export function roleCard(role, task, t, locale, actions) {
775
774
  card.append(eff);
776
775
  }
777
776
 
777
+ if (role.sessionTokens) {
778
+ const tokenMeta = node("div", "record-meta");
779
+ const cumulative = role.sessionTokens.cumulativeTotal || {};
780
+ const maximum = role.sessionTokens.maximumRequestInput || {};
781
+ tokenMeta.append(node("span", "", t("detail.sessionTotalTokens") + " · "
782
+ + (cumulative.status === "observed" ? cumulative.totalTokens : t("detail.unobserved"))));
783
+ tokenMeta.append(node("span", "", t("detail.maximumRequestInputTokens") + " · "
784
+ + (maximum.status === "observed" ? maximum.inputTokens : t("detail.unobserved"))));
785
+ card.append(tokenMeta);
786
+ }
787
+
778
788
  if (role.description) card.append(richText(null, role.description, t, { muted: true }));
779
789
 
780
790
  const cols = node("div", "record-cols");
@@ -61,17 +61,19 @@ const messages = {
61
61
  "detail.stageAttempt": "Attempt",
62
62
  "detail.cost": "Cost",
63
63
  "detail.tokens": "Tokens",
64
+ "detail.sessionTotalTokens": "Session total tokens",
65
+ "detail.maximumRequestInputTokens": "Maximum request input",
64
66
  "detail.toolCalls": "Tool calls",
65
67
  "detail.wallClock": "Wall clock",
66
68
  "detail.ready": "Ready",
67
69
  "detail.quorum": "Quorum",
68
70
  "detail.contextSnapshots": "Context snapshots",
69
- "detail.contextPeak": "Context peak",
70
71
  "detail.contextBytes": "Context bytes",
71
72
  "detail.compression": "Compression",
72
73
  "detail.marginalValue": "Marginal value",
73
74
  "detail.partial": "partial",
74
75
  "detail.unavailable": "unavailable",
76
+ "detail.unobserved": "unobserved",
75
77
  "detail.openFindings": "Open findings",
76
78
  "detail.desired": "Desired launch",
77
79
  "detail.desiredAgent": "Desired Agent",
@@ -397,17 +399,19 @@ const messages = {
397
399
  "detail.stageAttempt": "阶段尝试",
398
400
  "detail.cost": "成本",
399
401
  "detail.tokens": "Token",
402
+ "detail.sessionTotalTokens": "Session 累计 Token",
403
+ "detail.maximumRequestInputTokens": "单次请求最大输入",
400
404
  "detail.toolCalls": "工具调用",
401
405
  "detail.wallClock": "墙钟时间",
402
406
  "detail.ready": "可就绪",
403
407
  "detail.quorum": "法定数量",
404
408
  "detail.contextSnapshots": "上下文快照",
405
- "detail.contextPeak": "上下文峰值",
406
409
  "detail.contextBytes": "上下文字节",
407
410
  "detail.compression": "压缩",
408
411
  "detail.marginalValue": "边际价值",
409
412
  "detail.partial": "部分可见",
410
413
  "detail.unavailable": "不可用",
414
+ "detail.unobserved": "未观测",
411
415
  "detail.openFindings": "未解决发现",
412
416
  "detail.desired": "期望启动配置",
413
417
  "detail.desiredAgent": "期望 Agent",
@@ -7,6 +7,7 @@ import { classifyRuntimeHealth, projectRuntimeTaskEvents } from "../runtime/runt
7
7
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
8
8
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
9
9
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
10
+ import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
10
11
  export function buildWebDashboardSnapshot(store, now = new Date()) {
11
12
  return store.transaction((reader) => {
12
13
  const statusCounts = {
@@ -97,6 +98,9 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
97
98
  const effectiveLaunch = activeRun?.effective ?? activeSession?.effective ?? null;
98
99
  return {
99
100
  ...role,
101
+ sessionTokens: projectSessionTokenMetrics(events, resolveSessionTokenIdentity(activeSession === undefined
102
+ ? null
103
+ : { taskId, roleName: role.name, ...activeSession })),
100
104
  effectiveLaunch,
101
105
  effectiveLaunchSource: activeRun === undefined
102
106
  ? activeSession === undefined ? null : "session"
@@ -308,7 +308,7 @@ yui task work group resolve <task-id>/<work-item-id> \
308
308
 
309
309
  每个新阶段还会冻结一份 Resource Broker 契约:token、工具调用和墙钟预算,
310
310
  quorum 与 deadline,straggler 窗口,以及继续增加 Lane 所需的最低边际价值。
311
- 省略这些参数时复用现有 context budget 与 runtime-health 时间窗;同一阶段的 retry
311
+ 省略这些参数时使用独立的执行成本默认值与 runtime-health 时间窗;同一阶段的 retry
312
312
  累计原有花费并共享绝对 deadline。执行、Lane retry 和 Reviewer panel 准入统一核算
313
313
  Home、Task、WorkItem、Group、Provider、Agent 和模型层级的活动 Lane;容量不足的
314
314
  Lane 会耐久保留为 pending,不会把整个 Group 判失败。容量释放或 deadline 到达会沿
@@ -484,7 +484,7 @@ yui task input request <task-id> --question "默认使用哪种格式?" \
484
484
 
485
485
  推荐项会明确展示给用户;如果截止时间前没有回答,独立的最近 deadline timer 会唤醒 Controller,原子采用这个确定选项,并排队恢复固定的 Leader session。自由文本和必须由用户回答的请求永远不会自动解决。
486
486
 
487
- `task input list` 是权威的全局开放输入 Inbox;可附加 Task ID 限定范围,或使用 `--all` 查看已回答和已取消的请求。Controller 还会尝试向已有且结构化状态为 ready 的 Operator process 投递一次带回执的提示;它不会为了通知而启动或打断 Operator。process 不可用或 pane fence 已变化时,请求仍保留在 Inbox,并在后续 Controller 定向处理中重新尝试。该路径不会读取或分类 Agent 终端文本。用户和 Operator 都可回答。存在开放请求时,无关的 pending wake 不会绕过等待,Task 也不能 complete 或 archive。原 Leader 也可执行 `yui task input cancel <task-id> <input-id> --reason "..."`,取消会排队恢复该固定 Leader session。
487
+ `task input list` 是权威的全局开放输入 Inbox;可附加 Task ID 限定范围,或使用 `--all` 查看已回答和已取消的请求。Task 完成、退役、Leader attention、stall 和开放输入只以不可变 TaskEvent 或 InputRequest 引用进入全局 Operator mailbox。Controller 把一个待处理 batch 合并成一条带回执的 `[Yui updates]` user message,仅投递给已有且 ready 的 Operator;Operator 再通过 CLI 读取引用记录,判断哪些信息值得呈现。Operator 正在运行或不可用时,Yui 不启动也不打断它,整批引用保持持久化,并在原生 turn 完成或后续 Controller 处理中重试。该路径是 user message,不是 tool call,也不会读取或分类 Agent 终端文本。用户和 Operator 都可回答。存在开放请求时,无关的 pending wake 不会绕过等待,Task 也不能 complete 或 archive。原 Leader 也可执行 `yui task input cancel <task-id> <input-id> --reason "..."`,取消会排队恢复该固定 Leader session。
488
488
 
489
489
  ```sh
490
490
  yui task context <task-id>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -323,12 +323,14 @@ Choose before creating the WorkItem:
323
323
  Keep review execution separate from implementation. No global Reviewer is
324
324
  required: when review is disabled, inspect and decide directly or delegate a
325
325
  bounded review to a native subagent or ordinary Worker. When a managed
326
- ReviewRound is explicitly configured, its reviewer uses the single built-in
327
- write-capable `reviewer` Profile, but Yui grants that capability only inside a
328
- fresh ReviewRound-owned worktree created from its exact frozen scope:
326
+ ReviewRound is explicitly requested, its reviewer uses the single built-in
327
+ write-capable `reviewer` Profile. Each Task Reviewer Role keeps one stable,
328
+ isolated Session and physical workspace slot; every ReviewRound updates that
329
+ slot to its exact frozen scope and records a new immutable ownership snapshot:
329
330
  the assigned WorkItem Candidate or the committed Integration heads of a
330
331
  Task-final Review. Never reuse the Candidate/Worker workspace or its
331
- implementation Role Session. Codex and Claude may use their normal configured
332
+ implementation Role Session. Multiple Reviewer Roles use independent slots
333
+ and may run in parallel. Codex and Claude may use their normal configured
332
334
  full capability in that isolated worktree; the behavioral boundary forbids
333
335
  push, Integration, Task mutation, other workspaces, stable checkouts, and the
334
336
  real Yui control-plane home. When
@@ -526,8 +528,8 @@ authorized expansions.
526
528
  bounded evidence-gathering review to a native subagent or ordinary Worker,
527
529
  then make the Leader-owned accept/reject decision. Do not create a Reviewer
528
530
  Role merely to satisfy an old setup convention.
529
- - `always`: wait for the automatically requested ReviewRound to become
530
- terminal. Never bypass an active round.
531
+ - `always`: keep the Candidate decision pending until its required ReviewRound
532
+ is terminal. The Review does not globally pause unrelated Leader work.
531
533
  - `leader`: decide whether the existing evidence is sufficient. Request Agent
532
534
  review with `yui task work review <work-id>` when it adds useful evidence.
533
535
  - `final`: keep WorkItem acceptance and integration independent. After all
@@ -535,10 +537,39 @@ authorized expansions.
535
537
  Task-final ReviewRound over the frozen Task candidate. A Task contract may
536
538
  require it. The final Reviewer evaluates the complete result across bound
537
539
  Projects; it is not a second per-WorkItem approval protocol.
538
- - A changed Task head creates a new semantic Task-final Round. Reuse the same
539
- compatible Reviewer Role Session and stable workspace when Yui offers it;
540
- do not create a new Role or native Session for every Round. Round identity
541
- still binds each Run to the exact frozen head.
540
+ - Before choosing a Task-final Review action, read `task context` or
541
+ `task next-action` and inspect every active AgentRun's purpose and exact
542
+ WorkItem/ReviewRound binding, the current durable heads, active Reviews,
543
+ each Reviewer's availability, the latest accepted baseline, candidate
544
+ relation, and Delta facts. These are decision support, not an autopilot;
545
+ a Review Run is evidence in progress, not a global Task lock.
546
+ - A changed Task head may justify a new semantic Task-final Round. Reuse the
547
+ same Reviewer Role Session and stable workspace; Round id, full versus Delta
548
+ mode, desired revision, and frozen commit do not require a replacement
549
+ Session. Round identity still binds each Run to the exact frozen head.
550
+ - An active Task-final Review freezes candidate A only. Continue handling new
551
+ user input and, when appropriate, advance candidate B. Always consume A's
552
+ result, then route it as exact evidence for A, a baseline for descendant B,
553
+ or historical evidence for a diverged candidate. Do not cancel or discard A
554
+ merely because Task main moved.
555
+ - Read active Review facts directly from its ReviewRound: frozen Project
556
+ commits define that Review's evidence boundary; current candidate relation,
557
+ active Run, and workspace references describe current execution. Do not infer
558
+ a Task lock or wait for a synthetic freeze lifecycle before advancing Task
559
+ main.
560
+ - If Yui reports a Reviewer `busy`, wait for the suggested interval, select a
561
+ different Reviewer, review directly, or continue other work. Busy is a
562
+ scheduling fact, not a failed Review and not a reason to reset the Session.
563
+ - Prefer Delta Recheck for a technically available, contiguous change over an
564
+ accepted baseline when the semantic risk is bounded. Use the exact changed
565
+ files, line counts, diff, previous evidence, Task intent, and Project Policy
566
+ to choose full Review, Delta, direct Review, another Reviewer, or no Review;
567
+ Yui does not choose the mode from generic thresholds.
568
+ - Delta `requires-full-review` returns control to the Leader. Decide whether a
569
+ full Review, another Reviewer, direct inspection, or more development is
570
+ useful; Yui must not auto-create the next Round. `repeated-full-review` is a
571
+ cost advisory for an unchanged candidate/Reviewer intent, never an exhausted
572
+ budget or a prohibition on new evidence.
542
573
  - A completed review is advice. Decide whether to accept, reject, review again,
543
574
  or ask the user.
544
575
  - Route a reachable final-Review finding to the original Worker while that
@@ -563,11 +594,16 @@ authorized expansions.
563
594
  full Round over the identical frozen heads. It fails closed for every
564
595
  semantic or ambiguous prior result; target the new failed Round explicitly
565
596
  if another non-semantic failure occurs.
597
+ - For `review.failed-to-start`, open the referenced ReviewRound and inspect its
598
+ exact reason, frozen candidate, and workspace when present. Decide whether to
599
+ retry, explicitly clean a conflicting workspace, select another Reviewer, or
600
+ continue other work. Preserve the failed Round as request history and do not
601
+ turn these choices into an automatic retry or cleanup loop.
566
602
  - Use `task next-action`'s derived Review outcome literally: non-semantic means
567
603
  recover the same frozen head with `force-fresh`; ambiguous means diagnose the
568
604
  inconsistent evidence without creating a Repair WorkItem; only semantic
569
- negative evidence may create a repair wave. Non-semantic and ambiguous
570
- attempts do not consume the full semantic Review budget.
605
+ negative evidence may create a repair wave. There is no semantic Review
606
+ budget; exact candidate/Reviewer/intent retries reuse existing evidence.
571
607
  - If the same non-resource user choice or unavailable external fact repeats,
572
608
  persist context and create an InputRequest instead of looping. Never use an
573
609
  InputRequest to solicit authorization for an unrequested real-resource test;
@@ -612,10 +648,11 @@ accept an isolated result while any writable Project's latest ChangeSet is
612
648
  unintegrated.
613
649
 
614
650
  Workspace ownership is not Role ownership. The WorkItem owns its Develop
615
- workspace even when a Task Role executes there; each ReviewRound owns a fresh
616
- workspace from the Candidate's frozen commit, and each IntegrationAttempt owns
617
- its candidate worktree. Dispatch attaches snapshots only. Review workspace
618
- cleanup is explicit, and review edits can never feed WorkItem ChangeSet capture.
651
+ workspace even when a Task Role executes there; a Task Reviewer Role keeps one
652
+ stable physical workspace while each ReviewRound owns the exact frozen
653
+ workspace evidence for its Run, and each IntegrationAttempt owns its candidate
654
+ worktree. Dispatch attaches snapshots only. Review workspace cleanup is
655
+ explicit, and review edits can never feed WorkItem ChangeSet capture.
619
656
 
620
657
  Yui validates a candidate and advances the target with compare-and-swap. A
621
658
  failed candidate does not advance the target. Inspect and resolve semantic
@@ -46,6 +46,15 @@ mind and avoid imposing a fixed heading, field, section, or character
46
46
  template; one semantic event should have one concise summary unless a later
47
47
  role adds a genuinely new decision or impact.
48
48
 
49
+ A `[Yui updates]` user message is only a wake envelope containing durable
50
+ InputRequest or TaskEvent references. Read those exact records through `yui`
51
+ before responding, merge related references into one user-level update, and
52
+ then end the turn normally. Never create or keep a Codex Goal, automatic
53
+ self-continuation, polling loop, or private follow-up task merely to monitor
54
+ Yui work. Future durable updates will wake the Operator with another user
55
+ message after the current Operator turn is ready; unchanged state needs no
56
+ response.
57
+
49
58
  ## Configure Yui through conversation
50
59
 
51
60
  Treat configuration as an Operator-owned conversation, not a list of commands
@@ -18,13 +18,18 @@ reinterpret its scope:
18
18
  anchor.
19
19
 
20
20
  A Role is an executor, not a workspace owner: each ReviewRound owns an exact
21
- workspace record. Consecutive Task-final Rounds for the same Reviewer may
22
- reassign one clean physical workspace and continue the same compatible native
23
- Session. Treat the new Run Context Pack and frozen head as the authority even
24
- when the conversation continues; never reuse an earlier verdict. Review edits
25
- are confined to that workspace, never modify the
21
+ workspace record. Consecutive Task-final Rounds for the same Reviewer reuse one
22
+ clean physical workspace and native Session while Yui updates the checkout and
23
+ records the new Round snapshot. Treat the new Run Context Pack and frozen head
24
+ as the authority even when the conversation continues; never reuse an earlier
25
+ verdict. Review edits are confined to that workspace, never modify the
26
26
  WorkItem Develop workspace, and never become a ChangeSet source.
27
27
 
28
+ For a dispatched Review, the Run Context Pack identifies the ReviewRound,
29
+ frozen Project commits, and assigned workspace. Inspect those exact commits.
30
+ The current mutable Task-main checkout is context only and must never replace,
31
+ widen, or silently update the assigned Review scope.
32
+
28
33
  ## Separate infrastructure failure from review judgment
29
34
 
30
35
  Verify the exact Run identity, Context Pack, frozen head, and ReviewRound-owned
@@ -44,10 +49,26 @@ binding fails before review begins:
44
49
 
45
50
  Yui derives `semantic`, `non-semantic`, or `ambiguous` from the immutable
46
51
  Round, Run receipt, completion Event, and finding evidence. Never write or
47
- simulate a classification field. A non-semantic attempt consumes no semantic
48
- Review budget and cannot satisfy acceptance; an ambiguous attempt requires
52
+ simulate a classification field. A non-semantic attempt cannot satisfy
53
+ acceptance; an ambiguous attempt requires
49
54
  Leader diagnosis before another review or repair decision.
50
55
 
56
+ The Review scope remains the current Run's frozen candidate even if the Leader
57
+ handles new user input or advances Task main while this Review is running. Do
58
+ not switch to the newer head, cancel the current inspection, or claim the
59
+ result covers anything beyond the frozen candidate.
60
+
61
+ For a Delta Recheck, judge only the verified baseline plus the exact supplied
62
+ diff. Return exactly one explicit disposition with reasoning:
63
+
64
+ - `equivalent-and-accepted` when the new candidate preserves the accepted
65
+ semantics and evidence;
66
+ - `finding` for a reachable material defect;
67
+ - `requires-full-review` when equivalence cannot be established.
68
+
69
+ Never create or request a follow-up Round yourself: `requires-full-review`, a
70
+ finding, and every uncertainty return to the Leader for routing.
71
+
51
72
  Keep the context layers distinct. Yui Core owns ReviewRound identity,
52
73
  lifecycle, access, workspace, and exact-yield safety; this generic Skill owns
53
74
  portable review behavior; Agent-native Project Skills and Project Policy and
@@ -1,71 +0,0 @@
1
- import { resolveContextBudget } from "../config/yuiConfig.js";
2
- import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
3
- export function evaluateSessionContextBudget(events, session, configured) {
4
- const budget = resolveContextBudget(configured);
5
- const peak = observedPeakInputTokens(events, session);
6
- const state = peak >= budget.hardTokens
7
- ? "hard"
8
- : peak >= budget.softTokens
9
- ? "soft"
10
- : "within";
11
- return Object.freeze({ state, peakTokens: peak, budget });
12
- }
13
- /**
14
- * Largest per-request input peak observed for one Session generation. Usage
15
- * observations carry cumulative session totals; consecutive deltas bound the
16
- * per-request input of the window between them. The first observation for a
17
- * generation is treated as one full request so a long-lived resumed Session
18
- * that was only sampled late still counts its inherited context.
19
- */
20
- export function observedPeakInputTokens(events, session) {
21
- let previous = 0;
22
- let peak = 0;
23
- for (const event of events) {
24
- const observation = runtimeObservationFromTaskEvent(event);
25
- if (observation === null)
26
- continue;
27
- if (!matchesSession(observation.fence, session))
28
- continue;
29
- const usage = usageTotal(observation.payload.usage);
30
- if (usage === null)
31
- continue;
32
- const delta = Math.max(0, usage - previous);
33
- if (delta > peak)
34
- peak = delta;
35
- previous = usage;
36
- }
37
- return peak;
38
- }
39
- function matchesSession(fence, session) {
40
- if (fence.taskId !== undefined && fence.taskId !== session.taskId)
41
- return false;
42
- if (fence.roleName !== session.roleName)
43
- return false;
44
- if (session.nativeSessionId !== undefined
45
- && fence.nativeSessionId !== undefined
46
- && fence.nativeSessionId !== session.nativeSessionId) {
47
- return false;
48
- }
49
- if (session.launchId !== undefined
50
- && fence.launchId !== undefined
51
- && fence.launchId !== session.launchId) {
52
- return false;
53
- }
54
- return true;
55
- }
56
- function usageTotal(usage) {
57
- if (typeof usage !== "object" || usage === null || Array.isArray(usage))
58
- return null;
59
- const record = usage;
60
- const inputTokens = integer(record.inputTokens);
61
- if (inputTokens === null)
62
- return null;
63
- // RuntimeUsageSnapshot.inputTokens is the normalized processed input total.
64
- // cachedInputTokens is an informational breakdown of that total, matching
65
- // runtimeProjection and the Driver documentation; adding it again would
66
- // fabricate context pressure and trigger premature Session rollover.
67
- return inputTokens;
68
- }
69
- function integer(value) {
70
- return Number.isSafeInteger(value) && value >= 0 ? value : null;
71
- }