@zq-silk/yui 0.10.0 → 0.11.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 (46) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commandCatalog.js +28 -7
  3. package/dist/cli.js +50 -47
  4. package/dist/commands/projectCommands.js +69 -6
  5. package/dist/commands/taskCommands.js +639 -89
  6. package/dist/commands/taskContextCommand.js +78 -27
  7. package/dist/commands/taskNextActionCommand.js +13 -2
  8. package/dist/commands/taskOverviewCommand.js +21 -5
  9. package/dist/commands/taskUpstreamCommands.js +136 -0
  10. package/dist/context/runContextPack.js +184 -17
  11. package/dist/controller/agentRuntimeObserver.js +31 -20
  12. package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
  13. package/dist/execution/candidateConvergence.js +623 -0
  14. package/dist/execution/executionGroup.js +255 -13
  15. package/dist/execution/executionHealth.js +324 -0
  16. package/dist/execution/resourceBroker.js +425 -0
  17. package/dist/executor/fileRoleLaunchPlanner.js +6 -9
  18. package/dist/executor/workspacePreflightClassification.js +117 -0
  19. package/dist/lifecycle/exactRunTerminalization.js +13 -2
  20. package/dist/lifecycle/taskRoleSessionReset.js +4 -2
  21. package/dist/repository/taskBaseFreshness.js +26 -1
  22. package/dist/repository/taskWorkspacePreparer.js +17 -1
  23. package/dist/review/reviewRound.js +27 -6
  24. package/dist/run/agentRun.js +2 -2
  25. package/dist/run/recoveryProjection.js +15 -0
  26. package/dist/runtime/runtimeContinuationProjection.js +7 -0
  27. package/dist/runtime/tmuxAdapters.js +8 -2
  28. package/dist/scheduler/actionability.js +169 -3
  29. package/dist/scheduler/activeTaskProgress.js +15 -10
  30. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  31. package/dist/scheduler/taskExecutionProjection.js +105 -8
  32. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  33. package/dist/storage/migration/productionRegistry.js +14 -0
  34. package/dist/storage/sqliteStore.js +12 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/task/completionReadiness.js +1 -1
  37. package/dist/task/nextAction.js +314 -2
  38. package/dist/web/assets/client/components.js +116 -0
  39. package/dist/web/assets/client/i18n.js +66 -0
  40. package/dist/web/assets/client/view.js +15 -0
  41. package/dist/web/assets/styles/cards.js +23 -0
  42. package/dist/web/assets/styles/responsive.js +2 -0
  43. package/dist/web/webSnapshot.js +8 -2
  44. package/dist/workItem/workItem.js +262 -5
  45. package/i18n/README.zh-CN.md +42 -0
  46. package/package.json +1 -1
@@ -2,8 +2,11 @@ import { createHash } from "node:crypto";
2
2
  import { changeSetDeliverySettled, governingChangeSets } from "../integration/deliveryObligation.js";
3
3
  import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
4
4
  import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
5
+ import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
6
+ import { candidateConvergenceDisagreement, candidateConvergenceEvidenceSufficient, candidateConvergenceStageResultsValid } from "../execution/candidateConvergence.js";
7
+ import { executionStageSpendClosed, routeExecutionStage } from "../execution/resourceBroker.js";
5
8
  import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
6
- import { currentWorkItemCandidate, governingWorkItemCandidate } from "../workItem/workItem.js";
9
+ import { currentWorkItemCandidate, currentWorkItemExecutionGroup, governingWorkItemCandidate } from "../workItem/workItem.js";
7
10
  const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
8
11
  export function projectNextAction(facts) {
9
12
  const { task } = facts;
@@ -40,6 +43,45 @@ export function projectNextAction(facts) {
40
43
  recommendedCommand: inconsistency.recommendedCommand
41
44
  });
42
45
  }
46
+ const laneRecovery = actionableExecutionLaneRecoveries(facts.executionGroups ?? [])
47
+ .find(hasExactRun);
48
+ if (laneRecovery !== undefined) {
49
+ return buildExecutionLaneRecoveryAction(facts, laneRecovery);
50
+ }
51
+ // Quick Win (EXE-03): a resume Run that failed before durable Provider
52
+ // acceptance must not be retried against the same native Session. The
53
+ // authoritative next action is to replace the Session, not to retry the
54
+ // same delivery. The guard only applies while the failed resume Run is the
55
+ // *latest* Leader Run: once a newer Run exists (the fresh-Session launch),
56
+ // the historical failure is stale and must not keep recommending a Session
57
+ // replacement.
58
+ const latestLeaderRun = facts.leaderRuns.at(-1);
59
+ const failedResumeWithoutAcceptance = latestLeaderRun !== undefined
60
+ && latestLeaderRun.mode === "resume"
61
+ && latestLeaderRun.status === "failed"
62
+ && latestLeaderRun.deliveredAt === undefined
63
+ ? latestLeaderRun
64
+ : undefined;
65
+ if (failedResumeWithoutAcceptance !== undefined) {
66
+ return buildAction(facts, {
67
+ kind: "replace-leader-session",
68
+ reason: `Leader resume Run ${failedResumeWithoutAcceptance.id} failed before Provider acceptance; `
69
+ + "the native Session is proven unusable for this delivery. Replace it with a fresh Session "
70
+ + "after exact cleanup/reset.",
71
+ refs: [ref("agent-run", failedResumeWithoutAcceptance.id)],
72
+ preconditions: [
73
+ { fact: "Resume Run failed without durable acceptance", satisfied: true, ref: ref("agent-run", failedResumeWithoutAcceptance.id) },
74
+ { fact: "Old Session is cleaned up or reset before fresh launch", satisfied: false }
75
+ ],
76
+ // The failed resume Run is already terminal, so `task run recover
77
+ // --action replace-session` (which requires an active Run) cannot act
78
+ // on it. The working recovery is to reset the Role's Session
79
+ // generation, then clear the Leader failure so the next wake launches
80
+ // a fresh Session (the failed-resume guard in the wakeup processor
81
+ // forces mode=new for the next launch).
82
+ recommendedCommand: `yui task role reset ${task.id} leader --reason "resume failed before acceptance" && yui jobs retry leader-recovery:${task.id}`
83
+ });
84
+ }
43
85
  const activeLeader = facts.activeRuns.find((run) => run.roleName === "leader");
44
86
  if (activeLeader !== undefined) {
45
87
  return buildAction(facts, {
@@ -99,6 +141,18 @@ export function projectNextAction(facts) {
99
141
  }
100
142
  const reviewRun = activeReviewRoundRun(activeReview, facts.activeRuns);
101
143
  if (reviewRun === undefined) {
144
+ if (reviewGroupHasResourceQueue(activeReview)) {
145
+ return buildAction(facts, {
146
+ kind: "resume-review",
147
+ reason: `ReviewRound ${activeReview.id} has Reviewer Lanes waiting for Resource Broker capacity.`,
148
+ refs: [reviewRef],
149
+ preconditions: [
150
+ { fact: "A Reviewer Lane is durably queued", satisfied: true, ref: reviewRef },
151
+ { fact: "Resource capacity is available", satisfied: false }
152
+ ],
153
+ recommendedCommand: `yui task work review ${task.id}/${candidateReady.id}`
154
+ });
155
+ }
102
156
  const runRef = ref("agent-run", activeReview.reviewerRunId);
103
157
  return buildAction(facts, {
104
158
  kind: "repair-protocol-inconsistency",
@@ -202,6 +256,18 @@ export function projectNextAction(facts) {
202
256
  recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
203
257
  });
204
258
  }
259
+ const explorationStop = exhaustedExplorationReason(failedWork, facts.executionGroups);
260
+ if (explorationStop !== undefined) {
261
+ return buildAction(facts, {
262
+ kind: "implement-current-work-item",
263
+ reason: explorationStop,
264
+ refs: [ref("work-item", failedWork.id)],
265
+ preconditions: [
266
+ { fact: "Work Item exploration cannot continue", satisfied: true, ref: ref("work-item", failedWork.id) }
267
+ ],
268
+ recommendedCommand: `yui task work retire ${task.id}/${failedWork.id} --summary \"<reason>\"`
269
+ });
270
+ }
205
271
  return buildAction(facts, {
206
272
  kind: "implement-current-work-item",
207
273
  reason: `Work Item ${failedWork.id} failed without a Review verdict; retry implementation.`,
@@ -209,7 +275,9 @@ export function projectNextAction(facts) {
209
275
  preconditions: [
210
276
  { fact: "Work Item is failed", satisfied: true, ref: ref("work-item", failedWork.id) }
211
277
  ],
212
- recommendedCommand: `yui task work update ${task.id}/${failedWork.id} running`
278
+ recommendedCommand: failedWork.assignee === undefined
279
+ ? `yui task work update ${task.id}/${failedWork.id} running`
280
+ : `yui task work dispatch ${task.id}/${failedWork.id}`
213
281
  });
214
282
  }
215
283
  const openWork = selectOpenWorkItem(facts.workItems);
@@ -230,6 +298,9 @@ export function projectNextAction(facts) {
230
298
  }
231
299
  if (openWork?.kind === "ready") {
232
300
  const item = openWork.item;
301
+ const stageAction = buildExecutionStageAction(facts, item);
302
+ if (stageAction !== null)
303
+ return stageAction;
233
304
  const refs = [ref("work-item", item.id)];
234
305
  return buildAction(facts, {
235
306
  kind: "implement-current-work-item",
@@ -379,6 +450,18 @@ export function projectNextAction(facts) {
379
450
  }
380
451
  const reviewRun = activeReviewRoundRun(activeFinal, facts.activeRuns);
381
452
  if (reviewRun === undefined) {
453
+ if (reviewGroupHasResourceQueue(activeFinal)) {
454
+ return buildAction(facts, {
455
+ kind: "resume-review",
456
+ reason: `Task-final ReviewRound ${activeFinal.id} has Reviewer Lanes waiting for Resource Broker capacity.`,
457
+ refs: [reviewRef],
458
+ preconditions: [
459
+ { fact: "A Reviewer Lane is durably queued", satisfied: true, ref: reviewRef },
460
+ { fact: "Resource capacity is available", satisfied: false }
461
+ ],
462
+ recommendedCommand: `yui task review request ${task.id} --role ${activeFinal.reviewerRoleName}`
463
+ });
464
+ }
382
465
  const runRef = ref("agent-run", activeFinal.reviewerRunId);
383
466
  return buildAction(facts, {
384
467
  kind: "repair-protocol-inconsistency",
@@ -486,6 +569,77 @@ export function projectNextAction(facts) {
486
569
  recommendedCommand: `yui task complete ${task.id} --summary-file -`
487
570
  });
488
571
  }
572
+ function hasExactRun(lane) {
573
+ return lane.runId !== undefined;
574
+ }
575
+ function buildExecutionLaneRecoveryAction(facts, lane) {
576
+ const refs = [
577
+ ref("execution-group", lane.groupId),
578
+ ref("execution-lane", lane.laneId),
579
+ ref("agent-run", lane.runId)
580
+ ];
581
+ if (lane.recovery === "retry-new-agent-run") {
582
+ return buildAction(facts, {
583
+ kind: "recover-execution-lane",
584
+ reason: `Execution Lane ${lane.laneId} is durably failed; retry only exact Run ${lane.runId} and retain sibling results.`,
585
+ refs,
586
+ preconditions: [
587
+ { fact: "Execution Lane is failed and unresolved", satisfied: true, ref: refs[1] },
588
+ { fact: "Exact failed AgentRun is retained", satisfied: true, ref: refs[2] }
589
+ ],
590
+ recommendedCommand: `yui task run retry ${facts.task.id}/${lane.runId}`
591
+ });
592
+ }
593
+ const action = lane.recovery === "diagnose" ? "diagnose" : "terminate";
594
+ const recovery = facts.runRecoveries?.find(({ runId }) => runId === lane.runId);
595
+ const plan = recovery?.actions.find((candidate) => candidate.action === action);
596
+ return buildAction(facts, {
597
+ kind: "recover-execution-lane",
598
+ reason: lane.recovery === "diagnose"
599
+ ? `Execution Lane ${lane.laneId} needs bounded diagnostics for exact Run ${lane.runId}.`
600
+ : `Execution Lane ${lane.laneId} has confirmed death evidence; terminate exact Run ${lane.runId}.`,
601
+ refs,
602
+ preconditions: [
603
+ { fact: `Lane recovery is ${lane.recovery}`, satisfied: true, ref: refs[1] },
604
+ {
605
+ fact: `Exact Run exposes a current ${action} recovery plan`,
606
+ satisfied: plan !== undefined,
607
+ ref: refs[2]
608
+ }
609
+ ],
610
+ ...(plan === undefined ? {} : { recommendedCommand: plan.command }),
611
+ ...(plan !== undefined && recovery?.judgmentRequired === undefined
612
+ ? {}
613
+ : {
614
+ judgmentRequired: recovery?.judgmentRequired
615
+ ?? `Inspect yui task run show ${facts.task.id}/${lane.runId}; its exact recovery fence is unavailable.`
616
+ })
617
+ });
618
+ }
619
+ function exhaustedExplorationReason(item, executionGroups) {
620
+ const group = currentWorkItemExecutionGroup(item);
621
+ if (group?.stage === undefined || group.resolution === undefined)
622
+ return undefined;
623
+ if (group.resolution.decision === "reject") {
624
+ return `Work Item ${item.id} exploration was rejected and has no legal continuation; retire it explicitly.`;
625
+ }
626
+ if (group.resolution.decision === "retry"
627
+ && group.stage.stage === "resolve"
628
+ && group.stage.round >= group.stage.maxRounds) {
629
+ return `Work Item ${item.id} exhausted its exploration round budget; retire it explicitly.`;
630
+ }
631
+ const resources = executionGroups?.find(({ groupId }) => groupId === group.id)?.resources;
632
+ if ((group.resolution.decision === "retry" || group.resolution.decision === "blocked")
633
+ && resources !== undefined
634
+ && executionStageSpendClosed(resources)) {
635
+ return `Work Item ${item.id} cannot retry its frozen exploration resource budget; retire it explicitly or replace it with a newly authorized delivery boundary.`;
636
+ }
637
+ if ((group.resolution.decision === "retry" || group.resolution.decision === "blocked")
638
+ && group.stage.stageAttempt >= group.stage.budget.maxAttempts) {
639
+ return `Work Item ${item.id} exhausted its ${group.stage.stage} stage attempt budget; retire it explicitly.`;
640
+ }
641
+ return undefined;
642
+ }
489
643
  /**
490
644
  * Stable fingerprint of the durable delivery position. It changes exactly
491
645
  * when a delivery record changes, so the semantic-progress budget can compare
@@ -527,6 +681,156 @@ function buildAction(facts, input) {
527
681
  fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
528
682
  };
529
683
  }
684
+ function buildExecutionStageAction(facts, item) {
685
+ const group = currentWorkItemExecutionGroup(item);
686
+ if (group?.stage?.resources === undefined || group.resolution !== undefined)
687
+ return null;
688
+ const projected = facts.executionGroups?.find(({ groupId }) => groupId === group.id);
689
+ const resources = projected?.resources;
690
+ if (resources === undefined)
691
+ return null;
692
+ const usableLaneIds = group.lanes.filter(({ status }) => (status === "yielded" || status === "completed")).map(({ id }) => id);
693
+ const stageResultsValid = candidateConvergenceStageResultsValid(group);
694
+ const disagreement = candidateConvergenceDisagreement(group);
695
+ const routing = routeExecutionStage({
696
+ group,
697
+ resources,
698
+ evidenceSufficient: candidateConvergenceEvidenceSufficient(item, group, usableLaneIds),
699
+ disagreement
700
+ });
701
+ const refs = [ref("work-item", item.id), ref("execution-group", group.id)];
702
+ const resolveCommand = (decision, suffix = "") => (`yui task work group resolve ${facts.task.id}/${item.id}`
703
+ + ` --decision ${decision} --summary \"<stage decision>\"${suffix}`);
704
+ if (routing.action === "blocked") {
705
+ return buildAction(facts, {
706
+ kind: "resolve-execution-stage",
707
+ reason: `${routing.reason}; dispatch would preserve the same pending Lanes without starting them.`,
708
+ refs,
709
+ preconditions: [
710
+ { fact: "Execution stage is unresolved", satisfied: true, ref: refs[1] },
711
+ {
712
+ fact: "Stage deadline or hard budget is exhausted",
713
+ satisfied: executionStageSpendClosed(resources),
714
+ ref: refs[1]
715
+ },
716
+ { fact: "Acceptance-level evidence is sufficient", satisfied: false, ref: refs[1] }
717
+ ],
718
+ recommendedCommand: resolveCommand("blocked"),
719
+ judgmentRequired: "Leader must record the resource-blocked stage, then retire or replace the delivery boundary; the frozen budget cannot be reopened by redispatch."
720
+ });
721
+ }
722
+ if (routing.action === "expand-parallel") {
723
+ return buildAction(facts, {
724
+ kind: "resolve-execution-stage",
725
+ reason: routing.reason,
726
+ refs,
727
+ preconditions: [
728
+ {
729
+ fact: "Stage quorum is open or structured results show material disagreement",
730
+ satisfied: !resources.quorumMet || disagreement === "high",
731
+ ref: refs[1]
732
+ },
733
+ {
734
+ fact: "Adaptive Lane capacity remains",
735
+ satisfied: group.strategy.mode === "adaptive"
736
+ && group.lanes.length < group.strategy.max,
737
+ ref: refs[1]
738
+ }
739
+ ],
740
+ recommendedCommand: `yui task work dispatch ${facts.task.id}/${item.id} --lane-role <independent-role>`,
741
+ ...(resources.quorumMet
742
+ ? {
743
+ alternatives: [{
744
+ kind: "deepen-sequential",
745
+ reason: "Resolve the current evidence and deepen sequentially when another independent Lane has lower value.",
746
+ recommendedCommand: resolveCommand("accept"),
747
+ refs
748
+ }]
749
+ }
750
+ : {}),
751
+ judgmentRequired: resources.quorumMet
752
+ ? "Leader must choose an unused compatible Task Role for expansion or deliberately select the sequential alternative."
753
+ : "Leader must choose an unused compatible Task Role so the frozen stage can satisfy quorum."
754
+ });
755
+ }
756
+ if (routing.action === "deepen-sequential") {
757
+ const resolveRequestsNextRound = group.stage.stage === "resolve";
758
+ const decision = usableLaneIds.length === 0
759
+ || !stageResultsValid
760
+ || !resources.quorumMet
761
+ || resolveRequestsNextRound
762
+ ? "retry"
763
+ : "accept";
764
+ return buildAction(facts, {
765
+ kind: "resolve-execution-stage",
766
+ reason: resolveRequestsNextRound
767
+ ? "Resolve evidence does not establish a Candidate; begin another bounded exploration round."
768
+ : !resources.quorumMet
769
+ ? "The stage exhausted its Lane capacity before quorum; resolve it as a bounded retry."
770
+ : decision === "retry"
771
+ ? "The stage has no structurally usable output; resolve it as a bounded retry before redispatch."
772
+ : routing.reason,
773
+ refs,
774
+ preconditions: [
775
+ { fact: "No stage Lane remains active or queued", satisfied: true, ref: refs[1] },
776
+ { fact: "Current stage has structurally usable output", satisfied: stageResultsValid, ref: refs[1] },
777
+ { fact: "Stage quorum is met", satisfied: resources.quorumMet, ref: refs[1] }
778
+ ],
779
+ recommendedCommand: resolveCommand(decision),
780
+ judgmentRequired: resolveRequestsNextRound
781
+ ? "Leader must judge whether the frozen round budget permits another exploration round or the WorkItem should be retired."
782
+ : decision === "retry"
783
+ ? "Leader must judge whether the frozen attempt budget permits one retry or the WorkItem should be retired."
784
+ : "Leader must select the usable stage evidence before advancing to the next bounded stage."
785
+ });
786
+ }
787
+ if (routing.action === "resolve") {
788
+ const earlyStop = routing.cancelPendingLaneIds.length === 0
789
+ ? ""
790
+ : " --early-stop <observed-marginal-value>";
791
+ return buildAction(facts, {
792
+ kind: "resolve-execution-stage",
793
+ reason: routing.reason,
794
+ refs,
795
+ preconditions: [
796
+ { fact: "Stage quorum is met", satisfied: resources.quorumMet, ref: refs[1] },
797
+ { fact: "Acceptance-level evidence is sufficient", satisfied: true, ref: refs[1] }
798
+ ],
799
+ recommendedCommand: resolveCommand("accept", earlyStop),
800
+ judgmentRequired: routing.cancelPendingLaneIds.length === 0
801
+ ? "Leader must select and accept the evidence that satisfies the stage contract."
802
+ : "Leader must record the observed marginal value before skipping never-started pending Lanes."
803
+ });
804
+ }
805
+ if (resources.pendingLaneIds.length > 0 && resources.activeLaneIds.length === 0) {
806
+ return buildAction(facts, {
807
+ kind: "implement-current-work-item",
808
+ reason: `ExecutionGroup ${group.id} has queued Lanes and an open resource budget; retry Broker admission after the capacity wake.`,
809
+ refs,
810
+ preconditions: [
811
+ { fact: "At least one Lane is durably queued", satisfied: true, ref: refs[1] },
812
+ {
813
+ fact: "Stage deadline and hard budgets remain open",
814
+ satisfied: !executionStageSpendClosed(resources),
815
+ ref: refs[1]
816
+ }
817
+ ],
818
+ recommendedCommand: `yui task work dispatch ${facts.task.id}/${item.id}`
819
+ });
820
+ }
821
+ if (resources.activeLaneIds.length > 0) {
822
+ return buildAction(facts, {
823
+ kind: "repair-protocol-inconsistency",
824
+ reason: `ExecutionGroup ${group.id} retains active Lanes but no delegated AgentRun is active.`,
825
+ refs,
826
+ conflicts: refs,
827
+ preconditions: [
828
+ { fact: "Every active Lane has an active exact AgentRun", satisfied: false, ref: refs[1] }
829
+ ]
830
+ });
831
+ }
832
+ return null;
833
+ }
530
834
  function latestActiveWorkItemReview(rounds, item, candidate) {
531
835
  if (candidate === undefined)
532
836
  return undefined;
@@ -567,6 +871,12 @@ function reviewGroupAwaitingResolution(round) {
567
871
  && isPanelReviewRound(round)
568
872
  && round.executionGroup.lanes.every((lane) => TERMINAL_REVIEW_LANE_STATUSES.has(lane.status));
569
873
  }
874
+ function reviewGroupHasResourceQueue(round) {
875
+ return round.executionGroup !== undefined
876
+ && round.executionGroup.resolution === undefined
877
+ && round.executionGroup.lanes.some((lane) => (lane.status === "pending"
878
+ && lane.runId === undefined));
879
+ }
570
880
  function buildResolveReviewGroupAction(facts, round) {
571
881
  const group = round.executionGroup;
572
882
  const refs = [
@@ -621,6 +931,8 @@ function reviewRoundConflict(round, activeRuns) {
621
931
  };
622
932
  }
623
933
  if (activeReviewRoundRun(round, activeRuns) === undefined) {
934
+ if (reviewGroupHasResourceQueue(round))
935
+ return null;
624
936
  const runRef = ref("agent-run", round.reviewerRunId);
625
937
  return {
626
938
  reason: `ReviewRound ${round.id} references Reviewer Run ${round.reviewerRunId}, but that Run is not active.`,
@@ -175,12 +175,37 @@ export function executionGroupCard(summary, t, locale) {
175
175
  head.append(pills);
176
176
  card.append(head);
177
177
 
178
+ if (summary.stage) {
179
+ const stage = node("div", "record-meta execution-stage-meta");
180
+ stage.append(node("span", "mono", t("detail.stage") + " · " + (summary.stage.stage || "—")));
181
+ stage.append(node("span", "", t("detail.round") + " · " + String(summary.stage.round)));
182
+ stage.append(node("span", "", t("detail.stageAttempt") + " · " + String(summary.stage.stageAttempt)));
183
+ stage.append(chip(t("mode." + summary.stage.mode)));
184
+ card.append(stage);
185
+ }
186
+ if (summary.resources) {
187
+ const resources = summary.resources;
188
+ const budget = node("div", "record-meta execution-resource-meta");
189
+ budget.append(node("span", "", t("detail.cost") + " · "
190
+ + formatResource(resources.tokens, resources.tokensRemaining, resources.tokensObservable) + " tokens"));
191
+ budget.append(node("span", "", formatResource(resources.toolCalls, resources.toolCallsRemaining, resources.toolCallsObservable) + " tools"));
192
+ budget.append(node("span", "", resources.wallClockSeconds + "s"));
193
+ budget.append(node("span", "", t("detail.quorum") + " · "
194
+ + resources.usableLaneCount + "/" + (summary.stage.resources?.quorum || "—")));
195
+ card.append(budget);
196
+ }
197
+
178
198
  const lanes = node("div", "lane-list");
179
199
  summary.laneSummaries.forEach(function (lane) {
180
200
  const row = node("div", "lane-row");
181
201
  row.append(statusDot(lane.status));
182
202
  row.append(node("span", "lane-role", lane.roleName));
183
203
  row.append(node("span", "lane-status", t("lane." + lane.status)));
204
+ if (lane.effective) {
205
+ const config = [lane.effective.adapterId, lane.effective.model, lane.effective.effort]
206
+ .filter(function (value) { return value; }).join(" · ");
207
+ if (config) row.append(chip(config));
208
+ }
184
209
  if (lane.summary) {
185
210
  row.append(node("span", "lane-summary", lane.summary));
186
211
  }
@@ -202,6 +227,63 @@ export function executionGroupCard(summary, t, locale) {
202
227
  return card;
203
228
  }
204
229
 
230
+ function formatResource(used, remaining, observable) {
231
+ const value = String(used) + (remaining === undefined ? "" : "/" + String(used + remaining));
232
+ return observable === false ? value + "*" : value;
233
+ }
234
+
235
+ export function observabilityMetricCard(observability, t) {
236
+ if (!observability) return null;
237
+ const card = node("div", "observability-metrics");
238
+ const cost = observability.cost || {};
239
+ const context = observability.context || {};
240
+ card.append(metricTile(t("detail.tokens"), cost.tokens + (cost.tokensObservable === false ? "*" : "")));
241
+ card.append(metricTile(t("detail.toolCalls"), cost.toolCalls + (cost.toolCallsObservable === false ? "*" : "")));
242
+ card.append(metricTile(t("detail.wallClock"), cost.wallClockSeconds + "s"));
243
+ card.append(metricTile(t("detail.ready"), (observability.dag?.readyIds || []).length, { hot: true }));
244
+ card.append(metricTile(t("detail.contextSnapshots"), context.snapshotCount));
245
+ card.append(metricTile(t("detail.contextPeak"), context.observedInputPeakTokens));
246
+ const contextMeta = node("div", "record-meta observability-context-meta");
247
+ contextMeta.append(node("span", "", t("detail.contextBytes") + " · "
248
+ + (context.totalBytes === null ? t("detail.partial") : context.totalBytes + " B")));
249
+ contextMeta.append(node("span", "", t("detail.compression") + " · " + t("detail.unavailable")));
250
+ contextMeta.append(node("span", "", t("detail.marginalValue") + " · " + t("detail.unavailable")));
251
+ card.append(contextMeta);
252
+ return card;
253
+ }
254
+
255
+ export function dagGraph(dag, t) {
256
+ if (!dag || !dag.nodes || !dag.nodes.length) return emptyRow(t);
257
+ const graph = node("div", "dag-graph");
258
+ const edgeByTarget = {};
259
+ (dag.edges || []).forEach(function (edge) {
260
+ if (!edgeByTarget[edge.to]) edgeByTarget[edge.to] = [];
261
+ edgeByTarget[edge.to].push(edge);
262
+ });
263
+ dag.nodes.forEach(function (item) {
264
+ const row = node("div", "dag-node is-" + item.projectedStatus);
265
+ const top = node("div", "dag-node-head");
266
+ top.append(statusDot(item.projectedStatus), node("strong", "dag-node-title", item.title));
267
+ top.append(pill(t, "dag", item.projectedStatus));
268
+ if ((dag.readyIds || []).indexOf(item.id) >= 0) top.append(chip(t("dag.ready"), "is-active"));
269
+ row.append(top);
270
+ const edges = edgeByTarget[item.id] || [];
271
+ if (edges.length) {
272
+ const deps = node("div", "dag-node-deps");
273
+ deps.append(node("small", "", t("dag.dependsOn")));
274
+ edges.forEach(function (edge) {
275
+ deps.append(chip(edge.from + " · " + t("dag.edge." + edge.status), "is-" + edge.status));
276
+ });
277
+ row.append(deps);
278
+ }
279
+ if (item.rootCauseIds && item.rootCauseIds.length) {
280
+ row.append(node("small", "dag-root-cause", t("dag.rootCause") + " · " + item.rootCauseIds.join(" ← ")));
281
+ }
282
+ graph.append(row);
283
+ });
284
+ return graph;
285
+ }
286
+
205
287
  // --- WorkItem Candidates -----------------------------------------------------
206
288
  export function candidateList(candidates, t, locale) {
207
289
  if (!candidates || !candidates.length) return null;
@@ -473,6 +555,40 @@ export function workItemCard(item, titles, t, locale, actions, taskId) {
473
555
  body.append(executionGroupCard(item.currentExecution, t, locale));
474
556
  }
475
557
 
558
+ if (item.observability) {
559
+ const observability = item.observability;
560
+ if (observability.executionGroups && observability.executionGroups.length) {
561
+ observability.executionGroups.forEach(function (group) {
562
+ if (!item.currentExecution || group.groupId !== item.currentExecution.groupId) {
563
+ body.append(executionGroupCard(group, t, locale));
564
+ }
565
+ });
566
+ }
567
+ const metrics = node("div", "record-meta work-item-observability");
568
+ metrics.append(node("span", "", t("detail.cost") + " · "
569
+ + observability.cost.tokens + " tokens"));
570
+ metrics.append(node("span", "", observability.cost.toolCalls + " tools"));
571
+ metrics.append(node("span", "", observability.cost.wallClockSeconds + "s"));
572
+ metrics.append(node("span", "", t("detail.contextSnapshots") + " · "
573
+ + observability.context.snapshotCount));
574
+ metrics.append(node("span", "", t("detail.evidence") + " · "
575
+ + observability.evidenceCount));
576
+ if (observability.openFindingCount > 0) {
577
+ metrics.append(chip(t("detail.openFindings") + " · " + observability.openFindingCount, "is-danger"));
578
+ }
579
+ body.append(metrics);
580
+ if (observability.stages && observability.stages.length) {
581
+ const stages = node("div", "chip-row work-item-stages");
582
+ observability.stages.forEach(function (stage) {
583
+ const label = (stage.stage || "single")
584
+ + (stage.round === undefined ? "" : " #" + stage.round)
585
+ + (stage.resolution ? " · " + stage.resolution : "");
586
+ stages.append(chip(label, stage.groupId === observability.currentGroupId ? "is-active" : ""));
587
+ });
588
+ body.append(stages);
589
+ }
590
+ }
591
+
476
592
  // Review candidates submitted for this WorkItem, newest first.
477
593
  const candidates = candidateList(item.candidates, t, locale);
478
594
  if (candidates) body.append(candidates);
@@ -55,6 +55,24 @@ const messages = {
55
55
  "detail.constraints": "Constraints",
56
56
  "detail.decisions": "Decisions",
57
57
  "detail.dependsOn": "Depends on",
58
+ "detail.dag": "Task DAG",
59
+ "detail.stage": "Stage",
60
+ "detail.round": "Round",
61
+ "detail.stageAttempt": "Attempt",
62
+ "detail.cost": "Cost",
63
+ "detail.tokens": "Tokens",
64
+ "detail.toolCalls": "Tool calls",
65
+ "detail.wallClock": "Wall clock",
66
+ "detail.ready": "Ready",
67
+ "detail.quorum": "Quorum",
68
+ "detail.contextSnapshots": "Context snapshots",
69
+ "detail.contextPeak": "Context peak",
70
+ "detail.contextBytes": "Context bytes",
71
+ "detail.compression": "Compression",
72
+ "detail.marginalValue": "Marginal value",
73
+ "detail.partial": "partial",
74
+ "detail.unavailable": "unavailable",
75
+ "detail.openFindings": "Open findings",
58
76
  "detail.desired": "Desired launch",
59
77
  "detail.desiredAgent": "Desired Agent",
60
78
  "detail.due": "Due",
@@ -241,6 +259,7 @@ const messages = {
241
259
  "exec.owner.none": "—",
242
260
  "exec.action.advance-task": "Advance task",
243
261
  "exec.action.wait-for-agents": "Wait for agents",
262
+ "exec.action.recover-execution": "Recover execution",
244
263
  "exec.action.answer-input": "Answer input",
245
264
  "exec.action.inspect-attention": "Inspect attention",
246
265
  "exec.action.recover-leader": "Recover leader",
@@ -270,6 +289,20 @@ const messages = {
270
289
  "lane.yielded": "Yielded",
271
290
  "lane.completed": "Completed",
272
291
  "lane.failed": "Failed",
292
+ "lane.skipped": "Skipped",
293
+ "dag.ready": "Ready",
294
+ "dag.dependsOn": "Dependencies",
295
+ "dag.rootCause": "Root cause",
296
+ "dag.blocked": "Blocked",
297
+ "dag.running": "Running",
298
+ "dag.awaiting_acceptance": "Awaiting acceptance",
299
+ "dag.completed": "Completed",
300
+ "dag.failed": "Failed",
301
+ "dag.retired": "Retired",
302
+ "dag.edge.satisfied": "satisfied",
303
+ "dag.edge.active": "active",
304
+ "dag.edge.failed-open": "failed",
305
+ "dag.edge.dead": "missing",
273
306
  "resolution.accept": "Accepted",
274
307
  "resolution.reject": "Rejected",
275
308
  "resolution.retry": "Retry",
@@ -358,6 +391,24 @@ const messages = {
358
391
  "detail.constraints": "约束",
359
392
  "detail.decisions": "决策",
360
393
  "detail.dependsOn": "依赖",
394
+ "detail.dag": "任务 DAG",
395
+ "detail.stage": "阶段",
396
+ "detail.round": "轮次",
397
+ "detail.stageAttempt": "阶段尝试",
398
+ "detail.cost": "成本",
399
+ "detail.tokens": "Token",
400
+ "detail.toolCalls": "工具调用",
401
+ "detail.wallClock": "墙钟时间",
402
+ "detail.ready": "可就绪",
403
+ "detail.quorum": "法定数量",
404
+ "detail.contextSnapshots": "上下文快照",
405
+ "detail.contextPeak": "上下文峰值",
406
+ "detail.contextBytes": "上下文字节",
407
+ "detail.compression": "压缩",
408
+ "detail.marginalValue": "边际价值",
409
+ "detail.partial": "部分可见",
410
+ "detail.unavailable": "不可用",
411
+ "detail.openFindings": "未解决发现",
361
412
  "detail.desired": "期望启动配置",
362
413
  "detail.desiredAgent": "期望 Agent",
363
414
  "detail.due": "截止时间",
@@ -544,6 +595,7 @@ const messages = {
544
595
  "exec.owner.none": "—",
545
596
  "exec.action.advance-task": "推进任务",
546
597
  "exec.action.wait-for-agents": "等待智能体",
598
+ "exec.action.recover-execution": "恢复执行",
547
599
  "exec.action.answer-input": "回答输入",
548
600
  "exec.action.inspect-attention": "检查注意项",
549
601
  "exec.action.recover-leader": "恢复负责人",
@@ -573,6 +625,20 @@ const messages = {
573
625
  "lane.yielded": "已交付",
574
626
  "lane.completed": "已完成",
575
627
  "lane.failed": "失败",
628
+ "lane.skipped": "已跳过",
629
+ "dag.ready": "可就绪",
630
+ "dag.dependsOn": "依赖",
631
+ "dag.rootCause": "根因",
632
+ "dag.blocked": "阻塞",
633
+ "dag.running": "运行中",
634
+ "dag.awaiting_acceptance": "等待接受",
635
+ "dag.completed": "已完成",
636
+ "dag.failed": "失败",
637
+ "dag.retired": "已退役",
638
+ "dag.edge.satisfied": "已满足",
639
+ "dag.edge.active": "进行中",
640
+ "dag.edge.failed-open": "失败传播",
641
+ "dag.edge.dead": "缺失",
576
642
  "resolution.accept": "已接受",
577
643
  "resolution.reject": "已拒绝",
578
644
  "resolution.retry": "已重试",