@voybio/ace-swarm 0.2.5 → 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 (144) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +21 -13
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/EVIDENCE_LOG.md +1 -1
  5. package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
  6. package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
  7. package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
  8. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
  9. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
  10. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  11. package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
  12. package/assets/agent-state/STATUS.md +2 -2
  13. package/assets/agent-state/runtime-tool-specs.json +70 -2
  14. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  15. package/assets/instructions/ACE_UI.instructions.md +11 -0
  16. package/assets/scripts/ace-hook-dispatch.mjs +70 -6
  17. package/assets/scripts/render-mcp-configs.sh +19 -5
  18. package/dist/ace-context.js +91 -11
  19. package/dist/ace-internal-tools.d.ts +3 -1
  20. package/dist/ace-internal-tools.js +10 -2
  21. package/dist/ace-server-instructions.js +3 -3
  22. package/dist/ace-state-resolver.js +5 -3
  23. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  24. package/dist/agent-runtime/role-adapters.js +49 -5
  25. package/dist/astgrep-index.d.ts +57 -1
  26. package/dist/astgrep-index.js +140 -4
  27. package/dist/cli.js +232 -35
  28. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  29. package/dist/discovery-runtime-wrappers.js +615 -0
  30. package/dist/handoff-registry.js +5 -5
  31. package/dist/helpers/artifacts.d.ts +19 -0
  32. package/dist/helpers/artifacts.js +152 -0
  33. package/dist/helpers/bootstrap.d.ts +24 -0
  34. package/dist/helpers/bootstrap.js +894 -0
  35. package/dist/helpers/constants.d.ts +53 -0
  36. package/dist/helpers/constants.js +295 -0
  37. package/dist/helpers/drift.d.ts +13 -0
  38. package/dist/helpers/drift.js +45 -0
  39. package/dist/helpers/path-utils.d.ts +24 -0
  40. package/dist/helpers/path-utils.js +123 -0
  41. package/dist/helpers/store-resolution.d.ts +19 -0
  42. package/dist/helpers/store-resolution.js +305 -0
  43. package/dist/helpers/workspace-root.d.ts +3 -0
  44. package/dist/helpers/workspace-root.js +80 -0
  45. package/dist/helpers.d.ts +8 -125
  46. package/dist/helpers.js +8 -1768
  47. package/dist/job-scheduler.js +33 -7
  48. package/dist/json-sanitizer.d.ts +16 -0
  49. package/dist/json-sanitizer.js +26 -0
  50. package/dist/local-model-policy.d.ts +27 -0
  51. package/dist/local-model-policy.js +84 -0
  52. package/dist/local-model-runtime.d.ts +6 -0
  53. package/dist/local-model-runtime.js +33 -21
  54. package/dist/model-bridge.d.ts +13 -1
  55. package/dist/model-bridge.js +410 -23
  56. package/dist/orchestrator-supervisor.d.ts +56 -0
  57. package/dist/orchestrator-supervisor.js +179 -1
  58. package/dist/plan-proposal.d.ts +115 -0
  59. package/dist/plan-proposal.js +1073 -0
  60. package/dist/run-ledger.js +3 -3
  61. package/dist/runtime-command.d.ts +8 -0
  62. package/dist/runtime-command.js +38 -6
  63. package/dist/runtime-executor.d.ts +20 -1
  64. package/dist/runtime-executor.js +737 -172
  65. package/dist/runtime-profile.d.ts +32 -0
  66. package/dist/runtime-profile.js +89 -13
  67. package/dist/runtime-tool-specs.d.ts +39 -0
  68. package/dist/runtime-tool-specs.js +144 -28
  69. package/dist/safe-edit.d.ts +7 -0
  70. package/dist/safe-edit.js +163 -37
  71. package/dist/schemas.js +48 -1
  72. package/dist/server.js +51 -0
  73. package/dist/shared.d.ts +3 -2
  74. package/dist/shared.js +2 -0
  75. package/dist/status-events.js +9 -6
  76. package/dist/store/ace-packed-store.d.ts +3 -2
  77. package/dist/store/ace-packed-store.js +188 -110
  78. package/dist/store/bootstrap-store.d.ts +2 -1
  79. package/dist/store/bootstrap-store.js +102 -83
  80. package/dist/store/cache-workspace.js +11 -5
  81. package/dist/store/materializers/context-snapshot-materializer.js +6 -2
  82. package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
  83. package/dist/store/materializers/hook-context-materializer.js +11 -21
  84. package/dist/store/materializers/host-file-materializer.js +6 -0
  85. package/dist/store/materializers/projection-manager.d.ts +0 -1
  86. package/dist/store/materializers/projection-manager.js +5 -13
  87. package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
  88. package/dist/store/materializers/vericify-projector.d.ts +7 -7
  89. package/dist/store/materializers/vericify-projector.js +11 -11
  90. package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
  91. package/dist/store/repositories/local-model-runtime-repository.js +242 -6
  92. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  93. package/dist/store/skills-install.d.ts +4 -0
  94. package/dist/store/skills-install.js +21 -12
  95. package/dist/store/state-reader.d.ts +2 -0
  96. package/dist/store/state-reader.js +20 -0
  97. package/dist/store/store-artifacts.d.ts +7 -0
  98. package/dist/store/store-artifacts.js +27 -1
  99. package/dist/store/store-authority-audit.d.ts +18 -1
  100. package/dist/store/store-authority-audit.js +115 -5
  101. package/dist/store/store-snapshot.d.ts +3 -0
  102. package/dist/store/store-snapshot.js +22 -2
  103. package/dist/store/workspace-store-paths.d.ts +39 -0
  104. package/dist/store/workspace-store-paths.js +94 -0
  105. package/dist/store/write-coordinator.d.ts +65 -0
  106. package/dist/store/write-coordinator.js +386 -0
  107. package/dist/todo-state.js +5 -5
  108. package/dist/tools-agent.d.ts +20 -0
  109. package/dist/tools-agent.js +789 -25
  110. package/dist/tools-discovery.js +136 -1
  111. package/dist/tools-files.d.ts +7 -0
  112. package/dist/tools-files.js +1002 -11
  113. package/dist/tools-framework.js +105 -66
  114. package/dist/tools-handoff.js +2 -2
  115. package/dist/tools-lifecycle.js +4 -4
  116. package/dist/tools-memory.js +6 -6
  117. package/dist/tools-todo.js +2 -2
  118. package/dist/tracker-adapters.d.ts +1 -1
  119. package/dist/tracker-adapters.js +13 -18
  120. package/dist/tracker-sync.js +5 -3
  121. package/dist/tui/agent-runner.js +3 -1
  122. package/dist/tui/chat.js +103 -7
  123. package/dist/tui/dashboard.d.ts +1 -0
  124. package/dist/tui/dashboard.js +43 -0
  125. package/dist/tui/index.js +10 -1
  126. package/dist/tui/layout.d.ts +20 -0
  127. package/dist/tui/layout.js +31 -1
  128. package/dist/tui/local-model-contract.d.ts +6 -2
  129. package/dist/tui/local-model-contract.js +16 -3
  130. package/dist/tui/ollama.d.ts +8 -1
  131. package/dist/tui/ollama.js +53 -12
  132. package/dist/tui/openai-compatible.d.ts +13 -0
  133. package/dist/tui/openai-compatible.js +305 -5
  134. package/dist/tui/provider-discovery.d.ts +1 -0
  135. package/dist/tui/provider-discovery.js +35 -11
  136. package/dist/vericify-bridge.d.ts +6 -1
  137. package/dist/vericify-bridge.js +27 -3
  138. package/dist/workspace-manager.d.ts +30 -3
  139. package/dist/workspace-manager.js +257 -27
  140. package/package.json +1 -2
  141. package/dist/internal-tool-runtime.d.ts +0 -21
  142. package/dist/internal-tool-runtime.js +0 -136
  143. package/dist/store/workspace-snapshot.d.ts +0 -26
  144. package/dist/store/workspace-snapshot.js +0 -107
@@ -1,8 +1,144 @@
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
- import { appendStatusEvent } from "./status-events.js";
5
+ import { appendStatusEventSafe } from "./status-events.js";
5
6
  import { safeRead, safeWrite, WORKSPACE_ROOT, wsPath } from "./helpers.js";
7
+ export function runAstgrepQuery(pattern, lang, roots, _contextLines) {
8
+ const cmd = detectAstgrepCommand();
9
+ const raw = runAstgrep(cmd, pattern, lang, roots);
10
+ return raw.slice(0, 200).map((m) => ({
11
+ file: m.file ?? "",
12
+ line: m.range?.start?.line ?? 0,
13
+ column: m.range?.start?.column ?? 0,
14
+ text: m.text ?? "",
15
+ context_lines: [],
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
+ }
6
142
  const CODE_EXTENSIONS = new Set(["ts", "tsx", "js", "mjs", "cjs", "py", "rs", "go"]);
7
143
  const EXCLUDE_DIRS = new Set([
8
144
  ".git",
@@ -28,7 +164,7 @@ function resolveScope(scope) {
28
164
  }
29
165
  return { abs, rel: rel || "." };
30
166
  }
31
- function detectAstgrepCommand() {
167
+ export function detectAstgrepCommand() {
32
168
  for (const cmd of ["ast-grep", "sg"]) {
33
169
  const probe = spawnSync(cmd, ["--version"], { encoding: "utf8" });
34
170
  if (probe.status === 0)
@@ -180,7 +316,7 @@ function appendEvidenceLine(timestamp, astgrepCmd, stats) {
180
316
  ].join("\n");
181
317
  safeWrite("agent-state/EVIDENCE_LOG.md", `${seed}${entry}\n`);
182
318
  }
183
- export function refreshAstgrepIndex(input = {}) {
319
+ export async function refreshAstgrepIndex(input = {}) {
184
320
  const generatedAt = new Date().toISOString();
185
321
  const scope = resolveScope(input.scope);
186
322
  const appendEvidence = input.append_evidence ?? true;
@@ -440,7 +576,7 @@ export function refreshAstgrepIndex(input = {}) {
440
576
  if (emitEvent) {
441
577
  const status = astgrepCmd ? "pass" : "blocked";
442
578
  const eventType = astgrepCmd ? "DISCOVERY_INDEX_READY" : "DISCOVERY_SCOPE_BLOCKED";
443
- const event = appendStatusEvent({
579
+ const event = await appendStatusEventSafe({
444
580
  source_module: "capability-astgrep",
445
581
  event_type: eventType,
446
582
  status,
package/dist/cli.js CHANGED
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, ALL_MCP_CLIENTS, ALL_LLM_PROVIDERS, DEFAULTS_ROOT, PACKAGE_ROOT, WORKSPACE_ROOT, fileExists, getAllMcpServerConfigSnippets, getMcpClientInstallHint, getMcpServerConfigSnippet, wsPath, } from "./helpers.js";
2
+ import { ACE_TASKS_ROOT_REL, ALL_MCP_CLIENTS, ALL_LLM_PROVIDERS, DEFAULTS_ROOT, PACKAGE_ROOT, WORKSPACE_ROOT, fileExists, getAllMcpServerConfigSnippets, getMcpClientInstallHint, getMcpServerConfigSnippet, wsPath, } from "./helpers.js";
3
3
  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";
11
- import { withStoreWriteQueue } from "./store/write-queue.js";
12
11
  import { DiscoveryRepository } from "./store/repositories/discovery-repository.js";
12
+ import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
13
13
  import { getWorkspaceStorePath, readStoreBlobSync, readStoreJsonSync, } from "./store/store-snapshot.js";
14
+ import { ensureCanonicalWorkspaceStore } from "./store/workspace-store-paths.js";
14
15
  import { readFileSync } from "node:fs";
15
16
  import { runTui } from "./tui/index.js";
16
- 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";
17
18
  import { diagnoseChatRuntimeConfig, OpenAICompatibleClient, } from "./tui/openai-compatible.js";
19
+ import { runShellCommand } from "./runtime-command.js";
18
20
  function printHelp() {
19
21
  console.log(`ACE Swarm CLI
20
22
 
@@ -23,6 +25,7 @@ Usage:
23
25
  ace serve Alias for mcp
24
26
  ace tui [options] Launch interactive TUI dashboard
25
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
26
29
  ace turnkey [options] Project minimal workspace bootstrap stubs from the ACE store
27
30
  ace doctor [options] Validate ACE runtime + MCP readiness
28
31
  ace cache [options] Cache ACE artifacts into ace-state.ace and optionally clean projections
@@ -38,7 +41,7 @@ Options for tui:
38
41
  --ollama-url <url> Legacy alias for --base-url
39
42
 
40
43
  Options for init:
41
- --project <name> Project name stored in ${ACE_ROOT_REL}/ace-state.ace metadata
44
+ --project <name> Project name stored in agent-state/ace-state.ace metadata
42
45
  --force Overwrite scaffolded files if they already exist
43
46
  --mcp-config Also write .vscode/mcp.json workspace bridge
44
47
  --client-config-bundle Also write minimal workspace host stubs (AGENTS.md, CLAUDE.md, .cursorrules, .github/copilot-instructions.md)
@@ -47,23 +50,32 @@ Options for init:
47
50
  --base-url <url> Runtime base URL override (local or OpenAI-compatible)
48
51
  --ollama-url <url> Legacy alias for --base-url
49
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
+
50
60
  Options for doctor:
51
- --llm <provider> ollama|llama.cpp|codex|claude|gemini|copilot|... (default: auto from ${ACE_ROOT_REL}/ace-state.ace)
61
+ --llm <provider> ollama|llama.cpp|codex|claude|gemini|copilot|... (default: auto from agent-state/ace-state.ace)
52
62
  --model <name> Model name override
53
63
  --base-url <url> Runtime base URL override
54
64
  --ollama-url <url> Legacy alias for --base-url
55
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
56
67
 
57
68
  Options for cache:
58
69
  --dry-run Preview what would be cached and cleaned (no writes/deletes)
59
70
  --no-clean Keep workspace ACE artifacts after caching them into ace-state.ace
60
71
 
61
72
  Options for mcp-config:
62
- --client <name> codex|vscode|claude|cursor|antigravity
73
+ --client <name> codex|vscode|copilot|claude|cursor|antigravity
63
74
  --all Print all client snippets for optional global install
64
75
 
65
76
  preconfig writes .mcp-config/ at the workspace root with ready-to-use config files for every
66
- supported MCP client. Run once after ace init. Each file includes the install hint for its client.
77
+ supported MCP client plus a root .mcp.json for GitHub Copilot CLI. Run once after ace init.
78
+ Each file includes the install hint for its client.
67
79
  `);
68
80
  }
69
81
  function readFlagValue(args, flag) {
@@ -96,6 +108,29 @@ function readLlmProfile() {
96
108
  function readBaseUrlFlag(args) {
97
109
  return normalizeLocalBaseUrl(readFlagValue(args, "--base-url") ?? readFlagValue(args, "--ollama-url"));
98
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
+ }
99
134
  function parseLlmOptions(args) {
100
135
  const profile = readLlmProfile();
101
136
  const selected = normalizeProvider(readFlagValue(args, "--llm")?.trim() ?? profile?.provider?.trim());
@@ -105,8 +140,11 @@ function parseLlmOptions(args) {
105
140
  throw new Error(`Unsupported LLM provider: ${selected}`);
106
141
  }
107
142
  const provider = selected;
108
- const defaultModel = defaultModelForProvider(provider);
109
- 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);
110
148
  const llmBaseUrl = readBaseUrlFlag(args) ?? normalizeLocalBaseUrl(profile?.base_url);
111
149
  return {
112
150
  llmProvider: provider,
@@ -116,12 +154,14 @@ function parseLlmOptions(args) {
116
154
  }
117
155
  async function writeLlmProfile(profile) {
118
156
  const storePath = getWorkspaceStorePath(WORKSPACE_ROOT);
119
- await withStoreWriteQueue(storePath, async () => {
157
+ await withStoreWriteCoordinator(storePath, async () => {
120
158
  const payload = {
121
159
  provider: profile.provider,
122
- model: profile.model,
123
160
  generated_at: new Date().toISOString(),
124
161
  };
162
+ if (profile.model) {
163
+ payload.model = profile.model;
164
+ }
125
165
  if (profile.baseUrl) {
126
166
  payload.base_url = profile.baseUrl;
127
167
  if (profile.provider === "ollama") {
@@ -129,6 +169,9 @@ async function writeLlmProfile(profile) {
129
169
  payload.default_api_key = "ollama";
130
170
  }
131
171
  }
172
+ if (profile.launcherCommand) {
173
+ payload.launch_command = profile.launcherCommand;
174
+ }
132
175
  const doctorCommands = buildProviderDoctorCommands(profile.provider, profile.model, profile.baseUrl);
133
176
  const store = await openStore(storePath);
134
177
  try {
@@ -148,12 +191,46 @@ async function writeLlmProfile(profile) {
148
191
  finally {
149
192
  await store.close();
150
193
  }
151
- });
194
+ }, { operation_label: "writeLlmProfile" });
152
195
  return `${storePath}#state/runtime/llm_profile`;
153
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
+ }
154
231
  async function recordDiscoveryProfile(input) {
155
232
  const storePath = getWorkspaceStorePath(WORKSPACE_ROOT);
156
- await withStoreWriteQueue(storePath, async () => {
233
+ await withStoreWriteCoordinator(storePath, async () => {
157
234
  const store = await openStore(storePath);
158
235
  try {
159
236
  const discovery = new DiscoveryRepository(store);
@@ -176,7 +253,7 @@ async function recordDiscoveryProfile(input) {
176
253
  finally {
177
254
  await store.close();
178
255
  }
179
- });
256
+ }, { operation_label: "recordDiscoveryProfile" });
180
257
  }
181
258
  async function runInit(args, mode = "init") {
182
259
  const projectName = readFlagValue(args, "--project");
@@ -197,7 +274,7 @@ async function runInit(args, mode = "init") {
197
274
  model: llm.llmModel ?? undefined,
198
275
  baseUrl: llm.llmBaseUrl ?? undefined,
199
276
  });
200
- const astIndex = refreshAstgrepIndex({
277
+ const astIndex = await refreshAstgrepIndex({
201
278
  scope: ".",
202
279
  append_evidence: true,
203
280
  emit_event: true,
@@ -253,7 +330,7 @@ async function runInit(args, mode = "init") {
253
330
  message: `ace ${mode} completed: ${storeResult.materialized.length} bootstrap files materialized`,
254
331
  artifacts: [
255
332
  `${ACE_TASKS_ROOT_REL}/`,
256
- `${ACE_ROOT_REL}/ace-state.ace`,
333
+ storeResult.storePath,
257
334
  delta.index_path,
258
335
  astIndex.output_json_path,
259
336
  ],
@@ -294,6 +371,13 @@ async function listLocalRuntimeModels(provider, baseUrl) {
294
371
  headers: { Accept: "application/json" },
295
372
  });
296
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
+ }
297
381
  throw new Error(`${response.status} ${response.statusText}`);
298
382
  }
299
383
  const payload = (await response.json().catch(() => ({})));
@@ -319,6 +403,7 @@ async function listLocalRuntimeModels(provider, baseUrl) {
319
403
  }
320
404
  async function runDoctor(args) {
321
405
  const llm = parseLlmOptions(args);
406
+ const repairOllama = args.includes("--repair-ollama");
322
407
  const checks = [];
323
408
  const mcpConfigPaths = [wsPath(".vscode", "mcp.json")];
324
409
  const hasWorkspaceMcpConfig = mcpConfigPaths.some((path) => fileExists(path));
@@ -340,12 +425,14 @@ async function runDoctor(args) {
340
425
  ok: hasProfile,
341
426
  detail: hasProfile
342
427
  ? profilePath
343
- : `Missing ${ACE_ROOT_REL}/ace-state.ace#state/runtime/llm_profile. Run \`ace init --llm <provider>\` or \`ace doctor --scan\` for a local runtime.`,
428
+ : `Missing agent-state/ace-state.ace#state/runtime/llm_profile. Run \`ace init --llm <provider>\` or \`ace doctor --scan\` for a local runtime.`,
344
429
  });
345
430
  let provider = llm.llmProvider;
346
431
  let model = llm.llmModel;
347
432
  let baseUrl = llm.llmBaseUrl;
348
- const shouldScan = args.includes("--scan") || (!provider && !baseUrl) || (isLocalLlmProvider(provider) && !baseUrl);
433
+ const shouldScan = args.includes("--scan") ||
434
+ (!provider && !baseUrl) ||
435
+ (isLocalLlmProvider(provider) && (!baseUrl || !model));
349
436
  if (shouldScan) {
350
437
  const scanned = await scanLocalModelRuntimes({
351
438
  workspaceRoot: WORKSPACE_ROOT,
@@ -357,7 +444,7 @@ async function runDoctor(args) {
357
444
  if (chosen) {
358
445
  provider = chosen.provider ?? provider;
359
446
  baseUrl = baseUrl ?? chosen.baseUrl;
360
- model = model || chosen.models[0] || defaultModelForProvider(provider);
447
+ model = model || chosen.models[0];
361
448
  checks.push({
362
449
  name: "Runtime endpoint discovered",
363
450
  ok: true,
@@ -365,7 +452,7 @@ async function runDoctor(args) {
365
452
  });
366
453
  const writtenProfilePath = await writeLlmProfile({
367
454
  provider: provider ?? chosen.provider,
368
- model: model ?? defaultModelForProvider(provider ?? chosen.provider),
455
+ model,
369
456
  baseUrl,
370
457
  });
371
458
  checks.push({
@@ -383,9 +470,33 @@ async function runDoctor(args) {
383
470
  }
384
471
  }
385
472
  if (!provider) {
386
- throw new Error(`No runtime provider configured. Use --llm <provider>, bootstrap one into ${ACE_ROOT_REL}/ace-state.ace#state/runtime/llm_profile, or run \`ace doctor --scan\` for a local runtime.`);
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.`);
387
474
  }
388
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
+ }
389
500
  model = defaultModelForProvider(provider);
390
501
  }
391
502
  if (isLocalLlmProvider(provider) && !baseUrl) {
@@ -423,6 +534,23 @@ async function runDoctor(args) {
423
534
  });
424
535
  }
425
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
+ }
426
554
  checks.push({
427
555
  name: "Runtime endpoint reachable",
428
556
  ok: false,
@@ -448,6 +576,44 @@ async function runDoctor(args) {
448
576
  ? `${model} found`
449
577
  : `${model} not reported by llama.cpp (available: ${modelNames.join(", ") || "none"})`,
450
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
+ }
451
617
  }
452
618
  else {
453
619
  const diagnosis = diagnoseChatRuntimeConfig(provider, model, baseUrl ? { baseUrl } : undefined);
@@ -520,16 +686,21 @@ function parseClientFlag(args) {
520
686
  return match;
521
687
  }
522
688
  async function runPreconfig() {
523
- const storePath = wsPath(".agents", "ACE", "ace-state.ace");
524
- if (!fileExists(storePath)) {
525
- console.error(`No ACE store found at ${storePath}. Run 'ace init' first.`);
689
+ const resolved = await ensureCanonicalWorkspaceStore(WORKSPACE_ROOT, {
690
+ operationLabel: "runPreconfig",
691
+ });
692
+ if (resolved.mode === "missing") {
693
+ console.error(`No ACE store found. Expected canonical ${resolved.canonicalPath}; legacy fallback ${resolved.legacyPath} was also not found. Run 'ace init' first.`);
526
694
  process.exit(1);
527
695
  }
528
- const store = await openStore(storePath);
696
+ for (const w of resolved.warnings) {
697
+ console.warn(`[preconfig] ${w}`);
698
+ }
699
+ const store = await openStore(resolved.storePath);
529
700
  try {
530
701
  const mat = new HostFileMaterializer(store, WORKSPACE_ROOT);
531
702
  const written = await mat.materializeMcpBundle();
532
- console.log(`ACE preconfig complete — wrote ${written.length} files to .mcp-config/\n`);
703
+ console.log(`ACE preconfig complete — wrote ${written.length} files to .mcp-config/ and root .mcp.json\n`);
533
704
  for (const p of written) {
534
705
  console.log(` ${p}`);
535
706
  }
@@ -581,14 +752,36 @@ async function main() {
581
752
  cliModel,
582
753
  cliBaseUrl,
583
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;
584
777
  await runTui({
585
- provider: discovered.provider,
586
- model: discovered.model,
587
- providers: discovered.providers,
588
- modelsByProvider: discovered.modelsByProvider,
589
- baseUrl: discovered.baseUrl,
590
- ollamaUrl: discovered.ollamaUrl,
591
- 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,
592
785
  workspaceRoot: WORKSPACE_ROOT,
593
786
  });
594
787
  return;
@@ -597,6 +790,10 @@ async function main() {
597
790
  await runInit(args.slice(1), "init");
598
791
  return;
599
792
  }
793
+ if (command === "connect") {
794
+ await runConnect(args.slice(1));
795
+ return;
796
+ }
600
797
  if (command === "turnkey") {
601
798
  await runInit(args.slice(1), "turnkey");
602
799
  return;
@@ -649,7 +846,7 @@ async function main() {
649
846
  }
650
847
  if (command === "migrate") {
651
848
  const { importFromFileWorkspace } = await import("./store/importer.js");
652
- const storePath = args[1] ?? `${WORKSPACE_ROOT}/.agents/ACE/ace-state.ace`;
849
+ const storePath = args[1] ?? getWorkspaceStorePath(WORKSPACE_ROOT);
653
850
  console.log(`Migrating file workspace at ${WORKSPACE_ROOT} to store at ${storePath}...`);
654
851
  const result = await importFromFileWorkspace(WORKSPACE_ROOT, storePath);
655
852
  console.log(`Migration complete:`);