@saadnvd1/agent-os 0.3.637 → 0.3.644
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/app/api/bus/branches/route.ts +25 -0
- package/app/api/bus/resurrect/route.test.ts +3 -6
- package/app/api/sessions/[id]/pr/route.ts +3 -3
- package/app/api/sessions/[id]/setup/route.ts +3 -3
- package/app/api/tasks/[id]/approve-plan/route.ts +3 -4
- package/app/api/tasks/[id]/arm/route.ts +6 -5
- package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
- package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
- package/app/api/tasks/[id]/checks/route.ts +1 -1
- package/app/api/tasks/[id]/diff/route.ts +1 -1
- package/app/api/tasks/[id]/done/route.ts +2 -2
- package/app/api/tasks/[id]/drop/route.ts +6 -2
- package/app/api/tasks/[id]/export/route.ts +3 -5
- package/app/api/tasks/[id]/merge/route.ts +3 -3
- package/app/api/tasks/[id]/message/route.ts +2 -2
- package/app/api/tasks/[id]/model/route.ts +5 -1
- package/app/api/tasks/[id]/move/route.ts +5 -1
- package/app/api/tasks/[id]/moved/route.ts +6 -8
- package/app/api/tasks/[id]/plan/route.ts +4 -6
- package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
- package/app/api/tasks/[id]/priority/route.ts +4 -1
- package/app/api/tasks/[id]/resume/route.ts +3 -4
- package/app/api/tasks/[id]/stop/route.ts +2 -2
- package/app/api/tasks/route.ts +1 -1
- package/app/api/worktree-sweep/route.test.ts +40 -0
- package/app/api/worktree-sweep/route.ts +31 -0
- package/app/api/worktree-sweep/run/route.ts +26 -0
- package/components/Health/HealthPanel.tsx +4 -1
- package/components/Health/SweepRuns.tsx +125 -0
- package/components/Merge/GlobalMergeSection.tsx +2 -0
- package/components/Merge/ProjectMergeSection.tsx +2 -0
- package/components/Merge/WorktreeSweepSection.tsx +125 -0
- package/components/Settings/NoApprovalsSection.tsx +1 -1
- package/components/Settings/SettingsDialog.tsx +1 -1
- package/data/worktree-sweep/index.ts +56 -0
- package/lib/approvals/enable.ts +1 -1
- package/lib/away/report.ts +5 -5
- package/lib/away/run.ts +2 -2
- package/lib/bookmarks/store.ts +3 -3
- package/lib/bus/held-reply.ts +2 -2
- package/lib/bus/index.ts +3 -3
- package/lib/chat/compact-context.ts +1 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/backup.ts +49 -0
- package/lib/db/index.ts +5 -1
- package/lib/db/lumifyhub.ts +8 -9
- package/lib/db/machine-scope.ts +12 -0
- package/lib/db/migrations/20261015T1733_gate_loop_guard.ts +40 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/20261015T1830_drop_moved_session_columns.ts +173 -0
- package/lib/db/migrations/20261015T1900_worktree_sweep.ts +30 -0
- package/lib/db/migrations/README.md +11 -0
- package/lib/db/queries.ts +16 -31
- package/lib/db/session-parts/columns.ts +186 -0
- package/lib/db/session-parts/fill.ts +33 -0
- package/lib/db/session-parts/index.ts +10 -0
- package/lib/db/session-parts/pauses.ts +11 -0
- package/lib/db/session-parts/placement.ts +11 -0
- package/lib/db/session-parts/plans.ts +31 -0
- package/lib/db/session-parts/prs.ts +31 -0
- package/lib/db/session-parts/rows.ts +69 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +50 -0
- package/lib/db/session-parts/write.ts +61 -0
- package/lib/done/archive.ts +2 -2
- package/lib/done/auto-archive.ts +1 -1
- package/lib/done/bulk.ts +1 -1
- package/lib/done/index.ts +8 -14
- package/lib/done/remote.ts +9 -20
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +1 -1
- package/lib/health/pieces.ts +1 -1
- package/lib/hosts/peer-actions.ts +2 -2
- package/lib/hosts/peer-mirror.ts +2 -2
- package/lib/hosts/peer-sync.ts +69 -32
- package/lib/learning-cards/input.ts +1 -1
- package/lib/learning-cards/store.ts +1 -1
- package/lib/models/plan-step.ts +10 -24
- package/lib/models/resume.ts +5 -7
- package/lib/notify/index.ts +1 -1
- package/lib/notify/push/events.ts +1 -1
- package/lib/notify/push/index.ts +1 -1
- package/lib/orchestrator/act.ts +1 -1
- package/lib/orchestrator/armed.test-helpers.ts +228 -0
- package/lib/orchestrator/armed.ts +18 -16
- package/lib/orchestrator/ask-approvals.ts +1 -1
- package/lib/orchestrator/ask-heads.ts +1 -1
- package/lib/orchestrator/ask-settle.ts +1 -1
- package/lib/orchestrator/ask-verdict.ts +111 -0
- package/lib/orchestrator/asks.ts +26 -0
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/escalate.ts +25 -12
- package/lib/orchestrator/external-gates.ts +1 -0
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/gates.ts +94 -25
- package/lib/orchestrator/held-asks.ts +8 -11
- package/lib/orchestrator/held-review.ts +8 -4
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/loop-guard.ts +145 -0
- package/lib/orchestrator/merge-approvals.ts +2 -2
- package/lib/orchestrator/overview.ts +1 -1
- package/lib/orchestrator/pr-wake.ts +3 -5
- package/lib/orchestrator/questions.ts +1 -1
- package/lib/orchestrator/quick-store.ts +1 -1
- package/lib/orchestrator/quick.ts +11 -8
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/review-prompt.ts +2 -0
- package/lib/orchestrator/review.ts +14 -4
- package/lib/orchestrator/signoff.test-helpers.ts +322 -0
- package/lib/orchestrator/signoff.ts +49 -11
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/orchestrator/testing.ts +11 -14
- package/lib/orchestrator/watcher.ts +4 -0
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/recipes/test-fixtures.ts +2 -3
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +44 -20
- package/lib/resurrect/swept.ts +61 -0
- package/lib/schedules/store.ts +1 -1
- package/lib/session-audit/sources.ts +3 -3
- package/lib/session-logs/index.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/setup-progress.ts +1 -1
- package/lib/sessions/stop.ts +1 -1
- package/lib/sessions/worktree-setup.ts +2 -1
- package/lib/simulator/index.ts +1 -1
- package/lib/simulator/task-devices.ts +1 -1
- package/lib/stacks/board.ts +1 -1
- package/lib/stacks/land.ts +1 -1
- package/lib/stacks/restack.ts +1 -1
- package/lib/stacks/start.ts +2 -2
- package/lib/stacks/testing.ts +17 -16
- package/lib/stacks/tick.ts +1 -1
- package/lib/stacks/view.ts +1 -1
- package/lib/status/incremental.ts +1 -1
- package/lib/tasks/contract/actions.ts +35 -0
- package/lib/tasks/contract/authorize.ts +82 -0
- package/lib/tasks/contract/client.ts +296 -0
- package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
- package/lib/tasks/contract/index.ts +28 -0
- package/lib/tasks/{remote-plan-path.ts → contract/plan-path.ts} +2 -2
- package/lib/tasks/{remote-plan.ts → contract/plan.ts} +76 -110
- package/lib/tasks/contract/routes.ts +26 -0
- package/lib/tasks/contract/start.ts +122 -0
- package/lib/tasks/contract/sync.ts +126 -0
- package/lib/tasks/contract/types.ts +41 -0
- package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
- package/lib/tasks/finish.ts +30 -23
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +14 -18
- package/lib/tasks/import.ts +19 -15
- package/lib/tasks/index.ts +34 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +1 -1
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move-testing.ts +16 -7
- package/lib/tasks/move.ts +23 -23
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +19 -23
- package/lib/tasks/plan-view.ts +1 -1
- package/lib/tasks/pr-activity.ts +11 -2
- package/lib/tasks/prune-branches.ts +193 -0
- package/lib/tasks/prune-scope.ts +22 -0
- package/lib/tasks/queue-placement.ts +1 -1
- package/lib/tasks/queue-rows.ts +4 -4
- package/lib/tasks/queue-start.ts +5 -3
- package/lib/tasks/queue-tick.ts +2 -2
- package/lib/tasks/queue.ts +14 -6
- package/lib/tasks/remote-branch.ts +170 -0
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/tasks/testing.ts +13 -7
- package/lib/usage/calls.ts +1 -1
- package/lib/usage/compare.ts +1 -1
- package/lib/usage/quality.ts +1 -1
- package/lib/usage/turns.ts +1 -1
- package/lib/voice/vocab.ts +1 -1
- package/lib/worktree-sweep/commits.ts +65 -0
- package/lib/worktree-sweep/findOrphans.ts +139 -0
- package/lib/worktree-sweep/findSettled.ts +112 -0
- package/lib/worktree-sweep/index.ts +10 -0
- package/lib/worktree-sweep/judge.ts +31 -0
- package/lib/worktree-sweep/panes.ts +18 -0
- package/lib/worktree-sweep/runSweep.ts +243 -0
- package/lib/worktree-sweep/settings.ts +83 -0
- package/lib/worktree-sweep/start.ts +64 -0
- package/lib/worktree-sweep/state.ts +36 -0
- package/lib/worktree-sweep/store.ts +96 -0
- package/lib/worktree-sweep/types.ts +53 -0
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/screenshots/seed.ts +19 -18
- package/server.ts +3 -0
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -255
package/lib/tasks/queue.ts
CHANGED
|
@@ -66,6 +66,7 @@ export type {
|
|
|
66
66
|
QueuedTaskView,
|
|
67
67
|
StartOutcome,
|
|
68
68
|
} from "./queue-types";
|
|
69
|
+
import { setTask } from "../db/session-parts";
|
|
69
70
|
export { afterMet, hasFinished, resolveAfter, slotState } from "./queue-rows";
|
|
70
71
|
export { findQueued, listQueue, queuedView } from "./queue-view";
|
|
71
72
|
export { tickOnce, tickQueue, tickQueueSoon } from "./queue-tick";
|
|
@@ -260,11 +261,18 @@ export function setPriority(id: string, priority: Priority): void {
|
|
|
260
261
|
`UPDATE task_queue SET priority = ? WHERE id = ? AND status IN ('queued', 'failed')`
|
|
261
262
|
)
|
|
262
263
|
.run(priority, id).changes;
|
|
263
|
-
const running = db
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
const running = db.transaction(() => {
|
|
265
|
+
const live = db
|
|
266
|
+
.prepare(
|
|
267
|
+
`SELECT 1 FROM session_rows WHERE id = ? AND task_status IS NOT NULL AND archived_at IS NULL`
|
|
268
|
+
)
|
|
269
|
+
.get(id);
|
|
270
|
+
return live
|
|
271
|
+
? setTask(db, id, {
|
|
272
|
+
priority: priority === DEFAULT_PRIORITY ? null : priority,
|
|
273
|
+
})
|
|
274
|
+
: false;
|
|
275
|
+
})();
|
|
268
276
|
if (!queued && !running) throw new Error("No such queued or running task");
|
|
269
277
|
tickQueueSoon();
|
|
270
278
|
}
|
|
@@ -272,7 +280,7 @@ export function setPriority(id: string, priority: Priority): void {
|
|
|
272
280
|
// A task started directly (never queued) keeps the priority it was given.
|
|
273
281
|
export function stampPriority(id: string, priority: Priority | undefined) {
|
|
274
282
|
if (!priority || priority === DEFAULT_PRIORITY) return;
|
|
275
|
-
db
|
|
283
|
+
setTask(db, id, { priority });
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
export function removeQueued(id: string): void {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Deleting a task's own branch on GitHub once its PR is merged or closed.
|
|
2
|
+
// Never throws: a branch that stays is a logged line, not a failed merge.
|
|
3
|
+
|
|
4
|
+
import { git, gitPush } from "../git-exec";
|
|
5
|
+
import { repoSlug } from "../git-remote";
|
|
6
|
+
import { run } from "./gh";
|
|
7
|
+
|
|
8
|
+
// Never deleted, whatever a setting says.
|
|
9
|
+
export const PROTECTED_BRANCHES = new Set(["main", "master", "HEAD"]);
|
|
10
|
+
|
|
11
|
+
export interface BranchDelete {
|
|
12
|
+
deleted: boolean;
|
|
13
|
+
why: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DeleteBranchOpts {
|
|
17
|
+
repo: string;
|
|
18
|
+
branch: string;
|
|
19
|
+
// The head the PR merged or closed at; the branch goes only if it is still
|
|
20
|
+
// exactly this commit.
|
|
21
|
+
prHead: string | null | undefined;
|
|
22
|
+
// A dropped task that never opened a PR has no head to compare with: its
|
|
23
|
+
// branch is rejected work and goes whatever it holds.
|
|
24
|
+
noPr?: boolean;
|
|
25
|
+
// owner/repo, when known; read from the checkout's origin otherwise.
|
|
26
|
+
slug?: string | null;
|
|
27
|
+
// Another open PR in the repo builds on this branch (decided by the caller
|
|
28
|
+
// when it knows from its own records; GitHub is asked too).
|
|
29
|
+
hasChildren?: boolean;
|
|
30
|
+
// Branches this one must never be (the default and base branches).
|
|
31
|
+
keep?: (string | null | undefined)[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const gone = (error: unknown) =>
|
|
35
|
+
/HTTP (404|422)|Not Found|Reference does not exist/i.test(
|
|
36
|
+
error instanceof Error ? error.message : String(error)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// repos/o/r/git/ref(s)/heads/<branch>, each path segment encoded.
|
|
40
|
+
export const refPath = (slug: string, branch: string, plural: boolean) =>
|
|
41
|
+
`repos/${slug}/git/${plural ? "refs" : "ref"}/heads/${branch
|
|
42
|
+
.split("/")
|
|
43
|
+
.map(encodeURIComponent)
|
|
44
|
+
.join("/")}`;
|
|
45
|
+
|
|
46
|
+
// The branch's commit on GitHub; null when it is gone.
|
|
47
|
+
export async function remoteTip(
|
|
48
|
+
repo: string,
|
|
49
|
+
slug: string,
|
|
50
|
+
branch: string
|
|
51
|
+
): Promise<string | null> {
|
|
52
|
+
try {
|
|
53
|
+
const out = await run(
|
|
54
|
+
"gh",
|
|
55
|
+
["api", refPath(slug, branch, false), "--jq", ".object.sha"],
|
|
56
|
+
repo,
|
|
57
|
+
20000
|
|
58
|
+
);
|
|
59
|
+
return out.trim() || null;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (gone(error)) return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Open PRs targeting the branch: a stacked child, which GitHub would close
|
|
67
|
+
// (and could no longer retarget) if its base vanished.
|
|
68
|
+
export async function openChildren(
|
|
69
|
+
repo: string,
|
|
70
|
+
slug: string,
|
|
71
|
+
branch: string
|
|
72
|
+
): Promise<number[]> {
|
|
73
|
+
const out = await run(
|
|
74
|
+
"gh",
|
|
75
|
+
[
|
|
76
|
+
"pr",
|
|
77
|
+
"list",
|
|
78
|
+
"--repo",
|
|
79
|
+
slug,
|
|
80
|
+
"--base",
|
|
81
|
+
branch,
|
|
82
|
+
"--state",
|
|
83
|
+
"open",
|
|
84
|
+
"--json",
|
|
85
|
+
"number",
|
|
86
|
+
],
|
|
87
|
+
repo,
|
|
88
|
+
20000
|
|
89
|
+
);
|
|
90
|
+
return (JSON.parse(out) as { number: number }[]).map((p) => p.number);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function decide(o: DeleteBranchOpts): Promise<BranchDelete> {
|
|
94
|
+
const { repo, branch, prHead } = o;
|
|
95
|
+
if (PROTECTED_BRANCHES.has(branch))
|
|
96
|
+
return { deleted: false, why: "a protected branch" };
|
|
97
|
+
if (o.keep?.includes(branch))
|
|
98
|
+
return { deleted: false, why: "it is a base branch" };
|
|
99
|
+
if (!prHead && !o.noPr)
|
|
100
|
+
return { deleted: false, why: "no PR head was recorded" };
|
|
101
|
+
if (o.hasChildren)
|
|
102
|
+
return { deleted: false, why: "stacked cards still build on it" };
|
|
103
|
+
const slug = o.slug ?? (await repoSlug(repo));
|
|
104
|
+
if (!slug) return deleteViaGit(repo, branch, prHead || null);
|
|
105
|
+
|
|
106
|
+
const kids = await openChildren(repo, slug, branch);
|
|
107
|
+
if (kids.length)
|
|
108
|
+
return {
|
|
109
|
+
deleted: false,
|
|
110
|
+
why: `open PR ${kids.map((n) => `#${n}`).join(", ")} is based on it`,
|
|
111
|
+
};
|
|
112
|
+
const tip = await remoteTip(repo, slug, branch);
|
|
113
|
+
if (!tip) return { deleted: false, why: "already gone" };
|
|
114
|
+
if (prHead && tip !== prHead)
|
|
115
|
+
return {
|
|
116
|
+
deleted: false,
|
|
117
|
+
why: `it moved to ${tip.slice(0, 7)} after the PR's ${prHead.slice(0, 7)}`,
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
await run(
|
|
121
|
+
"gh",
|
|
122
|
+
["api", "-X", "DELETE", refPath(slug, branch, true)],
|
|
123
|
+
repo,
|
|
124
|
+
30000
|
|
125
|
+
);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!gone(error)) throw error;
|
|
128
|
+
}
|
|
129
|
+
return { deleted: true, why: "deleted" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A checkout whose origin isn't on GitHub: ask and delete through git.
|
|
133
|
+
async function deleteViaGit(
|
|
134
|
+
repo: string,
|
|
135
|
+
branch: string,
|
|
136
|
+
prHead: string | null
|
|
137
|
+
): Promise<BranchDelete> {
|
|
138
|
+
const line = await git(
|
|
139
|
+
repo,
|
|
140
|
+
["ls-remote", "--heads", "origin", `refs/heads/${branch}`],
|
|
141
|
+
{ timeout: 30000 }
|
|
142
|
+
);
|
|
143
|
+
const tip = line.trim().split(/\s+/)[0];
|
|
144
|
+
if (!tip) return { deleted: false, why: "already gone" };
|
|
145
|
+
if (prHead && tip !== prHead)
|
|
146
|
+
return {
|
|
147
|
+
deleted: false,
|
|
148
|
+
why: `it moved to ${tip.slice(0, 7)} after the PR's ${prHead.slice(0, 7)}`,
|
|
149
|
+
};
|
|
150
|
+
await gitPush(repo, ["origin", "--delete", branch], { timeout: 60000 });
|
|
151
|
+
return { deleted: true, why: "deleted" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function deleteTaskBranch(
|
|
155
|
+
o: DeleteBranchOpts
|
|
156
|
+
): Promise<BranchDelete> {
|
|
157
|
+
let result: BranchDelete;
|
|
158
|
+
try {
|
|
159
|
+
result = await decide(o);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
162
|
+
result = {
|
|
163
|
+
deleted: false,
|
|
164
|
+
why: `couldn't delete it: ${text.split("\n")[0].slice(0, 200)}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (!result.deleted)
|
|
168
|
+
console.log(`[tasks] kept origin/${o.branch}: ${result.why}`);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
package/lib/tasks/session.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { notifyStatusChanged } from "../status/hub";
|
|
27
27
|
import type { TaskPR } from "./state";
|
|
28
28
|
import { markStatusDirty } from "../status/dirty";
|
|
29
|
+
import { setPr } from "../db/session-parts";
|
|
29
30
|
|
|
30
31
|
export const expandHome = (p: string) => p.replace(/^~/, os.homedir());
|
|
31
32
|
|
|
@@ -33,7 +34,7 @@ export const expandHome = (p: string) => p.replace(/^~/, os.homedir());
|
|
|
33
34
|
export function taskSessions(includeArchived = true): Session[] {
|
|
34
35
|
return db
|
|
35
36
|
.prepare(
|
|
36
|
-
`SELECT ${sessionListColumns(db)} FROM
|
|
37
|
+
`SELECT ${sessionListColumns(db)} FROM session_rows
|
|
37
38
|
WHERE task_status IS NOT NULL AND task_status != 'moved'
|
|
38
39
|
${includeArchived ? "" : "AND archived_at IS NULL"}
|
|
39
40
|
ORDER BY created_at DESC`
|
|
@@ -205,9 +206,11 @@ export async function prFor(
|
|
|
205
206
|
if (pr) pushPrChange(session, pr);
|
|
206
207
|
}
|
|
207
208
|
if (pr) {
|
|
208
|
-
db.
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
setPr(db, session.id, {
|
|
210
|
+
pr_url: pr.url,
|
|
211
|
+
pr_number: pr.number,
|
|
212
|
+
pr_status: pr.state.toLowerCase() as "open" | "merged" | "closed",
|
|
213
|
+
});
|
|
211
214
|
// Merged on GitHub rather than through AgentOS: its log is due too.
|
|
212
215
|
if (pr.state === "MERGED") {
|
|
213
216
|
if (session.pr_status !== "merged") pushMerged(session.id);
|
package/lib/tasks/start.ts
CHANGED
|
@@ -107,7 +107,7 @@ export async function finishTaskStart(
|
|
|
107
107
|
earlier?: TaskSetup
|
|
108
108
|
): Promise<TaskSetup> {
|
|
109
109
|
const session = db
|
|
110
|
-
.prepare(`SELECT * FROM
|
|
110
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
111
111
|
.get(sessionId) as Session | undefined;
|
|
112
112
|
if (!session?.worktree_path) throw new Error(`No task ${sessionId}`);
|
|
113
113
|
const project = session.project_id ? getProject(session.project_id) : null;
|
|
@@ -133,7 +133,7 @@ export async function finishTaskStart(
|
|
|
133
133
|
// The setup's own error is kept, for the agent's prompt when it launches.
|
|
134
134
|
if (held) return done({ ...setup, status: "held" });
|
|
135
135
|
const now = db
|
|
136
|
-
.prepare(`SELECT task_status, archived_at FROM
|
|
136
|
+
.prepare(`SELECT task_status, archived_at FROM session_rows WHERE id = ?`)
|
|
137
137
|
.get(sessionId) as
|
|
138
138
|
| { task_status: string | null; archived_at: string | null }
|
|
139
139
|
| undefined;
|
|
@@ -226,7 +226,7 @@ export async function resumeTaskStarts(
|
|
|
226
226
|
): Promise<string[]> {
|
|
227
227
|
const rows = db
|
|
228
228
|
.prepare(
|
|
229
|
-
`SELECT id, tmux_name, view, setup_ms, setup_error FROM
|
|
229
|
+
`SELECT id, tmux_name, view, setup_ms, setup_error FROM session_rows
|
|
230
230
|
WHERE setup_status = 'running' AND task_status = 'running'
|
|
231
231
|
AND archived_at IS NULL`
|
|
232
232
|
)
|
|
@@ -269,7 +269,7 @@ export async function resumeTaskStarts(
|
|
|
269
269
|
export function resumeHeldStarts(): string[] {
|
|
270
270
|
const rows = db
|
|
271
271
|
.prepare(
|
|
272
|
-
`SELECT id, setup_ms, setup_error FROM
|
|
272
|
+
`SELECT id, setup_ms, setup_error FROM session_rows WHERE setup_status = 'held'
|
|
273
273
|
AND task_status = 'running' AND archived_at IS NULL`
|
|
274
274
|
)
|
|
275
275
|
.all() as ({ id: string } & SetupColumns)[];
|
package/lib/tasks/starter.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { db } from "../db";
|
|
7
|
+
import { setPlacement } from "../db/session-parts";
|
|
7
8
|
|
|
8
9
|
export type StartVia =
|
|
9
10
|
| "session"
|
|
@@ -59,7 +60,7 @@ export function callerSession(input: {
|
|
|
59
60
|
const cwd = path.resolve(input.cwd);
|
|
60
61
|
const rows = db
|
|
61
62
|
.prepare(
|
|
62
|
-
`SELECT id, role, worktree_path FROM
|
|
63
|
+
`SELECT id, role, worktree_path FROM session_rows
|
|
63
64
|
WHERE worktree_path IS NOT NULL AND archived_at IS NULL
|
|
64
65
|
AND task_status IS NOT NULL
|
|
65
66
|
AND (host_id IS NULL OR host_id = 'local')`
|
|
@@ -111,14 +112,10 @@ export function orchestratorOf(startedBy: string | null | undefined) {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
export function recordStart(id: string, s: Starter): void {
|
|
114
|
-
db.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
s.
|
|
119
|
-
|
|
120
|
-
s.startedVia,
|
|
121
|
-
s.startedByDevice ?? null,
|
|
122
|
-
id
|
|
123
|
-
);
|
|
115
|
+
db.transaction(() => {
|
|
116
|
+
db.prepare(
|
|
117
|
+
`UPDATE sessions SET started_by = ?, parent_session_id = (SELECT id FROM sessions WHERE id = ?), started_via = ? WHERE id = ?`
|
|
118
|
+
).run(s.startedBy, s.parentSessionId, s.startedVia, id);
|
|
119
|
+
setPlacement(db, id, { started_by_device: s.startedByDevice ?? undefined });
|
|
120
|
+
})();
|
|
124
121
|
}
|
package/lib/tasks/testing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Test fixtures for task PR lookups: real repositories and worktrees, with
|
|
2
2
|
// gh faked by branch.
|
|
3
3
|
|
|
4
|
-
import { db, type Session } from "../db";
|
|
4
|
+
import { db, insertSessionRow, type Session } from "../db";
|
|
5
5
|
import { createProject } from "../projects";
|
|
6
6
|
import type { FindPROpts } from "./gh";
|
|
7
7
|
import type { TaskPR } from "./state";
|
|
@@ -53,13 +53,19 @@ export function seedTask(
|
|
|
53
53
|
workingDirectory: repo,
|
|
54
54
|
});
|
|
55
55
|
const id = crypto.randomUUID();
|
|
56
|
-
db
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
insertSessionRow(db, {
|
|
57
|
+
id,
|
|
58
|
+
name: "Read and execute the brief",
|
|
59
|
+
tmux_name: `claude-${id}`,
|
|
60
|
+
working_directory: worktree,
|
|
61
|
+
project_id: project.id,
|
|
62
|
+
task_status: "running",
|
|
63
|
+
branch_name: branch,
|
|
64
|
+
base_branch: "main",
|
|
65
|
+
worktree_path: worktree,
|
|
66
|
+
});
|
|
61
67
|
return taskRow(id);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
export const taskRow = (id: string) =>
|
|
65
|
-
db.prepare(`SELECT * FROM
|
|
71
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as Session;
|
package/lib/usage/calls.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function sessionContext(sessionId: string): SessionContext {
|
|
|
97
97
|
const s = db
|
|
98
98
|
.prepare(
|
|
99
99
|
`SELECT name, workspace_id, project_id, model_provider, role, task_status,
|
|
100
|
-
plan_phase, started_by FROM
|
|
100
|
+
plan_phase, started_by FROM session_rows WHERE id = ?`
|
|
101
101
|
)
|
|
102
102
|
.get(sessionId) as
|
|
103
103
|
| {
|
package/lib/usage/compare.ts
CHANGED
|
@@ -225,7 +225,7 @@ export function taskOutcomes(ids: string[]): Map<string, TaskOutcome> {
|
|
|
225
225
|
const marks = ids.map(() => "?").join(",");
|
|
226
226
|
const tasks = db
|
|
227
227
|
.prepare(
|
|
228
|
-
`SELECT id, task_status, plan_decided FROM
|
|
228
|
+
`SELECT id, task_status, plan_decided FROM session_rows
|
|
229
229
|
WHERE id IN (${marks}) AND task_status IS NOT NULL`
|
|
230
230
|
)
|
|
231
231
|
.all(...ids) as {
|
package/lib/usage/quality.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function mergedTasks(from: number, to: number): MergedTask[] {
|
|
|
82
82
|
db
|
|
83
83
|
.prepare(
|
|
84
84
|
`SELECT id, project_id AS projectId, pr_number AS prNumber, merged_at AS mergedAt
|
|
85
|
-
FROM
|
|
85
|
+
FROM session_rows WHERE task_status = 'merged' AND merged_at >= ? AND merged_at < ?`
|
|
86
86
|
)
|
|
87
87
|
.all(sqlTime(from), sqlTime(to)) as MergedTask[]
|
|
88
88
|
).filter((t) => t.mergedAt);
|
package/lib/usage/turns.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function recordTurn(
|
|
|
61
61
|
const current = db
|
|
62
62
|
.prepare(
|
|
63
63
|
`SELECT name, workspace_id, model, model_provider, role, task_status,
|
|
64
|
-
plan_phase, started_by FROM
|
|
64
|
+
plan_phase, started_by FROM session_rows WHERE id = ?`
|
|
65
65
|
)
|
|
66
66
|
.get(session.id) as
|
|
67
67
|
| Pick<
|
package/lib/voice/vocab.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function vocabFor(workspaceId: string): string[] {
|
|
|
44
44
|
workspaceId
|
|
45
45
|
);
|
|
46
46
|
const sessions = names(
|
|
47
|
-
`SELECT s.name FROM
|
|
47
|
+
`SELECT s.name FROM session_rows s
|
|
48
48
|
JOIN projects p ON p.id = s.project_id
|
|
49
49
|
WHERE p.workspace_id = ?
|
|
50
50
|
AND (s.task_status IS NULL OR s.task_status NOT IN ('merged','dropped','done','moved'))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getDefaultBranch } from "../git";
|
|
2
|
+
import { git as runGit } from "../git-exec";
|
|
3
|
+
|
|
4
|
+
const git = (cwd: string, ...args: string[]) =>
|
|
5
|
+
runGit(cwd, args, { timeout: 60_000 }).then((out) => out.trim());
|
|
6
|
+
|
|
7
|
+
const lines = (out: string) => out.split("\n").filter(Boolean);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Commits on the worktree's HEAD that nothing else holds: not on any remote
|
|
11
|
+
* branch, not in the PR a merge took, and with no patch-equivalent commit on
|
|
12
|
+
* the base or its own remote branch (a rewritten history re-lands the same
|
|
13
|
+
* changes under new hashes). The shas, so a caller can count and name them.
|
|
14
|
+
*/
|
|
15
|
+
export async function unpushedCommits(
|
|
16
|
+
worktree: string,
|
|
17
|
+
repo: string,
|
|
18
|
+
opts: { prHead?: string | null; branch?: string | null } = {}
|
|
19
|
+
): Promise<string[]> {
|
|
20
|
+
const head =
|
|
21
|
+
opts.prHead &&
|
|
22
|
+
(await git(
|
|
23
|
+
worktree,
|
|
24
|
+
"rev-parse",
|
|
25
|
+
"--verify",
|
|
26
|
+
"--quiet",
|
|
27
|
+
`${opts.prHead}^{commit}`
|
|
28
|
+
)
|
|
29
|
+
.then(() => [opts.prHead!])
|
|
30
|
+
.catch(() => []));
|
|
31
|
+
const stray = lines(
|
|
32
|
+
await git(
|
|
33
|
+
worktree,
|
|
34
|
+
"rev-list",
|
|
35
|
+
"HEAD",
|
|
36
|
+
"--not",
|
|
37
|
+
...(head || []),
|
|
38
|
+
"--remotes"
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
if (!stray.length) return [];
|
|
42
|
+
const base = await getDefaultBranch(repo).catch(() => "main");
|
|
43
|
+
const upstreams: string[] = [];
|
|
44
|
+
for (const ref of [
|
|
45
|
+
`origin/${base}`,
|
|
46
|
+
opts.branch && `origin/${opts.branch}`,
|
|
47
|
+
]) {
|
|
48
|
+
if (
|
|
49
|
+
ref &&
|
|
50
|
+
(await git(worktree, "rev-parse", "--verify", "--quiet", ref)
|
|
51
|
+
.then(() => true)
|
|
52
|
+
.catch(() => false))
|
|
53
|
+
)
|
|
54
|
+
upstreams.push(ref);
|
|
55
|
+
}
|
|
56
|
+
const left = new Set(stray);
|
|
57
|
+
for (const ref of upstreams) {
|
|
58
|
+
if (!left.size) break;
|
|
59
|
+
// "- <sha>" is a commit the upstream already has the same patch for.
|
|
60
|
+
const cherry = lines(await git(worktree, "cherry", ref, "HEAD"));
|
|
61
|
+
for (const l of cherry)
|
|
62
|
+
if (l.startsWith("- ")) left.delete(l.slice(2).trim());
|
|
63
|
+
}
|
|
64
|
+
return [...left];
|
|
65
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folders in the worktrees root that no session owns. Old ones go, after the
|
|
3
|
+
* same checks as a settled worktree; the throwaway repos tests once left
|
|
4
|
+
* there (they hold nothing of anyone's) go without them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { db } from "../db";
|
|
11
|
+
import { mainCheckoutOf } from "../worktrees";
|
|
12
|
+
import { resolveSweepDays } from "./settings";
|
|
13
|
+
import { inFolder } from "./panes";
|
|
14
|
+
import type { Candidate, SweepSkipped } from "./types";
|
|
15
|
+
|
|
16
|
+
const DAY_MS = 24 * 3600_000;
|
|
17
|
+
|
|
18
|
+
// What a test's quick-change worktree is called (lib/orchestrator/quick.test.ts).
|
|
19
|
+
const TEST_LEAK = /^(aos-quick-repo-|repo-quick-)/;
|
|
20
|
+
// Kept in a folder AgentOS made that holds no work: placed or rebuilt files.
|
|
21
|
+
const DISPOSABLE = new Set([
|
|
22
|
+
"node_modules",
|
|
23
|
+
".next",
|
|
24
|
+
".turbo",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
".git",
|
|
28
|
+
".dev-tmux",
|
|
29
|
+
".agent-os",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const real = (p: string) => {
|
|
33
|
+
try {
|
|
34
|
+
return fs.realpathSync(p);
|
|
35
|
+
} catch {
|
|
36
|
+
return path.resolve(p);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Every folder a session row names, on any machine's row: never an orphan.
|
|
41
|
+
function owned(): Set<string> {
|
|
42
|
+
const rows = db
|
|
43
|
+
.prepare(
|
|
44
|
+
`SELECT worktree_path AS a, working_directory AS b FROM session_rows`
|
|
45
|
+
)
|
|
46
|
+
.all() as Array<{ a: string | null; b: string | null }>;
|
|
47
|
+
const out = new Set<string>();
|
|
48
|
+
for (const r of rows) for (const p of [r.a, r.b]) if (p) out.add(real(p));
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Where a worktree's .git file points; null when it has none.
|
|
53
|
+
function gitdirOf(dir: string): string | null {
|
|
54
|
+
try {
|
|
55
|
+
const m = /^gitdir:\s*(.+)$/m.exec(
|
|
56
|
+
fs.readFileSync(path.join(dir, ".git"), "utf8")
|
|
57
|
+
);
|
|
58
|
+
return m ? m[1].trim() : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const inTemp = (p: string) =>
|
|
65
|
+
[os.tmpdir(), real(os.tmpdir())].some((t) => inFolder(p, t));
|
|
66
|
+
|
|
67
|
+
// Nothing in it but what AgentOS placed or rebuilds: no work to lose.
|
|
68
|
+
export const holdsNothing = (dir: string) =>
|
|
69
|
+
fs
|
|
70
|
+
.readdirSync(dir)
|
|
71
|
+
.filter((f) => !DISPOSABLE.has(f))
|
|
72
|
+
.every((f) => f.startsWith(".env"));
|
|
73
|
+
|
|
74
|
+
const newestMs = (dir: string) => {
|
|
75
|
+
let t = 0;
|
|
76
|
+
for (const p of [dir, path.join(dir, ".git")])
|
|
77
|
+
try {
|
|
78
|
+
t = Math.max(t, fs.statSync(p).mtimeMs);
|
|
79
|
+
} catch {
|
|
80
|
+
// not there
|
|
81
|
+
}
|
|
82
|
+
return t;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export interface OrphanDeps {
|
|
86
|
+
now: number;
|
|
87
|
+
root: string;
|
|
88
|
+
panes: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function findOrphans(deps: OrphanDeps): Promise<{
|
|
92
|
+
candidates: Candidate[];
|
|
93
|
+
skipped: SweepSkipped[];
|
|
94
|
+
}> {
|
|
95
|
+
const { days } = resolveSweepDays(null);
|
|
96
|
+
const candidates: Candidate[] = [];
|
|
97
|
+
const skipped: SweepSkipped[] = [];
|
|
98
|
+
if (days === 0 || !fs.existsSync(deps.root)) return { candidates, skipped };
|
|
99
|
+
const mine = owned();
|
|
100
|
+
for (const entry of fs.readdirSync(deps.root, { withFileTypes: true })) {
|
|
101
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
102
|
+
const dir = path.join(deps.root, entry.name);
|
|
103
|
+
if (mine.has(real(dir))) continue;
|
|
104
|
+
const skip = (reason: string) =>
|
|
105
|
+
skipped.push({ path: dir, kind: "orphan", reason });
|
|
106
|
+
if (deps.now - newestMs(dir) < days * DAY_MS) {
|
|
107
|
+
skip(`touched less than ${days} day${days === 1 ? "" : "s"} ago`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (deps.panes.some((p) => inFolder(p, real(dir)) || inFolder(p, dir))) {
|
|
111
|
+
skip("a terminal is open in it");
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const gitdir = gitdirOf(dir);
|
|
115
|
+
const registered = !!gitdir && fs.existsSync(gitdir);
|
|
116
|
+
const leak =
|
|
117
|
+
TEST_LEAK.test(entry.name) &&
|
|
118
|
+
!!gitdir &&
|
|
119
|
+
inTemp(real(path.dirname(gitdir)));
|
|
120
|
+
if (leak) {
|
|
121
|
+
candidates.push({
|
|
122
|
+
path: dir,
|
|
123
|
+
kind: "test-leak",
|
|
124
|
+
repo: registered ? path.resolve(gitdir!, "../../..") : undefined,
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (registered) {
|
|
129
|
+
const repo = await mainCheckoutOf(dir);
|
|
130
|
+
if (!repo) skip("its repository can't be found");
|
|
131
|
+
else candidates.push({ path: dir, kind: "orphan", repo });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// Not a git worktree any more: git can't say what's in it.
|
|
135
|
+
if (holdsNothing(dir)) candidates.push({ path: dir, kind: "orphan" });
|
|
136
|
+
else skip("it isn't a git worktree and holds files git can't check");
|
|
137
|
+
}
|
|
138
|
+
return { candidates, skipped };
|
|
139
|
+
}
|