@voybio/ace-swarm 0.2.5 → 2.4.1

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 (144) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +21 -13
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/EVIDENCE_LOG.md +1 -1
  5. package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
  6. package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
  7. package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
  8. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
  9. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
  10. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  11. package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
  12. package/assets/agent-state/STATUS.md +2 -2
  13. package/assets/agent-state/runtime-tool-specs.json +70 -2
  14. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  15. package/assets/instructions/ACE_UI.instructions.md +11 -0
  16. package/assets/scripts/ace-hook-dispatch.mjs +70 -6
  17. package/assets/scripts/render-mcp-configs.sh +19 -5
  18. package/dist/ace-context.js +91 -11
  19. package/dist/ace-internal-tools.d.ts +3 -1
  20. package/dist/ace-internal-tools.js +10 -2
  21. package/dist/ace-server-instructions.js +3 -3
  22. package/dist/ace-state-resolver.js +5 -3
  23. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  24. package/dist/agent-runtime/role-adapters.js +49 -5
  25. package/dist/astgrep-index.d.ts +57 -1
  26. package/dist/astgrep-index.js +140 -4
  27. package/dist/cli.js +232 -35
  28. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  29. package/dist/discovery-runtime-wrappers.js +615 -0
  30. package/dist/handoff-registry.js +5 -5
  31. package/dist/helpers/artifacts.d.ts +19 -0
  32. package/dist/helpers/artifacts.js +152 -0
  33. package/dist/helpers/bootstrap.d.ts +24 -0
  34. package/dist/helpers/bootstrap.js +894 -0
  35. package/dist/helpers/constants.d.ts +53 -0
  36. package/dist/helpers/constants.js +295 -0
  37. package/dist/helpers/drift.d.ts +13 -0
  38. package/dist/helpers/drift.js +45 -0
  39. package/dist/helpers/path-utils.d.ts +24 -0
  40. package/dist/helpers/path-utils.js +123 -0
  41. package/dist/helpers/store-resolution.d.ts +19 -0
  42. package/dist/helpers/store-resolution.js +305 -0
  43. package/dist/helpers/workspace-root.d.ts +3 -0
  44. package/dist/helpers/workspace-root.js +80 -0
  45. package/dist/helpers.d.ts +8 -125
  46. package/dist/helpers.js +8 -1768
  47. package/dist/job-scheduler.js +33 -7
  48. package/dist/json-sanitizer.d.ts +16 -0
  49. package/dist/json-sanitizer.js +26 -0
  50. package/dist/local-model-policy.d.ts +27 -0
  51. package/dist/local-model-policy.js +84 -0
  52. package/dist/local-model-runtime.d.ts +6 -0
  53. package/dist/local-model-runtime.js +33 -21
  54. package/dist/model-bridge.d.ts +13 -1
  55. package/dist/model-bridge.js +410 -23
  56. package/dist/orchestrator-supervisor.d.ts +56 -0
  57. package/dist/orchestrator-supervisor.js +179 -1
  58. package/dist/plan-proposal.d.ts +115 -0
  59. package/dist/plan-proposal.js +1073 -0
  60. package/dist/run-ledger.js +3 -3
  61. package/dist/runtime-command.d.ts +8 -0
  62. package/dist/runtime-command.js +38 -6
  63. package/dist/runtime-executor.d.ts +20 -1
  64. package/dist/runtime-executor.js +737 -172
  65. package/dist/runtime-profile.d.ts +32 -0
  66. package/dist/runtime-profile.js +89 -13
  67. package/dist/runtime-tool-specs.d.ts +39 -0
  68. package/dist/runtime-tool-specs.js +144 -28
  69. package/dist/safe-edit.d.ts +7 -0
  70. package/dist/safe-edit.js +163 -37
  71. package/dist/schemas.js +48 -1
  72. package/dist/server.js +51 -0
  73. package/dist/shared.d.ts +3 -2
  74. package/dist/shared.js +2 -0
  75. package/dist/status-events.js +9 -6
  76. package/dist/store/ace-packed-store.d.ts +3 -2
  77. package/dist/store/ace-packed-store.js +188 -110
  78. package/dist/store/bootstrap-store.d.ts +2 -1
  79. package/dist/store/bootstrap-store.js +102 -83
  80. package/dist/store/cache-workspace.js +11 -5
  81. package/dist/store/materializers/context-snapshot-materializer.js +6 -2
  82. package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
  83. package/dist/store/materializers/hook-context-materializer.js +11 -21
  84. package/dist/store/materializers/host-file-materializer.js +6 -0
  85. package/dist/store/materializers/projection-manager.d.ts +0 -1
  86. package/dist/store/materializers/projection-manager.js +5 -13
  87. package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
  88. package/dist/store/materializers/vericify-projector.d.ts +7 -7
  89. package/dist/store/materializers/vericify-projector.js +11 -11
  90. package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
  91. package/dist/store/repositories/local-model-runtime-repository.js +242 -6
  92. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  93. package/dist/store/skills-install.d.ts +4 -0
  94. package/dist/store/skills-install.js +21 -12
  95. package/dist/store/state-reader.d.ts +2 -0
  96. package/dist/store/state-reader.js +20 -0
  97. package/dist/store/store-artifacts.d.ts +7 -0
  98. package/dist/store/store-artifacts.js +27 -1
  99. package/dist/store/store-authority-audit.d.ts +18 -1
  100. package/dist/store/store-authority-audit.js +115 -5
  101. package/dist/store/store-snapshot.d.ts +3 -0
  102. package/dist/store/store-snapshot.js +22 -2
  103. package/dist/store/workspace-store-paths.d.ts +39 -0
  104. package/dist/store/workspace-store-paths.js +94 -0
  105. package/dist/store/write-coordinator.d.ts +65 -0
  106. package/dist/store/write-coordinator.js +386 -0
  107. package/dist/todo-state.js +5 -5
  108. package/dist/tools-agent.d.ts +20 -0
  109. package/dist/tools-agent.js +789 -25
  110. package/dist/tools-discovery.js +136 -1
  111. package/dist/tools-files.d.ts +7 -0
  112. package/dist/tools-files.js +1002 -11
  113. package/dist/tools-framework.js +105 -66
  114. package/dist/tools-handoff.js +2 -2
  115. package/dist/tools-lifecycle.js +4 -4
  116. package/dist/tools-memory.js +6 -6
  117. package/dist/tools-todo.js +2 -2
  118. package/dist/tracker-adapters.d.ts +1 -1
  119. package/dist/tracker-adapters.js +13 -18
  120. package/dist/tracker-sync.js +5 -3
  121. package/dist/tui/agent-runner.js +3 -1
  122. package/dist/tui/chat.js +103 -7
  123. package/dist/tui/dashboard.d.ts +1 -0
  124. package/dist/tui/dashboard.js +43 -0
  125. package/dist/tui/index.js +10 -1
  126. package/dist/tui/layout.d.ts +20 -0
  127. package/dist/tui/layout.js +31 -1
  128. package/dist/tui/local-model-contract.d.ts +6 -2
  129. package/dist/tui/local-model-contract.js +16 -3
  130. package/dist/tui/ollama.d.ts +8 -1
  131. package/dist/tui/ollama.js +53 -12
  132. package/dist/tui/openai-compatible.d.ts +13 -0
  133. package/dist/tui/openai-compatible.js +305 -5
  134. package/dist/tui/provider-discovery.d.ts +1 -0
  135. package/dist/tui/provider-discovery.js +35 -11
  136. package/dist/vericify-bridge.d.ts +6 -1
  137. package/dist/vericify-bridge.js +27 -3
  138. package/dist/workspace-manager.d.ts +30 -3
  139. package/dist/workspace-manager.js +257 -27
  140. package/package.json +1 -2
  141. package/dist/internal-tool-runtime.d.ts +0 -21
  142. package/dist/internal-tool-runtime.js +0 -136
  143. package/dist/store/workspace-snapshot.d.ts +0 -26
  144. package/dist/store/workspace-snapshot.js +0 -107
@@ -2,20 +2,22 @@
2
2
  * Agent, skill, kernel, and task-pack tool registrations.
3
3
  */
4
4
  import { z } from "zod";
5
- import { ALL_AGENTS, COMPOSABLE_AGENTS, SWARM_AGENTS, SWARM_SUBAGENT_MAP, classifyPathSource, getAgentInstructionPath, getAgentManifestPath, getKernelArtifactPath, isSwarmRole, listAvailableSkills, readAgentInstructions, readAgentManifest, readKernelArtifact, readSkillInstructions, readTaskArtifact, resolveWorkspaceRoot, resolveWritableTaskPath, safeWrite, } from "./helpers.js";
5
+ import { ALL_AGENTS, COMPOSABLE_AGENTS, SWARM_AGENTS, SWARM_SUBAGENT_MAP, classifyPathSource, getAgentInstructionPath, getAgentManifestPath, getKernelArtifactPath, isSwarmRole, listAvailableSkills, readAgentInstructions, readAgentManifest, readKernelArtifact, readSkillInstructions, readTaskArtifact, resolveWorkspaceRoot, resolveWritableTaskPath, safeWriteAsync, } from "./helpers.js";
6
6
  import { loadRuntimeProfile, readRuntimePromptTemplate, readRuntimeProfileState, validateRuntimeProfileContent, } from "./runtime-profile.js";
7
7
  import { getUnattendedSession, listUnattendedSessions, startUnattendedSession, stopUnattendedSession, validateRuntimeExecutorSessionRegistryContent, waitForUnattendedSession, } from "./runtime-executor.js";
8
8
  import { executeRuntimeTool, listRuntimeToolSpecs, loadRuntimeToolRegistry, validateRuntimeToolRegistryContent, } from "./runtime-tool-specs.js";
9
- import { createWorkspaceSession, listWorkspaceSessions, removeWorkspaceSession, resolveRuntimeWorkspaceRoot, validateManagedWorkspacePath, } from "./workspace-manager.js";
9
+ import { createWorkspaceSessionAsync, listWorkspaceSessions, removeWorkspaceSession, resolveRuntimeWorkspaceRoot, validateManagedWorkspacePath, } from "./workspace-manager.js";
10
10
  import { getTrackerAdapter, listTrackerAdapterKinds, loadTrackerSnapshot, validateTrackerSnapshotContent, } from "./tracker-adapters.js";
11
11
  import { refreshTrackerSnapshot } from "./tracker-sync.js";
12
12
  import { appendVericifyProcessPost, loadVericifyBridgeSnapshot, loadVericifyProcessPostLog, refreshVericifyBridgeSnapshot, validateVericifyBridgeSnapshotContent, validateVericifyProcessPostLogContent, } from "./vericify-bridge.js";
13
13
  import { getRoleTitle, ROLE_ENUM, KERNEL_KEY_ENUM, ROLE_TITLES } from "./shared.js";
14
- import { createDefaultModelBridgeClients, resolveLocalModelRuntime, runLocalModelTask, } from "./local-model-runtime.js";
14
+ import { createDefaultModelBridgeClients, resolveLocalModelRuntime, resolveTier, runLocalModelTask, } from "./local-model-runtime.js";
15
+ import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
15
16
  import { executeAceInternalTool } from "./ace-internal-tools.js";
16
17
  import { ModelBridge } from "./model-bridge.js";
17
18
  import { getVericifyContextPacket, getVericifyDelta } from "./vericify-context.js";
18
- import { createTaskPlan, superviseTaskPlan, } from "./orchestrator-supervisor.js";
19
+ import { proposePlan as proposePlanImpl, proposePlanDeterministic, validatePlan as validatePlanImpl, loadAcceptanceTraceContract as loadAcceptanceTraceContractImpl, persistAcceptanceTraceMapWithContract, } from "./plan-proposal.js";
20
+ import { amendTaskPlan, createTaskPlan, superviseTaskPlan, } from "./orchestrator-supervisor.js";
19
21
  function parseOptionalJsonObject(raw) {
20
22
  if (!raw)
21
23
  return {};
@@ -52,6 +54,11 @@ function extractToolTextContent(result) {
52
54
  .filter(Boolean)
53
55
  .join("\n");
54
56
  }
57
+ function didExecuteGatesPass(result) {
58
+ if (Boolean(result?.isError))
59
+ return false;
60
+ return !extractToolTextContent(result).trimStart().startsWith("❌");
61
+ }
55
62
  function mapRoleToTaskType(role) {
56
63
  switch (role) {
57
64
  case "vos":
@@ -71,9 +78,268 @@ function extractHandoffId(text) {
71
78
  const lineMatch = text.match(/handoff_id:\s*([A-Z0-9-]+)/i);
72
79
  return lineMatch?.[1];
73
80
  }
81
+ export function formatStepTaskForBridge(step) {
82
+ const upstreamOutputs = step.upstream_outputs ?? [];
83
+ if (upstreamOutputs.length === 0) {
84
+ return step.task;
85
+ }
86
+ const renderedOutputs = upstreamOutputs
87
+ .map((output) => `- ${output.step_id}: ${output.result_summary || "[no summary]"}${output.evidence_refs.length > 0 ? ` (evidence: ${output.evidence_refs.join(", ")})` : ""}`)
88
+ .join("\n");
89
+ return [
90
+ step.task,
91
+ "",
92
+ "Upstream outputs:",
93
+ renderedOutputs,
94
+ ].join("\n");
95
+ }
96
+ // ── Re-export plan-proposal API ────────────────────────────────────────────
97
+ export { loadAcceptanceTraceContractImpl as loadAcceptanceTraceContract };
98
+ export { proposePlanImpl as proposePlan, validatePlanImpl as validatePlan };
99
+ // ── proposalToSteps: maps PlanProposal steps to local PlannedStepInput ────
100
+ function proposalToSteps(proposal) {
101
+ return proposal.steps.map((s) => ({
102
+ role: s.role,
103
+ task: s.task,
104
+ depends_on: s.depends_on && s.depends_on.length > 0 ? s.depends_on : undefined,
105
+ tool_scope: s.tool_scope && s.tool_scope.length > 0 ? s.tool_scope : undefined,
106
+ expected_output_class: s.expected_output_class,
107
+ expected_artifacts: s.expected_artifacts,
108
+ allowed_tools: s.allowed_tools,
109
+ forbidden_patterns: s.forbidden_patterns,
110
+ required_evidence_refs: s.required_evidence_refs,
111
+ structural_edit_plan_required: s.structural_edit_plan_required,
112
+ structural_edit_waiver: s.structural_edit_waiver,
113
+ }));
114
+ }
115
+ function stripPlanIdFromVerdict(verdict) {
116
+ return {
117
+ ok: verdict.ok,
118
+ score: verdict.score,
119
+ blocking_findings: [...verdict.blocking_findings],
120
+ soft_findings: [...verdict.soft_findings],
121
+ };
122
+ }
123
+ function proposalToNormalization(proposal, verdict) {
124
+ const acMap = new Map();
125
+ const stopConditionsMap = new Map();
126
+ proposal.steps.forEach((ps, idx) => {
127
+ acMap.set(stepLabel(idx), ps.acceptance_criteria ?? []);
128
+ stopConditionsMap.set(stepLabel(idx), ps.stop_condition ?? []);
129
+ });
130
+ return {
131
+ planSource: proposal.plan_source,
132
+ steps: proposalToSteps(proposal),
133
+ insertedResearch: false,
134
+ shipFanoutEnabled: false,
135
+ intentSummary: proposal.intent_summary,
136
+ successCriteria: proposal.success_criteria,
137
+ validationVerdict: stripPlanIdFromVerdict(verdict),
138
+ acceptanceCriteriaByStep: acMap,
139
+ stopConditionsByStep: stopConditionsMap,
140
+ };
141
+ }
142
+ function stepLabel(index) {
143
+ return `step-${index + 1}`;
144
+ }
145
+ function isShipBoundaryTask(task) {
146
+ return /\b(ship|release|promot(?:e|ion)|land|merge)\b/i.test(task);
147
+ }
148
+ function isImplementationRole(role) {
149
+ return role === "coders" || role === "builder";
150
+ }
74
151
  async function buildOrchestratorSteps(task, sessionId) {
75
- void sessionId;
76
- return [{ role: "orchestrator", task }];
152
+ // (a) propose a plan via the planner model
153
+ const proposal = await proposePlanImpl(task, sessionId);
154
+ // (b) validate the proposal
155
+ const verdict = await validatePlanImpl({ proposal, sessionId });
156
+ // (c) if ok, return validated steps with planner enrichment
157
+ if (verdict.ok && proposal.steps.length > 0) {
158
+ return proposalToNormalization(proposal, verdict);
159
+ }
160
+ // Deterministic fallback: still validate the floor before returning it.
161
+ const fallbackProposal = proposePlanDeterministic(task);
162
+ const fallbackVerdict = await validatePlanImpl({ proposal: fallbackProposal, sessionId });
163
+ return proposalToNormalization(fallbackProposal, fallbackVerdict);
164
+ }
165
+ function normalizeExplicitPlanSteps(steps, task) {
166
+ const originalIdByLabel = new Map();
167
+ const normalized = steps.map((step, index) => {
168
+ const id = `explicit-${index + 1}`;
169
+ originalIdByLabel.set(stepLabel(index), id);
170
+ return {
171
+ ...step,
172
+ id,
173
+ depends_on_ids: [...(step.depends_on ?? [])],
174
+ };
175
+ });
176
+ for (const step of normalized) {
177
+ step.depends_on_ids = step.depends_on_ids.map((dependency) => originalIdByLabel.get(dependency) ?? dependency);
178
+ }
179
+ let insertedResearch = false;
180
+ for (let index = 0; index < normalized.length; index += 1) {
181
+ const step = normalized[index];
182
+ if (step.role !== "spec")
183
+ continue;
184
+ const hasResearchBefore = normalized.slice(0, index).some((candidate) => candidate.role === "research");
185
+ if (hasResearchBefore)
186
+ continue;
187
+ const researchId = `auto-research-${index + 1}`;
188
+ normalized.splice(index, 0, {
189
+ id: researchId,
190
+ role: "research",
191
+ task: `Validate source-backed claims and gather evidence before spec work: ${step.task}`,
192
+ tool_scope: ["recall_context", "read_workspace_file", "build_continuity_packet"],
193
+ depends_on_ids: [],
194
+ });
195
+ step.depends_on_ids = Array.from(new Set([researchId, ...step.depends_on_ids]));
196
+ insertedResearch = true;
197
+ index += 1;
198
+ }
199
+ const shipFanoutEnabled = isShipBoundaryTask(task) || normalized.some((step) => isShipBoundaryTask(step.task));
200
+ if (shipFanoutEnabled) {
201
+ const hasReviewRole = normalized.some((step) => step.role === "skeptic");
202
+ const hasSecurityRole = normalized.some((step) => step.role === "security");
203
+ const hasQaRole = normalized.some((step) => step.role === "qa");
204
+ const hasReleaseRole = normalized.some((step) => step.role === "release");
205
+ const lastImplementation = [...normalized].reverse().find((step) => isImplementationRole(step.role)) ?? normalized.at(-1);
206
+ const implementationDependency = lastImplementation ? [lastImplementation.id] : [];
207
+ const fanoutStepIds = [];
208
+ if (!hasReviewRole) {
209
+ const reviewId = `auto-review-${normalized.length + 1}`;
210
+ normalized.push({
211
+ id: reviewId,
212
+ role: "skeptic",
213
+ task: `Review ship readiness for: ${task}`,
214
+ tool_scope: ["execute_gates", "validate_framework"],
215
+ depends_on_ids: implementationDependency,
216
+ });
217
+ fanoutStepIds.push(reviewId);
218
+ }
219
+ if (!hasSecurityRole) {
220
+ const securityId = `auto-security-${normalized.length + 1}`;
221
+ normalized.push({
222
+ id: securityId,
223
+ role: "security",
224
+ task: `Assess security readiness for: ${task}`,
225
+ tool_scope: ["execute_gates", "validate_framework"],
226
+ depends_on_ids: implementationDependency,
227
+ });
228
+ fanoutStepIds.push(securityId);
229
+ }
230
+ if (!hasQaRole) {
231
+ const qaId = `auto-qa-${normalized.length + 1}`;
232
+ normalized.push({
233
+ id: qaId,
234
+ role: "qa",
235
+ task: `Validate ship readiness for: ${task}`,
236
+ tool_scope: ["run_tests", "execute_gates", "git_diff"],
237
+ depends_on_ids: implementationDependency,
238
+ });
239
+ fanoutStepIds.push(qaId);
240
+ }
241
+ if (!hasReleaseRole && fanoutStepIds.length > 0) {
242
+ normalized.push({
243
+ id: `auto-release-${normalized.length + 1}`,
244
+ role: "release",
245
+ task: `Finalize ship decision for: ${task}`,
246
+ tool_scope: ["git_status", "execute_gates", "validate_framework"],
247
+ depends_on_ids: fanoutStepIds,
248
+ });
249
+ }
250
+ }
251
+ const finalIdByInternal = new Map();
252
+ normalized.forEach((step, index) => {
253
+ finalIdByInternal.set(step.id, stepLabel(index));
254
+ });
255
+ return {
256
+ planSource: "explicit_steps",
257
+ steps: normalized.map((step) => ({
258
+ role: step.role,
259
+ task: step.task,
260
+ depends_on: step.depends_on_ids.length > 0
261
+ ? step.depends_on_ids.map((dependency) => finalIdByInternal.get(dependency) ?? dependency)
262
+ : undefined,
263
+ parallel_group: step.parallel_group,
264
+ tool_scope: step.tool_scope,
265
+ expected_output_class: step.expected_output_class,
266
+ expected_artifacts: step.expected_artifacts,
267
+ allowed_tools: step.allowed_tools,
268
+ forbidden_patterns: step.forbidden_patterns,
269
+ required_evidence_refs: step.required_evidence_refs,
270
+ structural_edit_plan_required: step.structural_edit_plan_required,
271
+ structural_edit_waiver: step.structural_edit_waiver,
272
+ })),
273
+ insertedResearch,
274
+ shipFanoutEnabled,
275
+ };
276
+ }
277
+ async function normalizeOrchestratorPlanSteps(task, steps, sessionId) {
278
+ if (!Array.isArray(steps) || steps.length === 0) {
279
+ const normalization = await buildOrchestratorSteps(task, sessionId);
280
+ return { planSource: normalization.planSource, normalization };
281
+ }
282
+ const normalization = normalizeExplicitPlanSteps(steps, task);
283
+ // Run the same artifact/output-class gate that planner-derived steps go through.
284
+ const syntheticProposal = {
285
+ plan_id: `explicit-${Date.now()}`,
286
+ status: "planning",
287
+ intent_summary: task,
288
+ success_criteria: [],
289
+ steps: normalization.steps.map((s) => ({
290
+ role: s.role,
291
+ task: s.task,
292
+ depends_on: s.depends_on,
293
+ tool_scope: s.tool_scope,
294
+ acceptance_criteria: [],
295
+ expected_output_class: s.expected_output_class,
296
+ expected_artifacts: s.expected_artifacts,
297
+ allowed_tools: s.allowed_tools,
298
+ forbidden_patterns: s.forbidden_patterns,
299
+ required_evidence_refs: s.required_evidence_refs,
300
+ structural_edit_plan_required: s.structural_edit_plan_required,
301
+ structural_edit_waiver: s.structural_edit_waiver,
302
+ })),
303
+ plan_source: "explicit_steps",
304
+ };
305
+ const verdict = await validatePlanImpl({ proposal: syntheticProposal, sessionId });
306
+ normalization.validationVerdict = stripPlanIdFromVerdict(verdict);
307
+ return { planSource: "explicit_steps", normalization };
308
+ }
309
+ async function persistAcceptanceTraceMap(input) {
310
+ return persistAcceptanceTraceMapWithContract({
311
+ plan_id: input.plan.plan_id,
312
+ task: input.task,
313
+ plan_source: input.planSource,
314
+ intent_summary: input.normalization?.intentSummary,
315
+ success_criteria: input.normalization?.successCriteria,
316
+ validation_verdict: input.normalization?.validationVerdict,
317
+ policies: {
318
+ inserted_research_before_spec: input.insertedResearch,
319
+ ship_fanout_enabled: input.shipFanoutEnabled,
320
+ },
321
+ steps: input.plan.steps.map((step, idx) => ({
322
+ step_id: step.step_id,
323
+ role: step.role,
324
+ task: step.task,
325
+ depends_on: step.depends_on ?? [],
326
+ tool_scope: step.tool_scope ?? [],
327
+ verification_role: step.role === "coders" || step.role === "builder"
328
+ ? "qa"
329
+ : step.role === "spec"
330
+ ? "research"
331
+ : null,
332
+ acceptance_criteria: input.normalization?.acceptanceCriteriaByStep?.get(`step-${idx + 1}`),
333
+ stop_condition: input.normalization?.stopConditionsByStep?.get(`step-${idx + 1}`),
334
+ expected_output_class: step.expected_output_class,
335
+ expected_artifacts: step.expected_artifacts,
336
+ allowed_tools: step.allowed_tools,
337
+ forbidden_patterns: step.forbidden_patterns,
338
+ required_evidence_refs: step.required_evidence_refs,
339
+ structural_edit_plan_required: step.structural_edit_plan_required,
340
+ structural_edit_waiver: step.structural_edit_waiver,
341
+ })),
342
+ });
77
343
  }
78
344
  function appendUniqueNote(target, note) {
79
345
  if (!target.includes(note)) {
@@ -116,6 +382,265 @@ function buildDefaultOrchestratorAmendment(input) {
116
382
  add_after_step_id: input.step.step_id,
117
383
  };
118
384
  }
385
+ function isAcceptanceTraceContract(value) {
386
+ if (!value || typeof value !== "object")
387
+ return false;
388
+ const candidate = value;
389
+ return Array.isArray(candidate.steps);
390
+ }
391
+ const INTENT_STOPWORDS = new Set([
392
+ "the",
393
+ "and",
394
+ "for",
395
+ "with",
396
+ "this",
397
+ "that",
398
+ "step",
399
+ "step1",
400
+ "step2",
401
+ "step3",
402
+ "step4",
403
+ "step5",
404
+ "step6",
405
+ "step7",
406
+ "step8",
407
+ "step9",
408
+ "step10",
409
+ "complete",
410
+ "completed",
411
+ "validate",
412
+ "validation",
413
+ "implement",
414
+ "implementation",
415
+ "done",
416
+ "ready",
417
+ ]);
418
+ function normalizeIntentText(value) {
419
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
420
+ }
421
+ function intentTokens(value) {
422
+ return normalizeIntentText(value)
423
+ .split(/\s+/)
424
+ .filter((token) => token.length > 2 && !INTENT_STOPWORDS.has(token));
425
+ }
426
+ function intentCriterionSatisfied(haystack, criterion) {
427
+ const normalizedHaystack = normalizeIntentText(haystack);
428
+ const tokens = intentTokens(criterion);
429
+ if (tokens.length === 0)
430
+ return false;
431
+ const hits = tokens.filter((token) => normalizedHaystack.includes(token)).length;
432
+ return hits >= Math.min(2, tokens.length) || hits / tokens.length >= 0.5;
433
+ }
434
+ function resultContractText(result) {
435
+ return [
436
+ result.summary,
437
+ JSON.stringify(result.tool_calls ?? []),
438
+ JSON.stringify(result.child_results ?? []),
439
+ ].join("\n");
440
+ }
441
+ const ARTIFACT_STUB_PATTERN = /\b(?:todo-only|placeholder|stub(?:bed|by)?|tbd|boilerplate(?:-only)?|scaffold(?:ing)?\s+(?:stub|only))\b/i;
442
+ function isUiPlainTextContract(step, contractStep) {
443
+ const expected = contractStep.expected_output_class ?? inferContractClassFromStep(step);
444
+ return step.role === "ui" && expected === "plain_text_plan";
445
+ }
446
+ function validateUiTopicAnchor(input) {
447
+ if (!isUiPlainTextContract(input.step, input.contract_step)) {
448
+ return { ok: true };
449
+ }
450
+ const acceptanceCriteria = (input.contract_step.acceptance_criteria ?? []).filter((criterion) => typeof criterion === "string" && criterion.trim().length > 0);
451
+ if (acceptanceCriteria.length === 0) {
452
+ return { ok: true };
453
+ }
454
+ const outputText = resultContractText(input.result);
455
+ if (intentTokens(outputText).length < 4) {
456
+ return { ok: true };
457
+ }
458
+ const taskAnchored = intentCriterionSatisfied(outputText, input.step.task);
459
+ const matchedCriteria = acceptanceCriteria.filter((criterion) => intentCriterionSatisfied(outputText, criterion));
460
+ if (!taskAnchored && matchedCriteria.length === 0) {
461
+ return {
462
+ ok: false,
463
+ reason_code: "role_drift_ui_off_topic",
464
+ reason: `UI step ${input.step.step_id} drifted off topic instead of staying anchored to the requested plan.`,
465
+ uncovered_clauses: acceptanceCriteria,
466
+ };
467
+ }
468
+ return { ok: true };
469
+ }
470
+ function inferContractClassFromStep(step) {
471
+ if (step.role === "qa" || step.role === "release")
472
+ return "qa_verdict";
473
+ if ((step.tool_scope ?? []).some((tool) => tool.includes("astgrep") || tool.includes("structural_edit"))) {
474
+ return "structural_edit_plan";
475
+ }
476
+ if ((step.tool_scope ?? []).some((tool) => tool.includes("write") || tool.includes("safe_edit"))) {
477
+ return "code_artifact";
478
+ }
479
+ if ((step.tool_scope ?? []).length > 0)
480
+ return "tool_envelope";
481
+ return "plain_text_plan";
482
+ }
483
+ function requiresContract(step) {
484
+ if ((step.tool_scope ?? []).length > 0)
485
+ return true;
486
+ if (step.role === "coders" || step.role === "builder" || step.role === "qa")
487
+ return true;
488
+ return (step.tool_scope ?? []).some((tool) => /write|edit|safe_edit|astgrep|structural/i.test(tool));
489
+ }
490
+ function validateContractClass(input) {
491
+ const expected = input.contract_step.expected_output_class ?? inferContractClassFromStep(input.step);
492
+ const toolCalls = input.result.tool_calls ?? [];
493
+ const evidenceRefs = input.result.evidence_refs ?? [];
494
+ if (input.contract_step.allowed_tools?.length) {
495
+ const allowed = new Set(input.contract_step.allowed_tools);
496
+ const disallowed = toolCalls.find((call) => !allowed.has(call.tool));
497
+ if (disallowed) {
498
+ return {
499
+ ok: false,
500
+ reason_code: "bridge_output_malformed_json",
501
+ reason: `Tool ${disallowed.tool} is not allowed for ${input.step.step_id}.`,
502
+ };
503
+ }
504
+ }
505
+ if (expected === "qa_verdict" && toolCalls.some((call) => /write|edit|rewrite|safe_edit/i.test(call.tool))) {
506
+ return {
507
+ ok: false,
508
+ reason_code: "qa_rewrote_artifact",
509
+ reason: `QA step ${input.step.step_id} attempted a mutation tool.`,
510
+ };
511
+ }
512
+ if ((expected === "tool_envelope" || expected === "structural_edit_plan") && toolCalls.length === 0) {
513
+ return {
514
+ ok: false,
515
+ reason_code: "bridge_output_malformed_json",
516
+ reason: `Step ${input.step.step_id} expected a tool envelope but produced none.`,
517
+ };
518
+ }
519
+ if (expected === "code_artifact" || expected === "structural_edit_plan") {
520
+ const expectedArtifacts = (input.contract_step.expected_artifacts ?? []).filter((artifact) => artifact.required !== false);
521
+ const missingArtifacts = expectedArtifacts
522
+ .map((artifact) => artifact.path)
523
+ .filter((artifactPath) => !evidenceRefs.some((ref) => ref.includes(artifactPath)));
524
+ if (missingArtifacts.length > 0) {
525
+ return {
526
+ ok: false,
527
+ reason_code: "artifact_mismatch",
528
+ reason: `Step ${input.step.step_id} did not prove expected artifact evidence.`,
529
+ uncovered_clauses: missingArtifacts,
530
+ };
531
+ }
532
+ }
533
+ if (expected === "code_artifact") {
534
+ const mutationSignal = toolCalls.some((call) => /write|edit|patch|create|apply|safe_edit/i.test(call.tool)) || evidenceRefs.length > 0;
535
+ if (mutationSignal && ARTIFACT_STUB_PATTERN.test(resultContractText(input.result))) {
536
+ return {
537
+ ok: false,
538
+ reason_code: "coder_artifact_stub",
539
+ reason: `Step ${input.step.step_id} reported a stub or placeholder artifact instead of a real implementation.`,
540
+ };
541
+ }
542
+ }
543
+ const forbiddenPatterns = input.contract_step.forbidden_patterns ?? [];
544
+ const outputText = resultContractText(input.result);
545
+ const forbiddenHit = forbiddenPatterns.find((pattern) => outputText.includes(pattern));
546
+ if (forbiddenHit) {
547
+ return {
548
+ ok: false,
549
+ reason_code: "forbidden_pattern",
550
+ reason: `Step ${input.step.step_id} output matched a forbidden pattern.`,
551
+ uncovered_clauses: [forbiddenHit],
552
+ };
553
+ }
554
+ const missingEvidence = (input.contract_step.required_evidence_refs ?? [])
555
+ .filter((required) => !evidenceRefs.some((ref) => ref.includes(required)));
556
+ if (missingEvidence.length > 0) {
557
+ return {
558
+ ok: false,
559
+ reason_code: "required_evidence_missing",
560
+ reason: `Step ${input.step.step_id} is missing required evidence refs.`,
561
+ uncovered_clauses: missingEvidence,
562
+ };
563
+ }
564
+ return { ok: true };
565
+ }
566
+ export function verifyIntentAgainstContract(input) {
567
+ const contract = isAcceptanceTraceContract(input.intent_contract)
568
+ ? input.intent_contract
569
+ : loadAcceptanceTraceContractImpl(input.plan.plan_id);
570
+ if (!contract) {
571
+ if (requiresContract(input.step)) {
572
+ return {
573
+ outcome: "revisit_step",
574
+ reason: `No acceptance trace contract available for ${input.plan.plan_id}.`,
575
+ reason_code: "contract_missing",
576
+ };
577
+ }
578
+ return {
579
+ outcome: "ok",
580
+ reason: `No acceptance trace contract available for ${input.plan.plan_id}.`,
581
+ };
582
+ }
583
+ const contractStep = contract.steps.find((candidate) => candidate.step_id === input.step.step_id);
584
+ if (!contractStep) {
585
+ return {
586
+ outcome: requiresContract(input.step) ? "revisit_step" : "ok",
587
+ reason: `No acceptance trace step recorded for ${input.step.step_id}.`,
588
+ reason_code: requiresContract(input.step) ? "contract_missing" : undefined,
589
+ };
590
+ }
591
+ const classValidation = validateContractClass({
592
+ step: input.step,
593
+ result: input.result,
594
+ contract_step: contractStep,
595
+ });
596
+ if (!classValidation.ok) {
597
+ return {
598
+ outcome: "revisit_step",
599
+ reason: classValidation.reason ?? `Step ${input.step.step_id} failed contract-class validation.`,
600
+ reason_code: classValidation.reason_code,
601
+ uncovered_clauses: classValidation.uncovered_clauses,
602
+ };
603
+ }
604
+ const acceptanceCriteria = (contractStep?.acceptance_criteria ?? []).filter((criterion) => typeof criterion === "string" && criterion.trim().length > 0);
605
+ if (acceptanceCriteria.length === 0) {
606
+ return {
607
+ outcome: "ok",
608
+ reason: `No acceptance criteria recorded for ${input.step.step_id}.`,
609
+ };
610
+ }
611
+ const uiTopicValidation = validateUiTopicAnchor({
612
+ step: input.step,
613
+ result: input.result,
614
+ contract_step: contractStep,
615
+ });
616
+ if (!uiTopicValidation.ok) {
617
+ return {
618
+ outcome: "revisit_step",
619
+ reason: uiTopicValidation.reason ?? `Step ${input.step.step_id} drifted off topic.`,
620
+ reason_code: uiTopicValidation.reason_code,
621
+ uncovered_clauses: uiTopicValidation.uncovered_clauses,
622
+ };
623
+ }
624
+ const haystack = [
625
+ input.step.task,
626
+ input.result.summary,
627
+ JSON.stringify(input.result.tool_calls ?? []),
628
+ JSON.stringify(input.result.child_results ?? []),
629
+ ].join("\n");
630
+ const uncovered = acceptanceCriteria.filter((criterion) => !intentCriterionSatisfied(haystack, criterion));
631
+ if (uncovered.length === 0) {
632
+ return {
633
+ outcome: "ok",
634
+ reason: `Step ${input.step.step_id} satisfied persisted acceptance criteria.`,
635
+ };
636
+ }
637
+ return {
638
+ outcome: "revisit_step",
639
+ reason: `Step ${input.step.step_id} did not satisfy acceptance criteria yet.`,
640
+ reason_code: "contract_invalid",
641
+ uncovered_clauses: uncovered,
642
+ };
643
+ }
119
644
  async function tryVericifyPacket(factory, onWarning) {
120
645
  try {
121
646
  return await factory();
@@ -282,6 +807,45 @@ export function registerAgentTools(server) {
282
807
  ],
283
808
  };
284
809
  });
810
+ server.tool("get_transition_log", "Read the transition log for a session. Answers 'why stopped?', 'what changed?', and 'what evidence caused it?' from the ACE transition record store.", {
811
+ session_id: z.string().describe("Session ID to read transitions for"),
812
+ limit: z.number().int().positive().optional().default(20).describe("Max transitions to return (default: 20)"),
813
+ }, async ({ session_id, limit }) => {
814
+ try {
815
+ const transitions = await withLocalModelRuntimeRepository(resolveWorkspaceRoot(), (repo) => repo.getTransitionLog(session_id, limit ?? 20));
816
+ return {
817
+ content: [{ type: "text", text: JSON.stringify(transitions, null, 2) }],
818
+ };
819
+ }
820
+ catch (error) {
821
+ return {
822
+ content: [{ type: "text", text: `Error reading transition log: ${error instanceof Error ? error.message : String(error)}` }],
823
+ isError: true,
824
+ };
825
+ }
826
+ });
827
+ server.tool("get_capability_snapshot", "Read the capability snapshot for a specific turn. Shows which tools were available, their cost class, and any unavailable tools with reasons.", {
828
+ session_id: z.string().describe("Session ID"),
829
+ turn_number: z.number().int().positive().describe("Turn number"),
830
+ }, async ({ session_id, turn_number }) => {
831
+ try {
832
+ const snapshot = await withLocalModelRuntimeRepository(resolveWorkspaceRoot(), (repo) => repo.getCapabilitySnapshot(session_id, turn_number));
833
+ if (!snapshot) {
834
+ return {
835
+ content: [{ type: "text", text: `No capability snapshot found for session ${session_id} turn ${turn_number}` }],
836
+ };
837
+ }
838
+ return {
839
+ content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }],
840
+ };
841
+ }
842
+ catch (error) {
843
+ return {
844
+ content: [{ type: "text", text: `Error reading capability snapshot: ${error instanceof Error ? error.message : String(error)}` }],
845
+ isError: true,
846
+ };
847
+ }
848
+ });
285
849
  server.tool("validate_runtime_profile", "Validate ACE runtime profile markdown content or the current ACE_WORKFLOW.md file", {
286
850
  content: z
287
851
  .string()
@@ -493,7 +1057,19 @@ export function registerAgentTools(server) {
493
1057
  .boolean()
494
1058
  .optional()
495
1059
  .describe("If false, keep the managed workspace session after completion"),
496
- }, async ({ task, context_json, workspace_name, workspace_path, objective_id, tracker_item_id, max_turns, turn_timeout_ms, auto_cleanup, }) => {
1060
+ emit_to: z
1061
+ .array(z.enum(["tui", "tracker", "handoff", "vericify"]))
1062
+ .optional()
1063
+ .describe("Output emission targets for this session. Defaults to ['tui', 'vericify']."),
1064
+ silent_unless_blocked: z
1065
+ .boolean()
1066
+ .optional()
1067
+ .describe("If true, turns that produce no tool calls are classified as no_op_success and do not emit."),
1068
+ require_approval_before_emit: z
1069
+ .boolean()
1070
+ .optional()
1071
+ .describe("If true, require operator approval before emitting meaningful_completion output."),
1072
+ }, async ({ task, context_json, workspace_name, workspace_path, objective_id, tracker_item_id, max_turns, turn_timeout_ms, auto_cleanup, emit_to, silent_unless_blocked, require_approval_before_emit, }) => {
497
1073
  const result = await startUnattendedSession({
498
1074
  task,
499
1075
  context: parseOptionalJsonObject(context_json),
@@ -504,6 +1080,9 @@ export function registerAgentTools(server) {
504
1080
  max_turns,
505
1081
  turn_timeout_ms,
506
1082
  auto_cleanup,
1083
+ emit_to,
1084
+ silent_unless_blocked,
1085
+ require_approval_before_emit,
507
1086
  });
508
1087
  return {
509
1088
  content: [
@@ -557,6 +1136,56 @@ export function registerAgentTools(server) {
557
1136
  ],
558
1137
  };
559
1138
  });
1139
+ server.tool("propose_plan", "Use the ACE Planner role to decompose a task into a multi-step PlanProposal with intent_summary, success_criteria, per-step acceptance_criteria, and explicit stop conditions. On model failure, falls back to the deterministic goal compiler scaffold.", {
1140
+ task: z.string().describe("The task to decompose into a plan"),
1141
+ session_id: z.string().optional().describe("Optional session ID for transition record linkage"),
1142
+ }, async ({ task, session_id }) => {
1143
+ const proposal = await proposePlanImpl(task, session_id);
1144
+ return {
1145
+ content: [
1146
+ {
1147
+ type: "text",
1148
+ text: JSON.stringify(proposal, null, 2),
1149
+ },
1150
+ ],
1151
+ };
1152
+ });
1153
+ server.tool("validate_plan", "Run shape checks against a PlanProposal (coverage, verification chain, tool-scope realism, acceptance criteria presence, stop conditions visibility). Returns { ok, score, blocking_findings, soft_findings }. Persists a transition record and Vericify process post.", {
1154
+ plan_id: z.string().optional().describe("ID of a previously proposed plan to validate"),
1155
+ proposal: z
1156
+ .object({
1157
+ plan_id: z.string(),
1158
+ status: z.literal("planning"),
1159
+ intent_summary: z.string(),
1160
+ success_criteria: z.array(z.string()),
1161
+ steps: z.array(z.object({
1162
+ role: z.string(),
1163
+ task: z.string(),
1164
+ depends_on: z.array(z.string()).optional(),
1165
+ tool_scope: z.array(z.string()).optional(),
1166
+ acceptance_criteria: z.array(z.string()),
1167
+ stop_condition: z.array(z.string()).optional(),
1168
+ })),
1169
+ plan_source: z.string(),
1170
+ })
1171
+ .optional()
1172
+ .describe("Inline proposal to validate; mutually exclusive with plan_id"),
1173
+ session_id: z.string().optional().describe("Optional session ID for transition record linkage"),
1174
+ }, async ({ plan_id, proposal, session_id }) => {
1175
+ const result = await validatePlanImpl({
1176
+ plan_id,
1177
+ proposal: proposal,
1178
+ sessionId: session_id,
1179
+ });
1180
+ return {
1181
+ content: [
1182
+ {
1183
+ type: "text",
1184
+ text: JSON.stringify(result, null, 2),
1185
+ },
1186
+ ],
1187
+ };
1188
+ });
560
1189
  server.tool("run_local_model", "Offload a governed ACE subtask to the provider-backed ACE bridge and return the result", {
561
1190
  task: z.string().describe("Task to execute with the ACE model bridge"),
562
1191
  role: ROLE_ENUM.optional().describe("Optional ACE role; defaults to orchestrator"),
@@ -578,6 +1207,10 @@ export function registerAgentTools(server) {
578
1207
  .string()
579
1208
  .optional()
580
1209
  .describe("Optional model override; otherwise discovered from workspace/runtime context"),
1210
+ model_class: z
1211
+ .enum(["frontier", "mid", "small_local"])
1212
+ .optional()
1213
+ .describe("Optional capability class override; provider name alone is not used as capability authority"),
581
1214
  base_url: z
582
1215
  .string()
583
1216
  .optional()
@@ -594,13 +1227,14 @@ export function registerAgentTools(server) {
594
1227
  .string()
595
1228
  .optional()
596
1229
  .describe("Optional workspace root override; defaults to the active workspace"),
597
- }, async ({ task, role, max_turns, tier, provider, model, base_url, ollama_url, tool_scope, workspace_root, }) => {
1230
+ }, async ({ task, role, max_turns, tier, provider, model, model_class, base_url, ollama_url, tool_scope, workspace_root, }) => {
598
1231
  const delegated = await runLocalModelTask({
599
1232
  task,
600
1233
  role,
601
1234
  workspaceRoot: workspace_root,
602
1235
  provider,
603
1236
  model,
1237
+ modelClass: model_class,
604
1238
  baseUrl: base_url,
605
1239
  ollamaUrl: ollama_url,
606
1240
  maxTurns: max_turns,
@@ -616,6 +1250,9 @@ export function registerAgentTools(server) {
616
1250
  `- role: ${delegated.role}`,
617
1251
  `- provider: ${delegated.runtime.provider}`,
618
1252
  `- model: ${delegated.runtime.model}`,
1253
+ `- model_class: ${delegated.policy.model_class}`,
1254
+ `- tier: ${delegated.policy.tier}`,
1255
+ `- mutation_lane: ${delegated.policy.mutation_lane}`,
619
1256
  `- workspace: ${delegated.runtime.workspaceRoot}`,
620
1257
  `- status: ${delegated.result.status}`,
621
1258
  `- turns: ${delegated.result.turns}`,
@@ -646,7 +1283,7 @@ export function registerAgentTools(server) {
646
1283
  ],
647
1284
  };
648
1285
  });
649
- server.tool("run_orchestrator", "Execute a supervised plan via model bridge child runs; when steps are omitted, the plan starts with ACE-Orchestrator", {
1286
+ server.tool("run_orchestrator", "Execute a supervised plan via model bridge child runs; when steps are omitted, the plan is compiled through the deterministic goal scaffold first", {
650
1287
  task: z.string().describe("The task to decompose and execute"),
651
1288
  steps: z
652
1289
  .array(z.object({
@@ -664,9 +1301,44 @@ export function registerAgentTools(server) {
664
1301
  .array(z.string())
665
1302
  .optional()
666
1303
  .describe("Optional ACE tool allowlist for the step"),
1304
+ expected_output_class: z
1305
+ .enum(["plain_text_plan", "tool_envelope", "code_artifact", "structural_edit_plan", "qa_verdict"])
1306
+ .optional()
1307
+ .describe("Optional expected output contract class for intent verification"),
1308
+ expected_artifacts: z
1309
+ .array(z.object({
1310
+ path: z.string(),
1311
+ required: z.boolean().optional(),
1312
+ evidence_ref_kind: z.enum(["artifact", "diff", "hash", "test", "gate"]).optional(),
1313
+ }))
1314
+ .optional()
1315
+ .describe("Optional artifact evidence expected from this step"),
1316
+ allowed_tools: z
1317
+ .array(z.string())
1318
+ .optional()
1319
+ .describe("Optional stricter tool allowlist for contract verification"),
1320
+ forbidden_patterns: z
1321
+ .array(z.string())
1322
+ .optional()
1323
+ .describe("Optional forbidden output substrings for contract verification"),
1324
+ required_evidence_refs: z
1325
+ .array(z.string())
1326
+ .optional()
1327
+ .describe("Optional evidence ref substrings required from this step"),
1328
+ structural_edit_plan_required: z
1329
+ .boolean()
1330
+ .optional()
1331
+ .describe("Require this code-mutating step to route through a structural edit plan"),
1332
+ structural_edit_waiver: z
1333
+ .object({
1334
+ reason: z.string(),
1335
+ evidence_ref: z.string(),
1336
+ })
1337
+ .optional()
1338
+ .describe("Evidence-backed waiver when a code-mutating step cannot use structural edits"),
667
1339
  }))
668
1340
  .optional()
669
- .describe("Pre-defined steps; if omitted, the orchestrator starts with a single ACE-Orchestrator step"),
1341
+ .describe("Pre-defined steps; if omitted, the orchestrator compiles a deterministic goal scaffold first"),
670
1342
  execution_mode: z
671
1343
  .enum(["sequential", "scheduled"])
672
1344
  .optional()
@@ -716,27 +1388,53 @@ export function registerAgentTools(server) {
716
1388
  const effectiveWorkspaceRoot = runtime?.workspaceRoot ??
717
1389
  (workspace_root ? resolveRuntimeWorkspaceRoot(workspace_root) : resolveWorkspaceRoot());
718
1390
  const sessionId = typeof extra?.sessionId === "string" ? extra.sessionId : undefined;
719
- const planSource = Array.isArray(steps) && steps.length > 0 ? "explicit_steps" : "orchestrator_default_step";
720
- const planSteps = Array.isArray(steps) && steps.length > 0
721
- ? steps
722
- : await buildOrchestratorSteps(task, sessionId);
1391
+ const { planSource, normalization } = await normalizeOrchestratorPlanSteps(task, steps, sessionId);
723
1392
  const plan = createTaskPlan({
724
1393
  task,
725
- steps: planSteps,
1394
+ steps: normalization.steps,
726
1395
  execution_mode: execution_mode ?? "sequential",
727
1396
  });
1397
+ let traceArtifactPath = await persistAcceptanceTraceMap({
1398
+ plan,
1399
+ task,
1400
+ planSource,
1401
+ insertedResearch: normalization.insertedResearch,
1402
+ shipFanoutEnabled: normalization.shipFanoutEnabled,
1403
+ normalization,
1404
+ });
1405
+ const intentContract = loadAcceptanceTraceContractImpl(plan.plan_id);
728
1406
  const bridge = runtime
729
1407
  ? new ModelBridge(createDefaultModelBridgeClients(runtime))
730
1408
  : undefined;
731
1409
  const fallbackHandoffPrefix = `LOCAL-${plan.plan_id}-`;
732
1410
  const vericifyWarnings = [];
1411
+ const appendSessionPlanTransition = async (stepId, from, to, reason, reasonCode) => {
1412
+ if (!sessionId)
1413
+ return;
1414
+ await withLocalModelRuntimeRepository(effectiveWorkspaceRoot, async (repo) => {
1415
+ const step = plan.steps.find((candidate) => candidate.step_id === stepId);
1416
+ if (!step)
1417
+ return;
1418
+ await repo.appendTransitionRecord({
1419
+ session_id: sessionId,
1420
+ subject_kind: "plan_step",
1421
+ subject_id: `${plan.plan_id}/${step.step_id}`,
1422
+ from,
1423
+ to,
1424
+ reason,
1425
+ reason_code: reasonCode,
1426
+ evidence_refs: step.tool_scope ?? [],
1427
+ });
1428
+ }).catch(() => undefined);
1429
+ };
733
1430
  const supervised = await superviseTaskPlan(plan, {
734
1431
  async spawnStep(step) {
735
1432
  if (bridge && runtime) {
736
1433
  return bridge.spawn({
737
- task: step.task,
1434
+ task: formatStepTaskForBridge(step),
738
1435
  role: step.role,
739
1436
  workspace: runtime.workspaceRoot,
1437
+ tier: resolveTier(undefined, runtime.provider, runtime.model, step.role),
740
1438
  maxTurns: max_turns_per_step ?? 6,
741
1439
  provider: runtime.provider,
742
1440
  model: runtime.model,
@@ -775,12 +1473,29 @@ export function registerAgentTools(server) {
775
1473
  note,
776
1474
  }, sessionId);
777
1475
  },
778
- amendPlan({ plan: activePlan, step, result }) {
779
- return buildDefaultOrchestratorAmendment({
1476
+ async amendPlan({ plan: activePlan, step, result }) {
1477
+ await appendSessionPlanTransition(step.step_id, "running", step.status, `Plan step ${step.step_id} ${step.status}: ${result.summary}`, step.status === "done"
1478
+ ? "step_completed"
1479
+ : step.status === "blocked"
1480
+ ? "step_blocked"
1481
+ : "step_failed");
1482
+ const amendment = buildDefaultOrchestratorAmendment({
780
1483
  plan: activePlan,
781
1484
  step,
782
1485
  result,
783
1486
  });
1487
+ if (!amendment)
1488
+ return undefined;
1489
+ const amendedPlan = amendTaskPlan(activePlan, amendment);
1490
+ traceArtifactPath = await persistAcceptanceTraceMap({
1491
+ plan: amendedPlan,
1492
+ task,
1493
+ planSource,
1494
+ insertedResearch: normalization.insertedResearch,
1495
+ shipFanoutEnabled: normalization.shipFanoutEnabled,
1496
+ normalization,
1497
+ });
1498
+ return amendedPlan;
784
1499
  },
785
1500
  async getVericifyContext() {
786
1501
  return tryVericifyPacket(() => getVericifyContextPacket({
@@ -793,6 +1508,34 @@ export function registerAgentTools(server) {
793
1508
  workspaceRoot: effectiveWorkspaceRoot,
794
1509
  }), (message) => appendUniqueNote(vericifyWarnings, `Vericify delta unavailable for ${plan.plan_id}: ${message}`));
795
1510
  },
1511
+ async verifyIntent({ plan: activePlan, step, result, intent_contract }) {
1512
+ const verification = verifyIntentAgainstContract({
1513
+ plan: activePlan,
1514
+ step,
1515
+ result,
1516
+ intent_contract: intent_contract ?? loadAcceptanceTraceContractImpl(activePlan.plan_id) ?? intentContract,
1517
+ });
1518
+ // Transition recording is deferred to recordIntentVerificationFailure so
1519
+ // the supervisor can supply the correct from/to based on retry state.
1520
+ return verification;
1521
+ },
1522
+ async replanForClauses({ uncovered_clauses }) {
1523
+ if (!uncovered_clauses.length) {
1524
+ return undefined;
1525
+ }
1526
+ return {
1527
+ append_steps: [
1528
+ {
1529
+ role: "research",
1530
+ task: `Resolve uncovered acceptance clauses: ${uncovered_clauses.join("; ")}`,
1531
+ tool_scope: ["recall_context", "read_workspace_file", "build_continuity_packet"],
1532
+ },
1533
+ ],
1534
+ };
1535
+ },
1536
+ async recordIntentVerificationFailure({ step, verification, from, to }) {
1537
+ await appendSessionPlanTransition(step.step_id, from, to, verification.reason, verification.reason_code);
1538
+ },
796
1539
  async openCircuitBreaker(reason) {
797
1540
  await executeAceInternalTool("open_circuit_breaker", {
798
1541
  reason,
@@ -807,7 +1550,7 @@ export function registerAgentTools(server) {
807
1550
  async executeGates() {
808
1551
  const result = await executeAceInternalTool("execute_gates", {}, sessionId);
809
1552
  return {
810
- ok: !Boolean(result?.isError),
1553
+ ok: didExecuteGatesPass(result),
811
1554
  summary: extractToolTextContent(result),
812
1555
  };
813
1556
  },
@@ -822,6 +1565,9 @@ export function registerAgentTools(server) {
822
1565
  }, sessionId);
823
1566
  },
824
1567
  async emitStatusEvent(event) {
1568
+ if (event.step_id) {
1569
+ await appendSessionPlanTransition(event.step_id, "planned", "running", `Plan step ${event.step_id} started: ${event.summary}`, "step_started");
1570
+ }
825
1571
  await executeAceInternalTool("emit_status_event", {
826
1572
  source_module: "capability-ops",
827
1573
  event_type: "ORCHESTRATOR_STEP",
@@ -831,6 +1577,14 @@ export function registerAgentTools(server) {
831
1577
  }, sessionId);
832
1578
  },
833
1579
  });
1580
+ traceArtifactPath = await persistAcceptanceTraceMap({
1581
+ plan: supervised.plan,
1582
+ task,
1583
+ planSource,
1584
+ insertedResearch: normalization.insertedResearch,
1585
+ shipFanoutEnabled: normalization.shipFanoutEnabled,
1586
+ normalization,
1587
+ });
834
1588
  const step_summaries = supervised.plan.steps.map((step) => ({
835
1589
  step_id: step.step_id,
836
1590
  role: step.role,
@@ -851,15 +1605,25 @@ export function registerAgentTools(server) {
851
1605
  runtime_warnings: runtimeWarnings,
852
1606
  workspace_root: effectiveWorkspaceRoot,
853
1607
  plan_source: planSource,
854
- planning_note: planSource === "orchestrator_default_step"
855
- ? "Auto-planning currently starts with ACE-Orchestrator. Pass explicit steps for multi-step orchestration."
856
- : null,
1608
+ planning_note: planSource === "orchestrator_default_step" || planSource === "deterministic_fallback" || planSource === "planner_model"
1609
+ ? "Auto-planning now starts with a deterministic goal compiler scaffold; planner_model means the scaffold was refined."
1610
+ : normalization.insertedResearch
1611
+ ? "Research was inserted ahead of spec work to require source-backed evidence before specification."
1612
+ : normalization.shipFanoutEnabled
1613
+ ? "Ship fan-out enforcement added review, security, QA, and release coordination steps."
1614
+ : null,
1615
+ trace_artifact_path: traceArtifactPath,
1616
+ engskills_imports: {
1617
+ inserted_research_before_spec: normalization.insertedResearch,
1618
+ ship_fanout_enabled: normalization.shipFanoutEnabled,
1619
+ },
857
1620
  plan: supervised.plan,
858
1621
  step_summaries,
859
1622
  handoff_ids: supervised.handoff_ids,
860
1623
  job_ids: supervised.job_ids,
861
1624
  circuit_opened: supervised.circuit_opened,
862
1625
  final_gate: supervised.final_gate ?? null,
1626
+ plan_validation_verdict: normalization.validationVerdict ?? null,
863
1627
  vericify_warnings: vericifyWarnings,
864
1628
  }, null, 2),
865
1629
  },
@@ -1153,7 +1917,7 @@ export function registerAgentTools(server) {
1153
1917
  .optional()
1154
1918
  .describe("Optional hook timeout override in milliseconds"),
1155
1919
  }, async ({ workspace_name, workspace_path, source, objective_id, tracker_item_id, root, hooks_timeout_ms, }) => {
1156
- const result = createWorkspaceSession({
1920
+ const result = await createWorkspaceSessionAsync({
1157
1921
  workspace_name,
1158
1922
  workspace_path,
1159
1923
  source,
@@ -1204,7 +1968,7 @@ export function registerAgentTools(server) {
1204
1968
  .optional()
1205
1969
  .describe("Optional hook timeout override in milliseconds"),
1206
1970
  }, async ({ session_id, workspace_path, root, hooks_timeout_ms }) => {
1207
- const result = removeWorkspaceSession({
1971
+ const result = await removeWorkspaceSession({
1208
1972
  session_id,
1209
1973
  workspace_path,
1210
1974
  root,
@@ -1540,7 +2304,7 @@ export function registerAgentTools(server) {
1540
2304
  "",
1541
2305
  ].join("\n");
1542
2306
  const existing = readTaskArtifact("lessons");
1543
- const path = safeWrite(resolveWritableTaskPath("lessons"), `${existing}${entry}`);
2307
+ const path = await safeWriteAsync(resolveWritableTaskPath("lessons"), `${existing}${entry}`);
1544
2308
  return {
1545
2309
  content: [{ type: "text", text: `✅ Lesson recorded in ${path}` }],
1546
2310
  };