@amistio/cli 0.1.6 → 0.1.8
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 +30 -0
- package/dist/index.js +818 -148
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createHash as
|
|
5
|
-
import { writeFile as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
import { createHash as createHash6, randomUUID } from "node:crypto";
|
|
5
|
+
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
6
|
+
import os7 from "node:os";
|
|
7
|
+
import path13 from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
// ../shared/src/schemas.ts
|
|
@@ -141,6 +141,21 @@ var runnerToolCapabilitySchema = z.object({
|
|
|
141
141
|
supportsBranchIsolation: z.boolean().optional(),
|
|
142
142
|
supportsGitWorktreeIsolation: z.boolean().optional()
|
|
143
143
|
});
|
|
144
|
+
var runnerResourceUsageSchema = z.object({
|
|
145
|
+
sampledAt: isoDateTimeSchema,
|
|
146
|
+
processUptimeSeconds: z.number().nonnegative().optional(),
|
|
147
|
+
processMemoryRssBytes: z.number().int().nonnegative().optional(),
|
|
148
|
+
processMemoryHeapUsedBytes: z.number().int().nonnegative().optional(),
|
|
149
|
+
processMemoryHeapTotalBytes: z.number().int().nonnegative().optional(),
|
|
150
|
+
processCpuUserMicros: z.number().int().nonnegative().optional(),
|
|
151
|
+
processCpuSystemMicros: z.number().int().nonnegative().optional(),
|
|
152
|
+
processCpuPercent: z.number().nonnegative().optional(),
|
|
153
|
+
systemMemoryTotalBytes: z.number().int().nonnegative().optional(),
|
|
154
|
+
systemMemoryFreeBytes: z.number().int().nonnegative().optional(),
|
|
155
|
+
systemLoadAverage1m: z.number().nonnegative().optional(),
|
|
156
|
+
systemLoadAverage5m: z.number().nonnegative().optional(),
|
|
157
|
+
systemLoadAverage15m: z.number().nonnegative().optional()
|
|
158
|
+
});
|
|
144
159
|
var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
|
|
145
160
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
146
161
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
@@ -341,6 +356,7 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
341
356
|
preferenceSource: runnerPreferenceSourceSchema.optional(),
|
|
342
357
|
preferenceStatus: runnerPreferenceStatusSchema.optional(),
|
|
343
358
|
preferenceMessage: z.string().optional(),
|
|
359
|
+
resourceUsage: runnerResourceUsageSchema.optional(),
|
|
344
360
|
lastSeenAt: isoDateTimeSchema
|
|
345
361
|
});
|
|
346
362
|
var runnerSettingsItemSchema = baseItemSchema.extend({
|
|
@@ -394,6 +410,7 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
|
394
410
|
projectId: z.string().min(1),
|
|
395
411
|
runnerCredentialId: z.string().min(1),
|
|
396
412
|
repositoryLinkId: z.string().min(1),
|
|
413
|
+
runnerId: z.string().min(1).optional(),
|
|
397
414
|
pairedByUserId: z.string().min(1).optional(),
|
|
398
415
|
machineId: z.string().min(1).optional(),
|
|
399
416
|
tokenHash: z.string().min(32),
|
|
@@ -703,6 +720,8 @@ var pairingSessionItemSchema = baseItemSchema.extend({
|
|
|
703
720
|
projectId: z.string().min(1),
|
|
704
721
|
createdByUserId: z.string().min(1),
|
|
705
722
|
expiresAt: isoDateTimeSchema,
|
|
723
|
+
failedAttemptCount: z.number().int().nonnegative().optional(),
|
|
724
|
+
lastFailedAttemptAt: isoDateTimeSchema.optional(),
|
|
706
725
|
status: z.enum(["pending", "confirmed", "expired", "revoked"])
|
|
707
726
|
});
|
|
708
727
|
var projectItemUnionSchema = z.discriminatedUnion("type", [
|
|
@@ -1231,8 +1250,11 @@ function credentialKey(accountId, projectId, repositoryLinkId) {
|
|
|
1231
1250
|
}
|
|
1232
1251
|
|
|
1233
1252
|
// src/control-plane.ts
|
|
1253
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1234
1254
|
import { mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1235
1255
|
import path3 from "node:path";
|
|
1256
|
+
import { promisify as promisify2 } from "node:util";
|
|
1257
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1236
1258
|
var controlPlaneFolders = [
|
|
1237
1259
|
path3.join("docs", "architecture"),
|
|
1238
1260
|
path3.join("docs", "context"),
|
|
@@ -1279,6 +1301,17 @@ async function writeProjectLink(rootDir, metadata) {
|
|
|
1279
1301
|
await writeFile2(filePath, createProjectLinkMarkdown(metadata), "utf8");
|
|
1280
1302
|
return filePath;
|
|
1281
1303
|
}
|
|
1304
|
+
async function resolvePairingRoot(rootDir, options = {}) {
|
|
1305
|
+
const requestedRoot = path3.resolve(rootDir);
|
|
1306
|
+
const gitRoot = await readGitTopLevel(requestedRoot).catch(() => void 0);
|
|
1307
|
+
if (gitRoot) {
|
|
1308
|
+
return gitRoot;
|
|
1309
|
+
}
|
|
1310
|
+
if (options.explicitRoot) {
|
|
1311
|
+
return requestedRoot;
|
|
1312
|
+
}
|
|
1313
|
+
throw new Error("Run `amistio pair` from inside the repository checkout, or pass --root <repo-root> explicitly.");
|
|
1314
|
+
}
|
|
1282
1315
|
async function readProjectLink(rootDir) {
|
|
1283
1316
|
const candidatePaths = [
|
|
1284
1317
|
path3.join(rootDir, "docs", "context", "amistio-project.md"),
|
|
@@ -1327,6 +1360,14 @@ async function exists(filePath) {
|
|
|
1327
1360
|
return false;
|
|
1328
1361
|
}
|
|
1329
1362
|
}
|
|
1363
|
+
async function readGitTopLevel(rootDir) {
|
|
1364
|
+
const { stdout } = await execFileAsync2("git", ["-C", rootDir, "rev-parse", "--show-toplevel"], { maxBuffer: 1024 * 1024 });
|
|
1365
|
+
const gitRoot = stdout.trim();
|
|
1366
|
+
if (!gitRoot) {
|
|
1367
|
+
throw new Error("Git top-level path was empty.");
|
|
1368
|
+
}
|
|
1369
|
+
return path3.resolve(gitRoot);
|
|
1370
|
+
}
|
|
1330
1371
|
|
|
1331
1372
|
// src/api-client.ts
|
|
1332
1373
|
import { z as z3 } from "zod";
|
|
@@ -1617,8 +1658,8 @@ var toolSessionMutationSchema = z3.object({
|
|
|
1617
1658
|
});
|
|
1618
1659
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
1619
1660
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
1620
|
-
const
|
|
1621
|
-
return new URL(`${base}${
|
|
1661
|
+
const path14 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1662
|
+
return new URL(`${base}${path14}`);
|
|
1622
1663
|
}
|
|
1623
1664
|
|
|
1624
1665
|
// src/orchestrator.ts
|
|
@@ -1827,7 +1868,7 @@ async function runLocalTool(options) {
|
|
|
1827
1868
|
promptFilePath,
|
|
1828
1869
|
streamOutput: Boolean(options.streamOutput),
|
|
1829
1870
|
...options.session ? { session: options.session } : {}
|
|
1830
|
-
});
|
|
1871
|
+
}, options.timeoutMs);
|
|
1831
1872
|
return {
|
|
1832
1873
|
toolName: runner2.toolName,
|
|
1833
1874
|
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
@@ -1912,16 +1953,16 @@ async function createToolRunner(options) {
|
|
|
1912
1953
|
}
|
|
1913
1954
|
throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
|
|
1914
1955
|
}
|
|
1915
|
-
async function executeToolRunner(runner2, input) {
|
|
1956
|
+
async function executeToolRunner(runner2, input, timeoutMs) {
|
|
1916
1957
|
if (runner2.kind === "command") {
|
|
1917
|
-
return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput);
|
|
1958
|
+
return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput, timeoutMs);
|
|
1918
1959
|
}
|
|
1919
1960
|
try {
|
|
1920
|
-
return await runner2.adapter.runWithSdk(input);
|
|
1961
|
+
return await withTimeout(runner2.adapter.runWithSdk(input), timeoutMs, runner2.displayCommand);
|
|
1921
1962
|
} catch (error) {
|
|
1922
1963
|
if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
|
|
1923
1964
|
const fallback = runner2.adapter.buildInvocation(input);
|
|
1924
|
-
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
|
|
1965
|
+
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput, timeoutMs);
|
|
1925
1966
|
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
|
|
1926
1967
|
return {
|
|
1927
1968
|
...result,
|
|
@@ -2010,7 +2051,7 @@ async function commandExists(command) {
|
|
|
2010
2051
|
lookup.on("close", (exitCode) => resolve(exitCode === 0));
|
|
2011
2052
|
});
|
|
2012
2053
|
}
|
|
2013
|
-
async function executeToolInvocation(invocation, rootDir, streamOutput) {
|
|
2054
|
+
async function executeToolInvocation(invocation, rootDir, streamOutput, timeoutMs) {
|
|
2014
2055
|
return new Promise((resolve, reject) => {
|
|
2015
2056
|
const child = spawn(invocation.command, invocation.args, {
|
|
2016
2057
|
cwd: rootDir,
|
|
@@ -2020,7 +2061,33 @@ async function executeToolInvocation(invocation, rootDir, streamOutput) {
|
|
|
2020
2061
|
});
|
|
2021
2062
|
let stdout = "";
|
|
2022
2063
|
let stderr = "";
|
|
2023
|
-
|
|
2064
|
+
let settled = false;
|
|
2065
|
+
let forceKillTimer;
|
|
2066
|
+
const timeout = timeoutMs && timeoutMs > 0 ? setTimeout(() => {
|
|
2067
|
+
if (settled) return;
|
|
2068
|
+
stderr += `${toolTimeoutMessage(invocation.displayCommand, timeoutMs)}
|
|
2069
|
+
`;
|
|
2070
|
+
child.kill("SIGTERM");
|
|
2071
|
+
forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
2072
|
+
forceKillTimer.unref?.();
|
|
2073
|
+
rejectOnce(new Error(toolTimeoutMessage(invocation.displayCommand, timeoutMs)));
|
|
2074
|
+
}, timeoutMs) : void 0;
|
|
2075
|
+
timeout?.unref?.();
|
|
2076
|
+
const resolveOnce = (value) => {
|
|
2077
|
+
if (settled) return;
|
|
2078
|
+
settled = true;
|
|
2079
|
+
if (timeout) clearTimeout(timeout);
|
|
2080
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2081
|
+
resolve(value);
|
|
2082
|
+
};
|
|
2083
|
+
const rejectOnce = (error) => {
|
|
2084
|
+
if (settled) return;
|
|
2085
|
+
settled = true;
|
|
2086
|
+
if (timeout) clearTimeout(timeout);
|
|
2087
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2088
|
+
reject(error);
|
|
2089
|
+
};
|
|
2090
|
+
child.on("error", rejectOnce);
|
|
2024
2091
|
child.stdout.setEncoding("utf8");
|
|
2025
2092
|
child.stderr.setEncoding("utf8");
|
|
2026
2093
|
child.stdout.on("data", (chunk) => {
|
|
@@ -2041,10 +2108,40 @@ async function executeToolInvocation(invocation, rootDir, streamOutput) {
|
|
|
2041
2108
|
}
|
|
2042
2109
|
child.stdin.end();
|
|
2043
2110
|
child.on("close", (exitCode) => {
|
|
2044
|
-
|
|
2111
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2112
|
+
resolveOnce({ exitCode: exitCode ?? 1, stdout, stderr });
|
|
2045
2113
|
});
|
|
2046
2114
|
});
|
|
2047
2115
|
}
|
|
2116
|
+
async function withTimeout(promise, timeoutMs, displayCommand) {
|
|
2117
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
2118
|
+
return promise;
|
|
2119
|
+
}
|
|
2120
|
+
return new Promise((resolve, reject) => {
|
|
2121
|
+
const timeout = setTimeout(() => reject(new Error(toolTimeoutMessage(displayCommand, timeoutMs))), timeoutMs);
|
|
2122
|
+
timeout.unref?.();
|
|
2123
|
+
promise.then(
|
|
2124
|
+
(value) => {
|
|
2125
|
+
clearTimeout(timeout);
|
|
2126
|
+
resolve(value);
|
|
2127
|
+
},
|
|
2128
|
+
(error) => {
|
|
2129
|
+
clearTimeout(timeout);
|
|
2130
|
+
reject(error);
|
|
2131
|
+
}
|
|
2132
|
+
);
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
function toolTimeoutMessage(displayCommand, timeoutMs) {
|
|
2136
|
+
return `Local tool timed out after ${formatTimeoutDuration(timeoutMs)}: ${displayCommand}`;
|
|
2137
|
+
}
|
|
2138
|
+
function formatTimeoutDuration(timeoutMs) {
|
|
2139
|
+
if (timeoutMs < 1e3) {
|
|
2140
|
+
return `${timeoutMs}ms`;
|
|
2141
|
+
}
|
|
2142
|
+
const seconds = timeoutMs / 1e3;
|
|
2143
|
+
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
|
|
2144
|
+
}
|
|
2048
2145
|
async function runOpencodeSdk(input) {
|
|
2049
2146
|
const { createOpencode } = await import("@opencode-ai/sdk");
|
|
2050
2147
|
const previousDirectory = process.cwd();
|
|
@@ -2132,7 +2229,7 @@ async function runCodexSdk(input) {
|
|
|
2132
2229
|
return { exitCode: 0, stdout: result.finalResponse, stderr: "" };
|
|
2133
2230
|
}
|
|
2134
2231
|
async function runCopilotSdk(input) {
|
|
2135
|
-
const { CopilotClient
|
|
2232
|
+
const { CopilotClient } = await import("@github/copilot-sdk");
|
|
2136
2233
|
const client = new CopilotClient({
|
|
2137
2234
|
cwd: input.rootDir,
|
|
2138
2235
|
logLevel: "error"
|
|
@@ -2145,7 +2242,7 @@ async function runCopilotSdk(input) {
|
|
|
2145
2242
|
workingDirectory: input.rootDir,
|
|
2146
2243
|
enableConfigDiscovery: true,
|
|
2147
2244
|
streaming: input.streamOutput,
|
|
2148
|
-
onPermissionRequest:
|
|
2245
|
+
onPermissionRequest: createCopilotPermissionHandler()
|
|
2149
2246
|
});
|
|
2150
2247
|
try {
|
|
2151
2248
|
let streamedOutput = "";
|
|
@@ -2167,6 +2264,18 @@ async function runCopilotSdk(input) {
|
|
|
2167
2264
|
await client.stop();
|
|
2168
2265
|
}
|
|
2169
2266
|
}
|
|
2267
|
+
function createCopilotPermissionHandler(env = process.env) {
|
|
2268
|
+
const allowAllPermissions = isCopilotApproveAllEnabled(env);
|
|
2269
|
+
return (request) => {
|
|
2270
|
+
if (allowAllPermissions || request.kind === "read") {
|
|
2271
|
+
return { kind: "approve-once" };
|
|
2272
|
+
}
|
|
2273
|
+
return { kind: "reject" };
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
function isCopilotApproveAllEnabled(env = process.env) {
|
|
2277
|
+
return /^(1|true|yes)$/i.test(env.AMISTIO_COPILOT_APPROVE_ALL ?? "");
|
|
2278
|
+
}
|
|
2170
2279
|
function extractTextParts(parts) {
|
|
2171
2280
|
if (!Array.isArray(parts)) {
|
|
2172
2281
|
return "";
|
|
@@ -2364,6 +2473,221 @@ async function readRunnerDaemonMetadataFile(filePath) {
|
|
|
2364
2473
|
}
|
|
2365
2474
|
}
|
|
2366
2475
|
|
|
2476
|
+
// src/runner-service.ts
|
|
2477
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2478
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2479
|
+
import { mkdir as mkdir6, readFile as readFile4, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2480
|
+
import os4 from "node:os";
|
|
2481
|
+
import path7 from "node:path";
|
|
2482
|
+
function detectRunnerServicePlatform(platform = process.platform) {
|
|
2483
|
+
if (platform === "darwin") return "launchd";
|
|
2484
|
+
if (platform === "linux") return "systemd";
|
|
2485
|
+
return "unsupported";
|
|
2486
|
+
}
|
|
2487
|
+
function createRunnerServiceDescriptor(input) {
|
|
2488
|
+
const platform = input.platform ?? detectRunnerServicePlatform();
|
|
2489
|
+
if (platform === "unsupported") {
|
|
2490
|
+
throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
|
|
2491
|
+
}
|
|
2492
|
+
const homeDir = input.homeDir ?? os4.homedir();
|
|
2493
|
+
const serviceName = runnerServiceName(input);
|
|
2494
|
+
const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
|
|
2495
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2496
|
+
const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
|
|
2497
|
+
const logPath = path7.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
|
|
2498
|
+
const metadata = {
|
|
2499
|
+
schemaVersion: 1,
|
|
2500
|
+
accountId: input.accountId,
|
|
2501
|
+
projectId: input.projectId,
|
|
2502
|
+
repositoryLinkId: input.repositoryLinkId,
|
|
2503
|
+
runnerId: input.runnerId,
|
|
2504
|
+
rootDir: path7.resolve(input.rootDir),
|
|
2505
|
+
apiUrl: input.apiUrl,
|
|
2506
|
+
serviceName,
|
|
2507
|
+
serviceFilePath,
|
|
2508
|
+
platform,
|
|
2509
|
+
status: "installed",
|
|
2510
|
+
createdAt: now,
|
|
2511
|
+
updatedAt: now,
|
|
2512
|
+
args: input.args
|
|
2513
|
+
};
|
|
2514
|
+
return {
|
|
2515
|
+
metadata,
|
|
2516
|
+
content: platform === "launchd" ? createLaunchdPlist({ command, label: serviceName, logPath, rootDir: metadata.rootDir }) : createSystemdUnit({ command, description: `Amistio runner ${input.runnerId}`, logPath, rootDir: metadata.rootDir })
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
async function installRunnerService(input, options = {}) {
|
|
2520
|
+
const descriptor = createRunnerServiceDescriptor(input);
|
|
2521
|
+
await mkdir6(path7.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
|
|
2522
|
+
await mkdir6(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
|
|
2523
|
+
await writeFile6(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
|
|
2524
|
+
await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
|
|
2525
|
+
if (options.activate !== false) {
|
|
2526
|
+
const activation = await activateRunnerService(descriptor.metadata);
|
|
2527
|
+
if (!activation.succeeded) {
|
|
2528
|
+
throw new Error(`Startup service file was written to ${descriptor.metadata.serviceFilePath}, but activation failed: ${activation.message}`);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
return descriptor.metadata;
|
|
2532
|
+
}
|
|
2533
|
+
async function removeRunnerService(input) {
|
|
2534
|
+
const metadata = await readRunnerServiceMetadata(input, input.metadataDir);
|
|
2535
|
+
if (!metadata) {
|
|
2536
|
+
return void 0;
|
|
2537
|
+
}
|
|
2538
|
+
await deactivateRunnerService(metadata).catch(() => void 0);
|
|
2539
|
+
await rm2(metadata.serviceFilePath, { force: true });
|
|
2540
|
+
await rm2(runnerServiceMetadataPath(input, input.metadataDir ?? defaultRunnerMetadataDir()), { force: true });
|
|
2541
|
+
return { ...metadata, status: "removed", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2542
|
+
}
|
|
2543
|
+
async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
2544
|
+
try {
|
|
2545
|
+
const parsed = JSON.parse(await readFile4(runnerServiceMetadataPath(input, metadataDir), "utf8"));
|
|
2546
|
+
if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
|
|
2547
|
+
return void 0;
|
|
2548
|
+
}
|
|
2549
|
+
return parsed;
|
|
2550
|
+
} catch {
|
|
2551
|
+
return void 0;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
2555
|
+
await mkdir6(metadataDir, { recursive: true });
|
|
2556
|
+
await writeFile6(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
2557
|
+
}
|
|
2558
|
+
async function runnerServiceRuntimeStatus(metadata) {
|
|
2559
|
+
if (metadata.platform === "launchd") {
|
|
2560
|
+
const target = launchdTarget(metadata);
|
|
2561
|
+
const result2 = await runProcess("launchctl", ["print", target], 5e3);
|
|
2562
|
+
return result2.exitCode === 0 ? "loaded" : "not loaded";
|
|
2563
|
+
}
|
|
2564
|
+
const result = await runProcess("systemctl", ["--user", "is-active", metadata.serviceName], 5e3);
|
|
2565
|
+
return result.exitCode === 0 ? result.output.trim() || "active" : "not active";
|
|
2566
|
+
}
|
|
2567
|
+
async function activateRunnerService(metadata) {
|
|
2568
|
+
if (metadata.platform === "launchd") {
|
|
2569
|
+
await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 5e3).catch(() => void 0);
|
|
2570
|
+
const result = await runProcess("launchctl", ["bootstrap", launchdDomain(), metadata.serviceFilePath], 1e4);
|
|
2571
|
+
return result.exitCode === 0 ? { succeeded: true, message: "launchd service loaded." } : { succeeded: false, message: result.output || `launchctl exited with ${result.exitCode}.` };
|
|
2572
|
+
}
|
|
2573
|
+
const reload = await runProcess("systemctl", ["--user", "daemon-reload"], 1e4);
|
|
2574
|
+
if (reload.exitCode !== 0) {
|
|
2575
|
+
return { succeeded: false, message: reload.output || `systemctl daemon-reload exited with ${reload.exitCode}.` };
|
|
2576
|
+
}
|
|
2577
|
+
const enable = await runProcess("systemctl", ["--user", "enable", "--now", metadata.serviceName], 2e4);
|
|
2578
|
+
return enable.exitCode === 0 ? { succeeded: true, message: "systemd user service enabled." } : { succeeded: false, message: enable.output || `systemctl enable exited with ${enable.exitCode}.` };
|
|
2579
|
+
}
|
|
2580
|
+
async function deactivateRunnerService(metadata) {
|
|
2581
|
+
if (metadata.platform === "launchd") {
|
|
2582
|
+
await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 1e4);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
await runProcess("systemctl", ["--user", "disable", "--now", metadata.serviceName], 2e4);
|
|
2586
|
+
await runProcess("systemctl", ["--user", "daemon-reload"], 1e4).catch(() => void 0);
|
|
2587
|
+
}
|
|
2588
|
+
function createLaunchdPlist(input) {
|
|
2589
|
+
const commandItems = input.command.map((item) => ` <string>${xmlEscape(item)}</string>`).join("\n");
|
|
2590
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2591
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2592
|
+
<plist version="1.0">
|
|
2593
|
+
<dict>
|
|
2594
|
+
<key>Label</key>
|
|
2595
|
+
<string>${xmlEscape(input.label)}</string>
|
|
2596
|
+
<key>ProgramArguments</key>
|
|
2597
|
+
<array>
|
|
2598
|
+
${commandItems}
|
|
2599
|
+
</array>
|
|
2600
|
+
<key>WorkingDirectory</key>
|
|
2601
|
+
<string>${xmlEscape(input.rootDir)}</string>
|
|
2602
|
+
<key>EnvironmentVariables</key>
|
|
2603
|
+
<dict>
|
|
2604
|
+
<key>AMISTIO_RUNNER_MODE</key>
|
|
2605
|
+
<string>background</string>
|
|
2606
|
+
</dict>
|
|
2607
|
+
<key>RunAtLoad</key>
|
|
2608
|
+
<true/>
|
|
2609
|
+
<key>KeepAlive</key>
|
|
2610
|
+
<true/>
|
|
2611
|
+
<key>StandardOutPath</key>
|
|
2612
|
+
<string>${xmlEscape(input.logPath)}</string>
|
|
2613
|
+
<key>StandardErrorPath</key>
|
|
2614
|
+
<string>${xmlEscape(input.logPath)}</string>
|
|
2615
|
+
</dict>
|
|
2616
|
+
</plist>
|
|
2617
|
+
`;
|
|
2618
|
+
}
|
|
2619
|
+
function createSystemdUnit(input) {
|
|
2620
|
+
return `[Unit]
|
|
2621
|
+
Description=${input.description}
|
|
2622
|
+
|
|
2623
|
+
[Service]
|
|
2624
|
+
Type=simple
|
|
2625
|
+
WorkingDirectory=${systemdEscape(input.rootDir)}
|
|
2626
|
+
Environment=AMISTIO_RUNNER_MODE=background
|
|
2627
|
+
ExecStart=${input.command.map(systemdEscape).join(" ")}
|
|
2628
|
+
Restart=always
|
|
2629
|
+
RestartSec=5
|
|
2630
|
+
StandardOutput=append:${systemdEscape(input.logPath)}
|
|
2631
|
+
StandardError=append:${systemdEscape(input.logPath)}
|
|
2632
|
+
|
|
2633
|
+
[Install]
|
|
2634
|
+
WantedBy=default.target
|
|
2635
|
+
`;
|
|
2636
|
+
}
|
|
2637
|
+
function runnerServiceFilePath(platform, serviceName, homeDir) {
|
|
2638
|
+
if (platform === "launchd") {
|
|
2639
|
+
return path7.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
|
|
2640
|
+
}
|
|
2641
|
+
return path7.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
|
|
2642
|
+
}
|
|
2643
|
+
function runnerServiceMetadataPath(input, metadataDir) {
|
|
2644
|
+
return path7.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
|
|
2645
|
+
}
|
|
2646
|
+
function runnerServiceName(input) {
|
|
2647
|
+
return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
|
|
2648
|
+
}
|
|
2649
|
+
function runnerServiceKey(input) {
|
|
2650
|
+
return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
|
|
2651
|
+
}
|
|
2652
|
+
function launchdDomain() {
|
|
2653
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
2654
|
+
return uid === void 0 ? "gui/0" : `gui/${uid}`;
|
|
2655
|
+
}
|
|
2656
|
+
function launchdTarget(metadata) {
|
|
2657
|
+
return `${launchdDomain()}/${metadata.serviceName}`;
|
|
2658
|
+
}
|
|
2659
|
+
function xmlEscape(value) {
|
|
2660
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
2661
|
+
}
|
|
2662
|
+
function systemdEscape(value) {
|
|
2663
|
+
return value.includes(" ") || value.includes(" ") || value.includes('"') ? `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"')}"` : value;
|
|
2664
|
+
}
|
|
2665
|
+
function runProcess(command, args, timeoutMs) {
|
|
2666
|
+
return new Promise((resolve) => {
|
|
2667
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2668
|
+
let output = "";
|
|
2669
|
+
const timeout = setTimeout(() => {
|
|
2670
|
+
output += `Timed out while running ${command}.
|
|
2671
|
+
`;
|
|
2672
|
+
child.kill("SIGTERM");
|
|
2673
|
+
}, timeoutMs);
|
|
2674
|
+
child.stdout?.on("data", (chunk) => {
|
|
2675
|
+
output += chunk.toString("utf8");
|
|
2676
|
+
});
|
|
2677
|
+
child.stderr?.on("data", (chunk) => {
|
|
2678
|
+
output += chunk.toString("utf8");
|
|
2679
|
+
});
|
|
2680
|
+
child.on("error", (error) => {
|
|
2681
|
+
clearTimeout(timeout);
|
|
2682
|
+
resolve({ exitCode: 1, output: error.message });
|
|
2683
|
+
});
|
|
2684
|
+
child.on("close", (code) => {
|
|
2685
|
+
clearTimeout(timeout);
|
|
2686
|
+
resolve({ exitCode: code ?? 1, output: output.trim() });
|
|
2687
|
+
});
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2367
2691
|
// src/session-policy.ts
|
|
2368
2692
|
var maxIdleMs = 24 * 60 * 60 * 1e3;
|
|
2369
2693
|
var maxTotalMs = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -2498,8 +2822,8 @@ function tokens(value) {
|
|
|
2498
2822
|
}
|
|
2499
2823
|
|
|
2500
2824
|
// src/sync.ts
|
|
2501
|
-
import { mkdir as
|
|
2502
|
-
import
|
|
2825
|
+
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile5, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
2826
|
+
import path8 from "node:path";
|
|
2503
2827
|
var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2504
2828
|
var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
|
|
2505
2829
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
@@ -2578,7 +2902,7 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2578
2902
|
const markdownFiles = await findMarkdownFiles(rootDir);
|
|
2579
2903
|
const documents = [];
|
|
2580
2904
|
for (const fullPath of markdownFiles) {
|
|
2581
|
-
const raw = await
|
|
2905
|
+
const raw = await readFile5(fullPath, "utf8");
|
|
2582
2906
|
const parsed = parseSyncedMarkdown(raw);
|
|
2583
2907
|
if (!parsed) {
|
|
2584
2908
|
continue;
|
|
@@ -2628,8 +2952,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
|
2628
2952
|
result.skipped.push(document.repoPath);
|
|
2629
2953
|
continue;
|
|
2630
2954
|
}
|
|
2631
|
-
await
|
|
2632
|
-
await
|
|
2955
|
+
await mkdir7(path8.dirname(fullPath), { recursive: true });
|
|
2956
|
+
await writeFile7(fullPath, createSyncedDocumentMarkdown(document), "utf8");
|
|
2633
2957
|
result.written.push(document.repoPath);
|
|
2634
2958
|
}
|
|
2635
2959
|
return result;
|
|
@@ -2690,7 +3014,7 @@ function parseSyncedMarkdown(content) {
|
|
|
2690
3014
|
}
|
|
2691
3015
|
async function readExistingSyncedDocument(fullPath) {
|
|
2692
3016
|
try {
|
|
2693
|
-
const raw = await
|
|
3017
|
+
const raw = await readFile5(fullPath, "utf8");
|
|
2694
3018
|
const parsed = parseSyncedMarkdown(raw);
|
|
2695
3019
|
if (!parsed) {
|
|
2696
3020
|
return { exists: true };
|
|
@@ -2715,7 +3039,7 @@ async function readExistingSyncedDocument(fullPath) {
|
|
|
2715
3039
|
async function findMarkdownFiles(rootDir) {
|
|
2716
3040
|
const files = [];
|
|
2717
3041
|
for (const syncRoot of syncRoots) {
|
|
2718
|
-
const fullRoot =
|
|
3042
|
+
const fullRoot = path8.join(rootDir, syncRoot);
|
|
2719
3043
|
if (!await exists2(fullRoot)) {
|
|
2720
3044
|
continue;
|
|
2721
3045
|
}
|
|
@@ -2725,7 +3049,7 @@ async function findMarkdownFiles(rootDir) {
|
|
|
2725
3049
|
}
|
|
2726
3050
|
async function walkMarkdownFiles(directory, files) {
|
|
2727
3051
|
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
2728
|
-
const fullPath =
|
|
3052
|
+
const fullPath = path8.join(directory, entry.name);
|
|
2729
3053
|
if (entry.isDirectory()) {
|
|
2730
3054
|
await walkMarkdownFiles(fullPath, files);
|
|
2731
3055
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2734,23 +3058,23 @@ async function walkMarkdownFiles(directory, files) {
|
|
|
2734
3058
|
}
|
|
2735
3059
|
}
|
|
2736
3060
|
function safeRepoPath(rootDir, repoPath) {
|
|
2737
|
-
if (
|
|
3061
|
+
if (path8.isAbsolute(repoPath)) {
|
|
2738
3062
|
throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
|
|
2739
3063
|
}
|
|
2740
|
-
const normalized =
|
|
2741
|
-
if (normalized === ".." || normalized.startsWith(`..${
|
|
3064
|
+
const normalized = path8.normalize(repoPath);
|
|
3065
|
+
if (normalized === ".." || normalized.startsWith(`..${path8.sep}`)) {
|
|
2742
3066
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
2743
3067
|
}
|
|
2744
|
-
const root =
|
|
2745
|
-
const fullPath =
|
|
2746
|
-
if (!fullPath.startsWith(`${root}${
|
|
3068
|
+
const root = path8.resolve(rootDir);
|
|
3069
|
+
const fullPath = path8.resolve(root, normalized);
|
|
3070
|
+
if (!fullPath.startsWith(`${root}${path8.sep}`)) {
|
|
2747
3071
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
2748
3072
|
}
|
|
2749
3073
|
return fullPath;
|
|
2750
3074
|
}
|
|
2751
3075
|
function isControlPlanePath(repoPath) {
|
|
2752
|
-
const normalized =
|
|
2753
|
-
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${
|
|
3076
|
+
const normalized = path8.normalize(repoPath);
|
|
3077
|
+
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`));
|
|
2754
3078
|
}
|
|
2755
3079
|
function canonicalControlPlaneRepoPath(repoPath) {
|
|
2756
3080
|
const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
@@ -2761,11 +3085,11 @@ function canonicalControlPlaneRepoPath(repoPath) {
|
|
|
2761
3085
|
return normalized;
|
|
2762
3086
|
}
|
|
2763
3087
|
function toRepoPath(rootDir, fullPath) {
|
|
2764
|
-
return
|
|
3088
|
+
return path8.relative(rootDir, fullPath).split(path8.sep).join("/");
|
|
2765
3089
|
}
|
|
2766
3090
|
function inferTitle(content, repoPath) {
|
|
2767
3091
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
2768
|
-
return heading ||
|
|
3092
|
+
return heading || path8.basename(repoPath, path8.extname(repoPath));
|
|
2769
3093
|
}
|
|
2770
3094
|
function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
2771
3095
|
return {
|
|
@@ -2786,9 +3110,9 @@ async function exists2(filePath) {
|
|
|
2786
3110
|
}
|
|
2787
3111
|
|
|
2788
3112
|
// src/tool-session-store.ts
|
|
2789
|
-
import { mkdir as
|
|
2790
|
-
import
|
|
2791
|
-
import
|
|
3113
|
+
import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
|
|
3114
|
+
import os5 from "node:os";
|
|
3115
|
+
import path9 from "node:path";
|
|
2792
3116
|
var LocalToolSessionStore = class {
|
|
2793
3117
|
constructor(filePath = defaultSessionStorePath()) {
|
|
2794
3118
|
this.filePath = filePath;
|
|
@@ -2802,12 +3126,12 @@ var LocalToolSessionStore = class {
|
|
|
2802
3126
|
async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
|
|
2803
3127
|
const data = await this.read();
|
|
2804
3128
|
data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2805
|
-
await
|
|
2806
|
-
await
|
|
3129
|
+
await mkdir8(path9.dirname(this.filePath), { recursive: true });
|
|
3130
|
+
await writeFile8(this.filePath, JSON.stringify(data, null, 2), "utf8");
|
|
2807
3131
|
}
|
|
2808
3132
|
async read() {
|
|
2809
3133
|
try {
|
|
2810
|
-
return JSON.parse(await
|
|
3134
|
+
return JSON.parse(await readFile6(this.filePath, "utf8"));
|
|
2811
3135
|
} catch {
|
|
2812
3136
|
return {};
|
|
2813
3137
|
}
|
|
@@ -2815,12 +3139,12 @@ var LocalToolSessionStore = class {
|
|
|
2815
3139
|
};
|
|
2816
3140
|
function defaultSessionStorePath() {
|
|
2817
3141
|
if (process.platform === "darwin") {
|
|
2818
|
-
return
|
|
3142
|
+
return path9.join(os5.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
|
|
2819
3143
|
}
|
|
2820
3144
|
if (process.platform === "win32") {
|
|
2821
|
-
return
|
|
3145
|
+
return path9.join(process.env.APPDATA ?? os5.homedir(), "Amistio", "tool-sessions.json");
|
|
2822
3146
|
}
|
|
2823
|
-
return
|
|
3147
|
+
return path9.join(process.env.XDG_STATE_HOME ?? path9.join(os5.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
|
|
2824
3148
|
}
|
|
2825
3149
|
|
|
2826
3150
|
// src/work-runner.ts
|
|
@@ -3107,7 +3431,7 @@ function stripJsonFence(value) {
|
|
|
3107
3431
|
}
|
|
3108
3432
|
|
|
3109
3433
|
// src/runner-status.ts
|
|
3110
|
-
import { createHash as
|
|
3434
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3111
3435
|
var watchStateReminderMs = 60 * 1e3;
|
|
3112
3436
|
function formatWatchStartupContext(input) {
|
|
3113
3437
|
return [
|
|
@@ -3128,17 +3452,136 @@ function watchStateKey(action) {
|
|
|
3128
3452
|
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
3129
3453
|
}
|
|
3130
3454
|
function stableRunnerId(input) {
|
|
3131
|
-
const digest =
|
|
3455
|
+
const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
|
|
3132
3456
|
return `runner_${digest}`;
|
|
3133
3457
|
}
|
|
3134
3458
|
|
|
3459
|
+
// src/runner-resources.ts
|
|
3460
|
+
import os6 from "node:os";
|
|
3461
|
+
var defaultRuntime = {
|
|
3462
|
+
nowMs: () => Date.now(),
|
|
3463
|
+
memoryUsage: () => process.memoryUsage(),
|
|
3464
|
+
uptime: () => process.uptime(),
|
|
3465
|
+
cpuUsage: () => process.cpuUsage(),
|
|
3466
|
+
totalmem: () => os6.totalmem(),
|
|
3467
|
+
freemem: () => os6.freemem(),
|
|
3468
|
+
loadavg: () => os6.loadavg()
|
|
3469
|
+
};
|
|
3470
|
+
var previousRunnerResourceSample;
|
|
3471
|
+
function sampleCurrentRunnerResourceUsage() {
|
|
3472
|
+
const sample = collectRunnerResourceUsage(previousRunnerResourceSample);
|
|
3473
|
+
previousRunnerResourceSample = sample.state;
|
|
3474
|
+
return sample.resourceUsage;
|
|
3475
|
+
}
|
|
3476
|
+
function collectRunnerResourceUsage(previous, runtime = defaultRuntime) {
|
|
3477
|
+
const sampledAtMs = runtime.nowMs();
|
|
3478
|
+
const memory = runtime.memoryUsage();
|
|
3479
|
+
const cpu = runtime.cpuUsage();
|
|
3480
|
+
const load = runtime.loadavg();
|
|
3481
|
+
const totalMemory = runtime.totalmem();
|
|
3482
|
+
const freeMemory = runtime.freemem();
|
|
3483
|
+
const resourceUsage = {
|
|
3484
|
+
sampledAt: new Date(sampledAtMs).toISOString(),
|
|
3485
|
+
processUptimeSeconds: roundNumber(runtime.uptime(), 3),
|
|
3486
|
+
processMemoryRssBytes: Math.round(memory.rss),
|
|
3487
|
+
processMemoryHeapUsedBytes: Math.round(memory.heapUsed),
|
|
3488
|
+
processMemoryHeapTotalBytes: Math.round(memory.heapTotal),
|
|
3489
|
+
processCpuUserMicros: Math.round(cpu.user),
|
|
3490
|
+
processCpuSystemMicros: Math.round(cpu.system),
|
|
3491
|
+
systemMemoryTotalBytes: Math.round(totalMemory),
|
|
3492
|
+
systemMemoryFreeBytes: Math.round(freeMemory)
|
|
3493
|
+
};
|
|
3494
|
+
const cpuPercent = processCpuPercent(previous, { sampledAtMs, processCpuUserMicros: cpu.user, processCpuSystemMicros: cpu.system });
|
|
3495
|
+
if (cpuPercent !== void 0) {
|
|
3496
|
+
resourceUsage.processCpuPercent = cpuPercent;
|
|
3497
|
+
}
|
|
3498
|
+
if (load.length >= 3) {
|
|
3499
|
+
resourceUsage.systemLoadAverage1m = roundNumber(load[0], 2);
|
|
3500
|
+
resourceUsage.systemLoadAverage5m = roundNumber(load[1], 2);
|
|
3501
|
+
resourceUsage.systemLoadAverage15m = roundNumber(load[2], 2);
|
|
3502
|
+
}
|
|
3503
|
+
return {
|
|
3504
|
+
resourceUsage,
|
|
3505
|
+
state: {
|
|
3506
|
+
sampledAtMs,
|
|
3507
|
+
processCpuUserMicros: cpu.user,
|
|
3508
|
+
processCpuSystemMicros: cpu.system
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
function formatRunnerResourceUsage(resourceUsage) {
|
|
3513
|
+
if (!resourceUsage) {
|
|
3514
|
+
return "unavailable until this runner reports a sample.";
|
|
3515
|
+
}
|
|
3516
|
+
const parts = [
|
|
3517
|
+
resourceUsage.processMemoryRssBytes !== void 0 ? `RSS ${formatBytes(resourceUsage.processMemoryRssBytes)}` : void 0,
|
|
3518
|
+
heapSummary(resourceUsage),
|
|
3519
|
+
resourceUsage.processCpuPercent !== void 0 ? `CPU ${formatPercent(resourceUsage.processCpuPercent)}` : "CPU warming up",
|
|
3520
|
+
systemMemorySummary(resourceUsage),
|
|
3521
|
+
loadSummary(resourceUsage),
|
|
3522
|
+
`sampled ${resourceUsage.sampledAt}`
|
|
3523
|
+
].filter(Boolean);
|
|
3524
|
+
return parts.length ? parts.join("; ") : "unavailable until this runner reports a complete sample.";
|
|
3525
|
+
}
|
|
3526
|
+
function formatBytes(bytes) {
|
|
3527
|
+
if (bytes === void 0 || !Number.isFinite(bytes)) {
|
|
3528
|
+
return "unknown";
|
|
3529
|
+
}
|
|
3530
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
3531
|
+
let value = Math.max(0, bytes);
|
|
3532
|
+
let unitIndex = 0;
|
|
3533
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
3534
|
+
value /= 1024;
|
|
3535
|
+
unitIndex += 1;
|
|
3536
|
+
}
|
|
3537
|
+
const digits = Number.isInteger(value) || value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
3538
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
3539
|
+
}
|
|
3540
|
+
function formatPercent(value) {
|
|
3541
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
3542
|
+
return "unknown";
|
|
3543
|
+
}
|
|
3544
|
+
return `${roundNumber(value, 1).toFixed(1)}%`;
|
|
3545
|
+
}
|
|
3546
|
+
function processCpuPercent(previous, current) {
|
|
3547
|
+
if (!previous) return void 0;
|
|
3548
|
+
const elapsedMicros = (current.sampledAtMs - previous.sampledAtMs) * 1e3;
|
|
3549
|
+
const cpuDeltaMicros = current.processCpuUserMicros + current.processCpuSystemMicros - previous.processCpuUserMicros - previous.processCpuSystemMicros;
|
|
3550
|
+
if (elapsedMicros <= 0 || cpuDeltaMicros < 0) return void 0;
|
|
3551
|
+
return roundNumber(cpuDeltaMicros / elapsedMicros * 100, 1);
|
|
3552
|
+
}
|
|
3553
|
+
function heapSummary(resourceUsage) {
|
|
3554
|
+
if (resourceUsage.processMemoryHeapUsedBytes === void 0 && resourceUsage.processMemoryHeapTotalBytes === void 0) {
|
|
3555
|
+
return void 0;
|
|
3556
|
+
}
|
|
3557
|
+
return `heap ${formatBytes(resourceUsage.processMemoryHeapUsedBytes)} / ${formatBytes(resourceUsage.processMemoryHeapTotalBytes)}`;
|
|
3558
|
+
}
|
|
3559
|
+
function systemMemorySummary(resourceUsage) {
|
|
3560
|
+
if (resourceUsage.systemMemoryTotalBytes === void 0 || resourceUsage.systemMemoryFreeBytes === void 0 || resourceUsage.systemMemoryTotalBytes <= 0) {
|
|
3561
|
+
return void 0;
|
|
3562
|
+
}
|
|
3563
|
+
const usedBytes = Math.max(0, resourceUsage.systemMemoryTotalBytes - resourceUsage.systemMemoryFreeBytes);
|
|
3564
|
+
const usedPercent = usedBytes / resourceUsage.systemMemoryTotalBytes * 100;
|
|
3565
|
+
return `system memory ${formatBytes(usedBytes)} / ${formatBytes(resourceUsage.systemMemoryTotalBytes)} used (${formatPercent(usedPercent)})`;
|
|
3566
|
+
}
|
|
3567
|
+
function loadSummary(resourceUsage) {
|
|
3568
|
+
if (resourceUsage.systemLoadAverage1m === void 0 || resourceUsage.systemLoadAverage5m === void 0 || resourceUsage.systemLoadAverage15m === void 0) {
|
|
3569
|
+
return void 0;
|
|
3570
|
+
}
|
|
3571
|
+
return `load ${resourceUsage.systemLoadAverage1m.toFixed(2)} / ${resourceUsage.systemLoadAverage5m.toFixed(2)} / ${resourceUsage.systemLoadAverage15m.toFixed(2)}`;
|
|
3572
|
+
}
|
|
3573
|
+
function roundNumber(value, digits) {
|
|
3574
|
+
const factor = 10 ** digits;
|
|
3575
|
+
return Math.round(value * factor) / factor;
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3135
3578
|
// src/importer.ts
|
|
3136
|
-
import { execFile as
|
|
3137
|
-
import { createHash as
|
|
3138
|
-
import { readdir as readdir4, readFile as
|
|
3139
|
-
import
|
|
3140
|
-
import { promisify as
|
|
3141
|
-
var
|
|
3579
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3580
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3581
|
+
import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
|
|
3582
|
+
import path10 from "node:path";
|
|
3583
|
+
import { promisify as promisify3 } from "node:util";
|
|
3584
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3142
3585
|
var defaultMaxFileKb = 256;
|
|
3143
3586
|
var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
3144
3587
|
var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
|
|
@@ -3155,12 +3598,12 @@ var documentFolderByType = {
|
|
|
3155
3598
|
workflow: "docs/workflows"
|
|
3156
3599
|
};
|
|
3157
3600
|
async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
3158
|
-
const requestedRoot =
|
|
3601
|
+
const requestedRoot = path10.resolve(rootDir);
|
|
3159
3602
|
const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
|
|
3160
3603
|
const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
|
|
3161
3604
|
const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
|
|
3162
3605
|
const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
|
|
3163
|
-
const repoName = (parsedCloneUrl?.repoName ??
|
|
3606
|
+
const repoName = (parsedCloneUrl?.repoName ?? path10.basename(root)) || "repository";
|
|
3164
3607
|
const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
|
|
3165
3608
|
return {
|
|
3166
3609
|
rootDir: root,
|
|
@@ -3172,7 +3615,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
|
3172
3615
|
};
|
|
3173
3616
|
}
|
|
3174
3617
|
async function scanLegacyDocuments(options) {
|
|
3175
|
-
const rootDir =
|
|
3618
|
+
const rootDir = path10.resolve(options.rootDir);
|
|
3176
3619
|
const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
|
|
3177
3620
|
const skipped = [];
|
|
3178
3621
|
const candidates = [];
|
|
@@ -3191,7 +3634,7 @@ async function scanLegacyDocuments(options) {
|
|
|
3191
3634
|
skipped.push({ repoPath, reason: "excluded" });
|
|
3192
3635
|
continue;
|
|
3193
3636
|
}
|
|
3194
|
-
const fullPath =
|
|
3637
|
+
const fullPath = path10.join(rootDir, ...repoPath.split("/"));
|
|
3195
3638
|
const fileStat = await stat4(fullPath).catch(() => void 0);
|
|
3196
3639
|
if (!fileStat?.isFile()) {
|
|
3197
3640
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
@@ -3201,7 +3644,7 @@ async function scanLegacyDocuments(options) {
|
|
|
3201
3644
|
skipped.push({ repoPath, reason: "tooLarge" });
|
|
3202
3645
|
continue;
|
|
3203
3646
|
}
|
|
3204
|
-
const content = await
|
|
3647
|
+
const content = await readFile7(fullPath, "utf8").catch(() => void 0);
|
|
3205
3648
|
if (content === void 0) {
|
|
3206
3649
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
3207
3650
|
continue;
|
|
@@ -3291,8 +3734,8 @@ async function listRepositoryPaths(rootDir) {
|
|
|
3291
3734
|
async function walkRepository(rootDir, directory, files) {
|
|
3292
3735
|
const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
|
|
3293
3736
|
for (const entry of entries) {
|
|
3294
|
-
const fullPath =
|
|
3295
|
-
const repoPath = normalizeRepoPath3(
|
|
3737
|
+
const fullPath = path10.join(directory, entry.name);
|
|
3738
|
+
const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
|
|
3296
3739
|
if (entry.isDirectory()) {
|
|
3297
3740
|
if (!excludedDirectoryNames.has(entry.name)) {
|
|
3298
3741
|
await walkRepository(rootDir, fullPath, files);
|
|
@@ -3357,9 +3800,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
|
3357
3800
|
usedPaths.add(basePath);
|
|
3358
3801
|
return basePath;
|
|
3359
3802
|
}
|
|
3360
|
-
const extension =
|
|
3361
|
-
const directory =
|
|
3362
|
-
const basename =
|
|
3803
|
+
const extension = path10.posix.extname(basePath) || ".md";
|
|
3804
|
+
const directory = path10.posix.dirname(basePath);
|
|
3805
|
+
const basename = path10.posix.basename(basePath, extension);
|
|
3363
3806
|
const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
|
|
3364
3807
|
usedPaths.add(uniquePath);
|
|
3365
3808
|
return uniquePath;
|
|
@@ -3376,7 +3819,7 @@ function inferTitle2(content, repoPath) {
|
|
|
3376
3819
|
const body = stripFrontmatter(content);
|
|
3377
3820
|
const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
|
|
3378
3821
|
if (heading) return heading;
|
|
3379
|
-
const basename =
|
|
3822
|
+
const basename = path10.posix.basename(repoPath, path10.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
|
|
3380
3823
|
return titleCase(basename || "Imported Document");
|
|
3381
3824
|
}
|
|
3382
3825
|
function stripFrontmatter(content) {
|
|
@@ -3398,19 +3841,19 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
|
|
|
3398
3841
|
return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
|
|
3399
3842
|
}
|
|
3400
3843
|
function hashText(value, length) {
|
|
3401
|
-
return
|
|
3844
|
+
return createHash5("sha256").update(value).digest("hex").slice(0, length);
|
|
3402
3845
|
}
|
|
3403
3846
|
function normalizeRepoPath3(value) {
|
|
3404
3847
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
3405
3848
|
}
|
|
3406
3849
|
async function runGit2(args) {
|
|
3407
|
-
const { stdout } = await
|
|
3850
|
+
const { stdout } = await execFileAsync3("git", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
3408
3851
|
return stdout.trim();
|
|
3409
3852
|
}
|
|
3410
3853
|
|
|
3411
3854
|
// src/runner-actions.ts
|
|
3412
|
-
import { spawn as
|
|
3413
|
-
import
|
|
3855
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3856
|
+
import path11 from "node:path";
|
|
3414
3857
|
function buildBackgroundRunnerArgs(options) {
|
|
3415
3858
|
const args = [
|
|
3416
3859
|
"run",
|
|
@@ -3420,7 +3863,7 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
3420
3863
|
"--runner-id",
|
|
3421
3864
|
options.runnerId,
|
|
3422
3865
|
"--root",
|
|
3423
|
-
|
|
3866
|
+
path11.resolve(options.root),
|
|
3424
3867
|
"--session",
|
|
3425
3868
|
options.session,
|
|
3426
3869
|
"--interval-seconds",
|
|
@@ -3441,6 +3884,8 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
3441
3884
|
if (options.maxIterations !== void 0) {
|
|
3442
3885
|
args.push("--max-iterations", String(options.maxIterations));
|
|
3443
3886
|
}
|
|
3887
|
+
args.push("--max-preflight-attempts", String(options.maxPreflightAttempts));
|
|
3888
|
+
args.push("--tool-timeout-seconds", String(options.toolTimeoutSeconds));
|
|
3444
3889
|
if (!options.stream) {
|
|
3445
3890
|
args.push("--no-stream");
|
|
3446
3891
|
}
|
|
@@ -3458,7 +3903,7 @@ async function runOfficialCliUpdate() {
|
|
|
3458
3903
|
}
|
|
3459
3904
|
function runOfficialUpdateProcess(command, args, timeoutMs) {
|
|
3460
3905
|
return new Promise((resolve) => {
|
|
3461
|
-
const child =
|
|
3906
|
+
const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3462
3907
|
let output = "";
|
|
3463
3908
|
const updateTimeout = setTimeout(() => {
|
|
3464
3909
|
output += "Timed out while running official CLI update.\n";
|
|
@@ -3486,11 +3931,11 @@ function truncateProcessOutput(value) {
|
|
|
3486
3931
|
}
|
|
3487
3932
|
|
|
3488
3933
|
// src/git-worktree.ts
|
|
3489
|
-
import { execFile as
|
|
3490
|
-
import { mkdir as
|
|
3491
|
-
import
|
|
3492
|
-
import { promisify as
|
|
3493
|
-
var
|
|
3934
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3935
|
+
import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
|
|
3936
|
+
import path12 from "node:path";
|
|
3937
|
+
import { promisify as promisify4 } from "node:util";
|
|
3938
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
3494
3939
|
function needsGitWorktreeIsolation(workItem) {
|
|
3495
3940
|
return (workItem.workKind ?? "implementation") === "implementation";
|
|
3496
3941
|
}
|
|
@@ -3517,7 +3962,7 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
|
3517
3962
|
await assertExistingWorktree(worktreePath, identity.branch);
|
|
3518
3963
|
return { ...identity, baseRevision, worktreePath };
|
|
3519
3964
|
}
|
|
3520
|
-
await
|
|
3965
|
+
await mkdir9(path12.dirname(worktreePath), { recursive: true });
|
|
3521
3966
|
const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
|
|
3522
3967
|
const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
|
|
3523
3968
|
await gitOutput(repoRoot, worktreeArgs).catch((error) => {
|
|
@@ -3526,9 +3971,9 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
|
3526
3971
|
return { ...identity, baseRevision, worktreePath };
|
|
3527
3972
|
}
|
|
3528
3973
|
function localWorktreePath(repoRoot, worktreeKey) {
|
|
3529
|
-
const repoName =
|
|
3974
|
+
const repoName = path12.basename(repoRoot);
|
|
3530
3975
|
const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
|
|
3531
|
-
return
|
|
3976
|
+
return path12.join(path12.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
|
|
3532
3977
|
}
|
|
3533
3978
|
async function assertExistingWorktree(worktreePath, branch) {
|
|
3534
3979
|
await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -3551,11 +3996,11 @@ async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
|
|
|
3551
3996
|
}
|
|
3552
3997
|
}
|
|
3553
3998
|
async function gitOutput(cwd, args) {
|
|
3554
|
-
const { stdout } = await
|
|
3999
|
+
const { stdout } = await execFileAsync4("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
3555
4000
|
return stdout.trim();
|
|
3556
4001
|
}
|
|
3557
4002
|
async function gitCommandSucceeds(cwd, args) {
|
|
3558
|
-
return
|
|
4003
|
+
return execFileAsync4("git", args, { cwd }).then(() => true, () => false);
|
|
3559
4004
|
}
|
|
3560
4005
|
async function pathExists(value) {
|
|
3561
4006
|
return stat5(value).then(() => true, () => false);
|
|
@@ -3588,6 +4033,10 @@ var CLI_VERSION = readCliPackageVersion();
|
|
|
3588
4033
|
var program = new Command();
|
|
3589
4034
|
var defaultRoot = process.env.INIT_CWD ?? process.cwd();
|
|
3590
4035
|
var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
|
|
4036
|
+
var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
|
|
4037
|
+
var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
|
|
4038
|
+
var RUNNER_WORK_LEASE_SECONDS = 300;
|
|
4039
|
+
var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
|
|
3591
4040
|
program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
|
|
3592
4041
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
3593
4042
|
const created = await initControlPlane(options.root);
|
|
@@ -3614,7 +4063,8 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
|
|
|
3614
4063
|
repositoryLinkId: options.repositoryLink,
|
|
3615
4064
|
repoName: parsedRepoUrl.repoName,
|
|
3616
4065
|
repoFingerprint: createRepoFingerprint(options.account, options.project, options.repositoryLink),
|
|
3617
|
-
defaultBranch: options.defaultBranch
|
|
4066
|
+
defaultBranch: options.defaultBranch,
|
|
4067
|
+
machineId: runnerMachineId()
|
|
3618
4068
|
});
|
|
3619
4069
|
const filePath = await writeProjectLink(checkout.targetDir, {
|
|
3620
4070
|
amistioAccountId: options.account,
|
|
@@ -3666,6 +4116,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
3666
4116
|
repoName: repository.repoName,
|
|
3667
4117
|
repoFingerprint: repository.repoFingerprint,
|
|
3668
4118
|
defaultBranch: repository.defaultBranch,
|
|
4119
|
+
machineId: runnerMachineId(),
|
|
3669
4120
|
...parsedCloneUrl ? { cloneUrl: parsedCloneUrl.cloneUrl } : {},
|
|
3670
4121
|
...parsedCloneUrl?.provider ? { provider: parsedCloneUrl.provider } : {},
|
|
3671
4122
|
...parsedCloneUrl?.repoOwner ? { repoOwner: parsedCloneUrl.repoOwner } : {},
|
|
@@ -3698,6 +4149,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
3698
4149
|
console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
|
|
3699
4150
|
});
|
|
3700
4151
|
program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
|
|
4152
|
+
const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
|
|
3701
4153
|
let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
|
|
3702
4154
|
let credential = options.token;
|
|
3703
4155
|
if (options.pairingCode) {
|
|
@@ -3709,15 +4161,16 @@ program.command("pair").description("Pair this repository with an Amistio web pr
|
|
|
3709
4161
|
projectId: options.project,
|
|
3710
4162
|
pairingCode: options.pairingCode,
|
|
3711
4163
|
repositoryLinkId,
|
|
3712
|
-
repoName: inferRepoName(
|
|
4164
|
+
repoName: inferRepoName(pairingRoot),
|
|
3713
4165
|
repoFingerprint: createRepoFingerprint(options.account, options.project, repositoryLinkId),
|
|
3714
|
-
defaultBranch: options.defaultBranch
|
|
4166
|
+
defaultBranch: options.defaultBranch,
|
|
4167
|
+
machineId: runnerMachineId()
|
|
3715
4168
|
});
|
|
3716
4169
|
repositoryLinkId = pairing.repositoryLink.repositoryLinkId;
|
|
3717
4170
|
credential = credential ?? pairing.token;
|
|
3718
4171
|
console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}.`);
|
|
3719
4172
|
}
|
|
3720
|
-
const filePath = await writeProjectLink(
|
|
4173
|
+
const filePath = await writeProjectLink(pairingRoot, {
|
|
3721
4174
|
amistioAccountId: options.account,
|
|
3722
4175
|
amistioProjectId: options.project,
|
|
3723
4176
|
repositoryLinkId,
|
|
@@ -3844,7 +4297,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
3844
4297
|
}
|
|
3845
4298
|
const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
|
|
3846
4299
|
if (options.out) {
|
|
3847
|
-
await
|
|
4300
|
+
await writeFile9(options.out, prompt, "utf8");
|
|
3848
4301
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
3849
4302
|
} else {
|
|
3850
4303
|
console.log(prompt);
|
|
@@ -3892,7 +4345,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
3892
4345
|
process.exitCode = result.exitCode;
|
|
3893
4346
|
}
|
|
3894
4347
|
});
|
|
3895
|
-
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
|
|
4348
|
+
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
|
|
3896
4349
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
3897
4350
|
if (!context) {
|
|
3898
4351
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -3921,7 +4374,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3921
4374
|
projectId: context.metadata.amistioProjectId,
|
|
3922
4375
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3923
4376
|
runnerId,
|
|
3924
|
-
rootDir:
|
|
4377
|
+
rootDir: path13.resolve(options.root),
|
|
3925
4378
|
apiUrl: options.apiUrl,
|
|
3926
4379
|
args: buildBackgroundRunnerArgs(resolvedOptions)
|
|
3927
4380
|
});
|
|
@@ -4006,6 +4459,7 @@ runner.command("status").description("Show background runner status for the pair
|
|
|
4006
4459
|
console.log("No background runner metadata found for this paired repository.");
|
|
4007
4460
|
if (runners.length) {
|
|
4008
4461
|
console.log(`Last runner heartbeat: ${runners[0].runnerId} ${runners[0].status} at ${runners[0].lastSeenAt}.`);
|
|
4462
|
+
console.log(`Resource usage: ${formatRunnerResourceUsage(runners[0].resourceUsage)}`);
|
|
4009
4463
|
}
|
|
4010
4464
|
return;
|
|
4011
4465
|
}
|
|
@@ -4026,6 +4480,7 @@ runner.command("status").description("Show background runner status for the pair
|
|
|
4026
4480
|
if (heartbeat) {
|
|
4027
4481
|
console.log(` Last heartbeat: ${heartbeat.status} at ${heartbeat.lastSeenAt}${heartbeat.version ? ` (${heartbeat.version})` : ""}`);
|
|
4028
4482
|
}
|
|
4483
|
+
console.log(` Resource usage: ${formatRunnerResourceUsage(heartbeat?.resourceUsage)}`);
|
|
4029
4484
|
}
|
|
4030
4485
|
});
|
|
4031
4486
|
runner.command("stop").description("Stop a background runner for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Runner ID to stop when multiple background runners exist").action(async (options) => {
|
|
@@ -4050,15 +4505,106 @@ runner.command("stop").description("Stop a background runner for the paired repo
|
|
|
4050
4505
|
return;
|
|
4051
4506
|
}
|
|
4052
4507
|
const record = records[0];
|
|
4508
|
+
const existingRunner = await context.client.listRunners(context.metadata.amistioProjectId).then((result) => result.runners.find((runner2) => runner2.runnerId === record.runnerId && runner2.repositoryLinkId === context.metadata.repositoryLinkId)).catch(() => void 0);
|
|
4053
4509
|
const stopResult = await stopRunnerDaemonProcess(record);
|
|
4054
4510
|
await markRunnerDaemonStopped(record);
|
|
4055
4511
|
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, record.runnerId, context.metadata.repositoryLinkId, "offline", {
|
|
4056
4512
|
version: CLI_VERSION,
|
|
4057
4513
|
mode: "background",
|
|
4058
|
-
hostname: record.hostname
|
|
4514
|
+
hostname: record.hostname,
|
|
4515
|
+
...existingRunner?.resourceUsage ? { resourceUsage: existingRunner.resourceUsage } : {}
|
|
4059
4516
|
}).catch(() => void 0);
|
|
4060
4517
|
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
4061
4518
|
});
|
|
4519
|
+
var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
|
|
4520
|
+
runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
|
|
4521
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4522
|
+
if (!context) {
|
|
4523
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
if (!context.token) {
|
|
4527
|
+
console.log("No local runner credential found. Run `amistio pair --pairing-code <code>` to store this machine credential.");
|
|
4528
|
+
process.exitCode = 1;
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
const platform = detectRunnerServicePlatform();
|
|
4532
|
+
if (platform === "unsupported") {
|
|
4533
|
+
console.log("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
|
|
4534
|
+
process.exitCode = 1;
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4538
|
+
accountId: context.metadata.amistioAccountId,
|
|
4539
|
+
projectId: context.metadata.amistioProjectId,
|
|
4540
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4541
|
+
machineId: runnerMachineId()
|
|
4542
|
+
});
|
|
4543
|
+
const args = buildBackgroundRunnerArgs({ ...options, runnerId, apiUrl: options.apiUrl, root: options.root });
|
|
4544
|
+
const serviceInput = {
|
|
4545
|
+
accountId: context.metadata.amistioAccountId,
|
|
4546
|
+
projectId: context.metadata.amistioProjectId,
|
|
4547
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4548
|
+
runnerId,
|
|
4549
|
+
rootDir: path13.resolve(options.root),
|
|
4550
|
+
apiUrl: options.apiUrl,
|
|
4551
|
+
args,
|
|
4552
|
+
platform
|
|
4553
|
+
};
|
|
4554
|
+
if (options.dryRun) {
|
|
4555
|
+
const descriptor = createRunnerServiceDescriptor(serviceInput);
|
|
4556
|
+
console.log(`Startup service file: ${descriptor.metadata.serviceFilePath}`);
|
|
4557
|
+
console.log(descriptor.content.trim());
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
try {
|
|
4561
|
+
const metadata = await installRunnerService(serviceInput);
|
|
4562
|
+
console.log(`Installed startup service ${metadata.serviceName}.`);
|
|
4563
|
+
console.log(`Service file: ${metadata.serviceFilePath}`);
|
|
4564
|
+
} catch (error) {
|
|
4565
|
+
console.error(errorMessage3(error));
|
|
4566
|
+
process.exitCode = 1;
|
|
4567
|
+
}
|
|
4568
|
+
});
|
|
4569
|
+
runnerService.command("status").description("Show the startup service status for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").action(async (options) => {
|
|
4570
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4571
|
+
if (!context) {
|
|
4572
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4573
|
+
return;
|
|
4574
|
+
}
|
|
4575
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4576
|
+
accountId: context.metadata.amistioAccountId,
|
|
4577
|
+
projectId: context.metadata.amistioProjectId,
|
|
4578
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4579
|
+
machineId: runnerMachineId()
|
|
4580
|
+
});
|
|
4581
|
+
const metadata = await readRunnerServiceMetadata({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
|
|
4582
|
+
if (!metadata) {
|
|
4583
|
+
console.log("No startup service metadata found for this paired repository runner.");
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
const runtimeStatus = await runnerServiceRuntimeStatus(metadata);
|
|
4587
|
+
console.log(`Startup service ${metadata.serviceName}: ${runtimeStatus}`);
|
|
4588
|
+
console.log(` Platform: ${metadata.platform}`);
|
|
4589
|
+
console.log(` File: ${metadata.serviceFilePath}`);
|
|
4590
|
+
console.log(` Root: ${metadata.rootDir}`);
|
|
4591
|
+
console.log(` API: ${metadata.apiUrl}`);
|
|
4592
|
+
});
|
|
4593
|
+
runnerService.command("remove").description("Remove the startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").action(async (options) => {
|
|
4594
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4595
|
+
if (!context) {
|
|
4596
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4600
|
+
accountId: context.metadata.amistioAccountId,
|
|
4601
|
+
projectId: context.metadata.amistioProjectId,
|
|
4602
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4603
|
+
machineId: runnerMachineId()
|
|
4604
|
+
});
|
|
4605
|
+
const removed = await removeRunnerService({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
|
|
4606
|
+
console.log(removed ? `Removed startup service ${removed.serviceName}.` : "No startup service metadata found for this paired repository runner.");
|
|
4607
|
+
});
|
|
4062
4608
|
async function runWatchIteration({ command, context, options, runnerId }) {
|
|
4063
4609
|
try {
|
|
4064
4610
|
return await runNextWorkItem({
|
|
@@ -4084,6 +4630,8 @@ async function runWatchIteration({ command, context, options, runnerId }) {
|
|
|
4084
4630
|
runnerId
|
|
4085
4631
|
},
|
|
4086
4632
|
suppressIdleOutput: Boolean(options.watch),
|
|
4633
|
+
maxPreflightAttempts: options.maxPreflightAttempts,
|
|
4634
|
+
toolTimeoutMs: options.toolTimeoutSeconds * 1e3,
|
|
4087
4635
|
verbose: Boolean(options.verbose)
|
|
4088
4636
|
});
|
|
4089
4637
|
} catch (error) {
|
|
@@ -4098,7 +4646,7 @@ ${detail}`);
|
|
|
4098
4646
|
} else {
|
|
4099
4647
|
console.error(`${message} Run with --verbose for details.`);
|
|
4100
4648
|
}
|
|
4101
|
-
await Promise.allSettled([
|
|
4649
|
+
const settlements = await Promise.allSettled([
|
|
4102
4650
|
context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "blocked", { ...runnerHeartbeatMetadata(), preferenceMessage: message }),
|
|
4103
4651
|
context.client.recordRunnerLog(context.metadata.amistioProjectId, {
|
|
4104
4652
|
runnerId,
|
|
@@ -4109,6 +4657,7 @@ ${detail}`);
|
|
|
4109
4657
|
machineId: runnerMachineId()
|
|
4110
4658
|
})
|
|
4111
4659
|
]);
|
|
4660
|
+
logRejectedSettlements("record watch error", settlements);
|
|
4112
4661
|
return { status: "failed", exitCode: 1, message };
|
|
4113
4662
|
}
|
|
4114
4663
|
}
|
|
@@ -4124,9 +4673,11 @@ async function runNextWorkItem({
|
|
|
4124
4673
|
explicitModel,
|
|
4125
4674
|
explicitInvocationChannel,
|
|
4126
4675
|
explicitTool,
|
|
4676
|
+
maxPreflightAttempts,
|
|
4127
4677
|
toolCommand,
|
|
4128
4678
|
commandContext,
|
|
4129
4679
|
suppressIdleOutput,
|
|
4680
|
+
toolTimeoutMs,
|
|
4130
4681
|
verbose
|
|
4131
4682
|
}) {
|
|
4132
4683
|
const toolConfig = await resolveRunnerToolConfig({
|
|
@@ -4149,7 +4700,7 @@ async function runNextWorkItem({
|
|
|
4149
4700
|
console.log(toolConfig.message);
|
|
4150
4701
|
return { status: "blocked", exitCode: 1 };
|
|
4151
4702
|
}
|
|
4152
|
-
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId,
|
|
4703
|
+
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId, RUNNER_WORK_LEASE_SECONDS, runnerIsolationCapabilityMetadata());
|
|
4153
4704
|
if (!result.workItem) {
|
|
4154
4705
|
const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
|
|
4155
4706
|
const message = formatProjectNextAction(nextAction);
|
|
@@ -4159,14 +4710,20 @@ async function runNextWorkItem({
|
|
|
4159
4710
|
return { status: "idle", exitCode: 0, nextAction, message };
|
|
4160
4711
|
}
|
|
4161
4712
|
const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
|
|
4713
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4714
|
+
status: "running",
|
|
4715
|
+
summary: "Prepared local runner execution prompt.",
|
|
4716
|
+
idempotencyKey: `runner_milestone_prompt_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
4717
|
+
metadata: { workKind: result.workItem.workKind ?? "implementation", attempt: result.workItem.attempt }
|
|
4718
|
+
});
|
|
4162
4719
|
if (dryRun || toolConfig.tool === "none") {
|
|
4163
4720
|
console.log(prompt);
|
|
4164
4721
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4165
4722
|
return { status: "preview", exitCode: 0 };
|
|
4166
4723
|
}
|
|
4167
|
-
const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
|
|
4168
|
-
if (worktreeIsolation.status
|
|
4169
|
-
return { status: "blocked", exitCode: 1, message: worktreeIsolation.message };
|
|
4724
|
+
const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
|
|
4725
|
+
if (worktreeIsolation.status !== "ready") {
|
|
4726
|
+
return { status: worktreeIsolation.status === "failed" ? "failed" : "blocked", exitCode: 1, message: worktreeIsolation.message };
|
|
4170
4727
|
}
|
|
4171
4728
|
const executionRoot = worktreeIsolation.isolation?.worktreePath ?? root;
|
|
4172
4729
|
const isolationTelemetry = workItemIsolationTelemetry(result.workItem, worktreeIsolation.isolation);
|
|
@@ -4203,6 +4760,7 @@ async function runNextWorkItem({
|
|
|
4203
4760
|
const providerSessionStore = new LocalToolSessionStore();
|
|
4204
4761
|
const providerSessionId = sessionContext.toolSession ? await providerSessionStore.getProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName) : void 0;
|
|
4205
4762
|
let toolResult;
|
|
4763
|
+
const stopLeaseRenewal = startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem: result.workItem, telemetry: isolationTelemetry });
|
|
4206
4764
|
try {
|
|
4207
4765
|
toolResult = await runLocalTool({
|
|
4208
4766
|
rootDir: executionRoot,
|
|
@@ -4212,6 +4770,7 @@ async function runNextWorkItem({
|
|
|
4212
4770
|
...toolCommand ? { toolCommand } : {},
|
|
4213
4771
|
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
4214
4772
|
streamOutput: stream,
|
|
4773
|
+
timeoutMs: toolTimeoutMs,
|
|
4215
4774
|
...sessionContext.toolSession ? {
|
|
4216
4775
|
session: {
|
|
4217
4776
|
toolSessionId: sessionContext.toolSession.toolSessionId,
|
|
@@ -4222,10 +4781,11 @@ async function runNextWorkItem({
|
|
|
4222
4781
|
} : {}
|
|
4223
4782
|
});
|
|
4224
4783
|
} catch (error) {
|
|
4784
|
+
stopLeaseRenewal();
|
|
4225
4785
|
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4226
4786
|
const durationMs2 = Date.now() - startedAt;
|
|
4227
4787
|
const message = `${preview.toolName} failed before returning a result.`;
|
|
4228
|
-
await Promise.allSettled([
|
|
4788
|
+
const settlements = await Promise.allSettled([
|
|
4229
4789
|
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
4230
4790
|
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
|
|
4231
4791
|
apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "failed", `run_failed_${result.workItem.workItemId}_${result.workItem.attempt}_${runnerId}`, runnerId, {
|
|
@@ -4247,11 +4807,13 @@ async function runNextWorkItem({
|
|
|
4247
4807
|
metadata: { tool: preview.toolName, error: detail }
|
|
4248
4808
|
})
|
|
4249
4809
|
]);
|
|
4810
|
+
logRejectedSettlements("record local tool failure", settlements);
|
|
4250
4811
|
if (verbose || !stream) {
|
|
4251
4812
|
console.error(detail);
|
|
4252
4813
|
}
|
|
4253
4814
|
return { status: "failed", exitCode: 1, message };
|
|
4254
4815
|
}
|
|
4816
|
+
stopLeaseRenewal();
|
|
4255
4817
|
if (sessionContext.toolSession && toolResult.providerSessionId) {
|
|
4256
4818
|
await providerSessionStore.setProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName, toolResult.providerSessionId);
|
|
4257
4819
|
}
|
|
@@ -4262,47 +4824,59 @@ async function runNextWorkItem({
|
|
|
4262
4824
|
console.error(toolResult.stderr.trim());
|
|
4263
4825
|
}
|
|
4264
4826
|
if (result.workItem.workKind === "brainGeneration" || result.workItem.workKind === "planRevision") {
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4827
|
+
try {
|
|
4828
|
+
return await finalizeBrainGenerationWork({
|
|
4829
|
+
apiClient,
|
|
4830
|
+
durationMs: Date.now() - startedAt,
|
|
4831
|
+
projectId,
|
|
4832
|
+
repositoryLinkId,
|
|
4833
|
+
runnerId,
|
|
4834
|
+
sessionContext,
|
|
4835
|
+
toolConfig,
|
|
4836
|
+
toolName: preview.toolName,
|
|
4837
|
+
toolResult,
|
|
4838
|
+
workItem: result.workItem
|
|
4839
|
+
});
|
|
4840
|
+
} catch (error) {
|
|
4841
|
+
return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
|
|
4842
|
+
}
|
|
4277
4843
|
}
|
|
4278
4844
|
if (result.workItem.workKind === "assistantQuestion") {
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4845
|
+
try {
|
|
4846
|
+
return await finalizeAssistantQuestionWork({
|
|
4847
|
+
apiClient,
|
|
4848
|
+
durationMs: Date.now() - startedAt,
|
|
4849
|
+
projectId,
|
|
4850
|
+
repositoryLinkId,
|
|
4851
|
+
runnerId,
|
|
4852
|
+
sessionContext,
|
|
4853
|
+
toolConfig,
|
|
4854
|
+
toolName: preview.toolName,
|
|
4855
|
+
toolResult,
|
|
4856
|
+
workItem: result.workItem
|
|
4857
|
+
});
|
|
4858
|
+
} catch (error) {
|
|
4859
|
+
return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
|
|
4860
|
+
}
|
|
4291
4861
|
}
|
|
4292
4862
|
if (result.workItem.workKind === "impactPreview") {
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4863
|
+
try {
|
|
4864
|
+
return await finalizeImpactPreviewWork({
|
|
4865
|
+
apiClient,
|
|
4866
|
+
durationMs: Date.now() - startedAt,
|
|
4867
|
+
projectId,
|
|
4868
|
+
repositoryLinkId,
|
|
4869
|
+
root,
|
|
4870
|
+
runnerId,
|
|
4871
|
+
sessionContext,
|
|
4872
|
+
toolConfig,
|
|
4873
|
+
toolName: preview.toolName,
|
|
4874
|
+
toolResult,
|
|
4875
|
+
workItem: result.workItem
|
|
4876
|
+
});
|
|
4877
|
+
} catch (error) {
|
|
4878
|
+
return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
|
|
4879
|
+
}
|
|
4306
4880
|
}
|
|
4307
4881
|
const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
|
|
4308
4882
|
const durationMs = Date.now() - startedAt;
|
|
@@ -4354,11 +4928,17 @@ async function runNextWorkItem({
|
|
|
4354
4928
|
console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
|
|
4355
4929
|
return { status: finalStatus, exitCode: toolResult.exitCode };
|
|
4356
4930
|
}
|
|
4357
|
-
async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
|
|
4931
|
+
async function prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
|
|
4358
4932
|
if (!needsGitWorktreeIsolation(workItem)) {
|
|
4359
4933
|
return { status: "ready" };
|
|
4360
4934
|
}
|
|
4361
4935
|
const identity = resolveWorktreeIdentity(workItem);
|
|
4936
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4937
|
+
status: "running",
|
|
4938
|
+
summary: `Checking Git worktree isolation for attempt ${workItem.attempt}/${maxPreflightAttempts}.`,
|
|
4939
|
+
idempotencyKey: `runner_milestone_worktree_preflight_${workItem.workItemId}_${workItem.attempt}`,
|
|
4940
|
+
metadata: { executionWorktreeKey: identity.worktreeKey, executionBranch: identity.branch, implementationScopeId: identity.implementationScopeId, attempt: workItem.attempt, maxAttempts: maxPreflightAttempts }
|
|
4941
|
+
});
|
|
4362
4942
|
try {
|
|
4363
4943
|
const isolation = await prepareGitWorktreeIsolation(root, workItem);
|
|
4364
4944
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
@@ -4371,29 +4951,111 @@ async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryL
|
|
|
4371
4951
|
} catch (error) {
|
|
4372
4952
|
const message = errorMessage3(error);
|
|
4373
4953
|
const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
|
|
4374
|
-
const
|
|
4954
|
+
const finalAttempt = workItem.attempt >= maxPreflightAttempts;
|
|
4955
|
+
const statusMessage = finalAttempt ? `Git worktree preflight failed after ${workItem.attempt}/${maxPreflightAttempts} attempts. ${message}` : `Git worktree preflight attempt ${workItem.attempt}/${maxPreflightAttempts} failed. Requeueing for retry. ${message}`;
|
|
4956
|
+
const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`, runnerId, {
|
|
4375
4957
|
...telemetry,
|
|
4376
|
-
message,
|
|
4377
|
-
blockerReason: message,
|
|
4958
|
+
message: statusMessage,
|
|
4959
|
+
...finalAttempt ? { blockerReason: message } : { releaseClaim: true },
|
|
4378
4960
|
error: message
|
|
4379
4961
|
});
|
|
4380
4962
|
await recordRunnerMilestone(apiClient, projectId, statusResult.workItem, runnerId, repositoryLinkId, {
|
|
4381
|
-
status: "
|
|
4382
|
-
summary:
|
|
4383
|
-
idempotencyKey: `
|
|
4384
|
-
metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "" }
|
|
4963
|
+
status: finalAttempt ? "failed" : "warning",
|
|
4964
|
+
summary: statusMessage,
|
|
4965
|
+
idempotencyKey: `runner_milestone_worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
|
|
4966
|
+
metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "", attempt: workItem.attempt, maxAttempts: maxPreflightAttempts, error: message }
|
|
4385
4967
|
});
|
|
4386
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "
|
|
4968
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", {
|
|
4387
4969
|
...runnerHeartbeatMetadata(toolConfig),
|
|
4388
|
-
|
|
4389
|
-
...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
|
|
4390
|
-
...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
4391
|
-
...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
|
|
4970
|
+
preferenceMessage: statusMessage
|
|
4392
4971
|
});
|
|
4393
|
-
console.error(
|
|
4394
|
-
return { status: "
|
|
4972
|
+
console.error(statusMessage);
|
|
4973
|
+
return { status: finalAttempt ? "failed" : "retrying", message: statusMessage };
|
|
4395
4974
|
}
|
|
4396
4975
|
}
|
|
4976
|
+
function startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem, telemetry }) {
|
|
4977
|
+
let stopped = false;
|
|
4978
|
+
const renew = async () => {
|
|
4979
|
+
if (stopped) return;
|
|
4980
|
+
const leaseExpiresAt = new Date(Date.now() + RUNNER_WORK_LEASE_SECONDS * 1e3).toISOString();
|
|
4981
|
+
try {
|
|
4982
|
+
await apiClient.updateWorkStatus(projectId, workItem.workItemId, "running", `lease_renewal_${workItem.workItemId}_${workItem.attempt}_${Date.now()}`, runnerId, {
|
|
4983
|
+
...telemetry,
|
|
4984
|
+
leaseExpiresAt
|
|
4985
|
+
});
|
|
4986
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", {
|
|
4987
|
+
...runnerHeartbeatMetadata(toolConfig),
|
|
4988
|
+
currentWorkItemId: workItem.workItemId,
|
|
4989
|
+
...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
|
|
4990
|
+
...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
4991
|
+
...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
|
|
4992
|
+
});
|
|
4993
|
+
} catch (error) {
|
|
4994
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4995
|
+
console.error(`Could not renew Amistio work lease for ${workItem.workItemId}: ${detail}`);
|
|
4996
|
+
await apiClient.recordRunnerLog(projectId, {
|
|
4997
|
+
runnerId,
|
|
4998
|
+
repositoryLinkId,
|
|
4999
|
+
status: "failed",
|
|
5000
|
+
workItemId: workItem.workItemId,
|
|
5001
|
+
workTitle: workItem.title,
|
|
5002
|
+
...workItem.workKind ? { workKind: workItem.workKind } : {},
|
|
5003
|
+
message: "Runner could not renew the active work lease.",
|
|
5004
|
+
error: detail,
|
|
5005
|
+
machineId: runnerMachineId()
|
|
5006
|
+
}).catch(() => void 0);
|
|
5007
|
+
}
|
|
5008
|
+
};
|
|
5009
|
+
const timer = setInterval(() => {
|
|
5010
|
+
void renew();
|
|
5011
|
+
}, RUNNER_WORK_LEASE_RENEWAL_MS);
|
|
5012
|
+
timer.unref?.();
|
|
5013
|
+
return () => {
|
|
5014
|
+
stopped = true;
|
|
5015
|
+
clearInterval(timer);
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
async function recordFinalizationFailure({ apiClient, durationMs, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName, workItem }) {
|
|
5019
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
5020
|
+
const message = `${toolName} completed, but Amistio could not finalize the result.`;
|
|
5021
|
+
const settlements = await Promise.allSettled([
|
|
5022
|
+
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
5023
|
+
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
|
|
5024
|
+
apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`, runnerId, {
|
|
5025
|
+
...isolationTelemetry,
|
|
5026
|
+
tool: toolName,
|
|
5027
|
+
durationMs,
|
|
5028
|
+
message,
|
|
5029
|
+
error: detail,
|
|
5030
|
+
...sessionContext.toolSession ? { toolSessionId: sessionContext.toolSession.toolSessionId } : {},
|
|
5031
|
+
sessionPolicy: sessionContext.policy,
|
|
5032
|
+
sessionDecision: sessionContext.decision,
|
|
5033
|
+
sessionDecisionReason: sessionContext.reason
|
|
5034
|
+
}),
|
|
5035
|
+
apiClient.recordRunnerLog(projectId, {
|
|
5036
|
+
runnerId,
|
|
5037
|
+
repositoryLinkId,
|
|
5038
|
+
status: "failed",
|
|
5039
|
+
workItemId: workItem.workItemId,
|
|
5040
|
+
workTitle: workItem.title,
|
|
5041
|
+
...workItem.workKind ? { workKind: workItem.workKind } : {},
|
|
5042
|
+
tool: toolName,
|
|
5043
|
+
durationMs,
|
|
5044
|
+
message,
|
|
5045
|
+
error: detail,
|
|
5046
|
+
machineId: runnerMachineId()
|
|
5047
|
+
}),
|
|
5048
|
+
recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5049
|
+
status: "failed",
|
|
5050
|
+
summary: message,
|
|
5051
|
+
idempotencyKey: `runner_milestone_finalization_failed_${workItem.workItemId}_${workItem.attempt}`,
|
|
5052
|
+
metadata: { tool: toolName, durationMs, error: detail }
|
|
5053
|
+
})
|
|
5054
|
+
]);
|
|
5055
|
+
logRejectedSettlements("record finalization failure", settlements);
|
|
5056
|
+
console.error(detail);
|
|
5057
|
+
return { status: "failed", exitCode: 1, message };
|
|
5058
|
+
}
|
|
4397
5059
|
function workItemIsolationTelemetry(workItem, isolation) {
|
|
4398
5060
|
const implementationScopeId = isolation?.implementationScopeId ?? workItem.implementationScopeId;
|
|
4399
5061
|
const executionBranch = isolation?.branch ?? workItem.executionBranch;
|
|
@@ -4422,6 +5084,13 @@ async function recordRunnerMilestone(apiClient, projectId, workItem, runnerId, r
|
|
|
4422
5084
|
...input
|
|
4423
5085
|
}).catch(() => void 0);
|
|
4424
5086
|
}
|
|
5087
|
+
function logRejectedSettlements(action, settlements) {
|
|
5088
|
+
for (const settlement of settlements) {
|
|
5089
|
+
if (settlement.status === "rejected") {
|
|
5090
|
+
console.error(`${action} failed: ${errorMessage3(settlement.reason)}`);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
4425
5094
|
async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
4426
5095
|
const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
|
|
4427
5096
|
const command = commands.filter((item) => item.status === "pending" || item.status === "acknowledged" || item.status === "running").sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt))[0];
|
|
@@ -4988,10 +5657,10 @@ function parseInvocationChannel(value) {
|
|
|
4988
5657
|
throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
|
|
4989
5658
|
}
|
|
4990
5659
|
function inferRepoName(root) {
|
|
4991
|
-
return
|
|
5660
|
+
return path13.basename(path13.resolve(root)) || "repository";
|
|
4992
5661
|
}
|
|
4993
5662
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
4994
|
-
return
|
|
5663
|
+
return createHash6("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
4995
5664
|
}
|
|
4996
5665
|
function defaultApiUrl() {
|
|
4997
5666
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -5121,8 +5790,9 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
5121
5790
|
return {
|
|
5122
5791
|
version: CLI_VERSION,
|
|
5123
5792
|
mode,
|
|
5124
|
-
hostname:
|
|
5793
|
+
hostname: os7.hostname(),
|
|
5125
5794
|
...runnerIsolationCapabilityMetadata(),
|
|
5795
|
+
resourceUsage: sampleCurrentRunnerResourceUsage(),
|
|
5126
5796
|
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
5127
5797
|
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
5128
5798
|
...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
|
|
@@ -5135,7 +5805,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
5135
5805
|
};
|
|
5136
5806
|
}
|
|
5137
5807
|
function runnerMachineId() {
|
|
5138
|
-
return
|
|
5808
|
+
return createHash6("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
|
|
5139
5809
|
}
|
|
5140
5810
|
async function delay(milliseconds) {
|
|
5141
5811
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|