@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 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 <temp-dir>/example-task-dag.json
67
+ --output .harness/tasks/example-task/dag.json
68
68
 
69
69
  loop-agent dag validate \
70
- --dag <temp-dir>/example-task-dag.json \
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 <temp-dir>/example-task-dag.json \
75
+ --dag .harness/tasks/example-task/dag.json \
76
76
  --cwd .
77
77
  ```
78
78
 
79
- `<temp-dir>` 应使用当前平台的原生临时目录。详细步骤见[第一次运行](website/docs/quick-start/first-run.md)和 [Agent DAG 工作流](website/docs/guides/agent-dag.md)。
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 { defaultHybridDagOutputPath, initHybridDagFromTask, } from "../../workflows/dag/init-hybrid.js";
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
- const raw = JSON.parse(await readFile(dagPath, "utf-8"));
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
- const spec = parseDagSpec(JSON.parse(await readFile(input.dagPath, "utf-8")));
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 && candidateResult.template === "frontend-implementation") {
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: defaultHybridDagOutputPath(parsed.taskId),
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 { defaultHybridDagOutputPath, initHybridDagFromTask, } from "../workflows/dag/init-hybrid.js";
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: defaultHybridDagOutputPath(parsed.taskId),
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 = path.join(os.tmpdir(), `${taskId}-dag.json`);
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
- ` loop-agent dag run-task ${taskId} --profile auto --strict-models --output ${JSON.stringify(dagPath)}`,
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 dagPath = path.join(os.tmpdir(), `${parsed.taskId}-delegate-dag.json`);
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: result.worktreePath,
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: result.worktreePath,
115
+ cwd: worktreeRoot,
116
116
  initOnly: false,
117
117
  dryRun: false,
118
118
  });
@@ -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 ['\\\"][^'\\\"]*\${forbidden_dir}/|import\\(['\\\"][^'\\\"]*\${forbidden_dir}/|require\\(['\\\"][^'\\\"]*\${forbidden_dir}/" "\${file}"; then
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 --output <temp-dir>/<task-id>-dag.json",
860
- "loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance",
861
- "loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd .",
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 --output <temp-dir>/<task-id>-dag.json",
1822
- "loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance",
1823
- "loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd .",
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 --output <temp-dir>/<task-id>-dag.json",
2054
- "loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance",
2055
- "loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd .",
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.prototype.hasOwnProperty.call(harness.executors.pi, "requiresApiKey")) {
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: defaultHybridDagOutputPath(taskId),
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: null,
665
+ coverage: coverageContract,
636
666
  criticalRiskCount: parsed.failed + parsed.errors > 0 ? 1 : 0,
637
667
  });
638
668
  const failureSummaries = parsed.cases
@@ -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 { defaultHybridDagOutputPath } from "../workflows/dag/init-hybrid.js";
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 --output ${input.dagDraftPath}`,
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 = defaultDagDraftPath(taskId);
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;
@@ -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 raw = await readFile(getTaskPaths(repoRoot, taskId).taskConfigPath, 'utf-8');
181
- return parseTaskConfigForRuntime(JSON.parse(raw));
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 --output /tmp/${taskId}-dag.json`;
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, eventCtx) {
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
- const spec = JSON.parse(raw);
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, writeFile, } from "node:fs/promises";
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, and a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
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 writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
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 ?? defaultHybridDagOutputPath(taskId);
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
- : path.join(dagEvidenceDir, `round-${round}.dag.json`);
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, and a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON); exit 0/1 with valid evidence continues.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -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 immutable PRD: loop-agent import-prd <task-id> --file <path>
31
- loop-agent dag run-task <task-id> --profile auto --strict-models --output <dag.json>
32
- loop-agent dag validate --dag <dag.json> --strict-models --strict-governance
33
- loop-agent run-dag --dag <dag.json> --cwd <repo-root>
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
- loop-agent dag run-task <task-id> --profile auto --strict-models --output <temp-dir>/<task-id>-dag.json
41
- loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance
42
- loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd <repo-root>
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
- `<temp-dir>` 表示平台原生临时目录;也可以省略 `--output`,再使用命令 JSON 输出里的 `outputPath`。主路径 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`。
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 <temp-dir>/<task-id>-dag.json
237
- loop-agent --repo-root <target-repo> dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance
238
- loop-agent --repo-root <target-repo> run-dag --dag <temp-dir>/<task-id>-dag.json --cwd <target-repo>
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 <temp-dir>/<task-id>.workflow.json
295
- loop-agent dag workflow-validate --workflow <temp-dir>/<task-id>.workflow.json --strict-governance
296
- loop-agent dag workflow-compile --workflow <temp-dir>/<task-id>.workflow.json --output <temp-dir>/<task-id>-dag.json --manifest <temp-dir>/<task-id>-compile-manifest.json
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
- # optional but recommended for user PRDs:
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 <temp-dir>/<task-id>-dag.json \
65
+ --output .harness/tasks/<task-id>/dag.json \
66
66
  [--repo-root <target-repo>]
67
67
 
68
68
  loop-agent dag validate \
69
- --dag <temp-dir>/<task-id>-dag.json \
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 <temp-dir>/<task-id>-dag.json \
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
- `<temp-dir>` means the platform-native temp directory. Use native paths for actual `--output`, `--dag`, and `--cwd` values on macOS and Windows; use `/` only for stable repo refs, JSON/Markdown evidence refs, and glob conventions.
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 <temp-dir>/<task-id>-hybrid-dag.json]
137
- loop-agent dag run-task <task-id> [--output <temp-dir>/<task-id>-hybrid-dag.json] # 安全默认:仅 generate + validate,standard-compatible
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
- - 临时 DAG input spec 放平台原生临时目录(例如 `<temp-dir>/<topic>-dag.json`);可复用 template `examples/` `ai_workspace/loop-agent/templates/`。
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
- 1. new-task <id>-r1 → 准备 source dag run-task → dag validate → run-dag → promote/closeout
17
- 2. new-task <id>-r2 重复
18
- 3. ...
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. source/需求.md + source/执行约束.md
25
- 3. dag run-task <id> --profile auto --strict-models --output <temp-dir>/<id>-dag.json
26
- 4. dag validate --dag <temp-dir>/<id>-dag.json --strict-models --strict-governance
27
- 5. run-dag --dag <temp-dir>/<id>-dag.json --cwd <repo-root>
28
- 6. promote-run / closeout / final verification
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
- `<temp-dir>` 表示平台原生临时目录;实际命令中使用 macOS/Windows 本机路径。
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 dag run-task <task-id> --profile auto --strict-models --output <temp-dir>/<task-id>-dag.json
12
- loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance
13
- loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd <repo-root>
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
- `<temp-dir>` 表示平台原生临时目录。实际文件路径必须兼容 macOS Windows;只有 repo refs、JSON/Markdown 证据 refs glob 约定默认使用 `/`。
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