@tea-agent/loop-agent 0.16.22 → 0.16.24

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.",
@@ -3312,7 +3313,17 @@ function buildBackendTestHybridDag(sources) {
3312
3313
  // Frontend browser-test RAG DAG template
3313
3314
  // ---------------------------------------------------------------------------
3314
3315
  function buildFrontendTestHybridDag(sources) {
3315
- const config = sources.taskConfig.frontendTest ?? { maxCasesPerBatch: 20 };
3316
+ const rawFrontendTest = sources.taskConfig.frontendTest;
3317
+ const config = {
3318
+ maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 20,
3319
+ maxTokensPerCase: rawFrontendTest?.maxTokensPerCase,
3320
+ maxTotalTokens: rawFrontendTest?.maxTotalTokens,
3321
+ reviewMode: rawFrontendTest?.reviewMode ?? "off",
3322
+ strictOutcomeGate: rawFrontendTest?.strictOutcomeGate === true,
3323
+ };
3324
+ const reviewMode = config.reviewMode;
3325
+ const blockingReview = reviewMode === "blocking";
3326
+ const strictOutcomeGate = config.strictOutcomeGate;
3316
3327
  const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
3317
3328
  pattern === "testcase/**" ||
3318
3329
  pattern === "**");
@@ -3323,6 +3334,36 @@ function buildFrontendTestHybridDag(sources) {
3323
3334
  const ragWriteSet = ["testcase/frontend/rag/**"];
3324
3335
  const casesWriteSet = ["testcase/frontend/cases/**"];
3325
3336
  const evidenceRoot = "testcase/frontend/evidence";
3337
+ const checklistValidation = [
3338
+ "node -e",
3339
+ JSON.stringify([
3340
+ "const fs=require('fs'),path=require('path');",
3341
+ "const root='testcase/frontend/cases';",
3342
+ "const draft=path.join(root,'manifest.draft.json');",
3343
+ "const final=path.join(root,'manifest.json');",
3344
+ "const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
3345
+ "if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
3346
+ "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
3347
+ "if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
3348
+ "const issues=[];",
3349
+ "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
3350
+ "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
3351
+ "const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
3352
+ "for(const c of manifest.cases){",
3353
+ " const id=c&&c.caseId||'?';",
3354
+ " const casePath=typeof c.casePath==='string'?c.casePath:null;",
3355
+ " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
3356
+ " const body=fs.readFileSync(casePath,'utf8');",
3357
+ " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
3358
+ " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
3359
+ " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
3360
+ " if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
3361
+ " if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required'});",
3362
+ "}",
3363
+ "if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
3364
+ "console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
3365
+ ].join("")),
3366
+ ].join(" ");
3326
3367
  const manifestValidation = [
3327
3368
  "node -e",
3328
3369
  JSON.stringify([
@@ -3338,7 +3379,6 @@ function buildFrontendTestHybridDag(sources) {
3338
3379
  " if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) throw new Error('invalid acIds');",
3339
3380
  " 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
3381
  " 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
3382
  " { 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
3383
  " if(!fs.existsSync(c.casePath)) throw new Error('missing case file '+c.casePath);",
3344
3384
  " if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
@@ -3360,6 +3400,290 @@ function buildFrontendTestHybridDag(sources) {
3360
3400
  ].join("")),
3361
3401
  ].join(" ");
3362
3402
  const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
3403
+ const tasks = [
3404
+ {
3405
+ id: "retrieve-frontend-test-context-pi",
3406
+ depends_on: [],
3407
+ role: "planner",
3408
+ executor: "pi",
3409
+ toolProfile: "write",
3410
+ complexity: "MED",
3411
+ writePolicy: "exclusive",
3412
+ writeSet: ragWriteSet,
3413
+ allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
3414
+ forbiddenPaths: forbidden,
3415
+ outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with machine-readable baseUrl and capability notes.",
3416
+ subtask_prompt: [
3417
+ "Build the frontend test RAG package (keep it short).",
3418
+ "Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
3419
+ "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.",
3420
+ "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>.",
3421
+ buildSourceContextBlock(sources),
3422
+ ].join("\n\n"),
3423
+ },
3424
+ {
3425
+ id: "materialize-frontend-test-execution-shell",
3426
+ depends_on: ["retrieve-frontend-test-context-pi"],
3427
+ role: "verifier",
3428
+ executor: "shell",
3429
+ complexity: "LOW",
3430
+ writePolicy: "read-only",
3431
+ allowedPaths: [...ragWriteSet],
3432
+ forbiddenPaths: forbidden,
3433
+ outputContract: "Fail-closed preflight: absolute non-production baseUrl required; fixture/reset not hard-gated.",
3434
+ 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.",
3435
+ shell: {
3436
+ commands: [
3437
+ [
3438
+ "node -e",
3439
+ 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);"),
3440
+ ].join(" "),
3441
+ ],
3442
+ cwd: ".",
3443
+ timeoutMs: 60000,
3444
+ },
3445
+ },
3446
+ {
3447
+ id: "generate-frontend-functional-cases-pi",
3448
+ depends_on: ["materialize-frontend-test-execution-shell"],
3449
+ role: "implementer",
3450
+ executor: "pi",
3451
+ toolProfile: "write",
3452
+ complexity: "HIGH",
3453
+ writePolicy: "exclusive",
3454
+ writeSet: casesWriteSet,
3455
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3456
+ forbiddenPaths: forbidden,
3457
+ outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1; no test source code.",
3458
+ subtask_prompt: [
3459
+ "Use skill playwright-cli-case-generator.",
3460
+ "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
3461
+ "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.",
3462
+ "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.",
3463
+ "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>.",
3464
+ "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.",
3465
+ ].join("\n\n"),
3466
+ },
3467
+ ];
3468
+ if (blockingReview) {
3469
+ tasks.push({
3470
+ id: "review-frontend-cases-pi",
3471
+ depends_on: ["generate-frontend-functional-cases-pi"],
3472
+ role: "reviewer",
3473
+ executor: "pi",
3474
+ complexity: "HIGH",
3475
+ writePolicy: "read-only",
3476
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3477
+ forbiddenPaths: forbidden,
3478
+ 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.",
3479
+ 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.",
3480
+ }, {
3481
+ id: "revise-frontend-cases-pi",
3482
+ depends_on: ["review-frontend-cases-pi"],
3483
+ runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3484
+ role: "implementer",
3485
+ executor: "pi",
3486
+ toolProfile: "write",
3487
+ complexity: "HIGH",
3488
+ writePolicy: "exclusive",
3489
+ writeSet: casesWriteSet,
3490
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3491
+ forbiddenPaths: forbidden,
3492
+ outputContract: "Apply the one permitted frontend case revision under testcase/frontend/cases/** only; no browser execution or evidence writes.",
3493
+ 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.",
3494
+ }, {
3495
+ id: "review-frontend-cases-final-pi",
3496
+ depends_on: ["revise-frontend-cases-pi"],
3497
+ runIf: "$.nodes['review-frontend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3498
+ role: "reviewer",
3499
+ executor: "pi",
3500
+ complexity: "HIGH",
3501
+ writePolicy: "read-only",
3502
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3503
+ forbiddenPaths: forbidden,
3504
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision after the single allowed case revision; no writes.",
3505
+ 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.",
3506
+ }, {
3507
+ id: "final-frontend-case-review-gate-shell",
3508
+ depends_on: ["review-frontend-cases-pi", "review-frontend-cases-final-pi"],
3509
+ dependsPolicy: "all-or-condition-skip",
3510
+ role: "verifier",
3511
+ executor: "shell",
3512
+ complexity: "LOW",
3513
+ writePolicy: "read-only",
3514
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3515
+ forbiddenPaths: forbidden,
3516
+ outputContract: "Pass-only effective frontend case review gate; final review takes precedence when the revision branch ran.",
3517
+ subtask_prompt: "Authorize checklist/manifest materialization only after the effective frontend case review passes.",
3518
+ shell: {
3519
+ commands: [],
3520
+ verdictGate: {
3521
+ fromNodeId: "review-frontend-cases-final-pi",
3522
+ fallbackFromNodeIds: ["review-frontend-cases-pi"],
3523
+ accept: ["VERDICT: pass"],
3524
+ label: "effective frontend case review",
3525
+ lineMode: "first-verdict-line",
3526
+ },
3527
+ cwd: ".",
3528
+ timeoutMs: 60000,
3529
+ },
3530
+ });
3531
+ }
3532
+ const checklistDependsOn = blockingReview
3533
+ ? ["final-frontend-case-review-gate-shell"]
3534
+ : ["generate-frontend-functional-cases-pi"];
3535
+ tasks.push({
3536
+ id: "frontend-case-checklist-shell",
3537
+ depends_on: checklistDependsOn,
3538
+ role: "verifier",
3539
+ executor: "shell",
3540
+ complexity: "LOW",
3541
+ writePolicy: "read-only",
3542
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3543
+ forbiddenPaths: forbidden,
3544
+ outputContract: "Mechanical checklist: open-prefix, non-prod absolute URL, acIds, no pytest/playwright test source; emit structured ruleId issues on failure.",
3545
+ subtask_prompt: "Scan generated cases/manifest against the shared blocking checklist. Do not use free-form LLM verdicts.",
3546
+ shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
3547
+ }, {
3548
+ id: "materialize-frontend-case-manifest-shell",
3549
+ depends_on: ["frontend-case-checklist-shell"],
3550
+ role: "verifier",
3551
+ executor: "shell",
3552
+ complexity: "LOW",
3553
+ writePolicy: "exclusive",
3554
+ writeSet: casesWriteSet,
3555
+ allowedPaths: casesWriteSet,
3556
+ forbiddenPaths: forbidden,
3557
+ 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.",
3558
+ subtask_prompt: "Validate manifest.draft.json and materialize manifest.json after the mechanical checklist (and optional blocking review) passes.",
3559
+ shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
3560
+ }, {
3561
+ id: "execute-frontend-cases-map",
3562
+ depends_on: ["materialize-frontend-case-manifest-shell"],
3563
+ role: "verifier",
3564
+ executor: "static",
3565
+ complexity: "LOW",
3566
+ writePolicy: "none",
3567
+ allowedPaths: [],
3568
+ forbiddenPaths: forbidden,
3569
+ outputContract: "Serial aggregate of case execution summaries, evidence paths, tokens, and token-budget or executor blocked/failed cases.",
3570
+ 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.",
3571
+ static: { resultMarkdown: "Frontend case map expansion barrier." },
3572
+ dynamicExpansion: {
3573
+ type: "map_agent",
3574
+ workflowNodeId: "execute-frontend-cases-map",
3575
+ itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases",
3576
+ itemName: "case",
3577
+ maxItems: config.maxCasesPerBatch,
3578
+ maxExpandedNodes: config.maxCasesPerBatch,
3579
+ childIdPrefix: "execute-frontend-case",
3580
+ workspaceTemplate: "{{case.evidenceDir}}",
3581
+ tolerateChildFailures: true,
3582
+ tokenBudget: {
3583
+ maxTokensPerCase: config.maxTokensPerCase,
3584
+ maxTotalTokens: config.maxTotalTokens,
3585
+ },
3586
+ childTask: {
3587
+ executor: "pi",
3588
+ role: "implementer",
3589
+ skills: ["playwright-cli"],
3590
+ toolProfile: "write",
3591
+ complexity: "MED",
3592
+ writePolicy: "exclusive",
3593
+ allowedPaths: [
3594
+ "testcase/frontend/cases/{{case.caseId}}.md",
3595
+ "testcase/frontend/rag/context.md",
3596
+ "testcase/frontend/rag/coverage-map.md",
3597
+ `${evidenceRoot}/{{case.caseId}}/**`,
3598
+ ],
3599
+ forbiddenPaths: forbidden,
3600
+ writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
3601
+ outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
3602
+ subtaskPromptTemplate: [
3603
+ "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.",
3604
+ "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.",
3605
+ "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)\".",
3606
+ "Business failed/blocked is a recorded result, not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
3607
+ ].join("\n\n"),
3608
+ },
3609
+ },
3610
+ }, {
3611
+ id: "validate-frontend-case-evidence-shell",
3612
+ depends_on: ["execute-frontend-cases-map"],
3613
+ role: "verifier",
3614
+ executor: "shell",
3615
+ complexity: "LOW",
3616
+ writePolicy: "read-only",
3617
+ allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3618
+ forbiddenPaths: forbidden,
3619
+ outputContract: "Deterministic validation that every manifest case has execution.md and valid matching case-result.json; blocked results require blockedReason.",
3620
+ subtask_prompt: "Validate all frontend case evidence before result materialization; fail closed on missing or malformed records.",
3621
+ shell: { commands: [evidenceValidation], cwd: ".", timeoutMs: 120000 },
3622
+ }, {
3623
+ id: "materialize-frontend-test-result-shell",
3624
+ depends_on: ["validate-frontend-case-evidence-shell"],
3625
+ role: "verifier",
3626
+ executor: "shell",
3627
+ complexity: "LOW",
3628
+ writePolicy: "read-only",
3629
+ allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
3630
+ forbiddenPaths: forbidden,
3631
+ outputContract: "Run-owned hash-bound frontend-test-result-v1 derived only from the manifest and validated case evidence.",
3632
+ subtask_prompt: "Materialize the authoritative frontend-test-result-v1. Do not use Pi prose or retrospective output as input.",
3633
+ shell: {
3634
+ commands: [],
3635
+ jsonArtifactGate: {
3636
+ fromNodeId: "validate-frontend-case-evidence-shell",
3637
+ schemaId: "frontend-test-result-v1",
3638
+ artifactName: "frontend-test-result.json",
3639
+ outputDir: "contracts",
3640
+ },
3641
+ cwd: ".",
3642
+ timeoutMs: 120000,
3643
+ },
3644
+ });
3645
+ if (strictOutcomeGate) {
3646
+ tasks.push({
3647
+ id: "frontend-test-result-outcome-gate-shell",
3648
+ depends_on: ["materialize-frontend-test-result-shell"],
3649
+ role: "verifier",
3650
+ executor: "shell",
3651
+ complexity: "LOW",
3652
+ writePolicy: "read-only",
3653
+ allowedPaths: [],
3654
+ forbiddenPaths: forbidden,
3655
+ 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.",
3656
+ subtask_prompt: "Opt-in Delivery/Worker quality gate (frontendTest.strictOutcomeGate=true). Retrospective does not depend on this node.",
3657
+ shell: { commands: [frontendTestOutcomeGate], cwd: ".", timeoutMs: 60000 },
3658
+ });
3659
+ }
3660
+ tasks.push({
3661
+ id: "frontend-test-retrospect-pi",
3662
+ depends_on: ["materialize-frontend-test-result-shell"],
3663
+ role: "closeout",
3664
+ executor: "pi",
3665
+ toolProfile: "write",
3666
+ complexity: "MED",
3667
+ writePolicy: "exclusive",
3668
+ writeSet: ["testcase/frontend/reports/**"],
3669
+ allowedPaths: ["testcase/frontend/**"],
3670
+ forbiddenPaths: forbidden,
3671
+ 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).",
3672
+ 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.",
3673
+ });
3674
+ const globalConstraints = [
3675
+ ...sources.taskConfig.hardConstraints,
3676
+ ...STANDARD_GLOBAL_CONSTRAINTS,
3677
+ "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
3678
+ "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
3679
+ "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
3680
+ "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.",
3681
+ "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
3682
+ "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.",
3683
+ blockingReview
3684
+ ? "frontendTest.reviewMode=blocking: a frontend case review must emit VERDICT: pass before checklist/manifest materialization; request-revision blocks browser execution."
3685
+ : "frontendTest.reviewMode is off|advisory by default: mechanical checklist-shell gates materialize/execute; LLM review is not a hard browser gate.",
3686
+ ];
3363
3687
  const spec = {
3364
3688
  version: 3,
3365
3689
  title: `Frontend test DAG: ${sources.taskConfig.title}`,
@@ -3367,16 +3691,7 @@ function buildFrontendTestHybridDag(sources) {
3367
3691
  outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3368
3692
  objective: extractObjective(sources.requirementMarkdown, sources.taskConfig.title),
3369
3693
  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
- ],
3694
+ globalConstraints,
3380
3695
  defaults: {
3381
3696
  ...HYBRID_DEFAULTS,
3382
3697
  skills: [],
@@ -3393,253 +3708,7 @@ function buildFrontendTestHybridDag(sources) {
3393
3708
  closeout: ["verification-before-completion"],
3394
3709
  },
3395
3710
  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
- ],
3711
+ tasks,
3643
3712
  };
3644
3713
  applyDefaultReadOnlyRetryPolicy(spec);
3645
3714
  parseDagSpec(spec);