@tea-agent/loop-agent 0.25.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -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 +32 -2
- 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 +98 -0
- 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,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.25.1] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 后端测试 DAG 节点新增 JaCoCo 覆盖率收集能力,支持在 L-5 dashboard 展示 Java 后端的代码行与分支覆盖率
|
|
10
|
+
- 规范任务 DAG 草稿的默认输出路径,统一指向 `.harness/tasks/<task-id>/dag.json`
|
|
11
|
+
- 修复 loop-agent 技能条目因注入预算超限被截断的问题,确保默认 DAG 下技能完整解析
|
|
12
|
+
|
|
13
|
+
### 新增
|
|
14
|
+
|
|
15
|
+
- backend-test DAG 节点 7 支持可选的 JaCoCo 覆盖率收集:通过跨网络 TCP dump JaCoCo tcpserver 数据,经 jacococli.jar 转换并解析为 code-coverage-v1 合约,供 L-5 dashboard 使用
|
|
16
|
+
- task shell 配置新增 `jacocoCoverage` 字段,支持配置 endpoint、cliJarPath、includes 和 connectTimeoutMs
|
|
17
|
+
- 新增面向开发与运维的 JaCoCo 覆盖率部署指南,涵盖 tcpserver agent 挂载、端口放行、cli 预装及安全提醒
|
|
18
|
+
|
|
19
|
+
### 改进
|
|
20
|
+
|
|
21
|
+
- `dag run-task` 与 `dag init-hybrid` 的默认输出路径改为 `.harness/tasks/<task-id>/dag.json`,不再写入系统临时目录
|
|
22
|
+
- agent-worker 在 strict validate 成功后,将校验通过的草稿原子同步到 task 根 `dag.json`
|
|
23
|
+
- task read-model、delegate worktree 提示及各文档示例统一指向规范化的任务草稿路径
|
|
24
|
+
- skills 补充 `source-and-plan-practice.md`,明确 `import-prd` 与 `plan create` 的决策表与推荐顺序,不强制微小任务
|
|
25
|
+
|
|
26
|
+
### 修复
|
|
27
|
+
|
|
28
|
+
- 压缩 SKILL.md 及 required-reference 体积,修复默认 DAG 技能注入预算超限导致 loop-agent 条目被截断的问题
|
|
29
|
+
|
|
5
30
|
## [0.25.0] - 2026-07-30
|
|
6
31
|
|
|
7
32
|
### 重点更新
|
|
@@ -14,6 +39,7 @@
|
|
|
14
39
|
|
|
15
40
|
### 新增
|
|
16
41
|
|
|
42
|
+
- 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
43
|
- 提供 agent-worker Feature Packet 脚手架,内置 backend-only、frontend-only 和 fe-with-api 模板,支持原子批量写入与 validate-feature 预检
|
|
18
44
|
- 新增 agent-worker feature advance 命令,编排 verify-final、delivery 到 closeout 的完整交付路径
|
|
19
45
|
- 新增 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";
|
|
@@ -629,10 +629,40 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
629
629
|
catch {
|
|
630
630
|
// manifest missing: L5 AC/automation metrics degrade to unavailable.
|
|
631
631
|
}
|
|
632
|
+
// Optional JaCoCo coverage collection (Java services). When jacocoCoverage
|
|
633
|
+
// is configured on the shell node, dump exec data over TCP from the
|
|
634
|
+
// JaCoCo tcpserver agent, convert via jacococli.jar, and parse into a
|
|
635
|
+
// code-coverage-v1 contract. Failure-safe: any error → coverage stays
|
|
636
|
+
// null and the L-5 dashboard degrades line/branch to unavailable.
|
|
637
|
+
let coverageContract = null;
|
|
638
|
+
const jacocoCfg = input.task.shell?.jacocoCoverage;
|
|
639
|
+
if (jacocoCfg) {
|
|
640
|
+
const sourceBinding = meta.spec.sourceBinding;
|
|
641
|
+
const requirementIds = sourceBinding?.requirementIds ?? [];
|
|
642
|
+
try {
|
|
643
|
+
coverageContract = await collectJacocoCoverage({
|
|
644
|
+
endpoint: jacocoCfg.endpoint,
|
|
645
|
+
cliJarPath: jacocoCfg.cliJarPath,
|
|
646
|
+
includes: jacocoCfg.includes ?? "*",
|
|
647
|
+
connectTimeoutMs: jacocoCfg.connectTimeoutMs ?? 5000,
|
|
648
|
+
workDir: reportsDir,
|
|
649
|
+
sourceScope: { requirementIds, paths: [] },
|
|
650
|
+
commitSha: null,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// coverage unavailable: JaCoCo endpoint unreachable or parse failed.
|
|
655
|
+
}
|
|
656
|
+
if (coverageContract) {
|
|
657
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
658
|
+
await mkdir(contractsDir, { recursive: true });
|
|
659
|
+
await writeFile(path.join(contractsDir, "code-coverage-v1.json"), JSON.stringify(coverageContract, null, 2), "utf8");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
632
662
|
const l5Metrics = computeL5ReportMetrics({
|
|
633
663
|
result: { passed: parsed.passed, failed: parsed.failed, error: parsed.errors, skipped: parsed.skipped },
|
|
634
664
|
manifest: manifestForL5,
|
|
635
|
-
coverage:
|
|
665
|
+
coverage: coverageContract,
|
|
636
666
|
criticalRiskCount: parsed.failed + parsed.errors > 0 ? 1 : 0,
|
|
637
667
|
});
|
|
638
668
|
const failureSummaries = parsed.cases
|
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,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { connect } from "node:net";
|
|
2
4
|
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createWriteStream } from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { parseJacocoXml, } from "./backend-test-coverage-contract.js";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
4
10
|
const CASE_ID = /\bBE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}\b/g;
|
|
5
11
|
const AC_ID = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
|
|
6
12
|
const SECRET = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization)\s*[:=]\s*\S+/i;
|
|
@@ -1079,6 +1085,98 @@ function renderQualityCard(title, summary) {
|
|
|
1079
1085
|
const label = summary.status === "Unavailable" ? "不可用" : summary.status;
|
|
1080
1086
|
return `<div style="padding:12px 14px;border:1px solid #e3e8ef;border-radius:10px;background:#fbfcfe"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px"><strong style="color:#17365d;font-size:0.88rem">${escapeHtml(title)}</strong><span style="display:inline-block;padding:2px 9px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${colors.fg};background:${colors.bg}">${label}</span></div><div style="color:#667085;font-size:0.78rem">Findings:${summary.findings ?? "不可用"}</div><div style="color:#475467;font-size:0.8rem;margin-top:2px">${escapeHtml(summary.firstFinding)}</div></div>`;
|
|
1081
1087
|
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Dump JaCoCo execution data over TCP from a JaCoCo tcpserver agent, convert
|
|
1090
|
+
* .exec → jacoco.xml via jacococli.jar, then parse into a code-coverage-v1
|
|
1091
|
+
* contract. Designed for Java services started with
|
|
1092
|
+
* `-javaagent:jacocoagent.jar=output=tcpserver,port=6300,append=false`.
|
|
1093
|
+
*
|
|
1094
|
+
* Failure-safe: any transport/conversion/parse error returns null so the L-5
|
|
1095
|
+
* dashboard degrades coverage to unavailable instead of blocking the run.
|
|
1096
|
+
*/
|
|
1097
|
+
export async function collectJacocoCoverage(input) {
|
|
1098
|
+
const [host, portStr] = input.endpoint.split(":");
|
|
1099
|
+
const port = Number(portStr);
|
|
1100
|
+
if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const execPath = path.join(input.workDir, "jacoco.exec");
|
|
1104
|
+
const xmlPath = path.join(input.workDir, "jacoco.xml");
|
|
1105
|
+
// Step 1: TCP dump of JaCoCo execution data. The protocol is JaCoCo's
|
|
1106
|
+
// binary "dump" command: a magic header + command byte. We send the dump
|
|
1107
|
+
// request and stream the response into jacoco.exec.
|
|
1108
|
+
try {
|
|
1109
|
+
await dumpJacocoExec(host, port, execPath, input.connectTimeoutMs);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
// Step 2: jacococli.jar report .exec → jacoco.xml
|
|
1115
|
+
try {
|
|
1116
|
+
await execFileAsync("java", [
|
|
1117
|
+
"-jar", input.cliJarPath,
|
|
1118
|
+
"report", execPath,
|
|
1119
|
+
"--xml", xmlPath,
|
|
1120
|
+
"--includes", input.includes,
|
|
1121
|
+
], { timeout: 60000, windowsHide: true });
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
// Step 3: parse jacoco.xml into a code-coverage-v1 contract.
|
|
1127
|
+
try {
|
|
1128
|
+
const xml = await readFile(xmlPath, "utf8");
|
|
1129
|
+
const artifactSha256 = createHash("sha256").update(xml).digest("hex");
|
|
1130
|
+
return parseJacocoXml(xml, {
|
|
1131
|
+
sourceScope: input.sourceScope,
|
|
1132
|
+
commitSha: input.commitSha,
|
|
1133
|
+
artifactPath: "reports/jacoco.xml",
|
|
1134
|
+
artifactSha256,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
catch {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Send a JaCoCo TCP dump command and stream the response into destPath.
|
|
1143
|
+
* JaCoCo tcpserver protocol: connect, send dump command header, read until close.
|
|
1144
|
+
*/
|
|
1145
|
+
function dumpJacocoExec(host, port, destPath, connectTimeoutMs) {
|
|
1146
|
+
return new Promise((resolve, reject) => {
|
|
1147
|
+
const socket = connect({ host, port });
|
|
1148
|
+
const writeStream = createWriteStream(destPath);
|
|
1149
|
+
const timer = setTimeout(() => {
|
|
1150
|
+
socket.destroy();
|
|
1151
|
+
reject(new Error("jacoco-tcp-connect-timeout"));
|
|
1152
|
+
}, connectTimeoutMs);
|
|
1153
|
+
socket.on("connect", () => {
|
|
1154
|
+
// JaCoCo TCP "dump" command: magic 0xC0FFEE + cmd=0x40 (dump)
|
|
1155
|
+
// Format: <magic 3 bytes><cmd id 1 byte>
|
|
1156
|
+
// The server responds with the exec data and closes.
|
|
1157
|
+
const magic = Buffer.from([0xC0, 0xFF, 0xEE]);
|
|
1158
|
+
const dumpCmd = Buffer.from([0x40]);
|
|
1159
|
+
socket.end(Buffer.concat([magic, dumpCmd]));
|
|
1160
|
+
});
|
|
1161
|
+
socket.on("error", (error) => {
|
|
1162
|
+
clearTimeout(timer);
|
|
1163
|
+
reject(error);
|
|
1164
|
+
});
|
|
1165
|
+
socket.on("close", () => {
|
|
1166
|
+
clearTimeout(timer);
|
|
1167
|
+
writeStream.end(() => {
|
|
1168
|
+
// Verify we got non-empty exec data.
|
|
1169
|
+
readFile(destPath).then((buf) => {
|
|
1170
|
+
if (buf.length === 0)
|
|
1171
|
+
reject(new Error("jacoco-dump-empty"));
|
|
1172
|
+
else
|
|
1173
|
+
resolve();
|
|
1174
|
+
}).catch(reject);
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
socket.pipe(writeStream);
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1082
1180
|
function inferModuleScript(classname) {
|
|
1083
1181
|
// classname like "test_process_definition_list.py::TestX::test_a" → take the file stem.
|
|
1084
1182
|
const first = classname.split("::")[0] ?? classname;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { access, readdir, readFile, realpath,
|
|
2
|
+
import { access, readdir, readFile, realpath, } from "node:fs/promises";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
7
7
|
import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
|
|
8
8
|
import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
@@ -3515,7 +3515,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3515
3515
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3516
3516
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3517
3517
|
].join("; ");
|
|
3518
|
-
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. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html 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 a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md,
|
|
3518
|
+
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. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html 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 a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3519
3519
|
if (execute.shell) {
|
|
3520
3520
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3521
3521
|
}
|
|
@@ -5741,9 +5741,6 @@ function buildSupervisedHybridDag(standard, sources) {
|
|
|
5741
5741
|
assertValidDagSpec(spec);
|
|
5742
5742
|
return spec;
|
|
5743
5743
|
}
|
|
5744
|
-
export function defaultHybridDagOutputPath(taskId) {
|
|
5745
|
-
return path.join(os.tmpdir(), `${taskId}-hybrid-dag.json`);
|
|
5746
|
-
}
|
|
5747
5744
|
export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
5748
5745
|
const routingProjectCapability = sources.taskConfig.taskKind === "standard" &&
|
|
5749
5746
|
(options.template === undefined || options.template === "standard-dag")
|
|
@@ -5774,7 +5771,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
5774
5771
|
taskId: preparedSources.taskId,
|
|
5775
5772
|
});
|
|
5776
5773
|
}
|
|
5777
|
-
await
|
|
5774
|
+
await writeJsonAtomic(outputPath, spec, { repoRoot: sources.repoRoot });
|
|
5778
5775
|
return {
|
|
5779
5776
|
taskId: sources.taskId,
|
|
5780
5777
|
outputPath,
|
|
@@ -5787,7 +5784,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
5787
5784
|
export async function initHybridDagFromTask(repoRoot, taskId, options = {}) {
|
|
5788
5785
|
const sources = await loadTaskHybridSources(repoRoot, taskId);
|
|
5789
5786
|
assertTaskAllowedPathsPreflight(sources.taskConfig);
|
|
5790
|
-
const outputPath = options.outputPath ??
|
|
5787
|
+
const outputPath = options.outputPath ?? getTaskPaths(repoRoot, taskId).dagDraftPath;
|
|
5791
5788
|
return writeHybridDagDraft(sources, outputPath, {
|
|
5792
5789
|
template: options.template,
|
|
5793
5790
|
});
|
|
@@ -326,6 +326,17 @@ export const dagShellConfigSchema = z.object({
|
|
|
326
326
|
frontendReviewContext: dagFrontendReviewContextSchema.optional(),
|
|
327
327
|
frontendTestEvidenceValidation: dagFrontendTestEvidenceValidationSchema.optional(),
|
|
328
328
|
backendTestPipeline: dagBackendTestPipelineSchema.optional(),
|
|
329
|
+
/** JaCoCo coverage collection for backend-test (Java services). When set, node 7 dumps coverage over TCP from a JaCoCo tcpserver agent and feeds it to the L-5 dashboard. */
|
|
330
|
+
jacocoCoverage: z.object({
|
|
331
|
+
/** JaCoCo tcpserver endpoint, e.g. "host:6300". */
|
|
332
|
+
endpoint: z.string().min(1),
|
|
333
|
+
/** Absolute path to jacococli.jar on this machine, used to convert .exec → jacoco.xml. */
|
|
334
|
+
cliJarPath: z.string().min(1),
|
|
335
|
+
/** Business package filter passed to JaCoCo includes, e.g. "com.example.*". Defaults to "*". */
|
|
336
|
+
includes: z.string().min(1).optional().default("*"),
|
|
337
|
+
/** TCP connect timeout in ms. Defaults to 5000. */
|
|
338
|
+
connectTimeoutMs: z.number().int().positive().optional().default(5000),
|
|
339
|
+
}).strict().optional(),
|
|
329
340
|
verifyEvidence: dagShellVerifyEvidenceSchema.optional(),
|
|
330
341
|
repairArtifactGate: dagRepairArtifactGateSchema.optional(),
|
|
331
342
|
/** fail (default): any nonzero command fails the node. record: finish node FINISHED with failure facts for downstream assess/repair. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { writeLoopDagRecord } from "../../../infrastructure/harness/loop-action-store.js";
|
|
4
|
+
import { getTaskPaths } from "../../../task/runtime.js";
|
|
4
5
|
import { getLoopPaths } from "../paths.js";
|
|
5
6
|
import { appendLoopRound } from "../rounds.js";
|
|
6
7
|
import { rewriteLoopContext } from "../context.js";
|
|
@@ -26,7 +27,7 @@ export async function runLoopDagAction(repoRoot, taskId, options = {}) {
|
|
|
26
27
|
await mkdir(dagEvidenceDir, { recursive: true });
|
|
27
28
|
const dagPath = options.dagPath
|
|
28
29
|
? path.resolve(repoRoot, options.dagPath)
|
|
29
|
-
:
|
|
30
|
+
: getTaskPaths(repoRoot, taskId).dagDraftPath;
|
|
30
31
|
const runner = options.runner ?? resolveDefaultRunner(repoRoot);
|
|
31
32
|
let generated = {};
|
|
32
33
|
if (!options.dagPath) {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"skills/agent-worker/references/agent-worker-operator.md",
|
|
56
56
|
"skills/loop-agent/SKILL.md",
|
|
57
57
|
"skills/loop-agent/references/command-reference.md",
|
|
58
|
+
"skills/loop-agent/references/source-and-plan-practice.md",
|
|
58
59
|
"skills/loop-agent/references/docs-converge.md",
|
|
59
60
|
"skills/ai-engineering-context/SKILL.md",
|
|
60
61
|
"skills/verification-before-completion/SKILL.md",
|
|
@@ -133,6 +134,7 @@
|
|
|
133
134
|
".agents/skills/agent-worker/references/agent-worker-operator.md",
|
|
134
135
|
".agents/skills/loop-agent/SKILL.md",
|
|
135
136
|
".agents/skills/loop-agent/references/command-reference.md",
|
|
137
|
+
".agents/skills/loop-agent/references/source-and-plan-practice.md",
|
|
136
138
|
".agents/skills/loop-agent/references/docs-converge.md",
|
|
137
139
|
".agents/skills/ai-engineering-context/SKILL.md",
|
|
138
140
|
".agents/skills/verification-before-completion/SKILL.md",
|
|
@@ -212,6 +214,7 @@
|
|
|
212
214
|
".agents/skills/agent-worker/references/agent-worker-operator.md": "copied",
|
|
213
215
|
".agents/skills/loop-agent/SKILL.md": "copied",
|
|
214
216
|
".agents/skills/loop-agent/references/command-reference.md": "copied",
|
|
217
|
+
".agents/skills/loop-agent/references/source-and-plan-practice.md": "copied",
|
|
215
218
|
".agents/skills/loop-agent/references/docs-converge.md": "copied",
|
|
216
219
|
".agents/skills/ai-engineering-context/SKILL.md": "copied",
|
|
217
220
|
".agents/skills/verification-before-completion/SKILL.md": "copied",
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
".harness/dag-runs/**",
|
|
251
251
|
"artifacts/**"
|
|
252
252
|
],
|
|
253
|
-
"outputContract": "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md,
|
|
253
|
+
"outputContract": "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.",
|
|
254
254
|
"subtask_prompt": "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html 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.",
|
|
255
255
|
"shell": {
|
|
256
256
|
"commands": [
|
package/package.json
CHANGED
|
@@ -27,10 +27,12 @@ Entry: routing and hard rules. Required details come from frontmatter references
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
loop-agent new-task <task-id> "Title"
|
|
30
|
-
# Prefer
|
|
31
|
-
|
|
32
|
-
loop-agent dag
|
|
33
|
-
|
|
30
|
+
# Prefer import-prd when a PRD exists; plan create for non-trivial work
|
|
31
|
+
# (see references/source-and-plan-practice.md)
|
|
32
|
+
loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
33
|
+
# default draft: .harness/tasks/<task-id>/dag.json
|
|
34
|
+
loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
|
|
35
|
+
loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <repo-root>
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
执行前审阅 `profileRouting`、`governanceProfile`、writer `writeSet`、`forbiddenPaths`、decision gate mode。
|
|
@@ -44,10 +46,6 @@ loop-agent run-dag --dag <dag.json> --cwd <repo-root>
|
|
|
44
46
|
| Operator commands、`agent-worker` | `references/command-reference.md` |
|
|
45
47
|
| 独立验证、failure handling、closeout | `references/verification-and-failure-handling.md` |
|
|
46
48
|
|
|
47
|
-
## Source Layout
|
|
48
|
-
|
|
49
|
-
CLI `src/cli/`;DAG `src/workflows/dag/`;loop `src/workflows/loop/`;executors `src/executors/`;worker `src/worker/`;governance `src/governance/`。不新增平行兼容入口。
|
|
50
|
-
|
|
51
49
|
## Hard Rules
|
|
52
50
|
|
|
53
51
|
1. Use vertical tracer bullets across real integration layers;each needs independent acceptance and verification.
|
|
@@ -37,12 +37,20 @@ loop-agent doctor
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
loop-agent new-task <task-id> "Task Title"
|
|
40
|
-
|
|
41
|
-
loop-agent
|
|
42
|
-
|
|
40
|
+
# 有原始 PRD 文件时(推荐默认):
|
|
41
|
+
# loop-agent import-prd <task-id> --file <path-to-prd.md>
|
|
42
|
+
# 写 source/需求.md、执行约束.md,并同步 task.json 路径边界
|
|
43
|
+
# 非微小 / 跨会话(推荐默认):
|
|
44
|
+
# loop-agent plan create <plan-id> "<title>"
|
|
45
|
+
loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
46
|
+
loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
|
|
47
|
+
loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <repo-root>
|
|
48
|
+
# 有 plan 时收尾:loop-agent plan complete <plan-id> --summary "..."
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
省略 `--output` 时默认写入 `.harness/tasks/<task-id>/dag.json`(CLI JSON 的 `outputPath` / `defaultOutputPath` 为解析后的绝对路径)。显式 `--output` 仍可指向 temp 或任意路径(兼容旧脚本)。主路径 JSON 输出含稳定 summary:`dag run-task` 的 `message` 为 `DAG draft created`,`dag validate` 的 `message` 为 `DAG validation passed` 且含 `checks.writeSets` / `checks.decisionGates`,`run-dag` 的 `message` 为 `DAG run finished`。
|
|
52
|
+
|
|
53
|
+
**`import-prd` / `plan create` 不是 `dag run-task` 的硬依赖**,但有 PRD 文件或非微小实现时应默认使用;决策表、反模式与案例见 `source-and-plan-practice.md`。
|
|
46
54
|
2. **Operator 工具**,用于 recovery、诊断、评测重放与 closeout:
|
|
47
55
|
|
|
48
56
|
```bash
|
|
@@ -233,9 +241,9 @@ loop-agent --repo-root /path/to/target-repo study init <task-id> "Title" \
|
|
|
233
241
|
--reference-glob "codex-rs/**/goal*.rs"
|
|
234
242
|
|
|
235
243
|
# 编辑 source/需求.md + source/执行约束.md,然后走 DAG 路径:
|
|
236
|
-
loop-agent --repo-root <target-repo> dag run-task <task-id> --profile auto --strict-models --output
|
|
237
|
-
loop-agent --repo-root <target-repo> dag validate --dag
|
|
238
|
-
loop-agent --repo-root <target-repo> run-dag --dag
|
|
244
|
+
loop-agent --repo-root <target-repo> dag run-task <task-id> --profile auto --strict-models --output .harness/tasks/<task-id>/dag.json
|
|
245
|
+
loop-agent --repo-root <target-repo> dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
|
|
246
|
+
loop-agent --repo-root <target-repo> run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <target-repo>
|
|
239
247
|
```
|
|
240
248
|
|
|
241
249
|
目标 repo 需有 `.harness/prompts/feature-study-analyze.md` 与 `feature-study-plan.md`(缺失时从 loop-agent 复制)。
|
|
@@ -291,9 +299,10 @@ loop-agent run-dag --dag <temp-dir>/hybrid-dag.json --init-only --canvas-path <t
|
|
|
291
299
|
bash scripts/run-dag-safe.sh --dag <temp-dir>/hybrid-dag.json --cwd <repo-root> [--timeout-secs 7200] # 后台运行 + 轮询,避免外层 bash timeout 杀进程(见 agent-dag-runner.md §Adaptive liveness)
|
|
292
300
|
loop-agent dag init-hybrid <task-id> # 生成可审阅的 DAG draft
|
|
293
301
|
loop-agent dag run-task <task-id> # generate + validate(安全默认;无 dag-runs;standard-compatible)
|
|
294
|
-
loop-agent dag workflow-plan <task-id> --profile pr-review --output
|
|
295
|
-
loop-agent dag workflow-validate --workflow
|
|
296
|
-
loop-agent dag workflow-compile --workflow
|
|
302
|
+
loop-agent dag workflow-plan <task-id> --profile pr-review --output .harness/tasks/<task-id>/workflows/planned/<name>.workflow.json
|
|
303
|
+
loop-agent dag workflow-validate --workflow .harness/tasks/<task-id>/workflows/planned/<name>.workflow.json --strict-governance
|
|
304
|
+
loop-agent dag workflow-compile --workflow .harness/tasks/<task-id>/workflows/planned/<name>.workflow.json --output .harness/tasks/<task-id>/workflows/compiled/<name>.dag.json --manifest .harness/tasks/<task-id>/workflows/compiled/<name>.compile-manifest.json
|
|
305
|
+
# compiled workflow DAGs stay explicit; they do not implicitly replace task root dag.json
|
|
297
306
|
loop-agent dag run-task <task-id> --profile auto # 推断 governanceProfile,经 workflowPolicy.dag.profileRouting 路由
|
|
298
307
|
loop-agent dag run-task <task-id> --profile minimal # 选择 minimal 通用路由;standard 前端任务仍可自动使用前端 DAG
|
|
299
308
|
loop-agent dag run-task <task-id> --profile standard # 选择 standard 通用路由;standard 前端任务仍可自动使用前端 DAG
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
## Command surface tiers
|
|
16
16
|
|
|
17
17
|
| Tier | Default purpose | Commands |
|
|
18
|
-
|
|
18
|
+
| --- | --- | --- |
|
|
19
19
|
| Primary | Normal autonomous implementation(经 CLI) | `new-task` -> `dag run-task --profile auto` -> `dag validate --strict-models --strict-governance` -> `run-dag` |
|
|
20
20
|
| Operator | Diagnose, recover, close out, inspect facts | `status`, `instructions`, `dag status`, `dag doctor`, `dag report`, `dag reconcile-run`, `dag closeout-draft`, `dag reconcile-tasks`, `dag final-verification`, `inspect`, `doctor`, `spine audit`, `knowledge curate`, `docs audit`, `handoff check`, `loop-benchmark`;Inspect:`agent-worker console serve`(`/inspect/`)与兼容 `observe serve\|snapshot` |
|
|
21
21
|
| Compatibility (CLI helpers) | Legacy task metadata and feature-study helpers | `goal`, `reference`, `study` |
|
|
@@ -54,7 +54,7 @@ Minimum governed path:
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
loop-agent new-task <task-id> "Task Title" [--repo-root <target-repo>]
|
|
57
|
-
#
|
|
57
|
+
# Prefer import-prd for PRD files; plan create for non-trivial work (source-and-plan-practice.md)
|
|
58
58
|
# loop-agent import-prd <task-id> --file <path-to-original-prd.md> [--repo-root <target-repo>]
|
|
59
59
|
# write derived <target-repo>/.harness/tasks/<task-id>/source/需求.md
|
|
60
60
|
# write <target-repo>/.harness/tasks/<task-id>/source/执行约束.md
|
|
@@ -62,16 +62,16 @@ loop-agent new-task <task-id> "Task Title" [--repo-root <target-repo>]
|
|
|
62
62
|
loop-agent dag run-task <task-id> \
|
|
63
63
|
--profile auto \
|
|
64
64
|
--strict-models \
|
|
65
|
-
--output
|
|
65
|
+
--output .harness/tasks/<task-id>/dag.json \
|
|
66
66
|
[--repo-root <target-repo>]
|
|
67
67
|
|
|
68
68
|
loop-agent dag validate \
|
|
69
|
-
--dag
|
|
69
|
+
--dag .harness/tasks/<task-id>/dag.json \
|
|
70
70
|
--strict-models \
|
|
71
71
|
--strict-governance
|
|
72
72
|
|
|
73
73
|
loop-agent run-dag \
|
|
74
|
-
--dag
|
|
74
|
+
--dag .harness/tasks/<task-id>/dag.json \
|
|
75
75
|
--cwd <target-repo>
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -81,7 +81,7 @@ The npm package carries static capability assets: `skills/` (bundled in-package,
|
|
|
81
81
|
|
|
82
82
|
For arbitrary target repositories, DAG skill instructions must not depend on loop-agent source history being copied into the target repo. Resolve configured, user, or target-local skills when present, then fall back to the package-bundled `skills/` (mirrored as `.agents/skills/` in the target project) as the stable default capability set.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Default DAG draft: `.harness/tasks/<task-id>/dag.json`. Explicit `--output` to temp remains an escape hatch.
|
|
85
85
|
|
|
86
86
|
Execution 前 review `dag run-task` JSON / `reviewPacket`:
|
|
87
87
|
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
|
|
19
19
|
前端专用链保留独立 contract/scout;plan 同时选择 Mock/API 策略并输出结构化 implementation contract。design initial pass 直接使用原计划,只有 request-revision 才运行 revision/final review;small-risk 只执行一次 design review。`frontend-prewrite-gate-shell` 合并生效 verdict、REQ/BR/AC 覆盖、Mock policy 和 contract 物化,是唯一写入授权。实现后 `frontend-verify-assess-shell` 合并 Mock/static/behavior/trace/assessment;只有 `eligible=true` 才运行同 writeSet 的 repair 和 `frontend-reverify-shell`。`frontend-review-context-shell` 绑定真实 diff 与有效验证证据后再 review/closeout。standard/high-risk 为 15 个顶层节点,small-risk 为 13;绿色路径执行 11 个节点、7 次 Pi。生成期 blocked Mock 只生成一个确定性阻塞节点且没有 writer。
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
> Backend-test Markdown-first:先由确定性环境 Shell 检查 clean env 中 Python/pytest、常见配置、conftest/fixture、test root、server entry 和 HTML renderer,失败时不消耗模型调用。随后 Pi 生成中文 README 索引与模块用例卡片并独立 Review `testcase/md/**`。第 4 节点只把前置条件、操作步骤、预期结果作为必选章节,并检查 Case ID、业务 AC、步骤/预期和占位措辞;不校验需求来源引用有效性或 Markdown sensitive-shaped 内容。pytest writer 为每次真实接口调用记录脱敏、有界的请求 method/URL/参数摘要和响应 status/body 摘要。第 6 节点只扫描每条 Case 明确映射的 pytest 脚本,同时支持模块级函数和 pytest class 方法,并把缺少请求/响应日志、递归脱敏或有界截断证据记录为 advisory。第 4/6 节点均写 PASS/FAIL findings 而不阻断后续;pytest 仍只运行一次,生成 JUnit,并把 Markdown 名称/场景/脚本映射与同一 JUnit 合成为按测试概览、质量校验、失败概览、用例执行明细和技术证据组织的中文 self-contained HTML 与 Markdown facts。最终 Pi 按固定简洁结构汇总 advisory 状态、执行事实和 L-5 结论。active 流程不要求模型生成 backend-test 业务 JSON。
|
|
23
22
|
|
|
24
23
|
显式专用 `taskKind` 保持兼容并优先于任务源分类。`backend-test` 选择固定 **8 个真实顶层节点**的 Markdown-first DAG:环境硬门、Markdown cases、独立 Review/修订、第 4 节点 advisory Markdown 校验、pytest 转换、第 6 节点 scoped advisory traceability、单次 pytest + JUnit/HTML/facts、最终 Markdown 报告与 L-5。历史 JSON contract/materializer 可继续读取旧 DAG,但新 runtime/template 不再生成模型业务 JSON。`knowledge-sync` 与 `knowledge-graph-bootstrap` 继续通过各自显式 taskKind 选择知识回写/图谱开荒 DAG。治理等级仍由 `minimal|standard|reviewed|supervised` 推断。
|
|
@@ -106,7 +105,7 @@ contract-pi → scout-src ∥ scout-tests → plan-pi → write-set-audit-pi
|
|
|
106
105
|
```
|
|
107
106
|
|
|
108
107
|
| Stage | 减少 operator 中途介入的方式 |
|
|
109
|
-
|
|
108
|
+
| ------- | ------------------------------ |
|
|
110
109
|
| `write-set-audit-pi` | implement 前捕获 missing/overlapping writeSet owner;第一条非空行必须是 canonical verdict |
|
|
111
110
|
| `write-set-audit-format-repair-pi` / `final-write-set-audit-format-repair-pi` | 初审和终审各有一次只读格式恢复;只补 canonical verdict/结构并保留 findings,结论不明确时返回 `request-revision`,不得从一般正文猜 pass |
|
|
112
111
|
| `write-set-format-gate-shell` | 接受格式有效的 pass/request-revision,让有界计划修订继续;本 gate 不授权写入 |
|
|
@@ -133,8 +132,8 @@ Prompt invariant:`ai_workspace/loop-agent/templates/agent-dag-process-supervis
|
|
|
133
132
|
### Level 3 task-to-DAG(`dag init-hybrid` / `dag run-task`)
|
|
134
133
|
|
|
135
134
|
```bash
|
|
136
|
-
loop-agent dag init-hybrid <task-id> [--output
|
|
137
|
-
loop-agent dag run-task <task-id> [--output
|
|
135
|
+
loop-agent dag init-hybrid <task-id> [--output .harness/tasks/<task-id>/dag.json]
|
|
136
|
+
loop-agent dag run-task <task-id> [--output .harness/tasks/<task-id>/dag.json] # 安全默认:仅 generate + validate,standard-compatible
|
|
138
137
|
loop-agent dag run-task <task-id> --profile auto # 推断 candidate governanceProfile,再经 workflowPolicy 路由
|
|
139
138
|
loop-agent dag run-task <task-id> --profile minimal # 选择 minimal 通用路由;standard 前端任务可自动使用前端 DAG
|
|
140
139
|
loop-agent dag run-task <task-id> --profile standard # 选择 standard 通用路由;standard 前端任务可自动使用前端 DAG
|
|
@@ -152,7 +151,9 @@ loop-agent dag run-task <task-id> --init-only --cwd <repo-root>
|
|
|
152
151
|
|
|
153
152
|
### DAG 与 artifacts source-of-truth 规则
|
|
154
153
|
|
|
155
|
-
-
|
|
154
|
+
- Canonical task DAG draft: `.harness/tasks/<task-id>/dag.json` (`dag run-task` / `init-hybrid` default).
|
|
155
|
+
- Worker per-run snapshot: `artifacts/<workerRunId>-dag.json`; compiled workflows stay under `workflows/compiled/` and need explicit `--dag`.
|
|
156
|
+
- Platform temp is only an explicit `--output` escape hatch. Reusable templates live in `examples/` or `ai_workspace/loop-agent/templates/`.
|
|
156
157
|
- **不要**在 `.harness/dag-runs/active/` root 保留手写 DAG input 副本。
|
|
157
158
|
- **不要**把 `.harness/dag-runs/` 内容 commit 到 git。
|
|
158
159
|
- canonical per-run DAG 历史是 `.harness/dag-runs/completed/<run-id>/run.json` 及该 run 目录的 `state.json`、`executor.jsonl`、node artifacts;新建 run directory 使用 `YYYYMMDD-<slug>`。
|
|
@@ -168,7 +169,7 @@ loop-agent dag run-task <task-id> --init-only --cwd <repo-root>
|
|
|
168
169
|
Pi reviewer node 设 `decisionGate.enabled: true` 时,runner 从 persisted `assistantText`(优先)或 `result.summary.md`(fallback)解析 **恰好一个** info string 为 `DECISION_ENVELOPE_JSON` 的 fenced block。Schema:`ai_workspace/loop-agent/templates/agent-dag-decision-envelope.schema.json`。Prompt:`ai_workspace/loop-agent/templates/agent-dag-decision-gate.prompt.md`(含 §Recovery Recommendation Consumption 与 schema-adherence 硬规则:勿发明 envelope schema、勿用 `decision: accept`、勿加 extra root key、`audit.runId` 须绑定当前 run id,并填 `audit.nodeId` / `audit.model`)。Playbook:`ai_workspace/loop-agent/agent-dag-recovery-playbook.md`。示例 DAG:`examples/decision-gate-agent-dag.json`。
|
|
169
170
|
|
|
170
171
|
| Milestone | `decisionGate.mode` | 行为 |
|
|
171
|
-
|
|
172
|
+
| ----------- | --------------------- | ---------- |
|
|
172
173
|
| **M3 record-only** | `record-only`(默认) | 写 `<node-id>/decision.envelope.json` + node record summary;**不 pause**,**不** branch 于 `decision`/`nextAction` |
|
|
173
174
|
| **M4 pause-on-human** | `pause-on-human` | parse OK 且 `requiresHuman=true`:run `status=paused`,移入 `.harness/dag-runs/paused/<run-id>/`,写 `human-escalation.json` / `.md`;downstream node 保持 `PENDING` |
|
|
174
175
|
| **M5 CLI** | (M4 pause 后) | 确定性 human decision — **无 LLM**,**无** `executor: human` / `executor: decision` |
|
|
@@ -228,7 +229,7 @@ review-heavy DAG 中长 shell stdout 可能掩盖 proof 时,用 **evidence-sum
|
|
|
228
229
|
> 与 `ai_workspace/loop-agent/agent-dag-runner.md` §「Agent DAG authoring checklist」相同。Calibration reports:`ai_workspace/loop-agent/reports/2026-06-08-agent-dag-practice-p1-evidence-summary-docs.md` … `p5-handoff-closeout-20260608.md`。
|
|
229
230
|
|
|
230
231
|
| # | Check | Expect |
|
|
231
|
-
|
|
232
|
+
| --- | ------- | -------- |
|
|
232
233
|
| 1 | Topology | 优先 same-rank parallel read-only scout/review;仅 output 真正需要时加 `depends_on` |
|
|
233
234
|
| 2 | Executor | 每个 task 显式声明 `executor`;`defaults.executor` 是 schema metadata,非 runtime fallback |
|
|
234
235
|
| 3 | Model routing | 用 node `complexity` + `executorModels`;Pi MED(`glm-5.2`)quota 耗尽时提高 Pi node 到 `complexity: HIGH`(`gpt-5.5`)— 勿 mutate canonical model matrix |
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
## 实现后处理
|
|
6
6
|
|
|
7
7
|
DAG run、promotion、closeout 和最终验证完成后:
|
|
8
|
+
|
|
8
9
|
1. 检查 git diff 确认预期变更
|
|
9
10
|
2. 用 git commit 并 push
|
|
10
11
|
3. 用户说「继续」时,跑 `status` 看 task 是否 `completed`,再创建下一个 task
|
|
@@ -12,32 +13,41 @@ DAG run、promotion、closeout 和最终验证完成后:
|
|
|
12
13
|
## 常见模式
|
|
13
14
|
|
|
14
15
|
### 处理多 item PRD
|
|
16
|
+
|
|
15
17
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
0. plan create <feature-plan> "…"(整份 PRD 一个 plan,多 task 共用)
|
|
19
|
+
1. new-task <id>-r1 → import-prd(同一 PRD 或切片说明)→ 派生 需求.md → dag … → promote/closeout
|
|
20
|
+
2. new-task <id>-r2 → 重复
|
|
21
|
+
3. plan complete <feature-plan> --summary "…"
|
|
19
22
|
```
|
|
20
23
|
|
|
24
|
+
详见 `source-and-plan-practice.md` 案例 A/C。
|
|
25
|
+
|
|
21
26
|
### Bounded task 路径
|
|
27
|
+
|
|
22
28
|
```
|
|
23
29
|
1. new-task <id>
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
26
|
-
4. dag
|
|
27
|
-
5.
|
|
28
|
-
6.
|
|
30
|
+
2. 有 PRD 文件:import-prd;再写 source/需求.md + source/执行约束.md
|
|
31
|
+
3. 非微小:plan create(或挂到已有 active plan)
|
|
32
|
+
4. dag run-task <id> --profile auto --strict-models
|
|
33
|
+
5. dag validate --dag .harness/tasks/<id>/dag.json --strict-models --strict-governance
|
|
34
|
+
6. run-dag --dag .harness/tasks/<id>/dag.json --cwd <repo-root>
|
|
35
|
+
7. promote-run / closeout / final verification
|
|
36
|
+
8. 有 plan:plan complete
|
|
29
37
|
```
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
默认 DAG 草稿:`.harness/tasks/<id>/dag.json`。何时可跳过 import/plan:见 `source-and-plan-practice.md`。
|
|
32
40
|
|
|
33
41
|
**关键**:实现 work 由 DAG node 和 executor 执行;main session 负责审 DAG、审 writeSet、跑验证和 handoff。
|
|
34
42
|
|
|
35
43
|
### 跨所有 task 快速 status
|
|
44
|
+
|
|
36
45
|
```bash
|
|
37
46
|
loop-agent stats
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
### 处理 pi timeout
|
|
50
|
+
|
|
41
51
|
- 默认每 step timeout 现为 30 分钟 — 通常足够
|
|
42
52
|
- pi step 仍 timeout 时,用 `stats` 看 duration 分布
|
|
43
53
|
- Bash tool call 应用宽松 timeout:analyze/plan 300s,implement 480s+,verify/retrospective 240s+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Source 与 Exec-plan 最佳实践
|
|
2
|
+
|
|
3
|
+
很多用户只跑「主路径 DAG」四步,**漏掉** `import-prd` 与 `plan create`。二者**不是** `dag run-task` 的硬依赖,但在「有原始 PRD / 非微小实现」场景下应成为默认纪律。本文给出**何时用、何时可跳、命令顺序与案例**。
|
|
4
|
+
|
|
5
|
+
相关命令细节:`command-reference.md`(import-prd / plan / status)。
|
|
6
|
+
Task 目录布局:`task-workflow.md`。
|
|
7
|
+
仓库治理全文:目标仓 `governanceRoot` 下 `feature-workflow.md`(若有)。
|
|
8
|
+
|
|
9
|
+
## 先分清三层
|
|
10
|
+
|
|
11
|
+
| 层 | 命令 / 路径 | 作用 | 主路径 DAG 是否强制 |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| Source 事实 | `import-prd` → `source/references/*` + `source-manifest.json` | 原始 PRD **不可变**归档 | 否(有 PRD 文件时**强烈推荐**) |
|
|
14
|
+
| Source 契约 | 手写 `source/需求.md`、`执行约束.md` + `task.json` 路径边界 | DAG 生成与验收真源 | **是**(至少 `需求.md`) |
|
|
15
|
+
| Exec-plan | `plan create` / `plan complete` / `plan check` | 仓库级计划索引与交接 | 否(**非微小**推荐) |
|
|
16
|
+
| DAG 运行时 | `dag run-task` → `dag validate` → `run-dag` | 可执行编排 | **是**(常规实现) |
|
|
17
|
+
|
|
18
|
+
规则记忆:
|
|
19
|
+
|
|
20
|
+
1. **`new-task` 不自动 import-prd,也不自动绑 plan。**
|
|
21
|
+
2. **`dag run-task` 主要消费派生 `需求.md`**;会校验 plan **索引一致性**,但不要求当前 task 已有 active plan。
|
|
22
|
+
3. 冲突时:**`source/references/*`(原始)> 派生 `需求.md` > 聊天口述。**
|
|
23
|
+
|
|
24
|
+
## 决策:何时 `import-prd`
|
|
25
|
+
|
|
26
|
+
### 应该用(默认「有就 import」)
|
|
27
|
+
|
|
28
|
+
- 用户给了独立 PRD / 需求文档 / 设计说明文件(`.md` / 导出稿)。
|
|
29
|
+
- 多轮对话已把需求定稿,且希望 **hash 可追溯**(避免模型把原文改成「唯一 source」)。
|
|
30
|
+
- Worker / 多人协作:后续 review 必须三方对照 references + 需求.md + 实现。
|
|
31
|
+
- 目标仓 `ai_workspace/loop-agent/` 或 `docs/` 里已有权威 PRD/spec,任务只是执行切片。
|
|
32
|
+
|
|
33
|
+
### 可以跳过 import-prd
|
|
34
|
+
|
|
35
|
+
- 真正微小:单文件 typo、一行配置、纯脚本命令说明,**没有**独立需求文档。
|
|
36
|
+
- 用户只在聊天里给了 3~5 条验收点,且你当场写入 `需求.md` 并标明「来源:会话 YYYY-MM-DD」(仍建议短小、可验证)。
|
|
37
|
+
- 已有 task 的 `source/references/` 与 manifest 完好,本次只是 re-run DAG。
|
|
38
|
+
|
|
39
|
+
### 反模式
|
|
40
|
+
|
|
41
|
+
- 把用户 PRD **整篇改写**进 `需求.md` 后删掉原文路径。
|
|
42
|
+
- AI 直接 edit `source/references/*`。
|
|
43
|
+
- 只把 PRD 路径写在聊天里,不落盘到 task 容器。
|
|
44
|
+
|
|
45
|
+
### 推荐顺序(有 PRD 时)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
loop-agent new-task <task-id> "简短标题"
|
|
49
|
+
loop-agent import-prd <task-id> --file <path-to-original-prd.md> [--json]
|
|
50
|
+
# 再写派生契约(映射 REQ/AC 或原文锚点)
|
|
51
|
+
# .harness/tasks/<task-id>/source/需求.md
|
|
52
|
+
# .harness/tasks/<task-id>/source/执行约束.md
|
|
53
|
+
# 同步 task.json.allowedPaths / forbiddenPaths
|
|
54
|
+
loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
55
|
+
loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
|
|
56
|
+
loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <repo-root>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`import-prd` 后用 `status` / `instructions source` 确认 source readiness,再生成 DAG。
|
|
60
|
+
|
|
61
|
+
## 决策:何时 `plan create`
|
|
62
|
+
|
|
63
|
+
### 应该用(默认「非微小就 create」)
|
|
64
|
+
|
|
65
|
+
- 跨多文件 / 多模块、需要 **Contract → 分块 → 验证 → 交接** 的实现。
|
|
66
|
+
- 预计超过一次会话,或要交给其他 agent / 人继续。
|
|
67
|
+
- 会改 CLI、runtime、init 投影、发布面、治理脚本等 **高 blast-radius** 区域。
|
|
68
|
+
- 团队要求 active plan 进 `docs/exec-plans/`(或目标仓 `governanceRoot/exec-plans/`)索引。
|
|
69
|
+
- 本仓库 `AGENTS.md` 对「非微小实现」要求 plan + DAG 时。
|
|
70
|
+
|
|
71
|
+
### 可以跳过 plan create
|
|
72
|
+
|
|
73
|
+
- 真正 one-shot:路径已知、验收一条命令、无长期决策要记。
|
|
74
|
+
- 纯 operator 维护:改一个 README 链接、修索引笔误(仍应验证,但不必开 plan)。
|
|
75
|
+
- 已有 **进行中** active plan 覆盖同一工作块:更新该 plan,而不是平行再开一个同题 plan。
|
|
76
|
+
|
|
77
|
+
### 与 task 的关系
|
|
78
|
+
|
|
79
|
+
- **一个 plan 可覆盖多个 task**;**一个 task 也不强制 1:1 plan**。
|
|
80
|
+
- plan id 建议 `YYYY-MM-DD-<slug>`;task id 可更短(业务切片名)。
|
|
81
|
+
- 完成后:`plan complete <plan-id> --summary "..."`(或目标仓等价流程),不要只关 task 忘了 plan 索引。
|
|
82
|
+
|
|
83
|
+
### 推荐顺序(非微小 + 有 PRD)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
loop-agent plan create <plan-id> "<title>"
|
|
87
|
+
# 在 plan 中写清范围、非目标、验证、允许路径(人类/agent 共读)
|
|
88
|
+
loop-agent new-task <task-id> "切片标题"
|
|
89
|
+
loop-agent import-prd <task-id> --file <prd>
|
|
90
|
+
# 写 需求.md / 执行约束.md / task.json 边界
|
|
91
|
+
loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
92
|
+
# … validate → run-dag → promote-run → closeout …
|
|
93
|
+
loop-agent plan complete <plan-id> --summary "结果导向摘要 + 验证证据"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
微小 escape hatch(须在 handoff / plan 或 task 备注写明边界):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
loop-agent new-task <task-id> "微小修复"
|
|
100
|
+
# 直接 需求.md(可选 import-prd)
|
|
101
|
+
loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
102
|
+
# …
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 实践案例
|
|
106
|
+
|
|
107
|
+
### 案例 A — 用户丢来一份 PRD 文件(默认完整路径)
|
|
108
|
+
|
|
109
|
+
**信号**:`帮我按这个 PRD 实现…` + 附件/路径。
|
|
110
|
+
**做法**:非微小则先 `plan create`(或复用 active plan)→ `new-task` → **`import-prd`** → 派生 `需求.md`(目标/非目标/AC,锚回 references)→ 路径边界 → DAG。
|
|
111
|
+
**验收**:`source/references/` 有原文;manifest hash 在;review 能三方对照。
|
|
112
|
+
|
|
113
|
+
### 案例 B — 聊天里三句话小需求(可跳过 import + plan)
|
|
114
|
+
|
|
115
|
+
**信号**:`把 X 按钮文案改成 Y`,单文件。
|
|
116
|
+
**做法**:`new-task` → 短 `需求.md`(写清唯一验收)→ `dag run-task`…
|
|
117
|
+
**不要**:为了「流程完整」空跑 `import-prd`(无文件)或堆一个空洞 plan。
|
|
118
|
+
|
|
119
|
+
### 案例 C — 源仓改 CLI 默认行为(必须 plan,PRD 视情况)
|
|
120
|
+
|
|
121
|
+
**信号**:行为变更、CHANGELOG、skills/init 多表面。
|
|
122
|
+
**做法**:**`plan create`** 先冻结契约与工作块 → 按块 `new-task` → 有设计文档则 import → DAG → 定向验证 → `plan complete`。
|
|
123
|
+
**验收**:active/completed 索引与 plan 正文一致;`plan check` / `dag run-task` 索引 preflight 不红。
|
|
124
|
+
|
|
125
|
+
### 案例 D — agent-worker / Feature 已 materialize source_docs
|
|
126
|
+
|
|
127
|
+
**信号**:TaskSpec 已把 `source_docs` 拷进 `source/references/`。
|
|
128
|
+
**做法**:通常 **不必再 import-prd** 同一文件;检查 references + 派生 `需求.md` 顶部「冲突以 references 为准」→ 补边界 → DAG。
|
|
129
|
+
**仍建议**:产品线级大功能在 Feature / 仓库层有 plan 或 Feature Packet 记录。
|
|
130
|
+
|
|
131
|
+
### 案例 E — 用户说「loop-agent 帮我完成 XXX」无附件
|
|
132
|
+
|
|
133
|
+
**信号**:强路由进 DAG,但无 PRD 路径。
|
|
134
|
+
**做法**:主会话 **先问清**是否有 PRD 文件;有则 import;无则把会话共识写入 `需求.md` 并标来源 → 判断微小 vs 非微小决定是否 `plan create` → 再 `dag run-task`。
|
|
135
|
+
**禁止**:主会话直接写业务代码代替 DAG。
|
|
136
|
+
|
|
137
|
+
## 宿主 agent 检查清单(编排时)
|
|
138
|
+
|
|
139
|
+
在第一次 `dag run-task` 前快速自问:
|
|
140
|
+
|
|
141
|
+
1. 是否存在用户/仓库原始需求文件?→ **有则 `import-prd`**。
|
|
142
|
+
2. `source/需求.md` 是否含目标、非目标、可验证验收?→ **无则先写**。
|
|
143
|
+
3. `allowedPaths` / `forbiddenPaths` 是否已结构化?→ **无则先写**。
|
|
144
|
+
4. 是否非微小 / 跨会话 / 高 blast-radius?→ **`plan create` 或更新已有 active plan**。
|
|
145
|
+
5. 是否仅聊天约束?→ **落盘到 source 或 plan**,不要只留在对话。
|
|
146
|
+
|
|
147
|
+
## 与「主路径 DAG」文档的关系
|
|
148
|
+
|
|
149
|
+
`SKILL.md` / `command-reference.md` 的 **主路径 DAG** 仍是最短 runtime 闭环(便于抄命令)。
|
|
150
|
+
**系统化默认**应读作:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
[非微小?] plan create(或复用 active plan)
|
|
154
|
+
new-task
|
|
155
|
+
[有 PRD?] import-prd
|
|
156
|
+
派生 需求.md / 执行约束.md / task.json 路径边界
|
|
157
|
+
dag run-task → validate → run-dag → promote/closeout
|
|
158
|
+
[有 plan?] plan complete
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
不要把 `import-prd` / `plan create` 伪造成「无文件也必须执行」的假步骤;用本节决策表选择,而不是一律省略。
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
loop-agent new-task <task-id> "Task Title"
|
|
11
|
-
loop-agent
|
|
12
|
-
loop-agent
|
|
13
|
-
loop-agent run-
|
|
11
|
+
# 有原始 PRD:loop-agent import-prd <task-id> --file <prd>
|
|
12
|
+
# 非微小:loop-agent plan create <plan-id> "<title>"(可与 task 解耦,见 source-and-plan-practice.md)
|
|
13
|
+
loop-agent dag run-task <task-id> --profile auto --strict-models
|
|
14
|
+
loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
|
|
15
|
+
loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <repo-root>
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
默认 DAG 草稿为 `.harness/tasks/<task-id>/dag.json`。repo-relative 示例用 `/`;Windows 由 Node 解析本地路径。何时必须 `import-prd` / `plan create`:见 `source-and-plan-practice.md`。
|
|
17
19
|
|
|
18
20
|
当目标仓库是 loop-agent 本仓库时,`loop-agent` 命令必须来自 npm 上已发布的安装包。首次安装或有意升级可用 `@tea-agent/loop-agent@latest`,但一次自举任务启动后不要中途升级控制器,并记录 `npm list -g @tea-agent/loop-agent --depth=0` 显示的实际版本。不要用当前工作区的 `npm link` 或 `npm run dev` 控制会改动 CLI、DAG runtime、executor、package metadata 或 build output 的任务;源码开发和 focused debugging 才使用 `npm run dev -- <args>`。
|
|
19
21
|
|
|
@@ -33,10 +35,11 @@ loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd <repo-root>
|
|
|
33
35
|
task.json
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
- 用户原始 PRD 用 `loop-agent import-prd <task-id> --file <prd>` 归档到 `source/references/`,禁止 AI
|
|
38
|
+
- 用户原始 PRD 用 `loop-agent import-prd <task-id> --file <prd>` 归档到 `source/references/`,禁止 AI 改写。**有文件就 import**;无独立文件才允许只写 `需求.md`(见 `source-and-plan-practice.md` 决策表)。
|
|
37
39
|
- `需求.md` 是派生执行契约:写清目标、验收标准、非目标,并用 REQ/AC 或原文锚点映射回 references。
|
|
38
40
|
- `执行约束.md` 写清允许文件、禁止改动、硬约束和验证命令。
|
|
39
41
|
- 若 `ai_workspace/loop-agent/` 已有权威 plan/spec/PRD,优先 `import-prd` 复制,再在 `需求.md` 引用;避免把长 PRD 直接改写成唯一 source。
|
|
42
|
+
- 仓库级 exec-plan(`plan create`)与 harness task **解耦**:非微小实现应有 plan 或复用 active plan;微小任务可不建 plan。
|
|
40
43
|
- Worker / TaskSpec materialize 路径会把 `source_docs` 复制到 `source/references/`,并在派生 `需求.md` 顶部声明“冲突以 references 为准”;`acceptance_refs` 应展开为短摘要而不只写 ID。
|
|
41
44
|
- review 节点必须三方对照:`source/references/*`(尤其 requirement/acceptance)、派生 `需求.md`、以及实现/验证证据。
|
|
42
45
|
|