@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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { beforeAll, beforeEach, expect, vi } from "vitest";
|
|
6
|
+
import type { TaskPR } from "@/lib/tasks/state";
|
|
7
|
+
import type { ClaudeRun } from "./claude-cli";
|
|
8
|
+
import type { SessionFacts } from "./facts";
|
|
9
|
+
|
|
10
|
+
// Shared fixtures for the signoff*.test.ts files: a real repository with a
|
|
11
|
+
// local bare remote, gh faked around it. Import this before anything it mocks.
|
|
12
|
+
// Tests change the fake world through S.
|
|
13
|
+
export const S: {
|
|
14
|
+
pr: TaskPR | null;
|
|
15
|
+
prError: Error | null;
|
|
16
|
+
pane: string;
|
|
17
|
+
refuseMerge: string | null;
|
|
18
|
+
probeOut: string | Error;
|
|
19
|
+
ciLog: string;
|
|
20
|
+
commitDate: string | undefined;
|
|
21
|
+
} = {
|
|
22
|
+
pr: null,
|
|
23
|
+
prError: null,
|
|
24
|
+
pane: "",
|
|
25
|
+
refuseMerge: null,
|
|
26
|
+
probeOut: "0",
|
|
27
|
+
ciLog:
|
|
28
|
+
"Test 1\tRun tests\t2026-01-01T00:00:00.0Z FAIL src/a.test.ts > adds\n",
|
|
29
|
+
commitDate: "2026-01-01T00:00:00Z",
|
|
30
|
+
};
|
|
31
|
+
export const ATTRIBUTED = "a Co-Authored-By trailer naming Claude" as const;
|
|
32
|
+
export const merges: string[][] = [];
|
|
33
|
+
export const JOB_URL = "https://github.com/o/r/actions/runs/1/job/2";
|
|
34
|
+
// CI failing on the head, with a log the loop guard can read.
|
|
35
|
+
export const failingCI = (failing = "unit") =>
|
|
36
|
+
({ ...S.pr!, checks: "fail", failing, failingUrl: JOB_URL }) as TaskPR;
|
|
37
|
+
export const isProbe = (args: string[]) =>
|
|
38
|
+
args[0] === "api" ||
|
|
39
|
+
(args[0] === "pr" &&
|
|
40
|
+
args[1] === "list" &&
|
|
41
|
+
args.includes("statusCheckRollup"));
|
|
42
|
+
|
|
43
|
+
vi.mock("@/lib/tasks/gh", async (importOriginal) => {
|
|
44
|
+
const real = await importOriginal<typeof import("@/lib/tasks/gh")>();
|
|
45
|
+
return {
|
|
46
|
+
...real,
|
|
47
|
+
run: async (cmd: string, args: string[], cwd: string, t?: number) => {
|
|
48
|
+
if (cmd === "gh" && args[0] === "run" && args[1] === "view")
|
|
49
|
+
return S.ciLog;
|
|
50
|
+
if (cmd === "gh" && isProbe(args)) {
|
|
51
|
+
if (S.probeOut instanceof Error) throw S.probeOut;
|
|
52
|
+
return S.probeOut;
|
|
53
|
+
}
|
|
54
|
+
if (cmd === "gh") {
|
|
55
|
+
if (S.refuseMerge) throw new Error(S.refuseMerge);
|
|
56
|
+
merges.push(args);
|
|
57
|
+
if (S.pr) S.pr = { ...S.pr, state: "MERGED" };
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
if (cmd === "tmux") throw new Error("no tmux in tests");
|
|
61
|
+
return real.run(cmd, args, cwd, t);
|
|
62
|
+
},
|
|
63
|
+
findPR: async () => S.pr,
|
|
64
|
+
findPRStrict: async () => {
|
|
65
|
+
if (S.prError) throw S.prError;
|
|
66
|
+
return S.pr;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
// The test clones have a local origin; the CI probes need a GitHub name.
|
|
71
|
+
vi.mock("./repo-slug", async (importOriginal) => ({
|
|
72
|
+
...(await importOriginal<typeof import("./repo-slug")>()),
|
|
73
|
+
repoSlug: async () => "o/r",
|
|
74
|
+
}));
|
|
75
|
+
vi.mock("@/lib/status-detector", () => ({
|
|
76
|
+
checkWaitingPatterns: (t: string) => /Allow\?/.test(t),
|
|
77
|
+
statusDetector: {
|
|
78
|
+
refreshCache: async () => {},
|
|
79
|
+
sessionExists: () => S.pane !== "",
|
|
80
|
+
getStatus: async () => "idle",
|
|
81
|
+
titleFor: () => "",
|
|
82
|
+
getTimestamp: () => 0,
|
|
83
|
+
hostFor: () => "local",
|
|
84
|
+
capturePane: async () => S.pane,
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
export const { db, stackQueries, updateSessionRow } = await import("@/lib/db");
|
|
89
|
+
export const { createProject } = await import("@/lib/projects");
|
|
90
|
+
export const { createWorkspace, setProjectWorkspace } =
|
|
91
|
+
await import("@/lib/workspaces");
|
|
92
|
+
export const { ensureOrchestrator } = await import("./home");
|
|
93
|
+
export const { runTool } = await import("./serve");
|
|
94
|
+
export const { judge, landDeps, mergeJudged, refundIfRefused } =
|
|
95
|
+
await import("./signoff");
|
|
96
|
+
export const { armTask, mergeArmed } = await import("./armed");
|
|
97
|
+
export const { resumeReviews, review } = await import("./review");
|
|
98
|
+
export const { getCheck, putCheck } = await import("./checks");
|
|
99
|
+
export const { failureOf, recordFailure } = await import("./gates");
|
|
100
|
+
export const { LOOP_GUARD_COMMITS } = await import("./loop-guard");
|
|
101
|
+
export const { clearRepoCI } = await import("./repo-ci");
|
|
102
|
+
export const { listNotes } = await import("./notes");
|
|
103
|
+
export const { listItems } = await import("@/lib/chat/store");
|
|
104
|
+
export const { claudeArgs } = await import("./claude-cli");
|
|
105
|
+
export const { seedSession } = await import("./testing");
|
|
106
|
+
export const { answerAsk, getAsk, openAsks, raiseAsk, BRAKE_SUBJECT } =
|
|
107
|
+
await import("./asks");
|
|
108
|
+
export const { setMergeApprovals } = await import("./merge-approvals");
|
|
109
|
+
export const { releaseInterruptedClaims, spendApproval } =
|
|
110
|
+
await import("./ask-approvals");
|
|
111
|
+
export const { amendBrief } = await import("./amendments");
|
|
112
|
+
export const { fetchRefs } = await import("./repo");
|
|
113
|
+
export const { getTaskSession } = await import("@/lib/tasks/session");
|
|
114
|
+
export const { changedFiles } = await import("./diff");
|
|
115
|
+
export const { followAskHeads } = await import("./ask-heads");
|
|
116
|
+
export const { followAskVerdicts } = await import("./ask-verdict");
|
|
117
|
+
export const { raiseHeldAsks, HOLD_MIN_MS, HOLD_MAX_MS } =
|
|
118
|
+
await import("./held-asks");
|
|
119
|
+
|
|
120
|
+
// The one ask a task's escalation leaves on Saad's list.
|
|
121
|
+
export function oneGateAsk(w: string, task: string, sha: string) {
|
|
122
|
+
const asks = openAsks(w);
|
|
123
|
+
expect(asks).toHaveLength(1);
|
|
124
|
+
expect(asks[0]).toMatchObject({
|
|
125
|
+
subject: `task:${task}`,
|
|
126
|
+
kind: "gate",
|
|
127
|
+
title: "Merge add-a?",
|
|
128
|
+
link: "https://github.com/o/r/pull/7",
|
|
129
|
+
sha,
|
|
130
|
+
});
|
|
131
|
+
return asks[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The loop guard's last strike: the gate already failed on the commits
|
|
135
|
+
// before this one.
|
|
136
|
+
export const failedBefore = (w: string, task: string, gate: string) => {
|
|
137
|
+
for (let i = 1; i < LOOP_GUARD_COMMITS; i++)
|
|
138
|
+
recordFailure(w, task, gate, `earlier ${i}`, String(i).repeat(40));
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Facts for the task: its agent idle, or busy.
|
|
142
|
+
export const taskFacts = (task: string, status: "idle" | "running" = "idle") =>
|
|
143
|
+
[
|
|
144
|
+
{ id: task, name: "add-a", status, task: { state: "review", pr: S.pr } },
|
|
145
|
+
] as unknown as SessionFacts[];
|
|
146
|
+
|
|
147
|
+
// A failed-twice escalation's ask goes up once the agent is idle and CI
|
|
148
|
+
// has settled; `after` ms from now.
|
|
149
|
+
// The first look at a head's checks starts CI's settle clock, so it looks
|
|
150
|
+
// twice, the second a settle period later.
|
|
151
|
+
export const askUp = async (
|
|
152
|
+
w: string,
|
|
153
|
+
task: string,
|
|
154
|
+
after = HOLD_MIN_MS + 1000,
|
|
155
|
+
status: "idle" | "running" = "idle"
|
|
156
|
+
) => {
|
|
157
|
+
await raiseHeldAsks(w, taskFacts(task, status), Date.now() + after);
|
|
158
|
+
await raiseHeldAsks(w, taskFacts(task, status), Date.now() + after + 200_000);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Poll for the condition, generously: a loaded machine is slow, not wrong.
|
|
162
|
+
export const POLL = { timeout: 20000, interval: 20 };
|
|
163
|
+
|
|
164
|
+
beforeAll(() => {
|
|
165
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "aos-orch-signoff-"));
|
|
166
|
+
vi.spyOn(os, "homedir").mockReturnValue(home);
|
|
167
|
+
});
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
merges.length = 0;
|
|
170
|
+
S.refuseMerge = null;
|
|
171
|
+
S.probeOut = "0";
|
|
172
|
+
S.ciLog =
|
|
173
|
+
"Test 1\tRun tests\t2026-01-01T00:00:00.0Z FAIL src/a.test.ts > adds\n";
|
|
174
|
+
clearRepoCI();
|
|
175
|
+
// A live terminal at its prompt, nothing waiting.
|
|
176
|
+
S.pane = "❯ ";
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const git = (cwd: string, ...args: string[]) =>
|
|
180
|
+
execFileSync("git", args, {
|
|
181
|
+
cwd,
|
|
182
|
+
encoding: "utf8",
|
|
183
|
+
stdio: "pipe",
|
|
184
|
+
env: {
|
|
185
|
+
...process.env,
|
|
186
|
+
...(S.commitDate ? { GIT_COMMITTER_DATE: S.commitDate } : {}),
|
|
187
|
+
},
|
|
188
|
+
}).trim();
|
|
189
|
+
|
|
190
|
+
export function commit(
|
|
191
|
+
repo: string,
|
|
192
|
+
file: string,
|
|
193
|
+
text: string,
|
|
194
|
+
link?: string
|
|
195
|
+
): string {
|
|
196
|
+
fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true });
|
|
197
|
+
if (link) fs.symlinkSync(link, path.join(repo, file));
|
|
198
|
+
else fs.writeFileSync(path.join(repo, file), text);
|
|
199
|
+
git(repo, "add", "-A");
|
|
200
|
+
git(repo, "commit", "-qm", `change ${file}`);
|
|
201
|
+
git(repo, "push", "-q", "origin", "HEAD");
|
|
202
|
+
return git(repo, "rev-parse", "HEAD");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// A workspace whose project is a real clone, with a task on its branch
|
|
206
|
+
// and its PR open at the branch's head.
|
|
207
|
+
export function setup(
|
|
208
|
+
opts: {
|
|
209
|
+
card?: boolean;
|
|
210
|
+
onMain?: Array<[string, string]>;
|
|
211
|
+
approvals?: boolean;
|
|
212
|
+
} = {}
|
|
213
|
+
) {
|
|
214
|
+
setMergeApprovals(opts.approvals ?? false);
|
|
215
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aos-orch-repo-"));
|
|
216
|
+
const remote = path.join(root, "remote.git");
|
|
217
|
+
const repo = path.join(root, "repo");
|
|
218
|
+
git(root, "init", "-q", "--bare", "-b", "main", remote);
|
|
219
|
+
git(root, "clone", "-q", remote, repo);
|
|
220
|
+
git(repo, "config", "user.email", "t@example.com");
|
|
221
|
+
git(repo, "config", "user.name", "T");
|
|
222
|
+
commit(repo, "README.md", "hello\n");
|
|
223
|
+
for (const file of opts.onMain ?? []) commit(repo, ...file);
|
|
224
|
+
git(repo, "checkout", "-qb", "feature/t");
|
|
225
|
+
const sha = commit(repo, "src/a.ts", "export const a = 1;\n");
|
|
226
|
+
|
|
227
|
+
const workspace = createWorkspace(`ws-${path.basename(root)}`);
|
|
228
|
+
const project = createProject({
|
|
229
|
+
name: `app-${path.basename(root)}`,
|
|
230
|
+
workingDirectory: repo,
|
|
231
|
+
});
|
|
232
|
+
setProjectWorkspace(project.id, workspace.id);
|
|
233
|
+
ensureOrchestrator(workspace.id);
|
|
234
|
+
const task = seedSession({
|
|
235
|
+
projectId: project.id,
|
|
236
|
+
name: "add-a",
|
|
237
|
+
task: true,
|
|
238
|
+
branch: "feature/t",
|
|
239
|
+
});
|
|
240
|
+
updateSessionRow(db, task, {
|
|
241
|
+
base_branch: "main",
|
|
242
|
+
task_prompt: "Add a",
|
|
243
|
+
lh_card_id: opts.card ? "card-1" : null,
|
|
244
|
+
pr_number: 7,
|
|
245
|
+
});
|
|
246
|
+
S.pr = {
|
|
247
|
+
number: 7,
|
|
248
|
+
url: "https://github.com/o/r/pull/7",
|
|
249
|
+
state: "OPEN",
|
|
250
|
+
checks: "pass",
|
|
251
|
+
head: sha,
|
|
252
|
+
failing: null,
|
|
253
|
+
checkCount: 1,
|
|
254
|
+
attribution: null,
|
|
255
|
+
};
|
|
256
|
+
const w = workspace.id;
|
|
257
|
+
// CI has looked settled on every head for a while.
|
|
258
|
+
const settled = () =>
|
|
259
|
+
putCheck({
|
|
260
|
+
workspaceId: w,
|
|
261
|
+
sessionId: task,
|
|
262
|
+
sha: S.pr!.head!,
|
|
263
|
+
kind: "ci",
|
|
264
|
+
status: "pass",
|
|
265
|
+
detail: JSON.stringify({ count: S.pr!.checkCount, at: 0 }),
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
w,
|
|
269
|
+
repo,
|
|
270
|
+
task,
|
|
271
|
+
sha,
|
|
272
|
+
signOff: (settle = true) => {
|
|
273
|
+
if (settle) settled();
|
|
274
|
+
return runTool(w, "sign_off", { task: "add-a" });
|
|
275
|
+
},
|
|
276
|
+
push: (file: string, text: string, link?: string) => {
|
|
277
|
+
const next = commit(repo, file, text, link);
|
|
278
|
+
S.pr = { ...S.pr!, head: next };
|
|
279
|
+
return next;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A reviewer that answers without a model, and remembers how it was run.
|
|
285
|
+
export function reviewer(verdict: "pass" | "block", within = true) {
|
|
286
|
+
const runs: ClaudeRun[] = [];
|
|
287
|
+
const claude = async (run: ClaudeRun) => {
|
|
288
|
+
runs.push(run);
|
|
289
|
+
if (!run.tools.length)
|
|
290
|
+
return {
|
|
291
|
+
within,
|
|
292
|
+
reason: within ? "matches the card" : "adds billing, not on the card",
|
|
293
|
+
};
|
|
294
|
+
return verdict === "pass"
|
|
295
|
+
? { verdict, summary: "Looks right.", findings: [] }
|
|
296
|
+
: {
|
|
297
|
+
verdict,
|
|
298
|
+
summary: "One bug.",
|
|
299
|
+
findings: [
|
|
300
|
+
{
|
|
301
|
+
severity: "blocking",
|
|
302
|
+
file: "src/a.ts",
|
|
303
|
+
line: 1,
|
|
304
|
+
summary: "off by one",
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
return { claude, runs };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const reviewNow = (
|
|
313
|
+
w: string,
|
|
314
|
+
verdict: "pass" | "block" = "pass",
|
|
315
|
+
within = true
|
|
316
|
+
) => {
|
|
317
|
+
const r = reviewer(verdict, within);
|
|
318
|
+
return review(w, "add-a", { wait: true, claude: r.claude }).then((text) => ({
|
|
319
|
+
text,
|
|
320
|
+
runs: r.runs,
|
|
321
|
+
}));
|
|
322
|
+
};
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* anything touching CI config, build and hook scripts, agent config, deploys
|
|
5
5
|
* or secrets handling goes to Saad whatever the gates say. A repository with
|
|
6
6
|
* no CI (decided from the repository, never from one head with no checks)
|
|
7
|
-
* and a
|
|
8
|
-
* task is with Saad the orchestrator doesn't merge it; he signs
|
|
7
|
+
* and a task the loop guard finds stuck (loop-guard.ts) go to him either
|
|
8
|
+
* way. Once a task is with Saad the orchestrator doesn't merge it; he signs
|
|
9
|
+
* it off or drops it.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { MERGED_AS, type MergeMethod } from "../tasks/merge-policy";
|
|
@@ -28,9 +29,17 @@ import { countFailure, escalate } from "./escalate";
|
|
|
28
29
|
import {
|
|
29
30
|
ciGate,
|
|
30
31
|
evaluateGates,
|
|
32
|
+
failureCounted,
|
|
31
33
|
holdingEscalations,
|
|
34
|
+
recordPass,
|
|
32
35
|
type GateInput,
|
|
36
|
+
type GateOutcome,
|
|
33
37
|
} from "./gates";
|
|
38
|
+
import {
|
|
39
|
+
readCiSignature,
|
|
40
|
+
reasonSignature,
|
|
41
|
+
reviewSignature,
|
|
42
|
+
} from "./loop-guard";
|
|
34
43
|
import { addNote } from "./notes";
|
|
35
44
|
import { latestApproval, refundApproval, spendApproval } from "./ask-approvals";
|
|
36
45
|
import { workSubject } from "./asks";
|
|
@@ -121,6 +130,7 @@ export async function judge(
|
|
|
121
130
|
async () => ({
|
|
122
131
|
checks: pr.checks,
|
|
123
132
|
failing: pr.failing,
|
|
133
|
+
failingUrl: pr.failingUrl,
|
|
124
134
|
settleIn: ciSettleIn({
|
|
125
135
|
workspaceId,
|
|
126
136
|
taskId: task.id,
|
|
@@ -257,17 +267,43 @@ export async function gateVerdict(
|
|
|
257
267
|
const waiting = outcomes.filter((o) => o.state === "wait");
|
|
258
268
|
if (!failed.length && !waiting.length)
|
|
259
269
|
return { ok: true, sha, pr: pr.number };
|
|
260
|
-
const lines = [
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
const lines: string[] = [];
|
|
271
|
+
if (opts.count === false)
|
|
272
|
+
lines.push(...failed.map((o) => `- ${o.gate} failed: ${o.reason}`));
|
|
273
|
+
else {
|
|
274
|
+
for (const o of outcomes)
|
|
275
|
+
if (o.state === "pass") recordPass(work.id, o.gate);
|
|
276
|
+
for (const o of failed) {
|
|
277
|
+
const signature = await failureSignature(work, o, live);
|
|
278
|
+
lines.push(
|
|
279
|
+
countFailure(workspaceId, work, o, pr.url, sha, { signature }).line
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
lines.push(...waiting.map((o) => `- ${o.gate}: not yet, ${o.reason}`));
|
|
268
284
|
return no(`${head}\n${lines.join("\n")}`, !failed.length);
|
|
269
285
|
}
|
|
270
286
|
|
|
287
|
+
// What the loop guard compares between this failure and the one before.
|
|
288
|
+
// The CI log is read once per head: a head already counted needs none.
|
|
289
|
+
async function failureSignature(
|
|
290
|
+
work: GateWork,
|
|
291
|
+
o: GateOutcome,
|
|
292
|
+
i: Omit<GateInput, "sha" | "ruleBreaks">
|
|
293
|
+
): Promise<string | null> {
|
|
294
|
+
if (o.gate === "review") return reviewSignature(i.review?.detail ?? null);
|
|
295
|
+
if (o.gate === "scope" && i.scope?.status === "block")
|
|
296
|
+
return reasonSignature(i.scope.detail);
|
|
297
|
+
if (o.gate !== "ci" || i.checks !== "fail") return reasonSignature(o.reason);
|
|
298
|
+
if (failureCounted(work.id, "ci", work.sha)) return null;
|
|
299
|
+
return readCiSignature({
|
|
300
|
+
repo: work.repo,
|
|
301
|
+
prUrl: work.pr.url,
|
|
302
|
+
failing: i.failing,
|
|
303
|
+
failingUrl: i.failingUrl,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
271
307
|
// Asked only when it can change the answer: a head with no checks that
|
|
272
308
|
// isn't settling or conflicting.
|
|
273
309
|
async function repoCIFor(
|
|
@@ -331,7 +367,9 @@ export async function refundIfRefused(
|
|
|
331
367
|
await merge();
|
|
332
368
|
} catch (e) {
|
|
333
369
|
const merged = db
|
|
334
|
-
.prepare(
|
|
370
|
+
.prepare(
|
|
371
|
+
`SELECT 1 FROM session_rows WHERE id = ? AND task_status = 'merged'`
|
|
372
|
+
)
|
|
335
373
|
.get(taskId);
|
|
336
374
|
if (approval && !merged) refundApproval(approval);
|
|
337
375
|
throw e;
|
|
@@ -56,7 +56,7 @@ export function workspaceTask(
|
|
|
56
56
|
if (pr !== null) {
|
|
57
57
|
const rows = db
|
|
58
58
|
.prepare(
|
|
59
|
-
`SELECT s.*, p.name AS project_name FROM
|
|
59
|
+
`SELECT s.*, p.name AS project_name FROM session_rows s
|
|
60
60
|
JOIN projects p ON p.id = s.project_id
|
|
61
61
|
WHERE p.workspace_id = ? AND s.pr_number = ? AND s.task_status IS NOT NULL
|
|
62
62
|
ORDER BY s.created_at DESC`
|
|
@@ -9,8 +9,8 @@ import { blockedReason } from "../tasks/state";
|
|
|
9
9
|
import { checkWaitingPatterns, statusDetector } from "../status-detector";
|
|
10
10
|
import { getCheck, putCheck } from "./checks";
|
|
11
11
|
import { cleanRemoteText, hostLink, type HostLink } from "../hosts/remote-api";
|
|
12
|
-
import { heldChatFacts, isHeldRemote } from "../tasks/held-
|
|
13
|
-
import { hostTasks } from "../tasks/
|
|
12
|
+
import { heldChatFacts, isHeldRemote } from "../tasks/contract/held-calls";
|
|
13
|
+
import { list as hostTasks } from "../tasks/contract/client";
|
|
14
14
|
import { peerPane } from "../hosts/peer-sessions";
|
|
15
15
|
import { untrusted } from "./untrusted";
|
|
16
16
|
|
|
@@ -104,6 +104,8 @@ async function peerChatWaiting(
|
|
|
104
104
|
: `its chat on ${link.hostName} couldn't say whether a question is open`,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
+
// compat: task-contract-1. A machine without held-chat-facts is asked
|
|
108
|
+
// through its task list; drop this once every linked machine has it.
|
|
107
109
|
const view = await hostTasks(link, true)
|
|
108
110
|
.then(({ tasks, capabilities }) =>
|
|
109
111
|
// One that can't say its chat's turn can't rule out an open question.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// test database.
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
-
import { db } from "../db";
|
|
5
|
+
import { db, insertSessionRow } from "../db";
|
|
6
6
|
import { saveItem } from "../chat/store";
|
|
7
7
|
import { createProject } from "../projects";
|
|
8
8
|
import { createWorkspace, setProjectWorkspace } from "../workspaces";
|
|
@@ -16,20 +16,17 @@ export function seedSession(opts: {
|
|
|
16
16
|
updatedAt?: string;
|
|
17
17
|
}): string {
|
|
18
18
|
const id = randomUUID();
|
|
19
|
-
db
|
|
20
|
-
`INSERT INTO sessions (id, name, tmux_name, working_directory, project_id, view,
|
|
21
|
-
task_status, branch_name, updated_at)
|
|
22
|
-
VALUES (?, ?, ?, '/tmp', ?, ?, ?, ?, COALESCE(?, datetime('now')))`
|
|
23
|
-
).run(
|
|
19
|
+
insertSessionRow(db, {
|
|
24
20
|
id,
|
|
25
|
-
opts.name,
|
|
26
|
-
`claude-${id}`,
|
|
27
|
-
|
|
28
|
-
opts.
|
|
29
|
-
opts.
|
|
30
|
-
opts.
|
|
31
|
-
opts.
|
|
32
|
-
|
|
21
|
+
name: opts.name,
|
|
22
|
+
tmux_name: `claude-${id}`,
|
|
23
|
+
working_directory: "/tmp",
|
|
24
|
+
project_id: opts.projectId,
|
|
25
|
+
view: opts.view ?? "terminal",
|
|
26
|
+
task_status: opts.task ? "running" : null,
|
|
27
|
+
branch_name: opts.branch ?? null,
|
|
28
|
+
updated_at: opts.updatedAt,
|
|
29
|
+
});
|
|
33
30
|
return id;
|
|
34
31
|
}
|
|
35
32
|
|
|
@@ -18,6 +18,7 @@ import { settleStaleAsks } from "./ask-settle";
|
|
|
18
18
|
import { brakesOn, liftBrake } from "./brakes";
|
|
19
19
|
import { anyArmed, mergeArmed } from "./armed";
|
|
20
20
|
import { followAskHeads } from "./ask-heads";
|
|
21
|
+
import { followAskVerdicts } from "./ask-verdict";
|
|
21
22
|
import { raiseHeldAsks } from "./held-asks";
|
|
22
23
|
import { rebaseConflicting } from "./rebase";
|
|
23
24
|
import { wakeTasks } from "./pr-wake";
|
|
@@ -91,6 +92,9 @@ function mergePipeline(workspaceId: string, facts: SessionFacts[]): void {
|
|
|
91
92
|
await raiseHeldAsks(workspaceId, facts).catch((error) =>
|
|
92
93
|
console.error(`Raising held asks in ${workspaceId}:`, error)
|
|
93
94
|
);
|
|
95
|
+
await followAskVerdicts(workspaceId, facts).catch((error) =>
|
|
96
|
+
console.error(`Judging asked commits in ${workspaceId}:`, error)
|
|
97
|
+
);
|
|
94
98
|
await rebaseConflicting(workspaceId, facts);
|
|
95
99
|
await mergeArmed(workspaceId, facts);
|
|
96
100
|
})()
|
package/lib/plans/features.ts
CHANGED
|
@@ -160,7 +160,7 @@ export function pieceOfSession(sessionId: string): FeaturePiece | undefined {
|
|
|
160
160
|
export function prOf(p: FeaturePiece): number | null {
|
|
161
161
|
if (!p.session_id) return null;
|
|
162
162
|
const s = db
|
|
163
|
-
.prepare(`SELECT pr_number, project_id FROM
|
|
163
|
+
.prepare(`SELECT pr_number, project_id FROM session_rows WHERE id = ?`)
|
|
164
164
|
.get(p.session_id) as
|
|
165
165
|
| { pr_number: number | null; project_id: string | null }
|
|
166
166
|
| undefined;
|
package/lib/plans/release.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface ReleaseDeps {
|
|
|
38
38
|
|
|
39
39
|
const sessionOf = (id: string | null) =>
|
|
40
40
|
id
|
|
41
|
-
? (db.prepare(`SELECT * FROM
|
|
41
|
+
? (db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
42
42
|
| Session
|
|
43
43
|
| undefined)
|
|
44
44
|
: undefined;
|
package/lib/ports.ts
CHANGED
|
@@ -70,7 +70,7 @@ async function reclaimEnded(
|
|
|
70
70
|
): Promise<void> {
|
|
71
71
|
const rows = db
|
|
72
72
|
.prepare(
|
|
73
|
-
`SELECT id, tmux_name, task_status, host_id FROM
|
|
73
|
+
`SELECT id, tmux_name, task_status, host_id FROM session_rows
|
|
74
74
|
WHERE port_slot IS NOT NULL
|
|
75
75
|
AND (archived_at IS NOT NULL OR task_status IN ('done', 'merged'))`
|
|
76
76
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// them, and a session standing in for a run's.
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
-
import { db } from "../db";
|
|
5
|
+
import { db, updateSessionRow } from "../db";
|
|
6
6
|
import { createProject } from "../projects";
|
|
7
7
|
import { createWorkspace, setProjectWorkspace } from "../workspaces";
|
|
8
8
|
import { createRecipe } from "./store";
|
|
@@ -43,7 +43,6 @@ export function makeSession(
|
|
|
43
43
|
db.prepare(
|
|
44
44
|
`INSERT INTO sessions (id, name, tmux_name, working_directory, view) VALUES (?, 's', ?, '/tmp', 'chat')`
|
|
45
45
|
).run(id, `claude-${id}`);
|
|
46
|
-
|
|
47
|
-
db.prepare(`UPDATE sessions SET ${k} = ? WHERE id = ?`).run(v, id);
|
|
46
|
+
updateSessionRow(db, id, fields);
|
|
48
47
|
return id;
|
|
49
48
|
}
|
package/lib/resurrect/find.ts
CHANGED
|
@@ -65,7 +65,7 @@ function rows(scope: EndedScope, limit: number, kind?: EndedKind): Row[] {
|
|
|
65
65
|
`SELECT s.id, s.name, s.tmux_name, s.project_id, p.name AS project_name,
|
|
66
66
|
s.task_status, s.archived_at, s.plan_phase,
|
|
67
67
|
COALESCE(s.task_ended_at, s.archived_at, s.settled_at, s.updated_at) AS ended_at
|
|
68
|
-
FROM
|
|
68
|
+
FROM session_rows s LEFT JOIN projects p ON p.id = s.project_id
|
|
69
69
|
WHERE ${where.join(" AND ")}
|
|
70
70
|
ORDER BY ended_at DESC LIMIT ?`
|
|
71
71
|
)
|