@voybio/ace-swarm 2.4.4 → 2.4.7

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 (120) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +72 -532
  3. package/assets/eval-battery/battery.json +85 -0
  4. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  5. package/assets/scripts/render-mcp-configs.sh +12 -0
  6. package/dist/ace-context.d.ts +18 -0
  7. package/dist/ace-context.js +161 -16
  8. package/dist/ace-server-instructions.js +41 -12
  9. package/dist/bridge-trace.d.ts +17 -0
  10. package/dist/bridge-trace.js +23 -0
  11. package/dist/cli.d.ts +9 -1
  12. package/dist/cli.js +623 -11
  13. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  14. package/dist/discovery-runtime-wrappers.js +65 -9
  15. package/dist/eval-harness.d.ts +110 -0
  16. package/dist/eval-harness.js +254 -0
  17. package/dist/experience-reflector.d.ts +13 -0
  18. package/dist/experience-reflector.js +62 -0
  19. package/dist/goal-engine.d.ts +155 -0
  20. package/dist/goal-engine.js +795 -0
  21. package/dist/helpers/store-resolution.js +40 -39
  22. package/dist/hermes/session-manager.d.ts +10 -0
  23. package/dist/hermes/session-manager.js +130 -27
  24. package/dist/job-scheduler.d.ts +58 -0
  25. package/dist/job-scheduler.js +202 -2
  26. package/dist/job-worker.d.ts +87 -0
  27. package/dist/job-worker.js +182 -0
  28. package/dist/local-model-policy.d.ts +8 -2
  29. package/dist/local-model-policy.js +31 -17
  30. package/dist/local-model-runtime.d.ts +6 -0
  31. package/dist/local-model-runtime.js +130 -2
  32. package/dist/measured-classing.d.ts +66 -0
  33. package/dist/measured-classing.js +165 -0
  34. package/dist/model-bridge.d.ts +45 -0
  35. package/dist/model-bridge.js +502 -44
  36. package/dist/orchestrator-supervisor.js +1 -1
  37. package/dist/plan-proposal.d.ts +7 -0
  38. package/dist/plan-proposal.js +74 -4
  39. package/dist/playbook-adaptation.d.ts +37 -0
  40. package/dist/playbook-adaptation.js +159 -0
  41. package/dist/playbook-curator.d.ts +16 -0
  42. package/dist/playbook-curator.js +106 -0
  43. package/dist/playbook-llm-reflector.d.ts +26 -0
  44. package/dist/playbook-llm-reflector.js +157 -0
  45. package/dist/public-surface.d.ts +2 -0
  46. package/dist/public-surface.js +26 -14
  47. package/dist/safe-edit.d.ts +20 -0
  48. package/dist/safe-edit.js +355 -78
  49. package/dist/server.d.ts +11 -2
  50. package/dist/server.js +55 -5
  51. package/dist/shared.d.ts +12 -0
  52. package/dist/shared.js +13 -1
  53. package/dist/source-mutation-coordinator.d.ts +54 -0
  54. package/dist/source-mutation-coordinator.js +259 -0
  55. package/dist/status-events.d.ts +4 -1
  56. package/dist/status-events.js +41 -6
  57. package/dist/store/ace-packed-store.d.ts +28 -9
  58. package/dist/store/ace-packed-store.js +324 -57
  59. package/dist/store/bgzf.d.ts +4 -0
  60. package/dist/store/bgzf.js +94 -0
  61. package/dist/store/bootstrap-store.d.ts +4 -1
  62. package/dist/store/bootstrap-store.js +57 -3
  63. package/dist/store/cache-workspace.d.ts +8 -0
  64. package/dist/store/cache-workspace.js +67 -1
  65. package/dist/store/crc32.d.ts +2 -0
  66. package/dist/store/crc32.js +16 -0
  67. package/dist/store/fsck.d.ts +16 -0
  68. package/dist/store/fsck.js +240 -0
  69. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  70. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  71. package/dist/store/materializers/projection-manager.d.ts +7 -0
  72. package/dist/store/materializers/projection-manager.js +37 -0
  73. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  74. package/dist/store/repositories/goal-repository.d.ts +112 -0
  75. package/dist/store/repositories/goal-repository.js +157 -0
  76. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  77. package/dist/store/repositories/playbook-repository.js +314 -0
  78. package/dist/store/store-artifacts.js +44 -2
  79. package/dist/store/store-authority-audit.js +1 -0
  80. package/dist/store/store-cli.d.ts +30 -0
  81. package/dist/store/store-cli.js +242 -0
  82. package/dist/store/store-snapshot.d.ts +12 -0
  83. package/dist/store/store-snapshot.js +74 -11
  84. package/dist/store/types.d.ts +1 -0
  85. package/dist/store/write-coordinator.d.ts +7 -0
  86. package/dist/store/write-coordinator.js +11 -0
  87. package/dist/todo-state.d.ts +21 -0
  88. package/dist/todo-state.js +150 -53
  89. package/dist/tools-agent.js +86 -22
  90. package/dist/tools-discovery.js +5 -3
  91. package/dist/tools-files.js +39 -18
  92. package/dist/tools-framework.js +31 -3
  93. package/dist/tools-goal.d.ts +11 -0
  94. package/dist/tools-goal.js +108 -0
  95. package/dist/tools-handoff.js +7 -11
  96. package/dist/tools-lifecycle.js +2 -0
  97. package/dist/tools-scheduler.js +44 -1
  98. package/dist/tools-skills.js +3 -2
  99. package/dist/tools-todo.js +64 -3
  100. package/dist/tools.js +2 -0
  101. package/dist/tui/chat.js +4 -0
  102. package/dist/tui/index.js +4 -1
  103. package/dist/tui/ollama.d.ts +2 -0
  104. package/dist/tui/openai-compatible.d.ts +14 -0
  105. package/dist/tui/openai-compatible.js +15 -2
  106. package/dist/tui/provider-discovery.d.ts +1 -0
  107. package/dist/tui/provider-discovery.js +2 -2
  108. package/docs/cli-reference.md +127 -0
  109. package/docs/development.md +46 -0
  110. package/docs/evaluation-safety.md +27 -0
  111. package/docs/experience-playbook.md +35 -0
  112. package/docs/hermes.md +129 -0
  113. package/docs/library-api.md +31 -0
  114. package/docs/mcp-tool-reference.md +290 -0
  115. package/docs/provider-security.md +85 -0
  116. package/docs/state-model.md +113 -0
  117. package/docs/troubleshooting.md +95 -0
  118. package/docs/tui-operator-guide.md +75 -0
  119. package/package.json +3 -1
  120. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
package/dist/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
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
+ import { realpathSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { refreshAstgrepIndex } from "./astgrep-index.js";
4
6
  import { scanWorkspaceDelta } from "./index-store.js";
5
7
  import { startHermesShadowStdioServer, startStdioServer } from "./server.js";
@@ -20,11 +22,13 @@ import { buildOpenAiCompatibleBaseUrl, buildProviderDoctorCommands, defaultModel
20
22
  import { diagnoseChatRuntimeConfig, OpenAICompatibleClient, } from "./tui/openai-compatible.js";
21
23
  import { runShellCommand } from "./runtime-command.js";
22
24
  import { resolveHermesLaunchProfile } from "./hermes/launch-profile.js";
25
+ import { gcStaging } from "./safe-edit.js";
26
+ import { ROLE_TOOL_DEFAULTS } from "./ace-context.js";
23
27
  function printHelp() {
24
28
  console.log(`ACE Swarm CLI
25
29
 
26
30
  Usage:
27
- ace mcp Start MCP server over stdio
31
+ ace mcp [--profile <name>] Start MCP server over stdio (host-min|full; default full)
28
32
  ace mcp-shadow --tools <csv> Start filtered Hermes-local MCP shadow server over stdio
29
33
  ace serve Alias for mcp
30
34
  ace tui [options] Launch interactive TUI dashboard
@@ -32,12 +36,36 @@ Usage:
32
36
  ace connect [provider] Register a live local runtime or hosted provider in the store
33
37
  ace turnkey [options] Project minimal workspace bootstrap stubs from the ACE store
34
38
  ace doctor [options] Validate ACE runtime + MCP readiness
35
- ace cache [options] Cache ACE artifacts into ace-state.ace and optionally clean projections
39
+ ace goal <sub> [options] Drive a self-paced, evidence-gated goal loop
40
+ ace playbook <sub> [args] Inspect and curate the experience playbook (experimental)
41
+ ace eval [--adapt] [options] Run the eval harness / playbook adaptation cycle (experimental)
42
+ ace compact [--no-sweep] Compact the store and sweep loose generated artifacts
43
+ ace cache [options] DEPRECATED: ingest+clean only. Use \`ace compact\`.
44
+ ace migrate [store-path] Import a legacy file workspace into the store
45
+ ace repair Re-materialize host files and repair store assets
46
+ ace add skills [name|all] Install packaged skill packs into the store
47
+ ace fsck [--json] [--repair] Validate ACEPACK store structure and loose workspace artifacts
48
+ ace ls [prefix] [options] List store-backed artifacts (operational / knowledge / state)
49
+ ace read <path> [options] Read a store-backed or workspace file (aliases: get, cat)
36
50
  ace mcp-config [options] Print global/client MCP config snippet(s) from store
37
51
  ace preconfig Write .mcp-config/ bundle for all supported clients to workspace root
38
52
  ace paths Show resolved package/workspace paths
39
53
  ace help Show help
40
54
 
55
+ Options for goal:
56
+ start "<objective>" [--verify <cmd>] [--budget-hours N] [--max-replans N]
57
+ [--max-ticks N] [--goal-id <id>] [--plan-only] [--detach]
58
+ status [<goal_id>] List goals, or show one goal's plan and escalations
59
+ run <goal_id> Drive an already-executing goal to a terminal state
60
+ resume <goal_id> Revive a planning/blocked/stopped goal, then drive it
61
+ stop <goal_id> Request a cooperative stop (honoured between steps)
62
+
63
+ A goal only reaches \`done\` when its success contract is satisfied: every
64
+ required artifact exists and every verification command exits 0. Declare one
65
+ with \`--verify <cmd>\`, or by appending \`verify: <cmd>\` to the objective.
66
+ A goal that declares neither is refused at \`start\` with \`contract_uncheckable\`,
67
+ because nothing would stand between the model and \`done\` but its own summary.
68
+
41
69
  Options for tui:
42
70
  --provider <name> Provider override (ollama|llama.cpp|codex|gemini|claude|copilot|...)
43
71
  --model <name> Model name override (defaults from profile/settings discovery)
@@ -64,6 +92,22 @@ Options for init:
64
92
  --base-url <url> Runtime base URL override (local or OpenAI-compatible)
65
93
  --ollama-url <url> Legacy alias for --base-url
66
94
 
95
+ Options for turnkey:
96
+ --no-client-config-bundle Skip the host-facing config stubs turnkey creates by default
97
+
98
+ Options for mcp:
99
+ --profile <host-min|full> Select the visible tool schema (default: full)
100
+
101
+ Options for playbook add:
102
+ --reason-code <code> Scope the lesson to a matching failure reason code
103
+
104
+ Options for eval:
105
+ --model, --models <csv> Model name or comma-separated model names (required)
106
+ --battery <path> Evaluation battery JSON (default: packaged battery)
107
+ --out <dir> Scorecard output directory
108
+ --adapt Compare baseline and playbook-adapted runs
109
+ --epochs <n> Adaptation epochs (only with --adapt; default: 1)
110
+
67
111
  Options for connect:
68
112
  llama-server|llama-cli Optional launcher name for llama.cpp HF runtimes
69
113
  --hf <repo> Hugging Face model repo or local model identifier
@@ -88,6 +132,16 @@ Options for cache:
88
132
  --dry-run Preview what would be cached and cleaned (no writes/deletes)
89
133
  --no-clean Keep workspace ACE artifacts after caching them into ace-state.ace
90
134
 
135
+ Options for ls:
136
+ --keys Show raw store keys instead of logical paths
137
+ --json Machine-readable output
138
+
139
+ Options for read (aliases: get, cat):
140
+ --out <file> Materialize a copy to <file> instead of printing to stdout
141
+ --source Print the resolved source (virtual/store/disk) and key to stderr
142
+ --prefer <disk|store> When store and disk copies diverge, force which one is shown
143
+ --json Machine-readable output ({ path, source, key, divergent, content })
144
+
91
145
  Options for mcp-config:
92
146
  --client <name> codex|vscode|copilot|claude|cursor|antigravity
93
147
  --all Print all client snippets for optional global install
@@ -111,6 +165,75 @@ function readFlagValue(args, flag) {
111
165
  return undefined;
112
166
  return args[index + 1];
113
167
  }
168
+ /** `--flag`, `--flag=value`, or a short `-x`. `-5` is a number, not a flag. */
169
+ function isFlagToken(arg) {
170
+ return arg.startsWith("--") || /^-[A-Za-z]/.test(arg);
171
+ }
172
+ /**
173
+ * Positional arguments, with value-taking flags and their values removed.
174
+ * A bare `.filter(a => !a.startsWith("--"))` drops the flag but keeps its value,
175
+ * which silently folds `--budget-hours 2` into a goal objective. Supports both
176
+ * `--flag value` and `--flag=value`, plus `--` as an end-of-flags separator.
177
+ */
178
+ export function positionalArgs(args, valueFlags) {
179
+ const out = [];
180
+ for (let i = 0; i < args.length; i += 1) {
181
+ const arg = args[i];
182
+ if (arg === "--") {
183
+ out.push(...args.slice(i + 1));
184
+ break;
185
+ }
186
+ if (isFlagToken(arg)) {
187
+ if (!arg.includes("=") && valueFlags.has(arg))
188
+ i += 1; // consume the value
189
+ continue;
190
+ }
191
+ out.push(arg);
192
+ }
193
+ return out;
194
+ }
195
+ export const GOAL_VALUE_FLAGS = new Set([
196
+ "--budget-hours",
197
+ "--max-replans",
198
+ "--max-ticks",
199
+ "--goal-id",
200
+ "--verify",
201
+ ]);
202
+ /**
203
+ * A flag's numeric value, or `undefined` when absent. Exits non-zero on anything
204
+ * that is not a finite number: `Number("abc")` is `NaN`, `typeof NaN` is
205
+ * `"number"`, so an unvalidated NaN reaches `startGoal`, disables the budget it
206
+ * was meant to set, and then serialises to `null` — which makes the same budget
207
+ * fire immediately on the next reload.
208
+ */
209
+ function readNumberFlag(args, flag) {
210
+ const raw = readFlagValue(args, flag);
211
+ if (raw === undefined)
212
+ return undefined;
213
+ const value = Number(raw);
214
+ if (!Number.isFinite(value)) {
215
+ console.error(`${flag} expects a number, got: ${raw}`);
216
+ process.exit(1);
217
+ }
218
+ return value;
219
+ }
220
+ /** Every value of a repeatable flag, in order. Supports `--flag v` and `--flag=v`. */
221
+ function readFlagValues(args, flag) {
222
+ const out = [];
223
+ for (let i = 0; i < args.length; i += 1) {
224
+ const arg = args[i];
225
+ if (arg.startsWith(`${flag}=`))
226
+ out.push(arg.slice(flag.length + 1));
227
+ else if (arg === flag && i + 1 < args.length)
228
+ out.push(args[++i]);
229
+ }
230
+ return out.map((v) => v.trim()).filter(Boolean);
231
+ }
232
+ export const PLAYBOOK_ADD_VALUE_FLAGS = new Set([
233
+ "--section",
234
+ "--content",
235
+ "--reason-code",
236
+ ]);
114
237
  async function runCommandArray(command, args, options) {
115
238
  const [bin, ...prefixArgs] = command;
116
239
  if (!bin)
@@ -203,6 +326,76 @@ async function probeHermesShadowMcp(cliPath, toolAllowlist) {
203
326
  child.stdin.write(mcpFrame({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }));
204
327
  });
205
328
  }
329
+ // Start a shadow server with an arbitrary scope and return the tool names it actually
330
+ // serves (parsed from the tools/list response). Used to assert that non-coders scopes are
331
+ // really served — not just the hand-picked get_all_agents_summary — which is the WS-A gate.
332
+ async function listHermesShadowTools(cliPath, scope) {
333
+ return await new Promise((resolveProbe, rejectProbe) => {
334
+ const child = spawn(process.execPath, [cliPath, "mcp-shadow", "--tools", scope.join(",")], {
335
+ cwd: PACKAGE_ROOT,
336
+ stdio: ["pipe", "pipe", "pipe"],
337
+ });
338
+ let stdout = "";
339
+ let stderr = "";
340
+ const finish = () => {
341
+ const names = new Set();
342
+ for (const line of stdout.split(/\r?\n/)) {
343
+ if (!line.includes('"tools"'))
344
+ continue;
345
+ try {
346
+ const parsed = JSON.parse(line);
347
+ for (const tool of parsed.result?.tools ?? []) {
348
+ if (typeof tool.name === "string")
349
+ names.add(tool.name);
350
+ }
351
+ }
352
+ catch {
353
+ // partial/non-JSON line; ignore
354
+ }
355
+ }
356
+ return names;
357
+ };
358
+ const timeout = setTimeout(() => {
359
+ child.kill("SIGTERM");
360
+ const names = finish();
361
+ if (names.size > 0)
362
+ resolveProbe(names);
363
+ else
364
+ rejectProbe(new Error(`shadow scope probe timed out${stderr ? `: ${stderr.slice(0, 200)}` : ""}`));
365
+ }, 2500);
366
+ child.stdout.on("data", (chunk) => {
367
+ stdout += chunk.toString("utf8");
368
+ if (stdout.includes('"tools"')) {
369
+ clearTimeout(timeout);
370
+ child.kill("SIGTERM");
371
+ resolveProbe(finish());
372
+ }
373
+ });
374
+ child.stderr.on("data", (chunk) => {
375
+ stderr += chunk.toString("utf8");
376
+ });
377
+ child.on("error", (error) => {
378
+ clearTimeout(timeout);
379
+ rejectProbe(error);
380
+ });
381
+ child.on("close", () => {
382
+ clearTimeout(timeout);
383
+ resolveProbe(finish());
384
+ });
385
+ child.stdin.write(mcpFrame({
386
+ jsonrpc: "2.0",
387
+ id: 1,
388
+ method: "initialize",
389
+ params: {
390
+ protocolVersion: "2024-11-05",
391
+ capabilities: {},
392
+ clientInfo: { name: "ace-doctor", version: "0.0.0" },
393
+ },
394
+ }));
395
+ child.stdin.write(mcpFrame({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }));
396
+ child.stdin.write(mcpFrame({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }));
397
+ });
398
+ }
206
399
  async function appendHermesLocalReadinessChecks(checks, profile) {
207
400
  const hermesRoot = profile.hermes_root;
208
401
  const workerPath = profile.worker_path;
@@ -276,6 +469,27 @@ async function appendHermesLocalReadinessChecks(checks, profile) {
276
469
  detail: error instanceof Error ? error.message : String(error),
277
470
  });
278
471
  }
472
+ // WS-A gate: assert a computed non-coders (orchestrator) scope is actually served, so
473
+ // the D1 zero-tool regression cannot recur for conversational/orchestration turns.
474
+ try {
475
+ const orchestratorScope = ROLE_TOOL_DEFAULTS["orchestrator"] ?? [];
476
+ const served = await listHermesShadowTools(cliPath, orchestratorScope);
477
+ const covered = orchestratorScope.filter((tool) => served.has(tool));
478
+ checks.push({
479
+ name: "ACE shadow MCP non-coders scope",
480
+ ok: covered.length >= 3,
481
+ detail: covered.length >= 3
482
+ ? `orchestrator scope served ${covered.length}/${orchestratorScope.length} tools (e.g. ${covered.slice(0, 3).join(", ")})`
483
+ : `orchestrator scope served only ${covered.length} tools — expected >=3 (D1 zero-tool regression risk)`,
484
+ });
485
+ }
486
+ catch (error) {
487
+ checks.push({
488
+ name: "ACE shadow MCP non-coders scope",
489
+ ok: false,
490
+ detail: error instanceof Error ? error.message : String(error),
491
+ });
492
+ }
279
493
  }
280
494
  else {
281
495
  checks.push({
@@ -412,6 +626,8 @@ async function runConnect(args) {
412
626
  includeClientConfigBundle: false,
413
627
  launcherCommand: parsed.launcherCommand,
414
628
  });
629
+ const { waitForPendingLifecycleSweep } = await import("./store/materializers/projection-manager.js");
630
+ await waitForPendingLifecycleSweep();
415
631
  console.log("ACE connect complete");
416
632
  console.log(`Workspace: ${WORKSPACE_ROOT}`);
417
633
  console.log(`Store: ${storeResult.storePath}`);
@@ -520,6 +736,10 @@ async function runInit(args, mode = "init") {
520
736
  // Drain that queue before appending the init run-ledger entry so init/turnkey
521
737
  // never race two packed-store writers in the same process.
522
738
  await waitForPendingStatusEventMirrors();
739
+ {
740
+ const { waitForPendingLifecycleSweep } = await import("./store/materializers/projection-manager.js");
741
+ await waitForPendingLifecycleSweep();
742
+ }
523
743
  // Record CLI execution in run-ledger for observability
524
744
  try {
525
745
  await appendRunLedgerEntrySafe({
@@ -616,6 +836,22 @@ async function runDoctor(args) {
616
836
  : "Store-backed MCP snippets are baked into ace-state.ace. Run `ace mcp-config --all` for global install."
617
837
  : "No MCP bridge or baked snippets found. Run `ace init` first.",
618
838
  });
839
+ const { fsckWorkspaceStore } = await import("./store/fsck.js");
840
+ const fsck = fsckWorkspaceStore(WORKSPACE_ROOT);
841
+ checks.push({
842
+ name: "ACEPACK fsck",
843
+ ok: fsck.ok,
844
+ detail: fsck.ok
845
+ ? `${fsck.checks.length} checks passed`
846
+ : `${fsck.checks.filter((check) => !check.ok).length} fsck checks failed; run ace fsck`,
847
+ });
848
+ checks.push({
849
+ name: "ACEPACK store-first mode",
850
+ ok: true,
851
+ detail: process.env.ACE_STORE_FIRST === "0"
852
+ ? "legacy disk precedence enabled via ACE_STORE_FIRST=0; projection allowlist: .agents/ACE/tasks/todo.md"
853
+ : "store-first reads enabled; projection allowlist: .agents/ACE/tasks/todo.md",
854
+ });
619
855
  const profilePath = `${getWorkspaceStorePath(WORKSPACE_ROOT)}#state/runtime/llm_profile`;
620
856
  const hasProfile = Boolean(readLlmProfile());
621
857
  checks.push({
@@ -763,7 +999,7 @@ async function runDoctor(args) {
763
999
  selectedModel: model,
764
1000
  available: modelNames.length > 0,
765
1001
  });
766
- checks.push({
1002
+ const requestedModelCheck = {
767
1003
  name: provider === "ollama" ? "Requested model installed" : "Requested model served",
768
1004
  ok: hasModel,
769
1005
  detail: provider === "ollama"
@@ -773,7 +1009,8 @@ async function runDoctor(args) {
773
1009
  : hasModel
774
1010
  ? `${model} found`
775
1011
  : `${model} not reported by llama.cpp (available: ${modelNames.join(", ") || "none"})`,
776
- });
1012
+ };
1013
+ checks.push(requestedModelCheck);
777
1014
  if (provider === "ollama" && !hasModel && repairOllama) {
778
1015
  const repair = await runShellCommand(`ollama pull ${model}`, {
779
1016
  cwd: WORKSPACE_ROOT,
@@ -811,6 +1048,28 @@ async function runDoctor(args) {
811
1048
  ? `ollama pull ${model} completed`
812
1049
  : `ollama pull ${model} failed with exit ${repair.exit_code}: ${repair.stderr || repair.stdout || "no output"}`,
813
1050
  });
1051
+ if (repair.exit_code === 0) {
1052
+ try {
1053
+ modelNames = await listLocalRuntimeModels("ollama", baseUrl);
1054
+ const repaired = modelNames.includes(model);
1055
+ requestedModelCheck.ok = repaired;
1056
+ requestedModelCheck.detail = repaired
1057
+ ? `${model} found after repair re-probe`
1058
+ : `${model} still missing after successful pull; runtime re-probe returned ${modelNames.join(", ") || "no models"}`;
1059
+ checks.push({
1060
+ name: "Ollama repair re-probe",
1061
+ ok: repaired,
1062
+ detail: requestedModelCheck.detail,
1063
+ });
1064
+ }
1065
+ catch (error) {
1066
+ checks.push({
1067
+ name: "Ollama repair re-probe",
1068
+ ok: false,
1069
+ detail: `pull succeeded but readiness re-probe failed: ${error instanceof Error ? error.message : String(error)}`,
1070
+ });
1071
+ }
1072
+ }
814
1073
  }
815
1074
  }
816
1075
  else {
@@ -950,11 +1209,277 @@ async function runMcpConfig(args) {
950
1209
  const body = readStoredMcpSnippet(client) ?? getMcpServerConfigSnippet(client);
951
1210
  console.log(body);
952
1211
  }
1212
+ async function runEvalCli(args) {
1213
+ const { mkdirSync, writeFileSync } = await import("node:fs");
1214
+ const { join, resolve } = await import("node:path");
1215
+ const harness = await import("./eval-harness.js");
1216
+ // Ships with the package (`assets` is in package.json files[]). The battery
1217
+ // used to live under test/, which is not published — `ace eval` was therefore
1218
+ // impossible to run from a global install. See E1 in
1219
+ // REPORT_GOAL_EVAL_PLAYBOOK_MITIGATIONS_2026-07-09.md.
1220
+ const defaultBattery = resolve(DEFAULTS_ROOT, "eval-battery", "battery.json");
1221
+ const batteryPath = readFlagValue(args, "--battery") ?? defaultBattery;
1222
+ const models = (readFlagValue(args, "--models") ?? readFlagValue(args, "--model") ?? "").split(",").map((m) => m.trim()).filter(Boolean);
1223
+ const outDir = resolve(WORKSPACE_ROOT, readFlagValue(args, "--out") ?? "agent-state/eval-reports");
1224
+ const adapt = args.includes("--adapt");
1225
+ if (models.length === 0) {
1226
+ console.error("Usage: ace eval [--adapt] --model <name>[,<name>...] [--battery <path>] [--out <dir>]");
1227
+ process.exit(1);
1228
+ }
1229
+ if (adapt) {
1230
+ const adaptation = await import("./playbook-adaptation.js");
1231
+ const epochs = Number(readFlagValue(args, "--epochs") ?? "1");
1232
+ // Validate the battery before creating anything: a bad path used to leave an
1233
+ // empty agent-state/eval-reports/ behind.
1234
+ const battery = harness.loadBattery(batteryPath);
1235
+ mkdirSync(outDir, { recursive: true });
1236
+ console.log(`Adaptation battery: ${battery.tasks.length} tasks (${batteryPath}) × ${models.length} model(s)`);
1237
+ for (const model of models) {
1238
+ const report = await adaptation.runAdaptationCycle({
1239
+ model,
1240
+ batteryPath,
1241
+ workspaceRoot: WORKSPACE_ROOT,
1242
+ epochs: Number.isFinite(epochs) ? epochs : 1,
1243
+ outputDir: outDir,
1244
+ });
1245
+ console.log(` ${model}: baseline ${report.baseline.summary.passed}/${report.baseline.summary.total}, ` +
1246
+ `adapted ${report.adapted.summary.passed}/${report.adapted.summary.total}, ` +
1247
+ `playbook added=${report.playbook_stats.added} merged=${report.playbook_stats.merged}`);
1248
+ console.log(` → ${report.report_path}`);
1249
+ }
1250
+ return;
1251
+ }
1252
+ const battery = harness.loadBattery(batteryPath);
1253
+ mkdirSync(outDir, { recursive: true });
1254
+ console.log(`Eval battery: ${battery.tasks.length} tasks (seed ${battery.seed}) × ${models.length} model(s)`);
1255
+ for (const model of models) {
1256
+ const runner = harness.makeLiveRunner(WORKSPACE_ROOT);
1257
+ const card = await harness.runBattery({ battery, model, runner, workspaceRoot: WORKSPACE_ROOT });
1258
+ const stem = `scorecard-${model.replace(/[^a-zA-Z0-9_.-]/g, "_")}`;
1259
+ writeFileSync(join(outDir, `${stem}.json`), JSON.stringify(card, null, 2));
1260
+ writeFileSync(join(outDir, `${stem}.md`), harness.scorecardToMarkdown(card));
1261
+ console.log(` ${model}: ${card.summary.passed}/${card.summary.total} passed (${(card.summary.success_rate * 100).toFixed(1)}%), ` +
1262
+ `parse_repairs=${card.summary.total_parse_repairs}, false_completions=${card.summary.total_false_completion_demotions}`);
1263
+ console.log(` → ${join(outDir, `${stem}.md`)}`);
1264
+ }
1265
+ }
1266
+ async function runPlaybookCli(args) {
1267
+ const { PLAYBOOK_SECTIONS, PLAYBOOK_STATUSES, isPlaybookSection, isPlaybookStatus, PlaybookRepository, materializePlaybookMarkdown, renderPlaybookMarkdown } = await import("./store/repositories/playbook-repository.js");
1268
+ const { curate } = await import("./playbook-curator.js");
1269
+ const repo = new PlaybookRepository(WORKSPACE_ROOT);
1270
+ const subcommand = args[0] ?? "list";
1271
+ if (subcommand === "list") {
1272
+ const section = readFlagValue(args, "--section");
1273
+ const status = readFlagValue(args, "--status");
1274
+ if (section && !isPlaybookSection(section)) {
1275
+ console.error(`--section expects ${PLAYBOOK_SECTIONS.join("|")}, got: ${section}`);
1276
+ process.exit(1);
1277
+ }
1278
+ if (status && !isPlaybookStatus(status)) {
1279
+ console.error(`--status expects ${PLAYBOOK_STATUSES.join("|")}, got: ${status}`);
1280
+ process.exit(1);
1281
+ }
1282
+ const validSection = section && isPlaybookSection(section) ? section : undefined;
1283
+ const validStatus = status && isPlaybookStatus(status) ? status : undefined;
1284
+ materializePlaybookMarkdown(WORKSPACE_ROOT);
1285
+ console.log(renderPlaybookMarkdown(repo, { section: validSection, status: validStatus }));
1286
+ return;
1287
+ }
1288
+ if (subcommand === "show") {
1289
+ const id = args[1];
1290
+ if (!id) {
1291
+ console.error("Usage: ace playbook show <id>");
1292
+ process.exit(1);
1293
+ }
1294
+ const bullet = repo.getBullet(id);
1295
+ if (!bullet) {
1296
+ console.error(`Playbook bullet not found: ${id}`);
1297
+ process.exit(1);
1298
+ }
1299
+ console.log(JSON.stringify(bullet, null, 2));
1300
+ return;
1301
+ }
1302
+ if (subcommand === "add") {
1303
+ const content = readFlagValue(args, "--content") ??
1304
+ positionalArgs(args.slice(1), PLAYBOOK_ADD_VALUE_FLAGS).join(" ").trim();
1305
+ if (!content) {
1306
+ console.error("Usage: ace playbook add --content <text> [--section verification] [--reason-code <code>]");
1307
+ process.exit(1);
1308
+ }
1309
+ const section = readFlagValue(args, "--section") ?? "failure_modes";
1310
+ if (!isPlaybookSection(section)) {
1311
+ console.error(`--section expects ${PLAYBOOK_SECTIONS.join("|")}, got: ${section}`);
1312
+ process.exit(1);
1313
+ }
1314
+ const reason = readFlagValue(args, "--reason-code");
1315
+ const bullet = repo.createBullet({
1316
+ section,
1317
+ scope: reason ? { reason_code: reason } : {},
1318
+ content,
1319
+ evidence_refs: [],
1320
+ provenance: { source: "operator" },
1321
+ });
1322
+ const path = materializePlaybookMarkdown(WORKSPACE_ROOT);
1323
+ console.log(`Added ${bullet.bullet_id}; projection ${path}`);
1324
+ return;
1325
+ }
1326
+ if (subcommand === "tag") {
1327
+ const id = args[1];
1328
+ const tag = args[2] === "helpful" || args[2] === "harmful" ? args[2] : undefined;
1329
+ if (!id || !tag) {
1330
+ console.error("Usage: ace playbook tag <id> helpful|harmful");
1331
+ process.exit(1);
1332
+ }
1333
+ // curate() skips unknown bullet ids (`if (!bullet) continue`), so a typo used
1334
+ // to print success and exit 0.
1335
+ const curated = await curate(repo, [{ type: "TAG", bullet_id: id, tag }]);
1336
+ if (curated.tagged === 0) {
1337
+ console.error(`Playbook bullet not found: ${id}`);
1338
+ process.exit(1);
1339
+ }
1340
+ const path = materializePlaybookMarkdown(WORKSPACE_ROOT);
1341
+ console.log(`Tagged ${id} ${tag}; projection ${path}`);
1342
+ return;
1343
+ }
1344
+ if (subcommand === "gc") {
1345
+ const result = await curate(repo, [], {});
1346
+ const path = materializePlaybookMarkdown(WORKSPACE_ROOT);
1347
+ console.log(`Playbook GC: pruned=${result.pruned} quarantined=${result.quarantined}; projection ${path}`);
1348
+ return;
1349
+ }
1350
+ console.error("Usage: ace playbook list|show <id>|add|tag <id> helpful|harmful|gc");
1351
+ process.exit(1);
1352
+ }
1353
+ async function runGoalCli(args) {
1354
+ try {
1355
+ await runGoalCliInner(args);
1356
+ }
1357
+ catch (error) {
1358
+ // Name-based rather than `instanceof`: goal-engine.js is dynamically
1359
+ // imported, so the class identity is not guaranteed to be shared.
1360
+ const name = error?.name;
1361
+ if (name === "UnknownGoalError") {
1362
+ console.error(`Unknown goal: ${error.goal_id ?? args[1]}`);
1363
+ process.exit(1);
1364
+ }
1365
+ if (name === "GoalNotResumableError") {
1366
+ console.error(error.message);
1367
+ process.exit(1);
1368
+ }
1369
+ throw error;
1370
+ }
1371
+ }
1372
+ async function runGoalCliInner(args) {
1373
+ const sub = (args[0] ?? "").toLowerCase();
1374
+ const engine = await import("./goal-engine.js");
1375
+ if (sub === "start") {
1376
+ const objective = positionalArgs(args.slice(1), GOAL_VALUE_FLAGS).join(" ").trim();
1377
+ if (!objective) {
1378
+ console.error('Usage: ace goal start "<objective>" [--verify <cmd>] [--budget-hours N] [--max-replans N] [--max-ticks N] [--goal-id <id>] [--plan-only] [--detach]');
1379
+ process.exit(1);
1380
+ }
1381
+ const started = await engine.startGoal(objective, {
1382
+ budget_hours: readNumberFlag(args, "--budget-hours"),
1383
+ max_replans: readNumberFlag(args, "--max-replans"),
1384
+ max_ticks: readNumberFlag(args, "--max-ticks"),
1385
+ goal_id: readFlagValue(args, "--goal-id"),
1386
+ verify_commands: readFlagValues(args, "--verify"),
1387
+ plan_only: args.includes("--plan-only"),
1388
+ });
1389
+ console.log(`Goal ${started.goal.goal_id}: ${started.goal.status}`);
1390
+ if (!started.ok) {
1391
+ console.error(` reason: ${started.reason_code}`);
1392
+ (started.blocking_findings ?? []).forEach((f) => console.error(` - ${f}`));
1393
+ process.exit(2);
1394
+ }
1395
+ if (args.includes("--plan-only"))
1396
+ return;
1397
+ if (args.includes("--detach")) {
1398
+ const { spawn } = await import("node:child_process");
1399
+ const { mkdirSync, openSync } = await import("node:fs");
1400
+ const { join } = await import("node:path");
1401
+ // `stdio: "ignore"` used to discard everything: a detached engine that died
1402
+ // at import time left the goal `executing` with a stale heartbeat and no
1403
+ // diagnostic anywhere. Give the child somewhere to say what happened.
1404
+ const goalsDir = join(WORKSPACE_ROOT, "agent-state", "goals");
1405
+ mkdirSync(goalsDir, { recursive: true });
1406
+ const logPath = join(goalsDir, `${started.goal.goal_id}.log`);
1407
+ const log = openSync(logPath, "a");
1408
+ const child = spawn(process.execPath, [process.argv[1], "goal", "run", started.goal.goal_id], {
1409
+ detached: true,
1410
+ stdio: ["ignore", log, log],
1411
+ });
1412
+ child.unref();
1413
+ console.log(` detached engine pid=${child.pid}; poll with: ace goal status ${started.goal.goal_id}`);
1414
+ console.log(` child output: ${logPath}`);
1415
+ return;
1416
+ }
1417
+ const run = await engine.runGoal(started.goal.goal_id);
1418
+ console.log(`Goal ${run.goal.goal_id} finished: ${run.goal.status}${run.goal.reason_code ? ` (${run.goal.reason_code})` : ""}`);
1419
+ return;
1420
+ }
1421
+ if (sub === "run" || sub === "resume") {
1422
+ const goalId = args[1];
1423
+ if (!goalId) {
1424
+ console.error(`Usage: ace goal ${sub} <goal_id>`);
1425
+ process.exit(1);
1426
+ }
1427
+ const run = sub === "resume" ? await engine.resumeGoal(goalId) : await engine.runGoal(goalId);
1428
+ console.log(`Goal ${run.goal.goal_id}: ${run.goal.status}${run.goal.reason_code ? ` (${run.goal.reason_code})` : ""}`);
1429
+ return;
1430
+ }
1431
+ if (sub === "stop") {
1432
+ const goalId = args[1];
1433
+ if (!goalId) {
1434
+ console.error("Usage: ace goal stop <goal_id>");
1435
+ process.exit(1);
1436
+ }
1437
+ const goal = engine.stopGoal(goalId);
1438
+ console.log(`Goal ${goal.goal_id}: stop requested (status ${goal.status}).`);
1439
+ return;
1440
+ }
1441
+ if (sub === "status") {
1442
+ const goalId = args[1];
1443
+ if (goalId) {
1444
+ const view = engine.getGoalStatus(goalId);
1445
+ if (!view) {
1446
+ console.error(`Unknown goal: ${goalId}`);
1447
+ process.exit(1);
1448
+ }
1449
+ console.log(`Goal ${view.goal.goal_id}: ${view.goal.status}`);
1450
+ console.log(` objective: ${view.goal.objective}`);
1451
+ console.log(` ticks: ${view.goal.tick_count} replans: ${view.goal.replan_count} last_heartbeat: ${view.goal.last_heartbeat_at ?? "-"}`);
1452
+ console.log(` plan: ${view.plan_status ?? "none"} ready: ${view.ready_steps.join(", ") || "-"}`);
1453
+ if (view.goal.escalations.length > 0) {
1454
+ console.log(" escalations:");
1455
+ view.goal.escalations.forEach((e) => console.log(` - [${e.kind}] ${e.reason_code}: ${e.summary}`));
1456
+ }
1457
+ return;
1458
+ }
1459
+ const goals = engine.listGoals();
1460
+ if (goals.length === 0) {
1461
+ console.log("No goals.");
1462
+ return;
1463
+ }
1464
+ for (const g of goals) {
1465
+ console.log(`${g.goal_id}\t${g.status}\t${g.objective}`);
1466
+ }
1467
+ return;
1468
+ }
1469
+ console.error("Usage: ace goal <start|status|stop|resume|run> ...");
1470
+ process.exit(1);
1471
+ }
953
1472
  async function main() {
954
1473
  const args = process.argv.slice(2);
955
1474
  const command = (args[0] ?? "mcp").toLowerCase();
956
1475
  if (command === "mcp" || command === "serve") {
957
- await startStdioServer();
1476
+ gcStaging();
1477
+ const profileArg = (readFlagValue(args.slice(1), "--profile") ?? "full").toLowerCase();
1478
+ if (profileArg !== "host-min" && profileArg !== "full") {
1479
+ process.stderr.write(`ace ${command}: error: --profile must be 'host-min' or 'full' (got '${profileArg}')\n`);
1480
+ process.exit(1);
1481
+ }
1482
+ await startStdioServer(true, profileArg);
958
1483
  return;
959
1484
  }
960
1485
  if (command === "mcp-shadow") {
@@ -963,6 +1488,13 @@ async function main() {
963
1488
  .split(",")
964
1489
  .map((tool) => tool.trim())
965
1490
  .filter(Boolean);
1491
+ if (tools.length === 0) {
1492
+ process.stderr.write("ace mcp-shadow: error: --tools must be a non-empty comma-separated list " +
1493
+ "(reason_code=hermes_empty_tool_scope). " +
1494
+ "An empty tool surface means the Hermes model has no ACE capabilities.\n");
1495
+ process.exit(1);
1496
+ }
1497
+ gcStaging();
966
1498
  await startHermesShadowStdioServer(tools);
967
1499
  return;
968
1500
  }
@@ -1146,6 +1678,8 @@ async function main() {
1146
1678
  const { repairWorkspace } = await import("./store/bootstrap-store.js");
1147
1679
  console.log(`Repairing workspace at ${WORKSPACE_ROOT}...`);
1148
1680
  const warnings = await repairWorkspace(WORKSPACE_ROOT);
1681
+ const { waitForPendingLifecycleSweep } = await import("./store/materializers/projection-manager.js");
1682
+ await waitForPendingLifecycleSweep();
1149
1683
  if (warnings.length) {
1150
1684
  for (const w of warnings)
1151
1685
  console.warn(` ! ${w}`);
@@ -1157,16 +1691,50 @@ async function main() {
1157
1691
  }
1158
1692
  if (command === "compact") {
1159
1693
  const { compactWorkspace } = await import("./store/bootstrap-store.js");
1160
- const result = await compactWorkspace(WORKSPACE_ROOT);
1694
+ const noSweep = args.includes("--no-sweep");
1695
+ const result = await compactWorkspace(WORKSPACE_ROOT, { sweep: !noSweep });
1161
1696
  const savedKb = Math.round((result.before - result.after) / 1024);
1162
1697
  console.log(`Store compacted: ${result.before} → ${result.after} bytes (saved ${savedKb}KB)`);
1698
+ if (result.sweep) {
1699
+ console.log(`Sweep: folded ${result.sweep.cached_files} artifact(s) into the store, freed ${result.sweep.removed_files} loose file(s) from disk, semantic GC removed ${result.sweep.removed_semantic_cache_files}`);
1700
+ if (result.sweep.removed_files > 0) {
1701
+ console.log(" Swept files are stored, not lost — read them back with `ace read <path>` (e.g. `ace read agent-state/TASK.md`). Do not recreate them.");
1702
+ }
1703
+ }
1704
+ return;
1705
+ }
1706
+ if (command === "fsck") {
1707
+ const { fsckWorkspaceStore } = await import("./store/fsck.js");
1708
+ const asJson = args.includes("--json");
1709
+ const repair = args.includes("--repair");
1710
+ const result = fsckWorkspaceStore(WORKSPACE_ROOT, { repair });
1711
+ if (asJson) {
1712
+ console.log(JSON.stringify(result, null, 2));
1713
+ }
1714
+ else {
1715
+ console.log("ACE fsck");
1716
+ console.log(`Workspace: ${WORKSPACE_ROOT}`);
1717
+ console.log(`Store: ${result.storePath}`);
1718
+ for (const check of result.checks) {
1719
+ console.log(`- [${check.ok ? "PASS" : "FAIL"}] ${check.name}: ${check.detail}`);
1720
+ }
1721
+ if (result.repaired_paths.length > 0) {
1722
+ console.log("Repaired paths:");
1723
+ for (const path of result.repaired_paths)
1724
+ console.log(`- ${path}`);
1725
+ }
1726
+ }
1727
+ if (!result.ok)
1728
+ process.exitCode = 1;
1163
1729
  return;
1164
1730
  }
1165
1731
  if (command === "cache") {
1166
1732
  const { cacheWorkspaceArtifacts } = await import("./store/cache-workspace.js");
1167
1733
  const dryRun = args.includes("--dry-run");
1168
1734
  const clean = !args.includes("--no-clean");
1735
+ const stagingGc = dryRun ? gcStaging({ max_age_hours: Number.POSITIVE_INFINITY }) : gcStaging();
1169
1736
  const result = await cacheWorkspaceArtifacts(WORKSPACE_ROOT, { dryRun, clean });
1737
+ console.warn("ace cache is deprecated; use `ace compact` for compaction plus workspace sweep.");
1170
1738
  console.log("ACE cache complete");
1171
1739
  console.log(`Workspace: ${WORKSPACE_ROOT}`);
1172
1740
  console.log(`Store: ${result.storePath}`);
@@ -1174,7 +1742,9 @@ async function main() {
1174
1742
  console.log(`Cached files: ${result.cached_files}`);
1175
1743
  console.log(`Kept projected files: ${result.kept_projected_files}`);
1176
1744
  console.log(`Skipped files: ${result.skipped_files}`);
1177
- console.log(`Removed files: ${result.removed_files}`);
1745
+ console.log(`Removed files: ${result.removed_files} (folded into the store, not lost — read back with \`ace read <path>\`; do not recreate)`);
1746
+ console.log(`Semantic cache GC removed files: ${result.removed_semantic_cache_files}`);
1747
+ console.log(`Staging GC removed entries: ${stagingGc.removed_entries}`);
1178
1748
  if (result.warnings.length > 0) {
1179
1749
  console.warn(`Warnings (${result.warnings.length}):`);
1180
1750
  for (const warning of result.warnings)
@@ -1182,6 +1752,28 @@ async function main() {
1182
1752
  }
1183
1753
  return;
1184
1754
  }
1755
+ if (command === "ls") {
1756
+ const { runLsCommand } = await import("./store/store-cli.js");
1757
+ await runLsCommand(WORKSPACE_ROOT, args.slice(1));
1758
+ return;
1759
+ }
1760
+ if (command === "read" || command === "get" || command === "cat") {
1761
+ const { runReadCommand } = await import("./store/store-cli.js");
1762
+ await runReadCommand(WORKSPACE_ROOT, args.slice(1));
1763
+ return;
1764
+ }
1765
+ if (command === "goal") {
1766
+ await runGoalCli(args.slice(1));
1767
+ return;
1768
+ }
1769
+ if (command === "eval") {
1770
+ await runEvalCli(args.slice(1));
1771
+ return;
1772
+ }
1773
+ if (command === "playbook") {
1774
+ await runPlaybookCli(args.slice(1));
1775
+ return;
1776
+ }
1185
1777
  if (command === "help" || command === "--help" || command === "-h") {
1186
1778
  printHelp();
1187
1779
  return;
@@ -1190,8 +1782,28 @@ async function main() {
1190
1782
  printHelp();
1191
1783
  process.exit(1);
1192
1784
  }
1193
- main().catch((err) => {
1194
- console.error("Fatal error:", err);
1195
- process.exit(1);
1196
- });
1785
+ /**
1786
+ * True when this module is the process entrypoint. `process.argv[1]` is the
1787
+ * path as invoked, which for a global install is the `ace` bin symlink, so both
1788
+ * sides are realpath'd before comparison. Guarding the `main()` call keeps the
1789
+ * module importable (tests exercise `positionalArgs` directly) instead of
1790
+ * running the CLI as an import side effect.
1791
+ */
1792
+ function isCliEntrypoint() {
1793
+ const entry = process.argv[1];
1794
+ if (!entry)
1795
+ return false;
1796
+ try {
1797
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
1798
+ }
1799
+ catch {
1800
+ return false;
1801
+ }
1802
+ }
1803
+ if (isCliEntrypoint()) {
1804
+ main().catch((err) => {
1805
+ console.error("Fatal error:", err);
1806
+ process.exit(1);
1807
+ });
1808
+ }
1197
1809
  //# sourceMappingURL=cli.js.map