@voybio/ace-swarm 2.4.8 → 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 (187) 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 +58 -13
  58. package/dist/job-scheduler.js +298 -48
  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 +111 -6
  89. package/dist/orchestrator-supervisor.js +67 -43
  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.d.ts +3 -2
  165. package/dist/tools-handoff.js +53 -23
  166. package/dist/tools-lifecycle.js +1 -1
  167. package/dist/tools-memory.d.ts +0 -3
  168. package/dist/tools-memory.js +51 -6
  169. package/dist/tools-scheduler.js +20 -17
  170. package/dist/tools-todo.js +160 -148
  171. package/dist/vericify-bridge.d.ts +8 -0
  172. package/dist/vericify-bridge.js +18 -0
  173. package/dist/vericify-context.d.ts +4 -0
  174. package/dist/vericify-context.js +26 -5
  175. package/dist/vericify-native.d.ts +101 -0
  176. package/dist/vericify-native.js +221 -0
  177. package/dist/verification-worker.d.ts +2 -0
  178. package/dist/verification-worker.js +31 -0
  179. package/docs/mcp-tool-reference.md +50 -10
  180. package/docs/troubleshooting.md +29 -0
  181. package/package.json +19 -6
  182. package/scripts/dev/run-tests-isolated.mjs +165 -0
  183. package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
  184. package/scripts/dev/test-isolation-guard.mjs +20 -0
  185. package/scripts/dev/test-preload.mjs +153 -0
  186. package/scripts/dev/test-source-inventory.mjs +31 -0
  187. 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: [
@@ -3,13 +3,14 @@
3
3
  */
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { HandoffRecord } from "./handoff-registry.js";
6
- import { DispatchJobsResult } from "./job-scheduler.js";
7
6
  export interface HandoffSchedulerSyncResult {
8
7
  ok: boolean;
9
8
  job_id: string;
10
9
  enqueued: boolean;
11
10
  enqueue_reason?: string;
12
- tick: DispatchJobsResult;
11
+ dispatch_performed: false;
12
+ reconciliation_required: boolean;
13
+ reconciliation_reason?: string;
13
14
  }
14
15
  export interface HandoffSchedulerBackfillResult {
15
16
  scanned: number;
@@ -9,10 +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";
13
- import { dispatchJobs, enqueueJob } from "./job-scheduler.js";
12
+ import { formatKanbanRefresh, refreshKanbanArtifacts } from "./kanban.js";
13
+ import { readJobQueue, syncHandoffLifecycleJob } from "./job-scheduler.js";
14
14
  import { appendStatusEventSafe } from "./status-events.js";
15
- const DEFAULT_SCHEDULER_OWNER = "capability-ops";
15
+ import { hostAgent, bindHostWork } from './host-work-context.js';
16
+ import { GoalRepository } from './store/repositories/goal-repository.js';
17
+ import { resolveWorkspaceRoot } from './helpers.js';
16
18
  function toSchedulerPriority(input) {
17
19
  if (input === "P0" || input === "P1" || input === "P2" || input === "P3") {
18
20
  return input;
@@ -62,9 +64,11 @@ async function recordHandoffSchedulerSyncEffects(record, sync) {
62
64
  }
63
65
  }
64
66
  export async function syncHandoffIntoScheduler(record, options = {}) {
65
- const owner = options.owner?.trim() || DEFAULT_SCHEDULER_OWNER;
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 };
66
69
  const jobId = handoffSchedulerJobId(record.handoff_id);
67
- const enqueue = await enqueueJob({
70
+ const enqueue = await syncHandoffLifecycleJob({
71
+ now_iso: options.now_iso,
68
72
  job_id: jobId,
69
73
  kind: "handoff.lifecycle",
70
74
  priority: toSchedulerPriority(record.priority),
@@ -85,28 +89,31 @@ export async function syncHandoffIntoScheduler(record, options = {}) {
85
89
  ],
86
90
  resource_requirements: buildHandoffResources(record),
87
91
  });
88
- const enqueued = enqueue.ok;
92
+ const enqueued = enqueue.enqueued;
89
93
  const enqueueReason = enqueue.ok || !enqueue.nack
90
94
  ? undefined
91
95
  : `${enqueue.nack.reason_code}: ${enqueue.nack.reason}`;
92
- const tick = await dispatchJobs({
93
- owner,
94
- ...(options.now_iso ? { now_iso: options.now_iso } : {}),
95
- });
96
96
  const syncResult = {
97
- ok: enqueue.ok || enqueue.nack?.reason_code === "duplicate_job_id",
97
+ ok: enqueue.ok,
98
98
  job_id: jobId,
99
99
  enqueued,
100
100
  enqueue_reason: enqueueReason,
101
- tick,
101
+ dispatch_performed: false,
102
+ reconciliation_required: enqueue.ack?.status === "unknown_recovery",
103
+ reconciliation_reason: enqueue.ack?.status === "unknown_recovery"
104
+ ? enqueue.queue.jobs.find((job) => job.job_id === jobId)?.error
105
+ : undefined,
102
106
  };
103
107
  await recordHandoffSchedulerSyncEffects(record, syncResult);
104
108
  return syncResult;
105
109
  }
106
110
  export async function backfillHandoffsIntoScheduler(options = {}) {
107
111
  const registry = readHandoffRegistry();
112
+ const existing = new Set(readJobQueue().jobs.filter((job) => job.kind === "handoff.lifecycle")
113
+ .map((job) => job.payload.handoff_id));
108
114
  const active = Object.values(registry.handoffs)
109
- .filter((record) => record.status !== "completed")
115
+ .filter(record => !record.step_ref)
116
+ .filter((record) => record.status !== "completed" || existing.has(record.handoff_id))
110
117
  .sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
111
118
  const result = {
112
119
  scanned: active.length,
@@ -117,16 +124,15 @@ export async function backfillHandoffsIntoScheduler(options = {}) {
117
124
  for (const record of active) {
118
125
  try {
119
126
  const sync = await syncHandoffIntoScheduler(record, options);
127
+ if (sync.enqueued)
128
+ result.enqueued += 1;
120
129
  if (sync.ok) {
121
130
  result.synced += 1;
122
- if (sync.enqueued) {
123
- result.enqueued += 1;
124
- }
125
131
  }
126
- else {
132
+ if (!sync.ok || sync.reconciliation_required) {
127
133
  result.failures.push({
128
134
  handoff_id: record.handoff_id,
129
- error: sync.enqueue_reason ?? "scheduler sync returned not-ok",
135
+ error: sync.reconciliation_reason ?? sync.enqueue_reason ?? "scheduler sync returned not-ok",
130
136
  });
131
137
  }
132
138
  }
@@ -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",
@@ -282,12 +296,15 @@ export function registerHandoffTools(server) {
282
296
  `Handoff registry: ${registry.path}`,
283
297
  `Run ledger: ${runLedger.path} (${runLedger.entry.id})`,
284
298
  `Scheduler job: ${schedulerSync.job_id} (${schedulerSync.enqueued ? "enqueued" : "already_exists"})`,
285
- `Scheduler dispatch: lease_acquired=${schedulerSync.tick.lease_acquired}, started=${schedulerSync.tick.summary.started_jobs}, ready=${schedulerSync.tick.summary.ready_jobs}`,
299
+ "Scheduler: handoff metadata synchronized; no execution dispatch",
300
+ schedulerSync.reconciliation_required
301
+ ? `Scheduler reconciliation required: ${schedulerSync.reconciliation_reason}`
302
+ : "",
286
303
  schedulerSync.enqueue_reason
287
304
  ? `Scheduler enqueue detail: ${schedulerSync.enqueue_reason}`
288
305
  : "",
289
306
  "",
290
- `Kanban updated: ${kanban.html_path}`,
307
+ formatKanbanRefresh(kanban),
291
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}`,
292
309
  "",
293
310
  "```json",
@@ -408,14 +425,19 @@ export function registerHandoffTools(server) {
408
425
  handoff_id: z.string().describe("Handoff identifier"),
409
426
  status: HANDOFF_LIFECYCLE_STATUS_ENUM.describe("Lifecycle status"),
410
427
  note: z.string().optional().describe("Optional status note"),
411
- }, 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;
412
431
  const ack = await acknowledgeHandoffSafe({
413
432
  handoff_id,
414
433
  status,
415
434
  note,
435
+ actor: linked ? hostAgent(agent, extra.sessionId) : undefined,
416
436
  });
417
437
  if (!ack.ok) {
418
438
  return {
439
+ ...(linked ? { isError: true } : {}),
440
+ structuredContent: { ...ack },
419
441
  content: [
420
442
  {
421
443
  type: "text",
@@ -435,6 +457,10 @@ export function registerHandoffTools(server) {
435
457
  ],
436
458
  };
437
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
+ }
438
464
  const runLedger = await appendRunLedgerEntrySafe({
439
465
  tool: "ack_handoff",
440
466
  category: status === "blocked" ? "regression" : "major_update",
@@ -452,6 +478,7 @@ export function registerHandoffTools(server) {
452
478
  : undefined;
453
479
  const kanban = refreshKanbanArtifacts("tool:ack_handoff");
454
480
  return {
481
+ structuredContent: { ...ack },
455
482
  content: [
456
483
  {
457
484
  type: "text",
@@ -466,7 +493,10 @@ export function registerHandoffTools(server) {
466
493
  ? `scheduler-job: ${schedulerSync.job_id} (${schedulerSync.enqueued ? "enqueued" : "already_exists"})`
467
494
  : "",
468
495
  schedulerSync
469
- ? `scheduler-dispatch: lease_acquired=${schedulerSync.tick.lease_acquired}, started=${schedulerSync.tick.summary.started_jobs}, ready=${schedulerSync.tick.summary.ready_jobs}`
496
+ ? "scheduler: handoff metadata synchronized; no execution dispatch"
497
+ : "",
498
+ schedulerSync?.reconciliation_required
499
+ ? `scheduler-reconciliation-required: ${schedulerSync.reconciliation_reason}`
470
500
  : "",
471
501
  schedulerSync?.enqueue_reason
472
502
  ? `scheduler-enqueue-detail: ${schedulerSync.enqueue_reason}`
@@ -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
  {