@tea-agent/loop-agent 0.28.2 → 0.28.3

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
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.28.3] - 2026-08-05
6
+
7
+ ### 重点更新
8
+
9
+ - 修复 Night Scheduler 准入控制因运行态文件自污染导致 clean-tree 校验总是误拒的问题
10
+
11
+ ### 修复
12
+
13
+ - Night Scheduler 准入控制:freezeCleanBase 忽略 .harness/** 与 .worktrees/** 运行态事实,避免 lifecycle 运行事实自污染导致 clean-tree 误拒,同时保留失败 admission 的 submitted → rejected 审计链
14
+ - 调整 freezeCleanBase 的执行顺序,将其提前至 submitSchedule 之前,确保 baseCommit 能在调度器写入控制文件前正确捕获 HEAD
15
+ - freezeCleanBase 将 .harness 与 .worktrees 视为非源码脏污(与 harvest 逻辑共享),避免准入流程自写入的调度事实干扰基线冻结
16
+
5
17
  ## [0.28.2] - 2026-08-05
6
18
 
7
19
  ### 重点更新
@@ -64,6 +64,10 @@ export async function prepareAdmission(input) {
64
64
  path.basename(featureDir) !== taskSpec.feature_id) {
65
65
  // Soft check: feature dir basename should match feature_id when conventional.
66
66
  }
67
+ // Freeze base before writing schedule facts. Runtime under .harness/.worktrees
68
+ // is ignored by freezeCleanBase, but freezing early still captures the intended HEAD.
69
+ const baseFactory = input.baseFactory ?? freezeCleanBase;
70
+ const base = await baseFactory(controlRepoRoot);
67
71
  const submitted = await submitSchedule({
68
72
  controlRepoRoot,
69
73
  featureId: taskSpec.feature_id,
@@ -108,8 +112,6 @@ export async function prepareAdmission(input) {
108
112
  toStatus: "validating",
109
113
  now,
110
114
  });
111
- const baseFactory = input.baseFactory ?? freezeCleanBase;
112
- const base = await baseFactory(controlRepoRoot);
113
115
  const workspaceFactory = input.workspaceFactory ?? prepareNightWorkspace;
114
116
  const workspace = await workspaceFactory({
115
117
  controlRepoRoot,
@@ -1,7 +1,39 @@
1
1
  import { spawn } from "node:child_process";
2
+ /**
3
+ * Scheduler/Task Pool runtime under `.harness` and night worktrees are expected
4
+ * control facts. They must not block admission/harvest of source commits.
5
+ */
6
+ export function isSchedulerRuntimeGitPath(filePath) {
7
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
8
+ return (normalized === ".harness" ||
9
+ normalized === ".worktrees" ||
10
+ normalized.startsWith(".harness/") ||
11
+ normalized.startsWith(".worktrees/"));
12
+ }
13
+ /**
14
+ * Parse `git status --porcelain=v1` and return source-path dirty lines only.
15
+ * XY markers occupy the first two columns; path starts at index 3.
16
+ */
17
+ export function filterSourceDirtyPorcelain(statusPorcelain) {
18
+ return statusPorcelain
19
+ .split("\n")
20
+ .map((line) => line.trimEnd())
21
+ .filter(Boolean)
22
+ .filter((line) => {
23
+ // Rename lines: `R old -> new` — treat either side as dirty source.
24
+ const body = line.slice(3).trim();
25
+ if (body.includes(" -> ")) {
26
+ const [from, to] = body.split(" -> ").map((part) => part.trim());
27
+ return ((from ? !isSchedulerRuntimeGitPath(from) : false) ||
28
+ (to ? !isSchedulerRuntimeGitPath(to) : false));
29
+ }
30
+ return !isSchedulerRuntimeGitPath(body);
31
+ });
32
+ }
2
33
  /**
3
34
  * Freeze the control repo HEAD as admission base.
4
- * MVP requires a named branch and clean working tree.
35
+ * MVP requires a named branch and a clean *source* working tree.
36
+ * Runtime facts under `.harness/**` / `.worktrees/**` are ignored (ADR 0009).
5
37
  */
6
38
  export async function freezeCleanBase(controlRepoRoot) {
7
39
  const branch = (await runGit(controlRepoRoot, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
@@ -11,13 +43,18 @@ export async function freezeCleanBase(controlRepoRoot) {
11
43
  "status",
12
44
  "--porcelain=v1",
13
45
  "--untracked-files=all",
14
- ])).trim();
15
- const isClean = statusPorcelain.length === 0;
46
+ ])).trimEnd();
47
+ const dirtySourceLines = filterSourceDirtyPorcelain(statusPorcelain);
48
+ const isClean = dirtySourceLines.length === 0;
16
49
  if (isDetached) {
17
50
  throw new Error("admission requires a named base branch (detached HEAD is not allowed)");
18
51
  }
19
52
  if (!isClean) {
20
- throw new Error("admission requires a clean Git working tree on the base branch");
53
+ const sample = dirtySourceLines
54
+ .slice(0, 5)
55
+ .map((line) => line.slice(3).trim())
56
+ .join(", ");
57
+ throw new Error(`admission requires a clean Git working tree on the base branch${sample ? ` (dirty source: ${sample})` : ""}`);
21
58
  }
22
59
  return {
23
60
  baseBranch: branch,
@@ -25,6 +62,7 @@ export async function freezeCleanBase(controlRepoRoot) {
25
62
  isClean,
26
63
  isDetached,
27
64
  statusPorcelain,
65
+ dirtySourceLines,
28
66
  };
29
67
  }
30
68
  export async function runGit(cwd, args) {
@@ -1,6 +1,7 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
+ import { filterSourceDirtyPorcelain } from "./git-base.js";
4
5
  import { getLeasePath } from "./paths.js";
5
6
  import { verifyEvidenceArchive } from "./evidence.js";
6
7
  import { patchScheduleIsolation, transitionSchedule, } from "./lifecycle.js";
@@ -50,20 +51,10 @@ export async function harvestNightSchedule(input) {
50
51
  "status",
51
52
  "--porcelain=v1",
52
53
  "--untracked-files=all",
53
- ])).trim();
54
+ ])).trimEnd();
54
55
  // Scheduler/Task Pool runtime under .harness and night worktrees are expected
55
56
  // control facts; they must not block harvest of source commits.
56
- const dirtySource = baseStatus
57
- .split("\n")
58
- .map((line) => line.trim())
59
- .filter(Boolean)
60
- .filter((line) => {
61
- const filePath = line.slice(3).trim().replace(/\\/g, "/");
62
- return (!filePath.startsWith(".harness/") &&
63
- !filePath.startsWith(".worktrees/") &&
64
- filePath !== ".harness" &&
65
- filePath !== ".worktrees");
66
- });
57
+ const dirtySource = filterSourceDirtyPorcelain(baseStatus);
67
58
  if (dirtySource.length > 0) {
68
59
  return blocked("base-dirty", `control repo has uncommitted source changes: ${dirtySource
69
60
  .slice(0, 5)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",