@tea-agent/loop-agent 0.25.2 → 0.25.4

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/AGENTS.md CHANGED
@@ -60,6 +60,7 @@
60
60
  - 长期决策写入 `docs/`;面向用户变更更新 `CHANGELOG.md`(结果导向中文)。
61
61
  - init/投影变更必须同步目标项目生成物与 package assets;init evolution 按 `docs/init-surface.manifest.json` 分级。
62
62
  - CLI/skill entry/runtime boundary/发布包变更同步 catalog、脚本与测试。
63
+ - 明确的前端页面/UI/组件/交互实现需求必须设置 `taskKind: "frontend-implementation"`(不是 `--profile`),不得保留默认 `standard`;浏览器/UI 自动化测试继续使用 `taskKind: "frontend-test"`。
63
64
  - 没有新鲜验证证据时不声明完成;新债写入 plan/progress/report。
64
65
 
65
66
  ## 验证
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.25.4] - 2026-07-31
6
+
7
+ ### 新增
8
+
9
+ - stream run progress heartbeats
10
+ - improve HTML failure triage
11
+ - add native checklist and HTML report gates
12
+
13
+ ### 修复
14
+
15
+ - tighten production URL detection
16
+ - preserve supported Pi default model
17
+ - sync packaged prompt contract
18
+ - normalize drifted case ids and decode nested HTML entities
19
+
20
+ ## [0.25.3] - 2026-07-30
21
+
22
+ ### 重点更新
23
+
24
+ - 新增 OpenCode 客户端瞬态会话恢复机制,在内置重试遗漏时自动补偿 UnknownError
25
+ - 优化前端实现需求路由,将前端开发与测试任务准确分发至对应 DAG 流程
26
+
27
+ ### 新增
28
+
29
+ - loop-agent init 支持 --client-recovery 参数,可在目标项目生成 OpenCode 插件以补偿瞬态 UnknownError
30
+ - 支持在用户级别原子写入 Pi retry 推荐配置,并严格遵守插件 ownership 不覆盖用户改动
31
+
32
+ ### 改进
33
+
34
+ - 初始化或刷新 AGENTS.md 时,要求将明确的前端实现需求路由至 frontend-implementation,与浏览器和 UI 自动化测试区分开
35
+
36
+ ### 修复
37
+
38
+ - 修复生成的 OpenCode 恢复插件无法按真实 API 工作的问题,现直接返回 Hooks.event 并按事件正确分发与续接
39
+ - 修复 Type validation failed 等真实错误被业务校验规则误杀的问题
40
+ - 修复 Pi retry 配置读取将权限或 I/O 错误误判为缺文件的问题,现仅 ENOENT 视为缺文件
41
+
5
42
  ## [0.25.2] - 2026-07-30
6
43
 
7
44
  ### 重点更新
@@ -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
  ]);
@@ -216,7 +216,7 @@ export const COMMAND_DEFINITIONS = [
216
216
  adapter: "none",
217
217
  tier: "primary",
218
218
  intent: "Initialize a target repository with loop-agent harness capabilities.",
219
- usage: "init [instructions|doctor|check-update|update|reconcile] [--profile full|minimal] [--merge] [--json|--markdown] [--bootstrap-surface|--apply-safe]",
219
+ usage: "init [instructions|doctor|check-update|update|reconcile] [--profile full|minimal] [--merge] [--json|--markdown] [--bootstrap-surface|--apply-safe] [--client-recovery=auto|project|user|off]",
220
220
  subcommands: [...INIT_SUBCOMMANDS],
221
221
  handler: async ({ repoRoot, subcommand, rest }) => {
222
222
  await runInit(repoRoot, [subcommand, ...rest].filter(Boolean), { readRuntimeActivity: readInitRuntimeActivity });
@@ -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")
@@ -617,7 +636,8 @@ export function buildLoopAgentProgram(options) {
617
636
  .option("--json", "print JSON")
618
637
  .option("--markdown", "print Markdown")
619
638
  .option("--bootstrap-surface", "write an inferred .harness/init-surface.json baseline")
620
- .option("--apply-safe", "apply deterministic safe init updates");
639
+ .option("--apply-safe", "apply deterministic safe init updates")
640
+ .option("--client-recovery <mode>", "auto|project|user|off — OpenCode project plugin and optional Pi user retry config", "auto");
621
641
  command.action(async (args, _options, actionCommand) => runInitCommand(args, actionCommand, options.defaultRepoRoot));
622
642
  addStandaloneSubcommands(command, entry.subcommands ?? [], (args, actionCommand) => runInitCommand(args, actionCommand, options.defaultRepoRoot));
623
643
  program.addCommand(command);