@tea-agent/loop-agent 0.25.4 → 0.25.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -0
- package/CHANGELOG.md +60 -0
- package/dist/commands/client-recovery.js +209 -62
- package/dist/commands/init.js +68 -129
- package/dist/executors/dag-pi-executor.js +80 -15
- package/dist/executors/model-routing.js +1 -1
- package/dist/executors/shell-executor.js +127 -0
- package/dist/executors/shell-write-guard.js +21 -7
- package/dist/worker/console/repo-fingerprint.js +7 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +1281 -0
- package/dist/workflows/dag/backend-test-case-manifest.js +59 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +236 -16
- package/dist/workflows/dag/convergence/controller.js +134 -9
- package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
- package/dist/workflows/dag/init-hybrid.js +270 -80
- package/dist/workflows/dag/node-execution.js +64 -11
- package/dist/workflows/dag/prompt.js +118 -4
- package/dist/workflows/dag/retry-policy.js +5 -4
- package/dist/workflows/dag/scheduler.js +32 -5
- package/dist/workflows/dag/types.js +7 -4
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/architecture/dag-execution.md +7 -4
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/init-surface.manifest.json +3 -1
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/backend-test-dag.json +41 -14
- package/docs/templates/frontend-test-dag.json +32 -2
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/init-managed-agents.md +137 -0
- package/examples/decision-gate-agent-dag.json +1 -1
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +5 -4
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/model-routing.md +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -4,7 +4,6 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { copyDir } from "../shared/copy-dir.js";
|
|
8
7
|
import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
|
|
9
8
|
import { loadHarnessManifest } from "../governance/harness.js";
|
|
10
9
|
import { OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, applyPiRetryMerge, buildOpenCodeTransientRetryPluginSource, inspectPiRetryConfig, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
|
|
@@ -119,6 +118,12 @@ const COMPAT_PROMPTS = {
|
|
|
119
118
|
].join("\n"),
|
|
120
119
|
};
|
|
121
120
|
const GOVERNANCE_ROOT_TOKEN = "__LOOP_AGENT_GOVERNANCE_ROOT__";
|
|
121
|
+
const PROJECT_NAME_TOKEN = "__LOOP_AGENT_PROJECT_NAME__";
|
|
122
|
+
/** Package-shipped render source for AGENTS.md managed block; not projected to target repos. */
|
|
123
|
+
const MANAGED_AGENTS_TEMPLATE_PATH = "docs/templates/init-managed-agents.md";
|
|
124
|
+
/** Files under package assets that init must not copy into target projects. */
|
|
125
|
+
const PACKAGE_ONLY_SURFACE_FILES = new Set([MANAGED_AGENTS_TEMPLATE_PATH]);
|
|
126
|
+
let managedAgentsTemplateCache;
|
|
122
127
|
const INIT_CHECK_ENGINEERING_STRUCTURE_SH = `#!/usr/bin/env bash
|
|
123
128
|
set -euo pipefail
|
|
124
129
|
|
|
@@ -798,130 +803,28 @@ function mergeRecord(base, patch) {
|
|
|
798
803
|
...patch,
|
|
799
804
|
};
|
|
800
805
|
}
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
"| **允许** | 已发布 `loop-agent` / `agent-worker` CLI;只读 status/doctor/report/inspect/observe;准备 `source/*` 与 `task.json` 边界;human gate;shell 验证与 handoff。 |",
|
|
824
|
-
"| **禁止** | 绕过 CLI 用宿主 Edit/Write/ApplyPatch 直接改业务实现;CLI/DAG 失败后「救火改文件」;用聊天自述代替 shell 验证。 |",
|
|
825
|
-
"| **失败时** | `dag doctor` / `dag report` / `dag reconcile-run`(及适用 worker reconcile);修正任务源/`task.json`/DAG 后经 CLI 重跑。 |",
|
|
826
|
-
"| **实现写入** | 业务代码 **只** 经受治理 DAG writer(`implement-pi` / `repair-pi`)经 `dag run-task` / `run-dag`。 |",
|
|
827
|
-
"",
|
|
828
|
-
"**永远不要**:`loop-agent` / `agent-worker` 失败 ⇒ 主会话直接改仓库实现。",
|
|
829
|
-
"",
|
|
830
|
-
"### 自然语言入口路由",
|
|
831
|
-
"",
|
|
832
|
-
"| 用户表达 | 入口 | 执行动作 |",
|
|
833
|
-
"|---|---|---|",
|
|
834
|
-
"| loop-agent 初始化 / loop agent 初始化 / loop agent初始化 / 初始化 loop-agent | 初始化 | 完成确定性初始化闭环 |",
|
|
835
|
-
"| 初始化更新校验 / 检查初始化更新 / loop-agent 初始化更新校验 / loop agent初始化更新校验 | 更新校验 | 只读报告,不写入 |",
|
|
836
|
-
"| 初始化对齐 / 升级后对齐 / reconcile 初始化 / loop-agent 初始化对齐 | 升级对齐 | 自动应用确定性安全动作;活跃 DAG/Worker 或人工决策时零写入 |",
|
|
837
|
-
"| 初始化安全更新 / 应用初始化更新 / loop-agent 初始化安全更新 / loop agent初始化安全更新 | 安全更新 | 先 check-update,再只执行确定性安全动作 |",
|
|
838
|
-
"| loop-agent 帮我完成 / 帮我实现 / 帮我修复 / 帮我开发 <需求>;使用 loop-agent 完成 <X>;按 loop-agent 流程处理 <X> | 通用需求实现 | 先建任务源与 `task.json.allowedPaths` / `forbiddenPaths`,再 `new-task` → `dag run-task` → `dag validate` → `run-dag`;主会话不得直接修改业务实现 |",
|
|
839
|
-
"",
|
|
840
|
-
"**更新校验(只读)**:只读执行下面命令;**不得自动**执行 `apply-safe` 或模型合并。",
|
|
841
|
-
"",
|
|
842
|
-
"```bash",
|
|
843
|
-
"loop-agent init check-update --repo-root . --markdown",
|
|
844
|
-
"```",
|
|
845
|
-
"",
|
|
846
|
-
"**升级对齐**:`loop-agent init reconcile --repo-root .`;存在活跃 DAG/Worker、human decisions 时必须零写入。",
|
|
847
|
-
"",
|
|
848
|
-
"**安全更新**:先 check-update,再 `apply-safe`;surface 缺失才 `--bootstrap-surface`。完成后复查 `init doctor` / `inspect` / `docs audit` 与目标治理检查。",
|
|
849
|
-
"",
|
|
850
|
-
"```bash",
|
|
851
|
-
"loop-agent init check-update --repo-root . --markdown",
|
|
852
|
-
"loop-agent init update --repo-root . --apply-safe",
|
|
853
|
-
"```",
|
|
854
|
-
"",
|
|
855
|
-
"### 初始化闭环",
|
|
856
|
-
"",
|
|
857
|
-
`当用户要求初始化 loop-agent(“初始化 loop-agent”“loop agent 初始化”“loop agent初始化”“loop-agent 初始化”),或发现 \`README.md\` / \`${input.governanceRoot}/verification-matrix.md\` 仍停留在通用初始化状态时,模型应在同一轮工作中完成初始化闭环,不要把补全文档和验证矩阵留给用户交互。`,
|
|
858
|
-
"",
|
|
859
|
-
"1. 运行 `loop-agent init instructions --repo-root .`。",
|
|
860
|
-
"2. 运行 `loop-agent init --repo-root . --profile full --merge`。",
|
|
861
|
-
`3. 基于真实项目文件补全根 \`README.md\`、\`${input.governanceRoot}/verification-matrix.md\`,必要时适配 \`scripts/ci-tests.sh\`。`,
|
|
862
|
-
"4. 复查 `init doctor` / `inspect` / `docs audit` 与 quick verification。",
|
|
863
|
-
"",
|
|
864
|
-
"信息不足时写明尚未识别到的事实;不要留下泛化占位符。",
|
|
865
|
-
"",
|
|
866
|
-
"### 文档收敛",
|
|
867
|
-
"",
|
|
868
|
-
"完成实现和验证后,必须检查 `README.md`、`AGENTS.md`、`CHANGELOG.md`(如果目标项目维护)、治理文档、skills references、初始化模板和脚本说明是否仍与实际行为一致。只更新与本次变更相关的内容;如果决定不更新,应在交接里写明理由。",
|
|
869
|
-
"",
|
|
870
|
-
"### 开始顺序",
|
|
871
|
-
"",
|
|
872
|
-
`pwd → \`README.md\` → \`harness.json\` → \`${input.governanceRoot}/README.md\` → 实现类再读 principles/feature-workflow/verification-matrix;测试纪律读 harness-methodology-*;\`git status\`;最小基线验证。`,
|
|
873
|
-
"",
|
|
874
|
-
"### Agent DAG 路径",
|
|
875
|
-
"",
|
|
876
|
-
"```bash",
|
|
877
|
-
'loop-agent new-task <task-id> "任务标题"',
|
|
878
|
-
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
879
|
-
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
880
|
-
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
881
|
-
"loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance",
|
|
882
|
-
"loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd .",
|
|
883
|
-
"```",
|
|
884
|
-
"",
|
|
885
|
-
"`source/需求.md` 与 `source/执行约束.md` 必需;写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。",
|
|
886
|
-
"",
|
|
887
|
-
"凡是影响项目公共契约、执行入口、交付流水线、自动化/治理、数据模型、安全或权限模型、跨模块行为、用户可见工作流的改动,都必须在编辑实现文件前先创建任务、写好两个 source 文件、生成 DAG,并审查 DAG/writeSet。",
|
|
888
|
-
"",
|
|
889
|
-
"### 任务类型路由(taskKind)",
|
|
890
|
-
"",
|
|
891
|
-
'- 用户明确提出后端测试、接口/API 测试、pytest,或语境明确为后端的自动化测试时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。',
|
|
892
|
-
"- `backend-test` 是 `taskKind`,不是 `--profile` 的可选值;运行 `dag run-task` 时继续使用 `--profile auto`。",
|
|
893
|
-
"- 仅出现“自动化测试”且无法判断前后端时,先阅读任务源与目标项目技术栈再决定,禁止无条件路由到 `backend-test`。",
|
|
894
|
-
'- 用户提示词明确是前端实现需求(例如前端页面、UI、组件或交互开发)时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"frontend-implementation"`,不得保留默认 `standard`。',
|
|
895
|
-
"- `frontend-implementation` 是 `taskKind`,不是 `--profile` 的可选值;运行 `dag run-task` 时继续使用 `--profile auto`,也可按需显式选择 `minimal` / `standard` / `reviewed` / `supervised`,不要把业务模板名当作 profile。",
|
|
896
|
-
'- 前端自动化测试(浏览器/UI 自动化、Playwright、E2E)继续使用 `taskKind: "frontend-test"`,不得设置为 `frontend-implementation`。',
|
|
897
|
-
"",
|
|
898
|
-
"### 运行看板(只读)",
|
|
899
|
-
"",
|
|
900
|
-
"```bash",
|
|
901
|
-
"agent-worker console # 默认 repo=当前目录,port=8790",
|
|
902
|
-
"agent-worker console serve --repo . --port 8790 # 兼容入口,等价于上面裸入口",
|
|
903
|
-
"```",
|
|
904
|
-
"",
|
|
905
|
-
"浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。`agent-worker observe serve` 仅为兼容入口。",
|
|
906
|
-
"",
|
|
907
|
-
"### DAG 诊断与收口",
|
|
908
|
-
"",
|
|
909
|
-
"用 `loop-agent dag report --run-id <run-id> --markdown` 读 facts;失败/paused 用 `loop-agent dag doctor --run-id <run-id> --markdown`;生命周期对齐用 `loop-agent dag reconcile-run`。失败 run 用 `dag closeout-draft` 生成 failure handoff,不要写成成功 closeout。",
|
|
910
|
-
"",
|
|
911
|
-
"恢复:doctor/report → classify → reconcile/replan → CLI 重跑 → shell verify。**禁止**把主会话直接 Edit 业务代码当作恢复手段。",
|
|
912
|
-
"",
|
|
913
|
-
"### 运行态与验证",
|
|
914
|
-
"",
|
|
915
|
-
"- `.harness/tasks/`、`.harness/dag-runs/`、`.harness/runs/` 保存运行事实;已完成事实只读。",
|
|
916
|
-
"- `.agents/skills/` 为本地 skills;缺失时可回退 npm 包内置。",
|
|
917
|
-
`- 验证命令选择:\`${input.governanceRoot}/verification-matrix.md\`。常用:\`bash scripts/check-repo.sh\`、\`bash scripts/ci.sh\`、\`loop-agent inspect\`、\`loop-agent doctor\`、\`loop-agent docs audit\`。`,
|
|
918
|
-
"- `scripts/ci-tests.sh` 必须反映目标项目真实工具链。Windows 脚本用 Git Bash;仓库引用用 `/`。",
|
|
919
|
-
"",
|
|
920
|
-
"### 交接",
|
|
921
|
-
"",
|
|
922
|
-
`交接写清变更、原因、验证证据、影响面、风险与下一步;长期结论进入 \`${input.governanceRoot}/progress\`、reports、exec-plans、decisions。`,
|
|
923
|
-
MANAGED_BLOCK_END,
|
|
924
|
-
].join("\n");
|
|
806
|
+
async function loadManagedAgentsTemplate(assetRoot) {
|
|
807
|
+
if (managedAgentsTemplateCache !== undefined)
|
|
808
|
+
return managedAgentsTemplateCache;
|
|
809
|
+
const templatePath = path.join(assetRoot, MANAGED_AGENTS_TEMPLATE_PATH);
|
|
810
|
+
const raw = await readFile(templatePath, "utf-8");
|
|
811
|
+
// Prefer a START marker on its own line so header notes mentioning the token are ignored.
|
|
812
|
+
const lines = raw.split(/\r?\n/);
|
|
813
|
+
const startLine = lines.findIndex((line) => line.trim() === MANAGED_BLOCK_START);
|
|
814
|
+
const endLine = startLine >= 0
|
|
815
|
+
? lines.findIndex((line, index) => index > startLine && line.trim() === MANAGED_BLOCK_END)
|
|
816
|
+
: -1;
|
|
817
|
+
if (startLine < 0 || endLine < 0) {
|
|
818
|
+
throw new Error(`managed agents template missing LOOP_AGENT_INIT markers: ${MANAGED_AGENTS_TEMPLATE_PATH}`);
|
|
819
|
+
}
|
|
820
|
+
managedAgentsTemplateCache = lines.slice(startLine, endLine + 1).join("\n");
|
|
821
|
+
return managedAgentsTemplateCache;
|
|
822
|
+
}
|
|
823
|
+
async function buildManagedAgentsBlock(input) {
|
|
824
|
+
const template = await loadManagedAgentsTemplate(input.assetRoot);
|
|
825
|
+
return template
|
|
826
|
+
.replaceAll(PROJECT_NAME_TOKEN, input.projectName)
|
|
827
|
+
.replaceAll(GOVERNANCE_ROOT_TOKEN, input.governanceRoot);
|
|
925
828
|
}
|
|
926
829
|
function mergeManagedBlock(existing, block) {
|
|
927
830
|
const start = existing.indexOf(MANAGED_BLOCK_START);
|
|
@@ -1137,11 +1040,39 @@ async function copyFileIfMissing(input) {
|
|
|
1137
1040
|
async function copyDirMerge(input) {
|
|
1138
1041
|
const source = path.join(input.assetRoot, input.sourceRelativePath);
|
|
1139
1042
|
const target = path.join(input.repoRoot, input.targetRelativePath);
|
|
1140
|
-
|
|
1043
|
+
const sourcePrefix = `${input.sourceRelativePath.replaceAll(path.sep, "/").replace(/\/$/, "")}/`;
|
|
1044
|
+
await copyDirSkippingPackageOnly({
|
|
1045
|
+
source,
|
|
1046
|
+
target,
|
|
1047
|
+
packageRelativePrefix: sourcePrefix,
|
|
1048
|
+
});
|
|
1141
1049
|
input.written.push(input.targetRelativePath.endsWith("/")
|
|
1142
1050
|
? input.targetRelativePath
|
|
1143
1051
|
: `${input.targetRelativePath}/`);
|
|
1144
1052
|
}
|
|
1053
|
+
/** Like copyDir, but skips package-only assets that must not land in target projects. */
|
|
1054
|
+
async function copyDirSkippingPackageOnly(input) {
|
|
1055
|
+
await mkdir(input.target, { recursive: true });
|
|
1056
|
+
const entries = await readdir(input.source, { withFileTypes: true });
|
|
1057
|
+
for (const entry of entries) {
|
|
1058
|
+
const srcPath = path.join(input.source, entry.name);
|
|
1059
|
+
const destPath = path.join(input.target, entry.name);
|
|
1060
|
+
const packageRelative = `${input.packageRelativePrefix}${entry.name}`;
|
|
1061
|
+
if (entry.isDirectory()) {
|
|
1062
|
+
await copyDirSkippingPackageOnly({
|
|
1063
|
+
source: srcPath,
|
|
1064
|
+
target: destPath,
|
|
1065
|
+
packageRelativePrefix: `${packageRelative}/`,
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (PACKAGE_ONLY_SURFACE_FILES.has(packageRelative))
|
|
1070
|
+
continue;
|
|
1071
|
+
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
1072
|
+
await copyFile(srcPath, destPath);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1145
1076
|
async function ensureHarnessDirs(repoRoot, written) {
|
|
1146
1077
|
const dirs = [
|
|
1147
1078
|
".harness/prompts",
|
|
@@ -1289,7 +1220,10 @@ async function discoverCopiedSurfaceFiles(assetRoot) {
|
|
|
1289
1220
|
continue;
|
|
1290
1221
|
const files = await listRelativeFiles(rootPath);
|
|
1291
1222
|
for (const relativePath of files) {
|
|
1292
|
-
|
|
1223
|
+
const manifestPath = `${prefix}${relativePath}`;
|
|
1224
|
+
if (PACKAGE_ONLY_SURFACE_FILES.has(manifestPath))
|
|
1225
|
+
continue;
|
|
1226
|
+
discovered.push(manifestPath);
|
|
1293
1227
|
}
|
|
1294
1228
|
}
|
|
1295
1229
|
return discovered.sort();
|
|
@@ -1335,7 +1269,8 @@ async function buildDesiredSurfaceContent(input) {
|
|
|
1335
1269
|
}
|
|
1336
1270
|
if (manifestPath === "AGENTS.md") {
|
|
1337
1271
|
return {
|
|
1338
|
-
content: buildManagedAgentsBlock({
|
|
1272
|
+
content: await buildManagedAgentsBlock({
|
|
1273
|
+
assetRoot: input.assetRoot,
|
|
1339
1274
|
projectName: input.projectName,
|
|
1340
1275
|
governanceRoot: input.governanceRoot,
|
|
1341
1276
|
}),
|
|
@@ -2427,7 +2362,11 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2427
2362
|
await writeText({
|
|
2428
2363
|
repoRoot,
|
|
2429
2364
|
relativePath: "AGENTS.md",
|
|
2430
|
-
content: mergeManagedBlock(existingAgents, buildManagedAgentsBlock({
|
|
2365
|
+
content: mergeManagedBlock(existingAgents, await buildManagedAgentsBlock({
|
|
2366
|
+
assetRoot,
|
|
2367
|
+
projectName,
|
|
2368
|
+
governanceRoot,
|
|
2369
|
+
})),
|
|
2431
2370
|
merge: true,
|
|
2432
2371
|
written,
|
|
2433
2372
|
skipped,
|
|
@@ -4,6 +4,7 @@ import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastruct
|
|
|
4
4
|
import { executePiStep, } from "./pi-executor.js";
|
|
5
5
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
6
6
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
7
|
+
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
7
8
|
export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
8
9
|
export const DAG_PI_WRITE_TOOLS = [
|
|
9
10
|
"read",
|
|
@@ -15,6 +16,7 @@ export const DAG_PI_WRITE_TOOLS = [
|
|
|
15
16
|
"ls",
|
|
16
17
|
];
|
|
17
18
|
export const DEFAULT_DAG_PI_PROVIDER = "wizard-local";
|
|
19
|
+
const WRITER_OUTCOME_PROTOCOL_LINE = "IMPLEMENTATION_OUTCOME:";
|
|
18
20
|
export const DAG_PI_MODEL_PROVIDERS = {
|
|
19
21
|
"gpt-5.3-codex-spark": "wizard-local",
|
|
20
22
|
"gpt-5.5": "wizard-local",
|
|
@@ -266,9 +268,14 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
266
268
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
267
269
|
beforePathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, snapshotGitStatusPorcelain(beforeStatus));
|
|
268
270
|
}
|
|
269
|
-
catch {
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
catch (error) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
stdout: "",
|
|
275
|
+
stderr: `writer Git baseline unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
276
|
+
failureCategory: "write-guard",
|
|
277
|
+
durationMs: Date.now() - started,
|
|
278
|
+
};
|
|
272
279
|
}
|
|
273
280
|
}
|
|
274
281
|
const reportActivity = input.reportActivity;
|
|
@@ -319,7 +326,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
319
326
|
persona,
|
|
320
327
|
step,
|
|
321
328
|
});
|
|
322
|
-
const mapped = mapPiResultToDagNodeResult(result, input.task.
|
|
329
|
+
const mapped = mapPiResultToDagNodeResult(result, input.task.writerOutcomePolicy
|
|
330
|
+
? WRITER_OUTCOME_PROTOCOL_LINE
|
|
331
|
+
: input.task.firstProtocolLine);
|
|
323
332
|
if (!isWriteTask) {
|
|
324
333
|
return mapped;
|
|
325
334
|
}
|
|
@@ -399,38 +408,94 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
399
408
|
};
|
|
400
409
|
}
|
|
401
410
|
export function validateWriterImplementationOutcome(text, changedFiles) {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
.find(Boolean);
|
|
406
|
-
const match = firstLine?.match(/^IMPLEMENTATION_OUTCOME: (changed|already-satisfied|blocked)$/);
|
|
407
|
-
if (!match) {
|
|
411
|
+
const parsed = parseWriterImplementationOutcome(text);
|
|
412
|
+
const diagnostics = writerOutcomeDiagnostics(text, parsed, changedFiles.length);
|
|
413
|
+
if (parsed.kind !== "valid") {
|
|
408
414
|
return {
|
|
409
415
|
ok: false,
|
|
410
|
-
reason:
|
|
416
|
+
reason: `writer outcome validation failed: ${parsed.kind} outcome; ${diagnostics}; expected IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked`,
|
|
411
417
|
};
|
|
412
418
|
}
|
|
413
|
-
const outcome =
|
|
419
|
+
const outcome = parsed.outcome;
|
|
414
420
|
if (outcome === "blocked") {
|
|
415
421
|
return {
|
|
416
422
|
ok: false,
|
|
417
|
-
reason:
|
|
423
|
+
reason: `writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully; ${diagnostics}`,
|
|
418
424
|
};
|
|
419
425
|
}
|
|
420
426
|
if (outcome === "changed" && changedFiles.length === 0) {
|
|
421
427
|
return {
|
|
422
428
|
ok: false,
|
|
423
|
-
reason:
|
|
429
|
+
reason: `writer outcome validation failed: changed outcome has an empty diff; ${diagnostics}`,
|
|
424
430
|
};
|
|
425
431
|
}
|
|
426
432
|
if (outcome === "already-satisfied" && changedFiles.length > 0) {
|
|
427
433
|
return {
|
|
428
434
|
ok: false,
|
|
429
|
-
reason:
|
|
435
|
+
reason: `writer outcome validation failed: already-satisfied outcome has a non-empty diff; ${diagnostics}`,
|
|
430
436
|
};
|
|
431
437
|
}
|
|
432
438
|
return { ok: true, outcome };
|
|
433
439
|
}
|
|
440
|
+
function parseWriterImplementationOutcome(text) {
|
|
441
|
+
const lines = text.split(/\r?\n/);
|
|
442
|
+
const candidates = [];
|
|
443
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
444
|
+
const nextLine = lines[lineIndex + 1];
|
|
445
|
+
const normalized = normalizeProtocolLine(line, WRITER_OUTCOME_PROTOCOL_LINE, nextLine);
|
|
446
|
+
if (normalized === undefined)
|
|
447
|
+
continue;
|
|
448
|
+
const value = normalized
|
|
449
|
+
.slice(WRITER_OUTCOME_PROTOCOL_LINE.length)
|
|
450
|
+
.trim();
|
|
451
|
+
const outcome = isWriterImplementationOutcome(value)
|
|
452
|
+
? value
|
|
453
|
+
: undefined;
|
|
454
|
+
candidates.push({
|
|
455
|
+
lineIndex,
|
|
456
|
+
value,
|
|
457
|
+
outcome,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (candidates.length === 0) {
|
|
461
|
+
return { kind: "missing", candidates };
|
|
462
|
+
}
|
|
463
|
+
if (candidates.some((candidate) => candidate.outcome === undefined)) {
|
|
464
|
+
return { kind: "unknown", candidates };
|
|
465
|
+
}
|
|
466
|
+
const outcomes = new Set(candidates.map((candidate) => candidate.outcome));
|
|
467
|
+
if (outcomes.size !== 1) {
|
|
468
|
+
return { kind: "conflicting", candidates };
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
kind: "valid",
|
|
472
|
+
outcome: candidates[0].outcome,
|
|
473
|
+
candidates,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function isWriterImplementationOutcome(value) {
|
|
477
|
+
return (value === "changed" ||
|
|
478
|
+
value === "already-satisfied" ||
|
|
479
|
+
value === "blocked");
|
|
480
|
+
}
|
|
481
|
+
function writerOutcomeDiagnostics(text, parsed, changedFiles) {
|
|
482
|
+
const firstNonEmpty = text
|
|
483
|
+
.split(/\r?\n/)
|
|
484
|
+
.map((line) => line.trim())
|
|
485
|
+
.find(Boolean) ?? "(empty)";
|
|
486
|
+
const candidateSummary = parsed.candidates.length === 0
|
|
487
|
+
? "none"
|
|
488
|
+
: parsed.candidates
|
|
489
|
+
.slice(0, 4)
|
|
490
|
+
.map((candidate) => candidate.outcome
|
|
491
|
+
? `${candidate.outcome}@line${candidate.lineIndex + 1}`
|
|
492
|
+
: `unknown(${boundedWriterDiagnostic(candidate.value)})@line${candidate.lineIndex + 1}`)
|
|
493
|
+
.join(",");
|
|
494
|
+
return `firstNonEmpty=${boundedWriterDiagnostic(firstNonEmpty)}; candidates=${candidateSummary}; changedFiles=${changedFiles}`;
|
|
495
|
+
}
|
|
496
|
+
function boundedWriterDiagnostic(value) {
|
|
497
|
+
return JSON.stringify(truncateUtf8Preview(redactSecrets(value), 160));
|
|
498
|
+
}
|
|
434
499
|
export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
|
|
435
500
|
const assistantText = canonicalizeProtocolFirstLine(result.assistantText, firstProtocolLine);
|
|
436
501
|
return {
|
|
@@ -10,6 +10,7 @@ import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend
|
|
|
10
10
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
11
11
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
12
12
|
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
13
|
+
import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
|
|
13
14
|
import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
|
|
14
15
|
import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
|
|
15
16
|
import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
|
|
@@ -20,6 +21,7 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
20
21
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
21
22
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
22
23
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
24
|
+
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
23
25
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
24
26
|
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
25
27
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
@@ -372,6 +374,14 @@ async function readRequiredRunReport(reportsDir, filename) {
|
|
|
372
374
|
throw new Error(`missing required upstream report: reports/${filename}`);
|
|
373
375
|
}
|
|
374
376
|
}
|
|
377
|
+
async function readAdvisoryRunReport(reportsDir, filename, title) {
|
|
378
|
+
try {
|
|
379
|
+
return await readFile(path.join(reportsDir, filename), "utf8");
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return `# ${title}\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Missing advisory upstream report: reports/${filename}\n`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
375
385
|
async function executeBackendTestPipeline(input, meta) {
|
|
376
386
|
const pipeline = input.task.shell?.backendTestPipeline;
|
|
377
387
|
const started = Date.now();
|
|
@@ -423,6 +433,48 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
423
433
|
durationMs: Date.now() - started,
|
|
424
434
|
};
|
|
425
435
|
}
|
|
436
|
+
const sourceBinding = meta.spec.sourceBinding;
|
|
437
|
+
if (!sourceBinding) {
|
|
438
|
+
const coverage = "# Backend Test Case Coverage Analysis\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Coverage analysis requires spec.sourceBinding; node 4 and node 7 fail closed on the same task/source binding contract.\n";
|
|
439
|
+
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage);
|
|
440
|
+
outputs.push(`caseCoverage=${coveragePath}`, coverage);
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
stdout: outputs.join("\n\n"),
|
|
444
|
+
stderr: "backend-test markdown-cases requires spec.sourceBinding",
|
|
445
|
+
failureCategory: "invalid-output",
|
|
446
|
+
durationMs: Date.now() - started,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const normalizedBinding = {
|
|
450
|
+
taskId: sourceBinding.taskId,
|
|
451
|
+
requirementPath: sourceBinding.sources.find((s) => s.kind === "requirement")?.path ?? "source/requirement.md",
|
|
452
|
+
requirementSha256: sourceBinding.sources.find((s) => s.kind === "requirement")?.sha256 ?? "0".repeat(64),
|
|
453
|
+
referencePaths: sourceBinding.sources.filter((s) => s.kind === "reference").map((s) => s.path),
|
|
454
|
+
requirementIds: sourceBinding.requirementIds,
|
|
455
|
+
};
|
|
456
|
+
try {
|
|
457
|
+
const coverage = await analyzeBackendTestCaseCoverage({ workspaceRoot: input.cwd, sourceBinding: normalizedBinding });
|
|
458
|
+
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
|
|
459
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
460
|
+
await mkdir(contractsDir, { recursive: true });
|
|
461
|
+
const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
|
|
462
|
+
await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
|
|
463
|
+
outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
467
|
+
const coverage = `# Backend Test Case Coverage Analysis\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Coverage analysis crashed and is treated as an infrastructure failure: ${message}\n`;
|
|
468
|
+
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage);
|
|
469
|
+
outputs.push(`caseCoverage=${coveragePath}`, coverage);
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
stdout: outputs.join("\n\n"),
|
|
473
|
+
stderr: `backend-test coverage analysis crashed: ${message}`,
|
|
474
|
+
failureCategory: "invalid-output",
|
|
475
|
+
durationMs: Date.now() - started,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
426
478
|
}
|
|
427
479
|
else if (pipeline === "markdown-traceability") {
|
|
428
480
|
let report;
|
|
@@ -434,6 +486,60 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
434
486
|
}
|
|
435
487
|
const reportPath = await writeRunReport(meta.runDir, "backend-test-traceability.md", report);
|
|
436
488
|
outputs.push(`traceability=${reportPath}`, report);
|
|
489
|
+
const sourceBinding = meta.spec.sourceBinding;
|
|
490
|
+
if (!sourceBinding) {
|
|
491
|
+
const correspondence = "# Backend Test Markdown → pytest Correspondence\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Correspondence analysis requires spec.sourceBinding; nodes 4, 6 and 7 fail closed on the same task/source binding contract.\n";
|
|
492
|
+
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence);
|
|
493
|
+
outputs.push(`correspondence=${correspondencePath}`, correspondence);
|
|
494
|
+
return {
|
|
495
|
+
ok: false,
|
|
496
|
+
stdout: outputs.join("\n\n"),
|
|
497
|
+
stderr: "backend-test markdown-traceability requires spec.sourceBinding",
|
|
498
|
+
failureCategory: "invalid-output",
|
|
499
|
+
durationMs: Date.now() - started,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({ workspaceRoot: input.cwd, taskId: sourceBinding.taskId });
|
|
504
|
+
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
|
|
505
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
506
|
+
await mkdir(contractsDir, { recursive: true });
|
|
507
|
+
const factsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-facts.json");
|
|
508
|
+
await writeFile(factsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
509
|
+
outputs.push(`correspondence=${correspondencePath}`, `correspondenceFacts=${factsPath}`, correspondence.markdown);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
513
|
+
const correspondence = `# Backend Test Markdown → pytest Correspondence\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Correspondence analysis crashed and is treated as an infrastructure failure: ${message}\n`;
|
|
514
|
+
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence);
|
|
515
|
+
outputs.push(`correspondence=${correspondencePath}`, correspondence);
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
stdout: outputs.join("\n\n"),
|
|
519
|
+
stderr: `backend-test correspondence analysis crashed: ${message}`,
|
|
520
|
+
failureCategory: "invalid-output",
|
|
521
|
+
durationMs: Date.now() - started,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (pipeline === "markdown-manifest") {
|
|
526
|
+
const sourceBinding = meta.spec.sourceBinding;
|
|
527
|
+
if (!sourceBinding)
|
|
528
|
+
throw new Error("backend-test markdown-manifest requires spec.sourceBinding");
|
|
529
|
+
const manifest = await materializeBackendTestCaseManifestFromFacts({
|
|
530
|
+
runDir: meta.runDir,
|
|
531
|
+
workspaceRoot: input.cwd,
|
|
532
|
+
sourceBinding: {
|
|
533
|
+
taskId: sourceBinding.taskId,
|
|
534
|
+
requirementPath: sourceBinding.sources.find((s) => s.kind === "requirement")?.path ?? "source/requirement.md",
|
|
535
|
+
requirementSha256: sourceBinding.sources.find((s) => s.kind === "requirement")?.sha256 ?? "0".repeat(64),
|
|
536
|
+
referencePaths: sourceBinding.sources.filter((s) => s.kind === "reference").map((s) => s.path),
|
|
537
|
+
requirementIds: sourceBinding.requirementIds,
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
const manifestPath = path.join(meta.runDir, "contracts", "backend-test-case-manifest.json");
|
|
541
|
+
const summary = manifest.coverageSummary;
|
|
542
|
+
outputs.push(`manifest=${manifestPath}`, `materializationStatus=${manifest.materializationStatus ?? "available"}`, `coverageSummary.explicitAcCount=${summary?.explicitAcCount ?? "unavailable"}`, `coverageSummary.coveredAcCount=${summary?.coveredAcCount ?? "unavailable"}`, `coverageSummary.caseCount=${summary?.caseCount ?? "unavailable"}`, `coverageSummary.generatedCount=${summary?.generatedCount ?? "unavailable"}`, `ruleCoverageSummary.ruleCount=${manifest.ruleCoverageSummary?.ruleCount ?? "unavailable"}`, `correspondenceSummary.exactCorrespondenceCount=${manifest.correspondenceSummary?.exactCorrespondenceCount ?? "unavailable"}`, `correspondenceSummary.primarySymbolCount=${manifest.correspondenceSummary?.primarySymbolCount ?? "unavailable"}`, `correspondenceSummary.testPoints=${manifest.correspondenceSummary?.mappedTestPointCount ?? "unavailable"}/${manifest.correspondenceSummary?.testPointCount ?? "unavailable"}`, `correspondenceSummary.variantTestPointCount=${manifest.correspondenceSummary?.variantTestPointCount ?? "unavailable"}`, `correspondenceSummary.assertionTestPointCount=${manifest.correspondenceSummary?.assertionTestPointCount ?? "unavailable"}`, `correspondenceSummary.crossCuttingTestPointCount=${manifest.correspondenceSummary?.crossCuttingTestPointCount ?? "unavailable"}`, `correspondenceSummary.unclassifiedTestPointCount=${manifest.correspondenceSummary?.unclassifiedTestPointCount ?? "unavailable"}`, `correspondenceSummary.duplicateBindingTestPointCount=${manifest.correspondenceSummary?.duplicateBindingTestPointCount ?? "unavailable"}`);
|
|
437
543
|
}
|
|
438
544
|
else if (pipeline === "markdown-execute-html") {
|
|
439
545
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
@@ -601,14 +707,18 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
601
707
|
});
|
|
602
708
|
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
603
709
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
710
|
+
const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
|
|
604
711
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
712
|
+
const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
|
|
605
713
|
const htmlContent = renderBackendTestHtml({
|
|
606
714
|
title: meta.spec.title,
|
|
607
715
|
parsed,
|
|
608
716
|
cases,
|
|
609
717
|
environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
|
|
610
718
|
caseValidationSummary,
|
|
719
|
+
caseCoverageSummary,
|
|
611
720
|
traceabilitySummary,
|
|
721
|
+
correspondenceSummary,
|
|
612
722
|
});
|
|
613
723
|
const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
|
|
614
724
|
const facts = renderBackendTestFacts({
|
|
@@ -618,7 +728,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
618
728
|
htmlRelativePath: "reports/backend-test.html",
|
|
619
729
|
htmlContent,
|
|
620
730
|
caseValidationSummary,
|
|
731
|
+
caseCoverageSummary,
|
|
621
732
|
traceabilitySummary,
|
|
733
|
+
correspondenceSummary,
|
|
622
734
|
});
|
|
623
735
|
const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
|
|
624
736
|
const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
|
|
@@ -681,7 +793,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
681
793
|
parsed,
|
|
682
794
|
metrics: l5Metrics,
|
|
683
795
|
caseValidationSummary,
|
|
796
|
+
caseCoverageSummary,
|
|
684
797
|
traceabilitySummary,
|
|
798
|
+
correspondenceSummary,
|
|
685
799
|
failures: failureSummaries.length > 0 ? failureSummaries : undefined,
|
|
686
800
|
});
|
|
687
801
|
const l5Path = await writeRunReport(meta.runDir, "backend-test-l5-dashboard.html", l5Html);
|
|
@@ -1159,6 +1273,16 @@ async function executeFrontendTestCaseChecklist(input, meta) {
|
|
|
1159
1273
|
return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
|
|
1160
1274
|
}
|
|
1161
1275
|
}
|
|
1276
|
+
async function executeFrontendTestL5Report(input, meta) {
|
|
1277
|
+
const started = Date.now();
|
|
1278
|
+
try {
|
|
1279
|
+
const output = await renderFrontendTestL5Report({ workspaceRoot: input.cwd, runDir: meta.runDir });
|
|
1280
|
+
return { ok: true, stdout: `Frontend L-5 report: ${output.htmlPath}\nMarkdown: ${output.markdownPath}\nStatus: ${output.metrics.status}`, stderr: "", failureCategory: "success", durationMs: Date.now() - started };
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1162
1286
|
async function executeFrontendTestHtmlReport(input, meta) {
|
|
1163
1287
|
const started = Date.now();
|
|
1164
1288
|
try {
|
|
@@ -1307,6 +1431,9 @@ export async function executeDagShellNode(input, meta) {
|
|
|
1307
1431
|
if (shell?.frontendTestEvidenceValidation) {
|
|
1308
1432
|
return executeFrontendTestEvidenceValidation(input);
|
|
1309
1433
|
}
|
|
1434
|
+
if (shell?.frontendTestL5Report) {
|
|
1435
|
+
return executeFrontendTestL5Report(input, meta);
|
|
1436
|
+
}
|
|
1310
1437
|
if (shell?.frontendTestHtmlReport) {
|
|
1311
1438
|
return executeFrontendTestHtmlReport(input, meta);
|
|
1312
1439
|
}
|