@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.
- package/CHANGELOG.md +69 -0
- package/README.md +72 -532
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +41 -12
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +623 -11
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +57 -3
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +39 -18
- package/dist/tools-framework.js +31 -3
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +127 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +3 -1
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|