@tea-agent/loop-agent 0.39.0-beta.4 → 0.39.0-beta.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 (116) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi-executor.js +167 -31
  4. package/dist/executors/pi-playwright-cli-tool.js +23 -16
  5. package/dist/executors/shell-executor.js +25 -8
  6. package/dist/shared/dag-failure-category.js +138 -0
  7. package/dist/task/config-types.js +6 -0
  8. package/dist/task/source-references.js +22 -1
  9. package/dist/worker/console/chat/operation-card.js +8 -1
  10. package/dist/worker/console/chat/routes.js +5 -3
  11. package/dist/worker/console/inspect-split.js +18 -0
  12. package/dist/worker/console/operation-run-facts.js +19 -3
  13. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-zsNyvGaH.js → abnfDiagram-N423BO3Z-CfDSkLtp.js} +1 -1
  14. package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BlefM6I5.js} +1 -1
  15. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-C4tHfzHn.js} +1 -1
  16. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-G-G77Rh1.js} +1 -1
  17. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-DWgxmY72.js} +1 -1
  18. package/dist/worker/console/static/assets/channel-DsdX3ZUX.js +1 -0
  19. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-CJQcfbCY.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-vUPviH9y.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-DzRXSTeh.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-CWfgqm07.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-BtTDauXA.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-Bc-NNt84.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-B_f-emL4.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-DV-jgJ66.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-DMqzn4RZ.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-CVNtlAm4.js} +1 -1
  29. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CrvCyJ-8.js +1 -0
  30. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CrvCyJ-8.js +1 -0
  31. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BBZBhVTd.js} +1 -1
  32. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-DbMU2PZ2.js} +1 -1
  33. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Dj0v8rjc.js} +1 -1
  34. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-BW8R4WvJ.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-DEgWz3ld.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-BAA01rlR.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Cau6qqpW.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-8HiPsLos.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-CmkXuTYL.js} +1 -1
  40. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DCV_0seW.js} +1 -1
  41. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-gckfWESj.js} +1 -1
  42. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-D2mT2BUu.js} +1 -1
  43. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-B_HR5qEw.js} +1 -1
  44. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-BUw4fpSa.js} +1 -1
  45. package/dist/worker/console/static/assets/{index-D5zGX6fG.js → index-DPbsruV5.js} +81 -79
  46. package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
  47. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BSRAqEj0.js} +1 -1
  48. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-CLxE8hUp.js} +1 -1
  49. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-juFzVXIj.js} +1 -1
  50. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-CwLHVhOU.js} +1 -1
  51. package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-9LfUyPAs.js} +1 -1
  52. package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-TPPHfQbe.js} +5 -5
  53. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-Dn_Z9RxF.js} +1 -1
  54. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CiEMKTgm.js} +1 -1
  55. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-JASaGxjZ.js} +1 -1
  56. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-ChmH0ply.js} +1 -1
  57. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-CtyM-HGQ.js} +1 -1
  58. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-BN9y_Bvs.js} +1 -1
  59. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-IyS0GQ1-.js} +1 -1
  60. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DjxGXurN.js} +1 -1
  61. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-Cr3Z0S8y.js} +1 -1
  62. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-BjJFC6pW.js} +1 -1
  63. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bq44O9Nt.js +1 -0
  64. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-DeMUJQiW.js} +2 -2
  65. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BT4tWyW3.js +8 -0
  66. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-Bc2Rv2Oo.js} +1 -1
  67. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-CDhRUeqG.js} +1 -1
  68. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DaN_z3Xy.js} +1 -1
  69. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-C1vTLBDG.js} +1 -1
  70. package/dist/worker/console/static/index.html +2 -2
  71. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  72. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  73. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  74. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  75. package/dist/worker/console/static-src/operator-chat/turn-submission.js +13 -1
  76. package/dist/worker/console/static-src/operator-chat/useChatStream.js +29 -9
  77. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  78. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  79. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  80. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  81. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  82. package/dist/worker/observability/read-model.js +22 -0
  83. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  84. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  85. package/dist/workflows/dag/failure-category.js +7 -128
  86. package/dist/workflows/dag/frontend-design-policy.js +12 -7
  87. package/dist/workflows/dag/frontend-implementation-contract.js +10 -5
  88. package/dist/workflows/dag/frontend-plan-render.js +0 -3
  89. package/dist/workflows/dag/frontend-shadow-dual-write.js +51 -10
  90. package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
  91. package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
  92. package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
  93. package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
  94. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  95. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  96. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  97. package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
  98. package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
  99. package/dist/workflows/dag/frontend-typed-event-store.js +13 -0
  100. package/dist/workflows/dag/init-hybrid.js +66 -19
  101. package/dist/workflows/dag/node-execution.js +21 -1
  102. package/dist/workflows/dag/retry-policy.js +32 -0
  103. package/dist/workflows/dag/types.js +12 -0
  104. package/dist/workflows/dag/validate.js +23 -19
  105. package/docs/templates/frontend-test-case-checklist.md +2 -2
  106. package/package.json +1 -1
  107. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  108. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  109. package/skills/playwright-cli/SKILL.md +1 -1
  110. package/skills/playwright-cli-case-generator/SKILL.md +10 -8
  111. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +0 -1
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +0 -1
  114. package/dist/worker/console/static/assets/index-DBbhESQ_.css +0 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +0 -1
  116. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +0 -8
@@ -58,7 +58,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
58
58
  export function useChatStream(params) {
59
59
  const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
60
60
  const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
61
- const { setInput, setPendingImages } = composer;
61
+ const { setInput, setPendingImages, recordHistory } = composer;
62
62
  const showNotice = useCallback((message) => {
63
63
  if (!setNotice)
64
64
  return;
@@ -393,7 +393,7 @@ export function useChatStream(params) {
393
393
  setInput("");
394
394
  setPendingImages([]);
395
395
  setError(null);
396
- showNotice(existing ? "已更新停止后发送的消息" : "已排队,将在上一轮停止后自动发送");
396
+ showNotice(existing ? "已更新排队消息" : "已排队,将在当前回合结束后自动发送");
397
397
  };
398
398
  const acceptDispatched = () => {
399
399
  setDeferredTurn(null);
@@ -430,7 +430,7 @@ export function useChatStream(params) {
430
430
  }
431
431
  if (body.deferredTurn)
432
432
  setDeferredTurn(body.deferredTurn);
433
- if (body.error?.code !== "TURN_NOT_STOPPING") {
433
+ if (body.error?.code !== "TURN_NOT_ACTIVE") {
434
434
  setError(body.error?.message ??
435
435
  `待发送消息未被服务端接受(HTTP ${response.status})`);
436
436
  return { action: "handled" };
@@ -495,7 +495,9 @@ export function useChatStream(params) {
495
495
  showNotice,
496
496
  fetchSessionState,
497
497
  ]);
498
- const cancelDeferredTurn = useCallback(async () => {
498
+ /** DELETE the queued turn; "restore" moves its text back into the composer,
499
+ * "discard" drops it entirely (2026-08-23 queue chip actions). */
500
+ const requestDeferredTurnDelete = useCallback(async (mode) => {
499
501
  const activeSession = refs.sessionRef.current;
500
502
  const deferred = thread.deferredTurn;
501
503
  if (!activeSession || !deferred)
@@ -519,10 +521,15 @@ export function useChatStream(params) {
519
521
  return;
520
522
  }
521
523
  setDeferredTurn(null);
522
- setInput(body.deferredTurn?.text ?? "");
523
- setPendingImages(body.deferredTurn?.images ?? []);
524
+ if (mode === "restore") {
525
+ setInput(body.deferredTurn?.text ?? "");
526
+ setPendingImages(body.deferredTurn?.images ?? []);
527
+ showNotice("待发送消息已移回输入框");
528
+ }
529
+ else {
530
+ showNotice("已删除排队消息");
531
+ }
524
532
  setError(null);
525
- showNotice("待发送消息已移回输入框");
526
533
  }
527
534
  catch (error) {
528
535
  setError(error instanceof Error ? error.message : String(error));
@@ -537,6 +544,8 @@ export function useChatStream(params) {
537
544
  setError,
538
545
  showNotice,
539
546
  ]);
547
+ const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
548
+ const discardDeferredTurn = useCallback(() => requestDeferredTurnDelete("discard"), [requestDeferredTurnDelete]);
540
549
  const sendPrompt = useCallback(async (text, images) => {
541
550
  const initialSession = refs.sessionRef.current;
542
551
  if (!initialSession || (!text.trim() && images.length === 0))
@@ -548,10 +557,14 @@ export function useChatStream(params) {
548
557
  });
549
558
  const clientRequestId = requestId("chat-submit");
550
559
  if (route.action === "hold") {
551
- if (turnPhaseFromOwnership(ownership) === "stopping")
560
+ const phase = turnPhaseFromOwnership(ownership);
561
+ if (phase === "stopping")
552
562
  showNotice("正在确认上一轮停止状态,当前消息已保留");
563
+ else if (phase === "running")
564
+ showNotice("正在确认当前回合状态,当前消息已保留");
553
565
  return;
554
566
  }
567
+ recordHistory?.(text);
555
568
  if (route.action === "queue-deferred") {
556
569
  const deferred = await queueDeferredTurn({
557
570
  text,
@@ -721,6 +734,7 @@ export function useChatStream(params) {
721
734
  setTurnPhase,
722
735
  setStreamingAssistantId,
723
736
  queueDeferredTurn,
737
+ recordHistory,
724
738
  ]);
725
739
  const handleStop = useCallback(async () => {
726
740
  // AC-R5-001: durable stopRequested is set synchronously before any await so
@@ -969,5 +983,11 @@ export function useChatStream(params) {
969
983
  }
970
984
  };
971
985
  }, [session, recoveryVersion, refs, startEventsStream]);
972
- return { sendPrompt, handleStop, cancelDeferredTurn, openEventsStream };
986
+ return {
987
+ sendPrompt,
988
+ handleStop,
989
+ cancelDeferredTurn,
990
+ discardDeferredTurn,
991
+ openEventsStream,
992
+ };
973
993
  }
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } f
2
2
  import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
3
  import { confirmationToken } from "./format.js";
4
4
  import { clampShortcutIndex } from "./slash-palette-nav.js";
5
+ import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
5
6
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
6
7
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
7
8
  * IME composition guards, auto-grow and server-side draft sync. */
@@ -31,6 +32,8 @@ export function useComposer(params) {
31
32
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
32
33
  const [commandsEpoch, setCommandsEpoch] = useState(0);
33
34
  const textareaRef = useRef(null);
35
+ /** Arrow-key input history; unsent drafts survive history browsing. */
36
+ const historyRef = useRef(createInputHistoryState());
34
37
  /** Active session generation for /commands; bumped immediately on switch (AC-2). */
35
38
  const commandsSessionRef = useRef(sessionId);
36
39
  const commandsGenerationRef = useRef(0);
@@ -38,6 +41,33 @@ export function useComposer(params) {
38
41
  setInput(value);
39
42
  setShortcutOpen(value.startsWith("/"));
40
43
  setShortcutIndex(0);
44
+ // Manual typing edits a recalled entry in place → back to present draft.
45
+ historyRef.current = resetInputHistoryBrowsing(historyRef.current);
46
+ }, []);
47
+ /** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
48
+ const navigateHistory = useCallback((direction) => {
49
+ const textarea = textareaRef.current;
50
+ const decision = navigateInputHistory(historyRef.current, direction, {
51
+ text: input,
52
+ caretStart: textarea?.selectionStart ?? input.length,
53
+ caretEnd: textarea?.selectionEnd ?? input.length,
54
+ });
55
+ if (!decision.handled)
56
+ return false;
57
+ historyRef.current = decision.state;
58
+ setInput(decision.text);
59
+ const caret = decision.text.length;
60
+ queueMicrotask(() => {
61
+ const el = textareaRef.current;
62
+ if (el && el.value === decision.text) {
63
+ el.setSelectionRange(caret, caret);
64
+ }
65
+ });
66
+ return true;
67
+ }, [input, setInput]);
68
+ /** Record a submitted text into history (consecutive duplicates collapse). */
69
+ const recordHistory = useCallback((text) => {
70
+ historyRef.current = recordInputHistoryEntry(historyRef.current, text);
41
71
  }, []);
42
72
  // Immediate invalidation on A→B even when B palette never opens (AC-2).
43
73
  useLayoutEffect(() => {
@@ -234,6 +264,8 @@ export function useComposer(params) {
234
264
  input,
235
265
  setInput,
236
266
  handleInputChange,
267
+ navigateHistory,
268
+ recordHistory,
237
269
  fileOptions,
238
270
  pendingImages,
239
271
  setPendingImages,
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Pure decision core for the TasksPage run-id resolver (2026-08-23).
3
+ *
4
+ * Extracted from the React effect so the full lifecycle — wizard first run,
5
+ * rerun dispatch, task switch, foreign shared selection, baseline-gated
6
+ * lookup — is unit-testable as state transitions without mounting React.
7
+ */
8
+ export function initialRunResolutionState() {
9
+ return { baseline: null, ownedRunId: "", pendingSelection: "" };
10
+ }
11
+ export function observeRunResolution(state, input) {
12
+ const firstObservation = state.task === undefined;
13
+ const taskChanged = !firstObservation && state.task !== input.task;
14
+ const dispatchChanged = !firstObservation && state.dispatch !== input.dispatchId;
15
+ const next = {
16
+ ...state,
17
+ task: input.task,
18
+ dispatch: input.dispatchId,
19
+ pendingSelection: "",
20
+ };
21
+ if (taskChanged) {
22
+ next.baseline = null;
23
+ next.ownedRunId = "";
24
+ return { state: next, observation: { action: "reset-selection" } };
25
+ }
26
+ if (dispatchChanged) {
27
+ next.ownedRunId = "";
28
+ if (input.selectedRunId) {
29
+ next.baseline = input.selectedRunId;
30
+ return { state: next, observation: { action: "invalidate-selection" } };
31
+ }
32
+ // Nothing shown yet: keep the existing baseline and keep probing.
33
+ }
34
+ const selected = input.selectedRunId;
35
+ if (selected && selected !== next.ownedRunId) {
36
+ next.pendingSelection = selected;
37
+ return { state: next, observation: { action: "verify-ownership" } };
38
+ }
39
+ return {
40
+ state: next,
41
+ observation: { action: selected ? "idle" : "probe" },
42
+ };
43
+ }
44
+ export function markBaselineUnknown(state) {
45
+ return { ...state, baseline: null };
46
+ }
47
+ export function applyBaselineSnapshot(state, dagRunId) {
48
+ return { ...state, baseline: dagRunId ?? "" };
49
+ }
50
+ /** Accept an observeLink lookup: only a run of this task that differs from
51
+ * the pre-dispatch baseline may be adopted — never the previous run, and
52
+ * never anything while the baseline is still unknown. */
53
+ export function applyRunLookup(state, outcome) {
54
+ if (!outcome.ok || !outcome.dagRunId) {
55
+ return { state, acceptedRunId: null };
56
+ }
57
+ if (state.baseline === null || outcome.dagRunId === state.baseline) {
58
+ return { state, acceptedRunId: null };
59
+ }
60
+ return {
61
+ state: { ...state, ownedRunId: outcome.dagRunId },
62
+ acceptedRunId: outcome.dagRunId,
63
+ };
64
+ }
65
+ /** Apply an authoritative ownership answer for the pending selection.
66
+ * `clearSelection` tells the adapter to drop the shared selection and
67
+ * snapshot a fresh baseline; owning marks it as resolver-owned. */
68
+ export function applyOwnership(state, owns) {
69
+ if (owns) {
70
+ return {
71
+ state: { ...state, ownedRunId: state.pendingSelection, pendingSelection: "" },
72
+ clearSelection: false,
73
+ };
74
+ }
75
+ return {
76
+ state: { ...state, ownedRunId: "", pendingSelection: "", baseline: null },
77
+ clearSelection: true,
78
+ };
79
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Ownership re-check backoff for an unverified shared DAG-run selection
3
+ * (2026-08-23): a transient candidates-lookup failure must neither leave
4
+ * another task's run displayed indefinitely (waiting for a state change
5
+ * that may never come) nor hammer the API.
6
+ */
7
+ const OWNERSHIP_RETRY_DELAYS_MS = [
8
+ 2_000, 2_000, 4_000, 4_000, 8_000, 8_000, 15_000, 15_000, 30_000, 30_000,
9
+ ];
10
+ /** `owns`: true = same task, false = foreign task, null = check unavailable. */
11
+ export function planOwnershipVerification(owns, attempt) {
12
+ if (owns === true)
13
+ return { kind: "own" };
14
+ if (owns === false)
15
+ return { kind: "clear" };
16
+ const delayMs = OWNERSHIP_RETRY_DELAYS_MS[attempt];
17
+ return delayMs === undefined
18
+ ? { kind: "give-up" }
19
+ : { kind: "retry", delayMs };
20
+ }
21
+ /** Total bounded re-verification window before giving up (keeps the last
22
+ * selection; the next state change restarts verification). */
23
+ export const OWNERSHIP_RETRY_WINDOW_MS = OWNERSHIP_RETRY_DELAYS_MS.reduce((total, delay) => total + delay, 0);
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Pure projection from a DAG run detail payload (GET /api/dag-runs/:id) into
3
+ * the structured run progress shown on the wizard RunPanel: node counts,
4
+ * current node + elapsed time, failure pointer, and stall signal. The panel
5
+ * used to show only a raw SSE log tail — node/phase information drowned in
6
+ * log noise during 20-40 minute runs.
7
+ */
8
+ export function classifyRunProgressNodeStatus(raw) {
9
+ const status = (raw ?? "").trim().toLowerCase();
10
+ if (!status)
11
+ return "pending";
12
+ if (status.includes("running") || status === "in_progress" || status === "active") {
13
+ return "running";
14
+ }
15
+ if (status.includes("fail") || status.includes("error") || status === "blocked") {
16
+ return "failed";
17
+ }
18
+ if (status.includes("finish") ||
19
+ status.includes("success") ||
20
+ status === "done" ||
21
+ status === "ok") {
22
+ return "finished";
23
+ }
24
+ if (status.includes("skip"))
25
+ return "skipped";
26
+ return "pending";
27
+ }
28
+ function toProgressNode(node) {
29
+ const nodeId = String(node.nodeId ?? node.id ?? "").trim();
30
+ return {
31
+ nodeId,
32
+ label: String((node.label ?? node.name ?? nodeId) || "未命名节点"),
33
+ status: String(node.status ?? ""),
34
+ rank: typeof node.rank === "string" ? node.rank : undefined,
35
+ startedAt: typeof node.startedAt === "string" ? node.startedAt : undefined,
36
+ durationMs: typeof node.durationMs === "number" ? node.durationMs : undefined,
37
+ errorPreview: typeof node.errorPreview === "string" ? node.errorPreview : undefined,
38
+ };
39
+ }
40
+ function elapsedSince(iso, now) {
41
+ if (!iso)
42
+ return null;
43
+ const started = Date.parse(iso);
44
+ if (Number.isNaN(started))
45
+ return null;
46
+ const elapsed = now - started;
47
+ return elapsed >= 0 ? elapsed : null;
48
+ }
49
+ export function summarizeRunProgress(dag, now = Date.now()) {
50
+ if (!dag) {
51
+ return {
52
+ total: 0,
53
+ finished: 0,
54
+ running: null,
55
+ failed: null,
56
+ terminal: false,
57
+ effectiveStatus: null,
58
+ stalled: false,
59
+ currentNodeElapsedMs: null,
60
+ runElapsedMs: null,
61
+ };
62
+ }
63
+ const rawNodes = Array.isArray(dag.nodes) ? dag.nodes : [];
64
+ const nodes = rawNodes.map(toProgressNode);
65
+ const classes = nodes.map((node) => classifyRunProgressNodeStatus(node.status));
66
+ const finished = classes.filter((cls) => cls === "finished" || cls === "skipped").length;
67
+ const runningIndex = classes.indexOf("running");
68
+ const running = runningIndex >= 0 ? nodes[runningIndex] : null;
69
+ const failedIndex = classes.indexOf("failed");
70
+ const failed = failedIndex >= 0 ? nodes[failedIndex] : null;
71
+ const effectiveStatus = String(dag.effectiveStatus ??
72
+ dag.status ??
73
+ "").toLowerCase();
74
+ const terminal = TERMINAL_RUN_PROGRESS_STATUSES.has(effectiveStatus);
75
+ const runStartedAt = typeof dag.startedAt === "string" ? dag.startedAt : undefined;
76
+ const runDurationMs = typeof dag.durationMs === "number" ? dag.durationMs : undefined;
77
+ return {
78
+ total: nodes.length,
79
+ finished,
80
+ running,
81
+ failed,
82
+ terminal,
83
+ effectiveStatus: effectiveStatus || null,
84
+ stalled: effectiveStatus === "running-suspected-stall",
85
+ currentNodeElapsedMs: running?.durationMs ?? elapsedSince(running?.startedAt, now),
86
+ runElapsedMs: terminal
87
+ ? (runDurationMs ?? elapsedSince(runStartedAt, now))
88
+ : elapsedSince(runStartedAt, now),
89
+ };
90
+ }
91
+ const TERMINAL_RUN_PROGRESS_STATUSES = new Set([
92
+ "finished",
93
+ "failed",
94
+ "partial_failed",
95
+ "superseded",
96
+ "abandoned",
97
+ ]);
98
+ export function formatDurationMs(ms) {
99
+ if (ms == null || !Number.isFinite(ms) || ms < 0)
100
+ return "—";
101
+ const totalSeconds = Math.floor(ms / 1000);
102
+ const hours = Math.floor(totalSeconds / 3600);
103
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
104
+ const seconds = totalSeconds % 60;
105
+ if (hours > 0)
106
+ return `${hours} 时 ${minutes} 分`;
107
+ if (minutes > 0)
108
+ return `${minutes} 分 ${seconds} 秒`;
109
+ return `${seconds} 秒`;
110
+ }
@@ -129,11 +129,14 @@ export async function resolveRevisionDigestsForTaskSpec(input) {
129
129
  };
130
130
  }
131
131
  /** Union TaskSpec allowed_paths with workflow-owned write roots. */
132
- export function expandAllowedPathsForWorkflow(workflow, allowedPaths) {
132
+ export function expandAllowedPathsForWorkflow(workflow, allowedPaths, options) {
133
133
  const extras = workflow === "backend-test"
134
134
  ? ["testcase/**", "docs/test-reports/**"]
135
135
  : workflow === "frontend-test"
136
- ? ["testcase/frontend/**", "docs/test-reports/**"]
136
+ ? [
137
+ `${options?.frontendTestRoot ?? "testcase/frontend"}/**`,
138
+ "docs/test-reports/**",
139
+ ]
137
140
  : [];
138
141
  return Array.from(new Set([...allowedPaths, ...extras]));
139
142
  }
@@ -1483,6 +1483,9 @@ function mergeDagRun(existing, incoming) {
1483
1483
  computeDurationMs(startedAt, finishedAt);
1484
1484
  return {
1485
1485
  dagRunId: incoming.dagRunId,
1486
+ ...((incoming.taskId ?? existing.taskId)
1487
+ ? { taskId: incoming.taskId ?? existing.taskId }
1488
+ : {}),
1486
1489
  status: incoming.status ?? existing.status,
1487
1490
  lifecycle: incoming.lifecycle ?? existing.lifecycle,
1488
1491
  executionMode: incoming.executionMode ?? existing.executionMode,
@@ -1914,8 +1917,10 @@ async function parseDagStateFile(statePath, now) {
1914
1917
  }
1915
1918
  : undefined;
1916
1919
  const failureCategory = readString(parsed, "failureCategory");
1920
+ const boundTaskId = await readRunSpecTaskBinding(runSpecPath);
1917
1921
  return {
1918
1922
  dagRunId,
1923
+ ...(boundTaskId ? { taskId: boundTaskId } : {}),
1919
1924
  status,
1920
1925
  ...(lifecycle ? { lifecycle } : {}),
1921
1926
  ...(executionMode ? { executionMode } : {}),
@@ -2435,6 +2440,23 @@ async function safeReadJson(filePath) {
2435
2440
  return undefined;
2436
2441
  return parsed;
2437
2442
  }
2443
+ /** Task binding for a DAG run from run.json — same precedence as
2444
+ * console/resolve-dag-run-for-task (taskContractBinding → sourceBinding →
2445
+ * Hybrid DAG title). */
2446
+ async function readRunSpecTaskBinding(runSpecPath) {
2447
+ const parsed = await safeReadJson(runSpecPath);
2448
+ if (!parsed)
2449
+ return undefined;
2450
+ const binding = parsed.taskContractBinding;
2451
+ const source = parsed.sourceBinding;
2452
+ const fromBinding = (typeof binding?.taskId === "string" && binding.taskId.trim()) ||
2453
+ (typeof source?.taskId === "string" && source.taskId.trim());
2454
+ if (fromBinding)
2455
+ return fromBinding;
2456
+ const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
2457
+ const hybrid = /^Hybrid DAG:\s*(.+)$/i.exec(title);
2458
+ return hybrid?.[1]?.trim() || undefined;
2459
+ }
2438
2460
  function safeParseJson(raw) {
2439
2461
  try {
2440
2462
  const parsed = JSON.parse(raw);
@@ -76,7 +76,7 @@ export async function executeDynamicLoopUntil(input) {
76
76
  bodyIdMap,
77
77
  }));
78
78
  for (const child of children) {
79
- assertValidMaterializedDagTask(child);
79
+ assertValidMaterializedDagTask(child, input.spec);
80
80
  if (!input.tasksById.has(child.id)) {
81
81
  input.tasksById.set(child.id, child);
82
82
  input.spec.tasks.push(child);
@@ -170,7 +170,7 @@ export async function executeDynamicMapExpansion(input) {
170
170
  nodeId,
171
171
  }));
172
172
  for (const child of children) {
173
- assertValidMaterializedDagTask(child);
173
+ assertValidMaterializedDagTask(child, input.spec);
174
174
  }
175
175
  const writeSetConflicts = collectDynamicChildWriteSetConflicts(children);
176
176
  if (writeSetConflicts.length > 0) {
@@ -1,130 +1,9 @@
1
1
  import { z } from "zod";
2
- export const dagNormalizedFailureCategorySchema = z.enum([
3
- "success",
4
- "validation",
5
- "executor",
6
- "write-guard",
7
- "timeout",
8
- "auth",
9
- "human-required",
10
- "human-rejected",
11
- "shell-command",
12
- "static-error",
13
- "decision-envelope",
14
- "skipped",
15
- "unknown",
16
- ]);
2
+ import { DAG_NORMALIZED_FAILURE_CATEGORY_VALUES } from "../../shared/dag-failure-category.js";
3
+ // The pure classification core lives in src/shared/dag-failure-category.ts so
4
+ // the Operator Console projection can normalize raw CLI categories without
5
+ // importing the workflows kernel (console/workflows boundary test). This
6
+ // module keeps the schema surface for the DAG kernel's existing consumers.
7
+ export { isDagFailureCategory, normalizeDagFailureCategory, } from "../../shared/dag-failure-category.js";
8
+ export const dagNormalizedFailureCategorySchema = z.enum(DAG_NORMALIZED_FAILURE_CATEGORY_VALUES);
17
9
  export const DAG_NORMALIZED_FAILURE_CATEGORIES = dagNormalizedFailureCategorySchema.options;
18
- const RAW_TO_NORMALIZED = {
19
- success: "success",
20
- "write-guard": "write-guard",
21
- timeout: "timeout",
22
- // Clean implementer stall with zero writes — still a timeout class for report,
23
- // but retryable under WRITER_TRANSPORT_RETRY_POLICY.
24
- "writer-clean-timeout": "timeout",
25
- "missing-api-key": "auth",
26
- auth: "auth",
27
- "human-rejected": "human-rejected",
28
- "human-required": "human-required",
29
- "decision-envelope-invalid": "decision-envelope",
30
- "nonzero-exit": "shell-command",
31
- "spawn-error": "shell-command",
32
- "test-timeout": "timeout",
33
- "static-error": "static-error",
34
- "invalid-output": "validation",
35
- "structured-output-truncated": "validation",
36
- "output-too-large": "validation",
37
- "structured-repair-exhausted": "validation",
38
- "governance-blocked": "validation",
39
- "path-violation": "validation",
40
- "empty-output": "validation",
41
- "verify-failure": "validation",
42
- "test-failure": "validation",
43
- "verification-plan-stale": "validation",
44
- network: "executor",
45
- quota: "executor",
46
- "rate-limit": "executor",
47
- unavailable: "executor",
48
- // Deterministic capability/policy refusal: missing verified launcher JS
49
- // entry, capability allowlist rejection, or missing bounded write tool
50
- // profile. These are environment/config defects the operator repairs
51
- // before rerunning; classify as executor so recovery recommends
52
- // rerun-after-fix WITHOUT forcing a human-decision gate on
53
- // standaloneTaskRerun (run-failed same-task rerun).
54
- "tool-policy": "executor",
55
- unknown: "unknown",
56
- };
57
- function isFinishedStatus(status) {
58
- return status === "FINISHED" || status === "finished";
59
- }
60
- function isInProgressStatus(status) {
61
- return (status === "PENDING" ||
62
- status === "RUNNING" ||
63
- status === "pending" ||
64
- status === "running");
65
- }
66
- function isFailureStatus(status) {
67
- return (status === "ERROR" ||
68
- status === "failed" ||
69
- status === "partial_failed");
70
- }
71
- function mapRawCategory(raw) {
72
- const direct = RAW_TO_NORMALIZED[raw];
73
- if (direct)
74
- return direct;
75
- const lower = raw.toLowerCase();
76
- if (lower.includes("timeout"))
77
- return "timeout";
78
- if (lower.includes("validation") ||
79
- lower.includes("invalid") ||
80
- lower.includes("verification-plan-stale") ||
81
- lower.includes("plan-stale"))
82
- return "validation";
83
- if (lower.includes("auth") || lower.includes("api-key"))
84
- return "auth";
85
- if (lower.includes("write-guard") || lower.includes("write_guard"))
86
- return "write-guard";
87
- if (lower.includes("decision-envelope") || lower.includes("decision_envelope"))
88
- return "decision-envelope";
89
- if (lower.includes("human-rejected"))
90
- return "human-rejected";
91
- if (lower.includes("human-required"))
92
- return "human-required";
93
- if (lower.includes("static-error"))
94
- return "static-error";
95
- if (lower.includes("nonzero-exit") ||
96
- lower.includes("spawn-error") ||
97
- lower.includes("shell"))
98
- return "shell-command";
99
- return undefined;
100
- }
101
- export function isDagFailureCategory(value) {
102
- return DAG_NORMALIZED_FAILURE_CATEGORIES.includes(value);
103
- }
104
- export function normalizeDagFailureCategory(raw, status) {
105
- if (status === "SKIPPED") {
106
- return "skipped";
107
- }
108
- if (raw !== undefined) {
109
- const mapped = mapRawCategory(raw);
110
- if (mapped) {
111
- if (mapped === "success") {
112
- return isFinishedStatus(status) ? "success" : "unknown";
113
- }
114
- return mapped;
115
- }
116
- }
117
- if (raw === undefined && isFinishedStatus(status)) {
118
- return "success";
119
- }
120
- if (isInProgressStatus(status)) {
121
- return "unknown";
122
- }
123
- if (isFailureStatus(status)) {
124
- return "unknown";
125
- }
126
- if (raw === "success" && isFinishedStatus(status)) {
127
- return "success";
128
- }
129
- return "unknown";
130
- }
@@ -26,6 +26,11 @@ export function checkUiDesignCoverage(contract) {
26
26
  const findings = [];
27
27
  const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
28
28
  const hasStyling = Boolean(contract.stylingStrategy?.trim());
29
+ // Reuse of an existing component/pattern covers behavioural interactions
30
+ // without forcing one uiComponentChoices row per interaction: the model
31
+ // should not enumerate every click/hover on a reused Button. Only
32
+ // interactions with no reuse and no explicit purpose are flagged.
33
+ const hasReuseChoice = (contract.uiComponentChoices ?? []).some((choice) => choice.decision === "reuse-existing");
29
34
  for (const state of contract.uiStates) {
30
35
  if (!state.applicable)
31
36
  continue;
@@ -39,13 +44,13 @@ export function checkUiDesignCoverage(contract) {
39
44
  }
40
45
  }
41
46
  for (const interaction of contract.interactions) {
42
- if (!purposes.has(interaction.name)) {
43
- findings.push({
44
- code: "ui-design-coverage-missing",
45
- message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose`,
46
- path: interaction.name,
47
- });
48
- }
47
+ if (purposes.has(interaction.name) || hasReuseChoice)
48
+ continue;
49
+ findings.push({
50
+ code: "ui-design-coverage-missing",
51
+ message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose and no reuse-existing choice covering it`,
52
+ path: interaction.name,
53
+ });
49
54
  }
50
55
  return findings;
51
56
  }