@voybio/ace-swarm 2.4.4 → 2.4.6

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 +36 -9
  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 +619 -10
  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 +125 -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
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ACE Goal Engine tool registrations (WS-F).
3
+ *
4
+ * These let a premium host delegate a long-running objective to ACE and poll
5
+ * it. `start_goal`/`resume_goal` never block the MCP server for the life of the
6
+ * goal: they compile/prepare, then hand execution to a detached `ace goal run`
7
+ * child and return immediately. The host polls `get_goal_status`.
8
+ */
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ export declare function registerGoalTools(server: McpServer): void;
11
+ //# sourceMappingURL=tools-goal.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ACE Goal Engine tool registrations (WS-F).
3
+ *
4
+ * These let a premium host delegate a long-running objective to ACE and poll
5
+ * it. `start_goal`/`resume_goal` never block the MCP server for the life of the
6
+ * goal: they compile/prepare, then hand execution to a detached `ace goal run`
7
+ * child and return immediately. The host polls `get_goal_status`.
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ import { fileURLToPath } from "node:url";
11
+ import { z } from "zod";
12
+ import { getGoalStatus, listGoals, startGoal, stopGoal, } from "./goal-engine.js";
13
+ function asJsonBlock(value) {
14
+ return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
15
+ }
16
+ function spawnDetachedRunner(goalId, verb) {
17
+ try {
18
+ // dist/cli.js is a sibling of this compiled module.
19
+ const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
20
+ const child = spawn(process.execPath, [cliPath, "goal", verb, goalId], {
21
+ detached: true,
22
+ stdio: "ignore",
23
+ });
24
+ child.unref();
25
+ return child.pid;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ export function registerGoalTools(server) {
32
+ server.tool("start_goal", "Compile and launch a long-running autonomous goal (ACE plans it, executes step-by-step under evidence-gated completion, and can never declare success by prose). Returns immediately; poll with get_goal_status.", {
33
+ objective: z.string().min(1).describe("What the goal should accomplish"),
34
+ budget_hours: z.number().positive().max(24).optional().describe("Wall-clock budget (default 4h)"),
35
+ max_replans: z.number().int().min(0).max(20).optional().describe("Max plan-level replans (default 3)"),
36
+ plan_only: z.boolean().optional().describe("Compile + validate the plan but do not execute"),
37
+ detach: z.boolean().optional().describe("Run in a detached background child (default true when not plan_only)"),
38
+ }, async ({ objective, budget_hours, max_replans, plan_only, detach }) => {
39
+ const started = await startGoal(objective, { budget_hours, max_replans, plan_only });
40
+ let runnerPid;
41
+ if (started.ok && !plan_only && detach !== false) {
42
+ runnerPid = spawnDetachedRunner(started.goal.goal_id, "run");
43
+ }
44
+ return {
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: [
49
+ started.ok ? "✅ Goal started" : "❌ Goal not started",
50
+ `goal_id: ${started.goal.goal_id}`,
51
+ `status: ${started.goal.status}`,
52
+ started.reason_code ? `reason: ${started.reason_code}` : "",
53
+ runnerPid ? `detached runner pid: ${runnerPid}` : "",
54
+ asJsonBlock({ ...started, runner_pid: runnerPid }),
55
+ ]
56
+ .filter(Boolean)
57
+ .join("\n"),
58
+ },
59
+ ],
60
+ };
61
+ });
62
+ server.tool("get_goal_status", "Get a goal's status (tick count, current step, last heartbeat, escalations, plan status), or list all goals when no goal_id is given.", {
63
+ goal_id: z.string().optional().describe("Goal id; omit to list all goals"),
64
+ }, async ({ goal_id }) => {
65
+ if (!goal_id) {
66
+ return {
67
+ content: [{ type: "text", text: asJsonBlock({ goals: listGoals() }) }],
68
+ };
69
+ }
70
+ const view = getGoalStatus(goal_id);
71
+ if (!view) {
72
+ return { content: [{ type: "text", text: `❌ Unknown goal: ${goal_id}` }] };
73
+ }
74
+ return { content: [{ type: "text", text: asJsonBlock(view) }] };
75
+ });
76
+ server.tool("stop_goal", "Request that a running goal stop at its next tick boundary.", { goal_id: z.string().describe("Goal id to stop") }, async ({ goal_id }) => {
77
+ try {
78
+ const goal = stopGoal(goal_id);
79
+ return {
80
+ content: [{ type: "text", text: `🛑 Stop requested for ${goal.goal_id} (status ${goal.status}).` }],
81
+ };
82
+ }
83
+ catch (err) {
84
+ return {
85
+ content: [{ type: "text", text: `❌ ${err instanceof Error ? err.message : String(err)}` }],
86
+ };
87
+ }
88
+ });
89
+ server.tool("resume_goal", "Resume a blocked/stopped goal: reset any step left running mid-crash and continue execution in a detached runner.", { goal_id: z.string().describe("Goal id to resume") }, async ({ goal_id }) => {
90
+ const view = getGoalStatus(goal_id);
91
+ if (!view) {
92
+ return { content: [{ type: "text", text: `❌ Unknown goal: ${goal_id}` }] };
93
+ }
94
+ const runnerPid = spawnDetachedRunner(goal_id, "resume");
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: [
100
+ `▶️ Resuming ${goal_id}`,
101
+ runnerPid ? `detached runner pid: ${runnerPid}` : "runner spawn failed; run `ace goal resume` manually",
102
+ ].join("\n"),
103
+ },
104
+ ],
105
+ };
106
+ });
107
+ }
108
+ //# sourceMappingURL=tools-goal.js.map
@@ -3,7 +3,8 @@
3
3
  */
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { z } from "zod";
6
- import { ACE_TASKS_ROOT_REL, normalizePathForValidation, readTaskArtifact, safeWriteAsync, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
6
+ import { ACE_TASKS_ROOT_REL, readTaskArtifact, WORKSPACE_ROOT, } from "./helpers.js";
7
+ import { writeStoreBlobQueued } from "./store/store-artifacts.js";
7
8
  import { ROLE_ENUM, HANDOFF_VALIDATION_MODE, HANDOFF_LIFECYCLE_STATUS_ENUM, } from "./shared.js";
8
9
  import { validateHandoffPayload, validateSwarmHandoffPayload, lintHandoffPayload, } from "./schemas.js";
9
10
  import { acknowledgeHandoffSafe, getHandoffRegistryPath, readHandoffRegistry, registerHandoffSafe, } from "./handoff-registry.js";
@@ -241,13 +242,8 @@ export function registerHandoffTools(server) {
241
242
  ],
242
243
  };
243
244
  }
244
- const timeStr = now
245
- .toISOString()
246
- .replace(/[-:]/g, "")
247
- .replace("T", "-")
248
- .slice(0, 15);
249
- const filePath = wsPath("tasks", `SWARM_HANDOFF.${from}_to_${to}_${timeStr}_${shortId}.json`);
250
- await safeWriteAsync(filePath, JSON.stringify(handoff, null, 2));
245
+ const payloadStoreKey = `state/handoffs/payloads/${handoff.handoff_id}.json`;
246
+ const payloadPath = await writeStoreBlobQueued(WORKSPACE_ROOT, payloadStoreKey, JSON.stringify(handoff, null, 2), { operation_label: "create_handoff.payload" });
251
247
  const registry = await registerHandoffSafe({
252
248
  handoff_id: handoff.handoff_id,
253
249
  from: handoff.router.from,
@@ -255,14 +251,14 @@ export function registerHandoffTools(server) {
255
251
  title: handoff.task.title,
256
252
  task_type: handoff.task.type,
257
253
  priority: handoff.task.priority,
258
- source_file: normalizePathForValidation(filePath),
254
+ source_file: payloadPath,
259
255
  });
260
256
  const runLedger = await appendRunLedgerEntrySafe({
261
257
  tool: "create_handoff",
262
258
  category: "major_update",
263
259
  message: `Created handoff ${handoff.handoff_id} (${handoff.router.from} -> ${handoff.router.to})`,
264
260
  artifacts: [
265
- normalizePathForValidation(filePath),
261
+ payloadPath,
266
262
  "agent-state/handoff-registry.json",
267
263
  ],
268
264
  metadata: {
@@ -282,7 +278,7 @@ export function registerHandoffTools(server) {
282
278
  "# Handoff Created",
283
279
  "",
284
280
  `Schema: ${validation.schema}`,
285
- `Saved to: ${filePath}`,
281
+ `Saved to: ${payloadPath}`,
286
282
  `Handoff registry: ${registry.path}`,
287
283
  `Run ledger: ${runLedger.path} (${runLedger.entry.id})`,
288
284
  `Scheduler job: ${schedulerSync.job_id} (${schedulerSync.enqueued ? "enqueued" : "already_exists"})`,
@@ -194,6 +194,8 @@ export function registerLifecycleTools(server) {
194
194
  "",
195
195
  `JSON: ${snapshot.json_path}`,
196
196
  `HTML: ${snapshot.html_path}`,
197
+ `Read: ace read agent-state/kanban.json`,
198
+ `Open: ace read agent-state/kanban.html --out /tmp/ace-kanban.html && open /tmp/ace-kanban.html`,
197
199
  `Updated: ${snapshot.generated_at}`,
198
200
  `Counts: backlog=${snapshot.counts.backlog}, blocked=${snapshot.counts.blocked}, in_progress=${snapshot.counts.in_progress}, gated=${snapshot.counts.gated}, done=${snapshot.counts.done}`,
199
201
  ].join("\n"),
@@ -2,7 +2,7 @@
2
2
  * Job scheduler tool registrations.
3
3
  */
4
4
  import { z } from "zod";
5
- import { acknowledgeJob, completeJob, dispatchJobNow, dispatchJobs, enqueueJob, getJobLockTablePath, getJobQueuePath, getSchedulerLeasePath, readJobQueue, readJobLockTable, readSchedulerLease, } from "./job-scheduler.js";
5
+ import { acknowledgeJob, completeJob, dispatchJobNow, dispatchJobs, enqueueJob, getJobLockTablePath, getJobQueuePath, getSchedulerLeasePath, heartbeatJob, reapStaleRunningJobsTick, readJobQueue, readJobLockTable, readSchedulerLease, } from "./job-scheduler.js";
6
6
  // Event emission handled in core job-scheduler.ts — no wrapper duplication.
7
7
  const PRIORITY_BAND_ENUM = z.enum(["P0", "P1", "P2", "P3"]);
8
8
  const JOB_STATUS_ENUM = z.enum([
@@ -196,6 +196,49 @@ export function registerSchedulerTools(server) {
196
196
  ],
197
197
  };
198
198
  });
199
+ server.tool("heartbeat_job", "Prove liveness for a running job so the heartbeat watchdog does not reap it; renews the owner lease", {
200
+ job_id: z.string(),
201
+ owner: z.string().optional().describe("Lease owner identity (defaults to capability-ops)"),
202
+ lease_ttl_seconds: z.number().int().min(5).max(3600).optional(),
203
+ now_iso: z.string().optional().describe("Optional deterministic clock override"),
204
+ }, async ({ job_id, owner, lease_ttl_seconds, now_iso }) => {
205
+ const result = await heartbeatJob({ job_id, owner, lease_ttl_seconds, now_iso });
206
+ return {
207
+ content: [
208
+ {
209
+ type: "text",
210
+ text: result.ok
211
+ ? ["✅ Job heartbeat recorded", asJsonBlock(result)].join("\n")
212
+ : ["❌ heartbeat_job failed", result.error ?? "Unknown error", asJsonBlock(result)].join("\n"),
213
+ },
214
+ ],
215
+ };
216
+ });
217
+ server.tool("reap_stale_jobs", "Run the heartbeat watchdog once: move running jobs with a stale worker heartbeat into unknown_recovery and release their locks", {
218
+ heartbeat_stale_seconds: z
219
+ .number()
220
+ .int()
221
+ .min(1)
222
+ .max(86_400)
223
+ .optional()
224
+ .describe("Staleness window in seconds (default 120)"),
225
+ now_iso: z.string().optional().describe("Optional deterministic clock override"),
226
+ }, async ({ heartbeat_stale_seconds, now_iso }) => {
227
+ const result = await reapStaleRunningJobsTick({ heartbeat_stale_seconds, now_iso });
228
+ return {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: [
233
+ result.reaped_jobs > 0
234
+ ? `🩺 Watchdog reaped ${result.reaped_jobs} stale job(s)`
235
+ : "🩺 Watchdog found no stale running jobs",
236
+ asJsonBlock(result),
237
+ ].join("\n"),
238
+ },
239
+ ],
240
+ };
241
+ });
199
242
  server.tool("get_lock_table", "Read the scheduler lock table", {}, async () => ({
200
243
  content: [
201
244
  {
@@ -49,9 +49,10 @@ export function registerSkillTools(server) {
49
49
  .describe("Optional workspace-relative path for the audit report"),
50
50
  }, async ({ write_artifact, artifact_path }) => {
51
51
  const result = auditSkillCatalog();
52
+ const artifactPath = artifact_path || "agent-state/SKILL_AUDIT_REPORT.md";
52
53
  const writtenPath = write_artifact === false
53
54
  ? undefined
54
- : writeSkillAuditReport(result, artifact_path || "agent-state/SKILL_AUDIT_REPORT.md");
55
+ : writeSkillAuditReport(result, artifactPath);
55
56
  const failCount = result.entries.filter((entry) => entry.status === "fail").length;
56
57
  const warnCount = result.entries.filter((entry) => entry.status === "warn").length;
57
58
  return {
@@ -62,7 +63,7 @@ export function registerSkillTools(server) {
62
63
  `Portable-contract-ready skills: ${result.summary.portable_contract_ready_skills}/${result.summary.total_skills}`,
63
64
  `Fail/Warn: ${failCount}/${warnCount}`,
64
65
  ...(result.rollout_backlog.slice(0, 5).map((line) => `- ${line}`)),
65
- ...(writtenPath ? [`- Artifact written: ${writtenPath}`] : []),
66
+ ...(writtenPath ? [`- Artifact: ${artifactPath}`, `- Read: ace read ${artifactPath}`, `- Store path: ${writtenPath}`] : []),
66
67
  ].join("\n"),
67
68
  },
68
69
  ],
@@ -4,7 +4,7 @@
4
4
  import { z } from "zod";
5
5
  import { ACE_TASKS_ROOT_REL, readTaskArtifact, resolveWritableTaskPath, safeWriteAsync, } from "./helpers.js";
6
6
  import { TODO_STATUS_ENUM } from "./shared.js";
7
- import { getTodoStatePath, readTodoState, syncTodoStateSafe, transitionTodoNodeSafe, } from "./todo-state.js";
7
+ import { appendTodoItemSafe, getTodoStatePath, readTodoState, syncTodoStateSafe, transitionTodoNodeSafe, } from "./todo-state.js";
8
8
  import { appendRunLedgerEntrySafe, getRunLedgerPath, readRunLedger, } from "./run-ledger.js";
9
9
  import { refreshKanbanArtifacts } from "./kanban.js";
10
10
  export function registerTodoTools(server) {
@@ -19,7 +19,7 @@ export function registerTodoTools(server) {
19
19
  },
20
20
  ],
21
21
  }));
22
- server.tool("update_todo", "Replace TODO board content or transition a TODO node status with gate validation", {
22
+ server.tool("update_todo", "Replace TODO board content, append a new TODO item, or transition a TODO node status with gate validation", {
23
23
  new_content: z
24
24
  .string()
25
25
  .optional()
@@ -30,7 +30,68 @@ export function registerTodoTools(server) {
30
30
  .describe("Todo node id to transition (transition mode)"),
31
31
  status: TODO_STATUS_ENUM.optional().describe("Target status for transition mode"),
32
32
  note: z.string().optional().describe("Optional transition context note"),
33
- }, async ({ new_content, node_id, status, note }) => {
33
+ append_title: z
34
+ .string()
35
+ .optional()
36
+ .describe("Title for a new TODO item to append atomically (append mode). Prefer this over " +
37
+ "new_content when adding a single item — it is safe under concurrent callers, " +
38
+ "unlike a read-then-full-replace round trip."),
39
+ append_section: z
40
+ .string()
41
+ .optional()
42
+ .describe("Section heading to append the new item under (default: Active Work)"),
43
+ append_depends_on: z
44
+ .array(z.string())
45
+ .optional()
46
+ .describe("Node ids the new append-mode item depends on"),
47
+ append_id: z
48
+ .string()
49
+ .optional()
50
+ .describe("Optional explicit node id for the new append-mode item"),
51
+ }, async ({ new_content, node_id, status, note, append_title, append_section, append_depends_on, append_id, }) => {
52
+ if (typeof append_title === "string") {
53
+ const appended = await appendTodoItemSafe(append_title, {
54
+ section: append_section,
55
+ depends_on: append_depends_on,
56
+ id: append_id,
57
+ });
58
+ if (!appended.ok) {
59
+ return {
60
+ content: [
61
+ { type: "text", text: `❌ ${appended.error ?? "Failed to append TODO item."}` },
62
+ ],
63
+ };
64
+ }
65
+ const runLedger = await appendRunLedgerEntrySafe({
66
+ tool: "update_todo",
67
+ category: "info",
68
+ message: `Appended TODO item ${appended.node_id}`,
69
+ artifacts: [`${ACE_TASKS_ROOT_REL}/todo.md`, "agent-state/todo-state.json"],
70
+ metadata: {
71
+ mode: "append",
72
+ node_id: appended.node_id,
73
+ section: appended.section,
74
+ },
75
+ });
76
+ const kanban = refreshKanbanArtifacts("tool:update_todo");
77
+ const counts = kanban.counts;
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: [
83
+ `✅ TODO item appended: ${appended.node_id}`,
84
+ `Section: ${appended.section}`,
85
+ `TODO state: ${appended.path}`,
86
+ `TODO markdown: ${appended.todo_path}`,
87
+ `Run ledger: ${runLedger.path} (${runLedger.entry.id})`,
88
+ `Kanban updated: ${kanban.html_path}`,
89
+ `Kanban counts: backlog=${counts.backlog}, blocked=${counts.blocked}, in_progress=${counts.in_progress}, gated=${counts.gated}, done=${counts.done}`,
90
+ ].join("\n"),
91
+ },
92
+ ],
93
+ };
94
+ }
34
95
  if (typeof new_content === "string") {
35
96
  const synced = await syncTodoStateSafe(new_content);
36
97
  const path = await safeWriteAsync(resolveWritableTaskPath("todo"), new_content);
package/dist/tools.js CHANGED
@@ -15,6 +15,7 @@ import { registerGitTools } from "./tools-git.js";
15
15
  import { registerMemoryTools } from "./tools-memory.js";
16
16
  import { registerDriftTools } from "./tools-drift.js";
17
17
  import { registerSchedulerTools } from "./tools-scheduler.js";
18
+ import { registerGoalTools } from "./tools-goal.js";
18
19
  import { registerSkillTools } from "./tools-skills.js";
19
20
  const TOOL_REGISTRARS = [
20
21
  registerAgentTools,
@@ -28,6 +29,7 @@ const TOOL_REGISTRARS = [
28
29
  registerMemoryTools,
29
30
  registerDriftTools,
30
31
  registerSchedulerTools,
32
+ registerGoalTools,
31
33
  registerSkillTools,
32
34
  ];
33
35
  function createAllowlistedServer(server, allowed) {
package/dist/tui/chat.js CHANGED
@@ -243,6 +243,10 @@ export class ChatSession extends EventEmitter {
243
243
  const activeProvider = this.activeStreamProvider ?? this.provider;
244
244
  this.streaming = false;
245
245
  if (this.activeAceBridge) {
246
+ if (this.executionEngine === "hermes_local") {
247
+ this.hermesExecutor?.interrupt?.();
248
+ return;
249
+ }
246
250
  this.aceBridge.interrupt();
247
251
  return;
248
252
  }
package/dist/tui/index.js CHANGED
@@ -190,7 +190,7 @@ export class AceTui {
190
190
  });
191
191
  // Handle signals
192
192
  process.on("SIGINT", () => {
193
- // Ctrl+C during streaming → interrupt; otherwise offer quit
193
+ // Ctrl+C interrupts an active stream; while idle it is deliberately a no-op.
194
194
  if (this.activeChatSession?.isStreaming()) {
195
195
  this.activeChatSession.interrupt();
196
196
  }
@@ -332,6 +332,9 @@ export class AceTui {
332
332
  case "w":
333
333
  this.closeCurrentTab();
334
334
  return;
335
+ case "z":
336
+ this.suspend();
337
+ return;
335
338
  }
336
339
  }
337
340
  // Alt+number → switch tab
@@ -36,6 +36,8 @@ export interface ChatRequest {
36
36
  model: string;
37
37
  messages: ChatMessage[];
38
38
  stream?: boolean;
39
+ /** "json" for generic JSON mode; a JSON Schema object for schema-constrained generation (Ollama ≥ 0.5). */
40
+ format?: "json" | Record<string, unknown> | string;
39
41
  options?: {
40
42
  temperature?: number;
41
43
  top_p?: number;
@@ -15,6 +15,19 @@ export interface OpenAICompatibleChatRequest {
15
15
  messages: OpenAICompatibleChatMessage[];
16
16
  temperature?: number;
17
17
  topP?: number;
18
+ maxTokens?: number;
19
+ responseFormat?: {
20
+ type: "json_object";
21
+ } | {
22
+ type: "json_schema";
23
+ json_schema: {
24
+ name: string;
25
+ schema: Record<string, unknown>;
26
+ strict?: boolean;
27
+ };
28
+ } | {
29
+ type: string;
30
+ };
18
31
  onProviderEvent?: (event: OpenAICompatibleProviderEvent) => void;
19
32
  }
20
33
  export interface OpenAICompatibleChatChunk {
@@ -66,6 +79,7 @@ export declare class OpenAICompatibleClient {
66
79
  private resolveProviderConfig;
67
80
  private streamSseData;
68
81
  }
82
+ export declare const ANTHROPIC_OPENAI_COMPATIBLE_BASE_URL = "https://api.anthropic.com/v1";
69
83
  /**
70
84
  * Resolve a GitHub / Copilot token from the environment or `gh` CLI.
71
85
  * Checks: GITHUB_TOKEN → GH_TOKEN → `gh auth token` (sync exec).
@@ -172,6 +172,8 @@ export class OpenAICompatibleClient {
172
172
  stream: true,
173
173
  temperature: request.temperature,
174
174
  top_p: request.topP,
175
+ max_tokens: request.maxTokens,
176
+ ...(request.responseFormat ? { response_format: request.responseFormat } : {}),
175
177
  };
176
178
  const streamRes = await fetch(`${config.baseUrl}/chat/completions`, {
177
179
  method: "POST",
@@ -263,6 +265,8 @@ export class OpenAICompatibleClient {
263
265
  stream: false,
264
266
  temperature: request.temperature,
265
267
  top_p: request.topP,
268
+ max_tokens: request.maxTokens,
269
+ ...(request.responseFormat ? { response_format: request.responseFormat } : {}),
266
270
  };
267
271
  const res = await fetch(`${config.baseUrl}/chat/completions`, {
268
272
  method: "POST",
@@ -348,6 +352,8 @@ export class OpenAICompatibleClient {
348
352
  stream: true,
349
353
  temperature: request.temperature,
350
354
  top_p: request.topP,
355
+ max_tokens: request.maxTokens,
356
+ ...(request.responseFormat ? { response_format: request.responseFormat } : {}),
351
357
  };
352
358
  // llama.cpp rejects custom stream_options; only include for non-llama providers
353
359
  if (provider !== 'llama.cpp') {
@@ -485,7 +491,10 @@ export class OpenAICompatibleClient {
485
491
  const apiKey = override?.apiKey || providerKey || genericKey || process.env.CODEX_API_KEY?.trim();
486
492
  return { provider, baseUrl, apiKey, headers: override?.headers };
487
493
  }
488
- const resolvedBaseUrl = override?.baseUrl || providerBase || genericBase || "";
494
+ const resolvedBaseUrl = override?.baseUrl ||
495
+ providerBase ||
496
+ genericBase ||
497
+ (provider === "claude" ? ANTHROPIC_OPENAI_COMPATIBLE_BASE_URL : "");
489
498
  const baseUrl = provider === "llama.cpp"
490
499
  ? buildOpenAiCompatibleBaseUrl(resolvedBaseUrl)
491
500
  : stripTrailingSlash(resolvedBaseUrl);
@@ -559,6 +568,7 @@ function providerEnvPrefix(provider) {
559
568
  return provider === "llama.cpp" ? "LLAMA_CPP" : provider.toUpperCase();
560
569
  }
561
570
  const GITHUB_MODELS_BASE_URL = "https://models.github.com/v1";
571
+ export const ANTHROPIC_OPENAI_COMPATIBLE_BASE_URL = "https://api.anthropic.com/v1";
562
572
  /**
563
573
  * Resolve a GitHub / Copilot token from the environment or `gh` CLI.
564
574
  * Checks: GITHUB_TOKEN → GH_TOKEN → `gh auth token` (sync exec).
@@ -612,7 +622,10 @@ export function diagnoseProviderRuntimeConfig(providerInput, override) {
612
622
  message: "Missing API key for provider 'codex'. Set CODEX_API_KEY or OPENAI_API_KEY before sending chat messages.",
613
623
  };
614
624
  }
615
- const baseUrl = stripTrailingSlash(override?.baseUrl || providerBase || genericBase || "");
625
+ const baseUrl = stripTrailingSlash(override?.baseUrl ||
626
+ providerBase ||
627
+ genericBase ||
628
+ (provider === "claude" ? ANTHROPIC_OPENAI_COMPATIBLE_BASE_URL : ""));
616
629
  const apiKey = override?.apiKey || providerKey || genericKey || "";
617
630
  const missing = [];
618
631
  if (!baseUrl) {
@@ -14,6 +14,7 @@
14
14
  */
15
15
  export declare const DEFAULT_OLLAMA_MODEL = "llama3.1:8b";
16
16
  export declare const DEFAULT_LLAMA_CPP_MODEL = "local-model";
17
+ export declare const DEFAULT_HOSTED_MODELS: Readonly<Record<"codex" | "claude" | "gemini" | "copilot", string>>;
17
18
  export declare const LOCAL_LLM_PROVIDERS: readonly ["ollama", "llama.cpp"];
18
19
  export type LocalLlmProvider = (typeof LOCAL_LLM_PROVIDERS)[number];
19
20
  export interface ProviderDiscoveryOptions {
@@ -18,9 +18,9 @@ import { join, resolve } from "node:path";
18
18
  import { readStoreJsonSync } from "../store/store-snapshot.js";
19
19
  export const DEFAULT_OLLAMA_MODEL = "llama3.1:8b";
20
20
  export const DEFAULT_LLAMA_CPP_MODEL = "local-model";
21
- const DEFAULT_HOSTED_MODELS = {
21
+ export const DEFAULT_HOSTED_MODELS = {
22
22
  codex: "gpt-5",
23
- claude: "claude-3-7-sonnet",
23
+ claude: "claude-opus-4-8",
24
24
  gemini: "gemini-2.5-pro",
25
25
  copilot: "copilot/gpt-5-mini",
26
26
  };
@@ -0,0 +1,125 @@
1
+ # CLI Reference
2
+
3
+ > Companion to the [Quick Start in the README](../README.md#quick-start).
4
+
5
+ The bare `ace` commands below assume a global install (`npm install -g @voybio/ace-swarm`); without one, prefix each with `npx -y @voybio/ace-swarm` — e.g. `npx -y @voybio/ace-swarm mcp`.
6
+
7
+ ## Commands
8
+
9
+ ```text
10
+ +--------------------------------------------+-------------------------------------------------------+
11
+ | Command | Purpose |
12
+ +--------------------------------------------+-------------------------------------------------------+
13
+ | `ace mcp [--profile host-min|full]` / `ace serve` | Start MCP server over stdio |
14
+ | `ace mcp-shadow --tools <csv>` | Start filtered shadow MCP server (Hermes use) |
15
+ | `ace tui [options]` | Open the terminal operator surface |
16
+ | `ace init [options]` | Seed the ACE store and optional host stubs |
17
+ | `ace connect [provider] [options]` | Register a live local runtime or hosted provider |
18
+ | `ace turnkey [options]` | Project store-backed host surface stubs |
19
+ | `ace doctor [options]` | Validate ACE runtime + MCP readiness |
20
+ | `ace goal <start|status|run|resume|stop>` | Drive a self-paced, evidence-gated goal loop |
21
+ | `ace ls [prefix] [--keys] [--json]` | List store-backed artifacts by logical path |
22
+ | `ace read <path> [--out f] [--source] [--json] [--prefer disk|store]` | Read a store/workspace file (aliases: get, cat) |
23
+ | `ace fsck [--json] [--repair]` | Validate ACEPACK structure, CRCs, orphans |
24
+ | `ace cache [options]` | DEPRECATED legacy ingest/clean command; it is not an alias for compaction |
25
+ | `ace compact [--no-sweep]` | Compact the store and sweep loose generated artifacts |
26
+ | `ace migrate [store-path]` | Import a legacy file workspace into the store |
27
+ | `ace repair` | Re-materialize host files and repair store assets |
28
+ | `ace add skills [name|all]` | Install packaged skill packs into the store |
29
+ | `ace playbook <list|show|add|tag|gc>` | Inspect and curate the experience playbook |
30
+ | `ace eval [--adapt] [options]` | Run the eval harness / adaptation cycle |
31
+ | `ace mcp-config [--client <name>|--all]` | Print baked client snippets for optional install |
32
+ | `ace preconfig` | Write .mcp-config/ bundle for all clients |
33
+ | `ace paths` | Show resolved package and workspace paths |
34
+ | `ace help` | Show help |
35
+ +--------------------------------------------+-------------------------------------------------------+
36
+ ```
37
+
38
+ ## Examples
39
+
40
+ ```bash
41
+ # Bootstrap a workspace with a hosted provider
42
+ ace init --project "My App" --llm claude --client-config-bundle
43
+
44
+ # Bootstrap with a local Ollama model and write VS Code MCP config
45
+ ace init --project "My App" --llm ollama --model qwen3:8b \
46
+ --base-url http://localhost:11434 --mcp-config
47
+
48
+ # Connect to a running llama.cpp server from a Hugging Face model
49
+ ace connect llama-server --hf bartowski/gemma-3-4b-it-GGUF \
50
+ --base-url http://localhost:8080
51
+
52
+ # Run doctor checks with auto-scan of common local endpoints
53
+ ace doctor --scan
54
+
55
+ # Run doctor checks for a specific provider + model
56
+ ace doctor --llm ollama --model qwen3:8b --base-url http://localhost:11434
57
+
58
+ # Run optional Hermes checks with user-supplied Hermes/Python
59
+ ACE_HERMES_ROOT=/path/to/hermes-agent HERMES_PYTHON=/path/to/python \
60
+ ace doctor --llm llama.cpp --model gemma-3-4b-it --base-url http://localhost:8080 --hermes
61
+
62
+ # Repair a missing Ollama model and re-probe runtime readiness
63
+ ace doctor --llm ollama --model qwen3:8b --repair-ollama
64
+
65
+ # Print VS Code MCP config snippet to stdout
66
+ ace mcp-config --client vscode
67
+
68
+ # Write config bundles for every supported client at workspace root
69
+ ace preconfig
70
+
71
+ # Launch TUI with optional Hermes engine against a local llama.cpp server
72
+ ACE_HERMES_ROOT=/path/to/hermes-agent HERMES_PYTHON=/path/to/python \
73
+ ace tui --provider llama.cpp --base-url http://localhost:8080 --model gemma-3-4b-it --hermes
74
+
75
+ # Compact the store and sweep loose generated artifacts
76
+ ace compact
77
+ ace compact --no-sweep
78
+
79
+ # List and read store-backed artifacts a human otherwise can't see on disk
80
+ ace ls agent-state
81
+ ace read agent-state/kanban.json
82
+ ace read agent-state/STORE_AUTHORITY_AUDIT.md --out /tmp/audit.md
83
+
84
+ # Validate ACEPACK structure/CRCs/orphans, optionally repairing safe issues
85
+ ace fsck
86
+ ace fsck --repair
87
+
88
+ # Start a self-paced goal, watch it, stop it if needed.
89
+ # The objective needs at least one machine-checkable obligation -- a declared
90
+ # artifact or a `verify: <command>` criterion -- or `start` refuses it with
91
+ # `contract_uncheckable` rather than completing on the model's own say-so.
92
+ ace goal start "Ship the auth module. verify: npm test" --budget-hours 2 --detach
93
+ ace goal status
94
+ ace goal stop <goal_id>
95
+
96
+ # Experimental: list and inspect the learned experience playbook
97
+ ace playbook list
98
+ ace playbook show pb-1234abcd
99
+ ace playbook add --section verification --content "Require evidence refs before completion."
100
+ ace playbook add --section failure_modes --reason-code false_completion_no_evidence \
101
+ --content "Require evidence refs before completion."
102
+ ace playbook tag pb-1234abcd helpful
103
+
104
+ # Experimental: run an eval adaptation cycle
105
+ ace eval --adapt --models qwen3:8b,gemma3:4b --epochs 2 \
106
+ --battery ./my-battery.json --out agent-state/eval-reports
107
+ ```
108
+
109
+ Playbook sections are `strategies`, `tool_usage`, `failure_modes`, `verification`,
110
+ and `env_facts`; list statuses are `active`, `quarantined`, and `retired`.
111
+ Lessons from every producer without evidence are stored as `quarantined` and
112
+ are not prompt-injected.
113
+
114
+ ## Selected options
115
+
116
+ - `ace mcp --profile <host-min|full>` selects the visible tool schema; `full` is the default.
117
+ - `ace turnkey --no-client-config-bundle` skips the host-facing config stubs created by default.
118
+ - `ace playbook add --reason-code <code>` scopes a lesson to a matching failure class.
119
+ - `ace eval` accepts `--model`/`--models`, `--battery <path>`, `--out <dir>`, and `--adapt`; `--epochs <n>` applies only to adaptation runs.
120
+ - `ace read` accepts `--source` and `--json` in addition to `--out` and `--prefer`.
121
+
122
+ Eval is non-destructive by default: every task, model, baseline, and adapted
123
+ epoch runs in a new disposable workspace seeded only from the battery's adjacent
124
+ `fixtures/` directory. Expected artifacts are scored inside that workspace and
125
+ recorded by SHA-256 in the scorecard, so stale project files cannot pass a run.