@tea-agent/loop-agent 0.39.0-next.8 → 0.39.0

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 (254) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +264 -67
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +124 -18
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +554 -33
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-failure-category.js +138 -0
  27. package/dist/shared/dag-prompt-override.js +28 -0
  28. package/dist/shared/operator/capabilities.js +181 -27
  29. package/dist/shared/playwright-cli-command-policy.js +15 -0
  30. package/dist/shared/update/console-notifier.js +100 -0
  31. package/dist/{cli → shared}/update/npm-client.js +41 -8
  32. package/dist/{cli → shared}/update/state.js +41 -13
  33. package/dist/task/config-types.js +28 -0
  34. package/dist/task/contract/constants.js +1 -0
  35. package/dist/task/contract/diff.js +26 -0
  36. package/dist/task/contract/project.js +5 -0
  37. package/dist/task/contract/schema.js +6 -1
  38. package/dist/task/source-prepare/build-draft.js +15 -0
  39. package/dist/task/source-prepare/parse-intent.js +24 -4
  40. package/dist/worker/cli.js +31 -1
  41. package/dist/worker/console/chat/deferred-turn.js +91 -0
  42. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  43. package/dist/worker/console/chat/operation-card.js +8 -1
  44. package/dist/worker/console/chat/pi-console-config.js +81 -10
  45. package/dist/worker/console/chat/pi-runtime.js +165 -83
  46. package/dist/worker/console/chat/repo-walk.js +10 -1
  47. package/dist/worker/console/chat/routes.js +480 -55
  48. package/dist/worker/console/chat/semantic-activity.js +20 -8
  49. package/dist/worker/console/chat/session-store.js +146 -2
  50. package/dist/worker/console/chat/turn-process.js +10 -10
  51. package/dist/worker/console/dag-execution-receipt.js +33 -0
  52. package/dist/worker/console/inspect-split.js +18 -0
  53. package/dist/worker/console/operation-run-facts.js +19 -3
  54. package/dist/worker/console/operator-actions.js +195 -4
  55. package/dist/worker/console/operator-user-error.js +2 -2
  56. package/dist/worker/console/pi-readiness.js +5 -4
  57. package/dist/worker/console/recovery-cta.js +4 -4
  58. package/dist/worker/console/recovery-error-copy.js +4 -4
  59. package/dist/worker/console/routes.js +43 -2
  60. package/dist/worker/console/security.js +63 -9
  61. package/dist/worker/console/server.js +42 -6
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
  94. package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
  95. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  121. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  122. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  123. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  124. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  125. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  126. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  127. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  128. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  129. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  130. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  131. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  132. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  133. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  134. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  135. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  136. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  137. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  138. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  139. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  140. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  141. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  142. package/dist/worker/observability/read-model.js +22 -0
  143. package/dist/worker/observe/node-input.js +74 -3
  144. package/dist/worker/observe/node-process.js +377 -0
  145. package/dist/worker/observe/routes.js +36 -3
  146. package/dist/worker/observe/shell-handler-keys.js +34 -0
  147. package/dist/worker/observe/static/api.js +66 -0
  148. package/dist/worker/observe/static/constants.js +8 -1
  149. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  150. package/dist/worker/observe/static/format.js +23 -0
  151. package/dist/worker/observe/static/markdown-render.js +20 -1
  152. package/dist/worker/observe/static/operator-chrome.js +4 -0
  153. package/dist/worker/observe/static/state.js +13 -2
  154. package/dist/worker/observe/static/styles.css +73 -0
  155. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  156. package/dist/worker/observe/static/views/dag.js +8 -1
  157. package/dist/worker/observe/static/views/failures.js +5 -2
  158. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  159. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  160. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  161. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  162. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  163. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  164. package/dist/workflows/dag/backend-test-layout.js +133 -0
  165. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  166. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  167. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  168. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  169. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  170. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  171. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  172. package/dist/workflows/dag/contract-output-registry.js +15 -0
  173. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  174. package/dist/workflows/dag/convergence/controller.js +141 -0
  175. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  176. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  177. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  178. package/dist/workflows/dag/failure-category.js +7 -116
  179. package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
  180. package/dist/workflows/dag/frontend-plan-render.js +1 -1
  181. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  182. package/dist/workflows/dag/frontend-repair.js +14 -0
  183. package/dist/workflows/dag/frontend-risk.js +15 -2
  184. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  185. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  186. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  187. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  188. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  189. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  190. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  191. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  192. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  193. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  194. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  195. package/dist/workflows/dag/init-hybrid.js +894 -205
  196. package/dist/workflows/dag/node-execution.js +183 -15
  197. package/dist/workflows/dag/prompt.js +60 -1
  198. package/dist/workflows/dag/rerun-feedback.js +212 -0
  199. package/dist/workflows/dag/rerun-plan.js +224 -27
  200. package/dist/workflows/dag/rerun-run.js +61 -6
  201. package/dist/workflows/dag/rerun-task.js +61 -2
  202. package/dist/workflows/dag/retry-policy.js +109 -0
  203. package/dist/workflows/dag/structured-output-repair.js +712 -0
  204. package/dist/workflows/dag/types.js +143 -1
  205. package/dist/workflows/dag/validate.js +64 -20
  206. package/docs/init-surface.manifest.json +5 -0
  207. package/docs/operations/README.md +1 -1
  208. package/docs/operations/local-development-environment.md +1 -5
  209. package/docs/skills/vetted-skill-registry.md +14 -0
  210. package/docs/templates/README.md +1 -1
  211. package/docs/templates/agent-dag.schema.json +44 -0
  212. package/docs/templates/backend-test-dag.json +60 -35
  213. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  214. package/docs/templates/frontend-test-case-checklist.md +2 -2
  215. package/docs/templates/frontend-test-dag.json +8 -10
  216. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  217. package/docs/templates/init-managed-agents.md +13 -3
  218. package/harness.json +6 -4
  219. package/package.json +12 -4
  220. package/scripts/next-info.mjs +356 -0
  221. package/scripts/next-publish-gate.mjs +238 -0
  222. package/scripts/release-source-binding.mjs +251 -0
  223. package/skills/analyze-product-requirements/SKILL.md +8 -5
  224. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  225. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  226. package/skills/analyze-product-requirements/references/example.md +50 -0
  227. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  228. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  229. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  230. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  231. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  232. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  233. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  234. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  235. package/skills/codebase-scout/SKILL.md +1 -1
  236. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  237. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  238. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  239. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  240. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  241. package/skills/improve-codebase-architecture/deepening.md +37 -0
  242. package/skills/improve-codebase-architecture/html-report.md +123 -0
  243. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  244. package/skills/improve-codebase-architecture/language.md +53 -0
  245. package/skills/loop-agent/references/command-reference.md +11 -4
  246. package/skills/playwright-cli/SKILL.md +23 -1
  247. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  248. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  249. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  250. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  251. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  252. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  253. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  254. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -0,0 +1,230 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { defaultFrontendTestLayout, frontendTestContextRel, frontendTestProbeRel, } from "./frontend-test-layout.js";
5
+ export const FRONTEND_TEST_CONTEXT_REL = "testcase/frontend/rag/context.md";
6
+ export const FRONTEND_TEST_PROBE_REL = "testcase/frontend/rag/environment-probe.json";
7
+ const CONNECTION_REFUSED_HINT = "被测服务未在 baseUrl 监听(connection refused)。请先启动本地前端(例如 scripts/serve.sh 或项目约定的 npm start),确认可访问后再从 materialize-frontend-test-execution-shell 续跑。";
8
+ export function classifyFrontendBaseUrlProbeFailure(input) {
9
+ if (input.spawnError)
10
+ return { errorClass: "spawn-error" };
11
+ const exit = input.curlExit ?? 0;
12
+ if (exit === 7) {
13
+ return { errorClass: "connection-refused", hint: CONNECTION_REFUSED_HINT };
14
+ }
15
+ if (exit === 6) {
16
+ return {
17
+ errorClass: "dns-unresolved",
18
+ hint: "无法解析 baseUrl 主机名。确认 context.md 中的地址可在本机解析,或改用 127.0.0.1。",
19
+ };
20
+ }
21
+ if (exit === 28) {
22
+ return {
23
+ errorClass: "connect-timeout",
24
+ hint: "连接 baseUrl 超时。确认被测服务已启动且端口可访问。",
25
+ };
26
+ }
27
+ if (exit !== 0)
28
+ return { errorClass: `curl-exit-${exit}` };
29
+ if (input.httpStatus && Number.isFinite(input.httpStatus)) {
30
+ return { errorClass: `http-${input.httpStatus}` };
31
+ }
32
+ return { errorClass: "unreachable" };
33
+ }
34
+ function defaultCurlRunner(args) {
35
+ const result = spawnSync("curl", [...args], { encoding: "utf8" });
36
+ return {
37
+ status: result.status,
38
+ stdout: result.stdout ?? "",
39
+ stderr: result.stderr ?? "",
40
+ error: result.error,
41
+ };
42
+ }
43
+ function redactUrl(value) {
44
+ try {
45
+ const parsed = new URL(value);
46
+ parsed.username = "";
47
+ parsed.password = "";
48
+ parsed.search = "";
49
+ return parsed.toString();
50
+ }
51
+ catch {
52
+ return value.replace(/\/\/[^@\s]+@/g, "//");
53
+ }
54
+ }
55
+ function resolveBaseUrl(context) {
56
+ const patterns = [
57
+ /baseUrl\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
58
+ /base[-_ ]url\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i,
59
+ /playwright-cli open --browser=chrome\s+((?:https?):\/\/[^\s"'`<>]+)/i,
60
+ /(https?:\/\/(?:localhost|127\.0\.0\.1)[^\s)}\],"']*)/i,
61
+ ];
62
+ for (const pattern of patterns) {
63
+ const match = context.match(pattern);
64
+ if (match?.[1])
65
+ return match[1].replace(/[)}\],"'.`]+$/g, "");
66
+ }
67
+ return null;
68
+ }
69
+ async function writeProbe(workspaceRoot, probe, layout) {
70
+ const probePath = path.join(workspaceRoot, frontendTestProbeRel(layout));
71
+ const contextPath = path.join(workspaceRoot, frontendTestContextRel(layout));
72
+ await mkdir(path.dirname(probePath), { recursive: true });
73
+ await writeFile(probePath, `${JSON.stringify(probe, null, 2)}\n`, "utf8");
74
+ let context = "";
75
+ try {
76
+ context = await readFile(contextPath, "utf8");
77
+ }
78
+ catch {
79
+ context = "";
80
+ }
81
+ const line = `environmentProbe: ${probe.status}${probe.blockedReason ? ` (${probe.blockedReason})` : ""}`;
82
+ const next = /environmentProbe\s*[:=]/i.test(context)
83
+ ? context.replace(/environmentProbe\s*[:=]\s*.*/i, line)
84
+ : `${context.trimEnd()}\n\n${line}\n`;
85
+ await writeFile(contextPath, next, "utf8");
86
+ }
87
+ export async function probeFrontendTestEnvironment(input) {
88
+ const layout = input.layout ?? defaultFrontendTestLayout();
89
+ const curl = input.curl ?? defaultCurlRunner;
90
+ const contextRel = frontendTestContextRel(layout);
91
+ const contextPath = path.join(input.workspaceRoot, contextRel);
92
+ let context;
93
+ try {
94
+ context = await readFile(contextPath, "utf8");
95
+ }
96
+ catch {
97
+ return { ok: false, stderr: `missing ${contextRel}` };
98
+ }
99
+ const rawUrl = resolveBaseUrl(context);
100
+ if (!rawUrl) {
101
+ return {
102
+ ok: false,
103
+ stderr: "frontend-test preflight missing absolute baseUrl from context.md",
104
+ };
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(rawUrl);
109
+ }
110
+ catch {
111
+ return { ok: false, stderr: `baseUrl must be absolute http(s): ${rawUrl}` };
112
+ }
113
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
114
+ parsed.username ||
115
+ parsed.password ||
116
+ parsed.search ||
117
+ parsed.hash) {
118
+ return {
119
+ ok: false,
120
+ stderr: `unsafe baseUrl from context.md: ${redactUrl(rawUrl)}`,
121
+ };
122
+ }
123
+ if (/(?:^|\.)(?:www\.)?[^.]*?(?:prod|production)/i.test(parsed.hostname)) {
124
+ return {
125
+ ok: false,
126
+ stderr: `production URL forbidden: ${redactUrl(rawUrl)}`,
127
+ };
128
+ }
129
+ const baseUrl = parsed.toString();
130
+ const safe = redactUrl(baseUrl);
131
+ const sourceMatch = context.match(/baseUrlSource\s*[:=]\s*([^\r\n]+)/i);
132
+ const baseUrlSource = sourceMatch ? sourceMatch[1].trim() : "context.md";
133
+ const failBlocked = async (reason, extra) => {
134
+ const probe = {
135
+ status: "unreachable",
136
+ blockedReason: reason,
137
+ baseUrlRedacted: extra.baseUrlRedacted ?? safe,
138
+ httpStatus: extra.httpStatus ?? null,
139
+ method: extra.method ?? null,
140
+ curlExit: extra.curlExit ?? null,
141
+ errorClass: extra.errorClass ?? null,
142
+ ...(extra.hint ? { hint: extra.hint } : {}),
143
+ ...(extra.baseUrlSource ? { baseUrlSource: extra.baseUrlSource } : {}),
144
+ };
145
+ await writeProbe(input.workspaceRoot, probe, layout);
146
+ const payload = {
147
+ blockedReason: reason,
148
+ baseUrl: probe.baseUrlRedacted,
149
+ httpStatus: probe.httpStatus,
150
+ errorClass: probe.errorClass,
151
+ ...(probe.hint ? { hint: probe.hint } : {}),
152
+ };
153
+ return {
154
+ ok: false,
155
+ stderr: `frontend-test preflight blocked: ${JSON.stringify(payload)}`,
156
+ probe,
157
+ };
158
+ };
159
+ const curlCheck = curl(["--version"]);
160
+ if (curlCheck.error || curlCheck.status !== 0) {
161
+ return failBlocked("curl-unavailable", {
162
+ baseUrlRedacted: safe,
163
+ errorClass: "curl-missing",
164
+ });
165
+ }
166
+ const probeMethod = (method) => curl([
167
+ "-sS",
168
+ "-o",
169
+ "/dev/null",
170
+ "-w",
171
+ "%{http_code}",
172
+ "--connect-timeout",
173
+ "3",
174
+ "--max-time",
175
+ "8",
176
+ "-X",
177
+ method,
178
+ "-L",
179
+ "--max-redirs",
180
+ "3",
181
+ "--http1.1",
182
+ "--proto-redir",
183
+ "=http,https",
184
+ safe,
185
+ ]);
186
+ let used = "HEAD";
187
+ let result = probeMethod("HEAD");
188
+ let code = String(result.stdout || "").trim();
189
+ let statusNum = Number.parseInt(code, 10);
190
+ const headRejected = result.status !== 0 || !statusNum || statusNum === 405 || statusNum === 501;
191
+ if (headRejected) {
192
+ used = "GET";
193
+ result = probeMethod("GET");
194
+ code = String(result.stdout || "").trim();
195
+ statusNum = Number.parseInt(code, 10);
196
+ }
197
+ const ok = statusNum >= 200 && statusNum < 400;
198
+ if (!ok) {
199
+ const classified = classifyFrontendBaseUrlProbeFailure({
200
+ spawnError: Boolean(result.error),
201
+ curlExit: result.status,
202
+ httpStatus: Number.isFinite(statusNum) ? statusNum : null,
203
+ });
204
+ return failBlocked("frontend-base-url-unreachable", {
205
+ baseUrlRedacted: safe,
206
+ httpStatus: Number.isFinite(statusNum) ? statusNum : null,
207
+ method: used,
208
+ curlExit: result.status,
209
+ errorClass: classified.errorClass,
210
+ hint: classified.hint,
211
+ });
212
+ }
213
+ const probe = {
214
+ status: "reachable",
215
+ blockedReason: null,
216
+ baseUrl: safe,
217
+ baseUrlRedacted: safe,
218
+ baseUrlSource,
219
+ httpStatus: statusNum,
220
+ method: used,
221
+ curlExit: result.status,
222
+ errorClass: null,
223
+ };
224
+ await writeProbe(input.workspaceRoot, probe, layout);
225
+ return {
226
+ ok: true,
227
+ stdout: `frontend-test-execution-v1 validated contextBaseUrl=${safe} source=${baseUrlSource} probe=reachable method=${used} httpStatus=${statusNum}`,
228
+ probe,
229
+ };
230
+ }
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { frontendTestResultContractSchema } from "./frontend-test-result-contract.js";
4
+ import { defaultFrontendTestLayout, } from "./frontend-test-layout.js";
4
5
  function escapeHtml(value) {
5
6
  return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6
7
  }
@@ -80,7 +81,8 @@ async function writePairAtomic(markdownPath, markdown, htmlPath, html) {
80
81
  export async function renderFrontendTestHtmlReport(input) {
81
82
  const resultPath = path.join(input.runDir, "contracts", "frontend-test-result.json");
82
83
  const result = frontendTestResultContractSchema.parse(JSON.parse(await readFile(resultPath, "utf8")));
83
- const outputDir = path.join(input.workspaceRoot, "testcase", "frontend", "reports");
84
+ const layout = input.layout ?? defaultFrontendTestLayout();
85
+ const outputDir = path.join(input.workspaceRoot, layout.reportsDir);
84
86
  const markdownPath = path.join(outputDir, "frontend-test-report.md");
85
87
  const htmlPath = path.join(outputDir, "frontend-test-report.html");
86
88
  const outcomeLabel = result.outcome === "passed" ? "测试通过" : result.outcome === "failed" ? "测试失败" : "测试未完成";
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { frontendTestResultContractSchema, } from "./frontend-test-result-contract.js";
4
+ import { defaultFrontendTestLayout, } from "./frontend-test-layout.js";
4
5
  function ratioMetric(numerator, denominator, threshold, label) {
5
6
  if (denominator === 0) {
6
7
  return { numerator, denominator, ratio: null, threshold, status: "unavailable", reason: `${label}-denominator-is-zero` };
@@ -169,7 +170,8 @@ export async function renderFrontendTestL5Report(input) {
169
170
  const resultPath = path.join(input.runDir, "contracts", "frontend-test-result.json");
170
171
  const result = frontendTestResultContractSchema.parse(JSON.parse(await readFile(resultPath, "utf8")));
171
172
  const metrics = computeFrontendL5ReportMetrics(result);
172
- const outputDir = path.join(input.workspaceRoot, "testcase", "frontend", "reports");
173
+ const layout = input.layout ?? defaultFrontendTestLayout();
174
+ const outputDir = path.join(input.workspaceRoot, layout.reportsDir);
173
175
  const markdownPath = path.join(outputDir, "frontend-test-l5-dashboard.md");
174
176
  const htmlPath = path.join(outputDir, "frontend-test-l5-dashboard.html");
175
177
  await writePairAtomic(markdownPath, renderMarkdown(result, metrics), htmlPath, renderHtml(result, metrics));
@@ -0,0 +1,159 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Frontend-test artifact layout (configurable testRoot).
4
+ *
5
+ * Resolved once from `task.frontendTest.testRoot` at DAG compile time and
6
+ * frozen on the spec for the whole run. Writers, gates, Playwright fences
7
+ * and TaskSpec allowedPaths all derive from this single object.
8
+ *
9
+ * Default layout keeps the historical frozen contract:
10
+ * testcase/frontend/rag/**
11
+ * testcase/frontend/cases/**
12
+ * testcase/frontend/evidence/**
13
+ * testcase/frontend/reports/**
14
+ * testcase/frontend/fixtures/**
15
+ */
16
+ export const FRONTEND_TEST_DEFAULT_TEST_ROOT = "testcase/frontend";
17
+ const FORBIDDEN_TEST_ROOT_PREFIXES = [
18
+ "src",
19
+ "app",
20
+ "lib",
21
+ "apps",
22
+ "packages",
23
+ ".harness",
24
+ ".git",
25
+ "node_modules",
26
+ "dist",
27
+ "build",
28
+ "docs",
29
+ "website",
30
+ "scripts",
31
+ "test",
32
+ "smoke",
33
+ "knowledge",
34
+ "features",
35
+ "dogfood",
36
+ "examples",
37
+ "bin",
38
+ ];
39
+ function normalizeRelPosix(value, label) {
40
+ const trimmed = value.trim();
41
+ if (!trimmed)
42
+ throw new Error(`frontendTest.${label} must not be empty`);
43
+ if (trimmed.includes("\\")) {
44
+ throw new Error(`frontendTest.${label} must use forward slashes: ${value}`);
45
+ }
46
+ if (path.isAbsolute(trimmed) || /^[A-Za-z]:/.test(trimmed)) {
47
+ throw new Error(`frontendTest.${label} must be a repository-relative path: ${value}`);
48
+ }
49
+ const segments = trimmed.split("/").filter((segment) => segment.length > 0);
50
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
51
+ throw new Error(`frontendTest.${label} must not contain "." or ".." segments: ${value}`);
52
+ }
53
+ if (segments.some((segment) => segment.startsWith("."))) {
54
+ throw new Error(`frontendTest.${label} must not target hidden directories: ${value}`);
55
+ }
56
+ if (segments.some((segment) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment))) {
57
+ throw new Error(`frontendTest.${label} contains unsafe path segments: ${value}`);
58
+ }
59
+ return segments.join("/");
60
+ }
61
+ function assertTestRootAllowed(testRoot) {
62
+ const topSegment = testRoot.split("/")[0];
63
+ if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
64
+ throw new Error(`frontendTest.testRoot must not target production/governance trees: ${testRoot}`);
65
+ }
66
+ if (topSegment.startsWith(".")) {
67
+ throw new Error(`frontendTest.testRoot must not target hidden directories: ${testRoot}`);
68
+ }
69
+ }
70
+ /**
71
+ * Resolve the frozen frontend-test artifact layout from task config.
72
+ * Throws on unsafe or ambiguous configuration (fail-closed).
73
+ */
74
+ export function resolveFrontendTestLayout(config) {
75
+ const testRoot = config?.testRoot === undefined || config?.testRoot === null
76
+ ? FRONTEND_TEST_DEFAULT_TEST_ROOT
77
+ : normalizeRelPosix(config.testRoot, "testRoot");
78
+ assertTestRootAllowed(testRoot);
79
+ const isDefault = testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT;
80
+ return {
81
+ testRoot,
82
+ ragDir: `${testRoot}/rag`,
83
+ casesDir: `${testRoot}/cases`,
84
+ evidenceDir: `${testRoot}/evidence`,
85
+ reportsDir: `${testRoot}/reports`,
86
+ fixturesDir: `${testRoot}/fixtures`,
87
+ isDefault,
88
+ };
89
+ }
90
+ /** Historical default layout. Old DAGs without frontendTestLayout use this. */
91
+ export function defaultFrontendTestLayout() {
92
+ return resolveFrontendTestLayout(undefined);
93
+ }
94
+ export function frontendTestLayoutFromSpec(spec) {
95
+ const frozen = spec?.frontendTestLayout;
96
+ if (!frozen)
97
+ return defaultFrontendTestLayout();
98
+ return resolveFrontendTestLayout({ testRoot: frozen.testRoot });
99
+ }
100
+ const DEFAULT_PREFIX = `${FRONTEND_TEST_DEFAULT_TEST_ROOT}/`;
101
+ /**
102
+ * Longest-prefix rewrite of historical `testcase/frontend/` paths.
103
+ * No-op when layout.isDefault so default DAGs stay byte-identical.
104
+ * Never replaces the short token `testcase/` (would collide with backend-test).
105
+ */
106
+ export function applyFrontendTestLayoutToText(text, layout) {
107
+ if (layout.isDefault)
108
+ return text;
109
+ const replacement = `${layout.testRoot}/`;
110
+ let output = "";
111
+ for (let i = 0; i < text.length;) {
112
+ if (text.startsWith(replacement, i)) {
113
+ output += replacement;
114
+ i += replacement.length;
115
+ continue;
116
+ }
117
+ if (text.startsWith(DEFAULT_PREFIX, i)) {
118
+ output += replacement;
119
+ i += DEFAULT_PREFIX.length;
120
+ continue;
121
+ }
122
+ output += text[i];
123
+ i += 1;
124
+ }
125
+ return output;
126
+ }
127
+ export function frontendTestCasePath(layout, caseId) {
128
+ return `${layout.casesDir}/${caseId}.md`;
129
+ }
130
+ export function frontendTestEvidenceDir(layout, caseId) {
131
+ return `${layout.evidenceDir}/${caseId}/`;
132
+ }
133
+ export function frontendTestEvidencePrefix(layout, caseId) {
134
+ return `${layout.evidenceDir}/${caseId}`;
135
+ }
136
+ export function frontendTestManifestPath(layout) {
137
+ return `${layout.casesDir}/manifest.json`;
138
+ }
139
+ export function frontendTestManifestDraftPath(layout) {
140
+ return `${layout.casesDir}/manifest.draft.json`;
141
+ }
142
+ export function frontendTestContextRel(layout) {
143
+ return `${layout.ragDir}/context.md`;
144
+ }
145
+ export function frontendTestCoverageMapRel(layout) {
146
+ return `${layout.ragDir}/coverage-map.md`;
147
+ }
148
+ export function frontendTestProbeRel(layout) {
149
+ return `${layout.ragDir}/environment-probe.json`;
150
+ }
151
+ export function frontendTestStandardScenariosRel(layout) {
152
+ return `${layout.ragDir}/standard-scenarios.v1.json`;
153
+ }
154
+ export function frontendTestUiAnchorsRel(layout) {
155
+ return `${layout.ragDir}/ui-anchors.md`;
156
+ }
157
+ export function escapeRegexLiteral(value) {
158
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Bounded ATX section/list parsing for frontend-test case Markdown.
3
+ * Generator cases use `##` headings; older fixtures use `###`.
4
+ */
5
+ const ATX_HEADING = /^(#{1,3})\s+(\S.*)$/;
6
+ function headingMatches(title, names) {
7
+ const normalized = title.trim();
8
+ return names.some((name) => {
9
+ if (normalized === name)
10
+ return true;
11
+ return (normalized.startsWith(name) &&
12
+ (normalized.length === name.length ||
13
+ /[\s::与((-]/.test(normalized.charAt(name.length))));
14
+ });
15
+ }
16
+ export function markdownSection(body, names) {
17
+ const lines = body.replace(/\r\n/g, "\n").split("\n");
18
+ let start = -1;
19
+ let startLevel = 0;
20
+ for (let index = 0; index < lines.length; index += 1) {
21
+ const match = ATX_HEADING.exec(lines[index]);
22
+ if (!match)
23
+ continue;
24
+ if (!headingMatches(match[2], names))
25
+ continue;
26
+ start = index;
27
+ startLevel = match[1].length;
28
+ break;
29
+ }
30
+ if (start < 0)
31
+ return "";
32
+ const collected = [];
33
+ for (let index = start + 1; index < lines.length; index += 1) {
34
+ const match = ATX_HEADING.exec(lines[index]);
35
+ if (match && match[1].length <= startLevel)
36
+ break;
37
+ collected.push(lines[index]);
38
+ }
39
+ return collected.join("\n").trim();
40
+ }
41
+ export function markdownList(value) {
42
+ const items = [];
43
+ for (const raw of value.replace(/\r\n/g, "\n").split("\n")) {
44
+ const listed = raw.match(/^\s*(?:\d+[.)]|[-*+])\s+(.*)$/);
45
+ if (listed) {
46
+ const text = listed[1].trim();
47
+ if (text)
48
+ items.push(text);
49
+ continue;
50
+ }
51
+ const continuation = raw.match(/^\s{2,}(\S.*)$/);
52
+ if (continuation && items.length > 0) {
53
+ items[items.length - 1] = `${items[items.length - 1]} ${continuation[1].trim()}`;
54
+ }
55
+ }
56
+ return items;
57
+ }
58
+ export function markdownH1Title(body) {
59
+ const match = /^#\s+(\S.*)$/m.exec(body.replace(/\r\n/g, "\n"));
60
+ return match?.[1]?.trim() ?? "";
61
+ }