@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.
- package/CHANGELOG.md +24 -0
- package/dist/application/dag/generate-task-dag.js +33 -0
- package/dist/commands/task-source-prepare.js +6 -0
- package/dist/executors/shell-executor.js +111 -0
- package/dist/executors/shell-presets.js +12 -4
- package/dist/task/config-types.js +6 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/project.js +8 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/frontend-preflight.js +131 -0
- package/dist/task/runtime.js +2 -4
- package/dist/task/source-prepare/build-draft.js +9 -0
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/worker/observability/read-model.js +134 -0
- package/dist/worker/observe/static/state.js +61 -0
- package/dist/worker/observe/static/styles.css +8 -0
- package/dist/worker/observe/static/views/dag-graph.js +107 -31
- package/dist/worker/observe/static/views/dag-inspector.js +374 -157
- package/dist/worker/observe/static/views/dag.js +4 -11
- package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
- package/dist/workflows/dag/convergence/controller.js +110 -21
- package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
- package/dist/workflows/dag/frontend-review-context.js +7 -1
- package/dist/workflows/dag/frontend-verification-trace.js +14 -3
- package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
- package/dist/workflows/dag/init-hybrid.js +96 -34
- package/dist/workflows/dag/output-protocol.js +180 -7
- package/dist/workflows/dag/runner.js +141 -52
- package/dist/workflows/dag/types.js +4 -0
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/templates/backend-test-dag.json +100 -8
- package/package.json +1 -1
- 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
|
-
|
|
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:
|
|
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
|
-
"
|
|
2648
|
-
"
|
|
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:
|
|
2669
|
-
subtask_prompt:
|
|
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: ["
|
|
2691
|
+
accept: ["pass"],
|
|
2692
|
+
routingAccept: ["request-revision"],
|
|
2675
2693
|
label: "frontend review",
|
|
2676
|
-
|
|
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
|
|
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
|
|
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;
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
5290
|
-
outputProtocol:
|
|
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
|
-
"
|
|
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:
|
|
5323
|
-
outputProtocol:
|
|
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
|
-
"
|
|
5327
|
-
"
|
|
5328
|
-
"If
|
|
5329
|
-
|
|
5330
|
-
|
|
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:
|
|
5347
|
-
subtask_prompt:
|
|
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: ["
|
|
5413
|
+
accept: ["pass"],
|
|
5414
|
+
routingAccept: ["request-revision"],
|
|
5353
5415
|
label: "review",
|
|
5354
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 {
|