@voybio/ace-swarm 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -0
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  5. package/assets/agent-state/runtime-tool-specs.json +70 -2
  6. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  7. package/assets/instructions/ACE_UI.instructions.md +11 -0
  8. package/dist/ace-context.js +70 -11
  9. package/dist/ace-internal-tools.d.ts +3 -1
  10. package/dist/ace-internal-tools.js +10 -2
  11. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  12. package/dist/agent-runtime/role-adapters.js +49 -5
  13. package/dist/astgrep-index.d.ts +48 -0
  14. package/dist/astgrep-index.js +126 -1
  15. package/dist/cli.js +205 -15
  16. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  17. package/dist/discovery-runtime-wrappers.js +615 -0
  18. package/dist/helpers/bootstrap.js +1 -1
  19. package/dist/helpers/constants.d.ts +2 -2
  20. package/dist/helpers/constants.js +7 -0
  21. package/dist/helpers/path-utils.d.ts +8 -1
  22. package/dist/helpers/path-utils.js +27 -8
  23. package/dist/helpers/store-resolution.js +7 -3
  24. package/dist/job-scheduler.js +30 -4
  25. package/dist/json-sanitizer.d.ts +16 -0
  26. package/dist/json-sanitizer.js +26 -0
  27. package/dist/local-model-policy.d.ts +27 -0
  28. package/dist/local-model-policy.js +84 -0
  29. package/dist/local-model-runtime.d.ts +6 -0
  30. package/dist/local-model-runtime.js +21 -20
  31. package/dist/model-bridge.d.ts +6 -1
  32. package/dist/model-bridge.js +338 -21
  33. package/dist/orchestrator-supervisor.d.ts +42 -0
  34. package/dist/orchestrator-supervisor.js +110 -3
  35. package/dist/plan-proposal.d.ts +115 -0
  36. package/dist/plan-proposal.js +1073 -0
  37. package/dist/runtime-executor.d.ts +6 -1
  38. package/dist/runtime-executor.js +72 -5
  39. package/dist/runtime-tool-specs.d.ts +19 -1
  40. package/dist/runtime-tool-specs.js +67 -26
  41. package/dist/schemas.js +29 -1
  42. package/dist/server.js +51 -0
  43. package/dist/shared.d.ts +1 -0
  44. package/dist/shared.js +2 -0
  45. package/dist/store/bootstrap-store.d.ts +1 -0
  46. package/dist/store/bootstrap-store.js +8 -2
  47. package/dist/store/repositories/local-model-runtime-repository.d.ts +1 -1
  48. package/dist/store/repositories/local-model-runtime-repository.js +1 -1
  49. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  50. package/dist/tools-agent.d.ts +20 -0
  51. package/dist/tools-agent.js +538 -28
  52. package/dist/tools-discovery.js +135 -0
  53. package/dist/tools-files.js +768 -66
  54. package/dist/tools-framework.js +80 -61
  55. package/dist/tui/index.js +10 -1
  56. package/dist/tui/ollama.d.ts +8 -1
  57. package/dist/tui/ollama.js +53 -12
  58. package/dist/tui/openai-compatible.d.ts +13 -0
  59. package/dist/tui/openai-compatible.js +305 -5
  60. package/dist/tui/provider-discovery.d.ts +1 -0
  61. package/dist/tui/provider-discovery.js +35 -11
  62. package/dist/vericify-bridge.d.ts +1 -1
  63. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
2
3
  import { isAbsolute, relative, resolve } from "node:path";
3
4
  import { spawnSync } from "node:child_process";
4
5
  import { appendStatusEventSafe } from "./status-events.js";
@@ -14,6 +15,130 @@ export function runAstgrepQuery(pattern, lang, roots, _contextLines) {
14
15
  context_lines: [],
15
16
  }));
16
17
  }
18
+ function hashTextContent(content) {
19
+ return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
20
+ }
21
+ function textPreview(text, maxChars = 160) {
22
+ const normalized = text.replace(/\s+/g, " ").trim();
23
+ if (normalized.length <= maxChars)
24
+ return normalized;
25
+ return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
26
+ }
27
+ function nodeKindForMatch(match) {
28
+ if (typeof match.nodeKind === "string" && match.nodeKind.length > 0)
29
+ return match.nodeKind;
30
+ if (typeof match.kind === "string" && match.kind.length > 0)
31
+ return match.kind;
32
+ return undefined;
33
+ }
34
+ function captureMapForMatch(match) {
35
+ if (!match.metaVariables)
36
+ return undefined;
37
+ const captures = Object.entries(match.metaVariables).reduce((acc, [key]) => {
38
+ const text = extractMetaVarText(match, key);
39
+ if (typeof text === "string" && text.length > 0)
40
+ acc[key] = text;
41
+ return acc;
42
+ }, {});
43
+ return Object.keys(captures).length > 0 ? captures : undefined;
44
+ }
45
+ function computeMatchId(input) {
46
+ const encoded = [
47
+ input.file,
48
+ input.range.start?.line ?? 0,
49
+ input.range.start?.column ?? 0,
50
+ input.range.end?.line ?? 0,
51
+ input.range.end?.column ?? 0,
52
+ input.file_hash,
53
+ input.pattern,
54
+ input.lang,
55
+ input.matched_text,
56
+ ].join("|");
57
+ return `match_${createHash("sha256").update(encoded, "utf8").digest("hex").slice(0, 16)}`;
58
+ }
59
+ function matchUsesSymbolHint(match, symbolHint) {
60
+ if ((match.text ?? "").includes(symbolHint))
61
+ return true;
62
+ if (!match.metaVariables)
63
+ return false;
64
+ return Object.keys(match.metaVariables).some((key) => {
65
+ const text = extractMetaVarText(match, key);
66
+ return typeof text === "string" && text.includes(symbolHint);
67
+ });
68
+ }
69
+ export function locateAstgrepMatches(input) {
70
+ const scope = resolveScope(input.scope ?? "src");
71
+ const astgrepCmd = detectAstgrepCommand();
72
+ if (!astgrepCmd) {
73
+ return {
74
+ ok: false,
75
+ pattern: input.pattern,
76
+ lang: input.lang,
77
+ scope: scope.rel,
78
+ symbol_hint: input.symbol_hint,
79
+ astgrep_command: null,
80
+ total_matches: 0,
81
+ matches: [],
82
+ error: "ast-grep command not available",
83
+ };
84
+ }
85
+ const rawMatches = runAstgrep(astgrepCmd, input.pattern, input.lang, [scope.abs]);
86
+ const effectiveMatches = input.symbol_hint && input.symbol_hint.trim().length > 0
87
+ ? (() => {
88
+ const hinted = rawMatches.filter((match) => matchUsesSymbolHint(match, input.symbol_hint));
89
+ return hinted.length > 0 ? hinted : rawMatches;
90
+ })()
91
+ : rawMatches;
92
+ const limit = input.max_results ?? 50;
93
+ const matches = effectiveMatches.slice(0, limit).flatMap((match) => {
94
+ const absFile = typeof match.file === "string" && match.file.length > 0
95
+ ? (isAbsolute(match.file) ? match.file : resolve(WORKSPACE_ROOT, match.file))
96
+ : undefined;
97
+ if (!absFile || !isInside(WORKSPACE_ROOT, absFile) || !match.range)
98
+ return [];
99
+ const matchedText = match.text ?? "";
100
+ const fileContent = safeReadText(absFile);
101
+ const fileHash = hashTextContent(fileContent);
102
+ const file = normalizeHitPath(relative(WORKSPACE_ROOT, absFile));
103
+ const range = {
104
+ start: {
105
+ line: match.range.start?.line ?? 0,
106
+ column: match.range.start?.column ?? 0,
107
+ },
108
+ end: {
109
+ line: match.range.end?.line ?? 0,
110
+ column: match.range.end?.column ?? 0,
111
+ },
112
+ };
113
+ return [{
114
+ match_id: computeMatchId({
115
+ file,
116
+ range,
117
+ matched_text: matchedText,
118
+ file_hash: fileHash,
119
+ pattern: input.pattern,
120
+ lang: input.lang,
121
+ }),
122
+ file,
123
+ range,
124
+ text_preview: textPreview(matchedText),
125
+ matched_text: matchedText,
126
+ captures: captureMapForMatch(match),
127
+ node_kind: nodeKindForMatch(match),
128
+ file_hash: fileHash,
129
+ }];
130
+ });
131
+ return {
132
+ ok: true,
133
+ pattern: input.pattern,
134
+ lang: input.lang,
135
+ scope: scope.rel,
136
+ symbol_hint: input.symbol_hint,
137
+ astgrep_command: astgrepCmd,
138
+ total_matches: effectiveMatches.length,
139
+ matches,
140
+ };
141
+ }
17
142
  const CODE_EXTENSIONS = new Set(["ts", "tsx", "js", "mjs", "cjs", "py", "rs", "go"]);
18
143
  const EXCLUDE_DIRS = new Set([
19
144
  ".git",
@@ -39,7 +164,7 @@ function resolveScope(scope) {
39
164
  }
40
165
  return { abs, rel: rel || "." };
41
166
  }
42
- function detectAstgrepCommand() {
167
+ export function detectAstgrepCommand() {
43
168
  for (const cmd of ["ast-grep", "sg"]) {
44
169
  const probe = spawnSync(cmd, ["--version"], { encoding: "utf8" });
45
170
  if (probe.status === 0)
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { refreshAstgrepIndex } from "./astgrep-index.js";
4
4
  import { scanWorkspaceDelta } from "./index-store.js";
5
5
  import { startStdioServer } from "./server.js";
6
6
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
7
- import { waitForPendingStatusEventMirrors } from "./status-events.js";
7
+ import { appendStatusEventSafe, waitForPendingStatusEventMirrors } from "./status-events.js";
8
8
  import { bootstrapStoreWorkspace } from "./store/bootstrap-store.js";
9
9
  import { HostFileMaterializer } from "./store/materializers/host-file-materializer.js";
10
10
  import { openStore } from "./store/ace-packed-store.js";
@@ -14,8 +14,9 @@ import { getWorkspaceStorePath, readStoreBlobSync, readStoreJsonSync, } from "./
14
14
  import { ensureCanonicalWorkspaceStore } from "./store/workspace-store-paths.js";
15
15
  import { readFileSync } from "node:fs";
16
16
  import { runTui } from "./tui/index.js";
17
- import { buildOpenAiCompatibleBaseUrl, buildProviderDoctorCommands, defaultModelForProvider, discoverProviderContext, isLocalLlmProvider, normalizeLocalBaseUrl, normalizeProvider, scanLocalModelRuntimes, } from "./tui/provider-discovery.js";
17
+ import { buildOpenAiCompatibleBaseUrl, buildProviderDoctorCommands, defaultModelForProvider, discoverProviderContext, isLocalLlmProvider, normalizeLocalBaseUrl, normalizeLlamaCppHfModelName, normalizeProvider, scanLocalModelRuntimes, } from "./tui/provider-discovery.js";
18
18
  import { diagnoseChatRuntimeConfig, OpenAICompatibleClient, } from "./tui/openai-compatible.js";
19
+ import { runShellCommand } from "./runtime-command.js";
19
20
  function printHelp() {
20
21
  console.log(`ACE Swarm CLI
21
22
 
@@ -24,6 +25,7 @@ Usage:
24
25
  ace serve Alias for mcp
25
26
  ace tui [options] Launch interactive TUI dashboard
26
27
  ace init [options] Bootstrap the ACE store into current workspace
28
+ ace connect [provider] Register a live local runtime or hosted provider in the store
27
29
  ace turnkey [options] Project minimal workspace bootstrap stubs from the ACE store
28
30
  ace doctor [options] Validate ACE runtime + MCP readiness
29
31
  ace cache [options] Cache ACE artifacts into ace-state.ace and optionally clean projections
@@ -48,12 +50,20 @@ Options for init:
48
50
  --base-url <url> Runtime base URL override (local or OpenAI-compatible)
49
51
  --ollama-url <url> Legacy alias for --base-url
50
52
 
53
+ Options for connect:
54
+ llama-server|llama-cli Optional launcher name for llama.cpp HF runtimes
55
+ --hf <repo> Hugging Face model repo or local model identifier
56
+ --model <name> Alias for --hf when you already know the model name
57
+ --base-url <url> OpenAI-compatible endpoint for the live runtime
58
+ --provider <name> Explicit provider override when not using a launcher alias
59
+
51
60
  Options for doctor:
52
61
  --llm <provider> ollama|llama.cpp|codex|claude|gemini|copilot|... (default: auto from agent-state/ace-state.ace)
53
62
  --model <name> Model name override
54
63
  --base-url <url> Runtime base URL override
55
64
  --ollama-url <url> Legacy alias for --base-url
56
65
  --scan Probe common local Ollama + llama.cpp endpoints when URL is unset
66
+ --repair-ollama Opt-in: run ollama pull <model> when doctor finds a missing Ollama model
57
67
 
58
68
  Options for cache:
59
69
  --dry-run Preview what would be cached and cleaned (no writes/deletes)
@@ -98,6 +108,29 @@ function readLlmProfile() {
98
108
  function readBaseUrlFlag(args) {
99
109
  return normalizeLocalBaseUrl(readFlagValue(args, "--base-url") ?? readFlagValue(args, "--ollama-url"));
100
110
  }
111
+ function readConnectModelFlag(args) {
112
+ return (readFlagValue(args, "--hf") ??
113
+ readFlagValue(args, "-hf") ??
114
+ readFlagValue(args, "--model"))?.trim();
115
+ }
116
+ function parseConnectArgs(args) {
117
+ const firstToken = args[0];
118
+ const hasLauncher = Boolean(firstToken && !firstToken.startsWith("-"));
119
+ const launcher = hasLauncher ? firstToken?.trim().toLowerCase() : undefined;
120
+ const providerOverride = normalizeProvider(readFlagValue(args, "--provider")?.trim());
121
+ const provider = providerOverride ??
122
+ (launcher === "llama-server" || launcher === "llama-cli" ? "llama.cpp" : providerOverride);
123
+ const rawModel = readConnectModelFlag(args);
124
+ const model = provider === "llama.cpp" ? normalizeLlamaCppHfModelName(rawModel) ?? rawModel : rawModel;
125
+ const baseUrl = readBaseUrlFlag(args);
126
+ const launcherCommand = hasLauncher ? args.join(" ").trim() : undefined;
127
+ return {
128
+ provider: provider,
129
+ model,
130
+ baseUrl,
131
+ launcherCommand,
132
+ };
133
+ }
101
134
  function parseLlmOptions(args) {
102
135
  const profile = readLlmProfile();
103
136
  const selected = normalizeProvider(readFlagValue(args, "--llm")?.trim() ?? profile?.provider?.trim());
@@ -107,8 +140,11 @@ function parseLlmOptions(args) {
107
140
  throw new Error(`Unsupported LLM provider: ${selected}`);
108
141
  }
109
142
  const provider = selected;
110
- const defaultModel = defaultModelForProvider(provider);
111
- const llmModel = readFlagValue(args, "--model")?.trim() || profile?.model || defaultModel;
143
+ const explicitModel = readFlagValue(args, "--model")?.trim();
144
+ const profileModel = profile?.model?.trim();
145
+ const llmModel = explicitModel ||
146
+ profileModel ||
147
+ (provider === "ollama" ? defaultModelForProvider(provider) : undefined);
112
148
  const llmBaseUrl = readBaseUrlFlag(args) ?? normalizeLocalBaseUrl(profile?.base_url);
113
149
  return {
114
150
  llmProvider: provider,
@@ -121,9 +157,11 @@ async function writeLlmProfile(profile) {
121
157
  await withStoreWriteCoordinator(storePath, async () => {
122
158
  const payload = {
123
159
  provider: profile.provider,
124
- model: profile.model,
125
160
  generated_at: new Date().toISOString(),
126
161
  };
162
+ if (profile.model) {
163
+ payload.model = profile.model;
164
+ }
127
165
  if (profile.baseUrl) {
128
166
  payload.base_url = profile.baseUrl;
129
167
  if (profile.provider === "ollama") {
@@ -131,6 +169,9 @@ async function writeLlmProfile(profile) {
131
169
  payload.default_api_key = "ollama";
132
170
  }
133
171
  }
172
+ if (profile.launcherCommand) {
173
+ payload.launch_command = profile.launcherCommand;
174
+ }
134
175
  const doctorCommands = buildProviderDoctorCommands(profile.provider, profile.model, profile.baseUrl);
135
176
  const store = await openStore(storePath);
136
177
  try {
@@ -153,6 +194,40 @@ async function writeLlmProfile(profile) {
153
194
  }, { operation_label: "writeLlmProfile" });
154
195
  return `${storePath}#state/runtime/llm_profile`;
155
196
  }
197
+ async function runConnect(args) {
198
+ const parsed = parseConnectArgs(args);
199
+ if (!parsed.provider) {
200
+ throw new Error("ace connect expects a provider or launcher command. Try `ace connect llama-server -hf <model> --base-url http://127.0.0.1:8080`.");
201
+ }
202
+ if (!ALL_LLM_PROVIDERS.includes(parsed.provider)) {
203
+ throw new Error(`Unsupported LLM provider: ${parsed.provider}`);
204
+ }
205
+ if (isLocalLlmProvider(parsed.provider) && !parsed.model) {
206
+ throw new Error("ace connect for local runtimes requires an explicit model. Pass `--hf <repo>` or `--model <name>`.");
207
+ }
208
+ const storeResult = await bootstrapStoreWorkspace({
209
+ workspaceRoot: WORKSPACE_ROOT,
210
+ llm: parsed.provider,
211
+ model: parsed.model,
212
+ baseUrl: parsed.baseUrl,
213
+ includeMcpConfig: false,
214
+ includeClientConfigBundle: false,
215
+ launcherCommand: parsed.launcherCommand,
216
+ });
217
+ console.log("ACE connect complete");
218
+ console.log(`Workspace: ${WORKSPACE_ROOT}`);
219
+ console.log(`Store: ${storeResult.storePath}`);
220
+ console.log(`Provider: ${parsed.provider}`);
221
+ console.log(`Model: ${parsed.model ?? "(not set)"}`);
222
+ console.log(`Base URL: ${parsed.baseUrl ?? "(not set)"}`);
223
+ if (parsed.launcherCommand) {
224
+ console.log(`Launcher: ${parsed.launcherCommand}`);
225
+ }
226
+ console.log(`Profile path: ${storeResult.storePath}#state/runtime/llm_profile`);
227
+ for (const warning of storeResult.warnings) {
228
+ console.warn(` [store] ${warning}`);
229
+ }
230
+ }
156
231
  async function recordDiscoveryProfile(input) {
157
232
  const storePath = getWorkspaceStorePath(WORKSPACE_ROOT);
158
233
  await withStoreWriteCoordinator(storePath, async () => {
@@ -296,6 +371,13 @@ async function listLocalRuntimeModels(provider, baseUrl) {
296
371
  headers: { Accept: "application/json" },
297
372
  });
298
373
  if (!response.ok) {
374
+ const text = await response.text().catch(() => "");
375
+ if (response.status >= 500 && /unable to load model/i.test(text)) {
376
+ const error = new Error(`${response.status} ${response.statusText}: ollama_model_load_error. ` +
377
+ `Suggested remediation: run ollama pull <model> or ace doctor --repair-ollama.`);
378
+ error.reason_code = "ollama_model_load_error";
379
+ throw error;
380
+ }
299
381
  throw new Error(`${response.status} ${response.statusText}`);
300
382
  }
301
383
  const payload = (await response.json().catch(() => ({})));
@@ -321,6 +403,7 @@ async function listLocalRuntimeModels(provider, baseUrl) {
321
403
  }
322
404
  async function runDoctor(args) {
323
405
  const llm = parseLlmOptions(args);
406
+ const repairOllama = args.includes("--repair-ollama");
324
407
  const checks = [];
325
408
  const mcpConfigPaths = [wsPath(".vscode", "mcp.json")];
326
409
  const hasWorkspaceMcpConfig = mcpConfigPaths.some((path) => fileExists(path));
@@ -347,7 +430,9 @@ async function runDoctor(args) {
347
430
  let provider = llm.llmProvider;
348
431
  let model = llm.llmModel;
349
432
  let baseUrl = llm.llmBaseUrl;
350
- const shouldScan = args.includes("--scan") || (!provider && !baseUrl) || (isLocalLlmProvider(provider) && !baseUrl);
433
+ const shouldScan = args.includes("--scan") ||
434
+ (!provider && !baseUrl) ||
435
+ (isLocalLlmProvider(provider) && (!baseUrl || !model));
351
436
  if (shouldScan) {
352
437
  const scanned = await scanLocalModelRuntimes({
353
438
  workspaceRoot: WORKSPACE_ROOT,
@@ -359,7 +444,7 @@ async function runDoctor(args) {
359
444
  if (chosen) {
360
445
  provider = chosen.provider ?? provider;
361
446
  baseUrl = baseUrl ?? chosen.baseUrl;
362
- model = model || chosen.models[0] || defaultModelForProvider(provider);
447
+ model = model || chosen.models[0];
363
448
  checks.push({
364
449
  name: "Runtime endpoint discovered",
365
450
  ok: true,
@@ -367,7 +452,7 @@ async function runDoctor(args) {
367
452
  });
368
453
  const writtenProfilePath = await writeLlmProfile({
369
454
  provider: provider ?? chosen.provider,
370
- model: model ?? defaultModelForProvider(provider ?? chosen.provider),
455
+ model,
371
456
  baseUrl,
372
457
  });
373
458
  checks.push({
@@ -388,6 +473,30 @@ async function runDoctor(args) {
388
473
  throw new Error(`No runtime provider configured. Use --llm <provider>, bootstrap one into agent-state/ace-state.ace#state/runtime/llm_profile, or run \`ace doctor --scan\` for a local runtime.`);
389
474
  }
390
475
  if (!model) {
476
+ if (isLocalLlmProvider(provider)) {
477
+ checks.push({
478
+ name: "Runtime model configured",
479
+ ok: false,
480
+ detail: "No local runtime model is configured yet. Run `ace connect` with a user-supplied HF model or run `ace doctor --scan` after the runtime is up.",
481
+ });
482
+ const failed = checks.filter((check) => !check.ok);
483
+ console.log("ACE Doctor Report");
484
+ console.log(`Workspace: ${WORKSPACE_ROOT}`);
485
+ console.log(`Provider: ${provider}`);
486
+ console.log("Model: (not set)");
487
+ if (baseUrl) {
488
+ console.log(`Base URL: ${baseUrl}`);
489
+ }
490
+ console.log("");
491
+ for (const check of checks) {
492
+ const status = check.ok ? "PASS" : "FAIL";
493
+ console.log(`- [${status}] ${check.name}: ${check.detail}`);
494
+ }
495
+ if (failed.length > 0) {
496
+ process.exitCode = 1;
497
+ }
498
+ return;
499
+ }
391
500
  model = defaultModelForProvider(provider);
392
501
  }
393
502
  if (isLocalLlmProvider(provider) && !baseUrl) {
@@ -425,6 +534,23 @@ async function runDoctor(args) {
425
534
  });
426
535
  }
427
536
  catch (error) {
537
+ const reasonCode = typeof error === "object" && error !== null && "reason_code" in error
538
+ ? String(error.reason_code)
539
+ : undefined;
540
+ if (reasonCode === "ollama_model_load_error") {
541
+ await appendStatusEventSafe({
542
+ source_module: "capability-ops",
543
+ event_type: "OLLAMA_MODEL_LOAD_ERROR",
544
+ status: "blocked",
545
+ summary: `Ollama model-load error for ${model}: ${error instanceof Error ? error.message : String(error)}`,
546
+ objective_id: "ollama-doctor",
547
+ payload: {
548
+ reason_code: "ollama_model_load_error",
549
+ model,
550
+ base_url: baseUrl,
551
+ },
552
+ }).catch(() => undefined);
553
+ }
428
554
  checks.push({
429
555
  name: "Runtime endpoint reachable",
430
556
  ok: false,
@@ -450,6 +576,44 @@ async function runDoctor(args) {
450
576
  ? `${model} found`
451
577
  : `${model} not reported by llama.cpp (available: ${modelNames.join(", ") || "none"})`,
452
578
  });
579
+ if (provider === "ollama" && !hasModel && repairOllama) {
580
+ const repair = await runShellCommand(`ollama pull ${model}`, {
581
+ cwd: WORKSPACE_ROOT,
582
+ timeout_ms: 10 * 60_000,
583
+ });
584
+ await appendRunLedgerEntrySafe({
585
+ tool: "doctor",
586
+ category: repair.exit_code === 0 ? "info" : "regression",
587
+ message: `Opt-in Ollama repair attempted for ${model}`,
588
+ artifacts: [],
589
+ metadata: {
590
+ reason_code: "ollama_model_load_error",
591
+ model,
592
+ exit_code: repair.exit_code,
593
+ timed_out: repair.timed_out,
594
+ },
595
+ }).catch(() => undefined);
596
+ await appendStatusEventSafe({
597
+ source_module: "capability-ops",
598
+ event_type: "OLLAMA_REPAIR_ATTEMPTED",
599
+ status: repair.exit_code === 0 ? "done" : "fail",
600
+ summary: `Opt-in Ollama repair attempted for ${model}`,
601
+ objective_id: "ollama-doctor",
602
+ payload: {
603
+ reason_code: "ollama_model_load_error",
604
+ model,
605
+ exit_code: repair.exit_code,
606
+ timed_out: repair.timed_out,
607
+ },
608
+ }).catch(() => undefined);
609
+ checks.push({
610
+ name: "Ollama model repair attempted",
611
+ ok: repair.exit_code === 0,
612
+ detail: repair.exit_code === 0
613
+ ? `ollama pull ${model} completed`
614
+ : `ollama pull ${model} failed with exit ${repair.exit_code}: ${repair.stderr || repair.stdout || "no output"}`,
615
+ });
616
+ }
453
617
  }
454
618
  else {
455
619
  const diagnosis = diagnoseChatRuntimeConfig(provider, model, baseUrl ? { baseUrl } : undefined);
@@ -588,14 +752,36 @@ async function main() {
588
752
  cliModel,
589
753
  cliBaseUrl,
590
754
  });
755
+ const needsLocalScan = isLocalLlmProvider(discovered.provider) &&
756
+ (!discovered.baseUrl || !discovered.model || discovered.model === defaultModelForProvider(discovered.provider));
757
+ const runtime = needsLocalScan
758
+ ? await scanLocalModelRuntimes({
759
+ workspaceRoot: WORKSPACE_ROOT,
760
+ preferredProvider: discovered.provider,
761
+ explicitBaseUrl: discovered.baseUrl,
762
+ })
763
+ : undefined;
764
+ const scanned = runtime?.candidates.find((candidate) => candidate.provider === discovered.provider) ??
765
+ runtime?.candidates[0];
766
+ const resolvedTui = scanned?.models[0]
767
+ ? {
768
+ ...discovered,
769
+ model: scanned.models[0],
770
+ baseUrl: discovered.baseUrl ?? scanned.baseUrl,
771
+ providerBaseUrls: {
772
+ ...discovered.providerBaseUrls,
773
+ [scanned.provider]: discovered.baseUrl ?? scanned.baseUrl,
774
+ },
775
+ }
776
+ : discovered;
591
777
  await runTui({
592
- provider: discovered.provider,
593
- model: discovered.model,
594
- providers: discovered.providers,
595
- modelsByProvider: discovered.modelsByProvider,
596
- baseUrl: discovered.baseUrl,
597
- ollamaUrl: discovered.ollamaUrl,
598
- providerBaseUrls: discovered.providerBaseUrls,
778
+ provider: resolvedTui.provider,
779
+ model: resolvedTui.model,
780
+ providers: resolvedTui.providers,
781
+ modelsByProvider: resolvedTui.modelsByProvider,
782
+ baseUrl: resolvedTui.baseUrl,
783
+ ollamaUrl: resolvedTui.ollamaUrl,
784
+ providerBaseUrls: resolvedTui.providerBaseUrls,
599
785
  workspaceRoot: WORKSPACE_ROOT,
600
786
  });
601
787
  return;
@@ -604,6 +790,10 @@ async function main() {
604
790
  await runInit(args.slice(1), "init");
605
791
  return;
606
792
  }
793
+ if (command === "connect") {
794
+ await runConnect(args.slice(1));
795
+ return;
796
+ }
607
797
  if (command === "turnkey") {
608
798
  await runInit(args.slice(1), "turnkey");
609
799
  return;
@@ -0,0 +1,108 @@
1
+ import { type RunShellCommandOptions, type ShellCommandResult } from "./runtime-command.js";
2
+ import { persistRuntimeToolInvocationArtifacts, type RuntimeToolExecutionContext, type RuntimeToolExecutionResult, type RuntimeToolSpecRegistryResult } from "./runtime-tool-specs.js";
3
+ import { type RefreshAstgrepIndexResult } from "./astgrep-index.js";
4
+ export interface WebResearchPacketInput extends RuntimeToolExecutionContext {
5
+ query: string;
6
+ sources_required: number;
7
+ fetch_urls?: string[];
8
+ }
9
+ export interface WebResearchCitation {
10
+ title: string;
11
+ url: string;
12
+ snippet?: string;
13
+ fetched_path?: string;
14
+ }
15
+ export interface WebResearchPacketResult {
16
+ ok: boolean;
17
+ summary: string;
18
+ tool_name: string;
19
+ packet_path?: string;
20
+ search_results_path?: string;
21
+ fetched_paths?: string[];
22
+ request_path?: string;
23
+ response_path?: string;
24
+ citations?: WebResearchCitation[];
25
+ error?: string;
26
+ }
27
+ export interface CodemunchSnapshotInput extends RuntimeToolExecutionContext {
28
+ repo_url: string;
29
+ branch?: string;
30
+ max_bytes?: number;
31
+ }
32
+ export interface CodemunchSnapshotResult {
33
+ ok: boolean;
34
+ summary: string;
35
+ mode: "runtime_tool" | "external_cli";
36
+ snapshot_path?: string;
37
+ checksum?: string;
38
+ provenance_path?: string;
39
+ request_path?: string;
40
+ response_path?: string;
41
+ error?: string;
42
+ }
43
+ export interface CodemunchIndexInput {
44
+ cxml_path: string;
45
+ append_evidence?: boolean;
46
+ emit_event?: boolean;
47
+ }
48
+ export interface CodemunchIndexResult {
49
+ ok: boolean;
50
+ summary: string;
51
+ source_cxml_path?: string;
52
+ checksum?: string;
53
+ rep_corpus_path?: string;
54
+ structural_summary_json_path?: string;
55
+ structural_summary_md_path?: string;
56
+ provenance_path?: string;
57
+ routing_hints?: string[];
58
+ error?: string;
59
+ }
60
+ export interface McpIngestionProbeInput {
61
+ name: string;
62
+ transport: "stdio" | "http";
63
+ command?: string;
64
+ args?: string[];
65
+ url?: string;
66
+ env?: Record<string, string>;
67
+ declared_tools?: Array<{
68
+ name: string;
69
+ description?: string;
70
+ input_schema?: unknown;
71
+ }>;
72
+ }
73
+ export interface McpIngestionProbeResult {
74
+ ok: boolean;
75
+ summary: string;
76
+ server_name: string;
77
+ transport: "stdio" | "http";
78
+ health: "configured" | "blocked";
79
+ tools: Array<{
80
+ name: string;
81
+ description?: string;
82
+ input_schema?: unknown;
83
+ }>;
84
+ request_path?: string;
85
+ response_path?: string;
86
+ error?: string;
87
+ reason_code?: string;
88
+ }
89
+ interface WrapperDeps {
90
+ executeRuntimeTool: (name: string, input: unknown, context?: RuntimeToolExecutionContext) => Promise<RuntimeToolExecutionResult>;
91
+ loadRuntimeToolRegistry: (explicitPath?: string) => RuntimeToolSpecRegistryResult;
92
+ persistRuntimeToolInvocationArtifacts: typeof persistRuntimeToolInvocationArtifacts;
93
+ runShellCommand: (command: string, options: RunShellCommandOptions) => Promise<ShellCommandResult>;
94
+ refreshAstgrepIndex: (input?: {
95
+ scope?: string;
96
+ append_evidence?: boolean;
97
+ emit_event?: boolean;
98
+ include_rep_corpus?: boolean;
99
+ }) => Promise<RefreshAstgrepIndexResult>;
100
+ now: () => Date;
101
+ randomId: () => string;
102
+ }
103
+ export declare function createWebResearchPacket(input: WebResearchPacketInput, deps?: Partial<WrapperDeps>): Promise<WebResearchPacketResult>;
104
+ export declare function createCodemunchSnapshot(input: CodemunchSnapshotInput, deps?: Partial<WrapperDeps>): Promise<CodemunchSnapshotResult>;
105
+ export declare function createMcpIngestionProbe(input: McpIngestionProbeInput, deps?: Partial<WrapperDeps>): Promise<McpIngestionProbeResult>;
106
+ export declare function createCodemunchIndex(input: CodemunchIndexInput, deps?: Partial<WrapperDeps>): Promise<CodemunchIndexResult>;
107
+ export {};
108
+ //# sourceMappingURL=discovery-runtime-wrappers.d.ts.map