@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
@@ -33,6 +33,13 @@ export const frontendTestResultContractSchema = z.object({
33
33
  status: caseStatusSchema,
34
34
  evidence: z.array(z.object({ path: safeRelativePathSchema, sha256: sha256Schema }).strict()),
35
35
  blockedReason: z.string().min(1).optional(),
36
+ caseContent: z.object({
37
+ purpose: z.string(),
38
+ preconditions: z.array(z.string()),
39
+ steps: z.array(z.string()),
40
+ expectedResults: z.array(z.string()),
41
+ }).strict().default({ purpose: "未提供测试目的", preconditions: [], steps: [], expectedResults: [] }),
42
+ errorAnalysis: z.string().min(1).optional(),
36
43
  }).strict()).min(1),
37
44
  advisoryFindings: z.array(z.object({
38
45
  ruleId: z.string().min(1),
@@ -176,6 +183,32 @@ export async function validateFrontendCaseEvidence(input) {
176
183
  function sha256(content) {
177
184
  return createHash("sha256").update(content).digest("hex");
178
185
  }
186
+ function markdownSection(body, names) {
187
+ const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
188
+ const marker = new RegExp(`^###\\s+(?:${escaped})\\s*$`, "mi");
189
+ const hit = marker.exec(body);
190
+ if (!hit)
191
+ return "";
192
+ const rest = body.slice(hit.index + hit[0].length);
193
+ const next = /^###\s+/m.exec(rest);
194
+ return rest.slice(0, next?.index ?? rest.length).trim();
195
+ }
196
+ function markdownList(value) {
197
+ const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
198
+ return items;
199
+ }
200
+ async function readFrontendCaseContent(workspaceRoot, casePath) {
201
+ if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
202
+ throw new Error(`unsafe frontend case path: ${casePath}`);
203
+ }
204
+ const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
205
+ return {
206
+ purpose: markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) || "未提供测试目的",
207
+ preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
208
+ steps: markdownList(markdownSection(body, ["Steps", "操作步骤"])),
209
+ expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
210
+ };
211
+ }
179
212
  function assertInside(root, candidate, label) {
180
213
  const relative = path.relative(root, candidate);
181
214
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
@@ -285,7 +318,17 @@ export async function materializeFrontendTestResult(input) {
285
318
  }
286
319
  if (status === "passed" && !evidence.some((entry) => /\.(png|jpg|jpeg|webp|zip|har|webm|mp4|ya?ml|md)$/i.test(entry.path)))
287
320
  advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
288
- cases.push({ caseId: item.caseId, acIds: item.acIds, status, evidence, ...(status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? { blockedReason: resultRaw.blockedReason } : {}) });
321
+ const caseContent = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
322
+ const blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
323
+ const explicitAnalysis = typeof resultRaw.errorAnalysis === "string" && resultRaw.errorAnalysis.trim()
324
+ ? resultRaw.errorAnalysis.trim()
325
+ : typeof resultRaw.errorSummary === "string" && resultRaw.errorSummary.trim()
326
+ ? resultRaw.errorSummary.trim()
327
+ : undefined;
328
+ const errorAnalysis = status === "passed"
329
+ ? undefined
330
+ : explicitAnalysis ?? (status === "blocked" ? `用例因 ${blockedReason ?? "未知原因"} 未能完成执行。` : "用例执行失败,但执行结果未提供详细错误分析。");
331
+ cases.push({ caseId: item.caseId, acIds: item.acIds, status, evidence, caseContent, ...(blockedReason ? { blockedReason } : {}), ...(errorAnalysis ? { errorAnalysis } : {}) });
289
332
  }
290
333
  const missing = [...requiredIds].filter((id) => !covered.has(id));
291
334
  const totals = {