@voybio/ace-swarm 2.4.0 → 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 (63) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -0
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  5. package/assets/agent-state/runtime-tool-specs.json +70 -2
  6. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  7. package/assets/instructions/ACE_UI.instructions.md +11 -0
  8. package/dist/ace-context.js +70 -11
  9. package/dist/ace-internal-tools.d.ts +3 -1
  10. package/dist/ace-internal-tools.js +10 -2
  11. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  12. package/dist/agent-runtime/role-adapters.js +49 -5
  13. package/dist/astgrep-index.d.ts +48 -0
  14. package/dist/astgrep-index.js +126 -1
  15. package/dist/cli.js +205 -15
  16. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  17. package/dist/discovery-runtime-wrappers.js +615 -0
  18. package/dist/helpers/bootstrap.js +1 -1
  19. package/dist/helpers/constants.d.ts +2 -2
  20. package/dist/helpers/constants.js +7 -0
  21. package/dist/helpers/path-utils.d.ts +8 -1
  22. package/dist/helpers/path-utils.js +27 -8
  23. package/dist/helpers/store-resolution.js +7 -3
  24. package/dist/job-scheduler.js +30 -4
  25. package/dist/json-sanitizer.d.ts +16 -0
  26. package/dist/json-sanitizer.js +26 -0
  27. package/dist/local-model-policy.d.ts +27 -0
  28. package/dist/local-model-policy.js +84 -0
  29. package/dist/local-model-runtime.d.ts +6 -0
  30. package/dist/local-model-runtime.js +21 -20
  31. package/dist/model-bridge.d.ts +6 -1
  32. package/dist/model-bridge.js +338 -21
  33. package/dist/orchestrator-supervisor.d.ts +42 -0
  34. package/dist/orchestrator-supervisor.js +110 -3
  35. package/dist/plan-proposal.d.ts +115 -0
  36. package/dist/plan-proposal.js +1073 -0
  37. package/dist/runtime-executor.d.ts +6 -1
  38. package/dist/runtime-executor.js +72 -5
  39. package/dist/runtime-tool-specs.d.ts +19 -1
  40. package/dist/runtime-tool-specs.js +67 -26
  41. package/dist/schemas.js +29 -1
  42. package/dist/server.js +51 -0
  43. package/dist/shared.d.ts +1 -0
  44. package/dist/shared.js +2 -0
  45. package/dist/store/bootstrap-store.d.ts +1 -0
  46. package/dist/store/bootstrap-store.js +8 -2
  47. package/dist/store/repositories/local-model-runtime-repository.d.ts +1 -1
  48. package/dist/store/repositories/local-model-runtime-repository.js +1 -1
  49. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  50. package/dist/tools-agent.d.ts +20 -0
  51. package/dist/tools-agent.js +538 -28
  52. package/dist/tools-discovery.js +135 -0
  53. package/dist/tools-files.js +768 -66
  54. package/dist/tools-framework.js +80 -61
  55. package/dist/tui/index.js +10 -1
  56. package/dist/tui/ollama.d.ts +8 -1
  57. package/dist/tui/ollama.js +53 -12
  58. package/dist/tui/openai-compatible.d.ts +13 -0
  59. package/dist/tui/openai-compatible.js +305 -5
  60. package/dist/tui/provider-discovery.d.ts +1 -0
  61. package/dist/tui/provider-discovery.js +35 -11
  62. package/dist/vericify-bridge.d.ts +1 -1
  63. package/package.json +1 -1
@@ -11,12 +11,13 @@ import { getTrackerAdapter, listTrackerAdapterKinds, loadTrackerSnapshot, valida
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
15
  import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
16
16
  import { executeAceInternalTool } from "./ace-internal-tools.js";
17
17
  import { ModelBridge } from "./model-bridge.js";
18
18
  import { getVericifyContextPacket, getVericifyDelta } from "./vericify-context.js";
19
- 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";
20
21
  function parseOptionalJsonObject(raw) {
21
22
  if (!raw)
22
23
  return {};
@@ -77,6 +78,67 @@ function extractHandoffId(text) {
77
78
  const lineMatch = text.match(/handoff_id:\s*([A-Z0-9-]+)/i);
78
79
  return lineMatch?.[1];
79
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
+ }
80
142
  function stepLabel(index) {
81
143
  return `step-${index + 1}`;
82
144
  }
@@ -87,8 +149,18 @@ function isImplementationRole(role) {
87
149
  return role === "coders" || role === "builder";
88
150
  }
89
151
  async function buildOrchestratorSteps(task, sessionId) {
90
- void sessionId;
91
- 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);
92
164
  }
93
165
  function normalizeExplicitPlanSteps(steps, task) {
94
166
  const originalIdByLabel = new Map();
@@ -181,6 +253,7 @@ function normalizeExplicitPlanSteps(steps, task) {
181
253
  finalIdByInternal.set(step.id, stepLabel(index));
182
254
  });
183
255
  return {
256
+ planSource: "explicit_steps",
184
257
  steps: normalized.map((step) => ({
185
258
  role: step.role,
186
259
  task: step.task,
@@ -189,6 +262,13 @@ function normalizeExplicitPlanSteps(steps, task) {
189
262
  : undefined,
190
263
  parallel_group: step.parallel_group,
191
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,
192
272
  })),
193
273
  insertedResearch,
194
274
  shipFanoutEnabled,
@@ -196,32 +276,49 @@ function normalizeExplicitPlanSteps(steps, task) {
196
276
  }
197
277
  async function normalizeOrchestratorPlanSteps(task, steps, sessionId) {
198
278
  if (!Array.isArray(steps) || steps.length === 0) {
199
- return {
200
- planSource: "orchestrator_default_step",
201
- normalization: {
202
- steps: await buildOrchestratorSteps(task, sessionId),
203
- insertedResearch: false,
204
- shipFanoutEnabled: false,
205
- },
206
- };
279
+ const normalization = await buildOrchestratorSteps(task, sessionId);
280
+ return { planSource: normalization.planSource, normalization };
207
281
  }
208
- return {
209
- planSource: "explicit_steps",
210
- normalization: normalizeExplicitPlanSteps(steps, task),
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",
211
304
  };
305
+ const verdict = await validatePlanImpl({ proposal: syntheticProposal, sessionId });
306
+ normalization.validationVerdict = stripPlanIdFromVerdict(verdict);
307
+ return { planSource: "explicit_steps", normalization };
212
308
  }
213
309
  async function persistAcceptanceTraceMap(input) {
214
- return safeWriteAsync("agent-state/ACCEPTANCE_TRACE_MAP.json", JSON.stringify({
215
- version: 1,
216
- generated_at: new Date().toISOString(),
310
+ return persistAcceptanceTraceMapWithContract({
217
311
  plan_id: input.plan.plan_id,
218
312
  task: input.task,
219
313
  plan_source: input.planSource,
314
+ intent_summary: input.normalization?.intentSummary,
315
+ success_criteria: input.normalization?.successCriteria,
316
+ validation_verdict: input.normalization?.validationVerdict,
220
317
  policies: {
221
318
  inserted_research_before_spec: input.insertedResearch,
222
319
  ship_fanout_enabled: input.shipFanoutEnabled,
223
320
  },
224
- steps: input.plan.steps.map((step) => ({
321
+ steps: input.plan.steps.map((step, idx) => ({
225
322
  step_id: step.step_id,
226
323
  role: step.role,
227
324
  task: step.task,
@@ -232,8 +329,17 @@ async function persistAcceptanceTraceMap(input) {
232
329
  : step.role === "spec"
233
330
  ? "research"
234
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,
235
341
  })),
236
- }, null, 2));
342
+ });
237
343
  }
238
344
  function appendUniqueNote(target, note) {
239
345
  if (!target.includes(note)) {
@@ -276,6 +382,265 @@ function buildDefaultOrchestratorAmendment(input) {
276
382
  add_after_step_id: input.step.step_id,
277
383
  };
278
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
+ }
279
644
  async function tryVericifyPacket(factory, onWarning) {
280
645
  try {
281
646
  return await factory();
@@ -771,6 +1136,56 @@ export function registerAgentTools(server) {
771
1136
  ],
772
1137
  };
773
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
+ });
774
1189
  server.tool("run_local_model", "Offload a governed ACE subtask to the provider-backed ACE bridge and return the result", {
775
1190
  task: z.string().describe("Task to execute with the ACE model bridge"),
776
1191
  role: ROLE_ENUM.optional().describe("Optional ACE role; defaults to orchestrator"),
@@ -792,6 +1207,10 @@ export function registerAgentTools(server) {
792
1207
  .string()
793
1208
  .optional()
794
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"),
795
1214
  base_url: z
796
1215
  .string()
797
1216
  .optional()
@@ -808,13 +1227,14 @@ export function registerAgentTools(server) {
808
1227
  .string()
809
1228
  .optional()
810
1229
  .describe("Optional workspace root override; defaults to the active workspace"),
811
- }, 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, }) => {
812
1231
  const delegated = await runLocalModelTask({
813
1232
  task,
814
1233
  role,
815
1234
  workspaceRoot: workspace_root,
816
1235
  provider,
817
1236
  model,
1237
+ modelClass: model_class,
818
1238
  baseUrl: base_url,
819
1239
  ollamaUrl: ollama_url,
820
1240
  maxTurns: max_turns,
@@ -830,6 +1250,9 @@ export function registerAgentTools(server) {
830
1250
  `- role: ${delegated.role}`,
831
1251
  `- provider: ${delegated.runtime.provider}`,
832
1252
  `- model: ${delegated.runtime.model}`,
1253
+ `- model_class: ${delegated.policy.model_class}`,
1254
+ `- tier: ${delegated.policy.tier}`,
1255
+ `- mutation_lane: ${delegated.policy.mutation_lane}`,
833
1256
  `- workspace: ${delegated.runtime.workspaceRoot}`,
834
1257
  `- status: ${delegated.result.status}`,
835
1258
  `- turns: ${delegated.result.turns}`,
@@ -860,7 +1283,7 @@ export function registerAgentTools(server) {
860
1283
  ],
861
1284
  };
862
1285
  });
863
- 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", {
864
1287
  task: z.string().describe("The task to decompose and execute"),
865
1288
  steps: z
866
1289
  .array(z.object({
@@ -878,9 +1301,44 @@ export function registerAgentTools(server) {
878
1301
  .array(z.string())
879
1302
  .optional()
880
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"),
881
1339
  }))
882
1340
  .optional()
883
- .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"),
884
1342
  execution_mode: z
885
1343
  .enum(["sequential", "scheduled"])
886
1344
  .optional()
@@ -936,13 +1394,15 @@ export function registerAgentTools(server) {
936
1394
  steps: normalization.steps,
937
1395
  execution_mode: execution_mode ?? "sequential",
938
1396
  });
939
- const traceArtifactPath = await persistAcceptanceTraceMap({
1397
+ let traceArtifactPath = await persistAcceptanceTraceMap({
940
1398
  plan,
941
1399
  task,
942
1400
  planSource,
943
1401
  insertedResearch: normalization.insertedResearch,
944
1402
  shipFanoutEnabled: normalization.shipFanoutEnabled,
1403
+ normalization,
945
1404
  });
1405
+ const intentContract = loadAcceptanceTraceContractImpl(plan.plan_id);
946
1406
  const bridge = runtime
947
1407
  ? new ModelBridge(createDefaultModelBridgeClients(runtime))
948
1408
  : undefined;
@@ -971,9 +1431,10 @@ export function registerAgentTools(server) {
971
1431
  async spawnStep(step) {
972
1432
  if (bridge && runtime) {
973
1433
  return bridge.spawn({
974
- task: step.task,
1434
+ task: formatStepTaskForBridge(step),
975
1435
  role: step.role,
976
1436
  workspace: runtime.workspaceRoot,
1437
+ tier: resolveTier(undefined, runtime.provider, runtime.model, step.role),
977
1438
  maxTurns: max_turns_per_step ?? 6,
978
1439
  provider: runtime.provider,
979
1440
  model: runtime.model,
@@ -1018,11 +1479,23 @@ export function registerAgentTools(server) {
1018
1479
  : step.status === "blocked"
1019
1480
  ? "step_blocked"
1020
1481
  : "step_failed");
1021
- return buildDefaultOrchestratorAmendment({
1482
+ const amendment = buildDefaultOrchestratorAmendment({
1022
1483
  plan: activePlan,
1023
1484
  step,
1024
1485
  result,
1025
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;
1026
1499
  },
1027
1500
  async getVericifyContext() {
1028
1501
  return tryVericifyPacket(() => getVericifyContextPacket({
@@ -1035,6 +1508,34 @@ export function registerAgentTools(server) {
1035
1508
  workspaceRoot: effectiveWorkspaceRoot,
1036
1509
  }), (message) => appendUniqueNote(vericifyWarnings, `Vericify delta unavailable for ${plan.plan_id}: ${message}`));
1037
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
+ },
1038
1539
  async openCircuitBreaker(reason) {
1039
1540
  await executeAceInternalTool("open_circuit_breaker", {
1040
1541
  reason,
@@ -1076,6 +1577,14 @@ export function registerAgentTools(server) {
1076
1577
  }, sessionId);
1077
1578
  },
1078
1579
  });
1580
+ traceArtifactPath = await persistAcceptanceTraceMap({
1581
+ plan: supervised.plan,
1582
+ task,
1583
+ planSource,
1584
+ insertedResearch: normalization.insertedResearch,
1585
+ shipFanoutEnabled: normalization.shipFanoutEnabled,
1586
+ normalization,
1587
+ });
1079
1588
  const step_summaries = supervised.plan.steps.map((step) => ({
1080
1589
  step_id: step.step_id,
1081
1590
  role: step.role,
@@ -1096,8 +1605,8 @@ export function registerAgentTools(server) {
1096
1605
  runtime_warnings: runtimeWarnings,
1097
1606
  workspace_root: effectiveWorkspaceRoot,
1098
1607
  plan_source: planSource,
1099
- planning_note: planSource === "orchestrator_default_step"
1100
- ? "Auto-planning currently starts with ACE-Orchestrator. Pass explicit steps for multi-step orchestration."
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."
1101
1610
  : normalization.insertedResearch
1102
1611
  ? "Research was inserted ahead of spec work to require source-backed evidence before specification."
1103
1612
  : normalization.shipFanoutEnabled
@@ -1114,6 +1623,7 @@ export function registerAgentTools(server) {
1114
1623
  job_ids: supervised.job_ids,
1115
1624
  circuit_opened: supervised.circuit_opened,
1116
1625
  final_gate: supervised.final_gate ?? null,
1626
+ plan_validation_verdict: normalization.validationVerdict ?? null,
1117
1627
  vericify_warnings: vericifyWarnings,
1118
1628
  }, null, 2),
1119
1629
  },