@tea-agent/loop-agent 0.27.1-beta.2 → 0.28.0
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 +40 -1
- package/dist/application/task-lifecycle/observe.js +5 -0
- package/dist/application/task-lifecycle/plan-transitions.js +7 -2
- package/dist/cli/program.js +1 -1
- package/dist/commands/client-recovery.js +439 -20
- package/dist/commands/init.js +42 -6
- package/dist/executors/dag-pi-executor.js +165 -56
- package/dist/executors/pi-playwright-cli-tool.js +955 -0
- package/dist/executors/pi-sdk-executor.js +56 -0
- package/dist/executors/playwright-cli-launcher.js +63 -0
- package/dist/executors/shell-executor.js +128 -0
- package/dist/shared/playwright-cli-command-policy.js +41 -0
- package/dist/worker/observability/read-model.js +66 -8
- package/dist/worker/observe/static/dag-model.js +85 -13
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +4 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +13 -13
- package/dist/workflows/dag/frontend-implementation-contract.js +6 -124
- package/dist/workflows/dag/frontend-prewrite-gate.js +5 -35
- package/dist/workflows/dag/frontend-test-case-checklist.js +201 -8
- package/dist/workflows/dag/frontend-test-result-contract.js +52 -3
- package/dist/workflows/dag/init-hybrid.js +154 -95
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/node-execution.js +11 -5
- package/dist/workflows/dag/output-protocol.js +25 -106
- package/dist/workflows/dag/report.js +9 -2
- package/dist/workflows/dag/rerun-run.js +62 -3
- package/dist/workflows/dag/run-store.js +6 -1
- package/dist/workflows/dag/runner.js +15 -3
- package/dist/workflows/dag/types.js +27 -0
- package/dist/workflows/dag/validate.js +121 -1
- package/docs/architecture/runtime-boundaries.md +13 -11
- package/docs/init-surface.manifest.json +6 -2
- package/docs/templates/README.md +9 -1
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +14 -7
- package/docs/templates/frontend-test-dag.json +55 -15
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -9
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-execution.prompt.md +1 -1
- package/harness.json +4 -4
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +18 -6
- package/skills/playwright-cli/SKILL.md +69 -402
- package/skills/playwright-cli/references/tracing.md +3 -137
- package/skills/playwright-cli/references/video-recording.md +3 -141
- package/skills/playwright-cli-case-generator/SKILL.md +53 -46
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -733,15 +734,16 @@ function resolveImplementPaths(taskConfig, options = {}) {
|
|
|
733
734
|
return normalized === "docs" || normalized.startsWith("docs/");
|
|
734
735
|
});
|
|
735
736
|
if (repoRoot && mayNeedDocIndex) {
|
|
736
|
-
const
|
|
737
|
+
const closure = mergeDocumentIndexCompanions({
|
|
737
738
|
repoRoot,
|
|
738
739
|
paths: allowed,
|
|
739
740
|
forbiddenPaths: forbidden,
|
|
740
741
|
});
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
742
|
+
const explicitAllowedPaths = new Set(allowed.map((entry) => entry.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")));
|
|
743
|
+
const missingCompanions = closure.companions.filter((companion) => !explicitAllowedPaths.has(companion));
|
|
744
|
+
if (missingCompanions.length > 0) {
|
|
745
|
+
throw new Error(`document-index-closure: catalog companion paths must be explicitly authorized in task allowedPaths: ${missingCompanions.map((companion) => `"${companion}"`).join(", ")}. Add each missing path with task advance --allowed-path <path>; required command(s): ${missingCompanions.map((companion) => `task advance --allowed-path ${companion}`).join("; ")}`);
|
|
746
|
+
}
|
|
745
747
|
}
|
|
746
748
|
return {
|
|
747
749
|
allowedPaths: allowed,
|
|
@@ -1078,14 +1080,40 @@ function isFrontendLintVerifyCommand(command) {
|
|
|
1078
1080
|
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
1079
1081
|
return /\b(?:lint|eslint)\b/i.test(text);
|
|
1080
1082
|
}
|
|
1083
|
+
function isManagedCiWrapperVerifyCommand(command) {
|
|
1084
|
+
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
1085
|
+
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
1086
|
+
}
|
|
1087
|
+
function repoHasNpmScript(repoRoot, scriptName) {
|
|
1088
|
+
if (!repoRoot)
|
|
1089
|
+
return false;
|
|
1090
|
+
const packagePath = path.join(repoRoot, "package.json");
|
|
1091
|
+
if (!existsSync(packagePath))
|
|
1092
|
+
return false;
|
|
1093
|
+
try {
|
|
1094
|
+
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1095
|
+
return typeof decoded.scripts?.[scriptName] === "string";
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1081
1101
|
function partitionFrontendStaticVerifyCommands(input) {
|
|
1082
1102
|
const commands = input.commands ?? [];
|
|
1083
|
-
|
|
1084
|
-
// project lint debt is allowed to remain outside this workflow, and an
|
|
1085
|
-
// explicitly mentioned lint command must not reintroduce the lint gate.
|
|
1103
|
+
const lintCommands = commands.filter(isFrontendLintVerifyCommand);
|
|
1086
1104
|
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
1105
|
+
if (lintCommands.length === 0 &&
|
|
1106
|
+
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
1107
|
+
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
1108
|
+
lintCommands.push({
|
|
1109
|
+
args: ["npm", "run", "lint"],
|
|
1110
|
+
cwd: input.repoRoot,
|
|
1111
|
+
label: "npm run lint",
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1087
1114
|
return {
|
|
1088
1115
|
lint: {
|
|
1116
|
+
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
1089
1117
|
commandSource: input.commandSource,
|
|
1090
1118
|
},
|
|
1091
1119
|
static: {
|
|
@@ -1184,6 +1212,7 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1184
1212
|
const staticCommands = firstExisting([
|
|
1185
1213
|
"typecheck",
|
|
1186
1214
|
"check-types",
|
|
1215
|
+
"lint",
|
|
1187
1216
|
"check",
|
|
1188
1217
|
"build",
|
|
1189
1218
|
]);
|
|
@@ -1660,6 +1689,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1660
1689
|
writeSet: implementPaths.writeSet,
|
|
1661
1690
|
allowedPaths: implementPaths.allowedPaths,
|
|
1662
1691
|
forbiddenPaths,
|
|
1692
|
+
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
1663
1693
|
subtask_prompt: [
|
|
1664
1694
|
"Implement the approved plan with minimal focused changes.",
|
|
1665
1695
|
"Stay within writeSet. Do not write root artifacts/** unless artifacts paths are explicitly declared in writeSet.",
|
|
@@ -2427,62 +2457,9 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2427
2457
|
sourceContext,
|
|
2428
2458
|
].join("\n\n"),
|
|
2429
2459
|
},
|
|
2430
|
-
{
|
|
2431
|
-
id: "frontend-contract-json-pi",
|
|
2432
|
-
depends_on: [
|
|
2433
|
-
"frontend-plan-revision-pi",
|
|
2434
|
-
"frontend-plan-pi",
|
|
2435
|
-
"frontend-final-design-review-pi",
|
|
2436
|
-
"frontend-design-review-pi",
|
|
2437
|
-
],
|
|
2438
|
-
dependsPolicy: "all-or-condition-skip",
|
|
2439
|
-
role: "planner",
|
|
2440
|
-
executor: "pi",
|
|
2441
|
-
complexity: "MED",
|
|
2442
|
-
writePolicy: "read-only",
|
|
2443
|
-
outputMode: "structured-required",
|
|
2444
|
-
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2445
|
-
allowedPaths: readOnlyPaths,
|
|
2446
|
-
forbiddenPaths,
|
|
2447
|
-
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2448
|
-
outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1. No Markdown, prose, comments, or code fences.",
|
|
2449
|
-
subtask_prompt: [
|
|
2450
|
-
"Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
|
|
2451
|
-
"Use frontend-plan-revision-pi when it is FINISHED; otherwise use frontend-plan-pi. Confirm the effective design review passed before producing the contract.",
|
|
2452
|
-
"Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
|
|
2453
|
-
"Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
|
|
2454
|
-
frontendContractSchemaBlock,
|
|
2455
|
-
sourceContext,
|
|
2456
|
-
].join("\n\n"),
|
|
2457
|
-
},
|
|
2458
|
-
{
|
|
2459
|
-
id: "frontend-contract-json-validate-shell",
|
|
2460
|
-
depends_on: ["frontend-contract-json-pi"],
|
|
2461
|
-
role: "verifier",
|
|
2462
|
-
executor: "shell",
|
|
2463
|
-
complexity: "LOW",
|
|
2464
|
-
writePolicy: "read-only",
|
|
2465
|
-
allowedPaths: readOnlyPaths,
|
|
2466
|
-
forbiddenPaths,
|
|
2467
|
-
outputContract: "Validated frontend implementation contract artifact with schema ID and SHA-256.",
|
|
2468
|
-
subtask_prompt: "Materialize and validate the structured frontend contract before prewrite authorization.",
|
|
2469
|
-
shell: {
|
|
2470
|
-
commands: [],
|
|
2471
|
-
jsonArtifactGate: {
|
|
2472
|
-
fromNodeId: "frontend-contract-json-pi",
|
|
2473
|
-
schemaId: "frontend-implementation-contract-v1",
|
|
2474
|
-
artifactName: "frontend-implementation-contract.json",
|
|
2475
|
-
outputDir: "contracts",
|
|
2476
|
-
},
|
|
2477
|
-
cwd: ".",
|
|
2478
|
-
timeoutMs: 60000,
|
|
2479
|
-
},
|
|
2480
|
-
},
|
|
2481
2460
|
{
|
|
2482
2461
|
id: "frontend-prewrite-gate-shell",
|
|
2483
2462
|
depends_on: [
|
|
2484
|
-
"frontend-contract-json-pi",
|
|
2485
|
-
"frontend-contract-json-validate-shell",
|
|
2486
2463
|
"frontend-final-design-review-pi",
|
|
2487
2464
|
"frontend-design-review-pi",
|
|
2488
2465
|
"frontend-plan-revision-pi",
|
|
@@ -2501,8 +2478,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2501
2478
|
commands: [],
|
|
2502
2479
|
frontendPrewriteGate: {
|
|
2503
2480
|
schemaVersion: 1,
|
|
2504
|
-
planFromNodeId: "frontend-
|
|
2505
|
-
planFallbackFromNodeIds: ["frontend-plan-
|
|
2481
|
+
planFromNodeId: "frontend-plan-revision-pi",
|
|
2482
|
+
planFallbackFromNodeIds: ["frontend-plan-pi"],
|
|
2506
2483
|
reviewFromNodeId: "frontend-final-design-review-pi",
|
|
2507
2484
|
reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
|
|
2508
2485
|
requiredRequirementIds: requirementIds,
|
|
@@ -3780,9 +3757,48 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3780
3757
|
assertValidDagSpec(spec);
|
|
3781
3758
|
return spec;
|
|
3782
3759
|
}
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3760
|
+
/**
|
|
3761
|
+
* Resolve the browser origin only from controller-owned task source bytes.
|
|
3762
|
+
* Model-authored RAG/case/evidence files are intentionally excluded.
|
|
3763
|
+
*/
|
|
3764
|
+
export function resolveControllerFrontendBaseUrl(sources) {
|
|
3765
|
+
const candidates = (sources.referenceDocuments ?? [])
|
|
3766
|
+
.filter((document) => /(?:^|[\\/])config\.md$/i.test(document.path))
|
|
3767
|
+
.map((document) => ({
|
|
3768
|
+
markdown: document.markdown,
|
|
3769
|
+
source: toDagSourcePath(sources, document.path),
|
|
3770
|
+
}));
|
|
3771
|
+
let rawBaseUrl = "http://localhost:5173";
|
|
3772
|
+
let baseUrlSource = "default-localhost-5173";
|
|
3773
|
+
for (const candidate of candidates) {
|
|
3774
|
+
const match = candidate.markdown.match(/(?:frontend[_-]?base[_-]?url|base[_-]?url|base[- ]url)\s*[:=]\s*["'`]?((?:https?):\/\/[^\s"'`<>]+)/i);
|
|
3775
|
+
if (!match?.[1]) {
|
|
3776
|
+
if (/(?:frontend[_-]?base[_-]?url|base[_-]?url|base[- ]url)\s*[:=]/i.test(candidate.markdown)) {
|
|
3777
|
+
throw new Error(`frontend-test controller baseUrl is invalid (${candidate.source})`);
|
|
3778
|
+
}
|
|
3779
|
+
continue;
|
|
3780
|
+
}
|
|
3781
|
+
rawBaseUrl = match[1].replace(/[)\]},.;]+$/, "");
|
|
3782
|
+
baseUrlSource = candidate.source;
|
|
3783
|
+
break;
|
|
3784
|
+
}
|
|
3785
|
+
let parsed;
|
|
3786
|
+
try {
|
|
3787
|
+
parsed = new URL(rawBaseUrl);
|
|
3788
|
+
}
|
|
3789
|
+
catch {
|
|
3790
|
+
throw new Error(`frontend-test controller baseUrl is invalid (${baseUrlSource})`);
|
|
3791
|
+
}
|
|
3792
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
3793
|
+
parsed.username ||
|
|
3794
|
+
parsed.password ||
|
|
3795
|
+
parsed.search ||
|
|
3796
|
+
parsed.hash ||
|
|
3797
|
+
/(?:^|\.)(?:www\.)?[^.]*(?:prod|production)/i.test(parsed.hostname)) {
|
|
3798
|
+
throw new Error(`frontend-test controller baseUrl is unsafe (${baseUrlSource})`);
|
|
3799
|
+
}
|
|
3800
|
+
return { baseUrl: parsed.toString(), baseUrlSource };
|
|
3801
|
+
}
|
|
3786
3802
|
function buildFrontendTestHybridDag(sources) {
|
|
3787
3803
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
3788
3804
|
const config = {
|
|
@@ -3805,7 +3821,14 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3805
3821
|
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
3806
3822
|
}
|
|
3807
3823
|
const forbidden = commonForbiddenPaths(sources);
|
|
3824
|
+
const controllerFrontend = resolveControllerFrontendBaseUrl(sources);
|
|
3808
3825
|
const ragWriteSet = ["testcase/frontend/rag/**"];
|
|
3826
|
+
const caseDraftWriteSet = [
|
|
3827
|
+
"testcase/frontend/cases/FE-*.md",
|
|
3828
|
+
"testcase/frontend/cases/index.md",
|
|
3829
|
+
"testcase/frontend/cases/manifest.draft.json",
|
|
3830
|
+
];
|
|
3831
|
+
// The shell materializer alone owns the final manifest boundary.
|
|
3809
3832
|
const casesWriteSet = ["testcase/frontend/cases/**"];
|
|
3810
3833
|
const evidenceRoot = "testcase/frontend/evidence";
|
|
3811
3834
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
@@ -3856,7 +3879,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3856
3879
|
// such as \\s/\\d and interprets Markdown backticks before Node sees them.
|
|
3857
3880
|
const checklistScriptBase64 = Buffer.from(checklistScript, "utf8").toString("base64");
|
|
3858
3881
|
const checklistValidation = [
|
|
3859
|
-
"node -e \"
|
|
3882
|
+
"node -e \"require('node:vm').runInThisContext(Buffer.from(process.argv[1],'base64').toString('utf8'))\"",
|
|
3860
3883
|
checklistScriptBase64,
|
|
3861
3884
|
].join(" ");
|
|
3862
3885
|
const manifestValidation = [
|
|
@@ -3915,8 +3938,26 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3915
3938
|
].join(" ");
|
|
3916
3939
|
const tasks = [
|
|
3917
3940
|
{
|
|
3918
|
-
id: "
|
|
3941
|
+
id: "preflight-frontend-browser-tool-shell",
|
|
3919
3942
|
depends_on: [],
|
|
3943
|
+
role: "verifier",
|
|
3944
|
+
executor: "shell",
|
|
3945
|
+
complexity: "LOW",
|
|
3946
|
+
writePolicy: "read-only",
|
|
3947
|
+
allowedPaths: [],
|
|
3948
|
+
forbiddenPaths: forbidden,
|
|
3949
|
+
outputContract: "Deterministic browser-tool preflight: SDK-only custom-tool capability + controller verified playwright-cli launcher + --help contract + frozen controller origin. Fail closed with playwright-cli-unavailable | playwright-cli-contract-incompatible | browser-command-capability-unavailable before any frontend-test Pi node.",
|
|
3950
|
+
subtask_prompt: `Verify CODE_AGENT_PI_BACKEND permits SDK, the Pi SDK structured custom-tool surface is available, the controller-resolved playwright-cli launcher and --help list open/close/find/snapshot/click, and the frozen origin is ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}. Do not install packages. Do not start a browser session. On failure exit non-zero so retrieve/generate/map never run (zero frontend-test Pi calls).`,
|
|
3951
|
+
shell: {
|
|
3952
|
+
commands: [],
|
|
3953
|
+
frontendBrowserToolPreflight: {},
|
|
3954
|
+
cwd: ".",
|
|
3955
|
+
timeoutMs: 60000,
|
|
3956
|
+
},
|
|
3957
|
+
},
|
|
3958
|
+
{
|
|
3959
|
+
id: "retrieve-frontend-test-context-pi",
|
|
3960
|
+
depends_on: ["preflight-frontend-browser-tool-shell"],
|
|
3920
3961
|
role: "planner",
|
|
3921
3962
|
executor: "pi",
|
|
3922
3963
|
toolProfile: "write",
|
|
@@ -3925,12 +3966,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3925
3966
|
writeSet: ragWriteSet,
|
|
3926
3967
|
allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
|
|
3927
3968
|
forbiddenPaths: forbidden,
|
|
3928
|
-
outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md
|
|
3969
|
+
outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md that copy the controller-frozen baseUrl/baseUrlSource verbatim, set environmentProbe=pending, and record capability notes.",
|
|
3929
3970
|
subtask_prompt: [
|
|
3930
3971
|
"Build the frontend test RAG package (keep it short).",
|
|
3931
3972
|
"Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
|
|
3932
3973
|
"Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
|
|
3933
|
-
|
|
3974
|
+
`Controller-frozen origin (required, not model-selectable): write exactly \`baseUrl: ${controllerFrontend.baseUrl}\` and \`baseUrlSource: ${controllerFrontend.baseUrlSource}\`. Do not derive, replace, or override the origin from model reasoning or other repository text. Write \`environmentProbe: pending\`. Include exact start prefix: playwright-cli open --browser=chrome --headed ${controllerFrontend.baseUrl}.`,
|
|
3934
3975
|
buildSourceContextBlock(sources),
|
|
3935
3976
|
].join("\n\n"),
|
|
3936
3977
|
},
|
|
@@ -3945,12 +3986,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3945
3986
|
allowedPaths: [...ragWriteSet],
|
|
3946
3987
|
forbiddenPaths: forbidden,
|
|
3947
3988
|
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (node ERROR so generate/map do not run).",
|
|
3948
|
-
subtask_prompt:
|
|
3989
|
+
subtask_prompt: `Probe only the controller-frozen baseUrl ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; never parse or accept an origin from context.md. Use curl HEAD then GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.`,
|
|
3949
3990
|
shell: {
|
|
3950
3991
|
commands: [
|
|
3951
3992
|
[
|
|
3952
3993
|
"node -e",
|
|
3953
|
-
JSON.stringify(
|
|
3994
|
+
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 baseUrl=${JSON.stringify(controllerFrontend.baseUrl)};const baseUrlSource=${JSON.stringify(controllerFrontend.baseUrlSource)};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 controllerBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
3954
3995
|
].join(" "),
|
|
3955
3996
|
],
|
|
3956
3997
|
cwd: ".",
|
|
@@ -3965,13 +4006,13 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3965
4006
|
toolProfile: "write",
|
|
3966
4007
|
complexity: "HIGH",
|
|
3967
4008
|
writePolicy: "exclusive",
|
|
3968
|
-
writeSet:
|
|
3969
|
-
allowedPaths: [...ragWriteSet, ...
|
|
4009
|
+
writeSet: caseDraftWriteSet,
|
|
4010
|
+
allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
|
|
3970
4011
|
forbiddenPaths: forbidden,
|
|
3971
|
-
outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1;
|
|
4012
|
+
outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1 only; the exclusive shell materializer promotes the validated draft to manifest.json. Do not write manifest.json or test source code.",
|
|
3972
4013
|
subtask_prompt: [
|
|
3973
4014
|
"Use skill playwright-cli-case-generator.",
|
|
3974
|
-
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and
|
|
4015
|
+
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
|
|
3975
4016
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
3976
4017
|
"HARD ID CONTRACT (do not confuse these):",
|
|
3977
4018
|
"- caseId / filename MUST be FE-<FEATURE>-<NNN>-<dimension> (example FE-LOGIN-001-core). NEVER use AC-FE-* as caseId or filename.",
|
|
@@ -3981,7 +4022,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3981
4022
|
"dimensions: core|boundary|flow|backend only.",
|
|
3982
4023
|
"Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
|
|
3983
4024
|
"HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
|
|
3984
|
-
|
|
4025
|
+
`Use only the controller-frozen baseUrl ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; context.md may reference it but cannot establish or override it. Every browser start command must be exactly: playwright-cli open --browser=chrome --headed ${controllerFrontend.baseUrl}. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
|
|
4026
|
+
"HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
|
|
4027
|
+
"HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
|
|
3985
4028
|
"Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
|
|
3986
4029
|
"U/D data ownership (required for modify/delete): (1) Only mutate data whose ownership is proven by current login identity + observable UI/API owner fields - never by name/guessed id/list order alone. (2) If current user has no data, create tagged cleanable data in current-user context, then U/D, then cleanup+verify. (3) Else only task-authorized Mock, labeled as Mock (not real backend proof). (4) If ownership unverifiable and create/Mock unavailable: write blocked with blockedReason current-user-data-unavailable | data-ownership-unverifiable | safe-test-data-setup-unavailable - do not risk cross-user data. (5) Never touch other users, shared fixtures, production, or non-cleanable data.",
|
|
3987
4030
|
].join("\n\n"),
|
|
@@ -4008,11 +4051,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4008
4051
|
toolProfile: "write",
|
|
4009
4052
|
complexity: "HIGH",
|
|
4010
4053
|
writePolicy: "exclusive",
|
|
4011
|
-
writeSet:
|
|
4012
|
-
allowedPaths: [...ragWriteSet, ...
|
|
4054
|
+
writeSet: caseDraftWriteSet,
|
|
4055
|
+
allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
|
|
4013
4056
|
forbiddenPaths: forbidden,
|
|
4014
|
-
outputContract: "Apply the one permitted frontend case revision
|
|
4015
|
-
subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases
|
|
4057
|
+
outputContract: "Apply the one permitted frontend case revision to FE-*.md, index.md, and manifest.draft.json only; the exclusive shell materializer remains the sole writer of manifest.json. No browser execution or evidence writes.",
|
|
4058
|
+
subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json; preserve traceable AC mappings. Preserve the dynamic-ref contract: executable playwright-cli lines use only eX/eY-style shell-safe documentation placeholders, never <...>; each placeholder is resolved from the immediately preceding latest snapshot and stale refs are not reused. Do not write testcase/frontend/cases/manifest.json, execute a browser, or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.",
|
|
4016
4059
|
}, {
|
|
4017
4060
|
id: "review-frontend-cases-final-pi",
|
|
4018
4061
|
depends_on: ["revise-frontend-cases-pi"],
|
|
@@ -4066,8 +4109,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4066
4109
|
writePolicy: "read-only",
|
|
4067
4110
|
allowedPaths: [...ragWriteSet, ...casesWriteSet],
|
|
4068
4111
|
forbiddenPaths: forbidden,
|
|
4069
|
-
outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and
|
|
4070
|
-
subtask_prompt: "
|
|
4112
|
+
outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, AC mapping, and executable command gate; rejects alternative executable instructions and non-allowlisted playwright-cli commands with ruleId-tagged location evidence.",
|
|
4113
|
+
subtask_prompt: "Inspect and reject alternative executable instructions in fenced command code, list/indented steps, and explicit shell/terminal command lines. Only allowlisted playwright-cli command instructions may pass; ordinary prose and explicit blocked reasons may describe prohibitions. Run the native deterministic checklist without spawning Bash, PowerShell, or node -e and do not use free-form LLM verdicts.",
|
|
4071
4114
|
shell: {
|
|
4072
4115
|
commands: [],
|
|
4073
4116
|
frontendTestCaseChecklist: {},
|
|
@@ -4118,6 +4161,10 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4118
4161
|
role: "implementer",
|
|
4119
4162
|
skills: ["playwright-cli"],
|
|
4120
4163
|
toolProfile: "write",
|
|
4164
|
+
commandPolicy: {
|
|
4165
|
+
mode: "capability-allowlist",
|
|
4166
|
+
capabilities: ["playwright-cli"],
|
|
4167
|
+
},
|
|
4121
4168
|
complexity: "MED",
|
|
4122
4169
|
writePolicy: "exclusive",
|
|
4123
4170
|
allowedPaths: [
|
|
@@ -4128,12 +4175,13 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4128
4175
|
],
|
|
4129
4176
|
forbiddenPaths: forbidden,
|
|
4130
4177
|
writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
|
|
4131
|
-
outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
|
|
4178
|
+
outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens. Browser actions must use structured playwright_cli tool. Passed authority requires same-child ordered controller receipts: successful open → successful find → successful post-execution cleanup.",
|
|
4132
4179
|
subtaskPromptTemplate: [
|
|
4133
4180
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
4134
|
-
"
|
|
4181
|
+
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
4182
|
+
`1) The controller-frozen baseUrl is ${controllerFrontend.baseUrl} from ${controllerFrontend.baseUrlSource}; model-authored context/case text cannot establish or override it. 2) Start browser ONLY via playwright_cli command=open with args [--browser=chrome, --headed, ${controllerFrontend.baseUrl}] (default session only; no -s=). 3) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 4) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 5) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
|
|
4135
4183
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
4136
|
-
"Business failed/blocked is a recorded result, not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
4184
|
+
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
4137
4185
|
].join("\n\n"),
|
|
4138
4186
|
},
|
|
4139
4187
|
},
|
|
@@ -4261,7 +4309,10 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4261
4309
|
"frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
|
|
4262
4310
|
"Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
|
|
4263
4311
|
"Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
|
|
4264
|
-
|
|
4312
|
+
`Browser startup for generated cases must be playwright-cli open --browser=chrome --headed ${controllerFrontend.baseUrl}; this origin is frozen by the controller from ${controllerFrontend.baseUrlSource}, and model-authored files cannot establish or override it; generated operations stay in the default browser session and must not use unverified named-session flags.`,
|
|
4313
|
+
"Browser-tool preflight runs before any frontend-test Pi node; cli-only rollback, missing/incompatible Pi SDK custom-tool capability, missing verified playwright-cli launcher, or incompatible --help fails with zero Pi calls.",
|
|
4314
|
+
"Browser case children use commandPolicy capability-allowlist playwright-cli and structured playwright_cli tool; ordinary writers remain without Bash.",
|
|
4315
|
+
"Passed cases require same-child ordered controller receipts: successful open → successful find → successful post-execution cleanup. Pre-start cleanup, snapshot/goto/screenshot/request/console and ordinary interactions are insufficient; missing or unordered receipts convert to blocked (browser-command-evidence-missing).",
|
|
4265
4316
|
"playwright-cli-only: generators and executors may call only skill-declared playwright-cli commands; bare playwright / npx playwright / @playwright/test / Playwright source are forbidden with no native Playwright fallback.",
|
|
4266
4317
|
"Environment preflight must curl-probe the frozen non-production baseUrl before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
|
|
4267
4318
|
"U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
|
|
@@ -5312,7 +5363,7 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
5312
5363
|
else if (template === "review-gated-dag")
|
|
5313
5364
|
spec = buildReviewGatedHybridDag(standard, sources);
|
|
5314
5365
|
else
|
|
5315
|
-
spec =
|
|
5366
|
+
spec = buildSupervisedHybridDag(standard, sources);
|
|
5316
5367
|
}
|
|
5317
5368
|
applyProjectGovernanceReview(spec, template, sources);
|
|
5318
5369
|
// New generate path always emits DagSpec v4 + bindings.
|
|
@@ -5498,8 +5549,18 @@ function buildReviewGateNode(sources) {
|
|
|
5498
5549
|
}
|
|
5499
5550
|
function enableProjectGovernanceOnNode(task) {
|
|
5500
5551
|
task.governanceStandardReview = true;
|
|
5501
|
-
task.outputProtocol = REVIEW_VERDICT_OUTPUT_PROTOCOL;
|
|
5502
5552
|
task.retryPolicy = PROTOCOL_AWARE_PI_RETRY_POLICY;
|
|
5553
|
+
if (task.outputProtocol?.type === "json-review-verdict") {
|
|
5554
|
+
const governanceInstruction = 'mandatory governance violations must use verdict "request-revision" with at least one finding; never emit verdict "pass" when any mandatory governance violation or Critical/Important finding remains.';
|
|
5555
|
+
if (!(task.outputContract ?? "").includes("mandatory governance violations")) {
|
|
5556
|
+
task.outputContract = `${task.outputContract ?? ""} Unresolved ${governanceInstruction} No file writes.`;
|
|
5557
|
+
}
|
|
5558
|
+
if (!task.subtask_prompt.includes(governanceInstruction)) {
|
|
5559
|
+
task.subtask_prompt = `${task.subtask_prompt}\n\n${governanceInstruction}`;
|
|
5560
|
+
}
|
|
5561
|
+
return;
|
|
5562
|
+
}
|
|
5563
|
+
task.outputProtocol = REVIEW_VERDICT_OUTPUT_PROTOCOL;
|
|
5503
5564
|
task.outputContract =
|
|
5504
5565
|
"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.";
|
|
5505
5566
|
const protocolInstruction = "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.";
|
|
@@ -5746,12 +5807,10 @@ function buildWriteSetGateNode(sources) {
|
|
|
5746
5807
|
},
|
|
5747
5808
|
};
|
|
5748
5809
|
}
|
|
5749
|
-
|
|
5810
|
+
function buildSoftVerifyNode(sources) {
|
|
5750
5811
|
const implementId = implementationNodeId();
|
|
5751
5812
|
const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
|
|
5752
|
-
const fallbackCommands =
|
|
5753
|
-
? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
|
|
5754
|
-
: ["npm run typecheck"];
|
|
5813
|
+
const fallbackCommands = ["npm run typecheck"];
|
|
5755
5814
|
const focusedIntermediate = sources.verifyCommands?.intermediate.filter((command) => !isFullSuiteVerifyCommand(command));
|
|
5756
5815
|
const plannedIntermediate = applyMavenVerificationPlanning({
|
|
5757
5816
|
repoRoot: sources.repoRoot,
|
|
@@ -5993,7 +6052,7 @@ function resolveSupervisedConvergence(taskConfig) {
|
|
|
5993
6052
|
chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
|
|
5994
6053
|
};
|
|
5995
6054
|
}
|
|
5996
|
-
|
|
6055
|
+
function buildSupervisedHybridDag(standard, sources) {
|
|
5997
6056
|
const contract = getTaskOrThrow(standard, "contract-pi");
|
|
5998
6057
|
const scoutSrc = getTaskOrThrow(standard, "scout-src");
|
|
5999
6058
|
const scoutTests = getTaskOrThrow(standard, "scout-tests");
|
|
@@ -6045,7 +6104,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
6045
6104
|
"final-write-set-audit-format-repair-pi",
|
|
6046
6105
|
],
|
|
6047
6106
|
}),
|
|
6048
|
-
|
|
6107
|
+
buildSoftVerifyNode(sources),
|
|
6049
6108
|
buildProcessSupervisorNode(sources),
|
|
6050
6109
|
buildProcessGateNode(sources),
|
|
6051
6110
|
buildRepairNode(sources),
|
|
@@ -5,13 +5,31 @@ import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
|
5
5
|
import { parseDagSpec } from "./types.js";
|
|
6
6
|
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
7
7
|
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
8
|
-
|
|
8
|
+
/** Resume/operator scan order for real execution lifecycles. */
|
|
9
|
+
const DAG_EXECUTION_LIFECYCLE_SCAN_ORDER = [
|
|
9
10
|
"paused",
|
|
10
11
|
"active",
|
|
11
12
|
"completed",
|
|
12
13
|
];
|
|
14
|
+
/** Full disk scan including non-execution audit lifecycles (R-A2). */
|
|
15
|
+
const DAG_LIFECYCLE_SCAN_ORDER = [
|
|
16
|
+
...DAG_EXECUTION_LIFECYCLE_SCAN_ORDER,
|
|
17
|
+
"dry-run",
|
|
18
|
+
"init-only",
|
|
19
|
+
];
|
|
13
20
|
const PAUSED_APPROVAL_FLOW_HINT = "Use dag approve / dag reject, then dag resume when approved.";
|
|
14
21
|
export const DAG_RUNS_DIR = path.join(".harness", "dag-runs");
|
|
22
|
+
/** Non-execution audit lifecycles (never resume targets). */
|
|
23
|
+
export const DAG_NON_EXECUTION_LIFECYCLES = ["dry-run", "init-only"];
|
|
24
|
+
export function isDagNonExecutionLifecycle(lifecycle) {
|
|
25
|
+
return (lifecycle === "dry-run" ||
|
|
26
|
+
lifecycle === "init-only");
|
|
27
|
+
}
|
|
28
|
+
export function isDagExecutionLifecycle(lifecycle) {
|
|
29
|
+
return (lifecycle === "active" ||
|
|
30
|
+
lifecycle === "completed" ||
|
|
31
|
+
lifecycle === "paused");
|
|
32
|
+
}
|
|
15
33
|
export function getDagRunDir(cwd, lifecycle, runId) {
|
|
16
34
|
return path.join(cwd, DAG_RUNS_DIR, lifecycle, runId);
|
|
17
35
|
}
|
|
@@ -36,7 +54,8 @@ export async function readDagRunSpec(runDir) {
|
|
|
36
54
|
return parseDagSpec(raw);
|
|
37
55
|
}
|
|
38
56
|
export async function locateDagRun(cwd, runId) {
|
|
39
|
-
|
|
57
|
+
// Prefer real execution lifecycles for resume/status; audit dirs last.
|
|
58
|
+
for (const lifecycle of DAG_LIFECYCLE_SCAN_ORDER) {
|
|
40
59
|
const runDir = getDagRunDir(cwd, lifecycle, runId);
|
|
41
60
|
if (await dagRunDirExists(runDir)) {
|
|
42
61
|
return { lifecycle, runDir };
|
|
@@ -233,6 +252,10 @@ export function deriveDagRunEffectiveStatus(input) {
|
|
|
233
252
|
if (input.lifecycle === "completed") {
|
|
234
253
|
return mapTerminalDagRunEffectiveStatus(input.state.status);
|
|
235
254
|
}
|
|
255
|
+
// dry-run / init-only are audit shells; surface pending without pretending execute.
|
|
256
|
+
if (isDagNonExecutionLifecycle(input.lifecycle)) {
|
|
257
|
+
return input.state.status === "pending" ? "pending" : "unknown";
|
|
258
|
+
}
|
|
236
259
|
if (input.state.status === "pending")
|
|
237
260
|
return "pending";
|
|
238
261
|
if (isTerminalDagRunStatus(input.state.status)) {
|
|
@@ -254,6 +277,14 @@ export function deriveDagRunEffectiveStatus(input) {
|
|
|
254
277
|
}
|
|
255
278
|
export function assessDagRunRecoveryEligibility(input) {
|
|
256
279
|
const reasons = [];
|
|
280
|
+
if (isDagNonExecutionLifecycle(input.lifecycle)) {
|
|
281
|
+
return {
|
|
282
|
+
canResume: false,
|
|
283
|
+
canReconcile: false,
|
|
284
|
+
allowedActions: [],
|
|
285
|
+
reasons: ["non-execution-lifecycle"],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
257
288
|
const canResume = input.lifecycle === "active" &&
|
|
258
289
|
input.state.status === "running" &&
|
|
259
290
|
Boolean(input.state.humanDecisionNodeId) &&
|
|
@@ -9,7 +9,7 @@ import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./p
|
|
|
9
9
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
10
10
|
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
11
11
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
12
|
-
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, validateOutputProtocol, } from "./output-protocol.js";
|
|
12
|
+
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
13
13
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
14
14
|
import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
|
|
15
15
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
@@ -83,8 +83,15 @@ export async function buildNodePromptWithResolvedSkillInstructions(spec, task, u
|
|
|
83
83
|
resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
export function canonicalNodeOutput(node) {
|
|
87
|
+
const assistantText = node?.assistantText ?? "";
|
|
88
|
+
return assistantText.trim().length > 0 ? assistantText : (node?.stdout ?? "");
|
|
89
|
+
}
|
|
86
90
|
export function parseProcessVerdict(node) {
|
|
87
|
-
const text =
|
|
91
|
+
const text = canonicalNodeOutput(node);
|
|
92
|
+
const jsonVerdict = parseJsonReviewVerdict(text);
|
|
93
|
+
if (jsonVerdict.ok)
|
|
94
|
+
return jsonVerdict.verdict;
|
|
88
95
|
for (const line of text.split("\n")) {
|
|
89
96
|
const trimmed = line.trim();
|
|
90
97
|
if (trimmed === "VERDICT: pass")
|
|
@@ -138,8 +145,7 @@ function assertRepairArtifactVerdictMatchesSupervisor(input) {
|
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
function parseSupervisorRepairArtifact(node) {
|
|
141
|
-
|
|
142
|
-
return parseRepairArtifactFromText(text);
|
|
148
|
+
return parseRepairArtifactFromText(canonicalNodeOutput(node));
|
|
143
149
|
}
|
|
144
150
|
function validateRepairArtifactGateBeforeShell(input) {
|
|
145
151
|
const gate = input.task.shell?.repairArtifactGate;
|
|
@@ -450,7 +456,7 @@ export async function executeDagNode(input) {
|
|
|
450
456
|
// R0: executor ok=true still fails closed when outputProtocol is violated.
|
|
451
457
|
// Valid semantic results (e.g. VERDICT: request-revision) pass validation.
|
|
452
458
|
if (result.ok && task.outputProtocol) {
|
|
453
|
-
const protocolText =
|
|
459
|
+
const protocolText = canonicalNodeOutput(result);
|
|
454
460
|
const protocolCheck = validateOutputProtocol(task.outputProtocol, protocolText);
|
|
455
461
|
if (!protocolCheck.ok) {
|
|
456
462
|
result = {
|