@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.
- package/AGENTS.md +6 -0
- package/CHANGELOG.md +55 -0
- package/dist/application/dag/args.js +21 -1
- package/dist/application/dag/run-dag.js +1 -0
- package/dist/cli/command-definitions.js +1 -1
- package/dist/cli/program.js +82 -63
- package/dist/commands/client-recovery.js +209 -62
- package/dist/commands/init.js +206 -82
- package/dist/commands/run-dag-progress.js +109 -0
- package/dist/commands/run-dag.js +16 -5
- package/dist/executors/dag-pi-executor.js +80 -15
- package/dist/executors/model-routing.js +1 -1
- package/dist/executors/shell-executor.js +159 -0
- package/dist/executors/shell-write-guard.js +21 -7
- package/dist/worker/console/repo-fingerprint.js +7 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
- package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +306 -30
- package/dist/workflows/dag/backend-test-result-contract.js +35 -9
- package/dist/workflows/dag/convergence/controller.js +134 -9
- package/dist/workflows/dag/frontend-test-case-checklist.js +71 -0
- package/dist/workflows/dag/frontend-test-html-report.js +77 -0
- package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +44 -1
- package/dist/workflows/dag/init-hybrid.js +267 -80
- package/dist/workflows/dag/node-execution.js +64 -11
- package/dist/workflows/dag/prompt.js +118 -4
- package/dist/workflows/dag/retry-policy.js +5 -4
- package/dist/workflows/dag/scheduler.js +32 -5
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +6 -3
- package/docs/architecture/dag-execution.md +7 -4
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.schema.json +6 -0
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/backend-test-dag.json +40 -13
- package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.json +62 -5
- package/docs/templates/hybrid-dag.json +1 -1
- package/examples/decision-gate-agent-dag.json +1 -1
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +3 -2
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- 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
|
|
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 [
|
|
937
|
-
|
|
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
|
|
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 &&
|
|
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) ??
|
|
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([
|
|
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
|
-
: [
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
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.", [
|
|
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",
|
|
3466
|
-
|
|
3467
|
-
|
|
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
|
-
"
|
|
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,
|
|
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",
|
|
3484
|
-
|
|
3485
|
-
|
|
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
|
-
"
|
|
3490
|
-
"
|
|
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,
|
|
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/
|
|
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",
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
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
|
|
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", "
|
|
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", [
|
|
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",
|
|
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
|
-
? {
|
|
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
|
|
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
|
|
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
|
|
3534
|
-
"Always state the exact PASS/FAIL status and findings from
|
|
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
|
|
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,
|
|
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,
|
|
3548
|
-
|
|
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: {
|
|
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: [
|
|
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: {
|
|
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: {
|
|
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
|
|
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-
|
|
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
|
|
3994
|
-
subtask_prompt: "
|
|
3995
|
-
shell: {
|
|
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,
|
|
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, {
|
|
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
|
|
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, {
|
|
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());
|