@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.
- package/CHANGELOG.md +47 -0
- package/dist/commands/init.js +1 -1
- package/dist/executors/dag-pi-executor.js +38 -11
- package/dist/executors/pi-playwright-cli-tool.js +14 -8
- package/dist/executors/shell-executor.js +153 -0
- package/dist/task/config-types.js +21 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +98 -14
- package/dist/workflows/dag/frontend-test-case-checklist.js +94 -15
- package/dist/workflows/dag/frontend-test-case-manifest.js +104 -0
- package/dist/workflows/dag/frontend-test-html-report.js +106 -24
- package/dist/workflows/dag/frontend-test-result-contract.js +3 -0
- package/dist/workflows/dag/init-hybrid.js +167 -102
- package/dist/workflows/dag/node-execution.js +41 -2
- package/dist/workflows/dag/types.js +40 -0
- package/dist/workflows/dag/validate.js +4 -1
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +6 -1
- package/docs/templates/frontend-test-case-checklist.md +1 -1
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +9 -1
- package/docs/templates/frontend-test-dag.json +125 -267
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-standard-scenarios.v1.json +114 -0
- package/package.json +1 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +1 -1
|
@@ -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+--
|
|
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 --
|
|
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+--
|
|
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: "
|
|
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 --
|
|
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 --
|
|
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: "
|
|
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: "
|
|
4315
|
+
writePolicy: "exclusive",
|
|
4316
|
+
writeSet: casesWriteSet,
|
|
4279
4317
|
allowedPaths: [...ragWriteSet, ...casesWriteSet],
|
|
4280
4318
|
forbiddenPaths: forbidden,
|
|
4281
|
-
outputContract: "Mechanical checklist
|
|
4282
|
-
subtask_prompt: "
|
|
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
|
-
|
|
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: ["
|
|
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['
|
|
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, --
|
|
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
|
-
|
|
4359
|
-
|
|
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: "
|
|
4368
|
-
subtask_prompt: "Validate
|
|
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
|
-
|
|
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-
|
|
4400
|
-
depends_on: ["
|
|
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
|
|
4409
|
-
subtask_prompt: "Render
|
|
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
|
-
"
|
|
4422
|
-
"frontend-test-
|
|
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
|
|
4431
|
-
subtask_prompt: "Opt-in Delivery/Worker quality gate (frontendTest.strictOutcomeGate=true).
|
|
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
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
"
|
|
4443
|
-
"
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
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 --
|
|
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
|
|
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`,
|