@tea-agent/loop-agent 0.21.0 → 0.23.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/AGENTS.md +42 -108
- package/CHANGELOG.md +114 -0
- package/README.md +7 -4
- package/bin/agent-worker.js +0 -0
- package/dist/adapters/loop-agent.js +52 -0
- package/dist/application/context-usage/skill-resolution-stats.js +263 -0
- package/dist/application/dag/generate-task-dag.js +17 -3
- package/dist/cli/command-definitions.js +8 -7
- package/dist/cli/program.js +17 -15
- package/dist/commands/doctor.js +269 -18
- package/dist/commands/init.js +198 -86
- package/dist/commands/stats.js +40 -11
- package/dist/executors/dag-pi-executor.js +2 -0
- package/dist/executors/shell-executor.js +162 -19
- package/dist/shared/openspec-spec.js +49 -0
- package/dist/shared/operator/capabilities.js +11 -1
- package/dist/worker/console/app-data.js +4 -0
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/model-resolver.js +106 -0
- package/dist/worker/console/chat/pi-runtime.js +605 -0
- package/dist/worker/console/chat/resource-loader.js +66 -0
- package/dist/worker/console/chat/routes.js +357 -0
- package/dist/worker/console/chat/session-store.js +238 -0
- package/dist/worker/console/chat/tool-adapter.js +238 -0
- package/dist/worker/console/chat/tools.js +171 -0
- package/dist/worker/console/server.js +55 -0
- package/dist/worker/console/static/assets/index-DVl7Jxt5.js +25 -0
- package/dist/worker/console/static/assets/index-lVcIr9Ju.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/feature/profile-schema.js +1 -1
- package/dist/worker/observability/read-model.js +21 -1
- package/dist/worker/observe/spec-evidence.js +12 -15
- package/dist/worker/observe/static/dag-helpers.js +22 -0
- package/dist/worker/observe/static/views/dag.js +5 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +37 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +141 -32
- package/dist/workflows/dag/frontend-lint-baseline.js +471 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +79 -16
- package/dist/workflows/dag/frontend-project-capability.js +11 -8
- package/dist/workflows/dag/frontend-repair.js +6 -4
- package/dist/workflows/dag/frontend-review-context.js +67 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +71 -66
- package/dist/workflows/dag/frontend-verification-trace.js +31 -1
- package/dist/workflows/dag/frontend-worktree-diff.js +81 -6
- package/dist/workflows/dag/init-hybrid.js +412 -84
- package/dist/workflows/dag/node-execution.js +38 -1
- package/dist/workflows/dag/output-protocol.js +89 -0
- package/dist/workflows/dag/prompt.js +35 -1
- package/dist/workflows/dag/recovery-recommendation.js +45 -0
- package/dist/workflows/dag/report.js +28 -1
- package/dist/workflows/dag/rerun-task.js +1 -1
- package/dist/workflows/dag/scheduler.js +9 -0
- package/dist/workflows/dag/types.js +74 -1
- package/dist/workflows/dag/validate.js +55 -0
- package/docs/README.md +73 -156
- package/docs/architecture/README.md +3 -2
- package/docs/architecture/dag-execution.md +2 -2
- package/docs/architecture/evolution.md +14 -12
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/governance/README.md +15 -0
- package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
- package/docs/init-surface.manifest.json +22 -4
- package/docs/operations/README.md +12 -0
- package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
- package/docs/skills/vetted-skill-registry.md +23 -3
- package/docs/templates/README.md +55 -0
- package/docs/templates/agent-dag.schema.json +15 -5
- package/docs/templates/backend-test-dag.json +1 -1
- package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
- package/docs/templates/frontend-implementation-contract.schema.json +4 -3
- package/docs/templates/frontend-test-case-checklist.md +6 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/progress-log.md +9 -2
- package/harness.json +5 -5
- package/package.json +5 -5
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/frontend-design-review/SKILL.md +12 -10
- package/skills/frontend-design-review/references/review-checklist.md +4 -4
- package/skills/frontend-implementation/SKILL.md +2 -2
- package/skills/frontend-implementation/references/code-standards.md +4 -3
- package/skills/frontend-implementation/references/design-spec.md +19 -14
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/frontend-review/SKILL.md +15 -28
- package/skills/frontend-review/references/review-findings.md +16 -18
- package/skills/frontend-verification/SKILL.md +16 -13
- package/skills/frontend-verification/references/verification-checklist.md +18 -30
- package/skills/grill-with-docs/SKILL.md +44 -52
- package/skills/grill-with-docs/adr-format.md +37 -26
- package/skills/grill-with-docs/context-format.md +18 -26
- package/skills/loop-agent/SKILL.md +28 -112
- package/skills/loop-agent/references/command-reference.md +9 -3
- package/skills/loop-agent/references/harness-policy.md +3 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/task-workflow.md +2 -0
- package/skills/systematic-debugging/SKILL.md +20 -4
- package/skills/test-driven-development/SKILL.md +10 -3
- package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
- package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
- /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
- /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -17,7 +18,8 @@ import { discoverProjectGovernancePresence } from "./project-governance-context.
|
|
|
17
18
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
18
19
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
19
20
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
20
|
-
import {
|
|
21
|
+
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
22
|
+
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
21
23
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
22
24
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
23
25
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
@@ -555,7 +557,7 @@ export function hasApiDependency(sources) {
|
|
|
555
557
|
/**
|
|
556
558
|
* Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
|
|
557
559
|
*
|
|
558
|
-
* Decision matrix (from docs/design/frontend-mock-data-workflow.md):
|
|
560
|
+
* Decision matrix (from docs/design/active/frontend-mock-data-workflow.md):
|
|
559
561
|
*
|
|
560
562
|
* | 接口/异步数据依赖 | 既有 Mock 服务 | policy | 结果 |
|
|
561
563
|
* |---|---|---|---|
|
|
@@ -829,6 +831,9 @@ function extractFrontendMockVerifyCommandsFromMarkdown(input) {
|
|
|
829
831
|
return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
|
|
830
832
|
}
|
|
831
833
|
function chooseFrontendVerifyCommands(input) {
|
|
834
|
+
if (input.explicitCommands && input.explicitCommands.length > 0) {
|
|
835
|
+
return { commands: input.explicitCommands, commandSource: "inline" };
|
|
836
|
+
}
|
|
832
837
|
if (input.parsedCommands.length > 0) {
|
|
833
838
|
return { commands: input.parsedCommands, commandSource: "inline" };
|
|
834
839
|
}
|
|
@@ -837,33 +842,132 @@ function chooseFrontendVerifyCommands(input) {
|
|
|
837
842
|
}
|
|
838
843
|
return { commandSource: "inline" };
|
|
839
844
|
}
|
|
840
|
-
function
|
|
845
|
+
function classifyFrontendVerifyCommand(command) {
|
|
846
|
+
return /\b(typecheck|check-types|lint|eslint|tsc|build|check)\b/i.test(command) ||
|
|
847
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)
|
|
848
|
+
? "static"
|
|
849
|
+
: "behavior";
|
|
850
|
+
}
|
|
851
|
+
function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
|
|
852
|
+
const staticCommands = [];
|
|
853
|
+
const behaviorCommands = [];
|
|
854
|
+
if (!repoRoot)
|
|
855
|
+
return { staticCommands, behaviorCommands };
|
|
856
|
+
for (const command of taskConfig.verifyCommands) {
|
|
857
|
+
const verifyCommand = {
|
|
858
|
+
args: ["bash", "-lc", command.command],
|
|
859
|
+
cwd: repoRoot,
|
|
860
|
+
label: command.label,
|
|
861
|
+
timeoutMs: command.timeoutMs,
|
|
862
|
+
};
|
|
863
|
+
if (classifyFrontendVerifyCommand(command.command) === "static") {
|
|
864
|
+
staticCommands.push(verifyCommand);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
behaviorCommands.push(verifyCommand);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { staticCommands, behaviorCommands };
|
|
871
|
+
}
|
|
872
|
+
function verifyCommandKey(command) {
|
|
873
|
+
return `${command.cwd}\0${command.args.join("\0")}`;
|
|
874
|
+
}
|
|
875
|
+
function resolveDagVerifyStrategy(taskConfig, defaultIntermediateQuotaWhenFull = "full") {
|
|
876
|
+
const explicitIntermediateQuota = taskConfig.dagVerifyStrategy?.intermediateQuota;
|
|
841
877
|
return {
|
|
842
|
-
intermediateQuota:
|
|
878
|
+
intermediateQuota: explicitIntermediateQuota ??
|
|
879
|
+
(taskConfig.verifyQuota === "full"
|
|
880
|
+
? defaultIntermediateQuotaWhenFull
|
|
881
|
+
: taskConfig.verifyQuota),
|
|
843
882
|
finalQuota: "full",
|
|
844
883
|
focusedCommandSource: taskConfig.dagVerifyStrategy?.focusedCommandSource ?? "adapter",
|
|
845
884
|
};
|
|
846
885
|
}
|
|
847
886
|
function buildVerifyEvidence(input) {
|
|
887
|
+
const selectedCommands = input.commands && input.commands.length > 0 ? input.commands : undefined;
|
|
888
|
+
const commandCount = selectedCommands?.length ?? input.fallbackCommands.length;
|
|
848
889
|
return {
|
|
849
890
|
phase: input.phase,
|
|
850
891
|
quota: input.quota,
|
|
851
892
|
commandSource: input.commandSource,
|
|
852
|
-
commandCount
|
|
853
|
-
commandLabels:
|
|
893
|
+
commandCount,
|
|
894
|
+
commandLabels: selectedCommands?.map((command) => command.label) ?? input.fallbackCommands,
|
|
895
|
+
commandTexts: input.commandTexts ?? [],
|
|
896
|
+
commandTimeoutMs: input.commandTimeoutMs,
|
|
897
|
+
totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
|
|
854
898
|
finalFullRequired: input.finalFullRequired,
|
|
855
899
|
};
|
|
856
900
|
}
|
|
901
|
+
function isFrontendLintVerifyCommand(command) {
|
|
902
|
+
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
903
|
+
return /\b(?:lint|eslint)\b/i.test(text);
|
|
904
|
+
}
|
|
905
|
+
function isManagedCiWrapperVerifyCommand(command) {
|
|
906
|
+
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
907
|
+
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
908
|
+
}
|
|
909
|
+
function repoHasNpmScript(repoRoot, scriptName) {
|
|
910
|
+
if (!repoRoot)
|
|
911
|
+
return false;
|
|
912
|
+
const packagePath = path.join(repoRoot, "package.json");
|
|
913
|
+
if (!existsSync(packagePath))
|
|
914
|
+
return false;
|
|
915
|
+
try {
|
|
916
|
+
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
917
|
+
return typeof decoded.scripts?.[scriptName] === "string";
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function partitionFrontendStaticVerifyCommands(input) {
|
|
924
|
+
const commands = input.commands ?? [];
|
|
925
|
+
const lintCommands = commands.filter(isFrontendLintVerifyCommand);
|
|
926
|
+
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
927
|
+
if (lintCommands.length === 0 &&
|
|
928
|
+
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
929
|
+
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
930
|
+
lintCommands.push({
|
|
931
|
+
args: ["npm", "run", "lint"],
|
|
932
|
+
cwd: input.repoRoot,
|
|
933
|
+
label: "npm run lint",
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
lint: {
|
|
938
|
+
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
939
|
+
commandSource: input.commandSource,
|
|
940
|
+
},
|
|
941
|
+
static: {
|
|
942
|
+
...(staticCommands.length > 0 ? { commands: staticCommands } : {}),
|
|
943
|
+
commandSource: input.commandSource,
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function isFrontendTestPathPattern(entry) {
|
|
948
|
+
const segments = entry.replaceAll("\\", "/").split("/");
|
|
949
|
+
return segments.some((segment) => /^(?:test|tests|spec|specs|e2e|__tests__|__specs__)$/i.test(segment));
|
|
950
|
+
}
|
|
951
|
+
function replaceSourceSegmentWithTestSegment(entry) {
|
|
952
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
953
|
+
const segments = normalized.split("/");
|
|
954
|
+
const sourceIndex = segments.findIndex((segment) => /^(?:src|source|app|lib)$/i.test(segment));
|
|
955
|
+
if (sourceIndex < 0)
|
|
956
|
+
return null;
|
|
957
|
+
const testSegments = [...segments];
|
|
958
|
+
testSegments[sourceIndex] = "test";
|
|
959
|
+
return testSegments.join("/");
|
|
960
|
+
}
|
|
857
961
|
function deriveParallelScoutPaths(taskConfig) {
|
|
858
962
|
const allowed = taskConfig.allowedPaths;
|
|
859
963
|
if (allowed.length === 0) {
|
|
860
964
|
return { srcPaths: ["**"], testPaths: ["**"] };
|
|
861
965
|
}
|
|
862
|
-
const srcPaths = allowed.filter((entry) => !entry
|
|
863
|
-
const explicitTestPaths = allowed.filter(
|
|
966
|
+
const srcPaths = allowed.filter((entry) => !isFrontendTestPathPattern(entry));
|
|
967
|
+
const explicitTestPaths = allowed.filter(isFrontendTestPathPattern);
|
|
864
968
|
const derivedTestPaths = allowed
|
|
865
|
-
.
|
|
866
|
-
.
|
|
969
|
+
.map(replaceSourceSegmentWithTestSegment)
|
|
970
|
+
.filter((entry) => Boolean(entry));
|
|
867
971
|
return {
|
|
868
972
|
srcPaths: srcPaths.length > 0 ? srcPaths : allowed,
|
|
869
973
|
testPaths: explicitTestPaths.length > 0
|
|
@@ -873,6 +977,80 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
873
977
|
: allowed,
|
|
874
978
|
};
|
|
875
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* Resolve frontend verification fallbacks from the target project's own
|
|
982
|
+
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
983
|
+
* package.json, so those fixtures retain the historical generic fallback.
|
|
984
|
+
* Real projects never inherit loop-agent's commands when package.json exists.
|
|
985
|
+
*/
|
|
986
|
+
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
987
|
+
const genericFallback = {
|
|
988
|
+
staticCommands: ["npm run typecheck", "npm run build"],
|
|
989
|
+
behaviorCommands: ["npm test"],
|
|
990
|
+
};
|
|
991
|
+
if (!repoRoot)
|
|
992
|
+
return genericFallback;
|
|
993
|
+
let scripts;
|
|
994
|
+
try {
|
|
995
|
+
const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
996
|
+
if (packageJson.scripts && typeof packageJson.scripts === "object") {
|
|
997
|
+
scripts = packageJson.scripts;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
return genericFallback;
|
|
1002
|
+
}
|
|
1003
|
+
if (!scripts)
|
|
1004
|
+
return genericFallback;
|
|
1005
|
+
let packageManager = "npm";
|
|
1006
|
+
for (const [lockfile, manager] of [
|
|
1007
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1008
|
+
["yarn.lock", "yarn"],
|
|
1009
|
+
["bun.lockb", "bun"],
|
|
1010
|
+
["bun.lock", "bun"],
|
|
1011
|
+
]) {
|
|
1012
|
+
try {
|
|
1013
|
+
await access(path.join(repoRoot, lockfile));
|
|
1014
|
+
packageManager = manager;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
// Try the next package-manager marker.
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const commandForScript = (script) => script === "test"
|
|
1022
|
+
? `${packageManager} test`
|
|
1023
|
+
: `${packageManager} run ${script}`;
|
|
1024
|
+
const hasScript = (script) => typeof scripts?.[script] === "string" &&
|
|
1025
|
+
String(scripts[script]).trim().length > 0;
|
|
1026
|
+
const firstExisting = (names) => {
|
|
1027
|
+
const selected = [];
|
|
1028
|
+
for (const name of names) {
|
|
1029
|
+
if (hasScript(name) && !selected.includes(name))
|
|
1030
|
+
selected.push(name);
|
|
1031
|
+
}
|
|
1032
|
+
return selected.map(commandForScript);
|
|
1033
|
+
};
|
|
1034
|
+
const staticCommands = firstExisting([
|
|
1035
|
+
"typecheck",
|
|
1036
|
+
"check-types",
|
|
1037
|
+
"lint",
|
|
1038
|
+
"check",
|
|
1039
|
+
"build",
|
|
1040
|
+
]);
|
|
1041
|
+
const behaviorCommands = firstExisting([
|
|
1042
|
+
"test:unit",
|
|
1043
|
+
"test:frontend",
|
|
1044
|
+
"test:component",
|
|
1045
|
+
"test",
|
|
1046
|
+
"test:e2e",
|
|
1047
|
+
"e2e",
|
|
1048
|
+
]);
|
|
1049
|
+
return {
|
|
1050
|
+
staticCommands: staticCommands.length > 0 ? staticCommands : behaviorCommands,
|
|
1051
|
+
behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : staticCommands,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
876
1054
|
function deriveFrontendBehaviorPaths(taskConfig) {
|
|
877
1055
|
if (taskConfig.allowedPaths.length === 0)
|
|
878
1056
|
return ["**"];
|
|
@@ -1200,9 +1378,10 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1200
1378
|
commands: finalVerifyCommands,
|
|
1201
1379
|
fallbackCommands: [],
|
|
1202
1380
|
finalFullRequired: true,
|
|
1381
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1203
1382
|
}),
|
|
1204
1383
|
cwd: ".",
|
|
1205
|
-
timeoutMs:
|
|
1384
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1206
1385
|
},
|
|
1207
1386
|
},
|
|
1208
1387
|
]
|
|
@@ -1507,9 +1686,11 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
|
|
|
1507
1686
|
commandSource: commands.length > 0 ? "inline" : "adapter",
|
|
1508
1687
|
commands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
1509
1688
|
fallbackCommands: [],
|
|
1689
|
+
commandTexts: commands,
|
|
1690
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1510
1691
|
}),
|
|
1511
1692
|
cwd: ".",
|
|
1512
|
-
timeoutMs:
|
|
1693
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1513
1694
|
},
|
|
1514
1695
|
};
|
|
1515
1696
|
}
|
|
@@ -1688,7 +1869,7 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
1688
1869
|
return { ...task, depends_on };
|
|
1689
1870
|
});
|
|
1690
1871
|
}
|
|
1691
|
-
function buildFrontendHybridDagFromTask(sources) {
|
|
1872
|
+
async function buildFrontendHybridDagFromTask(sources) {
|
|
1692
1873
|
const { taskConfig } = sources;
|
|
1693
1874
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
1694
1875
|
status: "absent",
|
|
@@ -1756,6 +1937,8 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1756
1937
|
"- verificationTargets is a TOP-LEVEL required array",
|
|
1757
1938
|
"- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
|
|
1758
1939
|
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
1940
|
+
"- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
|
|
1941
|
+
"- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
|
|
1759
1942
|
].join("\n");
|
|
1760
1943
|
})();
|
|
1761
1944
|
const sourceContext = [
|
|
@@ -1797,26 +1980,42 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1797
1980
|
if (mockMode === "blocked") {
|
|
1798
1981
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints);
|
|
1799
1982
|
}
|
|
1800
|
-
const
|
|
1801
|
-
const
|
|
1983
|
+
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
1984
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
|
|
1985
|
+
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
1802
1986
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
1803
1987
|
repoRoot: sources.repoRoot,
|
|
1804
1988
|
requirementMarkdown: sources.requirementMarkdown,
|
|
1805
1989
|
constraintMarkdown: sources.constraintMarkdown,
|
|
1806
1990
|
});
|
|
1991
|
+
const explicitFrontendVerifyCommands = buildExplicitFrontendVerifyCommands(taskConfig, sources.repoRoot);
|
|
1992
|
+
const explicitCommandKeys = new Set([...explicitFrontendVerifyCommands.staticCommands, ...explicitFrontendVerifyCommands.behaviorCommands].map(verifyCommandKey));
|
|
1993
|
+
const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
|
|
1807
1994
|
const staticVerifyCommands = chooseFrontendVerifyCommands({
|
|
1995
|
+
explicitCommands: explicitFrontendVerifyCommands.staticCommands,
|
|
1808
1996
|
parsedCommands: parsedFrontendVerifyCommands.staticCommands,
|
|
1809
|
-
adapterCommands:
|
|
1997
|
+
adapterCommands: adapterVerifyCommands,
|
|
1998
|
+
});
|
|
1999
|
+
const partitionedStaticVerifyCommands = partitionFrontendStaticVerifyCommands({
|
|
2000
|
+
repoRoot: sources.repoRoot,
|
|
2001
|
+
commands: staticVerifyCommands.commands,
|
|
2002
|
+
commandSource: staticVerifyCommands.commandSource,
|
|
1810
2003
|
});
|
|
1811
2004
|
const behaviorVerifyCommands = chooseFrontendVerifyCommands({
|
|
2005
|
+
explicitCommands: explicitFrontendVerifyCommands.behaviorCommands,
|
|
1812
2006
|
parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
|
|
1813
|
-
adapterCommands:
|
|
2007
|
+
adapterCommands: adapterVerifyCommands,
|
|
1814
2008
|
});
|
|
1815
2009
|
const staticShellCommands = buildVerifyShellCommands({
|
|
1816
2010
|
repoRoot: sources.repoRoot,
|
|
1817
|
-
commands:
|
|
2011
|
+
commands: partitionedStaticVerifyCommands.static.commands,
|
|
1818
2012
|
fallbackCommands: staticFallbackCommands,
|
|
1819
2013
|
});
|
|
2014
|
+
const lintShellCommands = buildVerifyShellCommands({
|
|
2015
|
+
repoRoot: sources.repoRoot,
|
|
2016
|
+
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2017
|
+
fallbackCommands: [],
|
|
2018
|
+
});
|
|
1820
2019
|
const behaviorShellCommands = buildVerifyShellCommands({
|
|
1821
2020
|
repoRoot: sources.repoRoot,
|
|
1822
2021
|
commands: behaviorVerifyCommands.commands,
|
|
@@ -1825,17 +2024,32 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1825
2024
|
const staticVerifyEvidence = buildVerifyEvidence({
|
|
1826
2025
|
phase: "intermediate",
|
|
1827
2026
|
quota: strategy.intermediateQuota ?? "full",
|
|
1828
|
-
commandSource:
|
|
1829
|
-
commands:
|
|
2027
|
+
commandSource: partitionedStaticVerifyCommands.static.commandSource,
|
|
2028
|
+
commands: partitionedStaticVerifyCommands.static.commands,
|
|
1830
2029
|
fallbackCommands: staticFallbackCommands,
|
|
2030
|
+
commandTexts: staticShellCommands,
|
|
2031
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1831
2032
|
});
|
|
2033
|
+
const lintVerifyEvidence = lintShellCommands.length > 0
|
|
2034
|
+
? buildVerifyEvidence({
|
|
2035
|
+
phase: "intermediate",
|
|
2036
|
+
quota: strategy.intermediateQuota ?? "full",
|
|
2037
|
+
commandSource: partitionedStaticVerifyCommands.lint.commandSource,
|
|
2038
|
+
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2039
|
+
fallbackCommands: [],
|
|
2040
|
+
commandTexts: lintShellCommands,
|
|
2041
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2042
|
+
})
|
|
2043
|
+
: undefined;
|
|
1832
2044
|
const behaviorVerifyEvidence = buildVerifyEvidence({
|
|
1833
2045
|
phase: "final",
|
|
1834
2046
|
quota: "full",
|
|
1835
2047
|
commandSource: behaviorVerifyCommands.commandSource,
|
|
1836
2048
|
commands: behaviorVerifyCommands.commands,
|
|
1837
2049
|
fallbackCommands: behaviorFallbackCommands,
|
|
2050
|
+
commandTexts: behaviorShellCommands,
|
|
1838
2051
|
finalFullRequired: true,
|
|
2052
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1839
2053
|
});
|
|
1840
2054
|
const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
|
|
1841
2055
|
? buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)
|
|
@@ -1897,6 +2111,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1897
2111
|
subtask_prompt: [
|
|
1898
2112
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
1899
2113
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2114
|
+
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
1900
2115
|
"Read-only: do not modify repository files.",
|
|
1901
2116
|
sourceContext,
|
|
1902
2117
|
].join("\n\n"),
|
|
@@ -1921,6 +2136,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1921
2136
|
"Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
|
|
1922
2137
|
"Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
|
|
1923
2138
|
"Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2139
|
+
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
1924
2140
|
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
|
|
1925
2141
|
requirementCoverageInstruction,
|
|
1926
2142
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
@@ -2042,16 +2258,47 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2042
2258
|
: ["native", "browser-intercept", "request-adapter", "not-needed"],
|
|
2043
2259
|
artifactName: "frontend-implementation-contract.json",
|
|
2044
2260
|
outputDir: "contracts",
|
|
2261
|
+
requireSourceFreshness: true,
|
|
2262
|
+
implementationWriteSet: implementPaths.writeSet,
|
|
2045
2263
|
openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence.normativePaths ?? [],
|
|
2046
2264
|
},
|
|
2047
2265
|
cwd: ".",
|
|
2048
2266
|
timeoutMs: 60000,
|
|
2049
2267
|
},
|
|
2050
2268
|
},
|
|
2269
|
+
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
2270
|
+
? [
|
|
2271
|
+
{
|
|
2272
|
+
id: "frontend-lint-baseline-shell",
|
|
2273
|
+
depends_on: ["frontend-prewrite-gate-shell"],
|
|
2274
|
+
role: "verifier",
|
|
2275
|
+
executor: "shell",
|
|
2276
|
+
complexity: "LOW",
|
|
2277
|
+
writePolicy: "read-only",
|
|
2278
|
+
allowedPaths: readOnlyPaths,
|
|
2279
|
+
forbiddenPaths,
|
|
2280
|
+
outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
|
|
2281
|
+
subtask_prompt: "Run the frozen lint commands read-only. Preserve raw output and mark the baseline unavailable on timeout, execution failure, unparseable output, or worktree mutation.",
|
|
2282
|
+
shell: {
|
|
2283
|
+
commands: lintShellCommands,
|
|
2284
|
+
frontendLintBaseline: {
|
|
2285
|
+
schemaVersion: 1,
|
|
2286
|
+
lintCommands: lintShellCommands,
|
|
2287
|
+
lintEvidence: lintVerifyEvidence,
|
|
2288
|
+
},
|
|
2289
|
+
cwd: ".",
|
|
2290
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2291
|
+
},
|
|
2292
|
+
},
|
|
2293
|
+
]
|
|
2294
|
+
: []),
|
|
2051
2295
|
{
|
|
2052
2296
|
id: implementId,
|
|
2053
2297
|
depends_on: [
|
|
2054
2298
|
"frontend-prewrite-gate-shell",
|
|
2299
|
+
...(lintShellCommands.length > 0
|
|
2300
|
+
? ["frontend-lint-baseline-shell"]
|
|
2301
|
+
: []),
|
|
2055
2302
|
"frontend-plan-revision-pi",
|
|
2056
2303
|
"frontend-final-design-review-pi",
|
|
2057
2304
|
"frontend-plan-pi",
|
|
@@ -2098,15 +2345,21 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2098
2345
|
frontendVerificationBundle: {
|
|
2099
2346
|
schemaVersion: 1,
|
|
2100
2347
|
mockCommands: mockShellCommands,
|
|
2348
|
+
lintCommands: lintShellCommands,
|
|
2101
2349
|
staticCommands: staticShellCommands,
|
|
2102
2350
|
behaviorCommands: behaviorShellCommands,
|
|
2103
2351
|
mockEvidence: mockVerifyEvidence,
|
|
2352
|
+
lintEvidence: lintVerifyEvidence,
|
|
2104
2353
|
staticEvidence: staticVerifyEvidence,
|
|
2105
2354
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
2355
|
+
lintBaselineNodeId: lintShellCommands.length > 0
|
|
2356
|
+
? "frontend-lint-baseline-shell"
|
|
2357
|
+
: undefined,
|
|
2358
|
+
writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
|
|
2106
2359
|
mode: "initial",
|
|
2107
2360
|
},
|
|
2108
2361
|
cwd: ".",
|
|
2109
|
-
timeoutMs:
|
|
2362
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2110
2363
|
},
|
|
2111
2364
|
},
|
|
2112
2365
|
{
|
|
@@ -2151,15 +2404,23 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2151
2404
|
frontendVerificationBundle: {
|
|
2152
2405
|
schemaVersion: 1,
|
|
2153
2406
|
mockCommands: mockShellCommands,
|
|
2407
|
+
lintCommands: lintShellCommands,
|
|
2154
2408
|
staticCommands: staticShellCommands,
|
|
2155
2409
|
behaviorCommands: behaviorShellCommands,
|
|
2156
2410
|
mockEvidence: mockVerifyEvidence,
|
|
2411
|
+
lintEvidence: lintVerifyEvidence,
|
|
2157
2412
|
staticEvidence: staticVerifyEvidence,
|
|
2158
2413
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
2414
|
+
lintBaselineNodeId: lintShellCommands.length > 0
|
|
2415
|
+
? "frontend-lint-baseline-shell"
|
|
2416
|
+
: undefined,
|
|
2417
|
+
writerNodeIds: lintShellCommands.length > 0
|
|
2418
|
+
? [implementId, "frontend-repair-pi"]
|
|
2419
|
+
: [],
|
|
2159
2420
|
mode: "repair",
|
|
2160
2421
|
},
|
|
2161
2422
|
cwd: ".",
|
|
2162
|
-
timeoutMs:
|
|
2423
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2163
2424
|
},
|
|
2164
2425
|
},
|
|
2165
2426
|
{
|
|
@@ -2177,11 +2438,11 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2177
2438
|
writePolicy: "read-only",
|
|
2178
2439
|
allowedPaths: readOnlyPaths,
|
|
2179
2440
|
forbiddenPaths,
|
|
2180
|
-
outputContract: "Canonical frontend review context containing validated contract, effective verification trace, repair assessment, and actual worktree diff.",
|
|
2441
|
+
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, repair assessment, and actual worktree diff.",
|
|
2181
2442
|
subtask_prompt: "Capture the actual diff and bind it to the effective initial-or-post-repair verification evidence for final review.",
|
|
2182
2443
|
shell: {
|
|
2183
2444
|
commands: [],
|
|
2184
|
-
frontendReviewContext: { schemaVersion: 1 },
|
|
2445
|
+
frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
|
|
2185
2446
|
cwd: ".",
|
|
2186
2447
|
timeoutMs: 120000,
|
|
2187
2448
|
},
|
|
@@ -2213,7 +2474,8 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2213
2474
|
"Review the frontend implementation and verification evidence.",
|
|
2214
2475
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2215
2476
|
"Any Critical or Important finding must force VERDICT: request-revision.",
|
|
2216
|
-
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
2477
|
+
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
2478
|
+
"Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
|
|
2217
2479
|
"Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
|
|
2218
2480
|
"Use the direct contract and the effective plan/design branch: original plan plus initial pass when revision was skipped, or revised plan plus final design review when revision ran. Do not infer them from the implementation summary.",
|
|
2219
2481
|
"Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
|
|
@@ -2271,7 +2533,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2271
2533
|
outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
|
|
2272
2534
|
subtask_prompt: [
|
|
2273
2535
|
"Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
|
|
2274
|
-
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
2536
|
+
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
2275
2537
|
`When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
|
|
2276
2538
|
`When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
|
|
2277
2539
|
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
@@ -2874,9 +3136,10 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
|
|
|
2874
3136
|
commandSource: "inline",
|
|
2875
3137
|
fallbackCommands: [pytestCommand],
|
|
2876
3138
|
finalFullRequired: true,
|
|
3139
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2877
3140
|
}),
|
|
2878
3141
|
cwd: ".",
|
|
2879
|
-
timeoutMs:
|
|
3142
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2880
3143
|
},
|
|
2881
3144
|
};
|
|
2882
3145
|
}
|
|
@@ -2993,6 +3256,7 @@ function buildBackendTestOutcomeGateNode(sources) {
|
|
|
2993
3256
|
commandSource: "inline",
|
|
2994
3257
|
fallbackCommands: [gateCommand],
|
|
2995
3258
|
finalFullRequired: true,
|
|
3259
|
+
commandTimeoutMs: 60_000,
|
|
2996
3260
|
}),
|
|
2997
3261
|
cwd: ".",
|
|
2998
3262
|
timeoutMs: 60000,
|
|
@@ -3100,7 +3364,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3100
3364
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3101
3365
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3102
3366
|
].join("; ");
|
|
3103
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with 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 valid JUnit with per-case captured output, self-contained HTML and reports/backend-test-facts.md; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3367
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with 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 valid JUnit with per-case captured output, self-contained HTML, reports/backend-test.md and reports/backend-test-facts.md; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3104
3368
|
if (execute.shell) {
|
|
3105
3369
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3106
3370
|
}
|
|
@@ -3175,51 +3439,52 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3175
3439
|
const evidenceRoot = "testcase/frontend/evidence";
|
|
3176
3440
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
3177
3441
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
3442
|
+
const checklistScript = [
|
|
3443
|
+
"const fs=require('fs'),path=require('path');",
|
|
3444
|
+
"const root='testcase/frontend/cases';",
|
|
3445
|
+
"const draft=path.join(root,'manifest.draft.json');",
|
|
3446
|
+
"const final=path.join(root,'manifest.json');",
|
|
3447
|
+
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
3448
|
+
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3449
|
+
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3450
|
+
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3451
|
+
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3452
|
+
"const issues=[];",
|
|
3453
|
+
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3454
|
+
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3455
|
+
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;", ,
|
|
3456
|
+
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3457
|
+
"for(const c of manifest.cases){",
|
|
3458
|
+
" const id=c&&c.caseId||'?';",
|
|
3459
|
+
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
3460
|
+
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
3461
|
+
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3462
|
+
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3463
|
+
" if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
|
|
3464
|
+
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3465
|
+
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>; playwright-cli is strongly recommended for browser execution'});", ,
|
|
3466
|
+
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3467
|
+
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3468
|
+
" if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
|
|
3469
|
+
" else {",
|
|
3470
|
+
" for(const ac of c.acIds){",
|
|
3471
|
+
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3472
|
+
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3473
|
+
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3474
|
+
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3475
|
+
" }",
|
|
3476
|
+
" }",
|
|
3477
|
+
"}",
|
|
3478
|
+
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3479
|
+
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
3480
|
+
].join("");
|
|
3481
|
+
// Pass generated JavaScript as base64 rather than embedding it inside a
|
|
3482
|
+
// shell-quoted `node -e` argument. Git Bash otherwise consumes backslashes
|
|
3483
|
+
// such as \\s/\\d and interprets Markdown backticks before Node sees them.
|
|
3484
|
+
const checklistScriptBase64 = Buffer.from(checklistScript, "utf8").toString("base64");
|
|
3178
3485
|
const checklistValidation = [
|
|
3179
|
-
"node -e",
|
|
3180
|
-
|
|
3181
|
-
"const fs=require('fs'),path=require('path');",
|
|
3182
|
-
"const root='testcase/frontend/cases';",
|
|
3183
|
-
"const draft=path.join(root,'manifest.draft.json');",
|
|
3184
|
-
"const final=path.join(root,'manifest.json');",
|
|
3185
|
-
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
3186
|
-
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3187
|
-
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3188
|
-
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3189
|
-
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3190
|
-
"const issues=[];",
|
|
3191
|
-
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3192
|
-
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3193
|
-
"const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
|
|
3194
|
-
"const barePwRe=/(?:^|[\\s\"'(])(?:npx\\s+playwright\\b|playwright\\s+test\\b|from\\s+['\"]@playwright\\/|require\\(['\"]@playwright\\/|import\\s+.*@playwright\\/|(?<![\\w-])playwright(?!-cli)\\b)/i;",
|
|
3195
|
-
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
3196
|
-
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3197
|
-
"for(const c of manifest.cases){",
|
|
3198
|
-
" const id=c&&c.caseId||'?';",
|
|
3199
|
-
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
3200
|
-
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
3201
|
-
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3202
|
-
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3203
|
-
" if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
|
|
3204
|
-
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3205
|
-
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
|
|
3206
|
-
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3207
|
-
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3208
|
-
" if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
|
|
3209
|
-
" if(barePwRe.test(body))issues.push({ruleId:'playwright-cli-only',caseId:id,detail:'only playwright-cli skill commands allowed; bare Playwright CLI/API/test runner forbidden'});",
|
|
3210
|
-
" if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
|
|
3211
|
-
" else {",
|
|
3212
|
-
" for(const ac of c.acIds){",
|
|
3213
|
-
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3214
|
-
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3215
|
-
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3216
|
-
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3217
|
-
" }",
|
|
3218
|
-
" }",
|
|
3219
|
-
"}",
|
|
3220
|
-
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3221
|
-
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
3222
|
-
].join("")),
|
|
3486
|
+
"node -e \"eval(Buffer.from(process.argv[1],'base64').toString('utf8'))\"",
|
|
3487
|
+
checklistScriptBase64,
|
|
3223
3488
|
].join(" ");
|
|
3224
3489
|
const manifestValidation = [
|
|
3225
3490
|
"node -e",
|
|
@@ -3260,6 +3525,23 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3260
3525
|
// Heal malformed/missing case evidence to blocked; only unsafe evidenceDir hard-fails.
|
|
3261
3526
|
const evidenceValidation = buildFrontendCaseEvidenceValidateShellSnippet();
|
|
3262
3527
|
const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
|
|
3528
|
+
const frontendCaseQualityAdvisory = [
|
|
3529
|
+
"node -e",
|
|
3530
|
+
JSON.stringify([
|
|
3531
|
+
"const fs=require('fs'),path=require('path');",
|
|
3532
|
+
"const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
|
|
3533
|
+
"const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
|
|
3534
|
+
"const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
|
|
3535
|
+
"const reports='testcase/frontend/reports'; fs.mkdirSync(reports,{recursive:true});",
|
|
3536
|
+
"const esc=v=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\\\"','"');",
|
|
3537
|
+
"const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
|
|
3538
|
+
"const md=['# 前端测试质量建议报告','','> Advisory:以下 finding 用于改进测试资产和证据质量,不阻塞后续流程。','','## 执行摘要','',...Object.entries(labels).map(([k,v])=>'- '+v+': '+Number(totals[k]||0)),'- 执行结果: '+String(r.outcome||'unknown'),'- 缺失 AC: '+(missing.join(', ')||'无'),'','## 建议项','',...(findings.length?findings.map(f=>'- ['+f.ruleId+']'+(f.caseId?' '+f.caseId:'')+': '+f.detail):['- 未发现建议项。']),''];",
|
|
3539
|
+
"fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.md'),md.join('\\n'));",
|
|
3540
|
+
"const rows=findings.length?findings.map(f=>'<tr><td><code>'+esc(f.ruleId)+'</code></td><td>'+esc(f.caseId||'-')+'</td><td>'+esc(f.detail)+'</td></tr>').join(''):'<tr><td colspan=3>未发现建议项。</td></tr>';",
|
|
3541
|
+
"const html='<!doctype html><html lang=\\\"zh-CN\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>前端测试质量建议报告</title><style>body{font:16px system-ui,Microsoft YaHei,sans-serif;background:#f5f7fb;color:#172033;margin:0;padding:32px}main{max-width:1100px;margin:auto;background:#fff;padding:32px;border-radius:16px}.badge{display:inline-block;padding:6px 10px;border-radius:999px;background:#fff3cd;color:#946200}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.card{padding:16px;background:#f8fafc;border-radius:10px}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px;border-bottom:1px solid #e5e7eb}code{color:#475467}@media(max-width:700px){.grid{grid-template-columns:1fr 1fr}}</style></head><body><main><h1>前端测试质量建议报告</h1><p><span class=\\\"badge\\\">Advisory,不阻塞后续流程</span></p><h2>执行摘要</h2><div class=\\\"grid\\\">'+Object.entries(labels).map(([k,v])=>'<div class=\\\"card\\\"><strong>'+v+'</strong><div>'+Number(totals[k]||0)+'</div></div>').join('')+'</div><p>执行结果:'+esc(r.outcome||'unknown')+';缺失 AC:'+esc(missing.join(', ')||'无')+'</p><h2>建议项</h2><table><thead><tr><th>规则</th><th>Case ID</th><th>说明与建议</th></tr></thead><tbody>'+rows+'</tbody></table></main></body></html>';",
|
|
3542
|
+
"fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.html'),html); console.log('frontend-test advisory report findings='+findings.length);",
|
|
3543
|
+
].join("")),
|
|
3544
|
+
].join(" ");
|
|
3263
3545
|
const tasks = [
|
|
3264
3546
|
{
|
|
3265
3547
|
id: "retrieve-frontend-test-context-pi",
|
|
@@ -3413,8 +3695,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3413
3695
|
writePolicy: "read-only",
|
|
3414
3696
|
allowedPaths: [...ragWriteSet, ...casesWriteSet],
|
|
3415
3697
|
forbiddenPaths: forbidden,
|
|
3416
|
-
outputContract: "Mechanical checklist:
|
|
3417
|
-
subtask_prompt: "Scan generated cases/manifest
|
|
3698
|
+
outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and AC mapping; alternative executable tool commands are not inspected or blocked.",
|
|
3699
|
+
subtask_prompt: "Scan generated cases/manifest for structural and safety rules only. Strongly recommend playwright-cli for browser execution, but do not inspect or reject alternative executable tool commands and do not use free-form LLM verdicts.",
|
|
3418
3700
|
shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
|
|
3419
3701
|
}, {
|
|
3420
3702
|
id: "materialize-frontend-case-manifest-shell",
|
|
@@ -3548,6 +3830,20 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3548
3830
|
outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete. Pipeline acceptance = this report exists (not case 100% pass).",
|
|
3549
3831
|
subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
|
|
3550
3832
|
});
|
|
3833
|
+
tasks.push({
|
|
3834
|
+
id: "frontend-test-quality-report-html-shell",
|
|
3835
|
+
depends_on: ["frontend-test-retrospect-pi"],
|
|
3836
|
+
role: "verifier",
|
|
3837
|
+
executor: "shell",
|
|
3838
|
+
complexity: "LOW",
|
|
3839
|
+
writePolicy: "exclusive",
|
|
3840
|
+
writeSet: ["testcase/frontend/reports/**"],
|
|
3841
|
+
allowedPaths: ["testcase/frontend/**"],
|
|
3842
|
+
forbiddenPaths: forbidden,
|
|
3843
|
+
outputContract: "Write a user-readable HTML advisory report after the final Markdown retrospective; findings never block the workflow.",
|
|
3844
|
+
subtask_prompt: "Generate the frontend case quality advisory Markdown and HTML report after the final test report. Record content findings only and never fail because findings exist.",
|
|
3845
|
+
shell: { commands: [frontendCaseQualityAdvisory], cwd: ".", timeoutMs: 120000 },
|
|
3846
|
+
});
|
|
3551
3847
|
const globalConstraints = [
|
|
3552
3848
|
...sources.taskConfig.hardConstraints,
|
|
3553
3849
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
@@ -3701,6 +3997,7 @@ function buildMultiPerspectiveReviewNodes(input) {
|
|
|
3701
3997
|
commandSource: "inline",
|
|
3702
3998
|
fallbackCommands: [aggregateScript],
|
|
3703
3999
|
finalFullRequired: true,
|
|
4000
|
+
commandTimeoutMs: 60_000,
|
|
3704
4001
|
}),
|
|
3705
4002
|
cwd: ".",
|
|
3706
4003
|
timeoutMs: 60000,
|
|
@@ -3885,6 +4182,7 @@ function buildKnowledgeSyncValidateNode(sources, featureId) {
|
|
|
3885
4182
|
commandSource: "inline",
|
|
3886
4183
|
fallbackCommands: [validateScript],
|
|
3887
4184
|
finalFullRequired: true,
|
|
4185
|
+
commandTimeoutMs: 120_000,
|
|
3888
4186
|
}),
|
|
3889
4187
|
cwd: ".",
|
|
3890
4188
|
timeoutMs: 120000,
|
|
@@ -4105,6 +4403,7 @@ function buildKgBootstrapPreflightNode(sources) {
|
|
|
4105
4403
|
quota: "full",
|
|
4106
4404
|
commandSource: "inline",
|
|
4107
4405
|
fallbackCommands: [script],
|
|
4406
|
+
commandTimeoutMs: 60_000,
|
|
4108
4407
|
}),
|
|
4109
4408
|
cwd: ".",
|
|
4110
4409
|
timeoutMs: 60000,
|
|
@@ -4152,6 +4451,7 @@ function buildKgBootstrapInventoryNode(sources) {
|
|
|
4152
4451
|
quota: "full",
|
|
4153
4452
|
commandSource: "inline",
|
|
4154
4453
|
fallbackCommands: [script],
|
|
4454
|
+
commandTimeoutMs: 120_000,
|
|
4155
4455
|
}),
|
|
4156
4456
|
cwd: ".",
|
|
4157
4457
|
timeoutMs: 120000,
|
|
@@ -4246,6 +4546,7 @@ function buildKgBootstrapValidateNode(sources) {
|
|
|
4246
4546
|
commandSource: "inline",
|
|
4247
4547
|
fallbackCommands: [script],
|
|
4248
4548
|
finalFullRequired: true,
|
|
4549
|
+
commandTimeoutMs: 120_000,
|
|
4249
4550
|
}),
|
|
4250
4551
|
cwd: ".",
|
|
4251
4552
|
timeoutMs: 120000,
|
|
@@ -4373,6 +4674,7 @@ function buildKgBootstrapPromoteNode(sources) {
|
|
|
4373
4674
|
commandSource: "inline",
|
|
4374
4675
|
fallbackCommands: [script],
|
|
4375
4676
|
finalFullRequired: true,
|
|
4677
|
+
commandTimeoutMs: 120_000,
|
|
4376
4678
|
}),
|
|
4377
4679
|
cwd: ".",
|
|
4378
4680
|
timeoutMs: 120000,
|
|
@@ -4438,6 +4740,7 @@ function buildKgBootstrapMaterializeNode(sources) {
|
|
|
4438
4740
|
commandSource: "inline",
|
|
4439
4741
|
fallbackCommands: [script],
|
|
4440
4742
|
finalFullRequired: true,
|
|
4743
|
+
commandTimeoutMs: 120_000,
|
|
4441
4744
|
}),
|
|
4442
4745
|
cwd: ".",
|
|
4443
4746
|
timeoutMs: 120000,
|
|
@@ -4494,7 +4797,22 @@ export async function requireManagedTaskContractBinding(input) {
|
|
|
4494
4797
|
taskId: input.taskId,
|
|
4495
4798
|
});
|
|
4496
4799
|
if (state.effectiveStatus !== "managed" || !state.ref) {
|
|
4497
|
-
const
|
|
4800
|
+
const expectedRevision = state.ref?.revision ?? 0;
|
|
4801
|
+
const observedCanonicalHash = state.observedCanonicalHash ?? "<observedCanonicalHash-from-show>";
|
|
4802
|
+
const recovery = state.effectiveStatus === "transaction-incomplete"
|
|
4803
|
+
? `Recover the unfinished transaction first: loop-agent task contract recover --task ${input.taskId} --json`
|
|
4804
|
+
: [
|
|
4805
|
+
"After reviewing the show JSON, either adopt the current on-disk facts:",
|
|
4806
|
+
`loop-agent task contract adopt --task ${input.taskId} --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-this-adopt-request> --json`,
|
|
4807
|
+
"or apply a reviewed draft:",
|
|
4808
|
+
`loop-agent task contract apply --task ${input.taskId} --input <reviewed-draft.json> --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-input-file> --json`,
|
|
4809
|
+
].join(" ");
|
|
4810
|
+
const err = new Error([
|
|
4811
|
+
`dag generate requires managed Task Contract for task ${input.taskId} (effectiveStatus=${state.effectiveStatus})`,
|
|
4812
|
+
`Inspect current state: loop-agent task contract show ${input.taskId} --json`,
|
|
4813
|
+
recovery,
|
|
4814
|
+
"Do not auto-adopt or auto-apply before confirming the Task Contract facts and concurrency fields.",
|
|
4815
|
+
].join("; "));
|
|
4498
4816
|
err.code =
|
|
4499
4817
|
state.effectiveStatus === "externally-modified"
|
|
4500
4818
|
? "EXTERNALLY_MODIFIED"
|
|
@@ -4548,10 +4866,6 @@ function synthesizeInMemoryTaskContractBinding(sources) {
|
|
|
4548
4866
|
};
|
|
4549
4867
|
}
|
|
4550
4868
|
async function resolveTaskContractBindingForGenerate(sources) {
|
|
4551
|
-
// Real CLI generation freezes the managed ref. Direct builder unit tests
|
|
4552
|
-
// may provide repoRoot-shaped fixture paths without a persisted task; keep
|
|
4553
|
-
// their deterministic synthetic binding path so topology/prompt tests remain
|
|
4554
|
-
// isolated from contract storage.
|
|
4555
4869
|
if (sources.repoRoot) {
|
|
4556
4870
|
const state = await observeTaskContract({
|
|
4557
4871
|
repoRoot: sources.repoRoot,
|
|
@@ -4570,7 +4884,7 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
4570
4884
|
const taskContractBinding = await resolveTaskContractBindingForGenerate(sources);
|
|
4571
4885
|
let spec;
|
|
4572
4886
|
if (template === "frontend-implementation") {
|
|
4573
|
-
spec = buildFrontendHybridDagFromTask(sources);
|
|
4887
|
+
spec = await buildFrontendHybridDagFromTask(sources);
|
|
4574
4888
|
}
|
|
4575
4889
|
else if (template === "frontend-test-dag")
|
|
4576
4890
|
spec = buildFrontendTestHybridDag(sources);
|
|
@@ -4706,7 +5020,12 @@ function buildReviewVerdictRecoveryNode(sources) {
|
|
|
4706
5020
|
return {
|
|
4707
5021
|
id: "review-verdict-recovery-pi",
|
|
4708
5022
|
depends_on: ["review-pi"],
|
|
5023
|
+
// AC3: recovery is a read-only failure-aware node. It runs after review-pi
|
|
5024
|
+
// settles, consuming either normal FINISHED output or an explicitly tolerated
|
|
5025
|
+
// ERROR (for example protocol-invalid after retries). It must not bypass the
|
|
5026
|
+
// writer, write guard, or final shell gate, which remain fail-closed.
|
|
4709
5027
|
role: "reviewer",
|
|
5028
|
+
failureAwareDependsOn: ["review-pi"],
|
|
4710
5029
|
executor: "pi",
|
|
4711
5030
|
complexity: "LOW",
|
|
4712
5031
|
writePolicy: "read-only",
|
|
@@ -4991,7 +5310,7 @@ function buildWriteSetGateNode(sources) {
|
|
|
4991
5310
|
}
|
|
4992
5311
|
function buildSoftVerifyNode(sources) {
|
|
4993
5312
|
const implementId = implementationNodeId();
|
|
4994
|
-
const strategy = resolveDagVerifyStrategy(sources.taskConfig);
|
|
5313
|
+
const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
|
|
4995
5314
|
const fallbackCommands = ["npm run typecheck"];
|
|
4996
5315
|
const commands = buildVerifyShellCommands({
|
|
4997
5316
|
repoRoot: sources.repoRoot,
|
|
@@ -5017,9 +5336,10 @@ function buildSoftVerifyNode(sources) {
|
|
|
5017
5336
|
commandSource: sources.verifyCommands ? "adapter" : "inline",
|
|
5018
5337
|
commands: sources.verifyCommands?.intermediate,
|
|
5019
5338
|
fallbackCommands,
|
|
5339
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5020
5340
|
}),
|
|
5021
5341
|
cwd: ".",
|
|
5022
|
-
timeoutMs:
|
|
5342
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5023
5343
|
},
|
|
5024
5344
|
};
|
|
5025
5345
|
}
|
|
@@ -5029,6 +5349,7 @@ function buildProcessSupervisorNode(sources) {
|
|
|
5029
5349
|
return {
|
|
5030
5350
|
id: "process-supervisor-pi",
|
|
5031
5351
|
depends_on: ["soft-verify-shell", implementId],
|
|
5352
|
+
failureAwareDependsOn: ["soft-verify-shell"],
|
|
5032
5353
|
role: "supervisor",
|
|
5033
5354
|
executor: "pi",
|
|
5034
5355
|
complexity: "HIGH",
|
|
@@ -5135,9 +5456,10 @@ function buildHardVerifyNode(sources) {
|
|
|
5135
5456
|
commands: sources.verifyCommands?.final,
|
|
5136
5457
|
fallbackCommands,
|
|
5137
5458
|
finalFullRequired: true,
|
|
5459
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5138
5460
|
}),
|
|
5139
5461
|
cwd: ".",
|
|
5140
|
-
timeoutMs:
|
|
5462
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5141
5463
|
},
|
|
5142
5464
|
};
|
|
5143
5465
|
}
|
|
@@ -5186,7 +5508,7 @@ function buildSupervisedHybridDag(standard, sources) {
|
|
|
5186
5508
|
objective: `${standard.objective ?? ""}\n\nRoute: supervised implementation DAG selected by workflowPolicy/governanceProfile or explicit CLI profile.`.trim(),
|
|
5187
5509
|
globalConstraints: supervisedConstraints,
|
|
5188
5510
|
convergence: sources.taskConfig.convergence,
|
|
5189
|
-
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5511
|
+
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig, "1"),
|
|
5190
5512
|
tasks: [
|
|
5191
5513
|
cloneTask(contract),
|
|
5192
5514
|
cloneTask(scoutSrc),
|
|
@@ -5256,6 +5578,12 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
5256
5578
|
? await prepareFrontendMockSources(sources)
|
|
5257
5579
|
: sources;
|
|
5258
5580
|
const spec = await buildHybridDagForTemplate(preparedSources, template);
|
|
5581
|
+
if (preparedSources.repoRoot && dagHasWriterExecution(spec)) {
|
|
5582
|
+
await requireManagedTaskContractBinding({
|
|
5583
|
+
repoRoot: preparedSources.repoRoot,
|
|
5584
|
+
taskId: preparedSources.taskId,
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5259
5587
|
await writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
|
|
5260
5588
|
return {
|
|
5261
5589
|
taskId: sources.taskId,
|