@agentrific/server 0.1.2 → 0.1.4

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/index.js CHANGED
@@ -33,6 +33,9 @@ var cronBounds = [
33
33
  [1, 12],
34
34
  [0, 6]
35
35
  ];
36
+ function cronRange(base, min, max, stepped) {
37
+ return base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), stepped ? max : Number(base)];
38
+ }
36
39
  function cronFields(expression) {
37
40
  const fields = expression.trim().split(/\s+/);
38
41
  if (fields.length !== 5)
@@ -45,7 +48,7 @@ function cronFields(expression) {
45
48
  if (!match) throw new Error("Use numeric cron fields, *, ranges, lists and steps.");
46
49
  const base = match[1];
47
50
  const step = Number(match[2] ?? 1);
48
- const [start, end] = base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), match[2] ? max : Number(base)];
51
+ const [start, end] = cronRange(base, min, max, Boolean(match[2]));
49
52
  if (!Number.isInteger(step) || step < 1 || step > max - min + 1 || start < min || end > max || start > end)
50
53
  throw new Error("Cron field is outside its allowed range.");
51
54
  for (let value = start; value <= end; value += step) values.add(value);
@@ -287,6 +290,7 @@ var TaskSchema = z.object({
287
290
  gitFlow: GitFlowSchema.optional(),
288
291
  status: TaskStatusSchema,
289
292
  sessionId: z.string().nullable(),
293
+ pullRequestUrl: z.string().url().optional(),
290
294
  createdAt: z.string().datetime(),
291
295
  updatedAt: z.string().datetime(),
292
296
  deletedAt: z.string().datetime().optional()
@@ -304,7 +308,7 @@ var UpdateTaskSchema = CreateTaskSchema.pick({
304
308
  prompt: true,
305
309
  attachmentIds: true,
306
310
  gitFlow: true
307
- }).strict();
311
+ }).extend({ gitFlow: GitFlowSchema.nullable().optional() }).strict();
308
312
  var PROTOCOL_VERSION = 1;
309
313
  var ServerUpdateStatusSchema = z.object({
310
314
  currentVersion: z.string(),
@@ -910,7 +914,7 @@ async function startControl(directory, security2, status, maintenance2) {
910
914
  // package.json
911
915
  var package_default = {
912
916
  name: "@agentrific/server",
913
- version: "0.1.2",
917
+ version: "0.1.4",
914
918
  description: "Self-hosted control plane for long-running coding agents",
915
919
  keywords: [
916
920
  "agentrific",
@@ -1169,6 +1173,9 @@ function taskStatusForSession(status, session) {
1169
1173
  return "needs_user_input";
1170
1174
  }
1171
1175
  }
1176
+ function pullRequestUrl(session) {
1177
+ return session.gitDelivery?.status === "pr_opened" ? session.gitDelivery.url : void 0;
1178
+ }
1172
1179
  function sessionTask(session) {
1173
1180
  return TaskSchema.parse({
1174
1181
  id: `session-${session.id}`,
@@ -1179,6 +1186,7 @@ function sessionTask(session) {
1179
1186
  gitFlow: session.gitFlow,
1180
1187
  status: taskStatusForSession(session.status, session),
1181
1188
  sessionId: session.id,
1189
+ pullRequestUrl: pullRequestUrl(session),
1182
1190
  createdAt: session.createdAt,
1183
1191
  updatedAt: session.updatedAt
1184
1192
  });
@@ -1190,12 +1198,13 @@ async function boardTasks(store2, projectId) {
1190
1198
  tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
1191
1199
  );
1192
1200
  const linkedTasks = tasks.filter((task) => !task.deletedAt).map((task) => {
1193
- const session = sessions.find((item) => item.taskId === task.id) ?? sessions.find((item) => item.id === task.sessionId);
1194
- if (!session) return { ...task, status: "backlog" };
1201
+ const session = sessions.find((item) => item.id === task.sessionId) ?? sessions.find((item) => item.taskId === task.id);
1202
+ if (!session) return { ...task, status: "backlog", pullRequestUrl: void 0 };
1195
1203
  linkedSessionIds.add(session.id);
1196
1204
  return {
1197
1205
  ...task,
1198
1206
  sessionId: session.id,
1207
+ pullRequestUrl: pullRequestUrl(session),
1199
1208
  status: taskStatusForSession(session.status, session)
1200
1209
  };
1201
1210
  });
@@ -1213,7 +1222,9 @@ async function findTask(store2, id) {
1213
1222
  function editTask(store2, id, input, resolveAttachments) {
1214
1223
  return withTaskLock(id, async () => {
1215
1224
  const task = await findTask(store2, id);
1216
- const { attachmentIds, ...updates2 } = input;
1225
+ const { attachmentIds, gitFlow, ...updates2 } = input;
1226
+ const configuration = { gitFlow: task.gitFlow };
1227
+ if (gitFlow !== void 0) configuration.gitFlow = gitFlow ?? void 0;
1217
1228
  let files = task.attachments;
1218
1229
  if (attachmentIds) {
1219
1230
  if (!resolveAttachments)
@@ -1224,6 +1235,7 @@ function editTask(store2, id, input, resolveAttachments) {
1224
1235
  store2.saveTask({
1225
1236
  ...task,
1226
1237
  ...updates2,
1238
+ ...configuration,
1227
1239
  attachments: files,
1228
1240
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1229
1241
  })
@@ -1467,7 +1479,7 @@ var AgentController = class {
1467
1479
  Effect7.gen({ self: this }, function* () {
1468
1480
  if (this.closed)
1469
1481
  return yield* Effect7.fail(new AgentError({ message: "Server is shutting down" }));
1470
- if (!session.branch)
1482
+ if (!session.branch && (session.gitFlow || !session.workspacePath))
1471
1483
  return yield* Effect7.fail(new AgentError({ message: "Session checkout is not ready" }));
1472
1484
  if (this.hasMessage(session.id, messageId)) return;
1473
1485
  const entry = yield* this.open(session);
@@ -1774,45 +1786,7 @@ ${error ? workflowError(error instanceof Error ? error : String(error)) : messag
1774
1786
  if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1775
1787
  throw new Error("Restore the assigned run branch before requesting delivery.");
1776
1788
  if (dirty) {
1777
- await this.git(session, ["add", "--all"], signal);
1778
- await this.stage(job, "drafting");
1779
- const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1780
- const files = await this.git(
1781
- session,
1782
- ["ls-files", "--others", "--exclude-standard"],
1783
- signal
1784
- );
1785
- if (this.options.draft) {
1786
- try {
1787
- const draft = await this.options.draft(
1788
- session,
1789
- input,
1790
- `${diff}
1791
- New files:
1792
- ${files}`.slice(0, 4e4),
1793
- signal
1794
- );
1795
- job.title = draft.title;
1796
- job.body = draft.body;
1797
- } catch (error) {
1798
- job.logs = workflowLog(
1799
- `${job.logs}
1800
- Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1801
- );
1802
- }
1803
- }
1804
- await this.stage(job, "committing");
1805
- if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1806
- throw new Error("The workspace changed branches while preparing delivery.");
1807
- const output = await this.git(
1808
- session,
1809
- ["commit", "-m", `${job.title}
1810
-
1811
- Agentrific run ${id}.`],
1812
- signal
1813
- );
1814
- job.logs = workflowLog(`${job.logs}
1815
- ${output}`);
1789
+ await this.commitChanges(session, job, input, signal);
1816
1790
  } else {
1817
1791
  await this.git(session, ["hook", "run", "--ignore-missing", "pre-commit"], signal);
1818
1792
  }
@@ -1839,6 +1813,44 @@ ${workflowError(error instanceof Error ? error : String(error))}`
1839
1813
  }
1840
1814
  });
1841
1815
  }
1816
+ async commitChanges(session, job, input, signal) {
1817
+ const id = session.id;
1818
+ await this.git(session, ["add", "--all"], signal);
1819
+ await this.stage(job, "drafting");
1820
+ const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1821
+ const files = await this.git(session, ["ls-files", "--others", "--exclude-standard"], signal);
1822
+ if (this.options.draft) {
1823
+ try {
1824
+ const draft = await this.options.draft(
1825
+ session,
1826
+ input,
1827
+ `${diff}
1828
+ New files:
1829
+ ${files}`.slice(0, 4e4),
1830
+ signal
1831
+ );
1832
+ job.title = draft.title;
1833
+ job.body = draft.body;
1834
+ } catch (error) {
1835
+ job.logs = workflowLog(
1836
+ `${job.logs}
1837
+ Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1838
+ );
1839
+ }
1840
+ }
1841
+ await this.stage(job, "committing");
1842
+ if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1843
+ throw new Error("The workspace changed branches while preparing delivery.");
1844
+ const output = await this.git(
1845
+ session,
1846
+ ["commit", "-m", `${job.title}
1847
+
1848
+ Agentrific run ${id}.`],
1849
+ signal
1850
+ );
1851
+ job.logs = workflowLog(`${job.logs}
1852
+ ${output}`);
1853
+ }
1842
1854
  fallbackBody(id, input) {
1843
1855
  return `${input.summary}
1844
1856
 
@@ -1911,6 +1923,31 @@ Agentrific run ${id}.`;
1911
1923
  );
1912
1924
  }
1913
1925
  }
1926
+ async mergeChanges(session, job, projectPath, commit) {
1927
+ await this.stage(job, "integrating");
1928
+ if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
1929
+ await this.command(projectPath, "git", ["switch", "main"]);
1930
+ if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
1931
+ throw new Error("Switch the project checkout to main, then retry delivery.");
1932
+ if (await this.command(projectPath, "git", ["status", "--porcelain"]))
1933
+ throw new Error(
1934
+ "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
1935
+ );
1936
+ const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
1937
+ if (!included) {
1938
+ try {
1939
+ await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
1940
+ } catch (error) {
1941
+ await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
1942
+ throw new Error(
1943
+ `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
1944
+ ${workflowError(error instanceof Error ? error : String(error))}`
1945
+ );
1946
+ }
1947
+ }
1948
+ job.result = "merged";
1949
+ job.message = "Committed changes are on main.";
1950
+ }
1914
1951
  async integrate(id) {
1915
1952
  const job = await this.job(id);
1916
1953
  if (!job?.commitSha) return;
@@ -1924,29 +1961,7 @@ Agentrific run ${id}.`;
1924
1961
  "The workspace changed after requesting delivery. Call deliver_changes again for the new changes."
1925
1962
  );
1926
1963
  if (session.gitFlow.delivery === "merge") {
1927
- await this.stage(job, "integrating");
1928
- if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
1929
- await this.command(projectPath, "git", ["switch", "main"]);
1930
- if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
1931
- throw new Error("Switch the project checkout to main, then retry delivery.");
1932
- if (await this.command(projectPath, "git", ["status", "--porcelain"]))
1933
- throw new Error(
1934
- "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
1935
- );
1936
- const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
1937
- if (!included) {
1938
- try {
1939
- await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
1940
- } catch (error) {
1941
- await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
1942
- throw new Error(
1943
- `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
1944
- ${workflowError(error instanceof Error ? error : String(error))}`
1945
- );
1946
- }
1947
- }
1948
- job.result = "merged";
1949
- job.message = "Committed changes are on main.";
1964
+ await this.mergeChanges(session, job, projectPath, commit);
1950
1965
  } else {
1951
1966
  await this.stage(job, "pushing");
1952
1967
  await this.git(session, ["push", "origin", `${commit}:refs/heads/${session.branch}`]);
@@ -2024,6 +2039,7 @@ var PreparationCoordinator = class {
2024
2039
  return [...this.preparing.values()].map((id) => `preparation:${id}`);
2025
2040
  }
2026
2041
  async prepare(session, projectPath, input) {
2042
+ if (!session.gitFlow) return { ...session, branch: null, workspacePath: projectPath };
2027
2043
  const preparation2 = Symbol(session.id);
2028
2044
  this.preparing.set(preparation2, session.id);
2029
2045
  return withTaskLock(`prepare:${session.id}`, async () => {
@@ -2056,56 +2072,7 @@ ${logs}` : ""}`);
2056
2072
  try {
2057
2073
  await record("creating", "Creating the run workspace\u2026");
2058
2074
  const workspace = session.workspacePath;
2059
- const checkout = session.gitFlow?.workspace === "checkout";
2060
- const branchAtWorkspace = await command(workspace, "git", [
2061
- "branch",
2062
- "--show-current"
2063
- ]).catch(() => void 0);
2064
- if (branchAtWorkspace !== session.branch) {
2065
- if (checkout) {
2066
- if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2067
- throw new Error(
2068
- "The project checkout must be clean and on main before starting a checkout run."
2069
- );
2070
- await command(
2071
- projectPath,
2072
- "git",
2073
- ["switch", "-c", session.branch, "main"],
2074
- void 0,
2075
- true
2076
- );
2077
- } else {
2078
- if (branchAtWorkspace !== void 0)
2079
- throw new Error("The run workspace already exists on a different branch.");
2080
- await mkdir3(path4.dirname(workspace), { recursive: true });
2081
- try {
2082
- await command(
2083
- projectPath,
2084
- "git",
2085
- [
2086
- "worktree",
2087
- "add",
2088
- "-b",
2089
- session.branch,
2090
- workspace,
2091
- session.gitFlow ? "main" : "HEAD"
2092
- ],
2093
- void 0,
2094
- true
2095
- );
2096
- } catch (error) {
2097
- if (await command(workspace, "git", ["branch", "--show-current"]).catch(
2098
- () => void 0
2099
- ) !== session.branch)
2100
- throw error;
2101
- await record(
2102
- "creating",
2103
- "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2104
- workflowError(error instanceof Error ? error : String(error))
2105
- );
2106
- }
2107
- }
2108
- }
2075
+ await this.createWorkspace(session, projectPath, command, record);
2109
2076
  await record("installing", "Installing run dependencies\u2026");
2110
2077
  await this.install(workspace, command);
2111
2078
  await record("ready", "Workspace ready.");
@@ -2121,6 +2088,57 @@ ${logs}` : ""}`);
2121
2088
  }
2122
2089
  }).finally(() => this.preparing.delete(preparation2));
2123
2090
  }
2091
+ async createWorkspace(session, projectPath, command, record) {
2092
+ const workspace = session.workspacePath;
2093
+ const checkout = session.gitFlow?.workspace === "checkout";
2094
+ if (!await command(projectPath, "git", ["rev-parse", "--verify", "HEAD"]).catch(() => void 0))
2095
+ throw new Error(
2096
+ "Git Flow needs an initial commit. Create one in the project, or turn off Git Flow for a plain chat."
2097
+ );
2098
+ const branchAtWorkspace = await command(workspace, "git", ["branch", "--show-current"]).catch(
2099
+ () => void 0
2100
+ );
2101
+ if (branchAtWorkspace !== session.branch) {
2102
+ if (checkout) {
2103
+ if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2104
+ throw new Error(
2105
+ "The project checkout must be clean and on main before starting a checkout run."
2106
+ );
2107
+ await command(
2108
+ projectPath,
2109
+ "git",
2110
+ ["switch", "-c", session.branch, "main"],
2111
+ void 0,
2112
+ true
2113
+ );
2114
+ } else {
2115
+ await this.createWorktree(session, projectPath, command, record, branchAtWorkspace);
2116
+ }
2117
+ }
2118
+ }
2119
+ async createWorktree(session, projectPath, command, record, branchAtWorkspace) {
2120
+ const workspace = session.workspacePath;
2121
+ if (branchAtWorkspace !== void 0)
2122
+ throw new Error("The run workspace already exists on a different branch.");
2123
+ await mkdir3(path4.dirname(workspace), { recursive: true });
2124
+ try {
2125
+ await command(
2126
+ projectPath,
2127
+ "git",
2128
+ ["worktree", "add", "-b", session.branch, workspace, "main"],
2129
+ void 0,
2130
+ true
2131
+ );
2132
+ } catch (error) {
2133
+ if (await command(workspace, "git", ["branch", "--show-current"]).catch(() => void 0) !== session.branch)
2134
+ throw error;
2135
+ await record(
2136
+ "creating",
2137
+ "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2138
+ workflowError(error instanceof Error ? error : String(error))
2139
+ );
2140
+ }
2141
+ }
2124
2142
  async install(workspace, command) {
2125
2143
  const packageFile = await readFile3(path4.join(workspace, "package.json"), "utf8").catch(
2126
2144
  () => void 0
@@ -2845,31 +2863,7 @@ var SqliteSessionStore = class {
2845
2863
  this.db.prepare("DELETE FROM projects WHERE id = ?").run(replacedId);
2846
2864
  }
2847
2865
  if (previousPath && previousPath !== project.path) {
2848
- this.db.prepare(
2849
- "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
2850
- ).run(project.path, previousPath);
2851
- const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
2852
- for (const row of this.db.prepare(
2853
- "SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?"
2854
- ).all(project.id)) {
2855
- const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
2856
- if (session.workspacePath) {
2857
- session.workspacePath = String(relocate(session.workspacePath));
2858
- this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
2859
- }
2860
- }
2861
- for (const row of this.db.prepare(
2862
- "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
2863
- ).all(project.id)) {
2864
- const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
2865
- if (event.workspacePath) {
2866
- event.workspacePath = String(relocate(event.workspacePath));
2867
- this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(
2868
- JSON.stringify({ type: "session.updated", session: event }),
2869
- Number(row.sequence)
2870
- );
2871
- }
2872
- }
2866
+ this.relocateProjectHistory(project, previousPath);
2873
2867
  }
2874
2868
  this.db.prepare(
2875
2869
  "INSERT INTO projects(id, payload) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
@@ -2882,6 +2876,28 @@ var SqliteSessionStore = class {
2882
2876
  }
2883
2877
  });
2884
2878
  }
2879
+ relocateProjectHistory(project, previousPath) {
2880
+ this.db.prepare(
2881
+ "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
2882
+ ).run(project.path, previousPath);
2883
+ const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
2884
+ for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
2885
+ const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
2886
+ if (session.workspacePath) {
2887
+ session.workspacePath = String(relocate(session.workspacePath));
2888
+ this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
2889
+ }
2890
+ }
2891
+ for (const row of this.db.prepare(
2892
+ "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
2893
+ ).all(project.id)) {
2894
+ const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
2895
+ if (event.workspacePath) {
2896
+ event.workspacePath = String(relocate(event.workspacePath));
2897
+ this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(JSON.stringify({ type: "session.updated", session: event }), Number(row.sequence));
2898
+ }
2899
+ }
2900
+ }
2885
2901
  defaultModel() {
2886
2902
  return this.query(() => {
2887
2903
  const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'default-model'").get();
@@ -3486,16 +3502,27 @@ app.get("/api/sessions/:id/events", async (request, reply) => {
3486
3502
  if (!session) return reply.code(404).send({ error: "Session not found." });
3487
3503
  return Effect14.runPromise(store.history(session.id, query.data.after, query.data.limit));
3488
3504
  });
3489
- async function createAndStartSession(input) {
3490
- maintenance.assertReady();
3491
- if (input.gitFlow?.delivery === "pull_request") {
3492
- const github = await githubCliStatus();
3493
- if (!github.githubCli.authenticated)
3494
- return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
3505
+ async function selectSessionModel(input, hasImages) {
3506
+ try {
3507
+ const reference = input.model ?? await models.defaultReference();
3508
+ const selected = await models.resolve(reference);
3509
+ if (hasImages && !selected.input.includes("image"))
3510
+ return {
3511
+ error: "This model cannot read images. Choose a vision model in Settings.",
3512
+ status: 400
3513
+ };
3514
+ return { model: models.identity(selected.provider, selected.id) };
3515
+ } catch {
3516
+ return {
3517
+ error: "The default model is unavailable. Choose an available model in Settings.",
3518
+ status: 400
3519
+ };
3495
3520
  }
3521
+ }
3522
+ async function resolveSessionProject(projectId) {
3496
3523
  const projectsRoot = await Effect14.runPromise(store.projectsRoot(defaultProjectsRoot));
3497
3524
  const project = (await Effect14.runPromise(discoverProjects(projectsRoot, store))).find(
3498
- (item) => item.id === input.projectId && item.available
3525
+ (item) => item.id === projectId && item.available
3499
3526
  );
3500
3527
  if (!project)
3501
3528
  return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
@@ -3503,6 +3530,37 @@ async function createAndStartSession(input) {
3503
3530
  const resolvedRoot = await realpath2(projectsRoot);
3504
3531
  if (path7.dirname(projectPath) !== resolvedRoot)
3505
3532
  return { error: "Project path is outside projects root.", status: 400 };
3533
+ return { projectPath };
3534
+ }
3535
+ async function resolveSessionWorkspace(id, gitFlow, projectPath) {
3536
+ const worktreePath = path7.join(worktreesRoot, id);
3537
+ const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
3538
+ const useProjectCheckout = gitFlow?.workspace === "checkout";
3539
+ const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
3540
+ if (useProjectCheckout) {
3541
+ const serverPath = await realpath2(process.cwd());
3542
+ if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
3543
+ return {
3544
+ error: "The server runs from this project. Choose a worktree for this task.",
3545
+ status: 409
3546
+ };
3547
+ }
3548
+ return { branch, workspacePath };
3549
+ }
3550
+ function sessionTaskUnavailable(task, projectId) {
3551
+ return !task || Boolean(task.deletedAt) || task.projectId !== projectId;
3552
+ }
3553
+ async function sessionDeliveryAvailable(gitFlow) {
3554
+ if (gitFlow?.delivery !== "pull_request") return true;
3555
+ return (await githubCliStatus()).githubCli.authenticated;
3556
+ }
3557
+ async function createAndStartSession(input) {
3558
+ maintenance.assertReady();
3559
+ if (!await sessionDeliveryAvailable(input.gitFlow))
3560
+ return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
3561
+ const location = await resolveSessionProject(input.projectId);
3562
+ if (location.error !== void 0) return { error: location.error, status: location.status };
3563
+ const projectPath = location.projectPath;
3506
3564
  let files;
3507
3565
  try {
3508
3566
  files = await Effect14.runPromise(
@@ -3511,25 +3569,15 @@ async function createAndStartSession(input) {
3511
3569
  } catch (error) {
3512
3570
  return { error: String(error), status: 400 };
3513
3571
  }
3514
- let selectedModel;
3515
- try {
3516
- const reference = input.model ?? await models.defaultReference();
3517
- const selected = await models.resolve(reference);
3518
- if (files.some((file) => file.kind === "image") && !selected.input.includes("image"))
3519
- return {
3520
- error: "This model cannot read images. Choose a vision model in Settings.",
3521
- status: 400
3522
- };
3523
- selectedModel = models.identity(selected.provider, selected.id);
3524
- } catch {
3525
- return {
3526
- error: "The default model is unavailable. Choose an available model in Settings.",
3527
- status: 400
3528
- };
3529
- }
3572
+ const selection = await selectSessionModel(
3573
+ input,
3574
+ files.some((file) => file.kind === "image")
3575
+ );
3576
+ if (selection.error !== void 0) return { error: selection.error, status: selection.status };
3577
+ const selectedModel = selection.model;
3530
3578
  const now = (/* @__PURE__ */ new Date()).toISOString();
3531
3579
  const task = input.taskId ? await Effect14.runPromise(store.getTask(input.taskId)) : void 0;
3532
- if (input.taskId && (!task || task.deletedAt || task.projectId !== input.projectId))
3580
+ if (input.taskId && sessionTaskUnavailable(task, input.projectId))
3533
3581
  return { error: "Task not found.", status: 404 };
3534
3582
  const session = SessionSchema.parse({
3535
3583
  id: randomUUID7(),
@@ -3544,18 +3592,9 @@ async function createAndStartSession(input) {
3544
3592
  createdAt: now,
3545
3593
  updatedAt: now
3546
3594
  });
3547
- const worktreePath = path7.join(worktreesRoot, session.id);
3548
- const branch = `agentrific/${session.id.slice(0, 8)}`;
3549
- const useProjectCheckout = input.gitFlow?.workspace === "checkout";
3550
- const workspacePath = useProjectCheckout ? projectPath : worktreePath;
3551
- if (useProjectCheckout) {
3552
- const serverPath = await realpath2(process.cwd());
3553
- if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
3554
- return {
3555
- error: "The server runs from this project. Choose a worktree for this task.",
3556
- status: 409
3557
- };
3558
- }
3595
+ const workspace = await resolveSessionWorkspace(session.id, input.gitFlow, projectPath);
3596
+ if (workspace.error !== void 0) return { error: workspace.error, status: workspace.status };
3597
+ const { branch, workspacePath } = workspace;
3559
3598
  const initial = { ...session, branch, workspacePath };
3560
3599
  publish({ type: "session.updated", session: initial });
3561
3600
  let running;