@tea-agent/loop-agent 0.33.5 → 0.33.6-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.33.6-beta.0] - 2026-08-11
6
+
7
+ ### 修复
8
+
9
+ - 修复 backend-test correspondence scanner 将下一 Case 的分隔注释误归入前一个 pytest function region,造成虚假的 `MULTIPLE_PYTEST`、script mismatch 和额外 Test Point;现在按函数实际缩进边界隔离 body,并只从精确 `Case-ID:` 元数据或 primary symbol 绑定 Case
10
+ - 修复 backend-test 将 README 其他区域的 Markdown 链接误提取为业务模块、允许 `a401606` 等 opaque/hash-like stem 和异常多模块进入 map 的问题;现在只解析唯一 `## Module Index`,非法、mixed 或超过 8 个模块时 README-only 有界重试并由 manifest fail-closed
11
+ - 修复 backend-test map 总 token 预算耗尽后只生成部分 Markdown/pytest 资产但 parent 仍可能显示成功的问题;budget-blocked child 现在使 backend map 明确 ERROR,并保留 expected/finished/blocked/token 事实
12
+
5
13
  ## [0.33.5] - 2026-08-11
6
14
 
7
15
  ### 重点更新
@@ -909,6 +909,10 @@ function symbolCaseId(symbol) {
909
909
  const match = symbol.match(/^test_(BE(?:_[A-Z0-9]+)+?_\d{2,3})(?:_|$)/i);
910
910
  return match ? canonicalCaseId(match[1].replaceAll("_", "-")) : undefined;
911
911
  }
912
+ function metadataCaseIds(region) {
913
+ return orderedUnique([...region.matchAll(/^\s*Case-ID\s*:\s*(BE-[A-Z0-9]+(?:-[A-Z0-9]+)*)\s*$/gmi)]
914
+ .map((match) => canonicalCaseId(match[1])));
915
+ }
912
916
  function metadataTestPoints(region, label) {
913
917
  const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
914
918
  const match = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "mi").exec(region);
@@ -928,6 +932,27 @@ function pytestFunctionRegionStart(source, functionIndex) {
928
932
  }
929
933
  return regionStart;
930
934
  }
935
+ function pytestFunctionBodyEnd(source, signatureEnd, functionIndent) {
936
+ let cursor = source.indexOf("\n", signatureEnd);
937
+ if (cursor < 0)
938
+ return source.length;
939
+ cursor += 1;
940
+ const baseIndent = functionIndent.replaceAll("\t", " ").length;
941
+ while (cursor < source.length) {
942
+ const nextLine = source.indexOf("\n", cursor);
943
+ const lineEnd = nextLine < 0 ? source.length : nextLine;
944
+ const line = source.slice(cursor, lineEnd).replace(/\r$/, "");
945
+ if (line.trim()) {
946
+ const indentation = line.match(/^[ \t]*/)?.[0]?.replaceAll("\t", " ").length ?? 0;
947
+ if (indentation <= baseIndent)
948
+ return cursor;
949
+ }
950
+ if (nextLine < 0)
951
+ return source.length;
952
+ cursor = nextLine + 1;
953
+ }
954
+ return source.length;
955
+ }
931
956
  function pytestParameterCollections(source) {
932
957
  const collections = new Map();
933
958
  for (const match of source.matchAll(/^([A-Z][A-Z0-9_]*)\s*=\s*\[([\s\S]*?)^\]/gm)) {
@@ -941,10 +966,10 @@ function pytestSymbols(script, source) {
941
966
  const parameterCollections = pytestParameterCollections(source);
942
967
  return matches.map((match, index) => {
943
968
  const regionStart = regionStarts[index];
944
- const regionEnd = regionStarts[index + 1] ?? source.length;
969
+ const regionEnd = pytestFunctionBodyEnd(source, match.index + match[0].length, match[1]);
945
970
  const region = source.slice(match.index, regionEnd);
946
971
  const decorators = source.slice(regionStart, match.index);
947
- const ids = caseIds(region);
972
+ const ids = metadataCaseIds(region);
948
973
  const fromSymbol = symbolCaseId(match[2]);
949
974
  if (fromSymbol)
950
975
  ids.unshift(fromSymbol);
@@ -1,5 +1,7 @@
1
1
  import path from "node:path";
2
2
  const PRIORITY_ONLY_MODULE_STEMS = new Set(["p0", "p1", "p2"]);
3
+ const OPAQUE_HASH_MODULE_STEM = /^[a-f][a-f0-9]{6,63}$/;
4
+ export const BACKEND_TEST_MAX_MODULE_COUNT = 8;
3
5
  export function normalizeBackendTestModuleStemCandidate(raw) {
4
6
  return path.basename(raw)
5
7
  .replace(/\.(?:md|py)$/i, "")
@@ -11,6 +13,9 @@ export function normalizeBackendTestModuleStemCandidate(raw) {
11
13
  export function isPriorityOnlyBackendTestModuleStem(raw) {
12
14
  return PRIORITY_ONLY_MODULE_STEMS.has(normalizeBackendTestModuleStemCandidate(raw));
13
15
  }
16
+ export function isOpaqueHashBackendTestModuleStem(raw) {
17
+ return OPAQUE_HASH_MODULE_STEM.test(normalizeBackendTestModuleStemCandidate(raw));
18
+ }
14
19
  export function isPriorityOnlyBackendPytestScript(script) {
15
20
  const basename = path.posix.basename(script.replaceAll("\\", "/"));
16
21
  const match = /^test_(.+)\.py$/i.exec(basename);
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
- import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
5
+ import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
6
6
  /**
7
7
  * Maps a backend-test generation writer task id to its writer-progress role.
8
8
  * Returns undefined for nodes that are not completeness-gated generators.
@@ -103,6 +103,8 @@ function moduleStemInvalidReason(raw) {
103
103
  const stem = normalizeBackendTestModuleStem(raw);
104
104
  if (isPriorityOnlyBackendTestModuleStem(stem))
105
105
  return "priority-only-module-stem";
106
+ if (isOpaqueHashBackendTestModuleStem(stem))
107
+ return "opaque-hash-module-stem";
106
108
  if (stem.toLowerCase() === "readme")
107
109
  return "reserved-module-stem";
108
110
  if (!/^[a-z][a-z0-9_]*$/.test(stem))
@@ -114,28 +116,40 @@ function moduleStemInvalidReason(raw) {
114
116
  function looksLikeValidModuleStem(raw) {
115
117
  return moduleStemInvalidReason(raw) === undefined;
116
118
  }
117
- /** Inspect only authoritative README module-index candidates. */
118
- export function inspectModuleStemsFromReadme(readme) {
119
- const candidates = [];
120
- const tableRowLines = readme
119
+ function extractCanonicalModuleIndexSection(readme) {
120
+ const lines = readme
121
121
  .replaceAll("\r\n", "\n")
122
122
  .replaceAll("\r", "\n")
123
- .split("\n")
124
- .filter((line) => line.includes("|"));
123
+ .split("\n");
124
+ const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
125
+ if (headings.length === 0) {
126
+ return { section: "", structureIssues: ["missing-module-index"] };
127
+ }
128
+ if (headings.length > 1) {
129
+ return { section: "", structureIssues: ["duplicate-module-index"] };
130
+ }
131
+ const start = headings[0] + 1;
132
+ const relativeEnd = lines.slice(start).findIndex((line) => /^##\s+\S/.test(line.trim()));
133
+ const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
134
+ return { section: lines.slice(start, end).join("\n"), structureIssues: [] };
135
+ }
136
+ /** Inspect only candidates inside the unique exact `## Module Index` section. */
137
+ export function inspectModuleStemsFromReadme(readme) {
138
+ const extracted = extractCanonicalModuleIndexSection(readme);
139
+ const candidates = [];
140
+ const tableRowLines = extracted.section.split("\n").filter((line) => line.includes("|"));
125
141
  for (const line of tableRowLines) {
126
142
  for (const match of line.matchAll(/`?testcase\/md\/([A-Za-z0-9_.-]+)\.md`?/g)) {
127
143
  candidates.push(match[1]);
128
144
  }
129
145
  for (const match of line.matchAll(/\|\s*`?([A-Za-z0-9_.-]+)`?\s*\|\s*`?testcase\/test_/g)) {
130
- // This compatibility shape can also capture a numeric count column.
131
- // Keep valid stems and the priority-only defect we diagnose, while
132
- // ignoring non-stem incidental cells such as `1`.
133
- if (looksLikeValidModuleStem(match[1]) || isPriorityOnlyBackendTestModuleStem(match[1])) {
134
- candidates.push(match[1]);
146
+ const raw = match[1];
147
+ if (looksLikeValidModuleStem(raw) || moduleStemInvalidReason(raw) !== "invalid-syntax") {
148
+ candidates.push(raw);
135
149
  }
136
150
  }
137
151
  }
138
- for (const match of readme.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
152
+ for (const match of extracted.section.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
139
153
  candidates.push(match[1]);
140
154
  }
141
155
  const uniqueCandidates = orderedUnique(candidates);
@@ -145,12 +159,18 @@ export function inspectModuleStemsFromReadme(readme) {
145
159
  ? [{ raw, normalized: normalizeBackendTestModuleStem(raw), reasonCode }]
146
160
  : [];
147
161
  });
162
+ const validStems = orderedUnique(uniqueCandidates
163
+ .filter(looksLikeValidModuleStem)
164
+ .map((stem) => normalizeBackendTestModuleStem(stem)));
165
+ const structureIssues = [...extracted.structureIssues];
166
+ if (validStems.length > BACKEND_TEST_MAX_MODULE_COUNT) {
167
+ structureIssues.push(`excessive-module-count: ${validStems.length} > ${BACKEND_TEST_MAX_MODULE_COUNT}`);
168
+ }
148
169
  return {
149
170
  candidates: uniqueCandidates,
150
- validStems: orderedUnique(uniqueCandidates
151
- .filter(looksLikeValidModuleStem)
152
- .map((stem) => normalizeBackendTestModuleStem(stem))),
171
+ validStems,
153
172
  invalid,
173
+ structureIssues,
154
174
  };
155
175
  }
156
176
  /**
@@ -917,6 +937,17 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
917
937
  // are produced by map_agent children and are NOT required here.
918
938
  const stemInspection = inspectModuleStemsFromReadme(readme);
919
939
  const indexedStems = stemInspection.validStems;
940
+ for (const structureIssue of stemInspection.structureIssues) {
941
+ if (!brokenPaths.includes("testcase/md/README.md")) {
942
+ brokenPaths.push("testcase/md/README.md");
943
+ }
944
+ issues.push({
945
+ code: "T5",
946
+ path: "testcase/md/README.md",
947
+ detail: structureIssue,
948
+ recoverable: true,
949
+ });
950
+ }
920
951
  for (const invalid of stemInspection.invalid) {
921
952
  if (!brokenPaths.includes("testcase/md/README.md")) {
922
953
  brokenPaths.push("testcase/md/README.md");
@@ -367,19 +367,26 @@ export async function executeDynamicMapExpansion(input) {
367
367
  }
368
368
  }
369
369
  const budgetBlockedIds = new Set(blockedChildren.map((entry) => entry.nodeId));
370
+ const failOnTokenBudgetExhaustion = input.expansion.failOnTokenBudgetExhaustion === true;
370
371
  const failedChildren = tolerateChildFailures
371
372
  ? []
372
373
  : childNodeIds.filter((nodeId) => {
373
374
  const record = input.state.nodes[nodeId];
374
375
  if (!record || record.status === "FINISHED")
375
376
  return false;
376
- if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
377
- return false;
377
+ if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record)) {
378
+ return failOnTokenBudgetExhaustion;
379
+ }
378
380
  return true;
379
381
  });
382
+ const finishedItemCount = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status === "FINISHED").length;
383
+ const blockedItemCount = budgetBlockedIds.size;
380
384
  const aggregate = {
381
385
  workflowNodeId: input.expansion.workflowNodeId,
382
386
  itemCount: items.length,
387
+ expectedItemCount: items.length,
388
+ finishedItemCount,
389
+ blockedItemCount,
383
390
  children: childNodeIds.map((nodeId, index) => ({
384
391
  nodeId,
385
392
  item: items[index],
@@ -399,21 +406,30 @@ export async function executeDynamicMapExpansion(input) {
399
406
  ?.reason,
400
407
  })),
401
408
  tokensUsed: totalTokensUsed,
409
+ maxTotalTokens: tokenBudget?.maxTotalTokens,
402
410
  caseOutcomeNotes: caseOutcomeNotes.length > 0 ? caseOutcomeNotes : undefined,
403
411
  };
412
+ const tokenBudgetFailure = failOnTokenBudgetExhaustion && blockedItemCount > 0;
404
413
  return {
405
- // With tolerateChildFailures: barrier always succeeds; product outcomes live in
406
- // case evidence + result materialization. Without it: real child failures fail-close.
407
- ok: failedChildren.length === 0,
414
+ // With tolerateChildFailures: barrier succeeds and product outcomes live in
415
+ // case evidence. Backend-test opts into token-budget fail-closed because a
416
+ // partial generated module set is never a valid downstream input.
417
+ ok: failedChildren.length === 0 && !tokenBudgetFailure,
408
418
  stdout: JSON.stringify(aggregate),
409
419
  stderr: caseOutcomeNotes.length > 0
410
420
  ? `map children recorded as case outcomes: ${caseOutcomeNotes
411
421
  .map((entry) => `${entry.nodeId}=${entry.status}:${entry.reason}`)
412
422
  .join(", ")}`
423
+ : tokenBudgetFailure
424
+ ? `dynamic map token budget exhausted: expected=${items.length} finished=${finishedItemCount} blocked=${blockedItemCount} tokensUsed=${totalTokensUsed} maxTotalTokens=${tokenBudget?.maxTotalTokens ?? "unbounded"}`
425
+ : failedChildren.length > 0
426
+ ? `dynamic map children failed: ${failedChildren.join(", ")}`
427
+ : "",
428
+ failureCategory: tokenBudgetFailure
429
+ ? "dynamic-expansion-token-budget-exhausted"
413
430
  : failedChildren.length > 0
414
- ? `dynamic map children failed: ${failedChildren.join(", ")}`
415
- : "",
416
- failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
431
+ ? "dynamic-expansion-child-failed"
432
+ : "success",
417
433
  durationMs: Date.now() - started,
418
434
  };
419
435
  }
@@ -3508,23 +3508,29 @@ 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 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;};
3511
+ const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(/^[a-f][a-f0-9]{6,63}$/.test(st))return 'opaque-hash-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
3512
  const valid=raw=>invalidReason(raw)===null;
3513
3513
  const rxMdPath=/testcase\\/md\\/([A-Za-z0-9_.-]+)\\.md/g;
3514
3514
  const rxTableRow=/\\|\\s*([A-Za-z0-9_.-]+)\\s*\\|\\s*testcase\\/test_/g;
3515
3515
  const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
3516
+ const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
3517
+ const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
3518
+ if(headings.length!==1){process.stderr.write((headings.length===0?'missing-module-index':'duplicate-module-index')+'; require exactly one exact ## Module Index section\\n');process.exit(2);}
3519
+ const start=headings[0]+1;let end=allLines.length;for(let i=start;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){end=i;break;}}
3520
+ const section=allLines.slice(start,end).join('\\n');
3516
3521
  const raw=[];
3517
- const lines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n').filter(l=>l.includes('|'));
3522
+ const lines=section.split('\\n').filter(l=>l.includes('|'));
3518
3523
  for(const line of lines){
3519
3524
  const bare=stripBackticks(line);
3520
3525
  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]);}
3526
+ for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])!=='invalid-syntax')raw.push(m[1]);}
3522
3527
  }
3523
- for(const m of readme.matchAll(rxRelLink)){raw.push(m[1]);}
3528
+ for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
3524
3529
  const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
3525
3530
  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);}
3526
3531
  const seen=new Set();const modules=[];
3527
3532
  for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
3533
+ if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
3528
3534
  process.stdout.write(JSON.stringify({modules}));
3529
3535
  `;
3530
3536
  const encoded = Buffer.from(script, "utf8").toString("base64");
@@ -3602,7 +3608,7 @@ async function buildBackendTestHybridDag(sources) {
3602
3608
  "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.",
3603
3609
  "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.",
3604
3610
  "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.",
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.",
3611
+ "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 protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. 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.",
3606
3612
  "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.",
3607
3613
  intake.boundedSourceContext,
3608
3614
  "## Authoritative reference index",
@@ -3647,10 +3653,11 @@ async function buildBackendTestHybridDag(sources) {
3647
3653
  workflowNodeId: "generate-backend-md-cases-map",
3648
3654
  itemsFrom: "$.nodes['materialize-backend-md-module-manifest-shell'].output.modules",
3649
3655
  itemName: "item",
3650
- maxItems: 64,
3651
- maxExpandedNodes: 64,
3656
+ maxItems: 8,
3657
+ maxExpandedNodes: 8,
3652
3658
  childIdPrefix: "generate-backend-md-case",
3653
- tokenBudget: { maxTokensPerCase: 16384, maxTotalTokens: 600000 },
3659
+ tokenBudget: { maxTotalTokens: 600000 },
3660
+ failOnTokenBudgetExhaustion: true,
3654
3661
  childTask: {
3655
3662
  executor: "pi",
3656
3663
  role: "implementer",
@@ -3673,7 +3680,7 @@ async function buildBackendTestHybridDag(sources) {
3673
3680
  "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.",
3674
3681
  "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.",
3675
3682
  '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.',
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.",
3683
+ "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`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. 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.",
3677
3684
  "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 ‘符合预期’.",
3678
3685
  "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.",
3679
3686
  intake.boundedSourceContext,
@@ -3777,10 +3784,11 @@ async function buildBackendTestHybridDag(sources) {
3777
3784
  workflowNodeId: "generate-backend-pytest-cases-map",
3778
3785
  itemsFrom: "$.nodes['materialize-backend-pytest-module-manifest-shell'].output.modules",
3779
3786
  itemName: "item",
3780
- maxItems: 64,
3781
- maxExpandedNodes: 64,
3787
+ maxItems: 8,
3788
+ maxExpandedNodes: 8,
3782
3789
  childIdPrefix: "generate-backend-pytest-case",
3783
- tokenBudget: { maxTokensPerCase: 16384, maxTotalTokens: 600000 },
3790
+ tokenBudget: { maxTotalTokens: 600000 },
3791
+ failOnTokenBudgetExhaustion: true,
3784
3792
  childTask: {
3785
3793
  executor: "pi",
3786
3794
  role: "implementer",
@@ -555,6 +555,13 @@ export const dagDynamicExpansionSchema = z.object({
555
555
  maxTotalTokens: z.number().int().positive().optional(),
556
556
  })
557
557
  .optional(),
558
+ /**
559
+ * When true, any child blocked by the aggregate token budget fails the map
560
+ * barrier instead of being retained as a case-level blocked outcome.
561
+ * Backend-test enables this because partial Markdown/pytest module sets are
562
+ * invalid generation assets; frontend-test keeps the default false.
563
+ */
564
+ failOnTokenBudgetExhaustion: z.boolean().optional(),
558
565
  /**
559
566
  * When true, map child ERROR/auth/timeout is recorded as case-level
560
567
  * failed/blocked evidence and the map barrier still succeeds (frontend-test).
@@ -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 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."
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 protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. 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+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IGludmFsaWRSZWFzb249cmF3PT57Y29uc3Qgc3Q9bm9ybShyYXcpO2lmKC9ecFswLTJdJC8udGVzdChzdCkpcmV0dXJuICdwcmlvcml0eS1vbmx5LW1vZHVsZS1zdGVtJztpZihzdD09PSdyZWFkbWUnKXJldHVybiAncmVzZXJ2ZWQtbW9kdWxlLXN0ZW0nO2lmKCEvXlthLXpdW2EtejAtOV9dKiQvLnRlc3Qoc3QpKXJldHVybiAnaW52YWxpZC1zeW50YXgnO2lmKC9eKD86YmV8dHB8YWN8cmVxfGJyKVtfLV0vaS50ZXN0KHN0KSlyZXR1cm4gJ2Nhc2UtbGlrZS1tb2R1bGUtc3RlbSc7cmV0dXJuIG51bGw7fTsKY29uc3QgdmFsaWQ9cmF3PT5pbnZhbGlkUmVhc29uKHJhdyk9PT1udWxsOwpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IHJhdz1bXTsKY29uc3QgbGluZXM9cmVhZG1lLnJlcGxhY2UoL1xyXG4vZywnXG4nKS5yZXBsYWNlKC9cci9nLCdcbicpLnNwbGl0KCdcbicpLmZpbHRlcihsPT5sLmluY2x1ZGVzKCd8JykpOwpmb3IoY29uc3QgbGluZSBvZiBsaW5lcyl7CiAgY29uc3QgYmFyZT1zdHJpcEJhY2t0aWNrcyhsaW5lKTsKICBmb3IoY29uc3QgbSBvZiBiYXJlLm1hdGNoQWxsKHJ4TWRQYXRoKSl7cmF3LnB1c2gobVsxXSk7fQogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhUYWJsZVJvdykpe2lmKHZhbGlkKG1bMV0pfHxpbnZhbGlkUmVhc29uKG1bMV0pPT09J3ByaW9yaXR5LW9ubHktbW9kdWxlLXN0ZW0nKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiByZWFkbWUubWF0Y2hBbGwocnhSZWxMaW5rKSl7cmF3LnB1c2gobVsxXSk7fQpjb25zdCBpbnZhbGlkPVtdO2Zvcihjb25zdCByIG9mIHJhdyl7Y29uc3QgcmVhc29uPWludmFsaWRSZWFzb24ocik7aWYocmVhc29uKWludmFsaWQucHVzaCh7c3RlbTpub3JtKHIpLHJlYXNvbn0pO30KaWYoaW52YWxpZC5sZW5ndGgpe2Zvcihjb25zdCBpdGVtIG9mIGludmFsaWQpcHJvY2Vzcy5zdGRlcnIud3JpdGUoaXRlbS5yZWFzb24rJzogJytpdGVtLnN0ZW0rJzsgdXNlIGEgc3RhYmxlIGJ1c2luZXNzIHJlc291cmNlL2RvbWFpbiBzdGVtXG4nKTtwcm9jZXNzLmV4aXQoMik7fQpjb25zdCBzZWVuPW5ldyBTZXQoKTtjb25zdCBtb2R1bGVzPVtdOwpmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHN0PW5vcm0ocik7aWYodmFsaWQocikmJiFzZWVuLmhhcyhzdCkpe3NlZW4uYWRkKHN0KTttb2R1bGVzLnB1c2goe3N0ZW06c3R9KTt9fQpwcm9jZXNzLnN0ZG91dC53cml0ZShKU09OLnN0cmluZ2lmeSh7bW9kdWxlc30pKTsK','base64').toString('utf8'))\""
167
+ "node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IGludmFsaWRSZWFzb249cmF3PT57Y29uc3Qgc3Q9bm9ybShyYXcpO2lmKC9ecFswLTJdJC8udGVzdChzdCkpcmV0dXJuICdwcmlvcml0eS1vbmx5LW1vZHVsZS1zdGVtJztpZigvXlthLWZdW2EtZjAtOV17Niw2M30kLy50ZXN0KHN0KSlyZXR1cm4gJ29wYXF1ZS1oYXNoLW1vZHVsZS1zdGVtJztpZihzdD09PSdyZWFkbWUnKXJldHVybiAncmVzZXJ2ZWQtbW9kdWxlLXN0ZW0nO2lmKCEvXlthLXpdW2EtejAtOV9dKiQvLnRlc3Qoc3QpKXJldHVybiAnaW52YWxpZC1zeW50YXgnO2lmKC9eKD86YmV8dHB8YWN8cmVxfGJyKVtfLV0vaS50ZXN0KHN0KSlyZXR1cm4gJ2Nhc2UtbGlrZS1tb2R1bGUtc3RlbSc7cmV0dXJuIG51bGw7fTsKY29uc3QgdmFsaWQ9cmF3PT5pbnZhbGlkUmVhc29uKHJhdyk9PT1udWxsOwpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IGFsbExpbmVzPXJlYWRtZS5yZXBsYWNlKC9cclxuL2csJ1xuJykucmVwbGFjZSgvXHIvZywnXG4nKS5zcGxpdCgnXG4nKTsKY29uc3QgaGVhZGluZ3M9W107Zm9yKGxldCBpPTA7aTxhbGxMaW5lcy5sZW5ndGg7aSsrKXtpZihhbGxMaW5lc1tpXS50cmltKCk9PT0nIyMgTW9kdWxlIEluZGV4JyloZWFkaW5ncy5wdXNoKGkpO30KaWYoaGVhZGluZ3MubGVuZ3RoIT09MSl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoKGhlYWRpbmdzLmxlbmd0aD09PTA/J21pc3NpbmctbW9kdWxlLWluZGV4JzonZHVwbGljYXRlLW1vZHVsZS1pbmRleCcpKyc7IHJlcXVpcmUgZXhhY3RseSBvbmUgZXhhY3QgIyMgTW9kdWxlIEluZGV4IHNlY3Rpb25cbicpO3Byb2Nlc3MuZXhpdCgyKTt9CmNvbnN0IHN0YXJ0PWhlYWRpbmdzWzBdKzE7bGV0IGVuZD1hbGxMaW5lcy5sZW5ndGg7Zm9yKGxldCBpPXN0YXJ0O2k8YWxsTGluZXMubGVuZ3RoO2krKyl7aWYoL14jI1xzK1xTLy50ZXN0KGFsbExpbmVzW2ldLnRyaW0oKSkpe2VuZD1pO2JyZWFrO319CmNvbnN0IHNlY3Rpb249YWxsTGluZXMuc2xpY2Uoc3RhcnQsZW5kKS5qb2luKCdcbicpOwpjb25zdCByYXc9W107CmNvbnN0IGxpbmVzPXNlY3Rpb24uc3BsaXQoJ1xuJykuZmlsdGVyKGw9PmwuaW5jbHVkZXMoJ3wnKSk7CmZvcihjb25zdCBsaW5lIG9mIGxpbmVzKXsKICBjb25zdCBiYXJlPXN0cmlwQmFja3RpY2tzKGxpbmUpOwogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhNZFBhdGgpKXtyYXcucHVzaChtWzFdKTt9CiAgZm9yKGNvbnN0IG0gb2YgYmFyZS5tYXRjaEFsbChyeFRhYmxlUm93KSl7aWYodmFsaWQobVsxXSl8fGludmFsaWRSZWFzb24obVsxXSkhPT0naW52YWxpZC1zeW50YXgnKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiBzZWN0aW9uLm1hdGNoQWxsKHJ4UmVsTGluaykpe3Jhdy5wdXNoKG1bMV0pO30KY29uc3QgaW52YWxpZD1bXTtmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHJlYXNvbj1pbnZhbGlkUmVhc29uKHIpO2lmKHJlYXNvbilpbnZhbGlkLnB1c2goe3N0ZW06bm9ybShyKSxyZWFzb259KTt9CmlmKGludmFsaWQubGVuZ3RoKXtmb3IoY29uc3QgaXRlbSBvZiBpbnZhbGlkKXByb2Nlc3Muc3RkZXJyLndyaXRlKGl0ZW0ucmVhc29uKyc6ICcraXRlbS5zdGVtKyc7IHVzZSBhIHN0YWJsZSBidXNpbmVzcyByZXNvdXJjZS9kb21haW4gc3RlbVxuJyk7cHJvY2Vzcy5leGl0KDIpO30KY29uc3Qgc2Vlbj1uZXcgU2V0KCk7Y29uc3QgbW9kdWxlcz1bXTsKZm9yKGNvbnN0IHIgb2YgcmF3KXtjb25zdCBzdD1ub3JtKHIpO2lmKHZhbGlkKHIpJiYhc2Vlbi5oYXMoc3QpKXtzZWVuLmFkZChzdCk7bW9kdWxlcy5wdXNoKHtzdGVtOnN0fSk7fX0KaWYobW9kdWxlcy5sZW5ndGg+OCl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoJ2V4Y2Vzc2l2ZS1tb2R1bGUtY291bnQ6ICcrbW9kdWxlcy5sZW5ndGgrJyA+IDg7IG1lcmdlIGJ5IHRoZSBzbWFsbGVzdCBzdGFibGUgYnVzaW5lc3MgcmVzb3VyY2UvZG9tYWluIHNldFxuJyk7cHJvY2Vzcy5leGl0KDIpO30KcHJvY2Vzcy5zdGRvdXQud3JpdGUoSlNPTi5zdHJpbmdpZnkoe21vZHVsZXN9KSk7Cg==','base64').toString('utf8'))\""
168
168
  ],
169
169
  "cwd": ".",
170
170
  "timeoutMs": 60000
@@ -195,13 +195,13 @@
195
195
  "workflowNodeId": "generate-backend-md-cases-map",
196
196
  "itemsFrom": "$.nodes['materialize-backend-md-module-manifest-shell'].output.modules",
197
197
  "itemName": "item",
198
- "maxItems": 64,
199
- "maxExpandedNodes": 64,
198
+ "maxItems": 8,
199
+ "maxExpandedNodes": 8,
200
200
  "childIdPrefix": "generate-backend-md-case",
201
201
  "tokenBudget": {
202
- "maxTokensPerCase": 16384,
203
202
  "maxTotalTokens": 600000
204
203
  },
204
+ "failOnTokenBudgetExhaustion": true,
205
205
  "childTask": {
206
206
  "executor": "pi",
207
207
  "role": "implementer",
@@ -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`). 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."
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`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. 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
  },
@@ -338,7 +338,7 @@
338
338
  "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.",
339
339
  "shell": {
340
340
  "commands": [
341
- "node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IHZhbGlkPXJhdz0+e2NvbnN0IHN0PW5vcm0ocmF3KTtpZihzdD09PSdyZWFkbWUnKXJldHVybiBmYWxzZTtpZighL15bYS16XVthLXowLTlfXSokLy50ZXN0KHN0KSlyZXR1cm4gZmFsc2U7aWYoL14oPzpiZXx0cHxhY3xyZXF8YnIpW18tXS9pLnRlc3Qoc3QpKXJldHVybiBmYWxzZTtyZXR1cm4gdHJ1ZTt9Owpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IHJhdz1bXTsKY29uc3QgbGluZXM9cmVhZG1lLnJlcGxhY2UoL1xyXG4vZywnXG4nKS5yZXBsYWNlKC9cci9nLCdcbicpLnNwbGl0KCdcbicpLmZpbHRlcihsPT5sLmluY2x1ZGVzKCd8JykpOwpmb3IoY29uc3QgbGluZSBvZiBsaW5lcyl7CiAgY29uc3QgYmFyZT1zdHJpcEJhY2t0aWNrcyhsaW5lKTsKICBmb3IoY29uc3QgbSBvZiBiYXJlLm1hdGNoQWxsKHJ4TWRQYXRoKSl7aWYodmFsaWQobVsxXSkpcmF3LnB1c2gobVsxXSk7fQogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhUYWJsZVJvdykpe2lmKHZhbGlkKG1bMV0pKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiByZWFkbWUubWF0Y2hBbGwocnhSZWxMaW5rKSl7aWYodmFsaWQobVsxXSkpcmF3LnB1c2gobVsxXSk7fQpjb25zdCBzZWVuPW5ldyBTZXQoKTtjb25zdCBtb2R1bGVzPVtdOwpmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHN0PW5vcm0ocik7aWYoIXNlZW4uaGFzKHN0KSl7c2Vlbi5hZGQoc3QpO21vZHVsZXMucHVzaCh7c3RlbTpzdH0pO319CnByb2Nlc3Muc3Rkb3V0LndyaXRlKEpTT04uc3RyaW5naWZ5KHttb2R1bGVzfSkpOwo=','base64').toString('utf8'))\""
341
+ "node -e \"eval(Buffer.from('Y29uc3QgZnM9cmVxdWlyZSgnZnMnKTsKY29uc3QgcmVhZG1lPWZzLmV4aXN0c1N5bmMoJ3Rlc3RjYXNlL21kL1JFQURNRS5tZCcpP2ZzLnJlYWRGaWxlU3luYygndGVzdGNhc2UvbWQvUkVBRE1FLm1kJywndXRmOCcpOicnOwpjb25zdCBub3JtPXM9PlN0cmluZyhzKS50b0xvd2VyQ2FzZSgpLnJlcGxhY2UoL1teYS16MC05XSsvZywnXycpLnJlcGxhY2UoL15fK3xfKyQvZywnJykucmVwbGFjZSgvXysvZywnXycpOwpjb25zdCBidD1TdHJpbmcuZnJvbUNoYXJDb2RlKDk2KTsKY29uc3Qgc3RyaXBCYWNrdGlja3M9cz0+cy5zcGxpdChidCkuam9pbignJyk7CmNvbnN0IGludmFsaWRSZWFzb249cmF3PT57Y29uc3Qgc3Q9bm9ybShyYXcpO2lmKC9ecFswLTJdJC8udGVzdChzdCkpcmV0dXJuICdwcmlvcml0eS1vbmx5LW1vZHVsZS1zdGVtJztpZigvXlthLWZdW2EtZjAtOV17Niw2M30kLy50ZXN0KHN0KSlyZXR1cm4gJ29wYXF1ZS1oYXNoLW1vZHVsZS1zdGVtJztpZihzdD09PSdyZWFkbWUnKXJldHVybiAncmVzZXJ2ZWQtbW9kdWxlLXN0ZW0nO2lmKCEvXlthLXpdW2EtejAtOV9dKiQvLnRlc3Qoc3QpKXJldHVybiAnaW52YWxpZC1zeW50YXgnO2lmKC9eKD86YmV8dHB8YWN8cmVxfGJyKVtfLV0vaS50ZXN0KHN0KSlyZXR1cm4gJ2Nhc2UtbGlrZS1tb2R1bGUtc3RlbSc7cmV0dXJuIG51bGw7fTsKY29uc3QgdmFsaWQ9cmF3PT5pbnZhbGlkUmVhc29uKHJhdyk9PT1udWxsOwpjb25zdCByeE1kUGF0aD0vdGVzdGNhc2VcL21kXC8oW0EtWmEtejAtOV8uLV0rKVwubWQvZzsKY29uc3QgcnhUYWJsZVJvdz0vXHxccyooW0EtWmEtejAtOV8uLV0rKVxzKlx8XHMqdGVzdGNhc2VcL3Rlc3RfL2c7CmNvbnN0IHJ4UmVsTGluaz0vXFtbXlxdXStcXVwoXC5cLyhbQS1aYS16MC05Xy4tXSspXC5tZFwpL2c7CmNvbnN0IGFsbExpbmVzPXJlYWRtZS5yZXBsYWNlKC9cclxuL2csJ1xuJykucmVwbGFjZSgvXHIvZywnXG4nKS5zcGxpdCgnXG4nKTsKY29uc3QgaGVhZGluZ3M9W107Zm9yKGxldCBpPTA7aTxhbGxMaW5lcy5sZW5ndGg7aSsrKXtpZihhbGxMaW5lc1tpXS50cmltKCk9PT0nIyMgTW9kdWxlIEluZGV4JyloZWFkaW5ncy5wdXNoKGkpO30KaWYoaGVhZGluZ3MubGVuZ3RoIT09MSl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoKGhlYWRpbmdzLmxlbmd0aD09PTA/J21pc3NpbmctbW9kdWxlLWluZGV4JzonZHVwbGljYXRlLW1vZHVsZS1pbmRleCcpKyc7IHJlcXVpcmUgZXhhY3RseSBvbmUgZXhhY3QgIyMgTW9kdWxlIEluZGV4IHNlY3Rpb25cbicpO3Byb2Nlc3MuZXhpdCgyKTt9CmNvbnN0IHN0YXJ0PWhlYWRpbmdzWzBdKzE7bGV0IGVuZD1hbGxMaW5lcy5sZW5ndGg7Zm9yKGxldCBpPXN0YXJ0O2k8YWxsTGluZXMubGVuZ3RoO2krKyl7aWYoL14jI1xzK1xTLy50ZXN0KGFsbExpbmVzW2ldLnRyaW0oKSkpe2VuZD1pO2JyZWFrO319CmNvbnN0IHNlY3Rpb249YWxsTGluZXMuc2xpY2Uoc3RhcnQsZW5kKS5qb2luKCdcbicpOwpjb25zdCByYXc9W107CmNvbnN0IGxpbmVzPXNlY3Rpb24uc3BsaXQoJ1xuJykuZmlsdGVyKGw9PmwuaW5jbHVkZXMoJ3wnKSk7CmZvcihjb25zdCBsaW5lIG9mIGxpbmVzKXsKICBjb25zdCBiYXJlPXN0cmlwQmFja3RpY2tzKGxpbmUpOwogIGZvcihjb25zdCBtIG9mIGJhcmUubWF0Y2hBbGwocnhNZFBhdGgpKXtyYXcucHVzaChtWzFdKTt9CiAgZm9yKGNvbnN0IG0gb2YgYmFyZS5tYXRjaEFsbChyeFRhYmxlUm93KSl7aWYodmFsaWQobVsxXSl8fGludmFsaWRSZWFzb24obVsxXSkhPT0naW52YWxpZC1zeW50YXgnKXJhdy5wdXNoKG1bMV0pO30KfQpmb3IoY29uc3QgbSBvZiBzZWN0aW9uLm1hdGNoQWxsKHJ4UmVsTGluaykpe3Jhdy5wdXNoKG1bMV0pO30KY29uc3QgaW52YWxpZD1bXTtmb3IoY29uc3QgciBvZiByYXcpe2NvbnN0IHJlYXNvbj1pbnZhbGlkUmVhc29uKHIpO2lmKHJlYXNvbilpbnZhbGlkLnB1c2goe3N0ZW06bm9ybShyKSxyZWFzb259KTt9CmlmKGludmFsaWQubGVuZ3RoKXtmb3IoY29uc3QgaXRlbSBvZiBpbnZhbGlkKXByb2Nlc3Muc3RkZXJyLndyaXRlKGl0ZW0ucmVhc29uKyc6ICcraXRlbS5zdGVtKyc7IHVzZSBhIHN0YWJsZSBidXNpbmVzcyByZXNvdXJjZS9kb21haW4gc3RlbVxuJyk7cHJvY2Vzcy5leGl0KDIpO30KY29uc3Qgc2Vlbj1uZXcgU2V0KCk7Y29uc3QgbW9kdWxlcz1bXTsKZm9yKGNvbnN0IHIgb2YgcmF3KXtjb25zdCBzdD1ub3JtKHIpO2lmKHZhbGlkKHIpJiYhc2Vlbi5oYXMoc3QpKXtzZWVuLmFkZChzdCk7bW9kdWxlcy5wdXNoKHtzdGVtOnN0fSk7fX0KaWYobW9kdWxlcy5sZW5ndGg+OCl7cHJvY2Vzcy5zdGRlcnIud3JpdGUoJ2V4Y2Vzc2l2ZS1tb2R1bGUtY291bnQ6ICcrbW9kdWxlcy5sZW5ndGgrJyA+IDg7IG1lcmdlIGJ5IHRoZSBzbWFsbGVzdCBzdGFibGUgYnVzaW5lc3MgcmVzb3VyY2UvZG9tYWluIHNldFxuJyk7cHJvY2Vzcy5leGl0KDIpO30KcHJvY2Vzcy5zdGRvdXQud3JpdGUoSlNPTi5zdHJpbmdpZnkoe21vZHVsZXN9KSk7Cg==','base64').toString('utf8'))\""
342
342
  ],
343
343
  "cwd": ".",
344
344
  "timeoutMs": 60000
@@ -369,13 +369,13 @@
369
369
  "workflowNodeId": "generate-backend-pytest-cases-map",
370
370
  "itemsFrom": "$.nodes['materialize-backend-pytest-module-manifest-shell'].output.modules",
371
371
  "itemName": "item",
372
- "maxItems": 64,
373
- "maxExpandedNodes": 64,
372
+ "maxItems": 8,
373
+ "maxExpandedNodes": 8,
374
374
  "childIdPrefix": "generate-backend-pytest-case",
375
375
  "tokenBudget": {
376
- "maxTokensPerCase": 16384,
377
376
  "maxTotalTokens": 600000
378
377
  },
378
+ "failOnTokenBudgetExhaustion": true,
379
379
  "childTask": {
380
380
  "executor": "pi",
381
381
  "role": "implementer",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.33.5",
3
+ "version": "0.33.6-beta.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",