@amistio/cli 0.1.38 → 0.1.40

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 -5
  2. package/dist/index.js +315 -183
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@ 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 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.
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
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
 
@@ -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,26 +57,29 @@ 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
 
72
78
  The runner advertises its supported work kinds in heartbeats. Current runners can claim read-only `projectContextRefresh` jobs from the workspace Context panel and create due runner-driven refreshes when no fresh approved map exists. Context refreshes inspect the paired checkout locally without modifying files and submit only bounded summaries, slices, entities, relations, safe citations, confidence, freshness, and repo-relative paths. If a submitted context refresh contains unsafe evidence, unsafe paths, or a map too large to store safely, Amistio marks the refresh failed with a safe reason instead of storing the rejected raw result. Approved maps are reused as context packs for source-aware assistant and impact-preview work. Current runners can also claim read-only issue diagnosis jobs from the web Issues panel, generate root-cause analysis and a proposed fix, and submit that result without modifying source. They can claim manual read-only `appEvaluationScan` jobs from the workspace Evaluate panel and create at most one due hourly evaluation during normal watch/background polling when app evaluation is enabled for the repository link. Evaluation results contain bounded summaries, safe evidence, suggested actions, lifecycle proposals, and repo-relative paths only. Current runners can also claim manual read-only `securityPostureScan` jobs from the workspace Security panel and create due daily posture checks during normal watch/background polling. Security scan results contain bounded summaries, standard references, safe evidence, and repo-relative paths only. Current runners can claim manual read-only `testQualityScan` jobs from the workspace Test panel and create one due daily Test scan per repository when Test quality is enabled. Test scans run only existing lint, typecheck, test, coverage, build, or verify commands and submit bounded command summaries, coverage summaries, safe findings, blocked reasons, warnings, and repo-relative paths. Missing tests, missing coverage, low coverage, failing checks, flaky tests, and test gaps create reviewable plan-backed findings in the app. Current runners also claim `implementationTestGate` jobs before implementation completion, PR handoff, or runner-managed push; a passing gate is required unless the web Test panel records an audited override. Blocked implementation Test gates submit structured Test findings, such as `blockedEnvironment`, with safe evidence, a suggested action, and a verification plan. Current runners can claim read-only `implementationVerification` jobs from Tasks to prove whether completed implementation work actually landed; verification submits bounded acceptance-criteria evidence, checks, gaps, outcome, and recommendation without mutating source. Source, secrets, environment variables, command lines, process lists, credentials, provider sessions, and arbitrary local paths stay local. Implementation or cleanup is queued separately only after the user approves an issue analysis, app evaluation finding, security remediation plan, or Test quality plan in the app.
73
79
 
74
- Approved implementation work uses Git as the handoff boundary. During worktree preflight, the runner locally copies eligible ignored root dotenv files such as `.env.local` or `.env.test.local` from the paired checkout into the implementation worktree when the target is missing and ignored, so local tests can use the same machine configuration. Dotenv values, variable names, file contents, and local paths are not uploaded to Amistio, and copied dotenv files stay ignored so PR handoff does not commit them. After the local tool completes successfully, the runner materializes approved Markdown, MDX, and HTML project-brain artifacts for the same work scope into the isolated worktree before final Git status. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; no-change completion requires no source changes and no approved artifact changes, and runner-created no-change worktrees are removed after final clean checks. Prepare the runner machine with Git commit identity, fetch/push permission to the linked remote, and `gh auth status`. If artifact materialization, commit, fetch/rebase, push, or PR creation fails, the work item is blocked with safe recovery choices; source files and patches are not uploaded to Amistio. The Work panel can queue scoped Retry handoff or Retry cleanup commands to the paired runner for the same work item, branch, and worktree key. Rebase conflicts capture bounded repo-relative conflict files and try `git rebase --abort` so the implementation branch can be retried or manually reviewed without leaving an active rebase. Dirty, unmerged, or ambiguous worktrees are preserved rather than discarded.
80
+ Approved implementation work uses Git as the handoff boundary. During worktree preflight, the runner locally copies eligible ignored root dotenv files such as `.env.local` or `.env.test.local` from the paired checkout into the implementation worktree when the target is missing and ignored, so local tests can use the same machine configuration. Dotenv values, variable names, file contents, and local paths are not uploaded to Amistio, and copied dotenv files stay ignored so PR handoff does not commit them. After the local tool completes successfully, the runner materializes approved Markdown, MDX, and HTML project-brain artifacts for the same work scope into the isolated worktree before final Git status. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; no-change completion requires no source changes and no approved artifact changes, and runner-created no-change worktrees are removed after final clean checks. Prepare the runner machine with Git commit identity, fetch/push permission to the linked remote, and `gh auth status`. If artifact materialization, commit, fetch/rebase, push, or PR creation fails, the work item is blocked with safe recovery choices; source files and patches are not uploaded to Amistio. The Work panel can queue scoped Retry handoff or Retry cleanup commands only to the runner that owns the preserved worktree for the same work item, branch, and worktree key. Rebase conflicts capture bounded repo-relative conflict files and try `git rebase --abort` so the implementation branch can be retried or manually reviewed without leaving an active rebase. Dirty, unmerged, or ambiguous worktrees are preserved rather than discarded.
75
81
 
76
- Failed or stale work can be requeued from the web Tasks panel. Requeue creates a new linked work attempt and preserves the original terminal attempt for audit history; it is blocked while equivalent work is already active or when the paired runner does not advertise the needed work kind. Completed implementation status is separate from proof: queue `implementationVerification` from Tasks when a plan needs source-aware evidence before cleanup or implementation status decisions.
82
+ Failed or stale work can be requeued from the web Tasks panel. Requeue creates a new linked work attempt and preserves the original terminal attempt for audit history; Requeue all sends one backend batch that recomputes safe candidates, reports already-active and skipped rows, and still uses linked attempts. Requeue is blocked while equivalent work is already active or when the paired runner does not advertise the needed work kind. Completed implementation status is separate from proof: queue `implementationVerification` from Tasks when a plan needs source-aware evidence before cleanup or implementation status decisions.
77
83
 
78
84
  Runner setup and local-tool execution use bounded failure controls. `amistio run --watch` retries Git worktree preflight failures by releasing the claim for another attempt, then fails the work item after `--max-preflight-attempts` attempts, defaulting to 3. Active local-tool runs renew the work lease, and `--tool-timeout-seconds` caps tool execution, defaulting to 1800 seconds.
79
85
 
@@ -83,7 +89,7 @@ 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
 
86
- If watch mode reports that the runner was forgotten by the server, stop the foreground process and start or pair a new runner from the Runner panel. Forgotten runner IDs are intentionally blocked from heartbeats and work claims, so the CLI exits instead of retrying with the tombstoned identity.
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.
87
93
 
88
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.
89
95
 
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
@@ -3145,8 +3145,8 @@ var toolSessionMutationSchema = z3.object({
3145
3145
  });
3146
3146
  function resolveApiUrl(apiUrl, urlPath) {
3147
3147
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
3148
- const path18 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
3149
- return new URL(`${base}${path18}`);
3148
+ const path19 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
3149
+ return new URL(`${base}${path19}`);
3150
3150
  }
3151
3151
 
3152
3152
  // src/orchestrator.ts
@@ -5494,6 +5494,102 @@ async function readRunnerDaemonMetadataFile(filePath) {
5494
5494
  }
5495
5495
  }
5496
5496
 
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";
5500
+ import os5 from "node:os";
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
+
5497
5593
  // src/runner-watch-errors.ts
5498
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.";
5499
5595
  async function handleRunnerWatchError(options) {
@@ -5531,10 +5627,10 @@ ${options.detail}`);
5531
5627
 
5532
5628
  // src/runner-service.ts
5533
5629
  import { spawn as spawn3 } from "node:child_process";
5534
- import { createHash as createHash4 } from "node:crypto";
5535
- import { mkdir as mkdir8, readFile as readFile7, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
5536
- import os5 from "node:os";
5537
- import path9 from "node:path";
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";
5538
5634
  function detectRunnerServicePlatform(platform = process.platform) {
5539
5635
  if (platform === "darwin") return "launchd";
5540
5636
  if (platform === "linux") return "systemd";
@@ -5545,19 +5641,19 @@ function createRunnerServiceDescriptor(input) {
5545
5641
  if (platform === "unsupported") {
5546
5642
  throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
5547
5643
  }
5548
- const homeDir = input.homeDir ?? os5.homedir();
5644
+ const homeDir = input.homeDir ?? os6.homedir();
5549
5645
  const serviceName = runnerServiceName(input);
5550
5646
  const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
5551
5647
  const now = (/* @__PURE__ */ new Date()).toISOString();
5552
5648
  const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
5553
- const logPath = path9.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
5649
+ const logPath = path10.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
5554
5650
  const metadata = {
5555
5651
  schemaVersion: 1,
5556
5652
  accountId: input.accountId,
5557
5653
  projectId: input.projectId,
5558
5654
  repositoryLinkId: input.repositoryLinkId,
5559
5655
  runnerId: input.runnerId,
5560
- rootDir: path9.resolve(input.rootDir),
5656
+ rootDir: path10.resolve(input.rootDir),
5561
5657
  apiUrl: input.apiUrl,
5562
5658
  serviceName,
5563
5659
  serviceFilePath,
@@ -5574,9 +5670,9 @@ function createRunnerServiceDescriptor(input) {
5574
5670
  }
5575
5671
  async function installRunnerService(input, options = {}) {
5576
5672
  const descriptor = createRunnerServiceDescriptor(input);
5577
- await mkdir8(path9.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
5578
- await mkdir8(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
5579
- 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 });
5580
5676
  await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
5581
5677
  if (options.activate !== false) {
5582
5678
  const activation = await activateRunnerService(descriptor.metadata);
@@ -5598,7 +5694,7 @@ async function removeRunnerService(input) {
5598
5694
  }
5599
5695
  async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
5600
5696
  try {
5601
- const parsed = JSON.parse(await readFile7(runnerServiceMetadataPath(input, metadataDir), "utf8"));
5697
+ const parsed = JSON.parse(await readFile8(runnerServiceMetadataPath(input, metadataDir), "utf8"));
5602
5698
  if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
5603
5699
  return void 0;
5604
5700
  }
@@ -5608,8 +5704,8 @@ async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetad
5608
5704
  }
5609
5705
  }
5610
5706
  async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
5611
- await mkdir8(metadataDir, { recursive: true });
5612
- 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 });
5613
5709
  }
5614
5710
  async function runnerServiceRuntimeStatus(metadata) {
5615
5711
  if (metadata.platform === "launchd") {
@@ -5692,18 +5788,18 @@ WantedBy=default.target
5692
5788
  }
5693
5789
  function runnerServiceFilePath(platform, serviceName, homeDir) {
5694
5790
  if (platform === "launchd") {
5695
- return path9.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
5791
+ return path10.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
5696
5792
  }
5697
- return path9.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
5793
+ return path10.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
5698
5794
  }
5699
5795
  function runnerServiceMetadataPath(input, metadataDir) {
5700
- return path9.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
5796
+ return path10.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
5701
5797
  }
5702
5798
  function runnerServiceName(input) {
5703
5799
  return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
5704
5800
  }
5705
5801
  function runnerServiceKey(input) {
5706
- 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");
5707
5803
  }
5708
5804
  function launchdDomain() {
5709
5805
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
@@ -6031,9 +6127,9 @@ function createSmokeSession({ now, status, lastActivityAt }) {
6031
6127
 
6032
6128
  // src/sync.ts
6033
6129
  import { execFile as execFile3 } from "node:child_process";
6034
- import { createHash as createHash5 } from "node:crypto";
6035
- import { mkdir as mkdir9, readdir as readdir5, readFile as readFile8, stat as stat4, writeFile as writeFile9 } from "node:fs/promises";
6036
- 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";
6037
6133
  import { promisify as promisify3 } from "node:util";
6038
6134
  var execFileAsync3 = promisify3(execFile3);
6039
6135
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
@@ -6129,7 +6225,7 @@ async function readLocalSyncedDocuments(rootDir) {
6129
6225
  const documentFiles = await findBrainDocumentFiles(rootDir);
6130
6226
  const documents = [];
6131
6227
  for (const fullPath of documentFiles) {
6132
- const raw = await readFile8(fullPath, "utf8");
6228
+ const raw = await readFile9(fullPath, "utf8");
6133
6229
  const repoPath = toRepoPath(rootDir, fullPath);
6134
6230
  const parsed = parseSyncedDocument(raw, repoPath);
6135
6231
  if (!parsed) {
@@ -6179,8 +6275,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
6179
6275
  result.skipped.push(document.repoPath);
6180
6276
  continue;
6181
6277
  }
6182
- await mkdir9(path10.dirname(fullPath), { recursive: true });
6183
- await writeFile9(fullPath, createSyncedDocumentContent(document), "utf8");
6278
+ await mkdir10(path11.dirname(fullPath), { recursive: true });
6279
+ await writeFile10(fullPath, createSyncedDocumentContent(document), "utf8");
6184
6280
  result.written.push(document.repoPath);
6185
6281
  }
6186
6282
  return result;
@@ -6309,7 +6405,7 @@ function parseSyncedHtml(content) {
6309
6405
  }
6310
6406
  async function readExistingSyncedDocument(fullPath) {
6311
6407
  try {
6312
- const raw = await readFile8(fullPath, "utf8");
6408
+ const raw = await readFile9(fullPath, "utf8");
6313
6409
  const parsed = parseSyncedDocument(raw, fullPath);
6314
6410
  if (!parsed) {
6315
6411
  return { exists: true };
@@ -6334,7 +6430,7 @@ async function readExistingSyncedDocument(fullPath) {
6334
6430
  async function findBrainDocumentFiles(rootDir) {
6335
6431
  const files = [];
6336
6432
  for (const syncRoot of [...syncRoots, htmlSyncRoot]) {
6337
- const fullRoot = path10.join(rootDir, syncRoot);
6433
+ const fullRoot = path11.join(rootDir, syncRoot);
6338
6434
  if (!await exists2(fullRoot)) {
6339
6435
  continue;
6340
6436
  }
@@ -6344,7 +6440,7 @@ async function findBrainDocumentFiles(rootDir) {
6344
6440
  }
6345
6441
  async function walkBrainDocumentFiles(directory, files) {
6346
6442
  for (const entry of await readdir5(directory, { withFileTypes: true })) {
6347
- const fullPath = path10.join(directory, entry.name);
6443
+ const fullPath = path11.join(directory, entry.name);
6348
6444
  if (entry.isDirectory()) {
6349
6445
  await walkBrainDocumentFiles(fullPath, files);
6350
6446
  } else if (entry.isFile() && /\.(md|mdx|html?)$/i.test(entry.name)) {
@@ -6353,23 +6449,23 @@ async function walkBrainDocumentFiles(directory, files) {
6353
6449
  }
6354
6450
  }
6355
6451
  function safeRepoPath(rootDir, repoPath) {
6356
- if (path10.isAbsolute(repoPath)) {
6452
+ if (path11.isAbsolute(repoPath)) {
6357
6453
  throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
6358
6454
  }
6359
- const normalized = path10.normalize(repoPath);
6360
- if (normalized === ".." || normalized.startsWith(`..${path10.sep}`)) {
6455
+ const normalized = path11.normalize(repoPath);
6456
+ if (normalized === ".." || normalized.startsWith(`..${path11.sep}`)) {
6361
6457
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
6362
6458
  }
6363
- const root = path10.resolve(rootDir);
6364
- const fullPath = path10.resolve(root, normalized);
6365
- 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}`)) {
6366
6462
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
6367
6463
  }
6368
6464
  return fullPath;
6369
6465
  }
6370
6466
  function isControlPlanePath(repoPath) {
6371
- const normalized = path10.normalize(repoPath);
6372
- 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}`);
6373
6469
  }
6374
6470
  function canonicalControlPlaneRepoPath(repoPath) {
6375
6471
  const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -6380,16 +6476,16 @@ function canonicalControlPlaneRepoPath(repoPath) {
6380
6476
  return normalized;
6381
6477
  }
6382
6478
  function toRepoPath(rootDir, fullPath) {
6383
- return path10.relative(rootDir, fullPath).split(path10.sep).join("/");
6479
+ return path11.relative(rootDir, fullPath).split(path11.sep).join("/");
6384
6480
  }
6385
6481
  function inferTitle(content, repoPath) {
6386
6482
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
6387
6483
  if (heading) return heading;
6388
6484
  const htmlHeading = content.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
6389
- return htmlHeading || path10.basename(repoPath, path10.extname(repoPath));
6485
+ return htmlHeading || path11.basename(repoPath, path11.extname(repoPath));
6390
6486
  }
6391
6487
  async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
6392
- const root = path10.resolve(rootDir);
6488
+ const root = path11.resolve(rootDir);
6393
6489
  const maxBytes = (options.maxFileKb ?? defaultAutoSyncMaxFileKb) * 1024;
6394
6490
  const syncedAt = options.syncedAt ?? (/* @__PURE__ */ new Date()).toISOString();
6395
6491
  const existingById = new Map(existingDocuments.map((document) => [document.documentId, document]));
@@ -6414,7 +6510,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
6414
6510
  skipped.push({ repoPath: normalizedRepoPath, reason: "tooLarge" });
6415
6511
  continue;
6416
6512
  }
6417
- const content = await readFile8(fullPath, "utf8").catch(() => void 0);
6513
+ const content = await readFile9(fullPath, "utf8").catch(() => void 0);
6418
6514
  if (content === void 0) {
6419
6515
  skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
6420
6516
  continue;
@@ -6479,7 +6575,7 @@ async function listAutoSyncCandidatePaths(rootDir) {
6479
6575
  }
6480
6576
  const files = [];
6481
6577
  for (const syncRoot of [...syncRoots, htmlSyncRoot, ...legacySyncRoots]) {
6482
- const fullRoot = path10.join(rootDir, syncRoot);
6578
+ const fullRoot = path11.join(rootDir, syncRoot);
6483
6579
  if (await exists2(fullRoot)) {
6484
6580
  await walkAutoSyncFiles(rootDir, fullRoot, files);
6485
6581
  }
@@ -6488,8 +6584,8 @@ async function listAutoSyncCandidatePaths(rootDir) {
6488
6584
  }
6489
6585
  async function walkAutoSyncFiles(rootDir, directory, files) {
6490
6586
  for (const entry of await readdir5(directory, { withFileTypes: true }).catch(() => [])) {
6491
- const fullPath = path10.join(directory, entry.name);
6492
- const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
6587
+ const fullPath = path11.join(directory, entry.name);
6588
+ const repoPath = normalizeRepoPath3(path11.relative(rootDir, fullPath));
6493
6589
  if (entry.isDirectory()) {
6494
6590
  if (!autoSyncExcludedDirectoryNames.has(entry.name)) {
6495
6591
  await walkAutoSyncFiles(rootDir, fullPath, files);
@@ -6523,7 +6619,7 @@ function legacyDocumentTypeForRepoPath(repoPath) {
6523
6619
  return root && root in documentTypeByRoot ? documentTypeByRoot[root] : void 0;
6524
6620
  }
6525
6621
  function stableExternalDocumentId(metadata, repoPath) {
6526
- 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)}`;
6527
6623
  }
6528
6624
  function normalizeRepoPath3(repoPath) {
6529
6625
  return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -6551,9 +6647,9 @@ async function exists2(filePath) {
6551
6647
  }
6552
6648
 
6553
6649
  // src/tool-session-store.ts
6554
- import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile10 } from "node:fs/promises";
6555
- import os6 from "node:os";
6556
- 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";
6557
6653
  var LocalToolSessionStore = class {
6558
6654
  constructor(filePath = defaultSessionStorePath()) {
6559
6655
  this.filePath = filePath;
@@ -6567,12 +6663,12 @@ var LocalToolSessionStore = class {
6567
6663
  async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
6568
6664
  const data = await this.read();
6569
6665
  data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
6570
- await mkdir10(path11.dirname(this.filePath), { recursive: true });
6571
- 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");
6572
6668
  }
6573
6669
  async read() {
6574
6670
  try {
6575
- return JSON.parse(await readFile9(this.filePath, "utf8"));
6671
+ return JSON.parse(await readFile10(this.filePath, "utf8"));
6576
6672
  } catch {
6577
6673
  return {};
6578
6674
  }
@@ -6580,16 +6676,16 @@ var LocalToolSessionStore = class {
6580
6676
  };
6581
6677
  function defaultSessionStorePath() {
6582
6678
  if (process.platform === "darwin") {
6583
- 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");
6584
6680
  }
6585
6681
  if (process.platform === "win32") {
6586
- 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");
6587
6683
  }
6588
- 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");
6589
6685
  }
6590
6686
 
6591
6687
  // src/work-runner.ts
6592
- import path12 from "node:path";
6688
+ import path13 from "node:path";
6593
6689
  var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
6594
6690
  var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
6595
6691
  var assistantAnswerStart = "AMISTIO_ASSISTANT_ANSWER_START";
@@ -8072,15 +8168,15 @@ function normalizeProjectContextRepoPath(value, options) {
8072
8168
  if (!trimmed || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed) || /^file:/i.test(trimmed) || /^[A-Za-z]:($|[^\\/])/.test(trimmed)) {
8073
8169
  throwUnsafeProjectContextPath();
8074
8170
  }
8075
- 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);
8076
8172
  if (!absolute) {
8077
8173
  return normalizeRelativeProjectContextPath(trimmed);
8078
8174
  }
8079
8175
  if (!options.repositoryRoot) {
8080
8176
  throwUnsafeProjectContextPath();
8081
8177
  }
8082
- const useWindowsPathRules = path12.win32.isAbsolute(trimmed) && !trimmed.startsWith("/");
8083
- 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));
8084
8180
  return normalizeRelativeProjectContextPath(relativePath);
8085
8181
  }
8086
8182
  function normalizeRelativeProjectContextPath(value) {
@@ -8131,48 +8227,16 @@ function stripJsonFence(value) {
8131
8227
  return trimmed.replace(/^```(?:json)?\s*/i, "").replace(/```$/i, "").trim();
8132
8228
  }
8133
8229
 
8134
- // src/runner-status.ts
8135
- import { createHash as createHash6 } from "node:crypto";
8136
- var watchStateReminderMs = 60 * 1e3;
8137
- function formatWatchStartupContext(input) {
8138
- return [
8139
- `Runner ${input.runnerId} is watching project ${input.projectId}.`,
8140
- `Repository link: ${input.repositoryLinkId}`,
8141
- `API: ${input.apiUrl}`,
8142
- `Polling interval: ${input.intervalSeconds}s. Press Ctrl+C to stop.`
8143
- ];
8144
- }
8145
- function formatWatchIdleLine(action, intervalSeconds) {
8146
- return `${formatProjectNextAction(action)} Checking again in ${intervalSeconds}s.`;
8147
- }
8148
- function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
8149
- const key = watchStateKey(action);
8150
- if (!previous || previous.key !== key) {
8151
- return true;
8152
- }
8153
- if (action.kind === "workCompleted") {
8154
- return false;
8155
- }
8156
- return nowMs - previous.printedAtMs >= reminderMs;
8157
- }
8158
- function watchStateKey(action) {
8159
- return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
8160
- }
8161
- function stableRunnerId(input) {
8162
- const digest = createHash6("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
8163
- return `runner_${digest}`;
8164
- }
8165
-
8166
8230
  // src/runner-resources.ts
8167
- import os7 from "node:os";
8231
+ import os8 from "node:os";
8168
8232
  var defaultRuntime = {
8169
8233
  nowMs: () => Date.now(),
8170
8234
  memoryUsage: () => process.memoryUsage(),
8171
8235
  uptime: () => process.uptime(),
8172
8236
  cpuUsage: () => process.cpuUsage(),
8173
- totalmem: () => os7.totalmem(),
8174
- freemem: () => os7.freemem(),
8175
- loadavg: () => os7.loadavg()
8237
+ totalmem: () => os8.totalmem(),
8238
+ freemem: () => os8.freemem(),
8239
+ loadavg: () => os8.loadavg()
8176
8240
  };
8177
8241
  var previousRunnerResourceSample;
8178
8242
  function sampleCurrentRunnerResourceUsage() {
@@ -8285,8 +8349,8 @@ function roundNumber(value, digits) {
8285
8349
  // src/importer.ts
8286
8350
  import { execFile as execFile4 } from "node:child_process";
8287
8351
  import { createHash as createHash7 } from "node:crypto";
8288
- import { readdir as readdir6, readFile as readFile10, stat as stat5 } from "node:fs/promises";
8289
- 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";
8290
8354
  import { promisify as promisify4 } from "node:util";
8291
8355
  var execFileAsync4 = promisify4(execFile4);
8292
8356
  var defaultMaxFileKb = 256;
@@ -8307,12 +8371,12 @@ var documentFolderByType = {
8307
8371
  workflow: "docs/workflows"
8308
8372
  };
8309
8373
  async function inspectLocalRepository(rootDir, defaultBranch) {
8310
- const requestedRoot = path13.resolve(rootDir);
8374
+ const requestedRoot = path14.resolve(rootDir);
8311
8375
  const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
8312
8376
  const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
8313
8377
  const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
8314
8378
  const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
8315
- const repoName = (parsedCloneUrl?.repoName ?? path13.basename(root)) || "repository";
8379
+ const repoName = (parsedCloneUrl?.repoName ?? path14.basename(root)) || "repository";
8316
8380
  const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
8317
8381
  return {
8318
8382
  rootDir: root,
@@ -8324,7 +8388,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
8324
8388
  };
8325
8389
  }
8326
8390
  async function scanLegacyDocuments(options) {
8327
- const rootDir = path13.resolve(options.rootDir);
8391
+ const rootDir = path14.resolve(options.rootDir);
8328
8392
  const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
8329
8393
  const skipped = [];
8330
8394
  const candidates = [];
@@ -8344,7 +8408,7 @@ async function scanLegacyDocuments(options) {
8344
8408
  skipped.push({ repoPath, reason: "excluded" });
8345
8409
  continue;
8346
8410
  }
8347
- const fullPath = path13.join(rootDir, ...repoPath.split("/"));
8411
+ const fullPath = path14.join(rootDir, ...repoPath.split("/"));
8348
8412
  const fileStat = await stat5(fullPath).catch(() => void 0);
8349
8413
  if (!fileStat?.isFile()) {
8350
8414
  skipped.push({ repoPath, reason: "unreadable" });
@@ -8354,7 +8418,7 @@ async function scanLegacyDocuments(options) {
8354
8418
  skipped.push({ repoPath, reason: "tooLarge" });
8355
8419
  continue;
8356
8420
  }
8357
- const content = await readFile10(fullPath, "utf8").catch(() => void 0);
8421
+ const content = await readFile11(fullPath, "utf8").catch(() => void 0);
8358
8422
  if (content === void 0) {
8359
8423
  skipped.push({ repoPath, reason: "unreadable" });
8360
8424
  continue;
@@ -8446,8 +8510,8 @@ async function listRepositoryPaths(rootDir) {
8446
8510
  async function walkRepository(rootDir, directory, files) {
8447
8511
  const entries = await readdir6(directory, { withFileTypes: true }).catch(() => []);
8448
8512
  for (const entry of entries) {
8449
- const fullPath = path13.join(directory, entry.name);
8450
- const repoPath = normalizeRepoPath4(path13.relative(rootDir, fullPath));
8513
+ const fullPath = path14.join(directory, entry.name);
8514
+ const repoPath = normalizeRepoPath4(path14.relative(rootDir, fullPath));
8451
8515
  if (entry.isDirectory()) {
8452
8516
  if (!excludedDirectoryNames.has(entry.name)) {
8453
8517
  await walkRepository(rootDir, fullPath, files);
@@ -8515,9 +8579,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
8515
8579
  usedPaths.add(basePath);
8516
8580
  return basePath;
8517
8581
  }
8518
- const extension = path13.posix.extname(basePath) || ".md";
8519
- const directory = path13.posix.dirname(basePath);
8520
- 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);
8521
8585
  const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
8522
8586
  usedPaths.add(uniquePath);
8523
8587
  return uniquePath;
@@ -8590,7 +8654,7 @@ function inferTitle2(content, repoPath) {
8590
8654
  if (heading) return heading;
8591
8655
  const htmlHeading = body.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
8592
8656
  if (htmlHeading) return htmlHeading;
8593
- 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();
8594
8658
  return titleCase(basename || "Imported Document");
8595
8659
  }
8596
8660
  function stripFrontmatter(content) {
@@ -8624,7 +8688,7 @@ async function runGit2(args) {
8624
8688
 
8625
8689
  // src/runner-actions.ts
8626
8690
  import { spawn as spawn4 } from "node:child_process";
8627
- import path14 from "node:path";
8691
+ import path15 from "node:path";
8628
8692
  function buildBackgroundRunnerArgs(options) {
8629
8693
  const args = [
8630
8694
  "run",
@@ -8634,7 +8698,7 @@ function buildBackgroundRunnerArgs(options) {
8634
8698
  "--runner-id",
8635
8699
  options.runnerId,
8636
8700
  "--root",
8637
- path14.resolve(options.root),
8701
+ path15.resolve(options.root),
8638
8702
  "--session",
8639
8703
  options.session,
8640
8704
  "--interval-seconds",
@@ -8752,8 +8816,8 @@ function truncateProcessOutput(value) {
8752
8816
 
8753
8817
  // src/git-worktree.ts
8754
8818
  import { execFile as execFile5 } from "node:child_process";
8755
- import { copyFile, lstat, mkdir as mkdir11, readdir as readdir7, stat as stat6 } from "node:fs/promises";
8756
- 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";
8757
8821
  import { promisify as promisify5 } from "node:util";
8758
8822
  var execFileAsync5 = promisify5(execFile5);
8759
8823
  var exactLocalEnvironmentFiles = /* @__PURE__ */ new Set([".env", ".env.local", ".env.development", ".env.development.local", ".env.test", ".env.test.local", ".env.production", ".env.production.local"]);
@@ -8785,7 +8849,7 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
8785
8849
  const preparedLocalEnvironmentFileCount2 = await prepareLocalWorktreeEnvironment(repoRoot, worktreePath);
8786
8850
  return { ...identity, baseRevision, worktreePath, ...preparedLocalEnvironmentFileCount2 ? { preparedLocalEnvironmentFileCount: preparedLocalEnvironmentFileCount2 } : {} };
8787
8851
  }
8788
- await mkdir11(path15.dirname(worktreePath), { recursive: true });
8852
+ await mkdir12(path16.dirname(worktreePath), { recursive: true });
8789
8853
  const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
8790
8854
  const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
8791
8855
  await gitOutput(repoRoot, worktreeArgs).catch((error) => {
@@ -8803,9 +8867,9 @@ async function resolveExistingGitWorktreeIsolation(rootDir, workItem) {
8803
8867
  return { ...identity, baseRevision, worktreePath };
8804
8868
  }
8805
8869
  function localWorktreePath(repoRoot, worktreeKey) {
8806
- const repoName = path15.basename(repoRoot);
8870
+ const repoName = path16.basename(repoRoot);
8807
8871
  const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
8808
- return path15.join(path15.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
8872
+ return path16.join(path16.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
8809
8873
  }
8810
8874
  async function assertExistingWorktree(worktreePath, branch) {
8811
8875
  await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
@@ -8831,8 +8895,8 @@ async function prepareLocalWorktreeEnvironment(repoRoot, worktreePath) {
8831
8895
  const candidates = await localEnvironmentFileCandidates(repoRoot);
8832
8896
  let preparedCount = 0;
8833
8897
  for (const candidate of candidates) {
8834
- const sourcePath = path15.join(repoRoot, candidate);
8835
- const targetPath = path15.join(worktreePath, candidate);
8898
+ const sourcePath = path16.join(repoRoot, candidate);
8899
+ const targetPath = path16.join(worktreePath, candidate);
8836
8900
  if (await pathExists(targetPath)) {
8837
8901
  continue;
8838
8902
  }
@@ -8863,7 +8927,7 @@ async function localEnvironmentFileCandidates(repoRoot) {
8863
8927
  if (!isRootFileName(name)) {
8864
8928
  continue;
8865
8929
  }
8866
- const source = await lstat(path15.join(repoRoot, name)).catch(() => void 0);
8930
+ const source = await lstat(path16.join(repoRoot, name)).catch(() => void 0);
8867
8931
  if (source?.isFile()) {
8868
8932
  candidates.push(name);
8869
8933
  }
@@ -8874,7 +8938,7 @@ function isAllowedLocalEnvironmentFile(name) {
8874
8938
  return exactLocalEnvironmentFiles.has(name) || localEnvironmentFilePattern.test(name);
8875
8939
  }
8876
8940
  function isRootFileName(name) {
8877
- return name === path15.basename(name) && !name.includes("/") && !name.includes("\\");
8941
+ return name === path16.basename(name) && !name.includes("/") && !name.includes("\\");
8878
8942
  }
8879
8943
  async function gitOutput(cwd, args) {
8880
8944
  const { stdout } = await execFileAsync5("git", args, { cwd, maxBuffer: 1024 * 1024 });
@@ -8907,7 +8971,7 @@ function safeFileError(error) {
8907
8971
 
8908
8972
  // src/implementation-handoff.ts
8909
8973
  import { execFile as execFile6 } from "node:child_process";
8910
- import path16 from "node:path";
8974
+ import path17 from "node:path";
8911
8975
  import { promisify as promisify6 } from "node:util";
8912
8976
  var execFileAsync6 = promisify6(execFile6);
8913
8977
  async function completeImplementationHandoff(input) {
@@ -9175,7 +9239,7 @@ async function cleanupWorktree(run, input) {
9175
9239
  return { status: "failed", message: "Cleanup skipped because the worktree is not clean after PR handoff." };
9176
9240
  }
9177
9241
  try {
9178
- 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]);
9179
9243
  return { status: "completed" };
9180
9244
  } catch (error) {
9181
9245
  return { status: "failed", message: `Cleanup failed: ${safeErrorMessage(error)}` };
@@ -9896,7 +9960,7 @@ program.command("import").description("Pair an existing checkout and import lega
9896
9960
  });
9897
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) => {
9898
9962
  const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
9899
- let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID2()}`;
9963
+ let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID3()}`;
9900
9964
  let credential = options.token;
9901
9965
  if (options.pairingCode) {
9902
9966
  const pairing = await new ApiClient({
@@ -10082,7 +10146,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
10082
10146
  }
10083
10147
  const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
10084
10148
  if (options.out) {
10085
- await writeFile11(options.out, prompt, "utf8");
10149
+ await writeFile12(options.out, prompt, "utf8");
10086
10150
  console.log(`Wrote work prompt to ${options.out}.`);
10087
10151
  } else {
10088
10152
  console.log(prompt);
@@ -10096,6 +10160,34 @@ program.command("tools").description("List local AI coding tools that the Amisti
10096
10160
  }
10097
10161
  console.log("custom - pass --tool-command to use any other local runner command.");
10098
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
+ });
10099
10191
  var hostHelper = program.command("host-helper").description("Inspect the optional Amistio host helper used for stronger local execution primitives");
10100
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) => {
10101
10193
  const config = options.path?.trim() ? { command: options.path.trim() } : nativeHostHelperConfigFromEnvironment();
@@ -10164,7 +10256,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
10164
10256
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
10165
10257
  ...localModelConfig,
10166
10258
  streamOutput: options.stream,
10167
- ...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) } }
10168
10260
  });
10169
10261
  if (!options.stream && result.stdout.trim()) {
10170
10262
  console.log(result.stdout.trim());
@@ -10187,12 +10279,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
10187
10279
  process.exitCode = 1;
10188
10280
  return;
10189
10281
  }
10190
- const runnerId = options.runnerId ?? stableRunnerId({
10191
- accountId: context.metadata.amistioAccountId,
10192
- projectId: context.metadata.amistioProjectId,
10193
- repositoryLinkId: context.metadata.repositoryLinkId,
10194
- machineId: runnerMachineId()
10195
- });
10282
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10196
10283
  const resolvedOptions = { ...options, runnerId };
10197
10284
  if (resolvedOptions.maxConcurrentWork > MAX_CONCURRENT_RUNNER_WORK) {
10198
10285
  console.log(`--max-concurrent-work is capped at ${MAX_CONCURRENT_RUNNER_WORK}.`);
@@ -10210,7 +10297,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
10210
10297
  projectId: context.metadata.amistioProjectId,
10211
10298
  repositoryLinkId: context.metadata.repositoryLinkId,
10212
10299
  runnerId,
10213
- rootDir: path17.resolve(options.root),
10300
+ rootDir: path18.resolve(options.root),
10214
10301
  apiUrl: options.apiUrl,
10215
10302
  args: buildBackgroundRunnerArgs(resolvedOptions)
10216
10303
  });
@@ -10281,6 +10368,38 @@ program.command("run").description("Claim and run approved Amistio work locally"
10281
10368
  }
10282
10369
  });
10283
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
+ });
10284
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) => {
10285
10404
  const context = await loadPairedApiContext(options.root, options.apiUrl);
10286
10405
  if (!context) {
@@ -10387,12 +10506,7 @@ runnerService.command("install").description("Install a user-level startup servi
10387
10506
  process.exitCode = 1;
10388
10507
  return;
10389
10508
  }
10390
- const runnerId = options.runnerId ?? stableRunnerId({
10391
- accountId: context.metadata.amistioAccountId,
10392
- projectId: context.metadata.amistioProjectId,
10393
- repositoryLinkId: context.metadata.repositoryLinkId,
10394
- machineId: runnerMachineId()
10395
- });
10509
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10396
10510
  if (options.maxConcurrentWork > MAX_CONCURRENT_RUNNER_WORK) {
10397
10511
  console.log(`--max-concurrent-work is capped at ${MAX_CONCURRENT_RUNNER_WORK}.`);
10398
10512
  process.exitCode = 1;
@@ -10404,7 +10518,7 @@ runnerService.command("install").description("Install a user-level startup servi
10404
10518
  projectId: context.metadata.amistioProjectId,
10405
10519
  repositoryLinkId: context.metadata.repositoryLinkId,
10406
10520
  runnerId,
10407
- rootDir: path17.resolve(options.root),
10521
+ rootDir: path18.resolve(options.root),
10408
10522
  apiUrl: options.apiUrl,
10409
10523
  args,
10410
10524
  platform
@@ -10430,12 +10544,7 @@ runnerService.command("status").description("Show the startup service status for
10430
10544
  console.log("Repository is not paired. Run `amistio pair` first.");
10431
10545
  return;
10432
10546
  }
10433
- const runnerId = options.runnerId ?? stableRunnerId({
10434
- accountId: context.metadata.amistioAccountId,
10435
- projectId: context.metadata.amistioProjectId,
10436
- repositoryLinkId: context.metadata.repositoryLinkId,
10437
- machineId: runnerMachineId()
10438
- });
10547
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10439
10548
  const metadata = await readRunnerServiceMetadata({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
10440
10549
  if (!metadata) {
10441
10550
  console.log("No startup service metadata found for this paired repository runner.");
@@ -10454,12 +10563,7 @@ runnerService.command("remove").description("Remove the startup service for this
10454
10563
  console.log("Repository is not paired. Run `amistio pair` first.");
10455
10564
  return;
10456
10565
  }
10457
- const runnerId = options.runnerId ?? stableRunnerId({
10458
- accountId: context.metadata.amistioAccountId,
10459
- projectId: context.metadata.amistioProjectId,
10460
- repositoryLinkId: context.metadata.repositoryLinkId,
10461
- machineId: runnerMachineId()
10462
- });
10566
+ const runnerId = options.runnerId ?? await resolveLocalRunnerId(runnerIdentityScope(context.metadata));
10463
10567
  const removed = await removeRunnerService({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
10464
10568
  console.log(removed ? `Removed startup service ${removed.serviceName}.` : "No startup service metadata found for this paired repository runner.");
10465
10569
  });
@@ -11088,7 +11192,7 @@ async function runNextWorkItem({
11088
11192
  projectId,
11089
11193
  result.workItem.workItemId,
11090
11194
  finalStatus,
11091
- `run_${result.workItem.workItemId}_${randomUUID2()}`,
11195
+ `run_${result.workItem.workItemId}_${randomUUID3()}`,
11092
11196
  runnerId,
11093
11197
  {
11094
11198
  tool: preview.toolName,
@@ -11182,7 +11286,7 @@ async function prepareWorktreeForClaimedItem({ apiClient, heartbeatConcurrency,
11182
11286
  const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
11183
11287
  const finalAttempt = workItem.attempt >= maxPreflightAttempts;
11184
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}`;
11185
- 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, {
11186
11290
  ...telemetry,
11187
11291
  message: statusMessage,
11188
11292
  ...finalAttempt ? { blockerReason: message } : { releaseClaim: true },
@@ -11251,7 +11355,7 @@ async function recordFinalizationFailure({ apiClient, durationMs, error, isolati
11251
11355
  const settlements = await Promise.allSettled([
11252
11356
  apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
11253
11357
  markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage6(error)),
11254
- 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, {
11255
11359
  ...isolationTelemetry,
11256
11360
  tool: toolName,
11257
11361
  durationMs,
@@ -11402,7 +11506,7 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
11402
11506
  runnerId: context.runnerId,
11403
11507
  repositoryLinkId: context.repositoryLinkId,
11404
11508
  status,
11405
- idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
11509
+ idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID3()}`,
11406
11510
  message,
11407
11511
  ...error ? { error } : {},
11408
11512
  ...providerAuthStatus ? { providerAuthStatus } : {}
@@ -11445,6 +11549,9 @@ async function executeImplementationHandoffRecoveryCommand(apiClient, command, c
11445
11549
  if (workItem.repositoryLinkId && workItem.repositoryLinkId !== context.repositoryLinkId) {
11446
11550
  return { succeeded: false, message: "Handoff recovery command is not scoped to this repository link." };
11447
11551
  }
11552
+ if (workItem.claimedByRunnerId && workItem.claimedByRunnerId !== context.runnerId) {
11553
+ return { succeeded: false, message: `Handoff recovery command belongs to runner ${workItem.claimedByRunnerId}. Restart that runner or use Requeue fresh attempt.` };
11554
+ }
11448
11555
  if (!workItem.implementationHandoff?.recovery?.availableActions.includes(command.handoffRecoveryAction)) {
11449
11556
  return { succeeded: false, message: "Handoff recovery action is no longer available for this work item." };
11450
11557
  }
@@ -11491,7 +11598,7 @@ async function findRecoveryWorkItem(apiClient, projectId, workItemId) {
11491
11598
  return workItems.find((item) => item.workItemId === workItemId);
11492
11599
  }
11493
11600
  async function submitRecoveredHandoff(apiClient, context, workItem, handoff, action) {
11494
- await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID2()}`, context.runnerId, {
11601
+ await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID3()}`, context.runnerId, {
11495
11602
  implementationHandoff: handoff,
11496
11603
  ...handoff.message ? { message: handoff.message } : {},
11497
11604
  ...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
@@ -11775,7 +11882,7 @@ ${toolResult.stderr}`);
11775
11882
  const resultMutation = {
11776
11883
  status: "completed",
11777
11884
  runnerId,
11778
- idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`,
11885
+ idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${randomUUID3()}`,
11779
11886
  artifacts,
11780
11887
  tool: toolName,
11781
11888
  durationMs,
@@ -11850,7 +11957,7 @@ ${toolResult.stderr}`);
11850
11957
  const failedResult2 = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
11851
11958
  status: "failed",
11852
11959
  runnerId,
11853
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
11960
+ idempotencyKey: `generation_${workItem.workItemId}_${randomUUID3()}`,
11854
11961
  tool: toolName,
11855
11962
  durationMs,
11856
11963
  ...failedSessionTelemetry,
@@ -11900,7 +12007,7 @@ ${toolResult.stderr}`);
11900
12007
  const resultMutation = {
11901
12008
  status: "completed",
11902
12009
  runnerId,
11903
- idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID2()}`,
12010
+ idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID3()}`,
11904
12011
  answer: answerResult.answer,
11905
12012
  sourceBoundary: answerResult.sourceBoundary,
11906
12013
  citations: answerResult.citations,
@@ -11936,7 +12043,7 @@ ${toolResult.stderr}`);
11936
12043
  const failedMutation = {
11937
12044
  status: "failed",
11938
12045
  runnerId,
11939
- idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID2()}`,
12046
+ idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID3()}`,
11940
12047
  tool: toolName,
11941
12048
  durationMs,
11942
12049
  ...sessionTelemetry,
@@ -11999,7 +12106,7 @@ ${toolResult.stderr}`);
11999
12106
  const resultMutation = {
12000
12107
  status: "completed",
12001
12108
  runnerId,
12002
- idempotencyKey: `impact_${workItem.workItemId}_${randomUUID2()}`,
12109
+ idempotencyKey: `impact_${workItem.workItemId}_${randomUUID3()}`,
12003
12110
  report: {
12004
12111
  ...report,
12005
12112
  analyzedRepoRevision: report.analyzedRepoRevision ?? metadata?.lastSyncedRevision
@@ -12036,7 +12143,7 @@ ${toolResult.stderr}`);
12036
12143
  const failedMutation = {
12037
12144
  status: "failed",
12038
12145
  runnerId,
12039
- idempotencyKey: `impact_${workItem.workItemId}_${randomUUID2()}`,
12146
+ idempotencyKey: `impact_${workItem.workItemId}_${randomUUID3()}`,
12040
12147
  tool: toolName,
12041
12148
  durationMs,
12042
12149
  ...sessionTelemetry,
@@ -12097,7 +12204,7 @@ ${toolResult.stderr}`);
12097
12204
  const resultMutation = {
12098
12205
  status: "completed",
12099
12206
  runnerId,
12100
- idempotencyKey: `issue_${workItem.workItemId}_${randomUUID2()}`,
12207
+ idempotencyKey: `issue_${workItem.workItemId}_${randomUUID3()}`,
12101
12208
  diagnosis,
12102
12209
  tool: toolName,
12103
12210
  durationMs,
@@ -12131,7 +12238,7 @@ ${toolResult.stderr}`);
12131
12238
  const failedMutation = {
12132
12239
  status: "failed",
12133
12240
  runnerId,
12134
- idempotencyKey: `issue_${workItem.workItemId}_${randomUUID2()}`,
12241
+ idempotencyKey: `issue_${workItem.workItemId}_${randomUUID3()}`,
12135
12242
  tool: toolName,
12136
12243
  durationMs,
12137
12244
  ...sessionTelemetry,
@@ -12192,7 +12299,7 @@ ${toolResult.stderr}`);
12192
12299
  const resultMutation = {
12193
12300
  status: "completed",
12194
12301
  runnerId,
12195
- idempotencyKey: `security_${workItem.workItemId}_${randomUUID2()}`,
12302
+ idempotencyKey: `security_${workItem.workItemId}_${randomUUID3()}`,
12196
12303
  result: scanResult,
12197
12304
  tool: toolName,
12198
12305
  durationMs,
@@ -12226,7 +12333,7 @@ ${toolResult.stderr}`);
12226
12333
  const failedMutation = {
12227
12334
  status: "failed",
12228
12335
  runnerId,
12229
- idempotencyKey: `security_${workItem.workItemId}_${randomUUID2()}`,
12336
+ idempotencyKey: `security_${workItem.workItemId}_${randomUUID3()}`,
12230
12337
  tool: toolName,
12231
12338
  durationMs,
12232
12339
  ...sessionTelemetry,
@@ -12287,7 +12394,7 @@ ${toolResult.stderr}`);
12287
12394
  const resultMutation = {
12288
12395
  status: "completed",
12289
12396
  runnerId,
12290
- idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID2()}`,
12397
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID3()}`,
12291
12398
  result: scanResult,
12292
12399
  tool: toolName,
12293
12400
  durationMs,
@@ -12321,7 +12428,7 @@ ${toolResult.stderr}`);
12321
12428
  const failedMutation = {
12322
12429
  status: "failed",
12323
12430
  runnerId,
12324
- idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID2()}`,
12431
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID3()}`,
12325
12432
  tool: toolName,
12326
12433
  durationMs,
12327
12434
  ...sessionTelemetry,
@@ -12382,7 +12489,7 @@ ${toolResult.stderr}`);
12382
12489
  const resultMutation = {
12383
12490
  status: "completed",
12384
12491
  runnerId,
12385
- idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID2()}`,
12492
+ idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID3()}`,
12386
12493
  result: scanResult,
12387
12494
  tool: toolName,
12388
12495
  durationMs,
@@ -12416,7 +12523,7 @@ ${toolResult.stderr}`);
12416
12523
  const failedMutation = {
12417
12524
  status: "failed",
12418
12525
  runnerId,
12419
- idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID2()}`,
12526
+ idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID3()}`,
12420
12527
  tool: toolName,
12421
12528
  durationMs,
12422
12529
  ...sessionTelemetry,
@@ -12478,7 +12585,7 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
12478
12585
  const resultMutation = {
12479
12586
  status: "completed",
12480
12587
  runnerId,
12481
- idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID2()}`,
12588
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID3()}`,
12482
12589
  result: refreshResult,
12483
12590
  tool: toolName,
12484
12591
  durationMs,
@@ -12524,7 +12631,7 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
12524
12631
  const failedMutation = {
12525
12632
  status: "failed",
12526
12633
  runnerId,
12527
- idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID2()}`,
12634
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID3()}`,
12528
12635
  tool: toolName,
12529
12636
  durationMs,
12530
12637
  ...sessionTelemetry,
@@ -12585,7 +12692,7 @@ ${toolResult.stderr}`);
12585
12692
  const resultMutation = {
12586
12693
  status: "completed",
12587
12694
  runnerId,
12588
- idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID2()}`,
12695
+ idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID3()}`,
12589
12696
  result: verificationResult,
12590
12697
  tool: toolName,
12591
12698
  durationMs,
@@ -12619,7 +12726,7 @@ ${toolResult.stderr}`);
12619
12726
  const failedMutation = {
12620
12727
  status: "failed",
12621
12728
  runnerId,
12622
- idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID2()}`,
12729
+ idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID3()}`,
12623
12730
  tool: toolName,
12624
12731
  durationMs,
12625
12732
  ...sessionTelemetry,
@@ -12680,7 +12787,7 @@ ${toolResult.stderr}`);
12680
12787
  const resultMutation = {
12681
12788
  status: "completed",
12682
12789
  runnerId,
12683
- idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID2()}`,
12790
+ idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID3()}`,
12684
12791
  result: scanResult,
12685
12792
  tool: toolName,
12686
12793
  durationMs,
@@ -12714,7 +12821,7 @@ ${toolResult.stderr}`);
12714
12821
  const failedMutation = {
12715
12822
  status: "failed",
12716
12823
  runnerId,
12717
- idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID2()}`,
12824
+ idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID3()}`,
12718
12825
  tool: toolName,
12719
12826
  durationMs,
12720
12827
  ...sessionTelemetry,
@@ -12775,7 +12882,7 @@ ${toolResult.stderr}`);
12775
12882
  const resultMutation = {
12776
12883
  status: "completed",
12777
12884
  runnerId,
12778
- idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID2()}`,
12885
+ idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID3()}`,
12779
12886
  result: gateResult,
12780
12887
  tool: toolName,
12781
12888
  durationMs,
@@ -12809,7 +12916,7 @@ ${toolResult.stderr}`);
12809
12916
  const failedMutation = {
12810
12917
  status: "failed",
12811
12918
  runnerId,
12812
- idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID2()}`,
12919
+ idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID3()}`,
12813
12920
  tool: toolName,
12814
12921
  durationMs,
12815
12922
  ...sessionTelemetry,
@@ -12954,6 +13061,14 @@ async function loadPairedApiContext(root, apiUrl) {
12954
13061
  })
12955
13062
  };
12956
13063
  }
13064
+ function runnerIdentityScope(metadata) {
13065
+ return {
13066
+ accountId: metadata.amistioAccountId,
13067
+ projectId: metadata.amistioProjectId,
13068
+ repositoryLinkId: metadata.repositoryLinkId,
13069
+ machineId: runnerMachineId()
13070
+ };
13071
+ }
12957
13072
  async function loadProjectNextAction(apiClient, projectId, repositoryLinkId, root) {
12958
13073
  const [{ workItems }, { documents }, { runners }] = await Promise.all([
12959
13074
  apiClient.listWorkItems(projectId),
@@ -13038,7 +13153,7 @@ async function prepareToolSession({
13038
13153
  });
13039
13154
  return { ...selection, toolSession: toolSession2 };
13040
13155
  }
13041
- const toolSessionId = `tool_session_${randomUUID2()}`;
13156
+ const toolSessionId = `tool_session_${randomUUID3()}`;
13042
13157
  const { toolSession } = await apiClient.createToolSession(projectId, {
13043
13158
  toolSessionId,
13044
13159
  repositoryLinkId,
@@ -13145,6 +13260,23 @@ function truncateLogExcerpt(value) {
13145
13260
  function formatHostHelperCapability(label, support) {
13146
13261
  return `${label}: ${support.supported ? "supported" : "unsupported"}${support.reason ? ` (${support.reason})` : ""}`;
13147
13262
  }
13263
+ function harnessProviderCheckTargets() {
13264
+ return [
13265
+ { label: "GitHub Models Direct", request: { providerId: "github-models", providerClientId: "github-models-api", routeType: "directProvider" } },
13266
+ { label: "GitHub Copilot SDK", request: { providerId: "github-copilot", providerClientId: "github-copilot-sdk", routeType: "agentClient" } }
13267
+ ];
13268
+ }
13269
+ function formatProviderAuthStatusLines(status) {
13270
+ const lines = [
13271
+ `Target: ${status.providerId}:${status.providerClientId}:${status.routeType}`,
13272
+ `Checked: ${status.checkedAt}`
13273
+ ];
13274
+ if (status.authMethodLabel) lines.push(`Auth: ${status.authMethodLabel}`);
13275
+ if (status.modelCount !== void 0) lines.push(`Models: ${status.modelCount}`);
13276
+ if (status.message) lines.push(`Message: ${status.message}`);
13277
+ if (status.errorCode) lines.push(`Code: ${status.errorCode}`);
13278
+ return lines;
13279
+ }
13148
13280
  function collectRepeatedOption(value, previous) {
13149
13281
  return [...previous, value];
13150
13282
  }
@@ -13171,7 +13303,7 @@ function parseReasoningEffort(value) {
13171
13303
  throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
13172
13304
  }
13173
13305
  function inferRepoName(root) {
13174
- return path17.basename(path17.resolve(root)) || "repository";
13306
+ return path18.basename(path18.resolve(root)) || "repository";
13175
13307
  }
13176
13308
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
13177
13309
  return createHash9("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
@@ -13448,7 +13580,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode(), concurr
13448
13580
  return {
13449
13581
  version: CLI_VERSION,
13450
13582
  mode,
13451
- hostname: os8.hostname(),
13583
+ hostname: os9.hostname(),
13452
13584
  ...runnerIsolationCapabilityMetadata(),
13453
13585
  maxConcurrentWork: concurrencyMetadata.maxConcurrentWork,
13454
13586
  activeClaimLaneIds: concurrencyMetadata.activeClaimLaneIds,
@@ -13473,7 +13605,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode(), concurr
13473
13605
  };
13474
13606
  }
13475
13607
  function runnerMachineId() {
13476
- return createHash9("sha256").update(`${os8.hostname()}:${os8.platform()}:${os8.arch()}`).digest("hex").slice(0, 20);
13608
+ return createHash9("sha256").update(`${os9.hostname()}:${os9.platform()}:${os9.arch()}`).digest("hex").slice(0, 20);
13477
13609
  }
13478
13610
  async function delay(milliseconds) {
13479
13611
  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.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",