@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/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 createHash5, randomUUID } from "node:crypto";
5
- import { writeFile as writeFile8 } from "node:fs/promises";
6
- import os5 from "node:os";
7
- import path12 from "node:path";
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 path13 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1621
- return new URL(`${base}${path13}`);
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
- child.on("error", reject);
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
- resolve({ exitCode: exitCode ?? 1, stdout, stderr });
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, approveAll } = await import("@github/copilot-sdk");
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: approveAll
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&apos;");
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 mkdir6, readdir as readdir3, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
2502
- import path7 from "node:path";
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 readFile4(fullPath, "utf8");
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 mkdir6(path7.dirname(fullPath), { recursive: true });
2632
- await writeFile6(fullPath, createSyncedDocumentMarkdown(document), "utf8");
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 readFile4(fullPath, "utf8");
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 = path7.join(rootDir, syncRoot);
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 = path7.join(directory, entry.name);
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 (path7.isAbsolute(repoPath)) {
3061
+ if (path8.isAbsolute(repoPath)) {
2738
3062
  throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
2739
3063
  }
2740
- const normalized = path7.normalize(repoPath);
2741
- if (normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
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 = path7.resolve(rootDir);
2745
- const fullPath = path7.resolve(root, normalized);
2746
- if (!fullPath.startsWith(`${root}${path7.sep}`)) {
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 = path7.normalize(repoPath);
2753
- return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path7.sep}`));
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 path7.relative(rootDir, fullPath).split(path7.sep).join("/");
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 || path7.basename(repoPath, path7.extname(repoPath));
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 mkdir7, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
2790
- import os4 from "node:os";
2791
- import path8 from "node:path";
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 mkdir7(path8.dirname(this.filePath), { recursive: true });
2806
- await writeFile7(this.filePath, JSON.stringify(data, null, 2), "utf8");
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 readFile5(this.filePath, "utf8"));
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 path8.join(os4.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
3142
+ return path9.join(os5.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
2819
3143
  }
2820
3144
  if (process.platform === "win32") {
2821
- return path8.join(process.env.APPDATA ?? os4.homedir(), "Amistio", "tool-sessions.json");
3145
+ return path9.join(process.env.APPDATA ?? os5.homedir(), "Amistio", "tool-sessions.json");
2822
3146
  }
2823
- return path8.join(process.env.XDG_STATE_HOME ?? path8.join(os4.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
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 createHash3 } from "node:crypto";
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 = createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
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 execFile2 } from "node:child_process";
3137
- import { createHash as createHash4 } from "node:crypto";
3138
- import { readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
3139
- import path9 from "node:path";
3140
- import { promisify as promisify2 } from "node:util";
3141
- var execFileAsync2 = promisify2(execFile2);
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 = path9.resolve(rootDir);
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 ?? path9.basename(root)) || "repository";
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 = path9.resolve(options.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 = path9.join(rootDir, ...repoPath.split("/"));
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 readFile6(fullPath, "utf8").catch(() => void 0);
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 = path9.join(directory, entry.name);
3295
- const repoPath = normalizeRepoPath3(path9.relative(rootDir, fullPath));
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 = path9.posix.extname(basePath) || ".md";
3361
- const directory = path9.posix.dirname(basePath);
3362
- const basename = path9.posix.basename(basePath, extension);
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 = path9.posix.basename(repoPath, path9.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
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 createHash4("sha256").update(value).digest("hex").slice(0, length);
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 execFileAsync2("git", args, { maxBuffer: 10 * 1024 * 1024 });
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 spawn3 } from "node:child_process";
3413
- import path10 from "node:path";
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
- path10.resolve(options.root),
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 = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
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 execFile3 } from "node:child_process";
3490
- import { mkdir as mkdir8, stat as stat5 } from "node:fs/promises";
3491
- import path11 from "node:path";
3492
- import { promisify as promisify3 } from "node:util";
3493
- var execFileAsync3 = promisify3(execFile3);
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 mkdir8(path11.dirname(worktreePath), { recursive: true });
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 = path11.basename(repoRoot);
3974
+ const repoName = path12.basename(repoRoot);
3530
3975
  const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
3531
- return path11.join(path11.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
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 execFileAsync3("git", args, { cwd, maxBuffer: 1024 * 1024 });
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 execFileAsync3("git", args, { cwd }).then(() => true, () => false);
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(options.root),
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(options.root, {
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 writeFile8(options.out, prompt, "utf8");
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: path12.resolve(options.root),
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, 300, runnerIsolationCapabilityMetadata());
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 === "blocked") {
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
- return finalizeBrainGenerationWork({
4266
- apiClient,
4267
- durationMs: Date.now() - startedAt,
4268
- projectId,
4269
- repositoryLinkId,
4270
- runnerId,
4271
- sessionContext,
4272
- toolConfig,
4273
- toolName: preview.toolName,
4274
- toolResult,
4275
- workItem: result.workItem
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
- return finalizeAssistantQuestionWork({
4280
- apiClient,
4281
- durationMs: Date.now() - startedAt,
4282
- projectId,
4283
- repositoryLinkId,
4284
- runnerId,
4285
- sessionContext,
4286
- toolConfig,
4287
- toolName: preview.toolName,
4288
- toolResult,
4289
- workItem: result.workItem
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
- return finalizeImpactPreviewWork({
4294
- apiClient,
4295
- durationMs: Date.now() - startedAt,
4296
- projectId,
4297
- repositoryLinkId,
4298
- root,
4299
- runnerId,
4300
- sessionContext,
4301
- toolConfig,
4302
- toolName: preview.toolName,
4303
- toolResult,
4304
- workItem: result.workItem
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 statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, "blocked", `worktree_${workItem.workItemId}_${randomUUID()}`, runnerId, {
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: "blocked",
4382
- summary: message,
4383
- idempotencyKey: `runner_milestone_worktree_blocked_${workItem.workItemId}_${statusResult.workItem.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, "blocked", {
4968
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", {
4387
4969
  ...runnerHeartbeatMetadata(toolConfig),
4388
- currentWorkItemId: workItem.workItemId,
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(message);
4394
- return { status: "blocked", message };
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 path12.basename(path12.resolve(root)) || "repository";
5660
+ return path13.basename(path13.resolve(root)) || "repository";
4992
5661
  }
4993
5662
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
4994
- return createHash5("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
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: os5.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 createHash5("sha256").update(`${os5.hostname()}:${os5.platform()}:${os5.arch()}`).digest("hex").slice(0, 20);
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));