@tea-agent/loop-agent 0.36.0 → 0.36.1-beta.0

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
@@ -1,6 +1,25 @@
1
1
  # 更新日志
2
2
 
3
- ## [Unreleased]
3
+ ## [0.36.1-beta.0] - 2026-08-16
4
+
5
+ > 标准 DAG planner 输出超长紧凑重试(beta,发布到 `beta` dist-tag)。
6
+
7
+ ### 改进
8
+
9
+ - 标准 DAG 的 planner 节点(`contract-pi` / `plan-pi`)输出超长(`output-too-large`)时自动紧凑重试:新增 `PLANNER_OUTPUT_LIMIT_RETRY_POLICY`(默认重试集合 + `output-too-large`),紧凑重试指令按节点类型区分——结构化契约节点只返回单个紧凑 JSON,普通计划节点返回最小计划(有序步骤、writeSet 边界、验证命令),并丢弃上游原文引用与长证据摘录
10
+
11
+ ## [0.35.4-beta.0] - 2026-08-15
12
+
13
+ > frontend prewrite 阻断误路由修正 + Mock 契约文档落地(beta,发布到 `beta` dist-tag)。
14
+
15
+ ### 改进
16
+
17
+ - prewrite 阻断导致的 writer `SKIPPED`(`skippedReason=frontend-prewrite-not-authorized`)失败路由从 `DependencyFailure`/`unblock dependency` 修正为 `ContractMismatch`/`fix-frontend-task-contract-and-regenerate-dag`;`dag report` / `dag doctor` 透传 `skippedReason`,对「Mock 策略契约缺陷需改 task.json + 重新生成 DAG」这类场景给出可操作建议,而非误导性的「解除依赖」
18
+
19
+ ### 文档
20
+
21
+ - `docs/templates/frontend-task-constraints.md` 的 Mock 约束从 TODO 改为可落地契约:要求从项目**探索**实际存在的确定性 Mock 验证命令(不硬编码命令名)并声明到 `task.json.frontendMock.verifyCommands`,说明生成期 `allowedMockStrategies` 来源、`mock-strategy-outside-allowed` 是生成期契约问题(修复 = 补契约 + 重新生成 DAG,plan-revision 无法修复)
22
+ - `docs/runtime/frontend-implementation-workflow.md` 的 task.json 输入标准补充 Mock 契约声明、命令探索与失败恢复指引
4
23
 
5
24
  ## [0.36.0] - 2026-08-15
6
25
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.36.0",
4
- "gitSha": "4737885305b1a168317cfc97ae06596ae7a39512",
5
- "builtAt": "2026-08-15T20:30:11.790Z"
3
+ "version": "0.36.1-beta.0",
4
+ "gitSha": "fcb0f71fdb20ce05a89d695d509539dac729286b",
5
+ "builtAt": "2026-08-16T06:52:46.623Z"
6
6
  }
@@ -44,7 +44,9 @@ function routeToProductLine(input) {
44
44
  case "decision-envelope":
45
45
  return "NeedsHuman";
46
46
  case "skipped":
47
- return "DependencyFailure";
47
+ return input.skippedReason === "frontend-prewrite-not-authorized"
48
+ ? "ContractMismatch"
49
+ : "DependencyFailure";
48
50
  case "shell-command":
49
51
  if (/\b(ENOENT|PATH|command not found|No such file|not found in PATH|bash: .*: No such file)\b/i.test(raw)) {
50
52
  return "EnvFailure";
@@ -90,9 +92,12 @@ export function routeDagFailure(input) {
90
92
  if (!productLineFailureCategory)
91
93
  return {};
92
94
  const recommendedFollowUp = productLineFailureCategory === "ContractMismatch" &&
93
- isFrontendDesignGateNode(input.nodeId ?? "")
94
- ? "frontend-plan-revision-and-rerun"
95
- : FOLLOW_UP_BY_PRODUCT_LINE[productLineFailureCategory];
95
+ input.skippedReason === "frontend-prewrite-not-authorized"
96
+ ? "fix-frontend-task-contract-and-regenerate-dag"
97
+ : productLineFailureCategory === "ContractMismatch" &&
98
+ isFrontendDesignGateNode(input.nodeId ?? "")
99
+ ? "frontend-plan-revision-and-rerun"
100
+ : FOLLOW_UP_BY_PRODUCT_LINE[productLineFailureCategory];
96
101
  return {
97
102
  productLineFailureCategory,
98
103
  recommendedFollowUp,
@@ -9,7 +9,7 @@ import { planMavenVerification, } from "../../verification/maven/index.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
11
11
  import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
12
- import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
12
+ import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
13
13
  import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
14
14
  import { resolveAdapter } from "../../adapters/index.js";
15
15
  import { loadHarnessManifest } from "../../governance/harness.js";
@@ -5783,7 +5783,9 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
5783
5783
  if (isSafeReadOnlyPiRetryCandidate(task)) {
5784
5784
  task.retryPolicy = task.outputProtocol
5785
5785
  ? PROTOCOL_AWARE_PI_RETRY_POLICY
5786
- : DEFAULT_READ_ONLY_PI_RETRY_POLICY;
5786
+ : task.role === "planner"
5787
+ ? PLANNER_OUTPUT_LIMIT_RETRY_POLICY
5788
+ : DEFAULT_READ_ONLY_PI_RETRY_POLICY;
5787
5789
  continue;
5788
5790
  }
5789
5791
  if (isWriterTransportRetryCandidate(task)) {
@@ -743,6 +743,7 @@ function findDoctorFailureNode(state) {
743
743
  nodeId: state.pausedByNodeId,
744
744
  status: node?.status,
745
745
  rawFailureCategory: node?.failureCategory,
746
+ skippedReason: node?.skippedReason,
746
747
  };
747
748
  }
748
749
  const errorEntry = Object.entries(state.nodes).find(([, node]) => node.status === "ERROR");
@@ -758,6 +759,7 @@ function findDoctorFailureNode(state) {
758
759
  nodeId: selected[0],
759
760
  status: selected[1].status,
760
761
  rawFailureCategory: selected[1].failureCategory,
762
+ skippedReason: selected[1].skippedReason,
761
763
  };
762
764
  }
763
765
  async function readRunOwnedBackendTestClassification(runDir) {
@@ -786,6 +788,7 @@ async function resolveDoctorFailureRouting(input) {
786
788
  normalizedFailureCategory: "unknown",
787
789
  nodeId: input.nodeId,
788
790
  productLineFailureCategory: classifiedCategory,
791
+ skippedReason: input.skippedReason,
789
792
  });
790
793
  }
791
794
  return routeDagFailure(input);
@@ -807,6 +810,7 @@ async function formatDagDoctorMarkdown(repoRoot, runId) {
807
810
  rawFailureCategory,
808
811
  normalizedFailureCategory: normalizedCategory,
809
812
  nodeId: failure.nodeId,
813
+ skippedReason: failure.skippedReason,
810
814
  });
811
815
  const evidence = failure.nodeId
812
816
  ? path.join(located.runDir, failure.nodeId, "result.summary.md")
@@ -216,18 +216,28 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
216
216
  "</retry_instruction>",
217
217
  ].join("\n");
218
218
  }
219
- if (task.outputMode !== "structured-required" ||
220
- previousFailureCategory !== "output-too-large") {
219
+ if (previousFailureCategory !== "output-too-large") {
221
220
  return basePrompt;
222
221
  }
222
+ if (task.outputMode === "structured-required") {
223
+ return [
224
+ basePrompt,
225
+ "",
226
+ "<retry_instruction>",
227
+ "Previous attempt exceeded the structured output size limit.",
228
+ "Return only the compact structured artifact required by this node's output contract.",
229
+ "Do not include explanatory prose, duplicated upstream context, long evidence excerpts, or additional markdown sections.",
230
+ "If a fenced JSON object is required, output exactly one fenced json block and nothing else.",
231
+ "</retry_instruction>",
232
+ ].join("\n");
233
+ }
223
234
  return [
224
235
  basePrompt,
225
236
  "",
226
237
  "<retry_instruction>",
227
- "Previous attempt exceeded the structured output size limit.",
228
- "Return only the compact structured artifact required by this node's output contract.",
229
- "Do not include explanatory prose, duplicated upstream context, long evidence excerpts, or additional markdown sections.",
230
- "If a fenced JSON object is required, output exactly one fenced json block and nothing else.",
238
+ "Previous attempt exceeded the output size limit.",
239
+ "Return a minimal plan: ordered steps, narrow writeSet boundaries, and verification commands.",
240
+ "Drop verbatim upstream quotes, long evidence excerpts, and repeated context.",
231
241
  "</retry_instruction>",
232
242
  ].join("\n");
233
243
  }
@@ -441,6 +441,7 @@ export async function buildDagRunReportEntry(input) {
441
441
  normalizedFailureCategory,
442
442
  nodeId,
443
443
  executor: node.executor,
444
+ skippedReason: node.skippedReason,
444
445
  });
445
446
  const followUp = node.status === "ERROR" || node.status === "SKIPPED"
446
447
  ? recommendFollowUpForFailureCategory(node.failureCategory)
@@ -505,6 +506,7 @@ export async function buildDagRunReportEntry(input) {
505
506
  normalizedFailureCategory: normalizeDagFailureCategory(pausedNode.failureCategory, pausedNode.status),
506
507
  failureCategory: pausedNode.failureCategory,
507
508
  nodeId: input.state.pausedByNodeId,
509
+ skippedReason: pausedNode.skippedReason,
508
510
  }
509
511
  : firstActionableNode
510
512
  ? {
@@ -513,6 +515,7 @@ export async function buildDagRunReportEntry(input) {
513
515
  normalizeDagFailureCategory(firstActionableNode.failureCategory, firstActionableNode.status),
514
516
  failureCategory: firstActionableNode.failureCategory,
515
517
  nodeId: firstActionableNode.nodeId,
518
+ skippedReason: input.state.nodes[firstActionableNode.nodeId]?.skippedReason,
516
519
  }
517
520
  : {
518
521
  status: input.state.status,
@@ -529,6 +532,9 @@ export async function buildDagRunReportEntry(input) {
529
532
  rawFailureCategory: runRecoverySource.failureCategory,
530
533
  normalizedFailureCategory: runRecoverySource.normalizedFailureCategory,
531
534
  nodeId: "nodeId" in runRecoverySource ? runRecoverySource.nodeId : undefined,
535
+ skippedReason: "skippedReason" in runRecoverySource
536
+ ? runRecoverySource.skippedReason
537
+ : undefined,
532
538
  });
533
539
  const runFollowUp = input.state.status === "failed" ||
534
540
  input.state.status === "partial_failed"
@@ -118,6 +118,17 @@ export const STRUCTURED_REQUIRED_PI_RETRY_POLICY = {
118
118
  ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
119
119
  retryCategories: [...STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES],
120
120
  };
121
+ /**
122
+ * Planner read-only nodes (standard DAG contract-pi / plan-pi) may retry with
123
+ * a compact-output instruction when the model produced an oversized assistant
124
+ * response. Only `output-too-large` is added on top of the default set;
125
+ * report/review and other read-only roles keep the default set so they do not
126
+ * silently learn new output semantics.
127
+ */
128
+ export const PLANNER_OUTPUT_LIMIT_RETRY_POLICY = {
129
+ ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
130
+ retryCategories: [...DEFAULT_DAG_RETRY_CATEGORIES, STRUCTURED_OUTPUT_RETRY_CATEGORY],
131
+ };
121
132
  /**
122
133
  * Default retry for reviewer / recovery nodes that declare outputProtocol.
123
134
  * Includes protocol-invalid so missing VERDICT lines are corrected in-node.
@@ -18,13 +18,19 @@ TODO
18
18
 
19
19
  ## Mock 约束(数据型任务)
20
20
 
21
- - Mock/API/schema 规范路径:TODO
22
- - 既有 Mock service root、handler/fixture/bootstrap:TODO
23
- - 既有 browser/e2e interception request adapter/DI seam:TODO
24
- - 启动、健康检查和专项验证命令:TODO
25
- - production 禁用边界:TODO
26
- - 真实请求默认路径与 Mock 显式启用方式:TODO
27
- - `task.json.frontendMock.policy`:`auto | required | disabled`
21
+ 本段决定生成期 `allowedMockStrategies` 与冻结的 Mock 验证命令。只有当生成期**能证明任务确实包含 Mock 且有确定性验证命令**时,DAG 才会放行 `native` / `browser-intercept` / `request-adapter`;否则自动收敛为 `not-needed`,design review 若按项目规范改选 `native`,会在 prewrite 被 `mock-strategy-outside-allowed` 拦截、`implement` 被跳过。
22
+
23
+ - **何时必须填**:项目规范(`openspec/project-specs/**`、`ai_workspace/**`、mock 规则或类似 DEC-* 决策)要求/建议 Mock;或需求涉及远端接口且后端未就绪。规范要求 Mock 时,优先 `task.json.frontendMock.policy: "required"`。
24
+ - **命令要探索、不要硬编码**:到项目 `package.json` scripts 里找实际存在的 Mock 相关脚本(如 `mock`、`mock:*`、`dev:mock`,或名字含 mock 的脚本),结合既有 service root 的 handler/fixture/bootstrap 启动方式,确定一条**真实存在、确定、可自终止**(启动→断言→退出 0)的验证命令。常驻 dev server 必须包装成自终止脚本(start→assert→stop),否则 verify shell 会超时 fail-closed。
25
+ - **写入 task.json**:把探索到的命令原样写进 `frontendMock.verifyCommands`(`label` 唯一、`command` 与项目脚本一致)。`label` 会进入冻结命令集,implementation contract 的 `verificationTarget.commandLabel` 必须逐字引用它。
26
+ - **命令来源白名单**:只允许项目 `package.json` 已有脚本、Mock capability seed、或本段声明的 `verifyCommands`;plan/design 阶段不能发明 shell 命令。
27
+ - **Mock/API/schema 规范路径**:TODO
28
+ - **既有 Mock service root、handler/fixture/bootstrap**:TODO
29
+ - **既有 browser/e2e interception 或 request adapter/DI seam**:TODO
30
+ - **production 禁用边界**:TODO
31
+ - **真实请求默认路径与 Mock 显式启用方式**:TODO
32
+ - **`task.json.frontendMock.policy`**:`auto | required | disabled`(规范强制 Mock 用 `required`)
33
+ - **被拦截时怎么修**:`mock-strategy-outside-allowed` / `no authorized Mock verification commands` 是**生成期契约问题,不是 plan 问题**——补 `frontendMock.verifyCommands`(或 `policy: "required"` + 命令)后**重新生成 DAG** 再跑,plan-revision 无法修复它。
28
34
 
29
35
  ## allowedPaths
30
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.36.0",
3
+ "version": "0.36.1-beta.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",