@tea-agent/loop-agent 0.25.0 → 0.25.2
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 +42 -0
- package/README.md +4 -4
- package/dist/application/dag/generate-task-dag.js +25 -8
- package/dist/commands/dag-init-hybrid.js +3 -2
- package/dist/commands/delegate.js +11 -11
- package/dist/commands/init.js +13 -12
- package/dist/commands/instructions.js +1 -2
- package/dist/executors/shell-executor.js +36 -4
- package/dist/task/read-model.js +3 -7
- package/dist/task/runtime.js +12 -3
- package/dist/worker/run-task/run-task.js +18 -2
- package/dist/workflows/dag/backend-test-markdown-workflow.js +240 -11
- package/dist/workflows/dag/backend-test-result-contract.js +46 -15
- package/dist/workflows/dag/init-hybrid.js +5 -8
- package/dist/workflows/dag/types.js +11 -0
- package/dist/workflows/loop/actions/dag-action.js +2 -1
- package/docs/init-surface.manifest.json +3 -0
- package/docs/templates/backend-test-dag.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +6 -8
- package/skills/loop-agent/references/command-reference.md +19 -10
- package/skills/loop-agent/references/harness-policy.md +6 -6
- package/skills/loop-agent/references/hybrid-dag.md +8 -7
- package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
- package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
- package/skills/loop-agent/references/task-workflow.md +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.25.2] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 加固后端测试中文 HTML 报告的用例关联与解析逻辑,提升报告的准确性与可读性
|
|
10
|
+
|
|
11
|
+
### 改进
|
|
12
|
+
|
|
13
|
+
- 改进基于 class 的 pytest node id 解析,准确提取真实脚本路径,避免显示为 unknown.py
|
|
14
|
+
- 当用例名称缺失后端 ID 时,支持按函数名与 Markdown catalog 进行回退关联
|
|
15
|
+
- 规范化 [REQ]/[RESP] HTTP 日志块,增强日志兼容性
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- 修复成功执行路径下原生 pytest-html 报告未正确保留的问题,现会将其另存为 backend-test-pytest-native.html
|
|
20
|
+
|
|
21
|
+
## [0.25.1] - 2026-07-30
|
|
22
|
+
|
|
23
|
+
### 重点更新
|
|
24
|
+
|
|
25
|
+
- 后端测试 DAG 节点新增 JaCoCo 覆盖率收集能力,支持在 L-5 dashboard 展示 Java 后端的代码行与分支覆盖率
|
|
26
|
+
- 规范任务 DAG 草稿的默认输出路径,统一指向 `.harness/tasks/<task-id>/dag.json`
|
|
27
|
+
- 修复 loop-agent 技能条目因注入预算超限被截断的问题,确保默认 DAG 下技能完整解析
|
|
28
|
+
|
|
29
|
+
### 新增
|
|
30
|
+
|
|
31
|
+
- backend-test DAG 节点 7 支持可选的 JaCoCo 覆盖率收集:通过跨网络 TCP dump JaCoCo tcpserver 数据,经 jacococli.jar 转换并解析为 code-coverage-v1 合约,供 L-5 dashboard 使用
|
|
32
|
+
- task shell 配置新增 `jacocoCoverage` 字段,支持配置 endpoint、cliJarPath、includes 和 connectTimeoutMs
|
|
33
|
+
- 新增面向开发与运维的 JaCoCo 覆盖率部署指南,涵盖 tcpserver agent 挂载、端口放行、cli 预装及安全提醒
|
|
34
|
+
|
|
35
|
+
### 改进
|
|
36
|
+
|
|
37
|
+
- `dag run-task` 与 `dag init-hybrid` 的默认输出路径改为 `.harness/tasks/<task-id>/dag.json`,不再写入系统临时目录
|
|
38
|
+
- agent-worker 在 strict validate 成功后,将校验通过的草稿原子同步到 task 根 `dag.json`
|
|
39
|
+
- task read-model、delegate worktree 提示及各文档示例统一指向规范化的任务草稿路径
|
|
40
|
+
- skills 补充 `source-and-plan-practice.md`,明确 `import-prd` 与 `plan create` 的决策表与推荐顺序,不强制微小任务
|
|
41
|
+
|
|
42
|
+
### 修复
|
|
43
|
+
|
|
44
|
+
- 压缩 SKILL.md 及 required-reference 体积,修复默认 DAG 技能注入预算超限导致 loop-agent 条目被截断的问题
|
|
45
|
+
|
|
5
46
|
## [0.25.0] - 2026-07-30
|
|
6
47
|
|
|
7
48
|
### 重点更新
|
|
@@ -14,6 +55,7 @@
|
|
|
14
55
|
|
|
15
56
|
### 新增
|
|
16
57
|
|
|
58
|
+
- backend-test DAG 节点 7 支持可选的 JaCoCo 覆盖率收集(Java 服务):task shell 配置新增 `jacocoCoverage`(endpoint + cliJarPath + includes + connectTimeoutMs);节点 7 在 pytest 后跨网络 TCP dump JaCoCo tcpserver 执行数据,用 jacococli.jar 转为 jacoco.xml,确定性解析为 `contracts/code-coverage-v1.json` 并喂给 L-5 dashboard 的 line/branch 指标;全程失败安全(endpoint 不通 / cli 缺失 / 解析失败 → 覆盖率降级 unavailable,不阻断测试与 L-5)
|
|
17
59
|
- 提供 agent-worker Feature Packet 脚手架,内置 backend-only、frontend-only 和 fe-with-api 模板,支持原子批量写入与 validate-feature 预检
|
|
18
60
|
- 新增 agent-worker feature advance 命令,编排 verify-final、delivery 到 closeout 的完整交付路径
|
|
19
61
|
- 新增 agent-worker feature doctor 命令,提供只读的交付就绪诊断(覆盖证据、Pool 状态与工作区改动)
|
package/README.md
CHANGED
|
@@ -64,19 +64,19 @@ loop-agent new-task example-task "实现一个有明确验收标准的小功能"
|
|
|
64
64
|
loop-agent dag run-task example-task \
|
|
65
65
|
--profile auto \
|
|
66
66
|
--strict-models \
|
|
67
|
-
--output
|
|
67
|
+
--output .harness/tasks/example-task/dag.json
|
|
68
68
|
|
|
69
69
|
loop-agent dag validate \
|
|
70
|
-
--dag
|
|
70
|
+
--dag .harness/tasks/example-task/dag.json \
|
|
71
71
|
--strict-models \
|
|
72
72
|
--strict-governance
|
|
73
73
|
|
|
74
74
|
loop-agent run-dag \
|
|
75
|
-
--dag
|
|
75
|
+
--dag .harness/tasks/example-task/dag.json \
|
|
76
76
|
--cwd .
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
省略 `--output` 时默认写入 `.harness/tasks/<task-id>/dag.json`。详细步骤见[第一次运行](website/docs/quick-start/first-run.md)和 [Agent DAG 工作流](website/docs/guides/agent-dag.md)。
|
|
80
80
|
|
|
81
81
|
## 查看执行状态
|
|
82
82
|
|
|
@@ -7,10 +7,10 @@ import { parseDagSpec } from "../../workflows/dag/types.js";
|
|
|
7
7
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
8
8
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
9
9
|
import { assertExecPlanIndexConsistent } from "../../governance/exec-plans.js";
|
|
10
|
-
import {
|
|
10
|
+
import { initHybridDagFromTask, } from "../../workflows/dag/init-hybrid.js";
|
|
11
11
|
import { validateDagUseCase } from "./validate-dag.js";
|
|
12
12
|
import { runDagUseCase } from "./run-dag.js";
|
|
13
|
-
import { loadTaskConfig } from "../../task/runtime.js";
|
|
13
|
+
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
14
14
|
import { writeTaskConfig } from "../../infrastructure/harness/task-store.js";
|
|
15
15
|
const PLACEHOLDER_WRITESET_MARKER = "REPLACE/WITH";
|
|
16
16
|
function buildValidateInput(repoRoot, dagPath, parsed) {
|
|
@@ -41,7 +41,15 @@ function isUnsafeWriteSetEntry(entry) {
|
|
|
41
41
|
return normalized.includes(PLACEHOLDER_WRITESET_MARKER);
|
|
42
42
|
}
|
|
43
43
|
export async function assertSafeForExecution(dagPath) {
|
|
44
|
-
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
const content = await readFile(dagPath, "utf-8");
|
|
47
|
+
raw = JSON.parse(content);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
throw new Error(`failed to read valid DAG JSON at ${dagPath}: ${message}`);
|
|
52
|
+
}
|
|
45
53
|
const unsafe = [];
|
|
46
54
|
for (const task of raw.tasks ?? []) {
|
|
47
55
|
if (task.writePolicy !== "exclusive")
|
|
@@ -126,7 +134,16 @@ function resolveReviewPacketShellCommands(task) {
|
|
|
126
134
|
return resolveShellCommands(task.shell);
|
|
127
135
|
}
|
|
128
136
|
async function buildReviewPacket(input) {
|
|
129
|
-
|
|
137
|
+
let raw;
|
|
138
|
+
try {
|
|
139
|
+
const content = await readFile(input.dagPath, "utf-8");
|
|
140
|
+
raw = JSON.parse(content);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
throw new Error(`failed to read valid DAG JSON at ${input.dagPath}: ${message}`);
|
|
145
|
+
}
|
|
146
|
+
const spec = parseDagSpec(raw);
|
|
130
147
|
const writers = collectWriterTasksForPacket(spec).map((task) => {
|
|
131
148
|
const writeSet = task.writeSet ?? [];
|
|
132
149
|
return {
|
|
@@ -247,8 +264,7 @@ export async function generateTaskDagUseCase(input) {
|
|
|
247
264
|
: "task-source";
|
|
248
265
|
profileRouting.routingReasons = candidateResult.templateSelection.reasons;
|
|
249
266
|
if (parsed.profile === "auto") {
|
|
250
|
-
profileRouting.selectedByProfile =
|
|
251
|
-
resolveAutoRoutingProfile(profileRouting.candidateProfile);
|
|
267
|
+
profileRouting.selectedByProfile = resolveAutoRoutingProfile(profileRouting.candidateProfile);
|
|
252
268
|
}
|
|
253
269
|
else if (parsed.profileExplicit) {
|
|
254
270
|
profileRouting.selectedByProfile = parsed.profile;
|
|
@@ -256,7 +272,8 @@ export async function generateTaskDagUseCase(input) {
|
|
|
256
272
|
}
|
|
257
273
|
// Persist the automatic business workflow route while preserving the
|
|
258
274
|
// governance profile as a separate routing dimension.
|
|
259
|
-
if (hasAutomaticTaskSourceRoute &&
|
|
275
|
+
if (hasAutomaticTaskSourceRoute &&
|
|
276
|
+
candidateResult.template === "frontend-implementation") {
|
|
260
277
|
const taskConfig = await loadTaskConfig(repoRoot, parsed.taskId);
|
|
261
278
|
if (!taskConfig.taskKind || taskConfig.taskKind === "standard") {
|
|
262
279
|
await writeTaskConfig(repoRoot, parsed.taskId, {
|
|
@@ -297,7 +314,7 @@ export async function generateTaskDagUseCase(input) {
|
|
|
297
314
|
ok: true,
|
|
298
315
|
taskId: initResult.taskId,
|
|
299
316
|
outputPath,
|
|
300
|
-
defaultOutputPath:
|
|
317
|
+
defaultOutputPath: getTaskPaths(repoRoot, parsed.taskId).dagDraftPath,
|
|
301
318
|
taskCount: initResult.taskCount,
|
|
302
319
|
nodeIds: initResult.nodeIds,
|
|
303
320
|
title: validateSummary.title,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { initHybridDagFromTask } from "../workflows/dag/init-hybrid.js";
|
|
3
|
+
import { getTaskPaths } from "../task/runtime.js";
|
|
3
4
|
export function parseDagInitHybridArgs(args) {
|
|
4
5
|
if (args.length === 0) {
|
|
5
6
|
throw new Error("usage: dag init-hybrid <task-id> [--output <path>]");
|
|
@@ -44,7 +45,7 @@ export async function runDagInitHybrid(repoRoot, rawArgs) {
|
|
|
44
45
|
ok: true,
|
|
45
46
|
taskId: result.taskId,
|
|
46
47
|
outputPath: result.outputPath,
|
|
47
|
-
defaultOutputPath:
|
|
48
|
+
defaultOutputPath: getTaskPaths(repoRoot, parsed.taskId).dagDraftPath,
|
|
48
49
|
taskCount: result.taskCount,
|
|
49
50
|
nodeIds: result.nodeIds,
|
|
50
51
|
next: [
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import { delegateTask } from "../task/delegate.js";
|
|
4
2
|
import { loadHarnessManifest } from "../governance/harness.js";
|
|
5
3
|
import { generateTaskDagUseCase } from "../application/dag/generate-task-dag.js";
|
|
6
4
|
import { validateDagUseCase } from "../application/dag/validate-dag.js";
|
|
7
5
|
import { runDagUseCase } from "../application/dag/run-dag.js";
|
|
6
|
+
import { getTaskPaths } from "../task/runtime.js";
|
|
8
7
|
const DELEGATE_USAGE = "usage: delegate <taskId> [--base <branch>] [--branch <name>] [--no-symlink] [--auto-run]";
|
|
9
8
|
export function parseDelegateArgs(args) {
|
|
10
9
|
if (args.length === 0) {
|
|
@@ -59,12 +58,13 @@ export function parseDelegateArgs(args) {
|
|
|
59
58
|
return { taskId, base, branch, symlinkNodeModules, autoRun };
|
|
60
59
|
}
|
|
61
60
|
export function printDelegateNextSteps(taskId, result) {
|
|
62
|
-
const dagPath =
|
|
61
|
+
const dagPath = getTaskPaths(result.worktreePath, taskId).dagDraftPath;
|
|
63
62
|
const lines = [
|
|
64
63
|
`[delegate] worktree ready (Pi-only).`,
|
|
65
64
|
` worktree: ${result.worktreePath}`,
|
|
66
65
|
` Next — generate, validate, and run DAG in the worktree:`,
|
|
67
|
-
`
|
|
66
|
+
` cd ${JSON.stringify(result.worktreePath)}`,
|
|
67
|
+
` loop-agent --repo-root ${JSON.stringify(result.worktreePath)} dag run-task ${taskId} --profile auto --strict-models`,
|
|
68
68
|
` loop-agent dag validate --dag ${JSON.stringify(dagPath)} --strict-models --strict-governance`,
|
|
69
69
|
` loop-agent run-dag --dag ${JSON.stringify(dagPath)} --cwd ${JSON.stringify(result.worktreePath)}`,
|
|
70
70
|
` Or: loop-agent delegate ${taskId} --auto-run`,
|
|
@@ -89,30 +89,30 @@ export async function runDelegate(repoRoot, rawArgs, dependencies = DEFAULT_RUN_
|
|
|
89
89
|
symlinkNodeModules: parsed.symlinkNodeModules,
|
|
90
90
|
});
|
|
91
91
|
if (parsed.autoRun) {
|
|
92
|
-
const
|
|
92
|
+
const worktreeRoot = result.worktreePath;
|
|
93
|
+
const dagPath = getTaskPaths(worktreeRoot, parsed.taskId).dagDraftPath;
|
|
93
94
|
await dependencies.generateTaskDag({
|
|
94
|
-
repoRoot,
|
|
95
|
+
repoRoot: worktreeRoot,
|
|
95
96
|
taskId: parsed.taskId,
|
|
96
|
-
outputPath: dagPath,
|
|
97
97
|
strictModels: true,
|
|
98
98
|
execute: false,
|
|
99
99
|
initOnly: false,
|
|
100
100
|
dryRun: false,
|
|
101
|
-
cwd:
|
|
101
|
+
cwd: worktreeRoot,
|
|
102
102
|
profile: "auto",
|
|
103
103
|
profileExplicit: false,
|
|
104
104
|
});
|
|
105
105
|
await dependencies.validateDag({
|
|
106
|
-
repoRoot,
|
|
106
|
+
repoRoot: worktreeRoot,
|
|
107
107
|
dagPath,
|
|
108
108
|
strictModelMatrix: true,
|
|
109
109
|
strictGovernance: true,
|
|
110
110
|
forbidExecutors: [],
|
|
111
111
|
});
|
|
112
112
|
const runSummary = await dependencies.runDag({
|
|
113
|
-
repoRoot,
|
|
113
|
+
repoRoot: worktreeRoot,
|
|
114
114
|
dagPath,
|
|
115
|
-
cwd:
|
|
115
|
+
cwd: worktreeRoot,
|
|
116
116
|
initOnly: false,
|
|
117
117
|
dryRun: false,
|
|
118
118
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -345,7 +345,7 @@ check_ts_import_boundary() {
|
|
|
345
345
|
local label="$3"
|
|
346
346
|
[[ -d "\${from_dir}" && -d "\${forbidden_dir}" ]] || return 0
|
|
347
347
|
while IFS= read -r -d '' file; do
|
|
348
|
-
if grep -Eq "from ['
|
|
348
|
+
if grep -Eq "from ['\\"][^'\\"]*\${forbidden_dir}/|import\\(['\\"][^'\\"]*\${forbidden_dir}/|require\\(['\\"][^'\\"]*\${forbidden_dir}/" "\${file}"; then
|
|
349
349
|
violations+=("\${label}: \${file} imports \${forbidden_dir}")
|
|
350
350
|
fi
|
|
351
351
|
done < <(find "\${from_dir}" -type f \\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.mjs' -o -name '*.cjs' \\) -print0)
|
|
@@ -412,7 +412,8 @@ check_skill "loop-agent" \\
|
|
|
412
412
|
"references/harness-policy.md" \\
|
|
413
413
|
"references/hybrid-dag.md" \\
|
|
414
414
|
"references/verification-and-failure-handling.md" \\
|
|
415
|
-
"references/command-reference.md"
|
|
415
|
+
"references/command-reference.md" \\
|
|
416
|
+
"references/source-and-plan-practice.md"
|
|
416
417
|
check_skill "agent-worker" "references/agent-worker-operator.md"
|
|
417
418
|
|
|
418
419
|
# loop-agent description is a YAML folded block (>-); grep ^description: only
|
|
@@ -856,9 +857,9 @@ function buildManagedAgentsBlock(input) {
|
|
|
856
857
|
"loop-agent new-task <task-id> \"任务标题\"",
|
|
857
858
|
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
858
859
|
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
859
|
-
"loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
860
|
-
"loop-agent dag validate --dag
|
|
861
|
-
"loop-agent run-dag --dag
|
|
860
|
+
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
861
|
+
"loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance",
|
|
862
|
+
"loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd .",
|
|
862
863
|
"```",
|
|
863
864
|
"",
|
|
864
865
|
"`source/需求.md` 与 `source/执行约束.md` 必需;写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。",
|
|
@@ -1818,9 +1819,9 @@ function buildManagedReadmeBlock(input) {
|
|
|
1818
1819
|
"loop-agent new-task <task-id> \"任务标题\"",
|
|
1819
1820
|
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
1820
1821
|
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
1821
|
-
"loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
1822
|
-
"loop-agent dag validate --dag
|
|
1823
|
-
"loop-agent run-dag --dag
|
|
1822
|
+
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
1823
|
+
"loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance",
|
|
1824
|
+
"loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd .",
|
|
1824
1825
|
"```",
|
|
1825
1826
|
"",
|
|
1826
1827
|
`详细工作流见 \`${input.governanceRoot}/feature-workflow.md\`;验证矩阵见 \`${input.governanceRoot}/verification-matrix.md\`。任务 source 是必需项:\`source/需求.md\` 写目标、范围、非目标、验收标准和相关链接,\`source/执行约束.md\` 写允许路径、禁止路径、受保护变更、不变量、预期验证和失败条件。`,
|
|
@@ -2050,9 +2051,9 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2050
2051
|
"loop-agent new-task <task-id> \"Task title\"",
|
|
2051
2052
|
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
2052
2053
|
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
2053
|
-
"loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
2054
|
-
"loop-agent dag validate --dag
|
|
2055
|
-
"loop-agent run-dag --dag
|
|
2054
|
+
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
2055
|
+
"loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance",
|
|
2056
|
+
"loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd .",
|
|
2056
2057
|
"```",
|
|
2057
2058
|
"",
|
|
2058
2059
|
DAG_HARD_GATE_TRIGGER,
|
|
@@ -2674,7 +2675,7 @@ async function applySafeAction(input) {
|
|
|
2674
2675
|
if (!isRecord(harness) || !isRecord(harness.executors) || !isRecord(harness.executors.pi)) {
|
|
2675
2676
|
return false;
|
|
2676
2677
|
}
|
|
2677
|
-
if (!Object.
|
|
2678
|
+
if (!Object.hasOwn(harness.executors.pi, "requiresApiKey")) {
|
|
2678
2679
|
return false;
|
|
2679
2680
|
}
|
|
2680
2681
|
const nextPi = { ...harness.executors.pi };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { access, readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getArtifactPath, getArtifactRelativePath, } from "../shared/artifacts-core.js";
|
|
4
|
-
import { defaultHybridDagOutputPath } from "../workflows/dag/init-hybrid.js";
|
|
5
4
|
import { getTaskDir, getTaskPaths, loadTaskConfig } from "../task/runtime.js";
|
|
6
5
|
const STAGES = new Set([
|
|
7
6
|
"source",
|
|
@@ -96,7 +95,7 @@ export async function buildInstructions(repoRoot, stage, taskId) {
|
|
|
96
95
|
if (stage === "dag-draft") {
|
|
97
96
|
return refreshInstructionBlockers(withMissingDependencies({
|
|
98
97
|
...base,
|
|
99
|
-
outputPath:
|
|
98
|
+
outputPath: paths.dagDraftPath,
|
|
100
99
|
dependencies: [
|
|
101
100
|
paths.taskConfigPath,
|
|
102
101
|
path.join(paths.sourceDir, "需求.md"),
|
|
@@ -19,7 +19,7 @@ import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } f
|
|
|
19
19
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
20
20
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
21
21
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
22
|
-
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
22
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
23
23
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
24
24
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
25
25
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -587,11 +587,13 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
587
587
|
throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
|
|
588
588
|
}
|
|
589
589
|
const parsed = parsePytestHtmlReport(pytestHtmlContent);
|
|
590
|
+
// Keep native pytest-html (data-jsonblob) for audit before styled overwrite.
|
|
591
|
+
const nativeHtmlPath = await writeRunReport(meta.runDir, "backend-test-pytest-native.html", pytestHtmlContent);
|
|
590
592
|
// Bind Result v1 from the native pytest-html report BEFORE overwriting with the
|
|
591
593
|
// styled renderer (which drops the data-jsonblob island).
|
|
592
594
|
const resultArtifact = await materializeBackendTestResultFromPytestHtml({
|
|
593
595
|
runDir: meta.runDir,
|
|
594
|
-
htmlRelativePath: "reports/backend-test.html",
|
|
596
|
+
htmlRelativePath: "reports/backend-test-pytest-native.html",
|
|
595
597
|
htmlContent: pytestHtmlContent,
|
|
596
598
|
pytestExitCode,
|
|
597
599
|
});
|
|
@@ -629,10 +631,40 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
629
631
|
catch {
|
|
630
632
|
// manifest missing: L5 AC/automation metrics degrade to unavailable.
|
|
631
633
|
}
|
|
634
|
+
// Optional JaCoCo coverage collection (Java services). When jacocoCoverage
|
|
635
|
+
// is configured on the shell node, dump exec data over TCP from the
|
|
636
|
+
// JaCoCo tcpserver agent, convert via jacococli.jar, and parse into a
|
|
637
|
+
// code-coverage-v1 contract. Failure-safe: any error → coverage stays
|
|
638
|
+
// null and the L-5 dashboard degrades line/branch to unavailable.
|
|
639
|
+
let coverageContract = null;
|
|
640
|
+
const jacocoCfg = input.task.shell?.jacocoCoverage;
|
|
641
|
+
if (jacocoCfg) {
|
|
642
|
+
const sourceBinding = meta.spec.sourceBinding;
|
|
643
|
+
const requirementIds = sourceBinding?.requirementIds ?? [];
|
|
644
|
+
try {
|
|
645
|
+
coverageContract = await collectJacocoCoverage({
|
|
646
|
+
endpoint: jacocoCfg.endpoint,
|
|
647
|
+
cliJarPath: jacocoCfg.cliJarPath,
|
|
648
|
+
includes: jacocoCfg.includes ?? "*",
|
|
649
|
+
connectTimeoutMs: jacocoCfg.connectTimeoutMs ?? 5000,
|
|
650
|
+
workDir: reportsDir,
|
|
651
|
+
sourceScope: { requirementIds, paths: [] },
|
|
652
|
+
commitSha: null,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// coverage unavailable: JaCoCo endpoint unreachable or parse failed.
|
|
657
|
+
}
|
|
658
|
+
if (coverageContract) {
|
|
659
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
660
|
+
await mkdir(contractsDir, { recursive: true });
|
|
661
|
+
await writeFile(path.join(contractsDir, "code-coverage-v1.json"), JSON.stringify(coverageContract, null, 2), "utf8");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
632
664
|
const l5Metrics = computeL5ReportMetrics({
|
|
633
665
|
result: { passed: parsed.passed, failed: parsed.failed, error: parsed.errors, skipped: parsed.skipped },
|
|
634
666
|
manifest: manifestForL5,
|
|
635
|
-
coverage:
|
|
667
|
+
coverage: coverageContract,
|
|
636
668
|
criticalRiskCount: parsed.failed + parsed.errors > 0 ? 1 : 0,
|
|
637
669
|
});
|
|
638
670
|
const failureSummaries = parsed.cases
|
|
@@ -652,7 +684,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
652
684
|
});
|
|
653
685
|
const l5Path = await writeRunReport(meta.runDir, "backend-test-l5-dashboard.html", l5Html);
|
|
654
686
|
const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
|
|
655
|
-
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `l5-dashboard=${l5Path}`, `result=${resultArtifact.path}`, facts);
|
|
687
|
+
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `nativeHtml=${nativeHtmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `l5-dashboard=${l5Path}`, `result=${resultArtifact.path}`, facts);
|
|
656
688
|
}
|
|
657
689
|
else if (pipeline === "contracts") {
|
|
658
690
|
const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
|
package/dist/task/read-model.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { queryCursorRuns } from "../records/one-shot-runs.js";
|
|
4
|
-
import {
|
|
5
|
-
import { listAllDagRunEntries, readDagRunSpec } from "../workflows/dag/lifecycle.js";
|
|
4
|
+
import { listAllDagRunEntries, readDagRunSpec, } from "../workflows/dag/lifecycle.js";
|
|
6
5
|
import { getTaskDir, getTaskPaths, getTaskStatus } from "./runtime.js";
|
|
7
6
|
async function exists(filePath) {
|
|
8
7
|
try {
|
|
@@ -28,9 +27,6 @@ async function listRelatedDagRuns(repoRoot, taskId) {
|
|
|
28
27
|
}
|
|
29
28
|
return related;
|
|
30
29
|
}
|
|
31
|
-
function defaultDagDraftPath(taskId) {
|
|
32
|
-
return defaultHybridDagOutputPath(taskId);
|
|
33
|
-
}
|
|
34
30
|
function closeoutPath(repoRoot, taskId) {
|
|
35
31
|
return path.join(repoRoot, "docs", "progress", `${taskId}.md`);
|
|
36
32
|
}
|
|
@@ -43,7 +39,7 @@ function deriveNextAction(input) {
|
|
|
43
39
|
}
|
|
44
40
|
if (!input.dagDraftExists && !input.latestDagRun) {
|
|
45
41
|
return {
|
|
46
|
-
command: `npm run dev -- dag run-task ${input.taskId} --profile auto --strict-models
|
|
42
|
+
command: `npm run dev -- dag run-task ${input.taskId} --profile auto --strict-models`,
|
|
47
43
|
reason: "source ready; DAG draft/run evidence not found",
|
|
48
44
|
};
|
|
49
45
|
}
|
|
@@ -86,7 +82,7 @@ export async function buildTaskReadModel(repoRoot, taskId) {
|
|
|
86
82
|
if (!(await exists(requirementPath)))
|
|
87
83
|
missing.push(requirementPath);
|
|
88
84
|
const sourceReady = missing.length === 0;
|
|
89
|
-
const dagDraftPath =
|
|
85
|
+
const dagDraftPath = paths.dagDraftPath;
|
|
90
86
|
const dagDraftExists = await exists(dagDraftPath);
|
|
91
87
|
const dagRuns = await listRelatedDagRuns(repoRoot, taskId);
|
|
92
88
|
const latestDagRun = dagRuns.at(-1) ?? null;
|
package/dist/task/runtime.js
CHANGED
|
@@ -68,6 +68,8 @@ export function getTaskPaths(repoRoot, taskId) {
|
|
|
68
68
|
contractPath: path.join(taskDir, 'contract.json'),
|
|
69
69
|
/** Journaled contract transactions directory. */
|
|
70
70
|
transactionsDir: path.join(taskDir, '.transactions'),
|
|
71
|
+
/** Canonical current DAG draft input for validate / run-dag. */
|
|
72
|
+
dagDraftPath: path.join(taskDir, 'dag.json'),
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
export async function createTask(repoRoot, taskId, title) {
|
|
@@ -177,8 +179,15 @@ export async function getTaskStatus(repoRoot, taskId) {
|
|
|
177
179
|
return loadWorkflowState(getTaskPaths(repoRoot, taskId).statePath);
|
|
178
180
|
}
|
|
179
181
|
export async function loadTaskConfig(repoRoot, taskId) {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
+
const configPath = getTaskPaths(repoRoot, taskId).taskConfigPath;
|
|
183
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
184
|
+
try {
|
|
185
|
+
return parseTaskConfigForRuntime(JSON.parse(raw));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
throw new Error(`invalid task config at ${configPath}: ${message}`);
|
|
190
|
+
}
|
|
182
191
|
}
|
|
183
192
|
export function parseTaskConfigForRuntime(rawConfig) {
|
|
184
193
|
const parsed = taskConfigSchema.parse(rawConfig);
|
|
@@ -292,7 +301,7 @@ export function getSupportedNextCommand(step, taskId, status, options) {
|
|
|
292
301
|
return `npm run dev -- loop status ${taskId}`;
|
|
293
302
|
}
|
|
294
303
|
if (step === 'analyze' || step === 'plan' || step === 'spec' || step === 'implement' || step === 'verify' || step === 'retrospective') {
|
|
295
|
-
return `npm run dev -- dag run-task ${taskId} --profile auto --strict-models
|
|
304
|
+
return `npm run dev -- dag run-task ${taskId} --profile auto --strict-models`;
|
|
296
305
|
}
|
|
297
306
|
return `${step} step not yet implemented in this prototype; human intervention required`;
|
|
298
307
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { getTaskPaths } from "../../task/runtime.js";
|
|
4
6
|
import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
|
|
5
7
|
import { redactForPreview, truncatePreview } from "../observability/events.js";
|
|
6
8
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
@@ -104,6 +106,13 @@ export async function runTaskSpec(options) {
|
|
|
104
106
|
], true, undefined, eventCtx);
|
|
105
107
|
});
|
|
106
108
|
progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
|
|
109
|
+
const canonicalDraftPath = getTaskPaths(options.repoRoot, materializeManifest.harnessTaskId).dagDraftPath;
|
|
110
|
+
await runObservedStep(eventCtx, "publish-canonical-dag-draft", async () => {
|
|
111
|
+
const dagBytes = await readFile(dagPath, "utf-8");
|
|
112
|
+
await writeTextAtomic(canonicalDraftPath, dagBytes, {
|
|
113
|
+
repoRoot: options.repoRoot,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
107
116
|
const runDagStep = "run-dag";
|
|
108
117
|
const runDagStartedAt = Date.now();
|
|
109
118
|
const dagEventsPath = path.join(getTaskPoolRoot(options.repoRoot), "observability", "runs", workerRunId, "dag-events.jsonl");
|
|
@@ -330,7 +339,7 @@ export function buildWorkerRunId(businessId, now) {
|
|
|
330
339
|
.slice(0, 6);
|
|
331
340
|
return `wr-${date}-${businessId}-${hash}`;
|
|
332
341
|
}
|
|
333
|
-
async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs,
|
|
342
|
+
async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs, _eventCtx) {
|
|
334
343
|
const result = await client.run(args, {
|
|
335
344
|
cwd: repoRoot,
|
|
336
345
|
artifactName,
|
|
@@ -743,7 +752,14 @@ function strictModelsArgs(piModel) {
|
|
|
743
752
|
*/
|
|
744
753
|
export async function applyPiModelOverride(dagPath, piModel) {
|
|
745
754
|
const raw = await readFile(dagPath, "utf-8");
|
|
746
|
-
|
|
755
|
+
let spec;
|
|
756
|
+
try {
|
|
757
|
+
spec = JSON.parse(raw);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
761
|
+
throw new Error(`invalid generated DAG JSON at ${dagPath}: ${message}`);
|
|
762
|
+
}
|
|
747
763
|
spec.executorModels = {
|
|
748
764
|
pi: {
|
|
749
765
|
LOW: piModel,
|