@tea-agent/loop-agent 0.20.1 → 0.22.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 +72 -0
- package/bin/agent-worker.js +0 -0
- package/dist/adapters/loop-agent.js +52 -0
- package/dist/commands/init.js +104 -0
- package/dist/executors/dag-pi-executor.js +26 -0
- package/dist/executors/pi-executor.js +111 -36
- package/dist/executors/pi-sdk-executor.js +105 -29
- package/dist/executors/shell-executor.js +215 -29
- package/dist/shared/openspec-spec.js +49 -0
- package/dist/worker/loop-agent/loop-agent-client.js +43 -9
- package/dist/worker/observability/read-model.js +28 -2
- package/dist/worker/observe/spec-evidence.js +12 -15
- package/dist/worker/observe/static/constants.js +5 -0
- package/dist/worker/observe/static/dag-helpers.js +22 -0
- package/dist/worker/observe/static/format-pool.js +22 -3
- package/dist/worker/observe/static/styles.css +32 -3
- package/dist/worker/observe/static/views/dag-inspector.js +2 -2
- package/dist/worker/observe/static/views/dag.js +5 -0
- package/dist/worker/run-task/run-task.js +16 -6
- package/dist/workflows/dag/backend-test-markdown-workflow.js +328 -97
- package/dist/workflows/dag/backend-test-result-contract.js +10 -4
- 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 +370 -79
- package/dist/workflows/dag/lifecycle.js +60 -4
- package/dist/workflows/dag/liveness-policy.js +250 -0
- package/dist/workflows/dag/node-execution.js +49 -0
- package/dist/workflows/dag/runner.js +21 -1
- package/dist/workflows/dag/types.js +67 -1
- package/docs/README.md +5 -6
- package/docs/architecture/dag-execution.md +11 -0
- package/docs/architecture/facts-and-state.md +1 -0
- package/docs/architecture/worker-and-feature.md +10 -0
- package/docs/templates/agent-dag.schema.json +15 -5
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -2
- package/docs/templates/backend-test-dag.json +15 -15
- 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/harness.json +1 -1
- package/package.json +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/loop-agent/references/command-reference.md +2 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
|
@@ -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";
|
|
@@ -829,6 +830,9 @@ function extractFrontendMockVerifyCommandsFromMarkdown(input) {
|
|
|
829
830
|
return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
|
|
830
831
|
}
|
|
831
832
|
function chooseFrontendVerifyCommands(input) {
|
|
833
|
+
if (input.explicitCommands && input.explicitCommands.length > 0) {
|
|
834
|
+
return { commands: input.explicitCommands, commandSource: "inline" };
|
|
835
|
+
}
|
|
832
836
|
if (input.parsedCommands.length > 0) {
|
|
833
837
|
return { commands: input.parsedCommands, commandSource: "inline" };
|
|
834
838
|
}
|
|
@@ -837,6 +841,36 @@ function chooseFrontendVerifyCommands(input) {
|
|
|
837
841
|
}
|
|
838
842
|
return { commandSource: "inline" };
|
|
839
843
|
}
|
|
844
|
+
function classifyFrontendVerifyCommand(command) {
|
|
845
|
+
return /\b(typecheck|check-types|lint|eslint|tsc|build|check)\b/i.test(command) ||
|
|
846
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)
|
|
847
|
+
? "static"
|
|
848
|
+
: "behavior";
|
|
849
|
+
}
|
|
850
|
+
function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
|
|
851
|
+
const staticCommands = [];
|
|
852
|
+
const behaviorCommands = [];
|
|
853
|
+
if (!repoRoot)
|
|
854
|
+
return { staticCommands, behaviorCommands };
|
|
855
|
+
for (const command of taskConfig.verifyCommands) {
|
|
856
|
+
const verifyCommand = {
|
|
857
|
+
args: ["bash", "-lc", command.command],
|
|
858
|
+
cwd: repoRoot,
|
|
859
|
+
label: command.label,
|
|
860
|
+
timeoutMs: command.timeoutMs,
|
|
861
|
+
};
|
|
862
|
+
if (classifyFrontendVerifyCommand(command.command) === "static") {
|
|
863
|
+
staticCommands.push(verifyCommand);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
behaviorCommands.push(verifyCommand);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return { staticCommands, behaviorCommands };
|
|
870
|
+
}
|
|
871
|
+
function verifyCommandKey(command) {
|
|
872
|
+
return `${command.cwd}\0${command.args.join("\0")}`;
|
|
873
|
+
}
|
|
840
874
|
function resolveDagVerifyStrategy(taskConfig) {
|
|
841
875
|
return {
|
|
842
876
|
intermediateQuota: taskConfig.dagVerifyStrategy?.intermediateQuota ?? taskConfig.verifyQuota,
|
|
@@ -851,19 +885,80 @@ function buildVerifyEvidence(input) {
|
|
|
851
885
|
commandSource: input.commandSource,
|
|
852
886
|
commandCount: input.commands?.length ?? input.fallbackCommands.length,
|
|
853
887
|
commandLabels: input.commands?.map((command) => command.label) ?? input.fallbackCommands,
|
|
888
|
+
commandTexts: input.commandTexts ?? [],
|
|
854
889
|
finalFullRequired: input.finalFullRequired,
|
|
855
890
|
};
|
|
856
891
|
}
|
|
892
|
+
function isFrontendLintVerifyCommand(command) {
|
|
893
|
+
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
894
|
+
return /\b(?:lint|eslint)\b/i.test(text);
|
|
895
|
+
}
|
|
896
|
+
function isManagedCiWrapperVerifyCommand(command) {
|
|
897
|
+
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
898
|
+
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
899
|
+
}
|
|
900
|
+
function repoHasNpmScript(repoRoot, scriptName) {
|
|
901
|
+
if (!repoRoot)
|
|
902
|
+
return false;
|
|
903
|
+
const packagePath = path.join(repoRoot, "package.json");
|
|
904
|
+
if (!existsSync(packagePath))
|
|
905
|
+
return false;
|
|
906
|
+
try {
|
|
907
|
+
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
908
|
+
return typeof decoded.scripts?.[scriptName] === "string";
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function partitionFrontendStaticVerifyCommands(input) {
|
|
915
|
+
const commands = input.commands ?? [];
|
|
916
|
+
const lintCommands = commands.filter(isFrontendLintVerifyCommand);
|
|
917
|
+
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
918
|
+
if (lintCommands.length === 0 &&
|
|
919
|
+
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
920
|
+
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
921
|
+
lintCommands.push({
|
|
922
|
+
args: ["npm", "run", "lint"],
|
|
923
|
+
cwd: input.repoRoot,
|
|
924
|
+
label: "npm run lint",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
lint: {
|
|
929
|
+
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
930
|
+
commandSource: input.commandSource,
|
|
931
|
+
},
|
|
932
|
+
static: {
|
|
933
|
+
...(staticCommands.length > 0 ? { commands: staticCommands } : {}),
|
|
934
|
+
commandSource: input.commandSource,
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function isFrontendTestPathPattern(entry) {
|
|
939
|
+
const segments = entry.replaceAll("\\", "/").split("/");
|
|
940
|
+
return segments.some((segment) => /^(?:test|tests|spec|specs|e2e|__tests__|__specs__)$/i.test(segment));
|
|
941
|
+
}
|
|
942
|
+
function replaceSourceSegmentWithTestSegment(entry) {
|
|
943
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
944
|
+
const segments = normalized.split("/");
|
|
945
|
+
const sourceIndex = segments.findIndex((segment) => /^(?:src|source|app|lib)$/i.test(segment));
|
|
946
|
+
if (sourceIndex < 0)
|
|
947
|
+
return null;
|
|
948
|
+
const testSegments = [...segments];
|
|
949
|
+
testSegments[sourceIndex] = "test";
|
|
950
|
+
return testSegments.join("/");
|
|
951
|
+
}
|
|
857
952
|
function deriveParallelScoutPaths(taskConfig) {
|
|
858
953
|
const allowed = taskConfig.allowedPaths;
|
|
859
954
|
if (allowed.length === 0) {
|
|
860
955
|
return { srcPaths: ["**"], testPaths: ["**"] };
|
|
861
956
|
}
|
|
862
|
-
const srcPaths = allowed.filter((entry) => !entry
|
|
863
|
-
const explicitTestPaths = allowed.filter(
|
|
957
|
+
const srcPaths = allowed.filter((entry) => !isFrontendTestPathPattern(entry));
|
|
958
|
+
const explicitTestPaths = allowed.filter(isFrontendTestPathPattern);
|
|
864
959
|
const derivedTestPaths = allowed
|
|
865
|
-
.
|
|
866
|
-
.
|
|
960
|
+
.map(replaceSourceSegmentWithTestSegment)
|
|
961
|
+
.filter((entry) => Boolean(entry));
|
|
867
962
|
return {
|
|
868
963
|
srcPaths: srcPaths.length > 0 ? srcPaths : allowed,
|
|
869
964
|
testPaths: explicitTestPaths.length > 0
|
|
@@ -873,6 +968,80 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
873
968
|
: allowed,
|
|
874
969
|
};
|
|
875
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Resolve frontend verification fallbacks from the target project's own
|
|
973
|
+
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
974
|
+
* package.json, so those fixtures retain the historical generic fallback.
|
|
975
|
+
* Real projects never inherit loop-agent's commands when package.json exists.
|
|
976
|
+
*/
|
|
977
|
+
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
978
|
+
const genericFallback = {
|
|
979
|
+
staticCommands: ["npm run typecheck", "npm run build"],
|
|
980
|
+
behaviorCommands: ["npm test"],
|
|
981
|
+
};
|
|
982
|
+
if (!repoRoot)
|
|
983
|
+
return genericFallback;
|
|
984
|
+
let scripts;
|
|
985
|
+
try {
|
|
986
|
+
const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
987
|
+
if (packageJson.scripts && typeof packageJson.scripts === "object") {
|
|
988
|
+
scripts = packageJson.scripts;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return genericFallback;
|
|
993
|
+
}
|
|
994
|
+
if (!scripts)
|
|
995
|
+
return genericFallback;
|
|
996
|
+
let packageManager = "npm";
|
|
997
|
+
for (const [lockfile, manager] of [
|
|
998
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
999
|
+
["yarn.lock", "yarn"],
|
|
1000
|
+
["bun.lockb", "bun"],
|
|
1001
|
+
["bun.lock", "bun"],
|
|
1002
|
+
]) {
|
|
1003
|
+
try {
|
|
1004
|
+
await access(path.join(repoRoot, lockfile));
|
|
1005
|
+
packageManager = manager;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
// Try the next package-manager marker.
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const commandForScript = (script) => script === "test"
|
|
1013
|
+
? `${packageManager} test`
|
|
1014
|
+
: `${packageManager} run ${script}`;
|
|
1015
|
+
const hasScript = (script) => typeof scripts?.[script] === "string" &&
|
|
1016
|
+
String(scripts[script]).trim().length > 0;
|
|
1017
|
+
const firstExisting = (names) => {
|
|
1018
|
+
const selected = [];
|
|
1019
|
+
for (const name of names) {
|
|
1020
|
+
if (hasScript(name) && !selected.includes(name))
|
|
1021
|
+
selected.push(name);
|
|
1022
|
+
}
|
|
1023
|
+
return selected.map(commandForScript);
|
|
1024
|
+
};
|
|
1025
|
+
const staticCommands = firstExisting([
|
|
1026
|
+
"typecheck",
|
|
1027
|
+
"check-types",
|
|
1028
|
+
"lint",
|
|
1029
|
+
"check",
|
|
1030
|
+
"build",
|
|
1031
|
+
]);
|
|
1032
|
+
const behaviorCommands = firstExisting([
|
|
1033
|
+
"test:unit",
|
|
1034
|
+
"test:frontend",
|
|
1035
|
+
"test:component",
|
|
1036
|
+
"test",
|
|
1037
|
+
"test:e2e",
|
|
1038
|
+
"e2e",
|
|
1039
|
+
]);
|
|
1040
|
+
return {
|
|
1041
|
+
staticCommands: staticCommands.length > 0 ? staticCommands : behaviorCommands,
|
|
1042
|
+
behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : staticCommands,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
876
1045
|
function deriveFrontendBehaviorPaths(taskConfig) {
|
|
877
1046
|
if (taskConfig.allowedPaths.length === 0)
|
|
878
1047
|
return ["**"];
|
|
@@ -1507,6 +1676,7 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
|
|
|
1507
1676
|
commandSource: commands.length > 0 ? "inline" : "adapter",
|
|
1508
1677
|
commands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
1509
1678
|
fallbackCommands: [],
|
|
1679
|
+
commandTexts: commands,
|
|
1510
1680
|
}),
|
|
1511
1681
|
cwd: ".",
|
|
1512
1682
|
timeoutMs: 300000,
|
|
@@ -1688,7 +1858,7 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
1688
1858
|
return { ...task, depends_on };
|
|
1689
1859
|
});
|
|
1690
1860
|
}
|
|
1691
|
-
function buildFrontendHybridDagFromTask(sources) {
|
|
1861
|
+
async function buildFrontendHybridDagFromTask(sources) {
|
|
1692
1862
|
const { taskConfig } = sources;
|
|
1693
1863
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
1694
1864
|
status: "absent",
|
|
@@ -1756,6 +1926,8 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1756
1926
|
"- verificationTargets is a TOP-LEVEL required array",
|
|
1757
1927
|
"- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
|
|
1758
1928
|
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
1929
|
+
"- 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.",
|
|
1930
|
+
"- 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
1931
|
].join("\n");
|
|
1760
1932
|
})();
|
|
1761
1933
|
const sourceContext = [
|
|
@@ -1797,26 +1969,42 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1797
1969
|
if (mockMode === "blocked") {
|
|
1798
1970
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints);
|
|
1799
1971
|
}
|
|
1800
|
-
const
|
|
1801
|
-
const
|
|
1972
|
+
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
1973
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
|
|
1974
|
+
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
1802
1975
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
1803
1976
|
repoRoot: sources.repoRoot,
|
|
1804
1977
|
requirementMarkdown: sources.requirementMarkdown,
|
|
1805
1978
|
constraintMarkdown: sources.constraintMarkdown,
|
|
1806
1979
|
});
|
|
1980
|
+
const explicitFrontendVerifyCommands = buildExplicitFrontendVerifyCommands(taskConfig, sources.repoRoot);
|
|
1981
|
+
const explicitCommandKeys = new Set([...explicitFrontendVerifyCommands.staticCommands, ...explicitFrontendVerifyCommands.behaviorCommands].map(verifyCommandKey));
|
|
1982
|
+
const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
|
|
1807
1983
|
const staticVerifyCommands = chooseFrontendVerifyCommands({
|
|
1984
|
+
explicitCommands: explicitFrontendVerifyCommands.staticCommands,
|
|
1808
1985
|
parsedCommands: parsedFrontendVerifyCommands.staticCommands,
|
|
1809
|
-
adapterCommands:
|
|
1986
|
+
adapterCommands: adapterVerifyCommands,
|
|
1987
|
+
});
|
|
1988
|
+
const partitionedStaticVerifyCommands = partitionFrontendStaticVerifyCommands({
|
|
1989
|
+
repoRoot: sources.repoRoot,
|
|
1990
|
+
commands: staticVerifyCommands.commands,
|
|
1991
|
+
commandSource: staticVerifyCommands.commandSource,
|
|
1810
1992
|
});
|
|
1811
1993
|
const behaviorVerifyCommands = chooseFrontendVerifyCommands({
|
|
1994
|
+
explicitCommands: explicitFrontendVerifyCommands.behaviorCommands,
|
|
1812
1995
|
parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
|
|
1813
|
-
adapterCommands:
|
|
1996
|
+
adapterCommands: adapterVerifyCommands,
|
|
1814
1997
|
});
|
|
1815
1998
|
const staticShellCommands = buildVerifyShellCommands({
|
|
1816
1999
|
repoRoot: sources.repoRoot,
|
|
1817
|
-
commands:
|
|
2000
|
+
commands: partitionedStaticVerifyCommands.static.commands,
|
|
1818
2001
|
fallbackCommands: staticFallbackCommands,
|
|
1819
2002
|
});
|
|
2003
|
+
const lintShellCommands = buildVerifyShellCommands({
|
|
2004
|
+
repoRoot: sources.repoRoot,
|
|
2005
|
+
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2006
|
+
fallbackCommands: [],
|
|
2007
|
+
});
|
|
1820
2008
|
const behaviorShellCommands = buildVerifyShellCommands({
|
|
1821
2009
|
repoRoot: sources.repoRoot,
|
|
1822
2010
|
commands: behaviorVerifyCommands.commands,
|
|
@@ -1825,16 +2013,28 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1825
2013
|
const staticVerifyEvidence = buildVerifyEvidence({
|
|
1826
2014
|
phase: "intermediate",
|
|
1827
2015
|
quota: strategy.intermediateQuota ?? "full",
|
|
1828
|
-
commandSource:
|
|
1829
|
-
commands:
|
|
2016
|
+
commandSource: partitionedStaticVerifyCommands.static.commandSource,
|
|
2017
|
+
commands: partitionedStaticVerifyCommands.static.commands,
|
|
1830
2018
|
fallbackCommands: staticFallbackCommands,
|
|
2019
|
+
commandTexts: staticShellCommands,
|
|
1831
2020
|
});
|
|
2021
|
+
const lintVerifyEvidence = lintShellCommands.length > 0
|
|
2022
|
+
? buildVerifyEvidence({
|
|
2023
|
+
phase: "intermediate",
|
|
2024
|
+
quota: strategy.intermediateQuota ?? "full",
|
|
2025
|
+
commandSource: partitionedStaticVerifyCommands.lint.commandSource,
|
|
2026
|
+
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2027
|
+
fallbackCommands: [],
|
|
2028
|
+
commandTexts: lintShellCommands,
|
|
2029
|
+
})
|
|
2030
|
+
: undefined;
|
|
1832
2031
|
const behaviorVerifyEvidence = buildVerifyEvidence({
|
|
1833
2032
|
phase: "final",
|
|
1834
2033
|
quota: "full",
|
|
1835
2034
|
commandSource: behaviorVerifyCommands.commandSource,
|
|
1836
2035
|
commands: behaviorVerifyCommands.commands,
|
|
1837
2036
|
fallbackCommands: behaviorFallbackCommands,
|
|
2037
|
+
commandTexts: behaviorShellCommands,
|
|
1838
2038
|
finalFullRequired: true,
|
|
1839
2039
|
});
|
|
1840
2040
|
const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
|
|
@@ -1897,6 +2097,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1897
2097
|
subtask_prompt: [
|
|
1898
2098
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
1899
2099
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2100
|
+
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
1900
2101
|
"Read-only: do not modify repository files.",
|
|
1901
2102
|
sourceContext,
|
|
1902
2103
|
].join("\n\n"),
|
|
@@ -1921,6 +2122,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1921
2122
|
"Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
|
|
1922
2123
|
"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
2124
|
"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.",
|
|
2125
|
+
"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
2126
|
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
|
|
1925
2127
|
requirementCoverageInstruction,
|
|
1926
2128
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
@@ -2042,16 +2244,47 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2042
2244
|
: ["native", "browser-intercept", "request-adapter", "not-needed"],
|
|
2043
2245
|
artifactName: "frontend-implementation-contract.json",
|
|
2044
2246
|
outputDir: "contracts",
|
|
2247
|
+
requireSourceFreshness: true,
|
|
2248
|
+
implementationWriteSet: implementPaths.writeSet,
|
|
2045
2249
|
openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence.normativePaths ?? [],
|
|
2046
2250
|
},
|
|
2047
2251
|
cwd: ".",
|
|
2048
2252
|
timeoutMs: 60000,
|
|
2049
2253
|
},
|
|
2050
2254
|
},
|
|
2255
|
+
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
2256
|
+
? [
|
|
2257
|
+
{
|
|
2258
|
+
id: "frontend-lint-baseline-shell",
|
|
2259
|
+
depends_on: ["frontend-prewrite-gate-shell"],
|
|
2260
|
+
role: "verifier",
|
|
2261
|
+
executor: "shell",
|
|
2262
|
+
complexity: "LOW",
|
|
2263
|
+
writePolicy: "read-only",
|
|
2264
|
+
allowedPaths: readOnlyPaths,
|
|
2265
|
+
forbiddenPaths,
|
|
2266
|
+
outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
|
|
2267
|
+
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.",
|
|
2268
|
+
shell: {
|
|
2269
|
+
commands: lintShellCommands,
|
|
2270
|
+
frontendLintBaseline: {
|
|
2271
|
+
schemaVersion: 1,
|
|
2272
|
+
lintCommands: lintShellCommands,
|
|
2273
|
+
lintEvidence: lintVerifyEvidence,
|
|
2274
|
+
},
|
|
2275
|
+
cwd: ".",
|
|
2276
|
+
timeoutMs: 300000,
|
|
2277
|
+
},
|
|
2278
|
+
},
|
|
2279
|
+
]
|
|
2280
|
+
: []),
|
|
2051
2281
|
{
|
|
2052
2282
|
id: implementId,
|
|
2053
2283
|
depends_on: [
|
|
2054
2284
|
"frontend-prewrite-gate-shell",
|
|
2285
|
+
...(lintShellCommands.length > 0
|
|
2286
|
+
? ["frontend-lint-baseline-shell"]
|
|
2287
|
+
: []),
|
|
2055
2288
|
"frontend-plan-revision-pi",
|
|
2056
2289
|
"frontend-final-design-review-pi",
|
|
2057
2290
|
"frontend-plan-pi",
|
|
@@ -2098,11 +2331,17 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2098
2331
|
frontendVerificationBundle: {
|
|
2099
2332
|
schemaVersion: 1,
|
|
2100
2333
|
mockCommands: mockShellCommands,
|
|
2334
|
+
lintCommands: lintShellCommands,
|
|
2101
2335
|
staticCommands: staticShellCommands,
|
|
2102
2336
|
behaviorCommands: behaviorShellCommands,
|
|
2103
2337
|
mockEvidence: mockVerifyEvidence,
|
|
2338
|
+
lintEvidence: lintVerifyEvidence,
|
|
2104
2339
|
staticEvidence: staticVerifyEvidence,
|
|
2105
2340
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
2341
|
+
lintBaselineNodeId: lintShellCommands.length > 0
|
|
2342
|
+
? "frontend-lint-baseline-shell"
|
|
2343
|
+
: undefined,
|
|
2344
|
+
writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
|
|
2106
2345
|
mode: "initial",
|
|
2107
2346
|
},
|
|
2108
2347
|
cwd: ".",
|
|
@@ -2151,11 +2390,19 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2151
2390
|
frontendVerificationBundle: {
|
|
2152
2391
|
schemaVersion: 1,
|
|
2153
2392
|
mockCommands: mockShellCommands,
|
|
2393
|
+
lintCommands: lintShellCommands,
|
|
2154
2394
|
staticCommands: staticShellCommands,
|
|
2155
2395
|
behaviorCommands: behaviorShellCommands,
|
|
2156
2396
|
mockEvidence: mockVerifyEvidence,
|
|
2397
|
+
lintEvidence: lintVerifyEvidence,
|
|
2157
2398
|
staticEvidence: staticVerifyEvidence,
|
|
2158
2399
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
2400
|
+
lintBaselineNodeId: lintShellCommands.length > 0
|
|
2401
|
+
? "frontend-lint-baseline-shell"
|
|
2402
|
+
: undefined,
|
|
2403
|
+
writerNodeIds: lintShellCommands.length > 0
|
|
2404
|
+
? [implementId, "frontend-repair-pi"]
|
|
2405
|
+
: [],
|
|
2159
2406
|
mode: "repair",
|
|
2160
2407
|
},
|
|
2161
2408
|
cwd: ".",
|
|
@@ -2177,11 +2424,11 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2177
2424
|
writePolicy: "read-only",
|
|
2178
2425
|
allowedPaths: readOnlyPaths,
|
|
2179
2426
|
forbiddenPaths,
|
|
2180
|
-
outputContract: "Canonical frontend review context containing validated contract, effective verification trace, repair assessment, and actual worktree diff.",
|
|
2427
|
+
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, repair assessment, and actual worktree diff.",
|
|
2181
2428
|
subtask_prompt: "Capture the actual diff and bind it to the effective initial-or-post-repair verification evidence for final review.",
|
|
2182
2429
|
shell: {
|
|
2183
2430
|
commands: [],
|
|
2184
|
-
frontendReviewContext: { schemaVersion: 1 },
|
|
2431
|
+
frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
|
|
2185
2432
|
cwd: ".",
|
|
2186
2433
|
timeoutMs: 120000,
|
|
2187
2434
|
},
|
|
@@ -2213,7 +2460,8 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2213
2460
|
"Review the frontend implementation and verification evidence.",
|
|
2214
2461
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2215
2462
|
"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.",
|
|
2463
|
+
"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.",
|
|
2464
|
+
"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
2465
|
"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
2466
|
"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
2467
|
"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 +2519,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2271
2519
|
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
2520
|
subtask_prompt: [
|
|
2273
2521
|
"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.",
|
|
2522
|
+
"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
2523
|
`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
2524
|
`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
2525
|
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
@@ -2801,12 +3049,18 @@ function buildBackendTestSemanticReviewNode(sources, options = {}) {
|
|
|
2801
3049
|
}
|
|
2802
3050
|
function collectBackendTestShellEnvAllowlist(sources) {
|
|
2803
3051
|
const names = new Set();
|
|
2804
|
-
|
|
3052
|
+
const collectAssignments = (text) => {
|
|
2805
3053
|
const assignmentPattern = /(?:^|[\s;&|])([A-Z_][A-Z0-9_]*)\s*=/g;
|
|
2806
|
-
for (const match of
|
|
3054
|
+
for (const match of text.matchAll(assignmentPattern)) {
|
|
2807
3055
|
if (match[1])
|
|
2808
3056
|
names.add(match[1]);
|
|
2809
3057
|
}
|
|
3058
|
+
};
|
|
3059
|
+
for (const constraint of sources.taskConfig.hardConstraints) {
|
|
3060
|
+
collectAssignments(constraint);
|
|
3061
|
+
}
|
|
3062
|
+
for (const verify of sources.taskConfig.verifyCommands) {
|
|
3063
|
+
collectAssignments(verify.command);
|
|
2810
3064
|
}
|
|
2811
3065
|
return [...names].sort();
|
|
2812
3066
|
}
|
|
@@ -3051,7 +3305,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3051
3305
|
"Read the upstream environment report. Generate a Markdown-first backend test strategy and cases under testcase/md/**.",
|
|
3052
3306
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
3053
3307
|
"Create testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.",
|
|
3054
|
-
"Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN
|
|
3308
|
+
"Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.",
|
|
3055
3309
|
"Place steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
3056
3310
|
"In `自动化映射`, record the planned script path and pytest function name when known. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3057
3311
|
intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
|
|
@@ -3065,7 +3319,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3065
3319
|
writeSet: ["testcase/md/**"], allowedPaths: ["testcase/md/**"], forbiddenPaths: forbidden,
|
|
3066
3320
|
outputContract: "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
|
|
3067
3321
|
subtask_prompt: [
|
|
3068
|
-
"Independently review generated Markdown cases against
|
|
3322
|
+
"Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
|
|
3069
3323
|
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, and missing script/function mapping where it can be derived.",
|
|
3070
3324
|
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, fix mappings/expectations, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations exact. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
3071
3325
|
"Read only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
@@ -3073,7 +3327,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3073
3327
|
"For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
3074
3328
|
].join("\n\n"),
|
|
3075
3329
|
};
|
|
3076
|
-
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "
|
|
3330
|
+
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for missing/duplicate IDs, missing core sections (preconditions, steps, expected results), AC coverage, executable steps, assertable results or placeholders. Do not validate source-reference existence. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected so downstream pytest/report nodes cannot consume them.", "Run-owned reports/backend-md-case-validation.md with PASS/FAIL advisory findings; downstream execution continues.");
|
|
3077
3331
|
const generatePytest = {
|
|
3078
3332
|
id: "generate-backend-pytest-pi", depends_on: [validateCases.id], role: "implementer",
|
|
3079
3333
|
executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive",
|
|
@@ -3081,20 +3335,23 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3081
3335
|
allowedPaths: Array.from(new Set([...ro, "testcase/**"])), forbiddenPaths: forbidden,
|
|
3082
3336
|
outputContract: "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring; no JSON and no pytest execution.",
|
|
3083
3337
|
subtask_prompt: [
|
|
3084
|
-
"Convert
|
|
3085
|
-
"Ensure every final Markdown Case ID appears in at least one real pytest test function or pytest test class method region, preferably as `test_BE_<MODULE>_<NNN>_<description>` and in that function/method docstring. Module-level functions and class-based pytest methods are both supported. Multiple test functions may cover one Case ID; assertions come only from 预期结果/Expected Results and setup comes only from
|
|
3338
|
+
"Convert testcase/md/** to pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3339
|
+
"Ensure every final Markdown Case ID appears in at least one real pytest test function or pytest test class method region, preferably as `test_BE_<MODULE>_<NNN>_<description>` and in that function/method docstring. Module-level functions and class-based pytest methods are both supported. Multiple test functions may cover one Case ID; assertions come only from 预期结果/Expected Results and setup comes only from 前置条件 plus any optional 测试数据/自动化映射 or their legacy English aliases.",
|
|
3340
|
+
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
|
|
3341
|
+
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
3342
|
+
"Before logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.",
|
|
3086
3343
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
|
|
3087
3344
|
].join("\n\n"),
|
|
3088
3345
|
};
|
|
3089
|
-
const traceability = shellNode("backend-test-traceability-gate-shell", [generatePytest.id], "markdown-traceability", "
|
|
3346
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [generatePytest.id], "markdown-traceability", "Record advisory findings when a real Markdown case heading has no associated pytest test function or class method in the script explicitly mapped by that Markdown case, or when a mapped HTTP test script lacks request parameters logging, response result logging, recursive redaction or bounded truncation evidence. Accept exact Case IDs in the function/method name or its decorator/body/docstring region. Do not scan unrelated test_*.py files and do not block pytest execution.", "Run-owned reports/backend-test-traceability.md with PASS/FAIL advisory findings for Markdown Case to mapped pytest script/symbol coverage.");
|
|
3090
3347
|
const pytestCommand = [
|
|
3091
3348
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3092
|
-
'
|
|
3093
|
-
"STATUS=$?", 'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
3094
|
-
'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.junit.xml" ]; then exit 0; fi',
|
|
3095
|
-
'exit "${STATUS}"',
|
|
3349
|
+
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3096
3350
|
].join("; ");
|
|
3097
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "
|
|
3351
|
+
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);
|
|
3352
|
+
if (execute.shell) {
|
|
3353
|
+
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3354
|
+
}
|
|
3098
3355
|
const canWriteReport = taskAllowsBackendTestReportWrite(sources);
|
|
3099
3356
|
const report = {
|
|
3100
3357
|
id: "backend-test-report-and-l5-pi", depends_on: [execute.id], role: "closeout", executor: "pi", complexity: "MED",
|
|
@@ -3104,7 +3361,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3104
3361
|
forbiddenPaths: forbidden,
|
|
3105
3362
|
outputContract: canWriteReport ? "Final Markdown report and L-5 conclusion under docs/test-reports/**; no JSON." : "Final Markdown report and L-5 conclusion in assistant output; no JSON or writes.",
|
|
3106
3363
|
subtask_prompt: [
|
|
3107
|
-
"Generate the final Markdown report from upstream facts and run-owned environment, case-validation, traceability, JUnit and HTML evidence. Do not emit JSON.",
|
|
3364
|
+
"Generate the final Markdown report from upstream facts and run-owned environment, advisory case-validation, advisory traceability, JUnit and HTML evidence. Do not emit JSON.",
|
|
3365
|
+
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion.",
|
|
3366
|
+
"Always state the exact PASS/FAIL status and findings from nodes 4 and 6. Their FAIL status does not block pytest, but it must remain visible as a quality/traceability risk and must never be rewritten as PASS.",
|
|
3108
3367
|
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage/stability availability, and L-5 READY/NOT READY.",
|
|
3109
3368
|
"Never override Shell/JUnit facts. One run cannot prove FlakyTest. Missing coverage/stability is Unavailable. L-5 requires pass=100%, AC=100%, automation>=90%, stability>=95% n>=5, line>=80%, branch>=70%, skipped=0 and no blocking Critical risk.",
|
|
3110
3369
|
canWriteReport ? "Write only under docs/test-reports/**." : "Keep the full report in assistant output.",
|
|
@@ -3118,9 +3377,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3118
3377
|
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
3119
3378
|
globalConstraints: [
|
|
3120
3379
|
...taskConfig.hardConstraints, ...STANDARD_GLOBAL_CONSTRAINTS,
|
|
3121
|
-
"backend-test-dag uses exactly 8 real top-level tasks and executes pytest exactly once.",
|
|
3380
|
+
"backend-test-dag uses exactly 8 real top-level tasks and executes pytest exactly once over only the safe scripts explicitly mapped by final Markdown cases.",
|
|
3122
3381
|
"Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
|
|
3123
|
-
"Environment, Markdown validation, traceability, JUnit, HTML and execution facts are deterministic
|
|
3382
|
+
"Environment, advisory Markdown validation, advisory traceability, JUnit, HTML and execution facts are deterministic evidence. Nodes 4 and 6 record findings without blocking nodes 5, 7 or 8.",
|
|
3124
3383
|
"Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
|
|
3125
3384
|
"Functional case IDs use BE-<MODULE>-<NNN>; production code/config, skip/xfail, repair and rerun are forbidden.",
|
|
3126
3385
|
],
|
|
@@ -3164,51 +3423,52 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3164
3423
|
const evidenceRoot = "testcase/frontend/evidence";
|
|
3165
3424
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
3166
3425
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
3426
|
+
const checklistScript = [
|
|
3427
|
+
"const fs=require('fs'),path=require('path');",
|
|
3428
|
+
"const root='testcase/frontend/cases';",
|
|
3429
|
+
"const draft=path.join(root,'manifest.draft.json');",
|
|
3430
|
+
"const final=path.join(root,'manifest.json');",
|
|
3431
|
+
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
3432
|
+
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3433
|
+
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3434
|
+
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3435
|
+
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3436
|
+
"const issues=[];",
|
|
3437
|
+
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3438
|
+
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3439
|
+
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;", ,
|
|
3440
|
+
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3441
|
+
"for(const c of manifest.cases){",
|
|
3442
|
+
" const id=c&&c.caseId||'?';",
|
|
3443
|
+
" 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-*'});",
|
|
3444
|
+
" 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'});",
|
|
3445
|
+
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3446
|
+
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3447
|
+
" 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'});",
|
|
3448
|
+
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3449
|
+
" 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'});", ,
|
|
3450
|
+
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3451
|
+
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3452
|
+
" 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)'});",
|
|
3453
|
+
" else {",
|
|
3454
|
+
" for(const ac of c.acIds){",
|
|
3455
|
+
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3456
|
+
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3457
|
+
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3458
|
+
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3459
|
+
" }",
|
|
3460
|
+
" }",
|
|
3461
|
+
"}",
|
|
3462
|
+
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3463
|
+
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
3464
|
+
].join("");
|
|
3465
|
+
// Pass generated JavaScript as base64 rather than embedding it inside a
|
|
3466
|
+
// shell-quoted `node -e` argument. Git Bash otherwise consumes backslashes
|
|
3467
|
+
// such as \\s/\\d and interprets Markdown backticks before Node sees them.
|
|
3468
|
+
const checklistScriptBase64 = Buffer.from(checklistScript, "utf8").toString("base64");
|
|
3167
3469
|
const checklistValidation = [
|
|
3168
|
-
"node -e",
|
|
3169
|
-
|
|
3170
|
-
"const fs=require('fs'),path=require('path');",
|
|
3171
|
-
"const root='testcase/frontend/cases';",
|
|
3172
|
-
"const draft=path.join(root,'manifest.draft.json');",
|
|
3173
|
-
"const final=path.join(root,'manifest.json');",
|
|
3174
|
-
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
3175
|
-
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3176
|
-
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3177
|
-
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3178
|
-
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3179
|
-
"const issues=[];",
|
|
3180
|
-
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3181
|
-
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3182
|
-
"const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
|
|
3183
|
-
"const barePwRe=/(?:^|[\\s\"'(])(?:npx\\s+playwright\\b|playwright\\s+test\\b|from\\s+['\"]@playwright\\/|require\\(['\"]@playwright\\/|import\\s+.*@playwright\\/|(?<![\\w-])playwright(?!-cli)\\b)/i;",
|
|
3184
|
-
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
3185
|
-
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3186
|
-
"for(const c of manifest.cases){",
|
|
3187
|
-
" const id=c&&c.caseId||'?';",
|
|
3188
|
-
" 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-*'});",
|
|
3189
|
-
" 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'});",
|
|
3190
|
-
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3191
|
-
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3192
|
-
" 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'});",
|
|
3193
|
-
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3194
|
-
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
|
|
3195
|
-
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3196
|
-
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3197
|
-
" if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
|
|
3198
|
-
" 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'});",
|
|
3199
|
-
" 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)'});",
|
|
3200
|
-
" else {",
|
|
3201
|
-
" for(const ac of c.acIds){",
|
|
3202
|
-
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3203
|
-
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3204
|
-
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3205
|
-
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3206
|
-
" }",
|
|
3207
|
-
" }",
|
|
3208
|
-
"}",
|
|
3209
|
-
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3210
|
-
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
3211
|
-
].join("")),
|
|
3470
|
+
"node -e \"eval(Buffer.from(process.argv[1],'base64').toString('utf8'))\"",
|
|
3471
|
+
checklistScriptBase64,
|
|
3212
3472
|
].join(" ");
|
|
3213
3473
|
const manifestValidation = [
|
|
3214
3474
|
"node -e",
|
|
@@ -3249,6 +3509,23 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3249
3509
|
// Heal malformed/missing case evidence to blocked; only unsafe evidenceDir hard-fails.
|
|
3250
3510
|
const evidenceValidation = buildFrontendCaseEvidenceValidateShellSnippet();
|
|
3251
3511
|
const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
|
|
3512
|
+
const frontendCaseQualityAdvisory = [
|
|
3513
|
+
"node -e",
|
|
3514
|
+
JSON.stringify([
|
|
3515
|
+
"const fs=require('fs'),path=require('path');",
|
|
3516
|
+
"const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
|
|
3517
|
+
"const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
|
|
3518
|
+
"const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
|
|
3519
|
+
"const reports='testcase/frontend/reports'; fs.mkdirSync(reports,{recursive:true});",
|
|
3520
|
+
"const esc=v=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\\\"','"');",
|
|
3521
|
+
"const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
|
|
3522
|
+
"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):['- 未发现建议项。']),''];",
|
|
3523
|
+
"fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.md'),md.join('\\n'));",
|
|
3524
|
+
"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>';",
|
|
3525
|
+
"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>';",
|
|
3526
|
+
"fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.html'),html); console.log('frontend-test advisory report findings='+findings.length);",
|
|
3527
|
+
].join("")),
|
|
3528
|
+
].join(" ");
|
|
3252
3529
|
const tasks = [
|
|
3253
3530
|
{
|
|
3254
3531
|
id: "retrieve-frontend-test-context-pi",
|
|
@@ -3402,8 +3679,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3402
3679
|
writePolicy: "read-only",
|
|
3403
3680
|
allowedPaths: [...ragWriteSet, ...casesWriteSet],
|
|
3404
3681
|
forbiddenPaths: forbidden,
|
|
3405
|
-
outputContract: "Mechanical checklist:
|
|
3406
|
-
subtask_prompt: "Scan generated cases/manifest
|
|
3682
|
+
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.",
|
|
3683
|
+
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.",
|
|
3407
3684
|
shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
|
|
3408
3685
|
}, {
|
|
3409
3686
|
id: "materialize-frontend-case-manifest-shell",
|
|
@@ -3537,6 +3814,20 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3537
3814
|
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).",
|
|
3538
3815
|
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.",
|
|
3539
3816
|
});
|
|
3817
|
+
tasks.push({
|
|
3818
|
+
id: "frontend-test-quality-report-html-shell",
|
|
3819
|
+
depends_on: ["frontend-test-retrospect-pi"],
|
|
3820
|
+
role: "verifier",
|
|
3821
|
+
executor: "shell",
|
|
3822
|
+
complexity: "LOW",
|
|
3823
|
+
writePolicy: "exclusive",
|
|
3824
|
+
writeSet: ["testcase/frontend/reports/**"],
|
|
3825
|
+
allowedPaths: ["testcase/frontend/**"],
|
|
3826
|
+
forbiddenPaths: forbidden,
|
|
3827
|
+
outputContract: "Write a user-readable HTML advisory report after the final Markdown retrospective; findings never block the workflow.",
|
|
3828
|
+
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.",
|
|
3829
|
+
shell: { commands: [frontendCaseQualityAdvisory], cwd: ".", timeoutMs: 120000 },
|
|
3830
|
+
});
|
|
3540
3831
|
const globalConstraints = [
|
|
3541
3832
|
...sources.taskConfig.hardConstraints,
|
|
3542
3833
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
@@ -4559,7 +4850,7 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
4559
4850
|
const taskContractBinding = await resolveTaskContractBindingForGenerate(sources);
|
|
4560
4851
|
let spec;
|
|
4561
4852
|
if (template === "frontend-implementation") {
|
|
4562
|
-
spec = buildFrontendHybridDagFromTask(sources);
|
|
4853
|
+
spec = await buildFrontendHybridDagFromTask(sources);
|
|
4563
4854
|
}
|
|
4564
4855
|
else if (template === "frontend-test-dag")
|
|
4565
4856
|
spec = buildFrontendTestHybridDag(sources);
|