@kylecheng3146/agent-ops 0.7.2 → 0.7.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.
@@ -141,6 +141,21 @@ export async function worktreeDiff(git, path, base) {
141
141
  }
142
142
  return { files: paths.length, insertions, deletions, paths: paths.slice(0, MAX_PATHS), recent: recent ?? paths[0] ?? null };
143
143
  }
144
+ /**
145
+ * Work already in the target branch by some route other than `worktree finish`
146
+ * (a merged pull request): commits past its base, all reachable from the
147
+ * target, and nothing uncommitted. A fresh worktree (HEAD at base) is not merged.
148
+ * ponytail: ancestry only, so a squash-merged branch stays visible until removed.
149
+ */
150
+ export async function worktreeMerged(git, path, base, targetBranch) {
151
+ const head = (await git(path, ["rev-parse", "HEAD"])).stdout.trim();
152
+ if (!/^[0-9a-f]{40,64}$/u.test(head) || head === base || !/^[A-Za-z0-9._/-]{1,128}$/u.test(targetBranch))
153
+ return false;
154
+ if ((await git(path, ["merge-base", "--is-ancestor", head, `refs/heads/${targetBranch}`])).exitCode !== 0)
155
+ return false;
156
+ const status = await git(path, ["status", "--porcelain"]);
157
+ return status.exitCode === 0 && status.stdout.trim() === "";
158
+ }
144
159
  /** Each run is read on its own: one unreadable run must not hide the building. */
145
160
  export async function readRuns(commonDir) {
146
161
  const directory = join(commonDir, "agent-ops", "runs");
@@ -207,14 +222,21 @@ export async function readReviewSlots(commonDir) {
207
222
  export async function collectOfficeInput(mainRoot, commonDir, git = officeGit, now = Date.now()) {
208
223
  const worktrees = [];
209
224
  const discovered = [];
225
+ const merged = new Set();
210
226
  for (const entry of parseWorktreeListPorcelain((await git(mainRoot, ["worktree", "list", "--porcelain"])).stdout)) {
211
227
  const path = await realpath(resolve(entry.path)).catch(() => resolve(entry.path));
212
228
  const record = await readWorktreeRecord(path).catch(() => null);
213
229
  if (record === null)
214
230
  continue;
231
+ if (record.runId === undefined && await worktreeMerged(git, path, record.base, record.targetBranch)) {
232
+ // A subagent's worktree shares its coordinator's session, which stays.
233
+ if (record.agentId === undefined)
234
+ merged.add(record.sessionId);
235
+ continue;
236
+ }
215
237
  discovered.push({ path, record });
216
238
  }
217
- const sessions = await readSessions(mainRoot, commonDir, now, [mainRoot, ...discovered.map(item => item.path)]);
239
+ const sessions = (await readSessions(mainRoot, commonDir, now, [mainRoot, ...discovered.map(item => item.path)])).filter(session => !merged.has(session.sessionId));
218
240
  for (const { path, record } of discovered) {
219
241
  const session = sessions.find(item => item.sessionId === record.sessionId);
220
242
  const task = await attachedTaskView(path, record.sessionId, session?.taskId);
@@ -68,13 +68,12 @@ function retained(record, now) {
68
68
  const lastSeen = Date.parse(record.lastSeenAt);
69
69
  if (!Number.isFinite(lastSeen))
70
70
  return false;
71
- if (record.status === "active") {
72
- // ponytail: one TTL handles crashed hook clients; a persistent host heartbeat can replace it if needed.
71
+ if (record.status === "active" || record.completedAt === null || record.completedAt === undefined) {
72
+ // ponytail: one TTL handles crashed hook clients and windows closed without `end`;
73
+ // a persistent host heartbeat can replace it if needed.
73
74
  return now - lastSeen <= OFFICE_SESSION_STALE_MS;
74
75
  }
75
- const completed = record.completedAt === null || record.completedAt === undefined
76
- ? lastSeen
77
- : Date.parse(record.completedAt);
76
+ const completed = Date.parse(record.completedAt);
78
77
  return Number.isFinite(completed) && now - completed < OFFICE_SESSION_RETENTION_MS;
79
78
  }
80
79
  function prune(records, now) {
@@ -236,7 +236,7 @@ export class CodexGoalHost {
236
236
  clientInfo: {
237
237
  name: "agent-ops",
238
238
  title: "agent-ops native goal host",
239
- version: "0.7.2"
239
+ version: "0.7.3"
240
240
  }
241
241
  });
242
242
  session.notify("initialized", {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Evidence-driven development loops for agy, Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",