@amistio/cli 0.1.37 → 0.1.39

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.
Files changed (3) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +365 -203
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,9 +9,9 @@ npm install -g @amistio/cli
9
9
  amistio --help
10
10
  ```
11
11
 
12
- The package install provides the `amistio` command and the optional `amistio-host-helper` executable. Repository cloning, project pairing, credential storage, sync watching, helper activation, and runner execution happen only when the user explicitly runs commands such as `amistio bootstrap`, `amistio import`, `amistio pair`, `amistio sync watch`, `amistio host-helper status`, or `amistio run --watch`. When the app copies a personal project into an organization, the CLI command syntax stays the same; create the org-scoped code and run `amistio import <code>` from the intended local checkout. Import scans repo-local project-brain docs plus recognized repo-local IDE, harness, and local AI tool memory or instruction files such as `AGENTS.md`, `.github/copilot-instructions.md`, `.cursor/rules/*.mdc`, `.windsurfrules`, and `memories/*.md`; durable Amistio memory belongs in `docs/memory/`, and global plugin or harness memory outside the repository is not scanned by default.
12
+ The package install provides the `amistio` command and the optional `amistio-host-helper` executable. Repository cloning, project pairing, credential storage, sync watching, harness diagnostics, helper activation, and runner execution happen only when the user explicitly runs commands such as `amistio bootstrap`, `amistio import`, `amistio pair`, `amistio sync watch`, `amistio harness status`, `amistio host-helper status`, or `amistio run --watch`. When the app copies a personal project into an organization, the CLI command syntax stays the same; create the org-scoped code and run `amistio import <code>` from the intended local checkout. Import scans repo-local project-brain docs plus recognized repo-local IDE and local AI tool memory or instruction files such as `AGENTS.md`, `.github/copilot-instructions.md`, `.cursor/rules/*.mdc`, `.windsurfrules`, and `memories/*.md`; durable Amistio memory belongs in `docs/memory/`, and global plugin memory outside the repository is not scanned by default.
13
13
 
14
- For enterprise setup, use the web Runner panel as the primary guide. It shows repository, pairing code, AI provider, local runner, and approved-work readiness with plain next actions; advanced CLI logs remain secondary diagnostics. The panel also shows trust/privacy boundaries, cost forecast and budget posture, runner health, blockers, verification health, and runner-version distribution from safe runner metadata.
14
+ For enterprise setup, use the web Runner panel as the primary guide. It shows repository, pairing code, GitHub access, AI provider, local runner, and verification readiness with plain next actions; advanced CLI logs remain secondary diagnostics. The panel also shows trust/privacy boundaries, cost forecast and budget posture, runner health, blockers, verification health, and runner-version distribution from safe runner metadata.
15
15
 
16
16
  Optional host helpers are configured outside the SaaS with `AMISTIO_HOST_HELPER_PATH`. The official npm package ships `amistio-host-helper` for Level 1 supervised process execution diagnostics; enable it with `AMISTIO_HOST_HELPER_PATH=$(command -v amistio-host-helper)` on reviewed runner machines and confirm `amistio host-helper status` plus `amistio host-helper conformance`. PTY and sandbox requests use a helper only when its versioned handshake advertises support; otherwise the CLI returns deterministic unsupported results instead of silently downgrading. The npm helper does not advertise PTY or sandbox support. Helper request envelopes include allowlisted environment values, explicit sandbox policy metadata, and bounded normalized output, and must never include provider tokens, OAuth material, runner API tokens, local auth files, or arbitrary SaaS-originated shell text.
17
17
 
@@ -38,6 +38,9 @@ Repository autopilot is disabled until the repository link option is enabled in
38
38
  After pairing, confirm that at least one local AI tool is available:
39
39
 
40
40
  ```sh
41
+ amistio harness status
42
+ amistio harness providers
43
+ amistio harness tools
41
44
  amistio tools
42
45
  amistio host-helper status
43
46
  amistio host-helper conformance
@@ -54,18 +57,21 @@ amistio run --watch --background --tool opencode
54
57
  AMISTIO_HOST_HELPER_PATH=$(command -v amistio-host-helper) amistio host-helper status
55
58
  AMISTIO_HOST_HELPER_PATH=$(command -v amistio-host-helper) amistio host-helper conformance
56
59
  amistio runner status
60
+ amistio runner repair
57
61
  amistio runner smoke-session-lifecycle
58
62
  ```
59
63
 
60
64
  Provider-backed model preferences use sanitized catalog fields: `--provider`, `--model-id`, optional `--model-variant`, and `--reasoning-effort` (`auto`, `low`, `medium`, `high`, or `xhigh`). Opencode catalog metadata is synthesized by Amistio or derived from readable local OpenCode JSON config until opencode exposes a native catalog. Provider credentials, API keys, and local secret paths stay in the local tool configuration; they are not stored in Amistio preferences or runner heartbeats.
61
65
 
66
+ `amistio harness status`, `amistio harness providers`, and `amistio harness tools` are local diagnostics for the built-in Amistio harness. They report the default harness boundary, safe direct-provider readiness, and bounded Amistio-owned tool adapters without uploading provider credentials, environment values, local config paths, or raw tool errors. The web Runner panel remains the primary setup flow.
67
+
62
68
  Opencode remains an optional compatibility route. When a provider/model is named for opencode, Amistio validates it against the safe provider catalog before launching the external tool instead of silently falling back. Command-mode local tool runs are bounded by `--tool-timeout-seconds`, wait for process cleanup after timeout, and return redacted/capped stdout and stderr so large external output, local execution-root paths, and secret-looking assignments are not persisted unbounded.
63
69
 
64
70
  When `--tool copilot` uses the GitHub Copilot SDK, Amistio approves read-only permission requests by default and denies mutating, network, MCP, hook, memory, and shell requests. Set `AMISTIO_COPILOT_APPROVE_ALL=1` only on a local machine where broad Copilot SDK approval is intentional.
65
71
 
66
72
  When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until the final response. For live Codex CLI logs, run `amistio run --watch --tool codex --invocation-channel command`.
67
73
 
68
- `amistio runner status` reports local background runner state, latest heartbeat, and bounded resource usage when available. Resource usage is latest-sample runner process memory/CPU plus safe aggregate system memory/load signals; it does not include source files, environment variables, command lines, process lists, credentials, or arbitrary local paths.
74
+ `amistio runner status` reports local background runner state, latest heartbeat, and bounded resource usage when available. `amistio runner repair` rotates the local runner ID for the paired checkout after a forgotten-runner tombstone; use `amistio runner repair --clear-credential` only when you need to delete the local credential and re-pair from the Runner panel. Resource usage is latest-sample runner process memory/CPU plus safe aggregate system memory/load signals; it does not include source files, environment variables, command lines, process lists, credentials, or arbitrary local paths.
69
75
 
70
76
  `amistio runner smoke-session-lifecycle` runs a local no-claim smoke for the runner tool-session lifecycle. It does not contact the API, claim production work, inspect source, or mutate local runner state; it verifies that completed one-shot sessions close, active sessions are treated as in use, stale sessions are not selected for reuse, and fresh related reusable sessions can still continue.
71
77
 
@@ -83,6 +89,8 @@ Watch mode prints a completed-work success once per work item, keeps fresh compl
83
89
 
84
90
  Known validation failures such as `unsafe_context_path` are printed with attention-needed next steps. For project-context refresh path-safety failures, deploy the latest web/API fix, update and restart the runner when applicable, retry the refresh, and capture only bounded non-secret output if it repeats.
85
91
 
92
+ If watch mode reports that the runner was forgotten by the server, run `amistio runner repair` from the paired checkout, then start `amistio run --watch` again. The repair command stores a fresh local runner ID because the default ID for a machine/project/repository is stable and can remain tombstoned. Use `--clear-credential` only when the Runner panel tells you to create a fresh pairing code.
93
+
86
94
  App-evaluation result finalization rejections print safe validation paths and preserve the local finalization evidence without exposing raw source or secrets. If a structured app-evaluation result is rejected, update and restart the runner, confirm the web/API deployment is current, and retry the evaluation before acting on cleanup or implementation recommendations.
87
95
 
88
96
  When brain generation or plan revision output is parsed but the Amistio API is temporarily unavailable during finalization, the runner keeps a safe pending result envelope in user-level Amistio config and replays it before claiming more work. The envelope uses a stable idempotency key and does not store raw tool stdout, provider sessions, credentials, or arbitrary local paths.
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 createHash9, randomUUID as randomUUID2 } from "node:crypto";
5
- import { writeFile as writeFile11 } from "node:fs/promises";
6
- import os8 from "node:os";
7
- import path17 from "node:path";
4
+ import { createHash as createHash9, randomUUID as randomUUID3 } from "node:crypto";
5
+ import { writeFile as writeFile12 } from "node:fs/promises";
6
+ import os9 from "node:os";
7
+ import path18 from "node:path";
8
8
  import { Command } from "commander";
9
9
 
10
10
  // ../shared/src/schemas.ts
@@ -2730,6 +2730,9 @@ var AmistioApiError = class extends Error {
2730
2730
  statusText;
2731
2731
  detail;
2732
2732
  };
2733
+ function isForgottenRunnerApiError(error) {
2734
+ return error instanceof AmistioApiError && error.status === 403 && error.detail.includes("This runner was forgotten");
2735
+ }
2733
2736
  function isRetryableApiError(error) {
2734
2737
  if (error instanceof AmistioApiError) {
2735
2738
  return error.status === 408 || error.status === 429 || error.status >= 500;
@@ -3142,8 +3145,8 @@ var toolSessionMutationSchema = z3.object({
3142
3145
  });
3143
3146
  function resolveApiUrl(apiUrl, urlPath) {
3144
3147
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
3145
- const path18 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
3146
- return new URL(`${base}${path18}`);
3148
+ const path19 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
3149
+ return new URL(`${base}${path19}`);
3147
3150
  }
3148
3151
 
3149
3152
  // src/orchestrator.ts
@@ -5491,12 +5494,143 @@ async function readRunnerDaemonMetadataFile(filePath) {
5491
5494
  }
5492
5495
  }
5493
5496
 
5494
- // src/runner-service.ts
5495
- import { spawn as spawn3 } from "node:child_process";
5496
- import { createHash as createHash4 } from "node:crypto";
5497
- import { mkdir as mkdir8, readFile as readFile7, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
5497
+ // src/runner-identity-store.ts
5498
+ import { randomUUID as randomUUID2 } from "node:crypto";
5499
+ import { chmod as chmod2, mkdir as mkdir8, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
5498
5500
  import os5 from "node:os";
5499
5501
  import path9 from "node:path";
5502
+
5503
+ // src/runner-status.ts
5504
+ import { createHash as createHash4 } from "node:crypto";
5505
+ var watchStateReminderMs = 60 * 1e3;
5506
+ function formatWatchStartupContext(input) {
5507
+ return [
5508
+ `Runner ${input.runnerId} is watching project ${input.projectId}.`,
5509
+ `Repository link: ${input.repositoryLinkId}`,
5510
+ `API: ${input.apiUrl}`,
5511
+ `Polling interval: ${input.intervalSeconds}s. Press Ctrl+C to stop.`
5512
+ ];
5513
+ }
5514
+ function formatWatchIdleLine(action, intervalSeconds) {
5515
+ return `${formatProjectNextAction(action)} Checking again in ${intervalSeconds}s.`;
5516
+ }
5517
+ function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
5518
+ const key = watchStateKey(action);
5519
+ if (!previous || previous.key !== key) {
5520
+ return true;
5521
+ }
5522
+ if (action.kind === "workCompleted") {
5523
+ return false;
5524
+ }
5525
+ return nowMs - previous.printedAtMs >= reminderMs;
5526
+ }
5527
+ function watchStateKey(action) {
5528
+ return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
5529
+ }
5530
+ function stableRunnerId(input) {
5531
+ const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
5532
+ return `runner_${digest}`;
5533
+ }
5534
+
5535
+ // src/runner-identity-store.ts
5536
+ var LocalRunnerIdentityStore = class {
5537
+ constructor(filePath = path9.join(os5.homedir(), ".config", "amistio", "runner-identities.json")) {
5538
+ this.filePath = filePath;
5539
+ }
5540
+ filePath;
5541
+ async get(key) {
5542
+ const data = await this.read();
5543
+ return data.runnerIds[key];
5544
+ }
5545
+ async set(key, runnerId) {
5546
+ const data = await this.read();
5547
+ data.runnerIds[key] = runnerId;
5548
+ await this.write(data);
5549
+ }
5550
+ async delete(key) {
5551
+ const data = await this.read();
5552
+ if (!(key in data.runnerIds)) {
5553
+ return;
5554
+ }
5555
+ delete data.runnerIds[key];
5556
+ await this.write(data);
5557
+ }
5558
+ async read() {
5559
+ try {
5560
+ return normalizeStoredRunnerIdentityFile(JSON.parse(await readFile7(this.filePath, "utf8")));
5561
+ } catch {
5562
+ return { runnerIds: {} };
5563
+ }
5564
+ }
5565
+ async write(data) {
5566
+ await mkdir8(path9.dirname(this.filePath), { recursive: true });
5567
+ await writeFile8(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
5568
+ await chmod2(this.filePath, 384);
5569
+ }
5570
+ };
5571
+ function runnerIdentityKey(scope) {
5572
+ return `${scope.accountId}:${scope.projectId}:${scope.repositoryLinkId}`;
5573
+ }
5574
+ function createFreshRunnerId() {
5575
+ return `runner_${randomUUID2().replace(/-/g, "").slice(0, 20)}`;
5576
+ }
5577
+ async function resolveLocalRunnerId(scope, store = new LocalRunnerIdentityStore()) {
5578
+ return await store.get(runnerIdentityKey(scope)) ?? stableRunnerId(scope);
5579
+ }
5580
+ function normalizeStoredRunnerIdentityFile(value) {
5581
+ if (!value || typeof value !== "object" || !("runnerIds" in value)) {
5582
+ return { runnerIds: {} };
5583
+ }
5584
+ const runnerIds = value.runnerIds;
5585
+ if (!runnerIds || typeof runnerIds !== "object") {
5586
+ return { runnerIds: {} };
5587
+ }
5588
+ return {
5589
+ runnerIds: Object.fromEntries(Object.entries(runnerIds).filter((entry) => typeof entry[1] === "string"))
5590
+ };
5591
+ }
5592
+
5593
+ // src/runner-watch-errors.ts
5594
+ var forgottenRunnerWatchMessage = "This runner was forgotten by the server. Start or pair a new runner from the Runner panel; this runner ID cannot heartbeat or claim work anymore.";
5595
+ async function handleRunnerWatchError(options) {
5596
+ const consoleError = options.consoleError ?? console.error;
5597
+ if (isForgottenRunnerApiError(options.error)) {
5598
+ if (options.verbose && options.detail) {
5599
+ consoleError(`${forgottenRunnerWatchMessage}
5600
+ ${options.detail}`);
5601
+ } else {
5602
+ consoleError(forgottenRunnerWatchMessage);
5603
+ }
5604
+ return { status: "failed", exitCode: 1, message: forgottenRunnerWatchMessage, stopRunner: true };
5605
+ }
5606
+ const message = "Runner hit an error while watching and will keep listening.";
5607
+ if (options.verbose) {
5608
+ consoleError(`${message}
5609
+ ${options.detail}`);
5610
+ } else {
5611
+ consoleError(`${message} Run with --verbose for details.`);
5612
+ }
5613
+ const settlements = await Promise.allSettled([
5614
+ options.client.sendRunnerHeartbeat(options.projectId, options.runnerId, options.repositoryLinkId, "blocked", { ...options.heartbeatMetadata, preferenceMessage: message }),
5615
+ options.client.recordRunnerLog(options.projectId, {
5616
+ runnerId: options.runnerId,
5617
+ repositoryLinkId: options.repositoryLinkId,
5618
+ status: "failed",
5619
+ message,
5620
+ error: options.detail,
5621
+ machineId: options.machineId
5622
+ })
5623
+ ]);
5624
+ options.logRejectedSettlements("record watch error", settlements);
5625
+ return { status: "failed", exitCode: 1, message };
5626
+ }
5627
+
5628
+ // src/runner-service.ts
5629
+ import { spawn as spawn3 } from "node:child_process";
5630
+ import { createHash as createHash5 } from "node:crypto";
5631
+ import { mkdir as mkdir9, readFile as readFile8, rm as rm3, writeFile as writeFile9 } from "node:fs/promises";
5632
+ import os6 from "node:os";
5633
+ import path10 from "node:path";
5500
5634
  function detectRunnerServicePlatform(platform = process.platform) {
5501
5635
  if (platform === "darwin") return "launchd";
5502
5636
  if (platform === "linux") return "systemd";
@@ -5507,19 +5641,19 @@ function createRunnerServiceDescriptor(input) {
5507
5641
  if (platform === "unsupported") {
5508
5642
  throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
5509
5643
  }
5510
- const homeDir = input.homeDir ?? os5.homedir();
5644
+ const homeDir = input.homeDir ?? os6.homedir();
5511
5645
  const serviceName = runnerServiceName(input);
5512
5646
  const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
5513
5647
  const now = (/* @__PURE__ */ new Date()).toISOString();
5514
5648
  const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
5515
- const logPath = path9.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
5649
+ const logPath = path10.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
5516
5650
  const metadata = {
5517
5651
  schemaVersion: 1,
5518
5652
  accountId: input.accountId,
5519
5653
  projectId: input.projectId,
5520
5654
  repositoryLinkId: input.repositoryLinkId,
5521
5655
  runnerId: input.runnerId,
5522
- rootDir: path9.resolve(input.rootDir),
5656
+ rootDir: path10.resolve(input.rootDir),
5523
5657
  apiUrl: input.apiUrl,
5524
5658
  serviceName,
5525
5659
  serviceFilePath,
@@ -5536,9 +5670,9 @@ function createRunnerServiceDescriptor(input) {
5536
5670
  }
5537
5671
  async function installRunnerService(input, options = {}) {
5538
5672
  const descriptor = createRunnerServiceDescriptor(input);
5539
- await mkdir8(path9.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
5540
- await mkdir8(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
5541
- await writeFile8(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
5673
+ await mkdir9(path10.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
5674
+ await mkdir9(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
5675
+ await writeFile9(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
5542
5676
  await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
5543
5677
  if (options.activate !== false) {
5544
5678
  const activation = await activateRunnerService(descriptor.metadata);
@@ -5560,7 +5694,7 @@ async function removeRunnerService(input) {
5560
5694
  }
5561
5695
  async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
5562
5696
  try {
5563
- const parsed = JSON.parse(await readFile7(runnerServiceMetadataPath(input, metadataDir), "utf8"));
5697
+ const parsed = JSON.parse(await readFile8(runnerServiceMetadataPath(input, metadataDir), "utf8"));
5564
5698
  if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
5565
5699
  return void 0;
5566
5700
  }
@@ -5570,8 +5704,8 @@ async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetad
5570
5704
  }
5571
5705
  }
5572
5706
  async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
5573
- await mkdir8(metadataDir, { recursive: true });
5574
- await writeFile8(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
5707
+ await mkdir9(metadataDir, { recursive: true });
5708
+ await writeFile9(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
5575
5709
  }
5576
5710
  async function runnerServiceRuntimeStatus(metadata) {
5577
5711
  if (metadata.platform === "launchd") {
@@ -5654,18 +5788,18 @@ WantedBy=default.target
5654
5788
  }
5655
5789
  function runnerServiceFilePath(platform, serviceName, homeDir) {
5656
5790
  if (platform === "launchd") {
5657
- return path9.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
5791
+ return path10.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
5658
5792
  }
5659
- return path9.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
5793
+ return path10.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
5660
5794
  }
5661
5795
  function runnerServiceMetadataPath(input, metadataDir) {
5662
- return path9.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
5796
+ return path10.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
5663
5797
  }
5664
5798
  function runnerServiceName(input) {
5665
5799
  return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
5666
5800
  }
5667
5801
  function runnerServiceKey(input) {
5668
- return createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
5802
+ return createHash5("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
5669
5803
  }
5670
5804
  function launchdDomain() {
5671
5805
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
@@ -5993,9 +6127,9 @@ function createSmokeSession({ now, status, lastActivityAt }) {
5993
6127
 
5994
6128
  // src/sync.ts
5995
6129
  import { execFile as execFile3 } from "node:child_process";
5996
- import { createHash as createHash5 } from "node:crypto";
5997
- import { mkdir as mkdir9, readdir as readdir5, readFile as readFile8, stat as stat4, writeFile as writeFile9 } from "node:fs/promises";
5998
- import path10 from "node:path";
6130
+ import { createHash as createHash6 } from "node:crypto";
6131
+ import { mkdir as mkdir10, readdir as readdir5, readFile as readFile9, stat as stat4, writeFile as writeFile10 } from "node:fs/promises";
6132
+ import path11 from "node:path";
5999
6133
  import { promisify as promisify3 } from "node:util";
6000
6134
  var execFileAsync3 = promisify3(execFile3);
6001
6135
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
@@ -6091,7 +6225,7 @@ async function readLocalSyncedDocuments(rootDir) {
6091
6225
  const documentFiles = await findBrainDocumentFiles(rootDir);
6092
6226
  const documents = [];
6093
6227
  for (const fullPath of documentFiles) {
6094
- const raw = await readFile8(fullPath, "utf8");
6228
+ const raw = await readFile9(fullPath, "utf8");
6095
6229
  const repoPath = toRepoPath(rootDir, fullPath);
6096
6230
  const parsed = parseSyncedDocument(raw, repoPath);
6097
6231
  if (!parsed) {
@@ -6141,8 +6275,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
6141
6275
  result.skipped.push(document.repoPath);
6142
6276
  continue;
6143
6277
  }
6144
- await mkdir9(path10.dirname(fullPath), { recursive: true });
6145
- await writeFile9(fullPath, createSyncedDocumentContent(document), "utf8");
6278
+ await mkdir10(path11.dirname(fullPath), { recursive: true });
6279
+ await writeFile10(fullPath, createSyncedDocumentContent(document), "utf8");
6146
6280
  result.written.push(document.repoPath);
6147
6281
  }
6148
6282
  return result;
@@ -6271,7 +6405,7 @@ function parseSyncedHtml(content) {
6271
6405
  }
6272
6406
  async function readExistingSyncedDocument(fullPath) {
6273
6407
  try {
6274
- const raw = await readFile8(fullPath, "utf8");
6408
+ const raw = await readFile9(fullPath, "utf8");
6275
6409
  const parsed = parseSyncedDocument(raw, fullPath);
6276
6410
  if (!parsed) {
6277
6411
  return { exists: true };
@@ -6296,7 +6430,7 @@ async function readExistingSyncedDocument(fullPath) {
6296
6430
  async function findBrainDocumentFiles(rootDir) {
6297
6431
  const files = [];
6298
6432
  for (const syncRoot of [...syncRoots, htmlSyncRoot]) {
6299
- const fullRoot = path10.join(rootDir, syncRoot);
6433
+ const fullRoot = path11.join(rootDir, syncRoot);
6300
6434
  if (!await exists2(fullRoot)) {
6301
6435
  continue;
6302
6436
  }
@@ -6306,7 +6440,7 @@ async function findBrainDocumentFiles(rootDir) {
6306
6440
  }
6307
6441
  async function walkBrainDocumentFiles(directory, files) {
6308
6442
  for (const entry of await readdir5(directory, { withFileTypes: true })) {
6309
- const fullPath = path10.join(directory, entry.name);
6443
+ const fullPath = path11.join(directory, entry.name);
6310
6444
  if (entry.isDirectory()) {
6311
6445
  await walkBrainDocumentFiles(fullPath, files);
6312
6446
  } else if (entry.isFile() && /\.(md|mdx|html?)$/i.test(entry.name)) {
@@ -6315,23 +6449,23 @@ async function walkBrainDocumentFiles(directory, files) {
6315
6449
  }
6316
6450
  }
6317
6451
  function safeRepoPath(rootDir, repoPath) {
6318
- if (path10.isAbsolute(repoPath)) {
6452
+ if (path11.isAbsolute(repoPath)) {
6319
6453
  throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
6320
6454
  }
6321
- const normalized = path10.normalize(repoPath);
6322
- if (normalized === ".." || normalized.startsWith(`..${path10.sep}`)) {
6455
+ const normalized = path11.normalize(repoPath);
6456
+ if (normalized === ".." || normalized.startsWith(`..${path11.sep}`)) {
6323
6457
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
6324
6458
  }
6325
- const root = path10.resolve(rootDir);
6326
- const fullPath = path10.resolve(root, normalized);
6327
- if (!fullPath.startsWith(`${root}${path10.sep}`)) {
6459
+ const root = path11.resolve(rootDir);
6460
+ const fullPath = path11.resolve(root, normalized);
6461
+ if (!fullPath.startsWith(`${root}${path11.sep}`)) {
6328
6462
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
6329
6463
  }
6330
6464
  return fullPath;
6331
6465
  }
6332
6466
  function isControlPlanePath(repoPath) {
6333
- const normalized = path10.normalize(repoPath);
6334
- return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path10.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path10.sep}`);
6467
+ const normalized = path11.normalize(repoPath);
6468
+ return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path11.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path11.sep}`);
6335
6469
  }
6336
6470
  function canonicalControlPlaneRepoPath(repoPath) {
6337
6471
  const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -6342,16 +6476,16 @@ function canonicalControlPlaneRepoPath(repoPath) {
6342
6476
  return normalized;
6343
6477
  }
6344
6478
  function toRepoPath(rootDir, fullPath) {
6345
- return path10.relative(rootDir, fullPath).split(path10.sep).join("/");
6479
+ return path11.relative(rootDir, fullPath).split(path11.sep).join("/");
6346
6480
  }
6347
6481
  function inferTitle(content, repoPath) {
6348
6482
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
6349
6483
  if (heading) return heading;
6350
6484
  const htmlHeading = content.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
6351
- return htmlHeading || path10.basename(repoPath, path10.extname(repoPath));
6485
+ return htmlHeading || path11.basename(repoPath, path11.extname(repoPath));
6352
6486
  }
6353
6487
  async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
6354
- const root = path10.resolve(rootDir);
6488
+ const root = path11.resolve(rootDir);
6355
6489
  const maxBytes = (options.maxFileKb ?? defaultAutoSyncMaxFileKb) * 1024;
6356
6490
  const syncedAt = options.syncedAt ?? (/* @__PURE__ */ new Date()).toISOString();
6357
6491
  const existingById = new Map(existingDocuments.map((document) => [document.documentId, document]));
@@ -6376,7 +6510,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
6376
6510
  skipped.push({ repoPath: normalizedRepoPath, reason: "tooLarge" });
6377
6511
  continue;
6378
6512
  }
6379
- const content = await readFile8(fullPath, "utf8").catch(() => void 0);
6513
+ const content = await readFile9(fullPath, "utf8").catch(() => void 0);
6380
6514
  if (content === void 0) {
6381
6515
  skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
6382
6516
  continue;
@@ -6441,7 +6575,7 @@ async function listAutoSyncCandidatePaths(rootDir) {
6441
6575
  }
6442
6576
  const files = [];
6443
6577
  for (const syncRoot of [...syncRoots, htmlSyncRoot, ...legacySyncRoots]) {
6444
- const fullRoot = path10.join(rootDir, syncRoot);
6578
+ const fullRoot = path11.join(rootDir, syncRoot);
6445
6579
  if (await exists2(fullRoot)) {
6446
6580
  await walkAutoSyncFiles(rootDir, fullRoot, files);
6447
6581
  }
@@ -6450,8 +6584,8 @@ async function listAutoSyncCandidatePaths(rootDir) {
6450
6584
  }
6451
6585
  async function walkAutoSyncFiles(rootDir, directory, files) {
6452
6586
  for (const entry of await readdir5(directory, { withFileTypes: true }).catch(() => [])) {
6453
- const fullPath = path10.join(directory, entry.name);
6454
- const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
6587
+ const fullPath = path11.join(directory, entry.name);
6588
+ const repoPath = normalizeRepoPath3(path11.relative(rootDir, fullPath));
6455
6589
  if (entry.isDirectory()) {
6456
6590
  if (!autoSyncExcludedDirectoryNames.has(entry.name)) {
6457
6591
  await walkAutoSyncFiles(rootDir, fullPath, files);
@@ -6485,7 +6619,7 @@ function legacyDocumentTypeForRepoPath(repoPath) {
6485
6619
  return root && root in documentTypeByRoot ? documentTypeByRoot[root] : void 0;
6486
6620
  }
6487
6621
  function stableExternalDocumentId(metadata, repoPath) {
6488
- return `doc_external_${createHash5("sha256").update(`${metadata.amistioAccountId}:${metadata.amistioProjectId}:${metadata.repositoryLinkId}:${repoPath}`).digest("hex").slice(0, 24)}`;
6622
+ return `doc_external_${createHash6("sha256").update(`${metadata.amistioAccountId}:${metadata.amistioProjectId}:${metadata.repositoryLinkId}:${repoPath}`).digest("hex").slice(0, 24)}`;
6489
6623
  }
6490
6624
  function normalizeRepoPath3(repoPath) {
6491
6625
  return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -6513,9 +6647,9 @@ async function exists2(filePath) {
6513
6647
  }
6514
6648
 
6515
6649
  // src/tool-session-store.ts
6516
- import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile10 } from "node:fs/promises";
6517
- import os6 from "node:os";
6518
- import path11 from "node:path";
6650
+ import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
6651
+ import os7 from "node:os";
6652
+ import path12 from "node:path";
6519
6653
  var LocalToolSessionStore = class {
6520
6654
  constructor(filePath = defaultSessionStorePath()) {
6521
6655
  this.filePath = filePath;
@@ -6529,12 +6663,12 @@ var LocalToolSessionStore = class {
6529
6663
  async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
6530
6664
  const data = await this.read();
6531
6665
  data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
6532
- await mkdir10(path11.dirname(this.filePath), { recursive: true });
6533
- await writeFile10(this.filePath, JSON.stringify(data, null, 2), "utf8");
6666
+ await mkdir11(path12.dirname(this.filePath), { recursive: true });
6667
+ await writeFile11(this.filePath, JSON.stringify(data, null, 2), "utf8");
6534
6668
  }
6535
6669
  async read() {
6536
6670
  try {
6537
- return JSON.parse(await readFile9(this.filePath, "utf8"));
6671
+ return JSON.parse(await readFile10(this.filePath, "utf8"));
6538
6672
  } catch {
6539
6673
  return {};
6540
6674
  }
@@ -6542,16 +6676,16 @@ var LocalToolSessionStore = class {
6542
6676
  };
6543
6677
  function defaultSessionStorePath() {
6544
6678
  if (process.platform === "darwin") {
6545
- return path11.join(os6.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
6679
+ return path12.join(os7.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
6546
6680
  }
6547
6681
  if (process.platform === "win32") {
6548
- return path11.join(process.env.APPDATA ?? os6.homedir(), "Amistio", "tool-sessions.json");
6682
+ return path12.join(process.env.APPDATA ?? os7.homedir(), "Amistio", "tool-sessions.json");
6549
6683
  }
6550
- return path11.join(process.env.XDG_STATE_HOME ?? path11.join(os6.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
6684
+ return path12.join(process.env.XDG_STATE_HOME ?? path12.join(os7.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
6551
6685
  }
6552
6686
 
6553
6687
  // src/work-runner.ts
6554
- import path12 from "node:path";
6688
+ import path13 from "node:path";
6555
6689
  var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
6556
6690
  var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
6557
6691
  var assistantAnswerStart = "AMISTIO_ASSISTANT_ANSWER_START";
@@ -8034,15 +8168,15 @@ function normalizeProjectContextRepoPath(value, options) {
8034
8168
  if (!trimmed || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed) || /^file:/i.test(trimmed) || /^[A-Za-z]:($|[^\\/])/.test(trimmed)) {
8035
8169
  throwUnsafeProjectContextPath();
8036
8170
  }
8037
- const absolute = trimmed.startsWith("/") || trimmed.startsWith("\\\\") || path12.isAbsolute(trimmed) || path12.win32.isAbsolute(trimmed);
8171
+ const absolute = trimmed.startsWith("/") || trimmed.startsWith("\\\\") || path13.isAbsolute(trimmed) || path13.win32.isAbsolute(trimmed);
8038
8172
  if (!absolute) {
8039
8173
  return normalizeRelativeProjectContextPath(trimmed);
8040
8174
  }
8041
8175
  if (!options.repositoryRoot) {
8042
8176
  throwUnsafeProjectContextPath();
8043
8177
  }
8044
- const useWindowsPathRules = path12.win32.isAbsolute(trimmed) && !trimmed.startsWith("/");
8045
- const relativePath = useWindowsPathRules ? path12.win32.relative(path12.win32.resolve(options.repositoryRoot), path12.win32.resolve(trimmed)) : path12.relative(path12.resolve(options.repositoryRoot), path12.resolve(trimmed));
8178
+ const useWindowsPathRules = path13.win32.isAbsolute(trimmed) && !trimmed.startsWith("/");
8179
+ const relativePath = useWindowsPathRules ? path13.win32.relative(path13.win32.resolve(options.repositoryRoot), path13.win32.resolve(trimmed)) : path13.relative(path13.resolve(options.repositoryRoot), path13.resolve(trimmed));
8046
8180
  return normalizeRelativeProjectContextPath(relativePath);
8047
8181
  }
8048
8182
  function normalizeRelativeProjectContextPath(value) {
@@ -8093,48 +8227,16 @@ function stripJsonFence(value) {
8093
8227
  return trimmed.replace(/^```(?:json)?\s*/i, "").replace(/```$/i, "").trim();
8094
8228
  }
8095
8229
 
8096
- // src/runner-status.ts
8097
- import { createHash as createHash6 } from "node:crypto";
8098
- var watchStateReminderMs = 60 * 1e3;
8099
- function formatWatchStartupContext(input) {
8100
- return [
8101
- `Runner ${input.runnerId} is watching project ${input.projectId}.`,
8102
- `Repository link: ${input.repositoryLinkId}`,
8103
- `API: ${input.apiUrl}`,
8104
- `Polling interval: ${input.intervalSeconds}s. Press Ctrl+C to stop.`
8105
- ];
8106
- }
8107
- function formatWatchIdleLine(action, intervalSeconds) {
8108
- return `${formatProjectNextAction(action)} Checking again in ${intervalSeconds}s.`;
8109
- }
8110
- function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
8111
- const key = watchStateKey(action);
8112
- if (!previous || previous.key !== key) {
8113
- return true;
8114
- }
8115
- if (action.kind === "workCompleted") {
8116
- return false;
8117
- }
8118
- return nowMs - previous.printedAtMs >= reminderMs;
8119
- }
8120
- function watchStateKey(action) {
8121
- return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
8122
- }
8123
- function stableRunnerId(input) {
8124
- const digest = createHash6("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
8125
- return `runner_${digest}`;
8126
- }
8127
-
8128
8230
  // src/runner-resources.ts
8129
- import os7 from "node:os";
8231
+ import os8 from "node:os";
8130
8232
  var defaultRuntime = {
8131
8233
  nowMs: () => Date.now(),
8132
8234
  memoryUsage: () => process.memoryUsage(),
8133
8235
  uptime: () => process.uptime(),
8134
8236
  cpuUsage: () => process.cpuUsage(),
8135
- totalmem: () => os7.totalmem(),
8136
- freemem: () => os7.freemem(),
8137
- loadavg: () => os7.loadavg()
8237
+ totalmem: () => os8.totalmem(),
8238
+ freemem: () => os8.freemem(),
8239
+ loadavg: () => os8.loadavg()
8138
8240
  };
8139
8241
  var previousRunnerResourceSample;
8140
8242
  function sampleCurrentRunnerResourceUsage() {
@@ -8247,8 +8349,8 @@ function roundNumber(value, digits) {
8247
8349
  // src/importer.ts
8248
8350
  import { execFile as execFile4 } from "node:child_process";
8249
8351
  import { createHash as createHash7 } from "node:crypto";
8250
- import { readdir as readdir6, readFile as readFile10, stat as stat5 } from "node:fs/promises";
8251
- import path13 from "node:path";
8352
+ import { readdir as readdir6, readFile as readFile11, stat as stat5 } from "node:fs/promises";
8353
+ import path14 from "node:path";
8252
8354
  import { promisify as promisify4 } from "node:util";
8253
8355
  var execFileAsync4 = promisify4(execFile4);
8254
8356
  var defaultMaxFileKb = 256;
@@ -8269,12 +8371,12 @@ var documentFolderByType = {
8269
8371
  workflow: "docs/workflows"
8270
8372
  };
8271
8373
  async function inspectLocalRepository(rootDir, defaultBranch) {
8272
- const requestedRoot = path13.resolve(rootDir);
8374
+ const requestedRoot = path14.resolve(rootDir);
8273
8375
  const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
8274
8376
  const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
8275
8377
  const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
8276
8378
  const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
8277
- const repoName = (parsedCloneUrl?.repoName ?? path13.basename(root)) || "repository";
8379
+ const repoName = (parsedCloneUrl?.repoName ?? path14.basename(root)) || "repository";
8278
8380
  const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
8279
8381
  return {
8280
8382
  rootDir: root,
@@ -8286,7 +8388,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
8286
8388
  };
8287
8389
  }
8288
8390
  async function scanLegacyDocuments(options) {
8289
- const rootDir = path13.resolve(options.rootDir);
8391
+ const rootDir = path14.resolve(options.rootDir);
8290
8392
  const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
8291
8393
  const skipped = [];
8292
8394
  const candidates = [];
@@ -8306,7 +8408,7 @@ async function scanLegacyDocuments(options) {
8306
8408
  skipped.push({ repoPath, reason: "excluded" });
8307
8409
  continue;
8308
8410
  }
8309
- const fullPath = path13.join(rootDir, ...repoPath.split("/"));
8411
+ const fullPath = path14.join(rootDir, ...repoPath.split("/"));
8310
8412
  const fileStat = await stat5(fullPath).catch(() => void 0);
8311
8413
  if (!fileStat?.isFile()) {
8312
8414
  skipped.push({ repoPath, reason: "unreadable" });
@@ -8316,7 +8418,7 @@ async function scanLegacyDocuments(options) {
8316
8418
  skipped.push({ repoPath, reason: "tooLarge" });
8317
8419
  continue;
8318
8420
  }
8319
- const content = await readFile10(fullPath, "utf8").catch(() => void 0);
8421
+ const content = await readFile11(fullPath, "utf8").catch(() => void 0);
8320
8422
  if (content === void 0) {
8321
8423
  skipped.push({ repoPath, reason: "unreadable" });
8322
8424
  continue;
@@ -8408,8 +8510,8 @@ async function listRepositoryPaths(rootDir) {
8408
8510
  async function walkRepository(rootDir, directory, files) {
8409
8511
  const entries = await readdir6(directory, { withFileTypes: true }).catch(() => []);
8410
8512
  for (const entry of entries) {
8411
- const fullPath = path13.join(directory, entry.name);
8412
- const repoPath = normalizeRepoPath4(path13.relative(rootDir, fullPath));
8513
+ const fullPath = path14.join(directory, entry.name);
8514
+ const repoPath = normalizeRepoPath4(path14.relative(rootDir, fullPath));
8413
8515
  if (entry.isDirectory()) {
8414
8516
  if (!excludedDirectoryNames.has(entry.name)) {
8415
8517
  await walkRepository(rootDir, fullPath, files);
@@ -8477,9 +8579,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
8477
8579
  usedPaths.add(basePath);
8478
8580
  return basePath;
8479
8581
  }
8480
- const extension = path13.posix.extname(basePath) || ".md";
8481
- const directory = path13.posix.dirname(basePath);
8482
- const basename = path13.posix.basename(basePath, extension);
8582
+ const extension = path14.posix.extname(basePath) || ".md";
8583
+ const directory = path14.posix.dirname(basePath);
8584
+ const basename = path14.posix.basename(basePath, extension);
8483
8585
  const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
8484
8586
  usedPaths.add(uniquePath);
8485
8587
  return uniquePath;
@@ -8552,7 +8654,7 @@ function inferTitle2(content, repoPath) {
8552
8654
  if (heading) return heading;
8553
8655
  const htmlHeading = body.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
8554
8656
  if (htmlHeading) return htmlHeading;
8555
- const basename = path13.posix.basename(repoPath, path13.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
8657
+ const basename = path14.posix.basename(repoPath, path14.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
8556
8658
  return titleCase(basename || "Imported Document");
8557
8659
  }
8558
8660
  function stripFrontmatter(content) {
@@ -8586,7 +8688,7 @@ async function runGit2(args) {
8586
8688
 
8587
8689
  // src/runner-actions.ts
8588
8690
  import { spawn as spawn4 } from "node:child_process";
8589
- import path14 from "node:path";
8691
+ import path15 from "node:path";
8590
8692
  function buildBackgroundRunnerArgs(options) {
8591
8693
  const args = [
8592
8694
  "run",
@@ -8596,7 +8698,7 @@ function buildBackgroundRunnerArgs(options) {
8596
8698
  "--runner-id",
8597
8699
  options.runnerId,
8598
8700
  "--root",
8599
- path14.resolve(options.root),
8701
+ path15.resolve(options.root),
8600
8702
  "--session",
8601
8703
  options.session,
8602
8704
  "--interval-seconds",
@@ -8714,8 +8816,8 @@ function truncateProcessOutput(value) {
8714
8816
 
8715
8817
  // src/git-worktree.ts
8716
8818
  import { execFile as execFile5 } from "node:child_process";
8717
- import { copyFile, lstat, mkdir as mkdir11, readdir as readdir7, stat as stat6 } from "node:fs/promises";
8718
- import path15 from "node:path";
8819
+ import { copyFile, lstat, mkdir as mkdir12, readdir as readdir7, stat as stat6 } from "node:fs/promises";
8820
+ import path16 from "node:path";
8719
8821
  import { promisify as promisify5 } from "node:util";
8720
8822
  var execFileAsync5 = promisify5(execFile5);
8721
8823
  var exactLocalEnvironmentFiles = /* @__PURE__ */ new Set([".env", ".env.local", ".env.development", ".env.development.local", ".env.test", ".env.test.local", ".env.production", ".env.production.local"]);
@@ -8747,7 +8849,7 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
8747
8849
  const preparedLocalEnvironmentFileCount2 = await prepareLocalWorktreeEnvironment(repoRoot, worktreePath);
8748
8850
  return { ...identity, baseRevision, worktreePath, ...preparedLocalEnvironmentFileCount2 ? { preparedLocalEnvironmentFileCount: preparedLocalEnvironmentFileCount2 } : {} };
8749
8851
  }
8750
- await mkdir11(path15.dirname(worktreePath), { recursive: true });
8852
+ await mkdir12(path16.dirname(worktreePath), { recursive: true });
8751
8853
  const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
8752
8854
  const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
8753
8855
  await gitOutput(repoRoot, worktreeArgs).catch((error) => {
@@ -8765,9 +8867,9 @@ async function resolveExistingGitWorktreeIsolation(rootDir, workItem) {
8765
8867
  return { ...identity, baseRevision, worktreePath };
8766
8868
  }
8767
8869
  function localWorktreePath(repoRoot, worktreeKey) {
8768
- const repoName = path15.basename(repoRoot);
8870
+ const repoName = path16.basename(repoRoot);
8769
8871
  const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
8770
- return path15.join(path15.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
8872
+ return path16.join(path16.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
8771
8873
  }
8772
8874
  async function assertExistingWorktree(worktreePath, branch) {
8773
8875
  await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
@@ -8793,8 +8895,8 @@ async function prepareLocalWorktreeEnvironment(repoRoot, worktreePath) {
8793
8895
  const candidates = await localEnvironmentFileCandidates(repoRoot);
8794
8896
  let preparedCount = 0;
8795
8897
  for (const candidate of candidates) {
8796
- const sourcePath = path15.join(repoRoot, candidate);
8797
- const targetPath = path15.join(worktreePath, candidate);
8898
+ const sourcePath = path16.join(repoRoot, candidate);
8899
+ const targetPath = path16.join(worktreePath, candidate);
8798
8900
  if (await pathExists(targetPath)) {
8799
8901
  continue;
8800
8902
  }
@@ -8825,7 +8927,7 @@ async function localEnvironmentFileCandidates(repoRoot) {
8825
8927
  if (!isRootFileName(name)) {
8826
8928
  continue;
8827
8929
  }
8828
- const source = await lstat(path15.join(repoRoot, name)).catch(() => void 0);
8930
+ const source = await lstat(path16.join(repoRoot, name)).catch(() => void 0);
8829
8931
  if (source?.isFile()) {
8830
8932
  candidates.push(name);
8831
8933
  }
@@ -8836,7 +8938,7 @@ function isAllowedLocalEnvironmentFile(name) {
8836
8938
  return exactLocalEnvironmentFiles.has(name) || localEnvironmentFilePattern.test(name);
8837
8939
  }
8838
8940
  function isRootFileName(name) {
8839
- return name === path15.basename(name) && !name.includes("/") && !name.includes("\\");
8941
+ return name === path16.basename(name) && !name.includes("/") && !name.includes("\\");
8840
8942
  }
8841
8943
  async function gitOutput(cwd, args) {
8842
8944
  const { stdout } = await execFileAsync5("git", args, { cwd, maxBuffer: 1024 * 1024 });
@@ -8869,7 +8971,7 @@ function safeFileError(error) {
8869
8971
 
8870
8972
  // src/implementation-handoff.ts
8871
8973
  import { execFile as execFile6 } from "node:child_process";
8872
- import path16 from "node:path";
8974
+ import path17 from "node:path";
8873
8975
  import { promisify as promisify6 } from "node:util";
8874
8976
  var execFileAsync6 = promisify6(execFile6);
8875
8977
  async function completeImplementationHandoff(input) {
@@ -9137,7 +9239,7 @@ async function cleanupWorktree(run, input) {
9137
9239
  return { status: "failed", message: "Cleanup skipped because the worktree is not clean after PR handoff." };
9138
9240
  }
9139
9241
  try {
9140
- await gitOutput2(run, input.primaryRepoRoot || path16.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
9242
+ await gitOutput2(run, input.primaryRepoRoot || path17.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
9141
9243
  return { status: "completed" };
9142
9244
  } catch (error) {
9143
9245
  return { status: "failed", message: `Cleanup failed: ${safeErrorMessage(error)}` };
@@ -9858,7 +9960,7 @@ program.command("import").description("Pair an existing checkout and import lega
9858
9960
  });
9859
9961
  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) => {
9860
9962
  const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
9861
- let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID2()}`;
9963
+ let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID3()}`;
9862
9964
  let credential = options.token;
9863
9965
  if (options.pairingCode) {
9864
9966
  const pairing = await new ApiClient({
@@ -10044,7 +10146,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
10044
10146
  }
10045
10147
  const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
10046
10148
  if (options.out) {
10047
- await writeFile11(options.out, prompt, "utf8");
10149
+ await writeFile12(options.out, prompt, "utf8");
10048
10150
  console.log(`Wrote work prompt to ${options.out}.`);
10049
10151
  } else {
10050
10152
  console.log(prompt);
@@ -10058,6 +10160,34 @@ program.command("tools").description("List local AI coding tools that the Amisti
10058
10160
  }
10059
10161
  console.log("custom - pass --tool-command to use any other local runner command.");
10060
10162
  });
10163
+ var harness = program.command("harness").description("Inspect the built-in Amistio harness, direct providers, and bounded tool adapters");
10164
+ harness.command("status").description("Show the built-in harness boundary and execution policy summary").action(() => {
10165
+ console.log(`Harness: ${builtinAmistioHarnessAdapter.displayName} (${builtinAmistioHarnessAdapter.id})`);
10166
+ console.log("Default: built-in Amistio harness");
10167
+ console.log("Runner-owned boundary: pairing, work claiming, leases, worktree isolation, redaction, finalization, and handoff.");
10168
+ console.log(`Read-only work policy: ${runnerSupportedWorkKinds.filter((workKind) => harnessMutationPolicyForWorkKind(workKind) === "readOnly").join(", ")}`);
10169
+ console.log(`Mutating work policy: ${runnerSupportedWorkKinds.filter((workKind) => harnessMutationPolicyForWorkKind(workKind) === "mutating").join(", ")}`);
10170
+ console.log("Run execution: amistio run --watch");
10171
+ console.log("Local AI clients: amistio tools");
10172
+ console.log("Host execution helper: amistio host-helper status");
10173
+ });
10174
+ harness.command("providers").description("Check safe local readiness for direct and agent-client provider routes").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
10175
+ for (const target of harnessProviderCheckTargets()) {
10176
+ const result = await checkProviderAuthLink({ request: target.request, root: options.root });
10177
+ console.log(`${target.label}: ${result.providerAuthStatus.status}`);
10178
+ for (const line of formatProviderAuthStatusLines(result.providerAuthStatus)) {
10179
+ console.log(` ${line}`);
10180
+ }
10181
+ }
10182
+ });
10183
+ harness.command("tools").description("List bounded Amistio-owned tool adapters available to the harness").action(() => {
10184
+ console.log("Bounded Amistio harness tools. For optional local AI clients, run `amistio tools`.");
10185
+ for (const adapter of boundedToolAdapterCatalog) {
10186
+ const mutation = adapter.mutating ? "mutating" : "read-only";
10187
+ const implemented = adapter.implemented ? "yes" : "no";
10188
+ console.log(`${implemented} ${adapter.kind} ${adapter.id} - ${adapter.displayName}; ${mutation}; ${adapter.concurrency}; resources: ${adapter.serializedResourceKinds.join(", ")}`);
10189
+ }
10190
+ });
10061
10191
  var hostHelper = program.command("host-helper").description("Inspect the optional Amistio host helper used for stronger local execution primitives");
10062
10192
  hostHelper.command("status").description("Show configured host-helper protocol and capability status").option("--path <path>", "Helper executable path; defaults to AMISTIO_HOST_HELPER_PATH").action(async (options) => {
10063
10193
  const config = options.path?.trim() ? { command: options.path.trim() } : nativeHostHelperConfigFromEnvironment();
@@ -10126,7 +10256,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
10126
10256
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
10127
10257
  ...localModelConfig,
10128
10258
  streamOutput: options.stream,
10129
- ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID2()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
10259
+ ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID3()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
10130
10260
  });
10131
10261
  if (!options.stream && result.stdout.trim()) {
10132
10262
  console.log(result.stdout.trim());
@@ -10149,12 +10279,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
10149
10279
  process.exitCode = 1;
10150
10280
  return;
10151
10281
  }
10152
- const runnerId = options.runnerId ?? stableRunnerId({
10153
- accountId: context.metadata.amistioAccountId,
10154
- projectId: context.metadata.amistioProjectId,
10155
- repositoryLinkId: context.metadata.repositoryLinkId,
10156
- machineId: runnerMachineId()
10157
- });
10282
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10158
10283
  const resolvedOptions = { ...options, runnerId };
10159
10284
  if (resolvedOptions.maxConcurrentWork > MAX_CONCURRENT_RUNNER_WORK) {
10160
10285
  console.log(`--max-concurrent-work is capped at ${MAX_CONCURRENT_RUNNER_WORK}.`);
@@ -10172,7 +10297,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
10172
10297
  projectId: context.metadata.amistioProjectId,
10173
10298
  repositoryLinkId: context.metadata.repositoryLinkId,
10174
10299
  runnerId,
10175
- rootDir: path17.resolve(options.root),
10300
+ rootDir: path18.resolve(options.root),
10176
10301
  apiUrl: options.apiUrl,
10177
10302
  args: buildBackgroundRunnerArgs(resolvedOptions)
10178
10303
  });
@@ -10225,6 +10350,9 @@ program.command("run").description("Claim and run approved Amistio work locally"
10225
10350
  }
10226
10351
  }
10227
10352
  if (result.stopRunner) {
10353
+ if (result.exitCode !== 0) {
10354
+ process.exitCode = result.exitCode;
10355
+ }
10228
10356
  return;
10229
10357
  }
10230
10358
  if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
@@ -10240,6 +10368,38 @@ program.command("run").description("Claim and run approved Amistio work locally"
10240
10368
  }
10241
10369
  });
10242
10370
  var runner = program.command("runner").description("Manage local Amistio runner processes");
10371
+ runner.command("repair").description("Repair local runner identity state for this paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Fresh runner ID to store instead of generating one").option("--clear-credential", "Delete the local runner credential so this checkout must be paired again").option("--dry-run", "Print repair actions without changing local config").action(async (options) => {
10372
+ const metadata = await readProjectLink(options.root);
10373
+ if (!metadata) {
10374
+ console.log("Repository is not paired. Run `amistio pair` first.");
10375
+ return;
10376
+ }
10377
+ const scope = runnerIdentityScope(metadata);
10378
+ const identityKey = runnerIdentityKey(scope);
10379
+ const identityStore = new LocalRunnerIdentityStore();
10380
+ const credentialStore = new LocalCredentialStore();
10381
+ const stableRunner = stableRunnerId(scope);
10382
+ const currentRunnerId = await identityStore.get(identityKey) ?? stableRunner;
10383
+ const nextRunnerId = options.runnerId?.trim() || createFreshRunnerId();
10384
+ if (options.dryRun) {
10385
+ console.log(`Would rotate local runner ID from ${currentRunnerId} to ${nextRunnerId}.`);
10386
+ if (options.clearCredential) {
10387
+ console.log("Would delete the local runner credential for this paired checkout.");
10388
+ }
10389
+ return;
10390
+ }
10391
+ await identityStore.set(identityKey, nextRunnerId);
10392
+ if (options.clearCredential) {
10393
+ await credentialStore.delete(credentialKey(metadata.amistioAccountId, metadata.amistioProjectId, metadata.repositoryLinkId));
10394
+ }
10395
+ console.log(`Local runner ID repaired: ${currentRunnerId} -> ${nextRunnerId}.`);
10396
+ if (options.clearCredential) {
10397
+ console.log("Deleted the local runner credential. Create a fresh pairing code in the Runner panel, then run the copied pair/import command.");
10398
+ } else {
10399
+ console.log(`Next: amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
10400
+ console.log("If a startup service uses the old runner ID, reinstall it from the Runner panel command.");
10401
+ }
10402
+ });
10243
10403
  runner.command("status").description("Show background runner status for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Limit status to one runner ID").action(async (options) => {
10244
10404
  const context = await loadPairedApiContext(options.root, options.apiUrl);
10245
10405
  if (!context) {
@@ -10346,12 +10506,7 @@ runnerService.command("install").description("Install a user-level startup servi
10346
10506
  process.exitCode = 1;
10347
10507
  return;
10348
10508
  }
10349
- const runnerId = options.runnerId ?? stableRunnerId({
10350
- accountId: context.metadata.amistioAccountId,
10351
- projectId: context.metadata.amistioProjectId,
10352
- repositoryLinkId: context.metadata.repositoryLinkId,
10353
- machineId: runnerMachineId()
10354
- });
10509
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10355
10510
  if (options.maxConcurrentWork > MAX_CONCURRENT_RUNNER_WORK) {
10356
10511
  console.log(`--max-concurrent-work is capped at ${MAX_CONCURRENT_RUNNER_WORK}.`);
10357
10512
  process.exitCode = 1;
@@ -10363,7 +10518,7 @@ runnerService.command("install").description("Install a user-level startup servi
10363
10518
  projectId: context.metadata.amistioProjectId,
10364
10519
  repositoryLinkId: context.metadata.repositoryLinkId,
10365
10520
  runnerId,
10366
- rootDir: path17.resolve(options.root),
10521
+ rootDir: path18.resolve(options.root),
10367
10522
  apiUrl: options.apiUrl,
10368
10523
  args,
10369
10524
  platform
@@ -10389,12 +10544,7 @@ runnerService.command("status").description("Show the startup service status for
10389
10544
  console.log("Repository is not paired. Run `amistio pair` first.");
10390
10545
  return;
10391
10546
  }
10392
- const runnerId = options.runnerId ?? stableRunnerId({
10393
- accountId: context.metadata.amistioAccountId,
10394
- projectId: context.metadata.amistioProjectId,
10395
- repositoryLinkId: context.metadata.repositoryLinkId,
10396
- machineId: runnerMachineId()
10397
- });
10547
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10398
10548
  const metadata = await readRunnerServiceMetadata({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
10399
10549
  if (!metadata) {
10400
10550
  console.log("No startup service metadata found for this paired repository runner.");
@@ -10413,12 +10563,7 @@ runnerService.command("remove").description("Remove the startup service for this
10413
10563
  console.log("Repository is not paired. Run `amistio pair` first.");
10414
10564
  return;
10415
10565
  }
10416
- const runnerId = options.runnerId ?? stableRunnerId({
10417
- accountId: context.metadata.amistioAccountId,
10418
- projectId: context.metadata.amistioProjectId,
10419
- repositoryLinkId: context.metadata.repositoryLinkId,
10420
- machineId: runnerMachineId()
10421
- });
10566
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10422
10567
  const removed = await removeRunnerService({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
10423
10568
  console.log(removed ? `Removed startup service ${removed.serviceName}.` : "No startup service metadata found for this paired repository runner.");
10424
10569
  });
@@ -10491,26 +10636,18 @@ async function runWatchIteration({ command, context, options, runnerId }) {
10491
10636
  throw error;
10492
10637
  }
10493
10638
  const detail = truncateLogExcerpt(errorDetail(error));
10494
- const message = "Runner hit an error while watching and will keep listening.";
10495
- if (options.verbose) {
10496
- console.error(`${message}
10497
- ${detail}`);
10498
- } else {
10499
- console.error(`${message} Run with --verbose for details.`);
10500
- }
10501
- const settlements = await Promise.allSettled([
10502
- context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "blocked", { ...runnerHeartbeatMetadata(), preferenceMessage: message }),
10503
- context.client.recordRunnerLog(context.metadata.amistioProjectId, {
10504
- runnerId,
10505
- repositoryLinkId: context.metadata.repositoryLinkId,
10506
- status: "failed",
10507
- message,
10508
- error: detail,
10509
- machineId: runnerMachineId()
10510
- })
10511
- ]);
10512
- logRejectedSettlements("record watch error", settlements);
10513
- return { status: "failed", exitCode: 1, message };
10639
+ return await handleRunnerWatchError({
10640
+ client: context.client,
10641
+ detail,
10642
+ error,
10643
+ heartbeatMetadata: runnerHeartbeatMetadata(),
10644
+ logRejectedSettlements,
10645
+ machineId: runnerMachineId(),
10646
+ projectId: context.metadata.amistioProjectId,
10647
+ repositoryLinkId: context.metadata.repositoryLinkId,
10648
+ runnerId,
10649
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
10650
+ });
10514
10651
  }
10515
10652
  }
10516
10653
  function claimLaneIds(maxConcurrentWork) {
@@ -11055,7 +11192,7 @@ async function runNextWorkItem({
11055
11192
  projectId,
11056
11193
  result.workItem.workItemId,
11057
11194
  finalStatus,
11058
- `run_${result.workItem.workItemId}_${randomUUID2()}`,
11195
+ `run_${result.workItem.workItemId}_${randomUUID3()}`,
11059
11196
  runnerId,
11060
11197
  {
11061
11198
  tool: preview.toolName,
@@ -11149,7 +11286,7 @@ async function prepareWorktreeForClaimedItem({ apiClient, heartbeatConcurrency,
11149
11286
  const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
11150
11287
  const finalAttempt = workItem.attempt >= maxPreflightAttempts;
11151
11288
  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}`;
11152
- const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`, runnerId, {
11289
+ const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${randomUUID3()}`, runnerId, {
11153
11290
  ...telemetry,
11154
11291
  message: statusMessage,
11155
11292
  ...finalAttempt ? { blockerReason: message } : { releaseClaim: true },
@@ -11218,7 +11355,7 @@ async function recordFinalizationFailure({ apiClient, durationMs, error, isolati
11218
11355
  const settlements = await Promise.allSettled([
11219
11356
  apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
11220
11357
  markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage6(error)),
11221
- apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`, runnerId, {
11358
+ apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${randomUUID3()}`, runnerId, {
11222
11359
  ...isolationTelemetry,
11223
11360
  tool: toolName,
11224
11361
  durationMs,
@@ -11369,7 +11506,7 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
11369
11506
  runnerId: context.runnerId,
11370
11507
  repositoryLinkId: context.repositoryLinkId,
11371
11508
  status,
11372
- idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
11509
+ idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID3()}`,
11373
11510
  message,
11374
11511
  ...error ? { error } : {},
11375
11512
  ...providerAuthStatus ? { providerAuthStatus } : {}
@@ -11458,7 +11595,7 @@ async function findRecoveryWorkItem(apiClient, projectId, workItemId) {
11458
11595
  return workItems.find((item) => item.workItemId === workItemId);
11459
11596
  }
11460
11597
  async function submitRecoveredHandoff(apiClient, context, workItem, handoff, action) {
11461
- await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID2()}`, context.runnerId, {
11598
+ await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID3()}`, context.runnerId, {
11462
11599
  implementationHandoff: handoff,
11463
11600
  ...handoff.message ? { message: handoff.message } : {},
11464
11601
  ...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
@@ -11742,7 +11879,7 @@ ${toolResult.stderr}`);
11742
11879
  const resultMutation = {
11743
11880
  status: "completed",
11744
11881
  runnerId,
11745
- idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`,
11882
+ idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${randomUUID3()}`,
11746
11883
  artifacts,
11747
11884
  tool: toolName,
11748
11885
  durationMs,
@@ -11817,7 +11954,7 @@ ${toolResult.stderr}`);
11817
11954
  const failedResult2 = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
11818
11955
  status: "failed",
11819
11956
  runnerId,
11820
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
11957
+ idempotencyKey: `generation_${workItem.workItemId}_${randomUUID3()}`,
11821
11958
  tool: toolName,
11822
11959
  durationMs,
11823
11960
  ...failedSessionTelemetry,
@@ -11867,7 +12004,7 @@ ${toolResult.stderr}`);
11867
12004
  const resultMutation = {
11868
12005
  status: "completed",
11869
12006
  runnerId,
11870
- idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID2()}`,
12007
+ idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID3()}`,
11871
12008
  answer: answerResult.answer,
11872
12009
  sourceBoundary: answerResult.sourceBoundary,
11873
12010
  citations: answerResult.citations,
@@ -11903,7 +12040,7 @@ ${toolResult.stderr}`);
11903
12040
  const failedMutation = {
11904
12041
  status: "failed",
11905
12042
  runnerId,
11906
- idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID2()}`,
12043
+ idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID3()}`,
11907
12044
  tool: toolName,
11908
12045
  durationMs,
11909
12046
  ...sessionTelemetry,
@@ -11966,7 +12103,7 @@ ${toolResult.stderr}`);
11966
12103
  const resultMutation = {
11967
12104
  status: "completed",
11968
12105
  runnerId,
11969
- idempotencyKey: `impact_${workItem.workItemId}_${randomUUID2()}`,
12106
+ idempotencyKey: `impact_${workItem.workItemId}_${randomUUID3()}`,
11970
12107
  report: {
11971
12108
  ...report,
11972
12109
  analyzedRepoRevision: report.analyzedRepoRevision ?? metadata?.lastSyncedRevision
@@ -12003,7 +12140,7 @@ ${toolResult.stderr}`);
12003
12140
  const failedMutation = {
12004
12141
  status: "failed",
12005
12142
  runnerId,
12006
- idempotencyKey: `impact_${workItem.workItemId}_${randomUUID2()}`,
12143
+ idempotencyKey: `impact_${workItem.workItemId}_${randomUUID3()}`,
12007
12144
  tool: toolName,
12008
12145
  durationMs,
12009
12146
  ...sessionTelemetry,
@@ -12064,7 +12201,7 @@ ${toolResult.stderr}`);
12064
12201
  const resultMutation = {
12065
12202
  status: "completed",
12066
12203
  runnerId,
12067
- idempotencyKey: `issue_${workItem.workItemId}_${randomUUID2()}`,
12204
+ idempotencyKey: `issue_${workItem.workItemId}_${randomUUID3()}`,
12068
12205
  diagnosis,
12069
12206
  tool: toolName,
12070
12207
  durationMs,
@@ -12098,7 +12235,7 @@ ${toolResult.stderr}`);
12098
12235
  const failedMutation = {
12099
12236
  status: "failed",
12100
12237
  runnerId,
12101
- idempotencyKey: `issue_${workItem.workItemId}_${randomUUID2()}`,
12238
+ idempotencyKey: `issue_${workItem.workItemId}_${randomUUID3()}`,
12102
12239
  tool: toolName,
12103
12240
  durationMs,
12104
12241
  ...sessionTelemetry,
@@ -12159,7 +12296,7 @@ ${toolResult.stderr}`);
12159
12296
  const resultMutation = {
12160
12297
  status: "completed",
12161
12298
  runnerId,
12162
- idempotencyKey: `security_${workItem.workItemId}_${randomUUID2()}`,
12299
+ idempotencyKey: `security_${workItem.workItemId}_${randomUUID3()}`,
12163
12300
  result: scanResult,
12164
12301
  tool: toolName,
12165
12302
  durationMs,
@@ -12193,7 +12330,7 @@ ${toolResult.stderr}`);
12193
12330
  const failedMutation = {
12194
12331
  status: "failed",
12195
12332
  runnerId,
12196
- idempotencyKey: `security_${workItem.workItemId}_${randomUUID2()}`,
12333
+ idempotencyKey: `security_${workItem.workItemId}_${randomUUID3()}`,
12197
12334
  tool: toolName,
12198
12335
  durationMs,
12199
12336
  ...sessionTelemetry,
@@ -12254,7 +12391,7 @@ ${toolResult.stderr}`);
12254
12391
  const resultMutation = {
12255
12392
  status: "completed",
12256
12393
  runnerId,
12257
- idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID2()}`,
12394
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID3()}`,
12258
12395
  result: scanResult,
12259
12396
  tool: toolName,
12260
12397
  durationMs,
@@ -12288,7 +12425,7 @@ ${toolResult.stderr}`);
12288
12425
  const failedMutation = {
12289
12426
  status: "failed",
12290
12427
  runnerId,
12291
- idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID2()}`,
12428
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID3()}`,
12292
12429
  tool: toolName,
12293
12430
  durationMs,
12294
12431
  ...sessionTelemetry,
@@ -12349,7 +12486,7 @@ ${toolResult.stderr}`);
12349
12486
  const resultMutation = {
12350
12487
  status: "completed",
12351
12488
  runnerId,
12352
- idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID2()}`,
12489
+ idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID3()}`,
12353
12490
  result: scanResult,
12354
12491
  tool: toolName,
12355
12492
  durationMs,
@@ -12383,7 +12520,7 @@ ${toolResult.stderr}`);
12383
12520
  const failedMutation = {
12384
12521
  status: "failed",
12385
12522
  runnerId,
12386
- idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID2()}`,
12523
+ idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID3()}`,
12387
12524
  tool: toolName,
12388
12525
  durationMs,
12389
12526
  ...sessionTelemetry,
@@ -12445,7 +12582,7 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
12445
12582
  const resultMutation = {
12446
12583
  status: "completed",
12447
12584
  runnerId,
12448
- idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID2()}`,
12585
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID3()}`,
12449
12586
  result: refreshResult,
12450
12587
  tool: toolName,
12451
12588
  durationMs,
@@ -12491,7 +12628,7 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
12491
12628
  const failedMutation = {
12492
12629
  status: "failed",
12493
12630
  runnerId,
12494
- idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID2()}`,
12631
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID3()}`,
12495
12632
  tool: toolName,
12496
12633
  durationMs,
12497
12634
  ...sessionTelemetry,
@@ -12552,7 +12689,7 @@ ${toolResult.stderr}`);
12552
12689
  const resultMutation = {
12553
12690
  status: "completed",
12554
12691
  runnerId,
12555
- idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID2()}`,
12692
+ idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID3()}`,
12556
12693
  result: verificationResult,
12557
12694
  tool: toolName,
12558
12695
  durationMs,
@@ -12586,7 +12723,7 @@ ${toolResult.stderr}`);
12586
12723
  const failedMutation = {
12587
12724
  status: "failed",
12588
12725
  runnerId,
12589
- idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID2()}`,
12726
+ idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID3()}`,
12590
12727
  tool: toolName,
12591
12728
  durationMs,
12592
12729
  ...sessionTelemetry,
@@ -12647,7 +12784,7 @@ ${toolResult.stderr}`);
12647
12784
  const resultMutation = {
12648
12785
  status: "completed",
12649
12786
  runnerId,
12650
- idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID2()}`,
12787
+ idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID3()}`,
12651
12788
  result: scanResult,
12652
12789
  tool: toolName,
12653
12790
  durationMs,
@@ -12681,7 +12818,7 @@ ${toolResult.stderr}`);
12681
12818
  const failedMutation = {
12682
12819
  status: "failed",
12683
12820
  runnerId,
12684
- idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID2()}`,
12821
+ idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID3()}`,
12685
12822
  tool: toolName,
12686
12823
  durationMs,
12687
12824
  ...sessionTelemetry,
@@ -12742,7 +12879,7 @@ ${toolResult.stderr}`);
12742
12879
  const resultMutation = {
12743
12880
  status: "completed",
12744
12881
  runnerId,
12745
- idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID2()}`,
12882
+ idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID3()}`,
12746
12883
  result: gateResult,
12747
12884
  tool: toolName,
12748
12885
  durationMs,
@@ -12776,7 +12913,7 @@ ${toolResult.stderr}`);
12776
12913
  const failedMutation = {
12777
12914
  status: "failed",
12778
12915
  runnerId,
12779
- idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID2()}`,
12916
+ idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID3()}`,
12780
12917
  tool: toolName,
12781
12918
  durationMs,
12782
12919
  ...sessionTelemetry,
@@ -12921,6 +13058,14 @@ async function loadPairedApiContext(root, apiUrl) {
12921
13058
  })
12922
13059
  };
12923
13060
  }
13061
+ function runnerIdentityScope(metadata) {
13062
+ return {
13063
+ accountId: metadata.amistioAccountId,
13064
+ projectId: metadata.amistioProjectId,
13065
+ repositoryLinkId: metadata.repositoryLinkId,
13066
+ machineId: runnerMachineId()
13067
+ };
13068
+ }
12924
13069
  async function loadProjectNextAction(apiClient, projectId, repositoryLinkId, root) {
12925
13070
  const [{ workItems }, { documents }, { runners }] = await Promise.all([
12926
13071
  apiClient.listWorkItems(projectId),
@@ -13005,7 +13150,7 @@ async function prepareToolSession({
13005
13150
  });
13006
13151
  return { ...selection, toolSession: toolSession2 };
13007
13152
  }
13008
- const toolSessionId = `tool_session_${randomUUID2()}`;
13153
+ const toolSessionId = `tool_session_${randomUUID3()}`;
13009
13154
  const { toolSession } = await apiClient.createToolSession(projectId, {
13010
13155
  toolSessionId,
13011
13156
  repositoryLinkId,
@@ -13112,6 +13257,23 @@ function truncateLogExcerpt(value) {
13112
13257
  function formatHostHelperCapability(label, support) {
13113
13258
  return `${label}: ${support.supported ? "supported" : "unsupported"}${support.reason ? ` (${support.reason})` : ""}`;
13114
13259
  }
13260
+ function harnessProviderCheckTargets() {
13261
+ return [
13262
+ { label: "GitHub Models Direct", request: { providerId: "github-models", providerClientId: "github-models-api", routeType: "directProvider" } },
13263
+ { label: "GitHub Copilot SDK", request: { providerId: "github-copilot", providerClientId: "github-copilot-sdk", routeType: "agentClient" } }
13264
+ ];
13265
+ }
13266
+ function formatProviderAuthStatusLines(status) {
13267
+ const lines = [
13268
+ `Target: ${status.providerId}:${status.providerClientId}:${status.routeType}`,
13269
+ `Checked: ${status.checkedAt}`
13270
+ ];
13271
+ if (status.authMethodLabel) lines.push(`Auth: ${status.authMethodLabel}`);
13272
+ if (status.modelCount !== void 0) lines.push(`Models: ${status.modelCount}`);
13273
+ if (status.message) lines.push(`Message: ${status.message}`);
13274
+ if (status.errorCode) lines.push(`Code: ${status.errorCode}`);
13275
+ return lines;
13276
+ }
13115
13277
  function collectRepeatedOption(value, previous) {
13116
13278
  return [...previous, value];
13117
13279
  }
@@ -13138,7 +13300,7 @@ function parseReasoningEffort(value) {
13138
13300
  throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
13139
13301
  }
13140
13302
  function inferRepoName(root) {
13141
- return path17.basename(path17.resolve(root)) || "repository";
13303
+ return path18.basename(path18.resolve(root)) || "repository";
13142
13304
  }
13143
13305
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
13144
13306
  return createHash9("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
@@ -13415,7 +13577,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode(), concurr
13415
13577
  return {
13416
13578
  version: CLI_VERSION,
13417
13579
  mode,
13418
- hostname: os8.hostname(),
13580
+ hostname: os9.hostname(),
13419
13581
  ...runnerIsolationCapabilityMetadata(),
13420
13582
  maxConcurrentWork: concurrencyMetadata.maxConcurrentWork,
13421
13583
  activeClaimLaneIds: concurrencyMetadata.activeClaimLaneIds,
@@ -13440,7 +13602,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode(), concurr
13440
13602
  };
13441
13603
  }
13442
13604
  function runnerMachineId() {
13443
- return createHash9("sha256").update(`${os8.hostname()}:${os8.platform()}:${os8.arch()}`).digest("hex").slice(0, 20);
13605
+ return createHash9("sha256").update(`${os9.hostname()}:${os9.platform()}:${os9.arch()}`).digest("hex").slice(0, 20);
13444
13606
  }
13445
13607
  async function delay(milliseconds) {
13446
13608
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amistio/cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",