@voybio/ace-swarm 2.4.9 → 2.4.10

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 (185) hide show
  1. package/CHANGELOG.md +52 -14
  2. package/README.md +14 -10
  3. package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
  4. package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
  5. package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
  6. package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
  7. package/assets/agent-state/ACE_WORKFLOW.md +25 -2
  8. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
  9. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
  10. package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
  11. package/assets/eval-battery/battery.json +1 -1
  12. package/assets/instructions/ACE_Coder.instructions.md +9 -13
  13. package/assets/scripts/ace-hook-dispatch.mjs +16 -411
  14. package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
  15. package/assets/vendor/README.md +9 -0
  16. package/assets/vendor/vericify-reader.mjs +272 -0
  17. package/assets/vendor/vericify-reader.provenance.json +10 -0
  18. package/dist/ace-autonomy.d.ts +58 -0
  19. package/dist/ace-autonomy.js +211 -6
  20. package/dist/ace-internal-tools.d.ts +1 -0
  21. package/dist/ace-internal-tools.js +3 -3
  22. package/dist/ace-server-instructions.d.ts +3 -3
  23. package/dist/ace-server-instructions.js +46 -192
  24. package/dist/cli.js +44 -6
  25. package/dist/experience-reflector.d.ts +7 -0
  26. package/dist/experience-reflector.js +39 -0
  27. package/dist/goal-engine.d.ts +64 -7
  28. package/dist/goal-engine.js +197 -41
  29. package/dist/handoff-registry.d.ts +10 -0
  30. package/dist/handoff-registry.js +59 -0
  31. package/dist/helpers/artifacts.d.ts +6 -2
  32. package/dist/helpers/artifacts.js +8 -6
  33. package/dist/helpers/bootstrap.js +13 -20
  34. package/dist/helpers/path-utils.js +2 -0
  35. package/dist/helpers/store-resolution.js +18 -3
  36. package/dist/helpers/workspace-root.d.ts +2 -0
  37. package/dist/helpers/workspace-root.js +9 -0
  38. package/dist/helpers.d.ts +1 -0
  39. package/dist/helpers.js +1 -0
  40. package/dist/hermes/bridge-protocol.d.ts +1 -1
  41. package/dist/hermes/session-manager.d.ts +17 -0
  42. package/dist/hermes/session-manager.js +67 -14
  43. package/dist/host-goal-engine.d.ts +138 -0
  44. package/dist/host-goal-engine.js +793 -0
  45. package/dist/host-goal-status.d.ts +113 -0
  46. package/dist/host-goal-status.js +175 -0
  47. package/dist/host-learning.d.ts +68 -0
  48. package/dist/host-learning.js +115 -0
  49. package/dist/host-observation.d.ts +12 -0
  50. package/dist/host-observation.js +91 -0
  51. package/dist/host-plan-revision.d.ts +14 -0
  52. package/dist/host-plan-revision.js +101 -0
  53. package/dist/host-verification-job.d.ts +12 -0
  54. package/dist/host-verification-job.js +183 -0
  55. package/dist/host-work-context.d.ts +12 -0
  56. package/dist/host-work-context.js +8 -0
  57. package/dist/job-scheduler.d.ts +48 -14
  58. package/dist/job-scheduler.js +169 -23
  59. package/dist/job-worker.d.ts +3 -1
  60. package/dist/job-worker.js +10 -3
  61. package/dist/kanban.d.ts +4 -1
  62. package/dist/kanban.js +44 -16
  63. package/dist/learned-artifacts.d.ts +64 -0
  64. package/dist/learned-artifacts.js +321 -0
  65. package/dist/learning-controls.d.ts +142 -0
  66. package/dist/learning-controls.js +135 -0
  67. package/dist/learning-input-context.d.ts +9 -0
  68. package/dist/learning-input-context.js +62 -0
  69. package/dist/learning-lifecycle.d.ts +10 -0
  70. package/dist/learning-lifecycle.js +11 -0
  71. package/dist/learning-overlays.d.ts +3 -0
  72. package/dist/learning-overlays.js +17 -0
  73. package/dist/learning-policy.d.ts +40 -0
  74. package/dist/learning-policy.js +38 -0
  75. package/dist/learning-records.d.ts +35 -0
  76. package/dist/learning-records.js +17 -0
  77. package/dist/learning-scorecard.d.ts +82 -0
  78. package/dist/learning-scorecard.js +86 -0
  79. package/dist/learning-serving.d.ts +29 -0
  80. package/dist/learning-serving.js +134 -0
  81. package/dist/linked-work.d.ts +9 -0
  82. package/dist/linked-work.js +28 -0
  83. package/dist/local-model-runtime.js +1 -1
  84. package/dist/mcp-command-progress.d.ts +16 -0
  85. package/dist/mcp-command-progress.js +70 -0
  86. package/dist/model-bridge.d.ts +14 -13
  87. package/dist/model-bridge.js +73 -41
  88. package/dist/orchestrator-supervisor.d.ts +108 -6
  89. package/dist/orchestrator-supervisor.js +4 -32
  90. package/dist/plan-proposal.js +3 -3
  91. package/dist/playbook-curator.d.ts +1 -0
  92. package/dist/playbook-curator.js +11 -9
  93. package/dist/process-identity.d.ts +8 -0
  94. package/dist/process-identity.js +38 -0
  95. package/dist/progress-command.d.ts +34 -0
  96. package/dist/progress-command.js +153 -0
  97. package/dist/prompts.js +15 -15
  98. package/dist/resources.js +2 -2
  99. package/dist/runtime-command.d.ts +8 -1
  100. package/dist/runtime-command.js +48 -7
  101. package/dist/runtime-executor.js +33 -11
  102. package/dist/runtime-profile-syntax.d.ts +6 -0
  103. package/dist/runtime-profile-syntax.js +121 -0
  104. package/dist/runtime-profile.d.ts +37 -5
  105. package/dist/runtime-profile.js +36 -129
  106. package/dist/runtime-tool-specs.js +1 -1
  107. package/dist/safe-edit.d.ts +7 -3
  108. package/dist/safe-edit.js +40 -47
  109. package/dist/schemas.js +4 -2
  110. package/dist/server.d.ts +2 -16
  111. package/dist/server.js +14 -20
  112. package/dist/shared.d.ts +2 -0
  113. package/dist/shared.js +2 -0
  114. package/dist/skill-auditor.d.ts +19 -0
  115. package/dist/skill-auditor.js +32 -2
  116. package/dist/skill-catalog.d.ts +4 -1
  117. package/dist/skill-catalog.js +6 -5
  118. package/dist/store/ace-packed-store.d.ts +5 -1
  119. package/dist/store/ace-packed-store.js +33 -1
  120. package/dist/store/bootstrap-store.js +19 -12
  121. package/dist/store/cache-workspace.d.ts +8 -0
  122. package/dist/store/cache-workspace.js +94 -27
  123. package/dist/store/importer.js +11 -20
  124. package/dist/store/knowledge-bake.d.ts +3 -1
  125. package/dist/store/knowledge-bake.js +10 -1
  126. package/dist/store/legacy-vericify-posts.d.ts +8 -0
  127. package/dist/store/legacy-vericify-posts.js +76 -0
  128. package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
  129. package/dist/store/materializers/hook-context-materializer.js +6 -2
  130. package/dist/store/materializers/host-file-materializer.js +2 -2
  131. package/dist/store/materializers/projection-manager.d.ts +4 -1
  132. package/dist/store/materializers/projection-manager.js +10 -3
  133. package/dist/store/materializers/todo-syncer.d.ts +39 -20
  134. package/dist/store/materializers/todo-syncer.js +208 -110
  135. package/dist/store/materializers/vericify-projector.js +15 -26
  136. package/dist/store/repositories/goal-repository.d.ts +51 -2
  137. package/dist/store/repositories/goal-repository.js +114 -48
  138. package/dist/store/repositories/playbook-repository.d.ts +41 -3
  139. package/dist/store/repositories/playbook-repository.js +61 -5
  140. package/dist/store/repositories/todo-repository.d.ts +8 -2
  141. package/dist/store/repositories/todo-repository.js +1 -1
  142. package/dist/store/repositories/vericify-repository.d.ts +4 -0
  143. package/dist/store/repositories/vericify-repository.js +46 -0
  144. package/dist/store/skills-install.d.ts +1 -0
  145. package/dist/store/skills-install.js +20 -16
  146. package/dist/store/source-preservation.d.ts +24 -0
  147. package/dist/store/source-preservation.js +79 -0
  148. package/dist/store/store-artifacts.js +8 -1
  149. package/dist/store/test-write-boundary.d.ts +16 -0
  150. package/dist/store/test-write-boundary.js +175 -0
  151. package/dist/store/types.d.ts +5 -1
  152. package/dist/store/write-coordinator.js +16 -0
  153. package/dist/task-plan-context.d.ts +11 -0
  154. package/dist/task-plan-context.js +59 -0
  155. package/dist/todo-state.d.ts +19 -9
  156. package/dist/todo-state.js +204 -321
  157. package/dist/tools-agent.d.ts +1 -1
  158. package/dist/tools-agent.js +38 -62
  159. package/dist/tools-files.d.ts +0 -3
  160. package/dist/tools-files.js +21 -11
  161. package/dist/tools-framework.js +16 -25
  162. package/dist/tools-goal.d.ts +2 -4
  163. package/dist/tools-goal.js +119 -16
  164. package/dist/tools-handoff.js +28 -4
  165. package/dist/tools-lifecycle.js +1 -1
  166. package/dist/tools-memory.d.ts +0 -3
  167. package/dist/tools-memory.js +51 -6
  168. package/dist/tools-scheduler.js +19 -17
  169. package/dist/tools-todo.js +160 -148
  170. package/dist/vericify-bridge.d.ts +8 -0
  171. package/dist/vericify-bridge.js +18 -0
  172. package/dist/vericify-context.d.ts +4 -0
  173. package/dist/vericify-context.js +26 -5
  174. package/dist/vericify-native.d.ts +101 -0
  175. package/dist/vericify-native.js +221 -0
  176. package/dist/verification-worker.d.ts +2 -0
  177. package/dist/verification-worker.js +31 -0
  178. package/docs/mcp-tool-reference.md +44 -9
  179. package/package.json +19 -6
  180. package/scripts/dev/run-tests-isolated.mjs +165 -0
  181. package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
  182. package/scripts/dev/test-isolation-guard.mjs +20 -0
  183. package/scripts/dev/test-preload.mjs +153 -0
  184. package/scripts/dev/test-source-inventory.mjs +31 -0
  185. package/docs/development.md +0 -46
@@ -1,15 +1,24 @@
1
1
  /**
2
2
  * ACE Goal Engine tool registrations (WS-F).
3
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`.
4
+ * Engine goals delegate to the existing detached runner. Host goals return
5
+ * assignments to the connected participant and never invoke that executor.
8
6
  */
9
7
  import { spawn } from "node:child_process";
10
8
  import { fileURLToPath } from "node:url";
11
9
  import { z } from "zod";
12
- import { getGoalStatus, listGoals, startGoal, stopGoal, } from "./goal-engine.js";
10
+ import { getGoalStatus, listGoals, startGoal, stopGoal, resumeGoal, beginStep, completeStep, } from "./goal-engine.js";
11
+ import { hostRefusal } from './host-goal-engine.js';
12
+ import { resolveCurrentWork } from './ace-autonomy.js';
13
+ import { resolveWorkspaceRoot } from './helpers.js';
14
+ import { bindHostWork, hostAgent } from './host-work-context.js';
15
+ import { getCompactGoalStatus } from './host-goal-status.js';
16
+ import { commandPolicySchema, withMcpCommandProgress } from './mcp-command-progress.js';
17
+ function hostResult(result) {
18
+ return { isError: !!result.isError, structuredContent: { ...result }, content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
19
+ }
20
+ const artifactSchema = z.object({ path: z.string().min(1), required: z.boolean().optional(), transient: z.boolean().optional() });
21
+ const hostStepSchema = z.object({ id: z.string().optional(), task: z.string().min(1), role: z.string().optional(), depends_on: z.array(z.string()).optional(), expected_artifacts: z.array(z.union([z.string().min(1), artifactSchema])).optional(), required_evidence_refs: z.array(z.string()).optional(), required_observation: z.enum(['action', 'command_pass']).optional() });
13
22
  function asJsonBlock(value) {
14
23
  return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
15
24
  }
@@ -29,14 +38,34 @@ function spawnDetachedRunner(goalId, verb) {
29
38
  }
30
39
  }
31
40
  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.", {
41
+ server.tool("start_goal", "Start a goal with an explicit executor choice: host accepts your steps and returns the first assignment; engine plans and runs through ACE's autonomous executor. Productive work has no default duration, tick, or total replan limit.", {
42
+ executor: z.enum(['engine', 'host']),
43
+ agent: z.string().min(1).optional(),
44
+ steps: z.array(hostStepSchema).min(1).optional(),
45
+ verify: z.array(z.union([z.string().min(1), commandPolicySchema.extend({ cmd: z.string().min(1) })])).optional(),
46
+ todo_ids: z.record(z.string(), z.string()).optional(),
33
47
  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)"),
48
+ budget_hours: z.number().positive().optional().describe("Explicit operator deadline measured from the original start; omit for unlimited work."),
49
+ max_replans: z.number().int().min(0).optional().describe("Explicit operator cap on corrections; omitted by default. Repeated unchanged failures are detected separately."),
36
50
  plan_only: z.boolean().optional().describe("Compile + validate the plan but do not execute"),
37
51
  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 });
52
+ }, async ({ objective, budget_hours, max_replans, plan_only, detach, executor, agent, steps, verify, todo_ids }, extra) => {
53
+ const participant = hostAgent(agent, extra.sessionId);
54
+ let started;
55
+ try {
56
+ if (budget_hours !== undefined && !Number.isFinite(budget_hours * 3600000))
57
+ throw new Error('budget_hours must be a finite positive allowance.');
58
+ started = await startGoal(objective, { budget_hours, max_replans, plan_only, executor, agent: participant, session_id: extra.sessionId, steps, verify_commands: verify, todo_ids });
59
+ }
60
+ catch (error) {
61
+ if (executor !== 'host')
62
+ throw error;
63
+ return hostResult(hostRefusal(resolveWorkspaceRoot(), 'host_plan_invalid', error instanceof Error ? error.message : String(error)));
64
+ }
65
+ if (executor === 'host') {
66
+ bindHostWork({ agent: participant, goal_id: started.goal.goal_id, step_id: started.assignment?.step_id, attempt: started.assignment?.attempt }, extra.sessionId);
67
+ return { isError: !started.ok, structuredContent: { ...started }, content: [{ type: 'text', text: JSON.stringify(started, null, 2) }] };
68
+ }
40
69
  let runnerPid;
41
70
  if (started.ok && !plan_only && detach !== false) {
42
71
  runnerPid = spawnDetachedRunner(started.goal.goal_id, "run");
@@ -59,25 +88,75 @@ export function registerGoalTools(server) {
59
88
  ],
60
89
  };
61
90
  });
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.", {
91
+ server.tool('next_step', 'Receive or resume your current assignment from a host goal. Never executes work itself.', {
92
+ goal_id: z.string().optional(), step_id: z.string().optional(), agent: z.string().min(1).optional(),
93
+ }, async ({ goal_id, step_id, agent }, extra) => {
94
+ const participant = hostAgent(agent, extra.sessionId);
95
+ const work = resolveCurrentWork({ goal_id, agent: participant });
96
+ const id = goal_id ?? work.objective?.goal_id ?? (work.idle_learning?.includes('learning step available') ? 'G-learning' : undefined);
97
+ if (!id)
98
+ return hostResult({ ...hostRefusal(resolveWorkspaceRoot(), work.state === 'ambiguous' ? 'ambiguous_goal' : 'no_active_goal', 'Select a host goal_id, or create one with start_goal executor:host.'), candidates: work.other_goals });
99
+ try {
100
+ const result = await beginStep(id, { step_id, agent: participant, session_id: extra.sessionId });
101
+ if (result.assignment)
102
+ bindHostWork({ agent: participant, goal_id: id, step_id: result.assignment.step_id, attempt: result.assignment.attempt }, extra.sessionId);
103
+ return hostResult(result);
104
+ }
105
+ catch (error) {
106
+ return hostResult(hostRefusal(resolveWorkspaceRoot(), 'next_step_failed', error instanceof Error ? error.message : String(error), id));
107
+ }
108
+ });
109
+ server.tool('complete_step', 'Record this attempt once, update linked state and optionally receive the next assignment. Identical retries return the original response.', {
110
+ goal_id: z.string(), step_id: z.string(), attempt: z.number().int().positive(), agent: z.string().min(1).optional(), continue: z.boolean().optional(),
111
+ outcome: z.object({ status: z.enum(['done', 'blocked', 'failed']), summary: z.string().min(1).max(2000), waiting_on: z.string().optional(), artifacts: z.array(z.string()).optional(),
112
+ feedback: z.array(z.object({ id: z.string(), helpful: z.boolean() })).optional(),
113
+ learned: z.object({
114
+ lessons: z.array(z.object({ op: z.enum(['add', 'edit', 'retire']), bullet_id: z.string().optional(), section: z.enum(['strategies', 'tool_usage', 'failure_modes', 'verification', 'env_facts']), content: z.string().max(400), scope: z.object({ role: z.string().optional(), paths: z.array(z.string()).optional(), task_tags: z.array(z.string()).optional(), failure_signature: z.string().optional() }), episodes: z.array(z.string()).min(1), retired_episodes: z.array(z.string()).optional() })).max(3).optional(),
115
+ skill: z.object({ op: z.enum(['add', 'edit']), slug: z.string(), skill_md: z.string(), references: z.record(z.string(), z.string()).optional(), episodes: z.array(z.string()).min(3), retired_episodes: z.array(z.string()).optional() }).optional(),
116
+ instruction: z.object({ role: z.string(), text: z.string().max(1200), episodes: z.array(z.string()).min(1) }).optional()
117
+ }).optional(),
118
+ requirement_updates: z.array(z.object({ step_id: z.string(), reason: z.string().min(1), task: z.string().min(1).optional(), expected_artifacts: z.array(artifactSchema).optional(), required_evidence_refs: z.array(z.string()).optional() })).optional(),
119
+ evidence: z.array(z.object({ ref: z.string(), kind: z.enum(['command', 'artifact', 'diff', 'note']), detail: z.string().optional() })).optional(),
120
+ follow_ups: z.array(z.object({ task: z.string().min(1), role: z.string().optional(), depends_on: z.array(z.string()).optional() })).optional() }),
121
+ }, async ({ goal_id, step_id, attempt, agent, outcome, continue: proceed }, extra) => {
122
+ const participant = hostAgent(agent, extra.sessionId);
123
+ try {
124
+ const result = await withMcpCommandProgress(extra, 'complete_step acceptance', () => completeStep(goal_id, step_id, outcome, { agent: participant, attempt, continue: proceed }));
125
+ if (result.ok)
126
+ bindHostWork({ agent: participant, goal_id, step_id: result.assignment?.step_id, attempt: result.assignment?.attempt }, extra.sessionId);
127
+ return hostResult(result);
128
+ }
129
+ catch (error) {
130
+ return hostResult(hostRefusal(resolveWorkspaceRoot(), 'completion_failed', error instanceof Error ? error.message : String(error), goal_id));
131
+ }
132
+ });
133
+ server.tool("get_goal_status", "Get goal status. view:compact gives bounded verification, blockers and executable next actions; view:full (default) retains the complete plan and evidence. Omit goal_id to list goals.", {
63
134
  goal_id: z.string().optional().describe("Goal id; omit to list all goals"),
64
- }, async ({ goal_id }) => {
135
+ view: z.enum(['compact', 'full']).optional(),
136
+ agent: z.string().min(1).optional(),
137
+ }, async ({ goal_id, view: mode, agent }, extra) => {
65
138
  if (!goal_id) {
66
139
  return {
67
140
  content: [{ type: "text", text: asJsonBlock({ goals: listGoals() }) }],
68
141
  };
69
142
  }
143
+ if (mode === 'compact') {
144
+ const compact = getCompactGoalStatus(goal_id, hostAgent(agent, extra.sessionId));
145
+ if (!compact)
146
+ return hostResult(hostRefusal(resolveWorkspaceRoot(), 'goal_not_found', `Unknown goal: ${goal_id}`, goal_id));
147
+ return { structuredContent: { ...compact }, content: [{ type: 'text', text: JSON.stringify(compact) }] };
148
+ }
70
149
  const view = getGoalStatus(goal_id);
71
150
  if (!view) {
72
151
  return { content: [{ type: "text", text: `❌ Unknown goal: ${goal_id}` }] };
73
152
  }
74
- return { content: [{ type: "text", text: asJsonBlock(view) }] };
153
+ return { structuredContent: { ...view }, content: [{ type: "text", text: asJsonBlock(view) }] };
75
154
  });
76
155
  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
156
  try {
78
- const goal = stopGoal(goal_id);
157
+ const goal = await stopGoal(goal_id);
79
158
  return {
80
- content: [{ type: "text", text: `🛑 Stop requested for ${goal.goal_id} (status ${goal.status}).` }],
159
+ content: [{ type: "text", text: `🛑 Stop requested for ${goal.goal_id} (status ${goal.status}).\n` }],
81
160
  };
82
161
  }
83
162
  catch (err) {
@@ -86,11 +165,35 @@ export function registerGoalTools(server) {
86
165
  };
87
166
  }
88
167
  });
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 }) => {
168
+ server.tool("resume_goal", "Resume a goal. budget_hours/max_replans increase explicit total allowances only with operator approval; never execute a status proposal automatically. Accepted work and original start time are preserved. Engine goals resume in the detached runner.", { goal_id: z.string().describe("Goal id to resume"),
169
+ budget_hours: z.number().positive().optional().describe('Operator-approved increase to an explicit total deadline from the original start.'),
170
+ max_replans: z.number().int().nonnegative().optional().describe('Operator-approved increase admitting retained pending follow-ups.') }, async ({ goal_id, budget_hours, max_replans }, extra) => {
90
171
  const view = getGoalStatus(goal_id);
91
172
  if (!view) {
92
173
  return { content: [{ type: "text", text: `❌ Unknown goal: ${goal_id}` }] };
93
174
  }
175
+ if (view.goal.executor === 'host') {
176
+ try {
177
+ const resumed = await resumeGoal(goal_id, {}, { budget_hours, max_replans });
178
+ const status = getCompactGoalStatus(goal_id, hostAgent(undefined, extra.sessionId));
179
+ const result = { ...resumed, options: status?.options, operator_proposal: status?.operator_proposal, waiting_on: status?.waiting_on, now: status?.now };
180
+ return { structuredContent: result, content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
181
+ }
182
+ catch (error) {
183
+ return hostResult(hostRefusal(resolveWorkspaceRoot(), 'budget_recovery_rejected', error instanceof Error ? error.message : String(error), goal_id));
184
+ }
185
+ }
186
+ if (budget_hours !== undefined || max_replans !== undefined) {
187
+ try {
188
+ const { recoverEngineGoalBudgets } = await import('./goal-engine.js');
189
+ await recoverEngineGoalBudgets(goal_id, { budget_hours, max_replans });
190
+ }
191
+ catch (error) {
192
+ const refusal = hostRefusal(resolveWorkspaceRoot(), 'budget_recovery_rejected', error instanceof Error ? error.message : String(error), goal_id);
193
+ refusal.options = [{ tool: 'get_goal_status', args: { goal_id, view: 'compact' }, why: 'Inspect the retained engine allowance and obtain operator approval for any sufficient increase.' }];
194
+ return hostResult(refusal);
195
+ }
196
+ }
94
197
  const runnerPid = spawnDetachedRunner(goal_id, "resume");
95
198
  return {
96
199
  content: [
@@ -9,9 +9,12 @@ import { ROLE_ENUM, HANDOFF_VALIDATION_MODE, HANDOFF_LIFECYCLE_STATUS_ENUM, } fr
9
9
  import { validateHandoffPayload, validateSwarmHandoffPayload, lintHandoffPayload, } from "./schemas.js";
10
10
  import { acknowledgeHandoffSafe, getHandoffRegistryPath, readHandoffRegistry, registerHandoffSafe, } from "./handoff-registry.js";
11
11
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
12
- import { refreshKanbanArtifacts } from "./kanban.js";
12
+ import { formatKanbanRefresh, refreshKanbanArtifacts } from "./kanban.js";
13
13
  import { readJobQueue, syncHandoffLifecycleJob } from "./job-scheduler.js";
14
14
  import { appendStatusEventSafe } from "./status-events.js";
15
+ import { hostAgent, bindHostWork } from './host-work-context.js';
16
+ import { GoalRepository } from './store/repositories/goal-repository.js';
17
+ import { resolveWorkspaceRoot } from './helpers.js';
15
18
  function toSchedulerPriority(input) {
16
19
  if (input === "P0" || input === "P1" || input === "P2" || input === "P3") {
17
20
  return input;
@@ -61,6 +64,8 @@ async function recordHandoffSchedulerSyncEffects(record, sync) {
61
64
  }
62
65
  }
63
66
  export async function syncHandoffIntoScheduler(record, options = {}) {
67
+ if (record.step_ref)
68
+ return { ok: true, job_id: '', enqueued: false, enqueue_reason: 'Host step ownership follows the handoff; no engine dispatch.', dispatch_performed: false, reconciliation_required: false };
64
69
  const jobId = handoffSchedulerJobId(record.handoff_id);
65
70
  const enqueue = await syncHandoffLifecycleJob({
66
71
  now_iso: options.now_iso,
@@ -107,6 +112,7 @@ export async function backfillHandoffsIntoScheduler(options = {}) {
107
112
  const existing = new Set(readJobQueue().jobs.filter((job) => job.kind === "handoff.lifecycle")
108
113
  .map((job) => job.payload.handoff_id));
109
114
  const active = Object.values(registry.handoffs)
115
+ .filter(record => !record.step_ref)
110
116
  .filter((record) => record.status !== "completed" || existing.has(record.handoff_id))
111
117
  .sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
112
118
  const result = {
@@ -161,11 +167,15 @@ export function registerHandoffTools(server) {
161
167
  })
162
168
  .optional()
163
169
  .describe("File:line pointers for context"),
164
- }, async ({ from, to, title, task_type, priority, directive, acceptance_criteria, context_pointers, }) => {
170
+ step_ref: z.object({ goal_id: z.string(), step_id: z.string() }).optional(),
171
+ agent: z.string().optional().describe('Submitting host label for a linked step'),
172
+ recipient_agent: z.string().optional().describe('Host label receiving a linked step; defaults to the target role label'),
173
+ }, async ({ from, to, title, task_type, priority, directive, acceptance_criteria, context_pointers, step_ref, agent, recipient_agent, }, extra) => {
165
174
  const now = new Date();
166
175
  const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
167
176
  const shortId = randomUUID().replace(/-/g, "").slice(0, 8);
168
177
  const handoff = {
178
+ ...(step_ref ? { step_ref, recipient_agent: recipient_agent ?? `ACE-${to.charAt(0).toUpperCase() + to.slice(1)}` } : {}),
169
179
  handoff_id: `HND-${dateStr}-${shortId}`,
170
180
  timestamp_utc: now.toISOString(),
171
181
  router: {
@@ -252,6 +262,9 @@ export function registerHandoffTools(server) {
252
262
  task_type: handoff.task.type,
253
263
  priority: handoff.task.priority,
254
264
  source_file: payloadPath,
265
+ step_ref,
266
+ actor: step_ref ? hostAgent(agent, extra.sessionId) : undefined,
267
+ ...(step_ref && recipient_agent ? { to: recipient_agent } : {}),
255
268
  });
256
269
  const runLedger = await appendRunLedgerEntrySafe({
257
270
  tool: "create_handoff",
@@ -271,6 +284,7 @@ export function registerHandoffTools(server) {
271
284
  const schedulerSync = await syncHandoffIntoScheduler(registry.record);
272
285
  const kanban = refreshKanbanArtifacts("tool:create_handoff");
273
286
  return {
287
+ structuredContent: { handoff_id: handoff.handoff_id, record: registry.record },
274
288
  content: [
275
289
  {
276
290
  type: "text",
@@ -290,7 +304,7 @@ export function registerHandoffTools(server) {
290
304
  ? `Scheduler enqueue detail: ${schedulerSync.enqueue_reason}`
291
305
  : "",
292
306
  "",
293
- `Kanban updated: ${kanban.html_path}`,
307
+ formatKanbanRefresh(kanban),
294
308
  `Kanban counts: backlog=${kanban.counts.backlog}, blocked=${kanban.counts.blocked}, in_progress=${kanban.counts.in_progress}, gated=${kanban.counts.gated}, done=${kanban.counts.done}`,
295
309
  "",
296
310
  "```json",
@@ -411,14 +425,19 @@ export function registerHandoffTools(server) {
411
425
  handoff_id: z.string().describe("Handoff identifier"),
412
426
  status: HANDOFF_LIFECYCLE_STATUS_ENUM.describe("Lifecycle status"),
413
427
  note: z.string().optional().describe("Optional status note"),
414
- }, async ({ handoff_id, status, note }) => {
428
+ agent: z.string().optional().describe('Recipient host label for a linked step'),
429
+ }, async ({ handoff_id, status, note, agent }, extra) => {
430
+ const linked = readHandoffRegistry().handoffs[handoff_id]?.step_ref;
415
431
  const ack = await acknowledgeHandoffSafe({
416
432
  handoff_id,
417
433
  status,
418
434
  note,
435
+ actor: linked ? hostAgent(agent, extra.sessionId) : undefined,
419
436
  });
420
437
  if (!ack.ok) {
421
438
  return {
439
+ ...(linked ? { isError: true } : {}),
440
+ structuredContent: { ...ack },
422
441
  content: [
423
442
  {
424
443
  type: "text",
@@ -438,6 +457,10 @@ export function registerHandoffTools(server) {
438
457
  ],
439
458
  };
440
459
  }
460
+ if (linked && ack.record) {
461
+ const step = new GoalRepository(resolveWorkspaceRoot()).getPlan(linked.goal_id)?.steps.find(s => s.step_id === linked.step_id);
462
+ bindHostWork({ agent: ack.record.to, goal_id: linked.goal_id, ...(status === 'accepted' ? { step_id: linked.step_id, attempt: step?.attempt } : {}) }, extra.sessionId);
463
+ }
441
464
  const runLedger = await appendRunLedgerEntrySafe({
442
465
  tool: "ack_handoff",
443
466
  category: status === "blocked" ? "regression" : "major_update",
@@ -455,6 +478,7 @@ export function registerHandoffTools(server) {
455
478
  : undefined;
456
479
  const kanban = refreshKanbanArtifacts("tool:ack_handoff");
457
480
  return {
481
+ structuredContent: { ...ack },
458
482
  content: [
459
483
  {
460
484
  type: "text",
@@ -190,7 +190,7 @@ export function registerLifecycleTools(server) {
190
190
  {
191
191
  type: "text",
192
192
  text: [
193
- "# Kanban Updated",
193
+ snapshot.refresh_scheduled ? "# Kanban refresh scheduled" : "# Kanban Updated",
194
194
  "",
195
195
  `JSON: ${snapshot.json_path}`,
196
196
  `HTML: ${snapshot.html_path}`,
@@ -1,6 +1,3 @@
1
- /**
2
- * Memory tool registrations – context snapshots and recall.
3
- */
4
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
2
  export declare function registerMemoryTools(server: McpServer): void;
6
3
  //# sourceMappingURL=tools-memory.d.ts.map
@@ -1,3 +1,5 @@
1
+ import { renderLearningTask } from './learning-input-context.js';
2
+ import { readLearningPolicy } from './learning-policy.js';
1
3
  /**
2
4
  * Memory tool registrations – context snapshots and recall.
3
5
  */
@@ -8,7 +10,10 @@ import { isReadError } from "./shared.js";
8
10
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
9
11
  import { appendStatusEventSafe } from "./status-events.js";
10
12
  import { readRuntimeProfile } from "./runtime-profile.js";
11
- import { buildAceContinuityPacket, formatAceContinuityPacketMarkdown, normalizeContinuityPolicy, } from "./ace-autonomy.js";
13
+ import { buildAceContinuityPacket, formatAceContinuityPacketMarkdown, normalizeContinuityPolicy, formatCurrentWork, readCurrentWorkContext, } from "./ace-autonomy.js";
14
+ import { GoalRepository } from "./store/repositories/goal-repository.js";
15
+ import { bindHostWork } from './host-work-context.js';
16
+ import { formatStepTaskForBridge, collectUpstreamOutputs } from "./task-plan-context.js";
12
17
  import { openStore } from "./store/ace-packed-store.js";
13
18
  import { ProjectionManager } from "./store/materializers/projection-manager.js";
14
19
  import { ContextSnapshotRepository } from "./store/repositories/context-snapshot-repository.js";
@@ -89,6 +94,8 @@ export function registerMemoryTools(server) {
89
94
  if (!isReadError(indexRaw)) {
90
95
  try {
91
96
  index = JSON.parse(indexRaw);
97
+ if (!Array.isArray(index.snapshots) || index.snapshots.some((s) => !s || typeof s.name !== "string" || typeof s.file !== "string" || typeof s.timestamp !== "string"))
98
+ throw new Error("Invalid snapshot index");
92
99
  }
93
100
  catch {
94
101
  /* keep default */
@@ -207,16 +214,43 @@ export function registerMemoryTools(server) {
207
214
  ],
208
215
  };
209
216
  });
210
- server.tool("recall_context", "Retrieve a previously saved context snapshot by name, or list all snapshots", {
217
+ server.tool("recall_context", "Return current work and linked context; retrieve a named snapshot or explicitly list snapshots", {
211
218
  name: z
212
219
  .string()
213
220
  .optional()
214
- .describe("Snapshot name to retrieve (omit for list)"),
215
- }, async ({ name }) => {
216
- const indexPath = wsPath(CONTEXT_STORE_REL, "index.json");
221
+ .describe("Named snapshot to retrieve; omit for current work"),
222
+ list: z.boolean().optional().describe("List saved snapshots instead of current work"),
223
+ goal_id: z.string().optional(),
224
+ agent: z.string().optional(),
225
+ }, async ({ name, list, goal_id, agent }, extra) => {
226
+ if (!name && !list) {
227
+ const context = readCurrentWorkContext({ goal_id, agent });
228
+ const lines = [formatCurrentWork(context.work, "block")];
229
+ const goalId = goal_id ?? context.work.objective?.goal_id;
230
+ if (goalId) {
231
+ const plan = new GoalRepository(resolveWorkspaceRoot()).getPlan(goalId);
232
+ if (plan)
233
+ for (const step of plan.steps.filter((s) => s.status === "running")) {
234
+ if (agent && step.assignee === agent)
235
+ bindHostWork({ agent, goal_id: goalId, step_id: step.step_id, attempt: step.attempt }, extra.sessionId);
236
+ lines.push(`Assignment ${step.step_id} (attempt ${step.attempt ?? 'engine'}, assignee ${step.assignee ?? step.role}):\n${formatStepTaskForBridge({ ...step, task: renderLearningTask(resolveWorkspaceRoot(), goalId, step), upstream_outputs: collectUpstreamOutputs(plan, step) })}${readLearningPolicy(resolveWorkspaceRoot()).enabled && step.served_learning?.text ? '\n' + step.served_learning.text : ''}`);
237
+ }
238
+ }
239
+ const failures = context.events.filter((e) => ["fail", "blocked"].includes(e.status));
240
+ const selected = [...new Map([...context.events.slice(-5), ...failures].map((e) => [e.event_id, e])).values()];
241
+ for (const event of selected.reverse())
242
+ lines.push(`[${event.event_id}] ${event.event_type}: ${event.payload.summary ?? ""}`);
243
+ for (const entry of context.entries.slice(-4).reverse())
244
+ lines.push(`[${entry.id}] ${entry.message}`);
245
+ if (context.events.length > selected.length || context.entries.length > 4)
246
+ lines.push("Additional linked detail: list_status_events / get_run_ledger.");
247
+ return { isError: false, content: [{ type: "text", text: lines.join("\n") }] };
248
+ }
249
+ const indexPath = `${CONTEXT_STORE_REL}/index.json`;
217
250
  const indexRaw = safeRead(indexPath);
218
251
  if (isReadError(indexRaw)) {
219
252
  return {
253
+ isError: !!name,
220
254
  content: [
221
255
  {
222
256
  type: "text",
@@ -228,9 +262,12 @@ export function registerMemoryTools(server) {
228
262
  let index;
229
263
  try {
230
264
  index = JSON.parse(indexRaw);
265
+ if (!Array.isArray(index.snapshots) || index.snapshots.some((s) => !s || typeof s.name !== "string" || typeof s.file !== "string" || typeof s.timestamp !== "string"))
266
+ throw new Error("Invalid snapshot index");
231
267
  }
232
268
  catch {
233
269
  return {
270
+ isError: true,
234
271
  content: [
235
272
  {
236
273
  type: "text",
@@ -257,6 +294,7 @@ export function registerMemoryTools(server) {
257
294
  const match = index.snapshots.find((s) => s.name.toLowerCase() === name.toLowerCase());
258
295
  if (!match) {
259
296
  return {
297
+ isError: true,
260
298
  content: [
261
299
  {
262
300
  type: "text",
@@ -265,9 +303,10 @@ export function registerMemoryTools(server) {
265
303
  ],
266
304
  };
267
305
  }
268
- const content = safeRead(wsPath(CONTEXT_STORE_REL, match.file));
306
+ const content = safeRead(`${CONTEXT_STORE_REL}/${match.file}`);
269
307
  if (isReadError(content)) {
270
308
  return {
309
+ isError: true,
271
310
  content: [
272
311
  {
273
312
  type: "text",
@@ -276,6 +315,12 @@ export function registerMemoryTools(server) {
276
315
  ],
277
316
  };
278
317
  }
318
+ try {
319
+ JSON.parse(content);
320
+ }
321
+ catch {
322
+ return { isError: true, content: [{ type: "text", text: `Snapshot ${match.name} is corrupt. Recover its source or select another named snapshot.` }] };
323
+ }
279
324
  return {
280
325
  content: [
281
326
  {
@@ -2,6 +2,7 @@
2
2
  * Job scheduler tool registrations.
3
3
  */
4
4
  import { z } from "zod";
5
+ import { resolveWorkspaceRoot } from "./helpers.js";
5
6
  import { acknowledgeJob, completeJob, dispatchJobNow, dispatchJobs, enqueueJob, getJobLockTablePath, getJobQueuePath, getSchedulerLeasePath, heartbeatJob, reapStaleRunningJobsTick, readJobQueue, readJobLockTable, readSchedulerLease, } from "./job-scheduler.js";
6
7
  // Event emission handled in core job-scheduler.ts — no wrapper duplication.
7
8
  const PRIORITY_BAND_ENUM = z.enum(["P0", "P1", "P2", "P3"]);
@@ -22,6 +23,7 @@ function asJsonBlock(value) {
22
23
  return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
23
24
  }
24
25
  export function registerSchedulerTools(server) {
26
+ const workspaceRoot = resolveWorkspaceRoot();
25
27
  server.tool("enqueue_job", "Add a handoff job to the scheduler queue with priority/dependency/resource metadata", {
26
28
  kind: z.string().describe("Job kind/type identifier"),
27
29
  payload: z
@@ -46,7 +48,7 @@ export function registerSchedulerTools(server) {
46
48
  backoff_ms: z.number().int().min(1000).max(86_400_000).optional(),
47
49
  job_id: z.string().optional().describe("Optional caller-provided job id"),
48
50
  }, async (input) => {
49
- const result = await enqueueJob(input);
51
+ const result = await enqueueJob(input, workspaceRoot);
50
52
  return {
51
53
  content: [
52
54
  {
@@ -78,15 +80,15 @@ export function registerSchedulerTools(server) {
78
80
  .describe("Include lock table payload (default true)"),
79
81
  limit: z.number().int().min(1).max(1000).optional().describe("Max jobs returned"),
80
82
  }, async ({ status, include_locks, limit }) => {
81
- const queue = readJobQueue();
83
+ const queue = readJobQueue(workspaceRoot);
82
84
  const filtered = queue.jobs.filter((job) => (status ? job.status === status : true));
83
85
  const clipped = typeof limit === "number" ? filtered.slice(0, limit) : filtered;
84
- const lockTable = readJobLockTable();
85
- const lease = readSchedulerLease();
86
+ const lockTable = readJobLockTable(workspaceRoot);
87
+ const lease = readSchedulerLease(workspaceRoot);
86
88
  const payload = {
87
- queue_path: getJobQueuePath(),
88
- lock_path: getJobLockTablePath(),
89
- lease_path: getSchedulerLeasePath(),
89
+ queue_path: getJobQueuePath(workspaceRoot),
90
+ lock_path: getJobLockTablePath(workspaceRoot),
91
+ lease_path: getSchedulerLeasePath(workspaceRoot),
90
92
  total_jobs: queue.jobs.length,
91
93
  returned_jobs: clipped.length,
92
94
  jobs: clipped,
@@ -109,7 +111,7 @@ export function registerSchedulerTools(server) {
109
111
  action: z.enum(["accept", "reject", "cancel", "resume"]),
110
112
  reason: z.string().optional(),
111
113
  }, async ({ job_id, action, reason }) => {
112
- const result = await acknowledgeJob({ job_id, action, reason });
114
+ const result = await acknowledgeJob({ job_id, action, reason }, workspaceRoot);
113
115
  return {
114
116
  content: [
115
117
  {
@@ -135,7 +137,7 @@ export function registerSchedulerTools(server) {
135
137
  lease_ttl_seconds: z.number().int().min(5).max(3600).optional(),
136
138
  now_iso: z.string().optional().describe("Optional deterministic clock override"),
137
139
  }, async (input) => {
138
- const result = await dispatchJobs(input);
140
+ const result = await dispatchJobs(input, workspaceRoot);
139
141
  return {
140
142
  content: [
141
143
  {
@@ -158,7 +160,7 @@ export function registerSchedulerTools(server) {
158
160
  owner: z.string().optional(),
159
161
  now_iso: z.string().optional(),
160
162
  }, async ({ job_id, owner, now_iso }) => {
161
- const result = await dispatchJobNow({ job_id, owner, now_iso });
163
+ const result = await dispatchJobNow({ job_id, owner, now_iso }, workspaceRoot);
162
164
  return {
163
165
  content: [
164
166
  {
@@ -185,7 +187,7 @@ export function registerSchedulerTools(server) {
185
187
  evidence_ref,
186
188
  error,
187
189
  now_iso,
188
- });
190
+ }, workspaceRoot);
189
191
  return {
190
192
  content: [
191
193
  {
@@ -203,7 +205,7 @@ export function registerSchedulerTools(server) {
203
205
  lease_ttl_seconds: z.number().int().min(5).max(3600).optional(),
204
206
  now_iso: z.string().optional().describe("Optional deterministic clock override"),
205
207
  }, async ({ job_id, owner, lease_ttl_seconds, now_iso }) => {
206
- const result = await heartbeatJob({ job_id, owner, lease_ttl_seconds, now_iso });
208
+ const result = await heartbeatJob({ job_id, owner, lease_ttl_seconds, now_iso }, workspaceRoot);
207
209
  return {
208
210
  content: [
209
211
  {
@@ -225,7 +227,7 @@ export function registerSchedulerTools(server) {
225
227
  .describe("Staleness window in seconds (default 120)"),
226
228
  now_iso: z.string().optional().describe("Optional deterministic clock override"),
227
229
  }, async ({ heartbeat_stale_seconds, now_iso }) => {
228
- const result = await reapStaleRunningJobsTick({ heartbeat_stale_seconds, now_iso });
230
+ const result = await reapStaleRunningJobsTick({ heartbeat_stale_seconds, now_iso }, workspaceRoot);
229
231
  return {
230
232
  content: [
231
233
  {
@@ -247,9 +249,9 @@ export function registerSchedulerTools(server) {
247
249
  text: [
248
250
  "# Lock Table",
249
251
  "",
250
- `Path: ${getJobLockTablePath()}`,
252
+ `Path: ${getJobLockTablePath(workspaceRoot)}`,
251
253
  "",
252
- asJsonBlock(readJobLockTable()),
254
+ asJsonBlock(readJobLockTable(workspaceRoot)),
253
255
  ].join("\n"),
254
256
  },
255
257
  ],
@@ -261,9 +263,9 @@ export function registerSchedulerTools(server) {
261
263
  text: [
262
264
  "# Scheduler Lease",
263
265
  "",
264
- `Path: ${getSchedulerLeasePath()}`,
266
+ `Path: ${getSchedulerLeasePath(workspaceRoot)}`,
265
267
  "",
266
- asJsonBlock(readSchedulerLease() ?? null),
268
+ asJsonBlock(readSchedulerLease(workspaceRoot) ?? null),
267
269
  ].join("\n"),
268
270
  },
269
271
  ],