@tea-agent/loop-agent 0.26.2 → 0.26.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 +43 -0
- package/dist/executors/dag-pi-executor.js +182 -10
- package/dist/executors/pi-executor.js +233 -147
- package/dist/executors/pi-sdk-executor.js +140 -81
- package/dist/executors/pi-writer-tool-policy.js +266 -0
- package/dist/executors/shell-executor.js +355 -52
- package/dist/executors/shell-write-guard.js +145 -12
- package/dist/governance/document-index-closure.js +164 -0
- package/dist/worker/observe/node-input.js +8 -11
- package/dist/workflows/dag/convergence/controller.js +100 -3
- package/dist/workflows/dag/frontend-repair.js +29 -29
- package/dist/workflows/dag/frontend-verification-trace.js +12 -2
- package/dist/workflows/dag/governance-profile.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +70 -16
- package/dist/workflows/dag/repair-artifact.js +100 -4
- package/dist/workflows/dag/rerun-plan.js +14 -7
- package/dist/workflows/dag/types.js +7 -1
- package/dist/workflows/dag/workspace-checkpoint.js +42 -0
- package/docs/templates/init-managed-agents.md +3 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +13 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.26.4] - 2026-08-03
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 强化 DAG 运行时安全与 writer 工具级写边界
|
|
10
|
+
- 收敛效率与可选 document catalog 兼容
|
|
11
|
+
- 防污染 review-pass 知识写入
|
|
12
|
+
|
|
13
|
+
### 改进
|
|
14
|
+
|
|
15
|
+
- fresh init 与旧项目 `init update --apply-safe` 刷新的 `AGENTS.md` managed block 现要求持续监控 DAG 至 `FINISHED`、`FAILED`、`partial_failed` 等已结束终态或 Decision Gate 需要 approve;判活须组合 runner heartbeat、session events 与 `dag doctor` liveness/provider meaningful progress,并禁止 exclusive writer 运行期间主会话或其他 writer 并发修改工作区
|
|
16
|
+
- DAG 治理闭包与收敛效率:已配置 `docs/document-catalog.json` 的项目在生成时从 `completeDirectoryIndexes` 注入具体 index companion 到 implement/repair 内存路径,未配置该可选 catalog 的项目保持原有 DAG 生成能力;repair artifact 支持 `blocked-boundary`/`requiredScope` 与 empty-manifest `no-progress` 提前止损;DAG shell 支持显式 `failFast`,supervised hard verify 在同 run、同工作区指纹与命令契约下仅复用成功命令并写可审计 receipt。真实非零退出与 finished 聚合规则不放宽,且不跨 run 缓存验证结果
|
|
17
|
+
- DAG Pi writer 工具级写边界:移除 writer unrestricted `bash`,以 SDK 同名受控 `edit`/`write` 在文件 API 执行前强制 repo containment、symlink/ancestor containment、`allowedPaths`/`writeSet`/`forbiddenPaths`,并硬拒绝 `.harness/tasks/**` 与 `.harness/dag-runs/**`;CLI-only 与 SDK→CLI 对 writer fail-closed(read-only `cli-only` rollback 保留),SDK resource loader 与 CLI 同时关闭 Pi extension 自动发现,阻止 extension 在 session 启动或覆盖工具后绕过沙箱。授权递归 glob(如 `src/**`)可创建其覆盖的中间目录,但同级越界目录仍在 mutation 前拒绝。same-run hard-verify workspace fingerprint 纳入当前 Task Contract 稳定 ignored 治理输入;这些文件漂移后成功命令全部 `reused=false`,runner-owned dag-runs volatile 不进入指纹。post-run write-guard 与 change-manifest 保持第二层防御
|
|
18
|
+
- DAG convergence 成功知识防污染:只有 `review-pi` 与 review verdict recovery 均 FINISHED 且明确 `VERDICT: pass` 才写入 `review-pass` knowledge pattern;review gate 仅完成路由但上游 authority/protocol/review verdict 非 pass 时按 `non-retry-failure` 收口,不再把 `request-revision` 或被跳过的 review 误记为成功
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
|
|
22
|
+
- 修复未配置 document catalog 的项目在 DAG 生成路径上的兼容问题,避免可选 catalog 成为硬依赖
|
|
23
|
+
- 提升 convergence 与 writer 可靠性
|
|
24
|
+
|
|
25
|
+
## [0.26.3] - 2026-08-02
|
|
26
|
+
|
|
27
|
+
### 重点更新
|
|
28
|
+
|
|
29
|
+
- DAG 无害工具副产物自动恢复
|
|
30
|
+
- 提升前端验证命令的精确度与失败分类
|
|
31
|
+
- 隔离测试固件与运行环境标记
|
|
32
|
+
|
|
33
|
+
### 新增
|
|
34
|
+
|
|
35
|
+
- DAG 无害工具副产物自动恢复:自动清理 Git Bash 误用 `> nul` / `2> nul` 在仓库根目录生成的普通非 symlink untracked `nul` 文件,避免其干扰 Git 状态复查与变更清单生成
|
|
36
|
+
|
|
37
|
+
### 改进
|
|
38
|
+
|
|
39
|
+
- 精确限定前端验证命令的作用域,提升验证执行的准确性
|
|
40
|
+
- 更精准地分类前端验证失败原因,避免模糊报错
|
|
41
|
+
- 将运行器标记与测试固件进行安全隔离,防止测试环境干扰
|
|
42
|
+
|
|
43
|
+
### 修复
|
|
44
|
+
|
|
45
|
+
- 修复配置验证目标错误处理符号引用的问题
|
|
46
|
+
- 修复 DAG 执行中无害的根目录 nul 副产物导致流程异常的问题
|
|
47
|
+
|
|
5
48
|
## [0.26.2] - 2026-08-02
|
|
6
49
|
|
|
7
50
|
### 重点更新
|
|
@@ -2,13 +2,14 @@ import path from "node:path";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
4
4
|
import { executePiStep, } from "./pi-executor.js";
|
|
5
|
+
import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
|
|
5
6
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
6
|
-
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
7
|
+
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
7
8
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
8
9
|
export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
10
|
+
/** Bounded writer tools: no unrestricted bash; edit/write are policy-wrapped via SDK customTools. */
|
|
9
11
|
export const DAG_PI_WRITE_TOOLS = [
|
|
10
12
|
"read",
|
|
11
|
-
"bash",
|
|
12
13
|
"edit",
|
|
13
14
|
"write",
|
|
14
15
|
"grep",
|
|
@@ -259,6 +260,7 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
|
|
|
259
260
|
}
|
|
260
261
|
const DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES = {
|
|
261
262
|
readGitStatusPorcelain,
|
|
263
|
+
recoverRootNulArtifact,
|
|
262
264
|
};
|
|
263
265
|
export async function executeDagPiNode(input, meta, piStepFn = executePiStep, writeGuardDependencies = DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES) {
|
|
264
266
|
const started = Date.now();
|
|
@@ -312,6 +314,29 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
312
314
|
});
|
|
313
315
|
}
|
|
314
316
|
: undefined;
|
|
317
|
+
let writerToolPolicy;
|
|
318
|
+
if (isWriteTask) {
|
|
319
|
+
try {
|
|
320
|
+
const policyContext = buildPiWriterToolPolicyContext({
|
|
321
|
+
repoRoot: input.cwd,
|
|
322
|
+
allowedPaths: input.task.allowedPaths,
|
|
323
|
+
writeSet: input.task.writeSet,
|
|
324
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
325
|
+
writePolicy: input.task.writePolicy,
|
|
326
|
+
});
|
|
327
|
+
const customTools = await createPiWriterCustomTools(policyContext);
|
|
328
|
+
writerToolPolicy = { requireSdk: true, customTools };
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
stdout: "",
|
|
334
|
+
stderr: `pi writer tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
+
failureCategory: "tool-policy",
|
|
336
|
+
durationMs: Date.now() - started,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
315
340
|
const result = await piStepFn({
|
|
316
341
|
attachedFiles: [],
|
|
317
342
|
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
@@ -325,6 +350,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
325
350
|
timeoutMs: input.timeoutMs,
|
|
326
351
|
stallTimeoutMs: input.stallTimeoutMs,
|
|
327
352
|
abortGraceMs: input.abortGraceMs,
|
|
353
|
+
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
328
354
|
onActivity: bridgeActivity
|
|
329
355
|
? (activity) => {
|
|
330
356
|
if (activity.kind === "lease" ||
|
|
@@ -360,10 +386,105 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
360
386
|
let changeManifestAfterStatus;
|
|
361
387
|
let changeManifestChangedFiles;
|
|
362
388
|
if (beforeStatus !== undefined) {
|
|
389
|
+
let recoveryEvidence;
|
|
390
|
+
let removalPendingRecheck;
|
|
363
391
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
392
|
+
let afterStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, { phase: "pi-writer-after" });
|
|
393
|
+
let afterSnapshot = snapshotGitStatusPorcelain(afterStatus);
|
|
394
|
+
let afterPathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, afterSnapshot);
|
|
395
|
+
// Benign tool artifact recovery: a new untracked bounded regular file
|
|
396
|
+
// literally named `nul` at the repository root is removed with
|
|
397
|
+
// the Node file API, then git status is recaptured so the guard,
|
|
398
|
+
// outcome validation and change manifest only ever see the
|
|
399
|
+
// sanitized diff. Every non-matching candidate stays fail-closed.
|
|
400
|
+
if (afterSnapshot.has("nul")) {
|
|
401
|
+
const recoverRootNulArtifactFn = writeGuardDependencies.recoverRootNulArtifact ??
|
|
402
|
+
recoverRootNulArtifact;
|
|
403
|
+
const recovery = await recoverRootNulArtifactFn({
|
|
404
|
+
rootCwd: input.cwd,
|
|
405
|
+
beforeSnapshot: snapshotGitStatusPorcelain(beforeStatus),
|
|
406
|
+
afterSnapshot,
|
|
407
|
+
afterPathFingerprints,
|
|
408
|
+
candidateAuthorized: validateShellWriteGuardFromDiff({
|
|
409
|
+
changedFiles: ["nul"],
|
|
410
|
+
task: input.task,
|
|
411
|
+
concurrentSiblingWriteSets: meta.concurrentSiblingWriteSets,
|
|
412
|
+
}).ok,
|
|
413
|
+
});
|
|
414
|
+
const beforeStatusSha256 = createHash("sha256")
|
|
415
|
+
.update(beforeStatus)
|
|
416
|
+
.digest("hex");
|
|
417
|
+
const afterStatusSha256 = createHash("sha256")
|
|
418
|
+
.update(afterStatus)
|
|
419
|
+
.digest("hex");
|
|
420
|
+
if (recovery.action === "removed") {
|
|
421
|
+
const evidenceBase = {
|
|
422
|
+
candidatePath: "nul",
|
|
423
|
+
reason: recovery.reason,
|
|
424
|
+
action: "recovered",
|
|
425
|
+
result: "removed",
|
|
426
|
+
observedSizeBytes: recovery.observedSizeBytes,
|
|
427
|
+
removedAt: recovery.removedAt,
|
|
428
|
+
beforeStatusSha256,
|
|
429
|
+
afterStatusSha256,
|
|
430
|
+
};
|
|
431
|
+
removalPendingRecheck = evidenceBase;
|
|
432
|
+
const recheckedStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, {
|
|
433
|
+
phase: "pi-writer-after-recheck",
|
|
434
|
+
});
|
|
435
|
+
const recheckedSnapshot = snapshotGitStatusPorcelain(recheckedStatus);
|
|
436
|
+
afterStatus = recheckedStatus;
|
|
437
|
+
afterSnapshot = recheckedSnapshot;
|
|
438
|
+
afterPathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, recheckedSnapshot);
|
|
439
|
+
recoveryEvidence = {
|
|
440
|
+
...evidenceBase,
|
|
441
|
+
recheck: {
|
|
442
|
+
statusSha256: createHash("sha256")
|
|
443
|
+
.update(recheckedStatus)
|
|
444
|
+
.digest("hex"),
|
|
445
|
+
nulStillPresent: recheckedSnapshot.has("nul"),
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
removalPendingRecheck = undefined;
|
|
449
|
+
}
|
|
450
|
+
else if (recovery.action === "skipped") {
|
|
451
|
+
recoveryEvidence = {
|
|
452
|
+
candidatePath: "nul",
|
|
453
|
+
reason: recovery.reason,
|
|
454
|
+
action: "skipped",
|
|
455
|
+
result: "kept",
|
|
456
|
+
...(recovery.observedSizeBytes === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { observedSizeBytes: recovery.observedSizeBytes }),
|
|
459
|
+
beforeStatusSha256,
|
|
460
|
+
afterStatusSha256,
|
|
461
|
+
recheck: {
|
|
462
|
+
statusSha256: afterStatusSha256,
|
|
463
|
+
nulStillPresent: true,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
recoveryEvidence = {
|
|
469
|
+
candidatePath: "nul",
|
|
470
|
+
reason: recovery.reason,
|
|
471
|
+
action: "failed",
|
|
472
|
+
result: "removal-failed",
|
|
473
|
+
observedSizeBytes: recovery.observedSizeBytes,
|
|
474
|
+
errorDetail: recovery.errorDetail,
|
|
475
|
+
beforeStatusSha256,
|
|
476
|
+
afterStatusSha256,
|
|
477
|
+
recheck: {
|
|
478
|
+
statusSha256: afterStatusSha256,
|
|
479
|
+
nulStillPresent: true,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
writeGuardOk = false;
|
|
483
|
+
writeGuardViolations = [
|
|
484
|
+
`nul artifact removal failed: ${recovery.errorDetail}`,
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
367
488
|
const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
|
|
368
489
|
changeManifestAfterStatus = afterStatus;
|
|
369
490
|
changeManifestChangedFiles = changedFiles;
|
|
@@ -372,16 +493,35 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
372
493
|
task: input.task,
|
|
373
494
|
concurrentSiblingWriteSets: meta.concurrentSiblingWriteSets,
|
|
374
495
|
});
|
|
375
|
-
writeGuardOk = guard.ok;
|
|
376
|
-
writeGuardViolations = guard.violations;
|
|
496
|
+
writeGuardOk = writeGuardOk && guard.ok;
|
|
497
|
+
writeGuardViolations = [...writeGuardViolations, ...guard.violations];
|
|
377
498
|
}
|
|
378
499
|
catch (error) {
|
|
379
500
|
await persistGitWriteGuardDiagnostics(meta.runDir, input.task.id, error);
|
|
501
|
+
if (removalPendingRecheck) {
|
|
502
|
+
// The artifact was already deleted but the recheck read failed:
|
|
503
|
+
// record the recovery facts with the recheck failure so the
|
|
504
|
+
// evidence trail stays complete while the node remains failed.
|
|
505
|
+
recoveryEvidence = {
|
|
506
|
+
...removalPendingRecheck,
|
|
507
|
+
recheck: {
|
|
508
|
+
failed: true,
|
|
509
|
+
errorDetail: boundedRecoveryErrorDetail(error),
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
380
513
|
writeGuardOk = false;
|
|
381
514
|
writeGuardViolations = [
|
|
382
515
|
`git status unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
383
516
|
];
|
|
384
517
|
}
|
|
518
|
+
if (recoveryEvidence) {
|
|
519
|
+
await persistBenignToolArtifactRecovery({
|
|
520
|
+
runDir: meta.runDir,
|
|
521
|
+
nodeId: input.task.id,
|
|
522
|
+
evidence: recoveryEvidence,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
385
525
|
}
|
|
386
526
|
let writerOutcomeViolation;
|
|
387
527
|
if (mapped.ok && input.task.writerOutcomePolicy) {
|
|
@@ -624,9 +764,7 @@ async function persistWriterGitBaseline(input) {
|
|
|
624
764
|
nodeId: input.nodeId,
|
|
625
765
|
phase: "pi-writer-before",
|
|
626
766
|
capturedAt: new Date().toISOString(),
|
|
627
|
-
statusSha256: createHash("sha256")
|
|
628
|
-
.update(input.beforeStatus)
|
|
629
|
-
.digest("hex"),
|
|
767
|
+
statusSha256: createHash("sha256").update(input.beforeStatus).digest("hex"),
|
|
630
768
|
dirtyPaths: [...input.beforeSnapshot.keys()].sort(),
|
|
631
769
|
pathFingerprintsSha256: createHash("sha256")
|
|
632
770
|
.update(JSON.stringify(fingerprintEntries))
|
|
@@ -639,6 +777,40 @@ async function persistGitWriteGuardDiagnostics(runDir, nodeId, error) {
|
|
|
639
777
|
return;
|
|
640
778
|
await writeDagNodeJsonArtifact(runDir, nodeId, "git-write-guard-diagnostics.json", error.diagnostics);
|
|
641
779
|
}
|
|
780
|
+
function boundedRecoveryErrorDetail(error) {
|
|
781
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
782
|
+
return detail.length <= 1000
|
|
783
|
+
? detail
|
|
784
|
+
: `${detail.slice(0, 1000)}...[truncated]`;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Persist bounded run-owned evidence for a recognized repository-root `nul`
|
|
788
|
+
* candidate: candidate path, reason, action, result and recheck facts plus
|
|
789
|
+
* status hashes. Never records environment variables or secrets.
|
|
790
|
+
*/
|
|
791
|
+
async function persistBenignToolArtifactRecovery(input) {
|
|
792
|
+
const artifact = {
|
|
793
|
+
schemaVersion: 1,
|
|
794
|
+
nodeId: input.nodeId,
|
|
795
|
+
candidatePath: input.evidence.candidatePath,
|
|
796
|
+
reason: input.evidence.reason,
|
|
797
|
+
action: input.evidence.action,
|
|
798
|
+
result: input.evidence.result,
|
|
799
|
+
...(input.evidence.observedSizeBytes === undefined
|
|
800
|
+
? {}
|
|
801
|
+
: { observedSizeBytes: input.evidence.observedSizeBytes }),
|
|
802
|
+
...(input.evidence.errorDetail
|
|
803
|
+
? { errorDetail: input.evidence.errorDetail }
|
|
804
|
+
: {}),
|
|
805
|
+
...(input.evidence.removedAt
|
|
806
|
+
? { removedAt: input.evidence.removedAt }
|
|
807
|
+
: {}),
|
|
808
|
+
beforeStatusSha256: input.evidence.beforeStatusSha256,
|
|
809
|
+
afterStatusSha256: input.evidence.afterStatusSha256,
|
|
810
|
+
recheck: input.evidence.recheck,
|
|
811
|
+
};
|
|
812
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "benign-tool-artifact-recovery.json", artifact);
|
|
813
|
+
}
|
|
642
814
|
/**
|
|
643
815
|
* Validate the writer's observed diff against its declared write boundary.
|
|
644
816
|
* Inlined mirror of runPostRunWriteGuard that reuses the already-computed diff
|