@tea-agent/loop-agent 0.25.3 → 0.25.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/application/dag/args.js +21 -1
  4. package/dist/application/dag/run-dag.js +1 -0
  5. package/dist/cli/command-definitions.js +1 -1
  6. package/dist/cli/program.js +82 -63
  7. package/dist/commands/client-recovery.js +209 -62
  8. package/dist/commands/init.js +206 -82
  9. package/dist/commands/run-dag-progress.js +109 -0
  10. package/dist/commands/run-dag.js +16 -5
  11. package/dist/executors/dag-pi-executor.js +80 -15
  12. package/dist/executors/model-routing.js +1 -1
  13. package/dist/executors/shell-executor.js +159 -0
  14. package/dist/executors/shell-write-guard.js +21 -7
  15. package/dist/worker/console/repo-fingerprint.js +7 -1
  16. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
  17. package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
  18. package/dist/workflows/dag/backend-test-markdown-workflow.js +306 -30
  19. package/dist/workflows/dag/backend-test-result-contract.js +35 -9
  20. package/dist/workflows/dag/convergence/controller.js +134 -9
  21. package/dist/workflows/dag/frontend-test-case-checklist.js +71 -0
  22. package/dist/workflows/dag/frontend-test-html-report.js +77 -0
  23. package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
  24. package/dist/workflows/dag/frontend-test-result-contract.js +44 -1
  25. package/dist/workflows/dag/init-hybrid.js +267 -80
  26. package/dist/workflows/dag/node-execution.js +64 -11
  27. package/dist/workflows/dag/prompt.js +118 -4
  28. package/dist/workflows/dag/retry-policy.js +5 -4
  29. package/dist/workflows/dag/scheduler.js +32 -5
  30. package/dist/workflows/dag/types.js +10 -3
  31. package/dist/workflows/dag/validate.js +6 -3
  32. package/docs/architecture/dag-execution.md +7 -4
  33. package/docs/architecture/runtime-boundaries.md +1 -1
  34. package/docs/templates/agent-dag.base.json +1 -1
  35. package/docs/templates/agent-dag.final-verification.json +1 -1
  36. package/docs/templates/agent-dag.schema.json +6 -0
  37. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  38. package/docs/templates/backend-test-dag.json +40 -13
  39. package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
  40. package/docs/templates/frontend-test-dag.json +62 -5
  41. package/docs/templates/hybrid-dag.json +1 -1
  42. package/examples/decision-gate-agent-dag.json +1 -1
  43. package/examples/example-dag.json +1 -1
  44. package/examples/hybrid-loop-agent-dag.json +1 -1
  45. package/harness.json +3 -2
  46. package/package.json +1 -1
  47. package/skills/loop-agent/references/command-reference.md +2 -1
  48. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  49. 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,55 +3467,106 @@ 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>|<中文用例名称>`. 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 writing cases, build a mandatory machine-readable Coverage Matrix. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix. 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.",
3512
+ "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.",
3513
+ "Coverage priority is strict: P0 product requirements/task hard constraints first; P1 exhaustively supplements documented API operations, fields, business rules, statuses and errors; P2 adds bounded protocol robustness only when it does not invent product behavior. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
3514
+ "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.",
3515
+ "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. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射`; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. Every automatable case explicitly names its target pytest script so traceability scans only that script.",
3474
3516
  "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
3517
  "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
3518
  "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
+ intake.boundedSourceContext,
3520
+ "## Authoritative reference index",
3521
+ JSON.stringify(intake.referenceIndex, null, 2),
3478
3522
  "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
3523
  "Read only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text.",
3480
3524
  ].join("\n\n"),
3481
3525
  };
3482
3526
  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,
3527
+ id: "review-and-revise-backend-md-cases-pi",
3528
+ depends_on: [generateCases.id],
3529
+ role: "reviewer",
3530
+ executor: "pi",
3531
+ toolProfile: "write",
3532
+ complexity: "MED",
3533
+ writePolicy: "exclusive",
3534
+ writeSet: ["testcase/md/**"],
3535
+ allowedPaths: ["testcase/md/**"],
3536
+ forbiddenPaths: forbidden,
3486
3537
  outputContract: "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
3487
3538
  subtask_prompt: [
3488
3539
  "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, 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 exact. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
3540
+ "Independently reconstruct P0 product scenarios and P1 documented API rules from authoritative sources before trusting the generated Coverage Matrix. Check every lifecycle/uniqueness state (including deleted-existing), every valid enum value, bounded invalid enum classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, endpoint, status/error codes, auth and state transitions. Directly add omissions; undefined expectations remain GAP/CONFLICT rather than invented behavior.",
3541
+ "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 unnumbered `## Coverage Matrix` heading, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point Case bindings. Directly repair malformed headings/rows/keys rather than merely commenting on them. 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.",
3542
+ "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.",
3491
3543
  "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),
3544
+ intake.boundedSourceContext,
3545
+ "## Authoritative reference index",
3546
+ JSON.stringify(intake.referenceIndex, null, 2),
3493
3547
  "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
3548
  ].join("\n\n"),
3495
3549
  };
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.");
3550
+ 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 Matrix against final Case rule/test-point bindings. Detect missing product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT and bidirectional Matrix/Case drift. 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 with PASS/FAIL/UNAVAILABLE advisory facts; downstream execution continues.");
3497
3551
  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,
3552
+ id: "generate-backend-pytest-pi",
3553
+ depends_on: [validateCases.id],
3554
+ role: "implementer",
3555
+ executor: "pi",
3556
+ toolProfile: "write",
3557
+ complexity: "HIGH",
3558
+ writePolicy: "exclusive",
3559
+ writeSet: [
3560
+ "testcase/**/test_*.py",
3561
+ "testcase/**/helpers/**",
3562
+ "testcase/**/factories/**",
3563
+ ],
3564
+ allowedPaths: Array.from(new Set([...ro, "testcase/**"])),
3565
+ forbiddenPaths: forbidden,
3502
3566
  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
3567
  subtask_prompt: [
3504
3568
  "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.",
3569
+ "Ensure every final Markdown Case ID appears in exactly one primary 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. When a Case has multiple `测试点`, use one parameterized primary symbol with stable `pytest.param(..., id=\"TP-...\")` IDs matching every Markdown Test Point, or split the Markdown into independent Cases before generation; do not create duplicate symbols for one Case. Enumerate the exact IDs from that Case's `测试点` section before writing parameters: every Markdown Test Point must appear once and no parameter ID may be invented, renamed or omitted. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.",
3506
3570
  "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
3571
  "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.",
3508
3572
  "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.",
@@ -3510,51 +3574,80 @@ async function buildBackendTestHybridDag(sources) {
3510
3574
  "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
3575
  ].join("\n\n"),
3512
3576
  };
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.");
3577
+ 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 also produce a bidirectional Markdown module/Case/Test Point pytest file/primary symbol/parameter ID correspondence analysis. Report 1:1, 1:0, 1:N, 0:1, script mismatch, missing Case ID and missing/extra parameter IDs. 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.");
3578
+ 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 coverageSummary, ruleCoverageSummary and correspondenceSummary; this is the single machine input for L-5 and closeout.");
3514
3579
  const pytestCommand = [
3515
3580
  'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
3516
3581
  'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
3517
3582
  ].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 测试结论, quality status, failure overview, and a polished per-case result card with 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);
3583
+ 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
3584
  if (execute.shell) {
3520
3585
  execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
3521
3586
  }
3522
3587
  const canWriteReport = taskAllowsBackendTestReportWrite(sources);
3523
3588
  const report = {
3524
- id: "backend-test-report-and-l5-pi", depends_on: [execute.id], role: "closeout", executor: "pi", complexity: "MED",
3589
+ id: "backend-test-report-and-l5-pi",
3590
+ depends_on: [execute.id],
3591
+ role: "closeout",
3592
+ executor: "pi",
3593
+ complexity: "MED",
3525
3594
  ...(canWriteReport
3526
- ? { toolProfile: "write", writePolicy: "exclusive", writeSet: ["docs/test-reports/**"], allowedPaths: ["docs/test-reports/**"] }
3595
+ ? {
3596
+ toolProfile: "write",
3597
+ writePolicy: "exclusive",
3598
+ writeSet: ["docs/test-reports/**"],
3599
+ allowedPaths: ["docs/test-reports/**"],
3600
+ }
3527
3601
  : { writePolicy: "read-only", allowedPaths: ro }),
3528
3602
  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.",
3603
+ outputContract: canWriteReport
3604
+ ? "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."
3605
+ : "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
3606
  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.",
3607
+ "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
3608
  "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.",
3609
+ "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.",
3610
+ "Always state the exact PASS/FAIL/UNAVAILABLE status and findings from node 4 case validation + coverage and node 6 traceability + correspondence. Their FAIL status does not block pytest, but it must remain visible and must never be rewritten as PASS.",
3535
3611
  "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.",
3536
3612
  "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.",
3613
+ canWriteReport
3614
+ ? "Write only under docs/test-reports/**."
3615
+ : "Keep the full report in assistant output.",
3538
3616
  ].join("\n\n"),
3539
3617
  };
3540
3618
  const spec = {
3541
- version: 3, title: `Backend test DAG: ${taskConfig.title}`,
3619
+ version: 3,
3620
+ title: `Backend test DAG: ${taskConfig.title}`,
3542
3621
  runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
3543
3622
  outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3544
3623
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
3545
3624
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
3546
3625
  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.",
3626
+ ...taskConfig.hardConstraints,
3627
+ ...STANDARD_GLOBAL_CONSTRAINTS,
3628
+ "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
3629
  "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.",
3630
+ "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
3631
  "Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
3552
3632
  "Functional case IDs use BE-<MODULE>-<NNN>; production code/config, skip/xfail, repair and rerun are forbidden.",
3553
3633
  ],
3554
- defaults: { ...BACKEND_TEST_DEFAULTS, contextProfile: taskConfig.contextProfile },
3634
+ defaults: {
3635
+ ...BACKEND_TEST_DEFAULTS,
3636
+ contextProfile: taskConfig.contextProfile,
3637
+ },
3555
3638
  skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
3556
3639
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3557
- tasks: [environment, generateCases, reviewCases, validateCases, generatePytest, traceability, execute, report],
3640
+ tasks: [
3641
+ environment,
3642
+ generateCases,
3643
+ reviewCases,
3644
+ validateCases,
3645
+ generatePytest,
3646
+ traceability,
3647
+ manifest,
3648
+ execute,
3649
+ report,
3650
+ ],
3558
3651
  };
3559
3652
  applyDefaultReadOnlyRetryPolicy(spec);
3560
3653
  parseDagSpec(spec);
@@ -3604,7 +3697,8 @@ function buildFrontendTestHybridDag(sources) {
3604
3697
  "const issues=[];",
3605
3698
  "const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
3606
3699
  "const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
3607
- "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;", ,
3700
+ "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
3701
+ ,
3608
3702
  "const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
3609
3703
  "for(const c of manifest.cases){",
3610
3704
  " const id=c&&c.caseId||'?';",
@@ -3614,7 +3708,8 @@ function buildFrontendTestHybridDag(sources) {
3614
3708
  " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
3615
3709
  " 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
3710
  " 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'});", ,
3711
+ " 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'});",
3712
+ ,
3618
3713
  " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
3619
3714
  " if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
3620
3715
  " 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)'});",
@@ -3729,7 +3824,7 @@ function buildFrontendTestHybridDag(sources) {
3729
3824
  commands: [
3730
3825
  [
3731
3826
  "node -e",
3732
- JSON.stringify("const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],\\\"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)');baseUrl=baseUrl.replace(/[)\\}\\],.\\\"']+$/,'');if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl);if(/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl);const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrlRedacted:safe,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated baseUrl='+safe+' probe=reachable method='+used+' httpStatus='+statusNum);"),
3827
+ JSON.stringify("const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],\\\"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)');baseUrl=baseUrl.replace(/[)\\}\\],.\\\"'\\x60]+$/,'');if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl);if(/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl);const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrlRedacted:safe,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated baseUrl='+safe+' probe=reachable method='+used+' httpStatus='+statusNum);"),
3733
3828
  ].join(" "),
3734
3829
  ],
3735
3830
  cwd: ".",
@@ -3847,7 +3942,12 @@ function buildFrontendTestHybridDag(sources) {
3847
3942
  forbiddenPaths: forbidden,
3848
3943
  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
3944
  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: [checklistValidation], cwd: ".", timeoutMs: 120000 },
3945
+ shell: {
3946
+ commands: [],
3947
+ frontendTestCaseChecklist: {},
3948
+ cwd: ".",
3949
+ timeoutMs: 120000,
3950
+ },
3851
3951
  }, {
3852
3952
  id: "materialize-frontend-case-manifest-shell",
3853
3953
  depends_on: ["frontend-case-checklist-shell"],
@@ -3923,7 +4023,12 @@ function buildFrontendTestHybridDag(sources) {
3923
4023
  forbiddenPaths: forbidden,
3924
4024
  outputContract: "Deterministic evidence gate: missing/malformed evidence is advisory; only unsafe evidenceDir or evidence paths hard-fail. Does not block retrospect.",
3925
4025
  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 },
4026
+ shell: {
4027
+ commands: [],
4028
+ frontendTestEvidenceValidation: {},
4029
+ cwd: ".",
4030
+ timeoutMs: 120000,
4031
+ },
3927
4032
  }, {
3928
4033
  id: "materialize-frontend-test-result-shell",
3929
4034
  depends_on: ["validate-frontend-case-evidence-shell"],
@@ -3947,10 +4052,24 @@ function buildFrontendTestHybridDag(sources) {
3947
4052
  timeoutMs: 120000,
3948
4053
  },
3949
4054
  });
4055
+ tasks.push({
4056
+ id: "frontend-test-l5-report-shell",
4057
+ depends_on: ["materialize-frontend-test-result-shell"],
4058
+ role: "verifier",
4059
+ executor: "shell",
4060
+ complexity: "LOW",
4061
+ writePolicy: "exclusive",
4062
+ writeSet: ["testcase/frontend/reports/**"],
4063
+ allowedPaths: ["testcase/frontend/**"],
4064
+ forbiddenPaths: forbidden,
4065
+ outputContract: "Deterministic frontend L-5 Markdown and self-contained HTML dashboard derived only from frontend-test-result-v1.",
4066
+ 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.",
4067
+ shell: { frontendTestL5Report: {}, commands: [], cwd: ".", timeoutMs: 120000 },
4068
+ });
3950
4069
  if (strictOutcomeGate) {
3951
4070
  tasks.push({
3952
4071
  id: "frontend-test-result-outcome-gate-shell",
3953
- depends_on: ["materialize-frontend-test-result-shell"],
4072
+ depends_on: ["materialize-frontend-test-result-shell", "frontend-test-l5-report-shell"],
3954
4073
  role: "verifier",
3955
4074
  executor: "shell",
3956
4075
  complexity: "LOW",
@@ -3968,7 +4087,7 @@ function buildFrontendTestHybridDag(sources) {
3968
4087
  }
3969
4088
  tasks.push({
3970
4089
  id: "frontend-test-retrospect-pi",
3971
- depends_on: ["materialize-frontend-test-result-shell"],
4090
+ depends_on: ["materialize-frontend-test-result-shell", "frontend-test-l5-report-shell"],
3972
4091
  role: "closeout",
3973
4092
  executor: "pi",
3974
4093
  toolProfile: "write",
@@ -3978,10 +4097,10 @@ function buildFrontendTestHybridDag(sources) {
3978
4097
  allowedPaths: ["testcase/frontend/**"],
3979
4098
  forbiddenPaths: forbidden,
3980
4099
  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.",
4100
+ 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
4101
  });
3983
4102
  tasks.push({
3984
- id: "frontend-test-quality-report-html-shell",
4103
+ id: "frontend-test-html-report-shell",
3985
4104
  depends_on: ["frontend-test-retrospect-pi"],
3986
4105
  role: "verifier",
3987
4106
  executor: "shell",
@@ -3990,9 +4109,14 @@ function buildFrontendTestHybridDag(sources) {
3990
4109
  writeSet: ["testcase/frontend/reports/**"],
3991
4110
  allowedPaths: ["testcase/frontend/**"],
3992
4111
  forbiddenPaths: forbidden,
3993
- outputContract: "Write a user-readable HTML advisory report after the final Markdown retrospective; findings never block the workflow.",
3994
- subtask_prompt: "Generate the frontend case quality advisory Markdown and HTML report after the final test report. Record content findings only and never fail because findings exist.",
3995
- shell: { commands: [frontendCaseQualityAdvisory], cwd: ".", timeoutMs: 120000 },
4112
+ 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.",
4113
+ 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.",
4114
+ shell: {
4115
+ commands: [],
4116
+ frontendTestHtmlReport: {},
4117
+ cwd: ".",
4118
+ timeoutMs: 120000,
4119
+ },
3996
4120
  });
3997
4121
  const globalConstraints = [
3998
4122
  ...sources.taskConfig.hardConstraints,
@@ -5114,7 +5238,7 @@ function cloneTask(task, patch = {}) {
5114
5238
  }
5115
5239
  /**
5116
5240
  * Apply the default read-only Pi retry policy to safe planner/scout/reviewer/
5117
- * verifier/closeout Pi nodes in the generated DAG. Writers, supervisors,
5241
+ * verifier/supervisor/closeout Pi nodes in the generated DAG. Writers,
5118
5242
  * dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
5119
5243
  * never overwrites an explicit retryPolicy a task already declares.
5120
5244
  */
@@ -5233,6 +5357,14 @@ function buildReviewGateNode(sources) {
5233
5357
  }
5234
5358
  function enableProjectGovernanceOnNode(task) {
5235
5359
  task.governanceStandardReview = true;
5360
+ task.outputProtocol = REVIEW_VERDICT_OUTPUT_PROTOCOL;
5361
+ task.retryPolicy = PROTOCOL_AWARE_PI_RETRY_POLICY;
5362
+ task.outputContract =
5363
+ "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.";
5364
+ const protocolInstruction = "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.";
5365
+ if (!task.subtask_prompt.includes(protocolInstruction)) {
5366
+ task.subtask_prompt = `${protocolInstruction}\n\n${task.subtask_prompt}`;
5367
+ }
5236
5368
  }
5237
5369
  /**
5238
5370
  * Apply governance only to general implementation DAGs, and only when the
@@ -5309,6 +5441,7 @@ function insertGovernanceStandardGate(spec, sources) {
5309
5441
  const closeoutIndex = spec.tasks.findIndex((task) => task.id === "closeout-pi");
5310
5442
  spec.tasks.splice(closeoutIndex, 0, gate);
5311
5443
  closeout.depends_on = ["governance-standard-gate-shell"];
5444
+ closeout.failureAwareDependsOn = ["governance-standard-gate-shell"];
5312
5445
  }
5313
5446
  function buildReviewGatedHybridDag(standard, sources) {
5314
5447
  const spec = {
@@ -5322,7 +5455,10 @@ function buildReviewGatedHybridDag(standard, sources) {
5322
5455
  tasks: standard.tasks.map((task) => ({ ...task })),
5323
5456
  };
5324
5457
  const closeout = getTaskOrThrow(spec, "closeout-pi");
5325
- replaceTask(spec, cloneTask(closeout, { depends_on: ["review-gate-shell"] }));
5458
+ replaceTask(spec, cloneTask(closeout, {
5459
+ depends_on: ["review-gate-shell"],
5460
+ failureAwareDependsOn: ["review-gate-shell"],
5461
+ }));
5326
5462
  spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
5327
5463
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
5328
5464
  applyDefaultReadOnlyRetryPolicy(spec);
@@ -5528,6 +5664,7 @@ function buildProcessSupervisorNode(sources) {
5528
5664
  allowedPaths: commonReadOnlyPaths(sources),
5529
5665
  forbiddenPaths: commonForbiddenPaths(sources),
5530
5666
  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.",
5667
+ outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
5531
5668
  subtask_prompt: [
5532
5669
  "Supervise the implementation process after soft verification.",
5533
5670
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
@@ -5663,6 +5800,53 @@ function buildDecisionNode(sources) {
5663
5800
  decisionGate: { enabled: true, schemaVersion: 1, mode: "record-only" },
5664
5801
  };
5665
5802
  }
5803
+ /**
5804
+ * Supervised convergence chain node ids. Extends the controller default
5805
+ * (process-supervisor → process-gate → repair → hard-verify) with the review
5806
+ * chain so that a legitimate review `request-revision` re-enters the same
5807
+ * bounded repair-reverify-review loop instead of only blocking closeout.
5808
+ */
5809
+ export const SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS = [
5810
+ "process-supervisor-pi",
5811
+ "process-gate-shell",
5812
+ "repair-pi",
5813
+ "hard-verify-shell",
5814
+ "review-pi",
5815
+ "review-verdict-recovery-pi",
5816
+ "review-gate-shell",
5817
+ ];
5818
+ /**
5819
+ * Derive the supervised DAG convergence spec from `maxFixLoops`. Unlike the
5820
+ * task-level `convergence` config (which stays opt-in / disabled by default
5821
+ * until smoke evidence is stronger), the supervised path uses `maxFixLoops`
5822
+ * as the authoritative bounded-repair budget:
5823
+ * - `maxFixLoops > 0` ⇒ convergence enabled, `maxPasses = maxFixLoops + 1`
5824
+ * (initial execution + repair budget), with the full supervised chain.
5825
+ * - `maxFixLoops === 0` ⇒ convergence disabled (no automatic repair).
5826
+ * An explicit user `convergence.enabled: true` does not override `maxFixLoops`;
5827
+ * the budget still wins so total pass count stays consistent (AC1/AC2).
5828
+ */
5829
+ function resolveSupervisedConvergence(taskConfig) {
5830
+ const maxFixLoops = taskConfig.maxFixLoops;
5831
+ if (maxFixLoops <= 0) {
5832
+ return {
5833
+ enabled: false,
5834
+ maxPasses: 1,
5835
+ stopOnVerdictPass: true,
5836
+ stopOnHardVerifyPass: true,
5837
+ pauseOnRegression: true,
5838
+ chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
5839
+ };
5840
+ }
5841
+ return {
5842
+ enabled: true,
5843
+ maxPasses: maxFixLoops + 1,
5844
+ stopOnVerdictPass: true,
5845
+ stopOnHardVerifyPass: true,
5846
+ pauseOnRegression: taskConfig.convergence?.pauseOnRegression ?? true,
5847
+ chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
5848
+ };
5849
+ }
5666
5850
  function buildSupervisedHybridDag(standard, sources) {
5667
5851
  const contract = getTaskOrThrow(standard, "contract-pi");
5668
5852
  const scoutSrc = getTaskOrThrow(standard, "scout-src");
@@ -5687,7 +5871,7 @@ function buildSupervisedHybridDag(standard, sources) {
5687
5871
  title: `Supervised ${standard.title}`,
5688
5872
  objective: `${standard.objective ?? ""}\n\nRoute: supervised implementation DAG selected by workflowPolicy/governanceProfile or explicit CLI profile.`.trim(),
5689
5873
  globalConstraints: supervisedConstraints,
5690
- convergence: sources.taskConfig.convergence,
5874
+ convergence: resolveSupervisedConvergence(sources.taskConfig),
5691
5875
  verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig, "1"),
5692
5876
  tasks: [
5693
5877
  cloneTask(contract),
@@ -5732,7 +5916,10 @@ function buildSupervisedHybridDag(standard, sources) {
5732
5916
  buildReviewVerdictRecoveryNode(sources),
5733
5917
  buildReviewGateNode(sources),
5734
5918
  buildDecisionNode(sources),
5735
- cloneTask(closeout, { depends_on: ["decision-pi"] }),
5919
+ cloneTask(closeout, {
5920
+ depends_on: ["decision-pi"],
5921
+ failureAwareDependsOn: ["decision-pi"],
5922
+ }),
5736
5923
  ],
5737
5924
  };
5738
5925
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());