@tea-agent/loop-agent 0.38.3 → 0.38.4

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 (140) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +153 -96
  3. package/README.md +9 -5
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +16 -5
  6. package/dist/cli/program.js +16 -2
  7. package/dist/cli/update/notifier.js +2 -2
  8. package/dist/cli.js +2 -1
  9. package/dist/commands/dag-rerun.js +55 -1
  10. package/dist/commands/init-model-catalog.js +464 -0
  11. package/dist/commands/init-upgrade.js +265 -97
  12. package/dist/commands/init.js +428 -27
  13. package/dist/commands/inspect-next.js +8 -0
  14. package/dist/executors/dag-pi-executor.js +18 -9
  15. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  16. package/dist/executors/shell-executor.js +209 -13
  17. package/dist/shared/dag-prompt-override.js +28 -0
  18. package/dist/shared/operator/capabilities.js +32 -0
  19. package/dist/shared/playwright-cli-command-policy.js +15 -0
  20. package/dist/shared/update/console-notifier.js +100 -0
  21. package/dist/{cli → shared}/update/npm-client.js +41 -8
  22. package/dist/{cli → shared}/update/state.js +41 -13
  23. package/dist/task/source-prepare/parse-intent.js +24 -4
  24. package/dist/worker/cli.js +31 -1
  25. package/dist/worker/console/chat/routes.js +1 -0
  26. package/dist/worker/console/dag-execution-receipt.js +33 -0
  27. package/dist/worker/console/operator-actions.js +116 -0
  28. package/dist/worker/console/operator-user-error.js +2 -2
  29. package/dist/worker/console/routes.js +43 -2
  30. package/dist/worker/console/security.js +63 -9
  31. package/dist/worker/console/server.js +36 -5
  32. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  33. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-pm3c2Rsl.js} +1 -1
  34. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  35. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  36. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  37. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  38. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  48. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  49. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  50. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  51. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  52. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  53. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  58. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  59. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  60. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  61. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  62. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  63. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  64. package/dist/worker/console/static/assets/{index-Ya5FE7cD.css → index-C8rOIAJr.css} +1 -1
  65. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-DEbyrEfW.js} +71 -71
  66. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  67. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  68. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  69. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  70. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-CAyXjQjN.js} +1 -1
  71. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-CcWKn6tS.js} +5 -5
  72. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  73. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  74. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  75. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  76. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  77. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  78. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  79. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  80. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  81. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  82. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  83. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  84. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  85. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  86. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  87. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  88. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  89. package/dist/worker/console/static/index.html +2 -2
  90. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  91. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  92. package/dist/worker/observe/node-input.js +2 -0
  93. package/dist/worker/observe/node-process.js +377 -0
  94. package/dist/worker/observe/routes.js +36 -3
  95. package/dist/worker/observe/shell-handler-keys.js +34 -0
  96. package/dist/worker/observe/static/api.js +66 -0
  97. package/dist/worker/observe/static/constants.js +8 -1
  98. package/dist/worker/observe/static/format.js +23 -0
  99. package/dist/worker/observe/static/markdown-render.js +20 -1
  100. package/dist/worker/observe/static/operator-chrome.js +4 -0
  101. package/dist/worker/observe/static/state.js +11 -0
  102. package/dist/worker/observe/static/styles.css +73 -0
  103. package/dist/worker/observe/static/views/dag-inspector.js +561 -8
  104. package/dist/worker/observe/static/views/dag.js +8 -1
  105. package/dist/worker/observe/static/views/failures.js +5 -2
  106. package/dist/worker/observe/static/views/session-timeline.js +60 -4
  107. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  108. package/dist/workflows/dag/failure-category.js +8 -0
  109. package/dist/workflows/dag/frontend-implementation-contract.js +5 -1
  110. package/dist/workflows/dag/frontend-repair.js +14 -0
  111. package/dist/workflows/dag/frontend-risk.js +15 -2
  112. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  113. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  114. package/dist/workflows/dag/frontend-test-result-contract.js +151 -11
  115. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  116. package/dist/workflows/dag/init-hybrid.js +125 -24
  117. package/dist/workflows/dag/node-execution.js +20 -2
  118. package/dist/workflows/dag/rerun-plan.js +112 -0
  119. package/dist/workflows/dag/rerun-run.js +58 -6
  120. package/dist/workflows/dag/retry-policy.js +91 -0
  121. package/dist/workflows/dag/types.js +9 -1
  122. package/docs/init-surface.manifest.json +5 -0
  123. package/docs/operations/README.md +1 -1
  124. package/docs/templates/init-managed-agents.md +13 -3
  125. package/harness.json +2 -2
  126. package/package.json +5 -1
  127. package/scripts/next-info.mjs +356 -0
  128. package/scripts/next-publish-gate.mjs +238 -0
  129. package/scripts/release-source-binding.mjs +251 -0
  130. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  131. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  132. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  133. package/skills/loop-agent/references/command-reference.md +11 -4
  134. package/skills/playwright-cli/SKILL.md +22 -0
  135. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  136. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  137. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  138. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  139. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  140. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -11,14 +11,22 @@ import {
11
11
  } from "../constants.js";
12
12
  import { clearNode, el } from "../dom.js";
13
13
  import { attemptFailureLabel } from "../dag-history-labels.js";
14
- import { fetchJson, fetchSpecEvidenceFile, fetchDagNodeInput } from "../api.js";
15
- import { badge, formatMs } from "../format.js";
16
- import { renderMarkdown } from "../markdown-render.js";
14
+ import {
15
+ fetchJson,
16
+ fetchSpecEvidenceFile,
17
+ fetchDagNodeInput,
18
+ fetchDagNodeProcess,
19
+ postOperatorAction,
20
+ fetchOperatorOperation,
21
+ } from "../api.js";
22
+ import { badge, formatMs, prettyJsonBlock } from "../format.js";
23
+ import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
17
24
  import {
18
25
  uiState,
19
26
  makeSessionEventIdentity,
20
27
  makeDagNodeInputIdentity,
21
28
  makeDagNodeExecutionOutputIdentity,
29
+ makeDagNodeProcessIdentity,
22
30
  getDagNodeExecutionOutputCache,
23
31
  setDagNodeExecutionOutputCache,
24
32
  clearDagNodeExecutionOutputCache,
@@ -68,6 +76,10 @@ function resetInspectorNodeSelection(dagRunId, nodeId) {
68
76
  uiState.dagNodeInputData = null;
69
77
  uiState.dagNodeInputError = null;
70
78
  uiState.dagNodeInputLoading = false;
79
+ uiState.dagNodeProcessData = null;
80
+ uiState.dagNodeProcessError = null;
81
+ uiState.dagNodeProcessLoading = false;
82
+ uiState.dagNodeProcessIdentity = null;
71
83
  // Drop execution-output cache/identity so node A's bodies never leak into B.
72
84
  clearDagNodeExecutionOutputCache();
73
85
  // Bump the session-event identity token so any in-flight response for the
@@ -748,6 +760,12 @@ function renderDagNodeInputDetail(content, data) {
748
760
  (shell.gates ?? []).join(", ") || "(none)",
749
761
  null,
750
762
  );
763
+ appendMetaRow(
764
+ shellDl,
765
+ "handlers",
766
+ (shell.handlers ?? []).join(", ") || "(none)",
767
+ null,
768
+ );
751
769
  section.appendChild(shellDl);
752
770
  if ((shell.commands ?? []).length > 0) {
753
771
  const list = el("ol", "node-input-command-list");
@@ -967,8 +985,304 @@ async function renderDagNodeInput(content, dagRunId, node) {
967
985
  finishInputRender();
968
986
  }
969
987
 
988
+ /**
989
+ * Experimental prompt-override rerun tab. Only reachable when the server
990
+ * injected the mutation token (switch on). Every node shows its frozen
991
+ * subtask_prompt here; only FAILED (ERROR) nodes additionally get the editor +
992
+ * restart flow: preflight (dagRerunPlan) binds the edit into planHash, dagRerun
993
+ * executes it as a Console operation, and on success we jump to the new run.
994
+ */
995
+ async function renderPromptRestart(content, dagRunId, node) {
996
+ const nodeId = node.nodeId;
997
+ clearNode(content);
998
+
999
+ const loading = el("p", "muted", "正在加载冻结提示词…");
1000
+ loading.setAttribute("role", "status");
1001
+ loading.setAttribute("aria-live", "polite");
1002
+ content.appendChild(loading);
1003
+ const input = await fetchDagNodeInput(dagRunId, nodeId);
1004
+ if (
1005
+ !content.isConnected ||
1006
+ content.dataset.dagNodeId !== String(nodeId) ||
1007
+ content.dataset.dagRunId !== String(dagRunId) ||
1008
+ activeInspectorTab() !== "prompt-restart"
1009
+ ) {
1010
+ return;
1011
+ }
1012
+ clearNode(content);
1013
+
1014
+ if (!input.ok || !input.body?.available) {
1015
+ const error = el("p", "node-input-error", "无法读取该节点的冻结提示词。");
1016
+ error.setAttribute("role", "alert");
1017
+ content.appendChild(error);
1018
+ return;
1019
+ }
1020
+ const basePrompt =
1021
+ typeof input.body.prompt?.text === "string" ? input.body.prompt.text : "";
1022
+ if (!basePrompt) {
1023
+ const error = el("p", "node-input-error", "该节点没有冻结提示词。");
1024
+ error.setAttribute("role", "alert");
1025
+ content.appendChild(error);
1026
+ return;
1027
+ }
1028
+
1029
+ if (node.status !== "ERROR") {
1030
+ // Non-failed nodes still show their frozen prompt — read-only.
1031
+ const hint = el(
1032
+ "p",
1033
+ "muted",
1034
+ `当前节点状态为 ${node.status ?? "unknown"};仅失败(ERROR)节点支持编辑提示词并重启,以下为该节点冻结提示词(只读)。`,
1035
+ );
1036
+ hint.setAttribute("role", "status");
1037
+ content.appendChild(hint);
1038
+ const pre = document.createElement("pre");
1039
+ pre.className = "prompt-restart-readonly";
1040
+ pre.setAttribute("data-testid", "prompt-restart-readonly");
1041
+ pre.textContent = basePrompt;
1042
+ content.appendChild(pre);
1043
+ return;
1044
+ }
1045
+
1046
+ if (input.body.prompt?.truncated === true) {
1047
+ const error = el(
1048
+ "p",
1049
+ "node-input-error",
1050
+ "冻结提示词被截断,为避免误替换请改用 CLI(dag rerun --prompt-override)。",
1051
+ );
1052
+ error.setAttribute("role", "alert");
1053
+ content.appendChild(error);
1054
+ return;
1055
+ }
1056
+
1057
+ const box = el("div", "prompt-restart-box");
1058
+ box.appendChild(
1059
+ el(
1060
+ "p",
1061
+ "muted",
1062
+ "编辑该失败节点的提示词并从它重启:父 run spec 保持冻结,将创建带修改提示词的续跑 run(实验能力,仅 ERROR 节点)。",
1063
+ ),
1064
+ );
1065
+
1066
+ const modeRow = el("div", "prompt-restart-mode");
1067
+ modeRow.appendChild(el("span", "muted", "编辑模式"));
1068
+ const modeSelect = document.createElement("select");
1069
+ modeSelect.className = "prompt-restart-mode-select";
1070
+ modeSelect.setAttribute("data-testid", "prompt-restart-mode");
1071
+ const appendOption = document.createElement("option");
1072
+ appendOption.value = "append";
1073
+ appendOption.textContent =
1074
+ "追加指令(推荐):保留原提示词,在末尾追加 operator_addendum 块";
1075
+ const replaceOption = document.createElement("option");
1076
+ replaceOption.value = "replace";
1077
+ replaceOption.textContent = "全文替换(高级):替换整个提示词";
1078
+ modeSelect.appendChild(appendOption);
1079
+ modeSelect.appendChild(replaceOption);
1080
+ modeSelect.value = "append";
1081
+ modeRow.appendChild(modeSelect);
1082
+ box.appendChild(modeRow);
1083
+
1084
+ const baseLabel = el("label", "prompt-restart-field");
1085
+ baseLabel.appendChild(
1086
+ el("span", "muted", `冻结提示词(只读底稿 · ${nodeId})`),
1087
+ );
1088
+ const baseBlock = document.createElement("pre");
1089
+ baseBlock.className = "prompt-restart-readonly";
1090
+ baseBlock.setAttribute("data-testid", "prompt-restart-base");
1091
+ baseBlock.textContent = basePrompt;
1092
+ baseLabel.appendChild(baseBlock);
1093
+ box.appendChild(baseLabel);
1094
+
1095
+ const addendumLabel = el("label", "prompt-restart-field");
1096
+ addendumLabel.appendChild(
1097
+ el(
1098
+ "span",
1099
+ "muted",
1100
+ "追加指令(将包裹为 <operator_addendum> 块附加到原提示词末尾)",
1101
+ ),
1102
+ );
1103
+ const addendumArea = document.createElement("textarea");
1104
+ addendumArea.className = "prompt-restart-textarea";
1105
+ addendumArea.rows = 6;
1106
+ addendumArea.placeholder = "例如:重点检查 X 边界;输出前先自检 Y 约束…";
1107
+ addendumArea.setAttribute("data-testid", "prompt-restart-addendum");
1108
+ addendumLabel.appendChild(addendumArea);
1109
+ box.appendChild(addendumLabel);
1110
+
1111
+ const replaceLabel = el("label", "prompt-restart-field");
1112
+ replaceLabel.appendChild(el("span", "muted", `完整提示词(${nodeId})`));
1113
+ const replaceArea = document.createElement("textarea");
1114
+ replaceArea.className = "prompt-restart-textarea";
1115
+ replaceArea.rows = 16;
1116
+ replaceArea.value = basePrompt;
1117
+ replaceArea.setAttribute("data-testid", "prompt-restart-textarea");
1118
+ replaceLabel.appendChild(replaceArea);
1119
+ box.appendChild(replaceLabel);
1120
+
1121
+ const applyModeVisibility = () => {
1122
+ const append = modeSelect.value === "append";
1123
+ baseLabel.style.display = append ? "" : "none";
1124
+ addendumLabel.style.display = append ? "" : "none";
1125
+ replaceLabel.style.display = append ? "none" : "";
1126
+ };
1127
+ modeSelect.addEventListener("change", applyModeVisibility);
1128
+ applyModeVisibility();
1129
+
1130
+ const reasonLabel = el("label", "prompt-restart-field");
1131
+ reasonLabel.appendChild(el("span", "muted", "重启原因(结构化说明)"));
1132
+ const reasonArea = document.createElement("textarea");
1133
+ reasonArea.className = "prompt-restart-textarea";
1134
+ reasonArea.rows = 3;
1135
+ reasonArea.value = `operator-prompt-override / run=${dagRunId} / node=${nodeId} / 手工编辑失败节点提示词后重启`;
1136
+ reasonArea.setAttribute("data-testid", "prompt-restart-reason");
1137
+ reasonLabel.appendChild(reasonArea);
1138
+ box.appendChild(reasonLabel);
1139
+
1140
+ const row = el("div", "prompt-restart-actions");
1141
+ const submit = document.createElement("button");
1142
+ submit.type = "button";
1143
+ submit.className = "prompt-restart-submit";
1144
+ submit.textContent = "资格预检并重启";
1145
+ submit.setAttribute("data-testid", "prompt-restart-submit");
1146
+ row.appendChild(submit);
1147
+ box.appendChild(row);
1148
+
1149
+ const status = el("p", "prompt-restart-status", "");
1150
+ status.setAttribute("role", "status");
1151
+ status.setAttribute("aria-live", "polite");
1152
+ box.appendChild(status);
1153
+
1154
+ content.appendChild(box);
1155
+
1156
+ const setStatus = (text, isError = false) => {
1157
+ status.textContent = text ?? "";
1158
+ status.classList.toggle("prompt-restart-error", isError);
1159
+ };
1160
+
1161
+ submit.addEventListener("click", () => {
1162
+ void (async () => {
1163
+ const mode = modeSelect.value === "replace" ? "replace" : "append";
1164
+ const prompt =
1165
+ mode === "append" ? addendumArea.value : replaceArea.value;
1166
+ const reason = reasonArea.value.trim();
1167
+ if (!prompt.trim()) {
1168
+ setStatus(
1169
+ mode === "append" ? "追加指令不能为空。" : "提示词不能为空。",
1170
+ true,
1171
+ );
1172
+ return;
1173
+ }
1174
+ if (mode === "replace" && prompt === basePrompt) {
1175
+ setStatus("提示词未修改;如需按原提示词重跑请使用恢复工作台。", true);
1176
+ return;
1177
+ }
1178
+ if (!reason) {
1179
+ setStatus("需要填写重启原因。", true);
1180
+ return;
1181
+ }
1182
+ submit.disabled = true;
1183
+ try {
1184
+ setStatus("正在做重跑资格预检(dagRerunPlan)…");
1185
+ const overrides = [{ nodeId, prompt, mode }];
1186
+ const planRes = await postOperatorAction("dagRerunPlan", {
1187
+ runId: dagRunId,
1188
+ fromNode: nodeId,
1189
+ promptOverrides: overrides,
1190
+ });
1191
+ const plan = planRes.body?.result ?? null;
1192
+ if (!planRes.ok || plan?.eligible !== true || !plan?.planHash) {
1193
+ setStatus(
1194
+ planRes.body?.error?.message ??
1195
+ plan?.blockedReasons?.join(", ") ??
1196
+ "资格预检未通过。",
1197
+ true,
1198
+ );
1199
+ return;
1200
+ }
1201
+
1202
+ const est = plan.estimatedExecution;
1203
+ const estText = est
1204
+ ? `(将执行 ${est.nodeCount ?? "?"} 个节点 / ${est.piCallCount ?? "?"} 次 Pi 调用)`
1205
+ : "";
1206
+ setStatus(`预检通过${estText},正在提交续跑(dagRerun)…`);
1207
+ const clientRequestId = `inspect-prompt-restart-${Date.now()}-${Math.random()
1208
+ .toString(36)
1209
+ .slice(2, 10)}`;
1210
+ const runRes = await postOperatorAction(
1211
+ "dagRerun",
1212
+ {
1213
+ runId: dagRunId,
1214
+ fromNode: nodeId,
1215
+ planHash: plan.planHash,
1216
+ reason,
1217
+ promptOverrides: overrides,
1218
+ },
1219
+ clientRequestId,
1220
+ );
1221
+ const operationId = runRes.body?.operationId;
1222
+ if (!runRes.ok || !operationId) {
1223
+ setStatus(runRes.body?.error?.message ?? "提交续跑失败。", true);
1224
+ return;
1225
+ }
1226
+
1227
+ const op = await pollOperationToTerminal(operationId, (text) =>
1228
+ setStatus(text),
1229
+ );
1230
+ if (op?.state === "succeeded") {
1231
+ const newRunId =
1232
+ op.dagRunId ?? op.result?.newRunId ?? op.runSummary?.runId ?? null;
1233
+ if (newRunId) {
1234
+ setStatus(`重启完成,新 run:${newRunId}`);
1235
+ const open = document.createElement("button");
1236
+ open.type = "button";
1237
+ open.className = "prompt-restart-submit";
1238
+ open.textContent = "打开新 run";
1239
+ open.addEventListener("click", () => {
1240
+ window.location.hash = `#/dag/${encodeURIComponent(String(newRunId))}`;
1241
+ });
1242
+ row.appendChild(open);
1243
+ } else {
1244
+ setStatus("重启完成(未能解析新 run id,请在 DAG 历史中查看)。");
1245
+ }
1246
+ } else if (op) {
1247
+ setStatus(
1248
+ `续跑操作终态:${op.state}${op.errorMessage ? ` — ${op.errorMessage}` : ""}`,
1249
+ true,
1250
+ );
1251
+ } else {
1252
+ setStatus("等待超时:操作仍在运行,可在操作台查看进度。", true);
1253
+ }
1254
+ } catch (err) {
1255
+ setStatus(err instanceof Error ? err.message : String(err), true);
1256
+ } finally {
1257
+ submit.disabled = false;
1258
+ }
1259
+ })();
1260
+ });
1261
+ }
1262
+
1263
+ /** Poll a Console operation until a terminal state; null on poll budget end. */
1264
+ async function pollOperationToTerminal(operationId, onProgress) {
1265
+ const terminal = new Set([
1266
+ "succeeded",
1267
+ "failed",
1268
+ "timed-out",
1269
+ "needs-reconcile",
1270
+ ]);
1271
+ for (let attempt = 0; attempt < 1200; attempt += 1) {
1272
+ const res = await fetchOperatorOperation(operationId);
1273
+ const op = res.body?.operation ?? res.body;
1274
+ const state = op?.state ?? "";
1275
+ if (res.ok && terminal.has(state)) {
1276
+ return op;
1277
+ }
1278
+ onProgress(`续跑执行中…(${state || "queued"})`);
1279
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1280
+ }
1281
+ return null;
1282
+ }
1283
+
970
1284
  function activeInspectorTab() {
971
- return ["input", "timeline", "spec-evidence"].includes(
1285
+ return ["input", "timeline", "spec-evidence", "prompt-restart"].includes(
972
1286
  uiState.dagInspectorTab,
973
1287
  )
974
1288
  ? uiState.dagInspectorTab
@@ -1305,7 +1619,7 @@ function appendOutputBody(content, outputText, errorText, options = null) {
1305
1619
  content.appendChild(el("p", "empty", UI_TEXT.noOutput));
1306
1620
  return;
1307
1621
  }
1308
- if (outputText) content.appendChild(renderMarkdown(outputText));
1622
+ if (outputText) content.appendChild(renderInspectorDocument(outputText));
1309
1623
  // Truncation note only for full/cached execution-output bodies (AC-003);
1310
1624
  // preview path never invents truncated/maxBytes.
1311
1625
  if (options?.truncated === true) {
@@ -1321,7 +1635,7 @@ function appendOutputBody(content, outputText, errorText, options = null) {
1321
1635
  if (errorText) {
1322
1636
  const error = el("section", "markdown-error");
1323
1637
  error.appendChild(el("p", "markdown-error-title", "错误输出"));
1324
- error.appendChild(renderMarkdown(errorText));
1638
+ error.appendChild(renderInspectorDocument(errorText));
1325
1639
  content.appendChild(error);
1326
1640
  }
1327
1641
  }
@@ -1455,6 +1769,228 @@ function repaintOutputTabBody(content, dagRunId, node) {
1455
1769
  restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
1456
1770
  }
1457
1771
 
1772
+ function processEmptyMessage(body) {
1773
+ const reason = body?.emptyReason;
1774
+ if (reason === "static-no-process") return UI_TEXT.staticNoProcess;
1775
+ if (reason === "shell-receipt-missing") return UI_TEXT.noShellProcess;
1776
+ if (reason === "shell-preset-pending") return UI_TEXT.noPresetProcess;
1777
+ if (reason === "pi-session") return UI_TEXT.noSessionEvents;
1778
+ return UI_TEXT.noSessionEvents;
1779
+ }
1780
+
1781
+ function formatProcessBlock(text) {
1782
+ if (!text) return "";
1783
+ return prettyJsonBlock(text) ?? text;
1784
+ }
1785
+
1786
+ function appendProcessPre(parent, text) {
1787
+ if (!text) return;
1788
+ const pre = el("pre", "process-timeline-code");
1789
+ pre.textContent = formatProcessBlock(text);
1790
+ parent.appendChild(pre);
1791
+ }
1792
+
1793
+ function renderProcessCommandCard(command) {
1794
+ const failed = command.ok === false;
1795
+ const card = el(
1796
+ "div",
1797
+ `process-timeline-card process-timeline-tool-card${failed ? " is-error" : ""}`,
1798
+ );
1799
+ const details = document.createElement("details");
1800
+ details.className = "process-timeline-details";
1801
+ details.open = failed;
1802
+ const summary = el("summary", "process-timeline-summary");
1803
+ summary.appendChild(
1804
+ el("span", "process-timeline-summary-title", command.command || "command"),
1805
+ );
1806
+ summary.appendChild(
1807
+ el(
1808
+ "span",
1809
+ `process-timeline-summary-status status-${failed ? "failed" : "succeeded"}`,
1810
+ failed ? "失败" : command.reused ? "复用" : "成功",
1811
+ ),
1812
+ );
1813
+ details.appendChild(summary);
1814
+ const body = el("div", "process-timeline-card-body");
1815
+ const meta = [
1816
+ command.exitCode != null ? `exit ${command.exitCode}` : null,
1817
+ command.durationMs != null ? `耗时 ${formatMs(command.durationMs)}` : null,
1818
+ command.failureCategory ? command.failureCategory : null,
1819
+ ].filter(Boolean);
1820
+ if (meta.length > 0) {
1821
+ body.appendChild(el("div", "process-timeline-section-label", meta.join(" · ")));
1822
+ }
1823
+ if (command.stdout) {
1824
+ body.appendChild(el("div", "process-timeline-section-label", "stdout"));
1825
+ appendProcessPre(body, command.stdout);
1826
+ }
1827
+ if (command.stderr) {
1828
+ body.appendChild(el("div", "process-timeline-section-label", "stderr"));
1829
+ appendProcessPre(body, command.stderr);
1830
+ }
1831
+ if (command.truncated) {
1832
+ body.appendChild(el("p", "muted", "内容过长,已截断。"));
1833
+ }
1834
+ details.appendChild(body);
1835
+ card.appendChild(details);
1836
+ return card;
1837
+ }
1838
+
1839
+ function renderProcessStepCard(step) {
1840
+ const failed = step.ok === false;
1841
+ const card = el(
1842
+ "div",
1843
+ `process-timeline-card process-timeline-tool-card${failed ? " is-error" : ""}`,
1844
+ );
1845
+ const details = document.createElement("details");
1846
+ details.className = "process-timeline-details";
1847
+ details.open = true;
1848
+ const summary = el("summary", "process-timeline-summary");
1849
+ summary.appendChild(
1850
+ el("span", "process-timeline-summary-title", step.label || step.id || "step"),
1851
+ );
1852
+ summary.appendChild(
1853
+ el(
1854
+ "span",
1855
+ `process-timeline-summary-status status-${failed ? "failed" : "succeeded"}`,
1856
+ failed ? "失败" : "成功",
1857
+ ),
1858
+ );
1859
+ details.appendChild(summary);
1860
+ const body = el("div", "process-timeline-card-body");
1861
+ if (step.durationMs != null) {
1862
+ body.appendChild(
1863
+ el("div", "process-timeline-section-label", `耗时 ${formatMs(step.durationMs)}`),
1864
+ );
1865
+ }
1866
+ if (step.detail) appendProcessPre(body, step.detail);
1867
+ if (step.truncated) {
1868
+ body.appendChild(el("p", "muted", "内容过长,已截断。"));
1869
+ }
1870
+ details.appendChild(body);
1871
+ card.appendChild(details);
1872
+ return card;
1873
+ }
1874
+
1875
+ function renderNonPiProcess(content, body) {
1876
+ content.classList.add("process-timeline-scroll");
1877
+ if (!body || body.emptyReason || ((body.commands ?? []).length === 0 && (body.steps ?? []).length === 0)) {
1878
+ content.appendChild(el("p", "empty", processEmptyMessage(body)));
1879
+ return;
1880
+ }
1881
+ const heading =
1882
+ body.kind === "shell-preset"
1883
+ ? UI_TEXT.shellPresetHeading
1884
+ : UI_TEXT.shellCommandsHeading;
1885
+ if (body.handler) {
1886
+ content.appendChild(
1887
+ el("p", "process-timeline-note", `${heading} · ${body.handler}`),
1888
+ );
1889
+ }
1890
+ const ul = el("ul", "process-timeline process-timeline-rich");
1891
+ const items = (body.commands ?? []).length > 0 ? body.commands : body.steps ?? [];
1892
+ for (let index = 0; index < items.length; index += 1) {
1893
+ const item = items[index];
1894
+ const failed = item.ok === false;
1895
+ const li = el(
1896
+ "li",
1897
+ `process-timeline-item process-timeline-tool-end${failed ? " is-error" : ""}${index === 0 ? " is-first" : ""}${index === items.length - 1 ? " is-last" : ""}`,
1898
+ );
1899
+ const rail = el("span", "process-timeline-rail");
1900
+ rail.appendChild(el("span", "process-timeline-dot"));
1901
+ li.appendChild(rail);
1902
+ const cardBody = el("div", "process-timeline-body");
1903
+ cardBody.appendChild(
1904
+ body.kind === "shell-preset"
1905
+ ? renderProcessStepCard(item)
1906
+ : renderProcessCommandCard(item),
1907
+ );
1908
+ li.appendChild(cardBody);
1909
+ ul.appendChild(li);
1910
+ }
1911
+ content.appendChild(ul);
1912
+ }
1913
+
1914
+ export async function mergeNodeProcess(dagRunId, node) {
1915
+ if (!dagRunId || !node?.nodeId) return;
1916
+ const nodeId = node.nodeId;
1917
+ const key = effectiveExecutionKey(dagRunId, node);
1918
+ const identity = makeDagNodeProcessIdentity(dagRunId, nodeId, key);
1919
+ uiState.dagNodeProcessIdentity = identity;
1920
+ uiState.dagNodeProcessLoading = true;
1921
+ const result = await fetchDagNodeProcess(dagRunId, nodeId, key);
1922
+ if (uiState.dagNodeProcessIdentity !== identity) return;
1923
+ if (!result.ok || !result.body) {
1924
+ uiState.dagNodeProcessError =
1925
+ result.body?.error || UI_TEXT.processError;
1926
+ uiState.dagNodeProcessData = {
1927
+ schemaVersion: 1,
1928
+ dagRunId,
1929
+ nodeId,
1930
+ kind: "none",
1931
+ executor: node.executor ?? null,
1932
+ handler: null,
1933
+ emptyReason: "not-executed",
1934
+ key: key ?? null,
1935
+ commands: [],
1936
+ steps: [],
1937
+ };
1938
+ uiState.dagNodeProcessLoading = false;
1939
+ return;
1940
+ }
1941
+ uiState.dagNodeProcessError = null;
1942
+ uiState.dagNodeProcessData = result.body;
1943
+ uiState.dagNodeProcessLoading = false;
1944
+ }
1945
+
1946
+ function renderExecutorAwareTimeline(content, dagRunId, node) {
1947
+ content.dataset.dagRunId = String(dagRunId ?? "");
1948
+ content.dataset.nodeId = String(node.nodeId ?? "");
1949
+ const key = effectiveExecutionKey(dagRunId, node);
1950
+ const cached = uiState.dagNodeProcessData;
1951
+ const sameNode =
1952
+ cached &&
1953
+ cached.dagRunId === dagRunId &&
1954
+ cached.nodeId === node.nodeId &&
1955
+ (cached.key ?? null) === (key ?? null);
1956
+ if (sameNode && uiState.dagNodeProcessError) {
1957
+ content.appendChild(el("p", "empty", uiState.dagNodeProcessError));
1958
+ return;
1959
+ }
1960
+ if (sameNode && cached.kind === "pi-session") {
1961
+ renderSessionTimeline(content, node.nodeId, {
1962
+ executionKey: effectiveExecutionKey(dagRunId, node),
1963
+ });
1964
+ return;
1965
+ }
1966
+ if (sameNode && cached.kind !== "pi-session") {
1967
+ renderNonPiProcess(content, cached);
1968
+ return;
1969
+ }
1970
+ if ((node.executor ?? "pi") === "pi") {
1971
+ renderSessionTimeline(content, node.nodeId, {
1972
+ executionKey: effectiveExecutionKey(dagRunId, node),
1973
+ });
1974
+ return;
1975
+ }
1976
+ if (uiState.dagNodeProcessError && sameNode) {
1977
+ content.appendChild(el("p", "empty", uiState.dagNodeProcessError));
1978
+ return;
1979
+ }
1980
+ const loading = el("p", "muted", UI_TEXT.processLoading);
1981
+ loading.setAttribute("role", "status");
1982
+ content.appendChild(loading);
1983
+ void mergeNodeProcess(dagRunId, node).then(() => {
1984
+ if (
1985
+ uiState.dagInspectorTab === "timeline" &&
1986
+ uiState.selectedDagNodeId === node.nodeId &&
1987
+ uiState.currentDagRunId === dagRunId
1988
+ ) {
1989
+ renderDagInspectorForCurrent();
1990
+ }
1991
+ });
1992
+ }
1993
+
1458
1994
  function fillDagInspectorContent(content, dagRunId, node) {
1459
1995
  const previousRenderedNode = content.dataset.dagNodeId ?? "";
1460
1996
  const previousRenderedRun = content.dataset.dagRunId ?? "";
@@ -1501,10 +2037,12 @@ function fillDagInspectorContent(content, dagRunId, node) {
1501
2037
  );
1502
2038
  if (activeTab === "spec-evidence") {
1503
2039
  void renderSpecEvidence(content, dagRunId, node.nodeId);
2040
+ } else if (activeTab === "prompt-restart") {
2041
+ void renderPromptRestart(content, dagRunId, node);
1504
2042
  } else if (activeTab === "timeline") {
1505
2043
  const attemptBuilt = buildAttemptBar(dagRunId, node);
1506
2044
  if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
1507
- renderSessionTimeline(content, node.nodeId);
2045
+ renderExecutorAwareTimeline(content, dagRunId, node);
1508
2046
  } else if (activeTab === "input") {
1509
2047
  void renderDagNodeInput(content, dagRunId, node);
1510
2048
  } else {
@@ -1568,6 +2106,14 @@ function buildDagInspectorHeader(dagRunId, node) {
1568
2106
  ["timeline", "执行过程", "ri-route-line"],
1569
2107
  ["spec-evidence", "规范证据", "ri-book-open-line"],
1570
2108
  ];
2109
+ // Experimental prompt-override rerun: the server injects the SCOPED
2110
+ // inspect mutation token into /inspect HTML only while
2111
+ // HARNESS_DAG_RERUN_PROMPT_OVERRIDE is enabled (and it only authorizes
2112
+ // dagRerunPlan/dagRerun). No token → no tab → the inspector stays
2113
+ // strictly read-only.
2114
+ if (globalThis.__INSPECT_MUTATION_TOKEN__) {
2115
+ tabDefs.push(["prompt-restart", "提示词重启", "ri-restart-line"]);
2116
+ }
1571
2117
  const tabs = el("div", "dag-inspector-tabs");
1572
2118
  tabs.setAttribute("role", "tablist");
1573
2119
  tabs.setAttribute("aria-label", "节点检查器面板");
@@ -1657,7 +2203,14 @@ function dagInspectorSignature(dagRunId, node) {
1657
2203
  uiState.dagNodeExecutionOutputLoading ? "loading" : "idle",
1658
2204
  ]
1659
2205
  : tab === "timeline"
1660
- ? [uiState.sessionEventOffset]
2206
+ ? [
2207
+ uiState.sessionEventOffset,
2208
+ uiState.dagNodeProcessData?.kind ?? "",
2209
+ uiState.dagNodeProcessData?.emptyReason ?? "",
2210
+ (uiState.dagNodeProcessData?.commands ?? []).length,
2211
+ (uiState.dagNodeProcessData?.steps ?? []).length,
2212
+ uiState.dagNodeProcessLoading ? "loading" : "idle",
2213
+ ]
1661
2214
  : [node.status ?? ""];
1662
2215
  // Attempt dimension: selection identity + history shape. Poll refreshes keep
1663
2216
  // the DOM while selection/history are unchanged; switching attempt or a
@@ -90,6 +90,7 @@ import {
90
90
  closeDagInspector,
91
91
  selectDagNode,
92
92
  renderDagInspector,
93
+ mergeNodeProcess,
93
94
  } from "./dag-inspector.js";
94
95
  import { mergeSessionEvents } from "./session-timeline.js";
95
96
 
@@ -451,13 +452,19 @@ export async function renderDagDetail(dagRunId, initial = true) {
451
452
  nodesEl.scrollLeft = nodesScrollLeft;
452
453
 
453
454
  if (uiState.dagInspectorOpen && uiState.selectedDagNodeId) {
455
+ const selectedNode = nodes.find(
456
+ (node) => node.nodeId === uiState.selectedDagNodeId,
457
+ );
454
458
  if (!uiState.sessionEventIdentity) {
455
459
  uiState.sessionEventIdentity = makeSessionEventIdentity(
456
460
  dagRunId,
457
461
  uiState.selectedDagNodeId,
458
462
  );
459
463
  }
460
- await mergeSessionEvents(dagRunId, uiState.selectedDagNodeId);
464
+ await Promise.all([
465
+ mergeSessionEvents(dagRunId, uiState.selectedDagNodeId),
466
+ selectedNode ? mergeNodeProcess(dagRunId, selectedNode) : Promise.resolve(),
467
+ ]);
461
468
  }
462
469
  renderDagInspector(
463
470
  dagRunId,
@@ -101,11 +101,14 @@ export async function renderFailures() {
101
101
 
102
102
  if (tasks.length === 0) {
103
103
  clearNode(listEl);
104
+ // 2026-08-22 (B-FIND-02): 空态文案与静态 #failures-empty / 页面标题
105
+ // 「异常 Task」保持一致;旧文案「当前没有失败或阻塞的 Task。」与静态
106
+ // HTML 及回归目录 AC-FE-INS-004 漂移,导致 frontend-test 字面 find 0 命中。
104
107
  if (emptyEl) {
105
108
  emptyEl.hidden = false;
106
- mountViewState(emptyEl, "empty", "当前没有失败或阻塞的 Task");
109
+ mountViewState(emptyEl, "empty", "当前没有异常 Task");
107
110
  } else {
108
- mountViewState(listEl, "empty", "当前没有失败或阻塞的 Task");
111
+ mountViewState(listEl, "empty", "当前没有异常 Task");
109
112
  }
110
113
  return;
111
114
  }