@tea-agent/loop-agent 0.39.0-beta.4 → 0.39.0-beta.6

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 (116) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi-executor.js +167 -31
  4. package/dist/executors/pi-playwright-cli-tool.js +23 -16
  5. package/dist/executors/shell-executor.js +25 -8
  6. package/dist/shared/dag-failure-category.js +138 -0
  7. package/dist/task/config-types.js +6 -0
  8. package/dist/task/source-references.js +22 -1
  9. package/dist/worker/console/chat/operation-card.js +8 -1
  10. package/dist/worker/console/chat/routes.js +5 -3
  11. package/dist/worker/console/inspect-split.js +18 -0
  12. package/dist/worker/console/operation-run-facts.js +19 -3
  13. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-zsNyvGaH.js → abnfDiagram-N423BO3Z-CfDSkLtp.js} +1 -1
  14. package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BlefM6I5.js} +1 -1
  15. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-C4tHfzHn.js} +1 -1
  16. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-G-G77Rh1.js} +1 -1
  17. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-DWgxmY72.js} +1 -1
  18. package/dist/worker/console/static/assets/channel-DsdX3ZUX.js +1 -0
  19. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-CJQcfbCY.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-vUPviH9y.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-DzRXSTeh.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-CWfgqm07.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-BtTDauXA.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-Bc-NNt84.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-B_f-emL4.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-DV-jgJ66.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-DMqzn4RZ.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-CVNtlAm4.js} +1 -1
  29. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CrvCyJ-8.js +1 -0
  30. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CrvCyJ-8.js +1 -0
  31. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BBZBhVTd.js} +1 -1
  32. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-DbMU2PZ2.js} +1 -1
  33. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Dj0v8rjc.js} +1 -1
  34. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-BW8R4WvJ.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-DEgWz3ld.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-BAA01rlR.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Cau6qqpW.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-8HiPsLos.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-CmkXuTYL.js} +1 -1
  40. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DCV_0seW.js} +1 -1
  41. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-gckfWESj.js} +1 -1
  42. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-D2mT2BUu.js} +1 -1
  43. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-B_HR5qEw.js} +1 -1
  44. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-BUw4fpSa.js} +1 -1
  45. package/dist/worker/console/static/assets/{index-D5zGX6fG.js → index-DPbsruV5.js} +81 -79
  46. package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
  47. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BSRAqEj0.js} +1 -1
  48. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-CLxE8hUp.js} +1 -1
  49. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-juFzVXIj.js} +1 -1
  50. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-CwLHVhOU.js} +1 -1
  51. package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-9LfUyPAs.js} +1 -1
  52. package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-TPPHfQbe.js} +5 -5
  53. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-Dn_Z9RxF.js} +1 -1
  54. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CiEMKTgm.js} +1 -1
  55. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-JASaGxjZ.js} +1 -1
  56. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-ChmH0ply.js} +1 -1
  57. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-CtyM-HGQ.js} +1 -1
  58. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-BN9y_Bvs.js} +1 -1
  59. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-IyS0GQ1-.js} +1 -1
  60. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DjxGXurN.js} +1 -1
  61. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-Cr3Z0S8y.js} +1 -1
  62. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-BjJFC6pW.js} +1 -1
  63. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bq44O9Nt.js +1 -0
  64. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-DeMUJQiW.js} +2 -2
  65. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BT4tWyW3.js +8 -0
  66. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-Bc2Rv2Oo.js} +1 -1
  67. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-CDhRUeqG.js} +1 -1
  68. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DaN_z3Xy.js} +1 -1
  69. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-C1vTLBDG.js} +1 -1
  70. package/dist/worker/console/static/index.html +2 -2
  71. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  72. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  73. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  74. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  75. package/dist/worker/console/static-src/operator-chat/turn-submission.js +13 -1
  76. package/dist/worker/console/static-src/operator-chat/useChatStream.js +29 -9
  77. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  78. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  79. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  80. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  81. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  82. package/dist/worker/observability/read-model.js +22 -0
  83. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  84. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  85. package/dist/workflows/dag/failure-category.js +7 -128
  86. package/dist/workflows/dag/frontend-design-policy.js +12 -7
  87. package/dist/workflows/dag/frontend-implementation-contract.js +10 -5
  88. package/dist/workflows/dag/frontend-plan-render.js +0 -3
  89. package/dist/workflows/dag/frontend-shadow-dual-write.js +51 -10
  90. package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
  91. package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
  92. package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
  93. package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
  94. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  95. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  96. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  97. package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
  98. package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
  99. package/dist/workflows/dag/frontend-typed-event-store.js +13 -0
  100. package/dist/workflows/dag/init-hybrid.js +66 -19
  101. package/dist/workflows/dag/node-execution.js +21 -1
  102. package/dist/workflows/dag/retry-policy.js +32 -0
  103. package/dist/workflows/dag/types.js +12 -0
  104. package/dist/workflows/dag/validate.js +23 -19
  105. package/docs/templates/frontend-test-case-checklist.md +2 -2
  106. package/package.json +1 -1
  107. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  108. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  109. package/skills/playwright-cli/SKILL.md +1 -1
  110. package/skills/playwright-cli-case-generator/SKILL.md +10 -8
  111. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +0 -1
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +0 -1
  114. package/dist/worker/console/static/assets/index-DBbhESQ_.css +0 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +0 -1
  116. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +0 -8
@@ -1,5 +1,6 @@
1
1
  import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { defaultFrontendTestLayout, frontendTestStandardScenariosRel, } from "./frontend-test-layout.js";
3
4
  export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
4
5
  export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
5
6
  const MINIMAL_STANDARD_SCENARIOS = {
@@ -47,7 +48,8 @@ async function readGovernanceRoot(workspaceRoot) {
47
48
  }
48
49
  }
49
50
  export async function copyFrontendTestStandardScenarios(input) {
50
- const destRel = FRONTEND_TEST_STANDARD_SCENARIOS_DEST;
51
+ const layout = input.layout ?? defaultFrontendTestLayout();
52
+ const destRel = frontendTestStandardScenariosRel(layout);
51
53
  const destAbs = path.join(input.workspaceRoot, destRel);
52
54
  const candidates = listFrontendTestStandardScenarioCandidates({
53
55
  governanceRoot: await readGovernanceRoot(input.workspaceRoot),
@@ -113,6 +113,18 @@ export const contractBlockingOwnerSchema = z.enum([
113
113
  * `finalize_plan` terminal fact.
114
114
  */
115
115
  export const PLAN_TERMINAL_FACT_KINDS = ["finalize_plan"];
116
+ /**
117
+ * A+B: incremental payload records for `frontend-plan-pi` (requirements,
118
+ * verification targets, evidence gaps). Each is committed by one
119
+ * `record_plan_*` call so a large plan never exceeds a single model output
120
+ * budget; `assemblePlanPatchFromCommittedFacts` aggregates them from the
121
+ * ledger in commit order.
122
+ */
123
+ export const PLAN_RECORD_FACT_KINDS = [
124
+ "plan-requirement",
125
+ "plan-verification-target",
126
+ "plan-evidence-gap",
127
+ ];
116
128
  /**
117
129
  * A+B (AC-009): typed issue category shared by review and design change
118
130
  * requests. The five-value enum replaces free-form issueCategory strings.
@@ -143,6 +155,7 @@ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
143
155
  ...FRONTEND_SHAPE_FACT_KINDS,
144
156
  ...CONTRACT_FACT_KINDS,
145
157
  ...PLAN_TERMINAL_FACT_KINDS,
158
+ ...PLAN_RECORD_FACT_KINDS,
146
159
  ]),
147
160
  ];
148
161
  export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
@@ -11,7 +11,7 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
11
11
  import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, DEFAULT_FRONTEND_SPEC_ROOTS, extractTaskSourceFrontendSpecPaths, } from "../../shared/openspec-spec.js";
12
12
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
13
13
  import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
14
- import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
14
+ import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, FRONTEND_REVIEW_TERMINAL_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
15
15
  import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
16
16
  import { resolveAdapter } from "../../adapters/index.js";
17
17
  import { loadHarnessManifest } from "../../governance/harness.js";
@@ -28,6 +28,7 @@ import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
28
28
  import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
29
29
  import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
30
30
  import { resolveBackendTestLayout, } from "./backend-test-layout.js";
31
+ import { applyFrontendTestLayoutToText, resolveFrontendTestLayout, } from "./frontend-test-layout.js";
31
32
  import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
32
33
  import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
33
34
  import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
@@ -2651,7 +2652,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2651
2652
  '{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
2652
2653
  "",
2653
2654
  "### optional plan fields - GOOD (all optional; omit when absent):",
2654
- '{"implementationSteps":["confirm contract","sync tests"],"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
2655
+ '{"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
2655
2656
  "",
2656
2657
  "### optional plan fields - BAD (present-but-empty strings are rejected):",
2657
2658
  '{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
@@ -2676,8 +2677,8 @@ async function buildFrontendHybridDagFromTask(sources) {
2676
2677
  "- targets: files, routes, publicApiChanges",
2677
2678
  "- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
2678
2679
  "- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
2679
- "- designEvidence: source, paths, conflicts; evidenceGaps[]",
2680
- "- optional: implementationSteps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
2680
+ "- designEvidence: source, paths, conflicts; evidenceGaps[] (optional)",
2681
+ "- optional: stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
2681
2682
  "- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
2682
2683
  "Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
2683
2684
  ].join("\n");
@@ -2982,13 +2983,14 @@ async function buildFrontendHybridDagFromTask(sources) {
2982
2983
  retryOnInvalid: true,
2983
2984
  skeleton: frontendContractSkeleton,
2984
2985
  },
2985
- outputContract: "Short Markdown narrative plus incremental typed plan record tools (record_target_surface / record_component_choice / record_state_flow / record_data_flow / record_mock_api / record_design_deviation / record_dependency) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from committed facts (requirements / verificationTargets / evidenceGaps / implementationSteps / residualRisks / realIntegrationGap). Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime compiles facts ⊕ skeleton into canonical full-contract JSON for downstream review. No file writes.",
2986
+ outputContract: "Short Markdown narrative plus incremental typed plan record tools (record_target_surface / record_component_choice / record_state_flow / record_data_flow / record_mock_api / record_design_deviation / record_dependency / record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from the committed ledger facts (requirements / verificationTargets / evidenceGaps / residualRisks / realIntegrationGap). Commit requirements and verification targets incrementally — one entry per record call — so the plan never needs a single large output; evidence gaps are optional and only needed for genuine gaps. Implementation steps are NOT part of the plan — the implementer designs its own ordering. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime compiles facts ⊕ skeleton into canonical full-contract JSON for downstream review. No file writes.",
2986
2987
  subtask_prompt: [
2987
2988
  "Use frontend-contract-pi typed requirement facts (stable REQ/BR/AC identifiers, dispositions, evidence expectations, and frontend-test handoff intents), frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Record the plan decision ledger through the incremental record_* tools, then call finalize_plan exactly once. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
2989
+ "Commit requirements with record_plan_requirement (one entry per call) and verification targets with record_plan_verification_target (one entry per call). Omit expectedOutcome in requirements — the runtime derives it from the contract; keep each entry to id + targets + verificationTargetIds. Evidence gaps are optional — call record_plan_evidence_gap only for genuine gaps. Never bundle these into finalize_plan arguments — finalize_plan only closes the plan. CRITICAL: emit exactly ONE record tool call per assistant message — never batch multiple record_* calls in the same message, even though they look independent. Parallel-batching them makes a single message as large as the old full-contract output and re-introduces the truncation bug. One call per message, many messages, then finalize_plan exactly once at the end. Do NOT plan implementation steps: the implementer decides ordering itself.",
2988
2990
  ...(requiresOpenspecClassification ? ["Additionally consume the frontend-contract-pi openspec classification. Successfully read every required selection (including mandatory paths) and declare any used component specReference in the typed decision ledger; relevant/irrelevant selections are not forced into the ledger unless the plan actually uses them."] : []),
2989
2991
  "The incremental record_* facts become the complete implementation plan after deterministic merge with the protected skeleton. Do not treat leftover JSON in the narrative as the compile authority.",
2990
2992
  "Select the Mock / API strategy only through record_mock_api. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
2991
- "Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the ledger payload. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2993
+ "Encode target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the ledger payload. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2992
2994
  "Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
2993
2995
  "Consume the Scout target surface and design evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
2994
2996
  "Output a short Markdown narrative, record the incremental facts, then call finalize_plan exactly once. Do NOT emit protected skeleton fields or a full contract as the authority.",
@@ -3234,6 +3236,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3234
3236
  executor: "pi",
3235
3237
  complexity: "HIGH",
3236
3238
  writePolicy: "read-only",
3239
+ retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
3237
3240
  allowedPaths: readOnlyPaths,
3238
3241
  forbiddenPaths,
3239
3242
  skills: FRONTEND_REVIEW_SKILLS,
@@ -4860,11 +4863,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
4860
4863
  spec.successCriteria = spec.successCriteria?.map(rewrite);
4861
4864
  spec.backendTestLayout = layout;
4862
4865
  }
4866
+ /** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
4867
+ function applyFrontendTestLayoutToDagSpec(spec, layout) {
4868
+ if (layout.isDefault) {
4869
+ spec.frontendTestLayout = layout;
4870
+ return;
4871
+ }
4872
+ const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
4873
+ for (const task of spec.tasks) {
4874
+ task.subtask_prompt = rewrite(task.subtask_prompt);
4875
+ if (task.outputContract)
4876
+ task.outputContract = rewrite(task.outputContract);
4877
+ task.writeSet = task.writeSet?.map(rewrite);
4878
+ task.allowedPaths = task.allowedPaths?.map(rewrite);
4879
+ task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
4880
+ if (task.dynamicExpansion?.childTask) {
4881
+ const child = task.dynamicExpansion.childTask;
4882
+ if (typeof child.subtaskPromptTemplate === "string") {
4883
+ child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
4884
+ }
4885
+ if (typeof child.outputContract === "string") {
4886
+ child.outputContract = rewrite(child.outputContract);
4887
+ }
4888
+ if (Array.isArray(child.allowedPaths)) {
4889
+ child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
4890
+ }
4891
+ if (Array.isArray(child.writeSet)) {
4892
+ child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
4893
+ }
4894
+ if (Array.isArray(child.forbiddenPaths)) {
4895
+ child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
4896
+ }
4897
+ }
4898
+ if (task.shell?.commands) {
4899
+ task.shell.commands = task.shell.commands.map(rewrite);
4900
+ }
4901
+ }
4902
+ spec.globalConstraints = spec.globalConstraints?.map(rewrite);
4903
+ spec.successCriteria = spec.successCriteria?.map(rewrite);
4904
+ spec.frontendTestLayout = layout;
4905
+ }
4906
+ function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
4907
+ const probe = `${testRoot}/_layout_probe_`;
4908
+ return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
4909
+ pathMatchesPattern(probe, pattern));
4910
+ }
4863
4911
  // ---------------------------------------------------------------------------
4864
4912
  // Frontend browser-test RAG DAG template
4865
4913
  // ---------------------------------------------------------------------------
4866
4914
  function buildFrontendTestHybridDag(sources) {
4867
4915
  const rawFrontendTest = sources.taskConfig.frontendTest;
4916
+ const layout = resolveFrontendTestLayout(rawFrontendTest);
4868
4917
  const config = {
4869
4918
  // Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
4870
4919
  maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
@@ -4889,9 +4938,9 @@ function buildFrontendTestHybridDag(sources) {
4889
4938
  // Optional project-local UI anchor ledger: only mention it in prompts when it
4890
4939
  // exists so ledger-less projects keep generating without noise.
4891
4940
  const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
4892
- existsSync(path.join(sources.repoRoot, "testcase/frontend/rag/ui-anchors.md")));
4941
+ existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
4893
4942
  const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
4894
- ? "Also read testcase/frontend/rag/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly."
4943
+ ? `Also read ${layout.ragDir}/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly.`
4895
4944
  : "";
4896
4945
  const reviewMode = config.reviewMode;
4897
4946
  const blockingReview = reviewMode === "blocking";
@@ -4899,22 +4948,19 @@ function buildFrontendTestHybridDag(sources) {
4899
4948
  const maxRerunAttempts = config.maxRerunAttempts;
4900
4949
  const enableRetrospect = config.reports?.retrospect === true;
4901
4950
  const enableL5Report = config.reports?.l5 !== false;
4902
- const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
4903
- pattern === "testcase/**" ||
4904
- pattern === "**");
4905
- if (!hasFrontendTestWriteScope) {
4906
- throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
4951
+ if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
4952
+ throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
4907
4953
  }
4908
4954
  const forbidden = commonForbiddenPaths(sources);
4909
- const ragWriteSet = ["testcase/frontend/rag/**"];
4955
+ const ragWriteSet = [`${layout.ragDir}/**`];
4910
4956
  const caseDraftWriteSet = [
4911
- "testcase/frontend/cases/FE-*.md",
4912
- "testcase/frontend/cases/index.md",
4913
- "testcase/frontend/cases/manifest.draft.json",
4957
+ `${layout.casesDir}/FE-*.md`,
4958
+ `${layout.casesDir}/index.md`,
4959
+ `${layout.casesDir}/manifest.draft.json`,
4914
4960
  ];
4915
4961
  // The shell materializer alone owns the final manifest boundary.
4916
- const casesWriteSet = ["testcase/frontend/cases/**"];
4917
- const evidenceRoot = "testcase/frontend/evidence";
4962
+ const casesWriteSet = [`${layout.casesDir}/**`];
4963
+ const evidenceRoot = layout.evidenceDir;
4918
4964
  const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
4919
4965
  const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
4920
4966
  const checklistScript = [
@@ -5506,6 +5552,7 @@ function buildFrontendTestHybridDag(sources) {
5506
5552
  verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
5507
5553
  tasks,
5508
5554
  };
5555
+ applyFrontendTestLayoutToDagSpec(spec, layout);
5509
5556
  applyDefaultReadOnlyRetryPolicy(spec);
5510
5557
  parseDagSpec(spec);
5511
5558
  assertValidDagSpec(spec);
@@ -228,6 +228,26 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
228
228
  buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
229
229
  ].join("\n");
230
230
  }
231
+ if (previousFailureCategory === "review-terminal-missing") {
232
+ return [
233
+ basePrompt,
234
+ "",
235
+ "<retry_instruction>",
236
+ "The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
237
+ "Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
238
+ "</retry_instruction>",
239
+ ].join("\n");
240
+ }
241
+ if (previousFailureCategory === "read-burst") {
242
+ return [
243
+ basePrompt,
244
+ "",
245
+ "<retry_instruction>",
246
+ "Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
247
+ "Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
248
+ "</retry_instruction>",
249
+ ].join("\n");
250
+ }
231
251
  if (previousFailureCategory === "invalid-output" &&
232
252
  task.structuredContractOutput &&
233
253
  previousProtocolReason) {
@@ -269,7 +289,7 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
269
289
  "",
270
290
  "<retry_instruction>",
271
291
  "Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
272
- "Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
292
+ "Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority. Do NOT re-read contract/scout outputs or source files — use the facts already in context. Commit record_* facts one tool call per message, then finalize_plan immediately.",
273
293
  "Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
274
294
  "</retry_instruction>",
275
295
  ].join("\n");
@@ -25,6 +25,21 @@ export const PROTOCOL_INVALID_RETRY_CATEGORY = "protocol-invalid";
25
25
  export const STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY = "invalid-output";
26
26
  /** Provider stopReason=length truncated the response before the JSON contract completed. */
27
27
  export const STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY = "structured-output-truncated";
28
+ /**
29
+ * The review node emitted a verdict in response text (e.g. "VERDICT: pass")
30
+ * but never committed the authoritative typed terminal tool call
31
+ * (approve_review / request_review_changes). Read-only and safe to retry with
32
+ * a corrected instruction; the typed terminal is the only authority.
33
+ */
34
+ export const REVIEW_TERMINAL_MISSING_RETRY_CATEGORY = "review-terminal-missing";
35
+ /**
36
+ * The plan node issued too many read-only tool calls (read/grep/ls/find),
37
+ * blowing up the context window and eventually a 400 request-too-large. The
38
+ * plan should trust upstream typed facts instead of re-reading contract/scout
39
+ * outputs and source files repeatedly. Read-only and safe to retry with a
40
+ * reduced-reading instruction.
41
+ */
42
+ export const READ_BURST_RETRY_CATEGORY = "read-burst";
28
43
  /** Retry only a proven no-op from an explicitly opt-in bounded Pi writer. */
29
44
  export const WRITER_EMPTY_DIFF_RETRY_CATEGORY = "writer-empty-diff";
30
45
  /** Retry when a backend-test writer finished but Completeness Gate found missing/broken targets. */
@@ -57,6 +72,8 @@ export const ALL_DAG_RETRY_CATEGORIES = [
57
72
  WRITER_EMPTY_DIFF_RETRY_CATEGORY,
58
73
  INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
59
74
  WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
75
+ REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
76
+ READ_BURST_RETRY_CATEGORY,
60
77
  ];
61
78
  const RETRY_SAFE_PI_ROLES = new Set([
62
79
  "planner",
@@ -143,6 +160,20 @@ export const PROTOCOL_AWARE_PI_RETRY_POLICY = {
143
160
  ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
144
161
  retryCategories: [...PROTOCOL_AWARE_DAG_RETRY_CATEGORIES],
145
162
  };
163
+ /**
164
+ * frontend-review-pi retry policy. The review verdict must be committed
165
+ * through the typed terminal tools (approve_review / request_review_changes);
166
+ * a model that only echoes "VERDICT: pass" text fails as
167
+ * review-terminal-missing. That is read-only and safe to retry with a
168
+ * corrected instruction so a single model slip does not burn the whole run.
169
+ */
170
+ export const FRONTEND_REVIEW_TERMINAL_RETRY_POLICY = {
171
+ ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
172
+ retryCategories: [
173
+ ...DEFAULT_DAG_RETRY_CATEGORIES,
174
+ REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
175
+ ],
176
+ };
146
177
  /**
147
178
  * The sole writer retry policy for requireChangedFiles writers. It is
148
179
  * intentionally not included in any read-only default: a writer may retry only
@@ -501,5 +532,6 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
501
532
  retryCategories: [
502
533
  ...DEFAULT_DAG_RETRY_CATEGORIES,
503
534
  STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
535
+ READ_BURST_RETRY_CATEGORY,
504
536
  ],
505
537
  };
@@ -1159,6 +1159,18 @@ export const dagSpecSchema = z
1159
1159
  isDefault: z.boolean(),
1160
1160
  })
1161
1161
  .optional(),
1162
+ /** Frozen frontend-test artifact layout; absent = historical testcase/frontend/ layout. */
1163
+ frontendTestLayout: z
1164
+ .object({
1165
+ testRoot: z.string(),
1166
+ ragDir: z.string(),
1167
+ casesDir: z.string(),
1168
+ evidenceDir: z.string(),
1169
+ reportsDir: z.string(),
1170
+ fixturesDir: z.string(),
1171
+ isDefault: z.boolean(),
1172
+ })
1173
+ .optional(),
1162
1174
  /** Bound user shared-setup document (plan B); absent = no shared setup references expected. */
1163
1175
  backendTestSharedSetup: z
1164
1176
  .object({
@@ -5,6 +5,7 @@ import { resolveRepairTaskForGate } from "./repair-artifact.js";
5
5
  import { topoSortToRanks } from "./topo.js";
6
6
  import { isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./retry-policy.js";
7
7
  import { isBackendTestCompletenessRetryCandidate } from "./backend-test-writer-completeness.js";
8
+ import { defaultFrontendTestLayout, frontendTestLayoutFromSpec, } from "./frontend-test-layout.js";
8
9
  const GOVERNANCE_WARNING_TYPES = new Set([
9
10
  "read-only-missing-artifacts-forbidden",
10
11
  "read-only-prompt-mentions-artifact-writes",
@@ -690,22 +691,23 @@ function validateWriterOutcomePolicyTaskConfig(task, issues) {
690
691
  });
691
692
  }
692
693
  }
693
- function isFrontendEvidenceWriteSet(writeSet) {
694
+ function isFrontendEvidenceWriteSet(writeSet, layout = defaultFrontendTestLayout()) {
694
695
  const entries = writeSet ?? [];
695
696
  if (entries.length === 0)
696
697
  return false;
698
+ const prefix = `${layout.evidenceDir}/`;
699
+ const glob = `${layout.evidenceDir}/**`;
697
700
  return entries.every((entry) => {
698
701
  const normalized = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
699
- return (normalized === "testcase/frontend/evidence/**" ||
700
- normalized.startsWith("testcase/frontend/evidence/"));
702
+ return normalized === glob || normalized.startsWith(prefix);
701
703
  });
702
704
  }
703
- function isFrontendBrowserExecutorTask(task) {
705
+ function isFrontendBrowserExecutorTask(task, layout = defaultFrontendTestLayout()) {
704
706
  const skills = task.skills ?? [];
705
707
  return (task.executor === "pi" &&
706
708
  task.toolProfile === "write" &&
707
709
  skills.includes("playwright-cli") &&
708
- isFrontendEvidenceWriteSet(task.writeSet));
710
+ isFrontendEvidenceWriteSet(task.writeSet, layout));
709
711
  }
710
712
  function taskMentionsPlaywrightCliContract(task) {
711
713
  const prompt = `${task.subtask_prompt}\n${task.outputContract ?? ""}`;
@@ -717,10 +719,11 @@ function taskMentionsPlaywrightCliContract(task) {
717
719
  * - capability is granted without skill/prompt contract or outside evidence writeSet
718
720
  * - non-browser nodes receive command capability
719
721
  */
720
- function validateCommandPolicyTaskConfig(task, issues) {
722
+ function validateCommandPolicyTaskConfig(task, issues, spec) {
723
+ const layout = frontendTestLayoutFromSpec(spec);
721
724
  const policy = resolveDagCommandPolicy(task.commandPolicy);
722
725
  const allowsPlaywright = dagCommandPolicyAllows(task.commandPolicy, "playwright-cli");
723
- const isBrowserExecutor = isFrontendBrowserExecutorTask(task);
726
+ const isBrowserExecutor = isFrontendBrowserExecutorTask(task, layout);
724
727
  if (isBrowserExecutor && !allowsPlaywright) {
725
728
  issues.push({
726
729
  type: "invalid-command-policy",
@@ -751,10 +754,10 @@ function validateCommandPolicyTaskConfig(task, issues) {
751
754
  message: `task ${task.id} playwright-cli command capability requires prompt/outputContract to reference playwright_cli`,
752
755
  });
753
756
  }
754
- if (!isFrontendEvidenceWriteSet(task.writeSet)) {
757
+ if (!isFrontendEvidenceWriteSet(task.writeSet, layout)) {
755
758
  issues.push({
756
759
  type: "invalid-command-policy",
757
- message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under testcase/frontend/evidence/`,
760
+ message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under ${layout.evidenceDir}/`,
758
761
  });
759
762
  }
760
763
  if (task.toolProfile !== "write") {
@@ -765,7 +768,7 @@ function validateCommandPolicyTaskConfig(task, issues) {
765
768
  }
766
769
  }
767
770
  }
768
- function validateDynamicChildCommandPolicy(task, issues) {
771
+ function validateDynamicChildCommandPolicy(task, issues, spec) {
769
772
  const expansion = task.dynamicExpansion;
770
773
  if (!expansion)
771
774
  return;
@@ -786,7 +789,7 @@ function validateDynamicChildCommandPolicy(task, issues) {
786
789
  writeSet: child.writeSet,
787
790
  outputContract: child.outputContract,
788
791
  };
789
- validateCommandPolicyTaskConfig(synthetic, issues);
792
+ validateCommandPolicyTaskConfig(synthetic, issues, spec);
790
793
  }
791
794
  function validateDecisionGateTaskConfig(task, issues) {
792
795
  if (!task.decisionGate?.enabled) {
@@ -893,12 +896,13 @@ export function collectForbiddenExecutorIssues(spec, forbiddenExecutors) {
893
896
  }));
894
897
  }
895
898
  /** Revalidate a rendered dynamic child before it enters state or executes. */
896
- export function assertValidMaterializedDagTask(task) {
899
+ export function assertValidMaterializedDagTask(task, parentSpec) {
897
900
  const issues = [];
898
- // A minimal v2 carrier makes write-policy validation strict while avoiding
899
- // unrelated parent graph/rank checks. Dynamic templates are validated at
900
- // init; this guards values introduced by {{item}} rendering.
901
- const spec = { version: 2, tasks: [task] };
901
+ const spec = {
902
+ version: 2,
903
+ tasks: [task],
904
+ frontendTestLayout: parentSpec?.frontendTestLayout,
905
+ };
902
906
  validateTaskWritePolicy(task, spec, issues);
903
907
  validateShellTaskConfig(task, spec, issues);
904
908
  validateStaticTaskConfig(task, issues);
@@ -906,7 +910,7 @@ export function assertValidMaterializedDagTask(task) {
906
910
  validateRetryPolicyTaskConfig(task, issues);
907
911
  validateOutputProtocolTaskConfig(task, issues);
908
912
  validateWriterOutcomePolicyTaskConfig(task, issues);
909
- validateCommandPolicyTaskConfig(task, issues);
913
+ validateCommandPolicyTaskConfig(task, issues, spec);
910
914
  if (issues.length > 0) {
911
915
  throw new Error(`invalid materialized dynamic child ${task.id}: ${issues.map((issue) => issue.message).join("; ")}`);
912
916
  }
@@ -962,8 +966,8 @@ export function validateDagSpec(spec) {
962
966
  validateRetryPolicyTaskConfig(task, issues);
963
967
  validateOutputProtocolTaskConfig(task, issues);
964
968
  validateWriterOutcomePolicyTaskConfig(task, issues);
965
- validateCommandPolicyTaskConfig(task, issues);
966
- validateDynamicChildCommandPolicy(task, issues);
969
+ validateCommandPolicyTaskConfig(task, issues, spec);
970
+ validateDynamicChildCommandPolicy(task, issues, spec);
967
971
  validateProjectGovernanceTaskConfig(task, spec, issues);
968
972
  validatePiExtensionsTaskConfig(task, issues);
969
973
  validateFailureAwareDependsOn(task, spec, issues);
@@ -15,7 +15,7 @@ LLM review (when `frontendTest.reviewMode=blocking`) must not invent blocking ru
15
15
  | `unknown-ac` | When task `sourceBinding.requirementIds` lists ACs, every `acIds` entry must be in that set |
16
16
  | `case-id-shape` | `caseId` matches `FE-<FEATURE>-<NNN>-...` (never `AC-FE-*`) |
17
17
  | `case-id-is-ac` | Do not use acceptance id as `caseId` / filename |
18
- | `case-path-mismatch` | `casePath === testcase/frontend/cases/{caseId}.md` |
18
+ | `case-path-mismatch` | `casePath === {casesDir}/{caseId}.md`(默认 `testcase/frontend/cases/{caseId}.md`) |
19
19
  | `case-file-missing` | `casePath` exists |
20
20
 
21
21
  ## Tool guidance (non-blocking)
@@ -40,5 +40,5 @@ LLM review (when `frontendTest.reviewMode=blocking`) must not invent blocking ru
40
40
 
41
41
  ## Pipeline vs quality
42
42
 
43
- - **Pipeline acceptance**: final `testcase/frontend/reports/frontend-test-retrospect-*.md` exists after result materialize
43
+ - **Pipeline acceptance**: final `{reportsDir}/frontend-test-retrospect-*.md` exists after result materialize (default `testcase/frontend/reports/`)
44
44
  - **Quality**: `frontend-test-result-v1.outcome=passed` with 0 blocked/failed (opt-in via `frontendTest.strictOutcomeGate`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.39.0-beta.4",
3
+ "version": "0.39.0-beta.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fe-test-ui-scout
3
- description: 在写 frontend-test Wave PRD/回归目录之前,对真实运行的被测页面做只读锚点侦察,产出并维护 testcase/frontend/rag/ui-anchors.md 锚点台账。用 playwright-cli snapshot/find 逐字面验证命中数与唯一性,防止 PRD 验收标准引用不存在或不唯一的文案。触发词:frontend-test、锚点、台账、ui-anchors、侦察、写 PRD 前、snapshot/find 校准、Wave PRD。
3
+ description: 在写 frontend-test Wave PRD/回归目录之前,对真实运行的被测页面做只读锚点侦察,产出并维护 {testRoot}/rag/ui-anchors.md 锚点台账(默认 testcase/frontend/rag/ui-anchors.md)。用 playwright-cli snapshot/find 逐字面验证命中数与唯一性,防止 PRD 验收标准引用不存在或不唯一的文案。触发词:frontend-test、锚点、台账、ui-anchors、侦察、写 PRD 前、snapshot/find 校准、Wave PRD。
4
4
  references:
5
5
  - path: references/ledger-schema.md
6
6
  required: true
@@ -13,7 +13,7 @@ references:
13
13
  ## 定位
14
14
 
15
15
  写 Wave PRD / 校准回归目录 AC **之前**的前置侦察。产出锚点台账
16
- `testcase/frontend/rag/ui-anchors.md`:每个「页面 × 状态」下真实可见、
16
+ `{testRoot}/rag/ui-anchors.md`(默认 `testcase/frontend/rag/ui-anchors.md`):每个「页面 × 状态」下真实可见、
17
17
  `find` 可命中的字面清单。PRD 的每条 `find "..."` 断言必须能在台账找到对应行;
18
18
  台账没有的字面禁止写进验收标准。
19
19
 
@@ -23,7 +23,7 @@ references:
23
23
 
24
24
  ## 输入
25
25
 
26
- - 被测 baseUrl(来自 `testcase/frontend/rag/context.md` 的 `environmentProbe=reachable` URL
26
+ - 被测 baseUrl(来自 `{testRoot}/rag/context.md` 的 `environmentProbe=reachable` URL;默认 `testcase/frontend/rag/context.md`)
27
27
  - 目标 Wave 的 AC 清单(回归目录中本 Wave 范围的行)
28
28
  - 既有台账(增量更新,不整表重写)
29
29
 
@@ -56,7 +56,7 @@ references:
56
56
 
57
57
  ## 输出
58
58
 
59
- - `testcase/frontend/rag/ui-anchors.md`(增量更新)
59
+ - `{testRoot}/rag/ui-anchors.md`(增量更新;默认 `testcase/frontend/rag/ui-anchors.md`)
60
60
  - 侦察小结(对话内输出,不落盘;结论回写 PRD 草稿的锚点引用)
61
61
 
62
62
  ## References
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 文件位置与命名
4
4
 
5
- - 路径:`testcase/frontend/rag/ui-anchors.md`
5
+ - 路径:`{testRoot}/rag/ui-anchors.md`(默认 `testcase/frontend/rag/ui-anchors.md`)
6
6
  - 编码:UTF-8,LF;表格用 GitHub Flavored Markdown。
7
7
  - 台账是**增量账本**:修订不删除历史行;失效行改状态列并注明原因。
8
8
 
@@ -55,7 +55,7 @@ request
55
55
 
56
56
  ## 推荐执行流程
57
57
 
58
- 使用 `testcase/frontend/rag/context.md` 中已由 environment shell 标记为 `reachable` 的非生产 `baseUrl` 和默认浏览器 session;不得创建 named session。交互前先获取 snapshot,并只使用 snapshot 中可见的 ref 或已知安全 locator
58
+ 使用 `{testRoot}/rag/context.md`(默认 `testcase/frontend/rag/context.md`)中已由 environment shell 标记为 `reachable` 的非生产 `baseUrl` 和默认浏览器 session;不得创建 named session。交互前先获取 snapshot,并只使用 snapshot 中可见的 ref 或已知安全 locator。截图写入当前 case 的 `{evidenceDir}/<caseId>/`,canonical 文件名仍是 `final.png`。
59
59
 
60
60
  ```text
61
61
  playwright-cli open --browser=chrome http://localhost:5173
@@ -13,10 +13,12 @@ description: 根据 FE-test RAG 知识包生成可由受限 playwright_cli runti
13
13
 
14
14
  ## 输入与边界
15
15
 
16
- - 只读取 `testcase/frontend/rag/context.md`、`coverage-map.md` 与已有 `testcase/frontend/cases/`。
17
- - 若存在 `testcase/frontend/rag/ui-anchors.md` 锚点台账,必须一并读取并作为 UI 断言的唯一事实源;台账缺失时按 RAG 知识包生成,但不得引用台账外的具体控件字面(缺失信息标 `blocked`)。
18
- - 只写 `testcase/frontend/cases/**`;不得回读 PRD、读取 `.harness/`,或写
19
- `testcase/frontend/evidence/**`。
16
+ 默认产物根是 `testcase/frontend/`。若任务配置了 `frontendTest.testRoot`,把下列路径中的 `testcase/frontend/` 整段替换为该根(不要只替换 `testcase/`)。
17
+
18
+ - 只读取 `{testRoot}/rag/context.md`、`coverage-map.md` 与已有 `{testRoot}/cases/`。
19
+ - 若存在 `{testRoot}/rag/ui-anchors.md` 锚点台账,必须一并读取并作为 UI 断言的唯一事实源;台账缺失时按 RAG 知识包生成,但不得引用台账外的具体控件字面(缺失信息标 `blocked`)。
20
+ - 只写 `{testRoot}/cases/**`;不得回读 PRD、读取 `.harness/`,或写
21
+ `{testRoot}/evidence/**`。
20
22
  - 所有 API、字段限制、状态流转、数据来源、SLA、URL 与账号要求必须能在 RAG 知识包中追溯。
21
23
  缺失信息标记 `blocked` 或“需人工确认”,不得猜测。
22
24
 
@@ -38,10 +40,10 @@ description: 根据 FE-test RAG 知识包生成可由受限 playwright_cli runti
38
40
  (例:`FE-LOGIN-001-core`)。禁止把验收标准写成 caseId。
39
41
  - `acIds` = **验收标准 ID 列表**,形态 `AC-FE-*` / `AC-*`
40
42
  (例:`["AC-FE-001"]`)。禁止把用例 ID 放进 acIds。
41
- - `casePath` 必须等于 `testcase/frontend/cases/<caseId>.md`;
42
- `evidenceDir` 必须等于 `testcase/frontend/evidence/<caseId>/`。
43
+ - `casePath` 必须等于 `{casesDir}/<caseId>.md`(默认 `testcase/frontend/cases/<caseId>.md`);
44
+ `evidenceDir` 必须等于 `{evidenceDir}/<caseId>/`(默认 `testcase/frontend/evidence/<caseId>/`)。
43
45
  - `manifest` 使用 `schemaVersion: 1`,每项只含 `caseId`、`casePath`、`dimension`、`acIds`、
44
- `evidenceDir`。所有 ID、路径和 evidenceDir 必须唯一,并位于 `testcase/frontend/` 内。
46
+ `evidenceDir`。所有 ID、路径和 evidenceDir 必须唯一,并位于 `{testRoot}/` 内。
45
47
  - `index.md` 按功能点列出 case、维度、AC、数据依赖、API 映射和预期执行状态。
46
48
 
47
49
  每个 case 必须包含:
@@ -73,7 +75,7 @@ description: 根据 FE-test RAG 知识包生成可由受限 playwright_cli runti
73
75
  7. 明确的 UI/API 预期与数据清理结果;无法满足的环境或数据依赖必须写为 `blocked`。
74
76
 
75
77
  所有文件型输出均由 controller 绑定到执行节点提供的
76
- `testcase/frontend/evidence/<case-id>/` 工作目录。截图一律使用 canonical 语法
78
+ `{evidenceDir}/<case-id>/` 工作目录(默认 `testcase/frontend/evidence/<case-id>/`)。截图一律使用 canonical 语法
77
79
  `playwright-cli screenshot --filename final.png`;如确有从最新 snapshot 解析出的真实元素 ref,写为
78
80
  `playwright-cli screenshot e5 --filename final.png`。`pdf` 必须写为
79
81
  `playwright-cli pdf --filename final.pdf`。`snapshot` 无 filename 时仅返回响应;需要文件时使用
@@ -1 +0,0 @@
1
- import{ag as o,ah as n}from"./mermaid.core-C-D3ZRUa.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-DB2G4H8V.js";import{_ as i}from"./mermaid.core-C-D3ZRUa.js";import"./chunk-5VM5RSS4-SLBibhf5.js";import"./chunk-XXDRQBXY-BKkkC4VA.js";import"./chunk-KBJHAD2P-CG78FGkn.js";import"./chunk-2GRJ4B5K-DF_YSvOS.js";import"./index-D5zGX6fG.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-DB2G4H8V.js";import{_ as i}from"./mermaid.core-C-D3ZRUa.js";import"./chunk-5VM5RSS4-SLBibhf5.js";import"./chunk-XXDRQBXY-BKkkC4VA.js";import"./chunk-KBJHAD2P-CG78FGkn.js";import"./chunk-2GRJ4B5K-DF_YSvOS.js";import"./index-D5zGX6fG.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};