@tea-agent/loop-agent 0.28.6 → 0.28.8

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,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.28.8] - 2026-08-05
6
+
7
+ ### 重点更新
8
+
9
+ - 修复 Night Scheduler 收尾提交被运行态文件污染的问题,确保提交内容仅限源码变更
10
+
11
+ ### 修复
12
+
13
+ - Night Scheduler 收尾提交现忽略 .harness/ 与 .worktrees/ 运行态目录,仅暂存 writeSet 源码路径,避免 verify 将 dag-runs 等运行态文件误判为 writer 变更
14
+ - Night Scheduler worktree 在 prepare 时尽力确保 .gitignore 包含 .harness/ 与 .worktrees/,防止运行态文件干扰工作区校验
15
+
16
+ ## [0.28.7] - 2026-08-05
17
+
18
+ ### 重点更新
19
+
20
+ - 修复 Night Scheduler 执行已准备好的 DAG 时因客户端超时中断任务的问题
21
+
22
+ ### 修复
23
+
24
+ - Night Scheduler 执行已准备好的 DAG 时,`task advance --approve-gate` 改用 `timeoutMs: 0`,避免默认 120 秒超时在 DAG 仍在运行时将其强制终止,现由内核负责执行监督
25
+
5
26
  ## [0.28.6] - 2026-08-05
6
27
 
7
28
  ### 重点更新
@@ -65,6 +65,10 @@ export async function executePreparedTaskSpec(options) {
65
65
  cwd: input.workspaceRoot,
66
66
  artifactName: `night-execute-${input.workerRunId}`,
67
67
  expectJson: true,
68
+ // Night prepared execute runs a full DAG under task advance; do not use
69
+ // the short LoopAgentClient default (120s). Zero disables client timeout
70
+ // so the DAG kernel owns long-running supervision (same as run-task).
71
+ timeoutMs: 0,
68
72
  }));
69
73
  const advanceResult = await advance({
70
74
  workspaceRoot,
@@ -1,9 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { parseGitStatusPorcelain, pathMatchesPattern, } from "../../shared/git-progress.js";
3
+ import { isSchedulerRuntimeGitPath } from "./git-base.js";
3
4
  import { formatCommitMessage, validateTaskCard } from "./traceability.js";
4
5
  /**
5
6
  * Create a single closing commit on the night branch after successful execution.
6
7
  * Does not merge to base. Fail-closed on writeSet violations or missing task card.
8
+ *
9
+ * Runtime facts under `.harness/**` / `.worktrees/**` are never staged.
10
+ * When writeSet is present, only matching source paths are added (not `git add -A`).
7
11
  */
8
12
  export async function createNightClosingCommit(input) {
9
13
  const card = validateTaskCard(input.schedule.traceability?.workItemRef, {
@@ -30,19 +34,38 @@ export async function createNightClosingCommit(input) {
30
34
  if (!status) {
31
35
  return { ok: false, reason: "no changes to commit after execution" };
32
36
  }
33
- const changedPaths = parseGitStatusPorcelain(status).map((entry) => entry.path);
37
+ const allChangedPaths = parseGitStatusPorcelain(status).map((entry) => entry.path);
38
+ const sourceChangedPaths = allChangedPaths.filter((p) => !isSchedulerRuntimeGitPath(p));
39
+ if (sourceChangedPaths.length === 0) {
40
+ return {
41
+ ok: false,
42
+ reason: "no source changes to commit after execution (only runtime .harness/.worktrees dirtiness)",
43
+ changedPaths: allChangedPaths,
44
+ };
45
+ }
34
46
  const writeSet = input.writeSet ?? input.admission.dag?.writeSet ?? undefined;
35
47
  if (writeSet && writeSet.length > 0) {
36
- const offenders = changedPaths.filter((p) => !pathAllowedByWriteSet(p, writeSet));
48
+ const offenders = sourceChangedPaths.filter((p) => !pathAllowedByWriteSet(p, writeSet));
37
49
  if (offenders.length > 0) {
38
50
  return {
39
51
  ok: false,
40
52
  reason: `changed paths outside frozen writeSet: ${offenders.join(", ")}`,
41
- changedPaths,
53
+ changedPaths: sourceChangedPaths,
42
54
  };
43
55
  }
44
56
  }
45
- await runGit(input.workspaceRoot, ["add", "-A"]);
57
+ // Stage only source paths (and preferably writeSet-bound ones). Never `git add -A`.
58
+ const pathsToStage = writeSet && writeSet.length > 0
59
+ ? sourceChangedPaths.filter((p) => pathAllowedByWriteSet(p, writeSet))
60
+ : sourceChangedPaths;
61
+ if (pathsToStage.length === 0) {
62
+ return {
63
+ ok: false,
64
+ reason: "no writeSet-allowed source paths to stage",
65
+ changedPaths: sourceChangedPaths,
66
+ };
67
+ }
68
+ await runGit(input.workspaceRoot, ["add", "--", ...pathsToStage]);
46
69
  const summary = `feat(${input.schedule.taskId.toLowerCase()}): night schedule ${input.schedule.id}`;
47
70
  const message = formatCommitMessage({
48
71
  summary,
@@ -60,7 +83,7 @@ export async function createNightClosingCommit(input) {
60
83
  ok: true,
61
84
  commitSha,
62
85
  message,
63
- changedPaths,
86
+ changedPaths: pathsToStage,
64
87
  };
65
88
  }
66
89
  function pathAllowedByWriteSet(filePath, writeSet) {
@@ -1,4 +1,4 @@
1
- import { access, realpath, symlink, unlink } from "node:fs/promises";
1
+ import { access, appendFile, readFile, realpath, symlink, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { ensureOwnedWorktree, verifyOwnedWorktree, } from "../../task/worktree.js";
4
4
  import { getSchedulerRoot } from "./paths.js";
@@ -33,6 +33,9 @@ export async function prepareNightWorkspace(spec) {
33
33
  ...(spec.ownershipToken ? { ownershipToken: spec.ownershipToken } : {}),
34
34
  ...(spec.now ? { now: spec.now } : {}),
35
35
  });
36
+ // Keep runtime roots out of writer/verify dirty trees and closing commits.
37
+ await ensureNightGitignore(ensured.path);
38
+ await ensureNightGitignore(spec.controlRepoRoot);
36
39
  let nodeModulesLinked = false;
37
40
  if (spec.hydrateNodeModules !== false) {
38
41
  nodeModulesLinked = await linkNodeModulesIfPresent(spec.controlRepoRoot, ensured.path);
@@ -98,3 +101,28 @@ async function linkNodeModulesIfPresent(controlRepoRoot, worktreePath) {
98
101
  return false;
99
102
  }
100
103
  }
104
+ async function ensureNightGitignore(repoRoot) {
105
+ const gitignorePath = path.join(repoRoot, ".gitignore");
106
+ const required = [".harness/", ".worktrees/"];
107
+ let existing = "";
108
+ try {
109
+ existing = await readFile(gitignorePath, "utf-8");
110
+ }
111
+ catch {
112
+ await writeFile(gitignorePath, `# Night Scheduler / loop-agent runtime (auto)
113
+ ${required.join("\n")}\n`, "utf-8");
114
+ return;
115
+ }
116
+ const lines = new Set(existing
117
+ .split(/\r?\n/)
118
+ .map((line) => line.trim())
119
+ .filter(Boolean));
120
+ const missing = required.filter((entry) => !lines.has(entry));
121
+ if (missing.length === 0)
122
+ return;
123
+ const suffix = (existing.endsWith("\n") || existing.length === 0 ? "" : "\n") +
124
+ `# Night Scheduler / loop-agent runtime (auto)\n` +
125
+ missing.join("\n") +
126
+ "\n";
127
+ await appendFile(gitignorePath, suffix, "utf-8");
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.28.6",
3
+ "version": "0.28.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",