@tea-agent/loop-agent 0.24.5 → 0.24.7
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 +32 -0
- package/dist/executors/shell-executor.js +46 -3
- package/dist/worker/delivery/verification-bundle.js +26 -5
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/worker/outcomes/projector.js +5 -1
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
- package/dist/workflows/dag/init-hybrid.js +4 -6
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/types.js +2 -0
- package/dist/workflows/dag/validate.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- Observe / Inspect 节点检查器在「节点输出」左侧新增「节点输入」页签:只读投影冻结 `run.json` 顶层节点定义(任务正文、依赖、执行器摘要、边界)与可选 assembled prompt 指纹;完整 assembled prompt 与动态展开子节点仍不在本轮范围
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
|
|
11
|
+
- 修复前端证据校验在 Windows 下通过长 `node -e` 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell 与 Unix shell
|
|
12
|
+
|
|
13
|
+
## [0.24.7] - 2026-07-29
|
|
14
|
+
|
|
15
|
+
### 重点更新
|
|
16
|
+
|
|
17
|
+
- 修复 Feature Verification Bundle 在 Delivery 重验时因绝对路径 artifact、`qa-testcode` 类型与 `run_record` 后写 hash 漂移而失败的问题
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- Outcome 投影强制写出 repo-relative artifact 路径
|
|
22
|
+
- Delivery `verifyArtifactRefs` 接受位于仓库内的绝对路径(兼容旧 envelope)
|
|
23
|
+
- `verifyBundleTaskSpecBindings` 接受 `qa-testcode`(backend-test)与 `qa-execute`(frontend-test)
|
|
24
|
+
- Bundle 重验仅 rehash typed evidence(backend/frontend-test-result),忽略可能后写的 run_record
|
|
25
|
+
|
|
26
|
+
## [0.24.6] - 2026-07-29
|
|
27
|
+
|
|
28
|
+
### 重点更新
|
|
29
|
+
|
|
30
|
+
- 修复后端测试 Markdown-first 成功路径未投影 `backend-test-result` 的问题,Ready Planner 可再次放行 FE-TEST
|
|
31
|
+
|
|
32
|
+
### 修复
|
|
33
|
+
|
|
34
|
+
- Markdown-first `markdown-execute-html` 在解析原生 pytest-html 后立即物化 `contracts/backend-test-result.json`(Result v1),避免 styled HTML 覆盖后丢失 JSON island
|
|
35
|
+
- `node-execution` 对 `markdown-execute-html` 绑定 `structuredArtifact*`,Outcome 适配器可投影 `kind: backend-test-result`
|
|
36
|
+
|
|
5
37
|
## [0.24.5] - 2026-07-28
|
|
6
38
|
|
|
7
39
|
### 重点更新
|
|
@@ -8,7 +8,7 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
|
|
|
8
8
|
import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
|
|
9
9
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
10
10
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
11
|
-
import { materializeFrontendTestResult, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
11
|
+
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
12
12
|
import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
|
|
13
13
|
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
14
14
|
import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
|
|
@@ -17,7 +17,7 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
17
17
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
18
18
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
19
19
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
20
|
-
import { materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
20
|
+
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
21
21
|
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
22
22
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
23
23
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -414,6 +414,14 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
414
414
|
if (![0, 1].includes(pytestExitCode))
|
|
415
415
|
throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
|
|
416
416
|
const parsed = parsePytestHtmlReport(pytestHtmlContent);
|
|
417
|
+
// Bind Result v1 from the native pytest-html report BEFORE overwriting with the
|
|
418
|
+
// styled renderer (which drops the data-jsonblob island).
|
|
419
|
+
const resultArtifact = await materializeBackendTestResultFromPytestHtml({
|
|
420
|
+
runDir: meta.runDir,
|
|
421
|
+
htmlRelativePath: "reports/backend-test.html",
|
|
422
|
+
htmlContent: pytestHtmlContent,
|
|
423
|
+
pytestExitCode,
|
|
424
|
+
});
|
|
417
425
|
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
418
426
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
419
427
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
@@ -430,7 +438,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
430
438
|
const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
|
|
431
439
|
const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
|
|
432
440
|
const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
|
|
433
|
-
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, facts);
|
|
441
|
+
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `result=${resultArtifact.path}`, facts);
|
|
434
442
|
}
|
|
435
443
|
else if (pipeline === "contracts") {
|
|
436
444
|
const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
|
|
@@ -889,6 +897,38 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
889
897
|
};
|
|
890
898
|
}
|
|
891
899
|
}
|
|
900
|
+
async function executeFrontendTestEvidenceValidation(input) {
|
|
901
|
+
const started = Date.now();
|
|
902
|
+
try {
|
|
903
|
+
const result = await validateFrontendCaseEvidence({ workspaceRoot: input.cwd });
|
|
904
|
+
const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
|
|
905
|
+
if (result.hardFail) {
|
|
906
|
+
return {
|
|
907
|
+
ok: false,
|
|
908
|
+
stdout: "",
|
|
909
|
+
stderr: `frontend-test evidence hard-fail: ${JSON.stringify(result.issues)}`,
|
|
910
|
+
failureCategory: "nonzero-exit",
|
|
911
|
+
durationMs: Date.now() - started,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
ok: true,
|
|
916
|
+
stdout: output,
|
|
917
|
+
stderr: "",
|
|
918
|
+
failureCategory: "success",
|
|
919
|
+
durationMs: Date.now() - started,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
return {
|
|
924
|
+
ok: false,
|
|
925
|
+
stdout: "",
|
|
926
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
927
|
+
failureCategory: "invalid-output",
|
|
928
|
+
durationMs: Date.now() - started,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
892
932
|
async function executeFrontendLintBaseline(input, meta) {
|
|
893
933
|
const started = Date.now();
|
|
894
934
|
const shell = input.task.shell;
|
|
@@ -989,6 +1029,9 @@ export async function executeDagShellNode(input, meta) {
|
|
|
989
1029
|
return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
|
|
990
1030
|
}
|
|
991
1031
|
}
|
|
1032
|
+
if (shell?.frontendTestEvidenceValidation) {
|
|
1033
|
+
return executeFrontendTestEvidenceValidation(input);
|
|
1034
|
+
}
|
|
992
1035
|
if (shell?.backendTestPipeline) {
|
|
993
1036
|
return executeBackendTestPipelineWithWriteGuard(input, meta);
|
|
994
1037
|
}
|
|
@@ -404,7 +404,23 @@ export async function verifyBundleOutcomes(repoRoot, bundle) {
|
|
|
404
404
|
return false;
|
|
405
405
|
if (JSON.stringify(verified.artifacts) !== JSON.stringify(outcome.artifacts))
|
|
406
406
|
return false;
|
|
407
|
-
|
|
407
|
+
// Rehash only durable typed evidence. run_record/dag_json may be rewritten
|
|
408
|
+
// after outcome projection (worker-run-record appends report commands).
|
|
409
|
+
const evidenceArtifacts = outcome.artifacts.filter((artifact) => {
|
|
410
|
+
const kind = artifact.kind ?? "";
|
|
411
|
+
if (kind === "run_record" || kind === "dag_json")
|
|
412
|
+
return false;
|
|
413
|
+
if (kind === "backend-test-result" ||
|
|
414
|
+
kind === "frontend-test-result" ||
|
|
415
|
+
kind.includes("backend-test-result") ||
|
|
416
|
+
kind.includes("frontend-test-result")) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
// Unknown kinds: still rehash unless clearly infrastructure paths.
|
|
420
|
+
return (!artifact.path.includes("worker-run-record") &&
|
|
421
|
+
!artifact.path.includes("-dag.json"));
|
|
422
|
+
});
|
|
423
|
+
if (!await verifyArtifactRefs(repoRoot, evidenceArtifacts))
|
|
408
424
|
return false;
|
|
409
425
|
}
|
|
410
426
|
return true;
|
|
@@ -417,9 +433,12 @@ export function verifyBundleTaskSpecBindings(bundle, specs) {
|
|
|
417
433
|
return false;
|
|
418
434
|
taskIds.add(outcome.taskId);
|
|
419
435
|
const spec = specs.get(outcome.taskId);
|
|
436
|
+
// Typed verification tasks may be qa-execute (frontend-test) or qa-testcode
|
|
437
|
+
// (backend-test Markdown-first). Delivery binds on feature_id + workflow.
|
|
438
|
+
const typedVerificationTypes = new Set(["qa-execute", "qa-testcode"]);
|
|
420
439
|
if (!spec ||
|
|
421
440
|
spec.feature_id !== bundle.featureId ||
|
|
422
|
-
spec.type
|
|
441
|
+
!typedVerificationTypes.has(spec.type) ||
|
|
423
442
|
spec.execution?.workflow !== outcome.workflow)
|
|
424
443
|
return false;
|
|
425
444
|
}
|
|
@@ -460,9 +479,11 @@ async function verifyArtifactRefs(repoRoot, artifacts) {
|
|
|
460
479
|
return false;
|
|
461
480
|
}
|
|
462
481
|
for (const artifact of artifacts) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const lexical = path.
|
|
482
|
+
// Accept absolute paths only when they resolve inside the repo (legacy
|
|
483
|
+
// envelopes may store abs paths). Prefer relative for new projections.
|
|
484
|
+
const lexical = path.isAbsolute(artifact.path)
|
|
485
|
+
? path.resolve(artifact.path)
|
|
486
|
+
: path.resolve(canonicalRepoRoot, artifact.path);
|
|
466
487
|
let resolved;
|
|
467
488
|
let content;
|
|
468
489
|
try {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
4
|
+
export const DAG_RUN_LIFECYCLES = ["active", "completed", "paused"];
|
|
5
|
+
function isPathInside(root, target) {
|
|
6
|
+
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
7
|
+
const normalizedTarget = path.normalize(target);
|
|
8
|
+
return (normalizedTarget === root || normalizedTarget.startsWith(normalizedRoot));
|
|
9
|
+
}
|
|
10
|
+
function normalizeRelativeSegments(relativeSegments) {
|
|
11
|
+
if (!Array.isArray(relativeSegments) || relativeSegments.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const segments = [];
|
|
15
|
+
for (const segment of relativeSegments) {
|
|
16
|
+
if (typeof segment !== "string" || !segment)
|
|
17
|
+
return null;
|
|
18
|
+
if (segment === "." || segment === "..")
|
|
19
|
+
return null;
|
|
20
|
+
if (segment.includes("/") || segment.includes("\\") || segment.includes("\0")) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
segments.push(segment);
|
|
24
|
+
}
|
|
25
|
+
return segments;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a path under `.harness/dag-runs/{lifecycle}/{dagRunId}/...`.
|
|
29
|
+
* Fail closed on invalid ids, path escape, or the same runId appearing in
|
|
30
|
+
* multiple lifecycle directories.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveDagRunArtifact(repoRoot, dagRunId, relativeSegments) {
|
|
33
|
+
if (!isSafeObservabilityIdentifier(dagRunId)) {
|
|
34
|
+
return { ok: false, reason: "invalid-id" };
|
|
35
|
+
}
|
|
36
|
+
const segments = normalizeRelativeSegments(relativeSegments);
|
|
37
|
+
if (!segments) {
|
|
38
|
+
return { ok: false, reason: "unsafe-path" };
|
|
39
|
+
}
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
if (segment !== "run.json" &&
|
|
42
|
+
segment !== "state.json" &&
|
|
43
|
+
!isSafeObservabilityIdentifier(segment) &&
|
|
44
|
+
!/^[a-zA-Z0-9._-]+$/.test(segment)) {
|
|
45
|
+
// Allow known artifact filenames under a validated node id prefix.
|
|
46
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(segment)) {
|
|
47
|
+
return { ok: false, reason: "unsafe-path" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const dagRunsRoot = path.resolve(repoRoot, ".harness", "dag-runs");
|
|
52
|
+
const matches = [];
|
|
53
|
+
for (const lifecycle of DAG_RUN_LIFECYCLES) {
|
|
54
|
+
const runDir = path.join(dagRunsRoot, lifecycle, dagRunId);
|
|
55
|
+
const absolutePath = path.resolve(runDir, ...segments);
|
|
56
|
+
if (!isPathInside(dagRunsRoot, absolutePath))
|
|
57
|
+
continue;
|
|
58
|
+
const relative = path.relative(dagRunsRoot, absolutePath);
|
|
59
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
60
|
+
if (parts[0] !== lifecycle || parts[1] !== dagRunId)
|
|
61
|
+
continue;
|
|
62
|
+
if (!existsSync(absolutePath))
|
|
63
|
+
continue;
|
|
64
|
+
matches.push({
|
|
65
|
+
lifecycle,
|
|
66
|
+
runDir,
|
|
67
|
+
absolutePath,
|
|
68
|
+
relativePath: path.join(lifecycle, dagRunId, ...segments),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (matches.length === 0) {
|
|
72
|
+
return { ok: false, reason: "not-found" };
|
|
73
|
+
}
|
|
74
|
+
if (matches.length > 1) {
|
|
75
|
+
return { ok: false, reason: "ambiguous" };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, result: matches[0] };
|
|
78
|
+
}
|
|
79
|
+
export function resolveDagRunJson(repoRoot, dagRunId) {
|
|
80
|
+
return resolveDagRunArtifact(repoRoot, dagRunId, ["run.json"]);
|
|
81
|
+
}
|
|
82
|
+
export function resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId) {
|
|
83
|
+
if (!isSafeObservabilityIdentifier(nodeId)) {
|
|
84
|
+
return { ok: false, reason: "invalid-id" };
|
|
85
|
+
}
|
|
86
|
+
return resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
87
|
+
nodeId,
|
|
88
|
+
"prompt.redacted.md",
|
|
89
|
+
]);
|
|
90
|
+
}
|