@saadnvd1/agent-os 0.3.671 → 0.3.673
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/files/upload-temp/route.test.ts +144 -0
- package/app/api/files/upload-temp/route.ts +52 -21
- package/app/api/projects/[id]/route.ts +26 -0
- package/app/api/sessions/[id]/route.ts +2 -0
- package/app/api/sessions/route.ts +6 -2
- package/components/Chat/ChatPanel.tsx +9 -2
- package/components/Chat/FileChip.tsx +67 -0
- package/components/Chat/Messages.tsx +8 -0
- package/components/Chat/Queue.tsx +6 -0
- package/components/Chat/composer/Attachments.tsx +16 -2
- package/components/Chat/composer/ComposerBody.tsx +35 -14
- package/components/Chat/composer/Toolbar.tsx +11 -8
- package/components/Chat/composer/editor.test.ts +1 -1
- package/components/Chat/composer/paste.ts +5 -4
- package/components/Chat/composer/useAttachFiles.ts +142 -0
- package/components/Chat/composer/useComposerEditor.ts +3 -3
- package/components/Chat/useDraft.ts +9 -2
- package/components/Draft/DraftPanel.tsx +7 -4
- package/components/Projects/ProjectSettingsDialog.tsx +7 -0
- package/components/Projects/ProjectTierField.tsx +39 -0
- package/components/Terminal/index.tsx +6 -5
- package/components/Workspaces/ProjectTiersForm.tsx +33 -0
- package/components/Workspaces/WorkspaceSettings.tsx +2 -0
- package/data/chat/useChat.ts +10 -3
- package/data/client-config/queries.ts +18 -0
- package/data/projects/queries.ts +4 -0
- package/data/sessions/queries.ts +2 -1
- package/lib/attachments/check.ts +50 -0
- package/lib/attachments/message.ts +49 -0
- package/lib/attachments/policy.ts +116 -0
- package/lib/attachments/store.ts +92 -0
- package/lib/attachments/types.ts +8 -0
- package/lib/chat/client-send.ts +6 -2
- package/lib/chat/driver.ts +4 -1
- package/lib/chat/drivers/claude.ts +2 -0
- package/lib/chat/events.ts +13 -1
- package/lib/chat/pending.ts +2 -1
- package/lib/chat/queued.ts +24 -7
- package/lib/chat/runner.ts +11 -2
- package/lib/chat/worker/host.ts +7 -3
- package/lib/chat/worker/protocol.ts +2 -0
- package/lib/config/client-config-types.ts +14 -3
- package/lib/config/client-config.ts +1 -1
- package/lib/db/migrations/20261018T2130_project_tier.ts +38 -0
- package/lib/db/migrations/20261019T1200_chat_queue_files.ts +14 -0
- package/lib/db/types.ts +4 -0
- package/lib/file-upload.ts +61 -27
- package/lib/intake/intake.ts +2 -17
- package/lib/intake/sniff.ts +1 -1
- package/lib/orchestrator/armed.test-helpers.ts +4 -0
- package/lib/orchestrator/armed.ts +14 -1
- package/lib/orchestrator/brief.ts +9 -2
- package/lib/orchestrator/fast-lane-state.ts +69 -0
- package/lib/orchestrator/fast-lane.ts +96 -0
- package/lib/orchestrator/gates.ts +8 -2
- package/lib/orchestrator/project-tier-tool.ts +33 -0
- package/lib/orchestrator/review.ts +3 -0
- package/lib/orchestrator/serve.ts +6 -1
- package/lib/orchestrator/signoff.test-helpers.ts +3 -0
- package/lib/orchestrator/signoff.ts +36 -19
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +8 -1
- package/lib/orchestrator/tools.ts +2 -0
- package/lib/orchestrator/watcher.ts +5 -0
- package/lib/project-tier/getProjectTier.ts +18 -0
- package/lib/project-tier/index.ts +3 -0
- package/lib/project-tier/setProjectTier.ts +14 -0
- package/lib/project-tier/types.ts +3 -0
- package/lib/sessions/launch.ts +12 -6
- package/lib/start-options.ts +8 -1
- package/lib/tasks/finish.ts +2 -1
- package/lib/tasks/index.ts +10 -5
- package/lib/tasks/state.ts +7 -1
- package/package.json +1 -1
- package/components/Chat/composer/useImageDrop.ts +0 -52
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
reviewSignature,
|
|
42
42
|
} from "./loop-guard";
|
|
43
43
|
import { addNote } from "./notes";
|
|
44
|
+
import { isFast } from "../project-tier/getProjectTier";
|
|
45
|
+
import { recordFastMerge } from "./fast-lane-state";
|
|
44
46
|
import { latestApproval, refundApproval, spendApproval } from "./ask-approvals";
|
|
45
47
|
import { workSubject } from "./asks";
|
|
46
48
|
import { carryApproval } from "./approval-carry";
|
|
@@ -71,7 +73,7 @@ export function restackedOf(task: Session) {
|
|
|
71
73
|
|
|
72
74
|
// `approval`: Saad approved this merge on his asks list, for this commit.
|
|
73
75
|
export type Verdict =
|
|
74
|
-
| { ok: true; sha: string; pr: number; approval?: number }
|
|
76
|
+
| { ok: true; sha: string; pr: number; approval?: number; fast?: true }
|
|
75
77
|
| { ok: false; wait: boolean; text: string };
|
|
76
78
|
|
|
77
79
|
// Whether the task may merge now, and at which commit. Landing merges a
|
|
@@ -107,13 +109,15 @@ export async function judge(
|
|
|
107
109
|
// A PR quick_pr opened (by the row AgentOS wrote, never the body): its
|
|
108
110
|
// code review is the independent review of this head, and until that
|
|
109
111
|
// passes it waits, before any gate is judged or counted.
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
const fast = isFast(task);
|
|
113
|
+
const quick =
|
|
114
|
+
isQuick(task) && !fast
|
|
115
|
+
? quickCodeReview(
|
|
116
|
+
sha,
|
|
117
|
+
getCheck(task.id, sha, "review"),
|
|
118
|
+
restackedOf(task)?.to === sha
|
|
119
|
+
)
|
|
120
|
+
: null;
|
|
117
121
|
// Saad's approval of this head stands in for that review (gateVerdict).
|
|
118
122
|
if (
|
|
119
123
|
quick?.wait &&
|
|
@@ -157,7 +161,7 @@ export async function judge(
|
|
|
157
161
|
return quick ? { refusal: gate.refusal, wait: null } : gate;
|
|
158
162
|
})(),
|
|
159
163
|
}),
|
|
160
|
-
opts
|
|
164
|
+
{ ...opts, fast }
|
|
161
165
|
);
|
|
162
166
|
}
|
|
163
167
|
|
|
@@ -178,8 +182,9 @@ export async function gateVerdict(
|
|
|
178
182
|
workspaceId: string,
|
|
179
183
|
work: GateWork,
|
|
180
184
|
inputs: () => Promise<Omit<GateInput, "sha" | "ruleBreaks">>,
|
|
181
|
-
opts: { count?: boolean } = {}
|
|
185
|
+
opts: { count?: boolean; fast?: boolean } = {}
|
|
182
186
|
): Promise<Verdict> {
|
|
187
|
+
const fast = !!opts.fast;
|
|
183
188
|
const no = (text: string, wait = false): Verdict => ({
|
|
184
189
|
ok: false,
|
|
185
190
|
wait,
|
|
@@ -216,7 +221,9 @@ export async function gateVerdict(
|
|
|
216
221
|
: null;
|
|
217
222
|
const refusal = i.codeReview.refusal ?? stack;
|
|
218
223
|
if (refusal) return no(`${approved} ${refusal}`);
|
|
219
|
-
const ci =
|
|
224
|
+
const ci = fast
|
|
225
|
+
? { state: "pass" as const }
|
|
226
|
+
: ciGate({ sha, ...i, repoCI: await repoCIFor(repo, work, i) });
|
|
220
227
|
if (ci.state === "fail") return no(`${approved} ${ci.reason}`);
|
|
221
228
|
if (ci.state === "wait") return no(`${approved} ${ci.reason}`, true);
|
|
222
229
|
// "escalate" is a repository with no CI: Saad is who that goes to.
|
|
@@ -245,7 +252,7 @@ export async function gateVerdict(
|
|
|
245
252
|
}
|
|
246
253
|
// Here too, not only in the review: one reviewed in parts while approvals
|
|
247
254
|
// were off goes to Saad once they're on.
|
|
248
|
-
if (mergeApprovalsOn()) {
|
|
255
|
+
if (mergeApprovalsOn() && !fast) {
|
|
249
256
|
const size = (await fullDiff(repo, base, sha)).length;
|
|
250
257
|
if (size > DIFF_CAP) {
|
|
251
258
|
const why = `PR #${pr.number}'s diff at ${short(sha)} is ${size} characters, too big to review whole`;
|
|
@@ -255,8 +262,11 @@ export async function gateVerdict(
|
|
|
255
262
|
|
|
256
263
|
const ruled = ruleBreaks(files, await addedLines(repo, base, sha));
|
|
257
264
|
const live = await inputs();
|
|
258
|
-
const repoCI = await repoCIFor(repo, base, live);
|
|
259
|
-
const outcomes = evaluateGates(
|
|
265
|
+
const repoCI = fast ? undefined : await repoCIFor(repo, base, live);
|
|
266
|
+
const outcomes = evaluateGates(
|
|
267
|
+
{ sha, ruleBreaks: ruled, ...live, repoCI },
|
|
268
|
+
{ fast }
|
|
269
|
+
);
|
|
260
270
|
const head = `${work.name} (PR #${pr.number} at ${short(sha)}) can't merge:`;
|
|
261
271
|
const toSaad = outcomes.find((o) => o.state === "escalate");
|
|
262
272
|
if (toSaad)
|
|
@@ -266,9 +276,12 @@ export async function gateVerdict(
|
|
|
266
276
|
const failed = outcomes.filter((o) => o.state === "fail");
|
|
267
277
|
const waiting = outcomes.filter((o) => o.state === "wait");
|
|
268
278
|
if (!failed.length && !waiting.length)
|
|
269
|
-
return
|
|
279
|
+
return fast
|
|
280
|
+
? { ok: true, sha, pr: pr.number, fast: true }
|
|
281
|
+
: { ok: true, sha, pr: pr.number };
|
|
270
282
|
const lines: string[] = [];
|
|
271
|
-
|
|
283
|
+
// The fast lane has no loop guard: a failure is told, never counted.
|
|
284
|
+
if (opts.count === false || fast)
|
|
272
285
|
lines.push(...failed.map((o) => `- ${o.gate} failed: ${o.reason}`));
|
|
273
286
|
else {
|
|
274
287
|
for (const o of outcomes)
|
|
@@ -346,11 +359,15 @@ export async function mergeJudged(
|
|
|
346
359
|
await refundIfRefused(task.id, verdict.approval, async () => {
|
|
347
360
|
method = await signOffTask(task.id, { head: verdict.sha, wait: opts.wait });
|
|
348
361
|
});
|
|
362
|
+
if (verdict.fast)
|
|
363
|
+
recordFastMerge(task.id, workspaceId, verdict.pr, verdict.sha);
|
|
349
364
|
addNote(
|
|
350
365
|
workspaceId,
|
|
351
|
-
verdict.
|
|
352
|
-
? `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}) on
|
|
353
|
-
:
|
|
366
|
+
verdict.fast
|
|
367
|
+
? `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}) on the fast lane: no CI wait or review.`
|
|
368
|
+
: verdict.approval
|
|
369
|
+
? `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}) on Saad's approval of that commit.`
|
|
370
|
+
: `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}): CI green, review passed, in scope.`
|
|
354
371
|
);
|
|
355
372
|
return `Merged ${task.name}: PR #${verdict.pr} ${method ? MERGED_AS[method] : "merged"} at ${short(verdict.sha)}.`;
|
|
356
373
|
}
|
|
@@ -40,6 +40,7 @@ export const TOOL_NAMES = {
|
|
|
40
40
|
archive: name("archive"),
|
|
41
41
|
resurrect: name("resurrect"),
|
|
42
42
|
add_project: name("add_project"),
|
|
43
|
+
project_tier: name("project_tier"),
|
|
43
44
|
note: name("note"),
|
|
44
45
|
question: name("question"),
|
|
45
46
|
resolve_question: name("resolve_question"),
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { BRANCH_NAME } from "../git";
|
|
6
6
|
import { RAISED_KINDS } from "./asks";
|
|
7
7
|
import { MAX_RULE_CHARS } from "./rules";
|
|
8
|
+
import { PROJECT_TIERS } from "../project-tier/types";
|
|
8
9
|
import { PROJECT_NAME } from "../project-add/name";
|
|
9
10
|
import { TASK_ROLES, type ModelRole } from "../models/roles";
|
|
10
11
|
import { PRIORITIES } from "../tasks/schedule-types";
|
|
@@ -160,8 +161,9 @@ export const TOOL_SHAPES = {
|
|
|
160
161
|
model: modelOverride,
|
|
161
162
|
plan: z
|
|
162
163
|
.boolean()
|
|
164
|
+
.optional()
|
|
163
165
|
.describe(
|
|
164
|
-
"Your decision for this job: true to plan first on the planning model, then implement on the implementation model; false to skip the plan and put the steps in the prompt yourself"
|
|
166
|
+
"Your decision for this job: true to plan first on the planning model, then implement on the implementation model; false to skip the plan and put the steps in the prompt yourself. Default: false in a fast project, the workspace's setting in a production one"
|
|
165
167
|
),
|
|
166
168
|
plan_only: z
|
|
167
169
|
.boolean()
|
|
@@ -174,6 +176,7 @@ export const TOOL_SHAPES = {
|
|
|
174
176
|
.trim()
|
|
175
177
|
.min(3)
|
|
176
178
|
.max(200)
|
|
179
|
+
.optional()
|
|
177
180
|
.describe(
|
|
178
181
|
'Why, in a few words Saad reads on the task card: "touches 6 files across gates and UI", "one-line default change"'
|
|
179
182
|
),
|
|
@@ -338,6 +341,10 @@ export const TOOL_SHAPES = {
|
|
|
338
341
|
.optional()
|
|
339
342
|
.describe("A new project's name (default: the folder's)"),
|
|
340
343
|
},
|
|
344
|
+
project_tier: {
|
|
345
|
+
project: z.string().trim().min(1).max(200).optional(),
|
|
346
|
+
tier: z.enum(PROJECT_TIERS).optional(),
|
|
347
|
+
},
|
|
341
348
|
note: { text: z.string().trim().min(1).max(2000) },
|
|
342
349
|
question: {
|
|
343
350
|
text: z
|
|
@@ -165,6 +165,8 @@ const DESCRIPTIONS: Record<ToolName, string> = {
|
|
|
165
165
|
"Bring a dropped, done or archived session back: the same session and conversation, its worktree (with its uncommitted files and plan) back from what was saved or from its branch, its status as it was (a plan-only task waits for approval again, one with a PR is idle), and a PR a drop closed reopened. Archived sessions just unarchive. Held by Pause and braked like a start. Refused when its conversation is gone, its PR was merged, its project was removed or its branch is in use. Give a reason; it goes in the decision log.",
|
|
166
166
|
add_project:
|
|
167
167
|
"Make a git repository on this machine a project of this workspace, by its absolute path, or move the project already at that path into this workspace when it's in no workspace. Says its default branch, detected from git. Refuses a folder that isn't a git repository or that holds secrets, and a project another workspace has.",
|
|
168
|
+
project_tier:
|
|
169
|
+
"Read or set a project's tier. production: every merge gate applies (CI, independent review, scope). fast: a task's PR merges as soon as it opens, with no CI wait or review. Without a project, lists this workspace's; with a project, reads it; with a tier too, sets it, only when Saad says to.",
|
|
168
170
|
note: "Add a line to this workspace's decision log; it shows in your chat.",
|
|
169
171
|
rule_add:
|
|
170
172
|
'Keep a standing rule for this workspace, in your brief on every start from now on ("never merge #111", "briefs must not name third-party products"). It restricts or informs; it can never approve anything.',
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* every 15s while anything in the workspace is working, every 60s when not.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { autoArmFast, followFastMerges } from "./fast-lane";
|
|
8
9
|
import { chatState, chatStateNow, sendChat } from "../chat/runner";
|
|
9
10
|
import type { ChatState } from "../chat/events";
|
|
10
11
|
import { getWorkspace } from "../workspaces";
|
|
@@ -96,7 +97,11 @@ function mergePipeline(workspaceId: string, facts: SessionFacts[]): void {
|
|
|
96
97
|
console.error(`Judging asked commits in ${workspaceId}:`, error)
|
|
97
98
|
);
|
|
98
99
|
await rebaseConflicting(workspaceId, facts);
|
|
100
|
+
await autoArmFast(workspaceId, facts);
|
|
99
101
|
await mergeArmed(workspaceId, facts);
|
|
102
|
+
await followFastMerges(workspaceId).catch((error) =>
|
|
103
|
+
console.error(`Following fast merges in ${workspaceId}:`, error)
|
|
104
|
+
);
|
|
100
105
|
})()
|
|
101
106
|
.catch((error) =>
|
|
102
107
|
console.error(`Merge pipeline for ${workspaceId}:`, error)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import type { ProjectTier } from "./types";
|
|
3
|
+
|
|
4
|
+
// An unknown project counts as production: a missing row never opens the
|
|
5
|
+
// fast lane.
|
|
6
|
+
export function projectTier(projectId: string | null): ProjectTier {
|
|
7
|
+
if (!projectId) return "production";
|
|
8
|
+
const row = db
|
|
9
|
+
.prepare(`SELECT tier FROM projects WHERE id = ?`)
|
|
10
|
+
.get(projectId) as { tier: string } | undefined;
|
|
11
|
+
return row?.tier === "fast" ? "fast" : "production";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const taskTier = (task: { project_id: string | null }): ProjectTier =>
|
|
15
|
+
projectTier(task.project_id);
|
|
16
|
+
|
|
17
|
+
export const isFast = (task: { project_id: string | null }): boolean =>
|
|
18
|
+
taskTier(task) === "fast";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getProject } from "@/lib/projects";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import type { Project } from "@/lib/db/types";
|
|
4
|
+
import type { ProjectTier } from "./types";
|
|
5
|
+
|
|
6
|
+
export function setProjectTier(projectId: string, tier: ProjectTier): Project {
|
|
7
|
+
const project = getProject(projectId);
|
|
8
|
+
if (!project || project.is_uncategorized)
|
|
9
|
+
throw new Error("Project not found or cannot be modified");
|
|
10
|
+
db.prepare(
|
|
11
|
+
`UPDATE projects SET tier = ?, updated_at = datetime('now') WHERE id = ?`
|
|
12
|
+
).run(tier, projectId);
|
|
13
|
+
return getProject(projectId)!;
|
|
14
|
+
}
|
package/lib/sessions/launch.ts
CHANGED
|
@@ -10,7 +10,7 @@ import os from "os";
|
|
|
10
10
|
import { randomUUID } from "crypto";
|
|
11
11
|
import { db, queries, type Session } from "../db";
|
|
12
12
|
import type { AgentType } from "../providers";
|
|
13
|
-
import type { ChatAccess, ChatImage } from "../chat/events";
|
|
13
|
+
import type { ChatAccess, ChatFile, ChatImage } from "../chat/events";
|
|
14
14
|
import { resolveModelForAgent } from "../model-catalog";
|
|
15
15
|
import { parseOverride, resolveRole, setSessionModel } from "../models/resolve";
|
|
16
16
|
import { ANTHROPIC } from "../models/roles";
|
|
@@ -69,6 +69,7 @@ export interface LaunchInput {
|
|
|
69
69
|
// The first message.
|
|
70
70
|
prompt?: string | null;
|
|
71
71
|
images?: ChatImage[];
|
|
72
|
+
files?: ChatFile[];
|
|
72
73
|
name?: string | null;
|
|
73
74
|
view?: "chat" | "terminal";
|
|
74
75
|
parentSessionId?: string | null;
|
|
@@ -103,9 +104,14 @@ function firstMessage(projectPrompt: string | null | undefined, text: string) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// Waits in the queue, so neither a restart nor a slow setup loses it.
|
|
106
|
-
function queueFirst(
|
|
107
|
+
function queueFirst(
|
|
108
|
+
sessionId: string,
|
|
109
|
+
text: string,
|
|
110
|
+
images?: ChatImage[],
|
|
111
|
+
files?: ChatFile[]
|
|
112
|
+
) {
|
|
107
113
|
const id = `user-${Date.now()}-${randomUUID().slice(0, 5)}`;
|
|
108
|
-
enqueue(sessionId, { id, text, images });
|
|
114
|
+
enqueue(sessionId, { id, text, images, files });
|
|
109
115
|
return id;
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -201,8 +207,8 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
201
207
|
// A linked machine's AgentOS starts it there; it is mirrored here.
|
|
202
208
|
const link = remote ? hostLink(runOn) : null;
|
|
203
209
|
if (link) {
|
|
204
|
-
if (input.images?.length)
|
|
205
|
-
throw new Error(`Images can't be sent to ${link.hostName} yet`);
|
|
210
|
+
if (input.images?.length || input.files?.length)
|
|
211
|
+
throw new Error(`Images and files can't be sent to ${link.hostName} yet`);
|
|
206
212
|
return startOnPeer(link, {
|
|
207
213
|
id,
|
|
208
214
|
project: scratch ? null : (project ?? null),
|
|
@@ -305,7 +311,7 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
305
311
|
: "";
|
|
306
312
|
const chatFirst =
|
|
307
313
|
view === "chat" && message
|
|
308
|
-
? queueFirst(id, message, input.images)
|
|
314
|
+
? queueFirst(id, message, input.images, input.files)
|
|
309
315
|
: undefined;
|
|
310
316
|
|
|
311
317
|
if (useWorktree) {
|
package/lib/start-options.ts
CHANGED
|
@@ -40,7 +40,14 @@ export async function startOptions(
|
|
|
40
40
|
const opts = { workspaceId: project?.workspace_id ?? null };
|
|
41
41
|
roles.plan = resolveRole("plan", opts, models).ref;
|
|
42
42
|
roles.implement = resolveRole("implement", opts, models).ref;
|
|
43
|
-
planDefault = taskRouting(
|
|
43
|
+
planDefault = taskRouting(
|
|
44
|
+
opts.workspaceId,
|
|
45
|
+
"chat",
|
|
46
|
+
undefined,
|
|
47
|
+
undefined,
|
|
48
|
+
undefined,
|
|
49
|
+
project?.tier
|
|
50
|
+
).plan;
|
|
44
51
|
} catch (error) {
|
|
45
52
|
planError = error instanceof Error ? error.message : String(error);
|
|
46
53
|
}
|
package/lib/tasks/finish.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { sessionEnded } from "../experiments/hooks";
|
|
|
40
40
|
import { snapshotWorktree } from "../resurrect/snapshot";
|
|
41
41
|
import { keepSessionFiles } from "../session-artifacts/collect";
|
|
42
42
|
import { setPr, setTask } from "../db/session-parts";
|
|
43
|
+
import { isFast } from "../project-tier/getProjectTier";
|
|
43
44
|
|
|
44
45
|
export const sessionMergePolicy = (session: Session): MergePolicy =>
|
|
45
46
|
mergePolicy(session.project_id ? getProject(session.project_id) : null);
|
|
@@ -167,7 +168,7 @@ export async function signOffTask(
|
|
|
167
168
|
throw new Error(`${opts.base} isn't a base ${session.name} may merge into`);
|
|
168
169
|
const judgedBase = opts.base ?? session.base_branch;
|
|
169
170
|
const pr = await prFor(session, true);
|
|
170
|
-
const verdict = canSignOff(pr);
|
|
171
|
+
const verdict = canSignOff(pr, { ci: !isFast(session) });
|
|
171
172
|
if (!verdict.ok) throw new Error(verdict.reason);
|
|
172
173
|
if (pr!.base && pr!.base !== judgedBase)
|
|
173
174
|
throw new Error(
|
package/lib/tasks/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { effectiveModels } from "../models/config";
|
|
|
15
15
|
import { ANTHROPIC } from "../models/roles";
|
|
16
16
|
import { processTable, runsSomething } from "../process-table";
|
|
17
17
|
import { randomUUID } from "crypto";
|
|
18
|
-
import { db, queries, type Session } from "../db";
|
|
18
|
+
import { db, queries, type ProjectTier, type Session } from "../db";
|
|
19
19
|
import { getProject } from "../projects";
|
|
20
20
|
import {
|
|
21
21
|
createWorktree,
|
|
@@ -267,7 +267,8 @@ async function startTask(
|
|
|
267
267
|
view,
|
|
268
268
|
opts.model,
|
|
269
269
|
opts.plan,
|
|
270
|
-
opts.planOnly
|
|
270
|
+
opts.planOnly,
|
|
271
|
+
project.tier
|
|
271
272
|
).plan;
|
|
272
273
|
const session = await startRemoteTask(hostId, project, {
|
|
273
274
|
id,
|
|
@@ -310,7 +311,8 @@ async function startTask(
|
|
|
310
311
|
view,
|
|
311
312
|
opts.model,
|
|
312
313
|
opts.plan,
|
|
313
|
-
opts.planOnly
|
|
314
|
+
opts.planOnly,
|
|
315
|
+
project.tier
|
|
314
316
|
)
|
|
315
317
|
: otherAgentRouting(opts.model);
|
|
316
318
|
const projectPath = expandHome(project.working_directory);
|
|
@@ -590,7 +592,9 @@ export function taskRouting(
|
|
|
590
592
|
view: StartView,
|
|
591
593
|
model: unknown,
|
|
592
594
|
plan?: boolean,
|
|
593
|
-
planOnly?: boolean
|
|
595
|
+
planOnly?: boolean,
|
|
596
|
+
// A fast project skips the plan step unless the caller asks for it.
|
|
597
|
+
tier: ProjectTier = "production"
|
|
594
598
|
) {
|
|
595
599
|
const override = parseOverride(model);
|
|
596
600
|
const models = effectiveModels();
|
|
@@ -601,7 +605,8 @@ export function taskRouting(
|
|
|
601
605
|
const planStep =
|
|
602
606
|
view === "chat" &&
|
|
603
607
|
(!!planOnly ||
|
|
604
|
-
((plan ??
|
|
608
|
+
((plan ?? (tier === "fast" ? false : models.planStep)) &&
|
|
609
|
+
planning.ref !== implement.ref));
|
|
605
610
|
return {
|
|
606
611
|
override,
|
|
607
612
|
plan: planStep,
|
package/lib/tasks/state.ts
CHANGED
|
@@ -97,13 +97,19 @@ export function needsHuman(state: TaskState): boolean {
|
|
|
97
97
|
].includes(state);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// `ci: false` (a fast project): checks that are failing or running don't
|
|
101
|
+
// refuse the merge.
|
|
102
|
+
export function canSignOff(
|
|
103
|
+
pr: TaskPR | null,
|
|
104
|
+
opts: { ci?: boolean } = {}
|
|
105
|
+
): {
|
|
101
106
|
ok: boolean;
|
|
102
107
|
reason?: string;
|
|
103
108
|
} {
|
|
104
109
|
if (!pr) return { ok: false, reason: "No pull request yet" };
|
|
105
110
|
if (pr.state !== "OPEN")
|
|
106
111
|
return { ok: false, reason: `PR is ${pr.state.toLowerCase()}` };
|
|
112
|
+
if (opts.ci === false) return { ok: true };
|
|
107
113
|
if (pr.checks === "fail")
|
|
108
114
|
return { ok: false, reason: "CI checks are failing" };
|
|
109
115
|
if (pr.checks === "pending")
|
package/package.json
CHANGED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import type { ChatImage } from "@/lib/chat/events";
|
|
5
|
-
import { isImage } from "./paste";
|
|
6
|
-
|
|
7
|
-
function readImage(file: File): Promise<ChatImage> {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
const reader = new FileReader();
|
|
10
|
-
reader.onload = () => {
|
|
11
|
-
const [meta, data] = String(reader.result).split(",");
|
|
12
|
-
resolve({ mediaType: meta.slice(5, meta.indexOf(";")), data });
|
|
13
|
-
};
|
|
14
|
-
reader.onerror = reject;
|
|
15
|
-
reader.readAsDataURL(file);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const carriesFiles = (e: React.DragEvent) =>
|
|
20
|
-
[...e.dataTransfer.types].includes("Files");
|
|
21
|
-
|
|
22
|
-
// Images picked, pasted or dropped onto the composer, and the outline
|
|
23
|
-
// shown while something is dragged over it.
|
|
24
|
-
export function useImageDrop(onImages: (images: ChatImage[]) => void) {
|
|
25
|
-
const [dragging, setDragging] = useState(false);
|
|
26
|
-
|
|
27
|
-
const addFiles = async (picked: Iterable<File>) => {
|
|
28
|
-
const read = await Promise.all([...picked].filter(isImage).map(readImage));
|
|
29
|
-
if (read.length) onImages(read);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const dropProps = {
|
|
33
|
-
onDragOver: (e: React.DragEvent) => {
|
|
34
|
-
if (!carriesFiles(e)) return;
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
e.dataTransfer.dropEffect = "copy";
|
|
37
|
-
setDragging(true);
|
|
38
|
-
},
|
|
39
|
-
onDragLeave: (e: React.DragEvent) => {
|
|
40
|
-
if (!e.currentTarget.contains(e.relatedTarget as Node | null))
|
|
41
|
-
setDragging(false);
|
|
42
|
-
},
|
|
43
|
-
onDrop: (e: React.DragEvent) => {
|
|
44
|
-
if (!carriesFiles(e)) return;
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
setDragging(false);
|
|
47
|
-
void addFiles(e.dataTransfer.files);
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return { dragging, dropProps, addFiles };
|
|
52
|
-
}
|