@tea-agent/loop-agent 0.16.23 → 0.16.25

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.
@@ -2546,6 +2546,7 @@ function buildAnalyzeInputsNode(sources) {
2546
2546
  "For every endpoint, explicitly set responseBody.kind=array|object|scalar|empty|unknown and ordering=specified|unspecified|not-applicable. Add itemSchemaRef for arrays when documented.",
2547
2547
  "For response fields, use comparison=exact|parseable-only|semantic when the source defines assertion semantics; date-time fields whose precision is unspecified should use parseable-only, not string equality.",
2548
2548
  "Endpoint sourceRefs and field sourceRefs must cite only requirement/reference evidence actually read. Empty sourceRefs are allowed only when normalizing legacy v1 input; newly generated v2 should cite evidence.",
2549
+ "For externalDependencies and risks, emit canonical items with exactly description plus optional name and sourceRef. For a dependency target, put the target value in name. Do not emit type, target, kind, required, severity, mitigation, level, impact, sourceRefs, or custom keys in newly generated v2 output.",
2549
2550
  "Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
2550
2551
  "Required top-level keys: schemaVersion=2, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
2551
2552
  "Read-only: do not modify code, docs, artifacts, or repository files.",
@@ -2911,6 +2912,8 @@ function buildGenerateBackendPytestNode(sources) {
2911
2912
  "- Validated Backend Test Analysis v1 under the current run contracts/ (analysis gate).",
2912
2913
  "- Validated Backend Test Execution Contract v1 under contracts/backend-test-execution.json (execution gate).",
2913
2914
  "Use only fixture names, env NAMES, testRoot, targetMode, and field/API facts already present in those contracts or reviewed cases. Do not invent production credentials or secret values.",
2915
+ "When targetMode is in-process (including demoted local npm/node managed servers): bootstrap the service inside function-scoped pytest fixtures under testcase/** — e.g. subprocess node server.js / startWelcomeServer with PORT=0 — and never require host-injected base URL env vars (clean-env shell will not provide WELCOME_BASE_URL / API_BASE_URL).",
2916
+ "Do not depend on requiredEnvNames being present at process start for in-process mode; if the contract still lists an env name, the fixture must set it or start the server without that env.",
2914
2917
  "",
2915
2918
  "## Output Steps (do in order):",
2916
2919
  "1. First, output a brief summary: how many files, how many test functions planned",
@@ -3061,8 +3064,9 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
3061
3064
  const reportStem = options.reportStem ?? "backend-test";
3062
3065
  const reportName = `${reportStem}-junit.xml`;
3063
3066
  const exitName = `${reportStem}-pytest-exit.txt`;
3064
- const pytestCommand = [
3065
- preflightCommand,
3067
+ // Preflight snippet is already fail-closed (&&). Only the pytest body may use
3068
+ // ";" so STATUS capture still runs after non-zero pytest exits.
3069
+ const pytestBody = [
3066
3070
  `REPORT="\${HARNESS_DAG_RUN_DIR}/reports/${reportName}"`,
3067
3071
  `EXIT_FILE="\${HARNESS_DAG_RUN_DIR}/reports/${exitName}"`,
3068
3072
  'mkdir -p "$(dirname "${REPORT}")"',
@@ -3074,6 +3078,7 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
3074
3078
  'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${REPORT}" ]; then exit 0; fi',
3075
3079
  'exit "${STATUS}"',
3076
3080
  ].join("; ");
3081
+ const pytestCommand = `${preflightCommand} && { ${pytestBody}; }`;
3077
3082
  return {
3078
3083
  id: nodeId,
3079
3084
  depends_on: options.dependsOn ?? [
@@ -3167,21 +3172,24 @@ function buildTestRetrospectNode(sources) {
3167
3172
  "## Stats authority (deterministic only):",
3168
3173
  "- Pass rate, failed/error/skipped counts, and failure list MUST come from contracts/backend-test-result.json only.",
3169
3174
  "- AC coverage ratio / case counts MUST come from contracts/backend-test-case-manifest.json coverageSummary (or gate-derived fields). Do NOT invent coverage %.",
3175
+ "- Automation coverage MUST use coverageSummary.generatedCount / coverageSummary.caseCount. If either field is missing, write unavailable; do not estimate.",
3176
+ "- Code coverage MUST come only from the validated contracts/code-coverage-v1.json artifact generated by coverage.py/pytest-cov or JaCoCo. Show line, branch, function/method, covered, total, ratio, threshold, status, source scope, requirement IDs, tool, commit, and artifact hash.",
3177
+ "- Stability MUST come from independent Stability Evidence: use successfulRuns / recordedRuns, same suite/version, and require n≥5; a single run is unavailable.",
3170
3178
  "- Use classify-backend-test-result-pi JSON as interpretive evidence only.",
3171
3179
  "- NEVER rewrite a failed result as passed. Outcome gate (not this report) is authoritative for task success.",
3172
3180
  "",
3173
3181
  "## Report Structure:",
3174
3182
  "1. Maturity Rating with rationale",
3175
- "2. Test Coverage Summary (manifest coverageSummary + Result v1 pass rate)",
3176
- "3. Review Findings and resolution status",
3177
- "4. Failed Test Analysis (if any) + classification category",
3178
- "5. Recommendations for improvement",
3183
+ "2. Test Coverage Summary (Result v1 pass rate, AC coverage, automation coverage, code coverage, and stability evidence)",
3184
+ "3. Failed Test Analysis (failure/error details, category, confidence, evidence, and owner direction)",
3185
+ "4. Defects (local Bug ledger in the same report directory; unavailable when absent)",
3186
+ "5. Risks (Critical/High/Medium/Low, impact, controls, residual risk, treatment; Critical risks block L-5, High risks do not automatically block)",
3187
+ "6. Regression Recommendations (immediate, related, periodic, deferred; every item links to failure/risk/AC/case IDs)",
3188
+ "7. L-5 conclusion with blocking items",
3179
3189
  "",
3180
3190
  "## Rating Criteria:",
3181
- "- A: coverageSummary.acCoverageRatio=1 + 100% pytest pass + no Critical findings",
3182
- "- B: acCoverageRatio≥0.8 + ≥90% pass + Low findings only",
3183
- "- C: acCoverageRatio≥0.6 + ≥70% pass + no Critical findings",
3184
- "- D: below C thresholds",
3191
+ "- L-5 ready requires pass rate=100%, AC coverage=100%, automation coverage≥90%, stability≥95% with n≥5, line coverage≥80%, branch coverage≥70%, skipped=0, and no blocking Critical risk.",
3192
+ "- Any required metric fail or unavailable means L-5 not-ready. Function/method coverage is displayed but not a gate. Preserve the existing A/B/C/D single-run rating separately.",
3185
3193
  "",
3186
3194
  "## Constraints:",
3187
3195
  canWriteReport
@@ -3196,7 +3204,7 @@ function buildBackendTestOutcomeGateNode(sources) {
3196
3204
  const gateCommand = buildBackendTestOutcomeGateShellSnippet();
3197
3205
  return {
3198
3206
  id: "backend-test-outcome-gate-shell",
3199
- depends_on: ["test-retrospect-pi"],
3207
+ depends_on: ["l5-metrics-pi"],
3200
3208
  role: "verifier",
3201
3209
  executor: "shell",
3202
3210
  complexity: "LOW",
@@ -3219,6 +3227,30 @@ function buildBackendTestOutcomeGateNode(sources) {
3219
3227
  },
3220
3228
  };
3221
3229
  }
3230
+ function buildL5MetricsNode(sources) {
3231
+ return {
3232
+ id: "l5-metrics-pi",
3233
+ depends_on: ["test-retrospect-pi"],
3234
+ role: "reviewer",
3235
+ executor: "pi",
3236
+ complexity: "MED",
3237
+ writePolicy: "read-only",
3238
+ allowedPaths: commonReadOnlyPaths(sources),
3239
+ forbiddenPaths: commonForbiddenPaths(sources),
3240
+ outputContract: "Exactly one JSON object with status=ready|not-ready, metrics, and blockingItems; no file writes.",
3241
+ subtask_prompt: [
3242
+ "You are the independent L-5 metrics node at the end of the existing backend-test DAG.",
3243
+ "The direct upstream test-retrospect-pi output is the primary report to assess. Read it together with the run-owned Result v1, Case Manifest v1, Code Coverage v1, and Stability Evidence artifacts when present.",
3244
+ "Do not create a new DAG, rewrite the retrospective report, change test outcome, or modify any repository file.",
3245
+ "Return exactly one JSON object and no surrounding prose.",
3246
+ "Required shape: {\"status\":\"ready\"|\"not-ready\",\"metrics\":{\"passRate\":metric,\"acCoverage\":metric,\"automationCoverage\":metric,\"stability\":metric,\"lineCoverage\":metric,\"branchCoverage\":metric,\"skipped\":metric,\"criticalRisks\":metric},\"blockingItems\":[string]}.",
3247
+ "Each metric must contain numerator, denominator, ratio, threshold, status=pass|fail|unavailable, and reason (null only when passed).",
3248
+ "Use only explicit evidence. Missing or invalid required evidence is unavailable, never zero or an estimate.",
3249
+ "L-5 ready requires pass rate=100%, AC coverage=100%, automation coverage>=90%, stability>=95% with n>=5, line coverage>=80%, branch coverage>=70%, skipped=0, and zero blocking Critical risks.",
3250
+ "Function/method coverage is display-only and does not gate L-5. Preserve the distinction between L-5 maturity and the Result v1 outcome gate.",
3251
+ ].join("\n\n"),
3252
+ };
3253
+ }
3222
3254
  const BACKEND_TEST_DEFAULTS = {
3223
3255
  ...HYBRID_DEFAULTS,
3224
3256
  writePolicy: "read-only",
@@ -3236,7 +3268,7 @@ function buildBackendTestHybridDag(sources) {
3236
3268
  const globalConstraints = [
3237
3269
  ...taskConfig.hardConstraints,
3238
3270
  ...STANDARD_GLOBAL_CONSTRAINTS,
3239
- "backend-test-dag uses exactly 15 real top-level tasks and executes pytest exactly once.",
3271
+ "backend-test-dag uses exactly 16 real top-level tasks and executes pytest exactly once.",
3240
3272
  "Case and semantic request-revision verdicts fail at deterministic gates; no in-run revision or repair writer is authorized.",
3241
3273
  "Analysis, execution, manifest, semantic review, single-run result, classification, canonical result, retrospective and outcome evidence remain run-owned and fail-closed.",
3242
3274
  "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
@@ -3300,8 +3332,10 @@ function buildBackendTestHybridDag(sources) {
3300
3332
  const retrospect = buildTestRetrospectNode(sources);
3301
3333
  retrospect.depends_on = [context.id];
3302
3334
  retrospect.subtask_prompt = retrospect.subtask_prompt.replaceAll("select-effective-backend-test-result-shell", context.id);
3335
+ const l5Metrics = buildL5MetricsNode(sources);
3336
+ l5Metrics.depends_on = [retrospect.id];
3303
3337
  const outcome = buildBackendTestOutcomeGateNode(sources);
3304
- const tasks = [analyze, contracts, generateCases, manifest, reviewCases, caseGate, generatePytest, semanticReview, semanticMaterialize, semanticGate, execute, classify, context, retrospect, outcome];
3338
+ const tasks = [analyze, contracts, generateCases, manifest, reviewCases, caseGate, generatePytest, semanticReview, semanticMaterialize, semanticGate, execute, classify, context, retrospect, l5Metrics, outcome];
3305
3339
  const spec = { version: 3, title: `Backend test DAG: ${taskConfig.title}`, runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT, outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE, objective: extractObjective(sources.requirementMarkdown, taskConfig.title), successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId), globalConstraints, defaults: { ...BACKEND_TEST_DEFAULTS, contextProfile: taskConfig.contextProfile }, skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE, executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS, tasks };
3306
3340
  applyDefaultReadOnlyRetryPolicy(spec);
3307
3341
  parseDagSpec(spec);
@@ -3312,7 +3346,17 @@ function buildBackendTestHybridDag(sources) {
3312
3346
  // Frontend browser-test RAG DAG template
3313
3347
  // ---------------------------------------------------------------------------
3314
3348
  function buildFrontendTestHybridDag(sources) {
3315
- const config = sources.taskConfig.frontendTest ?? { maxCasesPerBatch: 20 };
3349
+ const rawFrontendTest = sources.taskConfig.frontendTest;
3350
+ const config = {
3351
+ maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 20,
3352
+ maxTokensPerCase: rawFrontendTest?.maxTokensPerCase,
3353
+ maxTotalTokens: rawFrontendTest?.maxTotalTokens,
3354
+ reviewMode: rawFrontendTest?.reviewMode ?? "off",
3355
+ strictOutcomeGate: rawFrontendTest?.strictOutcomeGate === true,
3356
+ };
3357
+ const reviewMode = config.reviewMode;
3358
+ const blockingReview = reviewMode === "blocking";
3359
+ const strictOutcomeGate = config.strictOutcomeGate;
3316
3360
  const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
3317
3361
  pattern === "testcase/**" ||
3318
3362
  pattern === "**");
@@ -3323,6 +3367,36 @@ function buildFrontendTestHybridDag(sources) {
3323
3367
  const ragWriteSet = ["testcase/frontend/rag/**"];
3324
3368
  const casesWriteSet = ["testcase/frontend/cases/**"];
3325
3369
  const evidenceRoot = "testcase/frontend/evidence";
3370
+ const checklistValidation = [
3371
+ "node -e",
3372
+ JSON.stringify([
3373
+ "const fs=require('fs'),path=require('path');",
3374
+ "const root='testcase/frontend/cases';",
3375
+ "const draft=path.join(root,'manifest.draft.json');",
3376
+ "const final=path.join(root,'manifest.json');",
3377
+ "const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
3378
+ "if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
3379
+ "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
3380
+ "if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
3381
+ "const issues=[];",
3382
+ "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
3383
+ "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
3384
+ "const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
3385
+ "for(const c of manifest.cases){",
3386
+ " const id=c&&c.caseId||'?';",
3387
+ " const casePath=typeof c.casePath==='string'?c.casePath:null;",
3388
+ " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
3389
+ " const body=fs.readFileSync(casePath,'utf8');",
3390
+ " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
3391
+ " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
3392
+ " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
3393
+ " if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
3394
+ " if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required'});",
3395
+ "}",
3396
+ "if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
3397
+ "console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
3398
+ ].join("")),
3399
+ ].join(" ");
3326
3400
  const manifestValidation = [
3327
3401
  "node -e",
3328
3402
  JSON.stringify([
@@ -3338,7 +3412,6 @@ function buildFrontendTestHybridDag(sources) {
3338
3412
  " if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) throw new Error('invalid acIds');",
3339
3413
  " for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) throw new Error('unsafe '+k); }",
3340
3414
  " if(c.casePath!=='testcase/frontend/cases/'+c.caseId+'.md') throw new Error('casePath must match caseId');",
3341
- // Accept evidenceDir as the case root or a nested path under that root.
3342
3415
  " { const prefix='testcase/frontend/evidence/'+c.caseId; if(!(c.evidenceDir===prefix||c.evidenceDir.startsWith(prefix+'/'))) throw new Error('case path escapes frontend test roots'); }",
3343
3416
  " if(!fs.existsSync(c.casePath)) throw new Error('missing case file '+c.casePath);",
3344
3417
  " if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
@@ -3360,6 +3433,290 @@ function buildFrontendTestHybridDag(sources) {
3360
3433
  ].join("")),
3361
3434
  ].join(" ");
3362
3435
  const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
3436
+ const tasks = [
3437
+ {
3438
+ id: "retrieve-frontend-test-context-pi",
3439
+ depends_on: [],
3440
+ role: "planner",
3441
+ executor: "pi",
3442
+ toolProfile: "write",
3443
+ complexity: "MED",
3444
+ writePolicy: "exclusive",
3445
+ writeSet: ragWriteSet,
3446
+ allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
3447
+ forbiddenPaths: forbidden,
3448
+ outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with machine-readable baseUrl and capability notes.",
3449
+ subtask_prompt: [
3450
+ "Build the frontend test RAG package (keep it short).",
3451
+ "Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
3452
+ "Prefer fixed fields: baseUrl, baseUrlSource, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
3453
+ "Base URL resolution (required): (1) Prefer absolute http(s) frontend URL from task source config.md. (2) Else default http://localhost:5173. (3) Never production hosts. (4) Write `baseUrl: <url>` and `baseUrlSource: config.md|<path>|default-localhost-5173`. (5) Include exact start prefix: playwright-cli open --browser=chrome --headed <resolved-base-url>.",
3454
+ buildSourceContextBlock(sources),
3455
+ ].join("\n\n"),
3456
+ },
3457
+ {
3458
+ id: "materialize-frontend-test-execution-shell",
3459
+ depends_on: ["retrieve-frontend-test-context-pi"],
3460
+ role: "verifier",
3461
+ executor: "shell",
3462
+ complexity: "LOW",
3463
+ writePolicy: "read-only",
3464
+ allowedPaths: [...ragWriteSet],
3465
+ forbiddenPaths: forbidden,
3466
+ outputContract: "Fail-closed preflight: absolute non-production baseUrl required; fixture/reset not hard-gated.",
3467
+ subtask_prompt: "Hard-validate only an absolute non-production baseUrl in RAG context (from config.md or default http://localhost:5173). Fixture/reset and other isolation details are soft guidance for later nodes, not preflight failures.",
3468
+ shell: {
3469
+ commands: [
3470
+ [
3471
+ "node -e",
3472
+ JSON.stringify("const fs=require('fs'); const p='testcase/frontend/rag/context.md'; if(!fs.existsSync(p))throw new Error('missing '+p); const s=fs.readFileSync(p,'utf8'); const patterns=[ /baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i, /base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i, /playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i, /(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\]},\"']*)/i ]; let baseUrl=null; for(const re of patterns){const m=s.match(re); if(m){baseUrl=m[1]; break;}} if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)'); baseUrl=baseUrl.replace(/[)\\]},.\"']+$/,''); if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl); if(/(?:^|\\/\\/)(?:www\\.)?[^\\s/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl); console.log('frontend-test-execution-v1 validated baseUrl='+baseUrl);"),
3473
+ ].join(" "),
3474
+ ],
3475
+ cwd: ".",
3476
+ timeoutMs: 60000,
3477
+ },
3478
+ },
3479
+ {
3480
+ id: "generate-frontend-functional-cases-pi",
3481
+ depends_on: ["materialize-frontend-test-execution-shell"],
3482
+ role: "implementer",
3483
+ executor: "pi",
3484
+ toolProfile: "write",
3485
+ complexity: "HIGH",
3486
+ writePolicy: "exclusive",
3487
+ writeSet: casesWriteSet,
3488
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3489
+ forbiddenPaths: forbidden,
3490
+ outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1; no test source code.",
3491
+ subtask_prompt: [
3492
+ "Use skill playwright-cli-case-generator.",
3493
+ "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
3494
+ "Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.",
3495
+ "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.",
3496
+ "Copy the resolved absolute baseUrl from context.md (baseUrl field; resolved from config.md or default http://localhost:5173). Every browser start command must be: playwright-cli open --browser=chrome --headed <resolved-base-url-from-context.md> with that concrete URL — never leave a <base-url> placeholder. Use default browser session only; never write -s=<case-id>.",
3497
+ "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.",
3498
+ ].join("\n\n"),
3499
+ },
3500
+ ];
3501
+ if (blockingReview) {
3502
+ tasks.push({
3503
+ id: "review-frontend-cases-pi",
3504
+ depends_on: ["generate-frontend-functional-cases-pi"],
3505
+ role: "reviewer",
3506
+ executor: "pi",
3507
+ complexity: "HIGH",
3508
+ writePolicy: "read-only",
3509
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3510
+ forbiddenPaths: forbidden,
3511
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes. request-revision blocks manifest materialization.",
3512
+ subtask_prompt: "Review only the RAG package, frontend Markdown cases, and manifest.draft.json. Verify traceability, independent execution, safe data/environment handling, manifest correctness, session consistency, fixture/UI reset and fresh snapshot steps, and evidence requirements. Any Important or Critical finding requires VERDICT: request-revision. Browser execution is blocked unless this review passes.",
3513
+ }, {
3514
+ id: "revise-frontend-cases-pi",
3515
+ depends_on: ["review-frontend-cases-pi"],
3516
+ runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3517
+ role: "implementer",
3518
+ executor: "pi",
3519
+ toolProfile: "write",
3520
+ complexity: "HIGH",
3521
+ writePolicy: "exclusive",
3522
+ writeSet: casesWriteSet,
3523
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3524
+ forbiddenPaths: forbidden,
3525
+ outputContract: "Apply the one permitted frontend case revision under testcase/frontend/cases/** only; no browser execution or evidence writes.",
3526
+ subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/**, preserve traceable AC mappings, and do not execute a browser or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.",
3527
+ }, {
3528
+ id: "review-frontend-cases-final-pi",
3529
+ depends_on: ["revise-frontend-cases-pi"],
3530
+ runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3531
+ role: "reviewer",
3532
+ executor: "pi",
3533
+ complexity: "HIGH",
3534
+ writePolicy: "read-only",
3535
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3536
+ forbiddenPaths: forbidden,
3537
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision after the single allowed case revision; no writes.",
3538
+ subtask_prompt: "Perform the final frontend case review after the sole permitted revision. Apply the same traceability, isolation, manifest, reset, session, snapshot, and evidence checks. First verdict line must be exact; any Important or Critical finding requires request-revision. Do not write files.",
3539
+ }, {
3540
+ id: "final-frontend-case-review-gate-shell",
3541
+ depends_on: ["review-frontend-cases-pi", "review-frontend-cases-final-pi"],
3542
+ dependsPolicy: "all-or-condition-skip",
3543
+ role: "verifier",
3544
+ executor: "shell",
3545
+ complexity: "LOW",
3546
+ writePolicy: "read-only",
3547
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3548
+ forbiddenPaths: forbidden,
3549
+ outputContract: "Pass-only effective frontend case review gate; final review takes precedence when the revision branch ran.",
3550
+ subtask_prompt: "Authorize checklist/manifest materialization only after the effective frontend case review passes.",
3551
+ shell: {
3552
+ commands: [],
3553
+ verdictGate: {
3554
+ fromNodeId: "review-frontend-cases-final-pi",
3555
+ fallbackFromNodeIds: ["review-frontend-cases-pi"],
3556
+ accept: ["VERDICT: pass"],
3557
+ label: "effective frontend case review",
3558
+ lineMode: "first-verdict-line",
3559
+ },
3560
+ cwd: ".",
3561
+ timeoutMs: 60000,
3562
+ },
3563
+ });
3564
+ }
3565
+ const checklistDependsOn = blockingReview
3566
+ ? ["final-frontend-case-review-gate-shell"]
3567
+ : ["generate-frontend-functional-cases-pi"];
3568
+ tasks.push({
3569
+ id: "frontend-case-checklist-shell",
3570
+ depends_on: checklistDependsOn,
3571
+ role: "verifier",
3572
+ executor: "shell",
3573
+ complexity: "LOW",
3574
+ writePolicy: "read-only",
3575
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3576
+ forbiddenPaths: forbidden,
3577
+ outputContract: "Mechanical checklist: open-prefix, non-prod absolute URL, acIds, no pytest/playwright test source; emit structured ruleId issues on failure.",
3578
+ subtask_prompt: "Scan generated cases/manifest against the shared blocking checklist. Do not use free-form LLM verdicts.",
3579
+ shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
3580
+ }, {
3581
+ id: "materialize-frontend-case-manifest-shell",
3582
+ depends_on: ["frontend-case-checklist-shell"],
3583
+ role: "verifier",
3584
+ executor: "shell",
3585
+ complexity: "LOW",
3586
+ writePolicy: "exclusive",
3587
+ writeSet: casesWriteSet,
3588
+ allowedPaths: casesWriteSet,
3589
+ forbiddenPaths: forbidden,
3590
+ outputContract: "Validated frontend manifest payload { cases: [...] }; atomically materialize testcase/frontend/cases/manifest.json from manifest.draft.json; shell output may echo only the prefix before exactly one final JSON line.",
3591
+ subtask_prompt: "Validate manifest.draft.json and materialize manifest.json after the mechanical checklist (and optional blocking review) passes.",
3592
+ shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
3593
+ }, {
3594
+ id: "execute-frontend-cases-map",
3595
+ depends_on: ["materialize-frontend-case-manifest-shell"],
3596
+ role: "verifier",
3597
+ executor: "static",
3598
+ complexity: "LOW",
3599
+ writePolicy: "none",
3600
+ allowedPaths: [],
3601
+ forbiddenPaths: forbidden,
3602
+ outputContract: "Serial aggregate of case execution summaries, evidence paths, tokens, and token-budget or executor blocked/failed cases.",
3603
+ subtask_prompt: "Expand and execute the validated frontend case manifest serially. Child executor failures become case-level failed/blocked evidence so closeout can still run.",
3604
+ static: { resultMarkdown: "Frontend case map expansion barrier." },
3605
+ dynamicExpansion: {
3606
+ type: "map_agent",
3607
+ workflowNodeId: "execute-frontend-cases-map",
3608
+ itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases",
3609
+ itemName: "case",
3610
+ maxItems: config.maxCasesPerBatch,
3611
+ maxExpandedNodes: config.maxCasesPerBatch,
3612
+ childIdPrefix: "execute-frontend-case",
3613
+ workspaceTemplate: "{{case.evidenceDir}}",
3614
+ tolerateChildFailures: true,
3615
+ tokenBudget: {
3616
+ maxTokensPerCase: config.maxTokensPerCase,
3617
+ maxTotalTokens: config.maxTotalTokens,
3618
+ },
3619
+ childTask: {
3620
+ executor: "pi",
3621
+ role: "implementer",
3622
+ skills: ["playwright-cli"],
3623
+ toolProfile: "write",
3624
+ complexity: "MED",
3625
+ writePolicy: "exclusive",
3626
+ allowedPaths: [
3627
+ "testcase/frontend/cases/{{case.caseId}}.md",
3628
+ "testcase/frontend/rag/context.md",
3629
+ "testcase/frontend/rag/coverage-map.md",
3630
+ `${evidenceRoot}/{{case.caseId}}/**`,
3631
+ ],
3632
+ forbiddenPaths: forbidden,
3633
+ writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
3634
+ outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
3635
+ subtaskPromptTemplate: [
3636
+ "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). Prefer playwright-cli over prose review.",
3637
+ "1) Read baseUrl from testcase/frontend/rag/context.md (config.md preferred, else http://localhost:5173). 2) Start browser: playwright-cli open --browser=chrome --headed <resolved-base-url> (default session only; no -s=). 3) Follow the case steps with snapshot before element refs. 4) If env/CLI/baseUrl is unavailable, write blocked evidence and do not open a browser.",
3638
+ "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json (caseId, status passed|failed|blocked, evidencePaths; blocked needs blockedReason). Then validate: node -e \"const fs=require('fs');const p='{{case.evidenceDir}}';const r=JSON.parse(fs.readFileSync(p+'case-result.json','utf8'));if(!fs.existsSync(p+'execution.md')||r.caseId!=='{{case.caseId}}'||!['passed','failed','blocked'].includes(r.status)||!Array.isArray(r.evidencePaths)||(r.status==='blocked'&&!(typeof r.blockedReason==='string'&&r.blockedReason.trim())))process.exit(1)\".",
3639
+ "Business failed/blocked is a recorded result, not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
3640
+ ].join("\n\n"),
3641
+ },
3642
+ },
3643
+ }, {
3644
+ id: "validate-frontend-case-evidence-shell",
3645
+ depends_on: ["execute-frontend-cases-map"],
3646
+ role: "verifier",
3647
+ executor: "shell",
3648
+ complexity: "LOW",
3649
+ writePolicy: "read-only",
3650
+ allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3651
+ forbiddenPaths: forbidden,
3652
+ outputContract: "Deterministic validation that every manifest case has execution.md and valid matching case-result.json; blocked results require blockedReason.",
3653
+ subtask_prompt: "Validate all frontend case evidence before result materialization; fail closed on missing or malformed records.",
3654
+ shell: { commands: [evidenceValidation], cwd: ".", timeoutMs: 120000 },
3655
+ }, {
3656
+ id: "materialize-frontend-test-result-shell",
3657
+ depends_on: ["validate-frontend-case-evidence-shell"],
3658
+ role: "verifier",
3659
+ executor: "shell",
3660
+ complexity: "LOW",
3661
+ writePolicy: "read-only",
3662
+ allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3663
+ forbiddenPaths: forbidden,
3664
+ outputContract: "Run-owned hash-bound frontend-test-result-v1 derived only from the manifest and validated case evidence.",
3665
+ subtask_prompt: "Materialize the authoritative frontend-test-result-v1. Do not use Pi prose or retrospective output as input.",
3666
+ shell: {
3667
+ commands: [],
3668
+ jsonArtifactGate: {
3669
+ fromNodeId: "validate-frontend-case-evidence-shell",
3670
+ schemaId: "frontend-test-result-v1",
3671
+ artifactName: "frontend-test-result.json",
3672
+ outputDir: "contracts",
3673
+ },
3674
+ cwd: ".",
3675
+ timeoutMs: 120000,
3676
+ },
3677
+ });
3678
+ if (strictOutcomeGate) {
3679
+ tasks.push({
3680
+ id: "frontend-test-result-outcome-gate-shell",
3681
+ depends_on: ["materialize-frontend-test-result-shell"],
3682
+ role: "verifier",
3683
+ executor: "shell",
3684
+ complexity: "LOW",
3685
+ writePolicy: "read-only",
3686
+ allowedPaths: [],
3687
+ forbiddenPaths: forbidden,
3688
+ 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.",
3689
+ subtask_prompt: "Opt-in Delivery/Worker quality gate (frontendTest.strictOutcomeGate=true). Retrospective does not depend on this node.",
3690
+ shell: { commands: [frontendTestOutcomeGate], cwd: ".", timeoutMs: 60000 },
3691
+ });
3692
+ }
3693
+ tasks.push({
3694
+ id: "frontend-test-retrospect-pi",
3695
+ depends_on: ["materialize-frontend-test-result-shell"],
3696
+ role: "closeout",
3697
+ executor: "pi",
3698
+ toolProfile: "write",
3699
+ complexity: "MED",
3700
+ writePolicy: "exclusive",
3701
+ writeSet: ["testcase/frontend/reports/**"],
3702
+ allowedPaths: ["testcase/frontend/**"],
3703
+ forbiddenPaths: forbidden,
3704
+ 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).",
3705
+ subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, 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 replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
3706
+ });
3707
+ const globalConstraints = [
3708
+ ...sources.taskConfig.hardConstraints,
3709
+ ...STANDARD_GLOBAL_CONSTRAINTS,
3710
+ "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
3711
+ "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
3712
+ "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
3713
+ "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <resolved-base-url>; resolve baseUrl from task source config.md when present, otherwise default http://localhost:5173; generated operations stay in the default browser session and must not use unverified named-session flags.",
3714
+ "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
3715
+ "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.",
3716
+ blockingReview
3717
+ ? "frontendTest.reviewMode=blocking: a frontend case review must emit VERDICT: pass before checklist/manifest materialization; request-revision blocks browser execution."
3718
+ : "frontendTest.reviewMode is off|advisory by default: mechanical checklist-shell gates materialize/execute; LLM review is not a hard browser gate.",
3719
+ ];
3363
3720
  const spec = {
3364
3721
  version: 3,
3365
3722
  title: `Frontend test DAG: ${sources.taskConfig.title}`,
@@ -3367,16 +3724,7 @@ function buildFrontendTestHybridDag(sources) {
3367
3724
  outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3368
3725
  objective: extractObjective(sources.requirementMarkdown, sources.taskConfig.title),
3369
3726
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
3370
- globalConstraints: [
3371
- ...sources.taskConfig.hardConstraints,
3372
- ...STANDARD_GLOBAL_CONSTRAINTS,
3373
- "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
3374
- "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
3375
- "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
3376
- "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <resolved-base-url>; resolve baseUrl from task source config.md when present, otherwise default http://localhost:5173; generated operations stay in the default browser session and must not use unverified named-session flags.",
3377
- "A frontend case review must emit VERDICT: pass before manifest materialization; request-revision blocks browser execution.",
3378
- "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
3379
- ],
3727
+ globalConstraints,
3380
3728
  defaults: {
3381
3729
  ...HYBRID_DEFAULTS,
3382
3730
  skills: [],
@@ -3393,253 +3741,7 @@ function buildFrontendTestHybridDag(sources) {
3393
3741
  closeout: ["verification-before-completion"],
3394
3742
  },
3395
3743
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3396
- tasks: [
3397
- {
3398
- id: "retrieve-frontend-test-context-pi",
3399
- depends_on: [],
3400
- role: "planner",
3401
- executor: "pi",
3402
- toolProfile: "write",
3403
- complexity: "HIGH",
3404
- writePolicy: "exclusive",
3405
- writeSet: ragWriteSet,
3406
- allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
3407
- forbiddenPaths: forbidden,
3408
- outputContract: "Write testcase/frontend/rag/context.md and coverage-map.md with traceable UI/API/test-environment facts.",
3409
- subtask_prompt: [
3410
- "Build the frontend test RAG package.",
3411
- "Read task source, relevant routes/components/API or Mock facts, existing tests, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
3412
- "Record AC IDs, source paths, routes, states, roles, fixture/data prerequisites, API mapping status, risks, and isolated execution contract. Do not guess unavailable facts.",
3413
- "Base URL resolution (required): (1) Prefer an absolute http(s) frontend URL from task source config.md (source/references/**/config.md or any attached config.md), including keys baseUrl/base_url/frontendBaseUrl/FRONTEND_BASE_URL/url or labeled frontend base URL text. (2) If config.md has no usable absolute URL, default to http://localhost:5173. (3) Never use production hosts. (4) Write both a human-readable base URL line and machine-readable lines `baseUrl: <url>` and `baseUrlSource: config.md|<path>` or `baseUrlSource: default-localhost-5173`. (5) Include the exact browser start prefix with the resolved URL: playwright-cli open --browser=chrome --headed <resolved-base-url>.",
3414
- buildSourceContextBlock(sources),
3415
- ].join("\n\n"),
3416
- },
3417
- {
3418
- id: "materialize-frontend-test-execution-shell",
3419
- depends_on: ["retrieve-frontend-test-context-pi"],
3420
- role: "verifier",
3421
- executor: "shell",
3422
- complexity: "LOW",
3423
- writePolicy: "read-only",
3424
- allowedPaths: [...ragWriteSet],
3425
- forbiddenPaths: forbidden,
3426
- outputContract: "Fail-closed preflight: absolute non-production baseUrl required; fixture/reset not hard-gated.",
3427
- subtask_prompt: "Hard-validate only an absolute non-production baseUrl in RAG context (from config.md or default http://localhost:5173). Fixture/reset and other isolation details are soft guidance for later nodes, not preflight failures.",
3428
- shell: { commands: [["node -e", JSON.stringify("const fs=require('fs'); const p='testcase/frontend/rag/context.md'; if(!fs.existsSync(p))throw new Error('missing '+p); const s=fs.readFileSync(p,'utf8'); const patterns=[ /baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i, /base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i, /playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i, /(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\]},\"']*)/i ]; let baseUrl=null; for(const re of patterns){const m=s.match(re); if(m){baseUrl=m[1]; break;}} if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)'); baseUrl=baseUrl.replace(/[)\\]},.\"']+$/,''); if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl); if(/(?:^|\\/\\/)(?:www\\.)?[^\\s/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl); console.log('frontend-test-execution-v1 validated baseUrl='+baseUrl);")].join(" ")], cwd: ".", timeoutMs: 60000 },
3429
- },
3430
- {
3431
- id: "generate-frontend-functional-cases-pi",
3432
- depends_on: ["materialize-frontend-test-execution-shell"],
3433
- role: "implementer",
3434
- executor: "pi",
3435
- toolProfile: "write",
3436
- complexity: "HIGH",
3437
- writePolicy: "exclusive",
3438
- writeSet: casesWriteSet,
3439
- allowedPaths: [...ragWriteSet, ...casesWriteSet],
3440
- forbiddenPaths: forbidden,
3441
- outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1; no test source code.",
3442
- subtask_prompt: [
3443
- "Use skill playwright-cli-case-generator.",
3444
- "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
3445
- "Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.",
3446
- "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Copy the resolved absolute baseUrl from context.md (baseUrl field; resolved from config.md or default http://localhost:5173). Every browser start command must be: playwright-cli open --browser=chrome --headed <resolved-base-url-from-context.md> with that concrete URL — never leave a <base-url> placeholder. Use the same default browser session for every subsequent command; never write -s=<case-id> or assume named-session binding.",
3447
- "Each case must be independently reproducible: for every executable sub-scenario state fixture/reset, UI reset, a fresh snapshot before references are used, exact evidence write point, preconditions/data cleanup, UI assertions, and evidence paths under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
3448
- ].join("\n\n"),
3449
- },
3450
- {
3451
- id: "review-frontend-cases-pi",
3452
- depends_on: ["generate-frontend-functional-cases-pi"],
3453
- role: "reviewer",
3454
- executor: "pi",
3455
- complexity: "HIGH",
3456
- writePolicy: "read-only",
3457
- allowedPaths: [...ragWriteSet, ...casesWriteSet],
3458
- forbiddenPaths: forbidden,
3459
- outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes. request-revision blocks manifest materialization.",
3460
- subtask_prompt: "Review only the RAG package, frontend Markdown cases, and manifest.draft.json. Verify traceability, independent execution, safe data/environment handling, manifest correctness, session consistency, fixture/UI reset and fresh snapshot steps, and evidence requirements. Any Important or Critical finding requires VERDICT: request-revision. Browser execution is blocked unless this review passes.",
3461
- },
3462
- {
3463
- id: "revise-frontend-cases-pi",
3464
- depends_on: ["review-frontend-cases-pi"],
3465
- runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3466
- role: "implementer",
3467
- executor: "pi",
3468
- toolProfile: "write",
3469
- complexity: "HIGH",
3470
- writePolicy: "exclusive",
3471
- writeSet: casesWriteSet,
3472
- allowedPaths: [...ragWriteSet, ...casesWriteSet],
3473
- forbiddenPaths: forbidden,
3474
- outputContract: "Apply the one permitted frontend case revision under testcase/frontend/cases/** only; no browser execution or evidence writes.",
3475
- subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/**, preserve traceable AC mappings, and do not execute a browser or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.",
3476
- },
3477
- {
3478
- id: "review-frontend-cases-final-pi",
3479
- depends_on: ["revise-frontend-cases-pi"],
3480
- runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3481
- role: "reviewer",
3482
- executor: "pi",
3483
- complexity: "HIGH",
3484
- writePolicy: "read-only",
3485
- allowedPaths: [...ragWriteSet, ...casesWriteSet],
3486
- forbiddenPaths: forbidden,
3487
- outputContract: "First line VERDICT: pass or VERDICT: request-revision after the single allowed case revision; no writes.",
3488
- subtask_prompt: "Perform the final frontend case review after the sole permitted revision. Apply the same traceability, isolation, manifest, reset, session, snapshot, and evidence checks. First verdict line must be exact; any Important or Critical finding requires request-revision. Do not write files.",
3489
- },
3490
- {
3491
- id: "final-frontend-case-review-gate-shell",
3492
- depends_on: ["review-frontend-cases-pi", "review-frontend-cases-final-pi"],
3493
- dependsPolicy: "all-or-condition-skip",
3494
- role: "verifier",
3495
- executor: "shell",
3496
- complexity: "LOW",
3497
- writePolicy: "read-only",
3498
- allowedPaths: [...ragWriteSet, ...casesWriteSet],
3499
- forbiddenPaths: forbidden,
3500
- outputContract: "Pass-only effective frontend case review gate; final review takes precedence when the revision branch ran.",
3501
- subtask_prompt: "Authorize manifest materialization only after the effective frontend case review passes.",
3502
- shell: {
3503
- commands: [],
3504
- verdictGate: {
3505
- fromNodeId: "review-frontend-cases-final-pi",
3506
- fallbackFromNodeIds: ["review-frontend-cases-pi"],
3507
- accept: ["VERDICT: pass"],
3508
- label: "effective frontend case review",
3509
- lineMode: "first-verdict-line",
3510
- },
3511
- cwd: ".",
3512
- timeoutMs: 60000,
3513
- },
3514
- },
3515
- {
3516
- id: "materialize-frontend-case-manifest-shell",
3517
- depends_on: ["final-frontend-case-review-gate-shell"],
3518
- role: "verifier",
3519
- executor: "shell",
3520
- complexity: "LOW",
3521
- writePolicy: "exclusive",
3522
- writeSet: casesWriteSet,
3523
- allowedPaths: casesWriteSet,
3524
- forbiddenPaths: forbidden,
3525
- outputContract: "Validated frontend manifest payload { cases: [...] }; after the review gate, atomically materialize testcase/frontend/cases/manifest.json from manifest.draft.json; shell output may echo only the prefix before exactly one final JSON line.",
3526
- subtask_prompt: "Validate manifest.draft.json and materialize manifest.json only after the effective frontend case review has passed.",
3527
- shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
3528
- },
3529
- {
3530
- id: "execute-frontend-cases-map",
3531
- depends_on: ["materialize-frontend-case-manifest-shell"],
3532
- role: "verifier",
3533
- executor: "static",
3534
- complexity: "LOW",
3535
- writePolicy: "none",
3536
- allowedPaths: [],
3537
- forbiddenPaths: forbidden,
3538
- outputContract: "Serial aggregate of case execution summaries, evidence paths, tokens, and token-budget blocked cases.",
3539
- subtask_prompt: "Expand and execute the validated frontend case manifest serially.",
3540
- static: { resultMarkdown: "Frontend case map expansion barrier." },
3541
- dynamicExpansion: {
3542
- type: "map_agent",
3543
- workflowNodeId: "execute-frontend-cases-map",
3544
- itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases",
3545
- itemName: "case",
3546
- maxItems: config.maxCasesPerBatch,
3547
- maxExpandedNodes: config.maxCasesPerBatch,
3548
- childIdPrefix: "execute-frontend-case",
3549
- workspaceTemplate: "{{case.evidenceDir}}",
3550
- tokenBudget: {
3551
- maxTokensPerCase: config.maxTokensPerCase,
3552
- maxTotalTokens: config.maxTotalTokens,
3553
- },
3554
- childTask: {
3555
- executor: "pi",
3556
- role: "implementer",
3557
- skills: ["playwright-cli"],
3558
- toolProfile: "write",
3559
- complexity: "MED",
3560
- writePolicy: "exclusive",
3561
- allowedPaths: [
3562
- "testcase/frontend/cases/{{case.caseId}}.md",
3563
- "testcase/frontend/rag/context.md",
3564
- "testcase/frontend/rag/coverage-map.md",
3565
- `${evidenceRoot}/{{case.caseId}}/**`,
3566
- ],
3567
- forbiddenPaths: forbidden,
3568
- writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
3569
- outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
3570
- subtaskPromptTemplate: [
3571
- "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). Prefer playwright-cli over prose review.",
3572
- "1) Read baseUrl from testcase/frontend/rag/context.md (config.md preferred, else http://localhost:5173). 2) Start browser: playwright-cli open --browser=chrome --headed <resolved-base-url> (default session only; no -s=). 3) Follow the case steps with snapshot before element refs. 4) If env/CLI/baseUrl is unavailable, write blocked evidence and do not open a browser.",
3573
- "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json (caseId, status passed|failed|blocked, evidencePaths; blocked needs blockedReason). Then validate: node -e \"const fs=require('fs');const p='{{case.evidenceDir}}';const r=JSON.parse(fs.readFileSync(p+'case-result.json','utf8'));if(!fs.existsSync(p+'execution.md')||r.caseId!=='{{case.caseId}}'||!['passed','failed','blocked'].includes(r.status)||!Array.isArray(r.evidencePaths)||(r.status==='blocked'&&!(typeof r.blockedReason==='string'&&r.blockedReason.trim())))process.exit(1)\".",
3574
- "Business failed/blocked is a recorded result, not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
3575
- ].join("\n\n"),
3576
- },
3577
- },
3578
- },
3579
- {
3580
- id: "validate-frontend-case-evidence-shell",
3581
- depends_on: ["execute-frontend-cases-map"],
3582
- role: "verifier",
3583
- executor: "shell",
3584
- complexity: "LOW",
3585
- writePolicy: "read-only",
3586
- allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3587
- forbiddenPaths: forbidden,
3588
- outputContract: "Deterministic validation that every manifest case has execution.md and valid matching case-result.json; blocked results require blockedReason.",
3589
- subtask_prompt: "Validate all frontend case evidence before evidence review; fail closed on missing or malformed records.",
3590
- shell: { commands: [evidenceValidation], cwd: ".", timeoutMs: 120000 },
3591
- },
3592
- {
3593
- id: "materialize-frontend-test-result-shell",
3594
- depends_on: ["validate-frontend-case-evidence-shell"],
3595
- role: "verifier",
3596
- executor: "shell",
3597
- complexity: "LOW",
3598
- writePolicy: "read-only",
3599
- allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3600
- forbiddenPaths: forbidden,
3601
- outputContract: "Run-owned hash-bound frontend-test-result-v1 derived only from the manifest and validated case evidence.",
3602
- subtask_prompt: "Materialize the authoritative frontend-test-result-v1. Do not use Pi prose or retrospective output as input.",
3603
- shell: {
3604
- commands: [],
3605
- jsonArtifactGate: {
3606
- fromNodeId: "validate-frontend-case-evidence-shell",
3607
- schemaId: "frontend-test-result-v1",
3608
- artifactName: "frontend-test-result.json",
3609
- outputDir: "contracts",
3610
- },
3611
- cwd: ".",
3612
- timeoutMs: 120000,
3613
- },
3614
- },
3615
- {
3616
- id: "frontend-test-result-outcome-gate-shell",
3617
- depends_on: ["materialize-frontend-test-result-shell"],
3618
- role: "verifier",
3619
- executor: "shell",
3620
- complexity: "LOW",
3621
- writePolicy: "read-only",
3622
- allowedPaths: [],
3623
- forbiddenPaths: forbidden,
3624
- outputContract: "Pass only when the run-owned frontend-test-result-v1 records outcome=passed and integrationMode=real. Does not gate retrospective closeout.",
3625
- subtask_prompt: "Delivery/Worker gate for authoritative frontend-test result. Retrospective does not depend on this node so failed runs can still write reports.",
3626
- shell: { commands: [frontendTestOutcomeGate], cwd: ".", timeoutMs: 60000 },
3627
- },
3628
- {
3629
- id: "frontend-test-retrospect-pi",
3630
- depends_on: ["materialize-frontend-test-result-shell"],
3631
- role: "closeout",
3632
- executor: "pi",
3633
- toolProfile: "write",
3634
- complexity: "MED",
3635
- writePolicy: "exclusive",
3636
- writeSet: ["testcase/frontend/reports/**"],
3637
- allowedPaths: ["testcase/frontend/**"],
3638
- forbiddenPaths: forbidden,
3639
- 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.",
3640
- subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted), 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 replace browser evidence with model conclusions. Do not write docs/**.",
3641
- },
3642
- ],
3744
+ tasks,
3643
3745
  };
3644
3746
  applyDefaultReadOnlyRetryPolicy(spec);
3645
3747
  parseDagSpec(spec);