@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
@@ -1,9 +1,23 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdirSync, writeFileSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
2
+ import { existsSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
3
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
4
4
  import { executeAceInternalTool, listAceInternalToolCatalog, } from "./ace-internal-tools.js";
5
5
  import { appendVericifyProcessPostSafe, deriveWorkspaceVericifyRunRef, } from "./vericify-bridge.js";
6
6
  import { buildToolPlan, renderAceContext } from "./ace-context.js";
7
+ import { appendRunLedgerEntrySafe } from "./run-ledger.js";
8
+ import { appendStatusEventSafe } from "./status-events.js";
9
+ import { normalizeRelPath } from "./helpers.js";
10
+ import { sanitizeJsonLikeText } from "./json-sanitizer.js";
11
+ /**
12
+ * Roles that MUST produce a valid JSON envelope in every response.
13
+ * A parse_error from these roles is a contract failure, not a plain-text fallback.
14
+ * The bridge will inject a repair prompt (up to MAX_PARSE_REPAIR_ATTEMPTS) before
15
+ * marking the run as failed with status "role_contract_violation".
16
+ */
17
+ const ROLES_REQUIRING_JSON_ENVELOPE = new Set(["coders", "builder", "qa"]);
18
+ const VALID_ENVELOPE_STATUSES = new Set(["tool", "message", "complete", "need_input"]);
19
+ const MAX_PARSE_REPAIR_ATTEMPTS = 2;
20
+ const MAX_OUTPUT_DRIFT_REPAIRS = 1;
7
21
  function resolveProviderClient(input) {
8
22
  const display = input.trim() || "ollama";
9
23
  const normalized = display.toLowerCase();
@@ -11,8 +25,11 @@ function resolveProviderClient(input) {
11
25
  ? { display, client: "ollama" }
12
26
  : { display, client: "openai-compatible" };
13
27
  }
28
+ function sanitizeModelOutput(raw) {
29
+ return sanitizeJsonLikeText(raw).text;
30
+ }
14
31
  function extractJsonEnvelope(raw) {
15
- const trimmed = raw.trim();
32
+ const trimmed = sanitizeModelOutput(raw).trim();
16
33
  if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
17
34
  return trimmed;
18
35
  }
@@ -29,11 +46,11 @@ function extractJsonEnvelope(raw) {
29
46
  let escape = false;
30
47
  for (let index = firstBrace; index < trimmed.length; index += 1) {
31
48
  const ch = trimmed[index];
32
- if (escape) {
49
+ if (inString && escape) {
33
50
  escape = false;
34
51
  continue;
35
52
  }
36
- if (ch === "\\") {
53
+ if (inString && ch === "\\") {
37
54
  escape = true;
38
55
  continue;
39
56
  }
@@ -61,18 +78,60 @@ function parseEnvelope(raw) {
61
78
  if (!parsed || typeof parsed !== "object") {
62
79
  throw new Error("response is not an object");
63
80
  }
64
- if (!parsed.status) {
65
- throw new Error("missing status");
81
+ if (typeof parsed.status !== "string" || !VALID_ENVELOPE_STATUSES.has(parsed.status)) {
82
+ throw new Error("missing or invalid status");
83
+ }
84
+ if (parsed.tool_calls !== undefined && !Array.isArray(parsed.tool_calls)) {
85
+ throw new Error("tool_calls must be an array");
86
+ }
87
+ if (parsed.evidence_refs !== undefined) {
88
+ if (!Array.isArray(parsed.evidence_refs)) {
89
+ throw new Error("evidence_refs must be an array");
90
+ }
91
+ parsed.evidence_refs = parsed.evidence_refs
92
+ .filter((ref) => typeof ref === "string")
93
+ .map((ref) => ref.trim())
94
+ .filter(Boolean);
66
95
  }
67
96
  return parsed;
68
97
  }
69
98
  catch {
70
99
  return {
71
- status: "message",
72
- message: raw.trim(),
100
+ status: "parse_error",
101
+ message: summarizeSnippet(sanitizeModelOutput(raw).trim() || "[empty response]", 240),
73
102
  };
74
103
  }
75
104
  }
105
+ /**
106
+ * Validates the semantic shape of a completed bridge output against role-specific
107
+ * output contracts. Returns a violation description if drift is detected, or null
108
+ * if the output is clean.
109
+ *
110
+ * Thresholds are conservative to avoid false positives on legitimate inline snippets.
111
+ */
112
+ function checkOutputShapeDrift(role, text, _toolResults) {
113
+ if (role === "qa") {
114
+ // qa must return a short structured verdict, not a rewritten artifact.
115
+ // Large code fences in the output strongly suggest artifact rewriting.
116
+ const codeBlocks = [...text.matchAll(/```[\s\S]*?```/g)];
117
+ const totalCodeChars = codeBlocks.reduce((sum, match) => sum + match[0].length, 0);
118
+ if (totalCodeChars > 800) {
119
+ return ("output contains large code blocks — qa must return a short structured verdict, " +
120
+ "not a rewritten artifact. Include a one-paragraph verdict and failure classification, " +
121
+ "not the full file content.");
122
+ }
123
+ }
124
+ if (role === "vos" || role === "ui") {
125
+ // vos/ui primary output is prose; high HTML-tag density indicates a full document
126
+ // was produced instead of planning prose.
127
+ const htmlTagCount = (text.match(/<[a-z][^>]*>/gi) ?? []).length;
128
+ if (htmlTagCount > 8) {
129
+ return (`${role} output contains ${htmlTagCount} HTML opening tags — primary output must be ` +
130
+ `prose. HTML authoring is the coders role's responsibility. Restate as plain prose.`);
131
+ }
132
+ }
133
+ return null;
134
+ }
76
135
  function parseToolPlan(raw, catalog) {
77
136
  const candidate = extractJsonEnvelope(raw);
78
137
  const allowedTools = new Set(catalog.map((tool) => tool.name));
@@ -127,6 +186,9 @@ function summarizeSnippet(text, maxChars = 240) {
127
186
  return normalized;
128
187
  return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
129
188
  }
189
+ function readEnvelopeText(value) {
190
+ return typeof value === "string" ? value.trim() : "";
191
+ }
130
192
  function buildHistorySummary(messages, maxChars) {
131
193
  const lines = messages.slice(-8).map((message) => {
132
194
  const content = summarizeSnippet(messageText(message).replace(/^Conversation summary:\s*/i, ""), 220);
@@ -209,6 +271,105 @@ function truncateToolResult(result, workspace, maxChars = 3000) {
209
271
  function formatErrorMessage(error) {
210
272
  return error instanceof Error ? error.message : String(error);
211
273
  }
274
+ function normalizeEvidenceRefPath(ref) {
275
+ const pathPart = ref.split("#", 1)[0]?.trim() ?? "";
276
+ if (!pathPart || pathPart.includes("\0") || pathPart.startsWith("~"))
277
+ return undefined;
278
+ if (isAbsolute(pathPart))
279
+ return undefined;
280
+ const normalized = normalizeRelPath(pathPart);
281
+ if (!normalized || normalized === ".." || normalized.startsWith("../"))
282
+ return undefined;
283
+ return normalized;
284
+ }
285
+ function workspaceEvidenceExists(ref, workspace) {
286
+ const normalized = normalizeEvidenceRefPath(ref);
287
+ if (!normalized)
288
+ return false;
289
+ try {
290
+ const root = resolve(workspace);
291
+ const candidate = resolve(root, normalized);
292
+ // Prevent symlink escape: compare real (resolved) filesystem paths
293
+ const realRoot = realpathSync(root);
294
+ const realCandidate = realpathSync(candidate);
295
+ // Accept candidate only if its real path is equal to the root or nested under it
296
+ if (realCandidate === realRoot || realCandidate.startsWith(realRoot + sep)) {
297
+ return existsSync(realCandidate);
298
+ }
299
+ return false;
300
+ }
301
+ catch {
302
+ return false;
303
+ }
304
+ }
305
+ function mergeEvidenceRefs(...groups) {
306
+ const merged = groups
307
+ .flatMap((group) => group ?? [])
308
+ .map((ref) => ref.trim())
309
+ .filter(Boolean);
310
+ return merged.length > 0 ? [...new Set(merged)] : undefined;
311
+ }
312
+ async function appendFalseCompletionEvidence(input) {
313
+ await appendRunLedgerEntrySafe({
314
+ tool: "model-bridge",
315
+ category: "regression",
316
+ message: input.summary,
317
+ artifacts: input.evidence_refs ?? [],
318
+ metadata: {
319
+ reason_code: "false_completion_no_evidence",
320
+ role: input.role,
321
+ workspace: input.workspace,
322
+ evidence_refs: input.evidence_refs ?? [],
323
+ },
324
+ }).catch(() => undefined);
325
+ await appendStatusEventSafe({
326
+ source_module: "capability-qa",
327
+ event_type: "MODEL_BRIDGE_COMPLETION_BLOCKED",
328
+ status: "blocked",
329
+ summary: input.summary,
330
+ objective_id: "model-bridge-completion-verification",
331
+ payload: {
332
+ reason_code: "false_completion_no_evidence",
333
+ role: input.role,
334
+ workspace: input.workspace,
335
+ evidence_refs: input.evidence_refs ?? [],
336
+ },
337
+ }).catch(() => undefined);
338
+ }
339
+ async function verifyCompletionArtifacts(result, context) {
340
+ if (result.status !== "completed")
341
+ return result;
342
+ const expectedArtifacts = context.expectedArtifacts ?? [];
343
+ const mutationIntent = /\b(write|create|mutate|edit|persist|save|generate)\b/i.test(context.task);
344
+ const shouldVerify = ((context.role === "coders" || context.role === "builder") && mutationIntent) ||
345
+ expectedArtifacts.length > 0 ||
346
+ result.tool_calls.some((toolCall) => toolCall.tool === "write_workspace_file");
347
+ if (!shouldVerify)
348
+ return result;
349
+ const writeEvidenceOk = result.tool_calls.some((toolCall) => toolCall.tool === "write_workspace_file" && toolCall.ok) &&
350
+ context.touchedPaths.some((path) => workspaceEvidenceExists(path, context.workspace));
351
+ const evidenceRefsOk = (result.evidence_refs ?? []).length > 0 &&
352
+ (result.evidence_refs ?? []).some((ref) => workspaceEvidenceExists(ref, context.workspace));
353
+ const expectedArtifactsOk = expectedArtifacts.length > 0 &&
354
+ expectedArtifacts
355
+ .filter((artifact) => artifact.required !== false)
356
+ .every((artifact) => workspaceEvidenceExists(artifact.path, context.workspace));
357
+ if (writeEvidenceOk || evidenceRefsOk || expectedArtifactsOk)
358
+ return result;
359
+ const summary = "Model claimed completion but no persisted evidence or tool-calls found.";
360
+ await appendFalseCompletionEvidence({
361
+ role: context.role,
362
+ workspace: context.workspace,
363
+ summary,
364
+ evidence_refs: result.evidence_refs,
365
+ });
366
+ return {
367
+ ...result,
368
+ status: "blocked",
369
+ reason_code: "false_completion_no_evidence",
370
+ summary,
371
+ };
372
+ }
212
373
  function isRetryableProviderError(error) {
213
374
  const message = formatErrorMessage(error).toLowerCase();
214
375
  return !/(abort|aborted|cancelled|canceled|interrupted)/.test(message);
@@ -232,13 +393,21 @@ async function collectOllamaResponse(client, model, messages, options) {
232
393
  num_ctx: options?.num_ctx ?? 8192,
233
394
  },
234
395
  })) {
235
- combined += chunk.message?.content ?? "";
396
+ const text = chunk.message?.content ?? "";
397
+ combined += text;
398
+ if (text || chunk.done) {
399
+ options?.onProgress?.({
400
+ kind: "model_chunk",
401
+ at: Date.now(),
402
+ detail: { provider: "ollama", done: Boolean(chunk.done), bytes: Buffer.byteLength(text) },
403
+ });
404
+ }
236
405
  if (chunk.done)
237
406
  break;
238
407
  }
239
408
  return combined;
240
409
  }
241
- async function collectOpenAiCompatibleResponse(client, provider, model, messages) {
410
+ async function collectOpenAiCompatibleResponse(client, provider, model, messages, onProgress) {
242
411
  let combined = "";
243
412
  for await (const chunk of client.chat({
244
413
  provider,
@@ -246,8 +415,29 @@ async function collectOpenAiCompatibleResponse(client, provider, model, messages
246
415
  messages,
247
416
  temperature: 0.2,
248
417
  topP: 0.9,
418
+ onProviderEvent: (event) => {
419
+ onProgress?.({
420
+ kind: "thinking",
421
+ at: Date.now(),
422
+ detail: {
423
+ reason: "provider_adapter_event",
424
+ ...event,
425
+ },
426
+ });
427
+ },
249
428
  })) {
250
429
  combined += chunk.text;
430
+ if (chunk.text || chunk.done) {
431
+ onProgress?.({
432
+ kind: "model_chunk",
433
+ at: Date.now(),
434
+ detail: {
435
+ provider,
436
+ done: Boolean(chunk.done),
437
+ bytes: Buffer.byteLength(chunk.text),
438
+ },
439
+ });
440
+ }
251
441
  if (chunk.done)
252
442
  break;
253
443
  }
@@ -257,8 +447,9 @@ async function collectProviderResponse(clients, provider, options) {
257
447
  return provider.client === "ollama"
258
448
  ? collectOllamaResponse(clients.ollama, options.model, options.messages, {
259
449
  num_ctx: options.numCtx,
450
+ onProgress: options.onProgress,
260
451
  })
261
- : collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages);
452
+ : collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages, options.onProgress);
262
453
  }
263
454
  async function collectProviderResponseWithRetry(clients, provider, options, onThinking) {
264
455
  try {
@@ -269,6 +460,11 @@ async function collectProviderResponseWithRetry(clients, provider, options, onTh
269
460
  throw error;
270
461
  }
271
462
  onThinking?.(`Provider error, retrying once: ${formatErrorMessage(error)}`);
463
+ options.onProgress?.({
464
+ kind: "thinking",
465
+ at: Date.now(),
466
+ detail: { reason: "provider_retry" },
467
+ });
272
468
  return collectProviderResponse(clients, provider, options);
273
469
  }
274
470
  }
@@ -353,9 +549,12 @@ export class ModelBridge {
353
549
  const provider = resolveProviderClient(options.provider);
354
550
  const numCtx = options.numCtx ??
355
551
  (requestedTier === "brief" ? 4096 : requestedTier === "compressed" ? 8192 : 16384);
356
- const explicitToolScope = options.toolScope?.map((tool) => tool.trim()).filter((tool) => tool.length > 0) ?? [];
357
- const toolScopeLocked = explicitToolScope.length > 0;
358
- const selectedToolScope = explicitToolScope.length > 0
552
+ const toolScopeProvided = Array.isArray(options.toolScope);
553
+ const explicitToolScope = toolScopeProvided
554
+ ? options.toolScope.map((tool) => tool.trim()).filter((tool) => tool.length > 0)
555
+ : [];
556
+ const toolScopeLocked = toolScopeProvided;
557
+ const selectedToolScope = toolScopeProvided
359
558
  ? explicitToolScope
360
559
  : await this.selectToolScope({
361
560
  task: options.task,
@@ -396,6 +595,9 @@ export class ModelBridge {
396
595
  ];
397
596
  const toolResults = [];
398
597
  const childResults = [];
598
+ const touchedPaths = [];
599
+ const declaredEvidenceRefs = [];
600
+ const evidenceRefs = () => mergeEvidenceRefs(touchedPaths, declaredEvidenceRefs);
399
601
  const sessionId = this.bridgeId;
400
602
  const refs = deriveWorkspaceVericifyRunRef({
401
603
  session_id: this.bridgeId,
@@ -404,6 +606,13 @@ export class ModelBridge {
404
606
  const availableTools = new Set(listAceInternalToolCatalog().map((tool) => tool.name));
405
607
  const allowedTools = new Set(aceContext.tools.map((tool) => tool.name));
406
608
  this.currentRunChildResults = childResults;
609
+ const noteProgress = (kind, detail) => {
610
+ options.onProgress?.({ kind, at: Date.now(), detail });
611
+ };
612
+ // Tracks repair attempts for roles that require a JSON envelope (coders, builder, qa).
613
+ let parseRepairAttempts = 0;
614
+ // Tracks correction attempts for roles that produce semantically drifted output.
615
+ let outputDriftRepairs = 0;
407
616
  try {
408
617
  await appendVericifyProcessPostSafe({
409
618
  run_id: refs.run_id,
@@ -414,24 +623,117 @@ export class ModelBridge {
414
623
  summary: `Bridge started for ${role} via ${provider.display}: ${options.task}`,
415
624
  tool_refs: aceContext.tools.map((tool) => tool.name),
416
625
  });
626
+ noteProgress("process_post", { kind: "intent" });
417
627
  for (let turn = 1; turn <= options.maxTurns; turn += 1) {
418
628
  const compressed = compressConversationHistory(messages, numCtx);
419
629
  if (compressed.compressed) {
420
630
  messages.splice(0, messages.length, ...compressed.messages);
421
631
  options.onThinking?.(`Context window compressed to ~${compressed.promptTokens} tokens.`);
632
+ noteProgress("thinking", { reason: "context_compressed", turn });
422
633
  }
423
634
  this.activeProviderClient = provider.client;
424
635
  const rawResponse = await collectProviderResponseWithRetry(this.clients, provider, {
425
636
  model: options.model,
426
637
  messages,
427
638
  numCtx,
639
+ onProgress: options.onProgress,
428
640
  }, options.onThinking);
429
641
  this.activeProviderClient = null;
430
642
  const envelope = parseEnvelope(rawResponse);
643
+ if (envelope.status !== "parse_error") {
644
+ const refs = mergeEvidenceRefs(envelope.evidence_refs);
645
+ if (refs)
646
+ declaredEvidenceRefs.push(...refs);
647
+ }
431
648
  if (envelope.thinking) {
432
649
  options.onThinking?.(envelope.thinking);
650
+ noteProgress("thinking", { turn });
433
651
  }
434
652
  messages.push({ role: "assistant", content: rawResponse });
653
+ if (envelope.status === "parse_error") {
654
+ // Plain-text fallback: accepted only for roles that do NOT require a JSON envelope
655
+ // and only when they have no tool scope (i.e. they legitimately produce prose output).
656
+ if ((selectedToolScope ?? []).length === 0 && !ROLES_REQUIRING_JSON_ENVELOPE.has(role)) {
657
+ const summary = rawResponse.trim() || "Bridge completed.";
658
+ options.onOutput?.(summary);
659
+ noteProgress("output", { status: "complete", fallback: "plain_text" });
660
+ await appendVericifyProcessPostSafe({
661
+ run_id: refs.run_id,
662
+ branch_id: refs.branch_id,
663
+ lane_id: refs.lane_id,
664
+ agent_id: `agent-${role}`,
665
+ kind: "completion",
666
+ summary,
667
+ tool_refs: [],
668
+ });
669
+ noteProgress("process_post", { kind: "completion", fallback: "plain_text" });
670
+ return verifyCompletionArtifacts({
671
+ bridge_id: this.bridgeId,
672
+ role,
673
+ status: "completed",
674
+ summary,
675
+ turns: turn,
676
+ tool_calls: toolResults,
677
+ child_results: childResults,
678
+ evidence_refs: evidenceRefs(),
679
+ }, {
680
+ role,
681
+ task: options.task,
682
+ workspace: options.workspace,
683
+ touchedPaths,
684
+ expectedArtifacts: options.expectedArtifacts,
685
+ });
686
+ }
687
+ // Repair path: roles that require a JSON envelope get up to MAX_PARSE_REPAIR_ATTEMPTS
688
+ // chances to emit a valid response before the run is marked as failed.
689
+ if (ROLES_REQUIRING_JSON_ENVELOPE.has(role) && parseRepairAttempts < MAX_PARSE_REPAIR_ATTEMPTS) {
690
+ parseRepairAttempts += 1;
691
+ const repairPrompt = `Your previous response was not a valid JSON envelope and cannot be accepted. ` +
692
+ `As the ${role} role, every response MUST be a JSON object with a "status" key. ` +
693
+ `Valid response shapes:\n` +
694
+ ` {"status":"tool","tool_calls":[{"tool":"name","input":{}}]}\n` +
695
+ ` {"status":"complete","summary":"what you accomplished"}\n` +
696
+ `Do NOT output plain text, HTML, markdown, or code outside a JSON envelope. ` +
697
+ `Respond with valid JSON only. (repair attempt ${parseRepairAttempts}/${MAX_PARSE_REPAIR_ATTEMPTS})`;
698
+ messages.push({ role: "user", content: repairPrompt });
699
+ options.onThinking?.(`[drift-repair] ${role} parse_error — injecting repair prompt (attempt ${parseRepairAttempts}/${MAX_PARSE_REPAIR_ATTEMPTS})`);
700
+ noteProgress("thinking", {
701
+ reason: "role_contract_repair",
702
+ role,
703
+ attempt: parseRepairAttempts,
704
+ });
705
+ continue;
706
+ }
707
+ const isContractViolation = ROLES_REQUIRING_JSON_ENVELOPE.has(role);
708
+ const summary = isContractViolation
709
+ ? `[role_contract_violation] ${role} returned malformed JSON after ${parseRepairAttempts} repair attempt(s): ${envelope.message ?? "[empty response]"}`
710
+ : `Model bridge returned malformed or non-JSON output: ${envelope.message ?? "[empty response]"}`;
711
+ options.onOutput?.(summary);
712
+ noteProgress("output", {
713
+ status: "parse_error",
714
+ role_contract_violation: isContractViolation,
715
+ });
716
+ await appendVericifyProcessPostSafe({
717
+ run_id: refs.run_id,
718
+ branch_id: refs.branch_id,
719
+ lane_id: refs.lane_id,
720
+ agent_id: `agent-${role}`,
721
+ kind: "blocker",
722
+ summary,
723
+ tool_refs: toolResults.map((entry) => entry.tool),
724
+ });
725
+ noteProgress("process_post", { kind: "blocker" });
726
+ return {
727
+ bridge_id: this.bridgeId,
728
+ role,
729
+ status: "failed",
730
+ summary,
731
+ turns: turn,
732
+ tool_calls: toolResults,
733
+ child_results: childResults,
734
+ evidence_refs: evidenceRefs(),
735
+ };
736
+ }
435
737
  if (envelope.status === "tool" &&
436
738
  Array.isArray(envelope.tool_calls) &&
437
739
  envelope.tool_calls.length > 0) {
@@ -460,6 +762,11 @@ export class ModelBridge {
460
762
  : `Tool '${toolCall.tool}' is not available in the active ACE catalog.`,
461
763
  };
462
764
  options.onToolResult?.(toolCall.tool, result);
765
+ noteProgress("tool_finish", {
766
+ tool: toolCall.tool,
767
+ ok: false,
768
+ blocked: true,
769
+ });
463
770
  toolResults.push(result);
464
771
  return result;
465
772
  });
@@ -472,7 +779,10 @@ export class ModelBridge {
472
779
  const executed = await Promise.all(envelope.tool_calls.map(async (toolCall) => {
473
780
  const args = toolCall.input ?? {};
474
781
  options.onToolCall?.(toolCall.tool, args);
475
- const rawToolResult = await executeAceInternalTool(toolCall.tool, args, sessionId);
782
+ noteProgress("tool_start", { tool: toolCall.tool });
783
+ const rawToolResult = await executeAceInternalTool(toolCall.tool, args, sessionId, {
784
+ workspace_path: options.workspace,
785
+ });
476
786
  const result = truncateToolResult({
477
787
  tool: toolCall.tool,
478
788
  ok: !Boolean(rawToolResult?.isError),
@@ -480,7 +790,17 @@ export class ModelBridge {
480
790
  summary: summarizeToolText(rawToolResult),
481
791
  }, options.workspace);
482
792
  options.onToolResult?.(toolCall.tool, result);
793
+ noteProgress("tool_finish", { tool: toolCall.tool, ok: result.ok });
483
794
  toolResults.push(result);
795
+ if (result.ok) {
796
+ const pathArg = typeof args.path === "string"
797
+ ? args.path
798
+ : typeof args.file_path === "string"
799
+ ? args.file_path
800
+ : undefined;
801
+ if (pathArg)
802
+ touchedPaths.push(pathArg);
803
+ }
484
804
  return result;
485
805
  }));
486
806
  messages.push({
@@ -492,8 +812,9 @@ export class ModelBridge {
492
812
  continue;
493
813
  }
494
814
  if (envelope.status === "message") {
495
- const message = envelope.message?.trim() || rawResponse.trim();
815
+ const message = readEnvelopeText(envelope.message) || rawResponse.trim();
496
816
  options.onOutput?.(message);
817
+ noteProgress("output", { status: "message" });
497
818
  await appendVericifyProcessPostSafe({
498
819
  run_id: refs.run_id,
499
820
  branch_id: refs.branch_id,
@@ -503,7 +824,8 @@ export class ModelBridge {
503
824
  summary: message,
504
825
  tool_refs: [],
505
826
  });
506
- return {
827
+ noteProgress("process_post", { kind: "progress" });
828
+ return verifyCompletionArtifacts({
507
829
  bridge_id: this.bridgeId,
508
830
  role,
509
831
  status: "completed",
@@ -511,11 +833,19 @@ export class ModelBridge {
511
833
  turns: turn,
512
834
  tool_calls: toolResults,
513
835
  child_results: childResults,
514
- };
836
+ evidence_refs: evidenceRefs(),
837
+ }, {
838
+ role,
839
+ task: options.task,
840
+ workspace: options.workspace,
841
+ touchedPaths,
842
+ expectedArtifacts: options.expectedArtifacts,
843
+ });
515
844
  }
516
845
  if (envelope.status === "need_input") {
517
- const message = envelope.message?.trim() || "Additional operator input required.";
846
+ const message = readEnvelopeText(envelope.message) || "Additional operator input required.";
518
847
  options.onOutput?.(message);
848
+ noteProgress("output", { status: "need_input" });
519
849
  await appendVericifyProcessPostSafe({
520
850
  run_id: refs.run_id,
521
851
  branch_id: refs.branch_id,
@@ -525,6 +855,7 @@ export class ModelBridge {
525
855
  summary: message,
526
856
  tool_refs: [],
527
857
  });
858
+ noteProgress("process_post", { kind: "blocker" });
528
859
  return {
529
860
  bridge_id: this.bridgeId,
530
861
  role,
@@ -533,11 +864,57 @@ export class ModelBridge {
533
864
  turns: turn,
534
865
  tool_calls: toolResults,
535
866
  child_results: childResults,
867
+ evidence_refs: evidenceRefs(),
536
868
  };
537
869
  }
538
870
  if (envelope.status === "complete") {
539
- const summary = envelope.summary?.trim() || "Bridge completed.";
871
+ const summary = readEnvelopeText(envelope.summary) || "Bridge completed.";
872
+ // Output shape drift check: detect semantic violations before accepting the result.
873
+ const driftViolation = checkOutputShapeDrift(role, summary, toolResults);
874
+ if (driftViolation) {
875
+ if (outputDriftRepairs < MAX_OUTPUT_DRIFT_REPAIRS) {
876
+ outputDriftRepairs += 1;
877
+ const correctionPrompt = `Your previous completion violated the output contract for the ${role} role: ` +
878
+ `${driftViolation} Please restate your output, correcting the violation.`;
879
+ messages.push({ role: "user", content: correctionPrompt });
880
+ options.onThinking?.(`[drift-correction] ${role} output drift — injecting correction ` +
881
+ `(attempt ${outputDriftRepairs}/${MAX_OUTPUT_DRIFT_REPAIRS}): ${driftViolation}`);
882
+ noteProgress("thinking", {
883
+ reason: "output_drift_correction",
884
+ role,
885
+ attempt: outputDriftRepairs,
886
+ violation: driftViolation,
887
+ });
888
+ continue;
889
+ }
890
+ // Correction exhausted — reject the drifted output.
891
+ const driftSummary = `[output_drift_violation] ${role}: ${driftViolation}`;
892
+ options.onOutput?.(driftSummary);
893
+ noteProgress("output", { status: "output_drift_violation" });
894
+ await appendVericifyProcessPostSafe({
895
+ run_id: refs.run_id,
896
+ branch_id: refs.branch_id,
897
+ lane_id: refs.lane_id,
898
+ agent_id: `agent-${role}`,
899
+ kind: "blocker",
900
+ summary: driftSummary,
901
+ tool_refs: toolResults.map((entry) => entry.tool),
902
+ });
903
+ noteProgress("process_post", { kind: "blocker" });
904
+ return {
905
+ bridge_id: this.bridgeId,
906
+ role,
907
+ status: "failed",
908
+ summary: driftSummary,
909
+ turns: turn,
910
+ tool_calls: toolResults,
911
+ child_results: childResults,
912
+ evidence_refs: evidenceRefs(),
913
+ };
914
+ }
915
+ // Clean output — accept the completion.
540
916
  options.onOutput?.(summary);
917
+ noteProgress("output", { status: "complete" });
541
918
  await appendVericifyProcessPostSafe({
542
919
  run_id: refs.run_id,
543
920
  branch_id: refs.branch_id,
@@ -547,7 +924,8 @@ export class ModelBridge {
547
924
  summary,
548
925
  tool_refs: toolResults.map((entry) => entry.tool),
549
926
  });
550
- return {
927
+ noteProgress("process_post", { kind: "completion" });
928
+ return verifyCompletionArtifacts({
551
929
  bridge_id: this.bridgeId,
552
930
  role,
553
931
  status: "completed",
@@ -555,7 +933,14 @@ export class ModelBridge {
555
933
  turns: turn,
556
934
  tool_calls: toolResults,
557
935
  child_results: childResults,
558
- };
936
+ evidence_refs: evidenceRefs(),
937
+ }, {
938
+ role,
939
+ task: options.task,
940
+ workspace: options.workspace,
941
+ touchedPaths,
942
+ expectedArtifacts: options.expectedArtifacts,
943
+ });
559
944
  }
560
945
  }
561
946
  const summary = "Bridge stopped after reaching max turns.";
@@ -568,6 +953,7 @@ export class ModelBridge {
568
953
  summary,
569
954
  tool_refs: toolResults.map((entry) => entry.tool),
570
955
  });
956
+ noteProgress("process_post", { kind: "blocker" });
571
957
  return {
572
958
  bridge_id: this.bridgeId,
573
959
  role,
@@ -576,6 +962,7 @@ export class ModelBridge {
576
962
  turns: options.maxTurns,
577
963
  tool_calls: toolResults,
578
964
  child_results: childResults,
965
+ evidence_refs: evidenceRefs(),
579
966
  };
580
967
  }
581
968
  finally {