@amistio/cli 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createHash as createHash7, randomUUID } from "node:crypto";
5
- import { writeFile as writeFile9 } from "node:fs/promises";
6
- import os7 from "node:os";
7
- import path15 from "node:path";
4
+ import { createHash as createHash8, randomUUID } from "node:crypto";
5
+ import { writeFile as writeFile10 } from "node:fs/promises";
6
+ import os8 from "node:os";
7
+ import path16 from "node:path";
8
8
  import { Command } from "commander";
9
9
 
10
10
  // ../shared/src/schemas.ts
@@ -2178,6 +2178,24 @@ async function readGitTopLevel(rootDir) {
2178
2178
 
2179
2179
  // src/api-client.ts
2180
2180
  import { z as z3 } from "zod";
2181
+ var AmistioApiError = class extends Error {
2182
+ constructor(status, statusText, detail) {
2183
+ super(`Amistio API request failed: ${status} ${statusText}${detail ? ` - ${detail}` : ""}`);
2184
+ this.status = status;
2185
+ this.statusText = statusText;
2186
+ this.detail = detail;
2187
+ this.name = "AmistioApiError";
2188
+ }
2189
+ status;
2190
+ statusText;
2191
+ detail;
2192
+ };
2193
+ function isRetryableApiError(error) {
2194
+ if (error instanceof AmistioApiError) {
2195
+ return error.status === 408 || error.status === 429 || error.status >= 500;
2196
+ }
2197
+ return error instanceof TypeError;
2198
+ }
2181
2199
  var ApiClient = class {
2182
2200
  constructor(options) {
2183
2201
  this.options = options;
@@ -2519,7 +2537,7 @@ var ApiClient = class {
2519
2537
  });
2520
2538
  if (!response.ok) {
2521
2539
  const detail = await response.text().catch(() => "");
2522
- throw new Error(`Amistio API request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ""}`);
2540
+ throw new AmistioApiError(response.status, response.statusText, detail);
2523
2541
  }
2524
2542
  return schema.parse(await response.json());
2525
2543
  }
@@ -2554,8 +2572,8 @@ var toolSessionMutationSchema = z3.object({
2554
2572
  });
2555
2573
  function resolveApiUrl(apiUrl, urlPath) {
2556
2574
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
2557
- const path16 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
2558
- return new URL(`${base}${path16}`);
2575
+ const path17 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
2576
+ return new URL(`${base}${path17}`);
2559
2577
  }
2560
2578
 
2561
2579
  // src/orchestrator.ts
@@ -2619,11 +2637,155 @@ async function writePromptFile(filePath, prompt) {
2619
2637
  return filePath;
2620
2638
  }
2621
2639
 
2622
- // src/local-tool-runner.ts
2623
- import { spawn } from "node:child_process";
2624
- import { mkdtemp, readFile as readFile3, rm, writeFile as writeFile4 } from "node:fs/promises";
2640
+ // src/result-finalization-outbox.ts
2641
+ import { createHash as createHash2 } from "node:crypto";
2642
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile3, rename, rm, writeFile as writeFile4 } from "node:fs/promises";
2625
2643
  import os2 from "node:os";
2626
2644
  import path5 from "node:path";
2645
+ import { z as z4 } from "zod";
2646
+ var brainGenerationResultMutationSchema = z4.discriminatedUnion("status", [
2647
+ z4.object({
2648
+ status: z4.literal("completed"),
2649
+ runnerId: z4.string().min(1),
2650
+ idempotencyKey: z4.string().min(1),
2651
+ artifacts: z4.array(generatedBrainArtifactSchema).min(1),
2652
+ tool: z4.string().min(1).optional(),
2653
+ durationMs: z4.number().int().nonnegative().optional(),
2654
+ sessionPolicy: sessionPolicySchema.optional(),
2655
+ sessionGroupKey: z4.string().min(1).optional(),
2656
+ toolSessionId: z4.string().min(1).optional(),
2657
+ sessionDecision: sessionDecisionSchema.optional(),
2658
+ sessionDecisionReason: z4.string().min(1).optional(),
2659
+ message: z4.string().optional()
2660
+ }),
2661
+ z4.object({
2662
+ status: z4.literal("failed"),
2663
+ runnerId: z4.string().min(1),
2664
+ idempotencyKey: z4.string().min(1),
2665
+ tool: z4.string().min(1).optional(),
2666
+ durationMs: z4.number().int().nonnegative().optional(),
2667
+ sessionPolicy: sessionPolicySchema.optional(),
2668
+ sessionGroupKey: z4.string().min(1).optional(),
2669
+ toolSessionId: z4.string().min(1).optional(),
2670
+ sessionDecision: sessionDecisionSchema.optional(),
2671
+ sessionDecisionReason: z4.string().min(1).optional(),
2672
+ message: z4.string().optional(),
2673
+ error: z4.string().optional()
2674
+ })
2675
+ ]);
2676
+ var brainGenerationFinalizationEntrySchema = z4.object({
2677
+ schemaVersion: z4.literal(1),
2678
+ kind: z4.literal("brainGenerationResult"),
2679
+ status: z4.enum(["pending", "terminal"]),
2680
+ accountId: z4.string().min(1),
2681
+ projectId: z4.string().min(1),
2682
+ repositoryLinkId: z4.string().min(1),
2683
+ runnerId: z4.string().min(1),
2684
+ workItemId: z4.string().min(1),
2685
+ workKind: z4.enum(["brainGeneration", "planRevision"]),
2686
+ attempt: z4.number().int().nonnegative(),
2687
+ idempotencyKey: z4.string().min(1),
2688
+ result: brainGenerationResultMutationSchema,
2689
+ retryCount: z4.number().int().nonnegative(),
2690
+ createdAt: z4.string().min(1),
2691
+ updatedAt: z4.string().min(1),
2692
+ lastError: z4.string().optional()
2693
+ });
2694
+ function defaultResultFinalizationOutboxDir() {
2695
+ return path5.join(os2.homedir(), ".config", "amistio", "result-finalizations");
2696
+ }
2697
+ function createBrainGenerationFinalizationEntry(input, now = (/* @__PURE__ */ new Date()).toISOString()) {
2698
+ return brainGenerationFinalizationEntrySchema.parse({
2699
+ schemaVersion: 1,
2700
+ kind: "brainGenerationResult",
2701
+ status: "pending",
2702
+ accountId: input.accountId,
2703
+ projectId: input.projectId,
2704
+ repositoryLinkId: input.repositoryLinkId,
2705
+ runnerId: input.runnerId,
2706
+ workItemId: input.workItemId,
2707
+ workKind: input.workKind,
2708
+ attempt: input.attempt,
2709
+ idempotencyKey: input.idempotencyKey,
2710
+ result: input.result,
2711
+ retryCount: 0,
2712
+ createdAt: now,
2713
+ updatedAt: now
2714
+ });
2715
+ }
2716
+ async function upsertBrainGenerationFinalizationEntry(entry, outboxDir = defaultResultFinalizationOutboxDir()) {
2717
+ await mkdir5(outboxDir, { recursive: true });
2718
+ const filePath = brainGenerationFinalizationEntryPath(entry, outboxDir);
2719
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
2720
+ await writeFile4(tempPath, `${JSON.stringify(entry, null, 2)}
2721
+ `, "utf8");
2722
+ await rename(tempPath, filePath);
2723
+ }
2724
+ async function listPendingBrainGenerationFinalizations(scope, outboxDir = defaultResultFinalizationOutboxDir()) {
2725
+ const entries = await readdir2(outboxDir, { withFileTypes: true }).catch((error) => {
2726
+ if (error.code === "ENOENT") return [];
2727
+ throw error;
2728
+ });
2729
+ const pending = [];
2730
+ for (const entry of entries) {
2731
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
2732
+ const filePath = path5.join(outboxDir, entry.name);
2733
+ let raw;
2734
+ try {
2735
+ raw = JSON.parse(await readFile3(filePath, "utf8"));
2736
+ } catch {
2737
+ continue;
2738
+ }
2739
+ const parsed = brainGenerationFinalizationEntrySchema.safeParse(raw);
2740
+ if (!parsed.success) continue;
2741
+ const value = parsed.data;
2742
+ if (value.status !== "pending") continue;
2743
+ if (value.accountId !== scope.accountId || value.projectId !== scope.projectId || value.repositoryLinkId !== scope.repositoryLinkId || value.runnerId !== scope.runnerId) continue;
2744
+ pending.push(value);
2745
+ }
2746
+ return pending.sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt));
2747
+ }
2748
+ async function markBrainGenerationFinalizationRetry(entry, error, outboxDir = defaultResultFinalizationOutboxDir()) {
2749
+ const updated = brainGenerationFinalizationEntrySchema.parse({
2750
+ ...entry,
2751
+ status: "pending",
2752
+ retryCount: entry.retryCount + 1,
2753
+ lastError: truncateLocalError(error),
2754
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2755
+ });
2756
+ await upsertBrainGenerationFinalizationEntry(updated, outboxDir);
2757
+ return updated;
2758
+ }
2759
+ async function markBrainGenerationFinalizationTerminal(entry, error, outboxDir = defaultResultFinalizationOutboxDir()) {
2760
+ const updated = brainGenerationFinalizationEntrySchema.parse({
2761
+ ...entry,
2762
+ status: "terminal",
2763
+ lastError: truncateLocalError(error),
2764
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2765
+ });
2766
+ await upsertBrainGenerationFinalizationEntry(updated, outboxDir);
2767
+ return updated;
2768
+ }
2769
+ async function deleteBrainGenerationFinalizationEntry(entry, outboxDir = defaultResultFinalizationOutboxDir()) {
2770
+ await rm(brainGenerationFinalizationEntryPath(entry, outboxDir), { force: true });
2771
+ }
2772
+ function brainGenerationFinalizationEntryPath(entry, outboxDir) {
2773
+ return path5.join(outboxDir, `${brainGenerationFinalizationEntryKey(entry)}.json`);
2774
+ }
2775
+ function brainGenerationFinalizationEntryKey(entry) {
2776
+ const hash = createHash2("sha256").update([entry.accountId, entry.projectId, entry.repositoryLinkId, entry.runnerId, entry.workItemId, String(entry.attempt), entry.idempotencyKey].join("\0")).digest("hex").slice(0, 32);
2777
+ return `brain-generation-${hash}`;
2778
+ }
2779
+ function truncateLocalError(error) {
2780
+ const trimmed = error.trim();
2781
+ return trimmed.length > 600 ? `${trimmed.slice(0, 600)}...` : trimmed;
2782
+ }
2783
+
2784
+ // src/local-tool-runner.ts
2785
+ import { spawn } from "node:child_process";
2786
+ import { mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
2787
+ import os3 from "node:os";
2788
+ import path6 from "node:path";
2627
2789
  var localToolNames = runnerToolNames;
2628
2790
  var allReasoningEfforts = ["auto", "low", "medium", "high", "xhigh"];
2629
2791
  var highReasoningEfforts = ["auto", "high", "xhigh"];
@@ -2830,9 +2992,9 @@ async function detectLocalTools() {
2830
2992
  );
2831
2993
  }
2832
2994
  async function runLocalTool(options) {
2833
- const promptTempDir = await mkdtemp(path5.join(os2.tmpdir(), "amistio-prompt-"));
2834
- const promptFilePath = path5.join(promptTempDir, "prompt.md");
2835
- await writeFile4(promptFilePath, options.prompt, "utf8");
2995
+ const promptTempDir = await mkdtemp(path6.join(os3.tmpdir(), "amistio-prompt-"));
2996
+ const promptFilePath = path6.join(promptTempDir, "prompt.md");
2997
+ await writeFile5(promptFilePath, options.prompt, "utf8");
2836
2998
  const modelConfig = normalizeModelOptions(options);
2837
2999
  try {
2838
3000
  const runnerOptions = {
@@ -2864,11 +3026,11 @@ async function runLocalTool(options) {
2864
3026
  ...result
2865
3027
  };
2866
3028
  } finally {
2867
- await rm(promptTempDir, { recursive: true, force: true });
3029
+ await rm2(promptTempDir, { recursive: true, force: true });
2868
3030
  }
2869
3031
  }
2870
3032
  async function createToolRunPreview(options) {
2871
- const promptFilePath = path5.join(os2.tmpdir(), "amistio-generated-prompt.md");
3033
+ const promptFilePath = path6.join(os3.tmpdir(), "amistio-generated-prompt.md");
2872
3034
  const modelConfig = normalizeModelOptions(options);
2873
3035
  const runnerOptions = {
2874
3036
  rootDir: options.rootDir,
@@ -3061,13 +3223,13 @@ async function detectProviderCatalog(adapter) {
3061
3223
  }
3062
3224
  async function loadOpencodeProviderCatalog() {
3063
3225
  const configPaths = [
3064
- path5.join(os2.homedir(), ".config", "opencode", "opencode.json"),
3065
- path5.join(os2.homedir(), ".config", "opencode", "config.json"),
3066
- path5.join(process.cwd(), "opencode.json")
3226
+ path6.join(os3.homedir(), ".config", "opencode", "opencode.json"),
3227
+ path6.join(os3.homedir(), ".config", "opencode", "config.json"),
3228
+ path6.join(process.cwd(), "opencode.json")
3067
3229
  ];
3068
3230
  for (const configPath of configPaths) {
3069
3231
  try {
3070
- const parsed = JSON.parse(await readFile3(configPath, "utf8"));
3232
+ const parsed = JSON.parse(await readFile4(configPath, "utf8"));
3071
3233
  const providerValue = isRecord(parsed) ? parsed.provider : void 0;
3072
3234
  const catalog = sanitizeProviderCatalog(providerValue);
3073
3235
  if (catalog) return catalog;
@@ -3439,16 +3601,16 @@ function shellQuote(value) {
3439
3601
 
3440
3602
  // src/runner-daemon.ts
3441
3603
  import { spawn as spawn2 } from "node:child_process";
3442
- import { createHash as createHash2 } from "node:crypto";
3604
+ import { createHash as createHash3 } from "node:crypto";
3443
3605
  import { openSync } from "node:fs";
3444
- import { mkdir as mkdir5, readdir as readdir2, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
3445
- import os3 from "node:os";
3446
- import path6 from "node:path";
3606
+ import { mkdir as mkdir6, readdir as readdir3, readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
3607
+ import os4 from "node:os";
3608
+ import path7 from "node:path";
3447
3609
  function currentRunnerMode() {
3448
3610
  return process.env.AMISTIO_RUNNER_MODE === "background" ? "background" : "foreground";
3449
3611
  }
3450
3612
  function defaultRunnerMetadataDir() {
3451
- return path6.join(os3.homedir(), ".config", "amistio", "runners");
3613
+ return path7.join(os4.homedir(), ".config", "amistio", "runners");
3452
3614
  }
3453
3615
  function updatedCliRunnerLaunchOptions() {
3454
3616
  return { executablePath: "amistio", directExecutable: true };
@@ -3465,8 +3627,8 @@ async function startRunnerDaemon(input) {
3465
3627
  if (existing?.status === "running" && isProcessRunning(existing.pid)) {
3466
3628
  throw new Error(`Background runner ${existing.runnerId} is already running with PID ${existing.pid}.`);
3467
3629
  }
3468
- await mkdir5(metadataDir, { recursive: true });
3469
- const logPath = path6.join(metadataDir, `${runnerDaemonKey(input)}.log`);
3630
+ await mkdir6(metadataDir, { recursive: true });
3631
+ const logPath = path7.join(metadataDir, `${runnerDaemonKey(input)}.log`);
3470
3632
  const logFd = openSync(logPath, "a");
3471
3633
  const launch = resolveRunnerDaemonLaunch({
3472
3634
  args: input.args,
@@ -3493,13 +3655,13 @@ async function startRunnerDaemon(input) {
3493
3655
  projectId: input.projectId,
3494
3656
  repositoryLinkId: input.repositoryLinkId,
3495
3657
  runnerId: input.runnerId,
3496
- rootDir: path6.resolve(input.rootDir),
3658
+ rootDir: path7.resolve(input.rootDir),
3497
3659
  apiUrl: input.apiUrl,
3498
3660
  pid: child.pid,
3499
3661
  status: "running",
3500
3662
  startedAt: now,
3501
3663
  updatedAt: now,
3502
- hostname: os3.hostname(),
3664
+ hostname: os4.hostname(),
3503
3665
  logPath
3504
3666
  };
3505
3667
  await writeRunnerDaemonMetadata(metadata, metadataDir);
@@ -3507,8 +3669,8 @@ async function startRunnerDaemon(input) {
3507
3669
  }
3508
3670
  async function restartRunnerDaemonProcess(metadata, args, input = {}) {
3509
3671
  const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
3510
- await mkdir5(metadataDir, { recursive: true });
3511
- const logPath = metadata.logPath ?? path6.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
3672
+ await mkdir6(metadataDir, { recursive: true });
3673
+ const logPath = metadata.logPath ?? path7.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
3512
3674
  const logFd = openSync(logPath, "a");
3513
3675
  const launch = resolveRunnerDaemonLaunch({
3514
3676
  args,
@@ -3536,7 +3698,7 @@ async function restartRunnerDaemonProcess(metadata, args, input = {}) {
3536
3698
  status: "running",
3537
3699
  startedAt: now,
3538
3700
  updatedAt: now,
3539
- hostname: os3.hostname(),
3701
+ hostname: os4.hostname(),
3540
3702
  logPath
3541
3703
  };
3542
3704
  await writeRunnerDaemonMetadata(replacement, metadataDir);
@@ -3545,12 +3707,12 @@ async function restartRunnerDaemonProcess(metadata, args, input = {}) {
3545
3707
  async function listRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
3546
3708
  let entries;
3547
3709
  try {
3548
- entries = await readdir2(metadataDir);
3710
+ entries = await readdir3(metadataDir);
3549
3711
  } catch {
3550
3712
  return [];
3551
3713
  }
3552
3714
  const records = await Promise.all(
3553
- entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => readRunnerDaemonMetadataFile(path6.join(metadataDir, entry)))
3715
+ entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => readRunnerDaemonMetadataFile(path7.join(metadataDir, entry)))
3554
3716
  );
3555
3717
  return records.filter((record) => Boolean(record)).filter((record) => record.accountId === input.accountId && record.projectId === input.projectId && record.repositoryLinkId === input.repositoryLinkId).filter((record) => !input.runnerId || record.runnerId === input.runnerId).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
3556
3718
  }
@@ -3558,8 +3720,8 @@ async function readRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetada
3558
3720
  return readRunnerDaemonMetadataFile(runnerDaemonMetadataPath(input, metadataDir));
3559
3721
  }
3560
3722
  async function writeRunnerDaemonMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
3561
- await mkdir5(metadataDir, { recursive: true });
3562
- await writeFile5(runnerDaemonMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
3723
+ await mkdir6(metadataDir, { recursive: true });
3724
+ await writeFile6(runnerDaemonMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
3563
3725
  }
3564
3726
  async function markRunnerDaemonStopped(metadata, metadataDir = defaultRunnerMetadataDir()) {
3565
3727
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -3621,14 +3783,14 @@ function runnerDaemonUptime(metadata, now = Date.now()) {
3621
3783
  return `${seconds}s`;
3622
3784
  }
3623
3785
  function runnerDaemonMetadataPath(input, metadataDir) {
3624
- return path6.join(metadataDir, `${runnerDaemonKey(input)}.json`);
3786
+ return path7.join(metadataDir, `${runnerDaemonKey(input)}.json`);
3625
3787
  }
3626
3788
  function runnerDaemonKey(input) {
3627
- return createHash2("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
3789
+ return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
3628
3790
  }
3629
3791
  async function readRunnerDaemonMetadataFile(filePath) {
3630
3792
  try {
3631
- const parsed = JSON.parse(await readFile4(filePath, "utf8"));
3793
+ const parsed = JSON.parse(await readFile5(filePath, "utf8"));
3632
3794
  if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
3633
3795
  return void 0;
3634
3796
  }
@@ -3640,10 +3802,10 @@ async function readRunnerDaemonMetadataFile(filePath) {
3640
3802
 
3641
3803
  // src/runner-service.ts
3642
3804
  import { spawn as spawn3 } from "node:child_process";
3643
- import { createHash as createHash3 } from "node:crypto";
3644
- import { mkdir as mkdir6, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
3645
- import os4 from "node:os";
3646
- import path7 from "node:path";
3805
+ import { createHash as createHash4 } from "node:crypto";
3806
+ import { mkdir as mkdir7, readFile as readFile6, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
3807
+ import os5 from "node:os";
3808
+ import path8 from "node:path";
3647
3809
  function detectRunnerServicePlatform(platform = process.platform) {
3648
3810
  if (platform === "darwin") return "launchd";
3649
3811
  if (platform === "linux") return "systemd";
@@ -3654,19 +3816,19 @@ function createRunnerServiceDescriptor(input) {
3654
3816
  if (platform === "unsupported") {
3655
3817
  throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
3656
3818
  }
3657
- const homeDir = input.homeDir ?? os4.homedir();
3819
+ const homeDir = input.homeDir ?? os5.homedir();
3658
3820
  const serviceName = runnerServiceName(input);
3659
3821
  const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
3660
3822
  const now = (/* @__PURE__ */ new Date()).toISOString();
3661
3823
  const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
3662
- const logPath = path7.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
3824
+ const logPath = path8.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
3663
3825
  const metadata = {
3664
3826
  schemaVersion: 1,
3665
3827
  accountId: input.accountId,
3666
3828
  projectId: input.projectId,
3667
3829
  repositoryLinkId: input.repositoryLinkId,
3668
3830
  runnerId: input.runnerId,
3669
- rootDir: path7.resolve(input.rootDir),
3831
+ rootDir: path8.resolve(input.rootDir),
3670
3832
  apiUrl: input.apiUrl,
3671
3833
  serviceName,
3672
3834
  serviceFilePath,
@@ -3683,9 +3845,9 @@ function createRunnerServiceDescriptor(input) {
3683
3845
  }
3684
3846
  async function installRunnerService(input, options = {}) {
3685
3847
  const descriptor = createRunnerServiceDescriptor(input);
3686
- await mkdir6(path7.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
3687
- await mkdir6(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
3688
- await writeFile6(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
3848
+ await mkdir7(path8.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
3849
+ await mkdir7(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
3850
+ await writeFile7(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
3689
3851
  await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
3690
3852
  if (options.activate !== false) {
3691
3853
  const activation = await activateRunnerService(descriptor.metadata);
@@ -3701,13 +3863,13 @@ async function removeRunnerService(input) {
3701
3863
  return void 0;
3702
3864
  }
3703
3865
  await deactivateRunnerService(metadata).catch(() => void 0);
3704
- await rm2(metadata.serviceFilePath, { force: true });
3705
- await rm2(runnerServiceMetadataPath(input, input.metadataDir ?? defaultRunnerMetadataDir()), { force: true });
3866
+ await rm3(metadata.serviceFilePath, { force: true });
3867
+ await rm3(runnerServiceMetadataPath(input, input.metadataDir ?? defaultRunnerMetadataDir()), { force: true });
3706
3868
  return { ...metadata, status: "removed", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
3707
3869
  }
3708
3870
  async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
3709
3871
  try {
3710
- const parsed = JSON.parse(await readFile5(runnerServiceMetadataPath(input, metadataDir), "utf8"));
3872
+ const parsed = JSON.parse(await readFile6(runnerServiceMetadataPath(input, metadataDir), "utf8"));
3711
3873
  if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
3712
3874
  return void 0;
3713
3875
  }
@@ -3717,8 +3879,8 @@ async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetad
3717
3879
  }
3718
3880
  }
3719
3881
  async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
3720
- await mkdir6(metadataDir, { recursive: true });
3721
- await writeFile6(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
3882
+ await mkdir7(metadataDir, { recursive: true });
3883
+ await writeFile7(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
3722
3884
  }
3723
3885
  async function runnerServiceRuntimeStatus(metadata) {
3724
3886
  if (metadata.platform === "launchd") {
@@ -3801,18 +3963,18 @@ WantedBy=default.target
3801
3963
  }
3802
3964
  function runnerServiceFilePath(platform, serviceName, homeDir) {
3803
3965
  if (platform === "launchd") {
3804
- return path7.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
3966
+ return path8.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
3805
3967
  }
3806
- return path7.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
3968
+ return path8.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
3807
3969
  }
3808
3970
  function runnerServiceMetadataPath(input, metadataDir) {
3809
- return path7.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
3971
+ return path8.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
3810
3972
  }
3811
3973
  function runnerServiceName(input) {
3812
3974
  return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
3813
3975
  }
3814
3976
  function runnerServiceKey(input) {
3815
- return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
3977
+ return createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
3816
3978
  }
3817
3979
  function launchdDomain() {
3818
3980
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
@@ -3988,9 +4150,9 @@ function tokens(value) {
3988
4150
 
3989
4151
  // src/sync.ts
3990
4152
  import { execFile as execFile3 } from "node:child_process";
3991
- import { createHash as createHash4 } from "node:crypto";
3992
- import { mkdir as mkdir7, readdir as readdir3, readFile as readFile6, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
3993
- import path8 from "node:path";
4153
+ import { createHash as createHash5 } from "node:crypto";
4154
+ import { mkdir as mkdir8, readdir as readdir4, readFile as readFile7, stat as stat3, writeFile as writeFile8 } from "node:fs/promises";
4155
+ import path9 from "node:path";
3994
4156
  import { promisify as promisify3 } from "node:util";
3995
4157
  var execFileAsync3 = promisify3(execFile3);
3996
4158
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
@@ -4086,7 +4248,7 @@ async function readLocalSyncedDocuments(rootDir) {
4086
4248
  const documentFiles = await findBrainDocumentFiles(rootDir);
4087
4249
  const documents = [];
4088
4250
  for (const fullPath of documentFiles) {
4089
- const raw = await readFile6(fullPath, "utf8");
4251
+ const raw = await readFile7(fullPath, "utf8");
4090
4252
  const repoPath = toRepoPath(rootDir, fullPath);
4091
4253
  const parsed = parseSyncedDocument(raw, repoPath);
4092
4254
  if (!parsed) {
@@ -4136,8 +4298,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
4136
4298
  result.skipped.push(document.repoPath);
4137
4299
  continue;
4138
4300
  }
4139
- await mkdir7(path8.dirname(fullPath), { recursive: true });
4140
- await writeFile7(fullPath, createSyncedDocumentContent(document), "utf8");
4301
+ await mkdir8(path9.dirname(fullPath), { recursive: true });
4302
+ await writeFile8(fullPath, createSyncedDocumentContent(document), "utf8");
4141
4303
  result.written.push(document.repoPath);
4142
4304
  }
4143
4305
  return result;
@@ -4266,7 +4428,7 @@ function parseSyncedHtml(content) {
4266
4428
  }
4267
4429
  async function readExistingSyncedDocument(fullPath) {
4268
4430
  try {
4269
- const raw = await readFile6(fullPath, "utf8");
4431
+ const raw = await readFile7(fullPath, "utf8");
4270
4432
  const parsed = parseSyncedDocument(raw, fullPath);
4271
4433
  if (!parsed) {
4272
4434
  return { exists: true };
@@ -4291,7 +4453,7 @@ async function readExistingSyncedDocument(fullPath) {
4291
4453
  async function findBrainDocumentFiles(rootDir) {
4292
4454
  const files = [];
4293
4455
  for (const syncRoot of [...syncRoots, htmlSyncRoot]) {
4294
- const fullRoot = path8.join(rootDir, syncRoot);
4456
+ const fullRoot = path9.join(rootDir, syncRoot);
4295
4457
  if (!await exists2(fullRoot)) {
4296
4458
  continue;
4297
4459
  }
@@ -4300,8 +4462,8 @@ async function findBrainDocumentFiles(rootDir) {
4300
4462
  return files;
4301
4463
  }
4302
4464
  async function walkBrainDocumentFiles(directory, files) {
4303
- for (const entry of await readdir3(directory, { withFileTypes: true })) {
4304
- const fullPath = path8.join(directory, entry.name);
4465
+ for (const entry of await readdir4(directory, { withFileTypes: true })) {
4466
+ const fullPath = path9.join(directory, entry.name);
4305
4467
  if (entry.isDirectory()) {
4306
4468
  await walkBrainDocumentFiles(fullPath, files);
4307
4469
  } else if (entry.isFile() && /\.(md|mdx|html?)$/i.test(entry.name)) {
@@ -4310,23 +4472,23 @@ async function walkBrainDocumentFiles(directory, files) {
4310
4472
  }
4311
4473
  }
4312
4474
  function safeRepoPath(rootDir, repoPath) {
4313
- if (path8.isAbsolute(repoPath)) {
4475
+ if (path9.isAbsolute(repoPath)) {
4314
4476
  throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
4315
4477
  }
4316
- const normalized = path8.normalize(repoPath);
4317
- if (normalized === ".." || normalized.startsWith(`..${path8.sep}`)) {
4478
+ const normalized = path9.normalize(repoPath);
4479
+ if (normalized === ".." || normalized.startsWith(`..${path9.sep}`)) {
4318
4480
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
4319
4481
  }
4320
- const root = path8.resolve(rootDir);
4321
- const fullPath = path8.resolve(root, normalized);
4322
- if (!fullPath.startsWith(`${root}${path8.sep}`)) {
4482
+ const root = path9.resolve(rootDir);
4483
+ const fullPath = path9.resolve(root, normalized);
4484
+ if (!fullPath.startsWith(`${root}${path9.sep}`)) {
4323
4485
  throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
4324
4486
  }
4325
4487
  return fullPath;
4326
4488
  }
4327
4489
  function isControlPlanePath(repoPath) {
4328
- const normalized = path8.normalize(repoPath);
4329
- return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path8.sep}`);
4490
+ const normalized = path9.normalize(repoPath);
4491
+ return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path9.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path9.sep}`);
4330
4492
  }
4331
4493
  function canonicalControlPlaneRepoPath(repoPath) {
4332
4494
  const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -4337,16 +4499,16 @@ function canonicalControlPlaneRepoPath(repoPath) {
4337
4499
  return normalized;
4338
4500
  }
4339
4501
  function toRepoPath(rootDir, fullPath) {
4340
- return path8.relative(rootDir, fullPath).split(path8.sep).join("/");
4502
+ return path9.relative(rootDir, fullPath).split(path9.sep).join("/");
4341
4503
  }
4342
4504
  function inferTitle(content, repoPath) {
4343
4505
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
4344
4506
  if (heading) return heading;
4345
4507
  const htmlHeading = content.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
4346
- return htmlHeading || path8.basename(repoPath, path8.extname(repoPath));
4508
+ return htmlHeading || path9.basename(repoPath, path9.extname(repoPath));
4347
4509
  }
4348
4510
  async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
4349
- const root = path8.resolve(rootDir);
4511
+ const root = path9.resolve(rootDir);
4350
4512
  const maxBytes = (options.maxFileKb ?? defaultAutoSyncMaxFileKb) * 1024;
4351
4513
  const syncedAt = options.syncedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4352
4514
  const existingById = new Map(existingDocuments.map((document) => [document.documentId, document]));
@@ -4371,7 +4533,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
4371
4533
  skipped.push({ repoPath: normalizedRepoPath, reason: "tooLarge" });
4372
4534
  continue;
4373
4535
  }
4374
- const content = await readFile6(fullPath, "utf8").catch(() => void 0);
4536
+ const content = await readFile7(fullPath, "utf8").catch(() => void 0);
4375
4537
  if (content === void 0) {
4376
4538
  skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
4377
4539
  continue;
@@ -4436,7 +4598,7 @@ async function listAutoSyncCandidatePaths(rootDir) {
4436
4598
  }
4437
4599
  const files = [];
4438
4600
  for (const syncRoot of [...syncRoots, htmlSyncRoot, ...legacySyncRoots]) {
4439
- const fullRoot = path8.join(rootDir, syncRoot);
4601
+ const fullRoot = path9.join(rootDir, syncRoot);
4440
4602
  if (await exists2(fullRoot)) {
4441
4603
  await walkAutoSyncFiles(rootDir, fullRoot, files);
4442
4604
  }
@@ -4444,9 +4606,9 @@ async function listAutoSyncCandidatePaths(rootDir) {
4444
4606
  return uniqueSortedRepoPaths(files);
4445
4607
  }
4446
4608
  async function walkAutoSyncFiles(rootDir, directory, files) {
4447
- for (const entry of await readdir3(directory, { withFileTypes: true }).catch(() => [])) {
4448
- const fullPath = path8.join(directory, entry.name);
4449
- const repoPath = normalizeRepoPath3(path8.relative(rootDir, fullPath));
4609
+ for (const entry of await readdir4(directory, { withFileTypes: true }).catch(() => [])) {
4610
+ const fullPath = path9.join(directory, entry.name);
4611
+ const repoPath = normalizeRepoPath3(path9.relative(rootDir, fullPath));
4450
4612
  if (entry.isDirectory()) {
4451
4613
  if (!autoSyncExcludedDirectoryNames.has(entry.name)) {
4452
4614
  await walkAutoSyncFiles(rootDir, fullPath, files);
@@ -4480,7 +4642,7 @@ function legacyDocumentTypeForRepoPath(repoPath) {
4480
4642
  return root && root in documentTypeByRoot ? documentTypeByRoot[root] : void 0;
4481
4643
  }
4482
4644
  function stableExternalDocumentId(metadata, repoPath) {
4483
- return `doc_external_${createHash4("sha256").update(`${metadata.amistioAccountId}:${metadata.amistioProjectId}:${metadata.repositoryLinkId}:${repoPath}`).digest("hex").slice(0, 24)}`;
4645
+ return `doc_external_${createHash5("sha256").update(`${metadata.amistioAccountId}:${metadata.amistioProjectId}:${metadata.repositoryLinkId}:${repoPath}`).digest("hex").slice(0, 24)}`;
4484
4646
  }
4485
4647
  function normalizeRepoPath3(repoPath) {
4486
4648
  return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -4508,9 +4670,9 @@ async function exists2(filePath) {
4508
4670
  }
4509
4671
 
4510
4672
  // src/tool-session-store.ts
4511
- import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
4512
- import os5 from "node:os";
4513
- import path9 from "node:path";
4673
+ import { mkdir as mkdir9, readFile as readFile8, writeFile as writeFile9 } from "node:fs/promises";
4674
+ import os6 from "node:os";
4675
+ import path10 from "node:path";
4514
4676
  var LocalToolSessionStore = class {
4515
4677
  constructor(filePath = defaultSessionStorePath()) {
4516
4678
  this.filePath = filePath;
@@ -4524,12 +4686,12 @@ var LocalToolSessionStore = class {
4524
4686
  async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
4525
4687
  const data = await this.read();
4526
4688
  data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
4527
- await mkdir8(path9.dirname(this.filePath), { recursive: true });
4528
- await writeFile8(this.filePath, JSON.stringify(data, null, 2), "utf8");
4689
+ await mkdir9(path10.dirname(this.filePath), { recursive: true });
4690
+ await writeFile9(this.filePath, JSON.stringify(data, null, 2), "utf8");
4529
4691
  }
4530
4692
  async read() {
4531
4693
  try {
4532
- return JSON.parse(await readFile7(this.filePath, "utf8"));
4694
+ return JSON.parse(await readFile8(this.filePath, "utf8"));
4533
4695
  } catch {
4534
4696
  return {};
4535
4697
  }
@@ -4537,16 +4699,16 @@ var LocalToolSessionStore = class {
4537
4699
  };
4538
4700
  function defaultSessionStorePath() {
4539
4701
  if (process.platform === "darwin") {
4540
- return path9.join(os5.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
4702
+ return path10.join(os6.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
4541
4703
  }
4542
4704
  if (process.platform === "win32") {
4543
- return path9.join(process.env.APPDATA ?? os5.homedir(), "Amistio", "tool-sessions.json");
4705
+ return path10.join(process.env.APPDATA ?? os6.homedir(), "Amistio", "tool-sessions.json");
4544
4706
  }
4545
- return path9.join(process.env.XDG_STATE_HOME ?? path9.join(os5.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
4707
+ return path10.join(process.env.XDG_STATE_HOME ?? path10.join(os6.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
4546
4708
  }
4547
4709
 
4548
4710
  // src/work-runner.ts
4549
- import path10 from "node:path";
4711
+ import path11 from "node:path";
4550
4712
  var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
4551
4713
  var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
4552
4714
  var assistantAnswerStart = "AMISTIO_ASSISTANT_ANSWER_START";
@@ -4570,6 +4732,14 @@ var projectContextTopLevelWarningMaxLength = 600;
4570
4732
  var validProjectContextSliceKinds = /* @__PURE__ */ new Set(["overview", "architecture", "domain", "data", "api", "frontend", "backend", "cli", "workflow", "operations", "security", "testing", "unknown"]);
4571
4733
  var validProjectContextEntityTypes = /* @__PURE__ */ new Set(["project", "system", "component", "domain", "tool", "decision", "feature", "risk", "team", "workflow", "unknown"]);
4572
4734
  var validProjectContextRelationTypes = /* @__PURE__ */ new Set(["uses", "depends_on", "decides", "supersedes", "touches", "blocks", "implements", "mentions"]);
4735
+ var canonicalProjectContextCitationSources = /* @__PURE__ */ new Map([
4736
+ ["projectbrain", "projectBrain"],
4737
+ ["approvedbrain", "projectBrain"],
4738
+ ["approvedprojectbrain", "projectBrain"],
4739
+ ["localsource", "localSource"],
4740
+ ["runnerstate", "runnerState"],
4741
+ ["mixed", "mixed"]
4742
+ ]);
4573
4743
  function createImplementationVerificationPrompt(workItem) {
4574
4744
  return [
4575
4745
  "# Amistio Implementation Verification",
@@ -4727,6 +4897,7 @@ function createProjectContextRefreshPrompt(workItem, context) {
4727
4897
  "- Use only these exact singular slice kind values: overview, architecture, domain, data, api, frontend, backend, cli, workflow, operations, security, testing, unknown.",
4728
4898
  "- Capture entities and relations that explain how the app is put together and where future work should look first.",
4729
4899
  "- Prefer summaries, repository-relative paths, short citations, tags, and freshness status over raw source excerpts.",
4900
+ "- Use only these exact citation source values: projectBrain, localSource, runnerState, mixed. Approved project-brain records use projectBrain, not approvedBrain.",
4730
4901
  "- Mark stale or missing areas explicitly instead of guessing.",
4731
4902
  "- Keep coverage.missingAreas entries concise noun phrases under 160 characters.",
4732
4903
  "- Keep coverage.warnings and verificationPlan entries under 300 characters; keep top-level warnings under 600 characters.",
@@ -5400,6 +5571,7 @@ function normalizeProjectContextCitationPaths(value, options) {
5400
5571
  return citation;
5401
5572
  }
5402
5573
  const normalizedCitation = { ...citation };
5574
+ normalizedCitation.source = normalizeProjectContextCitationSource(normalizedCitation.source);
5403
5575
  if (typeof normalizedCitation.repoPath === "string") {
5404
5576
  normalizedCitation.repoPath = normalizeProjectContextRepoPath(normalizedCitation.repoPath, options);
5405
5577
  }
@@ -5408,20 +5580,27 @@ function normalizeProjectContextCitationPaths(value, options) {
5408
5580
  }
5409
5581
  return normalized;
5410
5582
  }
5583
+ function normalizeProjectContextCitationSource(value) {
5584
+ if (typeof value !== "string") {
5585
+ return value;
5586
+ }
5587
+ const sourceKey = value.trim().replace(/[\s_-]+/g, "").toLowerCase();
5588
+ return canonicalProjectContextCitationSources.get(sourceKey) ?? value;
5589
+ }
5411
5590
  function normalizeProjectContextRepoPath(value, options) {
5412
5591
  const trimmed = value.trim();
5413
5592
  if (!trimmed || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed) || /^file:/i.test(trimmed) || /^[A-Za-z]:($|[^\\/])/.test(trimmed)) {
5414
5593
  throwUnsafeProjectContextPath();
5415
5594
  }
5416
- const absolute = trimmed.startsWith("/") || trimmed.startsWith("\\\\") || path10.isAbsolute(trimmed) || path10.win32.isAbsolute(trimmed);
5595
+ const absolute = trimmed.startsWith("/") || trimmed.startsWith("\\\\") || path11.isAbsolute(trimmed) || path11.win32.isAbsolute(trimmed);
5417
5596
  if (!absolute) {
5418
5597
  return normalizeRelativeProjectContextPath(trimmed);
5419
5598
  }
5420
5599
  if (!options.repositoryRoot) {
5421
5600
  throwUnsafeProjectContextPath();
5422
5601
  }
5423
- const useWindowsPathRules = path10.win32.isAbsolute(trimmed) && !trimmed.startsWith("/");
5424
- const relativePath = useWindowsPathRules ? path10.win32.relative(path10.win32.resolve(options.repositoryRoot), path10.win32.resolve(trimmed)) : path10.relative(path10.resolve(options.repositoryRoot), path10.resolve(trimmed));
5602
+ const useWindowsPathRules = path11.win32.isAbsolute(trimmed) && !trimmed.startsWith("/");
5603
+ const relativePath = useWindowsPathRules ? path11.win32.relative(path11.win32.resolve(options.repositoryRoot), path11.win32.resolve(trimmed)) : path11.relative(path11.resolve(options.repositoryRoot), path11.resolve(trimmed));
5425
5604
  return normalizeRelativeProjectContextPath(relativePath);
5426
5605
  }
5427
5606
  function normalizeRelativeProjectContextPath(value) {
@@ -5473,7 +5652,7 @@ function stripJsonFence(value) {
5473
5652
  }
5474
5653
 
5475
5654
  // src/runner-status.ts
5476
- import { createHash as createHash5 } from "node:crypto";
5655
+ import { createHash as createHash6 } from "node:crypto";
5477
5656
  var watchStateReminderMs = 60 * 1e3;
5478
5657
  function formatWatchStartupContext(input) {
5479
5658
  return [
@@ -5500,20 +5679,20 @@ function watchStateKey(action) {
5500
5679
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
5501
5680
  }
5502
5681
  function stableRunnerId(input) {
5503
- const digest = createHash5("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
5682
+ const digest = createHash6("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
5504
5683
  return `runner_${digest}`;
5505
5684
  }
5506
5685
 
5507
5686
  // src/runner-resources.ts
5508
- import os6 from "node:os";
5687
+ import os7 from "node:os";
5509
5688
  var defaultRuntime = {
5510
5689
  nowMs: () => Date.now(),
5511
5690
  memoryUsage: () => process.memoryUsage(),
5512
5691
  uptime: () => process.uptime(),
5513
5692
  cpuUsage: () => process.cpuUsage(),
5514
- totalmem: () => os6.totalmem(),
5515
- freemem: () => os6.freemem(),
5516
- loadavg: () => os6.loadavg()
5693
+ totalmem: () => os7.totalmem(),
5694
+ freemem: () => os7.freemem(),
5695
+ loadavg: () => os7.loadavg()
5517
5696
  };
5518
5697
  var previousRunnerResourceSample;
5519
5698
  function sampleCurrentRunnerResourceUsage() {
@@ -5625,9 +5804,9 @@ function roundNumber(value, digits) {
5625
5804
 
5626
5805
  // src/importer.ts
5627
5806
  import { execFile as execFile4 } from "node:child_process";
5628
- import { createHash as createHash6 } from "node:crypto";
5629
- import { readdir as readdir4, readFile as readFile8, stat as stat4 } from "node:fs/promises";
5630
- import path11 from "node:path";
5807
+ import { createHash as createHash7 } from "node:crypto";
5808
+ import { readdir as readdir5, readFile as readFile9, stat as stat4 } from "node:fs/promises";
5809
+ import path12 from "node:path";
5631
5810
  import { promisify as promisify4 } from "node:util";
5632
5811
  var execFileAsync4 = promisify4(execFile4);
5633
5812
  var defaultMaxFileKb = 256;
@@ -5648,12 +5827,12 @@ var documentFolderByType = {
5648
5827
  workflow: "docs/workflows"
5649
5828
  };
5650
5829
  async function inspectLocalRepository(rootDir, defaultBranch) {
5651
- const requestedRoot = path11.resolve(rootDir);
5830
+ const requestedRoot = path12.resolve(rootDir);
5652
5831
  const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
5653
5832
  const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
5654
5833
  const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
5655
5834
  const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
5656
- const repoName = (parsedCloneUrl?.repoName ?? path11.basename(root)) || "repository";
5835
+ const repoName = (parsedCloneUrl?.repoName ?? path12.basename(root)) || "repository";
5657
5836
  const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
5658
5837
  return {
5659
5838
  rootDir: root,
@@ -5665,7 +5844,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
5665
5844
  };
5666
5845
  }
5667
5846
  async function scanLegacyDocuments(options) {
5668
- const rootDir = path11.resolve(options.rootDir);
5847
+ const rootDir = path12.resolve(options.rootDir);
5669
5848
  const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
5670
5849
  const skipped = [];
5671
5850
  const candidates = [];
@@ -5685,7 +5864,7 @@ async function scanLegacyDocuments(options) {
5685
5864
  skipped.push({ repoPath, reason: "excluded" });
5686
5865
  continue;
5687
5866
  }
5688
- const fullPath = path11.join(rootDir, ...repoPath.split("/"));
5867
+ const fullPath = path12.join(rootDir, ...repoPath.split("/"));
5689
5868
  const fileStat = await stat4(fullPath).catch(() => void 0);
5690
5869
  if (!fileStat?.isFile()) {
5691
5870
  skipped.push({ repoPath, reason: "unreadable" });
@@ -5695,7 +5874,7 @@ async function scanLegacyDocuments(options) {
5695
5874
  skipped.push({ repoPath, reason: "tooLarge" });
5696
5875
  continue;
5697
5876
  }
5698
- const content = await readFile8(fullPath, "utf8").catch(() => void 0);
5877
+ const content = await readFile9(fullPath, "utf8").catch(() => void 0);
5699
5878
  if (content === void 0) {
5700
5879
  skipped.push({ repoPath, reason: "unreadable" });
5701
5880
  continue;
@@ -5785,10 +5964,10 @@ async function listRepositoryPaths(rootDir) {
5785
5964
  return files;
5786
5965
  }
5787
5966
  async function walkRepository(rootDir, directory, files) {
5788
- const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
5967
+ const entries = await readdir5(directory, { withFileTypes: true }).catch(() => []);
5789
5968
  for (const entry of entries) {
5790
- const fullPath = path11.join(directory, entry.name);
5791
- const repoPath = normalizeRepoPath4(path11.relative(rootDir, fullPath));
5969
+ const fullPath = path12.join(directory, entry.name);
5970
+ const repoPath = normalizeRepoPath4(path12.relative(rootDir, fullPath));
5792
5971
  if (entry.isDirectory()) {
5793
5972
  if (!excludedDirectoryNames.has(entry.name)) {
5794
5973
  await walkRepository(rootDir, fullPath, files);
@@ -5856,9 +6035,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
5856
6035
  usedPaths.add(basePath);
5857
6036
  return basePath;
5858
6037
  }
5859
- const extension = path11.posix.extname(basePath) || ".md";
5860
- const directory = path11.posix.dirname(basePath);
5861
- const basename = path11.posix.basename(basePath, extension);
6038
+ const extension = path12.posix.extname(basePath) || ".md";
6039
+ const directory = path12.posix.dirname(basePath);
6040
+ const basename = path12.posix.basename(basePath, extension);
5862
6041
  const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
5863
6042
  usedPaths.add(uniquePath);
5864
6043
  return uniquePath;
@@ -5931,7 +6110,7 @@ function inferTitle2(content, repoPath) {
5931
6110
  if (heading) return heading;
5932
6111
  const htmlHeading = body.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
5933
6112
  if (htmlHeading) return htmlHeading;
5934
- const basename = path11.posix.basename(repoPath, path11.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
6113
+ const basename = path12.posix.basename(repoPath, path12.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
5935
6114
  return titleCase(basename || "Imported Document");
5936
6115
  }
5937
6116
  function stripFrontmatter(content) {
@@ -5953,7 +6132,7 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
5953
6132
  return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
5954
6133
  }
5955
6134
  function hashText(value, length) {
5956
- return createHash6("sha256").update(value).digest("hex").slice(0, length);
6135
+ return createHash7("sha256").update(value).digest("hex").slice(0, length);
5957
6136
  }
5958
6137
  function normalizeRepoPath4(value) {
5959
6138
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -5965,7 +6144,7 @@ async function runGit2(args) {
5965
6144
 
5966
6145
  // src/runner-actions.ts
5967
6146
  import { spawn as spawn4 } from "node:child_process";
5968
- import path12 from "node:path";
6147
+ import path13 from "node:path";
5969
6148
  function buildBackgroundRunnerArgs(options) {
5970
6149
  const args = [
5971
6150
  "run",
@@ -5975,7 +6154,7 @@ function buildBackgroundRunnerArgs(options) {
5975
6154
  "--runner-id",
5976
6155
  options.runnerId,
5977
6156
  "--root",
5978
- path12.resolve(options.root),
6157
+ path13.resolve(options.root),
5979
6158
  "--session",
5980
6159
  options.session,
5981
6160
  "--interval-seconds",
@@ -6092,8 +6271,8 @@ function truncateProcessOutput(value) {
6092
6271
 
6093
6272
  // src/git-worktree.ts
6094
6273
  import { execFile as execFile5 } from "node:child_process";
6095
- import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
6096
- import path13 from "node:path";
6274
+ import { mkdir as mkdir10, stat as stat5 } from "node:fs/promises";
6275
+ import path14 from "node:path";
6097
6276
  import { promisify as promisify5 } from "node:util";
6098
6277
  var execFileAsync5 = promisify5(execFile5);
6099
6278
  function needsGitWorktreeIsolation(workItem) {
@@ -6122,7 +6301,7 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
6122
6301
  await assertExistingWorktree(worktreePath, identity.branch);
6123
6302
  return { ...identity, baseRevision, worktreePath };
6124
6303
  }
6125
- await mkdir9(path13.dirname(worktreePath), { recursive: true });
6304
+ await mkdir10(path14.dirname(worktreePath), { recursive: true });
6126
6305
  const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
6127
6306
  const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
6128
6307
  await gitOutput(repoRoot, worktreeArgs).catch((error) => {
@@ -6131,9 +6310,9 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
6131
6310
  return { ...identity, baseRevision, worktreePath };
6132
6311
  }
6133
6312
  function localWorktreePath(repoRoot, worktreeKey) {
6134
- const repoName = path13.basename(repoRoot);
6313
+ const repoName = path14.basename(repoRoot);
6135
6314
  const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
6136
- return path13.join(path13.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
6315
+ return path14.join(path14.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
6137
6316
  }
6138
6317
  async function assertExistingWorktree(worktreePath, branch) {
6139
6318
  await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
@@ -6180,7 +6359,7 @@ function errorMessage2(error) {
6180
6359
 
6181
6360
  // src/implementation-handoff.ts
6182
6361
  import { execFile as execFile6 } from "node:child_process";
6183
- import path14 from "node:path";
6362
+ import path15 from "node:path";
6184
6363
  import { promisify as promisify6 } from "node:util";
6185
6364
  var execFileAsync6 = promisify6(execFile6);
6186
6365
  async function completeImplementationHandoff(input) {
@@ -6266,7 +6445,7 @@ async function cleanupWorktree(run, input) {
6266
6445
  return { status: "failed", message: "Cleanup skipped because the worktree is not clean after PR handoff." };
6267
6446
  }
6268
6447
  try {
6269
- await gitOutput2(run, input.primaryRepoRoot || path14.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
6448
+ await gitOutput2(run, input.primaryRepoRoot || path15.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
6270
6449
  return { status: "completed" };
6271
6450
  } catch (error) {
6272
6451
  return { status: "failed", message: `Cleanup failed: ${safeErrorMessage(error)}` };
@@ -6677,7 +6856,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
6677
6856
  }
6678
6857
  const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
6679
6858
  if (options.out) {
6680
- await writeFile9(options.out, prompt, "utf8");
6859
+ await writeFile10(options.out, prompt, "utf8");
6681
6860
  console.log(`Wrote work prompt to ${options.out}.`);
6682
6861
  } else {
6683
6862
  console.log(prompt);
@@ -6755,7 +6934,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
6755
6934
  projectId: context.metadata.amistioProjectId,
6756
6935
  repositoryLinkId: context.metadata.repositoryLinkId,
6757
6936
  runnerId,
6758
- rootDir: path15.resolve(options.root),
6937
+ rootDir: path16.resolve(options.root),
6759
6938
  apiUrl: options.apiUrl,
6760
6939
  args: buildBackgroundRunnerArgs(resolvedOptions)
6761
6940
  });
@@ -6927,7 +7106,7 @@ runnerService.command("install").description("Install a user-level startup servi
6927
7106
  projectId: context.metadata.amistioProjectId,
6928
7107
  repositoryLinkId: context.metadata.repositoryLinkId,
6929
7108
  runnerId,
6930
- rootDir: path15.resolve(options.root),
7109
+ rootDir: path16.resolve(options.root),
6931
7110
  apiUrl: options.apiUrl,
6932
7111
  args,
6933
7112
  platform
@@ -6998,6 +7177,18 @@ async function runWatchIteration({ command, context, options, runnerId }) {
6998
7177
  runnerId
6999
7178
  });
7000
7179
  }
7180
+ if (!options.dryRun) {
7181
+ const replayResult = await replayPendingBrainGenerationFinalizations({
7182
+ accountId: context.metadata.amistioAccountId,
7183
+ apiClient: context.client,
7184
+ projectId: context.metadata.amistioProjectId,
7185
+ repositoryLinkId: context.metadata.repositoryLinkId,
7186
+ runnerId
7187
+ });
7188
+ if (replayResult) {
7189
+ return replayResult;
7190
+ }
7191
+ }
7001
7192
  return await runNextWorkItem({
7002
7193
  apiClient: context.client,
7003
7194
  projectId: context.metadata.amistioProjectId,
@@ -7745,6 +7936,58 @@ function runnerCommandLabel(commandKind) {
7745
7936
  if (commandKind === "restart") return "restart";
7746
7937
  return "remove";
7747
7938
  }
7939
+ async function replayPendingBrainGenerationFinalizations({ accountId, apiClient, projectId, repositoryLinkId, runnerId }) {
7940
+ const pendingEntries = await listPendingBrainGenerationFinalizations({ accountId, projectId, repositoryLinkId, runnerId });
7941
+ if (!pendingEntries.length) {
7942
+ return void 0;
7943
+ }
7944
+ let completedCount = 0;
7945
+ for (const entry of pendingEntries) {
7946
+ const replay = await submitBrainGenerationFinalizationEntry(apiClient, entry, { recordReplayTelemetry: true });
7947
+ if (replay.status === "completed") {
7948
+ completedCount += 1;
7949
+ continue;
7950
+ }
7951
+ return { status: "failed", exitCode: 1, message: replay.message };
7952
+ }
7953
+ const message = `Replayed ${completedCount} pending brain generation finalization${completedCount === 1 ? "" : "s"}.`;
7954
+ console.log(message);
7955
+ return { status: "completed", exitCode: 0, message };
7956
+ }
7957
+ async function submitBrainGenerationFinalizationEntry(apiClient, entry, options = {}) {
7958
+ let result;
7959
+ try {
7960
+ result = await apiClient.submitBrainGenerationResult(entry.projectId, entry.workItemId, entry.result);
7961
+ } catch (error) {
7962
+ const detail = truncateLogExcerpt(errorMessage3(error));
7963
+ if (isRetryableApiError(error)) {
7964
+ const updated = await markBrainGenerationFinalizationRetry(entry, detail);
7965
+ const message2 = `Pending brain generation finalization ${entry.workItemId} could not be replayed yet (${updated.retryCount} attempt${updated.retryCount === 1 ? "" : "s"}): ${detail}`;
7966
+ console.error(message2);
7967
+ return { status: "failed", message: message2, retryable: true };
7968
+ }
7969
+ await markBrainGenerationFinalizationTerminal(entry, detail);
7970
+ const message = `Pending brain generation finalization ${entry.workItemId} reached a terminal API failure: ${detail}`;
7971
+ console.error(message);
7972
+ return { status: "failed", message, retryable: false };
7973
+ }
7974
+ await deleteBrainGenerationFinalizationEntry(entry).catch((error) => {
7975
+ console.error(`delete pending brain generation finalization ${entry.workItemId} failed: ${errorMessage3(error)}`);
7976
+ });
7977
+ if (options.recordReplayTelemetry) {
7978
+ await recordRunnerMilestone(apiClient, entry.projectId, result.workItem, entry.runnerId, entry.repositoryLinkId, {
7979
+ status: entry.result.status,
7980
+ summary: entry.result.status === "completed" ? "Replayed pending brain generation result finalization." : "Replayed pending brain generation failure finalization.",
7981
+ idempotencyKey: `runner_milestone_generation_replayed_${entry.workItemId}_${result.workItem.idempotencyKey}`,
7982
+ metadata: { artifactCount: result.documents.length, replayedFinalization: true, workKind: entry.workKind }
7983
+ });
7984
+ await apiClient.sendRunnerHeartbeat(entry.projectId, entry.runnerId, entry.repositoryLinkId, "online", {
7985
+ ...runnerHeartbeatMetadata(),
7986
+ preferenceMessage: "Pending result finalization replayed."
7987
+ }).catch(() => void 0);
7988
+ }
7989
+ return { status: "completed", workItem: result.workItem, documentCount: result.documents.length };
7990
+ }
7748
7991
  async function finalizeBrainGenerationWork({
7749
7992
  apiClient,
7750
7993
  durationMs,
@@ -7770,55 +8013,97 @@ ${toolResult.stderr}`);
7770
8013
  generationError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
7771
8014
  }
7772
8015
  const finalStatus = artifacts ? "completed" : "failed";
7773
- const updatedToolSession = await finalizeToolSession({
7774
- apiClient,
7775
- projectId,
7776
- status: finalStatus,
7777
- runnerId,
7778
- workItemId: workItem.workItemId,
7779
- stdout: toolResult.stdout,
7780
- ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
7781
- ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
7782
- ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
7783
- ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
7784
- ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
7785
- });
7786
8016
  const sessionTelemetry = {
7787
8017
  sessionPolicy: sessionContext.policy,
7788
8018
  sessionDecision: sessionContext.decision,
7789
8019
  sessionDecisionReason: sessionContext.reason,
7790
- ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
7791
- ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
8020
+ ...sessionContext.toolSession ? { toolSessionId: sessionContext.toolSession.toolSessionId } : {},
8021
+ ...sessionContext.toolSession?.sessionGroupKey ? { sessionGroupKey: sessionContext.toolSession.sessionGroupKey } : {}
7792
8022
  };
7793
8023
  if (artifacts) {
7794
8024
  const completionMessage = workItem.workKind === "planRevision" ? `${toolName} returned a revised plan for review.` : `${toolName} generated ${artifacts.length} brain artifact${artifacts.length === 1 ? "" : "s"}.`;
7795
- const result = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
8025
+ const resultMutation = {
7796
8026
  status: "completed",
7797
8027
  runnerId,
7798
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
8028
+ idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`,
7799
8029
  artifacts,
7800
8030
  tool: toolName,
7801
8031
  durationMs,
7802
8032
  ...sessionTelemetry,
7803
8033
  message: completionMessage
8034
+ };
8035
+ const entry = createBrainGenerationFinalizationEntry({
8036
+ accountId: workItem.accountId,
8037
+ projectId,
8038
+ repositoryLinkId,
8039
+ runnerId,
8040
+ workItemId: workItem.workItemId,
8041
+ workKind: brainGenerationFinalizationWorkKind(workItem.workKind),
8042
+ attempt: workItem.attempt,
8043
+ idempotencyKey: resultMutation.idempotencyKey,
8044
+ result: resultMutation
7804
8045
  });
8046
+ await upsertBrainGenerationFinalizationEntry(entry);
8047
+ const replay = await submitBrainGenerationFinalizationEntry(apiClient, entry);
8048
+ if (replay.status === "failed") {
8049
+ if (!replay.retryable) {
8050
+ throw new Error(replay.message);
8051
+ }
8052
+ return { status: "failed", exitCode: 1 };
8053
+ }
8054
+ const toolSessionSettlement = await Promise.allSettled([
8055
+ finalizeToolSession({
8056
+ apiClient,
8057
+ projectId,
8058
+ status: finalStatus,
8059
+ runnerId,
8060
+ workItemId: workItem.workItemId,
8061
+ stdout: toolResult.stdout,
8062
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
8063
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
8064
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
8065
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
8066
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
8067
+ })
8068
+ ]);
8069
+ logRejectedSettlements("finalize generation tool session", toolSessionSettlement);
7805
8070
  await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7806
8071
  status: "completed",
7807
8072
  summary: completionMessage,
7808
- idempotencyKey: `runner_milestone_generation_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
7809
- metadata: { tool: toolName, durationMs, artifactCount: result.documents.length, verificationSummary: "Generated artifacts were accepted by the Amistio API for review." }
8073
+ idempotencyKey: `runner_milestone_generation_completed_${workItem.workItemId}_${entry.idempotencyKey}`,
8074
+ metadata: { tool: toolName, durationMs, artifactCount: replay.documentCount, verificationSummary: "Generated artifacts were accepted by the Amistio API for review." }
7810
8075
  });
7811
- await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7812
- console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
8076
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)).catch((error) => {
8077
+ console.error(`send generation completion heartbeat failed: ${errorMessage3(error)}`);
8078
+ });
8079
+ console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${replay.documentCount} brain artifact${replay.documentCount === 1 ? "" : "s"} for review.`);
7813
8080
  return { status: "completed", exitCode: 0 };
7814
8081
  }
8082
+ const updatedToolSession = await finalizeToolSession({
8083
+ apiClient,
8084
+ projectId,
8085
+ status: finalStatus,
8086
+ runnerId,
8087
+ workItemId: workItem.workItemId,
8088
+ stdout: toolResult.stdout,
8089
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
8090
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
8091
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
8092
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
8093
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
8094
+ });
8095
+ const failedSessionTelemetry = {
8096
+ ...sessionTelemetry,
8097
+ ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
8098
+ ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
8099
+ };
7815
8100
  const failedResult = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
7816
8101
  status: "failed",
7817
8102
  runnerId,
7818
8103
  idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
7819
8104
  tool: toolName,
7820
8105
  durationMs,
7821
- ...sessionTelemetry,
8106
+ ...failedSessionTelemetry,
7822
8107
  message: `${toolName} did not produce valid brain artifacts.`,
7823
8108
  ...generationError ? { error: generationError } : {}
7824
8109
  });
@@ -7832,6 +8117,9 @@ ${toolResult.stderr}`);
7832
8117
  console.error(generationError ?? "Local runner generation failed.");
7833
8118
  return { status: "failed", exitCode: toolResult.exitCode || 1 };
7834
8119
  }
8120
+ function brainGenerationFinalizationWorkKind(workKind) {
8121
+ return workKind === "planRevision" ? "planRevision" : "brainGeneration";
8122
+ }
7835
8123
  async function finalizeAssistantQuestionWork({
7836
8124
  apiClient,
7837
8125
  durationMs,
@@ -8750,10 +9038,10 @@ function parseReasoningEffort(value) {
8750
9038
  throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
8751
9039
  }
8752
9040
  function inferRepoName(root) {
8753
- return path15.basename(path15.resolve(root)) || "repository";
9041
+ return path16.basename(path16.resolve(root)) || "repository";
8754
9042
  }
8755
9043
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
8756
- return createHash7("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
9044
+ return createHash8("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
8757
9045
  }
8758
9046
  function defaultApiUrl() {
8759
9047
  const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
@@ -9011,7 +9299,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
9011
9299
  return {
9012
9300
  version: CLI_VERSION,
9013
9301
  mode,
9014
- hostname: os7.hostname(),
9302
+ hostname: os8.hostname(),
9015
9303
  ...runnerIsolationCapabilityMetadata(),
9016
9304
  resourceUsage: sampleCurrentRunnerResourceUsage(),
9017
9305
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
@@ -9034,7 +9322,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
9034
9322
  };
9035
9323
  }
9036
9324
  function runnerMachineId() {
9037
- return createHash7("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
9325
+ return createHash8("sha256").update(`${os8.hostname()}:${os8.platform()}:${os8.arch()}`).digest("hex").slice(0, 20);
9038
9326
  }
9039
9327
  async function delay(milliseconds) {
9040
9328
  await new Promise((resolve) => setTimeout(resolve, milliseconds));