@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 +12 -0
- package/dist/executors/shell-executor.js +41 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +127 -0
- package/dist/workflows/dag/init-hybrid.js +46 -7
- package/dist/workflows/dag/types.js +1 -0
- package/package.json +1 -1
- package/skills/frontend-review/SKILL.md +6 -2
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
|
-
|
|
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
|
|
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);
|
package/package.json
CHANGED
|
@@ -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.
|
|
16
|
-
|
|
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
|
|