@tea-agent/loop-agent 0.38.2 → 0.38.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +171 -96
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +428 -27
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/commands/task-advance.js +31 -0
  17. package/dist/executors/dag-pi-executor.js +76 -10
  18. package/dist/executors/pi-event-serializer.js +42 -1
  19. package/dist/executors/pi-executor.js +51 -0
  20. package/dist/executors/pi-extension-resolver.js +233 -0
  21. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  22. package/dist/executors/pi-sdk-executor.js +187 -59
  23. package/dist/executors/shell-executor.js +265 -15
  24. package/dist/executors/shell-write-guard.js +7 -0
  25. package/dist/shared/dag-prompt-override.js +28 -0
  26. package/dist/shared/operator/capabilities.js +32 -0
  27. package/dist/shared/playwright-cli-command-policy.js +15 -0
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/contract/constants.js +1 -0
  32. package/dist/task/contract/diff.js +26 -0
  33. package/dist/task/contract/project.js +5 -0
  34. package/dist/task/contract/schema.js +6 -1
  35. package/dist/task/source-prepare/build-draft.js +15 -0
  36. package/dist/task/source-prepare/parse-intent.js +24 -4
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/routes.js +1 -0
  39. package/dist/worker/console/dag-execution-receipt.js +33 -0
  40. package/dist/worker/console/operator-actions.js +116 -0
  41. package/dist/worker/console/operator-user-error.js +2 -2
  42. package/dist/worker/console/pi-readiness.js +4 -4
  43. package/dist/worker/console/routes.js +43 -2
  44. package/dist/worker/console/security.js +63 -9
  45. package/dist/worker/console/server.js +36 -5
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  78. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  79. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  105. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  106. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  107. package/dist/worker/observe/node-input.js +74 -3
  108. package/dist/worker/observe/node-process.js +377 -0
  109. package/dist/worker/observe/routes.js +36 -3
  110. package/dist/worker/observe/shell-handler-keys.js +34 -0
  111. package/dist/worker/observe/static/api.js +66 -0
  112. package/dist/worker/observe/static/constants.js +8 -1
  113. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  114. package/dist/worker/observe/static/format.js +23 -0
  115. package/dist/worker/observe/static/markdown-render.js +20 -1
  116. package/dist/worker/observe/static/operator-chrome.js +4 -0
  117. package/dist/worker/observe/static/state.js +13 -2
  118. package/dist/worker/observe/static/styles.css +73 -0
  119. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  120. package/dist/worker/observe/static/views/dag.js +8 -1
  121. package/dist/worker/observe/static/views/failures.js +5 -2
  122. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  123. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
  124. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  125. package/dist/workflows/dag/contract-output-registry.js +15 -0
  126. package/dist/workflows/dag/failure-category.js +12 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  129. package/dist/workflows/dag/frontend-repair.js +14 -0
  130. package/dist/workflows/dag/frontend-risk.js +15 -2
  131. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  132. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  133. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  134. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  135. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  136. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  137. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  138. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  139. package/dist/workflows/dag/init-hybrid.js +196 -117
  140. package/dist/workflows/dag/node-execution.js +139 -10
  141. package/dist/workflows/dag/prompt.js +15 -1
  142. package/dist/workflows/dag/rerun-plan.js +134 -3
  143. package/dist/workflows/dag/rerun-run.js +58 -6
  144. package/dist/workflows/dag/retry-policy.js +91 -0
  145. package/dist/workflows/dag/structured-output-repair.js +712 -0
  146. package/dist/workflows/dag/types.js +32 -1
  147. package/dist/workflows/dag/validate.js +41 -1
  148. package/docs/init-surface.manifest.json +5 -0
  149. package/docs/operations/README.md +1 -1
  150. package/docs/operations/local-development-environment.md +1 -5
  151. package/docs/skills/vetted-skill-registry.md +14 -0
  152. package/docs/templates/README.md +1 -1
  153. package/docs/templates/agent-dag.schema.json +44 -0
  154. package/docs/templates/backend-test-dag.json +2 -2
  155. package/docs/templates/frontend-test-dag.json +8 -10
  156. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  157. package/docs/templates/init-managed-agents.md +13 -3
  158. package/harness.json +2 -2
  159. package/package.json +12 -4
  160. package/scripts/next-info.mjs +356 -0
  161. package/scripts/next-publish-gate.mjs +238 -0
  162. package/scripts/release-source-binding.mjs +251 -0
  163. package/skills/analyze-product-requirements/SKILL.md +8 -5
  164. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  165. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  166. package/skills/analyze-product-requirements/references/example.md +50 -0
  167. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  168. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  169. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  170. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  171. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  172. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  173. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  174. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  175. package/skills/codebase-scout/SKILL.md +1 -1
  176. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  178. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  179. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  180. package/skills/improve-codebase-architecture/deepening.md +37 -0
  181. package/skills/improve-codebase-architecture/html-report.md +123 -0
  182. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  183. package/skills/improve-codebase-architecture/language.md +53 -0
  184. package/skills/loop-agent/references/command-reference.md +11 -4
  185. package/skills/playwright-cli/SKILL.md +22 -0
  186. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  187. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  188. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  189. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  190. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  191. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  192. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -5,7 +5,27 @@ 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";
8
9
  export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
10
+ /**
11
+ * Truncated acceptance-id shapes observed in generator output: a bare
12
+ * numeric segment ("AC-002" dropped its AC-FE-<FEATURE> prefix) or an
13
+ * AC-FE-<FEATURE> literal without the trailing -NNN sequence number
14
+ * ("AC-FE-CHAT"). Generic short ids like "AC-1"/"AC-FE-001" stay valid;
15
+ * declared-source cross-checks (unknown-ac) remain the primary guard when a
16
+ * sourceBinding exists.
17
+ */
18
+ export function isTruncatedFrontendAcId(acId) {
19
+ return /^AC-FE-[A-Z]+$/i.test(acId.trim());
20
+ }
21
+ /**
22
+ * Bare numeric ids ("AC-002") only indicate truncation when no declared AC
23
+ * list exists to adjudicate them: with a sourceBinding they are ordinary
24
+ * unknown-ac findings ("AC-999" may be a legitimately declared id style).
25
+ */
26
+ export function isTruncatedWhenUndeclaredFrontendAcId(acId) {
27
+ return /^AC-\d{3}$/i.test(acId.trim()) || isTruncatedFrontendAcId(acId);
28
+ }
9
29
  const safeRelativePathSchema = z.string().min(1).refine((value) => !path.posix.isAbsolute(value) &&
10
30
  !path.win32.isAbsolute(value) &&
11
31
  !value.includes("\\") &&
@@ -56,6 +76,8 @@ export const frontendTestResultContractSchema = z.object({
56
76
  passed: z.number().int().min(0),
57
77
  failed: z.number().int().min(0),
58
78
  blocked: z.number().int().min(0),
79
+ /** Stability metric: passed on attempt 0 (no rerun/self-heal retry). */
80
+ firstTryPassed: z.number().int().min(0).optional(),
59
81
  }).strict(),
60
82
  acceptanceCoverage: z.object({
61
83
  covered: z.array(z.string().min(1)),
@@ -109,7 +131,13 @@ export async function validateFrontendCaseEvidence(input) {
109
131
  catch {
110
132
  throw new Error("missing testcase/frontend/cases/manifest.json");
111
133
  }
112
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
134
+ let manifest;
135
+ try {
136
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
137
+ }
138
+ catch {
139
+ throw new Error("invalid frontend case manifest: malformed JSON");
140
+ }
113
141
  if (!Array.isArray(manifest.cases))
114
142
  throw new Error("invalid frontend case manifest");
115
143
  const statuses = new Set(["passed", "failed", "blocked"]);
@@ -194,11 +222,17 @@ export function evaluatePassedCaseBrowserReceipts(summary, caseId) {
194
222
  }
195
223
  return { ok: true };
196
224
  }
197
- async function loadCaseBrowserReceiptSummary(input) {
225
+ async function loadCaseBrowserReceiptsWithSummary(input) {
198
226
  try {
199
227
  const entries = await readdir(input.runDir, { withFileTypes: true });
200
228
  // A passed chain must belong to exactly one browser child stream. Never
201
- // stitch receipts across DAG nodes or accept ambiguous duplicate authority.
229
+ // stitch receipts across DAG nodes or accept ambiguous duplicate
230
+ // authority — with one exception: bounded rerun creates a second,
231
+ // later stream for the same case (the rerun overwrites the disk
232
+ // case-result, so the final attempt is the authoritative one). When
233
+ // multiple streams authorize, prefer the highest rerun round
234
+ // (rerun-frontend-case-rN-<idx> directories); ties within the same
235
+ // round remain ambiguous and fail closed.
202
236
  const authorizingStreams = [];
203
237
  for (const entry of entries) {
204
238
  if (!entry.isDirectory())
@@ -206,48 +240,121 @@ async function loadCaseBrowserReceiptSummary(input) {
206
240
  const nodeReceipts = (await readPlaywrightCliReceipts(input.runDir, entry.name))
207
241
  .filter((item) => item.caseId === input.caseId);
208
242
  const summary = summarizePlaywrightCliReceipts(nodeReceipts);
209
- if (summary.hasOrderedPassedReceiptChain)
210
- authorizingStreams.push(summary);
243
+ if (summary.hasOrderedPassedReceiptChain) {
244
+ const roundMatch = /rerun-frontend-case-r(\d+)-/i.exec(entry.name);
245
+ authorizingStreams.push({
246
+ receipts: nodeReceipts,
247
+ summary,
248
+ round: roundMatch ? Number(roundMatch[1]) : 0,
249
+ });
250
+ }
251
+ }
252
+ if (authorizingStreams.length === 0) {
253
+ return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
211
254
  }
212
- return authorizingStreams.length === 1
213
- ? authorizingStreams[0]
214
- : summarizePlaywrightCliReceipts([]);
255
+ const maxRound = Math.max(...authorizingStreams.map((s) => s.round));
256
+ const finalists = authorizingStreams.filter((s) => s.round === maxRound);
257
+ return finalists.length === 1
258
+ ? finalists[0]
259
+ : { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
215
260
  }
216
261
  catch {
217
- return summarizePlaywrightCliReceipts([]);
262
+ return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
218
263
  }
219
264
  }
220
- function sha256(content) {
221
- return createHash("sha256").update(content).digest("hex");
265
+ /**
266
+ * Extract `find "literal" >=N` / `find "literal"` declarations from a case
267
+ * document. Only quoted literals are binding assertions — the count suffix
268
+ * (`>=N`, `==N`) is the expected minimum/exact match count from the ledger.
269
+ * Placeholder literals ("...", "…", "<字面>", "eX") appear in prose discussing
270
+ * the syntax itself; they are documentation, never declarations.
271
+ */
272
+ export function extractDeclaredFindAssertions(caseMarkdown) {
273
+ const source = stripFencedCodeBlocks(caseMarkdown);
274
+ const out = [];
275
+ const seen = new Set();
276
+ const pattern = /find\s+"((?:[^"\\]|\\.)+)"\s*(>=|==)\s*(\d+)/g;
277
+ const isPlaceholder = (literal) => /^(?:\.{2,}|…+|<[^>]*>|e\d+)$/.test(literal.trim());
278
+ let match;
279
+ while ((match = pattern.exec(source)) !== null) {
280
+ const literal = match[1].replace(/\\(.)/g, "$1");
281
+ if (isPlaceholder(literal))
282
+ continue;
283
+ const operator = match[2];
284
+ const expected = Number(match[3]);
285
+ const key = `${operator}:${literal}`;
286
+ if (seen.has(key))
287
+ continue;
288
+ seen.add(key);
289
+ out.push({ literal, expected, operator });
290
+ }
291
+ return out;
222
292
  }
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)
293
+ function stripFencedCodeBlocks(markdown) {
294
+ return markdown.replace(/```[\s\S]*?```/g, "");
295
+ }
296
+ async function readCaseMarkdownForAudit(workspaceRoot, casePath) {
297
+ try {
298
+ return await readFile(path.join(workspaceRoot, casePath), "utf8");
299
+ }
300
+ catch {
228
301
  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();
302
+ }
303
+ }
304
+ /**
305
+ * Controller-side assertion audit: every find-family declaration in the case
306
+ * document must be backed by a successful find receipt whose parsed match
307
+ * count satisfies the declared operator. This moves pass authority from
308
+ * "the model says it asserted" to "receipts prove it asserted".
309
+ */
310
+ export function auditDeclaredFindAssertions(declared, receipts) {
311
+ if (declared.length === 0)
312
+ return { ok: true };
313
+ const successfulFinds = receipts.filter((receipt) => (receipt.command === "find" || receipt.command === "find-unique") &&
314
+ receipt.exitCode === 0 &&
315
+ !receipt.timedOut);
316
+ const missing = [];
317
+ for (const decl of declared) {
318
+ const backing = successfulFinds.some((receipt) => receipt.argsRedacted.some((arg) => arg === decl.literal || arg === `"${decl.literal}"`) &&
319
+ typeof receipt.matchCount === "number" &&
320
+ (decl.operator === ">="
321
+ ? receipt.matchCount >= decl.expected
322
+ : receipt.matchCount === decl.expected));
323
+ if (!backing) {
324
+ missing.push(`${decl.operator}${decl.expected} "${decl.literal.slice(0, 40)}"`);
325
+ }
326
+ }
327
+ if (missing.length > 0) {
328
+ return {
329
+ ok: false,
330
+ reason: "assertion-not-receipt-backed",
331
+ missing,
332
+ };
333
+ }
334
+ return { ok: true };
232
335
  }
233
- function markdownList(value) {
234
- const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
235
- return items;
336
+ function sha256(content) {
337
+ return createHash("sha256").update(content).digest("hex");
236
338
  }
237
339
  /**
238
340
  * 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.
341
+ * Chinese headings at `##` or `###`, including `测试点` (test points) and
342
+ * `测试步骤` (planned test steps), alongside the legacy `操作步骤`/`Steps`
343
+ * headings. Numbered and bulleted lists are extracted; a missing 测试目的
344
+ * falls back to the case H1 title.
241
345
  */
242
346
  async function readFrontendCaseContent(workspaceRoot, casePath) {
243
347
  if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
244
348
  throw new Error(`unsafe frontend case path: ${casePath}`);
245
349
  }
246
350
  const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
351
+ const purpose = markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) ||
352
+ markdownH1Title(body).replace(/^FE-[^\s::]+[::]\s*/, "").trim() ||
353
+ "未提供测试目的";
247
354
  return {
248
355
  caseContent: {
249
- purpose: markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) || "未提供测试目的",
250
- preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
356
+ purpose,
357
+ preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件", "前置条件与重置"])),
251
358
  steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
252
359
  expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
253
360
  },
@@ -332,7 +439,13 @@ export async function materializeFrontendTestResult(input) {
332
439
  }
333
440
  const manifestPath = "testcase/frontend/cases/manifest.json";
334
441
  const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
335
- const manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
442
+ let manifest;
443
+ try {
444
+ manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
445
+ }
446
+ catch {
447
+ throw new Error("invalid or empty frontend case manifest: malformed JSON");
448
+ }
336
449
  if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.cases) || manifest.cases.length === 0) {
337
450
  throw new Error("invalid or empty frontend case manifest");
338
451
  }
@@ -395,7 +508,7 @@ export async function materializeFrontendTestResult(input) {
395
508
  advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
396
509
  let blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
397
510
  if (status === "passed") {
398
- const receiptSummary = await loadCaseBrowserReceiptSummary({
511
+ const { receipts: caseReceipts, summary: receiptSummary } = await loadCaseBrowserReceiptsWithSummary({
399
512
  runDir: input.runDir,
400
513
  caseId: item.caseId,
401
514
  });
@@ -409,6 +522,24 @@ export async function materializeFrontendTestResult(input) {
409
522
  detail: "passed case lacks controller-owned open + interaction/assertion + cleanup receipts",
410
523
  });
411
524
  }
525
+ else {
526
+ // Controller-side assertion audit: every `find "literal" >=N` declared
527
+ // in the case document must be backed by a successful find receipt
528
+ // whose parsed matchCount satisfies the operator. Pass authority moves
529
+ // from model prose to receipt evidence.
530
+ const caseMarkdown = await readCaseMarkdownForAudit(input.workspaceRoot, item.casePath);
531
+ const declared = extractDeclaredFindAssertions(caseMarkdown);
532
+ const audit = auditDeclaredFindAssertions(declared, caseReceipts);
533
+ if (!audit.ok) {
534
+ status = "blocked";
535
+ blockedReason = audit.reason;
536
+ advisoryFindings.push({
537
+ ruleId: "assertion-not-receipt-backed",
538
+ caseId: item.caseId,
539
+ detail: `declared find assertions without receipt backing: ${audit.missing.join("; ")}`,
540
+ });
541
+ }
542
+ }
412
543
  }
413
544
  const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
414
545
  // Parse bounded fixed sections from execution.md. Missing optional
@@ -459,6 +590,7 @@ export async function materializeFrontendTestResult(input) {
459
590
  passed: cases.filter((item) => item.status === "passed").length,
460
591
  failed: cases.filter((item) => item.status === "failed").length,
461
592
  blocked: cases.filter((item) => item.status === "blocked").length,
593
+ firstTryPassed: cases.filter((item) => item.status === "passed" && !item.rerunAttempt).length,
462
594
  };
463
595
  const outcome = totals.failed > 0 ? "failed" : totals.blocked > 0 || missing.length > 0 ? "incomplete" : "passed";
464
596
  const result = frontendTestResultContractSchema.parse({
@@ -0,0 +1,68 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
4
+ export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
5
+ const MINIMAL_STANDARD_SCENARIOS = {
6
+ schemaVersion: 1,
7
+ id: "frontend-test-standard-scenarios-v1",
8
+ scenarios: [
9
+ {
10
+ id: "STD-FE-SMOKE-ENTRY",
11
+ title: "入口可打开",
12
+ category: "smoke",
13
+ priority: "must",
14
+ testPoints: ["open"],
15
+ minCases: 1,
16
+ },
17
+ ],
18
+ };
19
+ function isSafeRelativeDir(value) {
20
+ if (!value || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
21
+ return false;
22
+ }
23
+ return value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
24
+ }
25
+ export function listFrontendTestStandardScenarioCandidates(input) {
26
+ const seen = new Set();
27
+ const add = (relative) => {
28
+ const normalized = relative.replaceAll("\\", "/");
29
+ if (!seen.has(normalized))
30
+ seen.add(normalized);
31
+ };
32
+ add(`docs/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
33
+ const governanceRoot = input.governanceRoot?.trim().replaceAll("\\", "/").replace(/\/+$/, "");
34
+ if (governanceRoot && isSafeRelativeDir(governanceRoot)) {
35
+ add(`${governanceRoot}/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
36
+ }
37
+ add(`ai_workspace/loop-agent/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
38
+ return [...seen];
39
+ }
40
+ async function readGovernanceRoot(workspaceRoot) {
41
+ try {
42
+ const raw = JSON.parse(await readFile(path.join(workspaceRoot, "harness.json"), "utf8"));
43
+ return typeof raw.governanceRoot === "string" ? raw.governanceRoot : undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ export async function copyFrontendTestStandardScenarios(input) {
50
+ const destRel = FRONTEND_TEST_STANDARD_SCENARIOS_DEST;
51
+ const destAbs = path.join(input.workspaceRoot, destRel);
52
+ const candidates = listFrontendTestStandardScenarioCandidates({
53
+ governanceRoot: await readGovernanceRoot(input.workspaceRoot),
54
+ });
55
+ await mkdir(path.dirname(destAbs), { recursive: true });
56
+ for (const relative of candidates) {
57
+ const sourceAbs = path.join(input.workspaceRoot, relative);
58
+ try {
59
+ await copyFile(sourceAbs, destAbs);
60
+ return { status: "copied", from: relative, to: destRel };
61
+ }
62
+ catch {
63
+ // try the next candidate
64
+ }
65
+ }
66
+ await writeFile(destAbs, `${JSON.stringify(MINIMAL_STANDARD_SCENARIOS, null, 2)}\n`, "utf8");
67
+ return { status: "fallback", to: destRel };
68
+ }
@@ -2,6 +2,18 @@ import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
4
4
  export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
5
+ /**
6
+ * Shared marker for a project that exposes no usable verification command at
7
+ * generation time. The marker command runs (and logs not-run honestly), but it
8
+ * must never count as successful verification evidence: labels carrying this
9
+ * text are excluded from successful evidence and targets bound to it fail the
10
+ * trace gate instead of passing it.
11
+ */
12
+ export const FRONTEND_NO_VERIFICATION_MARKER_TEXT = "No frontend verification command configured";
13
+ export function isFrontendNoVerificationMarkerLabel(label) {
14
+ return label.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT);
15
+ }
16
+ export const FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE = `frontend verification unavailable: the only frozen static command is the no-op not-run marker (${FRONTEND_NO_VERIFICATION_MARKER_TEXT}); declare verification commands via task.json --verify or 执行约束.md and regenerate the DAG`;
5
17
  async function readJsonFile(filePath) {
6
18
  return JSON.parse(await readFile(filePath, "utf8"));
7
19
  }
@@ -52,7 +64,14 @@ export async function evaluateVerificationTargets(input) {
52
64
  const hardIssues = [];
53
65
  for (const target of input.contract.verificationTargets) {
54
66
  const issues = [];
55
- const owners = input.labelOwners.get(target.commandLabel);
67
+ if (isFrontendNoVerificationMarkerLabel(target.commandLabel)) {
68
+ // A target bound to the not-run marker has no verification behind it;
69
+ // passing it would let the run finish green without any check.
70
+ issues.push(`${target.id}: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`);
71
+ }
72
+ const owners = isFrontendNoVerificationMarkerLabel(target.commandLabel)
73
+ ? []
74
+ : (input.labelOwners.get(target.commandLabel) ?? []);
56
75
  if (!owners?.length) {
57
76
  issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
58
77
  }
@@ -189,11 +208,17 @@ export async function runFrontendVerificationTraceGate(input) {
189
208
  throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
190
209
  }
191
210
  if (input.evidence) {
192
- if (input.evidence.static.commandLabels.length === 0) {
193
- throw new Error("trace: no successful static verification commands in current run");
211
+ // The no-op not-run marker never counts as successful verification
212
+ // evidence, even if it executed with exit code 0.
213
+ const effectiveStaticLabels = input.evidence.static.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
214
+ const effectiveBehaviorLabels = input.evidence.behavior.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
215
+ if (effectiveStaticLabels.length === 0) {
216
+ throw new Error(input.evidence.static.commandLabels.length > 0
217
+ ? `trace: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`
218
+ : "trace: no successful static verification commands in current run");
194
219
  }
195
220
  if (requiresBehaviorVerification &&
196
- input.evidence.behavior.commandLabels.length === 0) {
221
+ effectiveBehaviorLabels.length === 0) {
197
222
  throw new Error("trace: no successful behavior verification commands in current run");
198
223
  }
199
224
  }