@tea-agent/loop-agent 0.16.2 → 0.16.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/CHANGELOG.md CHANGED
@@ -15,6 +15,18 @@
15
15
  - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
16
16
  - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为 initial/final Result、repair eligibility、traceability 与 Observe 投影保留结构化运行证据。
17
17
 
18
+ ## [0.16.4] - 2026-07-19
19
+
20
+ ### 修复
21
+
22
+ - 后端测试 `backend-test-semantic-final-gate-shell` 改为复合 pipeline `semantic-effective`,不再手写读 contracts 的 inline shell,避免 `--strict-governance` 误判为 fragile verdict gate 并阻断 BE-TEST DAG 校验。
23
+
24
+ ## [0.16.3] - 2026-07-19
25
+
26
+ ### 修复
27
+
28
+ - 前端实现 DAG 在 review 前新增确定性 `frontend-worktree-diff-shell`,写出 run-owned `diff_patch` 与清单,避免 review 因找不到 actual diff 而误拦截。
29
+
18
30
  ## [0.16.2] - 2026-07-19
19
31
 
20
32
  ### 修复
@@ -8,6 +8,7 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
8
8
  import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
9
9
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
10
10
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
11
+ import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
11
12
  import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
12
13
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
13
14
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
@@ -333,6 +334,20 @@ async function executeBackendTestPipeline(input, meta) {
333
334
  throw new Error("backend pytest semantic review did not pass");
334
335
  }
335
336
  }
337
+ else if (pipeline === "semantic-effective") {
338
+ // Prefer final review contract when revision ran; otherwise accept initial pass.
339
+ const finalPath = path.join(meta.runDir, "contracts", "backend-test-semantic-review-final.json");
340
+ const initialPath = path.join(meta.runDir, "contracts", "backend-test-semantic-review.json");
341
+ const chosen = existsSync(finalPath) ? finalPath : initialPath;
342
+ if (!existsSync(chosen)) {
343
+ throw new Error("missing backend-test semantic review contract for effective gate");
344
+ }
345
+ const parsed = JSON.parse(await readFile(chosen, "utf8"));
346
+ if (parsed.verdict !== "pass") {
347
+ throw new Error("backend pytest semantic review did not pass");
348
+ }
349
+ outputs.push(`semantic=${chosen}`, `verdict=${parsed.verdict}`);
350
+ }
336
351
  else if (pipeline === "execute-parse-initial") {
337
352
  const results = await executePipelineCommands(input, meta);
338
353
  if (!results.every((result) => result.ok)) {
@@ -606,6 +621,32 @@ export async function executeDagShellNode(input, meta) {
606
621
  };
607
622
  }
608
623
  }
624
+ if (shell?.commands?.length === 1 &&
625
+ shell.commands[0] === "frontend-worktree-diff-gate") {
626
+ const started = Date.now();
627
+ try {
628
+ const result = await runFrontendWorktreeDiffGate({
629
+ runDir: meta.runDir,
630
+ workspaceRoot: input.cwd,
631
+ });
632
+ return {
633
+ ok: true,
634
+ stdout: formatFrontendWorktreeDiffStdout(result),
635
+ stderr: "",
636
+ failureCategory: "success",
637
+ durationMs: Date.now() - started,
638
+ };
639
+ }
640
+ catch (error) {
641
+ return {
642
+ ok: false,
643
+ stdout: "",
644
+ stderr: error instanceof Error ? error.message : String(error),
645
+ failureCategory: "invalid-output",
646
+ durationMs: Date.now() - started,
647
+ };
648
+ }
649
+ }
609
650
  if (shell?.commands?.length === 1 &&
610
651
  shell.commands[0] === "frontend-failure-assess-gate") {
611
652
  const started = Date.now();
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
6
+ export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
7
+ function runGit(cwd, args) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn("git", args, {
10
+ cwd,
11
+ env: process.env,
12
+ stdio: ["ignore", "pipe", "pipe"],
13
+ });
14
+ let stdout = "";
15
+ let stderr = "";
16
+ child.stdout.on("data", (chunk) => {
17
+ stdout += String(chunk);
18
+ });
19
+ child.stderr.on("data", (chunk) => {
20
+ stderr += String(chunk);
21
+ });
22
+ child.on("error", reject);
23
+ child.on("close", (code) => {
24
+ resolve({ code: code ?? 1, stdout, stderr });
25
+ });
26
+ });
27
+ }
28
+ function splitLines(text) {
29
+ return text
30
+ .split(/\r?\n/)
31
+ .map((line) => line.trim())
32
+ .filter(Boolean);
33
+ }
34
+ /**
35
+ * Capture the current workspace worktree as a run-owned review artifact.
36
+ *
37
+ * Frontend review is contractually required to inspect an "actual diff". The
38
+ * model cannot invent one, and failure-path `changes.patch` under Task Pool is
39
+ * not visible while the DAG is still active. This gate freezes a deterministic
40
+ * patch + inventory under the current run directory before review runs.
41
+ */
42
+ export async function runFrontendWorktreeDiffGate(input) {
43
+ const root = path.resolve(input.workspaceRoot);
44
+ const revParse = await runGit(root, ["rev-parse", "--is-inside-work-tree"]);
45
+ if (revParse.code !== 0 || revParse.stdout.trim() !== "true") {
46
+ throw new Error(`frontend worktree diff gate requires a git worktree: ${revParse.stderr.trim() || revParse.stdout.trim() || "not a git repository"}`);
47
+ }
48
+ // Match failure capture semantics: binary-capable tracked diff vs HEAD.
49
+ const diff = await runGit(root, ["diff", "--binary", "HEAD"]);
50
+ if (diff.code !== 0) {
51
+ throw new Error(`frontend worktree diff gate git diff failed: ${diff.stderr.trim() || diff.stdout.trim()}`);
52
+ }
53
+ const nameOnly = await runGit(root, [
54
+ "diff",
55
+ "--name-only",
56
+ "--diff-filter=ACDMRTUXB",
57
+ "HEAD",
58
+ ]);
59
+ if (nameOnly.code !== 0) {
60
+ throw new Error(`frontend worktree diff gate git name-only failed: ${nameOnly.stderr.trim() || nameOnly.stdout.trim()}`);
61
+ }
62
+ const untracked = await runGit(root, [
63
+ "ls-files",
64
+ "--others",
65
+ "--exclude-standard",
66
+ ]);
67
+ if (untracked.code !== 0) {
68
+ throw new Error(`frontend worktree diff gate ls-files failed: ${untracked.stderr.trim() || untracked.stdout.trim()}`);
69
+ }
70
+ const changedFiles = splitLines(nameOnly.stdout).sort();
71
+ const untrackedFiles = splitLines(untracked.stdout).sort();
72
+ const patchBody = diff.stdout.endsWith("\n") || diff.stdout.length === 0
73
+ ? diff.stdout
74
+ : `${diff.stdout}\n`;
75
+ const untrackedSection = untrackedFiles.length === 0
76
+ ? ""
77
+ : [
78
+ "",
79
+ "# Untracked files (inventory only; contents not inlined)",
80
+ ...untrackedFiles.map((file) => `# untracked: ${file}`),
81
+ "",
82
+ ].join("\n");
83
+ const patchText = `${patchBody}${untrackedSection}`;
84
+ const patchSha256 = createHash("sha256").update(patchText).digest("hex");
85
+ const relativePatch = "artifacts/diff_patch.patch";
86
+ const absolutePatch = path.join(input.runDir, ...relativePatch.split("/"));
87
+ await mkdir(path.dirname(absolutePatch), { recursive: true });
88
+ await writeFile(absolutePatch, patchText, "utf8");
89
+ const payload = {
90
+ schemaVersion: 1,
91
+ schemaId: FRONTEND_WORKTREE_DIFF_SCHEMA_ID,
92
+ patchPath: relativePatch,
93
+ patchSha256,
94
+ changedFiles,
95
+ untrackedFiles,
96
+ empty: changedFiles.length === 0 && untrackedFiles.length === 0,
97
+ capturedAt: new Date().toISOString(),
98
+ };
99
+ const artifactPath = await writeDagRunJsonArtifact(input.runDir, "contracts/frontend-worktree-diff.json", payload);
100
+ return {
101
+ ok: true,
102
+ schemaId: FRONTEND_WORKTREE_DIFF_SCHEMA_ID,
103
+ patchPath: relativePatch,
104
+ patchAbsolutePath: absolutePatch,
105
+ patchSha256,
106
+ changedFiles,
107
+ untrackedFiles,
108
+ empty: payload.empty,
109
+ capturedAt: payload.capturedAt,
110
+ artifactPath,
111
+ };
112
+ }
113
+ export function formatFrontendWorktreeDiffStdout(result) {
114
+ return [
115
+ `schemaId=${result.schemaId}`,
116
+ `patchPath=${result.patchPath}`,
117
+ `patchSha256=${result.patchSha256}`,
118
+ `changedFiles=${result.changedFiles.length}`,
119
+ `untrackedFiles=${result.untrackedFiles.length}`,
120
+ `empty=${result.empty ? "true" : "false"}`,
121
+ result.artifactPath ? `artifactPath=${result.artifactPath}` : "",
122
+ ...result.changedFiles.map((file) => `changed:${file}`),
123
+ ...result.untrackedFiles.map((file) => `untracked:${file}`),
124
+ ]
125
+ .filter(Boolean)
126
+ .join("\n");
127
+ }
@@ -2328,9 +2328,33 @@ function buildFrontendHybridDagFromTask(sources) {
2328
2328
  timeoutMs: 120000,
2329
2329
  },
2330
2330
  },
2331
+ {
2332
+ id: "frontend-worktree-diff-shell",
2333
+ depends_on: [
2334
+ "frontend-verification-retrace-shell",
2335
+ "frontend-behavior-reverify-shell",
2336
+ "frontend-static-reverify-shell",
2337
+ "frontend-repair-pi",
2338
+ implementId,
2339
+ ],
2340
+ role: "verifier",
2341
+ executor: "shell",
2342
+ complexity: "LOW",
2343
+ writePolicy: "read-only",
2344
+ allowedPaths: readOnlyPaths,
2345
+ forbiddenPaths,
2346
+ outputContract: "Run-owned actual worktree diff_patch (artifacts/diff_patch.patch) plus contracts/frontend-worktree-diff.json inventory/hash for review. No product worktree writes.",
2347
+ subtask_prompt: "Capture the authoritative actual diff after implement/repair/reverify so frontend-review-pi can audit changed files without relying on failure-path patches or model summaries.",
2348
+ shell: {
2349
+ commands: ["frontend-worktree-diff-gate"],
2350
+ cwd: ".",
2351
+ timeoutMs: 120000,
2352
+ },
2353
+ },
2331
2354
  {
2332
2355
  id: "frontend-review-pi",
2333
2356
  depends_on: [
2357
+ "frontend-worktree-diff-shell",
2334
2358
  "frontend-verification-retrace-shell",
2335
2359
  "frontend-static-reverify-shell",
2336
2360
  "frontend-behavior-reverify-shell",
@@ -2359,7 +2383,7 @@ function buildFrontendHybridDagFromTask(sources) {
2359
2383
  "Review the frontend implementation and verification evidence.",
2360
2384
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2361
2385
  "Any Critical or Important finding must force VERDICT: request-revision.",
2362
- "Read the validated frontend-implementation-contract, frontend-verification-trace evidence, static/behavior shell facts, and actual diff. Trace proves command/file/symbol binding only—not semantic correctness.",
2386
+ "Read the validated frontend-implementation-contract, frontend-verification-trace evidence, static/behavior shell facts, and the run-owned actual diff from frontend-worktree-diff-shell (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
2363
2387
  "Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
2364
2388
  "Use the direct contract, original plan, revision/no-op result, and final design review to reconstruct the approved plan and design verdict; do not infer them from the implementation summary.",
2365
2389
  "Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
@@ -3104,10 +3128,8 @@ function buildBackendTestSemanticFinalGateNode(sources) {
3104
3128
  outputContract: "Pass-only semantic authorization for traceability and initial pytest.",
3105
3129
  subtask_prompt: "Accept initial pass path or final semantic review pass; fail closed otherwise.",
3106
3130
  shell: {
3107
- commands: [[
3108
- 'test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2',
3109
- 'node -e \'const fs=require("fs"),path=require("path");const r=process.env.HARNESS_DAG_RUN_DIR;const final=path.join(r,"contracts","backend-test-semantic-review-final.json");const first=path.join(r,"contracts","backend-test-semantic-review.json");const p=fs.existsSync(final)?final:first;const v=JSON.parse(fs.readFileSync(p,"utf8"));if(v.verdict!=="pass")throw new Error("backend pytest semantic review did not pass");console.log("backend pytest semantic gate: pass");\'',
3110
- ].join("; ")],
3131
+ commands: [],
3132
+ backendTestPipeline: "semantic-effective",
3111
3133
  cwd: ".",
3112
3134
  timeoutMs: 60000,
3113
3135
  },
@@ -3542,9 +3564,26 @@ function buildBackendTestHybridDag(sources) {
3542
3564
  revisePytest.runIf = "$.nodes['validate-semantic-review-and-traceability-shell'].json.verdict == 'request-revision'";
3543
3565
  const finalSemanticReview = buildBackendTestSemanticReviewNode(sources, { id: "review-generated-backend-pytest-final-pi", dependsOn: [revisePytest.id], final: true });
3544
3566
  finalSemanticReview.runIf = revisePytest.runIf;
3545
- const semanticFinal = { id: "backend-test-semantic-final-gate-shell", depends_on: [semanticInitial.id, finalSemanticReview.id], dependsPolicy: "all-or-condition-skip", role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Pass-only effective semantic review gate with final traceability after revision.", subtask_prompt: "Accept initial semantic pass or validate the single final review and traceability.", shell: { commands: [['test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2', 'node -e \'const fs=require("fs"),path=require("path");const r=process.env.HARNESS_DAG_RUN_DIR;const f=path.join(r,"contracts","backend-test-semantic-review-final.json");const i=path.join(r,"contracts","backend-test-semantic-review.json");const v=JSON.parse(fs.readFileSync(fs.existsSync(f)?f:i,"utf8"));if(v.verdict!=="pass")throw new Error("backend pytest semantic review did not pass");\''].join("; ")], cwd: ".", timeoutMs: 60000 } };
3546
3567
  const finalSemanticMaterialize = { id: "materialize-final-semantic-review-shell", depends_on: [finalSemanticReview.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), runIf: revisePytest.runIf, outputContract: "Materialize final semantic review and re-check traceability.", subtask_prompt: "Validate final semantic review and traceability.", shell: { commands: [], backendTestPipeline: "semantic-final", cwd: ".", timeoutMs: 60000 } };
3547
- semanticFinal.depends_on = [semanticInitial.id, finalSemanticMaterialize.id];
3568
+ const semanticFinal = {
3569
+ id: "backend-test-semantic-final-gate-shell",
3570
+ depends_on: [semanticInitial.id, finalSemanticMaterialize.id],
3571
+ dependsPolicy: "all-or-condition-skip",
3572
+ role: "verifier",
3573
+ executor: "shell",
3574
+ complexity: "LOW",
3575
+ writePolicy: "read-only",
3576
+ allowedPaths: commonReadOnlyPaths(sources),
3577
+ forbiddenPaths: commonForbiddenPaths(sources),
3578
+ outputContract: "Pass-only effective semantic review gate with final traceability after revision.",
3579
+ subtask_prompt: "Accept initial semantic pass or validate the single final review and traceability.",
3580
+ shell: {
3581
+ commands: [],
3582
+ backendTestPipeline: "semantic-effective",
3583
+ cwd: ".",
3584
+ timeoutMs: 60000,
3585
+ },
3586
+ };
3548
3587
  const executeInitial = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-initial-shell", dependsOn: [semanticFinal.id, contracts.id], reportStem: "backend-test-initial" });
3549
3588
  executeInitial.shell.backendTestPipeline = "execute-parse-initial";
3550
3589
  const classify = buildClassifyBackendTestResultNode(sources);
@@ -145,6 +145,7 @@ export const dagBackendTestPipelineSchema = z.enum([
145
145
  "contracts",
146
146
  "semantic-initial",
147
147
  "semantic-final",
148
+ "semantic-effective",
148
149
  "execute-parse-initial",
149
150
  "classification-eligibility",
150
151
  "repair-safety-traceability",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -12,8 +12,12 @@ references:
12
12
  Use for `frontend-review-pi`; read the findings guide first. Required inputs are
13
13
  original task/reference material, contract/constraints, Mock assessment, original and
14
14
  revised/confirmed plan, final design verdict, implementation summary, actual diff,
15
- and static/behavior/optional Mock shell evidence. Missing actual diff or required
16
- evidence forces revision; never infer it from a summary.
15
+ and static/behavior/optional Mock shell evidence. The actual diff is the run-owned
16
+ `frontend-worktree-diff-shell` artifacts: `contracts/frontend-worktree-diff.json` and
17
+ `artifacts/diff_patch.patch` under the current DAG run directory. When those artifacts
18
+ exist, treat them as the authoritative actual diff—do not request revision solely for
19
+ "missing diff". Only force revision for a missing actual diff when both artifacts are
20
+ absent; never invent a diff from an implementation summary alone.
17
21
 
18
22
  ## Verdict Contract
19
23