@tea-agent/loop-agent 0.39.0 → 0.39.1

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 (135) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +9 -2
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/executors/shell-executor.js +1 -1
  9. package/dist/shared/dag-prompt-override.js +4 -5
  10. package/dist/shared/update/npm-client.js +33 -11
  11. package/dist/task/contract/apply.js +11 -1
  12. package/dist/task/contract/constants.js +2 -0
  13. package/dist/task/contract/hash.js +3 -0
  14. package/dist/task/contract/observe.js +25 -4
  15. package/dist/task/contract/paths.js +2 -1
  16. package/dist/task/contract/project.js +15 -1
  17. package/dist/task/contract/schema.js +2 -0
  18. package/dist/task/contract/transaction.js +12 -0
  19. package/dist/task/source-prepare/completeness.js +188 -0
  20. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  21. package/dist/task/source-prepare/index.js +3 -0
  22. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  23. package/dist/task/source-prepare/ledger-review.js +214 -0
  24. package/dist/task/source-prepare/ledger.js +545 -0
  25. package/dist/task/source-prepare/prepare.js +262 -1
  26. package/dist/task/source-prepare/semantic-intake.js +19 -3
  27. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  28. package/dist/task/source-prepare/types.js +22 -0
  29. package/dist/worker/cli.js +12 -26
  30. package/dist/worker/console/console-handoff.js +82 -0
  31. package/dist/worker/console/console-update-and-init.js +115 -0
  32. package/dist/worker/console/console-update-runtime.js +84 -0
  33. package/dist/worker/console/draft-store.js +49 -0
  34. package/dist/worker/console/operation-store.js +1 -1
  35. package/dist/worker/console/operator-actions.js +129 -4
  36. package/dist/worker/console/operator-surface-health.js +1 -0
  37. package/dist/worker/console/prd-intake-bridge.js +13 -0
  38. package/dist/worker/console/routes.js +3 -0
  39. package/dist/worker/console/server.js +73 -5
  40. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  41. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-B4i9cVxX.js} +1 -1
  42. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  43. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  44. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  45. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  46. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  56. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  57. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  58. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  59. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  60. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  61. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  65. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  66. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  67. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  68. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  69. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  70. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  71. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  72. package/dist/worker/console/static/assets/{index-Ci8ksvBm.js → index-CW88Fjbw.js} +81 -81
  73. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  74. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  75. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  76. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  77. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  78. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-aGmcjgTn.js} +1 -1
  79. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-Biyo-tqA.js} +5 -5
  80. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  81. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  82. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  83. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  84. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  85. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  86. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  87. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  88. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  89. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  90. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  91. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  92. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  93. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  94. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  95. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  96. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  97. package/dist/worker/console/static/index.html +2 -2
  98. package/dist/worker/console/static-src/app/console-types.js +40 -1
  99. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  100. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  101. package/dist/worker/observability/init-runtime-activity.js +26 -0
  102. package/dist/worker/observe/node-input.js +88 -2
  103. package/dist/worker/observe/routes.js +188 -0
  104. package/dist/worker/observe/static/api.js +3 -3
  105. package/dist/worker/observe/static/format.js +37 -0
  106. package/dist/worker/observe/static/index.html +5 -0
  107. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  108. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  109. package/dist/worker/observe/static/styles.css +12 -0
  110. package/dist/worker/observe/static/views/dag-inspector.js +316 -16
  111. package/dist/worker/observe/static/views/task.js +55 -0
  112. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  113. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  114. package/dist/workflows/dag/backend-test-pytest-collection.js +87 -4
  115. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  116. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  117. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  119. package/dist/workflows/dag/frontend-review-context.js +92 -10
  120. package/dist/workflows/dag/init-hybrid.js +125 -4
  121. package/dist/workflows/dag/node-execution.js +45 -0
  122. package/dist/workflows/dag/types.js +42 -11
  123. package/docs/README.md +1 -0
  124. package/docs/architecture/evolution.md +45 -0
  125. package/docs/init-surface.manifest.json +1 -0
  126. package/docs/templates/README.md +1 -1
  127. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  128. package/package.json +1 -1
  129. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  130. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  131. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  132. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  133. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  135. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
@@ -19,8 +19,10 @@ import {
19
19
  postOperatorAction,
20
20
  fetchOperatorOperation,
21
21
  } from "../api.js";
22
- import { badge, formatMs, prettyJsonBlock } from "../format.js";
22
+ import { badge, formatLedgerSummary, formatMs, prettyJsonBlock } from "../format.js";
23
23
  import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
24
+ import { buildPromptRestartCandidateOptions } from "../prompt-restart-candidates.js";
25
+ import { copyText } from "../copy.js";
24
26
  import {
25
27
  uiState,
26
28
  makeSessionEventIdentity,
@@ -52,6 +54,85 @@ import { createCustomSelect } from "../custom-select.js";
52
54
  // reference, always resolving the currently selected node identity.
53
55
  const inspectorSelection = { dagRunId: null, node: null };
54
56
 
57
+ function createPromptRestartCopyButton(readCandidate) {
58
+ const buildIcon = (kind) => {
59
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
60
+ svg.setAttribute(
61
+ "class",
62
+ `dags-copy-icon dags-copy-icon-${kind === "check" ? "check" : "rects"}`,
63
+ );
64
+ svg.setAttribute("viewBox", "0 0 16 16");
65
+ svg.setAttribute("width", "16");
66
+ svg.setAttribute("height", "16");
67
+ svg.setAttribute("aria-hidden", "true");
68
+ svg.setAttribute("focusable", "false");
69
+ if (kind === "check") {
70
+ const check = document.createElementNS(
71
+ "http://www.w3.org/2000/svg",
72
+ "path",
73
+ );
74
+ check.setAttribute("d", "M3.5 8.5 6.5 11.5 12.5 4.5");
75
+ check.setAttribute("fill", "none");
76
+ check.setAttribute("stroke", "currentColor");
77
+ check.setAttribute("stroke-width", "1.8");
78
+ check.setAttribute("stroke-linecap", "round");
79
+ check.setAttribute("stroke-linejoin", "round");
80
+ svg.appendChild(check);
81
+ return svg;
82
+ }
83
+ for (const [x, y] of [
84
+ [2, 2],
85
+ [5, 5],
86
+ ]) {
87
+ const rect = document.createElementNS(
88
+ "http://www.w3.org/2000/svg",
89
+ "rect",
90
+ );
91
+ rect.setAttribute("x", String(x));
92
+ rect.setAttribute("y", String(y));
93
+ rect.setAttribute("width", "9");
94
+ rect.setAttribute("height", "9");
95
+ rect.setAttribute("rx", "1.5");
96
+ rect.setAttribute("fill", "none");
97
+ rect.setAttribute("stroke", "currentColor");
98
+ rect.setAttribute("stroke-width", "1.5");
99
+ svg.appendChild(rect);
100
+ }
101
+ return svg;
102
+ };
103
+ const button = el(
104
+ "button",
105
+ "dags-copy-btn dags-copy-btn-icon prompt-restart-copy-icon",
106
+ );
107
+ button.type = "button";
108
+ button.setAttribute("data-testid", "prompt-restart-copy");
109
+ button.setAttribute("aria-label", "复制当前提示词候选");
110
+ button.setAttribute("title", "复制当前提示词候选");
111
+ button.appendChild(buildIcon("copy"));
112
+ button.appendChild(buildIcon("check"));
113
+ button.addEventListener("click", (event) => {
114
+ event.preventDefault();
115
+ event.stopPropagation();
116
+ const candidate = readCandidate();
117
+ void copyText(candidate?.prompt ?? "").then((ok) => {
118
+ const feedback = ok
119
+ ? candidate?.truncated
120
+ ? "已复制截断预览"
121
+ : "已复制"
122
+ : "复制失败";
123
+ button.classList.toggle("is-copied", ok);
124
+ button.setAttribute("aria-label", feedback);
125
+ button.setAttribute("title", feedback);
126
+ setTimeout(() => {
127
+ button.classList.remove("is-copied");
128
+ button.setAttribute("aria-label", "复制当前提示词候选");
129
+ button.setAttribute("title", "复制当前提示词候选");
130
+ }, 1500);
131
+ });
132
+ });
133
+ return button;
134
+ }
135
+
55
136
  function renderDagInspectorForCurrent() {
56
137
  if (!inspectorSelection.dagRunId || !inspectorSelection.node) return;
57
138
  renderDagInspector(
@@ -983,6 +1064,121 @@ async function renderDagNodeInput(content, dagRunId, node) {
983
1064
 
984
1065
  renderDagNodeInputDetail(content, data);
985
1066
  finishInputRender();
1067
+ // Frontend prewrite 门禁 / source fidelity ledger 展示(AC-009):只读安全摘要,
1068
+ // 从 run 产物(frontend-prewrite-result.json)与 /api/tasks/:id/ledger 拉取;
1069
+ // 接口失败进入安全降级,不泄漏敏感内容。
1070
+ if (/prewrite/.test(nodeId)) {
1071
+ void renderFrontendGateLedger(content, dagRunId);
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Frontend prewrite 门禁 + ledger 摘要行(AC-BLOCK-005):纯函数,测试直接断言。
1077
+ * prewriteResult = {ok:true, body}|{ok:false, status}|null(网络失败);ledgerBody 为
1078
+ * /api/tasks/:id/ledger 响应或 null。绝不把 envelope/预览原文塞入 <pre>(删除
1079
+ * 旧 lines.push(previewText) 路径),缺失/拒绝显示明确 fail-closed 文案。
1080
+ */
1081
+ export function frontendGateLedgerLines(input) {
1082
+ const { prewriteResult, ledgerBody, taskId } = input;
1083
+ const lines = [];
1084
+ if (prewriteResult?.ok && prewriteResult.body) {
1085
+ const parsed = prewriteResult.body;
1086
+ if (parsed?.classification) {
1087
+ lines.push(`classification: ${String(parsed.classification)}`);
1088
+ if (parsed.failureCode) {
1089
+ lines.push(`failureCode: ${String(parsed.failureCode)}`);
1090
+ }
1091
+ if (parsed.failureReason) {
1092
+ lines.push(`failureReason: ${String(parsed.failureReason)}`);
1093
+ }
1094
+ } else {
1095
+ lines.push("frontend-prewrite-result.json 内容无法解析为结构化结果。");
1096
+ }
1097
+ } else if (prewriteResult && prewriteResult.status === 404) {
1098
+ lines.push("frontend-prewrite-result.json 尚未生成(门禁未执行或缺失)。");
1099
+ } else if (prewriteResult) {
1100
+ lines.push(
1101
+ "frontend-prewrite-result.json 读取失败(只读接口拒绝/服务异常,安全降级)。",
1102
+ );
1103
+ } else {
1104
+ lines.push("frontend-prewrite-result.json 尚未生成(门禁未执行或缺失)。");
1105
+ }
1106
+ if (ledgerBody && ledgerBody.present === true) {
1107
+ lines.push(formatLedgerSummary(ledgerBody));
1108
+ } else if (ledgerBody) {
1109
+ lines.push("ledger: 无(任务无引用或 ledger 缺失,不伪造计数)");
1110
+ } else if (taskId) {
1111
+ lines.push("ledger: 暂不可查(只读接口失败,安全降级)");
1112
+ }
1113
+ return lines;
1114
+ }
1115
+
1116
+ /** DAG run 专属 prewrite-result 只读拉取(内联 fetch,不新增 api.js 依赖)。 */
1117
+ async function fetchPrewriteResultJson(dagRunId) {
1118
+ try {
1119
+ const res = await fetch(
1120
+ `/api/dag-runs/${encodeURIComponent(dagRunId)}/prewrite-result`,
1121
+ { credentials: "same-origin" },
1122
+ );
1123
+ if (!res.ok) return { ok: false, status: res.status };
1124
+ return { ok: true, body: await res.json() };
1125
+ } catch {
1126
+ return null; // 网络失败
1127
+ }
1128
+ }
1129
+
1130
+ /**
1131
+ * Resolve the DAG-owned task identity, prewrite result, and safe ledger summary.
1132
+ * Exported so the real browser request sequence can be regression-tested without
1133
+ * relying on source-string assertions.
1134
+ */
1135
+ export async function fetchFrontendGateLedgerData(dagRunId) {
1136
+ const run = await fetchJson(
1137
+ `/api/dag-runs/${encodeURIComponent(dagRunId)}`,
1138
+ );
1139
+ const taskId = typeof run?.taskId === "string" ? run.taskId : "";
1140
+ const prewriteResult = await fetchPrewriteResultJson(dagRunId);
1141
+ let ledgerBody = null;
1142
+ if (taskId) {
1143
+ ledgerBody = await fetchJson(
1144
+ `/api/tasks/${encodeURIComponent(taskId)}/ledger`,
1145
+ );
1146
+ }
1147
+ return { taskId, prewriteResult, ledgerBody };
1148
+ }
1149
+
1150
+ /**
1151
+ * Inspect 节点面板:frontend prewrite 门禁分类/稳定 failure code + ledger 安全摘要。
1152
+ * 数据全部来自只读接口与确定性产物(AC-BLOCK-005):task identity 从 DAG run
1153
+ * GET /api/dag-runs/:dagRunId 读取,prewrite 门禁调 DAG run 专属
1154
+ * GET /api/dag-runs/:dagRunId/prewrite-result;任何失败仅显示安全降级文案,
1155
+ * 绝不 push envelope。
1156
+ */
1157
+ async function renderFrontendGateLedger(content, dagRunId) {
1158
+ const section = el("section", "node-input-section");
1159
+ section.appendChild(
1160
+ el("h4", null, "Frontend prewrite 门禁 / source fidelity ledger"),
1161
+ );
1162
+ const status = el("p", "muted", "加载中…");
1163
+ status.setAttribute("role", "status");
1164
+ section.appendChild(status);
1165
+ content.appendChild(section);
1166
+ try {
1167
+ const { taskId, prewriteResult, ledgerBody } =
1168
+ await fetchFrontendGateLedgerData(dagRunId);
1169
+ const lines = frontendGateLedgerLines({
1170
+ prewriteResult,
1171
+ ledgerBody,
1172
+ taskId,
1173
+ });
1174
+ const pre = el("pre", "cmd", lines.join("\n"));
1175
+ pre.dataset.testid = "frontend-prewrite-ledger-summary";
1176
+ status.replaceWith(pre);
1177
+ } catch (error) {
1178
+ status.textContent = `加载失败:${
1179
+ error instanceof Error ? error.message : String(error)
1180
+ }(安全降级,不展示敏感内容)`;
1181
+ }
986
1182
  }
987
1183
 
988
1184
  /**
@@ -1017,29 +1213,78 @@ async function renderPromptRestart(content, dagRunId, node) {
1017
1213
  content.appendChild(error);
1018
1214
  return;
1019
1215
  }
1020
- const basePrompt =
1216
+ const frozenBasePrompt =
1021
1217
  typeof input.body.prompt?.text === "string" ? input.body.prompt.text : "";
1022
- if (!basePrompt) {
1218
+ if (!frozenBasePrompt) {
1023
1219
  const error = el("p", "node-input-error", "该节点没有冻结提示词。");
1024
1220
  error.setAttribute("role", "alert");
1025
1221
  content.appendChild(error);
1026
1222
  return;
1027
1223
  }
1224
+ const promptCandidateOptions = buildPromptRestartCandidateOptions(
1225
+ frozenBasePrompt,
1226
+ input.body.promptCandidates,
1227
+ );
1228
+ let selectedCandidate = promptCandidateOptions[0];
1229
+ const historicalCandidatesMissing = Array.isArray(input.body.warnings)
1230
+ ? input.body.warnings.some((warning) =>
1231
+ String(warning).startsWith("Failed prompt candidate missing for attempt "),
1232
+ )
1233
+ : false;
1028
1234
 
1029
1235
  if (node.status !== "ERROR") {
1030
1236
  // Non-failed nodes still show their frozen prompt — read-only.
1031
1237
  const hint = el(
1032
1238
  "p",
1033
1239
  "muted",
1034
- `当前节点状态为 ${node.status ?? "unknown"};仅失败(ERROR)节点支持编辑提示词并重启,以下为该节点冻结提示词(只读)。`,
1240
+ `当前节点状态为 ${node.status ?? "unknown"};仅失败(ERROR)节点支持编辑提示词并重启。以下可只读查看冻结提示词与历史失败候选。`,
1035
1241
  );
1036
1242
  hint.setAttribute("role", "status");
1037
1243
  content.appendChild(hint);
1244
+ const picker = el("label", "prompt-restart-field");
1245
+ picker.appendChild(el("span", "muted", "提示词版本(只读)"));
1246
+ const select = document.createElement("select");
1247
+ select.className = "prompt-restart-mode-select";
1248
+ select.setAttribute("data-testid", "prompt-restart-candidate");
1249
+ for (const option of promptCandidateOptions) {
1250
+ const element = document.createElement("option");
1251
+ element.value = option.key;
1252
+ element.textContent = option.label;
1253
+ select.appendChild(element);
1254
+ }
1255
+ picker.appendChild(select);
1256
+ if (promptCandidateOptions.length === 1) {
1257
+ picker.appendChild(
1258
+ el(
1259
+ "span",
1260
+ "muted",
1261
+ historicalCandidatesMissing
1262
+ ? "该历史运行未保存失败 attempt 的实际提示词;新运行会在此列出。"
1263
+ : "该节点没有已保存的失败 attempt。",
1264
+ ),
1265
+ );
1266
+ }
1267
+ content.appendChild(picker);
1268
+
1269
+ const promptCard = el("div", "prompt-restart-prompt-card");
1270
+ const promptHead = el("div", "prompt-restart-prompt-head");
1271
+ promptHead.appendChild(el("span", "muted", "当前提示词"));
1272
+ promptHead.appendChild(
1273
+ createPromptRestartCopyButton(() => selectedCandidate),
1274
+ );
1275
+ promptCard.appendChild(promptHead);
1038
1276
  const pre = document.createElement("pre");
1039
1277
  pre.className = "prompt-restart-readonly";
1040
1278
  pre.setAttribute("data-testid", "prompt-restart-readonly");
1041
- pre.textContent = basePrompt;
1042
- content.appendChild(pre);
1279
+ pre.textContent = selectedCandidate.prompt;
1280
+ promptCard.appendChild(pre);
1281
+ content.appendChild(promptCard);
1282
+ select.addEventListener("change", () => {
1283
+ selectedCandidate =
1284
+ promptCandidateOptions.find((option) => option.key === select.value) ??
1285
+ promptCandidateOptions[0];
1286
+ pre.textContent = selectedCandidate.prompt;
1287
+ });
1043
1288
  return;
1044
1289
  }
1045
1290
 
@@ -1062,6 +1307,29 @@ async function renderPromptRestart(content, dagRunId, node) {
1062
1307
  "编辑该失败节点的提示词并从它重启:父 run spec 保持冻结,将创建带修改提示词的续跑 run(实验能力,仅 ERROR 节点)。",
1063
1308
  ),
1064
1309
  );
1310
+ const candidateRow = el("label", "prompt-restart-field");
1311
+ candidateRow.appendChild(el("span", "muted", "提示词版本"));
1312
+ const candidateSelect = document.createElement("select");
1313
+ candidateSelect.className = "prompt-restart-mode-select";
1314
+ candidateSelect.setAttribute("data-testid", "prompt-restart-candidate");
1315
+ for (const option of promptCandidateOptions) {
1316
+ const element = document.createElement("option");
1317
+ element.value = option.key;
1318
+ element.textContent = option.label;
1319
+ candidateSelect.appendChild(element);
1320
+ }
1321
+ candidateRow.appendChild(candidateSelect);
1322
+ const candidateHint = el(
1323
+ "span",
1324
+ "muted",
1325
+ promptCandidateOptions.length > 1
1326
+ ? "选择失败候选会载入全文替换;冻结原始提示词仍可使用追加模式。"
1327
+ : historicalCandidatesMissing
1328
+ ? "该历史运行未保存失败 attempt 的实际提示词;新运行会在此列出。"
1329
+ : "该节点没有已保存的失败 attempt;当前仅有冻结原始提示词。",
1330
+ );
1331
+ candidateRow.appendChild(candidateHint);
1332
+ box.appendChild(candidateRow);
1065
1333
 
1066
1334
  const modeRow = el("div", "prompt-restart-mode");
1067
1335
  modeRow.appendChild(el("span", "muted", "编辑模式"));
@@ -1081,14 +1349,20 @@ async function renderPromptRestart(content, dagRunId, node) {
1081
1349
  modeRow.appendChild(modeSelect);
1082
1350
  box.appendChild(modeRow);
1083
1351
 
1084
- const baseLabel = el("label", "prompt-restart-field");
1085
- baseLabel.appendChild(
1352
+ const baseLabel = el(
1353
+ "div",
1354
+ "prompt-restart-field prompt-restart-prompt-card",
1355
+ );
1356
+ const baseHead = el("div", "prompt-restart-prompt-head");
1357
+ baseHead.appendChild(
1086
1358
  el("span", "muted", `冻结提示词(只读底稿 · ${nodeId})`),
1087
1359
  );
1360
+ baseHead.appendChild(createPromptRestartCopyButton(() => selectedCandidate));
1361
+ baseLabel.appendChild(baseHead);
1088
1362
  const baseBlock = document.createElement("pre");
1089
1363
  baseBlock.className = "prompt-restart-readonly";
1090
1364
  baseBlock.setAttribute("data-testid", "prompt-restart-base");
1091
- baseBlock.textContent = basePrompt;
1365
+ baseBlock.textContent = selectedCandidate.prompt;
1092
1366
  baseLabel.appendChild(baseBlock);
1093
1367
  box.appendChild(baseLabel);
1094
1368
 
@@ -1113,7 +1387,7 @@ async function renderPromptRestart(content, dagRunId, node) {
1113
1387
  const replaceArea = document.createElement("textarea");
1114
1388
  replaceArea.className = "prompt-restart-textarea";
1115
1389
  replaceArea.rows = 16;
1116
- replaceArea.value = basePrompt;
1390
+ replaceArea.value = selectedCandidate.prompt;
1117
1391
  replaceArea.setAttribute("data-testid", "prompt-restart-textarea");
1118
1392
  replaceLabel.appendChild(replaceArea);
1119
1393
  box.appendChild(replaceLabel);
@@ -1125,6 +1399,25 @@ async function renderPromptRestart(content, dagRunId, node) {
1125
1399
  replaceLabel.style.display = append ? "none" : "";
1126
1400
  };
1127
1401
  modeSelect.addEventListener("change", applyModeVisibility);
1402
+ candidateSelect.addEventListener("change", () => {
1403
+ selectedCandidate =
1404
+ promptCandidateOptions.find(
1405
+ (option) => option.key === candidateSelect.value,
1406
+ ) ?? promptCandidateOptions[0];
1407
+ baseBlock.textContent = selectedCandidate.prompt;
1408
+ replaceArea.value = selectedCandidate.prompt;
1409
+ if (selectedCandidate.key !== "frozen") {
1410
+ modeSelect.value = "replace";
1411
+ }
1412
+ if (candidateHint) {
1413
+ candidateHint.textContent = selectedCandidate.truncated
1414
+ ? "该候选已截断,仅供查看,不能直接提交重启。"
1415
+ : selectedCandidate.key === "frozen"
1416
+ ? "冻结原始提示词可使用追加或全文替换。"
1417
+ : "已载入该失败候选;可在全文替换编辑器中修订后重启。";
1418
+ }
1419
+ applyModeVisibility();
1420
+ });
1128
1421
  applyModeVisibility();
1129
1422
 
1130
1423
  const reasonLabel = el("label", "prompt-restart-field");
@@ -1160,6 +1453,10 @@ async function renderPromptRestart(content, dagRunId, node) {
1160
1453
 
1161
1454
  submit.addEventListener("click", () => {
1162
1455
  void (async () => {
1456
+ if (selectedCandidate.truncated) {
1457
+ setStatus("所选失败提示词已截断,不能作为完整提示词重启。", true);
1458
+ return;
1459
+ }
1163
1460
  const mode = modeSelect.value === "replace" ? "replace" : "append";
1164
1461
  const prompt =
1165
1462
  mode === "append" ? addendumArea.value : replaceArea.value;
@@ -1171,7 +1468,11 @@ async function renderPromptRestart(content, dagRunId, node) {
1171
1468
  );
1172
1469
  return;
1173
1470
  }
1174
- if (mode === "replace" && prompt === basePrompt) {
1471
+ if (
1472
+ mode === "replace" &&
1473
+ selectedCandidate.key === "frozen" &&
1474
+ prompt === frozenBasePrompt
1475
+ ) {
1175
1476
  setStatus("提示词未修改;如需按原提示词重跑请使用恢复工作台。", true);
1176
1477
  return;
1177
1478
  }
@@ -2106,11 +2407,10 @@ function buildDagInspectorHeader(dagRunId, node) {
2106
2407
  ["timeline", "执行过程", "ri-route-line"],
2107
2408
  ["spec-evidence", "规范证据", "ri-book-open-line"],
2108
2409
  ];
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.
2410
+ // Prompt-override rerun: the server injects the SCOPED inspect mutation
2411
+ // token into /inspect HTML by default (it only authorizes
2412
+ // dagRerunPlan/dagRerun). The emergency kill switch suppresses the token;
2413
+ // no token → no tab → the inspector stays strictly read-only.
2114
2414
  if (globalThis.__INSPECT_MUTATION_TOKEN__) {
2115
2415
  tabDefs.push(["prompt-restart", "提示词重启", "ri-restart-line"]);
2116
2416
  }
@@ -34,6 +34,7 @@ import {
34
34
  repoBaseName,
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
+ formatLedgerSummary,
37
38
  } from "../format.js";
38
39
  import { parseRoute, navigate, buildHashPath } from "../router.js";
39
40
  import {
@@ -94,6 +95,7 @@ export async function renderTaskDetail(featureId, taskId) {
94
95
  const linksEl = document.getElementById("task-links");
95
96
  const evidenceEl = document.getElementById("task-evidence");
96
97
  const runsEl = document.getElementById("task-runs");
98
+ const ledgerEl = document.getElementById("task-ledger");
97
99
  if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
98
100
  if (titleEl) titleEl.textContent = displayLabel;
99
101
  mountViewState(metaEl, "loading", "加载 Task…");
@@ -252,9 +254,62 @@ export async function renderTaskDetail(featureId, taskId) {
252
254
  const active = ["running", "pending", "blocked", "stale"].includes(
253
255
  (task.status ?? "").toLowerCase(),
254
256
  );
257
+ if (ledgerEl) {
258
+ void renderTaskLedger(ledgerEl, taskId);
259
+ }
255
260
  return active;
256
261
  }
257
262
 
263
+ /**
264
+ * Task 详情页 ledger 安全摘要(AC-009):从只读 GET /api/tasks/:id/ledger 拉取,
265
+ * 计数/状态/failure code + 有界排除理由;无 ledger 显示空态,失败安全降级。
266
+ */
267
+ async function renderTaskLedger(ledgerEl, taskId) {
268
+ clearNode(ledgerEl);
269
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
270
+ mountViewState(ledgerEl, "loading", "加载 ledger…");
271
+ let body = null;
272
+ try {
273
+ body = await fetchJson(`/api/tasks/${encodeURIComponent(taskId)}/ledger`);
274
+ } catch {
275
+ body = null;
276
+ }
277
+ if (!body) {
278
+ mountViewState(ledgerEl, "error", "ledger 只读接口失败(安全降级)。");
279
+ return;
280
+ }
281
+ if (body.present !== true) {
282
+ mountViewState(
283
+ ledgerEl,
284
+ body.error ? "error" : "empty",
285
+ body.error
286
+ ? `ledger 不可用(${body.error})。`
287
+ : "无 source fidelity ledger(任务无引用或 ledger 缺失,不伪造计数)。",
288
+ );
289
+ return;
290
+ }
291
+ const pre = el(
292
+ "pre",
293
+ "cmd",
294
+ formatLedgerSummary({
295
+ present: true,
296
+ fragmentCount: body.fragmentCount,
297
+ canonicalRequirementCount: body.canonicalRequirementCount,
298
+ contextOnlyCount: body.contextOnlyCount,
299
+ duplicateCount: body.duplicateCount,
300
+ unresolvedCount: body.unresolvedCount,
301
+ conflictCount: body.conflictCount,
302
+ gateStatus: body.gateStatus,
303
+ failureCode: body.failureCode,
304
+ exclusions: body.exclusions ?? [],
305
+ }),
306
+ );
307
+ pre.dataset.testid = "task-ledger-summary";
308
+ clearNode(ledgerEl);
309
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
310
+ ledgerEl.appendChild(pre);
311
+ }
312
+
258
313
  export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
259
314
  if (runsEl === undefined) {
260
315
  runsEl = before;
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { z } from "zod";
6
6
  import YAML from "yaml";
7
7
  import { resolveBackendTestLayout, } from "./backend-test-layout.js";
8
+ import { resolveBackendTestManifestModules, backendManifestStatusFinding } from "./backend-test-markdown-workflow.js";
8
9
  import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
9
10
  import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
10
11
  const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
@@ -659,10 +660,18 @@ async function markdownModuleFiles(workspaceRoot, layout) {
659
660
  const resolved = layout ?? resolveBackendTestLayout(undefined);
660
661
  const dir = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
661
662
  const entries = await readdir(dir, { withFileTypes: true });
662
- return entries
663
+ const all = entries
663
664
  .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
664
665
  .map((entry) => path.join(dir, entry.name))
665
666
  .sort();
667
+ // Current-round Markdown scope is the README Module Index manifest: unmanifested
668
+ // legacy .md files must not enter Case/coverage/partition statistics. Workspaces
669
+ // without a Module Index fall back to the whole directory (prior behaviour).
670
+ const manifest = await resolveBackendTestManifestModules(workspaceRoot, resolved);
671
+ if (manifest.manifestStatus !== "valid")
672
+ return [];
673
+ const declaredAbs = new Set(manifest.declaredMdFiles.map((rel) => path.join(workspaceRoot, ...rel.split("/")).toLowerCase()));
674
+ return all.filter((file) => declaredAbs.has(file.toLowerCase())).sort();
666
675
  }
667
676
  async function parseMarkdownCases(workspaceRoot, layout) {
668
677
  const result = [];
@@ -874,6 +883,16 @@ export async function analyzeBackendTestCaseCoverage(input) {
874
883
  findings.push(...coverageScope.findings);
875
884
  const matrix = parseCoverageMatrix(readme);
876
885
  findings.push(...matrix.findings);
886
+ const manifest = await resolveBackendTestManifestModules(input.workspaceRoot, layout);
887
+ // Fail closed on README Module Index entries declared but never materialized.
888
+ if (manifest.declaredMissing.length > 0) {
889
+ findings.push(`declared Markdown module is missing (Module Index): ${manifest.declaredMissing.join(", ")}`);
890
+ }
891
+ // Fail closed on every non-valid Module Index state: without a valid unique
892
+ // Module Index the current round must not re-scan historical `.md` files.
893
+ if (manifest.manifestStatus !== "valid") {
894
+ findings.push(backendManifestStatusFinding(manifest.manifestStatus, manifest.moduleIndexHeadingCount));
895
+ }
877
896
  const cases = await parseMarkdownCases(input.workspaceRoot, layout);
878
897
  for (const file of await markdownModuleFiles(input.workspaceRoot, layout)) {
879
898
  const markdown = await readFile(file, "utf8");
@@ -1109,6 +1128,15 @@ export async function analyzeBackendTestCaseCoverage(input) {
1109
1128
  "",
1110
1129
  facts.status,
1111
1130
  "",
1131
+ `- Manifest scope: ${manifest.manifestStatus === "valid" ? "declared-modules" : `fails-closed:${manifest.manifestStatus}`} (declared-missing=${manifest.declaredMissing.length}, unmanifested-advisory=${manifest.unmanifested.length}, module-index-heading-count=${manifest.moduleIndexHeadingCount})`,
1132
+ "",
1133
+ "## Unmanifested Markdown Modules",
1134
+ "",
1135
+ "- Advisory: existing .md files not declared by the README Module Index are excluded from Case/coverage/partition statistics for this round.",
1136
+ ...(manifest.unmanifested.length > 0
1137
+ ? manifest.unmanifested.map((item) => `- ${item}`)
1138
+ : ["- None"]),
1139
+ "",
1112
1140
  "## Coverage Scope",
1113
1141
  "",
1114
1142
  `- Change Classification: ${facts.coverageScope.changeClassification}`,