@kylecheng3146/agent-ops 0.3.5 → 0.3.6
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.
|
@@ -6,7 +6,7 @@ import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../secur
|
|
|
6
6
|
import { checkTaskCompletionEvidence, findIncompleteSubtask } from "../task/completion.js";
|
|
7
7
|
import { collectBaseChangePaths, collectChangeSurface } from "../verify/change-surface.js";
|
|
8
8
|
import { calculateSourceFingerprint } from "../verify/source-fingerprint.js";
|
|
9
|
-
import { readWorktreeRecord } from "../parallel/service.js";
|
|
9
|
+
import { NOTES_REF, noteSessionLine, readWorktreeRecord } from "../parallel/service.js";
|
|
10
10
|
const FINGERPRINT = /^[a-f0-9]{64}$/u;
|
|
11
11
|
const SESSION = /^[^\0\r\n]{1,256}$/u;
|
|
12
12
|
function gateDirectory(root) {
|
|
@@ -248,12 +248,32 @@ export class CompletionGateService {
|
|
|
248
248
|
event.event === "command-batch" ? event.commands : [];
|
|
249
249
|
return commands.some(({ command, args }) => [command, ...args].includes("allow-stop"));
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Whether `worktree finish` merged this session's reviewed work: a note
|
|
253
|
+
* naming the session on a commit HEAD contains, and nothing uncommitted on
|
|
254
|
+
* top. finish moves the task into that note, so the checkout has none left.
|
|
255
|
+
*/
|
|
256
|
+
async #finishedByWorktree(sessionId) {
|
|
257
|
+
const runner = this.#options.gitRunner;
|
|
258
|
+
if ((await collectChangeSurface(runner)).paths.length > 0)
|
|
259
|
+
return false;
|
|
260
|
+
// ponytail: newest 500 commits only; older merges need a one-time allow-stop
|
|
261
|
+
const log = await runner.run(["log", `--notes=${NOTES_REF}`, "--format=%N%x00", "-n", "500", "HEAD"]);
|
|
262
|
+
if (log.exitCode !== 0)
|
|
263
|
+
return false;
|
|
264
|
+
const line = noteSessionLine(sessionId);
|
|
265
|
+
return new TextDecoder().decode(log.stdout).split("\0").some((note) => note.split("\n").includes(line));
|
|
266
|
+
}
|
|
251
267
|
async #validateTask(sessionId) {
|
|
252
268
|
let stored;
|
|
253
269
|
try {
|
|
254
270
|
stored = await this.#options.taskService.status({ sessionId });
|
|
255
271
|
}
|
|
256
272
|
catch (error) {
|
|
273
|
+
if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED" &&
|
|
274
|
+
await this.#finishedByWorktree(sessionId)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
257
277
|
return error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED"
|
|
258
278
|
? gateResult("block", "FAIL", "COMPLETION_GATE_TASK_REQUIRED", `Attach this conversation to a formal task: agent-ops task create --title <title> --criterion <json> --criterion <json> (two to five criteria), then agent-ops task attach --task <task-id> --session ${shellQuote(sessionId)}.`)
|
|
259
279
|
: gateResult("block", "UNKNOWN", "COMPLETION_GATE_TASK_UNAVAILABLE", "Repair task state with agent-ops doctor before stopping.");
|
|
@@ -5,8 +5,7 @@ import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
|
5
5
|
import { currentGateFingerprint } from "../hooks/completion-gate.js";
|
|
6
6
|
import { collectChangeSurface } from "../verify/change-surface.js";
|
|
7
7
|
import { aggregateVerificationStatus, executeConfiguredCommand } from "../verify/command-executor.js";
|
|
8
|
-
import { assertWorktreeName, parseWorktreeListPorcelain, readWorktreeRecord, removeCheckout, resolveCheckouts, worktreePath } from "./service.js";
|
|
9
|
-
export const NOTES_REF = "agent-ops";
|
|
8
|
+
import { assertWorktreeName, NOTES_REF, noteSessionLine, parseWorktreeListPorcelain, readWorktreeRecord, removeCheckout, resolveCheckouts, worktreePath } from "./service.js";
|
|
10
9
|
const LOCK_NAME = "agent-ops-finish.lock";
|
|
11
10
|
const LOCK_POLL_MS = 2_000;
|
|
12
11
|
const LOCK_WAIT_MS = 30 * 60 * 1000;
|
|
@@ -180,6 +179,7 @@ function taskTree(tasks) {
|
|
|
180
179
|
function noteText(record, tasks) {
|
|
181
180
|
return [
|
|
182
181
|
`worktree: ${record.name} (${record.branch})`,
|
|
182
|
+
noteSessionLine(record.sessionId),
|
|
183
183
|
...tasks.flatMap(({ task, depth }) => {
|
|
184
184
|
const indent = " ".repeat(depth);
|
|
185
185
|
return [
|
|
@@ -314,8 +314,11 @@ export async function finishWorktree(deps, options) {
|
|
|
314
314
|
if (gateEnabled) {
|
|
315
315
|
await attempt("gate", async () => {
|
|
316
316
|
const mainGate = await deps.gate(mainRoot, mainConfig);
|
|
317
|
-
await
|
|
318
|
-
await mainGate.rebase(before,
|
|
317
|
+
const after = await currentGateFingerprint(mainRoot, mainRunner);
|
|
318
|
+
await mainGate.rebase(before, after);
|
|
319
|
+
// rebase only moves baselines equal to `before`; a main that moved since
|
|
320
|
+
// this session began would leave it reading the merge as its own change.
|
|
321
|
+
await mainGate.seed(record.sessionId, after);
|
|
319
322
|
});
|
|
320
323
|
}
|
|
321
324
|
await attempt("trust", async () => await deps.trust.revoke(record.path, worktreeConfig));
|
|
@@ -15,6 +15,10 @@ import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../secur
|
|
|
15
15
|
export const WORKTREE_DIRECTORY = ".worktrees";
|
|
16
16
|
export const WORKTREE_BRANCH_PREFIX = "agent-ops/";
|
|
17
17
|
export const WORKTREE_RECORD_PATH = ".agent-ops/tasks/worktree.json";
|
|
18
|
+
/** The notes ref `worktree finish` records a merge in. */
|
|
19
|
+
export const NOTES_REF = "agent-ops";
|
|
20
|
+
/** The note line binding a merge to the session that finished it. */
|
|
21
|
+
export const noteSessionLine = (sessionId) => `session: ${sessionId}`;
|
|
18
22
|
const EXCLUDE_LINE = `/${WORKTREE_DIRECTORY}/`;
|
|
19
23
|
const NAME = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
20
24
|
const SESSION = /^[^\0\r\n]{1,256}$/u;
|
|
@@ -213,7 +213,13 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
213
213
|
another worktree is refused, naming that path. If the target moved, it rebases; a clean rebase whose own patch
|
|
214
214
|
is unchanged is re-verified before merging, and a conflict is reported with
|
|
215
215
|
the `refs/notes/agent-ops` intent of the work that landed first. The note
|
|
216
|
-
records every task of the worktree, subtasks indented under their parent
|
|
216
|
+
records every task of the worktree, subtasks indented under their parent,
|
|
217
|
+
and a `session: <id>` line naming the session that finished it. Finish
|
|
218
|
+
also resets that session's gate baseline to the merged source, so the
|
|
219
|
+
session can stop although the task now lives only in the note. As a
|
|
220
|
+
fallback, the gate lets an unattached session stop when the checkout is
|
|
221
|
+
clean and a note with its `session:` line sits within the newest 500
|
|
222
|
+
commits of `HEAD`; older merges need a one-time `allow-stop`.
|
|
217
223
|
- `worktree list`, `resume <name> --session <id>` and `remove <name>` manage
|
|
218
224
|
what is left; `remove --force` discards work and asks the user first.
|
|
219
225
|
`doctor` reports worktrees idle for more than seven days.
|
|
@@ -183,7 +183,12 @@ fingerprint,因此兩個對話修改同一個 checkout 會互相作廢對方
|
|
|
183
183
|
target 的 ref,checkout、檔案與未 commit 的變更都不會被動到。Target 若被其他
|
|
184
184
|
worktree checkout 則拒絕,並指出該路徑。Target 若已前進會先 rebase;沒有衝突且自身 patch 不變的 rebase 會先
|
|
185
185
|
重新驗證再合併;衝突則連同先合併那份工作在 `refs/notes/agent-ops` 的意圖一起
|
|
186
|
-
回報。Note 會記錄 worktree 的每個 task,subtask 縮排在其 parent
|
|
186
|
+
回報。Note 會記錄 worktree 的每個 task,subtask 縮排在其 parent 之下,並附上
|
|
187
|
+
一行 `session: <id>`,標明完成這次 finish 的 session。Finish 也會把該 session
|
|
188
|
+
的 gate baseline 重設為合併後的原始碼,所以即使 task 現在只存在於 note 裡,
|
|
189
|
+
session 仍可停止。作為後備,gate 在 checkout 乾淨、且 `HEAD` 最近 500 個 commit
|
|
190
|
+
內有帶該 `session:` 行的 note 時,會讓未附著 task 的 session 停止;更舊的合併需要
|
|
191
|
+
一次性的 `allow-stop`。
|
|
187
192
|
- `worktree list`、`resume <name> --session <id>` 與 `remove <name>` 管理剩下的
|
|
188
193
|
worktree;`remove --force` 會丟棄工作,執行前會先詢問使用者。`doctor` 會回報
|
|
189
194
|
閒置超過七天的 worktree。
|