@saadnvd1/agent-os 0.3.470 → 0.3.472
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/README.md +139 -36
- package/app/api/fresh-start/route.ts +9 -5
- package/app/api/models/route.ts +34 -0
- package/app/api/models/test/route.ts +17 -0
- package/app/api/sessions/[id]/summarize/route.ts +4 -1
- package/app/api/tasks/[id]/model/route.ts +23 -0
- package/app/api/tasks/route.ts +5 -1
- package/app/api/usage/compare/route.ts +21 -0
- package/app/api/usage/route.ts +8 -3
- package/app/api/workspaces/[id]/models/route.ts +27 -0
- package/components/Chat/ChatPanel.tsx +5 -5
- package/components/Chat/StaleChip.tsx +7 -6
- package/components/CommandPalette/useAppCommands.ts +19 -1
- package/components/Draft/DraftChips.tsx +28 -0
- package/components/Draft/DraftPanel.tsx +1 -1
- package/components/Models/ModelSelect.tsx +67 -0
- package/components/Models/ModelsSection.tsx +199 -0
- package/components/Models/ProviderCard.tsx +271 -0
- package/components/SessionList/SidebarHeader.tsx +5 -1
- package/components/Settings/SettingsDialog.test.ts +3 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/commands.ts +19 -0
- package/components/Settings/entry-points.test.ts +1 -0
- package/components/Tasks/ResumeOnModel.tsx +72 -0
- package/components/Tasks/TaskRow.tsx +4 -0
- package/components/Usage/CompareView.tsx +211 -0
- package/components/Usage/ModelSpend.tsx +133 -0
- package/components/Usage/UsageDialog.tsx +69 -5
- package/components/Usage/format.ts +9 -0
- package/components/Workspaces/FreshStartForm.tsx +13 -13
- package/components/Workspaces/WorkspaceModelsForm.tsx +71 -0
- package/components/Workspaces/WorkspaceSettings.tsx +2 -0
- package/data/models/index.ts +125 -0
- package/data/usage/index.ts +5 -0
- package/lib/agents/launch.ts +13 -2
- package/lib/chat/context.ts +1 -1
- package/lib/chat/refresh.ts +31 -13
- package/lib/chat/runner.ts +5 -0
- package/lib/chat/stale.ts +25 -16
- package/lib/chat/worker/extras.ts +12 -0
- package/lib/chat/worker/host.ts +70 -24
- package/lib/chat/worker/main.ts +5 -2
- package/lib/db/migrations.ts +70 -0
- package/lib/db/types.ts +6 -0
- package/lib/drafts.ts +3 -0
- package/lib/models/config.ts +218 -0
- package/lib/models/launch.ts +93 -0
- package/lib/models/plan-step.ts +94 -0
- package/lib/models/providers.ts +163 -0
- package/lib/models/resolve.ts +246 -0
- package/lib/models/resume.ts +99 -0
- package/lib/models/roles.ts +98 -0
- package/lib/models/secrets.ts +86 -0
- package/lib/models/session-model.ts +40 -0
- package/lib/models/test-provider.ts +116 -0
- package/lib/orchestrator/act.ts +13 -1
- package/lib/orchestrator/armed.ts +1 -2
- package/lib/orchestrator/brief.ts +5 -4
- package/lib/orchestrator/claude-cli.ts +48 -14
- package/lib/orchestrator/diff.ts +1 -1
- package/lib/orchestrator/external-gates.ts +8 -43
- package/lib/orchestrator/gates.ts +10 -10
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/pr-wake.ts +2 -2
- package/lib/orchestrator/rebase.ts +6 -45
- package/lib/orchestrator/review.ts +20 -5
- package/lib/orchestrator/scope.ts +11 -0
- package/lib/orchestrator/serve.ts +13 -0
- package/lib/orchestrator/signoff.ts +13 -8
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +38 -0
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/pr-generation.ts +5 -2
- package/lib/providers/registry.ts +1 -1
- package/lib/session-audit/render.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/launch.ts +19 -4
- package/lib/settings-url.ts +1 -0
- package/lib/stacks/land.ts +23 -16
- package/lib/tasks/brief.ts +3 -14
- package/lib/tasks/code-review.ts +69 -107
- package/lib/tasks/gh.ts +3 -2
- package/lib/tasks/index.ts +60 -9
- package/lib/tasks/queue.ts +14 -5
- package/lib/tasks/start.ts +7 -2
- package/lib/tasks/state.ts +4 -4
- package/lib/usage/by-model.ts +105 -0
- package/lib/usage/check-runs.ts +170 -0
- package/lib/usage/compare.ts +280 -0
- package/lib/usage/cost.ts +71 -0
- package/lib/usage/report.ts +51 -0
- package/lib/usage/turns.ts +107 -18
- package/package.json +1 -1
- package/stores/usageUi.ts +11 -2
- package/scripts/check-code-review.ts +0 -21
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
* only while its agent is idle with nothing uncommitted, and pushed to the
|
|
9
9
|
* task's own branch with a lease on the commit fetched (the restack's code,
|
|
10
10
|
* stacks/git.ts). The base itself and every other branch are never written.
|
|
11
|
-
* The heads are recorded
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* head carries over; otherwise a fresh one starts.
|
|
11
|
+
* The heads are recorded, and when the PR's own patch is unchanged (same
|
|
12
|
+
* `git patch-id` against the base before and after), the orchestrator's
|
|
13
|
+
* review of the old head carries over to the new one (tasks/code-review.ts);
|
|
14
|
+
* otherwise a fresh one starts.
|
|
16
15
|
*
|
|
17
16
|
* Not clean, or not rebasable (uncommitted work, commits only on origin):
|
|
18
17
|
* the task is asked once, per head, to rebase by hand. The orchestrator
|
|
@@ -28,7 +27,6 @@ import { hostLink } from "../hosts/remote-api";
|
|
|
28
27
|
import type { TaskPR } from "../tasks/state";
|
|
29
28
|
import { run } from "../tasks/gh";
|
|
30
29
|
import { git as runGit, gitFetch, revParse } from "../git-exec";
|
|
31
|
-
import { withRestackedLine } from "../tasks/code-review";
|
|
32
30
|
import { outputOf, type Runner } from "../stacks/git";
|
|
33
31
|
import { restackedHeads, stackNotice } from "../stacks/restack";
|
|
34
32
|
import { getCheck, putCheck } from "./checks";
|
|
@@ -53,37 +51,6 @@ export interface RebaseDeps {
|
|
|
53
51
|
pr: TaskPR,
|
|
54
52
|
sha: string
|
|
55
53
|
) => Promise<unknown>;
|
|
56
|
-
// Add the Restacked line to the PR body's Code review section.
|
|
57
|
-
editBody: (
|
|
58
|
-
repo: string,
|
|
59
|
-
pr: number,
|
|
60
|
-
from: string,
|
|
61
|
-
to: string
|
|
62
|
-
) => Promise<void>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function editBody(repo: string, pr: number, from: string, to: string) {
|
|
66
|
-
const body = await run(
|
|
67
|
-
"gh",
|
|
68
|
-
["pr", "view", String(pr), "--json", "body", "--jq", ".body"],
|
|
69
|
-
repo,
|
|
70
|
-
15000
|
|
71
|
-
);
|
|
72
|
-
const next = withRestackedLine(body.replace(/\n$/, ""), from, to);
|
|
73
|
-
if (!next) return;
|
|
74
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aos-body-"));
|
|
75
|
-
const file = path.join(dir, "body.md");
|
|
76
|
-
try {
|
|
77
|
-
fs.writeFileSync(file, next);
|
|
78
|
-
await run(
|
|
79
|
-
"gh",
|
|
80
|
-
["pr", "edit", String(pr), "--body-file", file],
|
|
81
|
-
repo,
|
|
82
|
-
30000
|
|
83
|
-
);
|
|
84
|
-
} finally {
|
|
85
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
54
|
}
|
|
88
55
|
|
|
89
56
|
const defaults: RebaseDeps = {
|
|
@@ -94,7 +61,6 @@ const defaults: RebaseDeps = {
|
|
|
94
61
|
idle: async (task) => (await statusOf(task)).status === "idle",
|
|
95
62
|
review: (w, task, pr, sha) =>
|
|
96
63
|
reviewAt(w, taskReviewTarget(task, { ...pr, head: sha }), sha),
|
|
97
|
-
editBody,
|
|
98
64
|
};
|
|
99
65
|
|
|
100
66
|
// The (head, base tip) pairs already rebased or found not to need it, so a
|
|
@@ -263,11 +229,6 @@ async function rebaseOne(
|
|
|
263
229
|
deps
|
|
264
230
|
);
|
|
265
231
|
}
|
|
266
|
-
if (heads.restacked_from && heads.restacked_to)
|
|
267
|
-
await deps
|
|
268
|
-
.editBody(repo, pr.number, heads.restacked_from, heads.restacked_to)
|
|
269
|
-
.catch(() => {});
|
|
270
|
-
|
|
271
232
|
// The task's worktree follows while it's still idle and untouched;
|
|
272
233
|
// otherwise the task is told how to catch up.
|
|
273
234
|
const follows =
|
|
@@ -277,7 +238,7 @@ async function rebaseOne(
|
|
|
277
238
|
if (!follows)
|
|
278
239
|
await deps.send(
|
|
279
240
|
task.id,
|
|
280
|
-
`AgentOS rebased your branch ${task.branch_name} onto ${branch} on origin (now ${short(to)}), because PR #${pr.number} conflicted with it. Your worktree is still on the old head: before changing anything, run \`git fetch origin && git reset --keep origin/${task.branch_name}\` (commit your work first).
|
|
241
|
+
`AgentOS rebased your branch ${task.branch_name} onto ${branch} on origin (now ${short(to)}), because PR #${pr.number} conflicted with it. Your worktree is still on the old head: before changing anything, run \`git fetch origin && git reset --keep origin/${task.branch_name}\` (commit your work first). Its review carries over when its own patch didn't change.`
|
|
281
242
|
);
|
|
282
243
|
const reviewed = await carryReview(workspaceId, task, pr, onto, to, deps);
|
|
283
244
|
queueEvent(
|
|
@@ -422,6 +383,6 @@ async function askToRebase(
|
|
|
422
383
|
);
|
|
423
384
|
await deps.send(
|
|
424
385
|
task.id,
|
|
425
|
-
`Your PR #${pr.number} conflicts with ${base}, and AgentOS couldn't rebase it for you: ${why.slice(0, 300)}. At your next stopping point: commit your work, run \`git fetch origin && git rebase origin/${base}\`, resolve the conflicts, re-run your checks, push with \`git push --force-with-lease
|
|
386
|
+
`Your PR #${pr.number} conflicts with ${base}, and AgentOS couldn't rebase it for you: ${why.slice(0, 300)}. At your next stopping point: commit your work, run \`git fetch origin && git rebase origin/${base}\`, resolve the conflicts, re-run your checks, push with \`git push --force-with-lease\`. The orchestrator reviews the new head.`
|
|
426
387
|
);
|
|
427
388
|
}
|
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
toVerdict,
|
|
39
39
|
} from "./review-prompt";
|
|
40
40
|
import { checkScope } from "./scope";
|
|
41
|
+
import {
|
|
42
|
+
checkRunRecorder,
|
|
43
|
+
checkRunTotals,
|
|
44
|
+
describeCheckRuns,
|
|
45
|
+
} from "../usage/check-runs";
|
|
41
46
|
import { workspaceTask } from "./targets";
|
|
42
47
|
import { untrusted } from "./untrusted";
|
|
43
48
|
|
|
@@ -52,10 +57,8 @@ export function describeReview(row: CheckRow): string {
|
|
|
52
57
|
if (row.status === "running") return `The review of ${at} is still running.`;
|
|
53
58
|
if (row.status === "error")
|
|
54
59
|
return `The review of ${at} couldn't run: ${row.detail}`;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
? `Review of ${at}: pass.`
|
|
58
|
-
: `Review of ${at}: blocking findings.`;
|
|
60
|
+
const cost = checkRunTotals(row.session_id, row.sha);
|
|
61
|
+
const head = `${row.status === "pass" ? `Review of ${at}: pass.` : `Review of ${at}: blocking findings.`}${cost ? ` (${describeCheckRuns(cost)})` : ""}`;
|
|
59
62
|
return row.detail ? `${head}\n${untrusted("reviewer", row.detail)}` : head;
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -115,6 +118,14 @@ async function reviewJob(
|
|
|
115
118
|
const amendments = task ? amendmentsOf(task.id).map((a) => a.text) : [];
|
|
116
119
|
const scope = task ? { amendments, scopeClaim: pr.scopeChange ?? null } : {};
|
|
117
120
|
const dir = await checkout(repo, sha);
|
|
121
|
+
const onUsage = checkRunRecorder({
|
|
122
|
+
workspaceId,
|
|
123
|
+
sessionId: target.id,
|
|
124
|
+
label: target.label,
|
|
125
|
+
prNumber: pr.number,
|
|
126
|
+
sha,
|
|
127
|
+
kind: "review",
|
|
128
|
+
});
|
|
118
129
|
try {
|
|
119
130
|
const verdicts = [];
|
|
120
131
|
for (const [i, part] of parts.entries()) {
|
|
@@ -143,6 +154,9 @@ async function reviewJob(
|
|
|
143
154
|
schema: REVIEW_SCHEMA,
|
|
144
155
|
tools: ["Read", "Grep", "Glob"],
|
|
145
156
|
allow: [`Read(/${dir}/**)`, `Grep(/${dir}/**)`, `Glob(/${dir}/**)`],
|
|
157
|
+
onUsage,
|
|
158
|
+
role: "review",
|
|
159
|
+
workspaceId,
|
|
146
160
|
});
|
|
147
161
|
verdicts.push(toVerdict(answer));
|
|
148
162
|
}
|
|
@@ -246,11 +260,12 @@ export async function reviewAt(
|
|
|
246
260
|
: done.status === "block"
|
|
247
261
|
? "found blocking issues"
|
|
248
262
|
: "couldn't run";
|
|
263
|
+
const cost = checkRunTotals(target.id, sha);
|
|
249
264
|
queueEvent(
|
|
250
265
|
workspaceId,
|
|
251
266
|
`review:${target.id}:${sha}`,
|
|
252
267
|
target.id,
|
|
253
|
-
`${target.label}: review of ${short(sha)} ${said}${countVerdicts(workspaceId, target, sha)}`
|
|
268
|
+
`${target.label}: review of ${short(sha)} ${said}${cost ? ` (${describeCheckRuns(cost)})` : ""}${countVerdicts(workspaceId, target, sha)}`
|
|
254
269
|
);
|
|
255
270
|
return done;
|
|
256
271
|
});
|
|
@@ -8,6 +8,7 @@ import { connectedClient } from "../lumifyhub/connection";
|
|
|
8
8
|
import { promptFromCard } from "../lumifyhub/task-cards";
|
|
9
9
|
import { putCheck, type CheckRow } from "./checks";
|
|
10
10
|
import type { ClaudeRunner } from "./claude-cli";
|
|
11
|
+
import { checkRunRecorder } from "../usage/check-runs";
|
|
11
12
|
import type { ChangedFile } from "./diff";
|
|
12
13
|
import { fence } from "./review-prompt";
|
|
13
14
|
|
|
@@ -78,6 +79,16 @@ export async function checkScope(input: {
|
|
|
78
79
|
prompt: `The card, as data:\n${fence("card", card)}${amended}\n\nFiles changed:\n${list}\n\n${label}\n${fence("diff", part)}`,
|
|
79
80
|
schema: SCOPE_SCHEMA,
|
|
80
81
|
tools: [],
|
|
82
|
+
onUsage: checkRunRecorder({
|
|
83
|
+
workspaceId,
|
|
84
|
+
sessionId: task.id,
|
|
85
|
+
label: `task ${task.name}`,
|
|
86
|
+
prNumber: task.pr_number ?? null,
|
|
87
|
+
sha,
|
|
88
|
+
kind: "scope",
|
|
89
|
+
}),
|
|
90
|
+
role: "scope",
|
|
91
|
+
workspaceId,
|
|
81
92
|
})) as { within?: boolean; reason?: string };
|
|
82
93
|
if (answer.within !== true) break;
|
|
83
94
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resumeOnModel } from "../models/resume";
|
|
2
|
+
import { workspaceTask } from "./targets";
|
|
1
3
|
import { getWorkspace } from "../workspaces";
|
|
2
4
|
import {
|
|
3
5
|
done,
|
|
@@ -33,6 +35,7 @@ const ACTING = new Set<ToolName>([
|
|
|
33
35
|
"send",
|
|
34
36
|
"message_orchestrator",
|
|
35
37
|
"start_task",
|
|
38
|
+
"set_task_model",
|
|
36
39
|
"start_session",
|
|
37
40
|
"stack",
|
|
38
41
|
"land",
|
|
@@ -97,8 +100,18 @@ export async function runTool(
|
|
|
97
100
|
name: a.name,
|
|
98
101
|
view: a.view,
|
|
99
102
|
after: a.after,
|
|
103
|
+
model: a.model,
|
|
104
|
+
plan: a.plan,
|
|
100
105
|
});
|
|
101
106
|
}
|
|
107
|
+
case "set_task_model": {
|
|
108
|
+
const a = p(tool, raw);
|
|
109
|
+
const task = workspaceTask(w, a.task);
|
|
110
|
+
const out = await resumeOnModel(task.id, a.model, a.message);
|
|
111
|
+
return out.resumed
|
|
112
|
+
? `Resumed ${task.name} on ${out.ref}: same conversation and worktree.`
|
|
113
|
+
: `${task.name} hasn't launched yet; it starts on ${out.ref}.`;
|
|
114
|
+
}
|
|
102
115
|
case "start_session": {
|
|
103
116
|
const a = p(tool, raw);
|
|
104
117
|
return startSession(w, a.project, a.prompt, {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { MERGED_AS, type MergeMethod } from "../tasks/merge-policy";
|
|
11
11
|
import { prFor } from "../tasks/session";
|
|
12
|
-
import {
|
|
12
|
+
import { codeReviewGate, signOffTask } from "../tasks";
|
|
13
13
|
import { signOffRefusal } from "../stacks/guard";
|
|
14
14
|
import { getStack, landInBackground } from "../stacks";
|
|
15
15
|
import { LAND_DEFAULTS } from "../stacks/land";
|
|
@@ -91,11 +91,12 @@ export async function judge(
|
|
|
91
91
|
fromCard: !!task.lh_card_id,
|
|
92
92
|
scope: getCheck(task.id, sha, "scope"),
|
|
93
93
|
stackRefusal: landing ? null : signOffRefusal(task.id),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
codeReview: codeReviewGate({
|
|
95
|
+
head: sha,
|
|
96
|
+
attribution: pr.attribution,
|
|
97
|
+
review: (at) => getCheck(task.id, at, "review"),
|
|
98
|
+
restacked: restackedOf(task),
|
|
99
|
+
}),
|
|
99
100
|
}),
|
|
100
101
|
opts
|
|
101
102
|
);
|
|
@@ -129,7 +130,8 @@ export async function gateVerdict(
|
|
|
129
130
|
const held = holdingEscalations(work.id);
|
|
130
131
|
// Saad approved merging this exact commit. On an ask the orchestrator
|
|
131
132
|
// raised (ask_saad with its task and sha), held or not, it merges once
|
|
132
|
-
// whatever the gates say, but only
|
|
133
|
+
// whatever the gates say, but only once an independent review of that
|
|
134
|
+
// commit finished (its findings are his to override, not that it ran) and
|
|
133
135
|
// its stack parent merged: the orchestrator wrote that ask, and no answer
|
|
134
136
|
// to it stands in for those. An approval of the gates' own ask on a held
|
|
135
137
|
// task merges as it always has (heldVerdict).
|
|
@@ -139,7 +141,10 @@ export async function gateVerdict(
|
|
|
139
141
|
(approval.raised_by === "orchestrator" || !held.length)
|
|
140
142
|
) {
|
|
141
143
|
const i = await inputs();
|
|
142
|
-
const refusal =
|
|
144
|
+
const refusal =
|
|
145
|
+
i.codeReview.refusal ??
|
|
146
|
+
(i.codeReview.ran ? null : i.codeReview.wait) ??
|
|
147
|
+
i.stackRefusal;
|
|
143
148
|
if (refusal)
|
|
144
149
|
return no(
|
|
145
150
|
`${work.name} (PR #${pr.number} at ${short(sha)}): Saad approved this commit, but ${refusal}`
|
|
@@ -14,6 +14,7 @@ export const TOOL_NAMES = {
|
|
|
14
14
|
orchestrators: name("orchestrators"),
|
|
15
15
|
message_orchestrator: name("message_orchestrator"),
|
|
16
16
|
start_task: name("start_task"),
|
|
17
|
+
set_task_model: name("set_task_model"),
|
|
17
18
|
start_session: name("start_session"),
|
|
18
19
|
stack: name("stack"),
|
|
19
20
|
stack_status: name("stack_status"),
|
|
@@ -6,6 +6,7 @@ import { BRANCH_NAME } from "../git";
|
|
|
6
6
|
import { RAISED_KINDS } from "./asks";
|
|
7
7
|
import { MAX_RULE_CHARS } from "./rules";
|
|
8
8
|
import { PROJECT_NAME } from "../project-add/name";
|
|
9
|
+
import { TASK_ROLES, type ModelRole } from "../models/roles";
|
|
9
10
|
|
|
10
11
|
const ref = (what: string) => z.string().trim().min(1).max(300).describe(what);
|
|
11
12
|
const session = ref("Session name, project/name, or id from sessions");
|
|
@@ -19,6 +20,23 @@ const view = z
|
|
|
19
20
|
"How its agent runs: chat (the default), or terminal only when the job needs a TUI (an interactive program, a full-screen tool)"
|
|
20
21
|
);
|
|
21
22
|
|
|
23
|
+
const modelRef = z
|
|
24
|
+
.string()
|
|
25
|
+
.trim()
|
|
26
|
+
.regex(/^[a-z0-9][a-z0-9_-]*\/[\w.:@/-]+$/i, 'not "provider/model"');
|
|
27
|
+
const modelOverride = z
|
|
28
|
+
.union([
|
|
29
|
+
modelRef,
|
|
30
|
+
z.partialRecord(
|
|
31
|
+
z.enum(TASK_ROLES as [ModelRole, ...ModelRole[]]),
|
|
32
|
+
modelRef
|
|
33
|
+
),
|
|
34
|
+
])
|
|
35
|
+
.optional()
|
|
36
|
+
.describe(
|
|
37
|
+
"Its own model, as \"provider/model\" (anthropic/claude-sonnet-5-5, deepseek/deepseek-v4-pro) for all its roles, or a map of role (plan, implement, review, scope) to one. Default: the workspace's and Settings' role models; another provider only when asked"
|
|
38
|
+
);
|
|
39
|
+
|
|
22
40
|
export const TOOL_SHAPES = {
|
|
23
41
|
sessions: {},
|
|
24
42
|
read: {
|
|
@@ -79,6 +97,26 @@ export const TOOL_SHAPES = {
|
|
|
79
97
|
.describe(
|
|
80
98
|
'Queue it until this task finishes (id or name), or "any" for whichever running task finishes first'
|
|
81
99
|
),
|
|
100
|
+
model: modelOverride,
|
|
101
|
+
plan: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe(
|
|
105
|
+
"Plan first on the planning model, then implement on the implementation model (default: Settings)"
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
set_task_model: {
|
|
109
|
+
task,
|
|
110
|
+
model: modelOverride.unwrap(),
|
|
111
|
+
message: z
|
|
112
|
+
.string()
|
|
113
|
+
.trim()
|
|
114
|
+
.min(1)
|
|
115
|
+
.max(8000)
|
|
116
|
+
.optional()
|
|
117
|
+
.describe(
|
|
118
|
+
"What it's told on resuming (default: carry on where it left off)"
|
|
119
|
+
),
|
|
82
120
|
},
|
|
83
121
|
start_session: {
|
|
84
122
|
project,
|
|
@@ -61,7 +61,9 @@ const DESCRIPTIONS: Record<ToolName, string> = {
|
|
|
61
61
|
message_orchestrator:
|
|
62
62
|
"Message another workspace's orchestrator, by workspace name; never any other session there. It arrives as its next prompt, marked as from you and fenced as untrusted.",
|
|
63
63
|
start_task:
|
|
64
|
-
"Start a task: an agent in its own worktree that ends in a PR. It runs as a chat unless view is terminal (only for a job that needs a TUI). With after (a task's id or name, or \"any\"), or over the workspace's running task limit, it's queued and starts by itself; the result says Queued (position N). Refused while a brake holds.",
|
|
64
|
+
"Start a task: an agent in its own worktree that ends in a PR. It runs as a chat unless view is terminal (only for a job that needs a TUI). With after (a task's id or name, or \"any\"), or over the workspace's running task limit, it's queued and starts by itself; the result says Queued (position N). Its models follow the role settings (planning, implementation, review, scope); give model only when Saad asks for one. Refused while a brake holds.",
|
|
65
|
+
set_task_model:
|
|
66
|
+
'Resume a paused task on another model ("provider/model", or a map of role to one), keeping its conversation and worktree; it\'s told to carry on (or message). Refused mid-turn.',
|
|
65
67
|
start_session:
|
|
66
68
|
"Start an interactive agent session in a project with a prompt. It runs as a chat unless view is terminal (only for a job that needs a TUI). Refused while a brake holds.",
|
|
67
69
|
stack:
|
package/lib/pr-generation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routedRun } from "./models/launch";
|
|
1
2
|
import { execFile } from "child_process";
|
|
2
3
|
import { git as runGit } from "./git-exec";
|
|
3
4
|
import { run } from "./tasks/gh";
|
|
@@ -154,13 +155,15 @@ async function generateWithClaude(
|
|
|
154
155
|
|
|
155
156
|
// `claude --print` with the prompt on stdin, off the event loop and killed
|
|
156
157
|
// at the ceiling.
|
|
157
|
-
function claudePrint(cwd: string, prompt: string): Promise<string> {
|
|
158
|
+
async function claudePrint(cwd: string, prompt: string): Promise<string> {
|
|
159
|
+
const routed = await routedRun("helper");
|
|
158
160
|
return new Promise((resolve, reject) => {
|
|
159
161
|
const child = execFile(
|
|
160
162
|
"claude",
|
|
161
|
-
["--print"],
|
|
163
|
+
["--print", "--model", routed.model],
|
|
162
164
|
{
|
|
163
165
|
cwd,
|
|
166
|
+
env: { ...process.env, ...routed.env },
|
|
164
167
|
encoding: "utf-8",
|
|
165
168
|
timeout: 30_000,
|
|
166
169
|
killSignal: "SIGKILL",
|
|
@@ -73,7 +73,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|
|
73
73
|
supportsFork: true,
|
|
74
74
|
resumeArgs: (id) => ["--resume", id],
|
|
75
75
|
forkArgs: (id) => ["--resume", id, "--fork-session"],
|
|
76
|
-
modelFlag:
|
|
76
|
+
modelFlag: "--model",
|
|
77
77
|
initialPromptFlag: "", // Positional argument
|
|
78
78
|
},
|
|
79
79
|
{
|
|
@@ -427,7 +427,7 @@ export function renderSummary(
|
|
|
427
427
|
)
|
|
428
428
|
);
|
|
429
429
|
out.push(
|
|
430
|
-
"\nThe reviewer the orchestrator runs (`review`) uses `claude -p --no-session-persistence` and leaves no transcript;
|
|
430
|
+
"\nThe reviewer the orchestrator runs (`review`) uses `claude -p --no-session-persistence` and leaves no transcript; each run's tokens are recorded in `check_runs` and shown in Usage."
|
|
431
431
|
);
|
|
432
432
|
return out.join("\n") + "\n";
|
|
433
433
|
}
|
package/lib/session-titles.ts
CHANGED
|
@@ -174,7 +174,7 @@ export async function generateTitle(
|
|
|
174
174
|
prompt: input,
|
|
175
175
|
schema: SCHEMA,
|
|
176
176
|
tools: [],
|
|
177
|
-
|
|
177
|
+
role: "helper",
|
|
178
178
|
timeoutMs: 30_000,
|
|
179
179
|
})) as { title?: unknown };
|
|
180
180
|
return cleanTitle(out?.title);
|
package/lib/sessions/launch.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { db, queries, type Session } from "../db";
|
|
|
11
11
|
import type { AgentType } from "../providers";
|
|
12
12
|
import type { ChatAccess, ChatImage } from "../chat/events";
|
|
13
13
|
import { resolveModelForAgent } from "../model-catalog";
|
|
14
|
+
import { resolveRole, setSessionModel } from "../models/resolve";
|
|
15
|
+
import { ANTHROPIC } from "../models/roles";
|
|
14
16
|
import { getProject } from "../projects";
|
|
15
17
|
import { getHost, isRemoteHost } from "../hosts";
|
|
16
18
|
import { hostLink } from "../hosts/remote-api";
|
|
@@ -214,14 +216,26 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
214
216
|
await fs.promises.mkdir(SCRATCH_DIR, { recursive: true });
|
|
215
217
|
|
|
216
218
|
const agentType = input.agentType;
|
|
217
|
-
const model = resolveModelForAgent(
|
|
218
|
-
agentType,
|
|
219
|
-
input.model?.trim() || project?.default_model
|
|
220
|
-
);
|
|
221
219
|
const view =
|
|
222
220
|
input.view === "terminal" || !supportsChat(agentType) || remote
|
|
223
221
|
? "terminal"
|
|
224
222
|
: DEFAULT_START_VIEW;
|
|
223
|
+
// Claude with no model picked runs on the interactive role's, here; on
|
|
224
|
+
// another provider only as a chat, whose worker carries its endpoint.
|
|
225
|
+
const routed =
|
|
226
|
+
agentType === "claude" && !input.model?.trim() && !remote
|
|
227
|
+
? resolveRole("interactive", { workspaceId: project?.workspace_id })
|
|
228
|
+
: null;
|
|
229
|
+
const interactive =
|
|
230
|
+
routed && (routed.provider.id === ANTHROPIC || view === "chat")
|
|
231
|
+
? routed
|
|
232
|
+
: null;
|
|
233
|
+
const model = interactive
|
|
234
|
+
? interactive.model
|
|
235
|
+
: resolveModelForAgent(
|
|
236
|
+
agentType,
|
|
237
|
+
input.model?.trim() || project?.default_model
|
|
238
|
+
);
|
|
225
239
|
const text = input.prompt?.trim() ?? "";
|
|
226
240
|
const naming: Naming = text
|
|
227
241
|
? await nameFor(text, projectPath, input.name)
|
|
@@ -252,6 +266,7 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
252
266
|
db.prepare(
|
|
253
267
|
`UPDATE sessions SET view = ?, name_source = ?, setup_status = ? WHERE id = ?`
|
|
254
268
|
).run(view, naming.source, useWorktree ? "running" : null, id);
|
|
269
|
+
if (interactive) setSessionModel(id, interactive);
|
|
255
270
|
if (input.access)
|
|
256
271
|
db.prepare(`UPDATE sessions SET chat_access = ? WHERE id = ?`).run(
|
|
257
272
|
input.access,
|
package/lib/settings-url.ts
CHANGED
package/lib/stacks/land.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Land a stack: merge every PR bottom-up, ported from dispatch's `land`.
|
|
3
|
-
* A preflight checks EVERY PR before anything merges: CI, and a
|
|
4
|
-
*
|
|
3
|
+
* A preflight checks EVERY PR before anything merges: CI, and a passing
|
|
4
|
+
* independent review of its head commit (tasks/code-review.ts). Each sign-off restacks
|
|
5
5
|
* what sits on it, so a restacked PR's checks are waited out again (its green
|
|
6
6
|
* was against the parent's branch). It stops at the first failure and says
|
|
7
7
|
* what landed. A chat item's agent is stopped between turns before anything
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
codeReviewRefusal,
|
|
18
18
|
type TaskPR,
|
|
19
19
|
} from "../tasks";
|
|
20
|
+
import type { ReviewState } from "../tasks/code-review";
|
|
21
|
+
import { getCheck } from "../orchestrator/checks";
|
|
20
22
|
import { itemName } from "./guard";
|
|
21
23
|
import { refreshItems } from "./tick";
|
|
22
24
|
import { outputOf } from "./git";
|
|
@@ -34,6 +36,8 @@ export interface LandDeps {
|
|
|
34
36
|
signOff: (sessionId: string, head?: string) => Promise<void>;
|
|
35
37
|
beforeMerge?: MergeGate;
|
|
36
38
|
prOf: (session: Session) => Promise<TaskPR | null>;
|
|
39
|
+
// The independent review of a task at a commit.
|
|
40
|
+
reviewOf: (sessionId: string, sha: string) => ReviewState | null;
|
|
37
41
|
sleep: (ms: number) => Promise<void>;
|
|
38
42
|
refresh: (stackId: string) => Promise<void>;
|
|
39
43
|
waitMs: number;
|
|
@@ -52,6 +56,7 @@ export const LAND_DEFAULTS: LandDeps = {
|
|
|
52
56
|
await signOffTask(id, { wait: true, head });
|
|
53
57
|
},
|
|
54
58
|
prOf: (s) => taskPR(s, true),
|
|
59
|
+
reviewOf: (id, sha) => getCheck(id, sha, "review"),
|
|
55
60
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
56
61
|
refresh: refreshItems,
|
|
57
62
|
waitMs: 45 * 60_000,
|
|
@@ -90,8 +95,20 @@ async function waitGreen(
|
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
// Why an item can't merge on its review: none passed of its head, or of
|
|
99
|
+
// the head the task pushed before a restack moved it.
|
|
100
|
+
function unreviewed(item: StackItemRow, pr: TaskPR, deps: LandDeps) {
|
|
101
|
+
return codeReviewRefusal({
|
|
102
|
+
head: pr.head,
|
|
103
|
+
attribution: pr.attribution,
|
|
104
|
+
review: (sha) =>
|
|
105
|
+
item.session_id ? deps.reviewOf(item.session_id, sha) : null,
|
|
106
|
+
restacked: { from: item.restacked_from, to: item.restacked_to },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
// Without the orchestrator's gate: the item's head right before merging,
|
|
94
|
-
// only if
|
|
111
|
+
// only if a passing review covers that commit. A push since the
|
|
95
112
|
// preflight needs a new review; the merge is pinned to this head.
|
|
96
113
|
async function reviewedHead(
|
|
97
114
|
item: StackItemRow,
|
|
@@ -100,12 +117,7 @@ async function reviewedHead(
|
|
|
100
117
|
const now = q.item(db, item.id) ?? item;
|
|
101
118
|
const session = sessionOf(now.session_id);
|
|
102
119
|
const pr = session ? await deps.prOf(session) : null;
|
|
103
|
-
const refusal = pr
|
|
104
|
-
? codeReviewRefusal(pr.codeReview, pr.head, {
|
|
105
|
-
from: now.restacked_from,
|
|
106
|
-
to: now.restacked_to,
|
|
107
|
-
})
|
|
108
|
-
: "its PR is gone";
|
|
120
|
+
const refusal = pr ? unreviewed(now, pr, deps) : "its PR is gone";
|
|
109
121
|
if (refusal) throw new Stop(`stopped at ${itemName(now)}: ${refusal}`);
|
|
110
122
|
return pr!.head!;
|
|
111
123
|
}
|
|
@@ -157,14 +169,9 @@ export async function landStack(
|
|
|
157
169
|
const pr =
|
|
158
170
|
session && item.status === "pr" ? await deps.prOf(session) : null;
|
|
159
171
|
const verdict = canSignOff(pr);
|
|
160
|
-
const unreviewed
|
|
161
|
-
pr &&
|
|
162
|
-
codeReviewRefusal(pr.codeReview, pr.head, {
|
|
163
|
-
from: item.restacked_from,
|
|
164
|
-
to: item.restacked_to,
|
|
165
|
-
});
|
|
172
|
+
const refusal = pr && unreviewed(item, pr, deps);
|
|
166
173
|
if (!verdict.ok) missing.push(`${itemName(item)}: ${verdict.reason}`);
|
|
167
|
-
else if (
|
|
174
|
+
else if (refusal) missing.push(`${itemName(item)}: ${refusal}`);
|
|
168
175
|
else if (item.error) missing.push(`${itemName(item)}: ${item.error}`);
|
|
169
176
|
hadChecks.set(item.id, pr?.checks !== "none");
|
|
170
177
|
}
|
package/lib/tasks/brief.ts
CHANGED
|
@@ -16,18 +16,9 @@ function stackedLines(baseBranch: string, stack: StackedOn): string {
|
|
|
16
16
|
return `
|
|
17
17
|
- This task is STACKED on ${stack.name} (${on}), which has not merged. Its commits are already in your branch and are not yours: don't change or revert them.
|
|
18
18
|
- Open your pull request against ${baseBranch} (\`gh pr create --base ${baseBranch}\`) so its diff is only your commits, and start the body with the line "Stacked on ${on} (${stack.name})".
|
|
19
|
-
- Never merge or rebase onto the default branch yourself. When the parent merges, AgentOS rebases your branch and retargets your PR, then tells you; re-run your checks when it does.
|
|
19
|
+
- Never merge or rebase onto the default branch yourself. When the parent merges, AgentOS rebases your branch and retargets your PR, then tells you; re-run your checks when it does. AgentOS reviews your PR itself; a review of your head still counts for the head its restack left, and a push after that gets a new one.${also}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// What the PR body's review section looks like. lib/tasks/code-review.ts
|
|
23
|
-
// parses it, and merges refuse a PR without one for its head commit.
|
|
24
|
-
export const CODE_REVIEW_SECTION = `
|
|
25
|
-
## Code review
|
|
26
|
-
Reviewed: <the full sha of the commit you reviewed, which is the PR's head>
|
|
27
|
-
Agents: <the review agents that ran>
|
|
28
|
-
Fixed: <each finding fixed, one line each, or "none">
|
|
29
|
-
Deferred: <each finding not fixed and why, or "none">`;
|
|
30
|
-
|
|
31
22
|
// The system brief every task agent runs under. It finishes by opening a PR
|
|
32
23
|
// and stopping: merging is the human's sign-off, never the agent's.
|
|
33
24
|
export function buildTaskBrief(opts: {
|
|
@@ -39,10 +30,8 @@ export function buildTaskBrief(opts: {
|
|
|
39
30
|
|
|
40
31
|
- Branch: ${opts.branch} (based on ${opts.baseBranch}). Stay in this directory and on this branch.
|
|
41
32
|
- Do the task fully. Run the project's own checks (typecheck, tests, lint) when they exist and keep them passing.
|
|
42
|
-
- When the work is done, commit with a conventional commit message,
|
|
43
|
-
-
|
|
44
|
-
${CODE_REVIEW_SECTION}
|
|
45
|
-
Merges are refused without it, and when its "Reviewed:" commit is not the PR's head: if you push again, review again and update the body (\`gh pr edit --body-file\`). Then stop and wait.
|
|
33
|
+
- When the work is done, commit with a conventional commit message, run \`git push -u origin ${opts.branch}\` and open a pull request against ${opts.baseBranch} with \`gh pr create\`, using a clear title and a short body that says what changed and how you verified it. Then stop and wait.
|
|
34
|
+
- You don't need to review it yourself: AgentOS runs an independent review of the PR's exact head commit before it can merge, and the orchestrator sends you any blocking findings to fix. For a risky change you may still run \`/do-code-review\` (or \`/code-review\`) before the PR.
|
|
46
35
|
- Never merge the pull request, never push to ${opts.baseBranch}, and never delete branches or worktrees. A human reviews and merges.${opts.stack ? stackedLines(opts.baseBranch, opts.stack) : ""}
|
|
47
36
|
- Write scratch files (PR bodies, review notes, logs, test output) outside the worktree, in your scratchpad or \$TMPDIR. Anything uncommitted left in the worktree keeps it from being removed when the task is done.
|
|
48
37
|
- Do not add any AI or assistant attribution to commits or the pull request.
|