@tea-agent/loop-agent 0.29.0 → 0.29.2

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.
@@ -3977,12 +3977,25 @@ function buildFrontendTestHybridDag(sources) {
3977
3977
  maxTotalTokens: rawFrontendTest?.maxTotalTokens,
3978
3978
  reviewMode: rawFrontendTest?.reviewMode ?? "off",
3979
3979
  strictOutcomeGate: rawFrontendTest?.strictOutcomeGate === true,
3980
+ maxRerunAttempts: (() => {
3981
+ const raw = rawFrontendTest?.maxRerunAttempts;
3982
+ if (raw === undefined || raw === null || Number.isNaN(Number(raw)))
3983
+ return 2;
3984
+ return Math.min(4, Math.max(0, Math.trunc(Number(raw))));
3985
+ })(),
3986
+ reports: {
3987
+ retrospect: rawFrontendTest?.reports?.retrospect === true,
3988
+ l5: rawFrontendTest?.reports?.l5 !== false,
3989
+ },
3980
3990
  };
3981
3991
  const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
3982
3992
  const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
3983
3993
  const reviewMode = config.reviewMode;
3984
3994
  const blockingReview = reviewMode === "blocking";
3985
3995
  const strictOutcomeGate = config.strictOutcomeGate;
3996
+ const maxRerunAttempts = config.maxRerunAttempts ?? 2;
3997
+ const enableRetrospect = config.reports?.retrospect === true;
3998
+ const enableL5Report = config.reports?.l5 !== false;
3986
3999
  const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
3987
4000
  pattern === "testcase/**" ||
3988
4001
  pattern === "**");
@@ -4013,7 +4026,7 @@ function buildFrontendTestHybridDag(sources) {
4013
4026
  "if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
4014
4027
  `const declaredAc=new Set(${declaredAcIdsLiteral});`,
4015
4028
  "const issues=[];",
4016
- "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
4029
+ "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headless\\s+https?:\\/\\/\\S+/i;",
4017
4030
  "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
4018
4031
  "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
4019
4032
  ,
@@ -4026,9 +4039,9 @@ function buildFrontendTestHybridDag(sources) {
4026
4039
  " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
4027
4040
  " if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
4028
4041
  " const body=fs.readFileSync(casePath,'utf8');",
4029
- " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>; playwright-cli is strongly recommended for browser execution'});",
4042
+ " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headless <absolute-url>; playwright-cli is strongly recommended for browser execution'});",
4030
4043
  ,
4031
- " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
4044
+ " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headless\\s+(https?:\\/\\/\\S+)/i);",
4032
4045
  " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
4033
4046
  " if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
4034
4047
  " else {",
@@ -4125,13 +4138,34 @@ function buildFrontendTestHybridDag(sources) {
4125
4138
  },
4126
4139
  },
4127
4140
  {
4128
- id: "retrieve-frontend-test-context-pi",
4141
+ id: "prepare-frontend-test-package-shell",
4129
4142
  depends_on: ["preflight-frontend-browser-tool-shell"],
4143
+ role: "verifier",
4144
+ executor: "shell",
4145
+ complexity: "LOW",
4146
+ writePolicy: "exclusive",
4147
+ writeSet: ragWriteSet,
4148
+ allowedPaths: [...ragWriteSet],
4149
+ forbiddenPaths: forbidden,
4150
+ outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage.",
4151
+ subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package.",
4152
+ shell: {
4153
+ commands: [
4154
+ ["node -e", JSON.stringify("const fs=require('fs'),path=require('path');const dest='testcase/frontend/rag/standard-scenarios.v1.json';const candidates=[path.join('docs','templates','frontend-test-standard-scenarios.v1.json')];let src=null;for(const c of candidates){if(fs.existsSync(c)){src=c;break;}}fs.mkdirSync(path.dirname(dest),{recursive:true});if(src){fs.copyFileSync(src,dest);process.stdout.write(JSON.stringify({status:'copied',from:src,to:dest}));}else{const minimal={schemaVersion:1,id:'frontend-test-standard-scenarios-v1',scenarios:[{id:'STD-FE-SMOKE-ENTRY',title:'入口可打开',category:'smoke',priority:'must',testPoints:['open'],minCases:1}]};fs.writeFileSync(dest,JSON.stringify(minimal,null,2)+'\n');process.stdout.write(JSON.stringify({status:'fallback',to:dest}));}")].join(" "),
4155
+ ],
4156
+ cwd: ".",
4157
+ timeoutMs: 60_000,
4158
+ },
4159
+ },
4160
+ {
4161
+ id: "retrieve-frontend-test-context-pi",
4162
+ depends_on: ["prepare-frontend-test-package-shell"],
4130
4163
  role: "planner",
4131
4164
  executor: "pi",
4132
4165
  toolProfile: "write",
4133
4166
  complexity: "MED",
4134
4167
  writePolicy: "exclusive",
4168
+ writeGuardPolicy: "tools-only",
4135
4169
  writeSet: ragWriteSet,
4136
4170
  allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
4137
4171
  forbiddenPaths: forbidden,
@@ -4140,7 +4174,7 @@ function buildFrontendTestHybridDag(sources) {
4140
4174
  "Build the frontend test RAG package (keep it short).",
4141
4175
  "Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
4142
4176
  "Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
4143
- `Controller-frozen origin (required, not model-selectable): write exactly \`baseUrl: ${controllerFrontend.baseUrl}\` and \`baseUrlSource: ${controllerFrontend.baseUrlSource}\`. Do not derive, replace, or override the origin from model reasoning or other repository text. Write \`environmentProbe: pending\`. Include exact start prefix: playwright-cli open --browser=chrome --headed ${controllerFrontend.baseUrl}.`,
4177
+ `Controller-frozen origin (required, not model-selectable): write exactly \`baseUrl: ${controllerFrontend.baseUrl}\` and \`baseUrlSource: ${controllerFrontend.baseUrlSource}\`. Do not derive, replace, or override the origin from model reasoning or other repository text. Write \`environmentProbe: pending\`. Include exact start prefix: playwright-cli open --browser=chrome --headless ${controllerFrontend.baseUrl}.`,
4144
4178
  buildSourceContextBlock(sources),
4145
4179
  ].join("\n\n"),
4146
4180
  },
@@ -4175,12 +4209,14 @@ function buildFrontendTestHybridDag(sources) {
4175
4209
  toolProfile: "write",
4176
4210
  complexity: "HIGH",
4177
4211
  writePolicy: "exclusive",
4212
+ writeGuardPolicy: "tools-only",
4178
4213
  writeSet: caseDraftWriteSet,
4179
4214
  allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
4180
4215
  forbiddenPaths: forbidden,
4181
4216
  outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1 only; the exclusive shell materializer promotes the validated draft to manifest.json. Do not write manifest.json or test source code.",
4182
4217
  subtask_prompt: [
4183
4218
  "Use skill playwright-cli-case-generator.",
4219
+ "Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
4184
4220
  "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
4185
4221
  "Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
4186
4222
  "HARD ID CONTRACT (do not confuse these):",
@@ -4191,7 +4227,7 @@ function buildFrontendTestHybridDag(sources) {
4191
4227
  "dimensions: core|boundary|flow|backend only.",
4192
4228
  "Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
4193
4229
  "HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
4194
- `Use only the controller-frozen baseUrl ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; context.md may reference it but cannot establish or override it. Every browser start command must be exactly: playwright-cli open --browser=chrome --headed ${controllerFrontend.baseUrl}. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
4230
+ `Use only the controller-frozen baseUrl ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; context.md may reference it but cannot establish or override it. Every browser start command must be exactly: playwright-cli open --browser=chrome --headless ${controllerFrontend.baseUrl}. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
4195
4231
  "HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
4196
4232
  "HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
4197
4233
  "Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
@@ -4220,6 +4256,7 @@ function buildFrontendTestHybridDag(sources) {
4220
4256
  toolProfile: "write",
4221
4257
  complexity: "HIGH",
4222
4258
  writePolicy: "exclusive",
4259
+ writeGuardPolicy: "tools-only",
4223
4260
  writeSet: caseDraftWriteSet,
4224
4261
  allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
4225
4262
  forbiddenPaths: forbidden,
@@ -4270,38 +4307,28 @@ function buildFrontendTestHybridDag(sources) {
4270
4307
  ? ["final-frontend-case-review-gate-shell"]
4271
4308
  : ["generate-frontend-functional-cases-pi"];
4272
4309
  tasks.push({
4273
- id: "frontend-case-checklist-shell",
4310
+ id: "checklist-and-materialize-manifest-shell",
4274
4311
  depends_on: checklistDependsOn,
4275
4312
  role: "verifier",
4276
4313
  executor: "shell",
4277
4314
  complexity: "LOW",
4278
- writePolicy: "read-only",
4315
+ writePolicy: "exclusive",
4316
+ writeSet: casesWriteSet,
4279
4317
  allowedPaths: [...ragWriteSet, ...casesWriteSet],
4280
4318
  forbiddenPaths: forbidden,
4281
- outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, AC mapping, and executable command gate; rejects alternative executable instructions and non-allowlisted playwright-cli commands with ruleId-tagged location evidence.",
4282
- subtask_prompt: "Inspect and reject alternative executable instructions in fenced command code, list/indented steps, and explicit shell/terminal command lines. Only allowlisted playwright-cli command instructions may pass; ordinary prose and explicit blocked reasons may describe prohibitions. Run the native deterministic checklist without spawning Bash, PowerShell, or node -e and do not use free-form LLM verdicts.",
4319
+ outputContract: "Mechanical checklist then atomic manifest.json materialization; stdout one final JSON line {cases}.",
4320
+ subtask_prompt: "Run deterministic checklist then materialize manifest.json from draft after optional blocking review.",
4283
4321
  shell: {
4284
4322
  commands: [],
4285
- frontendTestCaseChecklist: {},
4323
+ frontendTestCaseManifest: {
4324
+ maxCases: config.maxCasesPerBatch,
4325
+ },
4286
4326
  cwd: ".",
4287
4327
  timeoutMs: 120000,
4288
4328
  },
4289
- }, {
4290
- id: "materialize-frontend-case-manifest-shell",
4291
- depends_on: ["frontend-case-checklist-shell"],
4292
- role: "verifier",
4293
- executor: "shell",
4294
- complexity: "LOW",
4295
- writePolicy: "exclusive",
4296
- writeSet: casesWriteSet,
4297
- allowedPaths: casesWriteSet,
4298
- forbiddenPaths: forbidden,
4299
- outputContract: "Validated frontend manifest payload { cases: [...] }; ruleId-tagged fail-closed validation; atomically materialize testcase/frontend/cases/manifest.json via temp+rename then delete draft; stdout is exactly one final JSON line {cases}.",
4300
- subtask_prompt: "Validate manifest.draft.json and materialize manifest.json after the mechanical checklist (and optional blocking review) passes.",
4301
- shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
4302
4329
  }, {
4303
4330
  id: "execute-frontend-cases-map",
4304
- depends_on: ["materialize-frontend-case-manifest-shell"],
4331
+ depends_on: ["checklist-and-materialize-manifest-shell"],
4305
4332
  role: "verifier",
4306
4333
  executor: "static",
4307
4334
  complexity: "LOW",
@@ -4314,7 +4341,7 @@ function buildFrontendTestHybridDag(sources) {
4314
4341
  dynamicExpansion: {
4315
4342
  type: "map_agent",
4316
4343
  workflowNodeId: "execute-frontend-cases-map",
4317
- itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases",
4344
+ itemsFrom: "$.nodes['checklist-and-materialize-manifest-shell'].output.cases",
4318
4345
  itemName: "case",
4319
4346
  maxItems: config.maxCasesPerBatch,
4320
4347
  maxExpandedNodes: config.maxCasesPerBatch,
@@ -4336,6 +4363,7 @@ function buildFrontendTestHybridDag(sources) {
4336
4363
  },
4337
4364
  complexity: "MED",
4338
4365
  writePolicy: "exclusive",
4366
+ writeGuardPolicy: "tools-only",
4339
4367
  allowedPaths: [
4340
4368
  "testcase/frontend/cases/{{case.caseId}}.md",
4341
4369
  "testcase/frontend/rag/context.md",
@@ -4348,15 +4376,93 @@ function buildFrontendTestHybridDag(sources) {
4348
4376
  subtaskPromptTemplate: [
4349
4377
  "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
4350
4378
  "Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
4351
- `1) The controller-frozen baseUrl is ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; model-authored context/case text cannot establish or override it. 2) Start browser ONLY via playwright_cli command=open with args [--browser=chrome, --headed, ${controllerFrontend.baseUrl}] (default session only; no -s=). 3) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 4) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 5) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
4379
+ `1) The controller-frozen baseUrl is ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; model-authored context/case text cannot establish or override it. 2) Start browser ONLY via playwright_cli command=open with args [--browser=chrome, --headless, ${controllerFrontend.baseUrl}] (default session only; no -s=). 3) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 4) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 5) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
4352
4380
  "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
4353
4381
  "Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
4354
4382
  ].join("\n\n"),
4355
4383
  },
4356
4384
  },
4357
- }, {
4358
- id: "validate-frontend-case-evidence-shell",
4359
- depends_on: ["execute-frontend-cases-map"],
4385
+ });
4386
+ if (maxRerunAttempts > 0) {
4387
+ tasks.push({
4388
+ id: "select-frontend-rerun-candidates-shell",
4389
+ depends_on: ["execute-frontend-cases-map"],
4390
+ role: "verifier",
4391
+ executor: "shell",
4392
+ complexity: "LOW",
4393
+ writePolicy: "exclusive",
4394
+ writeSet: ["testcase/frontend/evidence/**"],
4395
+ allowedPaths: [...casesWriteSet, "testcase/frontend/evidence/**"],
4396
+ forbiddenPaths: forbidden,
4397
+ outputContract: "Stdout JSON {cases} for blocked or missing-result-file cases with rerunAttempt < maxRerunAttempts.",
4398
+ subtask_prompt: "Select frontend-test cases eligible for bounded rerun.",
4399
+ shell: {
4400
+ commands: [
4401
+ ["node -e", JSON.stringify("const fs=require('fs'),path=require('path');const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(e){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + maxRerunAttempts + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/rerun-candidates.json',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));")].join(" "),
4402
+ ],
4403
+ cwd: ".",
4404
+ timeoutMs: 120_000,
4405
+ },
4406
+ }, {
4407
+ id: "rerun-frontend-cases-map",
4408
+ depends_on: ["select-frontend-rerun-candidates-shell"],
4409
+ role: "verifier",
4410
+ executor: "static",
4411
+ complexity: "LOW",
4412
+ writePolicy: "none",
4413
+ allowedPaths: [],
4414
+ forbiddenPaths: forbidden,
4415
+ outputContract: "Serial rerun of blocked/missing-result frontend cases.",
4416
+ subtask_prompt: "Expand rerun candidates into serial browser case children.",
4417
+ static: { resultMarkdown: "Frontend case rerun map expansion barrier." },
4418
+ dynamicExpansion: {
4419
+ type: "map_agent",
4420
+ workflowNodeId: "rerun-frontend-cases-map",
4421
+ itemsFrom: "$.nodes['select-frontend-rerun-candidates-shell'].output.cases",
4422
+ itemName: "case",
4423
+ maxItems: config.maxCasesPerBatch,
4424
+ maxExpandedNodes: config.maxCasesPerBatch,
4425
+ childIdPrefix: "rerun-frontend-case",
4426
+ workspaceTemplate: "{{case.evidenceDir}}",
4427
+ tolerateChildFailures: true,
4428
+ tokenBudget: {
4429
+ maxTokensPerCase: config.maxTokensPerCase,
4430
+ maxTotalTokens: config.maxTotalTokens,
4431
+ },
4432
+ childTask: {
4433
+ executor: "pi",
4434
+ role: "implementer",
4435
+ skills: ["playwright-cli"],
4436
+ toolProfile: "write",
4437
+ commandPolicy: {
4438
+ mode: "capability-allowlist",
4439
+ capabilities: ["playwright-cli"],
4440
+ },
4441
+ complexity: "MED",
4442
+ writePolicy: "exclusive",
4443
+ writeGuardPolicy: "tools-only",
4444
+ allowedPaths: [
4445
+ "testcase/frontend/cases/{{case.caseId}}.md",
4446
+ "testcase/frontend/rag/context.md",
4447
+ "testcase/frontend/rag/coverage-map.md",
4448
+ `${evidenceRoot}/{{case.caseId}}/**`,
4449
+ ],
4450
+ forbiddenPaths: forbidden,
4451
+ writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
4452
+ outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, tokens, and rerunAttempt.",
4453
+ subtaskPromptTemplate: [
4454
+ "RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite authoritative evidence; set rerunAttempt={{case.rerunAttempt}}.",
4455
+ "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
4456
+ `Start via playwright_cli command=open with args [--browser=chrome, --headless, ${controllerFrontend.baseUrl}]. Passed requires open → find → cleanup receipts.`,
4457
+ "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}.",
4458
+ ].join("\n\n"),
4459
+ },
4460
+ },
4461
+ });
4462
+ }
4463
+ tasks.push({
4464
+ id: "finalize-frontend-test-result-shell",
4465
+ depends_on: [maxRerunAttempts > 0 ? "rerun-frontend-cases-map" : "execute-frontend-cases-map"],
4360
4466
  role: "verifier",
4361
4467
  executor: "shell",
4362
4468
  complexity: "LOW",
@@ -4364,40 +4470,18 @@ function buildFrontendTestHybridDag(sources) {
4364
4470
  writeSet: [`${evidenceRoot}/**`],
4365
4471
  allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
4366
4472
  forbiddenPaths: forbidden,
4367
- outputContract: "Deterministic evidence gate: missing/malformed evidence is advisory; only unsafe evidenceDir or evidence paths hard-fail. Does not block retrospect.",
4368
- subtask_prompt: "Validate frontend case evidence before result materialization. Keep missing or malformed evidence as advisory findings; only path-escape failures abort the node.",
4369
- shell: {
4370
- commands: [],
4371
- frontendTestEvidenceValidation: {},
4372
- cwd: ".",
4373
- timeoutMs: 120000,
4374
- },
4375
- }, {
4376
- id: "materialize-frontend-test-result-shell",
4377
- depends_on: ["validate-frontend-case-evidence-shell"],
4378
- role: "verifier",
4379
- executor: "shell",
4380
- complexity: "LOW",
4381
- writePolicy: "read-only",
4382
- allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
4383
- forbiddenPaths: forbidden,
4384
- outputContract: "Run-owned hash-bound frontend-test-result-v1 derived only from the manifest and validated case evidence.",
4385
- subtask_prompt: "Materialize the authoritative frontend-test-result-v1. Do not use Pi prose or retrospective output as input.",
4473
+ outputContract: "Evidence validation (advisory missing/malformed does not fail the node; hard-fail only path escape) then hash-bound frontend-test-result-v1. Node success means result-v1 was written, not that all cases passed.",
4474
+ subtask_prompt: "Validate case evidence then materialize authoritative frontend-test-result-v1. Missing/malformed case evidence is advisory; only unsafe evidence paths hard-fail. Do not use Pi prose as input.",
4386
4475
  shell: {
4387
4476
  commands: [],
4388
- jsonArtifactGate: {
4389
- fromNodeId: "validate-frontend-case-evidence-shell",
4390
- schemaId: "frontend-test-result-v1",
4391
- artifactName: "frontend-test-result.json",
4392
- outputDir: "contracts",
4393
- },
4477
+ frontendTestResultFinalize: {},
4394
4478
  cwd: ".",
4395
4479
  timeoutMs: 120000,
4396
4480
  },
4397
4481
  });
4398
4482
  tasks.push({
4399
- id: "frontend-test-l5-report-shell",
4400
- depends_on: ["materialize-frontend-test-result-shell"],
4483
+ id: "frontend-test-reports-shell",
4484
+ depends_on: ["finalize-frontend-test-result-shell"],
4401
4485
  role: "verifier",
4402
4486
  executor: "shell",
4403
4487
  complexity: "LOW",
@@ -4405,11 +4489,11 @@ function buildFrontendTestHybridDag(sources) {
4405
4489
  writeSet: ["testcase/frontend/reports/**"],
4406
4490
  allowedPaths: ["testcase/frontend/**"],
4407
4491
  forbiddenPaths: forbidden,
4408
- outputContract: "Deterministic frontend L-5 Markdown and self-contained HTML dashboard derived only from frontend-test-result-v1.",
4409
- subtask_prompt: "Render the authoritative frontend L-5 report from frontend-test-result-v1. Do not use Pi prose or invent code coverage. Missing line/branch coverage remains unavailable and makes L-5 NOT READY.",
4492
+ outputContract: "Deterministic L-5 (optional) plus main frontend-test-report.md/html from frontend-test-result-v1. Pipeline acceptance = result-v1 + main HTML.",
4493
+ subtask_prompt: "Render operational reports from frontend-test-result-v1 only. Do not invent coverage. Main HTML is required; L-5 follows frontendTest.reports.l5.",
4410
4494
  shell: {
4411
- frontendTestL5Report: {},
4412
4495
  commands: [],
4496
+ frontendTestReports: { l5: enableL5Report },
4413
4497
  cwd: ".",
4414
4498
  timeoutMs: 120000,
4415
4499
  },
@@ -4418,8 +4502,8 @@ function buildFrontendTestHybridDag(sources) {
4418
4502
  tasks.push({
4419
4503
  id: "frontend-test-result-outcome-gate-shell",
4420
4504
  depends_on: [
4421
- "materialize-frontend-test-result-shell",
4422
- "frontend-test-l5-report-shell",
4505
+ "finalize-frontend-test-result-shell",
4506
+ "frontend-test-reports-shell",
4423
4507
  ],
4424
4508
  role: "verifier",
4425
4509
  executor: "shell",
@@ -4427,8 +4511,8 @@ function buildFrontendTestHybridDag(sources) {
4427
4511
  writePolicy: "read-only",
4428
4512
  allowedPaths: [],
4429
4513
  forbiddenPaths: forbidden,
4430
- outputContract: "Optional quality gate: pass only when frontend-test-result-v1 is outcome=passed and integrationMode=real with 0 failed/blocked and no missing AC. Does not gate retrospective closeout.",
4431
- subtask_prompt: "Opt-in Delivery/Worker quality gate (frontendTest.strictOutcomeGate=true). Retrospective does not depend on this node.",
4514
+ outputContract: "Optional quality gate: pass only when frontend-test-result-v1 is outcome=passed and integrationMode=real with 0 failed/blocked and no missing AC. Does not gate reports closeout.",
4515
+ subtask_prompt: "Opt-in Delivery/Worker quality gate (frontendTest.strictOutcomeGate=true). Main reports do not depend on this node.",
4432
4516
  shell: {
4433
4517
  commands: [frontendTestOutcomeGate],
4434
4518
  cwd: ".",
@@ -4436,49 +4520,30 @@ function buildFrontendTestHybridDag(sources) {
4436
4520
  },
4437
4521
  });
4438
4522
  }
4439
- tasks.push({
4440
- id: "frontend-test-retrospect-pi",
4441
- depends_on: [
4442
- "materialize-frontend-test-result-shell",
4443
- "frontend-test-l5-report-shell",
4444
- ],
4445
- role: "closeout",
4446
- executor: "pi",
4447
- toolProfile: "write",
4448
- complexity: "MED",
4449
- writePolicy: "exclusive",
4450
- writeSet: ["testcase/frontend/reports/**"],
4451
- allowedPaths: ["testcase/frontend/**"],
4452
- forbiddenPaths: forbidden,
4453
- outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete. Pipeline acceptance = this report exists (not case 100% pass).",
4454
- subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization and L-5 report generation. Combine AC→case→browser-evidence review with the closeout report: coverage, the exact L-5 READY/NOT READY decision, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not recompute L-5 metrics or replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
4455
- });
4456
- tasks.push({
4457
- id: "frontend-test-html-report-shell",
4458
- depends_on: ["frontend-test-retrospect-pi"],
4459
- role: "verifier",
4460
- executor: "shell",
4461
- complexity: "LOW",
4462
- writePolicy: "exclusive",
4463
- writeSet: ["testcase/frontend/reports/**"],
4464
- allowedPaths: ["testcase/frontend/**"],
4465
- forbiddenPaths: forbidden,
4466
- outputContract: "Write testcase/frontend/reports/frontend-test-report.md and frontend-test-report.html from frontend-test-result-v1, containing only case execution results, case content, and failed/blocked error analysis.",
4467
- subtask_prompt: "Render the formal frontend test Markdown and HTML report from the current run frontend-test-result-v1. Do not include evidence chains, evidence paths or hashes, advisory findings, quality suggestions, improvement suggestions, or ratings.",
4468
- shell: {
4469
- commands: [],
4470
- frontendTestHtmlReport: {},
4471
- cwd: ".",
4472
- timeoutMs: 120000,
4473
- },
4474
- });
4523
+ if (enableRetrospect) {
4524
+ tasks.push({
4525
+ id: "frontend-test-retrospect-pi",
4526
+ depends_on: ["finalize-frontend-test-result-shell", "frontend-test-reports-shell"],
4527
+ role: "closeout",
4528
+ executor: "pi",
4529
+ toolProfile: "write",
4530
+ complexity: "MED",
4531
+ writePolicy: "exclusive",
4532
+ writeGuardPolicy: "tools-only",
4533
+ writeSet: ["testcase/frontend/reports/**"],
4534
+ allowedPaths: ["testcase/frontend/**"],
4535
+ forbiddenPaths: forbidden,
4536
+ outputContract: "Optional retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md. Not required for pipeline acceptance.",
4537
+ subtask_prompt: "Write optional frontend-test retrospective after result + reports. Do not recompute L-5. Pipeline success does not require this file.",
4538
+ });
4539
+ }
4475
4540
  const globalConstraints = [
4476
4541
  ...sources.taskConfig.hardConstraints,
4477
4542
  ...STANDARD_GLOBAL_CONSTRAINTS,
4478
4543
  "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
4479
4544
  "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
4480
4545
  "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
4481
- `Browser startup for generated cases must be playwright-cli open --browser=chrome --headed ${controllerFrontend.baseUrl}; this origin is frozen by the controller from ${controllerFrontend.baseUrlSource}, and model-authored files cannot establish or override it; generated operations stay in the default browser session and must not use unverified named-session flags.`,
4546
+ `Browser startup for generated cases must be playwright-cli open --browser=chrome --headless ${controllerFrontend.baseUrl}; this origin is frozen by the controller from ${controllerFrontend.baseUrlSource}, and model-authored files cannot establish or override it; generated operations stay in the default browser session and must not use unverified named-session flags.`,
4482
4547
  "Browser-tool preflight runs before any frontend-test Pi node; cli-only rollback, missing/incompatible Pi SDK custom-tool capability, missing verified playwright-cli launcher, or incompatible --help fails with zero Pi calls.",
4483
4548
  "Browser case children use commandPolicy capability-allowlist playwright-cli and structured playwright_cli tool; ordinary writers remain without Bash.",
4484
4549
  "Passed cases require same-child ordered controller receipts: successful open → successful find → successful post-execution cleanup. Pre-start cleanup, snapshot/goto/screenshot/request/console and ordinary interactions are insufficient; missing or unordered receipts convert to blocked (browser-command-evidence-missing).",
@@ -4486,7 +4551,7 @@ function buildFrontendTestHybridDag(sources) {
4486
4551
  "Environment preflight must curl-probe the frozen non-production baseUrl before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
4487
4552
  "U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
4488
4553
  "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
4489
- "Pipeline acceptance for frontend-test is the final retrospect report under testcase/frontend/reports/; case pass rate and outcome=passed are quality signals, not the default pipeline success condition.",
4554
+ "Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under testcase/frontend/reports/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).",
4490
4555
  blockingReview
4491
4556
  ? "frontendTest.reviewMode=blocking: a frontend case review must emit VERDICT: pass before checklist/manifest materialization; request-revision blocks browser execution."
4492
4557
  : "frontendTest.reviewMode is off|advisory by default: mechanical checklist-shell gates materialize/execute; LLM review is not a hard browser gate.",
@@ -8,6 +8,7 @@ import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
8
8
  import { resolveContextPolicy } from "./context-policy.js";
9
9
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
10
10
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
11
+ import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
11
12
  import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, } from "./retry-policy.js";
12
13
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
13
14
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
@@ -125,7 +126,7 @@ export function buildNodePrompt(spec, task, upstream, options) {
125
126
  convergenceFeedback: options?.convergenceFeedback,
126
127
  });
127
128
  }
128
- function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason) {
129
+ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths) {
129
130
  if (attemptNumber <= 1)
130
131
  return basePrompt;
131
132
  if (previousFailureCategory === "protocol-invalid" &&
@@ -138,6 +139,22 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
138
139
  ].join("\n");
139
140
  }
140
141
  if (previousFailureCategory === "writer-empty-diff") {
142
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
143
+ // When a completeness progress exists for this writer, fold the concrete
144
+ // target paths into the empty-diff retry so the model does not guess and
145
+ // does not need to read a forbidden `.harness/**` evidence file.
146
+ if (recoveryTargetPaths && recoveryTargetPaths.length > 0) {
147
+ return [
148
+ basePrompt,
149
+ "",
150
+ buildOutputLimitRecoverySection({
151
+ attempt: attemptNumber,
152
+ maxAttempts,
153
+ reason: "T4",
154
+ targetPaths: recoveryTargetPaths,
155
+ }),
156
+ ].join("\n");
157
+ }
141
158
  return [
142
159
  basePrompt,
143
160
  "",
@@ -150,6 +167,22 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
150
167
  }
151
168
  if (previousFailureCategory === "incomplete-write-set") {
152
169
  const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
170
+ // Embed concrete missing/broken paths from the run-owned progress facts
171
+ // so the continuation attempt is fully self-contained and never reads
172
+ // a forbidden `.harness/**` evidence file. When the loader finds no
173
+ // progress facts yet (rare), fall back to the path-pointing contract.
174
+ if (recoveryTargetPaths) {
175
+ return [
176
+ basePrompt,
177
+ "",
178
+ buildOutputLimitRecoverySection({
179
+ attempt: attemptNumber,
180
+ maxAttempts,
181
+ reason: "T3_or_T5",
182
+ targetPaths: recoveryTargetPaths,
183
+ }),
184
+ ].join("\n");
185
+ }
153
186
  return [
154
187
  basePrompt,
155
188
  "",
@@ -597,12 +630,18 @@ export async function executeDagNode(input) {
597
630
  tasksById,
598
631
  state,
599
632
  });
633
+ // For backend-test generation writers, load the most recent
634
+ // completeness progress from the run dir so the next attempt's
635
+ // prompt embeds concrete target paths. Undefined for non-writers
636
+ // or when no progress facts exist yet (no-op).
637
+ const recoveryProgress = await loadBackendTestWriterProgressForRetry(runDir, task.id);
638
+ const recoveryTargetPaths = recoveryProgress?.targetPaths;
600
639
  result = await executeNode({
601
640
  task,
602
641
  cwd,
603
642
  model,
604
643
  ...(thinking ? { thinking } : {}),
605
- prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason),
644
+ prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths),
606
645
  attempt: attemptNumber,
607
646
  reportActivity,
608
647
  timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
@@ -324,6 +324,8 @@ export const dagWritePolicySchema = z.enum([
324
324
  "exclusive",
325
325
  "none",
326
326
  ]);
327
+ /** Git/post-diff write guard intensity for exclusive Pi writers. */
328
+ export const dagWriteGuardPolicySchema = z.enum(["full", "tools-only"]);
327
329
  export const contextPolicyIdSchema = z.enum([
328
330
  "baseline-v1",
329
331
  "role-specialized-v1",
@@ -367,6 +369,38 @@ export const dagFrontendBrowserToolPreflightSchema = z.object({}).strict();
367
369
  export const dagFrontendTestEvidenceValidationSchema = z.object({}).strict();
368
370
  export const dagFrontendTestCaseChecklistSchema = z.object({}).strict();
369
371
  export const dagFrontendTestHtmlReportSchema = z.object({}).strict();
372
+ /**
373
+ * Combined checklist + manifest materialization gate (lean frontend-test DAG).
374
+ * Runs the mechanical frontend case checklist first, then atomically
375
+ * materializes testcase/frontend/cases/manifest.json from manifest.draft.json.
376
+ * Stdout is exactly one final JSON line {cases}.
377
+ */
378
+ export const dagFrontendTestCaseManifestSchema = z
379
+ .object({
380
+ maxCases: z.number().int().min(1).optional(),
381
+ declaredAcIds: z.array(z.string()).optional(),
382
+ })
383
+ .strict();
384
+ /**
385
+ * Combined evidence validation + result materialization gate (lean frontend-test DAG).
386
+ * Runs deterministic evidence validation first (advisory on missing/malformed,
387
+ * hard-fail on path escape), then materializes frontend-test-result-v1.
388
+ */
389
+ export const dagFrontendTestResultFinalizeSchema = z
390
+ .object({
391
+ declaredAcIds: z.array(z.string()).optional(),
392
+ })
393
+ .strict();
394
+ /**
395
+ * Combined reports gate (lean frontend-test DAG). Renders the deterministic
396
+ * frontend L-5 dashboard (when l5 is true, the default) and then the main
397
+ * frontend-test Markdown + self-contained HTML report from frontend-test-result-v1.
398
+ */
399
+ export const dagFrontendTestReportsSchema = z
400
+ .object({
401
+ l5: z.boolean().optional(),
402
+ })
403
+ .strict();
370
404
  export const dagFinalWriteSetApprovalGateSchema = z
371
405
  .object({
372
406
  writerNodeId: z
@@ -426,6 +460,9 @@ export const dagShellConfigSchema = z.object({
426
460
  finalWriteSetApprovalGate: dagFinalWriteSetApprovalGateSchema.optional(),
427
461
  frontendTestL5Report: z.object({}).strict().optional(),
428
462
  frontendTestHtmlReport: dagFrontendTestHtmlReportSchema.optional(),
463
+ frontendTestCaseManifest: dagFrontendTestCaseManifestSchema.optional(),
464
+ frontendTestResultFinalize: dagFrontendTestResultFinalizeSchema.optional(),
465
+ frontendTestReports: dagFrontendTestReportsSchema.optional(),
429
466
  backendTestPipeline: dagBackendTestPipelineSchema.optional(),
430
467
  /** JaCoCo coverage collection for backend-test (Java services). When set, node 7 dumps coverage over TCP from a JaCoCo tcpserver agent and feeds it to the L-5 dashboard. */
431
468
  jacocoCoverage: z
@@ -475,6 +512,7 @@ export const dagDynamicExpansionChildTaskSchema = z.object({
475
512
  allowedPaths: z.array(z.string()).optional().default([]),
476
513
  forbiddenPaths: z.array(z.string()).optional().default([]),
477
514
  writeSet: z.array(z.string()).optional(),
515
+ writeGuardPolicy: dagWriteGuardPolicySchema.optional(),
478
516
  });
479
517
  export const dagDynamicExpansionSchema = z.object({
480
518
  type: z.enum(["map_agent", "verify_agent"]),
@@ -657,6 +695,8 @@ export const dagTaskSchema = z.object({
657
695
  * run-attributed diff.
658
696
  */
659
697
  writerOutcomePolicy: dagWriterOutcomePolicySchema.optional(),
698
+ /** full (default): git baseline + post-diff write-guard. tools-only: keep tool sandbox, skip git baseline/post-diff hard fail (frontend-test). */
699
+ writeGuardPolicy: dagWriteGuardPolicySchema.optional(),
660
700
  /** Required for supervised writers that consume a final audited writeSet. */
661
701
  finalWriteSetApproval: dagFinalWriteSetApprovalBindingSchema.optional(),
662
702
  /** Runtime-only authorization facts injected after final approval validation. */
@@ -468,7 +468,10 @@ function validateShellTaskConfig(task, spec, issues) {
468
468
  !shell.frontendTestCaseChecklist &&
469
469
  !shell.frontendTestEvidenceValidation &&
470
470
  !shell.frontendTestL5Report &&
471
- !shell.frontendTestHtmlReport) {
471
+ !shell.frontendTestHtmlReport &&
472
+ !shell.frontendTestCaseManifest &&
473
+ !shell.frontendTestResultFinalize &&
474
+ !shell.frontendTestReports) {
472
475
  issues.push({
473
476
  type: "missing-shell-commands",
474
477
  message: `shell task ${task.id} requires a supported shell operation or non-empty shell.commands`,