@agentrific/server 0.1.0 → 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/CHANGELOG.md +25 -0
- package/README.md +39 -5
- package/cli.js +534 -364
- package/cli.js.map +3 -3
- package/index.js +236 -185
- package/index.js.map +2 -2
- package/package.json +14 -2
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
|
|
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,8 +914,20 @@ 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.
|
|
917
|
+
version: "0.1.4",
|
|
914
918
|
description: "Self-hosted control plane for long-running coding agents",
|
|
919
|
+
keywords: [
|
|
920
|
+
"agentrific",
|
|
921
|
+
"ai",
|
|
922
|
+
"android",
|
|
923
|
+
"cli",
|
|
924
|
+
"coding-agents",
|
|
925
|
+
"ios",
|
|
926
|
+
"pi",
|
|
927
|
+
"remote-control",
|
|
928
|
+
"self-hosted"
|
|
929
|
+
],
|
|
930
|
+
homepage: "https://agentrific.com",
|
|
915
931
|
repository: {
|
|
916
932
|
type: "git",
|
|
917
933
|
url: "git+https://github.com/devular/agentrific.git",
|
|
@@ -931,7 +947,7 @@ var package_default = {
|
|
|
931
947
|
cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
|
|
932
948
|
},
|
|
933
949
|
dependencies: {
|
|
934
|
-
"@earendil-works/pi-coding-agent": "
|
|
950
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
935
951
|
"@fastify/cors": "^11.3.0",
|
|
936
952
|
"@fastify/multipart": "^10.1.2",
|
|
937
953
|
"@inquirer/select": "^5.2.5",
|
|
@@ -1157,6 +1173,9 @@ function taskStatusForSession(status, session) {
|
|
|
1157
1173
|
return "needs_user_input";
|
|
1158
1174
|
}
|
|
1159
1175
|
}
|
|
1176
|
+
function pullRequestUrl(session) {
|
|
1177
|
+
return session.gitDelivery?.status === "pr_opened" ? session.gitDelivery.url : void 0;
|
|
1178
|
+
}
|
|
1160
1179
|
function sessionTask(session) {
|
|
1161
1180
|
return TaskSchema.parse({
|
|
1162
1181
|
id: `session-${session.id}`,
|
|
@@ -1167,6 +1186,7 @@ function sessionTask(session) {
|
|
|
1167
1186
|
gitFlow: session.gitFlow,
|
|
1168
1187
|
status: taskStatusForSession(session.status, session),
|
|
1169
1188
|
sessionId: session.id,
|
|
1189
|
+
pullRequestUrl: pullRequestUrl(session),
|
|
1170
1190
|
createdAt: session.createdAt,
|
|
1171
1191
|
updatedAt: session.updatedAt
|
|
1172
1192
|
});
|
|
@@ -1178,12 +1198,13 @@ async function boardTasks(store2, projectId) {
|
|
|
1178
1198
|
tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
|
|
1179
1199
|
);
|
|
1180
1200
|
const linkedTasks = tasks.filter((task) => !task.deletedAt).map((task) => {
|
|
1181
|
-
const session = sessions.find((item) => item.
|
|
1182
|
-
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 };
|
|
1183
1203
|
linkedSessionIds.add(session.id);
|
|
1184
1204
|
return {
|
|
1185
1205
|
...task,
|
|
1186
1206
|
sessionId: session.id,
|
|
1207
|
+
pullRequestUrl: pullRequestUrl(session),
|
|
1187
1208
|
status: taskStatusForSession(session.status, session)
|
|
1188
1209
|
};
|
|
1189
1210
|
});
|
|
@@ -1201,7 +1222,9 @@ async function findTask(store2, id) {
|
|
|
1201
1222
|
function editTask(store2, id, input, resolveAttachments) {
|
|
1202
1223
|
return withTaskLock(id, async () => {
|
|
1203
1224
|
const task = await findTask(store2, id);
|
|
1204
|
-
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;
|
|
1205
1228
|
let files = task.attachments;
|
|
1206
1229
|
if (attachmentIds) {
|
|
1207
1230
|
if (!resolveAttachments)
|
|
@@ -1212,6 +1235,7 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1212
1235
|
store2.saveTask({
|
|
1213
1236
|
...task,
|
|
1214
1237
|
...updates2,
|
|
1238
|
+
...configuration,
|
|
1215
1239
|
attachments: files,
|
|
1216
1240
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1217
1241
|
})
|
|
@@ -1455,7 +1479,7 @@ var AgentController = class {
|
|
|
1455
1479
|
Effect7.gen({ self: this }, function* () {
|
|
1456
1480
|
if (this.closed)
|
|
1457
1481
|
return yield* Effect7.fail(new AgentError({ message: "Server is shutting down" }));
|
|
1458
|
-
if (!session.branch)
|
|
1482
|
+
if (!session.branch && (session.gitFlow || !session.workspacePath))
|
|
1459
1483
|
return yield* Effect7.fail(new AgentError({ message: "Session checkout is not ready" }));
|
|
1460
1484
|
if (this.hasMessage(session.id, messageId)) return;
|
|
1461
1485
|
const entry = yield* this.open(session);
|
|
@@ -1762,45 +1786,7 @@ ${error ? workflowError(error instanceof Error ? error : String(error)) : messag
|
|
|
1762
1786
|
if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
|
|
1763
1787
|
throw new Error("Restore the assigned run branch before requesting delivery.");
|
|
1764
1788
|
if (dirty) {
|
|
1765
|
-
await this.
|
|
1766
|
-
await this.stage(job, "drafting");
|
|
1767
|
-
const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
|
|
1768
|
-
const files = await this.git(
|
|
1769
|
-
session,
|
|
1770
|
-
["ls-files", "--others", "--exclude-standard"],
|
|
1771
|
-
signal
|
|
1772
|
-
);
|
|
1773
|
-
if (this.options.draft) {
|
|
1774
|
-
try {
|
|
1775
|
-
const draft = await this.options.draft(
|
|
1776
|
-
session,
|
|
1777
|
-
input,
|
|
1778
|
-
`${diff}
|
|
1779
|
-
New files:
|
|
1780
|
-
${files}`.slice(0, 4e4),
|
|
1781
|
-
signal
|
|
1782
|
-
);
|
|
1783
|
-
job.title = draft.title;
|
|
1784
|
-
job.body = draft.body;
|
|
1785
|
-
} catch (error) {
|
|
1786
|
-
job.logs = workflowLog(
|
|
1787
|
-
`${job.logs}
|
|
1788
|
-
Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
|
|
1789
|
-
);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
await this.stage(job, "committing");
|
|
1793
|
-
if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
|
|
1794
|
-
throw new Error("The workspace changed branches while preparing delivery.");
|
|
1795
|
-
const output = await this.git(
|
|
1796
|
-
session,
|
|
1797
|
-
["commit", "-m", `${job.title}
|
|
1798
|
-
|
|
1799
|
-
Agentrific run ${id}.`],
|
|
1800
|
-
signal
|
|
1801
|
-
);
|
|
1802
|
-
job.logs = workflowLog(`${job.logs}
|
|
1803
|
-
${output}`);
|
|
1789
|
+
await this.commitChanges(session, job, input, signal);
|
|
1804
1790
|
} else {
|
|
1805
1791
|
await this.git(session, ["hook", "run", "--ignore-missing", "pre-commit"], signal);
|
|
1806
1792
|
}
|
|
@@ -1827,6 +1813,44 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
1827
1813
|
}
|
|
1828
1814
|
});
|
|
1829
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
|
+
}
|
|
1830
1854
|
fallbackBody(id, input) {
|
|
1831
1855
|
return `${input.summary}
|
|
1832
1856
|
|
|
@@ -1899,6 +1923,31 @@ Agentrific run ${id}.`;
|
|
|
1899
1923
|
);
|
|
1900
1924
|
}
|
|
1901
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
|
+
}
|
|
1902
1951
|
async integrate(id) {
|
|
1903
1952
|
const job = await this.job(id);
|
|
1904
1953
|
if (!job?.commitSha) return;
|
|
@@ -1912,29 +1961,7 @@ Agentrific run ${id}.`;
|
|
|
1912
1961
|
"The workspace changed after requesting delivery. Call deliver_changes again for the new changes."
|
|
1913
1962
|
);
|
|
1914
1963
|
if (session.gitFlow.delivery === "merge") {
|
|
1915
|
-
await this.
|
|
1916
|
-
if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
|
|
1917
|
-
await this.command(projectPath, "git", ["switch", "main"]);
|
|
1918
|
-
if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
|
|
1919
|
-
throw new Error("Switch the project checkout to main, then retry delivery.");
|
|
1920
|
-
if (await this.command(projectPath, "git", ["status", "--porcelain"]))
|
|
1921
|
-
throw new Error(
|
|
1922
|
-
"Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
|
|
1923
|
-
);
|
|
1924
|
-
const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
|
|
1925
|
-
if (!included) {
|
|
1926
|
-
try {
|
|
1927
|
-
await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
|
|
1928
|
-
} catch (error) {
|
|
1929
|
-
await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
|
|
1930
|
-
throw new Error(
|
|
1931
|
-
`Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
|
|
1932
|
-
${workflowError(error instanceof Error ? error : String(error))}`
|
|
1933
|
-
);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
job.result = "merged";
|
|
1937
|
-
job.message = "Committed changes are on main.";
|
|
1964
|
+
await this.mergeChanges(session, job, projectPath, commit);
|
|
1938
1965
|
} else {
|
|
1939
1966
|
await this.stage(job, "pushing");
|
|
1940
1967
|
await this.git(session, ["push", "origin", `${commit}:refs/heads/${session.branch}`]);
|
|
@@ -2012,6 +2039,7 @@ var PreparationCoordinator = class {
|
|
|
2012
2039
|
return [...this.preparing.values()].map((id) => `preparation:${id}`);
|
|
2013
2040
|
}
|
|
2014
2041
|
async prepare(session, projectPath, input) {
|
|
2042
|
+
if (!session.gitFlow) return { ...session, branch: null, workspacePath: projectPath };
|
|
2015
2043
|
const preparation2 = Symbol(session.id);
|
|
2016
2044
|
this.preparing.set(preparation2, session.id);
|
|
2017
2045
|
return withTaskLock(`prepare:${session.id}`, async () => {
|
|
@@ -2044,56 +2072,7 @@ ${logs}` : ""}`);
|
|
|
2044
2072
|
try {
|
|
2045
2073
|
await record("creating", "Creating the run workspace\u2026");
|
|
2046
2074
|
const workspace = session.workspacePath;
|
|
2047
|
-
|
|
2048
|
-
const branchAtWorkspace = await command(workspace, "git", [
|
|
2049
|
-
"branch",
|
|
2050
|
-
"--show-current"
|
|
2051
|
-
]).catch(() => void 0);
|
|
2052
|
-
if (branchAtWorkspace !== session.branch) {
|
|
2053
|
-
if (checkout) {
|
|
2054
|
-
if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
|
|
2055
|
-
throw new Error(
|
|
2056
|
-
"The project checkout must be clean and on main before starting a checkout run."
|
|
2057
|
-
);
|
|
2058
|
-
await command(
|
|
2059
|
-
projectPath,
|
|
2060
|
-
"git",
|
|
2061
|
-
["switch", "-c", session.branch, "main"],
|
|
2062
|
-
void 0,
|
|
2063
|
-
true
|
|
2064
|
-
);
|
|
2065
|
-
} else {
|
|
2066
|
-
if (branchAtWorkspace !== void 0)
|
|
2067
|
-
throw new Error("The run workspace already exists on a different branch.");
|
|
2068
|
-
await mkdir3(path4.dirname(workspace), { recursive: true });
|
|
2069
|
-
try {
|
|
2070
|
-
await command(
|
|
2071
|
-
projectPath,
|
|
2072
|
-
"git",
|
|
2073
|
-
[
|
|
2074
|
-
"worktree",
|
|
2075
|
-
"add",
|
|
2076
|
-
"-b",
|
|
2077
|
-
session.branch,
|
|
2078
|
-
workspace,
|
|
2079
|
-
session.gitFlow ? "main" : "HEAD"
|
|
2080
|
-
],
|
|
2081
|
-
void 0,
|
|
2082
|
-
true
|
|
2083
|
-
);
|
|
2084
|
-
} catch (error) {
|
|
2085
|
-
if (await command(workspace, "git", ["branch", "--show-current"]).catch(
|
|
2086
|
-
() => void 0
|
|
2087
|
-
) !== session.branch)
|
|
2088
|
-
throw error;
|
|
2089
|
-
await record(
|
|
2090
|
-
"creating",
|
|
2091
|
-
"Workspace created; a checkout hook failed. Preparing dependencies\u2026",
|
|
2092
|
-
workflowError(error instanceof Error ? error : String(error))
|
|
2093
|
-
);
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2075
|
+
await this.createWorkspace(session, projectPath, command, record);
|
|
2097
2076
|
await record("installing", "Installing run dependencies\u2026");
|
|
2098
2077
|
await this.install(workspace, command);
|
|
2099
2078
|
await record("ready", "Workspace ready.");
|
|
@@ -2109,6 +2088,57 @@ ${logs}` : ""}`);
|
|
|
2109
2088
|
}
|
|
2110
2089
|
}).finally(() => this.preparing.delete(preparation2));
|
|
2111
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
|
+
}
|
|
2112
2142
|
async install(workspace, command) {
|
|
2113
2143
|
const packageFile = await readFile3(path4.join(workspace, "package.json"), "utf8").catch(
|
|
2114
2144
|
() => void 0
|
|
@@ -2198,10 +2228,10 @@ ${logs}` : ""}`);
|
|
|
2198
2228
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2199
2229
|
import { z as z9 } from "zod";
|
|
2200
2230
|
var DraftSchema = z9.object({ title: z9.string().trim().min(1).max(120), body: z9.string().trim().min(1).max(1e4) }).strict();
|
|
2201
|
-
function deliveryWriter(models2) {
|
|
2231
|
+
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2202
2232
|
return async (run, input, diff, signal) => {
|
|
2203
2233
|
signal?.throwIfAborted();
|
|
2204
|
-
const { session } = await
|
|
2234
|
+
const { session } = await createSession({
|
|
2205
2235
|
cwd: run.workspacePath,
|
|
2206
2236
|
model: await models2.resolve(run.model ?? await models2.defaultReference()),
|
|
2207
2237
|
modelRuntime: models2.runtime,
|
|
@@ -2404,11 +2434,11 @@ function toolOutput(value) {
|
|
|
2404
2434
|
const text = parsed.success ? parsed.data.content?.filter((part) => part.type === "text").map((part) => part.text || "").join("\n") || "Completed without text output." : boundedJson(value, 4e3);
|
|
2405
2435
|
return text.length > 4e3 ? `${text.slice(0, 4e3)}\u2026 [truncated]` : text;
|
|
2406
2436
|
}
|
|
2407
|
-
function createPiFactory(files, models2, delivery2) {
|
|
2437
|
+
function createPiFactory(files, models2, delivery2, createSession = createAgentSession2) {
|
|
2408
2438
|
return {
|
|
2409
2439
|
async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
|
|
2410
2440
|
const model = await models2.resolve(selected);
|
|
2411
|
-
const { session } = await
|
|
2441
|
+
const { session } = await createSession({
|
|
2412
2442
|
cwd,
|
|
2413
2443
|
model,
|
|
2414
2444
|
modelRuntime: models2.runtime,
|
|
@@ -2833,31 +2863,7 @@ var SqliteSessionStore = class {
|
|
|
2833
2863
|
this.db.prepare("DELETE FROM projects WHERE id = ?").run(replacedId);
|
|
2834
2864
|
}
|
|
2835
2865
|
if (previousPath && previousPath !== project.path) {
|
|
2836
|
-
this.
|
|
2837
|
-
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
2838
|
-
).run(project.path, previousPath);
|
|
2839
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
2840
|
-
for (const row of this.db.prepare(
|
|
2841
|
-
"SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?"
|
|
2842
|
-
).all(project.id)) {
|
|
2843
|
-
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
2844
|
-
if (session.workspacePath) {
|
|
2845
|
-
session.workspacePath = String(relocate(session.workspacePath));
|
|
2846
|
-
this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
for (const row of this.db.prepare(
|
|
2850
|
-
"SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
|
|
2851
|
-
).all(project.id)) {
|
|
2852
|
-
const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
|
|
2853
|
-
if (event.workspacePath) {
|
|
2854
|
-
event.workspacePath = String(relocate(event.workspacePath));
|
|
2855
|
-
this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(
|
|
2856
|
-
JSON.stringify({ type: "session.updated", session: event }),
|
|
2857
|
-
Number(row.sequence)
|
|
2858
|
-
);
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2866
|
+
this.relocateProjectHistory(project, previousPath);
|
|
2861
2867
|
}
|
|
2862
2868
|
this.db.prepare(
|
|
2863
2869
|
"INSERT INTO projects(id, payload) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
|
|
@@ -2870,6 +2876,28 @@ var SqliteSessionStore = class {
|
|
|
2870
2876
|
}
|
|
2871
2877
|
});
|
|
2872
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
|
+
}
|
|
2873
2901
|
defaultModel() {
|
|
2874
2902
|
return this.query(() => {
|
|
2875
2903
|
const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'default-model'").get();
|
|
@@ -3474,16 +3502,27 @@ app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
|
3474
3502
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3475
3503
|
return Effect14.runPromise(store.history(session.id, query.data.after, query.data.limit));
|
|
3476
3504
|
});
|
|
3477
|
-
async function
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
const
|
|
3481
|
-
if (!
|
|
3482
|
-
return {
|
|
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
|
+
};
|
|
3483
3520
|
}
|
|
3521
|
+
}
|
|
3522
|
+
async function resolveSessionProject(projectId) {
|
|
3484
3523
|
const projectsRoot = await Effect14.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3485
3524
|
const project = (await Effect14.runPromise(discoverProjects(projectsRoot, store))).find(
|
|
3486
|
-
(item) => item.id ===
|
|
3525
|
+
(item) => item.id === projectId && item.available
|
|
3487
3526
|
);
|
|
3488
3527
|
if (!project)
|
|
3489
3528
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
@@ -3491,6 +3530,37 @@ async function createAndStartSession(input) {
|
|
|
3491
3530
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
3492
3531
|
if (path7.dirname(projectPath) !== resolvedRoot)
|
|
3493
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;
|
|
3494
3564
|
let files;
|
|
3495
3565
|
try {
|
|
3496
3566
|
files = await Effect14.runPromise(
|
|
@@ -3499,25 +3569,15 @@ async function createAndStartSession(input) {
|
|
|
3499
3569
|
} catch (error) {
|
|
3500
3570
|
return { error: String(error), status: 400 };
|
|
3501
3571
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
error: "This model cannot read images. Choose a vision model in Settings.",
|
|
3509
|
-
status: 400
|
|
3510
|
-
};
|
|
3511
|
-
selectedModel = models.identity(selected.provider, selected.id);
|
|
3512
|
-
} catch {
|
|
3513
|
-
return {
|
|
3514
|
-
error: "The default model is unavailable. Choose an available model in Settings.",
|
|
3515
|
-
status: 400
|
|
3516
|
-
};
|
|
3517
|
-
}
|
|
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;
|
|
3518
3578
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3519
3579
|
const task = input.taskId ? await Effect14.runPromise(store.getTask(input.taskId)) : void 0;
|
|
3520
|
-
if (input.taskId && (
|
|
3580
|
+
if (input.taskId && sessionTaskUnavailable(task, input.projectId))
|
|
3521
3581
|
return { error: "Task not found.", status: 404 };
|
|
3522
3582
|
const session = SessionSchema.parse({
|
|
3523
3583
|
id: randomUUID7(),
|
|
@@ -3532,18 +3592,9 @@ async function createAndStartSession(input) {
|
|
|
3532
3592
|
createdAt: now,
|
|
3533
3593
|
updatedAt: now
|
|
3534
3594
|
});
|
|
3535
|
-
const
|
|
3536
|
-
|
|
3537
|
-
const
|
|
3538
|
-
const workspacePath = useProjectCheckout ? projectPath : worktreePath;
|
|
3539
|
-
if (useProjectCheckout) {
|
|
3540
|
-
const serverPath = await realpath2(process.cwd());
|
|
3541
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
|
|
3542
|
-
return {
|
|
3543
|
-
error: "The server runs from this project. Choose a worktree for this task.",
|
|
3544
|
-
status: 409
|
|
3545
|
-
};
|
|
3546
|
-
}
|
|
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;
|
|
3547
3598
|
const initial = { ...session, branch, workspacePath };
|
|
3548
3599
|
publish({ type: "session.updated", session: initial });
|
|
3549
3600
|
let running;
|