@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
@@ -0,0 +1,328 @@
1
+ /** Shown near the preview header when window.open is blocked or Blob/URL creation fails. */
2
+ export const PREVIEW_OPEN_POPUP_BLOCKED = "无法在浏览器中打开:弹窗被拦截";
3
+ export const PREVIEW_TRUNCATION_NOTICE = "内容已截断,以下为已加载片段。";
4
+ /** Window features that force a real OS window instead of a browser tab.
5
+ * Do not include `noopener` here: browsers then return null even on success. */
6
+ export function previewOpenWindowFeatures(input) {
7
+ const availWidth = input?.availWidth ?? 1440;
8
+ const availHeight = input?.availHeight ?? 900;
9
+ const width = Math.min(1280, Math.max(880, Math.floor(availWidth * 0.82)));
10
+ const height = Math.min(960, Math.max(640, Math.floor(availHeight * 0.88)));
11
+ return [
12
+ "popup=yes",
13
+ `width=${width}`,
14
+ `height=${height}`,
15
+ "left=72",
16
+ "top=36",
17
+ "menubar=yes",
18
+ "toolbar=yes",
19
+ "location=yes",
20
+ "status=yes",
21
+ "resizable=yes",
22
+ "scrollbars=yes",
23
+ ].join(",");
24
+ }
25
+ export function escapeHtml(text) {
26
+ return text
27
+ .replaceAll("&", "&")
28
+ .replaceAll("<", "&lt;")
29
+ .replaceAll(">", "&gt;")
30
+ .replaceAll('"', "&quot;")
31
+ .replaceAll("'", "&#39;");
32
+ }
33
+ function normalizedLanguage(language) {
34
+ return language.trim().toLowerCase();
35
+ }
36
+ export function isHtmlPreviewLanguage(language) {
37
+ const lang = normalizedLanguage(language);
38
+ return lang === "html" || lang === "htm";
39
+ }
40
+ export function isMarkdownPreviewLanguage(language) {
41
+ const lang = normalizedLanguage(language);
42
+ return lang === "md" || lang === "markdown";
43
+ }
44
+ function formatTextBody(preview) {
45
+ if (normalizedLanguage(preview.language) !== "json")
46
+ return preview.content;
47
+ try {
48
+ return JSON.stringify(JSON.parse(preview.content), null, 2);
49
+ }
50
+ catch {
51
+ return preview.content;
52
+ }
53
+ }
54
+ function isSafeHttpHref(href) {
55
+ return /^https?:\/\//i.test(href.trim());
56
+ }
57
+ function renderMarkdownInline(text) {
58
+ const withCode = [];
59
+ const coded = text.replace(/`([^`]+)`/g, (_all, code) => {
60
+ withCode.push(`<code>${code}</code>`);
61
+ return `\u0000${withCode.length - 1}\u0000`;
62
+ });
63
+ const linked = coded.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_all, label, href) => {
64
+ if (!isSafeHttpHref(href))
65
+ return label;
66
+ return `<a href="${href}" rel="noopener noreferrer" target="_blank">${label}</a>`;
67
+ });
68
+ const emphasized = linked
69
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
70
+ .replace(/__([^_]+)__/g, "<strong>$1</strong>")
71
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>");
72
+ return emphasized.replace(/\u0000(\d+)\u0000/g, (_all, idx) => {
73
+ return withCode[Number(idx)] ?? "";
74
+ });
75
+ }
76
+ function flushParagraph(lines) {
77
+ if (!lines.length)
78
+ return "";
79
+ return `<p>${renderMarkdownInline(lines.join(" "))}</p>`;
80
+ }
81
+ function renderMarkdownBlocks(escaped) {
82
+ const lines = escaped.replaceAll("\r\n", "\n").split("\n");
83
+ const out = [];
84
+ let paragraph = [];
85
+ let listKind = null;
86
+ const closeList = () => {
87
+ if (listKind) {
88
+ out.push(listKind === "ul" ? "</ul>" : "</ol>");
89
+ listKind = null;
90
+ }
91
+ };
92
+ const closeParagraph = () => {
93
+ if (paragraph.length) {
94
+ out.push(flushParagraph(paragraph));
95
+ paragraph = [];
96
+ }
97
+ };
98
+ for (const line of lines) {
99
+ const heading = /^(#{1,6})\s+(.+)$/.exec(line);
100
+ if (heading) {
101
+ closeParagraph();
102
+ closeList();
103
+ const level = heading[1].length;
104
+ out.push(`<h${level}>${renderMarkdownInline(heading[2])}</h${level}>`);
105
+ continue;
106
+ }
107
+ if (/^---+$/.test(line.trim()) || /^\*\*\*+$/.test(line.trim())) {
108
+ closeParagraph();
109
+ closeList();
110
+ out.push("<hr>");
111
+ continue;
112
+ }
113
+ const quote = /^&gt;\s?(.*)$/.exec(line);
114
+ if (quote) {
115
+ closeParagraph();
116
+ closeList();
117
+ out.push(`<blockquote><p>${renderMarkdownInline(quote[1] ?? "")}</p></blockquote>`);
118
+ continue;
119
+ }
120
+ const ul = /^[-*]\s+(.+)$/.exec(line);
121
+ if (ul) {
122
+ closeParagraph();
123
+ if (listKind !== "ul") {
124
+ closeList();
125
+ out.push("<ul>");
126
+ listKind = "ul";
127
+ }
128
+ out.push(`<li>${renderMarkdownInline(ul[1])}</li>`);
129
+ continue;
130
+ }
131
+ const ol = /^\d+\.\s+(.+)$/.exec(line);
132
+ if (ol) {
133
+ closeParagraph();
134
+ if (listKind !== "ol") {
135
+ closeList();
136
+ out.push("<ol>");
137
+ listKind = "ol";
138
+ }
139
+ out.push(`<li>${renderMarkdownInline(ol[1])}</li>`);
140
+ continue;
141
+ }
142
+ if (line.trim() === "") {
143
+ closeParagraph();
144
+ closeList();
145
+ continue;
146
+ }
147
+ closeList();
148
+ paragraph.push(line);
149
+ }
150
+ closeParagraph();
151
+ closeList();
152
+ return out.join("\n");
153
+ }
154
+ /** Conservative Markdown → HTML for the detached reading tab. Input is escaped first. */
155
+ export function renderMarkdownLite(source) {
156
+ const escaped = escapeHtml(source);
157
+ const chunks = [];
158
+ const fence = /```([^\n`]*)\n([\s\S]*?)```/g;
159
+ let last = 0;
160
+ let match;
161
+ while ((match = fence.exec(escaped))) {
162
+ if (match.index > last) {
163
+ chunks.push(renderMarkdownBlocks(escaped.slice(last, match.index)));
164
+ }
165
+ chunks.push(`<pre class="md-code"><code>${match[2] ?? ""}</code></pre>`);
166
+ last = match.index + match[0].length;
167
+ }
168
+ if (last < escaped.length) {
169
+ chunks.push(renderMarkdownBlocks(escaped.slice(last)));
170
+ }
171
+ return chunks.filter(Boolean).join("\n");
172
+ }
173
+ function readingPageCss() {
174
+ return `
175
+ :root { color-scheme: light dark; }
176
+ html, body { margin: 0; min-height: 100%; background: #f7f5f0; color: #1f1b16; }
177
+ body { font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
178
+ .bar { position: sticky; top: 0; z-index: 1; display: flex; gap: 12px; align-items: center; padding: 10px 16px 10px 20px; border-bottom: 1px solid #e4ddd2; background: #fffdf8; }
179
+ .bar .meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
180
+ .bar strong { font-size: 13px; font-weight: 650; word-break: break-all; }
181
+ .bar .kind { color: #6b6258; font-size: 12px; }
182
+ .modes { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid #e4ddd2; border-radius: 8px; background: #f3eee6; }
183
+ .modes button { appearance: none; border: 0; background: transparent; color: #6b6258; font: 12px/1.2 inherit; padding: 5px 10px; border-radius: 6px; cursor: pointer; }
184
+ .modes button:hover { color: #1f1b16; }
185
+ .modes button.is-active { background: #fff; color: #1f1b16; font-weight: 600; box-shadow: 0 1px 2px rgb(0 0 0 / 8%); }
186
+ .notice { margin: 0; padding: 8px 20px; background: #fff6d8; color: #5c4b00; font-size: 13px; }
187
+ .pane { display: none; }
188
+ .pane.is-active { display: block; }
189
+ .doc { max-width: 52rem; margin: 0 auto; padding: 32px 24px 96px; }
190
+ .doc.md h1, .doc.md h2, .doc.md h3, .doc.md h4 { line-height: 1.25; margin: 1.4em 0 0.5em; }
191
+ .doc.md h1 { font-size: 2rem; }
192
+ .doc.md h2 { font-size: 1.45rem; }
193
+ .doc.md p { margin: 0.8em 0; }
194
+ .doc.md ul, .doc.md ol { margin: 0.6em 0; padding-left: 1.4em; }
195
+ .doc.md blockquote { margin: 0.8em 0; padding: 0.2em 0.9em; border-left: 3px solid #d9cfc0; color: #4a433c; }
196
+ .doc.md hr { border: 0; border-top: 1px solid #e4ddd2; margin: 1.6em 0; }
197
+ .doc.md a { color: #0b57d0; }
198
+ .doc.md code, .md-code, .source { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
199
+ .doc.md :not(pre) > code { padding: 0.1em 0.35em; border-radius: 4px; background: #efe8dc; }
200
+ .md-code, .source { margin: 1em 0; padding: 14px 16px; overflow: auto; white-space: pre-wrap; background: #fff; border: 1px solid #e4ddd2; border-radius: 8px; }
201
+ .frame { margin: 0; padding: 0; }
202
+ iframe.page { display: block; width: 100%; height: calc(100vh - 53px); border: 0; background: #fff; }
203
+ `.trim();
204
+ }
205
+ function formattedSource(preview) {
206
+ return escapeHtml(formatTextBody(preview));
207
+ }
208
+ function previewPane(preview) {
209
+ if (isHtmlPreviewLanguage(preview.language)) {
210
+ return `<div class="frame"><iframe class="page" sandbox="" srcdoc="${escapeHtml(preview.content)}"></iframe></div>`;
211
+ }
212
+ if (isMarkdownPreviewLanguage(preview.language)) {
213
+ return `<article class="doc md">${renderMarkdownLite(preview.content)}</article>`;
214
+ }
215
+ return `<article class="doc"><pre class="source">${formattedSource(preview)}</pre></article>`;
216
+ }
217
+ function sourcePane(preview) {
218
+ return `<article class="doc"><pre class="source">${formattedSource(preview)}</pre></article>`;
219
+ }
220
+ function readingPageScript() {
221
+ return `<script>
222
+ (function () {
223
+ var previewBtn = document.getElementById("view-preview");
224
+ var sourceBtn = document.getElementById("view-source");
225
+ var previewPane = document.getElementById("pane-preview");
226
+ var sourcePane = document.getElementById("pane-source");
227
+ if (!previewBtn || !sourceBtn || !previewPane || !sourcePane) return;
228
+ function show(mode) {
229
+ var source = mode === "source";
230
+ previewBtn.classList.toggle("is-active", !source);
231
+ sourceBtn.classList.toggle("is-active", source);
232
+ previewBtn.setAttribute("aria-pressed", String(!source));
233
+ sourceBtn.setAttribute("aria-pressed", String(source));
234
+ previewPane.classList.toggle("is-active", !source);
235
+ sourcePane.classList.toggle("is-active", source);
236
+ }
237
+ previewBtn.addEventListener("click", function () { show("preview"); });
238
+ sourceBtn.addEventListener("click", function () { show("source"); });
239
+ })();
240
+ </script>`;
241
+ }
242
+ /** Build the Blob document HTML from the already-loaded preview snapshot. */
243
+ export function buildPreviewBrowserDocument(preview) {
244
+ const title = escapeHtml(preview.path);
245
+ const notice = preview.truncated
246
+ ? `<p class="notice">${escapeHtml(PREVIEW_TRUNCATION_NOTICE)}</p>`
247
+ : "";
248
+ const lang = normalizedLanguage(preview.language);
249
+ return `<!doctype html>
250
+ <html lang="zh-CN">
251
+ <head>
252
+ <meta charset="utf-8">
253
+ <meta name="viewport" content="width=device-width, initial-scale=1">
254
+ <title>${title}</title>
255
+ <style>
256
+ ${readingPageCss()}
257
+ </style>
258
+ </head>
259
+ <body>
260
+ <header class="bar">
261
+ <div class="meta">
262
+ <strong>${title}</strong>
263
+ <span class="kind">${escapeHtml(lang || "text")}${preview.truncated ? " · 已截断" : ""}</span>
264
+ </div>
265
+ <div class="modes" role="group" aria-label="查看模式">
266
+ <button type="button" id="view-preview" class="is-active" aria-pressed="true">预览</button>
267
+ <button type="button" id="view-source" aria-pressed="false">查看原文</button>
268
+ </div>
269
+ </header>
270
+ ${notice}
271
+ <div id="pane-preview" class="pane is-active">${previewPane(preview)}</div>
272
+ <div id="pane-source" class="pane">${sourcePane(preview)}</div>
273
+ ${readingPageScript()}
274
+ </body>
275
+ </html>`;
276
+ }
277
+ function screenAvail() {
278
+ const screen = globalThis.screen;
279
+ return {
280
+ availWidth: screen?.availWidth,
281
+ availHeight: screen?.availHeight,
282
+ };
283
+ }
284
+ function defaultHost() {
285
+ const g = globalThis;
286
+ if (typeof g.open !== "function") {
287
+ throw new Error("window.open is unavailable");
288
+ }
289
+ return {
290
+ open: g.open.bind(g),
291
+ createObjectURL: g.URL.createObjectURL.bind(g.URL),
292
+ revokeObjectURL: g.URL.revokeObjectURL.bind(g.URL),
293
+ };
294
+ }
295
+ /**
296
+ * Open the current preview in a new OS window from already-loaded
297
+ * path/content/language/truncated. Does not fetch the repo file API again.
298
+ *
299
+ * Width/height window features ask the browser for a real window, not a tab.
300
+ * Do not pass `noopener` as a feature: browsers then return null even on
301
+ * success. Detach via `opener = null` after a real handle is obtained. Keep
302
+ * the Blob URL alive — revoking immediately blanks the page.
303
+ */
304
+ export function openPreviewInBrowser(preview, host = defaultHost(), features = previewOpenWindowFeatures(screenAvail())) {
305
+ let objectUrl;
306
+ try {
307
+ const documentHtml = buildPreviewBrowserDocument(preview);
308
+ const blob = new Blob([documentHtml], { type: "text/html;charset=utf-8" });
309
+ objectUrl = host.createObjectURL(blob);
310
+ const opened = host.open(objectUrl, "_blank", features);
311
+ if (!opened) {
312
+ host.revokeObjectURL(objectUrl);
313
+ return { ok: false, error: PREVIEW_OPEN_POPUP_BLOCKED };
314
+ }
315
+ try {
316
+ opened.opener = null;
317
+ }
318
+ catch {
319
+ // Some hosts expose a frozen handle; the window is still open.
320
+ }
321
+ return { ok: true };
322
+ }
323
+ catch {
324
+ if (objectUrl)
325
+ host.revokeObjectURL(objectUrl);
326
+ return { ok: false, error: PREVIEW_OPEN_POPUP_BLOCKED };
327
+ }
328
+ }
@@ -292,9 +292,13 @@ function assertControllerIdentityUnchangedBeforeSpawn(identity) {
292
292
  }
293
293
  }
294
294
  function controllerIdentityDriftError(message, cause) {
295
- return new Error(`controller identity changed before spawn: ${message}`, {
296
- cause,
297
- });
295
+ // Preserve the raw drift detail verbatim; the actionable suffix serves
296
+ // every consumer surface — operation errorMessage, SSE events, and agent/
297
+ // API callers that never reach the UI translation layer — with the exact
298
+ // recovery step: the running Console holds a stale controller fingerprint
299
+ // after rebuilds or commits touching fingerprint-scoped paths
300
+ // (package.json, bin/**, dist/**, skills/**).
301
+ return new Error(`controller identity changed before spawn: ${message}. Fix: restart the Console process so it re-resolves the current build, then retry the operation; avoid rebuilding or committing fingerprint-scoped files (package.json, bin/**, dist/**, skills/**) mid-session.`, { cause });
298
302
  }
299
303
  /**
300
304
  * Detect whether loopAgentBin is a Node-like executable and baseArgs
@@ -2,7 +2,8 @@ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
4
4
  import { resolveDagNodeSkills } from "../../workflows/dag/skills.js";
5
- import { resolveDagNodePromptRedacted, resolveDagRunJson, } from "./dag-run-artifacts.js";
5
+ import { resolveDagNodePromptRedacted, resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
6
+ import { namedShellHandlers } from "./shell-handler-keys.js";
6
7
  export const NODE_INPUT_SCHEMA_VERSION = 1;
7
8
  export const NODE_INPUT_PREVIEW_MAX_BYTES = 64 * 1024;
8
9
  const SHELL_GATE_KEYS = [
@@ -33,6 +34,7 @@ function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
33
34
  executorInput: null,
34
35
  boundaries: null,
35
36
  promptFingerprint: null,
37
+ piExtensionRunFacts: null,
36
38
  warnings,
37
39
  };
38
40
  }
@@ -137,7 +139,7 @@ function resolveModelHint(spec, task) {
137
139
  }
138
140
  return null;
139
141
  }
140
- function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warnings) {
142
+ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings) {
141
143
  const defaults = spec.defaults && typeof spec.defaults === "object"
142
144
  ? spec.defaults
143
145
  : undefined;
@@ -216,6 +218,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
216
218
  : null,
217
219
  failFast: typeof shellRaw.failFast === "boolean" ? shellRaw.failFast : null,
218
220
  gates: shellGateNames(shellRaw),
221
+ handlers: namedShellHandlers(shellRaw),
219
222
  }
220
223
  : null;
221
224
  const staticPreview = staticRaw && typeof staticRaw.resultMarkdown === "string"
@@ -268,6 +271,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
268
271
  kind,
269
272
  skills,
270
273
  modelHint: resolveModelHint(spec, task),
274
+ piExtensions: redactStringList(task.piExtensions),
271
275
  shell,
272
276
  static: staticBlock,
273
277
  },
@@ -284,9 +288,72 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
284
288
  : null,
285
289
  },
286
290
  promptFingerprint: fingerprint,
291
+ piExtensionRunFacts,
287
292
  warnings,
288
293
  };
289
294
  }
295
+ /**
296
+ * Load run-time Pi extension facts from <node>/pi-extensions.json (plan
297
+ * 2026-08-22 R2). Returns null when the node declares no piExtensions
298
+ * (no expected artifact). Never throws: unreadable/corrupt artifacts degrade
299
+ * to parseError stubs so the node-input endpoint stays 200.
300
+ */
301
+ async function loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declared, warnings) {
302
+ if (declared.length === 0)
303
+ return null;
304
+ const artifact = path.posix.join(nodeId, "pi-extensions.json");
305
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [
306
+ nodeId,
307
+ "pi-extensions.json",
308
+ ]);
309
+ if (!resolved.ok) {
310
+ if (resolved.reason === "ambiguous") {
311
+ warnings.push("Ambiguous pi-extensions.json lifecycle copies.");
312
+ }
313
+ return {
314
+ artifact,
315
+ exists: false,
316
+ requested: [...declared],
317
+ resolved: [],
318
+ missing: [],
319
+ };
320
+ }
321
+ const empty = {
322
+ artifact,
323
+ exists: false,
324
+ requested: [...declared],
325
+ resolved: [],
326
+ missing: [],
327
+ };
328
+ try {
329
+ const raw = JSON.parse(await readFile(resolved.result.absolutePath, "utf-8"));
330
+ const resolvedList = Array.isArray(raw.resolved) ? raw.resolved : [];
331
+ const missingList = Array.isArray(raw.missing) ? raw.missing : [];
332
+ const projected = resolvedList
333
+ .filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
334
+ .map((entry) => ({
335
+ id: redactSecrets(String(entry.id ?? "")),
336
+ packageName: redactSecrets(String(entry.packageName ?? "")),
337
+ version: redactSecrets(String(entry.version ?? "")),
338
+ }));
339
+ const missingProjected = missingList
340
+ .filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
341
+ .map((entry) => ({
342
+ id: redactSecrets(String(entry.id ?? "")),
343
+ reason: redactSecrets(String(entry.reason ?? "")),
344
+ }));
345
+ return {
346
+ artifact,
347
+ exists: true,
348
+ requested: redactStringList(raw.requested ?? declared),
349
+ resolved: projected,
350
+ missing: missingProjected,
351
+ };
352
+ }
353
+ catch {
354
+ return { ...empty, parseError: "read-failed" };
355
+ }
356
+ }
290
357
  async function loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings) {
291
358
  const resolved = resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId);
292
359
  if (!resolved.ok) {
@@ -415,8 +482,12 @@ export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
415
482
  }
416
483
  const warnings = [];
417
484
  const fingerprint = await loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings);
485
+ const declaredPiExtensions = Array.isArray(task.piExtensions)
486
+ ? task.piExtensions.filter((item) => typeof item === "string")
487
+ : [];
488
+ const piExtensionRunFacts = await loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declaredPiExtensions, warnings);
418
489
  // Reject unknown nested leakage by projecting only through allowlist helpers.
419
- const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, warnings);
490
+ const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings);
420
491
  // Defense-in-depth: ensure raw task never sneaks in.
421
492
  if ("task" in body) {
422
493
  return {