@tea-agent/loop-agent 0.26.1 → 0.26.2

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 (33) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/application/dag/generate-task-dag.js +33 -0
  3. package/dist/commands/task-source-prepare.js +6 -0
  4. package/dist/executors/shell-executor.js +111 -0
  5. package/dist/executors/shell-presets.js +12 -4
  6. package/dist/task/config-types.js +6 -0
  7. package/dist/task/contract/constants.js +1 -0
  8. package/dist/task/contract/project.js +8 -0
  9. package/dist/task/contract/schema.js +1 -0
  10. package/dist/task/frontend-preflight.js +131 -0
  11. package/dist/task/runtime.js +2 -4
  12. package/dist/task/source-prepare/build-draft.js +9 -0
  13. package/dist/task/source-prepare/completeness.js +1 -1
  14. package/dist/worker/observability/read-model.js +134 -0
  15. package/dist/worker/observe/static/state.js +61 -0
  16. package/dist/worker/observe/static/styles.css +8 -0
  17. package/dist/worker/observe/static/views/dag-graph.js +107 -31
  18. package/dist/worker/observe/static/views/dag-inspector.js +374 -157
  19. package/dist/worker/observe/static/views/dag.js +4 -11
  20. package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
  21. package/dist/workflows/dag/convergence/controller.js +110 -21
  22. package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
  23. package/dist/workflows/dag/frontend-review-context.js +7 -1
  24. package/dist/workflows/dag/frontend-verification-trace.js +14 -3
  25. package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
  26. package/dist/workflows/dag/init-hybrid.js +96 -34
  27. package/dist/workflows/dag/output-protocol.js +180 -7
  28. package/dist/workflows/dag/runner.js +141 -52
  29. package/dist/workflows/dag/types.js +4 -0
  30. package/dist/workflows/dag/validate.js +3 -2
  31. package/docs/templates/backend-test-dag.json +100 -8
  32. package/package.json +1 -1
  33. package/skills/loop-agent/references/hybrid-dag.md +1 -1
@@ -10,7 +10,7 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
11
11
  import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
12
12
  import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
13
- import { REVIEW_VERDICT_OUTPUT_PROTOCOL } from "./output-protocol.js";
13
+ import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
14
14
  import { resolveAdapter } from "../../adapters/index.js";
15
15
  import { loadHarnessManifest } from "../../governance/harness.js";
16
16
  import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
@@ -1252,7 +1252,19 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
1252
1252
  if (fromSection.length > 0)
1253
1253
  return fromSection;
1254
1254
  }
1255
- return extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown);
1255
+ const explicit = extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown);
1256
+ if (explicit.length > 0)
1257
+ return explicit;
1258
+ // Requirements frequently use a numbered acceptance list instead of
1259
+ // writing AC-* identifiers. Give that list a deterministic canonical
1260
+ // namespace so model-generated AC-1 references bind to the same source
1261
+ // facts instead of failing as unknown requirements at the prewrite gate.
1262
+ const acceptanceSection = requirementMarkdown.match(/(?:^|\n)##\s*(?:验收标准|Acceptance Criteria)\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i)?.[1] ?? "";
1263
+ const numbered = [...acceptanceSection.matchAll(/(?:^|\n)\s*(\d+)[.、)]\s+/g)]
1264
+ .map((match) => Number(match[1]))
1265
+ .filter((value, index, values) => Number.isFinite(value) && values.indexOf(value) === index)
1266
+ .sort((a, b) => a - b);
1267
+ return numbered.map((value) => `AC-${value}`);
1256
1268
  }
1257
1269
  function buildDagSourceBinding(sources) {
1258
1270
  const sourceEntries = [
@@ -2287,10 +2299,11 @@ async function buildFrontendHybridDagFromTask(sources) {
2287
2299
  allowedPaths: readOnlyPaths,
2288
2300
  forbiddenPaths,
2289
2301
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2290
- outputContract: "Markdown scout report covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
2302
+ outputContract: "Markdown scout report with a required TARGET_SURFACE section covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
2291
2303
  subtask_prompt: [
2292
2304
  "Inspect frontend code, routing, components, styles, package scripts, and tests.",
2293
2305
  "Return code and design observations, existing reuse opportunities, and verification entry points.",
2306
+ "Begin with a TARGET_SURFACE section containing exactly these labels: entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts. Use repository-relative POSIX paths. implementationPaths and testPaths must name the existing files/directories that actually own the requested behavior; allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
2294
2307
  "Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
2295
2308
  "Read-only: do not modify repository files.",
2296
2309
  sourceContext,
@@ -2314,6 +2327,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2314
2327
  "Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
2315
2328
  "Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2316
2329
  "Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
2330
+ "Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
2317
2331
  "End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
2318
2332
  "Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
2319
2333
  requirementCoverageInstruction,
@@ -2641,11 +2655,14 @@ async function buildFrontendHybridDagFromTask(sources) {
2641
2655
  allowedPaths: readOnlyPaths,
2642
2656
  forbiddenPaths,
2643
2657
  skills: FRONTEND_REVIEW_SKILLS,
2644
- outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Verification Assessment, UX Assessment, and Residual Risks. No file writes.",
2658
+ outputContract: 'Structured JSON review verdict only: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[...],"verificationAssessment":"...","uxAssessment":"...","residualRisks":[...]}. No file writes.',
2659
+ outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
2645
2660
  subtask_prompt: [
2646
2661
  "Review the frontend implementation and verification evidence.",
2647
- "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2648
- "Any Critical or Important finding must force VERDICT: request-revision.",
2662
+ "Return exactly one final JSON object in this response. Do not repeat it, do not emit a second revision, do not wrap it in Markdown, and do not include prose outside the JSON.",
2663
+ 'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
2664
+ 'verdict "request-revision" requires at least one finding. verdict "pass" is invalid if any finding severity is Critical or Important.',
2665
+ 'Any Critical or Important finding must force verdict "request-revision".',
2649
2666
  "Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
2650
2667
  "Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
2651
2668
  "Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
@@ -2665,15 +2682,16 @@ async function buildFrontendHybridDagFromTask(sources) {
2665
2682
  writePolicy: "read-only",
2666
2683
  allowedPaths: readOnlyPaths,
2667
2684
  forbiddenPaths,
2668
- outputContract: "Deterministic frontend review verdict gate: exit 0 only when frontend-review-pi emits VERDICT: pass.",
2669
- subtask_prompt: "Deterministic gate: block downstream closeout unless frontend-review-pi emitted VERDICT: pass.",
2685
+ outputContract: 'Deterministic frontend review verdict gate: exit 0 only when frontend-review-pi emits JSON verdict "pass".',
2686
+ subtask_prompt: 'Deterministic gate: block downstream closeout unless frontend-review-pi emitted JSON verdict "pass".',
2670
2687
  shell: {
2671
2688
  commands: [],
2672
2689
  verdictGate: {
2673
2690
  fromNodeId: "frontend-review-pi",
2674
- accept: ["VERDICT: pass"],
2691
+ accept: ["pass"],
2692
+ routingAccept: ["request-revision"],
2675
2693
  label: "frontend review",
2676
- lineMode: "first-verdict-line",
2694
+ source: "json-review-verdict",
2677
2695
  },
2678
2696
  cwd: ".",
2679
2697
  timeoutMs: 60000,
@@ -3577,13 +3595,50 @@ async function buildBackendTestHybridDag(sources) {
3577
3595
  "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`.",
3578
3596
  ].join("\n\n"),
3579
3597
  };
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.");
3598
+ const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytest.id], "markdown-collection-assess", "Run pytest collection only over final Markdown-mapped scripts before any business test body execution. Materialize hash-bound PASS/REPAIRABLE/BLOCKED facts. Only generated testcase-local syntax/import inconsistencies are repairable; dependency, plugin, production-module, environment, safety and unknown failures remain blocked.", "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json with bounded diagnostics, asset hashes, collected item IDs and deterministic repair eligibility.", [], 120000);
3599
+ const repairPytest = {
3600
+ id: "repair-backend-pytest-collection-pi",
3601
+ depends_on: [collectionAssess.id],
3602
+ runIf: "$.nodes['assess-backend-pytest-collection-shell'].json.repairEligible == true",
3603
+ role: "implementer",
3604
+ executor: "pi",
3605
+ toolProfile: "write",
3606
+ complexity: "HIGH",
3607
+ writePolicy: "exclusive",
3608
+ writeSet: [
3609
+ "testcase/**/test_*.py",
3610
+ "testcase/**/helpers/**",
3611
+ "testcase/**/factories/**",
3612
+ ],
3613
+ allowedPaths: Array.from(new Set([...ro, "testcase/**"])),
3614
+ forbiddenPaths: Array.from(new Set([
3615
+ ...forbidden,
3616
+ "testcase/md/**",
3617
+ "conftest.py",
3618
+ "pytest.ini",
3619
+ "pyproject.toml",
3620
+ "setup.cfg",
3621
+ ])),
3622
+ writerOutcomePolicy: { type: "implementation-outcome-v1" },
3623
+ outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked, followed by a concise repair summary. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
3624
+ subtask_prompt: [
3625
+ "Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.",
3626
+ "Fix only collection-proven generated testcase-local syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent.",
3627
+ "Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
3628
+ "Do not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
3629
+ "Do not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.",
3630
+ "Do not execute pytest; the deterministic effective collection gate owns the final collection attempt.",
3631
+ ].join("\n\n"),
3632
+ };
3633
+ const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection passed, verify unchanged asset hashes and reuse it without another collection. If the single repair ran, collect the final mapped scripts once and fail closed unless it passes. BLOCKED initial facts, repair failure, final collection failure or hash drift must prevent business pytest execution.", "Run-owned reports/backend-test-pytest-collection-effective.md and contracts/backend-test-pytest-collection-effective.json proving the exact final assets are collectable; initial PASS is reused, repair path records attempt=1.", [], 120000);
3634
+ collectionEffective.dependsPolicy = "all-or-condition-skip";
3635
+ const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only Markdown-mapped pytest scripts after the effective hash-bound collection gate. 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 bound after effective collection.");
3581
3636
  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.");
3582
3637
  const pytestCommand = [
3583
3638
  'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
3584
3639
  'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
3585
3640
  ].join("; ");
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);
3641
+ 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 9 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);
3587
3642
  if (execute.shell) {
3588
3643
  execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
3589
3644
  }
@@ -3607,10 +3662,10 @@ async function buildBackendTestHybridDag(sources) {
3607
3662
  ? "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
3663
  : "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.",
3609
3664
  subtask_prompt: [
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.",
3665
+ "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; nodes 6/8 backend-test-pytest-collection initial/effective reports and facts; node 9 backend-test-traceability.md and backend-test-markdown-pytest-correspondence.md; node 10 contracts/backend-test-case-manifest.json; and node 11 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.",
3611
3666
  "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.",
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.",
3667
+ "The L-5 metrics and visualization are produced deterministically by node 11 at reports/backend-test-l5-dashboard.html. Link to that dashboard as the authoritative L-5 view. Pytest execution facts come from node 11; coverage/correspondence numbers and materializationStatus come from node 10; collection authorization comes from nodes 6/8; detailed coverage findings come from node 4; detailed mapping findings come from node 9. Never recompute these values. If machine manifest and human reports disagree, report evidence inconsistency rather than silently choosing.",
3668
+ "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 9 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
3669
  "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.",
3615
3670
  "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.",
3616
3671
  canWriteReport
@@ -3628,11 +3683,11 @@ async function buildBackendTestHybridDag(sources) {
3628
3683
  globalConstraints: [
3629
3684
  ...taskConfig.hardConstraints,
3630
3685
  ...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.",
3686
+ "backend-test-dag uses exactly 12 real top-level tasks. Pytest collection runs once on the green path and at most twice only when one bounded pre-execution repair is eligible; business pytest test bodies execute exactly once over safe scripts explicitly mapped by final Markdown cases.",
3632
3687
  "Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
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.",
3688
+ "Environment, advisory Markdown validation/coverage, collection initial/effective facts, advisory traceability/correspondence, canonical manifest, pytest-html, HTML and execution facts are deterministic evidence. Node 4 quality findings stay advisory; nodes 6/8 form the fail-closed collection authorization; node 9 traceability findings stay advisory; node 10 partial/unavailable manifest does not block node 11 when effective collection remains fresh.",
3634
3689
  "Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
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.",
3690
+ "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, execution-result repair and business pytest rerun are forbidden; the only repair is one pre-execution collection-proven generated-test asset repair.",
3636
3691
  ],
3637
3692
  defaults: {
3638
3693
  ...BACKEND_TEST_DEFAULTS,
@@ -3646,6 +3701,9 @@ async function buildBackendTestHybridDag(sources) {
3646
3701
  reviewCases,
3647
3702
  validateCases,
3648
3703
  generatePytest,
3704
+ collectionAssess,
3705
+ repairPytest,
3706
+ collectionEffective,
3649
3707
  traceability,
3650
3708
  manifest,
3651
3709
  execute,
@@ -5286,11 +5344,13 @@ function buildReviewNode(sources) {
5286
5344
  writePolicy: "read-only",
5287
5345
  allowedPaths: commonReadOnlyPaths(sources),
5288
5346
  forbiddenPaths: commonForbiddenPaths(sources),
5289
- outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; Critical/Important findings force request-revision. No file writes.",
5290
- outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
5347
+ outputContract: 'Structured JSON review verdict only: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[...]}; Critical/Important findings force request-revision. No file writes.',
5348
+ outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
5291
5349
  subtask_prompt: [
5292
5350
  "Review upstream implementation and verification evidence.",
5293
- "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
5351
+ "Return exactly one final JSON object in this response. Do not repeat it, do not emit a second revision, do not wrap it in Markdown, and do not include prose outside the JSON.",
5352
+ 'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
5353
+ 'verdict "request-revision" requires at least one finding. verdict "pass" is invalid if any finding severity is Critical or Important.',
5294
5354
  "List Critical/Important findings when present; any Critical/Important finding must force request-revision. Read-only: do not modify files.",
5295
5355
  [
5296
5356
  "Three-way source fidelity check (required):",
@@ -5319,15 +5379,16 @@ function buildReviewVerdictRecoveryNode(sources) {
5319
5379
  writePolicy: "read-only",
5320
5380
  allowedPaths: commonReadOnlyPaths(sources),
5321
5381
  forbiddenPaths: commonForbiddenPaths(sources),
5322
- outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision, followed by the original review findings without substantive changes. No file writes.",
5323
- outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
5382
+ outputContract: 'Structured JSON review verdict only, preserving the original review conclusion/findings without substantive changes. No file writes.',
5383
+ outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
5324
5384
  subtask_prompt: [
5325
5385
  "Normalize the output format of review-pi; this is the single read-only format-recovery attempt for the review verdict protocol.",
5326
- "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
5327
- "If review-pi already contains a valid VERDICT line, preserve that verdict exactly and keep the original findings.",
5328
- "If it omitted or malformed the VERDICT line but states an unambiguous request-revision conclusion, emit VERDICT: request-revision and preserve the findings.",
5329
- "Do not invent VERDICT: pass from natural-language phrases such as 通过、PASS、✅, or general approval prose.",
5330
- "If the upstream conclusion is ambiguous or cannot be preserved safely, emit VERDICT: request-revision and report the format ambiguity.",
5386
+ "Return only one JSON object. Do not wrap it in Markdown and do not include prose outside the JSON.",
5387
+ 'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
5388
+ "If review-pi already contains valid JSON, preserve its verdict exactly and keep the original findings.",
5389
+ 'If it omitted or malformed JSON but states an unambiguous request-revision conclusion, emit verdict "request-revision" and preserve the findings.',
5390
+ 'Do not invent verdict "pass" from natural-language phrases such as 通过, PASS, or general approval prose.',
5391
+ 'If the upstream conclusion is ambiguous or cannot be preserved safely, emit verdict "request-revision" and report the format ambiguity as a finding.',
5331
5392
  "Do not re-review code, expand task allowedPaths, or edit files.",
5332
5393
  buildSourceContextBlock(sources),
5333
5394
  ].join("\n\n"),
@@ -5343,15 +5404,16 @@ function buildReviewGateNode(sources) {
5343
5404
  writePolicy: "read-only",
5344
5405
  allowedPaths: commonReadOnlyPaths(sources),
5345
5406
  forbiddenPaths: commonForbiddenPaths(sources),
5346
- outputContract: "Deterministic review verdict gate: exit 0 only when review-verdict-recovery-pi first VERDICT line is pass.",
5347
- subtask_prompt: "Deterministic gate: block downstream closeout unless review-verdict-recovery-pi emitted VERDICT: pass.",
5407
+ outputContract: 'Deterministic review verdict gate: exit 0 only when review-verdict-recovery-pi emits JSON verdict "pass".',
5408
+ subtask_prompt: 'Deterministic gate: block downstream closeout unless review-verdict-recovery-pi emitted JSON verdict "pass".',
5348
5409
  shell: {
5349
5410
  commands: [],
5350
5411
  verdictGate: {
5351
5412
  fromNodeId: "review-verdict-recovery-pi",
5352
- accept: ["VERDICT: pass"],
5413
+ accept: ["pass"],
5414
+ routingAccept: ["request-revision"],
5353
5415
  label: "review",
5354
- lineMode: "first-verdict-line",
5416
+ source: "json-review-verdict",
5355
5417
  },
5356
5418
  cwd: ".",
5357
5419
  timeoutMs: 60000,
@@ -5920,8 +5982,8 @@ function buildSupervisedHybridDag(standard, sources) {
5920
5982
  buildReviewGateNode(sources),
5921
5983
  buildDecisionNode(sources),
5922
5984
  cloneTask(closeout, {
5923
- depends_on: ["decision-pi"],
5924
- failureAwareDependsOn: ["decision-pi"],
5985
+ depends_on: ["decision-pi", "review-gate-shell"],
5986
+ failureAwareDependsOn: ["decision-pi", "review-gate-shell"],
5925
5987
  }),
5926
5988
  ],
5927
5989
  };
@@ -3,22 +3,54 @@ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
3
3
  /**
4
4
  * Machine-readable output protocol for safe read-only Pi nodes.
5
5
  *
6
- * Phase 1 only supports first-line-enum (e.g. VERDICT lines). Structured JSON
7
- * continues to use existing structured-required / deterministic gates.
8
6
  */
9
- export const dagOutputProtocolSchema = z
7
+ const firstLineEnumOutputProtocolSchema = z
10
8
  .object({
11
9
  type: z.literal("first-line-enum"),
12
10
  validLines: z.array(z.string().min(1)).min(1),
13
11
  retryOnInvalid: z.boolean().default(true),
14
12
  })
15
13
  .strict();
14
+ const jsonReviewVerdictOutputProtocolSchema = z
15
+ .object({
16
+ type: z.literal("json-review-verdict"),
17
+ retryOnInvalid: z.boolean().default(true),
18
+ })
19
+ .strict();
20
+ export const dagOutputProtocolSchema = z.discriminatedUnion("type", [
21
+ firstLineEnumOutputProtocolSchema,
22
+ jsonReviewVerdictOutputProtocolSchema,
23
+ ]);
16
24
  /** Reviewer VERDICT protocol used by reviewed/supervised DAGs. */
17
25
  export const REVIEW_VERDICT_OUTPUT_PROTOCOL = {
18
26
  type: "first-line-enum",
19
27
  validLines: ["VERDICT: pass", "VERDICT: request-revision"],
20
28
  retryOnInvalid: true,
21
29
  };
30
+ /** Structured reviewer verdict protocol used when deterministic gates parse JSON. */
31
+ export const REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL = {
32
+ type: "json-review-verdict",
33
+ retryOnInvalid: true,
34
+ };
35
+ const reviewFindingSchema = z
36
+ .object({
37
+ severity: z.enum(["Critical", "Important", "Minor", "Info"]),
38
+ file: z.string().min(1).optional(),
39
+ line: z.number().int().positive().optional(),
40
+ issue: z.string().min(1),
41
+ requiredChange: z.string().min(1).optional(),
42
+ })
43
+ .strict();
44
+ const reviewJsonVerdictSchema = z
45
+ .object({
46
+ schemaVersion: z.literal(1),
47
+ verdict: z.enum(["pass", "request-revision"]),
48
+ findings: z.array(reviewFindingSchema),
49
+ verificationAssessment: z.string().min(1).optional(),
50
+ uxAssessment: z.string().min(1).optional(),
51
+ residualRisks: z.array(z.string().min(1)).optional(),
52
+ })
53
+ .strict();
22
54
  /**
23
55
  * Extract the first non-empty line from assistant/stdout text.
24
56
  */
@@ -30,12 +62,136 @@ export function firstNonEmptyLine(text) {
30
62
  }
31
63
  return undefined;
32
64
  }
65
+ function extractSingleJsonObjectText(text) {
66
+ const trimmed = String(text).trim();
67
+ if (!trimmed)
68
+ return { ok: false, reason: "missing JSON output" };
69
+ const extractBalancedObject = (source) => {
70
+ for (let start = 0; start < source.length; start += 1) {
71
+ if (source[start] !== "{")
72
+ continue;
73
+ let depth = 0;
74
+ let inString = false;
75
+ let escaped = false;
76
+ for (let index = start; index < source.length; index += 1) {
77
+ const char = source[index];
78
+ if (inString) {
79
+ if (escaped)
80
+ escaped = false;
81
+ else if (char === "\\")
82
+ escaped = true;
83
+ else if (char === '"')
84
+ inString = false;
85
+ continue;
86
+ }
87
+ if (char === '"')
88
+ inString = true;
89
+ else if (char === "{")
90
+ depth += 1;
91
+ else if (char === "}" && --depth === 0) {
92
+ const candidate = source.slice(start, index + 1);
93
+ try {
94
+ const parsed = JSON.parse(candidate);
95
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
96
+ return candidate;
97
+ }
98
+ catch {
99
+ // Continue scanning for the next complete object.
100
+ }
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ return undefined;
106
+ };
107
+ const direct = extractBalancedObject(trimmed);
108
+ if (direct)
109
+ return { ok: true, jsonText: direct };
110
+ const fencedMatches = Array.from(trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/gi));
111
+ if (fencedMatches.length === 1) {
112
+ const jsonText = fencedMatches[0][1].trim();
113
+ const fencedObject = extractBalancedObject(jsonText);
114
+ if (fencedObject)
115
+ return { ok: true, jsonText: fencedObject };
116
+ return {
117
+ ok: false,
118
+ reason: "single fenced block is not a JSON object",
119
+ };
120
+ }
121
+ if (fencedMatches.length > 1) {
122
+ return {
123
+ ok: false,
124
+ reason: "multiple fenced JSON candidates found; expected exactly one JSON object",
125
+ };
126
+ }
127
+ return {
128
+ ok: false,
129
+ reason: "output is not a single JSON object; expected only JSON with no Markdown or prose",
130
+ };
131
+ }
132
+ function validateJsonReviewVerdict(text) {
133
+ const extracted = extractSingleJsonObjectText(text);
134
+ if (!extracted.ok) {
135
+ return {
136
+ ok: false,
137
+ failureCategory: "protocol-invalid",
138
+ reason: extracted.reason,
139
+ firstNonEmptyLine: firstNonEmptyLine(text),
140
+ };
141
+ }
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(extracted.jsonText);
145
+ }
146
+ catch (error) {
147
+ return {
148
+ ok: false,
149
+ failureCategory: "protocol-invalid",
150
+ reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
151
+ firstNonEmptyLine: firstNonEmptyLine(text),
152
+ };
153
+ }
154
+ const checked = reviewJsonVerdictSchema.safeParse(parsed);
155
+ if (!checked.success) {
156
+ return {
157
+ ok: false,
158
+ failureCategory: "protocol-invalid",
159
+ reason: `JSON review verdict schema violation: ${checked.error.issues
160
+ .map((issue) => `${issue.path.join(".") || "<root>"} ${issue.message}`)
161
+ .join("; ")}`,
162
+ firstNonEmptyLine: firstNonEmptyLine(text),
163
+ };
164
+ }
165
+ const blockingFindings = checked.data.findings.filter((finding) => finding.severity === "Critical" || finding.severity === "Important");
166
+ if (checked.data.verdict === "pass" && blockingFindings.length > 0) {
167
+ return {
168
+ ok: false,
169
+ failureCategory: "protocol-invalid",
170
+ reason: "JSON review verdict cannot be pass when Critical or Important findings are present",
171
+ firstNonEmptyLine: firstNonEmptyLine(text),
172
+ };
173
+ }
174
+ if (checked.data.verdict === "request-revision" &&
175
+ checked.data.findings.length === 0) {
176
+ return {
177
+ ok: false,
178
+ failureCategory: "protocol-invalid",
179
+ reason: "JSON review verdict request-revision requires at least one finding",
180
+ firstNonEmptyLine: firstNonEmptyLine(text),
181
+ };
182
+ }
183
+ return { ok: true, verdict: checked.data.verdict };
184
+ }
33
185
  /**
34
186
  * Validate node output against an explicit outputProtocol.
35
187
  * Pure function — does not mutate run facts.
36
188
  */
37
189
  export function validateOutputProtocol(protocol, text) {
38
- if (protocol.type !== "first-line-enum") {
190
+ if (protocol.type === "json-review-verdict") {
191
+ return validateJsonReviewVerdict(text);
192
+ }
193
+ const validLines = "validLines" in protocol ? protocol.validLines : undefined;
194
+ if (!validLines) {
39
195
  return {
40
196
  ok: false,
41
197
  failureCategory: "protocol-invalid",
@@ -64,6 +220,17 @@ export function validateOutputProtocol(protocol, text) {
64
220
  * Correction instruction appended on protocol-invalid retry attempts.
65
221
  */
66
222
  export function buildProtocolRetryInstruction(protocol, reason) {
223
+ if (protocol.type === "json-review-verdict") {
224
+ return [
225
+ "<retry_instruction>",
226
+ "Previous attempt violated the structured review output protocol:",
227
+ reason,
228
+ "Return ONLY a JSON object, with no Markdown fence and no surrounding prose.",
229
+ 'Required schema: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[{"severity":"Critical|Important|Minor|Info","file":"optional path","line":1,"issue":"required","requiredChange":"optional"}],"verificationAssessment":"optional","uxAssessment":"optional","residualRisks":["optional"]}.',
230
+ 'Rules: verdict "request-revision" requires at least one finding; verdict "pass" is invalid if any finding severity is Critical or Important.',
231
+ "</retry_instruction>",
232
+ ].join("\n");
233
+ }
67
234
  const expected = protocol.validLines
68
235
  .map((line) => JSON.stringify(line))
69
236
  .join(" or ");
@@ -102,9 +269,15 @@ export function buildProtocolRetryInstruction(protocol, reason) {
102
269
  * output, and structured protocols are never loosely tolerated.
103
270
  */
104
271
  export function normalizeReviewVerdictAfterRetries(protocol, text) {
105
- const isReviewVerdictProtocol = protocol.type === REVIEW_VERDICT_OUTPUT_PROTOCOL.type &&
106
- Array.from(protocol.validLines).every((line, index) => line === REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines[index]) &&
107
- protocol.validLines.length ===
272
+ if (protocol.type !== "first-line-enum") {
273
+ return {
274
+ ok: false,
275
+ reason: "deterministic verdict normalization only applies to the canonical review verdict protocol",
276
+ };
277
+ }
278
+ const validLines = protocol.validLines;
279
+ const isReviewVerdictProtocol = Array.from(validLines).every((line, index) => line === REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines[index]) &&
280
+ validLines.length ===
108
281
  REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines.length;
109
282
  if (!isReviewVerdictProtocol) {
110
283
  return {