@tea-agent/loop-agent 0.38.2 → 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 (192) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +171 -96
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +428 -27
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/commands/task-advance.js +31 -0
  17. package/dist/executors/dag-pi-executor.js +76 -10
  18. package/dist/executors/pi-event-serializer.js +42 -1
  19. package/dist/executors/pi-executor.js +51 -0
  20. package/dist/executors/pi-extension-resolver.js +233 -0
  21. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  22. package/dist/executors/pi-sdk-executor.js +187 -59
  23. package/dist/executors/shell-executor.js +265 -15
  24. package/dist/executors/shell-write-guard.js +7 -0
  25. package/dist/shared/dag-prompt-override.js +28 -0
  26. package/dist/shared/operator/capabilities.js +32 -0
  27. package/dist/shared/playwright-cli-command-policy.js +15 -0
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/contract/constants.js +1 -0
  32. package/dist/task/contract/diff.js +26 -0
  33. package/dist/task/contract/project.js +5 -0
  34. package/dist/task/contract/schema.js +6 -1
  35. package/dist/task/source-prepare/build-draft.js +15 -0
  36. package/dist/task/source-prepare/parse-intent.js +24 -4
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/routes.js +1 -0
  39. package/dist/worker/console/dag-execution-receipt.js +33 -0
  40. package/dist/worker/console/operator-actions.js +116 -0
  41. package/dist/worker/console/operator-user-error.js +2 -2
  42. package/dist/worker/console/pi-readiness.js +4 -4
  43. package/dist/worker/console/routes.js +43 -2
  44. package/dist/worker/console/security.js +63 -9
  45. package/dist/worker/console/server.js +36 -5
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  78. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  79. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  105. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  106. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  107. package/dist/worker/observe/node-input.js +74 -3
  108. package/dist/worker/observe/node-process.js +377 -0
  109. package/dist/worker/observe/routes.js +36 -3
  110. package/dist/worker/observe/shell-handler-keys.js +34 -0
  111. package/dist/worker/observe/static/api.js +66 -0
  112. package/dist/worker/observe/static/constants.js +8 -1
  113. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  114. package/dist/worker/observe/static/format.js +23 -0
  115. package/dist/worker/observe/static/markdown-render.js +20 -1
  116. package/dist/worker/observe/static/operator-chrome.js +4 -0
  117. package/dist/worker/observe/static/state.js +13 -2
  118. package/dist/worker/observe/static/styles.css +73 -0
  119. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  120. package/dist/worker/observe/static/views/dag.js +8 -1
  121. package/dist/worker/observe/static/views/failures.js +5 -2
  122. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  123. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
  124. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  125. package/dist/workflows/dag/contract-output-registry.js +15 -0
  126. package/dist/workflows/dag/failure-category.js +12 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  129. package/dist/workflows/dag/frontend-repair.js +14 -0
  130. package/dist/workflows/dag/frontend-risk.js +15 -2
  131. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  132. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  133. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  134. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  135. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  136. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  137. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  138. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  139. package/dist/workflows/dag/init-hybrid.js +196 -117
  140. package/dist/workflows/dag/node-execution.js +139 -10
  141. package/dist/workflows/dag/prompt.js +15 -1
  142. package/dist/workflows/dag/rerun-plan.js +134 -3
  143. package/dist/workflows/dag/rerun-run.js +58 -6
  144. package/dist/workflows/dag/retry-policy.js +91 -0
  145. package/dist/workflows/dag/structured-output-repair.js +712 -0
  146. package/dist/workflows/dag/types.js +32 -1
  147. package/dist/workflows/dag/validate.js +41 -1
  148. package/docs/init-surface.manifest.json +5 -0
  149. package/docs/operations/README.md +1 -1
  150. package/docs/operations/local-development-environment.md +1 -5
  151. package/docs/skills/vetted-skill-registry.md +14 -0
  152. package/docs/templates/README.md +1 -1
  153. package/docs/templates/agent-dag.schema.json +44 -0
  154. package/docs/templates/backend-test-dag.json +2 -2
  155. package/docs/templates/frontend-test-dag.json +8 -10
  156. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  157. package/docs/templates/init-managed-agents.md +13 -3
  158. package/harness.json +2 -2
  159. package/package.json +12 -4
  160. package/scripts/next-info.mjs +356 -0
  161. package/scripts/next-publish-gate.mjs +238 -0
  162. package/scripts/release-source-binding.mjs +251 -0
  163. package/skills/analyze-product-requirements/SKILL.md +8 -5
  164. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  165. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  166. package/skills/analyze-product-requirements/references/example.md +50 -0
  167. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  168. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  169. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  170. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  171. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  172. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  173. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  174. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  175. package/skills/codebase-scout/SKILL.md +1 -1
  176. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  178. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  179. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  180. package/skills/improve-codebase-architecture/deepening.md +37 -0
  181. package/skills/improve-codebase-architecture/html-report.md +123 -0
  182. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  183. package/skills/improve-codebase-architecture/language.md +53 -0
  184. package/skills/loop-agent/references/command-reference.md +11 -4
  185. package/skills/playwright-cli/SKILL.md +22 -0
  186. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  187. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  188. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  189. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  190. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  191. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  192. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -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
  }
@@ -250,6 +250,54 @@ export function buildSessionTimelineItems(events) {
250
250
  return items;
251
251
  }
252
252
 
253
+ /**
254
+ * Split a node's shared session-events log into per-attempt segments. Every
255
+ * attempt appends one agent run beginning with agent_start; events recorded
256
+ * before the first agent_start (defensive) stay in an implicit first segment.
257
+ */
258
+ export function splitSessionEventsByAttempt(events) {
259
+ const segments = [];
260
+ let current = null;
261
+ for (const event of events) {
262
+ if (typeof event?.type === "string" && event.type === "agent_start") {
263
+ current = [];
264
+ segments.push(current);
265
+ }
266
+ if (current === null) {
267
+ current = [];
268
+ segments.push(current);
269
+ }
270
+ current.push(event);
271
+ }
272
+ return segments;
273
+ }
274
+
275
+ /**
276
+ * Resolve the events visible in the「执行过程」tab for the selected execution
277
+ * key. attempt-N shows only that attempt's segment when the shared log carries
278
+ * multiple attempts; other keys (pass-N, null) keep the full stream.
279
+ */
280
+ export function resolveTimelineVisibleEvents(events, executionKey) {
281
+ const match =
282
+ typeof executionKey === "string"
283
+ ? /^attempt-(\d+)$/.exec(executionKey)
284
+ : null;
285
+ if (!match) return { events, note: null };
286
+ const segments = splitSessionEventsByAttempt(events);
287
+ if (segments.length <= 1) return { events, note: null };
288
+ const requested = Number(match[1]);
289
+ if (requested >= 1 && requested <= segments.length) {
290
+ return {
291
+ events: segments[requested - 1],
292
+ note: `已按第 ${requested} 次执行过滤 · 共 ${segments.length} 次执行`,
293
+ };
294
+ }
295
+ return {
296
+ events,
297
+ note: `未检测到第 ${requested} 次执行的边界(共 ${segments.length} 次执行),暂显示全部事件`,
298
+ };
299
+ }
300
+
253
301
  /**
254
302
  * Build rich timeline items: merge tool start/end by toolCallId, collapse
255
303
  * protocol noise, surface turn thinking/text without dumping agent_end.messages.
@@ -439,12 +487,25 @@ function ensureOpenDetailsSet() {
439
487
  return uiState.sessionTimelineOpenDetails;
440
488
  }
441
489
 
442
- function bindDetailsToggle(details, detailKey) {
490
+ function closedDetailKey(detailKey) {
491
+ return `${detailKey}::user-closed`;
492
+ }
493
+
494
+ function bindDetailsToggle(details, detailKey, defaultOpen = false) {
443
495
  const openSet = ensureOpenDetailsSet();
444
- if (openSet.has(detailKey)) details.open = true;
496
+ if (defaultOpen) {
497
+ if (!openSet.has(closedDetailKey(detailKey))) details.open = true;
498
+ } else if (openSet.has(detailKey)) {
499
+ details.open = true;
500
+ }
445
501
  details.addEventListener("toggle", () => {
446
- if (details.open) openSet.add(detailKey);
447
- else openSet.delete(detailKey);
502
+ if (details.open) {
503
+ openSet.add(detailKey);
504
+ openSet.delete(closedDetailKey(detailKey));
505
+ } else {
506
+ openSet.delete(detailKey);
507
+ openSet.add(closedDetailKey(detailKey));
508
+ }
448
509
  });
449
510
  }
450
511
 
@@ -599,7 +660,7 @@ function renderProtocolCard(item) {
599
660
  const details = document.createElement("details");
600
661
  details.className = "process-timeline-details";
601
662
  details.dataset.detailKey = item.detailKey;
602
- bindDetailsToggle(details, item.detailKey);
663
+ bindDetailsToggle(details, item.detailKey, true);
603
664
  const summary = document.createElement("summary");
604
665
  summary.className = "process-timeline-summary";
605
666
  summary.textContent = `消息收发 · ${item.protocolCount} 条协议事件`;
@@ -644,7 +705,7 @@ export async function mergeSessionEvents(dagRunId, nodeId) {
644
705
  }
645
706
  }
646
707
 
647
- export function renderSessionTimeline(content, nodeId) {
708
+ export function renderSessionTimeline(content, nodeId, options = {}) {
648
709
  content.dataset.dagRunId = uiState.currentDagRunId ?? "";
649
710
  content.dataset.nodeId = nodeId ?? "";
650
711
  content.classList.add("process-timeline-scroll");
@@ -653,6 +714,14 @@ export function renderSessionTimeline(content, nodeId) {
653
714
  return;
654
715
  }
655
716
 
717
+ const visible = resolveTimelineVisibleEvents(
718
+ uiState.sessionEvents,
719
+ options.executionKey,
720
+ );
721
+ if (visible.note) {
722
+ content.appendChild(el("p", "process-timeline-note", visible.note));
723
+ }
724
+
656
725
  const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
657
726
  const toolbar = el("div", "process-timeline-toolbar");
658
727
  const protocolToggle = document.createElement("button");
@@ -668,17 +737,17 @@ export function renderSessionTimeline(content, nodeId) {
668
737
  const scrollTop = parent.scrollTop;
669
738
  const scrollLeft = parent.scrollLeft;
670
739
  while (parent.firstChild) parent.removeChild(parent.firstChild);
671
- renderSessionTimeline(parent, nodeId);
740
+ renderSessionTimeline(parent, nodeId, options);
672
741
  parent.scrollTop = scrollTop;
673
742
  parent.scrollLeft = scrollLeft;
674
743
  });
675
744
  toolbar.appendChild(protocolToggle);
676
745
  content.appendChild(toolbar);
677
746
 
678
- const timelineItems = buildRichTimelineItems(uiState.sessionEvents, {
747
+ const timelineItems = buildRichTimelineItems(visible.events, {
679
748
  showProtocol,
680
749
  });
681
- const hasEventTimes = uiState.sessionEvents.some((event) =>
750
+ const hasEventTimes = visible.events.some((event) =>
682
751
  Boolean(sessionEventTimestamp(event)),
683
752
  );
684
753
  if (!hasEventTimes) {
@@ -5,7 +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 { assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
8
+ import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
9
9
  import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
10
10
  const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
11
11
  const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
@@ -486,6 +486,55 @@ export async function extractBackendTestOpenApiRules(input) {
486
486
  }
487
487
  return rules.sort((left, right) => left.ruleKey.localeCompare(right.ruleKey));
488
488
  }
489
+ export async function extractBackendTestOpenApiPartitionAxes(input) {
490
+ const axes = [];
491
+ const seen = new Set();
492
+ for (const referencePath of input.sourceBinding.referencePaths) {
493
+ const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
494
+ let document;
495
+ try {
496
+ const parsed = YAML.parse(await readFile(physicalPath, "utf8"));
497
+ if (!isRecord(parsed) || !isRecord(parsed.paths))
498
+ continue;
499
+ document = parsed;
500
+ }
501
+ catch {
502
+ continue;
503
+ }
504
+ for (const [apiPath, pathItem] of Object.entries(document.paths)) {
505
+ if (!isRecord(pathItem))
506
+ continue;
507
+ const operation = pathItem.get;
508
+ if (!isRecord(operation))
509
+ continue;
510
+ const parameterValues = [
511
+ ...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
512
+ ...(Array.isArray(operation.parameters) ? operation.parameters : []),
513
+ ];
514
+ for (const rawParameter of parameterValues) {
515
+ const parameter = resolveOpenApiSchema(document, rawParameter);
516
+ if (!parameter || typeof parameter.name !== "string")
517
+ continue;
518
+ const location = parameter.in === "path" ? "path" : parameter.in === "query" ? "query" : undefined;
519
+ if (!location)
520
+ continue;
521
+ const property = resolveOpenApiSchema(document, parameter.schema);
522
+ const enumValues = property && Array.isArray(property.enum) ? property.enum.map(String) : null;
523
+ const identity = `GET ${apiPath}|${location}|${parameter.name}`;
524
+ if (seen.has(identity))
525
+ continue;
526
+ seen.add(identity);
527
+ axes.push({
528
+ operation: `GET ${apiPath}`,
529
+ name: parameter.name,
530
+ in: location,
531
+ enumValues,
532
+ });
533
+ }
534
+ }
535
+ }
536
+ return axes;
537
+ }
489
538
  function orderedUnique(values) {
490
539
  const result = [];
491
540
  const seen = new Set();
@@ -985,8 +1034,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
985
1034
  // Plan C: deterministic query/filter axis partitions (fail-closed on missing slots).
986
1035
  const parsedPartitions = parseScenarioPartitions(readme);
987
1036
  findings.push(...parsedPartitions.issues);
1037
+ const openApiPartitionAxes = await extractBackendTestOpenApiPartitionAxes(input);
1038
+ const partitionedBySource = applyOpenApiPartitionDomainPolicy(parsedPartitions.partitions, openApiPartitionAxes);
1039
+ findings.push(...partitionedBySource.issues);
988
1040
  const declaredPointUniverse = new Set(cases.flatMap((item) => item.testPoints));
989
- const partitionAssessment = assessScenarioPartitionCoverage(parsedPartitions.partitions, declaredPointUniverse);
1041
+ const partitionAssessment = assessScenarioPartitionCoverage(partitionedBySource.partitions, declaredPointUniverse);
990
1042
  findings.push(...partitionAssessment.findings);
991
1043
  const [enumValueCount, coveredEnumValueCount] = dimensionCounts(rules, /enum/);
992
1044
  const [boundaryPointCount, coveredBoundaryPointCount] = dimensionCounts(rules, /boundary|length|numeric/);
@@ -1003,7 +1055,7 @@ export async function analyzeBackendTestCaseCoverage(input) {
1003
1055
  sourceBinding: input.sourceBinding,
1004
1056
  inputFiles: await inputFileFacts(input.workspaceRoot, [
1005
1057
  ...((await exists(readmePath)) ? [readmePath] : []),
1006
- ...(await markdownModuleFiles(input.workspaceRoot)),
1058
+ ...(await markdownModuleFiles(input.workspaceRoot, layout)),
1007
1059
  ...(await Promise.all(input.sourceBinding.referencePaths.map(async (referencePath) => {
1008
1060
  const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
1009
1061
  return (await exists(physicalPath)) ? physicalPath : "";
@@ -1448,8 +1500,26 @@ print(json.dumps(out,ensure_ascii=True))
1448
1500
  // Windows Python may expose stdin as GBK; decoding generated Chinese/emoji
1449
1501
  // source through that locale can create surrogateescape code points and make
1450
1502
  // ast.parse fail with "surrogates not allowed".
1451
- const result = spawnSync("python", ["-c", program], { input: Buffer.from(source, "utf8"), encoding: "utf8", maxBuffer: 4 * 1024 * 1024, windowsHide: true });
1452
- if (result.status !== 0 || !result.stdout.trim())
1503
+ // POSIX images usually expose only `python3` (modern macOS has no `python`);
1504
+ // Windows often has a real `python` plus a Microsoft Store `python3` stub.
1505
+ // A missing interpreter must not silently degrade to empty payload shapes.
1506
+ const interpreterCandidates = process.platform === "win32" ? ["python", "python3"] : ["python3", "python"];
1507
+ let result;
1508
+ for (const interpreter of interpreterCandidates) {
1509
+ const attempt = spawnSync(interpreter, ["-c", program], {
1510
+ input: Buffer.from(source, "utf8"),
1511
+ encoding: "utf8",
1512
+ maxBuffer: 4 * 1024 * 1024,
1513
+ windowsHide: true,
1514
+ });
1515
+ // Interpreter missing/unspawnable → try the next candidate. A spawned
1516
+ // interpreter that exits nonzero analyzed real input; do not retry then.
1517
+ if (attempt.error)
1518
+ continue;
1519
+ result = attempt;
1520
+ break;
1521
+ }
1522
+ if (!result || result.status !== 0 || !result.stdout.trim())
1453
1523
  return new Map();
1454
1524
  try {
1455
1525
  const parsed = JSON.parse(result.stdout);
@@ -1794,7 +1864,7 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
1794
1864
  status: findings.length === 0 ? "PASS" : "FAIL",
1795
1865
  inputFiles: await inputFileFacts(input.workspaceRoot, [
1796
1866
  path.join(input.workspaceRoot, ...layout.readmePath.split("/")),
1797
- ...(await markdownModuleFiles(input.workspaceRoot)),
1867
+ ...(await markdownModuleFiles(input.workspaceRoot, layout)),
1798
1868
  ...scriptFiles,
1799
1869
  ].filter((file) => file && path.isAbsolute(file))),
1800
1870
  summary: {
@@ -35,6 +35,8 @@ export const scenarioPartitionRowSchema = z.object({
35
35
  const PARTITION_HEADING = /^##\s+Scenario Partitions\s*$/;
36
36
  const PARTITION_TABLE_HEADER = /^\|\s*Partition ID\s*\|\s*Operation\s*\|\s*Axis\s*\|\s*Domain\s*\|\s*Required Slots\s*\|\s*Expected by Slot\s*\|\s*Bind Rule\s*\|\s*$/;
37
37
  const TABLE_ROW = /^\|(.+)\|$/;
38
+ const FINITE_DOMAIN_MEMBER = /^[A-Za-z0-9]+(?:[._\- ][A-Za-z0-9]+)*$/;
39
+ const RESERVED_SLOT_TOKENS = new Set(["OMITTED", "NOT-IN-SET"]);
38
40
  function splitCells(raw) {
39
41
  return raw
40
42
  .split("|")
@@ -100,6 +102,11 @@ export function parseScenarioPartitions(readmeMarkdown) {
100
102
  continue;
101
103
  }
102
104
  seen.add(partitionId);
105
+ const domainIssue = describeInvalidPartitionDomain(domain);
106
+ if (domainIssue) {
107
+ issues.push(`INVALID_PARTITION_DOMAIN: partition ${partitionId} ${domainIssue}`);
108
+ continue;
109
+ }
103
110
  partitions.push({
104
111
  partitionId,
105
112
  operation,
@@ -119,6 +126,40 @@ function slotToken(value) {
119
126
  .replace(/[^A-Z0-9]+/g, "-")
120
127
  .replace(/^-+|-+$/g, "");
121
128
  }
129
+ function describeInvalidPartitionDomain(domain) {
130
+ const tokens = [];
131
+ const seenTokens = new Set();
132
+ for (const value of domain) {
133
+ if (!FINITE_DOMAIN_MEMBER.test(value)) {
134
+ return `value is not a finite identifier member: ${value}`;
135
+ }
136
+ const token = slotToken(value);
137
+ if (!token) {
138
+ return `value cannot produce a stable non-empty slot token: ${value}`;
139
+ }
140
+ if (RESERVED_SLOT_TOKENS.has(token)) {
141
+ return `value maps to reserved slot token ${token}: ${value}`;
142
+ }
143
+ if (seenTokens.has(token)) {
144
+ return `values collapse to duplicate slot token ${token}`;
145
+ }
146
+ seenTokens.add(token);
147
+ tokens.push(token);
148
+ }
149
+ return tokens.length === 0 ? "domain produced no slot tokens" : undefined;
150
+ }
151
+ function normalizePartitionAxisName(value) {
152
+ return value.trim().replace(/^(?:query|path)\s*:\s*/i, "").toLowerCase();
153
+ }
154
+ function normalizePartitionOperation(value) {
155
+ return value.trim().replace(/\s+/g, " ").toUpperCase();
156
+ }
157
+ function sameFiniteSet(left, right) {
158
+ if (left.length !== right.length)
159
+ return false;
160
+ const expected = new Set(left);
161
+ return right.every((value) => expected.has(value));
162
+ }
122
163
  const STABLE_PARTITION_ID = /^SP-[A-Z0-9][A-Z0-9._-]*$/i;
123
164
  /**
124
165
  * Slot IDs are `TP-<Partition ID>-<VALUE|OMITTED|NOT-IN-SET>`.
@@ -138,13 +179,16 @@ function partitionToken(row) {
138
179
  export function expandScenarioPartitions(partitions) {
139
180
  const slots = [];
140
181
  for (const row of partitions) {
182
+ if (describeInvalidPartitionDomain(row.domain))
183
+ continue;
141
184
  const prefix = partitionToken(row);
142
185
  for (const value of row.domain) {
186
+ const token = slotToken(value);
143
187
  slots.push({
144
188
  partitionId: row.partitionId,
145
189
  operation: row.operation,
146
190
  axis: row.axis,
147
- slotId: `${prefix}-${slotToken(value)}`,
191
+ slotId: `${prefix}-${token}`,
148
192
  kind: "each-value",
149
193
  value,
150
194
  intent: "nominal-filter",
@@ -176,6 +220,34 @@ export function expandScenarioPartitions(partitions) {
176
220
  }
177
221
  return slots;
178
222
  }
223
+ /**
224
+ * Bind parsed partitions to GET query/path OpenAPI axes.
225
+ * Unconfirmed axes keep identifier-valid Domain rows (requirement finite sets).
226
+ */
227
+ export function applyOpenApiPartitionDomainPolicy(partitions, axes) {
228
+ const issues = [];
229
+ const valid = [];
230
+ for (const row of partitions) {
231
+ const operation = normalizePartitionOperation(row.operation);
232
+ const axisName = normalizePartitionAxisName(row.axis);
233
+ const matched = axes.find((axis) => normalizePartitionOperation(axis.operation) === operation &&
234
+ normalizePartitionAxisName(axis.name) === axisName);
235
+ if (!matched) {
236
+ valid.push(row);
237
+ continue;
238
+ }
239
+ if (matched.enumValues === null) {
240
+ issues.push(`NO_FINITE_DOMAIN: partition ${row.partitionId} axis ${row.axis} exists on ${row.operation} without a documented enum`);
241
+ continue;
242
+ }
243
+ if (!sameFiniteSet(matched.enumValues, row.domain)) {
244
+ issues.push(`PARTITION_SOURCE_MISMATCH: partition ${row.partitionId} domain does not match OpenAPI enum for ${row.operation} ${row.axis}`);
245
+ continue;
246
+ }
247
+ valid.push(row);
248
+ }
249
+ return { partitions: valid, issues };
250
+ }
179
251
  /** Facts projection consumed by N6 and plan D gap-fill. */
180
252
  export const scenarioPartitionFactsSchema = z.object({
181
253
  partitions: z.array(z.object({
@@ -1,4 +1,19 @@
1
1
  import { structuredContractOutputSchemaIds, } from "./types.js";
2
+ /**
3
+ * Registry of node-level structured contract validators, keyed by the
4
+ * `structuredContractOutput.schemaId` declared on a DAG task.
5
+ *
6
+ * The executor looks the validator up here instead of hardcoding per-contract
7
+ * imports, so new contracts (backend-test manifests, coverage contracts, ...)
8
+ * can opt into producing-node self-validation without touching the executor.
9
+ */
10
+ export const structuredContractValidationClassifications = [
11
+ "format",
12
+ "schema",
13
+ "truncated",
14
+ "semantic-repair",
15
+ "governance-blocked",
16
+ ];
2
17
  const validators = new Map();
3
18
  export function registerStructuredContractValidator(schemaId, validator) {
4
19
  if (validators.has(schemaId)) {
@@ -29,8 +29,13 @@ const RAW_TO_NORMALIZED = {
29
29
  "decision-envelope-invalid": "decision-envelope",
30
30
  "nonzero-exit": "shell-command",
31
31
  "spawn-error": "shell-command",
32
+ "test-timeout": "timeout",
32
33
  "static-error": "static-error",
33
34
  "invalid-output": "validation",
35
+ "structured-output-truncated": "validation",
36
+ "output-too-large": "validation",
37
+ "structured-repair-exhausted": "validation",
38
+ "governance-blocked": "validation",
34
39
  "path-violation": "validation",
35
40
  "empty-output": "validation",
36
41
  "verify-failure": "validation",
@@ -40,6 +45,13 @@ const RAW_TO_NORMALIZED = {
40
45
  quota: "executor",
41
46
  "rate-limit": "executor",
42
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",
43
55
  unknown: "unknown",
44
56
  };
45
57
  function isFinishedStatus(status) {