@tea-agent/loop-agent 0.25.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.25.2] - 2026-07-30
6
+
7
+ ### 重点更新
8
+
9
+ - 加固后端测试中文 HTML 报告的用例关联与解析逻辑,提升报告的准确性与可读性
10
+
11
+ ### 改进
12
+
13
+ - 改进基于 class 的 pytest node id 解析,准确提取真实脚本路径,避免显示为 unknown.py
14
+ - 当用例名称缺失后端 ID 时,支持按函数名与 Markdown catalog 进行回退关联
15
+ - 规范化 [REQ]/[RESP] HTTP 日志块,增强日志兼容性
16
+
17
+ ### 修复
18
+
19
+ - 修复成功执行路径下原生 pytest-html 报告未正确保留的问题,现会将其另存为 backend-test-pytest-native.html
20
+
21
+ ## [0.25.1] - 2026-07-30
22
+
23
+ ### 重点更新
24
+
25
+ - 后端测试 DAG 节点新增 JaCoCo 覆盖率收集能力,支持在 L-5 dashboard 展示 Java 后端的代码行与分支覆盖率
26
+ - 规范任务 DAG 草稿的默认输出路径,统一指向 `.harness/tasks/<task-id>/dag.json`
27
+ - 修复 loop-agent 技能条目因注入预算超限被截断的问题,确保默认 DAG 下技能完整解析
28
+
29
+ ### 新增
30
+
31
+ - backend-test DAG 节点 7 支持可选的 JaCoCo 覆盖率收集:通过跨网络 TCP dump JaCoCo tcpserver 数据,经 jacococli.jar 转换并解析为 code-coverage-v1 合约,供 L-5 dashboard 使用
32
+ - task shell 配置新增 `jacocoCoverage` 字段,支持配置 endpoint、cliJarPath、includes 和 connectTimeoutMs
33
+ - 新增面向开发与运维的 JaCoCo 覆盖率部署指南,涵盖 tcpserver agent 挂载、端口放行、cli 预装及安全提醒
34
+
35
+ ### 改进
36
+
37
+ - `dag run-task` 与 `dag init-hybrid` 的默认输出路径改为 `.harness/tasks/<task-id>/dag.json`,不再写入系统临时目录
38
+ - agent-worker 在 strict validate 成功后,将校验通过的草稿原子同步到 task 根 `dag.json`
39
+ - task read-model、delegate worktree 提示及各文档示例统一指向规范化的任务草稿路径
40
+ - skills 补充 `source-and-plan-practice.md`,明确 `import-prd` 与 `plan create` 的决策表与推荐顺序,不强制微小任务
41
+
42
+ ### 修复
43
+
44
+ - 压缩 SKILL.md 及 required-reference 体积,修复默认 DAG 技能注入预算超限导致 loop-agent 条目被截断的问题
45
+
5
46
  ## [0.25.0] - 2026-07-30
6
47
 
7
48
  ### 重点更新
@@ -14,6 +55,7 @@
14
55
 
15
56
  ### 新增
16
57
 
58
+ - backend-test DAG 节点 7 支持可选的 JaCoCo 覆盖率收集(Java 服务):task shell 配置新增 `jacocoCoverage`(endpoint + cliJarPath + includes + connectTimeoutMs);节点 7 在 pytest 后跨网络 TCP dump JaCoCo tcpserver 执行数据,用 jacococli.jar 转为 jacoco.xml,确定性解析为 `contracts/code-coverage-v1.json` 并喂给 L-5 dashboard 的 line/branch 指标;全程失败安全(endpoint 不通 / cli 缺失 / 解析失败 → 覆盖率降级 unavailable,不阻断测试与 L-5)
17
59
  - 提供 agent-worker Feature Packet 脚手架,内置 backend-only、frontend-only 和 fe-with-api 模板,支持原子批量写入与 validate-feature 预检
18
60
  - 新增 agent-worker feature advance 命令,编排 verify-final、delivery 到 closeout 的完整交付路径
19
61
  - 新增 agent-worker feature doctor 命令,提供只读的交付就绪诊断(覆盖证据、Pool 状态与工作区改动)
package/README.md CHANGED
@@ -64,19 +64,19 @@ loop-agent new-task example-task "实现一个有明确验收标准的小功能"
64
64
  loop-agent dag run-task example-task \
65
65
  --profile auto \
66
66
  --strict-models \
67
- --output <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";
@@ -587,11 +587,13 @@ async function executeBackendTestPipeline(input, meta) {
587
587
  throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
588
588
  }
589
589
  const parsed = parsePytestHtmlReport(pytestHtmlContent);
590
+ // Keep native pytest-html (data-jsonblob) for audit before styled overwrite.
591
+ const nativeHtmlPath = await writeRunReport(meta.runDir, "backend-test-pytest-native.html", pytestHtmlContent);
590
592
  // Bind Result v1 from the native pytest-html report BEFORE overwriting with the
591
593
  // styled renderer (which drops the data-jsonblob island).
592
594
  const resultArtifact = await materializeBackendTestResultFromPytestHtml({
593
595
  runDir: meta.runDir,
594
- htmlRelativePath: "reports/backend-test.html",
596
+ htmlRelativePath: "reports/backend-test-pytest-native.html",
595
597
  htmlContent: pytestHtmlContent,
596
598
  pytestExitCode,
597
599
  });
@@ -629,10 +631,40 @@ async function executeBackendTestPipeline(input, meta) {
629
631
  catch {
630
632
  // manifest missing: L5 AC/automation metrics degrade to unavailable.
631
633
  }
634
+ // Optional JaCoCo coverage collection (Java services). When jacocoCoverage
635
+ // is configured on the shell node, dump exec data over TCP from the
636
+ // JaCoCo tcpserver agent, convert via jacococli.jar, and parse into a
637
+ // code-coverage-v1 contract. Failure-safe: any error → coverage stays
638
+ // null and the L-5 dashboard degrades line/branch to unavailable.
639
+ let coverageContract = null;
640
+ const jacocoCfg = input.task.shell?.jacocoCoverage;
641
+ if (jacocoCfg) {
642
+ const sourceBinding = meta.spec.sourceBinding;
643
+ const requirementIds = sourceBinding?.requirementIds ?? [];
644
+ try {
645
+ coverageContract = await collectJacocoCoverage({
646
+ endpoint: jacocoCfg.endpoint,
647
+ cliJarPath: jacocoCfg.cliJarPath,
648
+ includes: jacocoCfg.includes ?? "*",
649
+ connectTimeoutMs: jacocoCfg.connectTimeoutMs ?? 5000,
650
+ workDir: reportsDir,
651
+ sourceScope: { requirementIds, paths: [] },
652
+ commitSha: null,
653
+ });
654
+ }
655
+ catch {
656
+ // coverage unavailable: JaCoCo endpoint unreachable or parse failed.
657
+ }
658
+ if (coverageContract) {
659
+ const contractsDir = path.join(meta.runDir, "contracts");
660
+ await mkdir(contractsDir, { recursive: true });
661
+ await writeFile(path.join(contractsDir, "code-coverage-v1.json"), JSON.stringify(coverageContract, null, 2), "utf8");
662
+ }
663
+ }
632
664
  const l5Metrics = computeL5ReportMetrics({
633
665
  result: { passed: parsed.passed, failed: parsed.failed, error: parsed.errors, skipped: parsed.skipped },
634
666
  manifest: manifestForL5,
635
- coverage: null,
667
+ coverage: coverageContract,
636
668
  criticalRiskCount: parsed.failed + parsed.errors > 0 ? 1 : 0,
637
669
  });
638
670
  const failureSummaries = parsed.cases
@@ -652,7 +684,7 @@ async function executeBackendTestPipeline(input, meta) {
652
684
  });
653
685
  const l5Path = await writeRunReport(meta.runDir, "backend-test-l5-dashboard.html", l5Html);
654
686
  const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
655
- outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `l5-dashboard=${l5Path}`, `result=${resultArtifact.path}`, facts);
687
+ outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `nativeHtml=${nativeHtmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `l5-dashboard=${l5Path}`, `result=${resultArtifact.path}`, facts);
656
688
  }
657
689
  else if (pipeline === "contracts") {
658
690
  const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
@@ -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,