@tea-agent/loop-agent 0.33.4 → 0.33.5

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.
@@ -3508,7 +3508,8 @@ const readme=fs.existsSync('testcase/md/README.md')?fs.readFileSync('testcase/md
3508
3508
  const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
3509
3509
  const bt=String.fromCharCode(96);
3510
3510
  const stripBackticks=s=>s.split(bt).join('');
3511
- const valid=raw=>{const st=norm(raw);if(st==='readme')return false;if(!/^[a-z][a-z0-9_]*$/.test(st))return false;if(/^(?:be|tp|ac|req|br)[_-]/i.test(st))return false;return true;};
3511
+ const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(st==='readme')return 'reserved-module-stem';if(!/^[a-z][a-z0-9_]*$/.test(st))return 'invalid-syntax';if(/^(?:be|tp|ac|req|br)[_-]/i.test(st))return 'case-like-module-stem';return null;};
3512
+ const valid=raw=>invalidReason(raw)===null;
3512
3513
  const rxMdPath=/testcase\\/md\\/([A-Za-z0-9_.-]+)\\.md/g;
3513
3514
  const rxTableRow=/\\|\\s*([A-Za-z0-9_.-]+)\\s*\\|\\s*testcase\\/test_/g;
3514
3515
  const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
@@ -3516,12 +3517,14 @@ const raw=[];
3516
3517
  const lines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n').filter(l=>l.includes('|'));
3517
3518
  for(const line of lines){
3518
3519
  const bare=stripBackticks(line);
3519
- for(const m of bare.matchAll(rxMdPath)){if(valid(m[1]))raw.push(m[1]);}
3520
- for(const m of bare.matchAll(rxTableRow)){if(valid(m[1]))raw.push(m[1]);}
3520
+ for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
3521
+ for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])==='priority-only-module-stem')raw.push(m[1]);}
3521
3522
  }
3522
- for(const m of readme.matchAll(rxRelLink)){if(valid(m[1]))raw.push(m[1]);}
3523
+ for(const m of readme.matchAll(rxRelLink)){raw.push(m[1]);}
3524
+ const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
3525
+ if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
3523
3526
  const seen=new Set();const modules=[];
3524
- for(const r of raw){const st=norm(r);if(!seen.has(st)){seen.add(st);modules.push({stem:st});}}
3527
+ for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
3525
3528
  process.stdout.write(JSON.stringify({modules}));
3526
3529
  `;
3527
3530
  const encoded = Buffer.from(script, "utf8").toString("base64");
@@ -3599,7 +3602,7 @@ async function buildBackendTestHybridDag(sources) {
3599
3602
  "Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
3600
3603
  "Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
3601
3604
  "For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
3602
- "Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output budget, and keep the total module count at the smallest safe value. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
3605
+ "Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output budget, and keep the total module count at the smallest safe value. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
3603
3606
  "Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
3604
3607
  intake.boundedSourceContext,
3605
3608
  "## Authoritative reference index",
@@ -3670,7 +3673,7 @@ async function buildBackendTestHybridDag(sources) {
3670
3673
  "The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.",
3671
3674
  "Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
3672
3675
  'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.',
3673
- "Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
3676
+ "Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
3674
3677
  "Every Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
3675
3678
  "In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
3676
3679
  intake.boundedSourceContext,
@@ -3939,143 +3942,9 @@ async function buildBackendTestHybridDag(sources) {
3939
3942
  assertValidDagSpec(spec);
3940
3943
  return spec;
3941
3944
  }
3942
- // Explicit allowlist of controller-owned reference filenames that may declare
3943
- // a non-production browser URL. Hash-bound task sources are the only authority;
3944
- // model-authored RAG/case/evidence files are never consulted.
3945
- const FRONTEND_BASE_URL_DOCUMENT_ALLOWLIST = [
3946
- /(?:^|[\\/])config\.md$/i,
3947
- /(?:^|[\\/])environment\.md$/i,
3948
- /(?:^|[\\/])env\.md$/i,
3949
- /(?:^|[\\/])urls?\.md$/i,
3950
- /(?:^|[\\/])references\.md$/i,
3951
- /(?:^|[\\/])task-reference\.md$/i,
3952
- /(?:^|[\\/])task-source\.md$/i,
3953
- ];
3954
- // Explicit allowlist of URL keys. The semantic name drives a deterministic
3955
- // priority so two conflicting same-priority URLs fail closed instead of being
3956
- // silently picked.
3957
- const FRONTEND_BASE_URL_KEY_ALLOWLIST = [
3958
- // The primary frontend browser entry point is the strongest signal.
3959
- { name: "baseurl", priority: 0 },
3960
- { name: "base url", priority: 0 },
3961
- { name: "targeturl", priority: 0 },
3962
- { name: "target url", priority: 0 },
3963
- { name: "frontendbaseurl", priority: 1 },
3964
- { name: "frontend base url", priority: 1 },
3965
- { name: "frontendurl", priority: 1 },
3966
- { name: "frontend url", priority: 1 },
3967
- { name: "loginurl", priority: 2 },
3968
- { name: "login url", priority: 2 },
3969
- ];
3970
- const DEFAULT_FRONTEND_BASE_URL = "http://localhost:5173/";
3971
- const DEFAULT_FRONTEND_BASE_URL_SOURCE = "default-localhost-5173";
3972
- function isAllowedFrontendBaseUrlDocument(documentPath) {
3973
- return FRONTEND_BASE_URL_DOCUMENT_ALLOWLIST.some((pattern) => pattern.test(documentPath));
3974
- }
3975
- function normalizeFrontendBaseUrlKey(raw) {
3976
- // Normalize separators: kebab/snake/space variants collapse to a comparable
3977
- // token. Lowercase so casing differences do not bypass the allowlist.
3978
- return raw
3979
- .toLowerCase()
3980
- .replace(/[_\-]+/g, " ")
3981
- .replace(/\s+/g, " ")
3982
- .trim();
3983
- }
3984
- /**
3985
- * Resolve the browser origin only from controller-owned task source bytes.
3986
- * Model-authored RAG/case/evidence files are intentionally excluded.
3987
- *
3988
- * Safety contract:
3989
- * - Only hash-bound controller reference documents on the filename allowlist
3990
- * may declare a URL.
3991
- * - Only the explicit key allowlist (baseUrl / targetUrl / loginUrl + common
3992
- * case/separator variants) is recognized.
3993
- * - Candidates are ranked deterministically by key priority then by document
3994
- * order; equal-priority conflicting URLs fail closed.
3995
- * - The resolved URL must be non-production http(s) without credentials, query,
3996
- * or fragment. Only when zero candidates exist does the localhost default
3997
- * apply.
3998
- */
3999
- export function resolveControllerFrontendBaseUrl(sources) {
4000
- const allowedKeyByNormalized = new Map(FRONTEND_BASE_URL_KEY_ALLOWLIST.map((variant) => [
4001
- variant.name,
4002
- variant.priority,
4003
- ]));
4004
- const candidates = [];
4005
- const documents = sources.referenceDocuments ?? [];
4006
- documents.forEach((document, documentIndex) => {
4007
- if (!isAllowedFrontendBaseUrlDocument(document.path))
4008
- return;
4009
- const source = toDagSourcePath(sources, document.path);
4010
- // Only accept `key: value` / `key = value` lines. A colon or equals
4011
- // sign after a known key bounds the URL; free-form prose is ignored.
4012
- const keyPattern = /^[ ]*(?:#>*)?[ ]*([A-Za-z][A-Za-z0-9 _\-]*?)[ ]*[:=][ ]*["'`]?((?:https?:)?\/\/[\S]+?)(?:["'`])?[ ]*(?:#.*)?$/gm;
4013
- let match;
4014
- while ((match = keyPattern.exec(document.markdown)) !== null) {
4015
- const rawKey = match[1].trim();
4016
- const normalizedKey = normalizeFrontendBaseUrlKey(rawKey);
4017
- const priority = allowedKeyByNormalized.get(normalizedKey);
4018
- if (priority === undefined)
4019
- continue;
4020
- let rawUrl = match[2].replace(/[)\]},.;]+$/, "");
4021
- if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(rawUrl)) {
4022
- rawUrl = `https://${rawUrl.replace(/^\/\/+/, "")}`;
4023
- }
4024
- candidates.push({
4025
- url: rawUrl,
4026
- source,
4027
- keyName: normalizedKey,
4028
- priority,
4029
- documentIndex,
4030
- });
4031
- }
4032
- });
4033
- if (candidates.length === 0) {
4034
- return {
4035
- baseUrl: DEFAULT_FRONTEND_BASE_URL,
4036
- baseUrlSource: DEFAULT_FRONTEND_BASE_URL_SOURCE,
4037
- };
4038
- }
4039
- // Deterministic ranking: lowest priority number first, then earliest
4040
- // document, then earliest occurrence in that document.
4041
- candidates.sort((left, right) => {
4042
- if (left.priority !== right.priority) {
4043
- return left.priority - right.priority;
4044
- }
4045
- if (left.documentIndex !== right.documentIndex) {
4046
- return left.documentIndex - right.documentIndex;
4047
- }
4048
- return 0;
4049
- });
4050
- const bestPriority = candidates[0].priority;
4051
- const bestPriorityCandidates = candidates.filter((candidate) => candidate.priority === bestPriority);
4052
- // Fail closed: two distinct URLs at the same priority level are ambiguous.
4053
- const distinctUrls = new Set(bestPriorityCandidates.map((candidate) => candidate.url));
4054
- if (distinctUrls.size > 1) {
4055
- const conflictingSources = bestPriorityCandidates
4056
- .map((candidate) => `${candidate.source}:${candidate.keyName}`)
4057
- .join(", ");
4058
- throw new Error(`frontend-test controller baseUrl is ambiguous (${conflictingSources})`);
4059
- }
4060
- const selected = bestPriorityCandidates[0];
4061
- const baseUrlSource = selected.source;
4062
- let parsed;
4063
- try {
4064
- parsed = new URL(selected.url);
4065
- }
4066
- catch {
4067
- throw new Error(`frontend-test controller baseUrl is invalid (${baseUrlSource})`);
4068
- }
4069
- if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
4070
- parsed.username ||
4071
- parsed.password ||
4072
- parsed.search ||
4073
- parsed.hash ||
4074
- /(?:^|\.)(?:www\.)?[^.]*(?:prod|production)/i.test(parsed.hostname)) {
4075
- throw new Error(`frontend-test controller baseUrl is unsafe (${baseUrlSource})`);
4076
- }
4077
- return { baseUrl: parsed.toString(), baseUrlSource };
4078
- }
3945
+ // ---------------------------------------------------------------------------
3946
+ // Frontend browser-test RAG DAG template
3947
+ // ---------------------------------------------------------------------------
4079
3948
  function buildFrontendTestHybridDag(sources) {
4080
3949
  const rawFrontendTest = sources.taskConfig.frontendTest;
4081
3950
  const config = {
@@ -4087,8 +3956,9 @@ function buildFrontendTestHybridDag(sources) {
4087
3956
  strictOutcomeGate: rawFrontendTest?.strictOutcomeGate === true,
4088
3957
  maxRerunAttempts: (() => {
4089
3958
  const raw = rawFrontendTest?.maxRerunAttempts;
4090
- if (raw === undefined || raw === null || Number.isNaN(Number(raw)))
4091
- return 2;
3959
+ if (raw === undefined || raw === null || Number.isNaN(Number(raw))) {
3960
+ return rawFrontendTest?.reviewMode === "blocking" ? 2 : 1;
3961
+ }
4092
3962
  return Math.min(4, Math.max(0, Math.trunc(Number(raw))));
4093
3963
  })(),
4094
3964
  reports: {
@@ -4101,7 +3971,7 @@ function buildFrontendTestHybridDag(sources) {
4101
3971
  const reviewMode = config.reviewMode;
4102
3972
  const blockingReview = reviewMode === "blocking";
4103
3973
  const strictOutcomeGate = config.strictOutcomeGate;
4104
- const maxRerunAttempts = config.maxRerunAttempts ?? 2;
3974
+ const maxRerunAttempts = config.maxRerunAttempts;
4105
3975
  const enableRetrospect = config.reports?.retrospect === true;
4106
3976
  const enableL5Report = config.reports?.l5 !== false;
4107
3977
  const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
@@ -4111,7 +3981,6 @@ function buildFrontendTestHybridDag(sources) {
4111
3981
  throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
4112
3982
  }
4113
3983
  const forbidden = commonForbiddenPaths(sources);
4114
- const controllerFrontend = resolveControllerFrontendBaseUrl(sources);
4115
3984
  const ragWriteSet = ["testcase/frontend/rag/**"];
4116
3985
  const caseDraftWriteSet = [
4117
3986
  "testcase/frontend/cases/FE-*.md",
@@ -4237,7 +4106,7 @@ function buildFrontendTestHybridDag(sources) {
4237
4106
  allowedPaths: [],
4238
4107
  forbiddenPaths: forbidden,
4239
4108
  outputContract: "Deterministic browser-tool preflight: SDK-only custom-tool capability + controller verified playwright-cli launcher + --help contract + frozen controller origin. Fail closed with playwright-cli-unavailable | playwright-cli-contract-incompatible | browser-command-capability-unavailable before any frontend-test Pi node.",
4240
- subtask_prompt: `Verify CODE_AGENT_PI_BACKEND permits SDK, the Pi SDK structured custom-tool surface is available, the controller-resolved playwright-cli launcher and --help list open/close/find/snapshot/click, and the frozen origin is ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}. Do not install packages. Do not start a browser session. On failure exit non-zero so retrieve/generate/map never run (zero frontend-test Pi calls).`,
4109
+ subtask_prompt: "Verify CODE_AGENT_PI_BACKEND permits SDK, the Pi SDK structured custom-tool surface is available, and the controller-resolved playwright-cli launcher and --help list open/close/find/snapshot/click. Do not resolve or freeze the target URL in this node. Do not install packages. Do not start a browser session. On failure exit non-zero so retrieve/generate/map never run (zero frontend-test Pi calls).",
4241
4110
  shell: {
4242
4111
  commands: [],
4243
4112
  frontendBrowserToolPreflight: {},
@@ -4280,12 +4149,12 @@ function buildFrontendTestHybridDag(sources) {
4280
4149
  writeSet: ragWriteSet,
4281
4150
  allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
4282
4151
  forbiddenPaths: forbidden,
4283
- outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md that copy the controller-frozen baseUrl/baseUrlSource verbatim, set environmentProbe=pending, and record capability notes.",
4152
+ outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.",
4284
4153
  subtask_prompt: [
4285
4154
  "Build the frontend test RAG package (keep it short).",
4286
4155
  "Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
4287
4156
  "Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
4288
- `Controller-frozen origin (required, not model-selectable): write exactly \`baseUrl: ${controllerFrontend.baseUrl}\` and \`baseUrlSource: ${controllerFrontend.baseUrlSource}\`. Do not derive, replace, or override the origin from model reasoning or other repository text. Write \`environmentProbe: pending\`. Include exact start prefix: playwright-cli open --browser=chrome ${controllerFrontend.baseUrl}.`,
4157
+ "Base URL resolution (required): read the bound task source and config/environment references. Prefer TARGET_URL/targetUrl as the concrete tested entry, then BASE_URL/baseUrl or FRONTEND_BASE_URL/frontendUrl, then LOGIN_URL/loginUrl; accept common casing and underscore/kebab/space variants. Choose one absolute non-production http(s) URL, never API-only or production hosts. If no usable URL exists, use http://localhost:5173. Write `baseUrl: <url>`, `baseUrlSource: <bound source path>|default-localhost-5173`, and `environmentProbe: pending`. Include exact start prefix: playwright-cli open --browser=chrome <resolved-base-url> with the concrete selected URL.",
4289
4158
  buildSourceContextBlock(sources),
4290
4159
  ].join("\n\n"),
4291
4160
  },
@@ -4300,12 +4169,12 @@ function buildFrontendTestHybridDag(sources) {
4300
4169
  allowedPaths: [...ragWriteSet],
4301
4170
  forbiddenPaths: forbidden,
4302
4171
  outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (node ERROR so generate/map do not run).",
4303
- subtask_prompt: `Probe only the controller-frozen baseUrl ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; never parse or accept an origin from context.md. Use curl HEAD then GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.`,
4172
+ subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
4304
4173
  shell: {
4305
4174
  commands: [
4306
4175
  [
4307
4176
  "node -e",
4308
- JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const baseUrl=${JSON.stringify(controllerFrontend.baseUrl)};const baseUrlSource=${JSON.stringify(controllerFrontend.baseUrlSource)};const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated controllerBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
4177
+ JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*[\"'\\x60]?((?:https?):\\/\\/[^\\s\"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*[\"'\\x60]?((?:https?):\\/\\/[^\\s\"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s\"'\\x60<>]+)/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 from context.md');baseUrl=baseUrl.replace(/[)\\}\\],.\"'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
4309
4178
  ].join(" "),
4310
4179
  ],
4311
4180
  cwd: ".",
@@ -4338,7 +4207,7 @@ function buildFrontendTestHybridDag(sources) {
4338
4207
  "dimensions: core|boundary|flow|backend only.",
4339
4208
  "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.",
4340
4209
  "HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
4341
- `Use only the controller-frozen baseUrl ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; context.md may reference it but cannot establish or override it. Every browser start command must be exactly: playwright-cli open --browser=chrome ${controllerFrontend.baseUrl}. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
4210
+ "Copy the resolved absolute baseUrl from testcase/frontend/rag/context.md after the environment probe has marked it reachable. Every browser start command must be exactly: playwright-cli open --browser=chrome <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>.",
4342
4211
  "HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
4343
4212
  "HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
4344
4213
  "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.",
@@ -4487,7 +4356,7 @@ function buildFrontendTestHybridDag(sources) {
4487
4356
  subtaskPromptTemplate: [
4488
4357
  "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
4489
4358
  "Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
4490
- `1) The controller-frozen baseUrl is ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; model-authored context/case text cannot establish or override it. 2) Start browser ONLY via playwright_cli command=open with args [--browser=chrome, ${controllerFrontend.baseUrl}] (default session only; no -s=). 3) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 4) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 5) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
4359
+ "1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
4491
4360
  "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
4492
4361
  "Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
4493
4362
  ].join("\n\n"),
@@ -4510,8 +4379,8 @@ function buildFrontendTestHybridDag(sources) {
4510
4379
  "const manifestPath='testcase/frontend/cases/manifest.json';",
4511
4380
  "if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
4512
4381
  "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
4513
- "for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
4514
- `fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\n');process.stdout.write(JSON.stringify({cases}));`,
4382
+ "for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
4383
+ `fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
4515
4384
  ].join("");
4516
4385
  tasks.push({
4517
4386
  id: selectorId,
@@ -4577,7 +4446,7 @@ function buildFrontendTestHybridDag(sources) {
4577
4446
  subtaskPromptTemplate: [
4578
4447
  "RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
4579
4448
  "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
4580
- `Start via playwright_cli command=open with args [--browser=chrome, ${controllerFrontend.baseUrl}]. Passed requires open → find → cleanup receipts.`,
4449
+ "Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
4581
4450
  "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
4582
4451
  ].join("\n\n"),
4583
4452
  },
@@ -4673,12 +4542,12 @@ function buildFrontendTestHybridDag(sources) {
4673
4542
  "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
4674
4543
  "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
4675
4544
  "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
4676
- `Browser startup for generated cases must be playwright-cli open --browser=chrome ${controllerFrontend.baseUrl}; this origin is frozen by the controller from ${controllerFrontend.baseUrlSource}, and model-authored files cannot establish or override it; generated operations stay in the default browser session and must not use unverified named-session flags.`,
4545
+ "Browser startup for generated cases must use the concrete non-production baseUrl written by retrieve-frontend-test-context-pi and validated reachable by materialize-frontend-test-execution-shell; generated operations stay in the default browser session and must not use unverified named-session flags.",
4677
4546
  "Browser-tool preflight runs before any frontend-test Pi node; cli-only rollback, missing/incompatible Pi SDK custom-tool capability, missing verified playwright-cli launcher, or incompatible --help fails with zero Pi calls.",
4678
4547
  "Browser case children use commandPolicy capability-allowlist playwright-cli and structured playwright_cli tool; playwright-cli stays capability-gated while ordinary writers have bash.",
4679
4548
  "Passed cases require same-child ordered controller receipts: successful open → successful find → successful post-execution cleanup. Pre-start cleanup, snapshot/goto/screenshot/request/console and ordinary interactions are insufficient; missing or unordered receipts convert to blocked (browser-command-evidence-missing).",
4680
4549
  "playwright-cli-only: generators and executors may call only skill-declared playwright-cli commands; bare playwright / npx playwright / @playwright/test / Playwright source are forbidden with no native Playwright fallback.",
4681
- "Environment preflight must curl-probe the frozen non-production baseUrl before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
4550
+ "Environment preflight must parse the retrieve-authored context.md baseUrl, reject unsafe/non-production violations, and curl-probe it before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
4682
4551
  "U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
4683
4552
  "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
4684
4553
  "Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under testcase/frontend/reports/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).",
@@ -140,7 +140,7 @@
140
140
  ]
141
141
  },
142
142
  "outputContract": "Write a Chinese, human-readable testcase/md/README.md as the single Markdown-first entry page with Coverage Scope, Coverage Matrix and a machine-parseable module index. Do not write module case cards here; do not execute pytest or modify production code/config.",
143
- "subtask_prompt": "This is a required file-generation node. After reading the bounded inputs, immediately use write tools to create testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Write ONLY testcase/md/README.md in this node; module case cards are written by downstream sharded nodes.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the README has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nRead the upstream environment report. Generate the Markdown-first backend test README under testcase/md/README.md.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nCreate testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.\n\nBefore the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in README using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.\n\nCoverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.\n\nBefore writing cases, build the mandatory machine-readable Coverage Matrix inside `testcase/md/README.md` itself. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix; never place the canonical Matrix only in a module file. Use this exact header: `| Rule Key | Priority | Source | Endpoint/Field | Dimension | Rule | Required Test Points | Case IDs | Status |`. Every data row must contain exactly 9 pipe-delimited cells and must never omit `Dimension`; use concise dimensions such as requirement, operation, response-status, requiredness, enum, boundary, format, business-state or error. Use only P0/P1/P2 and COVERED/PARTIAL/GAP/CONFLICT. Use stable `TP-<UPPERCASE-HYPHENATED-ID>` test points separated by semicolons.\n\nEach Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.\n\nCoverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.\n\nFor uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.\n\nMandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output budget, and keep the total module count at the smallest safe value. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nBefore finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
143
+ "subtask_prompt": "This is a required file-generation node. After reading the bounded inputs, immediately use write tools to create testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Write ONLY testcase/md/README.md in this node; module case cards are written by downstream sharded nodes.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the README has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nRead the upstream environment report. Generate the Markdown-first backend test README under testcase/md/README.md.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nCreate testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.\n\nBefore the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in README using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.\n\nCoverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.\n\nBefore writing cases, build the mandatory machine-readable Coverage Matrix inside `testcase/md/README.md` itself. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix; never place the canonical Matrix only in a module file. Use this exact header: `| Rule Key | Priority | Source | Endpoint/Field | Dimension | Rule | Required Test Points | Case IDs | Status |`. Every data row must contain exactly 9 pipe-delimited cells and must never omit `Dimension`; use concise dimensions such as requirement, operation, response-status, requiredness, enum, boundary, format, business-state or error. Use only P0/P1/P2 and COVERED/PARTIAL/GAP/CONFLICT. Use stable `TP-<UPPERCASE-HYPHENATED-ID>` test points separated by semicolons.\n\nEach Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.\n\nCoverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.\n\nFor uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.\n\nMandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output budget, and keep the total module count at the smallest safe value. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nBefore finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
144
144
  },
145
145
  {
146
146
  "id": "materialize-backend-md-module-manifest-shell",
@@ -164,7 +164,7 @@
164
164
  "subtask_prompt": "Parse testcase/md/README.md and emit exactly one trailing JSON line {modules:[{stem}]} listing every trusted module stem (table-row testcase/md/<stem>.md mentions and canonical [label](./<stem>.md) relative links only). No file writes.",
165
165
  "shell": {
166
166
  "commands": [
167
- "node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IHZhbGlkPXJhdz0+e2NvbnN0IHN0PW5vcm0ocmF3KTtpZihzdD09PSdyZWFkbWUnKXJldHVybiBmYWxzZTtpZighL15bYS16XVthLXowLTlfXSokLy50ZXN0KHN0KSlyZXR1cm4gZmFsc2U7aWYoL14oPzpiZXx0cHxhY3xyZXF8YnIpW18tXS9pLnRlc3Qoc3QpKXJldHVybiBmYWxzZTtyZXR1cm4gdHJ1ZTt9Owpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IHJhdz1bXTsKY29uc3QgbGluZXM9cmVhZG1lLnJlcGxhY2UoL1xyXG4vZywnXG4nKS5yZXBsYWNlKC9cci9nLCdcbicpLnNwbGl0KCdcbicpLmZpbHRlcihsPT5sLmluY2x1ZGVzKCd8JykpOwpmb3IoY29uc3QgbGluZSBvZiBsaW5lcyl7CiAgY29uc3QgYmFyZT1zdHJpcEJhY2t0aWNrcyhsaW5lKTsKICBmb3IoY29uc3QgbSBvZiBiYXJlLm1hdGNoQWxsKHJ4TWRQYXRoKSl7aWYodmFsaWQobVsxXSkpcmF3LnB1c2gobVsxXSk7fQogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhUYWJsZVJvdykpe2lmKHZhbGlkKG1bMV0pKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiByZWFkbWUubWF0Y2hBbGwocnhSZWxMaW5rKSl7aWYodmFsaWQobVsxXSkpcmF3LnB1c2gobVsxXSk7fQpjb25zdCBzZWVuPW5ldyBTZXQoKTtjb25zdCBtb2R1bGVzPVtdOwpmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHN0PW5vcm0ocik7aWYoIXNlZW4uaGFzKHN0KSl7c2Vlbi5hZGQoc3QpO21vZHVsZXMucHVzaCh7c3RlbTpzdH0pO319CnByb2Nlc3Muc3Rkb3V0LndyaXRlKEpTT04uc3RyaW5naWZ5KHttb2R1bGVzfSkpOwo=','base64').toString('utf8'))\""
167
+ "node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IGludmFsaWRSZWFzb249cmF3PT57Y29uc3Qgc3Q9bm9ybShyYXcpO2lmKC9ecFswLTJdJC8udGVzdChzdCkpcmV0dXJuICdwcmlvcml0eS1vbmx5LW1vZHVsZS1zdGVtJztpZihzdD09PSdyZWFkbWUnKXJldHVybiAncmVzZXJ2ZWQtbW9kdWxlLXN0ZW0nO2lmKCEvXlthLXpdW2EtejAtOV9dKiQvLnRlc3Qoc3QpKXJldHVybiAnaW52YWxpZC1zeW50YXgnO2lmKC9eKD86YmV8dHB8YWN8cmVxfGJyKVtfLV0vaS50ZXN0KHN0KSlyZXR1cm4gJ2Nhc2UtbGlrZS1tb2R1bGUtc3RlbSc7cmV0dXJuIG51bGw7fTsKY29uc3QgdmFsaWQ9cmF3PT5pbnZhbGlkUmVhc29uKHJhdyk9PT1udWxsOwpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IHJhdz1bXTsKY29uc3QgbGluZXM9cmVhZG1lLnJlcGxhY2UoL1xyXG4vZywnXG4nKS5yZXBsYWNlKC9cci9nLCdcbicpLnNwbGl0KCdcbicpLmZpbHRlcihsPT5sLmluY2x1ZGVzKCd8JykpOwpmb3IoY29uc3QgbGluZSBvZiBsaW5lcyl7CiAgY29uc3QgYmFyZT1zdHJpcEJhY2t0aWNrcyhsaW5lKTsKICBmb3IoY29uc3QgbSBvZiBiYXJlLm1hdGNoQWxsKHJ4TWRQYXRoKSl7cmF3LnB1c2gobVsxXSk7fQogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhUYWJsZVJvdykpe2lmKHZhbGlkKG1bMV0pfHxpbnZhbGlkUmVhc29uKG1bMV0pPT09J3ByaW9yaXR5LW9ubHktbW9kdWxlLXN0ZW0nKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiByZWFkbWUubWF0Y2hBbGwocnhSZWxMaW5rKSl7cmF3LnB1c2gobVsxXSk7fQpjb25zdCBpbnZhbGlkPVtdO2Zvcihjb25zdCByIG9mIHJhdyl7Y29uc3QgcmVhc29uPWludmFsaWRSZWFzb24ocik7aWYocmVhc29uKWludmFsaWQucHVzaCh7c3RlbTpub3JtKHIpLHJlYXNvbn0pO30KaWYoaW52YWxpZC5sZW5ndGgpe2Zvcihjb25zdCBpdGVtIG9mIGludmFsaWQpcHJvY2Vzcy5zdGRlcnIud3JpdGUoaXRlbS5yZWFzb24rJzogJytpdGVtLnN0ZW0rJzsgdXNlIGEgc3RhYmxlIGJ1c2luZXNzIHJlc291cmNlL2RvbWFpbiBzdGVtXG4nKTtwcm9jZXNzLmV4aXQoMik7fQpjb25zdCBzZWVuPW5ldyBTZXQoKTtjb25zdCBtb2R1bGVzPVtdOwpmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHN0PW5vcm0ocik7aWYodmFsaWQocikmJiFzZWVuLmhhcyhzdCkpe3NlZW4uYWRkKHN0KTttb2R1bGVzLnB1c2goe3N0ZW06c3R9KTt9fQpwcm9jZXNzLnN0ZG91dC53cml0ZShKU09OLnN0cmluZ2lmeSh7bW9kdWxlc30pKTsK','base64').toString('utf8'))\""
168
168
  ],
169
169
  "cwd": ".",
170
170
  "timeoutMs": 60000
@@ -238,7 +238,7 @@
238
238
  ]
239
239
  },
240
240
  "outputContract": "Write exactly one Chinese module Markdown case-card file testcase/md/<stem>.md with BE-<MODULE>-<NNN> cases and the seven required h3 sections; keep machine IDs/literals exact and do not execute pytest or modify production code/config or the README.",
241
- "subtaskPromptTemplate": "This is a required file-generation node for exactly one Markdown module. After reading testcase/md/README.md (Coverage Scope + Coverage Matrix + module index) and the bounded references, immediately use write tools to create the single file testcase/md/{{item.stem}}.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify README.md or any other module file.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nWrite the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under \"## 测试类 ...\" (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case's `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.\n\nName this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nEvery Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.\n\nIn every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
241
+ "subtaskPromptTemplate": "This is a required file-generation node for exactly one Markdown module. After reading testcase/md/README.md (Coverage Scope + Coverage Matrix + module index) and the bounded references, immediately use write tools to create the single file testcase/md/{{item.stem}}.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify README.md or any other module file.\n\nOutput budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.\n\nThe first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nWrite the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under \"## 测试类 ...\" (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case's `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.\n\nName this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nEvery Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.\n\nIn every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
242
242
  }
243
243
  }
244
244
  },
@@ -16,9 +16,9 @@ Use `playwright-cli-case-generator`. Read only `testcase/frontend/rag/context.md
16
16
 
17
17
  Each case is independently executable and includes AC mapping (`acIds`), preconditions, cleanup, a semantic assertion, and its isolated evidence path. Use dimensions `core`, `boundary`, `flow`, or `backend`. Never guess API fields, constraints, SLA, credentials, or unrecorded data. For passed authority, include a successful `playwright-cli find ...` after `open` and before controller post-execution cleanup; `snapshot`, `goto`, `screenshot`, `request`/`console`, and ordinary interactions are not assertions.
18
18
 
19
- Every case must copy the **controller-frozen** absolute base URL supplied by the DAG. `testcase/frontend/rag/context.md` may reference that value, but model-authored context/case prose cannot establish or override the origin. Do not leave a base-url placeholder.
19
+ Every case must copy the concrete absolute base URL from `testcase/frontend/rag/context.md` only after the environment shell has marked `environmentProbe: reachable`. Do not derive a different origin and do not leave a base-url placeholder.
20
20
 
21
- Every case must start with `playwright-cli open --browser=chrome` followed by the concrete controller-frozen URL supplied by the DAG. Do not copy an angle-bracket URL placeholder into a generated executable case line.
21
+ Every case must start with `playwright-cli open --browser=chrome` followed by that concrete context URL. Do not copy an angle-bracket URL placeholder into a generated executable case line.
22
22
 
23
23
  ### Dynamic element refs (hard)
24
24
 
@@ -11,19 +11,19 @@
11
11
  "globalConstraints": [
12
12
  "Do not generate pytest or Playwright source code.",
13
13
  "Only use declared isolated test environments; production URLs and real credentials are blocked.",
14
- "Every generated browser start command uses playwright-cli open --browser=chrome followed by the concrete controller-resolved URL (resolved from an explicit allowlist of controller-owned, hash-bound task source/reference documents and URL keys: baseUrl, targetUrl, loginUrl and common case/separator variants; http://localhost:5173 is used only when no allowed candidate exists, and conflicting same-priority candidates fail closed); executable case lines never retain an angle-bracket URL/ref placeholder; subsequent commands stay in that default session and must not use unverified named-session flags.",
14
+ "Every generated browser start command uses playwright-cli open --browser=chrome followed by the concrete URL selected by retrieve-frontend-test-context-pi from bound task source/config and written to context.md; the environment shell must mark that URL reachable before generation/execution; executable case lines never retain an angle-bracket URL/ref placeholder; subsequent commands stay in that default session and must not use unverified named-session flags.",
15
15
  "Case children execute serially. Persist each case result, logs and browser evidence before the next child starts.",
16
16
  "A token threshold is a post-case stop check, not a model hard token cap; unstarted cases must be recorded as blocked: token-budget-exhausted.",
17
17
  "Default pipeline acceptance is the final frontend-test-result-v1 plus testcase/frontend/reports/frontend-test-report.html",
18
18
  "Default frontendTest.reviewMode=off uses mechanical checklist-shell before materialize; set reviewMode=blocking for legacy dual LLM review gate.",
19
19
  "playwright-cli-only: generators and executors may call only skill-declared playwright-cli commands; bare playwright / npx playwright / @playwright/test / Playwright source are forbidden with no native Playwright fallback.",
20
- "Browser-tool preflight (preflight-frontend-browser-tool-shell) must reject CODE_AGENT_PI_BACKEND=cli-only, verify the Pi SDK structured custom-tool surface, freeze baseUrl from allowlisted hash-bound task references and explicit baseUrl/targetUrl/loginUrl keys (or controller default localhost only when no candidate exists), and confirm the verified playwright-cli launcher + --help contract before any frontend-test Pi node; missing capability/CLI fails with zero Pi calls.",
20
+ "Browser-tool preflight (preflight-frontend-browser-tool-shell) must reject CODE_AGENT_PI_BACKEND=cli-only, verify the Pi SDK structured custom-tool surface, and confirm the verified playwright-cli launcher + --help open/close/find/snapshot/click contract before any frontend-test Pi node; it does not select or freeze the tested URL; missing capability/CLI fails with zero Pi calls.",
21
21
  "Case executors use structured playwright_cli custom tool under commandPolicy capability-allowlist; playwright-cli stays capability-gated while ordinary writers have bash.",
22
22
  "File outputs use canonical --filename: playwright-cli screenshot --filename final.png (a real target/ref may precede it), playwright-cli pdf --filename final.pdf, and playwright-cli snapshot --filename snapshot.txt only when a snapshot file is needed; a snapshot without filename is response-only. Never use --path, --output, --file, or an output path as a positional target.",
23
23
  "Passed cases require same-child ordered controller receipts: successful open → successful find → successful post-execution cleanup. Pre-start cleanup, snapshot/goto/screenshot/request/console, and ordinary interactions cannot establish passed authority; model prose cannot fake green.",
24
- "Environment preflight must curl-probe the frozen non-production baseUrl before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
24
+ "Environment preflight must parse the retrieve-authored context.md baseUrl, reject unsafe/non-production violations, and curl-probe it before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
25
25
  "U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
26
- "Rerun topology is bounded by frontendTest.maxRerunAttempts (0..4, default 2): the runtime hybrid emits one select+map pair per attempt round, each round only candidates blocked or missing-result cases and rewrites the authoritative case-result.json/execution.md, so the final round's evidence controls the report."
26
+ "Rerun topology is bounded by frontendTest.maxRerunAttempts (0..4): lean off/advisory defaults to 1 round, full reviewMode=blocking defaults to 2 rounds, and an explicit value overrides either default. The runtime hybrid emits one select+map pair per attempt round; each round only candidates blocked or missing-result cases and rewrites the authoritative case-result.json/execution.md, so the final round's evidence controls the report."
27
27
  ],
28
28
  "tasks": [
29
29
  {
@@ -38,8 +38,8 @@
38
38
  ".harness/**",
39
39
  "artifacts/**"
40
40
  ],
41
- "outputContract": "Deterministic SDK-only browser-tool preflight before any frontend-test Pi node; freeze a controller-owned origin (baseUrl/targetUrl/loginUrl allowlist with deterministic priority and fail-closed ambiguity) and fail closed with browser-command-capability-unavailable | playwright-cli-unavailable | playwright-cli-contract-incompatible.",
42
- "subtask_prompt": "Reject cli-only rollback, verify the Pi SDK structured custom-tool capability, freeze baseUrl from controller-owned task source/reference via the explicit document+key allowlist (baseUrl/targetUrl/loginUrl variants; default localhost only when no candidate; fail closed on ambiguity), and verify the controller-resolved playwright-cli launcher plus --help lists open/close/find/snapshot/click. Do not install packages. Do not start a browser session.",
41
+ "outputContract": "Deterministic SDK-only browser-tool capability preflight before any frontend-test Pi node; no target URL selection; fail closed with browser-command-capability-unavailable | playwright-cli-unavailable | playwright-cli-contract-incompatible.",
42
+ "subtask_prompt": "Reject cli-only rollback, verify the Pi SDK structured custom-tool capability, and verify the controller-resolved playwright-cli launcher plus --help lists open/close/find/snapshot/click. Do not resolve or freeze the tested URL. Do not install packages. Do not start a browser session.",
43
43
  "shell": {
44
44
  "commands": [],
45
45
  "frontendBrowserToolPreflight": {},
@@ -121,7 +121,7 @@
121
121
  "artifacts/**"
122
122
  ],
123
123
  "outputContract": "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (node ERROR so generate/map do not run).",
124
- "subtask_prompt": "Use the controller-frozen baseUrl; the runtime resolver recognizes allowlisted hash-bound reference documents and baseUrl/targetUrl/loginUrl variants, falling back only when no candidate exists. Reject production / non-http(s). Probe with curl (HEAD then GET fallback; connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Runtime hybrid generator embeds the authoritative probe script.",
124
+ "subtask_prompt": "Parse the concrete baseUrl selected by retrieve-frontend-test-context-pi from testcase/frontend/rag/context.md. Reject production, non-http(s), credentials, query and fragment. Probe with curl (HEAD then GET fallback; connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Runtime hybrid generator embeds the authoritative probe script.",
125
125
  "shell": {
126
126
  "commands": [
127
127
  "node -e \"console.log('template placeholder: runtime hybrid DAG embeds curl preflight; do not use this static command as source of truth')\""
@@ -271,11 +271,11 @@
271
271
  ".harness/**",
272
272
  "artifacts/**"
273
273
  ],
274
- "outputContract": "Round 1 of the bounded rerun topology (default 2 rounds): stdout final JSON line {cases:[...]} for blocked or missing-result-file cases with rerunAttempt < 1; the runtime hybrid generates one selector/map pair per configured maxRerunAttempts so the latest rerun evidence is authoritative.",
274
+ "outputContract": "Round 1 of the bounded rerun topology (lean default 1 round; full reviewMode=blocking default 2): stdout final JSON line {cases:[...]} for blocked or missing-result-file cases with rerunAttempt < 1; the runtime hybrid generates one selector/map pair per configured maxRerunAttempts so the latest rerun evidence is authoritative.",
275
275
  "subtask_prompt": "Select frontend-test cases eligible for bounded rerun.",
276
276
  "shell": {
277
277
  "commands": [
278
- "node -e \"const fs=require('fs'),path=require('path');const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(e){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)){const st=reason;if(st!=='blocked'){missing=true;reason=reason||'missing-result-files';}}const should=(reason==='blocked'||missing)&&attempt<2;if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}const out={schemaVersion:1,cases};fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/rerun-candidates.json',JSON.stringify(out,null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));"
278
+ "node -e \"const fs=require('fs'),path=require('path');const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(e){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)){const st=reason;if(st!=='blocked'){missing=true;reason=reason||'missing-result-files';}}const should=(reason==='blocked'||missing)&&attempt<1;if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}const out={schemaVersion:1,cases};fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/rerun-candidates.json',JSON.stringify(out,null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));"
279
279
  ],
280
280
  "cwd": ".",
281
281
  "timeoutMs": 120000