@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.10

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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -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/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -613
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -58
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -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
+ }
@@ -5,7 +5,28 @@ import { z } from "zod";
5
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
6
6
  import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
7
7
  import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
8
+ import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
9
+ import { defaultFrontendTestLayout, frontendTestEvidencePrefix, frontendTestManifestPath, } from "./frontend-test-layout.js";
8
10
  export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
11
+ /**
12
+ * Truncated acceptance-id shapes observed in generator output: a bare
13
+ * numeric segment ("AC-002" dropped its AC-FE-<FEATURE> prefix) or an
14
+ * AC-FE-<FEATURE> literal without the trailing -NNN sequence number
15
+ * ("AC-FE-CHAT"). Generic short ids like "AC-1"/"AC-FE-001" stay valid;
16
+ * declared-source cross-checks (unknown-ac) remain the primary guard when a
17
+ * sourceBinding exists.
18
+ */
19
+ export function isTruncatedFrontendAcId(acId) {
20
+ return /^AC-FE-[A-Z]+$/i.test(acId.trim());
21
+ }
22
+ /**
23
+ * Bare numeric ids ("AC-002") only indicate truncation when no declared AC
24
+ * list exists to adjudicate them: with a sourceBinding they are ordinary
25
+ * unknown-ac findings ("AC-999" may be a legitimately declared id style).
26
+ */
27
+ export function isTruncatedWhenUndeclaredFrontendAcId(acId) {
28
+ return /^AC-\d{3}$/i.test(acId.trim()) || isTruncatedFrontendAcId(acId);
29
+ }
9
30
  const safeRelativePathSchema = z.string().min(1).refine((value) => !path.posix.isAbsolute(value) &&
10
31
  !path.win32.isAbsolute(value) &&
11
32
  !value.includes("\\") &&
@@ -56,6 +77,8 @@ export const frontendTestResultContractSchema = z.object({
56
77
  passed: z.number().int().min(0),
57
78
  failed: z.number().int().min(0),
58
79
  blocked: z.number().int().min(0),
80
+ /** Stability metric: passed on attempt 0 (no rerun/self-heal retry). */
81
+ firstTryPassed: z.number().int().min(0).optional(),
59
82
  }).strict(),
60
83
  acceptanceCoverage: z.object({
61
84
  covered: z.array(z.string().min(1)),
@@ -102,14 +125,22 @@ function isSafeEvidenceShellPath(value) {
102
125
  * backslash interpretation for the former long `node -e` command.
103
126
  */
104
127
  export async function validateFrontendCaseEvidence(input) {
105
- const manifestPath = path.join(input.workspaceRoot, "testcase/frontend/cases/manifest.json");
128
+ const layout = input.layout ?? defaultFrontendTestLayout();
129
+ const manifestRel = frontendTestManifestPath(layout);
130
+ const manifestPath = path.join(input.workspaceRoot, manifestRel);
106
131
  try {
107
132
  await stat(manifestPath);
108
133
  }
109
134
  catch {
110
- throw new Error("missing testcase/frontend/cases/manifest.json");
135
+ throw new Error(`missing ${manifestRel}`);
136
+ }
137
+ let manifest;
138
+ try {
139
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
140
+ }
141
+ catch {
142
+ throw new Error("invalid frontend case manifest: malformed JSON");
111
143
  }
112
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
113
144
  if (!Array.isArray(manifest.cases))
114
145
  throw new Error("invalid frontend case manifest");
115
146
  const statuses = new Set(["passed", "failed", "blocked"]);
@@ -119,7 +150,7 @@ export async function validateFrontendCaseEvidence(input) {
119
150
  const item = rawCase;
120
151
  const id = typeof item?.caseId === "string" ? item.caseId : "?";
121
152
  const dir = item?.evidenceDir;
122
- const prefix = `testcase/frontend/evidence/${id}`;
153
+ const prefix = frontendTestEvidencePrefix(layout, id);
123
154
  if (!item ||
124
155
  typeof item.caseId !== "string" ||
125
156
  typeof dir !== "string" ||
@@ -163,12 +194,15 @@ export async function validateFrontendCaseEvidence(input) {
163
194
  continue;
164
195
  }
165
196
  const currentPrefix = `${prefix}/`;
166
- if (evidencePath.startsWith("testcase/frontend/evidence/") && !evidencePath.startsWith(currentPrefix)) {
197
+ if ((evidencePath.startsWith(`${layout.evidenceDir}/`) ||
198
+ evidencePath.startsWith("testcase/frontend/evidence/")) &&
199
+ !evidencePath.startsWith(currentPrefix)) {
167
200
  hardFail = true;
168
201
  issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
169
202
  continue;
170
203
  }
171
- const target = evidencePath.startsWith("testcase/")
204
+ const target = evidencePath.startsWith(`${layout.testRoot}/`) ||
205
+ evidencePath.startsWith("testcase/")
172
206
  ? path.join(input.workspaceRoot, evidencePath)
173
207
  : path.join(input.workspaceRoot, dir, evidencePath);
174
208
  if (!(await isNonEmptyFile(target))) {
@@ -194,11 +228,17 @@ export function evaluatePassedCaseBrowserReceipts(summary, caseId) {
194
228
  }
195
229
  return { ok: true };
196
230
  }
197
- async function loadCaseBrowserReceiptSummary(input) {
231
+ async function loadCaseBrowserReceiptsWithSummary(input) {
198
232
  try {
199
233
  const entries = await readdir(input.runDir, { withFileTypes: true });
200
234
  // A passed chain must belong to exactly one browser child stream. Never
201
- // stitch receipts across DAG nodes or accept ambiguous duplicate authority.
235
+ // stitch receipts across DAG nodes or accept ambiguous duplicate
236
+ // authority — with one exception: bounded rerun creates a second,
237
+ // later stream for the same case (the rerun overwrites the disk
238
+ // case-result, so the final attempt is the authoritative one). When
239
+ // multiple streams authorize, prefer the highest rerun round
240
+ // (rerun-frontend-case-rN-<idx> directories); ties within the same
241
+ // round remain ambiguous and fail closed.
202
242
  const authorizingStreams = [];
203
243
  for (const entry of entries) {
204
244
  if (!entry.isDirectory())
@@ -206,48 +246,123 @@ async function loadCaseBrowserReceiptSummary(input) {
206
246
  const nodeReceipts = (await readPlaywrightCliReceipts(input.runDir, entry.name))
207
247
  .filter((item) => item.caseId === input.caseId);
208
248
  const summary = summarizePlaywrightCliReceipts(nodeReceipts);
209
- if (summary.hasOrderedPassedReceiptChain)
210
- authorizingStreams.push(summary);
249
+ if (summary.hasOrderedPassedReceiptChain) {
250
+ const roundMatch = /rerun-frontend-case-r(\d+)-/i.exec(entry.name);
251
+ authorizingStreams.push({
252
+ receipts: nodeReceipts,
253
+ summary,
254
+ round: roundMatch ? Number(roundMatch[1]) : 0,
255
+ });
256
+ }
257
+ }
258
+ if (authorizingStreams.length === 0) {
259
+ return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
211
260
  }
212
- return authorizingStreams.length === 1
213
- ? authorizingStreams[0]
214
- : summarizePlaywrightCliReceipts([]);
261
+ const maxRound = Math.max(...authorizingStreams.map((s) => s.round));
262
+ const finalists = authorizingStreams.filter((s) => s.round === maxRound);
263
+ return finalists.length === 1
264
+ ? finalists[0]
265
+ : { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
215
266
  }
216
267
  catch {
217
- return summarizePlaywrightCliReceipts([]);
268
+ return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
218
269
  }
219
270
  }
220
- function sha256(content) {
221
- return createHash("sha256").update(content).digest("hex");
271
+ /**
272
+ * Extract `find "literal" >=N` / `find "literal"` declarations from a case
273
+ * document. Only quoted literals are binding assertions — the count suffix
274
+ * (`>=N`, `==N`) is the expected minimum/exact match count from the ledger.
275
+ * Placeholder literals ("...", "…", "<字面>", "eX") appear in prose discussing
276
+ * the syntax itself; they are documentation, never declarations.
277
+ */
278
+ export function extractDeclaredFindAssertions(caseMarkdown) {
279
+ const source = stripFencedCodeBlocks(caseMarkdown);
280
+ const out = [];
281
+ const seen = new Set();
282
+ const pattern = /find\s+"((?:[^"\\]|\\.)+)"\s*(>=|==)\s*(\d+)/g;
283
+ const isPlaceholder = (literal) => /^(?:\.{2,}|…+|<[^>]*>|e\d+)$/.test(literal.trim());
284
+ let match;
285
+ while ((match = pattern.exec(source)) !== null) {
286
+ const literal = match[1].replace(/\\(.)/g, "$1");
287
+ if (isPlaceholder(literal))
288
+ continue;
289
+ const operator = match[2];
290
+ const expected = Number(match[3]);
291
+ const key = `${operator}:${literal}`;
292
+ if (seen.has(key))
293
+ continue;
294
+ seen.add(key);
295
+ out.push({ literal, expected, operator });
296
+ }
297
+ return out;
222
298
  }
223
- function markdownSection(body, names) {
224
- const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
225
- const marker = new RegExp(`^###\\s+(?:${escaped})\\s*$`, "mi");
226
- const hit = marker.exec(body);
227
- if (!hit)
299
+ function stripFencedCodeBlocks(markdown) {
300
+ return markdown.replace(/```[\s\S]*?```/g, "");
301
+ }
302
+ async function readCaseMarkdownForAudit(workspaceRoot, casePath) {
303
+ try {
304
+ return await readFile(path.join(workspaceRoot, casePath), "utf8");
305
+ }
306
+ catch {
228
307
  return "";
229
- const rest = body.slice(hit.index + hit[0].length);
230
- const next = /^###\s+/m.exec(rest);
231
- return rest.slice(0, next?.index ?? rest.length).trim();
308
+ }
309
+ }
310
+ /**
311
+ * Controller-side assertion audit: every find-family declaration in the case
312
+ * document must be backed by a successful find receipt whose parsed match
313
+ * count satisfies the declared operator. This moves pass authority from
314
+ * "the model says it asserted" to "receipts prove it asserted".
315
+ */
316
+ export function auditDeclaredFindAssertions(declared, receipts) {
317
+ if (declared.length === 0)
318
+ return { ok: true };
319
+ const successfulFinds = receipts.filter((receipt) => (receipt.command === "find" || receipt.command === "find-unique") &&
320
+ receipt.exitCode === 0 &&
321
+ !receipt.timedOut);
322
+ const missing = [];
323
+ for (const decl of declared) {
324
+ const backing = successfulFinds.some((receipt) => receipt.argsRedacted.some((arg) => arg === decl.literal || arg === `"${decl.literal}"`) &&
325
+ typeof receipt.matchCount === "number" &&
326
+ (decl.operator === ">="
327
+ ? receipt.matchCount >= decl.expected
328
+ : receipt.matchCount === decl.expected));
329
+ if (!backing) {
330
+ missing.push(`${decl.operator}${decl.expected} "${decl.literal.slice(0, 40)}"`);
331
+ }
332
+ }
333
+ if (missing.length > 0) {
334
+ return {
335
+ ok: false,
336
+ reason: "assertion-not-receipt-backed",
337
+ missing,
338
+ };
339
+ }
340
+ return { ok: true };
232
341
  }
233
- function markdownList(value) {
234
- const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
235
- return items;
342
+ function sha256(content) {
343
+ return createHash("sha256").update(content).digest("hex");
236
344
  }
237
345
  /**
238
346
  * Read planned case facts from the case Markdown. Recognizes both English and
239
- * Chinese headings, including `测试点` (test points) and `测试步骤` (planned
240
- * test steps), alongside the legacy `操作步骤`/`Steps` headings.
347
+ * Chinese headings at `##` or `###`, including `测试点` (test points) and
348
+ * `测试步骤` (planned test steps), alongside the legacy `操作步骤`/`Steps`
349
+ * headings. Numbered and bulleted lists are extracted; a missing 测试目的
350
+ * falls back to the case H1 title.
241
351
  */
242
- async function readFrontendCaseContent(workspaceRoot, casePath) {
243
- if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
352
+ async function readFrontendCaseContent(workspaceRoot, casePath, layout) {
353
+ if (!safeRelativePathSchema.safeParse(casePath).success ||
354
+ !(casePath.startsWith(`${layout.casesDir}/`) ||
355
+ casePath.startsWith("testcase/frontend/cases/"))) {
244
356
  throw new Error(`unsafe frontend case path: ${casePath}`);
245
357
  }
246
358
  const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
359
+ const purpose = markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) ||
360
+ markdownH1Title(body).replace(/^FE-[^\s::]+[::]\s*/, "").trim() ||
361
+ "未提供测试目的";
247
362
  return {
248
363
  caseContent: {
249
- purpose: markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) || "未提供测试目的",
250
- preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
364
+ purpose,
365
+ preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件", "前置条件与重置"])),
251
366
  steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
252
367
  expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
253
368
  },
@@ -290,7 +405,7 @@ function assertInside(root, candidate, label) {
290
405
  throw new Error(`${label} escapes its allowed root`);
291
406
  }
292
407
  }
293
- async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
408
+ async function readEvidenceFile(repoRoot, evidenceRoot, relative, layout) {
294
409
  if (!safeRelativePathSchema.safeParse(relative).success) {
295
410
  throw new Error(`unsafe evidence path: ${relative}`);
296
411
  }
@@ -302,12 +417,16 @@ async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
302
417
  const normalized = relative;
303
418
  const evidenceRootRel = path.relative(repoRootResolved, evidenceRootResolved).replaceAll("\\", "/");
304
419
  const isCurrentCaseRepoPath = normalized === evidenceRootRel || normalized.startsWith(`${evidenceRootRel}/`);
305
- const isRepoRelative = normalized.startsWith("testcase/") && !isCurrentCaseRepoPath;
420
+ const isRepoRelative = (normalized.startsWith(`${layout.testRoot}/`) ||
421
+ normalized.startsWith("testcase/")) &&
422
+ !isCurrentCaseRepoPath;
306
423
  const absolute = isRepoRelative
307
424
  ? path.resolve(repoRootResolved, normalized)
308
425
  : path.resolve(evidenceRootResolved, normalized);
309
426
  assertInside(repoRootResolved, absolute, "evidence path");
310
- if (isRepoRelative && normalized.startsWith("testcase/frontend/evidence/")) {
427
+ if (isRepoRelative &&
428
+ (normalized.startsWith(`${layout.evidenceDir}/`) ||
429
+ normalized.startsWith("testcase/frontend/evidence/"))) {
311
430
  throw new Error(`evidence path escapes evidence root: ${relative}`);
312
431
  }
313
432
  if (!isRepoRelative)
@@ -330,9 +449,16 @@ export async function materializeFrontendTestResult(input) {
330
449
  if (!sourceBinding?.taskId || !sourceBinding.sources?.length) {
331
450
  throw new Error("frontend-test result requires frozen sourceBinding");
332
451
  }
333
- const manifestPath = "testcase/frontend/cases/manifest.json";
452
+ const layout = input.layout ?? defaultFrontendTestLayout();
453
+ const manifestPath = frontendTestManifestPath(layout);
334
454
  const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
335
- const manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
455
+ let manifest;
456
+ try {
457
+ manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
458
+ }
459
+ catch {
460
+ throw new Error("invalid or empty frontend case manifest: malformed JSON");
461
+ }
336
462
  if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.cases) || manifest.cases.length === 0) {
337
463
  throw new Error("invalid or empty frontend case manifest");
338
464
  }
@@ -340,6 +466,7 @@ export async function materializeFrontendTestResult(input) {
340
466
  const contentQuality = await validateFrontendCaseContent({
341
467
  workspaceRoot: input.workspaceRoot,
342
468
  requiredAcIds: sourceBinding.requirementIds ?? [],
469
+ layout,
343
470
  });
344
471
  advisoryFindings.push(...contentQuality.findings);
345
472
  const requiredIds = new Set(sourceBinding.requirementIds ?? []);
@@ -351,7 +478,7 @@ export async function materializeFrontendTestResult(input) {
351
478
  advisoryFindings.push({ ruleId: "invalid-manifest-case", detail: "manifest case is missing caseId, acIds, or evidenceDir" });
352
479
  continue;
353
480
  }
354
- const evidencePrefix = `testcase/frontend/evidence/${item.caseId}`;
481
+ const evidencePrefix = frontendTestEvidencePrefix(layout, item.caseId);
355
482
  if (!(item.evidenceDir === evidencePrefix || item.evidenceDir.startsWith(`${evidencePrefix}/`))) {
356
483
  throw new Error(`unsafe evidenceDir for ${item.caseId}`);
357
484
  }
@@ -382,7 +509,7 @@ export async function materializeFrontendTestResult(input) {
382
509
  if (typeof evidencePath !== "string" || evidencePath === "execution.md" && evidence.some((entry) => entry.path.endsWith("/execution.md")))
383
510
  continue;
384
511
  try {
385
- evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath));
512
+ evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath, layout));
386
513
  }
387
514
  catch (error) {
388
515
  const message = error instanceof Error ? error.message : String(error);
@@ -395,7 +522,7 @@ export async function materializeFrontendTestResult(input) {
395
522
  advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
396
523
  let blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
397
524
  if (status === "passed") {
398
- const receiptSummary = await loadCaseBrowserReceiptSummary({
525
+ const { receipts: caseReceipts, summary: receiptSummary } = await loadCaseBrowserReceiptsWithSummary({
399
526
  runDir: input.runDir,
400
527
  caseId: item.caseId,
401
528
  });
@@ -409,8 +536,26 @@ export async function materializeFrontendTestResult(input) {
409
536
  detail: "passed case lacks controller-owned open + interaction/assertion + cleanup receipts",
410
537
  });
411
538
  }
539
+ else {
540
+ // Controller-side assertion audit: every `find "literal" >=N` declared
541
+ // in the case document must be backed by a successful find receipt
542
+ // whose parsed matchCount satisfies the operator. Pass authority moves
543
+ // from model prose to receipt evidence.
544
+ const caseMarkdown = await readCaseMarkdownForAudit(input.workspaceRoot, item.casePath);
545
+ const declared = extractDeclaredFindAssertions(caseMarkdown);
546
+ const audit = auditDeclaredFindAssertions(declared, caseReceipts);
547
+ if (!audit.ok) {
548
+ status = "blocked";
549
+ blockedReason = audit.reason;
550
+ advisoryFindings.push({
551
+ ruleId: "assertion-not-receipt-backed",
552
+ caseId: item.caseId,
553
+ detail: `declared find assertions without receipt backing: ${audit.missing.join("; ")}`,
554
+ });
555
+ }
556
+ }
412
557
  }
413
- const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
558
+ const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath, layout);
414
559
  // Parse bounded fixed sections from execution.md. Missing optional
415
560
  // summary/steps are advisory only and never downgrade a passed status.
416
561
  let executionSummary = "";
@@ -459,6 +604,7 @@ export async function materializeFrontendTestResult(input) {
459
604
  passed: cases.filter((item) => item.status === "passed").length,
460
605
  failed: cases.filter((item) => item.status === "failed").length,
461
606
  blocked: cases.filter((item) => item.status === "blocked").length,
607
+ firstTryPassed: cases.filter((item) => item.status === "passed" && !item.rerunAttempt).length,
462
608
  };
463
609
  const outcome = totals.failed > 0 ? "failed" : totals.blocked > 0 || missing.length > 0 ? "incomplete" : "passed";
464
610
  const result = frontendTestResultContractSchema.parse({
@@ -0,0 +1,70 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { defaultFrontendTestLayout, frontendTestStandardScenariosRel, } from "./frontend-test-layout.js";
4
+ export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
5
+ export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
6
+ const MINIMAL_STANDARD_SCENARIOS = {
7
+ schemaVersion: 1,
8
+ id: "frontend-test-standard-scenarios-v1",
9
+ scenarios: [
10
+ {
11
+ id: "STD-FE-SMOKE-ENTRY",
12
+ title: "入口可打开",
13
+ category: "smoke",
14
+ priority: "must",
15
+ testPoints: ["open"],
16
+ minCases: 1,
17
+ },
18
+ ],
19
+ };
20
+ function isSafeRelativeDir(value) {
21
+ if (!value || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
22
+ return false;
23
+ }
24
+ return value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
25
+ }
26
+ export function listFrontendTestStandardScenarioCandidates(input) {
27
+ const seen = new Set();
28
+ const add = (relative) => {
29
+ const normalized = relative.replaceAll("\\", "/");
30
+ if (!seen.has(normalized))
31
+ seen.add(normalized);
32
+ };
33
+ add(`docs/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
34
+ const governanceRoot = input.governanceRoot?.trim().replaceAll("\\", "/").replace(/\/+$/, "");
35
+ if (governanceRoot && isSafeRelativeDir(governanceRoot)) {
36
+ add(`${governanceRoot}/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
37
+ }
38
+ add(`ai_workspace/loop-agent/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
39
+ return [...seen];
40
+ }
41
+ async function readGovernanceRoot(workspaceRoot) {
42
+ try {
43
+ const raw = JSON.parse(await readFile(path.join(workspaceRoot, "harness.json"), "utf8"));
44
+ return typeof raw.governanceRoot === "string" ? raw.governanceRoot : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ export async function copyFrontendTestStandardScenarios(input) {
51
+ const layout = input.layout ?? defaultFrontendTestLayout();
52
+ const destRel = frontendTestStandardScenariosRel(layout);
53
+ const destAbs = path.join(input.workspaceRoot, destRel);
54
+ const candidates = listFrontendTestStandardScenarioCandidates({
55
+ governanceRoot: await readGovernanceRoot(input.workspaceRoot),
56
+ });
57
+ await mkdir(path.dirname(destAbs), { recursive: true });
58
+ for (const relative of candidates) {
59
+ const sourceAbs = path.join(input.workspaceRoot, relative);
60
+ try {
61
+ await copyFile(sourceAbs, destAbs);
62
+ return { status: "copied", from: relative, to: destRel };
63
+ }
64
+ catch {
65
+ // try the next candidate
66
+ }
67
+ }
68
+ await writeFile(destAbs, `${JSON.stringify(MINIMAL_STANDARD_SCENARIOS, null, 2)}\n`, "utf8");
69
+ return { status: "fallback", to: destRel };
70
+ }