@amistio/cli 0.1.7 → 0.1.9

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createHash as createHash6, randomUUID } from "node:crypto";
4
+ import { createHash as createHash7, randomUUID } from "node:crypto";
5
5
  import { writeFile as writeFile9 } from "node:fs/promises";
6
6
  import os7 from "node:os";
7
7
  import path13 from "node:path";
@@ -120,13 +120,55 @@ var runnerToolNameSchema = z.enum(runnerToolNames);
120
120
  var runnerToolSelectionSchema = z.union([runnerToolNameSchema, z.literal("auto")]);
121
121
  var runnerPreferenceScopeSchema = z.enum(["account", "project"]);
122
122
  var runnerPreferenceSourceSchema = z.enum(["cli", "project", "account", "default"]);
123
- var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "channelUnsupported", "custom", "none"]);
123
+ var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "variantUnsupported", "reasoningUnsupported", "channelUnsupported", "custom", "none"]);
124
124
  var runnerInvocationChannelSchema = z.enum(["auto", "sdk", "command"]);
125
125
  var runnerEffectiveInvocationChannelSchema = z.enum(["sdk", "command"]);
126
+ var runnerReasoningEffortSchema = z.enum(["auto", "low", "medium", "high", "xhigh"]);
127
+ var runnerProviderModelStatusSchema = z.enum(["alpha", "beta", "deprecated", "active"]);
128
+ var runnerProviderIdSchema = z.string().trim().min(1).max(120);
129
+ var runnerProviderModelIdSchema = z.string().trim().min(1).max(200);
130
+ var runnerProviderModelLimitSchema = z.object({
131
+ context: z.number().nonnegative().optional(),
132
+ input: z.number().nonnegative().optional(),
133
+ output: z.number().nonnegative().optional()
134
+ }).strict();
135
+ var runnerProviderModelModalitiesSchema = z.object({
136
+ input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])).max(12).optional(),
137
+ output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])).max(12).optional()
138
+ }).strict();
139
+ var runnerProviderModelVariantSchema = z.object({
140
+ disabled: z.boolean().optional()
141
+ }).strict();
142
+ var runnerProviderModelSchema = z.object({
143
+ id: runnerProviderModelIdSchema.optional(),
144
+ name: z.string().trim().min(1).max(200).optional(),
145
+ family: z.string().trim().min(1).max(120).optional(),
146
+ releaseDate: z.string().trim().min(1).max(40).optional(),
147
+ attachment: z.boolean().optional(),
148
+ reasoning: z.boolean().optional(),
149
+ temperature: z.boolean().optional(),
150
+ toolCall: z.boolean().optional(),
151
+ status: runnerProviderModelStatusSchema.optional(),
152
+ limit: runnerProviderModelLimitSchema.optional(),
153
+ modalities: runnerProviderModelModalitiesSchema.optional(),
154
+ supportedReasoningEfforts: z.array(runnerReasoningEffortSchema).max(8).optional(),
155
+ variants: z.record(z.string().trim().min(1).max(120), runnerProviderModelVariantSchema).optional()
156
+ }).strict();
157
+ var runnerProviderConfigSchema = z.object({
158
+ id: runnerProviderIdSchema.optional(),
159
+ name: z.string().trim().min(1).max(160).optional(),
160
+ api: z.string().trim().min(1).max(80).optional(),
161
+ models: z.record(runnerProviderModelIdSchema, runnerProviderModelSchema).default({})
162
+ }).strict();
163
+ var runnerProviderCatalogSchema = z.record(runnerProviderIdSchema, runnerProviderConfigSchema);
126
164
  var runnerToolModelPreferenceSchema = z.object({
127
165
  tool: runnerToolSelectionSchema.optional(),
128
166
  invocationChannel: runnerInvocationChannelSchema.optional(),
129
- model: z.string().trim().min(1).max(160).optional()
167
+ model: z.string().trim().min(1).max(160).optional(),
168
+ providerId: runnerProviderIdSchema.optional(),
169
+ modelId: runnerProviderModelIdSchema.optional(),
170
+ modelVariant: z.string().trim().min(1).max(120).optional(),
171
+ reasoningEffort: runnerReasoningEffortSchema.optional()
130
172
  });
131
173
  var runnerToolCapabilitySchema = z.object({
132
174
  name: runnerToolNameSchema,
@@ -138,6 +180,7 @@ var runnerToolCapabilitySchema = z.object({
138
180
  supportsSessionReuse: z.boolean(),
139
181
  resumabilityScope: sessionResumabilityScopeSchema,
140
182
  supportsModelSelection: z.boolean(),
183
+ providerCatalog: runnerProviderCatalogSchema.optional(),
141
184
  supportsBranchIsolation: z.boolean().optional(),
142
185
  supportsGitWorktreeIsolation: z.boolean().optional()
143
186
  });
@@ -159,6 +202,7 @@ var runnerResourceUsageSchema = z.object({
159
202
  var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
160
203
  var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
161
204
  var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
205
+ var repositoryBrainAutoSyncStatusSchema = z.enum(["disabled", "enabledInactive", "active", "synced", "failed", "blocked", "conflicted"]);
162
206
  var projectStatusSchema = z.enum(["active", "archived"]);
163
207
  var workspaceScopeKindSchema = z.enum(["personal", "organization"]);
164
208
  var personalWorkspaceScopeSchema = z.object({
@@ -229,6 +273,7 @@ var repositoryLinkItemSchema = baseItemSchema.extend({
229
273
  linkedByUserId: z.string().min(1).optional(),
230
274
  linkSource: repositoryLinkSourceSchema.optional(),
231
275
  cloneStatus: repositoryCloneStatusSchema.optional(),
276
+ autoSyncEnabled: z.boolean().optional(),
232
277
  lastValidatedAt: isoDateTimeSchema.optional(),
233
278
  status: z.enum(["active", "revoked"]).default("active")
234
279
  });
@@ -350,13 +395,29 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
350
395
  capabilities: z.array(runnerToolCapabilitySchema).optional(),
351
396
  requestedTool: runnerToolSelectionSchema.optional(),
352
397
  requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
398
+ requestedProviderId: runnerProviderIdSchema.optional(),
399
+ requestedModelId: runnerProviderModelIdSchema.optional(),
400
+ requestedModelVariant: z.string().trim().min(1).max(120).optional(),
401
+ requestedReasoningEffort: runnerReasoningEffortSchema.optional(),
353
402
  effectiveTool: z.union([runnerToolNameSchema, z.literal("custom")]).optional(),
354
403
  effectiveInvocationChannel: runnerEffectiveInvocationChannelSchema.optional(),
355
404
  effectiveModel: z.string().min(1).optional(),
405
+ effectiveProviderId: runnerProviderIdSchema.optional(),
406
+ effectiveModelId: runnerProviderModelIdSchema.optional(),
407
+ effectiveModelVariant: z.string().trim().min(1).max(120).optional(),
408
+ effectiveReasoningEffort: runnerReasoningEffortSchema.optional(),
356
409
  preferenceSource: runnerPreferenceSourceSchema.optional(),
357
410
  preferenceStatus: runnerPreferenceStatusSchema.optional(),
358
411
  preferenceMessage: z.string().optional(),
359
412
  resourceUsage: runnerResourceUsageSchema.optional(),
413
+ autoSyncStatus: repositoryBrainAutoSyncStatusSchema.optional(),
414
+ autoSyncMessage: z.string().max(400).optional(),
415
+ autoSyncLastStartedAt: isoDateTimeSchema.optional(),
416
+ autoSyncLastSuccessAt: isoDateTimeSchema.optional(),
417
+ autoSyncLastFailureAt: isoDateTimeSchema.optional(),
418
+ autoSyncPushedCount: z.number().int().nonnegative().optional(),
419
+ autoSyncSkippedCount: z.number().int().nonnegative().optional(),
420
+ autoSyncConflictCount: z.number().int().nonnegative().optional(),
360
421
  lastSeenAt: isoDateTimeSchema
361
422
  });
362
423
  var runnerSettingsItemSchema = baseItemSchema.extend({
@@ -410,6 +471,7 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
410
471
  projectId: z.string().min(1),
411
472
  runnerCredentialId: z.string().min(1),
412
473
  repositoryLinkId: z.string().min(1),
474
+ runnerId: z.string().min(1).optional(),
413
475
  pairedByUserId: z.string().min(1).optional(),
414
476
  machineId: z.string().min(1).optional(),
415
477
  tokenHash: z.string().min(32),
@@ -719,6 +781,8 @@ var pairingSessionItemSchema = baseItemSchema.extend({
719
781
  projectId: z.string().min(1),
720
782
  createdByUserId: z.string().min(1),
721
783
  expiresAt: isoDateTimeSchema,
784
+ failedAttemptCount: z.number().int().nonnegative().optional(),
785
+ lastFailedAttemptAt: isoDateTimeSchema.optional(),
722
786
  status: z.enum(["pending", "confirmed", "expired", "revoked"])
723
787
  });
724
788
  var projectItemUnionSchema = z.discriminatedUnion("type", [
@@ -1433,6 +1497,13 @@ var ApiClient = class {
1433
1497
  { method: "GET" }
1434
1498
  );
1435
1499
  }
1500
+ async listRepositoryLinks(projectId) {
1501
+ return this.request(
1502
+ `/projects/${projectId}/repository-links`,
1503
+ z3.object({ repositoryLinks: z3.array(repositoryLinkItemSchema) }),
1504
+ { method: "GET" }
1505
+ );
1506
+ }
1436
1507
  async listPlanReviewMessages(projectId, documentId) {
1437
1508
  const suffix = documentId ? `?documentId=${encodeURIComponent(documentId)}` : "";
1438
1509
  return this.request(
@@ -1500,6 +1571,10 @@ var ApiClient = class {
1500
1571
  tool: runnerToolSelectionSchema,
1501
1572
  invocationChannel: runnerInvocationChannelSchema,
1502
1573
  model: z3.string().optional(),
1574
+ providerId: z3.string().optional(),
1575
+ modelId: z3.string().optional(),
1576
+ modelVariant: z3.string().optional(),
1577
+ reasoningEffort: runnerReasoningEffortSchema.optional(),
1503
1578
  source: runnerPreferenceSourceSchema
1504
1579
  })
1505
1580
  }),
@@ -1722,10 +1797,87 @@ async function writePromptFile(filePath, prompt) {
1722
1797
 
1723
1798
  // src/local-tool-runner.ts
1724
1799
  import { spawn } from "node:child_process";
1725
- import { mkdtemp, rm, writeFile as writeFile4 } from "node:fs/promises";
1800
+ import { mkdtemp, readFile as readFile3, rm, writeFile as writeFile4 } from "node:fs/promises";
1726
1801
  import os2 from "node:os";
1727
1802
  import path5 from "node:path";
1728
1803
  var localToolNames = runnerToolNames;
1804
+ var allReasoningEfforts = ["auto", "low", "medium", "high", "xhigh"];
1805
+ var highReasoningEfforts = ["auto", "high", "xhigh"];
1806
+ var builtInProviderCatalogs = {
1807
+ opencode: {
1808
+ anthropic: {
1809
+ id: "anthropic",
1810
+ name: "Anthropic",
1811
+ api: "anthropic",
1812
+ models: {
1813
+ "claude-opus-4.6": reasoningModel("claude-opus-4.6", "Claude Opus 4.6", "claude", highReasoningEfforts),
1814
+ "claude-sonnet-4.5": reasoningModel("claude-sonnet-4.5", "Claude Sonnet 4.5", "claude", highReasoningEfforts)
1815
+ }
1816
+ },
1817
+ openai: {
1818
+ id: "openai",
1819
+ name: "OpenAI",
1820
+ api: "openai",
1821
+ models: {
1822
+ "gpt-5": reasoningModel("gpt-5", "GPT-5", "gpt", allReasoningEfforts)
1823
+ }
1824
+ }
1825
+ },
1826
+ claude: {
1827
+ anthropic: {
1828
+ id: "anthropic",
1829
+ name: "Anthropic",
1830
+ api: "anthropic",
1831
+ models: {
1832
+ "claude-opus-4.6": reasoningModel("claude-opus-4.6", "Claude Opus 4.6", "claude", highReasoningEfforts),
1833
+ "claude-sonnet-4.5": reasoningModel("claude-sonnet-4.5", "Claude Sonnet 4.5", "claude", highReasoningEfforts)
1834
+ }
1835
+ }
1836
+ },
1837
+ codex: {
1838
+ openai: {
1839
+ id: "openai",
1840
+ name: "OpenAI",
1841
+ api: "openai",
1842
+ models: {
1843
+ "gpt-5": reasoningModel("gpt-5", "GPT-5", "gpt", allReasoningEfforts)
1844
+ }
1845
+ }
1846
+ },
1847
+ copilot: {
1848
+ "github-copilot": {
1849
+ id: "github-copilot",
1850
+ name: "GitHub Copilot",
1851
+ api: "copilot",
1852
+ models: {
1853
+ "gpt-5": reasoningModel("gpt-5", "GPT-5", "gpt", allReasoningEfforts),
1854
+ "claude-opus-4.6": reasoningModel("claude-opus-4.6", "Claude Opus 4.6", "claude", highReasoningEfforts)
1855
+ }
1856
+ }
1857
+ },
1858
+ gemini: {
1859
+ google: {
1860
+ id: "google",
1861
+ name: "Google",
1862
+ api: "google",
1863
+ models: {
1864
+ "gemini-3-pro": reasoningModel("gemini-3-pro", "Gemini 3 Pro", "gemini", highReasoningEfforts)
1865
+ }
1866
+ }
1867
+ }
1868
+ };
1869
+ function reasoningModel(id, name, family, supportedReasoningEfforts) {
1870
+ return {
1871
+ id,
1872
+ name,
1873
+ family,
1874
+ status: "active",
1875
+ reasoning: true,
1876
+ toolCall: true,
1877
+ supportedReasoningEfforts,
1878
+ variants: { standard: {} }
1879
+ };
1880
+ }
1729
1881
  var localToolAdapters = [
1730
1882
  {
1731
1883
  name: "opencode",
@@ -1734,13 +1886,15 @@ var localToolAdapters = [
1734
1886
  sdkDisplayCommand: "@opencode-ai/sdk createOpencode().client.session.prompt()",
1735
1887
  sdkRequiresExecutable: true,
1736
1888
  executable: "opencode",
1889
+ supportsModelSelection: true,
1737
1890
  supportsSessionReuse: true,
1738
1891
  resumabilityScope: "localMachine",
1892
+ providerCatalog: builtInProviderCatalogs.opencode,
1739
1893
  runWithSdk: runOpencodeSdk,
1740
- buildInvocation: ({ prompt }) => ({
1894
+ buildInvocation: ({ prompt, model }) => ({
1741
1895
  command: "opencode",
1742
- args: ["run", prompt],
1743
- displayCommand: "opencode run <generated prompt>"
1896
+ args: ["run", ...model ? ["--model", model] : [], prompt],
1897
+ displayCommand: model ? "opencode run --model <selected model> <generated prompt>" : "opencode run <generated prompt>"
1744
1898
  })
1745
1899
  },
1746
1900
  {
@@ -1749,13 +1903,15 @@ var localToolAdapters = [
1749
1903
  sdkPackageName: "@anthropic-ai/claude-agent-sdk",
1750
1904
  sdkDisplayCommand: "@anthropic-ai/claude-agent-sdk query()",
1751
1905
  executable: "claude",
1906
+ supportsModelSelection: true,
1752
1907
  supportsSessionReuse: false,
1753
1908
  resumabilityScope: "none",
1909
+ providerCatalog: builtInProviderCatalogs.claude,
1754
1910
  runWithSdk: runClaudeSdk,
1755
- buildInvocation: ({ prompt }) => ({
1911
+ buildInvocation: ({ prompt, model }) => ({
1756
1912
  command: "claude",
1757
- args: ["-p", prompt],
1758
- displayCommand: "claude -p <generated prompt>"
1913
+ args: [...model ? ["--model", model] : [], "-p", prompt],
1914
+ displayCommand: model ? "claude --model <selected model> -p <generated prompt>" : "claude -p <generated prompt>"
1759
1915
  })
1760
1916
  },
1761
1917
  {
@@ -1764,13 +1920,15 @@ var localToolAdapters = [
1764
1920
  sdkPackageName: "@openai/codex-sdk",
1765
1921
  sdkDisplayCommand: "@openai/codex-sdk Codex.startThread().run()",
1766
1922
  executable: "codex",
1923
+ supportsModelSelection: true,
1767
1924
  supportsSessionReuse: false,
1768
1925
  resumabilityScope: "none",
1926
+ providerCatalog: builtInProviderCatalogs.codex,
1769
1927
  runWithSdk: runCodexSdk,
1770
- buildInvocation: ({ prompt }) => ({
1928
+ buildInvocation: ({ prompt, model }) => ({
1771
1929
  command: "codex",
1772
- args: ["exec", prompt],
1773
- displayCommand: "codex exec <generated prompt>"
1930
+ args: ["exec", ...model ? ["--model", model] : [], prompt],
1931
+ displayCommand: model ? "codex exec --model <selected model> <generated prompt>" : "codex exec <generated prompt>"
1774
1932
  })
1775
1933
  },
1776
1934
  {
@@ -1781,18 +1939,21 @@ var localToolAdapters = [
1781
1939
  supportsModelSelection: true,
1782
1940
  supportsSessionReuse: false,
1783
1941
  resumabilityScope: "none",
1942
+ providerCatalog: builtInProviderCatalogs.copilot,
1784
1943
  runWithSdk: runCopilotSdk
1785
1944
  },
1786
1945
  {
1787
1946
  name: "gemini",
1788
1947
  description: "Gemini CLI adapter using prompt mode.",
1789
1948
  executable: "gemini",
1949
+ supportsModelSelection: true,
1790
1950
  supportsSessionReuse: false,
1791
1951
  resumabilityScope: "none",
1792
- buildInvocation: ({ prompt }) => ({
1952
+ providerCatalog: builtInProviderCatalogs.gemini,
1953
+ buildInvocation: ({ prompt, model }) => ({
1793
1954
  command: "gemini",
1794
- args: ["-p", prompt],
1795
- displayCommand: "gemini -p <generated prompt>"
1955
+ args: [...model ? ["--model", model] : [], "-p", prompt],
1956
+ displayCommand: model ? "gemini --model <selected model> -p <generated prompt>" : "gemini -p <generated prompt>"
1796
1957
  })
1797
1958
  },
1798
1959
  {
@@ -1828,6 +1989,7 @@ async function detectLocalTools() {
1828
1989
  localToolAdapters.map(async (adapter) => {
1829
1990
  const sdkAvailable = await isSdkAvailable(adapter);
1830
1991
  const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
1992
+ const providerCatalog = await detectProviderCatalog(adapter);
1831
1993
  return {
1832
1994
  name: adapter.name,
1833
1995
  description: adapter.description,
@@ -1837,7 +1999,8 @@ async function detectLocalTools() {
1837
1999
  execution: sdkAvailable ? "sdk" : commandAvailable ? "command" : "unavailable",
1838
2000
  supportsSessionReuse: Boolean(adapter.supportsSessionReuse),
1839
2001
  resumabilityScope: adapter.resumabilityScope ?? "none",
1840
- supportsModelSelection: Boolean(adapter.supportsModelSelection)
2002
+ supportsModelSelection: Boolean(adapter.supportsModelSelection),
2003
+ ...providerCatalog ? { providerCatalog } : {}
1841
2004
  };
1842
2005
  })
1843
2006
  );
@@ -1846,6 +2009,7 @@ async function runLocalTool(options) {
1846
2009
  const promptTempDir = await mkdtemp(path5.join(os2.tmpdir(), "amistio-prompt-"));
1847
2010
  const promptFilePath = path5.join(promptTempDir, "prompt.md");
1848
2011
  await writeFile4(promptFilePath, options.prompt, "utf8");
2012
+ const modelConfig = normalizeModelOptions(options);
1849
2013
  try {
1850
2014
  const runnerOptions = {
1851
2015
  rootDir: options.rootDir,
@@ -1853,7 +2017,7 @@ async function runLocalTool(options) {
1853
2017
  promptFilePath,
1854
2018
  tool: options.tool ?? "auto",
1855
2019
  invocationChannel: options.invocationChannel ?? "auto",
1856
- ...options.model ? { model: options.model } : {}
2020
+ ...modelConfig
1857
2021
  };
1858
2022
  if (options.toolCommand) {
1859
2023
  runnerOptions.toolCommand = options.toolCommand;
@@ -1864,14 +2028,15 @@ async function runLocalTool(options) {
1864
2028
  prompt: options.prompt,
1865
2029
  promptFilePath,
1866
2030
  streamOutput: Boolean(options.streamOutput),
2031
+ ...modelConfig,
1867
2032
  ...options.session ? { session: options.session } : {}
1868
- });
2033
+ }, options.timeoutMs);
1869
2034
  return {
1870
2035
  toolName: runner2.toolName,
1871
2036
  displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
1872
2037
  supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
1873
2038
  resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
1874
- ...options.model ? { model: options.model } : {},
2039
+ ...modelConfig,
1875
2040
  ...result
1876
2041
  };
1877
2042
  } finally {
@@ -1880,13 +2045,14 @@ async function runLocalTool(options) {
1880
2045
  }
1881
2046
  async function createToolRunPreview(options) {
1882
2047
  const promptFilePath = path5.join(os2.tmpdir(), "amistio-generated-prompt.md");
2048
+ const modelConfig = normalizeModelOptions(options);
1883
2049
  const runnerOptions = {
1884
2050
  rootDir: options.rootDir,
1885
2051
  prompt: options.prompt,
1886
2052
  promptFilePath,
1887
2053
  tool: options.tool ?? "auto",
1888
2054
  invocationChannel: options.invocationChannel ?? "auto",
1889
- ...options.model ? { model: options.model } : {}
2055
+ ...modelConfig
1890
2056
  };
1891
2057
  if (options.toolCommand) {
1892
2058
  runnerOptions.toolCommand = options.toolCommand;
@@ -1897,7 +2063,20 @@ async function createToolRunPreview(options) {
1897
2063
  displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
1898
2064
  supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
1899
2065
  resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
1900
- ...options.model ? { model: options.model } : {}
2066
+ ...modelConfig
2067
+ };
2068
+ }
2069
+ function normalizeModelOptions(options) {
2070
+ const model = options.model?.trim() || (options.providerId?.trim() && options.modelId?.trim() ? `${options.providerId.trim()}/${options.modelId.trim()}` : options.modelId?.trim()) || void 0;
2071
+ const providerId = options.providerId?.trim();
2072
+ const modelId = options.modelId?.trim();
2073
+ const modelVariant = options.modelVariant?.trim();
2074
+ return {
2075
+ ...model ? { model } : {},
2076
+ ...providerId ? { providerId } : {},
2077
+ ...modelId ? { modelId } : {},
2078
+ ...modelVariant ? { modelVariant } : {},
2079
+ ...options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}
1901
2080
  };
1902
2081
  }
1903
2082
  function createCustomToolInvocation(commandTemplate, input) {
@@ -1922,9 +2101,13 @@ async function createToolRunner(options) {
1922
2101
  if (tool === "none") {
1923
2102
  throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
1924
2103
  }
1925
- const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model), options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
1926
- if (options.model && !adapter.supportsModelSelection) {
1927
- throw new Error(`Model selection is not supported by ${adapter.name}. Remove --model or choose a model-aware adapter.`);
2104
+ const requiresModelSelection = Boolean(options.model || options.providerId || options.modelId || options.modelVariant || options.reasoningEffort && options.reasoningEffort !== "auto");
2105
+ if (requiresModelSelection && !options.model) {
2106
+ throw new Error("Provider-backed model configuration requires --model or --provider with --model-id.");
2107
+ }
2108
+ const adapter = tool === "auto" ? await selectFirstAvailableAdapter(requiresModelSelection, options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
2109
+ if (requiresModelSelection && !adapter.supportsModelSelection) {
2110
+ throw new Error(`Model selection is not supported by ${adapter.name}. Remove model configuration or choose a model-aware adapter.`);
1928
2111
  }
1929
2112
  if (options.invocationChannel !== "command" && adapter.runWithSdk && await isSdkAvailable(adapter)) {
1930
2113
  return {
@@ -1950,16 +2133,16 @@ async function createToolRunner(options) {
1950
2133
  }
1951
2134
  throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
1952
2135
  }
1953
- async function executeToolRunner(runner2, input) {
2136
+ async function executeToolRunner(runner2, input, timeoutMs) {
1954
2137
  if (runner2.kind === "command") {
1955
- return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput);
2138
+ return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput, timeoutMs);
1956
2139
  }
1957
2140
  try {
1958
- return await runner2.adapter.runWithSdk(input);
2141
+ return await withTimeout(runner2.adapter.runWithSdk(input), timeoutMs, runner2.displayCommand);
1959
2142
  } catch (error) {
1960
2143
  if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
1961
2144
  const fallback = runner2.adapter.buildInvocation(input);
1962
- const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
2145
+ const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput, timeoutMs);
1963
2146
  const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
1964
2147
  return {
1965
2148
  ...result,
@@ -2048,7 +2231,148 @@ async function commandExists(command) {
2048
2231
  lookup.on("close", (exitCode) => resolve(exitCode === 0));
2049
2232
  });
2050
2233
  }
2051
- async function executeToolInvocation(invocation, rootDir, streamOutput) {
2234
+ async function detectProviderCatalog(adapter) {
2235
+ const opencodeCatalog = adapter.name === "opencode" ? await loadOpencodeProviderCatalog() : void 0;
2236
+ return mergeProviderCatalogs(adapter.providerCatalog, opencodeCatalog);
2237
+ }
2238
+ async function loadOpencodeProviderCatalog() {
2239
+ const configPaths = [
2240
+ path5.join(os2.homedir(), ".config", "opencode", "opencode.json"),
2241
+ path5.join(os2.homedir(), ".config", "opencode", "config.json"),
2242
+ path5.join(process.cwd(), "opencode.json")
2243
+ ];
2244
+ for (const configPath of configPaths) {
2245
+ try {
2246
+ const parsed = JSON.parse(await readFile3(configPath, "utf8"));
2247
+ const providerValue = isRecord(parsed) ? parsed.provider : void 0;
2248
+ const catalog = sanitizeProviderCatalog(providerValue);
2249
+ if (catalog) return catalog;
2250
+ } catch {
2251
+ }
2252
+ }
2253
+ return void 0;
2254
+ }
2255
+ function mergeProviderCatalogs(...catalogs) {
2256
+ const merged = {};
2257
+ for (const catalog of catalogs) {
2258
+ if (!catalog) continue;
2259
+ for (const [providerId, provider] of Object.entries(catalog)) {
2260
+ const existing = isRecord(merged[providerId]) ? merged[providerId] : void 0;
2261
+ const existingModels = existing && isRecord(existing.models) ? existing.models : {};
2262
+ merged[providerId] = {
2263
+ ...existing,
2264
+ ...provider,
2265
+ id: provider.id ?? providerId,
2266
+ models: { ...existingModels, ...provider.models }
2267
+ };
2268
+ }
2269
+ }
2270
+ const parsed = runnerProviderCatalogSchema.safeParse(merged);
2271
+ return parsed.success && Object.keys(parsed.data).length ? parsed.data : void 0;
2272
+ }
2273
+ function sanitizeProviderCatalog(value) {
2274
+ if (!isRecord(value)) return void 0;
2275
+ const catalog = {};
2276
+ for (const [providerId, providerValue] of Object.entries(value)) {
2277
+ const provider = sanitizeProviderConfig(providerId, providerValue);
2278
+ if (provider) catalog[providerId] = provider;
2279
+ }
2280
+ const parsed = runnerProviderCatalogSchema.safeParse(catalog);
2281
+ return parsed.success && Object.keys(parsed.data).length ? parsed.data : void 0;
2282
+ }
2283
+ function sanitizeProviderConfig(providerId, value) {
2284
+ if (!isRecord(value)) return void 0;
2285
+ const models = sanitizeProviderModels(value.models);
2286
+ if (!Object.keys(models).length) return void 0;
2287
+ return {
2288
+ id: stringValue(value.id) ?? providerId,
2289
+ ...stringValue(value.name) ? { name: stringValue(value.name) } : {},
2290
+ ...stringValue(value.api) ? { api: stringValue(value.api) } : {},
2291
+ models
2292
+ };
2293
+ }
2294
+ function sanitizeProviderModels(value) {
2295
+ if (!isRecord(value)) return {};
2296
+ const models = {};
2297
+ for (const [modelId, modelValue] of Object.entries(value)) {
2298
+ const model = sanitizeProviderModel(modelId, modelValue);
2299
+ if (model) models[modelId] = model;
2300
+ }
2301
+ return models;
2302
+ }
2303
+ function sanitizeProviderModel(modelId, value) {
2304
+ if (!isRecord(value)) return void 0;
2305
+ const reasoning = booleanValue(value.reasoning);
2306
+ const releaseDate = stringValue(value.release_date) ?? stringValue(value.releaseDate);
2307
+ const toolCall = booleanValue(value.tool_call) ?? booleanValue(value.toolCall);
2308
+ const limit = sanitizeLimit(value.limit);
2309
+ const modalities = sanitizeModalities(value.modalities);
2310
+ const variants = sanitizeVariants(value.variants);
2311
+ const model = {
2312
+ id: stringValue(value.id) ?? modelId,
2313
+ ...stringValue(value.name) ? { name: stringValue(value.name) } : {},
2314
+ ...stringValue(value.family) ? { family: stringValue(value.family) } : {},
2315
+ ...releaseDate ? { releaseDate } : {},
2316
+ ...booleanValue(value.attachment) !== void 0 ? { attachment: booleanValue(value.attachment) } : {},
2317
+ ...reasoning !== void 0 ? { reasoning } : {},
2318
+ ...booleanValue(value.temperature) !== void 0 ? { temperature: booleanValue(value.temperature) } : {},
2319
+ ...toolCall !== void 0 ? { toolCall } : {},
2320
+ ...stringValue(value.status) ? { status: stringValue(value.status) } : {},
2321
+ ...limit ? { limit } : {},
2322
+ ...modalities ? { modalities } : {},
2323
+ ...variants ? { variants } : {}
2324
+ };
2325
+ const supportedReasoningEfforts = sanitizeReasoningEfforts(value.supportedReasoningEfforts) ?? sanitizeReasoningEfforts(value.supported_reasoning_efforts) ?? (reasoning ? allReasoningEfforts : void 0);
2326
+ if (supportedReasoningEfforts) model.supportedReasoningEfforts = supportedReasoningEfforts;
2327
+ return model;
2328
+ }
2329
+ function sanitizeLimit(value) {
2330
+ if (!isRecord(value)) return void 0;
2331
+ const limit = {
2332
+ ...numberValue(value.context) !== void 0 ? { context: numberValue(value.context) } : {},
2333
+ ...numberValue(value.input) !== void 0 ? { input: numberValue(value.input) } : {},
2334
+ ...numberValue(value.output) !== void 0 ? { output: numberValue(value.output) } : {}
2335
+ };
2336
+ return Object.keys(limit).length ? limit : void 0;
2337
+ }
2338
+ function sanitizeModalities(value) {
2339
+ if (!isRecord(value)) return void 0;
2340
+ const modalities = {
2341
+ ...stringArrayValue(value.input) ? { input: stringArrayValue(value.input) } : {},
2342
+ ...stringArrayValue(value.output) ? { output: stringArrayValue(value.output) } : {}
2343
+ };
2344
+ return Object.keys(modalities).length ? modalities : void 0;
2345
+ }
2346
+ function sanitizeVariants(value) {
2347
+ if (!isRecord(value)) return void 0;
2348
+ const variants = {};
2349
+ for (const [variantId, variantValue] of Object.entries(value)) {
2350
+ variants[variantId] = isRecord(variantValue) && booleanValue(variantValue.disabled) !== void 0 ? { disabled: booleanValue(variantValue.disabled) } : {};
2351
+ }
2352
+ return Object.keys(variants).length ? variants : void 0;
2353
+ }
2354
+ function sanitizeReasoningEfforts(value) {
2355
+ const values = stringArrayValue(value);
2356
+ if (!values) return void 0;
2357
+ const efforts = values.filter((candidate) => allReasoningEfforts.includes(candidate));
2358
+ return efforts.length ? efforts : void 0;
2359
+ }
2360
+ function isRecord(value) {
2361
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2362
+ }
2363
+ function stringValue(value) {
2364
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2365
+ }
2366
+ function numberValue(value) {
2367
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2368
+ }
2369
+ function booleanValue(value) {
2370
+ return typeof value === "boolean" ? value : void 0;
2371
+ }
2372
+ function stringArrayValue(value) {
2373
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
2374
+ }
2375
+ async function executeToolInvocation(invocation, rootDir, streamOutput, timeoutMs) {
2052
2376
  return new Promise((resolve, reject) => {
2053
2377
  const child = spawn(invocation.command, invocation.args, {
2054
2378
  cwd: rootDir,
@@ -2058,7 +2382,33 @@ async function executeToolInvocation(invocation, rootDir, streamOutput) {
2058
2382
  });
2059
2383
  let stdout = "";
2060
2384
  let stderr = "";
2061
- child.on("error", reject);
2385
+ let settled = false;
2386
+ let forceKillTimer;
2387
+ const timeout = timeoutMs && timeoutMs > 0 ? setTimeout(() => {
2388
+ if (settled) return;
2389
+ stderr += `${toolTimeoutMessage(invocation.displayCommand, timeoutMs)}
2390
+ `;
2391
+ child.kill("SIGTERM");
2392
+ forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
2393
+ forceKillTimer.unref?.();
2394
+ rejectOnce(new Error(toolTimeoutMessage(invocation.displayCommand, timeoutMs)));
2395
+ }, timeoutMs) : void 0;
2396
+ timeout?.unref?.();
2397
+ const resolveOnce = (value) => {
2398
+ if (settled) return;
2399
+ settled = true;
2400
+ if (timeout) clearTimeout(timeout);
2401
+ if (forceKillTimer) clearTimeout(forceKillTimer);
2402
+ resolve(value);
2403
+ };
2404
+ const rejectOnce = (error) => {
2405
+ if (settled) return;
2406
+ settled = true;
2407
+ if (timeout) clearTimeout(timeout);
2408
+ if (forceKillTimer) clearTimeout(forceKillTimer);
2409
+ reject(error);
2410
+ };
2411
+ child.on("error", rejectOnce);
2062
2412
  child.stdout.setEncoding("utf8");
2063
2413
  child.stderr.setEncoding("utf8");
2064
2414
  child.stdout.on("data", (chunk) => {
@@ -2079,10 +2429,40 @@ async function executeToolInvocation(invocation, rootDir, streamOutput) {
2079
2429
  }
2080
2430
  child.stdin.end();
2081
2431
  child.on("close", (exitCode) => {
2082
- resolve({ exitCode: exitCode ?? 1, stdout, stderr });
2432
+ if (forceKillTimer) clearTimeout(forceKillTimer);
2433
+ resolveOnce({ exitCode: exitCode ?? 1, stdout, stderr });
2083
2434
  });
2084
2435
  });
2085
2436
  }
2437
+ async function withTimeout(promise, timeoutMs, displayCommand) {
2438
+ if (!timeoutMs || timeoutMs <= 0) {
2439
+ return promise;
2440
+ }
2441
+ return new Promise((resolve, reject) => {
2442
+ const timeout = setTimeout(() => reject(new Error(toolTimeoutMessage(displayCommand, timeoutMs))), timeoutMs);
2443
+ timeout.unref?.();
2444
+ promise.then(
2445
+ (value) => {
2446
+ clearTimeout(timeout);
2447
+ resolve(value);
2448
+ },
2449
+ (error) => {
2450
+ clearTimeout(timeout);
2451
+ reject(error);
2452
+ }
2453
+ );
2454
+ });
2455
+ }
2456
+ function toolTimeoutMessage(displayCommand, timeoutMs) {
2457
+ return `Local tool timed out after ${formatTimeoutDuration(timeoutMs)}: ${displayCommand}`;
2458
+ }
2459
+ function formatTimeoutDuration(timeoutMs) {
2460
+ if (timeoutMs < 1e3) {
2461
+ return `${timeoutMs}ms`;
2462
+ }
2463
+ const seconds = timeoutMs / 1e3;
2464
+ return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
2465
+ }
2086
2466
  async function runOpencodeSdk(input) {
2087
2467
  const { createOpencode } = await import("@opencode-ai/sdk");
2088
2468
  const previousDirectory = process.cwd();
@@ -2170,7 +2550,7 @@ async function runCodexSdk(input) {
2170
2550
  return { exitCode: 0, stdout: result.finalResponse, stderr: "" };
2171
2551
  }
2172
2552
  async function runCopilotSdk(input) {
2173
- const { CopilotClient, approveAll } = await import("@github/copilot-sdk");
2553
+ const { CopilotClient } = await import("@github/copilot-sdk");
2174
2554
  const client = new CopilotClient({
2175
2555
  cwd: input.rootDir,
2176
2556
  logLevel: "error"
@@ -2183,7 +2563,7 @@ async function runCopilotSdk(input) {
2183
2563
  workingDirectory: input.rootDir,
2184
2564
  enableConfigDiscovery: true,
2185
2565
  streaming: input.streamOutput,
2186
- onPermissionRequest: approveAll
2566
+ onPermissionRequest: createCopilotPermissionHandler()
2187
2567
  });
2188
2568
  try {
2189
2569
  let streamedOutput = "";
@@ -2205,15 +2585,24 @@ async function runCopilotSdk(input) {
2205
2585
  await client.stop();
2206
2586
  }
2207
2587
  }
2588
+ function createCopilotPermissionHandler(env = process.env) {
2589
+ const allowAllPermissions = isCopilotApproveAllEnabled(env);
2590
+ return (request) => {
2591
+ if (allowAllPermissions || request.kind === "read") {
2592
+ return { kind: "approve-once" };
2593
+ }
2594
+ return { kind: "reject" };
2595
+ };
2596
+ }
2597
+ function isCopilotApproveAllEnabled(env = process.env) {
2598
+ return /^(1|true|yes)$/i.test(env.AMISTIO_COPILOT_APPROVE_ALL ?? "");
2599
+ }
2208
2600
  function extractTextParts(parts) {
2209
2601
  if (!Array.isArray(parts)) {
2210
2602
  return "";
2211
2603
  }
2212
2604
  return parts.filter(isRecord).map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
2213
2605
  }
2214
- function isRecord(value) {
2215
- return typeof value === "object" && value !== null;
2216
- }
2217
2606
  function errorMessage(error) {
2218
2607
  return error instanceof Error ? error.message : String(error);
2219
2608
  }
@@ -2225,7 +2614,7 @@ function shellQuote(value) {
2225
2614
  import { spawn as spawn2 } from "node:child_process";
2226
2615
  import { createHash as createHash2 } from "node:crypto";
2227
2616
  import { openSync } from "node:fs";
2228
- import { mkdir as mkdir5, readdir as readdir2, readFile as readFile3, writeFile as writeFile5 } from "node:fs/promises";
2617
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
2229
2618
  import os3 from "node:os";
2230
2619
  import path6 from "node:path";
2231
2620
  function currentRunnerMode() {
@@ -2392,7 +2781,7 @@ function runnerDaemonKey(input) {
2392
2781
  }
2393
2782
  async function readRunnerDaemonMetadataFile(filePath) {
2394
2783
  try {
2395
- const parsed = JSON.parse(await readFile3(filePath, "utf8"));
2784
+ const parsed = JSON.parse(await readFile4(filePath, "utf8"));
2396
2785
  if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
2397
2786
  return void 0;
2398
2787
  }
@@ -2405,7 +2794,7 @@ async function readRunnerDaemonMetadataFile(filePath) {
2405
2794
  // src/runner-service.ts
2406
2795
  import { spawn as spawn3 } from "node:child_process";
2407
2796
  import { createHash as createHash3 } from "node:crypto";
2408
- import { mkdir as mkdir6, readFile as readFile4, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
2797
+ import { mkdir as mkdir6, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
2409
2798
  import os4 from "node:os";
2410
2799
  import path7 from "node:path";
2411
2800
  function detectRunnerServicePlatform(platform = process.platform) {
@@ -2471,7 +2860,7 @@ async function removeRunnerService(input) {
2471
2860
  }
2472
2861
  async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
2473
2862
  try {
2474
- const parsed = JSON.parse(await readFile4(runnerServiceMetadataPath(input, metadataDir), "utf8"));
2863
+ const parsed = JSON.parse(await readFile5(runnerServiceMetadataPath(input, metadataDir), "utf8"));
2475
2864
  if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
2476
2865
  return void 0;
2477
2866
  }
@@ -2751,10 +3140,28 @@ function tokens(value) {
2751
3140
  }
2752
3141
 
2753
3142
  // src/sync.ts
2754
- import { mkdir as mkdir7, readdir as readdir3, readFile as readFile5, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
3143
+ import { execFile as execFile3 } from "node:child_process";
3144
+ import { createHash as createHash4 } from "node:crypto";
3145
+ import { mkdir as mkdir7, readdir as readdir3, readFile as readFile6, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
2755
3146
  import path8 from "node:path";
3147
+ import { promisify as promisify3 } from "node:util";
3148
+ var execFileAsync3 = promisify3(execFile3);
2756
3149
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
2757
3150
  var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
3151
+ var documentTypeByRoot = {
3152
+ architecture: "architecture",
3153
+ context: "context",
3154
+ decisions: "decision",
3155
+ features: "feature",
3156
+ memory: "memory",
3157
+ plans: "plan",
3158
+ prompts: "prompt",
3159
+ workflows: "workflow"
3160
+ };
3161
+ var autoSyncMetadataPaths = /* @__PURE__ */ new Set(["docs/context/amistio-project.md", "context/amistio-project.md"]);
3162
+ var autoSyncExcludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
3163
+ var autoSyncGeneratedPathSegments = /* @__PURE__ */ new Set(["generated", "__generated__", "vendor", "vendors"]);
3164
+ var defaultAutoSyncMaxFileKb = 256;
2758
3165
  async function collectSyncStatus(rootDir, webDocuments = []) {
2759
3166
  const localDocuments = await readLocalSyncedDocuments(rootDir);
2760
3167
  const normalizedWebDocuments = webDocuments.map((document) => ({ ...document, repoPath: canonicalControlPlaneRepoPath(document.repoPath) }));
@@ -2831,7 +3238,7 @@ async function readLocalSyncedDocuments(rootDir) {
2831
3238
  const markdownFiles = await findMarkdownFiles(rootDir);
2832
3239
  const documents = [];
2833
3240
  for (const fullPath of markdownFiles) {
2834
- const raw = await readFile5(fullPath, "utf8");
3241
+ const raw = await readFile6(fullPath, "utf8");
2835
3242
  const parsed = parseSyncedMarkdown(raw);
2836
3243
  if (!parsed) {
2837
3244
  continue;
@@ -2914,6 +3321,29 @@ async function collectDirtyDocumentsForPush(rootDir, metadata) {
2914
3321
  });
2915
3322
  });
2916
3323
  }
3324
+ async function collectAutoSyncDocumentsForPush(rootDir, metadata, existingDocuments = [], options = {}) {
3325
+ const dirtyDocuments = await collectDirtyDocumentsForPush(rootDir, metadata);
3326
+ const external = await collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options);
3327
+ return {
3328
+ documents: [...dirtyDocuments, ...external.documents],
3329
+ managedDocumentIds: dirtyDocuments.map((document) => document.documentId),
3330
+ skipped: external.skipped,
3331
+ counts: autoSyncSkipCounts(external.skipped)
3332
+ };
3333
+ }
3334
+ function autoSyncSkipCounts(skipped) {
3335
+ return {
3336
+ metadata: skipped.filter((item) => item.reason === "metadata").length,
3337
+ template: skipped.filter((item) => item.reason === "template").length,
3338
+ unsupported: skipped.filter((item) => item.reason === "unsupported").length,
3339
+ tooLarge: skipped.filter((item) => item.reason === "tooLarge").length,
3340
+ alreadyManaged: skipped.filter((item) => item.reason === "alreadyManaged").length,
3341
+ unchanged: skipped.filter((item) => item.reason === "unchanged").length,
3342
+ conflicted: skipped.filter((item) => item.reason === "conflicted").length,
3343
+ excluded: skipped.filter((item) => item.reason === "excluded").length,
3344
+ unreadable: skipped.filter((item) => item.reason === "unreadable").length
3345
+ };
3346
+ }
2917
3347
  function createSyncedDocumentMarkdown(document) {
2918
3348
  return [
2919
3349
  "---",
@@ -2943,7 +3373,7 @@ function parseSyncedMarkdown(content) {
2943
3373
  }
2944
3374
  async function readExistingSyncedDocument(fullPath) {
2945
3375
  try {
2946
- const raw = await readFile5(fullPath, "utf8");
3376
+ const raw = await readFile6(fullPath, "utf8");
2947
3377
  const parsed = parseSyncedMarkdown(raw);
2948
3378
  if (!parsed) {
2949
3379
  return { exists: true };
@@ -3020,6 +3450,145 @@ function inferTitle(content, repoPath) {
3020
3450
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
3021
3451
  return heading || path8.basename(repoPath, path8.extname(repoPath));
3022
3452
  }
3453
+ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
3454
+ const root = path8.resolve(rootDir);
3455
+ const maxBytes = (options.maxFileKb ?? defaultAutoSyncMaxFileKb) * 1024;
3456
+ const syncedAt = options.syncedAt ?? (/* @__PURE__ */ new Date()).toISOString();
3457
+ const existingById = new Map(existingDocuments.map((document) => [document.documentId, document]));
3458
+ const repoPaths = await listAutoSyncCandidatePaths(root);
3459
+ const documents = [];
3460
+ const skipped = [];
3461
+ for (const repoPath of repoPaths) {
3462
+ const normalizedRepoPath = normalizeRepoPath3(repoPath);
3463
+ const canonicalRepoPath = canonicalControlPlaneRepoPath(normalizedRepoPath);
3464
+ const skipReason = autoSyncPathSkipReason(normalizedRepoPath);
3465
+ if (skipReason) {
3466
+ skipped.push({ repoPath: normalizedRepoPath, reason: skipReason });
3467
+ continue;
3468
+ }
3469
+ const fullPath = safeRepoPath(root, normalizedRepoPath);
3470
+ const fileStat = await stat3(fullPath).catch(() => void 0);
3471
+ if (!fileStat?.isFile()) {
3472
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
3473
+ continue;
3474
+ }
3475
+ if (fileStat.size > maxBytes) {
3476
+ skipped.push({ repoPath: normalizedRepoPath, reason: "tooLarge" });
3477
+ continue;
3478
+ }
3479
+ const content = await readFile6(fullPath, "utf8").catch(() => void 0);
3480
+ if (content === void 0) {
3481
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
3482
+ continue;
3483
+ }
3484
+ if (parseSyncedMarkdown(content)) {
3485
+ skipped.push({ repoPath: normalizedRepoPath, reason: "alreadyManaged" });
3486
+ continue;
3487
+ }
3488
+ const documentType = documentTypeForRepoPath(canonicalRepoPath);
3489
+ if (!documentType) {
3490
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unsupported" });
3491
+ continue;
3492
+ }
3493
+ const contentHash = sha256ContentHash(content);
3494
+ const documentId = stableExternalDocumentId(metadata, canonicalRepoPath);
3495
+ const existing = existingById.get(documentId);
3496
+ if (existing?.syncState === "conflicted" || existing?.status === "conflicted") {
3497
+ skipped.push({ repoPath: normalizedRepoPath, reason: "conflicted" });
3498
+ continue;
3499
+ }
3500
+ const lastAutoSyncedHash = typeof existing?.frontmatter.autoSyncedSourceHash === "string" ? existing.frontmatter.autoSyncedSourceHash : void 0;
3501
+ if (existing?.contentHash === contentHash || lastAutoSyncedHash === contentHash) {
3502
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unchanged" });
3503
+ continue;
3504
+ }
3505
+ const revision = existing ? existing.revision + 1 : 0;
3506
+ documents.push(brainDocumentItemSchema.parse({
3507
+ id: documentId,
3508
+ type: "brainDocument",
3509
+ schemaVersion: 1,
3510
+ accountId: metadata.amistioAccountId,
3511
+ projectId: metadata.amistioProjectId,
3512
+ documentId,
3513
+ documentType,
3514
+ title: inferTitle(content, canonicalRepoPath),
3515
+ status: "reviewing",
3516
+ repoPath: canonicalRepoPath,
3517
+ content,
3518
+ contentHash,
3519
+ frontmatter: {
3520
+ ...existing?.frontmatter ?? {},
3521
+ externalBrainPath: normalizedRepoPath,
3522
+ autoSyncedByCommand: "amistio sync watch",
3523
+ autoSyncedAt: syncedAt,
3524
+ autoSyncedSourceHash: contentHash,
3525
+ ...lastAutoSyncedHash ? { amistioContentHash: lastAutoSyncedHash } : {}
3526
+ },
3527
+ revision,
3528
+ source: "repo",
3529
+ syncState: "dirtyInRepo",
3530
+ createdAt: existing?.createdAt ?? syncedAt,
3531
+ updatedAt: syncedAt
3532
+ }));
3533
+ }
3534
+ return { documents, skipped };
3535
+ }
3536
+ async function listAutoSyncCandidatePaths(rootDir) {
3537
+ const gitFiles = await execFileAsync3("git", ["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).then(({ stdout }) => stdout).catch(() => void 0);
3538
+ if (gitFiles !== void 0) {
3539
+ return uniqueSortedRepoPaths(gitFiles.split("\n").map(normalizeRepoPath3).filter((repoPath) => repoPath && isRecognizedBrainRepoPath(repoPath)));
3540
+ }
3541
+ const files = [];
3542
+ for (const syncRoot of [...syncRoots, ...legacySyncRoots]) {
3543
+ const fullRoot = path8.join(rootDir, syncRoot);
3544
+ if (await exists2(fullRoot)) {
3545
+ await walkAutoSyncFiles(rootDir, fullRoot, files);
3546
+ }
3547
+ }
3548
+ return uniqueSortedRepoPaths(files);
3549
+ }
3550
+ async function walkAutoSyncFiles(rootDir, directory, files) {
3551
+ for (const entry of await readdir3(directory, { withFileTypes: true }).catch(() => [])) {
3552
+ const fullPath = path8.join(directory, entry.name);
3553
+ const repoPath = normalizeRepoPath3(path8.relative(rootDir, fullPath));
3554
+ if (entry.isDirectory()) {
3555
+ if (!autoSyncExcludedDirectoryNames.has(entry.name)) {
3556
+ await walkAutoSyncFiles(rootDir, fullPath, files);
3557
+ }
3558
+ } else if (entry.isFile() && isRecognizedBrainRepoPath(repoPath)) {
3559
+ files.push(repoPath);
3560
+ }
3561
+ }
3562
+ }
3563
+ function autoSyncPathSkipReason(repoPath) {
3564
+ if (autoSyncMetadataPaths.has(repoPath)) return "metadata";
3565
+ if (isControlPlaneTemplateRepoPath(repoPath)) return "template";
3566
+ if (!/\.(md|mdx)$/i.test(repoPath)) return "unsupported";
3567
+ const basename = repoPath.split("/").at(-1)?.toLowerCase() ?? "";
3568
+ if (basename.startsWith(".") || basename.endsWith(".lock") || basename.includes(".env") || basename.includes("secret") || basename.includes("credential")) return "excluded";
3569
+ if (repoPath.split("/").some((segment) => autoSyncExcludedDirectoryNames.has(segment) || autoSyncGeneratedPathSegments.has(segment))) return "excluded";
3570
+ return void 0;
3571
+ }
3572
+ function isRecognizedBrainRepoPath(repoPath) {
3573
+ const normalized = normalizeRepoPath3(repoPath);
3574
+ const [firstSegment, secondSegment] = normalized.split("/");
3575
+ return Boolean(firstSegment === "docs" && secondSegment && legacySyncRoots.includes(secondSegment) || firstSegment && legacySyncRoots.includes(firstSegment));
3576
+ }
3577
+ function documentTypeForRepoPath(repoPath) {
3578
+ const normalized = normalizeRepoPath3(repoPath);
3579
+ const segments = normalized.split("/");
3580
+ const root = segments[0] === "docs" ? segments[1] : segments[0];
3581
+ return root && root in documentTypeByRoot ? documentTypeByRoot[root] : void 0;
3582
+ }
3583
+ function stableExternalDocumentId(metadata, repoPath) {
3584
+ return `doc_external_${createHash4("sha256").update(`${metadata.amistioAccountId}:${metadata.amistioProjectId}:${metadata.repositoryLinkId}:${repoPath}`).digest("hex").slice(0, 24)}`;
3585
+ }
3586
+ function normalizeRepoPath3(repoPath) {
3587
+ return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
3588
+ }
3589
+ function uniqueSortedRepoPaths(repoPaths) {
3590
+ return [...new Set(repoPaths)].sort((first, second) => first.localeCompare(second));
3591
+ }
3023
3592
  function parseFrontmatterFromSyncedDocument(frontmatter) {
3024
3593
  return {
3025
3594
  amistioDocumentId: frontmatter.amistioDocumentId,
@@ -3039,7 +3608,7 @@ async function exists2(filePath) {
3039
3608
  }
3040
3609
 
3041
3610
  // src/tool-session-store.ts
3042
- import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
3611
+ import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
3043
3612
  import os5 from "node:os";
3044
3613
  import path9 from "node:path";
3045
3614
  var LocalToolSessionStore = class {
@@ -3060,7 +3629,7 @@ var LocalToolSessionStore = class {
3060
3629
  }
3061
3630
  async read() {
3062
3631
  try {
3063
- return JSON.parse(await readFile6(this.filePath, "utf8"));
3632
+ return JSON.parse(await readFile7(this.filePath, "utf8"));
3064
3633
  } catch {
3065
3634
  return {};
3066
3635
  }
@@ -3360,7 +3929,7 @@ function stripJsonFence(value) {
3360
3929
  }
3361
3930
 
3362
3931
  // src/runner-status.ts
3363
- import { createHash as createHash4 } from "node:crypto";
3932
+ import { createHash as createHash5 } from "node:crypto";
3364
3933
  var watchStateReminderMs = 60 * 1e3;
3365
3934
  function formatWatchStartupContext(input) {
3366
3935
  return [
@@ -3381,7 +3950,7 @@ function watchStateKey(action) {
3381
3950
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
3382
3951
  }
3383
3952
  function stableRunnerId(input) {
3384
- const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
3953
+ const digest = createHash5("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
3385
3954
  return `runner_${digest}`;
3386
3955
  }
3387
3956
 
@@ -3505,12 +4074,12 @@ function roundNumber(value, digits) {
3505
4074
  }
3506
4075
 
3507
4076
  // src/importer.ts
3508
- import { execFile as execFile3 } from "node:child_process";
3509
- import { createHash as createHash5 } from "node:crypto";
3510
- import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
4077
+ import { execFile as execFile4 } from "node:child_process";
4078
+ import { createHash as createHash6 } from "node:crypto";
4079
+ import { readdir as readdir4, readFile as readFile8, stat as stat4 } from "node:fs/promises";
3511
4080
  import path10 from "node:path";
3512
- import { promisify as promisify3 } from "node:util";
3513
- var execFileAsync3 = promisify3(execFile3);
4081
+ import { promisify as promisify4 } from "node:util";
4082
+ var execFileAsync4 = promisify4(execFile4);
3514
4083
  var defaultMaxFileKb = 256;
3515
4084
  var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
3516
4085
  var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
@@ -3573,7 +4142,7 @@ async function scanLegacyDocuments(options) {
3573
4142
  skipped.push({ repoPath, reason: "tooLarge" });
3574
4143
  continue;
3575
4144
  }
3576
- const content = await readFile7(fullPath, "utf8").catch(() => void 0);
4145
+ const content = await readFile8(fullPath, "utf8").catch(() => void 0);
3577
4146
  if (content === void 0) {
3578
4147
  skipped.push({ repoPath, reason: "unreadable" });
3579
4148
  continue;
@@ -3654,7 +4223,7 @@ function parseOptionalOriginCloneUrl(originUrl) {
3654
4223
  async function listRepositoryPaths(rootDir) {
3655
4224
  const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
3656
4225
  if (gitFiles !== void 0) {
3657
- return gitFiles.split("\n").map((line) => normalizeRepoPath3(line)).filter((line) => line.length > 0);
4226
+ return gitFiles.split("\n").map((line) => normalizeRepoPath4(line)).filter((line) => line.length > 0);
3658
4227
  }
3659
4228
  const files = [];
3660
4229
  await walkRepository(rootDir, rootDir, files);
@@ -3664,7 +4233,7 @@ async function walkRepository(rootDir, directory, files) {
3664
4233
  const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
3665
4234
  for (const entry of entries) {
3666
4235
  const fullPath = path10.join(directory, entry.name);
3667
- const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
4236
+ const repoPath = normalizeRepoPath4(path10.relative(rootDir, fullPath));
3668
4237
  if (entry.isDirectory()) {
3669
4238
  if (!excludedDirectoryNames.has(entry.name)) {
3670
4239
  await walkRepository(rootDir, fullPath, files);
@@ -3684,7 +4253,7 @@ function matchesIncludeExclude(repoPath, include, exclude) {
3684
4253
  return true;
3685
4254
  }
3686
4255
  function wildcardMatch(pattern, repoPath) {
3687
- const normalizedPattern = normalizeRepoPath3(pattern);
4256
+ const normalizedPattern = normalizeRepoPath4(pattern);
3688
4257
  const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "::DOUBLE_STAR_SLASH::").replace(/\*\*/g, "::DOUBLE_STAR::").replace(/\?/g, "[^/]").replace(/\*/g, "[^/]*").replace(/::DOUBLE_STAR_SLASH::/g, "(?:.*/)?").replace(/::DOUBLE_STAR::/g, ".*");
3689
4258
  return new RegExp(`^${escaped}$`).test(repoPath);
3690
4259
  }
@@ -3737,7 +4306,7 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
3737
4306
  return uniquePath;
3738
4307
  }
3739
4308
  function isCanonicalControlPlanePath(repoPath) {
3740
- const [firstSegment, secondSegment] = normalizeRepoPath3(repoPath).split("/");
4309
+ const [firstSegment, secondSegment] = normalizeRepoPath4(repoPath).split("/");
3741
4310
  return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
3742
4311
  }
3743
4312
  function isLegacyControlPlanePath(repoPath) {
@@ -3770,13 +4339,13 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
3770
4339
  return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
3771
4340
  }
3772
4341
  function hashText(value, length) {
3773
- return createHash5("sha256").update(value).digest("hex").slice(0, length);
4342
+ return createHash6("sha256").update(value).digest("hex").slice(0, length);
3774
4343
  }
3775
- function normalizeRepoPath3(value) {
4344
+ function normalizeRepoPath4(value) {
3776
4345
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
3777
4346
  }
3778
4347
  async function runGit2(args) {
3779
- const { stdout } = await execFileAsync3("git", args, { maxBuffer: 10 * 1024 * 1024 });
4348
+ const { stdout } = await execFileAsync4("git", args, { maxBuffer: 10 * 1024 * 1024 });
3780
4349
  return stdout.trim();
3781
4350
  }
3782
4351
 
@@ -3810,9 +4379,24 @@ function buildBackgroundRunnerArgs(options) {
3810
4379
  if (options.model) {
3811
4380
  args.push("--model", options.model);
3812
4381
  }
4382
+ const providerId = options.providerId ?? options.provider;
4383
+ if (providerId) {
4384
+ args.push("--provider", providerId);
4385
+ }
4386
+ if (options.modelId) {
4387
+ args.push("--model-id", options.modelId);
4388
+ }
4389
+ if (options.modelVariant) {
4390
+ args.push("--model-variant", options.modelVariant);
4391
+ }
4392
+ if (options.reasoningEffort) {
4393
+ args.push("--reasoning-effort", options.reasoningEffort);
4394
+ }
3813
4395
  if (options.maxIterations !== void 0) {
3814
4396
  args.push("--max-iterations", String(options.maxIterations));
3815
4397
  }
4398
+ args.push("--max-preflight-attempts", String(options.maxPreflightAttempts));
4399
+ args.push("--tool-timeout-seconds", String(options.toolTimeoutSeconds));
3816
4400
  if (!options.stream) {
3817
4401
  args.push("--no-stream");
3818
4402
  }
@@ -3858,11 +4442,11 @@ function truncateProcessOutput(value) {
3858
4442
  }
3859
4443
 
3860
4444
  // src/git-worktree.ts
3861
- import { execFile as execFile4 } from "node:child_process";
4445
+ import { execFile as execFile5 } from "node:child_process";
3862
4446
  import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
3863
4447
  import path12 from "node:path";
3864
- import { promisify as promisify4 } from "node:util";
3865
- var execFileAsync4 = promisify4(execFile4);
4448
+ import { promisify as promisify5 } from "node:util";
4449
+ var execFileAsync5 = promisify5(execFile5);
3866
4450
  function needsGitWorktreeIsolation(workItem) {
3867
4451
  return (workItem.workKind ?? "implementation") === "implementation";
3868
4452
  }
@@ -3923,11 +4507,11 @@ async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
3923
4507
  }
3924
4508
  }
3925
4509
  async function gitOutput(cwd, args) {
3926
- const { stdout } = await execFileAsync4("git", args, { cwd, maxBuffer: 1024 * 1024 });
4510
+ const { stdout } = await execFileAsync5("git", args, { cwd, maxBuffer: 1024 * 1024 });
3927
4511
  return stdout.trim();
3928
4512
  }
3929
4513
  async function gitCommandSucceeds(cwd, args) {
3930
- return execFileAsync4("git", args, { cwd }).then(() => true, () => false);
4514
+ return execFileAsync5("git", args, { cwd }).then(() => true, () => false);
3931
4515
  }
3932
4516
  async function pathExists(value) {
3933
4517
  return stat5(value).then(() => true, () => false);
@@ -3960,6 +4544,10 @@ var CLI_VERSION = readCliPackageVersion();
3960
4544
  var program = new Command();
3961
4545
  var defaultRoot = process.env.INIT_CWD ?? process.cwd();
3962
4546
  var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
4547
+ var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
4548
+ var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
4549
+ var RUNNER_WORK_LEASE_SECONDS = 300;
4550
+ var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
3963
4551
  program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
3964
4552
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
3965
4553
  const created = await initControlPlane(options.root);
@@ -3986,7 +4574,8 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
3986
4574
  repositoryLinkId: options.repositoryLink,
3987
4575
  repoName: parsedRepoUrl.repoName,
3988
4576
  repoFingerprint: createRepoFingerprint(options.account, options.project, options.repositoryLink),
3989
- defaultBranch: options.defaultBranch
4577
+ defaultBranch: options.defaultBranch,
4578
+ machineId: runnerMachineId()
3990
4579
  });
3991
4580
  const filePath = await writeProjectLink(checkout.targetDir, {
3992
4581
  amistioAccountId: options.account,
@@ -4038,6 +4627,7 @@ program.command("import").description("Pair an existing checkout and import lega
4038
4627
  repoName: repository.repoName,
4039
4628
  repoFingerprint: repository.repoFingerprint,
4040
4629
  defaultBranch: repository.defaultBranch,
4630
+ machineId: runnerMachineId(),
4041
4631
  ...parsedCloneUrl ? { cloneUrl: parsedCloneUrl.cloneUrl } : {},
4042
4632
  ...parsedCloneUrl?.provider ? { provider: parsedCloneUrl.provider } : {},
4043
4633
  ...parsedCloneUrl?.repoOwner ? { repoOwner: parsedCloneUrl.repoOwner } : {},
@@ -4084,7 +4674,8 @@ program.command("pair").description("Pair this repository with an Amistio web pr
4084
4674
  repositoryLinkId,
4085
4675
  repoName: inferRepoName(pairingRoot),
4086
4676
  repoFingerprint: createRepoFingerprint(options.account, options.project, repositoryLinkId),
4087
- defaultBranch: options.defaultBranch
4677
+ defaultBranch: options.defaultBranch,
4678
+ machineId: runnerMachineId()
4088
4679
  });
4089
4680
  repositoryLinkId = pairing.repositoryLink.repositoryLinkId;
4090
4681
  credential = credential ?? pairing.token;
@@ -4175,6 +4766,45 @@ sync.command("push").description("Push local brain changes to Amistio for review
4175
4766
  await materializeBrainDocuments(options.root, documents, { allowDirtyOverwrite: true });
4176
4767
  console.log(`Pushed ${documents.length} local document${documents.length === 1 ? "" : "s"} for web review.`);
4177
4768
  });
4769
+ sync.command("watch").description("Watch repository brain folders and auto-sync eligible local changes").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable watcher runner ID").option("--interval-seconds <seconds>", "Polling interval", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--max-file-kb <kb>", "Maximum Markdown/MDX file size to auto-sync", parsePositiveInteger, 256).option("--once", "Run one auto-sync cycle and exit").action(async (options) => {
4770
+ const context = await loadPairedApiContext(options.root, options.apiUrl);
4771
+ if (!context) {
4772
+ console.log("Repository is not paired. Run `amistio pair` first.");
4773
+ return;
4774
+ }
4775
+ if (!context.token) {
4776
+ console.log("No local runner credential found. Run `amistio pair --pairing-code <code>` to store this machine credential.");
4777
+ process.exitCode = 1;
4778
+ return;
4779
+ }
4780
+ const runnerId = options.runnerId ?? stableRunnerId({
4781
+ accountId: context.metadata.amistioAccountId,
4782
+ projectId: context.metadata.amistioProjectId,
4783
+ repositoryLinkId: context.metadata.repositoryLinkId,
4784
+ machineId: runnerMachineId()
4785
+ });
4786
+ console.log(`Auto-sync watcher: ${runnerId}`);
4787
+ console.log(`Project: ${context.metadata.amistioProjectId}`);
4788
+ console.log(`Repository link: ${context.metadata.repositoryLinkId}`);
4789
+ let iterations = 0;
4790
+ while (true) {
4791
+ iterations += 1;
4792
+ const result = await runAutoSyncCycle({
4793
+ context,
4794
+ maxFileKb: options.maxFileKb,
4795
+ quietDisabled: false,
4796
+ root: options.root,
4797
+ runnerId
4798
+ });
4799
+ console.log(formatAutoSyncCycleResult(result));
4800
+ if (options.once || result.status === "disabled") return;
4801
+ if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
4802
+ console.log(`Auto-sync watcher stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
4803
+ return;
4804
+ }
4805
+ await delay(options.intervalSeconds * 1e3);
4806
+ }
4807
+ });
4178
4808
  var work = program.command("work").description("Inspect approved work items");
4179
4809
  work.command("list").description("List queued work without claiming it").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
4180
4810
  const context = await loadPairedApiContext(options.root, options.apiUrl);
@@ -4231,7 +4861,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
4231
4861
  }
4232
4862
  console.log("custom - pass --tool-command to use any other local runner command.");
4233
4863
  });
4234
- program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel, "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
4864
+ program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel, "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
4235
4865
  const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
4236
4866
  const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
4237
4867
  if (options.promptOut) {
@@ -4243,7 +4873,8 @@ program.command("orchestrate").description("Update the Amistio control plane thr
4243
4873
  return;
4244
4874
  }
4245
4875
  const sessionPolicy = normalizeSessionPolicy(options.session);
4246
- const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, invocationChannel: options.invocationChannel, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
4876
+ const localModelConfig = localModelConfigOptions(options);
4877
+ const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, invocationChannel: options.invocationChannel, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...localModelConfig });
4247
4878
  console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
4248
4879
  const result = await runLocalTool({
4249
4880
  rootDir: options.root,
@@ -4251,7 +4882,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
4251
4882
  tool: options.tool,
4252
4883
  invocationChannel: options.invocationChannel,
4253
4884
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
4254
- ...options.model ? { model: options.model } : {},
4885
+ ...localModelConfig,
4255
4886
  streamOutput: options.stream,
4256
4887
  ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
4257
4888
  });
@@ -4265,7 +4896,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
4265
4896
  process.exitCode = result.exitCode;
4266
4897
  }
4267
4898
  });
4268
- program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
4899
+ program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
4269
4900
  const context = await loadPairedApiContext(options.root, options.apiUrl);
4270
4901
  if (!context) {
4271
4902
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -4437,7 +5068,7 @@ runner.command("stop").description("Stop a background runner for the paired repo
4437
5068
  console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
4438
5069
  });
4439
5070
  var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
4440
- runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
5071
+ runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
4441
5072
  const context = await loadPairedApiContext(options.root, options.apiUrl);
4442
5073
  if (!context) {
4443
5074
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -4527,6 +5158,16 @@ runnerService.command("remove").description("Remove the startup service for this
4527
5158
  });
4528
5159
  async function runWatchIteration({ command, context, options, runnerId }) {
4529
5160
  try {
5161
+ if (options.watch && !options.dryRun) {
5162
+ await runAutoSyncCycle({
5163
+ context,
5164
+ maxFileKb: 256,
5165
+ quiet: true,
5166
+ quietDisabled: true,
5167
+ root: options.root,
5168
+ runnerId
5169
+ });
5170
+ }
4530
5171
  return await runNextWorkItem({
4531
5172
  apiClient: context.client,
4532
5173
  projectId: context.metadata.amistioProjectId,
@@ -4537,6 +5178,10 @@ async function runWatchIteration({ command, context, options, runnerId }) {
4537
5178
  ...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
4538
5179
  ...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
4539
5180
  ...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
5181
+ ...command.getOptionValueSource("provider") === "cli" && options.provider ? { explicitProviderId: options.provider } : {},
5182
+ ...command.getOptionValueSource("modelId") === "cli" && options.modelId ? { explicitModelId: options.modelId } : {},
5183
+ ...command.getOptionValueSource("modelVariant") === "cli" && options.modelVariant ? { explicitModelVariant: options.modelVariant } : {},
5184
+ ...command.getOptionValueSource("reasoningEffort") === "cli" && options.reasoningEffort ? { explicitReasoningEffort: options.reasoningEffort } : {},
4540
5185
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
4541
5186
  dryRun: Boolean(options.dryRun),
4542
5187
  stream: options.stream,
@@ -4550,6 +5195,8 @@ async function runWatchIteration({ command, context, options, runnerId }) {
4550
5195
  runnerId
4551
5196
  },
4552
5197
  suppressIdleOutput: Boolean(options.watch),
5198
+ maxPreflightAttempts: options.maxPreflightAttempts,
5199
+ toolTimeoutMs: options.toolTimeoutSeconds * 1e3,
4553
5200
  verbose: Boolean(options.verbose)
4554
5201
  });
4555
5202
  } catch (error) {
@@ -4564,7 +5211,7 @@ ${detail}`);
4564
5211
  } else {
4565
5212
  console.error(`${message} Run with --verbose for details.`);
4566
5213
  }
4567
- await Promise.allSettled([
5214
+ const settlements = await Promise.allSettled([
4568
5215
  context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "blocked", { ...runnerHeartbeatMetadata(), preferenceMessage: message }),
4569
5216
  context.client.recordRunnerLog(context.metadata.amistioProjectId, {
4570
5217
  runnerId,
@@ -4575,9 +5222,68 @@ ${detail}`);
4575
5222
  machineId: runnerMachineId()
4576
5223
  })
4577
5224
  ]);
5225
+ logRejectedSettlements("record watch error", settlements);
4578
5226
  return { status: "failed", exitCode: 1, message };
4579
5227
  }
4580
5228
  }
5229
+ async function runAutoSyncCycle({ context, maxFileKb, quiet, quietDisabled, root, runnerId }) {
5230
+ const projectId = context.metadata.amistioProjectId;
5231
+ const repositoryLinkId = context.metadata.repositoryLinkId;
5232
+ const heartbeatBase = runnerHeartbeatMetadata(void 0);
5233
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5234
+ try {
5235
+ const { repositoryLinks } = await context.client.listRepositoryLinks(projectId);
5236
+ const repositoryLink = repositoryLinks.find((link) => link.repositoryLinkId === repositoryLinkId && link.status !== "revoked");
5237
+ if (!repositoryLink) {
5238
+ const message2 = "Repository link is not active for auto-sync.";
5239
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", { ...heartbeatBase, autoSyncStatus: "blocked", autoSyncMessage: message2, autoSyncLastFailureAt: startedAt }).catch(() => void 0);
5240
+ return { status: "blocked", message: message2, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
5241
+ }
5242
+ if (!repositoryLink.autoSyncEnabled) {
5243
+ const message2 = "Repository brain auto-sync is disabled.";
5244
+ if (!quietDisabled) {
5245
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: "disabled", autoSyncMessage: message2 }).catch(() => void 0);
5246
+ }
5247
+ return { status: "disabled", message: message2, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
5248
+ }
5249
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: "active", autoSyncMessage: "Scanning repository brain folders.", autoSyncLastStartedAt: startedAt }).catch(() => void 0);
5250
+ const { documents: existingDocuments } = await context.client.listBrainDocuments(projectId);
5251
+ const collection = await collectAutoSyncDocumentsForPush(root, context.metadata, existingDocuments, { maxFileKb, syncedAt: startedAt });
5252
+ if (!collection.documents.length) {
5253
+ const skippedCount2 = collection.skipped.length;
5254
+ const message2 = skippedCount2 ? `No local brain changes pushed; ${skippedCount2} file${skippedCount2 === 1 ? "" : "s"} skipped or unchanged.` : "No local brain changes found.";
5255
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: "synced", autoSyncMessage: message2, autoSyncLastSuccessAt: startedAt, autoSyncPushedCount: 0, autoSyncSkippedCount: skippedCount2, autoSyncConflictCount: 0 }).catch(() => void 0);
5256
+ return { status: "synced", message: message2, pushedCount: 0, skippedCount: skippedCount2, conflictCount: 0, collection };
5257
+ }
5258
+ const { documents } = await context.client.pushBrainDocuments(projectId, collection.documents);
5259
+ const conflictedDocuments = documents.filter((document) => document.syncState === "conflicted" || document.status === "conflicted");
5260
+ const managedDocumentIds = new Set(collection.managedDocumentIds);
5261
+ const managedDocuments = documents.filter((document) => managedDocumentIds.has(document.documentId) && document.syncState !== "conflicted" && document.status !== "conflicted");
5262
+ if (managedDocuments.length) {
5263
+ await materializeBrainDocuments(root, managedDocuments, { allowDirtyOverwrite: true });
5264
+ }
5265
+ const conflictCount = conflictedDocuments.length;
5266
+ const skippedCount = collection.skipped.length;
5267
+ const pushedCount = documents.length - conflictCount;
5268
+ const status = conflictCount ? "conflicted" : "synced";
5269
+ const message = conflictCount ? `Auto-sync found ${conflictCount} conflict${conflictCount === 1 ? "" : "s"}.` : `Auto-sync pushed ${pushedCount} brain document${pushedCount === 1 ? "" : "s"}.`;
5270
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: status, autoSyncMessage: message, ...conflictCount ? { autoSyncLastFailureAt: startedAt } : { autoSyncLastSuccessAt: startedAt }, autoSyncPushedCount: pushedCount, autoSyncSkippedCount: skippedCount, autoSyncConflictCount: conflictCount }).catch(() => void 0);
5271
+ if (!quiet && conflictCount) {
5272
+ for (const document of conflictedDocuments) {
5273
+ console.log(`conflicted: ${document.repoPath} - web and repository revisions both changed`);
5274
+ }
5275
+ }
5276
+ return { status, message, pushedCount, skippedCount, conflictCount, collection };
5277
+ } catch (error) {
5278
+ const message = `Auto-sync failed: ${errorMessage3(error)}`;
5279
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", { ...heartbeatBase, autoSyncStatus: "failed", autoSyncMessage: message, autoSyncLastFailureAt: startedAt }).catch(() => void 0);
5280
+ return { status: "failed", message, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
5281
+ }
5282
+ }
5283
+ function formatAutoSyncCycleResult(result) {
5284
+ const details = [`pushed ${result.pushedCount}`, `skipped ${result.skippedCount}`, `conflicts ${result.conflictCount}`];
5285
+ return `${result.message} (${details.join("; ")})`;
5286
+ }
4581
5287
  async function runNextWorkItem({
4582
5288
  apiClient,
4583
5289
  dryRun,
@@ -4588,11 +5294,17 @@ async function runNextWorkItem({
4588
5294
  sessionPolicy,
4589
5295
  stream,
4590
5296
  explicitModel,
5297
+ explicitModelId,
5298
+ explicitModelVariant,
5299
+ explicitProviderId,
5300
+ explicitReasoningEffort,
4591
5301
  explicitInvocationChannel,
4592
5302
  explicitTool,
5303
+ maxPreflightAttempts,
4593
5304
  toolCommand,
4594
5305
  commandContext,
4595
5306
  suppressIdleOutput,
5307
+ toolTimeoutMs,
4596
5308
  verbose
4597
5309
  }) {
4598
5310
  const toolConfig = await resolveRunnerToolConfig({
@@ -4600,6 +5312,10 @@ async function runNextWorkItem({
4600
5312
  projectId,
4601
5313
  ...explicitInvocationChannel ? { explicitInvocationChannel } : {},
4602
5314
  ...explicitModel ? { explicitModel } : {},
5315
+ ...explicitProviderId ? { explicitProviderId } : {},
5316
+ ...explicitModelId ? { explicitModelId } : {},
5317
+ ...explicitModelVariant ? { explicitModelVariant } : {},
5318
+ ...explicitReasoningEffort ? { explicitReasoningEffort } : {},
4603
5319
  ...explicitTool ? { explicitTool } : {},
4604
5320
  ...toolCommand ? { toolCommand } : {}
4605
5321
  });
@@ -4615,7 +5331,7 @@ async function runNextWorkItem({
4615
5331
  console.log(toolConfig.message);
4616
5332
  return { status: "blocked", exitCode: 1 };
4617
5333
  }
4618
- const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId, 300, runnerIsolationCapabilityMetadata());
5334
+ const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId, RUNNER_WORK_LEASE_SECONDS, runnerIsolationCapabilityMetadata());
4619
5335
  if (!result.workItem) {
4620
5336
  const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
4621
5337
  const message = formatProjectNextAction(nextAction);
@@ -4625,14 +5341,20 @@ async function runNextWorkItem({
4625
5341
  return { status: "idle", exitCode: 0, nextAction, message };
4626
5342
  }
4627
5343
  const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
5344
+ await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
5345
+ status: "running",
5346
+ summary: "Prepared local runner execution prompt.",
5347
+ idempotencyKey: `runner_milestone_prompt_${result.workItem.workItemId}_${result.workItem.attempt}`,
5348
+ metadata: { workKind: result.workItem.workKind ?? "implementation", attempt: result.workItem.attempt }
5349
+ });
4628
5350
  if (dryRun || toolConfig.tool === "none") {
4629
5351
  console.log(prompt);
4630
5352
  await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
4631
5353
  return { status: "preview", exitCode: 0 };
4632
5354
  }
4633
- const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
4634
- if (worktreeIsolation.status === "blocked") {
4635
- return { status: "blocked", exitCode: 1, message: worktreeIsolation.message };
5355
+ const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
5356
+ if (worktreeIsolation.status !== "ready") {
5357
+ return { status: worktreeIsolation.status === "failed" ? "failed" : "blocked", exitCode: 1, message: worktreeIsolation.message };
4636
5358
  }
4637
5359
  const executionRoot = worktreeIsolation.isolation?.worktreePath ?? root;
4638
5360
  const isolationTelemetry = workItemIsolationTelemetry(result.workItem, worktreeIsolation.isolation);
@@ -4643,7 +5365,8 @@ async function runNextWorkItem({
4643
5365
  ...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
4644
5366
  ...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
4645
5367
  });
4646
- const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
5368
+ const resolvedModelConfig = toolConfigModelOptions(toolConfig);
5369
+ const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...resolvedModelConfig });
4647
5370
  const sessionContext = await prepareToolSession({
4648
5371
  apiClient,
4649
5372
  projectId,
@@ -4669,6 +5392,7 @@ async function runNextWorkItem({
4669
5392
  const providerSessionStore = new LocalToolSessionStore();
4670
5393
  const providerSessionId = sessionContext.toolSession ? await providerSessionStore.getProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName) : void 0;
4671
5394
  let toolResult;
5395
+ const stopLeaseRenewal = startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem: result.workItem, telemetry: isolationTelemetry });
4672
5396
  try {
4673
5397
  toolResult = await runLocalTool({
4674
5398
  rootDir: executionRoot,
@@ -4676,8 +5400,9 @@ async function runNextWorkItem({
4676
5400
  tool: toolConfig.tool,
4677
5401
  invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
4678
5402
  ...toolCommand ? { toolCommand } : {},
4679
- ...toolConfig.model ? { model: toolConfig.model } : {},
5403
+ ...resolvedModelConfig,
4680
5404
  streamOutput: stream,
5405
+ timeoutMs: toolTimeoutMs,
4681
5406
  ...sessionContext.toolSession ? {
4682
5407
  session: {
4683
5408
  toolSessionId: sessionContext.toolSession.toolSessionId,
@@ -4688,10 +5413,11 @@ async function runNextWorkItem({
4688
5413
  } : {}
4689
5414
  });
4690
5415
  } catch (error) {
5416
+ stopLeaseRenewal();
4691
5417
  const detail = truncateLogExcerpt(errorDetail(error));
4692
5418
  const durationMs2 = Date.now() - startedAt;
4693
5419
  const message = `${preview.toolName} failed before returning a result.`;
4694
- await Promise.allSettled([
5420
+ const settlements = await Promise.allSettled([
4695
5421
  apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
4696
5422
  markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
4697
5423
  apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "failed", `run_failed_${result.workItem.workItemId}_${result.workItem.attempt}_${runnerId}`, runnerId, {
@@ -4713,11 +5439,13 @@ async function runNextWorkItem({
4713
5439
  metadata: { tool: preview.toolName, error: detail }
4714
5440
  })
4715
5441
  ]);
5442
+ logRejectedSettlements("record local tool failure", settlements);
4716
5443
  if (verbose || !stream) {
4717
5444
  console.error(detail);
4718
5445
  }
4719
5446
  return { status: "failed", exitCode: 1, message };
4720
5447
  }
5448
+ stopLeaseRenewal();
4721
5449
  if (sessionContext.toolSession && toolResult.providerSessionId) {
4722
5450
  await providerSessionStore.setProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName, toolResult.providerSessionId);
4723
5451
  }
@@ -4728,47 +5456,59 @@ async function runNextWorkItem({
4728
5456
  console.error(toolResult.stderr.trim());
4729
5457
  }
4730
5458
  if (result.workItem.workKind === "brainGeneration" || result.workItem.workKind === "planRevision") {
4731
- return finalizeBrainGenerationWork({
4732
- apiClient,
4733
- durationMs: Date.now() - startedAt,
4734
- projectId,
4735
- repositoryLinkId,
4736
- runnerId,
4737
- sessionContext,
4738
- toolConfig,
4739
- toolName: preview.toolName,
4740
- toolResult,
4741
- workItem: result.workItem
4742
- });
5459
+ try {
5460
+ return await finalizeBrainGenerationWork({
5461
+ apiClient,
5462
+ durationMs: Date.now() - startedAt,
5463
+ projectId,
5464
+ repositoryLinkId,
5465
+ runnerId,
5466
+ sessionContext,
5467
+ toolConfig,
5468
+ toolName: preview.toolName,
5469
+ toolResult,
5470
+ workItem: result.workItem
5471
+ });
5472
+ } catch (error) {
5473
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
5474
+ }
4743
5475
  }
4744
5476
  if (result.workItem.workKind === "assistantQuestion") {
4745
- return finalizeAssistantQuestionWork({
4746
- apiClient,
4747
- durationMs: Date.now() - startedAt,
4748
- projectId,
4749
- repositoryLinkId,
4750
- runnerId,
4751
- sessionContext,
4752
- toolConfig,
4753
- toolName: preview.toolName,
4754
- toolResult,
4755
- workItem: result.workItem
4756
- });
5477
+ try {
5478
+ return await finalizeAssistantQuestionWork({
5479
+ apiClient,
5480
+ durationMs: Date.now() - startedAt,
5481
+ projectId,
5482
+ repositoryLinkId,
5483
+ runnerId,
5484
+ sessionContext,
5485
+ toolConfig,
5486
+ toolName: preview.toolName,
5487
+ toolResult,
5488
+ workItem: result.workItem
5489
+ });
5490
+ } catch (error) {
5491
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
5492
+ }
4757
5493
  }
4758
5494
  if (result.workItem.workKind === "impactPreview") {
4759
- return finalizeImpactPreviewWork({
4760
- apiClient,
4761
- durationMs: Date.now() - startedAt,
4762
- projectId,
4763
- repositoryLinkId,
4764
- root,
4765
- runnerId,
4766
- sessionContext,
4767
- toolConfig,
4768
- toolName: preview.toolName,
4769
- toolResult,
4770
- workItem: result.workItem
4771
- });
5495
+ try {
5496
+ return await finalizeImpactPreviewWork({
5497
+ apiClient,
5498
+ durationMs: Date.now() - startedAt,
5499
+ projectId,
5500
+ repositoryLinkId,
5501
+ root,
5502
+ runnerId,
5503
+ sessionContext,
5504
+ toolConfig,
5505
+ toolName: preview.toolName,
5506
+ toolResult,
5507
+ workItem: result.workItem
5508
+ });
5509
+ } catch (error) {
5510
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
5511
+ }
4772
5512
  }
4773
5513
  const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
4774
5514
  const durationMs = Date.now() - startedAt;
@@ -4820,11 +5560,17 @@ async function runNextWorkItem({
4820
5560
  console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
4821
5561
  return { status: finalStatus, exitCode: toolResult.exitCode };
4822
5562
  }
4823
- async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
5563
+ async function prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
4824
5564
  if (!needsGitWorktreeIsolation(workItem)) {
4825
5565
  return { status: "ready" };
4826
5566
  }
4827
5567
  const identity = resolveWorktreeIdentity(workItem);
5568
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
5569
+ status: "running",
5570
+ summary: `Checking Git worktree isolation for attempt ${workItem.attempt}/${maxPreflightAttempts}.`,
5571
+ idempotencyKey: `runner_milestone_worktree_preflight_${workItem.workItemId}_${workItem.attempt}`,
5572
+ metadata: { executionWorktreeKey: identity.worktreeKey, executionBranch: identity.branch, implementationScopeId: identity.implementationScopeId, attempt: workItem.attempt, maxAttempts: maxPreflightAttempts }
5573
+ });
4828
5574
  try {
4829
5575
  const isolation = await prepareGitWorktreeIsolation(root, workItem);
4830
5576
  await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
@@ -4837,29 +5583,111 @@ async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryL
4837
5583
  } catch (error) {
4838
5584
  const message = errorMessage3(error);
4839
5585
  const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
4840
- const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, "blocked", `worktree_${workItem.workItemId}_${randomUUID()}`, runnerId, {
5586
+ const finalAttempt = workItem.attempt >= maxPreflightAttempts;
5587
+ 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}`;
5588
+ const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`, runnerId, {
4841
5589
  ...telemetry,
4842
- message,
4843
- blockerReason: message,
5590
+ message: statusMessage,
5591
+ ...finalAttempt ? { blockerReason: message } : { releaseClaim: true },
4844
5592
  error: message
4845
5593
  });
4846
5594
  await recordRunnerMilestone(apiClient, projectId, statusResult.workItem, runnerId, repositoryLinkId, {
4847
- status: "blocked",
4848
- summary: message,
4849
- idempotencyKey: `runner_milestone_worktree_blocked_${workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
4850
- metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "" }
5595
+ status: finalAttempt ? "failed" : "warning",
5596
+ summary: statusMessage,
5597
+ idempotencyKey: `runner_milestone_worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
5598
+ metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "", attempt: workItem.attempt, maxAttempts: maxPreflightAttempts, error: message }
4851
5599
  });
4852
- await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", {
5600
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", {
4853
5601
  ...runnerHeartbeatMetadata(toolConfig),
4854
- currentWorkItemId: workItem.workItemId,
4855
- ...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
4856
- ...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
4857
- ...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
5602
+ preferenceMessage: statusMessage
4858
5603
  });
4859
- console.error(message);
4860
- return { status: "blocked", message };
5604
+ console.error(statusMessage);
5605
+ return { status: finalAttempt ? "failed" : "retrying", message: statusMessage };
4861
5606
  }
4862
5607
  }
5608
+ function startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem, telemetry }) {
5609
+ let stopped = false;
5610
+ const renew = async () => {
5611
+ if (stopped) return;
5612
+ const leaseExpiresAt = new Date(Date.now() + RUNNER_WORK_LEASE_SECONDS * 1e3).toISOString();
5613
+ try {
5614
+ await apiClient.updateWorkStatus(projectId, workItem.workItemId, "running", `lease_renewal_${workItem.workItemId}_${workItem.attempt}_${Date.now()}`, runnerId, {
5615
+ ...telemetry,
5616
+ leaseExpiresAt
5617
+ });
5618
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", {
5619
+ ...runnerHeartbeatMetadata(toolConfig),
5620
+ currentWorkItemId: workItem.workItemId,
5621
+ ...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
5622
+ ...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
5623
+ ...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
5624
+ });
5625
+ } catch (error) {
5626
+ const detail = truncateLogExcerpt(errorDetail(error));
5627
+ console.error(`Could not renew Amistio work lease for ${workItem.workItemId}: ${detail}`);
5628
+ await apiClient.recordRunnerLog(projectId, {
5629
+ runnerId,
5630
+ repositoryLinkId,
5631
+ status: "failed",
5632
+ workItemId: workItem.workItemId,
5633
+ workTitle: workItem.title,
5634
+ ...workItem.workKind ? { workKind: workItem.workKind } : {},
5635
+ message: "Runner could not renew the active work lease.",
5636
+ error: detail,
5637
+ machineId: runnerMachineId()
5638
+ }).catch(() => void 0);
5639
+ }
5640
+ };
5641
+ const timer = setInterval(() => {
5642
+ void renew();
5643
+ }, RUNNER_WORK_LEASE_RENEWAL_MS);
5644
+ timer.unref?.();
5645
+ return () => {
5646
+ stopped = true;
5647
+ clearInterval(timer);
5648
+ };
5649
+ }
5650
+ async function recordFinalizationFailure({ apiClient, durationMs, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName, workItem }) {
5651
+ const detail = truncateLogExcerpt(errorDetail(error));
5652
+ const message = `${toolName} completed, but Amistio could not finalize the result.`;
5653
+ const settlements = await Promise.allSettled([
5654
+ apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
5655
+ markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
5656
+ apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`, runnerId, {
5657
+ ...isolationTelemetry,
5658
+ tool: toolName,
5659
+ durationMs,
5660
+ message,
5661
+ error: detail,
5662
+ ...sessionContext.toolSession ? { toolSessionId: sessionContext.toolSession.toolSessionId } : {},
5663
+ sessionPolicy: sessionContext.policy,
5664
+ sessionDecision: sessionContext.decision,
5665
+ sessionDecisionReason: sessionContext.reason
5666
+ }),
5667
+ apiClient.recordRunnerLog(projectId, {
5668
+ runnerId,
5669
+ repositoryLinkId,
5670
+ status: "failed",
5671
+ workItemId: workItem.workItemId,
5672
+ workTitle: workItem.title,
5673
+ ...workItem.workKind ? { workKind: workItem.workKind } : {},
5674
+ tool: toolName,
5675
+ durationMs,
5676
+ message,
5677
+ error: detail,
5678
+ machineId: runnerMachineId()
5679
+ }),
5680
+ recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
5681
+ status: "failed",
5682
+ summary: message,
5683
+ idempotencyKey: `runner_milestone_finalization_failed_${workItem.workItemId}_${workItem.attempt}`,
5684
+ metadata: { tool: toolName, durationMs, error: detail }
5685
+ })
5686
+ ]);
5687
+ logRejectedSettlements("record finalization failure", settlements);
5688
+ console.error(detail);
5689
+ return { status: "failed", exitCode: 1, message };
5690
+ }
4863
5691
  function workItemIsolationTelemetry(workItem, isolation) {
4864
5692
  const implementationScopeId = isolation?.implementationScopeId ?? workItem.implementationScopeId;
4865
5693
  const executionBranch = isolation?.branch ?? workItem.executionBranch;
@@ -4888,6 +5716,13 @@ async function recordRunnerMilestone(apiClient, projectId, workItem, runnerId, r
4888
5716
  ...input
4889
5717
  }).catch(() => void 0);
4890
5718
  }
5719
+ function logRejectedSettlements(action, settlements) {
5720
+ for (const settlement of settlements) {
5721
+ if (settlement.status === "rejected") {
5722
+ console.error(`${action} failed: ${errorMessage3(settlement.reason)}`);
5723
+ }
5724
+ }
5725
+ }
4891
5726
  async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
4892
5727
  const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
4893
5728
  const command = commands.filter((item) => item.status === "pending" || item.status === "acknowledged" || item.status === "running").sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt))[0];
@@ -5453,11 +6288,17 @@ function parseInvocationChannel(value) {
5453
6288
  }
5454
6289
  throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
5455
6290
  }
6291
+ function parseReasoningEffort(value) {
6292
+ if (value === "auto" || value === "low" || value === "medium" || value === "high" || value === "xhigh") {
6293
+ return value;
6294
+ }
6295
+ throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
6296
+ }
5456
6297
  function inferRepoName(root) {
5457
6298
  return path13.basename(path13.resolve(root)) || "repository";
5458
6299
  }
5459
6300
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
5460
- return createHash6("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
6301
+ return createHash7("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
5461
6302
  }
5462
6303
  function defaultApiUrl() {
5463
6304
  const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
@@ -5472,9 +6313,10 @@ function formatApiUrlFlag(apiUrl) {
5472
6313
  function formatShellArg(value) {
5473
6314
  return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
5474
6315
  }
5475
- async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitTool, projectId, toolCommand }) {
6316
+ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitModelId, explicitModelVariant, explicitProviderId, explicitReasoningEffort, explicitTool, projectId, toolCommand }) {
5476
6317
  const capabilities = toRunnerToolCapabilities(await detectLocalTools());
5477
6318
  if (toolCommand) {
6319
+ const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
5478
6320
  return {
5479
6321
  ready: true,
5480
6322
  tool: explicitTool ?? "auto",
@@ -5485,33 +6327,42 @@ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, e
5485
6327
  requestedInvocationChannel: explicitInvocationChannel ?? "command",
5486
6328
  effectiveInvocationChannel: "command",
5487
6329
  ...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
5488
- ...explicitModel ? { model: explicitModel } : {},
6330
+ ...modelConfig2,
5489
6331
  message: "Using local custom tool command."
5490
6332
  };
5491
6333
  }
5492
6334
  if (explicitTool === "none") {
5493
- if (explicitModel) {
5494
- return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", model: explicitModel });
6335
+ const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
6336
+ if (hasModelConfig(modelConfig2)) {
6337
+ return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "Model configuration cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...modelConfig2 });
5495
6338
  }
5496
6339
  return { ready: true, tool: "none", capabilities, source: "cli", status: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", message: "No local tool selected." };
5497
6340
  }
5498
6341
  if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
5499
- return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...explicitModel ? { model: explicitModel } : {} });
6342
+ const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
6343
+ return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...modelConfig2 });
5500
6344
  }
5501
6345
  const remotePreference = await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
5502
6346
  const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
5503
6347
  const requestedInvocationChannel = explicitInvocationChannel ?? remotePreference?.invocationChannel ?? "auto";
5504
- const model = explicitModel ?? remotePreference?.model;
5505
- const source = explicitTool || explicitInvocationChannel || explicitModel ? "cli" : remotePreference?.source ?? "default";
5506
- return resolveRequestedTool({ capabilities, requestedInvocationChannel, requestedTool, source, ...model ? { model } : {} });
6348
+ const modelConfig = normalizeModelConfig({
6349
+ model: explicitModel ?? remotePreference?.model,
6350
+ providerId: explicitProviderId ?? remotePreference?.providerId,
6351
+ modelId: explicitModelId ?? remotePreference?.modelId,
6352
+ modelVariant: explicitModelVariant ?? remotePreference?.modelVariant,
6353
+ reasoningEffort: explicitReasoningEffort ?? remotePreference?.reasoningEffort
6354
+ });
6355
+ const source = explicitTool || explicitInvocationChannel || hasExplicitModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort }) ? "cli" : remotePreference?.source ?? "default";
6356
+ return resolveRequestedTool({ capabilities, modelConfig, requestedInvocationChannel, requestedTool, source });
5507
6357
  }
5508
- function resolveRequestedTool({ capabilities, model, requestedInvocationChannel, requestedTool, source }) {
6358
+ function resolveRequestedTool({ capabilities, modelConfig, requestedInvocationChannel, requestedTool, source }) {
6359
+ const needsModelSelection = hasModelConfig(modelConfig);
5509
6360
  if (requestedTool === "auto") {
5510
- const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!model || capability2.supportsModelSelection));
6361
+ const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!needsModelSelection || capability2.supportsModelSelection));
5511
6362
  if (!candidate) {
5512
6363
  const anyAvailable = capabilities.some((capability2) => capability2.available);
5513
6364
  const anyChannelAvailable = capabilities.some((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel));
5514
- const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : model ? "modelUnsupported" : "unavailable";
6365
+ const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : needsModelSelection ? "modelUnsupported" : "unavailable";
5515
6366
  return unavailableToolConfig({
5516
6367
  capabilities,
5517
6368
  source,
@@ -5519,23 +6370,31 @@ function resolveRequestedTool({ capabilities, model, requestedInvocationChannel,
5519
6370
  requestedTool,
5520
6371
  requestedInvocationChannel,
5521
6372
  tool: "auto",
5522
- ...model ? { model } : {},
5523
- message: status === "channelUnsupported" ? `No installed local AI tool can honor ${requestedInvocationChannel} invocation.` : model ? "No installed local tool can honor the selected model." : "No supported local AI tool is installed."
6373
+ ...modelConfig,
6374
+ message: status === "channelUnsupported" ? `No installed local AI tool can honor ${requestedInvocationChannel} invocation.` : needsModelSelection ? "No installed local tool can honor the selected provider/model configuration." : "No supported local AI tool is installed."
5524
6375
  });
5525
6376
  }
5526
- return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...model ? { model } : {} };
6377
+ const modelResolution2 = resolveProviderModelConfig(candidate, modelConfig);
6378
+ if (!modelResolution2.ready) {
6379
+ return unavailableToolConfig({ capabilities, source, status: modelResolution2.status, requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), tool: "auto", ...modelConfig, message: modelResolution2.message });
6380
+ }
6381
+ return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...modelResolution2.config };
5527
6382
  }
5528
6383
  const capability = capabilities.find((candidate) => candidate.name === requestedTool);
5529
6384
  if (!capability?.available) {
5530
- return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
6385
+ return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...modelConfig, message: `${requestedTool} is selected but is not available on this runner.` });
5531
6386
  }
5532
6387
  if (!capabilitySupportsInvocationChannel(capability, requestedInvocationChannel)) {
5533
- return unavailableToolConfig({ capabilities, source, status: "channelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is available but does not support ${requestedInvocationChannel} invocation on this runner.` });
6388
+ return unavailableToolConfig({ capabilities, source, status: "channelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, tool: requestedTool, ...modelConfig, message: `${requestedTool} is available but does not support ${requestedInvocationChannel} invocation on this runner.` });
5534
6389
  }
5535
- if (model && !capability.supportsModelSelection) {
5536
- return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, model, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
6390
+ if (needsModelSelection && !capability.supportsModelSelection) {
6391
+ return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, ...modelConfig, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
5537
6392
  }
5538
- return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...model ? { model } : {} };
6393
+ const modelResolution = resolveProviderModelConfig(capability, modelConfig);
6394
+ if (!modelResolution.ready) {
6395
+ return unavailableToolConfig({ capabilities, source, status: modelResolution.status, requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, ...modelConfig, message: modelResolution.message });
6396
+ }
6397
+ return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...modelResolution.config };
5539
6398
  }
5540
6399
  function unavailableToolConfig(input) {
5541
6400
  return {
@@ -5549,9 +6408,116 @@ function unavailableToolConfig(input) {
5549
6408
  ...input.requestedInvocationChannel ? { requestedInvocationChannel: input.requestedInvocationChannel } : {},
5550
6409
  ...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
5551
6410
  ...input.effectiveInvocationChannel ? { effectiveInvocationChannel: input.effectiveInvocationChannel } : {},
5552
- ...input.model ? { model: input.model } : {}
6411
+ ...normalizeModelConfig(input)
5553
6412
  };
5554
6413
  }
6414
+ function resolveProviderModelConfig(capability, modelConfig) {
6415
+ const normalized = normalizeModelConfig(modelConfig);
6416
+ if (!hasModelConfig(normalized)) {
6417
+ return { ready: true, config: {} };
6418
+ }
6419
+ if (!capability.supportsModelSelection) {
6420
+ return { ready: false, status: "modelUnsupported", message: `${capability.name} does not support Amistio model selection yet.` };
6421
+ }
6422
+ if (!normalized.providerId && !normalized.modelId && !normalized.modelVariant && (!normalized.reasoningEffort || normalized.reasoningEffort === "auto")) {
6423
+ return { ready: true, config: normalized };
6424
+ }
6425
+ const catalog = capability.providerCatalog;
6426
+ if (!catalog) {
6427
+ return { ready: false, status: "modelUnsupported", message: `${capability.name} does not report a provider model catalog yet.` };
6428
+ }
6429
+ const providerId = normalized.providerId ?? inferProviderIdForModel(catalog, normalized.modelId ?? normalized.model);
6430
+ if (!providerId) {
6431
+ return { ready: false, status: "modelUnsupported", message: "Select a provider id with the provider-backed model preference." };
6432
+ }
6433
+ const provider = catalog[providerId];
6434
+ if (!provider) {
6435
+ return { ready: false, status: "modelUnsupported", message: `${providerId} is not available in ${capability.name}'s provider catalog.` };
6436
+ }
6437
+ const modelId = normalized.modelId ?? inferModelId(provider.models, normalized.model);
6438
+ if (!modelId) {
6439
+ return { ready: false, status: "modelUnsupported", message: "Select a model id with the provider-backed model preference." };
6440
+ }
6441
+ const model = provider.models[modelId];
6442
+ if (!model) {
6443
+ return { ready: false, status: "modelUnsupported", message: `${providerId}/${modelId} is not available in ${capability.name}'s provider catalog.` };
6444
+ }
6445
+ if (normalized.modelVariant) {
6446
+ const variant = model.variants?.[normalized.modelVariant];
6447
+ if (!variant || variant.disabled) {
6448
+ return { ready: false, status: "variantUnsupported", message: `${providerId}/${modelId} does not support variant ${normalized.modelVariant}.` };
6449
+ }
6450
+ }
6451
+ if (normalized.reasoningEffort && normalized.reasoningEffort !== "auto") {
6452
+ const supportedEfforts = model.supportedReasoningEfforts ?? (model.reasoning ? ["auto", "low", "medium", "high", "xhigh"] : []);
6453
+ if (!supportedEfforts.includes(normalized.reasoningEffort)) {
6454
+ return { ready: false, status: "reasoningUnsupported", message: `${providerId}/${modelId} does not support ${normalized.reasoningEffort} reasoning effort.` };
6455
+ }
6456
+ }
6457
+ return {
6458
+ ready: true,
6459
+ config: {
6460
+ model: normalized.model ?? `${providerId}/${modelId}`,
6461
+ providerId,
6462
+ modelId,
6463
+ ...normalized.modelVariant ? { modelVariant: normalized.modelVariant } : {},
6464
+ ...normalized.reasoningEffort ? { reasoningEffort: normalized.reasoningEffort } : {}
6465
+ }
6466
+ };
6467
+ }
6468
+ function normalizeModelConfig(config) {
6469
+ const model = cleanPreferenceText(config.model);
6470
+ const providerId = cleanPreferenceText(config.providerId);
6471
+ const modelId = cleanPreferenceText(config.modelId);
6472
+ const modelVariant = cleanPreferenceText(config.modelVariant);
6473
+ return {
6474
+ ...model ? { model } : {},
6475
+ ...providerId ? { providerId } : {},
6476
+ ...modelId ? { modelId } : {},
6477
+ ...modelVariant ? { modelVariant } : {},
6478
+ ...config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {}
6479
+ };
6480
+ }
6481
+ function hasExplicitModelConfig(config) {
6482
+ return hasModelConfig(normalizeModelConfig(config));
6483
+ }
6484
+ function hasModelConfig(config) {
6485
+ return Boolean(config.model || config.providerId || config.modelId || config.modelVariant || config.reasoningEffort);
6486
+ }
6487
+ function cleanPreferenceText(value) {
6488
+ const trimmed = value?.trim();
6489
+ return trimmed ? trimmed : void 0;
6490
+ }
6491
+ function inferProviderIdForModel(catalog, model) {
6492
+ if (!model) return void 0;
6493
+ if (model.includes("/")) {
6494
+ const [providerId, modelId] = model.split("/", 2);
6495
+ if (providerId && modelId && catalog[providerId]?.models[modelId]) return providerId;
6496
+ }
6497
+ return Object.entries(catalog).find(([, provider]) => Boolean(provider.models[model]))?.[0];
6498
+ }
6499
+ function inferModelId(models, model) {
6500
+ if (!model) return void 0;
6501
+ if (models[model]) return model;
6502
+ const slashIndex = model.indexOf("/");
6503
+ if (slashIndex !== -1) {
6504
+ const modelId = model.slice(slashIndex + 1);
6505
+ if (models[modelId]) return modelId;
6506
+ }
6507
+ return void 0;
6508
+ }
6509
+ function toolConfigModelOptions(toolConfig) {
6510
+ return normalizeModelConfig(toolConfig);
6511
+ }
6512
+ function localModelConfigOptions(options) {
6513
+ return normalizeModelConfig({
6514
+ model: options.model,
6515
+ providerId: options.providerId ?? options.provider,
6516
+ modelId: options.modelId,
6517
+ modelVariant: options.modelVariant,
6518
+ reasoningEffort: options.reasoningEffort
6519
+ });
6520
+ }
5555
6521
  function capabilitySupportsInvocationChannel(capability, channel) {
5556
6522
  if (channel === "auto") return capability.available;
5557
6523
  if (channel === "sdk") return capability.sdkAvailable;
@@ -5584,6 +6550,8 @@ function runnerIsolationCapabilityMetadata() {
5584
6550
  };
5585
6551
  }
5586
6552
  function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
6553
+ const modelConfig = toolConfig ? toolConfigModelOptions(toolConfig) : {};
6554
+ const effectiveModelConfig = toolConfig?.ready ? modelConfig : {};
5587
6555
  return {
5588
6556
  version: CLI_VERSION,
5589
6557
  mode,
@@ -5593,16 +6561,24 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
5593
6561
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
5594
6562
  ...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
5595
6563
  ...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
6564
+ ...modelConfig.providerId ? { requestedProviderId: modelConfig.providerId } : {},
6565
+ ...modelConfig.modelId ? { requestedModelId: modelConfig.modelId } : {},
6566
+ ...modelConfig.modelVariant ? { requestedModelVariant: modelConfig.modelVariant } : {},
6567
+ ...modelConfig.reasoningEffort ? { requestedReasoningEffort: modelConfig.reasoningEffort } : {},
5596
6568
  ...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
5597
6569
  ...toolConfig?.effectiveInvocationChannel ? { effectiveInvocationChannel: toolConfig.effectiveInvocationChannel } : {},
5598
- ...toolConfig?.model ? { effectiveModel: toolConfig.model } : {},
6570
+ ...effectiveModelConfig.model ? { effectiveModel: effectiveModelConfig.model } : {},
6571
+ ...effectiveModelConfig.providerId ? { effectiveProviderId: effectiveModelConfig.providerId } : {},
6572
+ ...effectiveModelConfig.modelId ? { effectiveModelId: effectiveModelConfig.modelId } : {},
6573
+ ...effectiveModelConfig.modelVariant ? { effectiveModelVariant: effectiveModelConfig.modelVariant } : {},
6574
+ ...effectiveModelConfig.reasoningEffort ? { effectiveReasoningEffort: effectiveModelConfig.reasoningEffort } : {},
5599
6575
  ...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
5600
6576
  ...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},
5601
6577
  ...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
5602
6578
  };
5603
6579
  }
5604
6580
  function runnerMachineId() {
5605
- return createHash6("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
6581
+ return createHash7("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
5606
6582
  }
5607
6583
  async function delay(milliseconds) {
5608
6584
  await new Promise((resolve) => setTimeout(resolve, milliseconds));