@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.1
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 +67 -16
- package/dist/application/task-lifecycle/advance.js +309 -9
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/application/task-lifecycle/plan-transitions.js +13 -21
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/dag-pi-executor.js +18 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +98 -44
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +229 -18
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/session-store.js +39 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/interview/grill-me.js +7 -6
- package/dist/worker/console/operator-actions.js +785 -85
- package/dist/worker/console/prd-intake-bridge.js +393 -0
- package/dist/worker/console/recovery-cta.js +35 -6
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +1 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observability/read-model.js +3 -0
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/failure-category.js +3 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +49 -55
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/retry-policy.js +44 -11
- package/dist/workflows/dag/runner.js +6 -0
- package/dist/workflows/dag/scheduler.js +49 -1
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/validate.js +16 -2
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/agent-dag.schema.json +4 -4
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
|
@@ -1,31 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
7
|
export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
|
|
8
8
|
export const FRONTEND_WORKTREE_BASELINE_SCHEMA_ID = "frontend-worktree-baseline-v1";
|
|
9
|
-
function runGit(cwd, args) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
cwd,
|
|
13
|
-
env: process.env,
|
|
14
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
15
|
-
});
|
|
16
|
-
let stdout = "";
|
|
17
|
-
let stderr = "";
|
|
18
|
-
child.stdout.on("data", (chunk) => {
|
|
19
|
-
stdout += String(chunk);
|
|
20
|
-
});
|
|
21
|
-
child.stderr.on("data", (chunk) => {
|
|
22
|
-
stderr += String(chunk);
|
|
23
|
-
});
|
|
24
|
-
child.on("error", reject);
|
|
25
|
-
child.on("close", (code) => {
|
|
26
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
27
|
-
});
|
|
28
|
-
});
|
|
9
|
+
async function runGit(cwd, args) {
|
|
10
|
+
const result = await runResilientGitCommand({ cwd, args, readOnly: true });
|
|
11
|
+
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
29
12
|
}
|
|
30
13
|
function splitLines(text) {
|
|
31
14
|
return text
|
|
@@ -52,8 +35,9 @@ async function sha256File(filePath) {
|
|
|
52
35
|
}
|
|
53
36
|
}
|
|
54
37
|
export async function captureFrontendWorktreeBaseline(input) {
|
|
38
|
+
const run = input.dependencies?.runGit ?? runGit;
|
|
55
39
|
const root = path.resolve(input.workspaceRoot);
|
|
56
|
-
const status = await
|
|
40
|
+
const status = await run(root, [
|
|
57
41
|
"status",
|
|
58
42
|
"--porcelain=v1",
|
|
59
43
|
"--untracked-files=all",
|
|
@@ -84,6 +68,7 @@ export async function captureFrontendWorktreeBaseline(input) {
|
|
|
84
68
|
* patch + inventory under the current run directory before review runs.
|
|
85
69
|
*/
|
|
86
70
|
export async function runFrontendWorktreeDiffGate(input) {
|
|
71
|
+
const run = input.dependencies?.runGit ?? runGit;
|
|
87
72
|
const root = path.resolve(input.workspaceRoot);
|
|
88
73
|
let baseline;
|
|
89
74
|
try {
|
|
@@ -101,16 +86,16 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
101
86
|
if (input.requireBaseline && !baseline) {
|
|
102
87
|
throw new Error("frontend worktree baseline is required for this review context");
|
|
103
88
|
}
|
|
104
|
-
const revParse = await
|
|
89
|
+
const revParse = await run(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
105
90
|
if (revParse.code !== 0 || revParse.stdout.trim() !== "true") {
|
|
106
91
|
throw new Error(`frontend worktree diff gate requires a git worktree: ${revParse.stderr.trim() || revParse.stdout.trim() || "not a git repository"}`);
|
|
107
92
|
}
|
|
108
93
|
// Match failure capture semantics: binary-capable tracked diff vs HEAD.
|
|
109
|
-
const diff = await
|
|
94
|
+
const diff = await run(root, ["diff", "--binary", "HEAD"]);
|
|
110
95
|
if (diff.code !== 0) {
|
|
111
96
|
throw new Error(`frontend worktree diff gate git diff failed: ${diff.stderr.trim() || diff.stdout.trim()}`);
|
|
112
97
|
}
|
|
113
|
-
const nameOnly = await
|
|
98
|
+
const nameOnly = await run(root, [
|
|
114
99
|
"diff",
|
|
115
100
|
"--name-only",
|
|
116
101
|
"--diff-filter=ACDMRTUXB",
|
|
@@ -119,7 +104,7 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
119
104
|
if (nameOnly.code !== 0) {
|
|
120
105
|
throw new Error(`frontend worktree diff gate git name-only failed: ${nameOnly.stderr.trim() || nameOnly.stdout.trim()}`);
|
|
121
106
|
}
|
|
122
|
-
const untracked = await
|
|
107
|
+
const untracked = await run(root, [
|
|
123
108
|
"ls-files",
|
|
124
109
|
"--others",
|
|
125
110
|
"--exclude-standard",
|
|
@@ -152,7 +137,7 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
152
137
|
});
|
|
153
138
|
const patchSource = baseline
|
|
154
139
|
? changedFiles.length > 0
|
|
155
|
-
? await
|
|
140
|
+
? await run(root, ["diff", "--binary", "HEAD", "--", ...changedFiles])
|
|
156
141
|
: { code: 0, stdout: "", stderr: "" }
|
|
157
142
|
: diff;
|
|
158
143
|
if (patchSource.code !== 0) {
|
|
@@ -9,7 +9,7 @@ import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
|
9
9
|
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
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
12
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
13
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";
|
|
@@ -3508,29 +3508,23 @@ const readme=fs.existsSync('testcase/md/README.md')?fs.readFileSync('testcase/md
|
|
|
3508
3508
|
const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
|
|
3509
3509
|
const bt=String.fromCharCode(96);
|
|
3510
3510
|
const stripBackticks=s=>s.split(bt).join('');
|
|
3511
|
-
const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(
|
|
3511
|
+
const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(st==='readme')return 'reserved-module-stem';if(!/^[a-z][a-z0-9_]*$/.test(st))return 'invalid-syntax';if(/^(?:be|tp|ac|req|br)[_-]/i.test(st))return 'case-like-module-stem';return null;};
|
|
3512
3512
|
const valid=raw=>invalidReason(raw)===null;
|
|
3513
3513
|
const rxMdPath=/testcase\\/md\\/([A-Za-z0-9_.-]+)\\.md/g;
|
|
3514
3514
|
const rxTableRow=/\\|\\s*([A-Za-z0-9_.-]+)\\s*\\|\\s*testcase\\/test_/g;
|
|
3515
3515
|
const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
|
|
3516
|
-
const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
|
|
3517
|
-
const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
|
|
3518
|
-
if(headings.length!==1){process.stderr.write((headings.length===0?'missing-module-index':'duplicate-module-index')+'; require exactly one exact ## Module Index section\\n');process.exit(2);}
|
|
3519
|
-
const start=headings[0]+1;let end=allLines.length;for(let i=start;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){end=i;break;}}
|
|
3520
|
-
const section=allLines.slice(start,end).join('\\n');
|
|
3521
3516
|
const raw=[];
|
|
3522
|
-
const lines=
|
|
3517
|
+
const lines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n').filter(l=>l.includes('|'));
|
|
3523
3518
|
for(const line of lines){
|
|
3524
3519
|
const bare=stripBackticks(line);
|
|
3525
3520
|
for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
|
|
3526
|
-
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])
|
|
3521
|
+
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])==='priority-only-module-stem')raw.push(m[1]);}
|
|
3527
3522
|
}
|
|
3528
|
-
for(const m of
|
|
3523
|
+
for(const m of readme.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
3529
3524
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
3530
3525
|
if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
|
|
3531
3526
|
const seen=new Set();const modules=[];
|
|
3532
3527
|
for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
|
|
3533
|
-
if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
|
|
3534
3528
|
process.stdout.write(JSON.stringify({modules}));
|
|
3535
3529
|
`;
|
|
3536
3530
|
const encoded = Buffer.from(script, "utf8").toString("base64");
|
|
@@ -3608,7 +3602,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3608
3602
|
"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.",
|
|
3609
3603
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
3610
3604
|
"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.",
|
|
3611
|
-
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output
|
|
3605
|
+
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output budget, and keep the total module count at the smallest safe value. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. 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.",
|
|
3612
3606
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3613
3607
|
intake.boundedSourceContext,
|
|
3614
3608
|
"## Authoritative reference index",
|
|
@@ -3653,11 +3647,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3653
3647
|
workflowNodeId: "generate-backend-md-cases-map",
|
|
3654
3648
|
itemsFrom: "$.nodes['materialize-backend-md-module-manifest-shell'].output.modules",
|
|
3655
3649
|
itemName: "item",
|
|
3656
|
-
maxItems:
|
|
3657
|
-
maxExpandedNodes:
|
|
3650
|
+
maxItems: 64,
|
|
3651
|
+
maxExpandedNodes: 64,
|
|
3658
3652
|
childIdPrefix: "generate-backend-md-case",
|
|
3659
|
-
tokenBudget: { maxTotalTokens:
|
|
3660
|
-
failOnTokenBudgetExhaustion: true,
|
|
3653
|
+
tokenBudget: { maxTokensPerCase: 16384, maxTotalTokens: 600000 },
|
|
3661
3654
|
childTask: {
|
|
3662
3655
|
executor: "pi",
|
|
3663
3656
|
role: "implementer",
|
|
@@ -3679,10 +3672,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3679
3672
|
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
3680
3673
|
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.",
|
|
3681
3674
|
"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.",
|
|
3682
|
-
'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.
|
|
3683
|
-
"Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`.
|
|
3675
|
+
'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol.',
|
|
3676
|
+
"Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3684
3677
|
"Every Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
3685
|
-
"In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`,
|
|
3678
|
+
"In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3686
3679
|
intake.boundedSourceContext,
|
|
3687
3680
|
"## Authoritative reference index",
|
|
3688
3681
|
JSON.stringify(intake.referenceIndex, null, 2),
|
|
@@ -3695,34 +3688,32 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3695
3688
|
const reviewCases = {
|
|
3696
3689
|
id: "review-and-revise-backend-md-cases-pi",
|
|
3697
3690
|
depends_on: [generateMdCasesMap.id],
|
|
3698
|
-
role: "
|
|
3699
|
-
executor: "
|
|
3700
|
-
|
|
3701
|
-
complexity: "MED",
|
|
3691
|
+
role: "reviewer",
|
|
3692
|
+
executor: "static",
|
|
3693
|
+
complexity: "LOW",
|
|
3702
3694
|
writePolicy: "exclusive",
|
|
3703
3695
|
writeSet: ["testcase/md/**"],
|
|
3704
|
-
allowedPaths:
|
|
3696
|
+
allowedPaths: ["testcase/md/**"],
|
|
3705
3697
|
forbiddenPaths: forbidden,
|
|
3706
|
-
|
|
3707
|
-
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked. Perform exactly one bounded incremental synchronization of testcase/md/** against all bound source references; preserve valid Cases and report a concise summary.",
|
|
3698
|
+
outputContract: "Deterministic advisory handoff that reserves testcase/md/** in the approved DAG write set for upstream map children; no model invocation or writes.",
|
|
3708
3699
|
subtask_prompt: [
|
|
3709
|
-
"
|
|
3710
|
-
"Output budget protocol: never dump full Matrix/case bodies into assistant chat.
|
|
3700
|
+
"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.",
|
|
3701
|
+
"Output budget protocol: default to local edit per file; never dump full Matrix/case bodies into assistant chat. Review order is README (Scope/Matrix) then one module file per turn. When adding omitted in-scope cases, write one file per tool call and keep every required section. Do not bulk-delete in-scope cases to save tokens.",
|
|
3711
3702
|
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable with an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target.",
|
|
3712
3703
|
"Treat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. For every in-scope operation, check applicable lifecycle/uniqueness states (including deleted-existing when in scope), valid enum values, bounded invalid classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, status/error codes, auth and state transitions. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.",
|
|
3713
3704
|
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in `testcase/md/README.md`, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point ↔ Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
|
|
3714
|
-
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each
|
|
3715
|
-
"
|
|
3716
|
-
"For affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints.",
|
|
3717
|
-
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in README or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
|
|
3718
|
-
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
3705
|
+
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol, assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, ensure every binding-list Test Point is also present in that Case's `### 测试点`, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
3706
|
+
"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.",
|
|
3719
3707
|
intake.boundedSourceContext,
|
|
3720
3708
|
"## Authoritative reference index",
|
|
3721
3709
|
JSON.stringify(intake.referenceIndex, null, 2),
|
|
3722
3710
|
"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.",
|
|
3723
3711
|
].join("\n\n"),
|
|
3712
|
+
static: {
|
|
3713
|
+
resultMarkdown: "Markdown module writers completed. Deterministic node 6 validation owns advisory structure/coverage findings; no model review or rewrite was invoked.",
|
|
3714
|
+
},
|
|
3724
3715
|
};
|
|
3725
|
-
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope and Coverage Matrix against final Case rule/test-point bindings. Validate the classification-policy pair, affected operations/rules, scope evidence and regression floor; require documented OpenAPI completeness only for declared affected operations, while all explicit AC/REQ/BR remain in scope. Detect missing in-scope product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT, bidirectional Matrix/Case drift, non-canonical Case IDs, unclassified Test Points, duplicate binding modes and non-cross-cutting Test Points bound by multiple Cases. Do not validate source-reference existence. Write human and machine evidence from the same facts. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected. Coverage FAIL stays advisory.", "Run-owned reports/backend-md-case-validation.md, reports/backend-test-case-coverage-analysis.md and contracts/backend-test-case-coverage-facts.json
|
|
3716
|
+
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope and Coverage Matrix against final Case rule/test-point bindings. Validate the classification-policy pair, affected operations/rules, scope evidence and regression floor; require documented OpenAPI completeness only for declared affected operations, while all explicit AC/REQ/BR remain in scope. Detect missing in-scope product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT, bidirectional Matrix/Case drift, non-canonical Case IDs, unclassified Test Points, duplicate binding modes and non-cross-cutting Test Points bound by multiple Cases. Do not validate source-reference existence. Write human and machine evidence from the same facts. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected. Coverage FAIL stays advisory.", "Run-owned reports/backend-md-case-validation.md, reports/backend-test-case-coverage-analysis.md and contracts/backend-test-case-coverage-facts.json v3 with Coverage Scope plus PASS/FAIL/UNAVAILABLE advisory facts; downstream execution continues.");
|
|
3726
3717
|
// N5 line (sharded): pytest shared-asset plan → manifest shell → map_agent barrier.
|
|
3727
3718
|
// Shared helpers/factories are written once by the plan node; each module's
|
|
3728
3719
|
// test_<stem>.py is written by an independent Pi child (own 16K budget).
|
|
@@ -3786,11 +3777,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3786
3777
|
workflowNodeId: "generate-backend-pytest-cases-map",
|
|
3787
3778
|
itemsFrom: "$.nodes['materialize-backend-pytest-module-manifest-shell'].output.modules",
|
|
3788
3779
|
itemName: "item",
|
|
3789
|
-
maxItems:
|
|
3790
|
-
maxExpandedNodes:
|
|
3780
|
+
maxItems: 64,
|
|
3781
|
+
maxExpandedNodes: 64,
|
|
3791
3782
|
childIdPrefix: "generate-backend-pytest-case",
|
|
3792
|
-
tokenBudget: { maxTotalTokens:
|
|
3793
|
-
failOnTokenBudgetExhaustion: true,
|
|
3783
|
+
tokenBudget: { maxTokensPerCase: 16384, maxTotalTokens: 600000 },
|
|
3794
3784
|
childTask: {
|
|
3795
3785
|
executor: "pi",
|
|
3796
3786
|
role: "implementer",
|
|
@@ -3818,10 +3808,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3818
3808
|
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. After reading the module Markdown and the bounded pytest config/conftest, immediately use write tools to create the single file testcase/test_{{item.stem}}.py. Define any bounded HTTP client fixture, request logging/redaction/truncation helper and payload builders needed by this module inside that same file; do not import generated testcase/**/helpers/** or testcase/**/factories/** assets. Do not end after analysis or planning. Do not modify Markdown, conftest, helpers/factories, or any other module's pytest script.",
|
|
3819
3809
|
"Output budget protocol (hard, max output <=16K per turn): Write exactly one test_{{item.stem}}.py. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
3820
3810
|
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them.",
|
|
3821
|
-
'Ensure every
|
|
3811
|
+
'Ensure every final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. Use `pytest.param(..., id=...)` for every row; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
3822
3812
|
"Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly one `testcase/test_{{item.stem}}.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `resource_notes` → `testcase/test_resource_notes.py`, `health` → `testcase/test_health.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. 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.",
|
|
3823
3813
|
"Keep this module self-contained: define module-local fixtures and helpers directly in testcase/test_{{item.stem}}.py, so pytest discovers every fixture dependency without external plugin registration. 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. Recursively redact sensitive values and apply bounded truncation before logging.",
|
|
3824
|
-
"Materialize every automatable Markdown Case exactly once as one canonical primary pytest symbol. Preserve every explicit variant Test Point as a stable pytest.param id and every assertion/cross-cutting binding as declared. Build request payloads from the effective Markdown test data literally: keep all declared DTO keys, nested shapes, enum values, missing/null/boundary variants and business-state preconditions; never substitute guessed convenience fields or rename contract fields.",
|
|
3825
3814
|
"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`.",
|
|
3826
3815
|
].join("\n\n"),
|
|
3827
3816
|
},
|
|
@@ -3855,24 +3844,23 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3855
3844
|
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.",
|
|
3856
3845
|
subtask_prompt: [
|
|
3857
3846
|
"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.",
|
|
3858
|
-
"Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, close generated fixture dependencies
|
|
3859
|
-
"This is the single pytest incremental synchronization round. For every assessment-listed path, compare the effective Markdown Case/Test Points/test data and its `Payload Contract`/`Payload Required Paths`/`Payload Allowed Paths`/`Payload Enum` labels with the generated module. Incrementally add or repair only missing symbols, params, assertions and payload builders. Repair every assessment-listed missing nested path, unexpected key and enum mismatch; preserve exact DTO keys, nested shapes, enum/boundary literals, operation transport and business preconditions; remove guessed replacement keys only when the effective Markdown proves the exact contract.",
|
|
3847
|
+
"Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, or close generated fixture dependencies and plugin registration. For fixture defects inspect both provider and importer; when a shared fixture depends on sibling fixtures, register the whole provider module through an exact pytest_plugins declaration rather than importing only the outer fixture. Do not create unrelated pytest scripts.",
|
|
3860
3848
|
"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.",
|
|
3861
3849
|
"Use local edit only on assessment-listed paths; keep summaries short; never rewrite unrelated modules.",
|
|
3862
|
-
"Do not
|
|
3850
|
+
"Do not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
|
|
3863
3851
|
"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.",
|
|
3864
3852
|
"Do not execute pytest; the deterministic effective collection gate owns the final collection attempt.",
|
|
3865
3853
|
].join("\n\n"),
|
|
3866
3854
|
};
|
|
3867
|
-
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection+fixture readiness passed, verify unchanged asset hashes and reuse it. If the single repair ran, rerun scenario-param preflight, final collection and no-business-body fixture resolution once.
|
|
3855
|
+
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection+fixture readiness passed, verify unchanged asset hashes and reuse it. If the single repair ran, rerun scenario-param preflight, final collection and no-business-body fixture resolution once. BLOCKED facts, repair failure, residual fixture failure or hash drift prevent business pytest execution. Materialize canonical backend-test-execution-readiness.json.", "Run-owned effective collection-v3 facts plus contracts/backend-test-execution-readiness.json proving exact final assets are collectable, fixture-resolvable and hash-bound; initial PASS is reused, repair path records attempt=1.", [], 120000);
|
|
3868
3856
|
collectionEffective.dependsPolicy = "all-or-condition-skip";
|
|
3869
|
-
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only final readiness-authorized Markdown-mapped pytest scripts. Produce bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence
|
|
3857
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only final readiness-authorized Markdown-mapped pytest scripts. Produce bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence and logging findings. scenario-param assessment/repair already ran before collection; consume and display its final PASS/PARTIAL/FAIL/UNAVAILABLE facts without modifying pytest assets after readiness was frozen. Correspondence findings remain advisory; Never block pytest solely on correspondence FAIL.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md, contracts/backend-test-markdown-pytest-correspondence-facts.json, reports/backend-test-scenario-param-consistency.md and contracts/backend-test-scenario-param-consistency-facts.json (initial+final) with optional repair audit; PASS/FAIL/UNAVAILABLE correspondence facts bound after effective collection.");
|
|
3870
3858
|
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.");
|
|
3871
3859
|
const pytestCommand = [
|
|
3872
3860
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3873
|
-
'echo "pytest targets are resolved at runtime from
|
|
3861
|
+
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3874
3862
|
].join("; ");
|
|
3875
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Read canonical contracts/backend-test-execution-readiness.json
|
|
3863
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Read canonical contracts/backend-test-execution-readiness.json, verify final asset hashes, then 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 6 Markdown validation + case coverage and node 13 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);
|
|
3876
3864
|
if (execute.shell) {
|
|
3877
3865
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3878
3866
|
}
|
|
@@ -4186,7 +4174,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4186
4174
|
commands: [
|
|
4187
4175
|
[
|
|
4188
4176
|
"node -e",
|
|
4189
|
-
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*[
|
|
4177
|
+
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*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s"'\\x60<>]+)/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 from context.md');baseUrl=baseUrl.replace(/[)\\}\\],."'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();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,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
4190
4178
|
].join(" "),
|
|
4191
4179
|
],
|
|
4192
4180
|
cwd: ".",
|
|
@@ -5666,20 +5654,26 @@ function cloneTask(task, patch = {}) {
|
|
|
5666
5654
|
return { ...task, ...patch };
|
|
5667
5655
|
}
|
|
5668
5656
|
/**
|
|
5669
|
-
* Apply
|
|
5670
|
-
* verifier/supervisor/closeout
|
|
5671
|
-
*
|
|
5657
|
+
* Apply default Pi retry policies to generated DAG nodes:
|
|
5658
|
+
* - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
|
|
5659
|
+
* - exclusive implementers get a single clean-timeout transport retry
|
|
5660
|
+
* (provider stall with zero attributed writes only)
|
|
5661
|
+
* Dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
|
|
5672
5662
|
* never overwrites an explicit retryPolicy a task already declares.
|
|
5673
5663
|
*/
|
|
5674
5664
|
function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
5675
5665
|
for (const task of spec.tasks) {
|
|
5676
5666
|
if (task.retryPolicy !== undefined)
|
|
5677
5667
|
continue;
|
|
5678
|
-
if (
|
|
5668
|
+
if (isSafeReadOnlyPiRetryCandidate(task)) {
|
|
5669
|
+
task.retryPolicy = task.outputProtocol
|
|
5670
|
+
? PROTOCOL_AWARE_PI_RETRY_POLICY
|
|
5671
|
+
: DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
5679
5672
|
continue;
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5673
|
+
}
|
|
5674
|
+
if (isWriterTransportRetryCandidate(task)) {
|
|
5675
|
+
task.retryPolicy = WRITER_TRANSPORT_RETRY_POLICY;
|
|
5676
|
+
}
|
|
5683
5677
|
}
|
|
5684
5678
|
}
|
|
5685
5679
|
function getTaskOrThrow(spec, id) {
|
|
@@ -9,7 +9,7 @@ import { resolveContextPolicy } from "./context-policy.js";
|
|
|
9
9
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
10
10
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
11
11
|
import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
|
|
12
|
-
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, } from "./retry-policy.js";
|
|
12
|
+
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
13
13
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
14
14
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
15
15
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
@@ -577,7 +577,8 @@ export async function executeDagNode(input) {
|
|
|
577
577
|
}
|
|
578
578
|
const retryPolicy = task.retryPolicy &&
|
|
579
579
|
(isSafeReadOnlyPiRetryCandidate(task) ||
|
|
580
|
-
isWriterEmptyDiffRetryCandidate(task)
|
|
580
|
+
isWriterEmptyDiffRetryCandidate(task) ||
|
|
581
|
+
isWriterTransportRetryCandidate(task))
|
|
581
582
|
? task.retryPolicy
|
|
582
583
|
: undefined;
|
|
583
584
|
const maxAttempts = retryPolicy?.maxAttempts ?? 1;
|
|
@@ -25,6 +25,13 @@ export const STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY = "invalid-output";
|
|
|
25
25
|
export const WRITER_EMPTY_DIFF_RETRY_CATEGORY = "writer-empty-diff";
|
|
26
26
|
/** Retry when a backend-test writer finished but Completeness Gate found missing/broken targets. */
|
|
27
27
|
export const INCOMPLETE_WRITE_SET_RETRY_CATEGORY = "incomplete-write-set";
|
|
28
|
+
/**
|
|
29
|
+
* Provider/transport stall or absolute timeout on a Pi exclusive writer that
|
|
30
|
+
* left zero attributed workspace writes and zero write-tool calls. Safe to
|
|
31
|
+
* retry once because the attempt was a pure provider flake (no side effects).
|
|
32
|
+
* Partial writes keep the original `timeout` category and do NOT retry.
|
|
33
|
+
*/
|
|
34
|
+
export const WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY = "writer-clean-timeout";
|
|
28
35
|
export const STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES = [
|
|
29
36
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
30
37
|
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
@@ -43,6 +50,7 @@ export const ALL_DAG_RETRY_CATEGORIES = [
|
|
|
43
50
|
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
44
51
|
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
45
52
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
53
|
+
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
46
54
|
];
|
|
47
55
|
const RETRY_SAFE_PI_ROLES = new Set([
|
|
48
56
|
"planner",
|
|
@@ -119,9 +127,9 @@ export const PROTOCOL_AWARE_PI_RETRY_POLICY = {
|
|
|
119
127
|
retryCategories: [...PROTOCOL_AWARE_DAG_RETRY_CATEGORIES],
|
|
120
128
|
};
|
|
121
129
|
/**
|
|
122
|
-
* The sole writer retry policy. It is
|
|
123
|
-
* read-only default: a writer may retry only
|
|
124
|
-
* otherwise successful attempt changed no files.
|
|
130
|
+
* The sole writer retry policy for requireChangedFiles writers. It is
|
|
131
|
+
* intentionally not included in any read-only default: a writer may retry only
|
|
132
|
+
* after its executor proves an otherwise successful attempt changed no files.
|
|
125
133
|
*/
|
|
126
134
|
export const WRITER_EMPTY_DIFF_RETRY_POLICY = {
|
|
127
135
|
maxAttempts: 2,
|
|
@@ -130,6 +138,18 @@ export const WRITER_EMPTY_DIFF_RETRY_POLICY = {
|
|
|
130
138
|
maxDelayMs: 30000,
|
|
131
139
|
retryCategories: [WRITER_EMPTY_DIFF_RETRY_CATEGORY],
|
|
132
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Bounded transport retry for standard exclusive implementers when a provider
|
|
143
|
+
* stall/timeout left no workspace writes. maxAttempts=2 (one retry). Never
|
|
144
|
+
* retries timeout with partial writes (those stay non-retryable `timeout`).
|
|
145
|
+
*/
|
|
146
|
+
export const WRITER_TRANSPORT_RETRY_POLICY = {
|
|
147
|
+
maxAttempts: 2,
|
|
148
|
+
backoff: "exponential",
|
|
149
|
+
initialDelayMs: 5000,
|
|
150
|
+
maxDelayMs: 30000,
|
|
151
|
+
retryCategories: [WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY],
|
|
152
|
+
};
|
|
133
153
|
/**
|
|
134
154
|
* Backend-test generation writers: empty-diff once, plus bounded incomplete-write-set
|
|
135
155
|
* recovery attempts driven by Completeness Gate (missing/broken target files).
|
|
@@ -154,24 +174,37 @@ export function isRetryablePiFailureCategory(rawFailureCategory, options = {}) {
|
|
|
154
174
|
const categories = options.retryCategories ?? DEFAULT_DAG_RETRY_CATEGORIES;
|
|
155
175
|
return categories.includes(rawFailureCategory);
|
|
156
176
|
}
|
|
157
|
-
|
|
158
|
-
* Static eligibility for the sole retryable writer class. This deliberately
|
|
159
|
-
* does not infer a no-op: the Pi executor assigns writer-empty-diff only after
|
|
160
|
-
* post-write-guard attribution proves an empty changedFiles list.
|
|
161
|
-
*/
|
|
162
|
-
export function isWriterEmptyDiffRetryCandidate(task) {
|
|
177
|
+
function isExclusivePiImplementer(task) {
|
|
163
178
|
return (task.executor === "pi" &&
|
|
164
179
|
task.role === "implementer" &&
|
|
165
180
|
task.toolProfile === "write" &&
|
|
166
181
|
task.writePolicy === "exclusive" &&
|
|
167
182
|
(task.writeSet?.length ?? 0) > 0 &&
|
|
168
|
-
task.writerOutcomePolicy?.requireChangedFiles === true &&
|
|
169
183
|
!task.decisionGate?.enabled &&
|
|
170
184
|
!task.dynamicExpansion &&
|
|
171
185
|
!task.dynamicReduction &&
|
|
172
186
|
!task.dynamicCondition &&
|
|
173
187
|
!task.dynamicLoopUntil);
|
|
174
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Static eligibility for the requireChangedFiles writer-empty-diff class.
|
|
191
|
+
* The Pi executor assigns writer-empty-diff only after post-write-guard
|
|
192
|
+
* attribution proves an empty changedFiles list.
|
|
193
|
+
*/
|
|
194
|
+
export function isWriterEmptyDiffRetryCandidate(task) {
|
|
195
|
+
return (isExclusivePiImplementer(task) &&
|
|
196
|
+
task.writerOutcomePolicy?.requireChangedFiles === true);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Standard exclusive implementers (implementation-outcome writers) may take a
|
|
200
|
+
* single clean-timeout transport retry. Backend completeness writers already
|
|
201
|
+
* have their own policy and are excluded here.
|
|
202
|
+
*/
|
|
203
|
+
export function isWriterTransportRetryCandidate(task) {
|
|
204
|
+
return (isExclusivePiImplementer(task) &&
|
|
205
|
+
task.writerOutcomePolicy?.type === "implementation-outcome-v1" &&
|
|
206
|
+
task.writerOutcomePolicy.requireChangedFiles !== true);
|
|
207
|
+
}
|
|
175
208
|
export function isSafeReadOnlyPiRetryCandidate(task) {
|
|
176
209
|
if (task.executor !== "pi")
|
|
177
210
|
return false;
|
|
@@ -210,6 +243,6 @@ export function computeBackoffDelayMs(attemptNumber, policy) {
|
|
|
210
243
|
return Math.min(policy.initialDelayMs, policy.maxDelayMs);
|
|
211
244
|
}
|
|
212
245
|
const exponent = attemptNumber - 2;
|
|
213
|
-
const raw = policy.initialDelayMs *
|
|
246
|
+
const raw = policy.initialDelayMs * 2 ** exponent;
|
|
214
247
|
return Math.min(raw, policy.maxDelayMs);
|
|
215
248
|
}
|
|
@@ -278,6 +278,7 @@ export async function runDag(spec, opts) {
|
|
|
278
278
|
maxConcurrent,
|
|
279
279
|
executeNode: opts.executeNode,
|
|
280
280
|
observer: opts.observer,
|
|
281
|
+
abortSignal: opts.abortSignal,
|
|
281
282
|
activeRunDir,
|
|
282
283
|
completedRunDir,
|
|
283
284
|
pausedRunDir,
|
|
@@ -309,6 +310,7 @@ export async function runDagContinuation(opts) {
|
|
|
309
310
|
maxConcurrent,
|
|
310
311
|
executeNode: opts.executeNode,
|
|
311
312
|
observer: opts.observer,
|
|
313
|
+
abortSignal: opts.abortSignal,
|
|
312
314
|
activeRunDir,
|
|
313
315
|
completedRunDir,
|
|
314
316
|
pausedRunDir,
|
|
@@ -403,6 +405,7 @@ export async function resumeDagRun(opts) {
|
|
|
403
405
|
maxConcurrent,
|
|
404
406
|
executeNode: opts.executeNode,
|
|
405
407
|
observer: opts.observer,
|
|
408
|
+
// Resume path intentionally has no abortSignal field on ResumeDagRunOptions yet.
|
|
406
409
|
activeRunDir,
|
|
407
410
|
completedRunDir,
|
|
408
411
|
pausedRunDir,
|
|
@@ -501,6 +504,7 @@ async function executeDagCheckpoint(input) {
|
|
|
501
504
|
tasksById,
|
|
502
505
|
maxConcurrent,
|
|
503
506
|
persistState,
|
|
507
|
+
abortSignal: input.abortSignal,
|
|
504
508
|
createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
|
|
505
509
|
baseExecuteNode,
|
|
506
510
|
customExecuteNode: input.executeNode,
|
|
@@ -509,6 +513,8 @@ async function executeDagCheckpoint(input) {
|
|
|
509
513
|
meta: { runDir, runId: state.runId, spec },
|
|
510
514
|
}),
|
|
511
515
|
executeScheduledNode: async (nodeId, executeNode, onPause) => {
|
|
516
|
+
if (input.abortSignal?.aborted)
|
|
517
|
+
return;
|
|
512
518
|
if (isHardBudgetBreached(state.budgetLedger))
|
|
513
519
|
return;
|
|
514
520
|
const preBreach = preflightBudgetOrBreach(state);
|