@tea-agent/loop-agent 0.40.0-next.11 → 0.40.0-next.13

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 (128) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +1 -1
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/cli/command-definitions.js +9 -5
  5. package/dist/cli/program.js +38 -3
  6. package/dist/commands/client-recovery.js +8 -36
  7. package/dist/commands/task-advance.js +23 -1
  8. package/dist/commands/task-source-prepare.js +5 -0
  9. package/dist/commands/task-status.js +1 -0
  10. package/dist/executors/pi-sdk-executor.js +33 -5
  11. package/dist/shared/operator/capabilities.js +9 -1
  12. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  13. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  14. package/dist/shared/pi-context-pressure/env.js +58 -0
  15. package/dist/shared/pi-context-pressure/extension.js +100 -0
  16. package/dist/shared/pi-context-pressure/index.js +7 -0
  17. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  18. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  19. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  20. package/dist/worker/console/chat/assistant-content.js +7 -0
  21. package/dist/worker/console/chat/chat-event-store.js +82 -11
  22. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  23. package/dist/worker/console/chat/goals.js +300 -0
  24. package/dist/worker/console/chat/pi-runtime.js +285 -8
  25. package/dist/worker/console/chat/provider-error.js +2 -1
  26. package/dist/worker/console/chat/repo-browser.js +94 -12
  27. package/dist/worker/console/chat/routes.js +485 -29
  28. package/dist/worker/console/chat/session-catalog.js +48 -0
  29. package/dist/worker/console/chat/session-store.js +27 -18
  30. package/dist/worker/console/chat/shortcuts.js +7 -0
  31. package/dist/worker/console/chat/sift-bridge.js +1 -0
  32. package/dist/worker/console/chat/turn-process.js +32 -3
  33. package/dist/worker/console/operator-actions.js +16 -2
  34. package/dist/worker/console/operator-selection.js +6 -2
  35. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  36. package/dist/worker/console/pi-plugins.js +100 -0
  37. package/dist/worker/console/routes.js +221 -6
  38. package/dist/worker/console/server.js +172 -42
  39. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Wj-fsQlk.js} +1 -1
  40. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-CkXT-vzQ.js} +1 -1
  41. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-xlfnojWU.js} +1 -1
  42. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BilahvuB.js} +1 -1
  43. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-DqtAVhjz.js} +1 -1
  44. package/dist/worker/console/static/assets/channel-SnQ_u-Td.js +1 -0
  45. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-pAqQXZ5y.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BEWxeq2r.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-CvlbGDTp.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-D0uGd9-f.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-DSdWIteg.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-Dweg7f0v.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-BpZKI_k7.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-IPLknC4m.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-E93k9lVI.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-BrESwkPq.js} +1 -1
  55. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-SGM51NVY.js +1 -0
  56. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-SGM51NVY.js +1 -0
  57. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-DmFHvJkB.js} +1 -1
  58. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CDigXiSL.js} +1 -1
  59. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-B0zhrbaA.js} +1 -1
  60. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DrKybVxm.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-CeUrnbFk.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-BJyunUMi.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-a5TBP73u.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-ChRgBURJ.js} +1 -1
  65. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-CqCnPRSk.js} +1 -1
  66. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BI-UD9_S.js} +1 -1
  67. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-CDLvvkYZ.js} +1 -1
  68. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-Heoo4afj.js} +1 -1
  69. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-kFpOitHd.js} +1 -1
  70. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-FvyZ9WAY.js} +1 -1
  71. package/dist/worker/console/static/assets/index-C80SO8y0.js +437 -0
  72. package/dist/worker/console/static/assets/index-CmJCHmJP.css +1 -0
  73. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-HBzlwtgI.js} +1 -1
  74. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C9Iim5sm.js} +1 -1
  75. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-CJwJTg0S.js} +1 -1
  76. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-CZNJ7S4q.js} +1 -1
  77. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-RZ0AnGi8.js} +1 -1
  78. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-EYSdm-nd.js} +5 -5
  79. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-D0OLECFN.js} +1 -1
  80. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CPTvGbh7.js} +1 -1
  81. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-Ddc1aDea.js} +1 -1
  82. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CWCTCdAt.js} +1 -1
  83. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-CfsaBL8E.js} +1 -1
  84. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-BH2rqGf6.js} +1 -1
  85. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-BZgJ1Til.js} +1 -1
  86. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-DmPr-o-y.js} +1 -1
  87. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CF6HzDh_.js} +1 -1
  88. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-DhS8N_B9.js} +1 -1
  89. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-N56zxwsk.js +1 -0
  90. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-CYNzoiaD.js} +2 -2
  91. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-o47kGgVt.js +8 -0
  92. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-QBICF5qH.js} +1 -1
  93. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-fquBvWcf.js} +1 -1
  94. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-BYaJLlSd.js} +1 -1
  95. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M--ePe_JCJ.js} +1 -1
  96. package/dist/worker/console/static/index.html +2 -2
  97. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  98. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  99. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  100. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +24 -19
  101. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  102. package/dist/worker/console/static-src/operator-chat/useChatThread.js +8 -1
  103. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  104. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  105. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  106. package/dist/worker/console/workspace-context.js +28 -3
  107. package/dist/worker/console/workspace-registry.js +333 -60
  108. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  109. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  110. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  111. package/dist/worker/observe/static/operator-chrome.css +23 -4
  112. package/dist/worker/observe/static/operator-chrome.js +6 -2
  113. package/dist/worker/observe/static/styles.css +87 -25
  114. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  115. package/dist/worker/observe/static/views/dags.js +7 -5
  116. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  117. package/dist/workflows/dag/init-hybrid.js +3 -3
  118. package/docs/architecture/runtime-boundaries.md +2 -1
  119. package/docs/templates/backend-test-dag.json +3 -3
  120. package/package.json +3 -2
  121. package/skills/loop-agent/references/command-reference.md +14 -0
  122. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  123. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  124. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  125. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  126. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  127. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  128. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
@@ -49,16 +49,18 @@
49
49
  .och-inner {
50
50
  box-sizing: border-box;
51
51
  width: 100%;
52
+ min-width: 0;
52
53
  padding-inline: 14px;
53
54
  margin: 0 auto;
54
55
  }
55
56
 
56
- /* Band 1 — 全局带:品牌 / 仓库身份 / 模式 / 对象上下文 / 状态格 */
57
+ /* Band 1 — 全局带:品牌 / 仓库身份 / 模式 / 状态格(对象上下文在下方独立行) */
57
58
  .och-global {
58
59
  position: relative;
59
60
  display: flex;
60
61
  align-items: center;
61
62
  gap: 12px;
63
+ min-width: 0;
62
64
  /* DSH 2026-08-23:约 44px 与内容同色的压缩标题栏。 */
63
65
  height: 44px;
64
66
  }
@@ -318,11 +320,21 @@
318
320
  z-index: 1;
319
321
  }
320
322
 
321
- /* 对象上下文 chip:跨模式选中连续性 + 显式返回路径 */
323
+ /* 对象上下文 chip:跨模式选中连续性 + 显式返回路径。
324
+ * 不放进 44px 全局带:≥720px 模式开关 position:fixed 居中,长 run id
325
+ * 会从左侧把「返回 恢复」顶进滑块底下。独立一行后按钮始终完整可见。 */
326
+ .och-context-row {
327
+ display: flex;
328
+ align-items: center;
329
+ min-width: 0;
330
+ padding: 0 0 8px;
331
+ }
322
332
  .och-context {
323
333
  display: inline-flex;
324
334
  align-items: center;
325
335
  gap: 8px;
336
+ min-width: 0;
337
+ max-width: 100%;
326
338
  min-height: 30px;
327
339
  padding: 0 6px 0 12px;
328
340
  border: 1px solid
@@ -331,26 +343,29 @@
331
343
  background: color-mix(in srgb, var(--och-peach) 42%, var(--och-surface));
332
344
  font-size: 12px;
333
345
  color: var(--och-ink);
334
- white-space: nowrap;
335
346
  }
336
347
  .och-context[hidden] {
337
348
  display: none;
338
349
  }
339
350
  .och-context-kind {
351
+ flex-shrink: 0;
340
352
  color: var(--och-orange-active);
341
353
  font-weight: 700;
342
354
  }
343
355
  .och-context-id {
356
+ flex: 1 1 auto;
357
+ min-width: 0;
344
358
  font-family: var(--och-mono);
345
359
  font-size: 11px;
346
360
  color: var(--och-body);
347
- max-width: 260px;
348
361
  overflow: hidden;
349
362
  text-overflow: ellipsis;
363
+ white-space: nowrap;
350
364
  }
351
365
  .och-context-return {
352
366
  display: inline-flex;
353
367
  align-items: center;
368
+ flex-shrink: 0;
354
369
  gap: 4px;
355
370
  height: 22px;
356
371
  padding: 0 9px;
@@ -360,6 +375,7 @@
360
375
  font-size: 11px;
361
376
  font-weight: 600;
362
377
  text-decoration: none;
378
+ white-space: nowrap;
363
379
  }
364
380
  .och-context-return:hover {
365
381
  background: var(--och-orange-active);
@@ -369,6 +385,7 @@
369
385
  display: inline-flex;
370
386
  align-items: center;
371
387
  justify-content: center;
388
+ flex-shrink: 0;
372
389
  width: 20px;
373
390
  height: 20px;
374
391
  padding: 0;
@@ -401,6 +418,7 @@
401
418
  font-size: 12px;
402
419
  font-variant-numeric: tabular-nums;
403
420
  white-space: nowrap;
421
+ flex-shrink: 0;
404
422
  }
405
423
  .och-fresh-dot {
406
424
  width: 7px;
@@ -425,6 +443,7 @@
425
443
  color: var(--och-body);
426
444
  text-decoration: none;
427
445
  white-space: nowrap;
446
+ flex-shrink: 0;
428
447
  transition: border-color 0.15s ease;
429
448
  }
430
449
  .och-pi:hover {
@@ -653,7 +653,9 @@ export function mountOperatorChrome(root, config = {}) {
653
653
  if (model.context) {
654
654
  const chip = el("div", "och-context");
655
655
  chip.appendChild(el("span", "och-context-kind", model.context.kindLabel));
656
- chip.appendChild(el("span", "och-context-id", model.context.id));
656
+ const idEl = el("span", "och-context-id", model.context.id);
657
+ idEl.title = model.context.id;
658
+ chip.appendChild(idEl);
657
659
  const back = el("a", "och-context-return");
658
660
  back.href = model.context.returnHref;
659
661
  back.appendChild(makeIcon("arrowLeft"));
@@ -675,7 +677,9 @@ export function mountOperatorChrome(root, config = {}) {
675
677
  render();
676
678
  });
677
679
  chip.appendChild(close);
678
- global.appendChild(chip);
680
+ const row = el("div", "och-context-row");
681
+ row.appendChild(chip);
682
+ inner.appendChild(row);
679
683
  }
680
684
 
681
685
  global.appendChild(el("span", "och-spacer"));
@@ -1,4 +1,7 @@
1
- :root {
1
+ html:has(body[data-shell="observe-console"]),
2
+ body[data-shell="observe-console"],
3
+ .inspect-workspace-root {
4
+ color-scheme: light;
2
5
  --canvas: #f7f7f4;
3
6
  --canvas-soft: #fafaf7;
4
7
  --surface: #ffffff;
@@ -38,14 +41,14 @@
38
41
  --transition-base: 220ms ease;
39
42
  --shadow-panel: 0 1px 2px rgb(38 37 30 / 4%), 0 8px 24px rgb(38 37 30 / 3%);
40
43
  --shadow-soft: 0 10px 30px rgb(38 37 30 / 6%);
41
- color-scheme: light;
42
44
  }
43
45
 
44
- * {
46
+ body[data-shell="observe-console"] *,
47
+ .inspect-workspace-root * {
45
48
  box-sizing: border-box;
46
49
  }
47
50
 
48
- html {
51
+ html:has(body[data-shell="observe-console"]) {
49
52
  background:
50
53
  radial-gradient(
51
54
  1200px 480px at 8% -10%,
@@ -62,7 +65,7 @@ html {
62
65
  scroll-behavior: smooth;
63
66
  }
64
67
 
65
- body {
68
+ body[data-shell="observe-console"] {
66
69
  min-width: 320px;
67
70
  margin: 0;
68
71
  min-height: 100vh;
@@ -76,29 +79,46 @@ body {
76
79
  line-height: 1.5;
77
80
  }
78
81
 
79
- button,
80
- input,
81
- select,
82
- textarea {
82
+ body[data-shell="observe-console"] button,
83
+ body[data-shell="observe-console"] input,
84
+ body[data-shell="observe-console"] select,
85
+ body[data-shell="observe-console"] textarea,
86
+ .inspect-workspace-root button,
87
+ .inspect-workspace-root input,
88
+ .inspect-workspace-root select,
89
+ .inspect-workspace-root textarea {
83
90
  font: inherit;
84
91
  }
85
92
 
86
- a {
93
+ body[data-shell="observe-console"] a,
94
+ .inspect-workspace-root a {
87
95
  color: inherit;
88
96
  }
89
97
 
90
- code,
91
- pre,
92
- .output-block,
93
- .dag-node-label,
94
- .dag-node-meta,
95
- .dag-node-status,
96
- .dag-node-table td:nth-child(2),
97
- td:first-child,
98
- .meta-item dd,
99
- .kpi-value,
100
- .risk-total,
101
- .process-timeline-time {
98
+ body[data-shell="observe-console"] code,
99
+ body[data-shell="observe-console"] pre,
100
+ body[data-shell="observe-console"] .output-block,
101
+ body[data-shell="observe-console"] .dag-node-label,
102
+ body[data-shell="observe-console"] .dag-node-meta,
103
+ body[data-shell="observe-console"] .dag-node-status,
104
+ body[data-shell="observe-console"] .dag-node-table td:nth-child(2),
105
+ body[data-shell="observe-console"] td:first-child,
106
+ body[data-shell="observe-console"] .meta-item dd,
107
+ body[data-shell="observe-console"] .kpi-value,
108
+ body[data-shell="observe-console"] .risk-total,
109
+ body[data-shell="observe-console"] .process-timeline-time,
110
+ .inspect-workspace-root code,
111
+ .inspect-workspace-root pre,
112
+ .inspect-workspace-root .output-block,
113
+ .inspect-workspace-root .dag-node-label,
114
+ .inspect-workspace-root .dag-node-meta,
115
+ .inspect-workspace-root .dag-node-status,
116
+ .inspect-workspace-root .dag-node-table td:nth-child(2),
117
+ .inspect-workspace-root td:first-child,
118
+ .inspect-workspace-root .meta-item dd,
119
+ .inspect-workspace-root .kpi-value,
120
+ .inspect-workspace-root .risk-total,
121
+ .inspect-workspace-root .process-timeline-time {
102
122
  font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
103
123
  }
104
124
 
@@ -125,6 +145,30 @@ td:first-child,
125
145
  padding: var(--space-5) 0 56px;
126
146
  }
127
147
 
148
+ .inspect-workspace-root {
149
+ position: relative;
150
+ min-height: 100%;
151
+ color: var(--ink);
152
+ font-family:
153
+ system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB",
154
+ "Noto Sans SC", BlinkMacSystemFont, sans-serif;
155
+ font-size: 14px;
156
+ line-height: 1.5;
157
+ }
158
+
159
+ .inspect-workspace-root .dag-inspector {
160
+ top: 8px;
161
+ right: 8px;
162
+ bottom: 8px;
163
+ width: min(var(--dag-inspector-width, 50%), calc(100% - 16px));
164
+ }
165
+
166
+ .inspect-workspace-root #app {
167
+ width: 100%;
168
+ margin: 0;
169
+ padding: var(--space-4) var(--space-5) var(--space-6);
170
+ }
171
+
128
172
  .breadcrumb {
129
173
  display: flex;
130
174
  min-height: 16px;
@@ -257,7 +301,8 @@ td:first-child,
257
301
  color: var(--body);
258
302
  }
259
303
 
260
- .panel {
304
+ body[data-shell="observe-console"] .panel,
305
+ .inspect-workspace-root .panel {
261
306
  min-width: 0;
262
307
  background: color-mix(in srgb, var(--surface) 96%, transparent);
263
308
  border: 1px solid var(--hairline);
@@ -288,7 +333,8 @@ td:first-child,
288
333
  }
289
334
 
290
335
  .section-heading,
291
- .panel h3 {
336
+ body[data-shell="observe-console"] .panel h3,
337
+ .inspect-workspace-root .panel h3 {
292
338
  margin: 0 0 var(--space-4);
293
339
  color: var(--ink);
294
340
  font-size: 14px;
@@ -1810,6 +1856,15 @@ body.is-resizing-dag-graph {
1810
1856
  color: var(--muted);
1811
1857
  font-size: 11px;
1812
1858
  }
1859
+ .process-timeline-export-bar {
1860
+ display: flex;
1861
+ justify-content: flex-end;
1862
+ padding: 2px 0 var(--space-2);
1863
+ }
1864
+ .process-timeline-export-bar .btn {
1865
+ font-size: 11px;
1866
+ padding: 3px 10px;
1867
+ }
1813
1868
  .process-timeline {
1814
1869
  display: grid;
1815
1870
  gap: 0;
@@ -2526,7 +2581,8 @@ body.is-resizing-dag-graph {
2526
2581
  flex-wrap: wrap;
2527
2582
  gap: 6px;
2528
2583
  }
2529
- .muted {
2584
+ body[data-shell="observe-console"] .muted,
2585
+ .inspect-workspace-root .muted {
2530
2586
  color: var(--muted);
2531
2587
  }
2532
2588
 
@@ -2746,6 +2802,12 @@ body.is-resizing-dag-graph {
2746
2802
  bottom: 12px;
2747
2803
  width: calc(100vw - 24px);
2748
2804
  }
2805
+ .inspect-workspace-root .dag-inspector {
2806
+ width: calc(100% - 16px);
2807
+ top: 8px;
2808
+ right: 8px;
2809
+ bottom: 8px;
2810
+ }
2749
2811
  .dag-inspector-resize-handle {
2750
2812
  display: none;
2751
2813
  }
@@ -24,6 +24,7 @@ import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
24
24
  import { describeDagNodePurpose } from "../dag-node-purpose.js";
25
25
  import { buildPromptRestartCandidateOptions } from "../prompt-restart-candidates.js";
26
26
  import { copyText } from "../copy.js";
27
+ import { navigate } from "../router.js";
27
28
  import {
28
29
  fetchWithWorkspaceScope,
29
30
  uiState,
@@ -47,6 +48,7 @@ import {
47
48
  import {
48
49
  formatSessionEventTime,
49
50
  renderSessionTimeline,
51
+ buildPageStyledDoc,
50
52
  } from "./session-timeline.js";
51
53
  import { createCustomSelect } from "../custom-select.js";
52
54
 
@@ -1671,7 +1673,7 @@ async function renderPromptRestart(content, dagRunId, node) {
1671
1673
  open.className = "prompt-restart-submit is-secondary";
1672
1674
  open.textContent = "打开新 run";
1673
1675
  open.addEventListener("click", () => {
1674
- window.location.hash = `#/dag/${encodeURIComponent(String(newRunId))}`;
1676
+ navigate(`/dag/${encodeURIComponent(String(newRunId))}`);
1675
1677
  });
1676
1678
  row.appendChild(open);
1677
1679
  } else {
@@ -2343,6 +2345,45 @@ function renderNonPiProcess(content, body) {
2343
2345
  ul.appendChild(li);
2344
2346
  }
2345
2347
  content.appendChild(ul);
2348
+ appendProcessExportButton(content, body, body);
2349
+ }
2350
+
2351
+ function downloadTextFile(filename, text) {
2352
+ const blob = new Blob([text], { type: "text/html;charset=utf-8" });
2353
+ const url = URL.createObjectURL(blob);
2354
+ const a = document.createElement("a");
2355
+ a.href = url;
2356
+ a.download = filename;
2357
+ document.body.appendChild(a);
2358
+ a.click();
2359
+ a.remove();
2360
+ URL.revokeObjectURL(url);
2361
+ }
2362
+
2363
+ function appendProcessExportButton(content, body, meta) {
2364
+ if (!body || ((body.commands ?? []).length === 0 && (body.steps ?? []).length === 0)) return;
2365
+ const toolbar = el("div", "process-timeline-export-bar");
2366
+ const btn = el("button", "btn btn-secondary", "导出 HTML");
2367
+ btn.type = "button";
2368
+ btn.addEventListener("click", async () => {
2369
+ let cssText = "";
2370
+ try {
2371
+ const res = await fetch("/inspect/styles.css", { cache: "no-cache" });
2372
+ if (res.ok) cssText = await res.text();
2373
+ } catch {
2374
+ /* 忽略:导出仍会用内联兜底样式 */
2375
+ }
2376
+ // 复用页面真实渲染 DOM,保证导出排版与页面一致。
2377
+ const clone = content.cloneNode(true);
2378
+ const bar = clone.querySelector(".process-timeline-export-bar");
2379
+ if (bar) bar.remove();
2380
+ downloadTextFile(
2381
+ `node-process-${meta?.nodeId || "node"}-${Date.now()}.html`,
2382
+ buildPageStyledDoc(clone.innerHTML, meta ?? {}, cssText),
2383
+ );
2384
+ });
2385
+ toolbar.appendChild(btn);
2386
+ content.insertBefore(toolbar, content.firstChild);
2346
2387
  }
2347
2388
 
2348
2389
  export async function mergeNodeProcess(dagRunId, node) {
@@ -15,6 +15,7 @@ import {
15
15
  DAG_HISTORY_DEFAULT_PAGE_SIZE,
16
16
  navigate,
17
17
  parseRoute,
18
+ inspectPathToCanonicalHref,
18
19
  } from "../router.js";
19
20
  import { fetchDagRunHistory, summarizeFetchError } from "../api.js";
20
21
  import { copyText } from "../copy.js";
@@ -57,7 +58,8 @@ function currentHistoryQuery() {
57
58
  }
58
59
 
59
60
  function syncHash(page, pageSize, { replace = false } = {}) {
60
- const next = buildDagHistoryHash(page, pageSize);
61
+ const path = buildDagHistoryHash(page, pageSize);
62
+ const next = inspectPathToCanonicalHref(path).replace(/^\/#/, "") || "/inspect";
61
63
  const current = (location.hash || "").replace(/^#/, "") || "/";
62
64
  const currentPath = current.split("?")[0];
63
65
  const nextPath = next.split("?")[0];
@@ -72,15 +74,15 @@ function syncHash(page, pageSize, { replace = false } = {}) {
72
74
  location.hash = next;
73
75
  }
74
76
  }
75
- return next;
77
+ return path;
76
78
  }
77
79
  if (replace && typeof history !== "undefined" && history.replaceState) {
78
80
  const base = `${location.pathname}${location.search}`;
79
81
  history.replaceState(null, "", `${base}#${next}`);
80
- return next;
82
+ return path;
81
83
  }
82
- navigate(next);
83
- return next;
84
+ navigate(path);
85
+ return path;
84
86
  }
85
87
 
86
88
  function setControlsDisabled(disabled) {
@@ -30,6 +30,112 @@ export function sessionEventArgs(event) {
30
30
  return args && typeof args === "object" && !Array.isArray(args) ? args : {};
31
31
  }
32
32
 
33
+ function escapeHtml(text) {
34
+ return String(text ?? "")
35
+ .replaceAll("&", "&")
36
+ .replaceAll("<", "&lt;")
37
+ .replaceAll(">", "&gt;")
38
+ .replaceAll('"', "&quot;")
39
+ .replaceAll("'", "&#39;");
40
+ }
41
+
42
+ function downloadTextFile(filename, text) {
43
+ const blob = new Blob([text], { type: "text/html;charset=utf-8" });
44
+ const url = URL.createObjectURL(blob);
45
+ const a = document.createElement("a");
46
+ a.href = url;
47
+ a.download = filename;
48
+ document.body.appendChild(a);
49
+ a.click();
50
+ a.remove();
51
+ URL.revokeObjectURL(url);
52
+ }
53
+
54
+ /** 兜底样式:仅在无法抓取页面 /inspect/styles.css 时使用(与页面 process-timeline 排版对齐)。 */
55
+ const EXPORT_FALLBACK_CSS = `
56
+ :root{--canvas:#f7f7f4;--surface:#ffffff;--canvas-soft:#fafaf7;--surface-muted:#f0efe9;--ink:#26251e;--body:#5a5852;--muted:#807d72;--muted-soft:#a09c92;--hairline:#e6e5e0;--hairline-strong:#cfcdc4;--orange:#f54e00;--orange-active:#d04200;--blue:#3f6ea4;--green:#1f8a65;--amber:#a66a00;--red:#c72d53;--lavender:#e4daf1;--radius-sm:6px;--space-2:8px;--space-3:12px;--space-5:20px}
57
+ body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas);color:var(--body);font-size:13px}
58
+ .process-timeline{display:grid;gap:0;margin:0;padding:var(--space-2) 0;list-style:none}
59
+ .process-timeline-item{display:grid;grid-template-columns:20px minmax(0,1fr);align-items:stretch;min-height:34px}
60
+ .process-timeline-item.has-time{grid-template-columns:20px 92px minmax(0,1fr)}
61
+ .process-timeline-rail{position:relative;display:grid;place-items:center;min-height:34px}
62
+ .process-timeline-rail::before{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline-strong);content:""}
63
+ .process-timeline-item.is-first .process-timeline-rail::before{top:50%}
64
+ .process-timeline-item.is-last .process-timeline-rail::before{bottom:50%}
65
+ .process-timeline-dot{position:relative;z-index:1;width:8px;height:8px;border:2px solid var(--surface);border-radius:50%;background:var(--muted-soft)}
66
+ .process-timeline-lifecycle .process-timeline-dot{background:var(--lavender)}
67
+ .process-timeline-tool-start .process-timeline-dot{background:var(--orange)}
68
+ .process-timeline-tool-end .process-timeline-dot{background:var(--green)}
69
+ .process-timeline-assistant .process-timeline-dot{background:var(--blue)}
70
+ .process-timeline-protocol .process-timeline-dot{background:var(--muted-soft)}
71
+ .process-timeline-item.is-error .process-timeline-dot{background:var(--red)}
72
+ .process-timeline-time{padding:9px 0;color:var(--muted);font-size:10px;font-variant-numeric:tabular-nums}
73
+ .process-timeline-label{min-width:0;padding:8px 0 8px var(--space-2);color:var(--body);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
74
+ .process-timeline-protocol .process-timeline-label{color:var(--muted);font-size:11px}
75
+ .process-timeline-body{min-width:0;padding:4px 0 8px var(--space-2)}
76
+ .process-timeline-card{display:grid;gap:6px;min-width:0}
77
+ .process-timeline-details{min-width:0;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface)}
78
+ .process-timeline-summary{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;cursor:pointer;list-style:none;color:var(--ink);font-size:12px;font-weight:650}
79
+ .process-timeline-summary::-webkit-details-marker{display:none}
80
+ .process-timeline-summary-title{min-width:0;overflow-wrap:anywhere}
81
+ .process-timeline-summary-status{flex-shrink:0;color:var(--muted);font-size:11px;font-weight:700}
82
+ .process-timeline-summary-status.status-succeeded{color:var(--green)}
83
+ .process-timeline-summary-status.status-failed{color:var(--red)}
84
+ .process-timeline-summary-status.status-pending{color:var(--orange)}
85
+ .process-timeline-tool-card.is-error .process-timeline-details{border-color:var(--red);background:var(--canvas-soft)}
86
+ .process-timeline-card-body{display:grid;gap:6px;padding:0 10px 10px}
87
+ .process-timeline-section-label{color:var(--muted);font-size:11px;font-weight:700}
88
+ .process-timeline-code,.process-timeline-thinking-body{margin:0;padding:8px 10px;overflow:auto;border-radius:var(--radius-sm);background:var(--canvas-soft);color:var(--body);font-family:"JetBrains Mono","SFMono-Regular",Consolas,monospace;font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}
89
+ .process-timeline-more{margin-top:2px}
90
+ .process-timeline-more>summary{cursor:pointer;color:var(--blue);font-size:11px;font-weight:650}
91
+ .process-timeline-thinking>summary{background:var(--lavender)}
92
+ .process-timeline-turn-text{display:grid;gap:6px;padding:8px 10px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface)}
93
+ .process-timeline-turn-text .markdown-output{font-size:12px}
94
+ .process-timeline-tool-only-note{margin:0;font-size:12px}
95
+ .process-timeline-turn-tools{display:grid;gap:4px;margin:0;padding:0 0 0 1.1em;color:var(--body);font-size:12px;font-family:"JetBrains Mono","SFMono-Regular",Consolas,monospace;overflow-wrap:anywhere}
96
+ .process-timeline-turn-tools li{padding:2px 0}
97
+ .process-timeline-agent-end-note{margin:0;font-size:11px}
98
+ .markdown-output{display:grid;gap:var(--space-3);color:var(--body);font-size:13px;line-height:1.65}
99
+ .markdown-output>:first-child{margin-top:0}
100
+ .markdown-output>:last-child{margin-bottom:0}
101
+ .markdown-output h1,.markdown-output h2,.markdown-output h3,.markdown-output h4{margin:var(--space-5) 0 var(--space-2);color:var(--ink);line-height:1.3}
102
+ .markdown-output h1{font-size:21px}.markdown-output h2{font-size:18px}.markdown-output h3{font-size:16px}.markdown-output h4{font-size:14px}
103
+ .markdown-output p,.markdown-output blockquote,.markdown-output ul,.markdown-output ol{margin:0;overflow-wrap:anywhere}
104
+ .markdown-output ul,.markdown-output ol{padding-left:22px}
105
+ .markdown-output blockquote{padding-left:12px}
106
+ `;
107
+
108
+ const EXPORT_SHELL_CSS = `
109
+ body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas)}
110
+ .export-shell{max-width:1000px;margin:0 auto;padding:24px 28px;background:var(--surface);min-height:100vh;box-shadow:0 0 0 1px var(--hairline)}
111
+ .export-head h1{font-size:15px;color:var(--ink);margin:0;font-weight:700}
112
+ .export-sub{color:var(--muted);font-size:12px;margin:4px 0 16px}
113
+ .export-controls{display:flex;justify-content:flex-start;margin:0 0 12px}
114
+ .export-toggle{min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--body);font-size:11px;font-weight:600;cursor:pointer;user-select:none}
115
+ .export-toggle:hover,.export-toggle:focus-visible{border-color:var(--orange);color:var(--orange-active);outline:none}
116
+ body.hide-protocol .process-timeline-item.process-timeline-protocol{display:none}
117
+ `;
118
+
119
+ /**
120
+ * 把页面渲染出的时间线 innerHTML 封装成自包含 HTML。
121
+ * @param {string} bodyHTML 页面渲染的 .process-timeline 等真实 DOM 序列化结果
122
+ * @param {{nodeId?:string,dagRunId?:string}} meta
123
+ * @param {string} [cssText] 页面 /inspect/styles.css 原文;缺失时用兜底样式
124
+ */
125
+ export function buildPageStyledDoc(bodyHTML, meta = {}, cssText = "") {
126
+ const now = new Date().toLocaleString();
127
+ const title = meta.nodeId ? `节点执行过程 · ${meta.nodeId}` : "节点执行过程";
128
+ const style = cssText.trim()
129
+ ? `${cssText}\n\n/* 导出外壳 */\n${EXPORT_SHELL_CSS}`
130
+ : `${EXPORT_FALLBACK_CSS}\n${EXPORT_SHELL_CSS}`;
131
+ // 有协议行时,在左上角提供一个「展开/隐藏原始协议事件」按钮(与页面一致),默认隐藏。
132
+ const hasProtocol = bodyHTML.includes("process-timeline-protocol");
133
+ const controls = hasProtocol
134
+ ? `<div class="export-controls"><button type="button" id="exportToggleProtocol" class="export-toggle">展开原始协议事件</button></div><script>(function(){var b=document.body,t=document.getElementById('exportToggleProtocol'),on=false;function apply(){b.classList.toggle('hide-protocol',!on);t.textContent=on?'隐藏原始协议事件':'展开原始协议事件';}if(t){t.addEventListener('click',function(){on=!on;apply();});apply();}})();</script>`
135
+ : "";
136
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${style}</style></head><body><div class="export-shell"><div class="export-head"><h1>${escapeHtml(title)}</h1></div><p class="export-sub">DAG ${escapeHtml(meta.dagRunId ?? "")} · 导出于 ${escapeHtml(now)}</p>${controls}<div class="export-timeline">${bodyHTML}</div></div></body></html>`;
137
+ }
138
+
33
139
  export function sessionEventResult(event) {
34
140
  return normalizeSessionEventFields(event).result;
35
141
  }
@@ -742,6 +848,35 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
742
848
  parent.scrollLeft = scrollLeft;
743
849
  });
744
850
  toolbar.appendChild(protocolToggle);
851
+ const exportBtn = el("button", "btn btn-secondary", "导出 HTML");
852
+ exportBtn.type = "button";
853
+ exportBtn.addEventListener("click", async () => {
854
+ let cssText = "";
855
+ try {
856
+ const res = await fetch("/inspect/styles.css", { cache: "no-cache" });
857
+ if (res.ok) cssText = await res.text();
858
+ } catch {
859
+ /* 忽略:导出仍会用内联兜底样式 */
860
+ }
861
+ // 复用页面真实渲染(含原始协议事件),保证导出的排版与页面一致。
862
+ const prevShow = uiState.sessionTimelineShowProtocol;
863
+ uiState.sessionTimelineShowProtocol = true;
864
+ const tmp = document.createElement("div");
865
+ tmp.className = "process-timeline-scroll";
866
+ renderSessionTimeline(tmp, nodeId, options);
867
+ const toolbar = tmp.querySelector(".process-timeline-toolbar");
868
+ if (toolbar) toolbar.remove();
869
+ const innerHTML = tmp.innerHTML;
870
+ uiState.sessionTimelineShowProtocol = prevShow;
871
+ downloadTextFile(
872
+ `node-process-${nodeId || "node"}-${Date.now()}.html`,
873
+ buildPageStyledDoc(innerHTML, {
874
+ nodeId,
875
+ dagRunId: uiState.currentDagRunId ?? "",
876
+ }, cssText),
877
+ );
878
+ });
879
+ toolbar.appendChild(exportBtn);
745
880
  content.appendChild(toolbar);
746
881
 
747
882
  const timelineItems = buildRichTimelineItems(visible.events, {
@@ -4520,7 +4520,7 @@ async function buildBackendTestHybridDag(sources) {
4520
4520
  "Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
4521
4521
  "For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
4522
4522
  "Mandatory module index: include a `## Module Index` table in the plan artifact that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
4523
- "Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
4523
+ "Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional. Before returning, expand every declared partition into its complete deterministic exact slot ID set: one `TP-<Partition ID>-<VALUE-TOKEN>` per Domain value, `TP-<Partition ID>-OMITTED` only for an optional axis, and exactly one `TP-<Partition ID>-NOT-IN-SET`. Every expanded slot ID must appear verbatim in the binding Rule's `Required Test Points` cell and be assigned to concrete Case IDs in that same Coverage Matrix row; ordinary alias/family Test Points do not replace this inventory. Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
4524
4524
  "Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
4525
4525
  ...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
4526
4526
  intake.boundedSourceContext,
@@ -4603,7 +4603,7 @@ async function buildBackendTestHybridDag(sources) {
4603
4603
  "Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
4604
4604
  'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol. Evidence-only meta cases that exist solely for non-executable assertion/cross-cutting process evidence may declare `脚本:无` and `primary symbol:无` with empty `变体测试点`, and must not invent a business pytest item.',
4605
4605
  "Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
4606
- "Scenario Partition slots: when README declares `## Scenario Partitions`, every slot of each declared partition MUST appear in this module's Cases as exactly one variant Test Point with the deterministic ID `TP-<Partition ID>-<VALUE-TOKEN>` (each-value), `TP-<Partition ID>-OMITTED` (optional axis only) and exactly one `TP-<Partition ID>-NOT-IN-SET` complement slot with `intent=enum-invalid`. Example: Partition ID `SP-GET-API-RESOURCE-NOTES-STATUS` → `TP-SP-GET-API-RESOURCE-NOTES-STATUS-ACTIVE`. Slot IDs copy the declared Partition ID exactly; never drop the HTTP method, invent, merge, renumber or split slot IDs. Prefer ONE Case per partition with a parameter table over duplicated Cases per value. The not-in-set slot value must be a concrete literal absent from the Domain (e.g. `UNKNOWN_TYPE`) and its expected result must come from the bound source — when Expected by Slot is GAP, the Case states the expectation as GAP evidence, never a guessed 空列表/400. Never create cross-axis combination variants beyond the single documented nominal.",
4606
+ "Scenario Partition slots: when README declares `## Scenario Partitions`, every slot of each declared partition MUST appear in this module's Cases as exactly one variant Test Point with the deterministic ID `TP-<Partition ID>-<VALUE-TOKEN>` (each-value), `TP-<Partition ID>-OMITTED` (optional axis only) and exactly one `TP-<Partition ID>-NOT-IN-SET` complement slot with `intent=enum-invalid`. Example: Partition ID `SP-GET-API-RESOURCE-NOTES-STATUS` → `TP-SP-GET-API-RESOURCE-NOTES-STATUS-ACTIVE`. Slot IDs copy the declared Partition ID exactly; never drop the HTTP method, invent, merge, renumber or split slot IDs. Before returning, derive the complete exact slot set from every applicable Scenario Partitions row and verify that the module Cases declare and bind every slot assigned by the Coverage Matrix; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Prefer ONE Case per partition with a parameter table over duplicated Cases per value. The not-in-set slot value must be a concrete literal absent from the Domain (e.g. `UNKNOWN_TYPE`) and its expected result must come from the bound source — when Expected by Slot is GAP, the Case states the expectation as GAP evidence, never a guessed 空列表/400. Never create cross-axis combination variants beyond the single documented nominal.",
4607
4607
  "For every variant Test Point, write its machine-checkable `场景意图: <TP-ID>; operation=...; target=...; intent=...` line inside that same Case body/自动化映射. Never collect Scenario Intent lines in a file-level appendix, implementation-details block, or another Case; local TP ownership is mandatory.",
4608
4608
  "Every Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
4609
4609
  "In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`, plus a deterministic payload contract. For operations without a request body write `Payload Contract: none`. Otherwise write `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum` (write `none` when there is no enum); nested fields use dot paths such as `approver.name`. Each Case describes exactly one target request payload contract: put every payload label on its own list line, never concatenate multiple operations or setup POST/PUT contracts into one label line, and never repeat a `Payload Contract:` token inside explanatory prose/details after the machine-readable line. Values must come only from bound API/DTO evidence, never guesses. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For evidence-only meta Cases with no executable business journey, write `脚本:无` and `primary symbol:无`, keep `变体测试点:无`, and place process evidence only in assertion/cross-cutting lists. If the bound contract only says an identifier is returned/present, do not declare a concrete identifier type. If a 404 Case needs a nonexistent path identifier but its syntax/type is unspecified, define a create-delete-derived valid identifier journey instead of an arbitrary UUID/text placeholder. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
@@ -4648,7 +4648,7 @@ async function buildBackendTestHybridDag(sources) {
4648
4648
  "Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, then perform an exact-set check: each Case's `### 测试点` set must equal (not merely contain) the union of those three binding lists; delete stale/legacy aliases and ensure every binding-list Test Point is present, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
4649
4649
  "This is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; every label must occupy its own machine-readable list line, and a Case must never concatenate target/setup operations or multiple `Payload Contract` tokens onto one line, and explanatory prose/details must not repeat any `Payload Contract:` token; never infer missing keys or enum values. A target GET/DELETE operation with no request body must remain `Payload Contract: none` even when its setup journey performs POST/PUT with a DTO; setup payloads never redefine the target Case payload contract. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.",
4650
4650
  "For affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints. Do not invent a concrete identifier type when the source only requires presence; for a missing-resource 404 path with unspecified identifier syntax/type, synchronize the Case to a create-delete-derived valid identifier journey rather than an arbitrary UUID/text placeholder.",
4651
- "Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
4651
+ "Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
4652
4652
  "Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in the run-owned plan or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
4653
4653
  "Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
4654
4654
  ...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
@@ -207,7 +207,8 @@ Runtime 变更另需 `npm run typecheck` 及对应 targeted Vitest(见 exec pl
207
207
  | --- | --- | --- |
208
208
  | OpenCode transient-retry | `.opencode/plugins/loop-agent-transient-retry.js`(init surface `generated`) | 仅瞬态 UnknownError resume;busy/retry 门控 + backoff;overflow/401/配额/取消/业务标 permanent(`plugin-ignore-permanent-error`),**不** compact |
209
209
  | OpenCode overflow-compact | `.opencode/plugins/loop-agent-context-overflow-compact.js`(init surface `generated`) | 仅 context overflow → `session.compact`(无则 `summarize`)+ 有限 `promptAsync` 续接;`MAX_OVERFLOW_RECOVERIES ≤ 2`;session 锁;非 overflow 不动作 |
210
- | Pi project extension | `.pi/extensions/loop-agent-context-overflow.js`(init surface `generated`) | `message_end` 上将 overflow `errorMessage` 幂等改写为 `context_length_exceeded:` 前缀,触发 Pi 原生 compact/retry;**不**写 `~/.pi` settings / compaction / 模型窗口 |
210
+ | Pi project extension | `.pi/extensions/loop-agent-context-overflow.js`(init surface `generated`) | `message_end` 上将 overflow `errorMessage` 幂等改写为 `context_length_exceeded:` 前缀,触发 Pi 原生 compact/retry;分类器与控制器共享 P1–P10 / N1–N5 语料(含嵌套 JSON);**不**写 `~/.pi` settings / compaction / 模型窗口,**不**投影 sift native |
211
+ | Console / DAG 运行时 | npm 内置 inline extension + 可选 `@agent-context/sift` | 默认开启 L0/L1/L2;`HARNESS_CONTEXT_PRESSURE_MODE=off` 关 L0/L2;`HARNESS_CONSOLE_SIFT` / `HARNESS_DAG_SIFT` 的 `0\|false\|off` 关 L1。DAG 不做 streaming 主动 compact。不进入目标项目 init 文件 |
211
212
  | Pi 用户配置 | `~/.pi/agent/settings.json` | **不**进入项目 `.harness/init-surface.json` hash;仅 `--client-recovery=user` 可字段级补缺并原子写;`auto`/`project`/`off` 与 `check-update` 默认零写 home;读取时只有 `ENOENT` 视为缺文件,其他 I/O 错误 fail closed |
212
213
  | CLI mode | `--client-recovery=auto\|project\|user\|off`(默认 `auto`) | `auto`/`project` 安装上述三份项目产物;`user` = 三产物 + 显式 Pi retry 合并;`off` 全跳过 |
213
214
  | Ownership | recorded sha256 + apply-safe | 三份 generated 缺失可补、与 recorded hash 一致可升级;用户改过 → model merge / human decision,禁止静默覆盖 |