@amistio/cli 0.1.6 → 0.1.7

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({
@@ -1231,8 +1247,11 @@ function credentialKey(accountId, projectId, repositoryLinkId) {
1231
1247
  }
1232
1248
 
1233
1249
  // src/control-plane.ts
1250
+ import { execFile as execFile2 } from "node:child_process";
1234
1251
  import { mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
1235
1252
  import path3 from "node:path";
1253
+ import { promisify as promisify2 } from "node:util";
1254
+ var execFileAsync2 = promisify2(execFile2);
1236
1255
  var controlPlaneFolders = [
1237
1256
  path3.join("docs", "architecture"),
1238
1257
  path3.join("docs", "context"),
@@ -1279,6 +1298,17 @@ async function writeProjectLink(rootDir, metadata) {
1279
1298
  await writeFile2(filePath, createProjectLinkMarkdown(metadata), "utf8");
1280
1299
  return filePath;
1281
1300
  }
1301
+ async function resolvePairingRoot(rootDir, options = {}) {
1302
+ const requestedRoot = path3.resolve(rootDir);
1303
+ const gitRoot = await readGitTopLevel(requestedRoot).catch(() => void 0);
1304
+ if (gitRoot) {
1305
+ return gitRoot;
1306
+ }
1307
+ if (options.explicitRoot) {
1308
+ return requestedRoot;
1309
+ }
1310
+ throw new Error("Run `amistio pair` from inside the repository checkout, or pass --root <repo-root> explicitly.");
1311
+ }
1282
1312
  async function readProjectLink(rootDir) {
1283
1313
  const candidatePaths = [
1284
1314
  path3.join(rootDir, "docs", "context", "amistio-project.md"),
@@ -1327,6 +1357,14 @@ async function exists(filePath) {
1327
1357
  return false;
1328
1358
  }
1329
1359
  }
1360
+ async function readGitTopLevel(rootDir) {
1361
+ const { stdout } = await execFileAsync2("git", ["-C", rootDir, "rev-parse", "--show-toplevel"], { maxBuffer: 1024 * 1024 });
1362
+ const gitRoot = stdout.trim();
1363
+ if (!gitRoot) {
1364
+ throw new Error("Git top-level path was empty.");
1365
+ }
1366
+ return path3.resolve(gitRoot);
1367
+ }
1330
1368
 
1331
1369
  // src/api-client.ts
1332
1370
  import { z as z3 } from "zod";
@@ -1617,8 +1655,8 @@ var toolSessionMutationSchema = z3.object({
1617
1655
  });
1618
1656
  function resolveApiUrl(apiUrl, urlPath) {
1619
1657
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
1620
- const path13 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1621
- return new URL(`${base}${path13}`);
1658
+ const path14 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1659
+ return new URL(`${base}${path14}`);
1622
1660
  }
1623
1661
 
1624
1662
  // src/orchestrator.ts
@@ -2364,6 +2402,221 @@ async function readRunnerDaemonMetadataFile(filePath) {
2364
2402
  }
2365
2403
  }
2366
2404
 
2405
+ // src/runner-service.ts
2406
+ import { spawn as spawn3 } from "node:child_process";
2407
+ import { createHash as createHash3 } from "node:crypto";
2408
+ import { mkdir as mkdir6, readFile as readFile4, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
2409
+ import os4 from "node:os";
2410
+ import path7 from "node:path";
2411
+ function detectRunnerServicePlatform(platform = process.platform) {
2412
+ if (platform === "darwin") return "launchd";
2413
+ if (platform === "linux") return "systemd";
2414
+ return "unsupported";
2415
+ }
2416
+ function createRunnerServiceDescriptor(input) {
2417
+ const platform = input.platform ?? detectRunnerServicePlatform();
2418
+ if (platform === "unsupported") {
2419
+ throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
2420
+ }
2421
+ const homeDir = input.homeDir ?? os4.homedir();
2422
+ const serviceName = runnerServiceName(input);
2423
+ const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
2424
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2425
+ const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
2426
+ const logPath = path7.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
2427
+ const metadata = {
2428
+ schemaVersion: 1,
2429
+ accountId: input.accountId,
2430
+ projectId: input.projectId,
2431
+ repositoryLinkId: input.repositoryLinkId,
2432
+ runnerId: input.runnerId,
2433
+ rootDir: path7.resolve(input.rootDir),
2434
+ apiUrl: input.apiUrl,
2435
+ serviceName,
2436
+ serviceFilePath,
2437
+ platform,
2438
+ status: "installed",
2439
+ createdAt: now,
2440
+ updatedAt: now,
2441
+ args: input.args
2442
+ };
2443
+ return {
2444
+ metadata,
2445
+ content: platform === "launchd" ? createLaunchdPlist({ command, label: serviceName, logPath, rootDir: metadata.rootDir }) : createSystemdUnit({ command, description: `Amistio runner ${input.runnerId}`, logPath, rootDir: metadata.rootDir })
2446
+ };
2447
+ }
2448
+ async function installRunnerService(input, options = {}) {
2449
+ const descriptor = createRunnerServiceDescriptor(input);
2450
+ await mkdir6(path7.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
2451
+ await mkdir6(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
2452
+ await writeFile6(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
2453
+ await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
2454
+ if (options.activate !== false) {
2455
+ const activation = await activateRunnerService(descriptor.metadata);
2456
+ if (!activation.succeeded) {
2457
+ throw new Error(`Startup service file was written to ${descriptor.metadata.serviceFilePath}, but activation failed: ${activation.message}`);
2458
+ }
2459
+ }
2460
+ return descriptor.metadata;
2461
+ }
2462
+ async function removeRunnerService(input) {
2463
+ const metadata = await readRunnerServiceMetadata(input, input.metadataDir);
2464
+ if (!metadata) {
2465
+ return void 0;
2466
+ }
2467
+ await deactivateRunnerService(metadata).catch(() => void 0);
2468
+ await rm2(metadata.serviceFilePath, { force: true });
2469
+ await rm2(runnerServiceMetadataPath(input, input.metadataDir ?? defaultRunnerMetadataDir()), { force: true });
2470
+ return { ...metadata, status: "removed", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2471
+ }
2472
+ async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
2473
+ try {
2474
+ const parsed = JSON.parse(await readFile4(runnerServiceMetadataPath(input, metadataDir), "utf8"));
2475
+ if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
2476
+ return void 0;
2477
+ }
2478
+ return parsed;
2479
+ } catch {
2480
+ return void 0;
2481
+ }
2482
+ }
2483
+ async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
2484
+ await mkdir6(metadataDir, { recursive: true });
2485
+ await writeFile6(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
2486
+ }
2487
+ async function runnerServiceRuntimeStatus(metadata) {
2488
+ if (metadata.platform === "launchd") {
2489
+ const target = launchdTarget(metadata);
2490
+ const result2 = await runProcess("launchctl", ["print", target], 5e3);
2491
+ return result2.exitCode === 0 ? "loaded" : "not loaded";
2492
+ }
2493
+ const result = await runProcess("systemctl", ["--user", "is-active", metadata.serviceName], 5e3);
2494
+ return result.exitCode === 0 ? result.output.trim() || "active" : "not active";
2495
+ }
2496
+ async function activateRunnerService(metadata) {
2497
+ if (metadata.platform === "launchd") {
2498
+ await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 5e3).catch(() => void 0);
2499
+ const result = await runProcess("launchctl", ["bootstrap", launchdDomain(), metadata.serviceFilePath], 1e4);
2500
+ return result.exitCode === 0 ? { succeeded: true, message: "launchd service loaded." } : { succeeded: false, message: result.output || `launchctl exited with ${result.exitCode}.` };
2501
+ }
2502
+ const reload = await runProcess("systemctl", ["--user", "daemon-reload"], 1e4);
2503
+ if (reload.exitCode !== 0) {
2504
+ return { succeeded: false, message: reload.output || `systemctl daemon-reload exited with ${reload.exitCode}.` };
2505
+ }
2506
+ const enable = await runProcess("systemctl", ["--user", "enable", "--now", metadata.serviceName], 2e4);
2507
+ return enable.exitCode === 0 ? { succeeded: true, message: "systemd user service enabled." } : { succeeded: false, message: enable.output || `systemctl enable exited with ${enable.exitCode}.` };
2508
+ }
2509
+ async function deactivateRunnerService(metadata) {
2510
+ if (metadata.platform === "launchd") {
2511
+ await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 1e4);
2512
+ return;
2513
+ }
2514
+ await runProcess("systemctl", ["--user", "disable", "--now", metadata.serviceName], 2e4);
2515
+ await runProcess("systemctl", ["--user", "daemon-reload"], 1e4).catch(() => void 0);
2516
+ }
2517
+ function createLaunchdPlist(input) {
2518
+ const commandItems = input.command.map((item) => ` <string>${xmlEscape(item)}</string>`).join("\n");
2519
+ return `<?xml version="1.0" encoding="UTF-8"?>
2520
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2521
+ <plist version="1.0">
2522
+ <dict>
2523
+ <key>Label</key>
2524
+ <string>${xmlEscape(input.label)}</string>
2525
+ <key>ProgramArguments</key>
2526
+ <array>
2527
+ ${commandItems}
2528
+ </array>
2529
+ <key>WorkingDirectory</key>
2530
+ <string>${xmlEscape(input.rootDir)}</string>
2531
+ <key>EnvironmentVariables</key>
2532
+ <dict>
2533
+ <key>AMISTIO_RUNNER_MODE</key>
2534
+ <string>background</string>
2535
+ </dict>
2536
+ <key>RunAtLoad</key>
2537
+ <true/>
2538
+ <key>KeepAlive</key>
2539
+ <true/>
2540
+ <key>StandardOutPath</key>
2541
+ <string>${xmlEscape(input.logPath)}</string>
2542
+ <key>StandardErrorPath</key>
2543
+ <string>${xmlEscape(input.logPath)}</string>
2544
+ </dict>
2545
+ </plist>
2546
+ `;
2547
+ }
2548
+ function createSystemdUnit(input) {
2549
+ return `[Unit]
2550
+ Description=${input.description}
2551
+
2552
+ [Service]
2553
+ Type=simple
2554
+ WorkingDirectory=${systemdEscape(input.rootDir)}
2555
+ Environment=AMISTIO_RUNNER_MODE=background
2556
+ ExecStart=${input.command.map(systemdEscape).join(" ")}
2557
+ Restart=always
2558
+ RestartSec=5
2559
+ StandardOutput=append:${systemdEscape(input.logPath)}
2560
+ StandardError=append:${systemdEscape(input.logPath)}
2561
+
2562
+ [Install]
2563
+ WantedBy=default.target
2564
+ `;
2565
+ }
2566
+ function runnerServiceFilePath(platform, serviceName, homeDir) {
2567
+ if (platform === "launchd") {
2568
+ return path7.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
2569
+ }
2570
+ return path7.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
2571
+ }
2572
+ function runnerServiceMetadataPath(input, metadataDir) {
2573
+ return path7.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
2574
+ }
2575
+ function runnerServiceName(input) {
2576
+ return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
2577
+ }
2578
+ function runnerServiceKey(input) {
2579
+ return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
2580
+ }
2581
+ function launchdDomain() {
2582
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
2583
+ return uid === void 0 ? "gui/0" : `gui/${uid}`;
2584
+ }
2585
+ function launchdTarget(metadata) {
2586
+ return `${launchdDomain()}/${metadata.serviceName}`;
2587
+ }
2588
+ function xmlEscape(value) {
2589
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&apos;");
2590
+ }
2591
+ function systemdEscape(value) {
2592
+ return value.includes(" ") || value.includes(" ") || value.includes('"') ? `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"')}"` : value;
2593
+ }
2594
+ function runProcess(command, args, timeoutMs) {
2595
+ return new Promise((resolve) => {
2596
+ const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
2597
+ let output = "";
2598
+ const timeout = setTimeout(() => {
2599
+ output += `Timed out while running ${command}.
2600
+ `;
2601
+ child.kill("SIGTERM");
2602
+ }, timeoutMs);
2603
+ child.stdout?.on("data", (chunk) => {
2604
+ output += chunk.toString("utf8");
2605
+ });
2606
+ child.stderr?.on("data", (chunk) => {
2607
+ output += chunk.toString("utf8");
2608
+ });
2609
+ child.on("error", (error) => {
2610
+ clearTimeout(timeout);
2611
+ resolve({ exitCode: 1, output: error.message });
2612
+ });
2613
+ child.on("close", (code) => {
2614
+ clearTimeout(timeout);
2615
+ resolve({ exitCode: code ?? 1, output: output.trim() });
2616
+ });
2617
+ });
2618
+ }
2619
+
2367
2620
  // src/session-policy.ts
2368
2621
  var maxIdleMs = 24 * 60 * 60 * 1e3;
2369
2622
  var maxTotalMs = 7 * 24 * 60 * 60 * 1e3;
@@ -2498,8 +2751,8 @@ function tokens(value) {
2498
2751
  }
2499
2752
 
2500
2753
  // 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";
2754
+ import { mkdir as mkdir7, readdir as readdir3, readFile as readFile5, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
2755
+ import path8 from "node:path";
2503
2756
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
2504
2757
  var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
2505
2758
  async function collectSyncStatus(rootDir, webDocuments = []) {
@@ -2578,7 +2831,7 @@ async function readLocalSyncedDocuments(rootDir) {
2578
2831
  const markdownFiles = await findMarkdownFiles(rootDir);
2579
2832
  const documents = [];
2580
2833
  for (const fullPath of markdownFiles) {
2581
- const raw = await readFile4(fullPath, "utf8");
2834
+ const raw = await readFile5(fullPath, "utf8");
2582
2835
  const parsed = parseSyncedMarkdown(raw);
2583
2836
  if (!parsed) {
2584
2837
  continue;
@@ -2628,8 +2881,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
2628
2881
  result.skipped.push(document.repoPath);
2629
2882
  continue;
2630
2883
  }
2631
- await mkdir6(path7.dirname(fullPath), { recursive: true });
2632
- await writeFile6(fullPath, createSyncedDocumentMarkdown(document), "utf8");
2884
+ await mkdir7(path8.dirname(fullPath), { recursive: true });
2885
+ await writeFile7(fullPath, createSyncedDocumentMarkdown(document), "utf8");
2633
2886
  result.written.push(document.repoPath);
2634
2887
  }
2635
2888
  return result;
@@ -2690,7 +2943,7 @@ function parseSyncedMarkdown(content) {
2690
2943
  }
2691
2944
  async function readExistingSyncedDocument(fullPath) {
2692
2945
  try {
2693
- const raw = await readFile4(fullPath, "utf8");
2946
+ const raw = await readFile5(fullPath, "utf8");
2694
2947
  const parsed = parseSyncedMarkdown(raw);
2695
2948
  if (!parsed) {
2696
2949
  return { exists: true };
@@ -2715,7 +2968,7 @@ async function readExistingSyncedDocument(fullPath) {
2715
2968
  async function findMarkdownFiles(rootDir) {
2716
2969
  const files = [];
2717
2970
  for (const syncRoot of syncRoots) {
2718
- const fullRoot = path7.join(rootDir, syncRoot);
2971
+ const fullRoot = path8.join(rootDir, syncRoot);
2719
2972
  if (!await exists2(fullRoot)) {
2720
2973
  continue;
2721
2974
  }
@@ -2725,7 +2978,7 @@ async function findMarkdownFiles(rootDir) {
2725
2978
  }
2726
2979
  async function walkMarkdownFiles(directory, files) {
2727
2980
  for (const entry of await readdir3(directory, { withFileTypes: true })) {
2728
- const fullPath = path7.join(directory, entry.name);
2981
+ const fullPath = path8.join(directory, entry.name);
2729
2982
  if (entry.isDirectory()) {
2730
2983
  await walkMarkdownFiles(fullPath, files);
2731
2984
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -2734,23 +2987,23 @@ async function walkMarkdownFiles(directory, files) {
2734
2987
  }
2735
2988
  }
2736
2989
  function safeRepoPath(rootDir, repoPath) {
2737
- if (path7.isAbsolute(repoPath)) {
2990
+ if (path8.isAbsolute(repoPath)) {
2738
2991
  throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
2739
2992
  }
2740
- const normalized = path7.normalize(repoPath);
2741
- if (normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
2993
+ const normalized = path8.normalize(repoPath);
2994
+ if (normalized === ".." || normalized.startsWith(`..${path8.sep}`)) {
2742
2995
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
2743
2996
  }
2744
- const root = path7.resolve(rootDir);
2745
- const fullPath = path7.resolve(root, normalized);
2746
- if (!fullPath.startsWith(`${root}${path7.sep}`)) {
2997
+ const root = path8.resolve(rootDir);
2998
+ const fullPath = path8.resolve(root, normalized);
2999
+ if (!fullPath.startsWith(`${root}${path8.sep}`)) {
2747
3000
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
2748
3001
  }
2749
3002
  return fullPath;
2750
3003
  }
2751
3004
  function isControlPlanePath(repoPath) {
2752
- const normalized = path7.normalize(repoPath);
2753
- return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path7.sep}`));
3005
+ const normalized = path8.normalize(repoPath);
3006
+ return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`));
2754
3007
  }
2755
3008
  function canonicalControlPlaneRepoPath(repoPath) {
2756
3009
  const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -2761,11 +3014,11 @@ function canonicalControlPlaneRepoPath(repoPath) {
2761
3014
  return normalized;
2762
3015
  }
2763
3016
  function toRepoPath(rootDir, fullPath) {
2764
- return path7.relative(rootDir, fullPath).split(path7.sep).join("/");
3017
+ return path8.relative(rootDir, fullPath).split(path8.sep).join("/");
2765
3018
  }
2766
3019
  function inferTitle(content, repoPath) {
2767
3020
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
2768
- return heading || path7.basename(repoPath, path7.extname(repoPath));
3021
+ return heading || path8.basename(repoPath, path8.extname(repoPath));
2769
3022
  }
2770
3023
  function parseFrontmatterFromSyncedDocument(frontmatter) {
2771
3024
  return {
@@ -2786,9 +3039,9 @@ async function exists2(filePath) {
2786
3039
  }
2787
3040
 
2788
3041
  // 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";
3042
+ import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
3043
+ import os5 from "node:os";
3044
+ import path9 from "node:path";
2792
3045
  var LocalToolSessionStore = class {
2793
3046
  constructor(filePath = defaultSessionStorePath()) {
2794
3047
  this.filePath = filePath;
@@ -2802,12 +3055,12 @@ var LocalToolSessionStore = class {
2802
3055
  async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
2803
3056
  const data = await this.read();
2804
3057
  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");
3058
+ await mkdir8(path9.dirname(this.filePath), { recursive: true });
3059
+ await writeFile8(this.filePath, JSON.stringify(data, null, 2), "utf8");
2807
3060
  }
2808
3061
  async read() {
2809
3062
  try {
2810
- return JSON.parse(await readFile5(this.filePath, "utf8"));
3063
+ return JSON.parse(await readFile6(this.filePath, "utf8"));
2811
3064
  } catch {
2812
3065
  return {};
2813
3066
  }
@@ -2815,12 +3068,12 @@ var LocalToolSessionStore = class {
2815
3068
  };
2816
3069
  function defaultSessionStorePath() {
2817
3070
  if (process.platform === "darwin") {
2818
- return path8.join(os4.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
3071
+ return path9.join(os5.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
2819
3072
  }
2820
3073
  if (process.platform === "win32") {
2821
- return path8.join(process.env.APPDATA ?? os4.homedir(), "Amistio", "tool-sessions.json");
3074
+ return path9.join(process.env.APPDATA ?? os5.homedir(), "Amistio", "tool-sessions.json");
2822
3075
  }
2823
- return path8.join(process.env.XDG_STATE_HOME ?? path8.join(os4.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
3076
+ return path9.join(process.env.XDG_STATE_HOME ?? path9.join(os5.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
2824
3077
  }
2825
3078
 
2826
3079
  // src/work-runner.ts
@@ -3107,7 +3360,7 @@ function stripJsonFence(value) {
3107
3360
  }
3108
3361
 
3109
3362
  // src/runner-status.ts
3110
- import { createHash as createHash3 } from "node:crypto";
3363
+ import { createHash as createHash4 } from "node:crypto";
3111
3364
  var watchStateReminderMs = 60 * 1e3;
3112
3365
  function formatWatchStartupContext(input) {
3113
3366
  return [
@@ -3128,17 +3381,136 @@ function watchStateKey(action) {
3128
3381
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
3129
3382
  }
3130
3383
  function stableRunnerId(input) {
3131
- const digest = createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
3384
+ const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
3132
3385
  return `runner_${digest}`;
3133
3386
  }
3134
3387
 
3388
+ // src/runner-resources.ts
3389
+ import os6 from "node:os";
3390
+ var defaultRuntime = {
3391
+ nowMs: () => Date.now(),
3392
+ memoryUsage: () => process.memoryUsage(),
3393
+ uptime: () => process.uptime(),
3394
+ cpuUsage: () => process.cpuUsage(),
3395
+ totalmem: () => os6.totalmem(),
3396
+ freemem: () => os6.freemem(),
3397
+ loadavg: () => os6.loadavg()
3398
+ };
3399
+ var previousRunnerResourceSample;
3400
+ function sampleCurrentRunnerResourceUsage() {
3401
+ const sample = collectRunnerResourceUsage(previousRunnerResourceSample);
3402
+ previousRunnerResourceSample = sample.state;
3403
+ return sample.resourceUsage;
3404
+ }
3405
+ function collectRunnerResourceUsage(previous, runtime = defaultRuntime) {
3406
+ const sampledAtMs = runtime.nowMs();
3407
+ const memory = runtime.memoryUsage();
3408
+ const cpu = runtime.cpuUsage();
3409
+ const load = runtime.loadavg();
3410
+ const totalMemory = runtime.totalmem();
3411
+ const freeMemory = runtime.freemem();
3412
+ const resourceUsage = {
3413
+ sampledAt: new Date(sampledAtMs).toISOString(),
3414
+ processUptimeSeconds: roundNumber(runtime.uptime(), 3),
3415
+ processMemoryRssBytes: Math.round(memory.rss),
3416
+ processMemoryHeapUsedBytes: Math.round(memory.heapUsed),
3417
+ processMemoryHeapTotalBytes: Math.round(memory.heapTotal),
3418
+ processCpuUserMicros: Math.round(cpu.user),
3419
+ processCpuSystemMicros: Math.round(cpu.system),
3420
+ systemMemoryTotalBytes: Math.round(totalMemory),
3421
+ systemMemoryFreeBytes: Math.round(freeMemory)
3422
+ };
3423
+ const cpuPercent = processCpuPercent(previous, { sampledAtMs, processCpuUserMicros: cpu.user, processCpuSystemMicros: cpu.system });
3424
+ if (cpuPercent !== void 0) {
3425
+ resourceUsage.processCpuPercent = cpuPercent;
3426
+ }
3427
+ if (load.length >= 3) {
3428
+ resourceUsage.systemLoadAverage1m = roundNumber(load[0], 2);
3429
+ resourceUsage.systemLoadAverage5m = roundNumber(load[1], 2);
3430
+ resourceUsage.systemLoadAverage15m = roundNumber(load[2], 2);
3431
+ }
3432
+ return {
3433
+ resourceUsage,
3434
+ state: {
3435
+ sampledAtMs,
3436
+ processCpuUserMicros: cpu.user,
3437
+ processCpuSystemMicros: cpu.system
3438
+ }
3439
+ };
3440
+ }
3441
+ function formatRunnerResourceUsage(resourceUsage) {
3442
+ if (!resourceUsage) {
3443
+ return "unavailable until this runner reports a sample.";
3444
+ }
3445
+ const parts = [
3446
+ resourceUsage.processMemoryRssBytes !== void 0 ? `RSS ${formatBytes(resourceUsage.processMemoryRssBytes)}` : void 0,
3447
+ heapSummary(resourceUsage),
3448
+ resourceUsage.processCpuPercent !== void 0 ? `CPU ${formatPercent(resourceUsage.processCpuPercent)}` : "CPU warming up",
3449
+ systemMemorySummary(resourceUsage),
3450
+ loadSummary(resourceUsage),
3451
+ `sampled ${resourceUsage.sampledAt}`
3452
+ ].filter(Boolean);
3453
+ return parts.length ? parts.join("; ") : "unavailable until this runner reports a complete sample.";
3454
+ }
3455
+ function formatBytes(bytes) {
3456
+ if (bytes === void 0 || !Number.isFinite(bytes)) {
3457
+ return "unknown";
3458
+ }
3459
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
3460
+ let value = Math.max(0, bytes);
3461
+ let unitIndex = 0;
3462
+ while (value >= 1024 && unitIndex < units.length - 1) {
3463
+ value /= 1024;
3464
+ unitIndex += 1;
3465
+ }
3466
+ const digits = Number.isInteger(value) || value >= 10 || unitIndex === 0 ? 0 : 1;
3467
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
3468
+ }
3469
+ function formatPercent(value) {
3470
+ if (value === void 0 || !Number.isFinite(value)) {
3471
+ return "unknown";
3472
+ }
3473
+ return `${roundNumber(value, 1).toFixed(1)}%`;
3474
+ }
3475
+ function processCpuPercent(previous, current) {
3476
+ if (!previous) return void 0;
3477
+ const elapsedMicros = (current.sampledAtMs - previous.sampledAtMs) * 1e3;
3478
+ const cpuDeltaMicros = current.processCpuUserMicros + current.processCpuSystemMicros - previous.processCpuUserMicros - previous.processCpuSystemMicros;
3479
+ if (elapsedMicros <= 0 || cpuDeltaMicros < 0) return void 0;
3480
+ return roundNumber(cpuDeltaMicros / elapsedMicros * 100, 1);
3481
+ }
3482
+ function heapSummary(resourceUsage) {
3483
+ if (resourceUsage.processMemoryHeapUsedBytes === void 0 && resourceUsage.processMemoryHeapTotalBytes === void 0) {
3484
+ return void 0;
3485
+ }
3486
+ return `heap ${formatBytes(resourceUsage.processMemoryHeapUsedBytes)} / ${formatBytes(resourceUsage.processMemoryHeapTotalBytes)}`;
3487
+ }
3488
+ function systemMemorySummary(resourceUsage) {
3489
+ if (resourceUsage.systemMemoryTotalBytes === void 0 || resourceUsage.systemMemoryFreeBytes === void 0 || resourceUsage.systemMemoryTotalBytes <= 0) {
3490
+ return void 0;
3491
+ }
3492
+ const usedBytes = Math.max(0, resourceUsage.systemMemoryTotalBytes - resourceUsage.systemMemoryFreeBytes);
3493
+ const usedPercent = usedBytes / resourceUsage.systemMemoryTotalBytes * 100;
3494
+ return `system memory ${formatBytes(usedBytes)} / ${formatBytes(resourceUsage.systemMemoryTotalBytes)} used (${formatPercent(usedPercent)})`;
3495
+ }
3496
+ function loadSummary(resourceUsage) {
3497
+ if (resourceUsage.systemLoadAverage1m === void 0 || resourceUsage.systemLoadAverage5m === void 0 || resourceUsage.systemLoadAverage15m === void 0) {
3498
+ return void 0;
3499
+ }
3500
+ return `load ${resourceUsage.systemLoadAverage1m.toFixed(2)} / ${resourceUsage.systemLoadAverage5m.toFixed(2)} / ${resourceUsage.systemLoadAverage15m.toFixed(2)}`;
3501
+ }
3502
+ function roundNumber(value, digits) {
3503
+ const factor = 10 ** digits;
3504
+ return Math.round(value * factor) / factor;
3505
+ }
3506
+
3135
3507
  // 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);
3508
+ import { execFile as execFile3 } from "node:child_process";
3509
+ import { createHash as createHash5 } from "node:crypto";
3510
+ import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
3511
+ import path10 from "node:path";
3512
+ import { promisify as promisify3 } from "node:util";
3513
+ var execFileAsync3 = promisify3(execFile3);
3142
3514
  var defaultMaxFileKb = 256;
3143
3515
  var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
3144
3516
  var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
@@ -3155,12 +3527,12 @@ var documentFolderByType = {
3155
3527
  workflow: "docs/workflows"
3156
3528
  };
3157
3529
  async function inspectLocalRepository(rootDir, defaultBranch) {
3158
- const requestedRoot = path9.resolve(rootDir);
3530
+ const requestedRoot = path10.resolve(rootDir);
3159
3531
  const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
3160
3532
  const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
3161
3533
  const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
3162
3534
  const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
3163
- const repoName = (parsedCloneUrl?.repoName ?? path9.basename(root)) || "repository";
3535
+ const repoName = (parsedCloneUrl?.repoName ?? path10.basename(root)) || "repository";
3164
3536
  const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
3165
3537
  return {
3166
3538
  rootDir: root,
@@ -3172,7 +3544,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
3172
3544
  };
3173
3545
  }
3174
3546
  async function scanLegacyDocuments(options) {
3175
- const rootDir = path9.resolve(options.rootDir);
3547
+ const rootDir = path10.resolve(options.rootDir);
3176
3548
  const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
3177
3549
  const skipped = [];
3178
3550
  const candidates = [];
@@ -3191,7 +3563,7 @@ async function scanLegacyDocuments(options) {
3191
3563
  skipped.push({ repoPath, reason: "excluded" });
3192
3564
  continue;
3193
3565
  }
3194
- const fullPath = path9.join(rootDir, ...repoPath.split("/"));
3566
+ const fullPath = path10.join(rootDir, ...repoPath.split("/"));
3195
3567
  const fileStat = await stat4(fullPath).catch(() => void 0);
3196
3568
  if (!fileStat?.isFile()) {
3197
3569
  skipped.push({ repoPath, reason: "unreadable" });
@@ -3201,7 +3573,7 @@ async function scanLegacyDocuments(options) {
3201
3573
  skipped.push({ repoPath, reason: "tooLarge" });
3202
3574
  continue;
3203
3575
  }
3204
- const content = await readFile6(fullPath, "utf8").catch(() => void 0);
3576
+ const content = await readFile7(fullPath, "utf8").catch(() => void 0);
3205
3577
  if (content === void 0) {
3206
3578
  skipped.push({ repoPath, reason: "unreadable" });
3207
3579
  continue;
@@ -3291,8 +3663,8 @@ async function listRepositoryPaths(rootDir) {
3291
3663
  async function walkRepository(rootDir, directory, files) {
3292
3664
  const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
3293
3665
  for (const entry of entries) {
3294
- const fullPath = path9.join(directory, entry.name);
3295
- const repoPath = normalizeRepoPath3(path9.relative(rootDir, fullPath));
3666
+ const fullPath = path10.join(directory, entry.name);
3667
+ const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
3296
3668
  if (entry.isDirectory()) {
3297
3669
  if (!excludedDirectoryNames.has(entry.name)) {
3298
3670
  await walkRepository(rootDir, fullPath, files);
@@ -3357,9 +3729,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
3357
3729
  usedPaths.add(basePath);
3358
3730
  return basePath;
3359
3731
  }
3360
- const extension = path9.posix.extname(basePath) || ".md";
3361
- const directory = path9.posix.dirname(basePath);
3362
- const basename = path9.posix.basename(basePath, extension);
3732
+ const extension = path10.posix.extname(basePath) || ".md";
3733
+ const directory = path10.posix.dirname(basePath);
3734
+ const basename = path10.posix.basename(basePath, extension);
3363
3735
  const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
3364
3736
  usedPaths.add(uniquePath);
3365
3737
  return uniquePath;
@@ -3376,7 +3748,7 @@ function inferTitle2(content, repoPath) {
3376
3748
  const body = stripFrontmatter(content);
3377
3749
  const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
3378
3750
  if (heading) return heading;
3379
- const basename = path9.posix.basename(repoPath, path9.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
3751
+ const basename = path10.posix.basename(repoPath, path10.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
3380
3752
  return titleCase(basename || "Imported Document");
3381
3753
  }
3382
3754
  function stripFrontmatter(content) {
@@ -3398,19 +3770,19 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
3398
3770
  return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
3399
3771
  }
3400
3772
  function hashText(value, length) {
3401
- return createHash4("sha256").update(value).digest("hex").slice(0, length);
3773
+ return createHash5("sha256").update(value).digest("hex").slice(0, length);
3402
3774
  }
3403
3775
  function normalizeRepoPath3(value) {
3404
3776
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
3405
3777
  }
3406
3778
  async function runGit2(args) {
3407
- const { stdout } = await execFileAsync2("git", args, { maxBuffer: 10 * 1024 * 1024 });
3779
+ const { stdout } = await execFileAsync3("git", args, { maxBuffer: 10 * 1024 * 1024 });
3408
3780
  return stdout.trim();
3409
3781
  }
3410
3782
 
3411
3783
  // src/runner-actions.ts
3412
- import { spawn as spawn3 } from "node:child_process";
3413
- import path10 from "node:path";
3784
+ import { spawn as spawn4 } from "node:child_process";
3785
+ import path11 from "node:path";
3414
3786
  function buildBackgroundRunnerArgs(options) {
3415
3787
  const args = [
3416
3788
  "run",
@@ -3420,7 +3792,7 @@ function buildBackgroundRunnerArgs(options) {
3420
3792
  "--runner-id",
3421
3793
  options.runnerId,
3422
3794
  "--root",
3423
- path10.resolve(options.root),
3795
+ path11.resolve(options.root),
3424
3796
  "--session",
3425
3797
  options.session,
3426
3798
  "--interval-seconds",
@@ -3458,7 +3830,7 @@ async function runOfficialCliUpdate() {
3458
3830
  }
3459
3831
  function runOfficialUpdateProcess(command, args, timeoutMs) {
3460
3832
  return new Promise((resolve) => {
3461
- const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
3833
+ const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
3462
3834
  let output = "";
3463
3835
  const updateTimeout = setTimeout(() => {
3464
3836
  output += "Timed out while running official CLI update.\n";
@@ -3486,11 +3858,11 @@ function truncateProcessOutput(value) {
3486
3858
  }
3487
3859
 
3488
3860
  // 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);
3861
+ import { execFile as execFile4 } from "node:child_process";
3862
+ import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
3863
+ import path12 from "node:path";
3864
+ import { promisify as promisify4 } from "node:util";
3865
+ var execFileAsync4 = promisify4(execFile4);
3494
3866
  function needsGitWorktreeIsolation(workItem) {
3495
3867
  return (workItem.workKind ?? "implementation") === "implementation";
3496
3868
  }
@@ -3517,7 +3889,7 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
3517
3889
  await assertExistingWorktree(worktreePath, identity.branch);
3518
3890
  return { ...identity, baseRevision, worktreePath };
3519
3891
  }
3520
- await mkdir8(path11.dirname(worktreePath), { recursive: true });
3892
+ await mkdir9(path12.dirname(worktreePath), { recursive: true });
3521
3893
  const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
3522
3894
  const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
3523
3895
  await gitOutput(repoRoot, worktreeArgs).catch((error) => {
@@ -3526,9 +3898,9 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
3526
3898
  return { ...identity, baseRevision, worktreePath };
3527
3899
  }
3528
3900
  function localWorktreePath(repoRoot, worktreeKey) {
3529
- const repoName = path11.basename(repoRoot);
3901
+ const repoName = path12.basename(repoRoot);
3530
3902
  const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
3531
- return path11.join(path11.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
3903
+ return path12.join(path12.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
3532
3904
  }
3533
3905
  async function assertExistingWorktree(worktreePath, branch) {
3534
3906
  await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
@@ -3551,11 +3923,11 @@ async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
3551
3923
  }
3552
3924
  }
3553
3925
  async function gitOutput(cwd, args) {
3554
- const { stdout } = await execFileAsync3("git", args, { cwd, maxBuffer: 1024 * 1024 });
3926
+ const { stdout } = await execFileAsync4("git", args, { cwd, maxBuffer: 1024 * 1024 });
3555
3927
  return stdout.trim();
3556
3928
  }
3557
3929
  async function gitCommandSucceeds(cwd, args) {
3558
- return execFileAsync3("git", args, { cwd }).then(() => true, () => false);
3930
+ return execFileAsync4("git", args, { cwd }).then(() => true, () => false);
3559
3931
  }
3560
3932
  async function pathExists(value) {
3561
3933
  return stat5(value).then(() => true, () => false);
@@ -3698,6 +4070,7 @@ program.command("import").description("Pair an existing checkout and import lega
3698
4070
  console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
3699
4071
  });
3700
4072
  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) => {
4073
+ const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
3701
4074
  let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
3702
4075
  let credential = options.token;
3703
4076
  if (options.pairingCode) {
@@ -3709,7 +4082,7 @@ program.command("pair").description("Pair this repository with an Amistio web pr
3709
4082
  projectId: options.project,
3710
4083
  pairingCode: options.pairingCode,
3711
4084
  repositoryLinkId,
3712
- repoName: inferRepoName(options.root),
4085
+ repoName: inferRepoName(pairingRoot),
3713
4086
  repoFingerprint: createRepoFingerprint(options.account, options.project, repositoryLinkId),
3714
4087
  defaultBranch: options.defaultBranch
3715
4088
  });
@@ -3717,7 +4090,7 @@ program.command("pair").description("Pair this repository with an Amistio web pr
3717
4090
  credential = credential ?? pairing.token;
3718
4091
  console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}.`);
3719
4092
  }
3720
- const filePath = await writeProjectLink(options.root, {
4093
+ const filePath = await writeProjectLink(pairingRoot, {
3721
4094
  amistioAccountId: options.account,
3722
4095
  amistioProjectId: options.project,
3723
4096
  repositoryLinkId,
@@ -3844,7 +4217,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
3844
4217
  }
3845
4218
  const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
3846
4219
  if (options.out) {
3847
- await writeFile8(options.out, prompt, "utf8");
4220
+ await writeFile9(options.out, prompt, "utf8");
3848
4221
  console.log(`Wrote work prompt to ${options.out}.`);
3849
4222
  } else {
3850
4223
  console.log(prompt);
@@ -3921,7 +4294,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
3921
4294
  projectId: context.metadata.amistioProjectId,
3922
4295
  repositoryLinkId: context.metadata.repositoryLinkId,
3923
4296
  runnerId,
3924
- rootDir: path12.resolve(options.root),
4297
+ rootDir: path13.resolve(options.root),
3925
4298
  apiUrl: options.apiUrl,
3926
4299
  args: buildBackgroundRunnerArgs(resolvedOptions)
3927
4300
  });
@@ -4006,6 +4379,7 @@ runner.command("status").description("Show background runner status for the pair
4006
4379
  console.log("No background runner metadata found for this paired repository.");
4007
4380
  if (runners.length) {
4008
4381
  console.log(`Last runner heartbeat: ${runners[0].runnerId} ${runners[0].status} at ${runners[0].lastSeenAt}.`);
4382
+ console.log(`Resource usage: ${formatRunnerResourceUsage(runners[0].resourceUsage)}`);
4009
4383
  }
4010
4384
  return;
4011
4385
  }
@@ -4026,6 +4400,7 @@ runner.command("status").description("Show background runner status for the pair
4026
4400
  if (heartbeat) {
4027
4401
  console.log(` Last heartbeat: ${heartbeat.status} at ${heartbeat.lastSeenAt}${heartbeat.version ? ` (${heartbeat.version})` : ""}`);
4028
4402
  }
4403
+ console.log(` Resource usage: ${formatRunnerResourceUsage(heartbeat?.resourceUsage)}`);
4029
4404
  }
4030
4405
  });
4031
4406
  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 +4425,106 @@ runner.command("stop").description("Stop a background runner for the paired repo
4050
4425
  return;
4051
4426
  }
4052
4427
  const record = records[0];
4428
+ 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
4429
  const stopResult = await stopRunnerDaemonProcess(record);
4054
4430
  await markRunnerDaemonStopped(record);
4055
4431
  await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, record.runnerId, context.metadata.repositoryLinkId, "offline", {
4056
4432
  version: CLI_VERSION,
4057
4433
  mode: "background",
4058
- hostname: record.hostname
4434
+ hostname: record.hostname,
4435
+ ...existingRunner?.resourceUsage ? { resourceUsage: existingRunner.resourceUsage } : {}
4059
4436
  }).catch(() => void 0);
4060
4437
  console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
4061
4438
  });
4439
+ var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
4440
+ 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("--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) => {
4441
+ const context = await loadPairedApiContext(options.root, options.apiUrl);
4442
+ if (!context) {
4443
+ console.log("Repository is not paired. Run `amistio pair` first.");
4444
+ return;
4445
+ }
4446
+ if (!context.token) {
4447
+ console.log("No local runner credential found. Run `amistio pair --pairing-code <code>` to store this machine credential.");
4448
+ process.exitCode = 1;
4449
+ return;
4450
+ }
4451
+ const platform = detectRunnerServicePlatform();
4452
+ if (platform === "unsupported") {
4453
+ console.log("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
4454
+ process.exitCode = 1;
4455
+ return;
4456
+ }
4457
+ const runnerId = options.runnerId ?? stableRunnerId({
4458
+ accountId: context.metadata.amistioAccountId,
4459
+ projectId: context.metadata.amistioProjectId,
4460
+ repositoryLinkId: context.metadata.repositoryLinkId,
4461
+ machineId: runnerMachineId()
4462
+ });
4463
+ const args = buildBackgroundRunnerArgs({ ...options, runnerId, apiUrl: options.apiUrl, root: options.root });
4464
+ const serviceInput = {
4465
+ accountId: context.metadata.amistioAccountId,
4466
+ projectId: context.metadata.amistioProjectId,
4467
+ repositoryLinkId: context.metadata.repositoryLinkId,
4468
+ runnerId,
4469
+ rootDir: path13.resolve(options.root),
4470
+ apiUrl: options.apiUrl,
4471
+ args,
4472
+ platform
4473
+ };
4474
+ if (options.dryRun) {
4475
+ const descriptor = createRunnerServiceDescriptor(serviceInput);
4476
+ console.log(`Startup service file: ${descriptor.metadata.serviceFilePath}`);
4477
+ console.log(descriptor.content.trim());
4478
+ return;
4479
+ }
4480
+ try {
4481
+ const metadata = await installRunnerService(serviceInput);
4482
+ console.log(`Installed startup service ${metadata.serviceName}.`);
4483
+ console.log(`Service file: ${metadata.serviceFilePath}`);
4484
+ } catch (error) {
4485
+ console.error(errorMessage3(error));
4486
+ process.exitCode = 1;
4487
+ }
4488
+ });
4489
+ 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) => {
4490
+ const context = await loadPairedApiContext(options.root, options.apiUrl);
4491
+ if (!context) {
4492
+ console.log("Repository is not paired. Run `amistio pair` first.");
4493
+ return;
4494
+ }
4495
+ const runnerId = options.runnerId ?? stableRunnerId({
4496
+ accountId: context.metadata.amistioAccountId,
4497
+ projectId: context.metadata.amistioProjectId,
4498
+ repositoryLinkId: context.metadata.repositoryLinkId,
4499
+ machineId: runnerMachineId()
4500
+ });
4501
+ const metadata = await readRunnerServiceMetadata({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
4502
+ if (!metadata) {
4503
+ console.log("No startup service metadata found for this paired repository runner.");
4504
+ return;
4505
+ }
4506
+ const runtimeStatus = await runnerServiceRuntimeStatus(metadata);
4507
+ console.log(`Startup service ${metadata.serviceName}: ${runtimeStatus}`);
4508
+ console.log(` Platform: ${metadata.platform}`);
4509
+ console.log(` File: ${metadata.serviceFilePath}`);
4510
+ console.log(` Root: ${metadata.rootDir}`);
4511
+ console.log(` API: ${metadata.apiUrl}`);
4512
+ });
4513
+ 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) => {
4514
+ const context = await loadPairedApiContext(options.root, options.apiUrl);
4515
+ if (!context) {
4516
+ console.log("Repository is not paired. Run `amistio pair` first.");
4517
+ return;
4518
+ }
4519
+ const runnerId = options.runnerId ?? stableRunnerId({
4520
+ accountId: context.metadata.amistioAccountId,
4521
+ projectId: context.metadata.amistioProjectId,
4522
+ repositoryLinkId: context.metadata.repositoryLinkId,
4523
+ machineId: runnerMachineId()
4524
+ });
4525
+ const removed = await removeRunnerService({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
4526
+ console.log(removed ? `Removed startup service ${removed.serviceName}.` : "No startup service metadata found for this paired repository runner.");
4527
+ });
4062
4528
  async function runWatchIteration({ command, context, options, runnerId }) {
4063
4529
  try {
4064
4530
  return await runNextWorkItem({
@@ -4988,10 +5454,10 @@ function parseInvocationChannel(value) {
4988
5454
  throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
4989
5455
  }
4990
5456
  function inferRepoName(root) {
4991
- return path12.basename(path12.resolve(root)) || "repository";
5457
+ return path13.basename(path13.resolve(root)) || "repository";
4992
5458
  }
4993
5459
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
4994
- return createHash5("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
5460
+ return createHash6("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
4995
5461
  }
4996
5462
  function defaultApiUrl() {
4997
5463
  const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
@@ -5121,8 +5587,9 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
5121
5587
  return {
5122
5588
  version: CLI_VERSION,
5123
5589
  mode,
5124
- hostname: os5.hostname(),
5590
+ hostname: os7.hostname(),
5125
5591
  ...runnerIsolationCapabilityMetadata(),
5592
+ resourceUsage: sampleCurrentRunnerResourceUsage(),
5126
5593
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
5127
5594
  ...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
5128
5595
  ...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
@@ -5135,7 +5602,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
5135
5602
  };
5136
5603
  }
5137
5604
  function runnerMachineId() {
5138
- return createHash5("sha256").update(`${os5.hostname()}:${os5.platform()}:${os5.arch()}`).digest("hex").slice(0, 20);
5605
+ return createHash6("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
5139
5606
  }
5140
5607
  async function delay(milliseconds) {
5141
5608
  await new Promise((resolve) => setTimeout(resolve, milliseconds));