@tea-agent/loop-agent 0.16.15-beta.0 → 0.16.16

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.
@@ -20,6 +20,7 @@ import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "
20
20
  import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
21
21
  import { buildBackendTestEffectiveResultSelectorShellSnippet, buildBackendTestRepairEligibilityShellSnippet, buildBackendTestRepairSafetyShellSnippet, } from "./backend-test-repair-contract.js";
22
22
  import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
23
+ import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
23
24
  import { classifyFrontendRisk, } from "./frontend-risk.js";
24
25
  import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
25
26
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
@@ -3660,11 +3661,8 @@ function buildFrontendTestHybridDag(sources) {
3660
3661
  const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
3661
3662
  pattern === "testcase/**" ||
3662
3663
  pattern === "**");
3663
- const hasReportWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" ||
3664
- pattern === "docs/**" ||
3665
- pattern === "**");
3666
- if (!hasFrontendTestWriteScope || !hasReportWriteScope) {
3667
- throw new Error('frontend-test requires task.json allowedPaths to include both "testcase/frontend/**" and "docs/test-reports/**" (or explicit containing globs).');
3664
+ if (!hasFrontendTestWriteScope) {
3665
+ throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
3668
3666
  }
3669
3667
  const forbidden = commonForbiddenPaths(sources);
3670
3668
  const ragWriteSet = ["testcase/frontend/rag/**"];
@@ -3675,7 +3673,7 @@ function buildFrontendTestHybridDag(sources) {
3675
3673
  JSON.stringify([
3676
3674
  "const fs=require('fs'),path=require('path');",
3677
3675
  "const file='testcase/frontend/cases/manifest.json'; if(!fs.existsSync(file)) throw new Error('missing '+file);",
3678
- "const manifest=JSON.parse(fs.readFileSync(file,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)) throw new Error('invalid frontend case manifest');",
3676
+ "const manifest=JSON.parse(fs.readFileSync(file,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)||manifest.cases.length===0) throw new Error('invalid or empty frontend case manifest');",
3679
3677
  "const dims=new Set(['core','boundary','flow','backend']);",
3680
3678
  "const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
3681
3679
  "for(const c of manifest.cases){",
@@ -3685,13 +3683,27 @@ function buildFrontendTestHybridDag(sources) {
3685
3683
  " if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) throw new Error('invalid acIds');",
3686
3684
  " for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) throw new Error('unsafe '+k); }",
3687
3685
  " if(c.casePath!=='testcase/frontend/cases/'+c.caseId+'.md') throw new Error('casePath must match caseId');",
3688
- " if(!c.evidenceDir.startsWith('testcase/frontend/evidence/'+c.caseId+'/')) throw new Error('case path escapes frontend test roots');",
3686
+ // Accept evidenceDir as the case root or a nested path under that root.
3687
+ " { const prefix='testcase/frontend/evidence/'+c.caseId; if(!(c.evidenceDir===prefix||c.evidenceDir.startsWith(prefix+'/'))) throw new Error('case path escapes frontend test roots'); }",
3688
+ " if(!fs.existsSync(c.casePath)) throw new Error('missing case file '+c.casePath);",
3689
3689
  " if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
3690
3690
  " if(seenEvidenceDir.has(c.evidenceDir)) throw new Error('duplicate evidenceDir'); seenEvidenceDir.add(c.evidenceDir);",
3691
3691
  "}",
3692
3692
  "process.stdout.write(JSON.stringify({cases:manifest.cases}));",
3693
3693
  ].join("")),
3694
3694
  ].join(" ");
3695
+ const evidenceValidation = [
3696
+ "node -e",
3697
+ JSON.stringify([
3698
+ "const fs=require('fs'),path=require('path');",
3699
+ "const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath))throw new Error('missing '+manifestPath);",
3700
+ "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));if(!Array.isArray(manifest.cases))throw new Error('invalid frontend case manifest');",
3701
+ "const statuses=new Set(['passed','failed','blocked']);let failed=false;",
3702
+ "for(const c of manifest.cases){const dir=c&&c.evidenceDir;if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||path.isAbsolute(dir)||dir.includes('..')||!dir.startsWith('testcase/frontend/evidence/'+c.caseId+'/')){console.error('invalid case evidence target');failed=true;continue;}const execution=path.join(dir,'execution.md'),resultPath=path.join(dir,'case-result.json');if(!fs.existsSync(execution)){console.error(c.caseId+': missing '+execution);failed=true;}if(!fs.existsSync(resultPath)){console.error(c.caseId+': missing '+resultPath);failed=true;continue;}let result;try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch{console.error(c.caseId+': invalid JSON '+resultPath);failed=true;continue;}if(!result||result.caseId!==c.caseId||!statuses.has(result.status)||!Array.isArray(result.evidencePaths)||result.evidencePaths.some(p=>typeof p!=='string'||path.isAbsolute(p)||p.includes('..'))){console.error(c.caseId+': result must have matching caseId, passed|failed|blocked status, and safe evidencePaths array');failed=true;continue;}if(result.status==='passed'&&(result.evidencePaths.length<2||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4)$/i.test(p)))){console.error(c.caseId+': passed result requires browser evidence');failed=true;}if(result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim())){console.error(c.caseId+': blocked result requires blockedReason');failed=true;}}",
3703
+ "if(failed)process.exit(1);console.log('frontend case evidence validation ok cases='+manifest.cases.length);",
3704
+ ].join("")),
3705
+ ].join(" ");
3706
+ const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
3695
3707
  const spec = {
3696
3708
  version: 3,
3697
3709
  title: `Frontend test DAG: ${sources.taskConfig.title}`,
@@ -3705,7 +3717,8 @@ function buildFrontendTestHybridDag(sources) {
3705
3717
  "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
3706
3718
  "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
3707
3719
  "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
3708
- "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <base-url>.",
3720
+ "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <base-url>; generated operations stay in the default browser session and must not use unverified named-session flags.",
3721
+ "A frontend case review must emit VERDICT: pass before manifest materialization; request-revision blocks browser execution.",
3709
3722
  "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
3710
3723
  ],
3711
3724
  defaults: {
@@ -3747,8 +3760,21 @@ function buildFrontendTestHybridDag(sources) {
3747
3760
  ].join("\n\n"),
3748
3761
  },
3749
3762
  {
3750
- id: "generate-frontend-functional-cases-pi",
3763
+ id: "materialize-frontend-test-execution-shell",
3751
3764
  depends_on: ["retrieve-frontend-test-context-pi"],
3765
+ role: "verifier",
3766
+ executor: "shell",
3767
+ complexity: "LOW",
3768
+ writePolicy: "read-only",
3769
+ allowedPaths: [...ragWriteSet],
3770
+ forbiddenPaths: forbidden,
3771
+ outputContract: "Fail-closed preflight for an isolated non-production frontend test execution contract.",
3772
+ subtask_prompt: "Validate that RAG context declares a non-production base URL/env name, fixture/reset isolation, browser startup, and no credential values.",
3773
+ shell: { commands: [["node -e", JSON.stringify("const fs=require('fs');const p='testcase/frontend/rag/context.md';if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const required=[['base URL',/base[- ]url/i],['fixture',/fixture/i],['reset',/reset|重置/i],['playwright-cli open --browser=chrome --headed',/playwright-cli open --browser=chrome --headed/i]];for(const [label,pattern] of required)if(!pattern.test(s))throw new Error('frontend-test execution contract missing '+label);if(/https?:\\/\\/(?:www\\.)?[^\\s]*(?:prod|production)/i.test(s))throw new Error('production URL forbidden');console.log('frontend-test-execution-v1 validated')")].join(" ")], cwd: ".", timeoutMs: 60000 },
3774
+ },
3775
+ {
3776
+ id: "generate-frontend-functional-cases-pi",
3777
+ depends_on: ["materialize-frontend-test-execution-shell"],
3752
3778
  role: "implementer",
3753
3779
  executor: "pi",
3754
3780
  toolProfile: "write",
@@ -3762,8 +3788,8 @@ function buildFrontendTestHybridDag(sources) {
3762
3788
  "Use skill playwright-cli-case-generator.",
3763
3789
  "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
3764
3790
  "Generate Markdown cases, index.md and manifest.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.",
3765
- "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Every browser start command is: playwright-cli open --browser=chrome --headed <base-url>.",
3766
- "Each case must be independent, declare its session/preconditions/data cleanup, UI assertions, evidence paths under testcase/frontend/evidence/<case-id>/, and mark unsafe/missing dependencies blocked.",
3791
+ "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Every browser start command is: playwright-cli open --browser=chrome --headed <base-url>. Use the same default browser session for every subsequent command; never write -s=<case-id> or assume named-session binding.",
3792
+ "Each case must be independently reproducible: for every executable sub-scenario state fixture/reset, UI reset, a fresh snapshot before references are used, exact evidence write point, preconditions/data cleanup, UI assertions, and evidence paths under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
3767
3793
  ].join("\n\n"),
3768
3794
  },
3769
3795
  {
@@ -3775,19 +3801,72 @@ function buildFrontendTestHybridDag(sources) {
3775
3801
  writePolicy: "read-only",
3776
3802
  allowedPaths: [...ragWriteSet, ...casesWriteSet],
3777
3803
  forbiddenPaths: forbidden,
3778
- outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes.",
3779
- subtask_prompt: "Review only the RAG package and frontend Markdown cases. Verify traceability, independent execution, safe data/environment handling, manifest correctness, and evidence requirements. The verdict is advisory and does not block case execution.",
3804
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes. request-revision blocks manifest materialization.",
3805
+ subtask_prompt: "Review only the RAG package and frontend Markdown cases. Verify traceability, independent execution, safe data/environment handling, manifest correctness, session consistency, fixture/UI reset and fresh snapshot steps, and evidence requirements. Any Important or Critical finding requires VERDICT: request-revision. Browser execution is blocked unless this review passes.",
3780
3806
  },
3781
3807
  {
3782
- id: "materialize-frontend-case-manifest-shell",
3808
+ id: "revise-frontend-cases-pi",
3783
3809
  depends_on: ["review-frontend-cases-pi"],
3810
+ runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3811
+ role: "implementer",
3812
+ executor: "pi",
3813
+ toolProfile: "write",
3814
+ complexity: "HIGH",
3815
+ writePolicy: "exclusive",
3816
+ writeSet: casesWriteSet,
3817
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3818
+ forbiddenPaths: forbidden,
3819
+ outputContract: "Apply the one permitted frontend case revision under testcase/frontend/cases/** only; no browser execution or evidence writes.",
3820
+ subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/**, preserve traceable AC mappings, and do not execute a browser or write evidence.",
3821
+ },
3822
+ {
3823
+ id: "review-frontend-cases-final-pi",
3824
+ depends_on: ["revise-frontend-cases-pi"],
3825
+ runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3826
+ role: "reviewer",
3827
+ executor: "pi",
3828
+ complexity: "HIGH",
3829
+ writePolicy: "read-only",
3830
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3831
+ forbiddenPaths: forbidden,
3832
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision after the single allowed case revision; no writes.",
3833
+ subtask_prompt: "Perform the final frontend case review after the sole permitted revision. Apply the same traceability, isolation, manifest, reset, session, snapshot, and evidence checks. First verdict line must be exact; any Important or Critical finding requires request-revision. Do not write files.",
3834
+ },
3835
+ {
3836
+ id: "final-frontend-case-review-gate-shell",
3837
+ depends_on: ["review-frontend-cases-pi", "review-frontend-cases-final-pi"],
3838
+ dependsPolicy: "all-or-condition-skip",
3839
+ role: "verifier",
3840
+ executor: "shell",
3841
+ complexity: "LOW",
3842
+ writePolicy: "read-only",
3843
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3844
+ forbiddenPaths: forbidden,
3845
+ outputContract: "Pass-only effective frontend case review gate; final review takes precedence when the revision branch ran.",
3846
+ subtask_prompt: "Authorize manifest materialization only after the effective frontend case review passes.",
3847
+ shell: {
3848
+ commands: [],
3849
+ verdictGate: {
3850
+ fromNodeId: "review-frontend-cases-final-pi",
3851
+ fallbackFromNodeIds: ["review-frontend-cases-pi"],
3852
+ accept: ["VERDICT: pass"],
3853
+ label: "effective frontend case review",
3854
+ lineMode: "first-verdict-line",
3855
+ },
3856
+ cwd: ".",
3857
+ timeoutMs: 60000,
3858
+ },
3859
+ },
3860
+ {
3861
+ id: "materialize-frontend-case-manifest-shell",
3862
+ depends_on: ["final-frontend-case-review-gate-shell"],
3784
3863
  role: "verifier",
3785
3864
  executor: "shell",
3786
3865
  complexity: "LOW",
3787
3866
  writePolicy: "read-only",
3788
3867
  allowedPaths: casesWriteSet,
3789
3868
  forbiddenPaths: forbidden,
3790
- outputContract: "stdout is exactly JSON { cases: [...] } after deterministic frontend manifest validation.",
3869
+ outputContract: "Validated frontend manifest payload { cases: [...] }; shell command echo is permitted only as the prefix before exactly one final JSON line.",
3791
3870
  subtask_prompt: "Validate and materialize the generated frontend case manifest.",
3792
3871
  shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
3793
3872
  },
@@ -3811,6 +3890,7 @@ function buildFrontendTestHybridDag(sources) {
3811
3890
  maxItems: config.maxCasesPerBatch,
3812
3891
  maxExpandedNodes: config.maxCasesPerBatch,
3813
3892
  childIdPrefix: "execute-frontend-case",
3893
+ workspaceTemplate: "{{case.evidenceDir}}",
3814
3894
  tokenBudget: {
3815
3895
  maxTokensPerCase: config.maxTokensPerCase,
3816
3896
  maxTotalTokens: config.maxTotalTokens,
@@ -3834,15 +3914,67 @@ function buildFrontendTestHybridDag(sources) {
3834
3914
  subtaskPromptTemplate: [
3835
3915
  "Execute exactly case {{case.caseId}} from {{case.casePath}} using playwright-cli and webapp-testing. This is a fresh Pi session; do not use /new.",
3836
3916
  "Use only the declared isolated test environment. If CLI/browser/base URL/credentials/fixture isolation is missing, record blocked rather than installing tools or guessing.",
3837
- "Use playwright-cli open --browser=chrome --headed <base-url>. Persist execution.md, case-result.json, screenshots/trace/video/logs under {{case.evidenceDir}} before returning.",
3917
+ "Use exactly this browser start command prefix: playwright-cli open --browser=chrome --headed <base-url>. Do not put session flags before open. Every later playwright-cli command must use that same default browser session; must not use -s=<case-id>, -s=, or any named-session flag because no session binding is verified.",
3918
+ "For every sub-scenario, record fixture/reset, UI reset, and a fresh snapshot before using element references. If the isolated environment is missing, write blocked evidence before any browser command; do not open or connect to a browser.",
3919
+ "Before returning, always persist {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json. case-result.json must be JSON with matching caseId, status as passed, failed, or blocked, and evidencePaths array; a blocked result must include non-empty blockedReason and must never imply pass.",
3920
+ "Run a local deterministic validation before returning: node -e \"const fs=require('fs');const p='{{case.evidenceDir}}';const r=JSON.parse(fs.readFileSync(p+'case-result.json','utf8'));if(!fs.existsSync(p+'execution.md')||r.caseId!=='{{case.caseId}}'||!['passed','failed','blocked'].includes(r.status)||!Array.isArray(r.evidencePaths)||(r.status==='blocked'&&!(typeof r.blockedReason==='string'&&r.blockedReason.trim())))process.exit(1)\". Persist screenshots/trace/video/logs when actually available. execution.md must record the executed or blocked steps, base URL safety decision, fixture/reset and request-observation availability, and evidence file list.",
3838
3921
  "A business failed or blocked case is a recorded result, not a node failure. Close the session and return only compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
3839
3922
  ].join("\n\n"),
3840
3923
  },
3841
3924
  },
3842
3925
  },
3843
3926
  {
3844
- id: "review-frontend-execution-pi",
3927
+ id: "validate-frontend-case-evidence-shell",
3845
3928
  depends_on: ["execute-frontend-cases-map"],
3929
+ role: "verifier",
3930
+ executor: "shell",
3931
+ complexity: "LOW",
3932
+ writePolicy: "read-only",
3933
+ allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3934
+ forbiddenPaths: forbidden,
3935
+ outputContract: "Deterministic validation that every manifest case has execution.md and valid matching case-result.json; blocked results require blockedReason.",
3936
+ subtask_prompt: "Validate all frontend case evidence before evidence review; fail closed on missing or malformed records.",
3937
+ shell: { commands: [evidenceValidation], cwd: ".", timeoutMs: 120000 },
3938
+ },
3939
+ {
3940
+ id: "materialize-frontend-test-result-shell",
3941
+ depends_on: ["validate-frontend-case-evidence-shell"],
3942
+ role: "verifier",
3943
+ executor: "shell",
3944
+ complexity: "LOW",
3945
+ writePolicy: "read-only",
3946
+ allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3947
+ forbiddenPaths: forbidden,
3948
+ outputContract: "Run-owned hash-bound frontend-test-result-v1 derived only from the manifest and validated case evidence.",
3949
+ subtask_prompt: "Materialize the authoritative frontend-test-result-v1. Do not use Pi prose or retrospective output as input.",
3950
+ shell: {
3951
+ commands: [],
3952
+ jsonArtifactGate: {
3953
+ fromNodeId: "validate-frontend-case-evidence-shell",
3954
+ schemaId: "frontend-test-result-v1",
3955
+ artifactName: "frontend-test-result.json",
3956
+ outputDir: "contracts",
3957
+ },
3958
+ cwd: ".",
3959
+ timeoutMs: 120000,
3960
+ },
3961
+ },
3962
+ {
3963
+ id: "frontend-test-result-outcome-gate-shell",
3964
+ depends_on: ["materialize-frontend-test-result-shell"],
3965
+ role: "verifier",
3966
+ executor: "shell",
3967
+ complexity: "LOW",
3968
+ writePolicy: "read-only",
3969
+ allowedPaths: [],
3970
+ forbiddenPaths: forbidden,
3971
+ outputContract: "Pass only when the run-owned frontend-test-result-v1 records outcome=passed and integrationMode=real.",
3972
+ subtask_prompt: "Gate the authoritative frontend-test result before Pi review and retrospective.",
3973
+ shell: { commands: [frontendTestOutcomeGate], cwd: ".", timeoutMs: 60000 },
3974
+ },
3975
+ {
3976
+ id: "review-frontend-execution-pi",
3977
+ depends_on: ["frontend-test-result-outcome-gate-shell"],
3846
3978
  role: "reviewer",
3847
3979
  executor: "pi",
3848
3980
  complexity: "HIGH",
@@ -3860,11 +3992,11 @@ function buildFrontendTestHybridDag(sources) {
3860
3992
  toolProfile: "write",
3861
3993
  complexity: "MED",
3862
3994
  writePolicy: "exclusive",
3863
- writeSet: ["docs/test-reports/**"],
3864
- allowedPaths: ["testcase/frontend/**", "docs/test-reports/**"],
3995
+ writeSet: ["testcase/frontend/reports/**"],
3996
+ allowedPaths: ["testcase/frontend/**"],
3865
3997
  forbiddenPaths: forbidden,
3866
- outputContract: "Write frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.",
3867
- subtask_prompt: "Write the frontend test retrospective under docs/test-reports/. Summarize coverage, passed/failed/blocked cases (including token-budget-exhausted), review findings, browser anomalies, residual risks, and A/B/C/D rating. Blocked cases never count as passed.",
3998
+ outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.",
3999
+ subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/. Summarize coverage, passed/failed/blocked cases (including token-budget-exhausted), review findings, browser anomalies, residual risks, and A/B/C/D rating. Blocked cases never count as passed. Do not write docs/**.",
3868
4000
  },
3869
4001
  ],
3870
4002
  };
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
@@ -421,6 +422,19 @@ export async function executeDagNode(input) {
421
422
  node.structuredArtifactSha256 = createHash("sha256").update(bytes).digest("hex");
422
423
  node.structuredArtifactSchemaId = task.shell.jsonArtifactGate.schemaId;
423
424
  }
425
+ else if (task.shell?.backendTestPipeline === "finalize-effective-result") {
426
+ // Pipeline materializes contracts/backend-test-result.json without jsonArtifactGate.
427
+ // Bind it so Outcome adapters project kind=backend-test-result for Ready Planner.
428
+ const artifactPath = path.join(runDir, "contracts", "backend-test-result.json");
429
+ if (existsSync(artifactPath)) {
430
+ const bytes = await readFile(artifactPath);
431
+ node.structuredArtifactPath = artifactPath;
432
+ node.structuredArtifactSha256 = createHash("sha256")
433
+ .update(bytes)
434
+ .digest("hex");
435
+ node.structuredArtifactSchemaId = "backend-test-result-v1";
436
+ }
437
+ }
424
438
  }
425
439
  state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
426
440
  await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
@@ -91,6 +91,7 @@ export const dagJsonArtifactSchemaIdSchema = z.enum([
91
91
  "backend-test-semantic-review-v1",
92
92
  "backend-test-case-manifest-v1",
93
93
  "frontend-implementation-contract-v1",
94
+ "frontend-test-result-v1",
94
95
  ]);
95
96
  export const dagJsonArtifactGateSchema = z.object({
96
97
  fromNodeId: z.string().regex(/^[a-z][a-z0-9-]*$/),
@@ -2,4 +2,21 @@
2
2
 
3
3
  Use `playwright-cli-case-generator`. Read only `testcase/frontend/rag/context.md`, `coverage-map.md`, and existing `testcase/frontend/cases/`; write only that cases directory. Produce Markdown cases, `index.md`, and schema-version-1 `manifest.json`. Do not generate pytest or Playwright source code.
4
4
 
5
- Each case is independently executable and includes AC mapping, preconditions, session, cleanup, UI assertions, and isolated evidence paths. Use dimensions `core`, `boundary`, `flow`, or `backend`. Never guess API fields, constraints, SLA, credentials, or unrecorded data. The open command is exactly `playwright-cli open --browser=chrome --headed <base-url>`.
5
+ Each case is independently executable and includes AC mapping, preconditions, cleanup, UI assertions, and its isolated evidence path. Use dimensions `core`, `boundary`, `flow`, or `backend`. Never guess API fields, constraints, SLA, credentials, or unrecorded data.
6
+
7
+ Every case must use this exact browser-start command prefix:
8
+
9
+ ```text
10
+ playwright-cli open --browser=chrome --headed <base-url>
11
+ ```
12
+
13
+ Do not put a session flag before `open`. Every later Playwright CLI command must stay in that same default browser session: do **not** emit `-s=<case-id>`, `-s=...`, or assume an undocumented named-session binding.
14
+
15
+ For every executable sub-scenario, state the fixture/reset operation, UI reset operation, a fresh snapshot before using element references, and the exact evidence write point. If the isolated environment is unavailable, require writing blocked evidence before any browser command; do not open or connect to a browser.
16
+
17
+ Each case must require the executor to persist, even when blocked:
18
+
19
+ - `testcase/frontend/evidence/<case-id>/execution.md`
20
+ - `testcase/frontend/evidence/<case-id>/case-result.json`
21
+
22
+ `case-result.json` must be valid JSON containing the matching `caseId`, `status` (`passed`, `failed`, or `blocked`), and an `evidencePaths` array. A blocked result must include a non-empty `blockedReason`, such as `isolated-test-environment-unavailable` or `token-budget-exhausted`, and must never claim or imply a pass. `execution.md` records attempted or blocked steps, base-URL safety decision, fixture/reset and request-observation availability, timestamps, and the evidence-file list. Screenshots, snapshots, traces, videos, and logs are required only when actually available and must stay under the same case evidence directory.