@tea-agent/loop-agent 0.25.4 → 0.25.6

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.
Files changed (40) hide show
  1. package/AGENTS.md +6 -0
  2. package/CHANGELOG.md +60 -0
  3. package/dist/commands/client-recovery.js +209 -62
  4. package/dist/commands/init.js +68 -129
  5. package/dist/executors/dag-pi-executor.js +80 -15
  6. package/dist/executors/model-routing.js +1 -1
  7. package/dist/executors/shell-executor.js +127 -0
  8. package/dist/executors/shell-write-guard.js +21 -7
  9. package/dist/worker/console/repo-fingerprint.js +7 -1
  10. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +1281 -0
  11. package/dist/workflows/dag/backend-test-case-manifest.js +59 -1
  12. package/dist/workflows/dag/backend-test-markdown-workflow.js +236 -16
  13. package/dist/workflows/dag/convergence/controller.js +134 -9
  14. package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
  15. package/dist/workflows/dag/init-hybrid.js +270 -80
  16. package/dist/workflows/dag/node-execution.js +64 -11
  17. package/dist/workflows/dag/prompt.js +118 -4
  18. package/dist/workflows/dag/retry-policy.js +5 -4
  19. package/dist/workflows/dag/scheduler.js +32 -5
  20. package/dist/workflows/dag/types.js +7 -4
  21. package/dist/workflows/dag/validate.js +3 -2
  22. package/docs/architecture/dag-execution.md +7 -4
  23. package/docs/architecture/runtime-boundaries.md +1 -1
  24. package/docs/init-surface.manifest.json +3 -1
  25. package/docs/templates/README.md +1 -0
  26. package/docs/templates/agent-dag.base.json +1 -1
  27. package/docs/templates/agent-dag.final-verification.json +1 -1
  28. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  29. package/docs/templates/backend-test-dag.json +41 -14
  30. package/docs/templates/frontend-test-dag.json +32 -2
  31. package/docs/templates/hybrid-dag.json +1 -1
  32. package/docs/templates/init-managed-agents.md +137 -0
  33. package/examples/decision-gate-agent-dag.json +1 -1
  34. package/examples/example-dag.json +1 -1
  35. package/examples/hybrid-loop-agent-dag.json +1 -1
  36. package/harness.json +1 -1
  37. package/package.json +1 -1
  38. package/skills/loop-agent/references/command-reference.md +5 -4
  39. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  40. package/skills/loop-agent/references/model-routing.md +1 -1
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, readdir, readFile, realpath, } from "node:fs/promises";
2
+ import { access, readdir, readFile, realpath } from "node:fs/promises";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
@@ -896,8 +896,7 @@ function chooseFrontendVerifyCommands(input) {
896
896
  return { commandSource: "inline" };
897
897
  }
898
898
  function classifyFrontendVerifyCommand(command) {
899
- return /\b(typecheck|check-types|lint|eslint|tsc|build|check)\b/i.test(command) ||
900
- /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)
899
+ return /\b(typecheck|check-types|lint|eslint|tsc|build|check)\b/i.test(command) || /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)
901
900
  ? "static"
902
901
  : "behavior";
903
902
  }
@@ -925,16 +924,18 @@ function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
925
924
  function verifyCommandKey(command) {
926
925
  const normalizedArgs = normalizeVerifyCommandArgs(command.args);
927
926
  const normalizedCwd = path.resolve(command.cwd).replace(/\\/g, "/");
928
- const cwdKey = process.platform === "win32"
929
- ? normalizedCwd.toLowerCase()
930
- : normalizedCwd;
927
+ const cwdKey = process.platform === "win32" ? normalizedCwd.toLowerCase() : normalizedCwd;
931
928
  const envKey = Object.entries(command.env ?? {})
932
929
  .filter((entry) => entry[1] !== undefined)
933
930
  .sort(([left], [right]) => left.localeCompare(right))
934
931
  .map(([key, value]) => `${key}=${value}`)
935
932
  .join("\0");
936
- return [cwdKey, normalizedArgs.join("\0"), envKey, command.timeoutMs ?? ""]
937
- .join("\u0001");
933
+ return [
934
+ cwdKey,
935
+ normalizedArgs.join("\0"),
936
+ envKey,
937
+ command.timeoutMs ?? "",
938
+ ].join("\u0001");
938
939
  }
939
940
  function normalizeVerifyCommandArgs(args) {
940
941
  if (args.length === 3 &&
@@ -1008,7 +1009,10 @@ function isFullSuiteVerifyCommand(command) {
1008
1009
  let index = 0;
1009
1010
  while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(args[index] ?? ""))
1010
1011
  index += 1;
1011
- const executable = path.basename(args[index] ?? "").toLowerCase().replace(/\.(?:exe|cmd)$/, "");
1012
+ const executable = path
1013
+ .basename(args[index] ?? "")
1014
+ .toLowerCase()
1015
+ .replace(/\.(?:exe|cmd)$/, "");
1012
1016
  const rest = args.slice(index + 1);
1013
1017
  if (["npm", "pnpm", "yarn", "bun"].includes(executable)) {
1014
1018
  return ((rest.length === 1 && rest[0] === "test") ||
@@ -1017,7 +1021,8 @@ function isFullSuiteVerifyCommand(command) {
1017
1021
  if (["bash", "sh"].includes(executable) && rest.length === 1) {
1018
1022
  return /(?:^|\/)scripts\/ci\.sh$/i.test(rest[0].replace(/\\/g, "/"));
1019
1023
  }
1020
- return rest.length === 0 && /(?:^|\/)scripts\/ci\.sh$/i.test((args[index] ?? "").replace(/\\/g, "/"));
1024
+ return (rest.length === 0 &&
1025
+ /(?:^|\/)scripts\/ci\.sh$/i.test((args[index] ?? "").replace(/\\/g, "/")));
1021
1026
  }
1022
1027
  function resolveDagVerifyStrategy(taskConfig, defaultIntermediateQuotaWhenFull = "full") {
1023
1028
  const explicitIntermediateQuota = taskConfig.dagVerifyStrategy?.intermediateQuota;
@@ -1038,7 +1043,8 @@ function buildVerifyEvidence(input) {
1038
1043
  quota: input.quota,
1039
1044
  commandSource: input.commandSource,
1040
1045
  commandCount,
1041
- commandLabels: selectedCommands?.map((command) => command.label) ?? input.fallbackCommands,
1046
+ commandLabels: selectedCommands?.map((command) => command.label) ??
1047
+ input.fallbackCommands,
1042
1048
  commandTexts: input.commandTexts ?? [],
1043
1049
  commandTimeoutMs: input.commandTimeoutMs,
1044
1050
  totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
@@ -1674,6 +1680,7 @@ export function buildStandardHybridDagFromTask(sources) {
1674
1680
  {
1675
1681
  id: "closeout-pi",
1676
1682
  depends_on: ["verify-pi"],
1683
+ failureAwareDependsOn: ["verify-pi"],
1677
1684
  role: "closeout",
1678
1685
  executor: "pi",
1679
1686
  complexity: "MED",
@@ -1685,6 +1692,7 @@ export function buildStandardHybridDagFromTask(sources) {
1685
1692
  outputContract: "Plain Markdown closeout summary; no file writes.",
1686
1693
  subtask_prompt: [
1687
1694
  "Return closeout summary: what changed, verification status, risks, and handoff notes.",
1695
+ "If upstream failure evidence or a failure-derived skip is present, report the run as failed/partial_failed and never infer a passing result.",
1688
1696
  "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
1689
1697
  sourceContext,
1690
1698
  ].join("\n\n"),
@@ -1874,7 +1882,8 @@ function buildBlockedFrontendMockDag(sources, readOnlyPaths, forbiddenPaths, glo
1874
1882
  },
1875
1883
  skillsByRole: FRONTEND_SKILLS_BY_ROLE,
1876
1884
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
1877
- tasks: [{
1885
+ tasks: [
1886
+ {
1878
1887
  id: "frontend-mock-blocked-shell",
1879
1888
  depends_on: [],
1880
1889
  role: "verifier",
@@ -1892,7 +1901,8 @@ function buildBlockedFrontendMockDag(sources, readOnlyPaths, forbiddenPaths, glo
1892
1901
  cwd: ".",
1893
1902
  timeoutMs: 60000,
1894
1903
  },
1895
- }],
1904
+ },
1905
+ ],
1896
1906
  };
1897
1907
  applyDefaultReadOnlyRetryPolicy(spec);
1898
1908
  parseDagSpec(spec);
@@ -2156,7 +2166,10 @@ async function buildFrontendHybridDagFromTask(sources) {
2156
2166
  constraintMarkdown: sources.constraintMarkdown,
2157
2167
  });
2158
2168
  const explicitFrontendVerifyCommands = buildExplicitFrontendVerifyCommands(taskConfig, sources.repoRoot);
2159
- const explicitCommandKeys = new Set([...explicitFrontendVerifyCommands.staticCommands, ...explicitFrontendVerifyCommands.behaviorCommands].map(verifyCommandKey));
2169
+ const explicitCommandKeys = new Set([
2170
+ ...explicitFrontendVerifyCommands.staticCommands,
2171
+ ...explicitFrontendVerifyCommands.behaviorCommands,
2172
+ ].map(verifyCommandKey));
2160
2173
  const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
2161
2174
  const staticVerifyCommands = chooseFrontendVerifyCommands({
2162
2175
  explicitCommands: explicitFrontendVerifyCommands.staticCommands,
@@ -2285,10 +2298,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2285
2298
  },
2286
2299
  {
2287
2300
  id: "frontend-plan-pi",
2288
- depends_on: [
2289
- "frontend-contract-pi",
2290
- "frontend-scout-pi",
2291
- ],
2301
+ depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
2292
2302
  role: "planner",
2293
2303
  executor: "pi",
2294
2304
  complexity: "MED",
@@ -2337,10 +2347,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2337
2347
  },
2338
2348
  {
2339
2349
  id: "frontend-plan-revision-pi",
2340
- depends_on: [
2341
- "frontend-plan-pi",
2342
- "frontend-design-review-pi",
2343
- ],
2350
+ depends_on: ["frontend-plan-pi", "frontend-design-review-pi"],
2344
2351
  runIf: "$.nodes['frontend-design-review-pi'].firstVerdictLine == 'VERDICT: request-revision'",
2345
2352
  role: "planner",
2346
2353
  executor: "pi",
@@ -2425,12 +2432,18 @@ async function buildFrontendHybridDagFromTask(sources) {
2425
2432
  ? ["not-needed"]
2426
2433
  : taskConfig.frontendMock?.policy === "required"
2427
2434
  ? ["native", "browser-intercept", "request-adapter"]
2428
- : ["native", "browser-intercept", "request-adapter", "not-needed"],
2435
+ : [
2436
+ "native",
2437
+ "browser-intercept",
2438
+ "request-adapter",
2439
+ "not-needed",
2440
+ ],
2429
2441
  artifactName: "frontend-implementation-contract.json",
2430
2442
  outputDir: "contracts",
2431
2443
  requireSourceFreshness: true,
2432
2444
  implementationWriteSet: implementPaths.writeSet,
2433
- openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence.normativePaths ?? [],
2445
+ openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence
2446
+ .normativePaths ?? [],
2434
2447
  },
2435
2448
  cwd: ".",
2436
2449
  timeoutMs: 60000,
@@ -2482,7 +2495,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2482
2495
  writerOutcomePolicy: {
2483
2496
  type: "implementation-outcome-v1",
2484
2497
  },
2485
- outputContract: "First non-empty line: IMPLEMENTATION_OUTCOME: changed | already-satisfied | blocked. Then a Markdown delivery summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
2498
+ outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a Markdown delivery summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
2486
2499
  subtask_prompt: [
2487
2500
  "Implement against the validated run-owned Frontend Implementation Contract from frontend-prewrite-gate-shell (path/schema/hash). Do not rebuild the contract from Markdown alone.",
2488
2501
  "The canonical contract already contains the approved requirement, target-file, UI-state, verification, design, and Mock/API decisions. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat broad repository research. Inspect only contract target files and directly related local code needed to implement them.",
@@ -2547,7 +2560,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2547
2560
  writerOutcomePolicy: {
2548
2561
  type: "implementation-outcome-v1",
2549
2562
  },
2550
- outputContract: "First non-empty line: IMPLEMENTATION_OUTCOME: changed | already-satisfied | blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
2563
+ outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
2551
2564
  subtask_prompt: [
2552
2565
  "Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
2553
2566
  "This node runs only for eligible=true. Apply the smallest fix for the classified repairable failure inside the original implement writeSet only.",
@@ -3454,107 +3467,190 @@ async function buildBackendTestHybridDag(sources) {
3454
3467
  const forbidden = commonForbiddenPaths(sources);
3455
3468
  const intake = await buildBackendTestIntakeContext(sources);
3456
3469
  const shellNode = (id, depends_on, pipeline, prompt, outputContract, commands = [], timeoutMs = 60000) => ({
3457
- id, depends_on, role: "verifier", executor: "shell", complexity: "LOW",
3458
- writePolicy: "read-only", allowedPaths: ro, forbiddenPaths: forbidden,
3459
- outputContract, subtask_prompt: prompt,
3460
- shell: { commands, backendTestPipeline: pipeline, cwd: ".", timeoutMs,
3461
- ...(commands.length ? { envAllowlist: collectBackendTestShellEnvAllowlist(sources) } : {}) },
3470
+ id,
3471
+ depends_on,
3472
+ role: "verifier",
3473
+ executor: "shell",
3474
+ complexity: "LOW",
3475
+ writePolicy: "read-only",
3476
+ allowedPaths: ro,
3477
+ forbiddenPaths: forbidden,
3478
+ outputContract,
3479
+ subtask_prompt: prompt,
3480
+ shell: {
3481
+ commands,
3482
+ backendTestPipeline: pipeline,
3483
+ cwd: ".",
3484
+ timeoutMs,
3485
+ ...(commands.length
3486
+ ? { envAllowlist: collectBackendTestShellEnvAllowlist(sources) }
3487
+ : {}),
3488
+ },
3462
3489
  });
3463
- const environment = shellNode("validate-backend-test-environment-shell", [], "markdown-environment", "Fail fast before model work when Python/pytest cannot run in the clean shell. Inspect only bounded common config, conftest, test-root and server-entry candidates; never read .env values or credentials.", "Run-owned reports/backend-test-environment.md with PASS/FAIL runtime, bounded project discovery, fixture and HTML-renderer facts; no secret values.", ["python --version", "python -m pytest --version", "python -m pytest --help"]);
3490
+ const environment = shellNode("validate-backend-test-environment-shell", [], "markdown-environment", "Fail fast before model work when Python/pytest cannot run in the clean shell. Inspect only bounded common config, conftest, test-root and server-entry candidates; never read .env values or credentials.", "Run-owned reports/backend-test-environment.md with PASS/FAIL runtime, bounded project discovery, fixture and HTML-renderer facts; no secret values.", [
3491
+ "python --version",
3492
+ "python -m pytest --version",
3493
+ "python -m pytest --help",
3494
+ ]);
3464
3495
  const generateCases = {
3465
- id: "generate-backend-md-cases-pi", depends_on: [environment.id], role: "implementer",
3466
- executor: "pi", toolProfile: "write", complexity: "MED", writePolicy: "exclusive",
3467
- writeSet: ["testcase/md/**"], allowedPaths: ["testcase/md/**"], forbiddenPaths: forbidden,
3496
+ id: "generate-backend-md-cases-pi",
3497
+ depends_on: [environment.id],
3498
+ role: "implementer",
3499
+ executor: "pi",
3500
+ toolProfile: "write",
3501
+ complexity: "MED",
3502
+ writePolicy: "exclusive",
3503
+ writeSet: ["testcase/md/**"],
3504
+ allowedPaths: ["testcase/md/**"],
3505
+ forbiddenPaths: forbidden,
3468
3506
  outputContract: "Write a Chinese, human-readable testcase/md/README.md plus module Markdown case cards using BE-<MODULE>-<NNN>; keep machine IDs/literals exact and do not execute pytest or modify production code/config.",
3469
3507
  subtask_prompt: [
3470
3508
  "Read the upstream environment report. Generate a Markdown-first backend test strategy and cases under testcase/md/**.",
3471
3509
  "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.",
3472
3510
  "Create 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.",
3473
- "Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`) and never a bare number. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.",
3511
+ "Before 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`. Classify from authoritative task/reference evidence, not merely whether a route already exists. Use only these pairs: `new-operation` `full-contract`; `contract-change` `affected-contract-full`; `behavior-change` `affected-behavior-full`; `bugfix` `reproduction-plus-neighbors`; `implementation-optimization` `change-focused-plus-regression-floor`. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.",
3512
+ "Coverage depth follows the declared change scope. For `new-operation`, fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected new operation, but do not re-test unrelated existing operations. For contract/behavior changes, fully cover the changed contract or behavior and its directly affected operations. For bugfix, cover exact reproduction, adjacent boundary/equivalence cases and a normal path. For `implementation-optimization`, cover explicit ACs, deterministic affected operations and a minimum regression floor; do not exhaustively regenerate unrelated POST/PUT/GET/DELETE rules. Every non-new classification must include `main-success-path` and `unchanged-response-shape`; contract changes also include `changed-contract-boundaries`, behavior changes `affected-state-transition`, and bugfixes `defect-reproduction` plus `adjacent-boundary`. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same changed path can affect them; unresolved impact stays visible as GAP/CONFLICT.",
3513
+ "Before 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.",
3514
+ "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.",
3515
+ "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.",
3516
+ "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.",
3517
+ "Write each module as readable case cards. 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 `### 自动化映射`; `覆盖规则` 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.",
3474
3518
  "Name each module file with a stable lowercase business stem such as `testcase/md/health.md` or `testcase/md/resource_notes.md`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. 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.",
3475
- "Place steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
3476
- "In `自动化映射`, record the planned script path and pytest function name when known, and keep the script path identical to the module one-to-one path above. 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.",
3477
- intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
3519
+ "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 ‘符合预期’.",
3520
+ "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. Before finalizing Markdown, 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.",
3521
+ intake.boundedSourceContext,
3522
+ "## Authoritative reference index",
3523
+ JSON.stringify(intake.referenceIndex, null, 2),
3478
3524
  "For 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.",
3479
3525
  "Read only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text.",
3480
3526
  ].join("\n\n"),
3481
3527
  };
3482
3528
  const reviewCases = {
3483
- id: "review-and-revise-backend-md-cases-pi", depends_on: [generateCases.id], role: "reviewer",
3484
- executor: "pi", toolProfile: "write", complexity: "MED", writePolicy: "exclusive",
3485
- writeSet: ["testcase/md/**"], allowedPaths: ["testcase/md/**"], forbiddenPaths: forbidden,
3529
+ id: "review-and-revise-backend-md-cases-pi",
3530
+ depends_on: [generateCases.id],
3531
+ role: "reviewer",
3532
+ executor: "pi",
3533
+ toolProfile: "write",
3534
+ complexity: "MED",
3535
+ writePolicy: "exclusive",
3536
+ writeSet: ["testcase/md/**"],
3537
+ allowedPaths: ["testcase/md/**"],
3538
+ forbiddenPaths: forbidden,
3486
3539
  outputContract: "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
3487
3540
  subtask_prompt: [
3488
3541
  "Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
3489
- "Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), and missing or drifted script/function mapping where it can be derived.",
3490
- "Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` `BE-RESOURCE-NOTES-001`) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
3542
+ "Independently reconstruct the change classification, affected operations/rules, P0 product scenarios and applicable P1 documented API rules from authoritative sources before trusting the generated Coverage Scope or Coverage Matrix. Perform an explicit coverage-scope review: reject `new-operation` when the task only optimizes an existing implementation without contract change; reject narrow optimization scope when shared validator/helper/DTO/query builder evidence directly affects more operations; reject full-contract expansion across unrelated operations. For every in-scope operation, check applicable lifecycle/uniqueness states (including deleted-existing when in scope), valid enum values, bounded invalid classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, status/error codes, auth and state transitions. Directly add in-scope omissions; undefined impact remains GAP/CONFLICT rather than invented behavior.",
3543
+ "Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in `testcase/md/README.md`, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
3544
+ "Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol, assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, ensure every binding-list Test Point is also present in that Case's `### 测试点`, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
3491
3545
  "Read only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
3492
- intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
3546
+ intake.boundedSourceContext,
3547
+ "## Authoritative reference index",
3548
+ JSON.stringify(intake.referenceIndex, null, 2),
3493
3549
  "For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. 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.",
3494
3550
  ].join("\n\n"),
3495
3551
  };
3496
- const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for missing/duplicate IDs, missing core sections (preconditions, steps, expected results), AC coverage, executable steps, assertable results or placeholders. Do not validate source-reference existence. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected so downstream pytest/report nodes cannot consume them.", "Run-owned reports/backend-md-case-validation.md with PASS/FAIL advisory findings; downstream execution continues.");
3552
+ const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope and Coverage Matrix against final Case rule/test-point bindings. Validate the classification-policy pair, affected operations/rules, scope evidence and regression floor; require documented OpenAPI completeness only for declared affected operations, while all explicit AC/REQ/BR remain in scope. Detect missing in-scope product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT, bidirectional Matrix/Case drift, non-canonical Case IDs, unclassified Test Points, duplicate binding modes and non-cross-cutting Test Points bound by multiple Cases. Do not validate source-reference existence. Write human and machine evidence from the same facts. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected. Coverage FAIL stays advisory.", "Run-owned reports/backend-md-case-validation.md, reports/backend-test-case-coverage-analysis.md and contracts/backend-test-case-coverage-facts.json v3 with Coverage Scope plus PASS/FAIL/UNAVAILABLE advisory facts; downstream execution continues.");
3497
3553
  const generatePytest = {
3498
- id: "generate-backend-pytest-pi", depends_on: [validateCases.id], role: "implementer",
3499
- executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive",
3500
- writeSet: ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"],
3501
- allowedPaths: Array.from(new Set([...ro, "testcase/**"])), forbiddenPaths: forbidden,
3554
+ id: "generate-backend-pytest-pi",
3555
+ depends_on: [validateCases.id],
3556
+ role: "implementer",
3557
+ executor: "pi",
3558
+ toolProfile: "write",
3559
+ complexity: "HIGH",
3560
+ writePolicy: "exclusive",
3561
+ writeSet: [
3562
+ "testcase/**/test_*.py",
3563
+ "testcase/**/helpers/**",
3564
+ "testcase/**/factories/**",
3565
+ ],
3566
+ allowedPaths: Array.from(new Set([...ro, "testcase/**"])),
3567
+ forbiddenPaths: forbidden,
3502
3568
  outputContract: "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring. Each testcase/md/<module>.md (excluding README.md) maps one-to-one to testcase/test_<module>.py; never merge or split modules. No JSON and no pytest execution.",
3503
3569
  subtask_prompt: [
3504
3570
  "Convert testcase/md/** to pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
3505
- "Ensure every final Markdown Case ID appears in at least one real pytest test function or pytest test class method region, preferably as `test_BE_<MODULE>_<NNN>_<description>` and in that function/method docstring. Module-level functions and class-based pytest methods are both supported. Multiple test functions may cover one Case ID; assertions come only from 预期结果/Expected Results and setup comes only from 前置条件 plus any optional 测试数据/自动化映射 or their legacy English aliases.",
3571
+ "Ensure every final Markdown Case ID appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id=\"TP-...\")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. Use `pytest.param(..., id=...)` for every row; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task's explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.",
3506
3572
  "Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`, `testcase/md/health.md` maps to `testcase/test_health.py`, `testcase/md/BE-HEALTH.md` maps to `testcase/test_be_health.py`, and `testcase/md/order-api.md` maps to `testcase/test_order_api.py`. If Markdown automation mapping names a different path than this module stem path, still write the module stem path and do not invent prefixes such as `test_be_*` unless the module filename itself normalizes to that stem. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
3507
3573
  "Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
3574
+ "HTTP response header names are case-insensitive. If the helper stores a lower-case normalized header map, every Content-Type or other header assertion must query the lower-case key (for example `content-type`) or use an explicitly case-insensitive accessor; never call a case-sensitive plain dict with `Content-Type` when the stored key is lower-case. Preserve the actual media-type assertion rather than dropping it.",
3508
3575
  "Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
3509
3576
  "Before logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.",
3510
3577
  "Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
3511
3578
  ].join("\n\n"),
3512
3579
  };
3513
- const traceability = shellNode("backend-test-traceability-gate-shell", [generatePytest.id], "markdown-traceability", "Record advisory findings when a real Markdown case heading has no associated pytest test function or class method in the script explicitly mapped by that Markdown case, or when a mapped HTTP test script lacks request parameters logging, response result logging, recursive redaction or bounded truncation evidence. Accept exact Case IDs in the function/method name or its decorator/body/docstring region. Do not scan unrelated test_*.py files and do not block pytest execution.", "Run-owned reports/backend-test-traceability.md with PASS/FAIL advisory findings for Markdown Case to mapped pytest script/symbol coverage.");
3580
+ const traceability = shellNode("backend-test-traceability-gate-shell", [generatePytest.id], "markdown-traceability", "Deterministically scan only Markdown-mapped pytest scripts. Keep the existing traceability/logging checks and produce a bidirectional Markdown module/Case/Test Point pytest file/primary symbol correspondence analysis. Map variant Test Points from stable parameter IDs, assertion Test Points from the primary symbol docstring, and cross-cutting Test Points from the primary symbol evidence binding. Report 1:1, 1:0, 1:N, 0:1, script/primary-symbol mismatch, missing Case ID, missing variant parameter IDs, missing assertion/cross-cutting bindings, duplicate modes and extra bindings. Human and machine evidence must come from the same facts. Findings are advisory and never block pytest.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md and contracts/backend-test-markdown-pytest-correspondence-facts.json with PASS/FAIL/UNAVAILABLE correspondence facts.");
3581
+ const manifest = shellNode("backend-test-case-manifest-shell", [traceability.id], "markdown-manifest", "Materialize the canonical Backend Test Case Manifest only from contracts/backend-test-case-coverage-facts.json and contracts/backend-test-markdown-pytest-correspondence-facts.json. Validate schema, task binding, input hashes and freshness; never re-read source semantics, re-analyze Coverage Matrix, rescan pytest symbols or recompute a second set of metrics. Missing/stale/conflicting facts produce partial/unavailable diagnostics rather than fabricated zeros.", "Run-owned contracts/backend-test-case-manifest.json with materializationStatus, sourceFactsIssues, validated coverageScope, coverageSummary, ruleCoverageSummary and correspondenceSummary; this is the single machine input for L-5 and closeout.");
3514
3582
  const pytestCommand = [
3515
3583
  'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
3516
3584
  'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
3517
3585
  ].join("; ");
3518
- const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 4 Markdown validation and node 6 Markdown-to-pytest traceability expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
3586
+ const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 4 Markdown validation + case coverage and node 6 traceability + Markdown-to-pytest correspondence expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
3519
3587
  if (execute.shell) {
3520
3588
  execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
3521
3589
  }
3522
3590
  const canWriteReport = taskAllowsBackendTestReportWrite(sources);
3523
3591
  const report = {
3524
- id: "backend-test-report-and-l5-pi", depends_on: [execute.id], role: "closeout", executor: "pi", complexity: "MED",
3592
+ id: "backend-test-report-and-l5-pi",
3593
+ depends_on: [execute.id],
3594
+ role: "closeout",
3595
+ executor: "pi",
3596
+ complexity: "MED",
3525
3597
  ...(canWriteReport
3526
- ? { toolProfile: "write", writePolicy: "exclusive", writeSet: ["docs/test-reports/**"], allowedPaths: ["docs/test-reports/**"] }
3598
+ ? {
3599
+ toolProfile: "write",
3600
+ writePolicy: "exclusive",
3601
+ writeSet: ["docs/test-reports/**"],
3602
+ allowedPaths: ["docs/test-reports/**"],
3603
+ }
3527
3604
  : { writePolicy: "read-only", allowedPaths: ro }),
3528
3605
  forbiddenPaths: forbidden,
3529
- outputContract: canWriteReport ? "Final Markdown report and L-5 conclusion under docs/test-reports/**; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked, not re-rendered; no JSON." : "Final Markdown report and L-5 conclusion in assistant output; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked; no JSON or writes.",
3606
+ outputContract: canWriteReport
3607
+ ? "Final Markdown report and L-5 conclusion under docs/test-reports/**; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked, not re-rendered; no JSON."
3608
+ : "Final Markdown report and L-5 conclusion in assistant output; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked; no JSON or writes.",
3530
3609
  subtask_prompt: [
3531
- "Generate the final Markdown report from upstream facts and run-owned environment, advisory case-validation, advisory traceability, pytest-html and HTML evidence. Do not emit JSON.",
3610
+ "Generate the final Markdown report only from authoritative run-owned artifacts. Read node 1 reports/backend-test-environment.md; node 4 backend-md-case-validation.md and backend-test-case-coverage-analysis.md; node 6 backend-test-traceability.md and backend-test-markdown-pytest-correspondence.md; node 7 contracts/backend-test-case-manifest.json; and node 8 backend-test-result.json, backend-test-facts.md, pytest-html/HTML and L-5 dashboard. Do not use node 2/3/5 assistant prose as facts. Do not emit JSON.",
3532
3611
  "Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion.",
3533
- "The L-5 metrics and visualization are already produced deterministically by node 7 at reports/backend-test-l5-dashboard.html (rendered from computeL5ReportMetrics). Link to that dashboard as the authoritative L-5 view; do not recompute pass/AC/automation/coverage numbers or re-render an HTML dashboard yourself. Quote its L-5 decision verbatim.",
3534
- "Always state the exact PASS/FAIL status and findings from nodes 4 and 6. Their FAIL status does not block pytest, but it must remain visible as a quality/traceability risk and must never be rewritten as PASS.",
3535
- "Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY.",
3612
+ "The L-5 metrics and visualization are produced deterministically by node 8 at reports/backend-test-l5-dashboard.html. Link to that dashboard as the authoritative L-5 view. Pytest execution facts come from node 8; coverage/correspondence numbers and materializationStatus come from node 7; detailed coverage findings come from node 4; detailed mapping findings come from node 6. Never recompute these values. If machine manifest and human reports disagree, report evidence inconsistency rather than silently choosing.",
3613
+ "Always state the exact Coverage Scope classification, policy, affected operations, regression floor, completeness claim, PASS/FAIL/UNAVAILABLE status and findings from node 4 case validation + coverage, plus node 6 traceability + correspondence. Affected-scope or affected-operations-full coverage must never be described as whole-API completeness unless every operation is explicitly listed. Their FAIL status does not block pytest, but it must remain visible and must never be rewritten as PASS.",
3614
+ "Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY. Distinguish Markdown Case count, primary pytest symbol count, collected pytest item count, variant/assertion/cross-cutting Test Point counts and execution amplification; never describe pytest item count as the number of business scenarios.",
3536
3615
  "Never override Shell/pytest-html facts. L-5 requires pass=100%, AC=100%, automation>=90%, line>=80%, branch>=70%, skipped=0 and no blocking Critical risk.",
3537
- canWriteReport ? "Write only under docs/test-reports/**." : "Keep the full report in assistant output.",
3616
+ canWriteReport
3617
+ ? "Write only under docs/test-reports/**."
3618
+ : "Keep the full report in assistant output.",
3538
3619
  ].join("\n\n"),
3539
3620
  };
3540
3621
  const spec = {
3541
- version: 3, title: `Backend test DAG: ${taskConfig.title}`,
3622
+ version: 3,
3623
+ title: `Backend test DAG: ${taskConfig.title}`,
3542
3624
  runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
3543
3625
  outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3544
3626
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
3545
3627
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
3546
3628
  globalConstraints: [
3547
- ...taskConfig.hardConstraints, ...STANDARD_GLOBAL_CONSTRAINTS,
3548
- "backend-test-dag uses exactly 8 real top-level tasks and executes pytest exactly once over only the safe scripts explicitly mapped by final Markdown cases.",
3629
+ ...taskConfig.hardConstraints,
3630
+ ...STANDARD_GLOBAL_CONSTRAINTS,
3631
+ "backend-test-dag uses exactly 9 real top-level tasks and executes pytest exactly once over only the safe scripts explicitly mapped by final Markdown cases.",
3549
3632
  "Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
3550
- "Environment, advisory Markdown validation, advisory traceability, pytest-html, HTML and execution facts are deterministic evidence. Nodes 4 and 6 record findings without blocking nodes 5, 7 or 8.",
3633
+ "Environment, advisory Markdown validation/coverage, advisory traceability/correspondence, canonical manifest, pytest-html, HTML and execution facts are deterministic evidence. Nodes 4 and 6 record findings without blocking nodes 5, 7 or 8; node 7 partial/unavailable does not block node 8.",
3551
3634
  "Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
3552
- "Functional case IDs use BE-<MODULE>-<NNN>; production code/config, skip/xfail, repair and rerun are forbidden.",
3635
+ "Functional case IDs use canonical BE-<MODULE>-<NNN> with exactly three digits and no alphabetic suffix. Every Test Point has exactly one variant/assertion/cross-cutting binding; only variant bindings create pytest parameter items. Production code/config, skip/xfail, repair and rerun are forbidden.",
3553
3636
  ],
3554
- defaults: { ...BACKEND_TEST_DEFAULTS, contextProfile: taskConfig.contextProfile },
3637
+ defaults: {
3638
+ ...BACKEND_TEST_DEFAULTS,
3639
+ contextProfile: taskConfig.contextProfile,
3640
+ },
3555
3641
  skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
3556
3642
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3557
- tasks: [environment, generateCases, reviewCases, validateCases, generatePytest, traceability, execute, report],
3643
+ tasks: [
3644
+ environment,
3645
+ generateCases,
3646
+ reviewCases,
3647
+ validateCases,
3648
+ generatePytest,
3649
+ traceability,
3650
+ manifest,
3651
+ execute,
3652
+ report,
3653
+ ],
3558
3654
  };
3559
3655
  applyDefaultReadOnlyRetryPolicy(spec);
3560
3656
  parseDagSpec(spec);
@@ -3604,7 +3700,8 @@ function buildFrontendTestHybridDag(sources) {
3604
3700
  "const issues=[];",
3605
3701
  "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
3606
3702
  "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
3607
- "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;", ,
3703
+ "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
3704
+ ,
3608
3705
  "const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
3609
3706
  "for(const c of manifest.cases){",
3610
3707
  " const id=c&&c.caseId||'?';",
@@ -3614,7 +3711,8 @@ function buildFrontendTestHybridDag(sources) {
3614
3711
  " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
3615
3712
  " if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
3616
3713
  " const body=fs.readFileSync(casePath,'utf8');",
3617
- " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>; playwright-cli is strongly recommended for browser execution'});", ,
3714
+ " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>; playwright-cli is strongly recommended for browser execution'});",
3715
+ ,
3618
3716
  " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
3619
3717
  " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
3620
3718
  " if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
@@ -3847,7 +3945,12 @@ function buildFrontendTestHybridDag(sources) {
3847
3945
  forbiddenPaths: forbidden,
3848
3946
  outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and AC mapping; alternative executable tool commands are not inspected or blocked.",
3849
3947
  subtask_prompt: "Scan generated cases/manifest for structural and safety rules only. Strongly recommend playwright-cli for browser execution, but do not inspect or reject alternative executable tool commands and do not use free-form LLM verdicts.",
3850
- shell: { commands: [], frontendTestCaseChecklist: {}, cwd: ".", timeoutMs: 120000 },
3948
+ shell: {
3949
+ commands: [],
3950
+ frontendTestCaseChecklist: {},
3951
+ cwd: ".",
3952
+ timeoutMs: 120000,
3953
+ },
3851
3954
  }, {
3852
3955
  id: "materialize-frontend-case-manifest-shell",
3853
3956
  depends_on: ["frontend-case-checklist-shell"],
@@ -3923,7 +4026,12 @@ function buildFrontendTestHybridDag(sources) {
3923
4026
  forbiddenPaths: forbidden,
3924
4027
  outputContract: "Deterministic evidence gate: missing/malformed evidence is advisory; only unsafe evidenceDir or evidence paths hard-fail. Does not block retrospect.",
3925
4028
  subtask_prompt: "Validate frontend case evidence before result materialization. Keep missing or malformed evidence as advisory findings; only path-escape failures abort the node.",
3926
- shell: { commands: [], frontendTestEvidenceValidation: {}, cwd: ".", timeoutMs: 120000 },
4029
+ shell: {
4030
+ commands: [],
4031
+ frontendTestEvidenceValidation: {},
4032
+ cwd: ".",
4033
+ timeoutMs: 120000,
4034
+ },
3927
4035
  }, {
3928
4036
  id: "materialize-frontend-test-result-shell",
3929
4037
  depends_on: ["validate-frontend-case-evidence-shell"],
@@ -3947,10 +4055,24 @@ function buildFrontendTestHybridDag(sources) {
3947
4055
  timeoutMs: 120000,
3948
4056
  },
3949
4057
  });
4058
+ tasks.push({
4059
+ id: "frontend-test-l5-report-shell",
4060
+ depends_on: ["materialize-frontend-test-result-shell"],
4061
+ role: "verifier",
4062
+ executor: "shell",
4063
+ complexity: "LOW",
4064
+ writePolicy: "exclusive",
4065
+ writeSet: ["testcase/frontend/reports/**"],
4066
+ allowedPaths: ["testcase/frontend/**"],
4067
+ forbiddenPaths: forbidden,
4068
+ outputContract: "Deterministic frontend L-5 Markdown and self-contained HTML dashboard derived only from frontend-test-result-v1.",
4069
+ subtask_prompt: "Render the authoritative frontend L-5 report from frontend-test-result-v1. Do not use Pi prose or invent code coverage. Missing line/branch coverage remains unavailable and makes L-5 NOT READY.",
4070
+ shell: { frontendTestL5Report: {}, commands: [], cwd: ".", timeoutMs: 120000 },
4071
+ });
3950
4072
  if (strictOutcomeGate) {
3951
4073
  tasks.push({
3952
4074
  id: "frontend-test-result-outcome-gate-shell",
3953
- depends_on: ["materialize-frontend-test-result-shell"],
4075
+ depends_on: ["materialize-frontend-test-result-shell", "frontend-test-l5-report-shell"],
3954
4076
  role: "verifier",
3955
4077
  executor: "shell",
3956
4078
  complexity: "LOW",
@@ -3968,7 +4090,7 @@ function buildFrontendTestHybridDag(sources) {
3968
4090
  }
3969
4091
  tasks.push({
3970
4092
  id: "frontend-test-retrospect-pi",
3971
- depends_on: ["materialize-frontend-test-result-shell"],
4093
+ depends_on: ["materialize-frontend-test-result-shell", "frontend-test-l5-report-shell"],
3972
4094
  role: "closeout",
3973
4095
  executor: "pi",
3974
4096
  toolProfile: "write",
@@ -3978,7 +4100,7 @@ function buildFrontendTestHybridDag(sources) {
3978
4100
  allowedPaths: ["testcase/frontend/**"],
3979
4101
  forbiddenPaths: forbidden,
3980
4102
  outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete. Pipeline acceptance = this report exists (not case 100% pass).",
3981
- subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
4103
+ subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization and L-5 report generation. Combine AC→case→browser-evidence review with the closeout report: coverage, the exact L-5 READY/NOT READY decision, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not recompute L-5 metrics or replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
3982
4104
  });
3983
4105
  tasks.push({
3984
4106
  id: "frontend-test-html-report-shell",
@@ -3992,7 +4114,12 @@ function buildFrontendTestHybridDag(sources) {
3992
4114
  forbiddenPaths: forbidden,
3993
4115
  outputContract: "Write testcase/frontend/reports/frontend-test-report.md and frontend-test-report.html from frontend-test-result-v1, containing only case execution results, case content, and failed/blocked error analysis.",
3994
4116
  subtask_prompt: "Render the formal frontend test Markdown and HTML report from the current run frontend-test-result-v1. Do not include evidence chains, evidence paths or hashes, advisory findings, quality suggestions, improvement suggestions, or ratings.",
3995
- shell: { commands: [], frontendTestHtmlReport: {}, cwd: ".", timeoutMs: 120000 },
4117
+ shell: {
4118
+ commands: [],
4119
+ frontendTestHtmlReport: {},
4120
+ cwd: ".",
4121
+ timeoutMs: 120000,
4122
+ },
3996
4123
  });
3997
4124
  const globalConstraints = [
3998
4125
  ...sources.taskConfig.hardConstraints,
@@ -5114,7 +5241,7 @@ function cloneTask(task, patch = {}) {
5114
5241
  }
5115
5242
  /**
5116
5243
  * Apply the default read-only Pi retry policy to safe planner/scout/reviewer/
5117
- * verifier/closeout Pi nodes in the generated DAG. Writers, supervisors,
5244
+ * verifier/supervisor/closeout Pi nodes in the generated DAG. Writers,
5118
5245
  * dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
5119
5246
  * never overwrites an explicit retryPolicy a task already declares.
5120
5247
  */
@@ -5233,6 +5360,14 @@ function buildReviewGateNode(sources) {
5233
5360
  }
5234
5361
  function enableProjectGovernanceOnNode(task) {
5235
5362
  task.governanceStandardReview = true;
5363
+ task.outputProtocol = REVIEW_VERDICT_OUTPUT_PROTOCOL;
5364
+ task.retryPolicy = PROTOCOL_AWARE_PI_RETRY_POLICY;
5365
+ task.outputContract =
5366
+ "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision; unresolved mandatory governance violations force request-revision. No file writes.";
5367
+ const protocolInstruction = "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.";
5368
+ if (!task.subtask_prompt.includes(protocolInstruction)) {
5369
+ task.subtask_prompt = `${protocolInstruction}\n\n${task.subtask_prompt}`;
5370
+ }
5236
5371
  }
5237
5372
  /**
5238
5373
  * Apply governance only to general implementation DAGs, and only when the
@@ -5309,6 +5444,7 @@ function insertGovernanceStandardGate(spec, sources) {
5309
5444
  const closeoutIndex = spec.tasks.findIndex((task) => task.id === "closeout-pi");
5310
5445
  spec.tasks.splice(closeoutIndex, 0, gate);
5311
5446
  closeout.depends_on = ["governance-standard-gate-shell"];
5447
+ closeout.failureAwareDependsOn = ["governance-standard-gate-shell"];
5312
5448
  }
5313
5449
  function buildReviewGatedHybridDag(standard, sources) {
5314
5450
  const spec = {
@@ -5322,7 +5458,10 @@ function buildReviewGatedHybridDag(standard, sources) {
5322
5458
  tasks: standard.tasks.map((task) => ({ ...task })),
5323
5459
  };
5324
5460
  const closeout = getTaskOrThrow(spec, "closeout-pi");
5325
- replaceTask(spec, cloneTask(closeout, { depends_on: ["review-gate-shell"] }));
5461
+ replaceTask(spec, cloneTask(closeout, {
5462
+ depends_on: ["review-gate-shell"],
5463
+ failureAwareDependsOn: ["review-gate-shell"],
5464
+ }));
5326
5465
  spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
5327
5466
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
5328
5467
  applyDefaultReadOnlyRetryPolicy(spec);
@@ -5528,6 +5667,7 @@ function buildProcessSupervisorNode(sources) {
5528
5667
  allowedPaths: commonReadOnlyPaths(sources),
5529
5668
  forbiddenPaths: commonForbiddenPaths(sources),
5530
5669
  outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by a REPAIR_ARTIFACT_JSON fenced block. Audit writeSet coverage, boundary drift, verification gaps, repair scope. No file writes.",
5670
+ outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
5531
5671
  subtask_prompt: [
5532
5672
  "Supervise the implementation process after soft verification.",
5533
5673
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
@@ -5663,6 +5803,53 @@ function buildDecisionNode(sources) {
5663
5803
  decisionGate: { enabled: true, schemaVersion: 1, mode: "record-only" },
5664
5804
  };
5665
5805
  }
5806
+ /**
5807
+ * Supervised convergence chain node ids. Extends the controller default
5808
+ * (process-supervisor → process-gate → repair → hard-verify) with the review
5809
+ * chain so that a legitimate review `request-revision` re-enters the same
5810
+ * bounded repair-reverify-review loop instead of only blocking closeout.
5811
+ */
5812
+ export const SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS = [
5813
+ "process-supervisor-pi",
5814
+ "process-gate-shell",
5815
+ "repair-pi",
5816
+ "hard-verify-shell",
5817
+ "review-pi",
5818
+ "review-verdict-recovery-pi",
5819
+ "review-gate-shell",
5820
+ ];
5821
+ /**
5822
+ * Derive the supervised DAG convergence spec from `maxFixLoops`. Unlike the
5823
+ * task-level `convergence` config (which stays opt-in / disabled by default
5824
+ * until smoke evidence is stronger), the supervised path uses `maxFixLoops`
5825
+ * as the authoritative bounded-repair budget:
5826
+ * - `maxFixLoops > 0` ⇒ convergence enabled, `maxPasses = maxFixLoops + 1`
5827
+ * (initial execution + repair budget), with the full supervised chain.
5828
+ * - `maxFixLoops === 0` ⇒ convergence disabled (no automatic repair).
5829
+ * An explicit user `convergence.enabled: true` does not override `maxFixLoops`;
5830
+ * the budget still wins so total pass count stays consistent (AC1/AC2).
5831
+ */
5832
+ function resolveSupervisedConvergence(taskConfig) {
5833
+ const maxFixLoops = taskConfig.maxFixLoops;
5834
+ if (maxFixLoops <= 0) {
5835
+ return {
5836
+ enabled: false,
5837
+ maxPasses: 1,
5838
+ stopOnVerdictPass: true,
5839
+ stopOnHardVerifyPass: true,
5840
+ pauseOnRegression: true,
5841
+ chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
5842
+ };
5843
+ }
5844
+ return {
5845
+ enabled: true,
5846
+ maxPasses: maxFixLoops + 1,
5847
+ stopOnVerdictPass: true,
5848
+ stopOnHardVerifyPass: true,
5849
+ pauseOnRegression: taskConfig.convergence?.pauseOnRegression ?? true,
5850
+ chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
5851
+ };
5852
+ }
5666
5853
  function buildSupervisedHybridDag(standard, sources) {
5667
5854
  const contract = getTaskOrThrow(standard, "contract-pi");
5668
5855
  const scoutSrc = getTaskOrThrow(standard, "scout-src");
@@ -5687,7 +5874,7 @@ function buildSupervisedHybridDag(standard, sources) {
5687
5874
  title: `Supervised ${standard.title}`,
5688
5875
  objective: `${standard.objective ?? ""}\n\nRoute: supervised implementation DAG selected by workflowPolicy/governanceProfile or explicit CLI profile.`.trim(),
5689
5876
  globalConstraints: supervisedConstraints,
5690
- convergence: sources.taskConfig.convergence,
5877
+ convergence: resolveSupervisedConvergence(sources.taskConfig),
5691
5878
  verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig, "1"),
5692
5879
  tasks: [
5693
5880
  cloneTask(contract),
@@ -5732,7 +5919,10 @@ function buildSupervisedHybridDag(standard, sources) {
5732
5919
  buildReviewVerdictRecoveryNode(sources),
5733
5920
  buildReviewGateNode(sources),
5734
5921
  buildDecisionNode(sources),
5735
- cloneTask(closeout, { depends_on: ["decision-pi"] }),
5922
+ cloneTask(closeout, {
5923
+ depends_on: ["decision-pi"],
5924
+ failureAwareDependsOn: ["decision-pi"],
5925
+ }),
5736
5926
  ],
5737
5927
  };
5738
5928
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());