@tea-agent/loop-agent 0.25.3 → 0.25.5

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 (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/application/dag/args.js +21 -1
  4. package/dist/application/dag/run-dag.js +1 -0
  5. package/dist/cli/command-definitions.js +1 -1
  6. package/dist/cli/program.js +82 -63
  7. package/dist/commands/client-recovery.js +209 -62
  8. package/dist/commands/init.js +206 -82
  9. package/dist/commands/run-dag-progress.js +109 -0
  10. package/dist/commands/run-dag.js +16 -5
  11. package/dist/executors/dag-pi-executor.js +80 -15
  12. package/dist/executors/model-routing.js +1 -1
  13. package/dist/executors/shell-executor.js +159 -0
  14. package/dist/executors/shell-write-guard.js +21 -7
  15. package/dist/worker/console/repo-fingerprint.js +7 -1
  16. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
  17. package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
  18. package/dist/workflows/dag/backend-test-markdown-workflow.js +306 -30
  19. package/dist/workflows/dag/backend-test-result-contract.js +35 -9
  20. package/dist/workflows/dag/convergence/controller.js +134 -9
  21. package/dist/workflows/dag/frontend-test-case-checklist.js +71 -0
  22. package/dist/workflows/dag/frontend-test-html-report.js +77 -0
  23. package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
  24. package/dist/workflows/dag/frontend-test-result-contract.js +44 -1
  25. package/dist/workflows/dag/init-hybrid.js +267 -80
  26. package/dist/workflows/dag/node-execution.js +64 -11
  27. package/dist/workflows/dag/prompt.js +118 -4
  28. package/dist/workflows/dag/retry-policy.js +5 -4
  29. package/dist/workflows/dag/scheduler.js +32 -5
  30. package/dist/workflows/dag/types.js +10 -3
  31. package/dist/workflows/dag/validate.js +6 -3
  32. package/docs/architecture/dag-execution.md +7 -4
  33. package/docs/architecture/runtime-boundaries.md +1 -1
  34. package/docs/templates/agent-dag.base.json +1 -1
  35. package/docs/templates/agent-dag.final-verification.json +1 -1
  36. package/docs/templates/agent-dag.schema.json +6 -0
  37. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  38. package/docs/templates/backend-test-dag.json +40 -13
  39. package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
  40. package/docs/templates/frontend-test-dag.json +62 -5
  41. package/docs/templates/hybrid-dag.json +1 -1
  42. package/examples/decision-gate-agent-dag.json +1 -1
  43. package/examples/example-dag.json +1 -1
  44. package/examples/hybrid-loop-agent-dag.json +1 -1
  45. package/harness.json +3 -2
  46. package/package.json +1 -1
  47. package/skills/loop-agent/references/command-reference.md +2 -1
  48. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  49. package/skills/loop-agent/references/model-routing.md +1 -1
package/AGENTS.md CHANGED
@@ -46,6 +46,12 @@
46
46
 
47
47
  这不是 DAG 节点序列。复杂实现默认 Agent DAG;主 agent 拆任务、写 contract、结构化路径、审查 DAG/writeSet/profile/shell verification。微小任务可用 one-shot escape hatch 并记录边界与验证证据。
48
48
 
49
+ 启动 loop-agent DAG 流程后,主 agent 应自主推进到整个流程结束,不要中途请求无谓的人工确认(如“要我现在执行 DAG 吗?”)。只要 dry-run / validate / writeSet 审查通过就应直接执行并跑完全部 ranks;遇到真问题(契约不一致、writeSet 越界、verify 失败且超出 maxFixLoops、用户明显未授权的高风险动作)才停。即便 profile 是 supervised/reviewed,也不默认在 review-gate 主动停下等人——控制器会在需要人工 approve 时自行提示,主 agent 的职责是推动流程跑完。
50
+
51
+ DAG 执行期间主 agent 必须持续轮询监视直到 run 结束(FINISHED / FAILED / 需要 approve),不能轮询一次就停下等用户;轮询间隔应合理(避免频繁唤醒浪费 token,也不得住一个节点上赌一次就走)。判活必须用可靠方式:看 `state.json` 的 `runner.heartbeatAt` 是否持续刷新 + `session-events.jsonl` 是否在增长 + `dag doctor` 的 `liveness` 字段,不要只用 `tasklist /FI "PID eq X"` 这类过滤语法在 Git Bash 下会误报 DEAD,从而错判一个正常工作的 run 为 orphaned。遇到疑似 stall 先按 `docs/runtime/agent-dag-runner.md` 查 `lastMeaningfulProgressAt` / provider 活动,有真实进展就继续等,绝不盲目 supersede。
52
+
53
+ DAG 执行期间,主 agent 不得修改 writeSet 外的任何工作区文件(包括 AGENTS.md、docs、根配置等)。bounded writer 节点的 write guard 用「节点运行期间的工作区 diff」作为越界证据,不区分改动来自 pi 还是主会话——主会话在 pi 节点跑的时候改了 writeSet 外文件,会让 write guard 把账算到 pi 头上、判节点 ERROR、下游 cascade skip。需要改文档/约束时,要么在 DAG 启动前改完,要么等 run 结束后改;务必与 DAG 写入节点在时间上互斥。排查任务路由问题时优先用 `resolveTaskDagTemplateSelection` / `classifyTaskDemand` 等纯函数探针或 `--output` 指定临时路径,不要反复 `dag run-task`——每次调用(含 `--dry-run`)都会在 `.harness/dag-runs/active/` 新建一个 run 目录,dry-run 不执行节点但目录残留,堆积成「一堆 active run」的假象。
54
+
49
55
  ## 项目地图
50
56
 
51
57
  - `CONTEXT.md`:术语表
package/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.25.5] - 2026-07-31
6
+
7
+ ### 重点更新
8
+
9
+ - 后端测试 DAG 新增确定性场景覆盖分析与 Markdown 到 pytest 的精准对应,显著提升测试质量度量的准确性与透明度
10
+ - 受监督的 DAG 默认启用有界自愈闭环,能够自动处理可恢复的执行失败并安全重算后续节点
11
+ - 全面加固客户端会话恢复机制,精准识别并补偿由模型服务瞬时异常引发的结构校验错误
12
+ - 大幅增强 DAG 执行的确定性收尾能力,确保在模型输出不稳定或前置快照失败时安全降级
13
+
14
+ ### 新增
15
+
16
+ - 后端测试固定 9 节点 DAG 新增可审计的场景覆盖与 Markdown→pytest 一一对应事实:节点 4 从严格 Coverage Matrix、Case Rule/Test Point 绑定及 OpenAPI required/enum/boundary/pattern/format 规则生成覆盖报告,节点 6 生成 1:1/1:0/1:N/0:1 对应报告,节点 7 校验一致性后物化 canonical manifest
17
+ - 受监督的 DAG 默认启用有界自愈闭环:通过 maxFixLoops 派生收敛预算,将可恢复失败与合法的审查修订请求统一纳入恢复链,并确定性注入上一轮失败证据指针,同时保持对安全失败的不自动重试
18
+ - 后端测试用例生成与独立 Review 改为产品需求优先,并穷举文档化 API 规则,涵盖生命周期、枚举等价类、长度数值边界及业务状态迁移等场景
19
+ - 后端测试 HTML/facts 新增可展开的“用例场景覆盖分析”和“Markdown → pytest 一一对应”质量详情
20
+
21
+ ### 改进
22
+
23
+ - 减少全量测试中的重复 CLI 冷启动和 self-host canary 二次打包安装场景,收敛低价值重复用例并保留关键端到端主路径
24
+ - DAG 执行期间的主会话行为约束得到增补:启动后自主推进至结束、持续可靠判活、严格限制工作区文件修改,并优先使用纯函数探针排查路由
25
+
26
+ ### 修复
27
+
28
+ - 修复后端测试节点 4/6 的确定性覆盖与对应分析在自身崩溃时被静默吞掉的问题,现会写入诊断信息并按基础设施故障安全停止
29
+ - 修复后端测试节点 4、6、7 在缺失 spec.sourceBinding 时未统一停止的问题,避免上游回退导致下游 manifest 突发报错
30
+ - 修复成功执行路径下 HTTP 请求/响应日志解析不规范的问题,现统一规范化大小写不敏感的日志前缀并完整解析 JSON,恢复报告中的请求参数与大响应正文
31
+ - 修复 Pi writer 已输出合法结果却因前置说明、Markdown 装饰或中文标点被误判失败的问题,现确定性规范化唯一 outcome 且不影响无副作用操作
32
+ - 修复 Pi writer 前置 Git 快照失败被延后处理的问题,现调用模型前即安全停止,并在失败信息中保留完整的尝试次数与错误上下文
33
+ - 修复不稳定模型输出导致受监督 DAG 无法可信收尾的问题,现使用精确判定协议和有限重试,确保最终状态准确且保持安全关闭
34
+ - 修复合并后 canonical DAG Pi MED 路由错误回退到旧模型的问题,统一恢复为正确配置
35
+ - 修复脚手架样例漂移检查误判当前 worktree 目录名的问题,避免附加 worktree 触发稳定测试失败
36
+ - 修复 Windows 8.3 短路径与长路径指向同一仓库时生成不同指纹的问题,避免应用数据命名空间冲突
37
+ - 修复生成的 OpenCode 恢复插件在 busy→idle 竞态下丢失续接的问题,现保留待处理错误并在明确空闲后安全重试
38
+ - 修复 Pi retry 配置读取将权限或 I/O 错误误判为缺文件的问题,现仅缺失文件时允许新建配置
39
+ - 修复客户端瞬态恢复无法正确处理 TypeValidation 瞬时错误的问题,现能准确识别携带瞬时信号的错误并安全重试,纯 schema 校验失败仍被拒绝
40
+
41
+ ## [0.25.4] - 2026-07-31
42
+
43
+ ### 新增
44
+
45
+ - stream run progress heartbeats
46
+
47
+ ### 新增
48
+
49
+ - stream run progress heartbeats
50
+ - improve HTML failure triage
51
+ - add native checklist and HTML report gates
52
+
53
+ ### 修复
54
+
55
+ - tighten production URL detection
56
+ - preserve supported Pi default model
57
+ - sync packaged prompt contract
58
+ - normalize drifted case ids and decode nested HTML entities
59
+
5
60
  ## [0.25.3] - 2026-07-30
6
61
 
7
62
  ### 重点更新
@@ -247,12 +247,14 @@ export function parseDagValidateArgs(args) {
247
247
  }
248
248
  export function parseRunDagArgs(args, defaultCwd) {
249
249
  if (args.length === 0) {
250
- throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--json]");
250
+ throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--quiet] [--progress-interval-ms N] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--json]");
251
251
  }
252
252
  let dagPath;
253
253
  let cwd;
254
254
  let initOnly = false;
255
255
  let dryRun = false;
256
+ let quiet = false;
257
+ let progressIntervalMs = 30_000;
256
258
  let maxConcurrent;
257
259
  let runId;
258
260
  let canvasPath;
@@ -280,6 +282,15 @@ export function parseRunDagArgs(args, defaultCwd) {
280
282
  else if (arg === "--dry-run") {
281
283
  dryRun = true;
282
284
  }
285
+ else if (arg === "--quiet") {
286
+ quiet = true;
287
+ }
288
+ else if (arg === "--progress-interval-ms") {
289
+ progressIntervalMs = parseProgressIntervalMs(args[++i]);
290
+ }
291
+ else if (arg.startsWith("--progress-interval-ms=")) {
292
+ progressIntervalMs = parseProgressIntervalMs(arg.slice("--progress-interval-ms=".length));
293
+ }
283
294
  else if (arg === "--max-concurrent") {
284
295
  maxConcurrent = parseMaxConcurrent(args[++i]);
285
296
  }
@@ -337,6 +348,8 @@ export function parseRunDagArgs(args, defaultCwd) {
337
348
  cwd: path.resolve(cwd ?? defaultCwd ?? process.cwd()),
338
349
  initOnly,
339
350
  dryRun,
351
+ quiet,
352
+ progressIntervalMs,
340
353
  maxConcurrent,
341
354
  runId,
342
355
  canvasPath,
@@ -346,6 +359,13 @@ export function parseRunDagArgs(args, defaultCwd) {
346
359
  ...(workerAssociation ? { workerAssociation } : {}),
347
360
  };
348
361
  }
362
+ function parseProgressIntervalMs(value) {
363
+ const parsed = Number(value);
364
+ if (!Number.isInteger(parsed) || parsed < 1_000) {
365
+ throw new Error("progress-interval-ms must be an integer >= 1000");
366
+ }
367
+ return parsed;
368
+ }
349
369
  function parseWorkerAssociation(raw) {
350
370
  if (!raw?.trim()) {
351
371
  throw new Error("run-dag --worker-association requires JSON");
@@ -55,6 +55,7 @@ export async function runDagUseCase(input) {
55
55
  })
56
56
  : undefined;
57
57
  const observer = composeDagRunObservers([
58
+ input.observer,
58
59
  canvas?.observer,
59
60
  eventObserver?.observer,
60
61
  ]);
@@ -662,7 +662,7 @@ export const COMMAND_DEFINITIONS = [
662
662
  adapter: "required",
663
663
  tier: "primary",
664
664
  intent: "Execute a reviewed Agent DAG spec.",
665
- usage: "run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--worker-association <json>]",
665
+ usage: "run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--quiet] [--progress-interval-ms N] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--worker-association <json>]",
666
666
  handler: async ({ repoRoot, subcommand, rest }) => {
667
667
  const runDagArgs = [subcommand, ...rest].filter((arg) => Boolean(arg));
668
668
  await runRunDag(repoRoot, runDagArgs);
@@ -124,18 +124,8 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
124
124
  await runDoctor(ctx.adapter, ctx.repoRoot, compactArgs([subcommand, ...rest]));
125
125
  return;
126
126
  case "docs":
127
- if (subcommand === "audit") {
128
- await runDocsAudit(ctx.repoRoot);
129
- return;
130
- }
131
- if (subcommand === "archive") {
132
- const [planPath] = rest;
133
- if (!planPath)
134
- throw new Error("usage: docs archive <active-plan-path>");
135
- await runDocsArchive(ctx.repoRoot, planPath);
136
- return;
137
- }
138
- throw new Error("usage: docs <audit|archive> ...");
127
+ await runDocsCommand(ctx.repoRoot, subcommand, rest);
128
+ return;
139
129
  case "new-task": {
140
130
  const [taskId, ...titleParts] = [subcommand, ...rest];
141
131
  if (!taskId)
@@ -189,53 +179,14 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
189
179
  await runStats(ctx.repoRoot, compactArgs([subcommand, ...rest]));
190
180
  return;
191
181
  case "plan":
192
- if (subcommand === "list" || subcommand === undefined) {
193
- await runPlanList(ctx.repoRoot);
194
- return;
195
- }
196
- if (subcommand === "create") {
197
- const [planId, ...titleParts] = rest;
198
- if (!planId)
199
- throw new Error('usage: plan create <plan-id> "<title>"');
200
- await runPlanCreate(ctx.repoRoot, planId, titleParts.join(" "));
201
- return;
202
- }
203
- if (subcommand === "complete") {
204
- const [planId, ...summaryParts] = rest;
205
- if (!planId)
206
- throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
207
- const summary = parsePlanSummaryFlag(summaryParts);
208
- if (!summary)
209
- throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
210
- await runPlanComplete(ctx.repoRoot, planId, { summary });
211
- return;
212
- }
213
- if (subcommand === "check") {
214
- await runPlanCheck(ctx.repoRoot);
215
- return;
216
- }
217
- throw new Error("usage: plan <list|create|complete|check> ...");
182
+ await runPlanCommand(ctx.repoRoot, subcommand, rest);
183
+ return;
218
184
  case "spine":
219
185
  await runSpine(ctx.repoRoot, compactArgs([subcommand, ...rest]));
220
186
  return;
221
187
  case "handoff":
222
- if (subcommand === "check") {
223
- await runHandoffCheck(ctx.repoRoot, rest[0]);
224
- return;
225
- }
226
- if (subcommand === "coverage") {
227
- const [taskId, ...flags] = rest;
228
- if (!taskId) {
229
- throw new Error("usage: handoff coverage <taskId> [--json|--markdown]");
230
- }
231
- const exitCode = await runCoverageAudit(ctx.repoRoot, taskId, {
232
- json: flags.includes("--json"),
233
- markdown: flags.includes("--markdown"),
234
- });
235
- process.exitCode = exitCode;
236
- return;
237
- }
238
- throw new Error("usage: handoff <check|coverage> [taskId]");
188
+ await runHandoffCommand(ctx.repoRoot, subcommand, rest);
189
+ return;
239
190
  case "coverage":
240
191
  await runCoverageReport(ctx.repoRoot, compactArgs([subcommand, ...rest]));
241
192
  return;
@@ -243,14 +194,8 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
243
194
  await runGoal(ctx.repoRoot, subcommand, rest);
244
195
  return;
245
196
  case "reference":
246
- if (subcommand === "index") {
247
- const [taskId] = rest;
248
- if (!taskId)
249
- throw new Error("usage: reference index <task-id>");
250
- await runReferenceIndex(ctx.repoRoot, taskId);
251
- return;
252
- }
253
- throw new Error("usage: reference <index> <task-id>");
197
+ await runReferenceCommand(ctx.repoRoot, subcommand, rest);
198
+ return;
254
199
  case "study":
255
200
  if (subcommand === "init") {
256
201
  await runStudyInit(ctx.repoRoot, rest);
@@ -305,6 +250,78 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
305
250
  throw new Error(`commander command is not wired to an action: ${command}`);
306
251
  }
307
252
  }
253
+ async function runDocsCommand(repoRoot, subcommand, rest) {
254
+ if (subcommand === "audit") {
255
+ await runDocsAudit(repoRoot);
256
+ return;
257
+ }
258
+ if (subcommand === "archive") {
259
+ const [planPath] = rest;
260
+ if (!planPath)
261
+ throw new Error("usage: docs archive <active-plan-path>");
262
+ await runDocsArchive(repoRoot, planPath);
263
+ return;
264
+ }
265
+ throw new Error("usage: docs <audit|archive> ...");
266
+ }
267
+ async function runPlanCommand(repoRoot, subcommand, rest) {
268
+ if (subcommand === "list" || subcommand === undefined) {
269
+ await runPlanList(repoRoot);
270
+ return;
271
+ }
272
+ if (subcommand === "create") {
273
+ const [planId, ...titleParts] = rest;
274
+ if (!planId)
275
+ throw new Error('usage: plan create <plan-id> "<title>"');
276
+ await runPlanCreate(repoRoot, planId, titleParts.join(" "));
277
+ return;
278
+ }
279
+ if (subcommand === "complete") {
280
+ const [planId, ...summaryParts] = rest;
281
+ if (!planId) {
282
+ throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
283
+ }
284
+ const summary = parsePlanSummaryFlag(summaryParts);
285
+ if (!summary) {
286
+ throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
287
+ }
288
+ await runPlanComplete(repoRoot, planId, { summary });
289
+ return;
290
+ }
291
+ if (subcommand === "check") {
292
+ await runPlanCheck(repoRoot);
293
+ return;
294
+ }
295
+ throw new Error("usage: plan <list|create|complete|check> ...");
296
+ }
297
+ async function runHandoffCommand(repoRoot, subcommand, rest) {
298
+ if (subcommand === "check") {
299
+ await runHandoffCheck(repoRoot, rest[0]);
300
+ return;
301
+ }
302
+ if (subcommand === "coverage") {
303
+ const [taskId, ...flags] = rest;
304
+ if (!taskId) {
305
+ throw new Error("usage: handoff coverage <taskId> [--json|--markdown]");
306
+ }
307
+ process.exitCode = await runCoverageAudit(repoRoot, taskId, {
308
+ json: flags.includes("--json"),
309
+ markdown: flags.includes("--markdown"),
310
+ });
311
+ return;
312
+ }
313
+ throw new Error("usage: handoff <check|coverage> [taskId]");
314
+ }
315
+ async function runReferenceCommand(repoRoot, subcommand, rest) {
316
+ if (subcommand === "index") {
317
+ const [taskId] = rest;
318
+ if (!taskId)
319
+ throw new Error("usage: reference index <task-id>");
320
+ await runReferenceIndex(repoRoot, taskId);
321
+ return;
322
+ }
323
+ throw new Error("usage: reference <index> <task-id>");
324
+ }
308
325
  async function runDagAction(repoRoot, subcommand, rest) {
309
326
  const args = compactArgs(rest);
310
327
  switch (subcommand) {
@@ -439,6 +456,8 @@ function configureRunDag(command) {
439
456
  .option("--dry-run", "validate and plan without executing")
440
457
  .option("--max-concurrent <n>", "maximum concurrent nodes")
441
458
  .option("--run-id <id>", "run id")
459
+ .option("--quiet", "disable run/node progress on stderr")
460
+ .option("--progress-interval-ms <n>", "stderr heartbeat interval in milliseconds (minimum 1000)")
442
461
  .option("--canvas-path <path>", "absolute canvas output path")
443
462
  .option("--canvas <name>", "canvas name")
444
463
  .option("--canvases-dir <dir>", "canvas output directory")