@tea-agent/loop-agent 0.39.0-next.9 → 0.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +269 -69
  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 +16 -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/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/dag-rerun.js +55 -1
  14. package/dist/commands/init-model-catalog.js +464 -0
  15. package/dist/commands/init-upgrade.js +265 -97
  16. package/dist/commands/init.js +428 -27
  17. package/dist/commands/inspect-next.js +8 -0
  18. package/dist/commands/task-advance.js +31 -0
  19. package/dist/executors/dag-pi-executor.js +124 -18
  20. package/dist/executors/pi-event-serializer.js +42 -1
  21. package/dist/executors/pi-executor.js +51 -0
  22. package/dist/executors/pi-extension-resolver.js +233 -0
  23. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  24. package/dist/executors/pi-sdk-executor.js +187 -59
  25. package/dist/executors/shell-executor.js +555 -34
  26. package/dist/executors/shell-write-guard.js +7 -0
  27. package/dist/shared/dag-failure-category.js +138 -0
  28. package/dist/shared/dag-prompt-override.js +27 -0
  29. package/dist/shared/operator/capabilities.js +181 -27
  30. package/dist/shared/playwright-cli-command-policy.js +15 -0
  31. package/dist/shared/update/console-notifier.js +100 -0
  32. package/dist/{cli → shared}/update/npm-client.js +70 -15
  33. package/dist/{cli → shared}/update/state.js +41 -13
  34. package/dist/task/config-types.js +28 -0
  35. package/dist/task/contract/apply.js +11 -1
  36. package/dist/task/contract/constants.js +3 -0
  37. package/dist/task/contract/diff.js +26 -0
  38. package/dist/task/contract/hash.js +3 -0
  39. package/dist/task/contract/observe.js +25 -4
  40. package/dist/task/contract/paths.js +2 -1
  41. package/dist/task/contract/project.js +20 -1
  42. package/dist/task/contract/schema.js +8 -1
  43. package/dist/task/contract/transaction.js +12 -0
  44. package/dist/task/source-prepare/build-draft.js +15 -0
  45. package/dist/task/source-prepare/completeness.js +188 -0
  46. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  47. package/dist/task/source-prepare/index.js +3 -0
  48. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  49. package/dist/task/source-prepare/ledger-review.js +214 -0
  50. package/dist/task/source-prepare/ledger.js +545 -0
  51. package/dist/task/source-prepare/parse-intent.js +24 -4
  52. package/dist/task/source-prepare/prepare.js +262 -1
  53. package/dist/task/source-prepare/semantic-intake.js +19 -3
  54. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  55. package/dist/task/source-prepare/types.js +22 -0
  56. package/dist/worker/cli.js +18 -2
  57. package/dist/worker/console/chat/deferred-turn.js +91 -0
  58. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  59. package/dist/worker/console/chat/operation-card.js +8 -1
  60. package/dist/worker/console/chat/pi-console-config.js +81 -10
  61. package/dist/worker/console/chat/pi-runtime.js +165 -83
  62. package/dist/worker/console/chat/repo-walk.js +10 -1
  63. package/dist/worker/console/chat/routes.js +480 -55
  64. package/dist/worker/console/chat/semantic-activity.js +20 -8
  65. package/dist/worker/console/chat/session-store.js +146 -2
  66. package/dist/worker/console/chat/turn-process.js +10 -10
  67. package/dist/worker/console/console-handoff.js +82 -0
  68. package/dist/worker/console/console-update-and-init.js +115 -0
  69. package/dist/worker/console/console-update-runtime.js +84 -0
  70. package/dist/worker/console/dag-execution-receipt.js +33 -0
  71. package/dist/worker/console/draft-store.js +49 -0
  72. package/dist/worker/console/inspect-split.js +18 -0
  73. package/dist/worker/console/operation-run-facts.js +19 -3
  74. package/dist/worker/console/operation-store.js +1 -1
  75. package/dist/worker/console/operator-actions.js +320 -4
  76. package/dist/worker/console/operator-surface-health.js +1 -0
  77. package/dist/worker/console/operator-user-error.js +2 -2
  78. package/dist/worker/console/pi-readiness.js +5 -4
  79. package/dist/worker/console/prd-intake-bridge.js +13 -0
  80. package/dist/worker/console/recovery-cta.js +4 -4
  81. package/dist/worker/console/recovery-error-copy.js +4 -4
  82. package/dist/worker/console/routes.js +46 -2
  83. package/dist/worker/console/security.js +63 -9
  84. package/dist/worker/console/server.js +111 -7
  85. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  86. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-B4i9cVxX.js} +1 -1
  87. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  88. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  89. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  90. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  91. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  101. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  102. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  103. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  105. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  106. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  111. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  112. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  113. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  114. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  115. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  116. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  117. package/dist/worker/console/static/assets/index-CW88Fjbw.js +406 -0
  118. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  119. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  120. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  121. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  122. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  123. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-aGmcjgTn.js} +1 -1
  124. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-Biyo-tqA.js} +5 -5
  125. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  126. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  127. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  128. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  129. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  130. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  131. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  132. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  133. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  134. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  135. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  136. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  137. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  138. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  139. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  140. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  141. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  142. package/dist/worker/console/static/index.html +2 -2
  143. package/dist/worker/console/static-src/app/console-types.js +40 -1
  144. package/dist/worker/console/static-src/app/useOperatorActions.js +6 -1
  145. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  146. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  147. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  148. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  149. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  150. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  151. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  152. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  153. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  154. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  157. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  158. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  159. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  160. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  161. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  162. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  163. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  164. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  165. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  166. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  167. package/dist/worker/observability/init-runtime-activity.js +26 -0
  168. package/dist/worker/observability/read-model.js +22 -0
  169. package/dist/worker/observe/node-input.js +160 -3
  170. package/dist/worker/observe/node-process.js +377 -0
  171. package/dist/worker/observe/routes.js +224 -3
  172. package/dist/worker/observe/shell-handler-keys.js +34 -0
  173. package/dist/worker/observe/static/api.js +67 -1
  174. package/dist/worker/observe/static/constants.js +8 -1
  175. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  176. package/dist/worker/observe/static/format.js +60 -0
  177. package/dist/worker/observe/static/index.html +5 -0
  178. package/dist/worker/observe/static/markdown-render.js +20 -1
  179. package/dist/worker/observe/static/operator-chrome.js +4 -0
  180. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  181. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  182. package/dist/worker/observe/static/state.js +13 -2
  183. package/dist/worker/observe/static/styles.css +85 -0
  184. package/dist/worker/observe/static/views/dag-inspector.js +912 -8
  185. package/dist/worker/observe/static/views/dag.js +8 -1
  186. package/dist/worker/observe/static/views/failures.js +5 -2
  187. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  188. package/dist/worker/observe/static/views/task.js +55 -0
  189. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +856 -54
  190. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  191. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  192. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  193. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  194. package/dist/workflows/dag/backend-test-layout.js +133 -0
  195. package/dist/workflows/dag/backend-test-markdown-workflow.js +343 -33
  196. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  197. package/dist/workflows/dag/backend-test-pytest-collection.js +454 -36
  198. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  199. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  200. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  201. package/dist/workflows/dag/backend-test-writer-completeness.js +110 -61
  202. package/dist/workflows/dag/contract-output-registry.js +15 -0
  203. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  204. package/dist/workflows/dag/convergence/controller.js +141 -0
  205. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  206. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  207. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  208. package/dist/workflows/dag/failure-category.js +7 -116
  209. package/dist/workflows/dag/frontend-implementation-contract.js +465 -6
  210. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  211. package/dist/workflows/dag/frontend-prewrite-gate.js +293 -84
  212. package/dist/workflows/dag/frontend-repair.js +14 -0
  213. package/dist/workflows/dag/frontend-review-context.js +92 -10
  214. package/dist/workflows/dag/frontend-risk.js +15 -2
  215. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  216. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  217. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  218. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  219. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  220. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  221. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  222. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  223. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  224. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  225. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  226. package/dist/workflows/dag/init-hybrid.js +1019 -209
  227. package/dist/workflows/dag/node-execution.js +228 -15
  228. package/dist/workflows/dag/prompt.js +60 -1
  229. package/dist/workflows/dag/rerun-feedback.js +212 -0
  230. package/dist/workflows/dag/rerun-plan.js +224 -27
  231. package/dist/workflows/dag/rerun-run.js +61 -6
  232. package/dist/workflows/dag/rerun-task.js +61 -2
  233. package/dist/workflows/dag/retry-policy.js +109 -0
  234. package/dist/workflows/dag/structured-output-repair.js +712 -0
  235. package/dist/workflows/dag/types.js +185 -12
  236. package/dist/workflows/dag/validate.js +64 -20
  237. package/docs/README.md +1 -0
  238. package/docs/architecture/evolution.md +45 -0
  239. package/docs/init-surface.manifest.json +6 -0
  240. package/docs/operations/README.md +1 -1
  241. package/docs/operations/local-development-environment.md +1 -5
  242. package/docs/skills/vetted-skill-registry.md +14 -0
  243. package/docs/templates/README.md +2 -2
  244. package/docs/templates/agent-dag.schema.json +44 -0
  245. package/docs/templates/backend-test-dag.json +60 -35
  246. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  247. package/docs/templates/frontend-test-case-checklist.md +2 -2
  248. package/docs/templates/frontend-test-dag.json +8 -10
  249. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  250. package/docs/templates/init-managed-agents.md +13 -3
  251. package/harness.json +6 -4
  252. package/package.json +12 -4
  253. package/scripts/next-info.mjs +356 -0
  254. package/scripts/next-publish-gate.mjs +238 -0
  255. package/scripts/release-source-binding.mjs +251 -0
  256. package/skills/analyze-product-requirements/SKILL.md +8 -5
  257. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  258. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  259. package/skills/analyze-product-requirements/references/example.md +50 -0
  260. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  261. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  262. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  263. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  264. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  265. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  266. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  267. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  268. package/skills/codebase-scout/SKILL.md +1 -1
  269. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  270. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  271. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  272. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  273. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  274. package/skills/improve-codebase-architecture/deepening.md +37 -0
  275. package/skills/improve-codebase-architecture/html-report.md +123 -0
  276. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  277. package/skills/improve-codebase-architecture/language.md +53 -0
  278. package/skills/loop-agent/references/command-reference.md +11 -4
  279. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  280. package/skills/playwright-cli/SKILL.md +23 -1
  281. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  282. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  283. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  284. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  285. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  286. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  287. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  288. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -2,6 +2,12 @@
2
2
  * Deterministic frontend-implementation risk classifier (M4).
3
3
  * Pure function: no I/O, no model, no side effects.
4
4
  */
5
+ /**
6
+ * Dependency-installation language in task text. Mentioning package.json or
7
+ * allowing its path is NOT dependency introduction by itself; the manifest
8
+ * path check in classifyFrontendRisk requires this language to escalate.
9
+ */
10
+ const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
5
11
  const HIGH_RISK_PATTERNS = [
6
12
  {
7
13
  id: "new-route",
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
25
31
  },
26
32
  {
27
33
  id: "new-dependency",
28
- re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖|package\.json/i,
34
+ re: DEPENDENCY_INSTALL_LANGUAGE,
29
35
  },
30
36
  {
31
37
  id: "schema-api-change",
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
88
94
  const paths = input.allowedPaths ?? [];
89
95
  const spread = pathSpreadScore(paths);
90
96
  if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
91
- signals.push("new-dependency");
97
+ if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
98
+ signals.push("new-dependency");
99
+ }
100
+ else {
101
+ // Editing manifest scripts/config is not dependency introduction;
102
+ // record the rejected escalation instead of forcing full gates.
103
+ rejectedSignals.push("new-dependency:manifest-path-without-install-language");
104
+ }
92
105
  }
93
106
  // Explicit multi-domain language only — path count alone is not high-risk
94
107
  if (spread >= 3 &&
@@ -1,16 +1,20 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { isPlaywrightCliCommand } from "../../shared/playwright-cli-command-policy.js";
4
+ import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
5
+ import { defaultFrontendTestLayout, frontendTestCasePath, } from "./frontend-test-layout.js";
4
6
  const COMMAND_FENCE_LANGUAGE = /^(?:|bash|sh|shell|zsh|fish|console|terminal|command|cmd|powershell|pwsh|javascript|js|typescript|ts|python|py)$/i;
5
7
  const LIST_ITEM = /^\s*(?:[-+*]|\d+[.)])\s+(.+)$/;
6
8
  const INDENTED_CODE = /^(?: {4,}|\t+)(\S.*)$/;
7
9
  const EXPLICIT_COMMAND = /^(?:(?:shell|terminal)(?:\s+command)?|command|run command|execute command)\s*:\s*(.+)$/i;
8
10
  const BLOCKED_REASON_PREFIX = /^(?:(?:blocked|forbidden|reject(?:ed)?|disallow(?:ed)?|prohibited)(?:\s+reason)?\s*(?::|\bbecause\b)|(?:do not|must not|never)\s+(?:run|execute|use)\b)/i;
9
11
  /** Standalone blockedReason / environment tokens (not executable commands). */
10
- const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
12
+ const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|unique-control-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
11
13
  const BLOCKED_PROSE_HINT = /(?:blockedReason|blocked\s*reason|environmentProbe|evidenceDir|\bunavailable\b|不可用|写\s*blocked|写入\s*blocked|伪命令|自然语言|元数据|fail-closed|探测失败)/i;
12
14
  const IMPERATIVE_COMMAND = /^(?:(?:run|execute|use)(?:\s+(?:the\s+)?(?:(?:shell|terminal)\s+)?command)?|in\s+(?:the\s+)?(?:shell|terminal|console)\s*,?\s*(?:run|execute|use))\s*:?\s+(.+)$/i;
13
15
  const INLINE_CODE_STEP = /^`([^`\r\n]+)`[.!?]?$/;
16
+ /** Assertion-count annotations appended to a playwright-cli literal (e.g. `find "锚点">=1`, `>=N`, `≥N`, `(≥1)`) document an expected match count; they are documentation, not shell arguments. */
17
+ const ASSERTION_COUNT_SUFFIX = /\s*[((]?\s*(?:>=|≥|>|超过|至少)\s*\d+\s*[))]?\s*$/;
14
18
  const AUTOMATION_EXECUTABLE = /^(?:playwright-cli\b|playwright\b|@playwright\/test\b|npx\b|npm\b|pnpm\b|yarn\b|bunx?\b|node(?:js)?\b|python(?:3)?\b|bash\b|sh\b|zsh\b|fish\b|powershell\b|pwsh\b|cmd(?:\.exe)?\b|cypress\b|selenium\b|webdriverio\b|chromedriver\b|google-chrome\b|chrome\b|firefox\b|curl\b|wget\b)/i;
15
19
  const SHELL_WRAPPED_EXECUTABLE = /^(?:sudo\s+|env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s]+)*\s+|(?:\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\/)[^\s]+)/i;
16
20
  const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)/;
@@ -44,6 +48,11 @@ function isBlockedReason(value) {
44
48
  return true;
45
49
  if (/^blockedReason\s*[:=]/i.test(trimmed))
46
50
  return true;
51
+ // Expected-outcome field lines ("status=blocked" / "status: failed") document
52
+ // the intended terminal state of a designed-negative case; they are
53
+ // case-result metadata, never executable commands.
54
+ if (/^["'`]?status["'`]?\s*[:=]\s*["'`]?(passed|failed|blocked)["'`]?\s*[.。]?$/i.test(trimmed))
55
+ return true;
47
56
  return false;
48
57
  }
49
58
  /**
@@ -238,6 +247,12 @@ function commandGateIssue(input) {
238
247
  if (input.instruction.fromFence && isBlockedReason(input.instruction.command)) {
239
248
  return null;
240
249
  }
250
+ // An expected-match-count annotation (`find "锚点">=1`, with or without a
251
+ // playwright-cli prefix) documents the assertion; it is not part of the
252
+ // command and never executable input.
253
+ if (/^(?:playwright-cli\s+)?find\s+"[^"]*"/i.test(input.instruction.command) && ASSERTION_COUNT_SUFFIX.test(input.instruction.command)) {
254
+ return null;
255
+ }
241
256
  const commandMatch = input.instruction.command.match(/^playwright-cli\s+([^\s`]+)/i);
242
257
  if (commandMatch) {
243
258
  const command = commandMatch[1].toLowerCase().replace(/^[`'"]+|[`'":,,。→]+$/g, "");
@@ -276,13 +291,20 @@ function commandGateIssue(input) {
276
291
  };
277
292
  }
278
293
  export async function validateFrontendCaseChecklist(input) {
279
- const root = path.join(input.workspaceRoot, "testcase/frontend/cases");
294
+ const layout = input.layout ?? defaultFrontendTestLayout();
295
+ const root = path.join(input.workspaceRoot, layout.casesDir);
280
296
  const draft = path.join(root, "manifest.draft.json");
281
297
  const final = path.join(root, "manifest.json");
282
298
  const manifestPath = await exists(draft) ? draft : await exists(final) ? final : "";
283
299
  if (!manifestPath)
284
300
  throw new Error("checklist: missing manifest.draft.json or manifest.json");
285
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
301
+ let manifest;
302
+ try {
303
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
304
+ }
305
+ catch {
306
+ throw new Error("checklist: manifest is malformed JSON");
307
+ }
286
308
  if (!Array.isArray(manifest.cases) || manifest.cases.length === 0)
287
309
  throw new Error("checklist: empty cases");
288
310
  const declaredAc = new Set(input.declaredAcIds ?? []);
@@ -299,7 +321,7 @@ export async function validateFrontendCaseChecklist(input) {
299
321
  if (typeof item?.caseId === "string" && /^AC-/i.test(item.caseId))
300
322
  issues.push({ ruleId: "case-id-is-ac", caseId: id, detail: "caseId must not be an acceptance id" });
301
323
  const casePath = typeof item?.casePath === "string" ? item.casePath : "";
302
- const expectedPath = typeof item?.caseId === "string" ? `testcase/frontend/cases/${item.caseId}.md` : "";
324
+ const expectedPath = typeof item?.caseId === "string" ? frontendTestCasePath(layout, item.caseId) : "";
303
325
  const absolute = path.resolve(input.workspaceRoot, casePath);
304
326
  const relative = path.relative(input.workspaceRoot, absolute);
305
327
  if (!casePath || relative.startsWith("..") || path.isAbsolute(relative) || !(await exists(absolute))) {
@@ -334,6 +356,10 @@ export async function validateFrontendCaseChecklist(input) {
334
356
  for (const ac of item.acIds) {
335
357
  if (typeof ac !== "string" || !acIdRe.test(ac))
336
358
  issues.push({ ruleId: "ac-id-shape", caseId: id, casePath, detail: String(ac) });
359
+ else if (declaredAc.size > 0 && isTruncatedFrontendAcId(ac))
360
+ issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
361
+ else if (declaredAc.size === 0 && isTruncatedWhenUndeclaredFrontendAcId(ac))
362
+ issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
337
363
  else if (declaredAc.size > 0 && !declaredAc.has(ac))
338
364
  issues.push({ ruleId: "unknown-ac", caseId: id, casePath, detail: `${ac} not in sourceBinding` });
339
365
  }
@@ -1,5 +1,7 @@
1
1
  import { readFile, rename, unlink, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
4
+ import { defaultFrontendTestLayout, frontendTestCasePath, frontendTestEvidenceDir, frontendTestManifestDraftPath, frontendTestManifestPath, } from "./frontend-test-layout.js";
3
5
  const DIMENSIONS = new Set(["core", "boundary", "flow", "backend"]);
4
6
  function fail(ruleId, detail) {
5
7
  const msg = JSON.stringify({ ruleId, detail: String(detail ?? "") });
@@ -18,8 +20,9 @@ function fail(ruleId, detail) {
18
20
  * present). When empty, ac-id-shape still applies.
19
21
  */
20
22
  export async function materializeFrontendTestCaseManifest(input) {
21
- const draft = path.join(input.workspaceRoot, "testcase/frontend/cases/manifest.draft.json");
22
- const file = path.join(input.workspaceRoot, "testcase/frontend/cases/manifest.json");
23
+ const layout = input.layout ?? defaultFrontendTestLayout();
24
+ const draft = path.join(input.workspaceRoot, frontendTestManifestDraftPath(layout));
25
+ const file = path.join(input.workspaceRoot, frontendTestManifestPath(layout));
23
26
  const tmp = `${file}.tmp`;
24
27
  const maxCases = input.maxCases ?? 32;
25
28
  const declaredAc = new Set(input.declaredAcIds ?? []);
@@ -62,12 +65,16 @@ export async function materializeFrontendTestCaseManifest(input) {
62
65
  for (const ac of c.acIds) {
63
66
  if (typeof ac !== "string" || !acIdRe.test(ac))
64
67
  fail("ac-id-shape", `acIds entry must be AC-* acceptance id, not caseId: ${ac}`);
68
+ if ((declaredAc.size > 0
69
+ ? isTruncatedFrontendAcId(ac)
70
+ : isTruncatedWhenUndeclaredFrontendAcId(ac)))
71
+ fail("ac-id-truncated", `acIds entry is a truncated acceptance id (missing feature prefix or sequence number): ${ac}`);
65
72
  if (declaredAc.size > 0 && !declaredAc.has(ac))
66
73
  fail("unknown-ac", `${ac} not in sourceBinding; repair generator input or AC list`);
67
74
  acIds.push(ac);
68
75
  }
69
- const casePath = `testcase/frontend/cases/${c.caseId}.md`;
70
- const evidenceDir = `testcase/frontend/evidence/${c.caseId}/`;
76
+ const casePath = frontendTestCasePath(layout, c.caseId);
77
+ const evidenceDir = frontendTestEvidenceDir(layout, c.caseId);
71
78
  for (const [k, v] of [
72
79
  ["casePath", casePath],
73
80
  ["evidenceDir", evidenceDir],
@@ -1,29 +1,23 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { markdownSection } from "./frontend-test-markdown.js";
4
+ import { defaultFrontendTestLayout, frontendTestCasePath, } from "./frontend-test-layout.js";
3
5
  const CASE_ID = /^FE-[A-Za-z0-9]+-\d{3}-(?:core|boundary|flow|backend)$/i;
4
6
  const AC_ID = /^AC(?:-[A-Z0-9]+)+$/i;
5
7
  const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|结果正确|页面正常|按实际情况处理|验证成功/i;
6
8
  const SECTION_ALIASES = {
7
9
  purpose: ["Test Purpose", "测试目的", "测试场景"],
8
10
  source: ["Source References", "需求依据"],
9
- preconditions: ["Preconditions", "前置条件"],
10
- steps: ["Steps", "操作步骤"],
11
+ preconditions: ["Preconditions", "前置条件", "前置条件与重置"],
12
+ steps: ["Test Steps", "测试步骤", "Steps", "操作步骤"],
11
13
  expected: ["Expected Results", "预期结果"],
12
14
  automation: ["Automation Notes", "自动化映射", "自动化说明"],
13
15
  };
14
- function section(body, names) {
15
- const marker = new RegExp(`^###\\s+(?:${names.map((v) => v.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")).join("|")})\\s*$`, "mi");
16
- const hit = marker.exec(body);
17
- if (!hit)
18
- return "";
19
- const rest = body.slice(hit.index + hit[0].length);
20
- const next = /^###\s+/m.exec(rest);
21
- return rest.slice(0, next?.index ?? rest.length).trim();
22
- }
23
16
  function hasList(value) { return /^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value); }
24
17
  function hasAssertion(value) { return /(?:status|状态|包含|显示|等于|为|可见|不可见|跳转|错误|成功|失败|should|expect|assert|must)/i.test(value); }
25
18
  export async function validateFrontendCaseContent(input) {
26
- const root = path.join(input.workspaceRoot, "testcase/frontend/cases");
19
+ const layout = input.layout ?? defaultFrontendTestLayout();
20
+ const root = path.join(input.workspaceRoot, layout.casesDir);
27
21
  const manifestPath = path.join(root, "manifest.json");
28
22
  const findings = [];
29
23
  let manifest;
@@ -57,7 +51,7 @@ export async function validateFrontendCaseContent(input) {
57
51
  findings.push({ ruleId: "unknown-ac", caseId: id, detail: `${ac} is not declared by the task` });
58
52
  }
59
53
  }
60
- const file = path.join(input.workspaceRoot, item?.casePath || `testcase/frontend/cases/${id}.md`);
54
+ const file = path.join(input.workspaceRoot, item?.casePath || frontendTestCasePath(layout, id));
61
55
  let body = "";
62
56
  try {
63
57
  body = await readFile(file, "utf8");
@@ -69,9 +63,9 @@ export async function validateFrontendCaseContent(input) {
69
63
  if (PLACEHOLDER.test(body))
70
64
  findings.push({ ruleId: "placeholder-wording", caseId: id, detail: "case contains placeholder or non-assertable wording" });
71
65
  for (const [key, names] of Object.entries(SECTION_ALIASES))
72
- if (!section(body, names))
66
+ if (!markdownSection(body, names))
73
67
  findings.push({ ruleId: `missing-${key}`, caseId: id, detail: `missing section: ${names.join(" or ")}` });
74
- const steps = section(body, SECTION_ALIASES.steps), expected = section(body, SECTION_ALIASES.expected);
68
+ const steps = markdownSection(body, SECTION_ALIASES.steps), expected = markdownSection(body, SECTION_ALIASES.expected);
75
69
  if (!hasList(steps))
76
70
  findings.push({ ruleId: "unstructured-steps", caseId: id, detail: "steps should contain numbered or bulleted executable actions" });
77
71
  if (!hasList(expected) || !hasAssertion(expected))
@@ -97,7 +91,8 @@ function render(findings) {
97
91
  }
98
92
  export async function writeFrontendCaseQualityReports(input) {
99
93
  const result = await validateFrontendCaseContent(input);
100
- const dir = path.join(input.workspaceRoot, input.outputDir ?? "testcase/frontend/reports");
94
+ const layout = input.layout ?? defaultFrontendTestLayout();
95
+ const dir = path.join(input.workspaceRoot, input.outputDir ?? layout.reportsDir);
101
96
  await mkdir(dir, { recursive: true });
102
97
  await writeFile(path.join(dir, "frontend-test-case-quality-advisory.md"), result.markdown, "utf8");
103
98
  await writeFile(path.join(dir, "frontend-test-case-quality-advisory.html"), result.html, "utf8");
@@ -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
+ }