@tea-agent/loop-agent 0.40.0-next.3 → 0.40.0-next.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 +1 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +46 -5
- package/dist/executors/shell-executor.js +28 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/task/config-types.js +10 -0
- package/dist/worker/observe/static/styles.css +20 -1
- package/dist/worker/observe/static/views/dag-inspector.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +19 -0
- package/dist/workflows/dag/types.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
### 改进
|
|
63
63
|
|
|
64
|
+
- 检查器「提示词重启」只读态的提示词卡片会撑满剩余高度并在卡片内滚动,不再在面板底部留下大块空白;失败节点的可编辑布局不变。
|
|
64
65
|
- DAG 节点表「备注」改为节点用途简介(是什么、做什么、目的),不再重复节点 ID;失败节点仍显示错误摘要。检查器标题与图节点悬浮提示同步展示同一句说明。
|
|
65
66
|
- 实验「提示词重启」会为 Pi 节点的每次失败 attempt 保存独立、脱敏且 64 KiB 有界的提示词候选;Inspector 始终展示提示词版本选择框,新 run 会列出冻结原始提示词与全部失败尝试(含自动重试产生的恢复提示词),历史 run 未保存候选时明确说明,成功尝试不混入。提示词卡片右上角提供复制图标并以勾选图标反馈成功;已截断候选复制时明确标记为截断预览,并保持只读、禁止直接提交,避免用不完整提示词发起续跑。选择失败候选会载入「全文替换」编辑器;即使节点后续自动重试成功,历史失败候选仍可只读切换查看。
|
|
66
67
|
- DAG 节点检查器「执行过程」按执行器分流:Pi 仍看 session-events;有命令的 shell 展示 `shell-command-receipt` 与 stdout/stderr;`frontendBrowserToolPreflight` 等内部处理器展示步骤回执(历史 run 可从 capability / attempt 产物合成)。空态不再把 shell/static 误说成「暂无过程日志」。节点输入补充 named handler 名称。脚本仍属于节点输入,不假装已执行。
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.40.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.40.0-next.4",
|
|
4
|
+
"gitSha": "84dabbbd0cdd880a29ed8cf22e11feb7f4550188",
|
|
5
|
+
"builtAt": "2026-08-26T13:58:53.067Z"
|
|
6
6
|
}
|
|
@@ -9,6 +9,7 @@ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND
|
|
|
9
9
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
10
10
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
11
11
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
12
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
12
13
|
import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
13
14
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
14
15
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
@@ -446,9 +447,28 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
446
447
|
};
|
|
447
448
|
}
|
|
448
449
|
let beforeStatus;
|
|
450
|
+
let beforeWorkspaceSnapshot;
|
|
449
451
|
let beforePathFingerprints;
|
|
450
|
-
/** tools-only
|
|
451
|
-
const
|
|
452
|
+
/** tools-only and filesystem-only keep the tool sandbox but skip Git baseline. */
|
|
453
|
+
const filesystemOnly = isWriteTask && meta.spec.backendTestWorkspaceControl === "filesystem-only";
|
|
454
|
+
const skipGitWriteGuard = isWriteTask && (input.task.writeGuardPolicy === "tools-only" || filesystemOnly);
|
|
455
|
+
if (isWriteTask && filesystemOnly) {
|
|
456
|
+
try {
|
|
457
|
+
beforeWorkspaceSnapshot = await captureWorkspaceWriteSnapshot({
|
|
458
|
+
rootCwd: input.cwd,
|
|
459
|
+
paths: input.task.writeSet ?? input.task.allowedPaths,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
stdout: "",
|
|
466
|
+
stderr: `workspace filesystem baseline unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
467
|
+
failureCategory: "write-guard",
|
|
468
|
+
durationMs: Date.now() - started,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
452
472
|
if (isWriteTask && !skipGitWriteGuard) {
|
|
453
473
|
try {
|
|
454
474
|
beforeStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, { phase: "pi-writer-before" });
|
|
@@ -678,6 +698,27 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
678
698
|
let writeGuardViolations = [];
|
|
679
699
|
let changeManifestAfterStatus;
|
|
680
700
|
let changeManifestChangedFiles;
|
|
701
|
+
let afterWorkspaceSnapshot;
|
|
702
|
+
if (filesystemOnly && beforeWorkspaceSnapshot) {
|
|
703
|
+
try {
|
|
704
|
+
afterWorkspaceSnapshot = await captureWorkspaceWriteSnapshot({
|
|
705
|
+
rootCwd: input.cwd,
|
|
706
|
+
paths: input.task.writeSet ?? input.task.allowedPaths,
|
|
707
|
+
});
|
|
708
|
+
changeManifestChangedFiles = diffWorkspaceWriteSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot);
|
|
709
|
+
const guard = validateShellWriteGuardFromDiff({
|
|
710
|
+
changedFiles: changeManifestChangedFiles,
|
|
711
|
+
task: input.task,
|
|
712
|
+
concurrentSiblingWriteSets: meta.concurrentSiblingWriteSets,
|
|
713
|
+
});
|
|
714
|
+
writeGuardOk = guard.ok;
|
|
715
|
+
writeGuardViolations = guard.violations;
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
writeGuardOk = false;
|
|
719
|
+
writeGuardViolations = [`workspace snapshot unavailable: ${error instanceof Error ? error.message : String(error)}`];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
681
722
|
if (beforeStatus !== undefined) {
|
|
682
723
|
let recoveryEvidence;
|
|
683
724
|
let removalPendingRecheck;
|
|
@@ -850,7 +891,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
850
891
|
}
|
|
851
892
|
}
|
|
852
893
|
if (writeGuardOk &&
|
|
853
|
-
beforeStatus !== undefined &&
|
|
894
|
+
(beforeStatus !== undefined || beforeWorkspaceSnapshot !== undefined) &&
|
|
854
895
|
changeManifestChangedFiles !== undefined) {
|
|
855
896
|
await persistWriterChangeManifest({
|
|
856
897
|
runDir: meta.runDir,
|
|
@@ -860,8 +901,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
860
901
|
approvalSourceNodeId: input.task.resolvedFinalWriteSetApproval?.approvalSourceNodeId,
|
|
861
902
|
approvalDigest: input.task.resolvedFinalWriteSetApproval?.approvalDigest,
|
|
862
903
|
changedFiles: changeManifestChangedFiles,
|
|
863
|
-
beforeStatus,
|
|
864
|
-
afterStatus: changeManifestAfterStatus ??
|
|
904
|
+
beforeStatus: beforeStatus ?? JSON.stringify(beforeWorkspaceSnapshot),
|
|
905
|
+
afterStatus: changeManifestAfterStatus ?? JSON.stringify(afterWorkspaceSnapshot),
|
|
865
906
|
});
|
|
866
907
|
}
|
|
867
908
|
let completenessFailure;
|
|
@@ -41,6 +41,7 @@ import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
|
41
41
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
42
42
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
43
43
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
44
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
44
45
|
import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
|
|
45
46
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
46
47
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
@@ -1719,6 +1720,33 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1719
1720
|
}
|
|
1720
1721
|
}
|
|
1721
1722
|
async function executeBackendTestPipelineWithWriteGuard(input, meta) {
|
|
1723
|
+
if (meta.spec.backendTestWorkspaceControl === "filesystem-only") {
|
|
1724
|
+
const paths = input.task.writeSet ?? input.task.allowedPaths;
|
|
1725
|
+
let beforeSnapshot;
|
|
1726
|
+
try {
|
|
1727
|
+
beforeSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1728
|
+
}
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
return { ok: false, stdout: "", stderr: `workspace filesystem baseline unavailable: ${error instanceof Error ? error.message : String(error)}`, failureCategory: "write-guard", durationMs: 0 };
|
|
1731
|
+
}
|
|
1732
|
+
const result = await executeBackendTestPipeline(input, meta);
|
|
1733
|
+
try {
|
|
1734
|
+
const afterSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1735
|
+
const guard = validateShellWriteGuard({
|
|
1736
|
+
changedPaths: diffWorkspaceWriteSnapshots(beforeSnapshot, afterSnapshot),
|
|
1737
|
+
writePolicy: input.task.writePolicy,
|
|
1738
|
+
writeSet: input.task.writeSet,
|
|
1739
|
+
allowedPaths: input.task.allowedPaths,
|
|
1740
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
1741
|
+
});
|
|
1742
|
+
if (!guard.ok)
|
|
1743
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${guard.violations.join(", ")}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
catch (error) {
|
|
1747
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1722
1750
|
let beforeStatus;
|
|
1723
1751
|
try {
|
|
1724
1752
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
5
|
+
const normalize = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
6
|
+
function rootBeforeWildcard(pattern) {
|
|
7
|
+
const value = normalize(pattern);
|
|
8
|
+
const wildcard = value.search(/[?*]/);
|
|
9
|
+
if (wildcard < 0)
|
|
10
|
+
return path.posix.dirname(value);
|
|
11
|
+
const prefix = value.slice(0, wildcard);
|
|
12
|
+
return prefix.slice(0, prefix.lastIndexOf("/")) || ".";
|
|
13
|
+
}
|
|
14
|
+
async function walk(rootCwd, relative, files) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(path.resolve(rootCwd, relative), { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if ([".git", ".harness", "node_modules"].includes(entry.name))
|
|
24
|
+
continue;
|
|
25
|
+
const child = normalize(path.posix.join(relative, entry.name));
|
|
26
|
+
if (entry.isDirectory())
|
|
27
|
+
await walk(rootCwd, child, files);
|
|
28
|
+
else if (entry.isFile())
|
|
29
|
+
files.add(child);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function captureWorkspaceWriteSnapshot(input) {
|
|
33
|
+
const candidates = new Set();
|
|
34
|
+
for (const pattern of input.paths) {
|
|
35
|
+
await walk(input.rootCwd, rootBeforeWildcard(pattern), candidates);
|
|
36
|
+
if (!/[?*]/.test(pattern)) {
|
|
37
|
+
try {
|
|
38
|
+
if ((await lstat(path.resolve(input.rootCwd, pattern))).isFile())
|
|
39
|
+
candidates.add(normalize(pattern));
|
|
40
|
+
}
|
|
41
|
+
catch { /* absent target */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const files = {};
|
|
45
|
+
let totalBytes = 0;
|
|
46
|
+
for (const relative of [...candidates].filter((file) => input.paths.some((pattern) => pathMatchesPattern(file, pattern))).sort()) {
|
|
47
|
+
if (Object.keys(files).length >= 20_000)
|
|
48
|
+
throw new Error("workspace snapshot file limit exceeded");
|
|
49
|
+
const absolute = path.resolve(input.rootCwd, relative);
|
|
50
|
+
const stat = await lstat(absolute);
|
|
51
|
+
if (!stat.isFile())
|
|
52
|
+
throw new Error(`workspace snapshot encountered non-regular file: ${relative}`);
|
|
53
|
+
totalBytes += stat.size;
|
|
54
|
+
if (totalBytes > 64 * 1024 * 1024)
|
|
55
|
+
throw new Error("workspace snapshot byte limit exceeded");
|
|
56
|
+
files[relative] = { size: stat.size, sha256: createHash("sha256").update(await readFile(absolute)).digest("hex") };
|
|
57
|
+
}
|
|
58
|
+
const body = JSON.stringify(files);
|
|
59
|
+
return { schemaVersion: 1, controlMode: "filesystem-only", files, sha256: createHash("sha256").update(body).digest("hex") };
|
|
60
|
+
}
|
|
61
|
+
export function diffWorkspaceWriteSnapshots(before, after) {
|
|
62
|
+
const paths = new Set([...Object.keys(before.files), ...Object.keys(after.files)]);
|
|
63
|
+
return [...paths].sort().filter((file) => {
|
|
64
|
+
const left = before.files[file];
|
|
65
|
+
const right = after.files[file];
|
|
66
|
+
return !left || !right || left.size !== right.size || left.sha256 !== right.sha256;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -147,6 +147,8 @@ export const backendTestConfigSchema = z
|
|
|
147
147
|
gapDoc: z.string().min(1).optional(),
|
|
148
148
|
/** gap-fill pytest execution scope: only affected items (default) or all eligible. */
|
|
149
149
|
executeScope: z.enum(["affected", "all"]).optional(),
|
|
150
|
+
/** Backend-test-only workspace mutation control; omitted means Git guard. */
|
|
151
|
+
workspaceControl: z.enum(["git", "filesystem-only"]).optional(),
|
|
150
152
|
})
|
|
151
153
|
.strict()
|
|
152
154
|
.optional();
|
|
@@ -237,5 +239,13 @@ export const taskConfigSchema = z.preprocess((raw) => {
|
|
|
237
239
|
if ("executor" in obj || "cursorModel" in obj) {
|
|
238
240
|
throw new Error(CURSOR_TASK_FIELD_REMOVED_ERROR);
|
|
239
241
|
}
|
|
242
|
+
const backendTest = obj.backendTest;
|
|
243
|
+
if (backendTest &&
|
|
244
|
+
typeof backendTest === "object" &&
|
|
245
|
+
!Array.isArray(backendTest) &&
|
|
246
|
+
"workspaceControl" in backendTest &&
|
|
247
|
+
obj.taskKind !== "backend-test") {
|
|
248
|
+
throw new Error("backendTest.workspaceControl is only valid when taskKind is backend-test");
|
|
249
|
+
}
|
|
240
250
|
return raw;
|
|
241
251
|
}, taskConfigObjectSchema);
|
|
@@ -3580,6 +3580,10 @@ button.copy-command:focus-visible {
|
|
|
3580
3580
|
#dag-inspector-panel-prompt-restart {
|
|
3581
3581
|
display: flex;
|
|
3582
3582
|
flex-direction: column;
|
|
3583
|
+
min-height: 0;
|
|
3584
|
+
}
|
|
3585
|
+
#dag-inspector-panel-prompt-restart:has(.prompt-restart-panel.is-readonly) {
|
|
3586
|
+
overflow: hidden;
|
|
3583
3587
|
}
|
|
3584
3588
|
.prompt-restart-panel,
|
|
3585
3589
|
.prompt-restart-box {
|
|
@@ -3589,6 +3593,14 @@ button.copy-command:focus-visible {
|
|
|
3589
3593
|
min-width: 0;
|
|
3590
3594
|
flex: 1;
|
|
3591
3595
|
}
|
|
3596
|
+
.prompt-restart-panel.is-readonly {
|
|
3597
|
+
min-height: 0;
|
|
3598
|
+
overflow: hidden;
|
|
3599
|
+
}
|
|
3600
|
+
.prompt-restart-panel.is-readonly .prompt-restart-prompt-card {
|
|
3601
|
+
flex: 1 1 auto;
|
|
3602
|
+
min-height: 12rem;
|
|
3603
|
+
}
|
|
3592
3604
|
.prompt-restart-toolbar {
|
|
3593
3605
|
display: grid;
|
|
3594
3606
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
@@ -3814,7 +3826,6 @@ button.copy-command:focus-visible {
|
|
|
3814
3826
|
.prompt-restart-readonly {
|
|
3815
3827
|
margin: 0;
|
|
3816
3828
|
padding: 14px 16px;
|
|
3817
|
-
max-height: min(36vh, 18rem);
|
|
3818
3829
|
overflow: auto;
|
|
3819
3830
|
background: var(--canvas-soft);
|
|
3820
3831
|
color: var(--ink);
|
|
@@ -3825,6 +3836,14 @@ button.copy-command:focus-visible {
|
|
|
3825
3836
|
overflow-wrap: anywhere;
|
|
3826
3837
|
tab-size: 2;
|
|
3827
3838
|
}
|
|
3839
|
+
.prompt-restart-panel.is-readonly .prompt-restart-readonly {
|
|
3840
|
+
flex: 1 1 auto;
|
|
3841
|
+
min-height: 0;
|
|
3842
|
+
max-height: none;
|
|
3843
|
+
}
|
|
3844
|
+
.prompt-restart-panel.is-edit .prompt-restart-readonly {
|
|
3845
|
+
max-height: min(36vh, 18rem);
|
|
3846
|
+
}
|
|
3828
3847
|
.prompt-restart-textarea {
|
|
3829
3848
|
width: 100%;
|
|
3830
3849
|
min-height: 8rem;
|
|
@@ -1404,7 +1404,7 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
1404
1404
|
|
|
1405
1405
|
if (node.status !== "ERROR") {
|
|
1406
1406
|
// Non-failed nodes still show their frozen prompt — read-only.
|
|
1407
|
-
const panel = el("div", "prompt-restart-panel");
|
|
1407
|
+
const panel = el("div", "prompt-restart-panel is-readonly");
|
|
1408
1408
|
panel.appendChild(
|
|
1409
1409
|
createPromptRestartBanner({
|
|
1410
1410
|
tone: "readonly",
|
|
@@ -4412,6 +4412,7 @@ async function buildBackendTestGapFillDag(sources) {
|
|
|
4412
4412
|
],
|
|
4413
4413
|
};
|
|
4414
4414
|
spec.backendTestLayout = layout;
|
|
4415
|
+
applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
|
|
4415
4416
|
if (sharedSetup) {
|
|
4416
4417
|
spec.backendTestSharedSetup = { ...sharedSetup };
|
|
4417
4418
|
}
|
|
@@ -4868,6 +4869,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4868
4869
|
// against the resolved layout. The default layout is identity, so the
|
|
4869
4870
|
// packaged template stays byte-identical with the historical contract.
|
|
4870
4871
|
applyBackendTestLayoutToDagSpec(spec, layout);
|
|
4872
|
+
applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
|
|
4871
4873
|
// Plan B: carry the bound shared-setup document on the spec so runtime
|
|
4872
4874
|
// validators (N6 markdown case validation) see the same binding as prompts.
|
|
4873
4875
|
if (intake.sharedSetup) {
|
|
@@ -4878,6 +4880,23 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4878
4880
|
assertValidDagSpec(spec);
|
|
4879
4881
|
return spec;
|
|
4880
4882
|
}
|
|
4883
|
+
function applyBackendTestWorkspaceControl(spec, control) {
|
|
4884
|
+
if (control !== "filesystem-only") {
|
|
4885
|
+
delete spec.backendTestWorkspaceControl;
|
|
4886
|
+
return;
|
|
4887
|
+
}
|
|
4888
|
+
spec.backendTestWorkspaceControl = control;
|
|
4889
|
+
for (const task of spec.tasks) {
|
|
4890
|
+
if ((task.executor === "pi" && task.toolProfile === "write") ||
|
|
4891
|
+
task.shell?.backendTestPipeline) {
|
|
4892
|
+
task.writeGuardPolicy = "filesystem-only";
|
|
4893
|
+
}
|
|
4894
|
+
const child = task.dynamicExpansion?.childTask;
|
|
4895
|
+
if (child?.executor === "pi" && child.toolProfile === "write") {
|
|
4896
|
+
child.writeGuardPolicy = "filesystem-only";
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4881
4900
|
/** Rewrite layout-dependent strings across a compiled backend-test DAG spec (plan A). */
|
|
4882
4901
|
function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
4883
4902
|
if (layout.isDefault) {
|
|
@@ -382,7 +382,7 @@ export const dagWritePolicySchema = z.enum([
|
|
|
382
382
|
"none",
|
|
383
383
|
]);
|
|
384
384
|
/** Git/post-diff write guard intensity for exclusive Pi writers. */
|
|
385
|
-
export const dagWriteGuardPolicySchema = z.enum(["full", "tools-only"]);
|
|
385
|
+
export const dagWriteGuardPolicySchema = z.enum(["full", "tools-only", "filesystem-only"]);
|
|
386
386
|
/**
|
|
387
387
|
* Fail-closed outcome/diff consistency contract for bounded Pi writers.
|
|
388
388
|
* The writer must begin with IMPLEMENTATION_OUTCOME: changed,
|
|
@@ -997,6 +997,8 @@ export const dagSpecSchema = z
|
|
|
997
997
|
taskContractBinding: dagTaskContractBindingSchema.optional(),
|
|
998
998
|
/** Optional unresolved parent-run feedback for a full task rerun. */
|
|
999
999
|
rerunFeedback: dagRerunFeedbackSchema.optional(),
|
|
1000
|
+
/** Backend-test-only workspace mutation control; absent means Git. */
|
|
1001
|
+
backendTestWorkspaceControl: z.enum(["git", "filesystem-only"]).optional(),
|
|
1000
1002
|
/** Frozen backend-test artifact layout (plan A); absent = historical default testcase/ layout. */
|
|
1001
1003
|
backendTestLayout: z
|
|
1002
1004
|
.object({
|