@tea-agent/loop-agent 0.24.11-beta.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/README.md +4 -4
  3. package/dist/application/dag/generate-task-dag.js +25 -8
  4. package/dist/commands/dag-init-hybrid.js +3 -2
  5. package/dist/commands/delegate.js +11 -11
  6. package/dist/commands/init.js +29 -14
  7. package/dist/commands/instructions.js +1 -2
  8. package/dist/executors/dag-pi-executor.js +74 -13
  9. package/dist/executors/pi-executor.js +25 -7
  10. package/dist/executors/pi-prompt-transport.js +198 -0
  11. package/dist/executors/pi-sdk-executor.js +1 -1
  12. package/dist/executors/process-tree.js +33 -0
  13. package/dist/executors/shell-executor.js +362 -24
  14. package/dist/executors/shell-verification.js +4 -2
  15. package/dist/infrastructure/harness/task-store.js +31 -0
  16. package/dist/shared/operator/capabilities.js +6 -0
  17. package/dist/task/read-model.js +3 -7
  18. package/dist/task/runtime.js +12 -3
  19. package/dist/verification/maven/cache.js +142 -0
  20. package/dist/verification/maven/index.js +120 -0
  21. package/dist/verification/maven/plan-commands.js +421 -0
  22. package/dist/verification/maven/pom-static.js +136 -0
  23. package/dist/verification/maven/scope-resolve.js +153 -0
  24. package/dist/verification/maven/stale.js +130 -0
  25. package/dist/verification/maven/types.js +23 -0
  26. package/dist/verification/maven/workspace-graph.js +322 -0
  27. package/dist/worker/cli.js +236 -21
  28. package/dist/worker/delivery/final-verification.js +12 -0
  29. package/dist/worker/delivery/git-transaction.js +32 -7
  30. package/dist/worker/delivery/package.js +9 -5
  31. package/dist/worker/delivery/verification-bundle.js +26 -5
  32. package/dist/worker/feature/advance.js +301 -0
  33. package/dist/worker/feature/doctor.js +223 -0
  34. package/dist/worker/feature/next-action.js +11 -3
  35. package/dist/worker/feature/scaffold.js +798 -0
  36. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  37. package/dist/worker/observe/node-input.js +444 -0
  38. package/dist/worker/observe/routes.js +17 -0
  39. package/dist/worker/observe/static/api.js +9 -0
  40. package/dist/worker/observe/static/constants.js +9 -0
  41. package/dist/worker/observe/static/state.js +14 -0
  42. package/dist/worker/observe/static/styles.css +74 -0
  43. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  44. package/dist/worker/outcomes/projector.js +5 -1
  45. package/dist/worker/run-task/run-task.js +18 -2
  46. package/dist/worker/runner/run-ready.js +41 -1
  47. package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -9
  48. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  49. package/dist/workflows/dag/failure-category.js +5 -1
  50. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  51. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  52. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  53. package/dist/workflows/dag/init-hybrid.js +224 -39
  54. package/dist/workflows/dag/node-execution.js +3 -2
  55. package/dist/workflows/dag/reconcile-run.js +24 -0
  56. package/dist/workflows/dag/types.js +57 -0
  57. package/dist/workflows/dag/validate.js +19 -2
  58. package/dist/workflows/loop/actions/dag-action.js +2 -1
  59. package/docs/architecture/dag-execution.md +5 -1
  60. package/docs/architecture/runtime-boundaries.md +11 -1
  61. package/docs/architecture/worker-and-feature.md +2 -0
  62. package/docs/init-surface.manifest.json +3 -0
  63. package/docs/templates/backend-test-dag.json +3 -3
  64. package/docs/templates/product-line/README.md +17 -1
  65. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  66. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  67. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  68. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  69. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  70. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  71. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  72. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  73. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  74. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  75. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  76. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  77. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  78. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  79. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  80. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  81. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  82. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  83. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  84. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  85. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  86. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  87. package/examples/l5-report-coms-process-definition.html +322 -0
  88. package/package.json +1 -1
  89. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  90. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  91. package/skills/loop-agent/SKILL.md +14 -16
  92. package/skills/loop-agent/references/command-reference.md +33 -12
  93. package/skills/loop-agent/references/harness-policy.md +28 -6
  94. package/skills/loop-agent/references/hybrid-dag.md +8 -7
  95. package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
  96. package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
  97. package/skills/loop-agent/references/task-workflow.md +13 -5
package/CHANGELOG.md CHANGED
@@ -2,15 +2,64 @@
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
+
5
19
  ### 改进
6
20
 
7
- - Feature Delivery 在省略 `--qa-evidence` / `--final-verification` 时默认使用 `feature verify-final` 写入的 evidence 路径(ADR 0007)
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 条目被截断的问题
8
29
 
9
- ### 文档
30
+ ## [0.25.0] - 2026-07-30
10
31
 
11
- - ADR 0007:Final Verification 唯一权威为 `agent-worker feature verify-final`;packet FINAL-VERIFY 仅 local smoke
12
- - F-2026-005 design / FINAL-VERIFY TaskSpec 与发布列车状态对齐 I39 Delivery + 收口下一步 I40
32
+ ### 重点更新
33
+
34
+ - 新增确定性 Feature Packet 脚手架,支持后端、前端及含 API 前端模板,并具备原子批量写入与预检能力
35
+ - 新增 Feature 交付编排命令,自动串联最终验证、交付与收口流程
36
+ - 显著提升 DAG 执行稳定性,修复超时残留子进程、重复验证及耗时记录不完整等问题
37
+ - 新增任务级 Maven 验证规划,自动定位并仅针对相关模块执行验证
38
+ - 后端测试节点现可自动生成指标机器计算的自包含 L-5 可视化报告
39
+
40
+ ### 新增
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)
43
+ - 提供 agent-worker Feature Packet 脚手架,内置 backend-only、frontend-only 和 fe-with-api 模板,支持原子批量写入与 validate-feature 预检
44
+ - 新增 agent-worker feature advance 命令,编排 verify-final、delivery 到 closeout 的完整交付路径
45
+ - 新增 agent-worker feature doctor 命令,提供只读的交付就绪诊断(覆盖证据、Pool 状态与工作区改动)
46
+ - Maven 验证现支持任务级范围规划,根据任务涉及的目录自动定位并验证正确的 POM 模块
47
+ - 后端测试 DAG 节点 7 现可确定性生成美观的自包含 L-5 可视化报告,指标完全由机器计算
48
+
49
+ ### 改进
50
+
51
+ - Feature Delivery 在省略特定参数时,默认使用 feature verify-final 写入的标准证据路径
52
+ - TaskConfig 持久化入口在写入前执行 schema 校验,拒绝不符合规范的数据格式
53
+ - Dogfood 前端测试服务器在检测到旧缓存导致接口数据异常时,现会自动强制重启恢复
54
+
55
+ ### 修复
13
56
 
57
+ - 显著加固 DAG 执行可靠性,修复超时或中断后子进程残留问题,降低文件占用与测试干扰概率
58
+ - 修复受监督流程可能重复执行完整验证的问题,避免高成本检查在不同阶段反复运行
59
+ - 修复 DAG 被放弃或替换时,运行中节点实际耗时记录不完整的问题
60
+ - 修复 Windows 环境下因提示内容过长导致 DAG 无法启动的问题,现安全采用受控临时文件传输
61
+ - 修复前端实现 DAG 在未确认项目 Mock 能力时误报缺失 Mock 文件的问题
62
+ - 修复结构化任务配置中数组类型校验不严格的问题
14
63
 
15
64
  ## [0.24.10] - 2026-07-29
16
65
 
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,9 +412,22 @@ 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
 
419
+ # loop-agent description is a YAML folded block (>-); grep ^description: only
420
+ # captures the indicator line. Validate the general-demand trigger terms
421
+ # against the whole skill file so host auto-discovery phrasing cannot drift.
422
+ loop_agent_skill=".agents/skills/loop-agent/SKILL.md"
423
+ loop_agent_text="$(tr '[:upper:]' '[:lower:]' < "\${loop_agent_skill}")"
424
+ for term in "loop-agent 帮我完成" "帮我实现" "帮我修复" "帮我开发" "agent dag" "new-task" "dag run-task" "dag validate" "run-dag"; do
425
+ if [[ "\${loop_agent_text}" != *"\${term}"* ]]; then
426
+ echo "skill entry 检查失败(loop-agent):description 缺少触发/路由词 \${term}" >&2
427
+ exit 1
428
+ fi
429
+ done
430
+
418
431
  description_line="$(grep -E '^description:' .agents/skills/agent-worker/SKILL.md | head -n 1 | tr '[:upper:]' '[:lower:]')"
419
432
  for term in "agent-worker" "feature packet" "taskspec" "task pool" "self-host" "candidate" "loop-agent"; do
420
433
  if [[ "\${description_line}" != *"\${term}"* ]]; then
@@ -802,6 +815,7 @@ function buildManagedAgentsBlock(input) {
802
815
  "| 初始化更新校验 / 检查初始化更新 / loop-agent 初始化更新校验 / loop agent初始化更新校验 | 更新校验 | 只读报告,不写入 |",
803
816
  "| 初始化对齐 / 升级后对齐 / reconcile 初始化 / loop-agent 初始化对齐 | 升级对齐 | 自动应用确定性安全动作;活跃 DAG/Worker 或人工决策时零写入 |",
804
817
  "| 初始化安全更新 / 应用初始化更新 / loop-agent 初始化安全更新 / loop agent初始化安全更新 | 安全更新 | 先 check-update,再只执行确定性安全动作 |",
818
+ "| loop-agent 帮我完成 / 帮我实现 / 帮我修复 / 帮我开发 <需求>;使用 loop-agent 完成 <X>;按 loop-agent 流程处理 <X> | 通用需求实现 | 先建任务源与 `task.json.allowedPaths` / `forbiddenPaths`,再 `new-task` → `dag run-task` → `dag validate` → `run-dag`;主会话不得直接修改业务实现 |",
805
819
  "",
806
820
  "**更新校验(只读)**:只读执行下面命令;**不得自动**执行 `apply-safe` 或模型合并。",
807
821
  "",
@@ -843,9 +857,9 @@ function buildManagedAgentsBlock(input) {
843
857
  "loop-agent new-task <task-id> \"任务标题\"",
844
858
  "# write .harness/tasks/<task-id>/source/需求.md",
845
859
  "# write .harness/tasks/<task-id>/source/执行约束.md",
846
- "loop-agent dag run-task <task-id> --profile auto --strict-models --output <temp-dir>/<task-id>-dag.json",
847
- "loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance",
848
- "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 .",
849
863
  "```",
850
864
  "",
851
865
  "`source/需求.md` 与 `source/执行约束.md` 必需;写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。",
@@ -861,10 +875,11 @@ function buildManagedAgentsBlock(input) {
861
875
  "### 运行看板(只读)",
862
876
  "",
863
877
  "```bash",
864
- "agent-worker console serve --repo . --port 8790",
878
+ "agent-worker console # 默认 repo=当前目录,port=8790",
879
+ "agent-worker console serve --repo . --port 8790 # 兼容入口,等价于上面裸入口",
865
880
  "```",
866
881
  "",
867
- "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。`agent-worker observe serve` 仅为兼容入口。",
882
+ "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。`agent-worker observe serve` 仅为兼容入口。",
868
883
  "",
869
884
  "### DAG 诊断与收口",
870
885
  "",
@@ -1804,9 +1819,9 @@ function buildManagedReadmeBlock(input) {
1804
1819
  "loop-agent new-task <task-id> \"任务标题\"",
1805
1820
  "# write .harness/tasks/<task-id>/source/需求.md",
1806
1821
  "# write .harness/tasks/<task-id>/source/执行约束.md",
1807
- "loop-agent dag run-task <task-id> --profile auto --strict-models --output <temp-dir>/<task-id>-dag.json",
1808
- "loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance",
1809
- "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 .",
1810
1825
  "```",
1811
1826
  "",
1812
1827
  `详细工作流见 \`${input.governanceRoot}/feature-workflow.md\`;验证矩阵见 \`${input.governanceRoot}/verification-matrix.md\`。任务 source 是必需项:\`source/需求.md\` 写目标、范围、非目标、验收标准和相关链接,\`source/执行约束.md\` 写允许路径、禁止路径、受保护变更、不变量、预期验证和失败条件。`,
@@ -2036,9 +2051,9 @@ function buildTargetFeatureWorkflow(input) {
2036
2051
  "loop-agent new-task <task-id> \"Task title\"",
2037
2052
  "# write .harness/tasks/<task-id>/source/需求.md",
2038
2053
  "# write .harness/tasks/<task-id>/source/执行约束.md",
2039
- "loop-agent dag run-task <task-id> --profile auto --strict-models --output <temp-dir>/<task-id>-dag.json",
2040
- "loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --strict-governance",
2041
- "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 .",
2042
2057
  "```",
2043
2058
  "",
2044
2059
  DAG_HARD_GATE_TRIGGER,
@@ -2660,7 +2675,7 @@ async function applySafeAction(input) {
2660
2675
  if (!isRecord(harness) || !isRecord(harness.executors) || !isRecord(harness.executors.pi)) {
2661
2676
  return false;
2662
2677
  }
2663
- if (!Object.prototype.hasOwnProperty.call(harness.executors.pi, "requiresApiKey")) {
2678
+ if (!Object.hasOwn(harness.executors.pi, "requiresApiKey")) {
2664
2679
  return false;
2665
2680
  }
2666
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"),
@@ -130,17 +130,23 @@ export function buildDagPiUserMessage(task, persona, step) {
130
130
  const role = task.role ?? "unspecified";
131
131
  const writePolicy = task.writePolicy ?? "read-only (default)";
132
132
  if (isDagPiWriteTask(task)) {
133
+ const outcomeInstruction = task.writerOutcomePolicy
134
+ ? "The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked. Use changed only after producing a non-empty bounded diff; use already-satisfied only when the contract is already met and no file changed; use blocked when implementation cannot proceed."
135
+ : undefined;
133
136
  return [
134
137
  `You are executing hybrid DAG node "${task.id}" (role=${role}, piStep=${step}, writePolicy=${writePolicy}).`,
135
138
  "The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
136
139
  "Use write tools only for the bounded implementation requested by this node.",
140
+ outcomeInstruction,
137
141
  `Allowed paths: ${(task.allowedPaths ?? []).join(", ") || "(none declared)"}.`,
138
142
  `Write set: ${(task.writeSet ?? []).join(", ") || "(none declared)"}.`,
139
143
  `Forbidden paths: ${(task.forbiddenPaths ?? []).join(", ") || "(none declared)"}.`,
140
144
  "Preserve unrelated files. Do not edit outside allowedPaths/writeSet. Never write forbiddenPaths, .harness/dag-runs/**, or undeclared root artifacts/**.",
141
145
  "Return concise Markdown with changed files, verification performed or still required, and residual risks.",
142
146
  "Do not wrap the output in code fences and do not add conversational preamble.",
143
- ].join(" ");
147
+ ]
148
+ .filter((value) => Boolean(value))
149
+ .join(" ");
144
150
  }
145
151
  // Decision Gate nodes must emit a mandatory DECISION_ENVELOPE_JSON fenced block;
146
152
  // do not inject the global no-fence instruction that would conflict with outputContract.
@@ -344,21 +350,39 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
344
350
  ];
345
351
  }
346
352
  }
347
- if (writeGuardOk) {
348
- if (beforeStatus !== undefined && changeManifestChangedFiles !== undefined) {
349
- await persistWriterChangeManifest({
350
- runDir: meta.runDir,
351
- nodeId: input.task.id,
352
- writerNodeId: input.task.id,
353
- changedFiles: changeManifestChangedFiles,
354
- beforeStatus,
355
- afterStatus: changeManifestAfterStatus ?? "",
356
- });
353
+ let writerOutcomeViolation;
354
+ if (mapped.ok && input.task.writerOutcomePolicy) {
355
+ if (changeManifestChangedFiles === undefined) {
356
+ writerOutcomeViolation =
357
+ "writer outcome validation failed: actual diff is unavailable";
358
+ }
359
+ else {
360
+ const outcomeValidation = validateWriterImplementationOutcome(mapped.assistantText || mapped.stdout, changeManifestChangedFiles);
361
+ if (!outcomeValidation.ok) {
362
+ writerOutcomeViolation = outcomeValidation.reason;
363
+ }
357
364
  }
365
+ }
366
+ if (writeGuardOk && beforeStatus !== undefined && changeManifestChangedFiles !== undefined) {
367
+ await persistWriterChangeManifest({
368
+ runDir: meta.runDir,
369
+ nodeId: input.task.id,
370
+ writerNodeId: input.task.id,
371
+ changedFiles: changeManifestChangedFiles,
372
+ beforeStatus,
373
+ afterStatus: changeManifestAfterStatus ?? "",
374
+ });
375
+ }
376
+ if (writeGuardOk && !writerOutcomeViolation) {
358
377
  return mapped;
359
378
  }
360
379
  const stderrParts = [mapped.stderr];
361
- stderrParts.push(`write guard failed: ${writeGuardViolations.join(", ")}`);
380
+ if (!writeGuardOk) {
381
+ stderrParts.push(`write guard failed: ${writeGuardViolations.join(", ")}`);
382
+ }
383
+ if (writerOutcomeViolation) {
384
+ stderrParts.push(writerOutcomeViolation);
385
+ }
362
386
  if (meta.writeGuardAttribution === "best-effort") {
363
387
  stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
364
388
  }
@@ -366,10 +390,47 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
366
390
  ...mapped,
367
391
  ok: false,
368
392
  stderr: stderrParts.filter(Boolean).join("\n\n"),
369
- failureCategory: mapped.ok ? "write-guard" : mapped.failureCategory,
393
+ failureCategory: mapped.ok
394
+ ? writeGuardOk
395
+ ? "invalid-output"
396
+ : "write-guard"
397
+ : mapped.failureCategory,
370
398
  durationMs: mapped.durationMs || Date.now() - started,
371
399
  };
372
400
  }
401
+ export function validateWriterImplementationOutcome(text, changedFiles) {
402
+ const firstLine = text
403
+ .split(/\r?\n/)
404
+ .map((line) => line.trim())
405
+ .find(Boolean);
406
+ const match = firstLine?.match(/^IMPLEMENTATION_OUTCOME: (changed|already-satisfied|blocked)$/);
407
+ if (!match) {
408
+ return {
409
+ ok: false,
410
+ reason: "writer outcome validation failed: missing or invalid first line; expected IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked",
411
+ };
412
+ }
413
+ const outcome = match[1];
414
+ if (outcome === "blocked") {
415
+ return {
416
+ ok: false,
417
+ reason: "writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully",
418
+ };
419
+ }
420
+ if (outcome === "changed" && changedFiles.length === 0) {
421
+ return {
422
+ ok: false,
423
+ reason: "writer outcome validation failed: changed outcome has an empty diff",
424
+ };
425
+ }
426
+ if (outcome === "already-satisfied" && changedFiles.length > 0) {
427
+ return {
428
+ ok: false,
429
+ reason: "writer outcome validation failed: already-satisfied outcome has a non-empty diff",
430
+ };
431
+ }
432
+ return { ok: true, outcome };
433
+ }
373
434
  export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
374
435
  const assistantText = canonicalizeProtocolFirstLine(result.assistantText, firstProtocolLine);
375
436
  return {
@@ -3,6 +3,8 @@ import { existsSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { PI_REUSE_RUNTIME_ENV, isPiReuseRuntimeEffective, resolvePiReuseRuntimeMode as resolveConfiguredPiReuseRuntimeMode, } from './pi-runtime-reuse.js';
6
+ import { cleanupPiPromptTransport, createPiPromptTransport, } from './pi-prompt-transport.js';
7
+ import { processTreeSpawnOptions, terminateProcessTree } from './process-tree.js';
6
8
  export function extractSubagentStats(stdout) {
7
9
  let totalCalls = 0;
8
10
  let failedCalls = 0;
@@ -507,12 +509,24 @@ function withCliBackend(result) {
507
509
  }
508
510
  /** CLI-only orchestration: primary attempt + optional fallback model retry. */
509
511
  async function runCliOrchestration(options, meta) {
512
+ const promptTransport = createPiPromptTransport(options.prompt);
513
+ try {
514
+ return await runCliOrchestrationWithPromptFile(options, promptTransport.filePath, meta);
515
+ }
516
+ finally {
517
+ const cleanup = cleanupPiPromptTransport(promptTransport);
518
+ for (const warning of cleanup.warnings) {
519
+ process.stderr.write(`${warning}\n`);
520
+ }
521
+ }
522
+ }
523
+ async function runCliOrchestrationWithPromptFile(options, promptFilePath, meta) {
510
524
  const primary = options.modelConfig ?? {};
511
525
  const attemptedModels = [];
512
526
  if (meta?.prependAttemptedModel) {
513
527
  attemptedModels.push(meta.prependAttemptedModel);
514
528
  }
515
- const primaryResult = await executeSingleCliAttempt(options, primary);
529
+ const primaryResult = await executeSingleCliAttempt(options, primary, promptFilePath);
516
530
  attemptedModels.push(primaryResult.modelDisplay);
517
531
  let primaryIssues = [];
518
532
  if (primaryResult.ok && options.validateOutput) {
@@ -525,7 +539,7 @@ async function runCliOrchestration(options, meta) {
525
539
  && shouldRetryWithFallback(primaryResult.stderr, primaryResult.stdout))
526
540
  || primaryInvalidOutput);
527
541
  if (shouldFallback) {
528
- const fallbackResult = await executeSingleCliAttempt(options, primary.fallback);
542
+ const fallbackResult = await executeSingleCliAttempt(options, primary.fallback, promptFilePath);
529
543
  attemptedModels.push(fallbackResult.modelDisplay);
530
544
  const combinedStderr = [primaryResult.stderr, fallbackResult.stderr].filter(Boolean).join('\n--- fallback ---\n');
531
545
  const combinedStdout = [primaryResult.stdout, fallbackResult.stdout].filter(Boolean).join('\n--- fallback ---\n');
@@ -659,7 +673,7 @@ export async function executePiStep(options) {
659
673
  });
660
674
  return { ...cliResult, reuseRuntimeActive };
661
675
  }
662
- async function executeSingleCliAttempt(options, modelConfig) {
676
+ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
663
677
  const modelDisplay = modelConfig.provider && modelConfig.model
664
678
  ? `${modelConfig.provider}/${modelConfig.model}`
665
679
  : modelConfig.model ?? 'default';
@@ -687,13 +701,17 @@ async function executeSingleCliAttempt(options, modelConfig) {
687
701
  piArgs.push('--thinking', modelConfig.thinking);
688
702
  }
689
703
  piArgs.push('--tools', options.toolNames.join(','));
690
- piArgs.push('--append-system-prompt', options.prompt);
704
+ piArgs.push('--append-system-prompt', promptFilePath);
691
705
  piArgs.push(...options.attachedFiles.map((file) => `@${file}`));
692
706
  piArgs.push(options.userMessage);
707
+ const recordedPiArgs = piArgs.map((arg, index) => index > 0 && piArgs[index - 1] === '--append-system-prompt'
708
+ ? '<system-prompt-file>'
709
+ : arg);
693
710
  const startedAt = Date.now();
694
711
  return await new Promise((resolve, reject) => {
695
712
  const child = spawn(process.execPath, [getPiCliPath(), ...piArgs], {
696
713
  cwd: options.repoRoot,
714
+ ...processTreeSpawnOptions(),
697
715
  env: {
698
716
  ...process.env,
699
717
  // Explicitly propagate API keys that pi needs to recognise providers.
@@ -748,7 +766,7 @@ async function executeSingleCliAttempt(options, modelConfig) {
748
766
  });
749
767
  resolve({
750
768
  assistantText: collected.assistantText,
751
- command: ['pi', ...piArgs],
769
+ command: ['pi', ...recordedPiArgs],
752
770
  durationMs,
753
771
  exitCode: code,
754
772
  failureCategory,
@@ -771,7 +789,7 @@ async function executeSingleCliAttempt(options, modelConfig) {
771
789
  return;
772
790
  timedOut = true;
773
791
  supervisionStderr = reason;
774
- child.kill('SIGTERM');
792
+ terminateProcessTree(child, 'SIGTERM');
775
793
  sigkillHandle = setTimeout(() => {
776
794
  if (settled)
777
795
  return;
@@ -779,7 +797,7 @@ async function executeSingleCliAttempt(options, modelConfig) {
779
797
  finish(child.exitCode);
780
798
  return;
781
799
  }
782
- child.kill('SIGKILL');
800
+ terminateProcessTree(child, 'SIGKILL');
783
801
  exitConfirmationHandle = setTimeout(() => {
784
802
  if (settled)
785
803
  return;