@voybio/ace-swarm 2.4.2 → 2.4.4

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 (32) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  3. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  4. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  7. package/assets/agent-state/MODULES/registry.json +1 -4
  8. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  9. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  10. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  11. package/assets/agent-state/QUALITY_GATES.md +25 -9
  12. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  13. package/dist/astgrep-index.d.ts +7 -1
  14. package/dist/astgrep-index.js +66 -39
  15. package/dist/cli.js +1 -1
  16. package/dist/gate-contract.d.ts +63 -0
  17. package/dist/gate-contract.js +178 -0
  18. package/dist/helpers/bootstrap.js +4 -4
  19. package/dist/runtime-executor.js +45 -2
  20. package/dist/runtime-tool-specs.d.ts +8 -1
  21. package/dist/runtime-tool-specs.js +19 -1
  22. package/dist/schemas.js +16 -15
  23. package/dist/store/bootstrap-store.js +30 -6
  24. package/dist/store/gate-contract-migration.d.ts +10 -0
  25. package/dist/store/gate-contract-migration.js +413 -0
  26. package/dist/store/materializers/host-file-materializer.js +9 -1
  27. package/dist/tools-files.js +68 -5
  28. package/dist/tools-framework.js +115 -37
  29. package/package.json +3 -1
  30. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  31. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  32. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
@@ -0,0 +1,413 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export const PACKAGE_GATE_CONTRACT_VERSION = 2;
4
+ export const PACKAGE_GATE_CONTRACT_VERSION_KEY = "meta/package_gate_contract_version";
5
+ const LEGACY_GATE_MIGRATIONS = [
6
+ {
7
+ fileName: "gate-autonomy.json",
8
+ legacyContent: JSON.stringify({
9
+ id: "gate-autonomy",
10
+ type: "artifact_scan",
11
+ invariant: "Every execution cycle records objective, strategy, evidence, and next state",
12
+ command: "",
13
+ evidence_requirement: "STATUS + EVIDENCE_LOG + HANDOFF pointers",
14
+ }, null, 2),
15
+ currentAssetRelativePath: "agent-state/MODULES/gates/gate-autonomy.json",
16
+ },
17
+ {
18
+ fileName: "gate-completeness.json",
19
+ legacyContent: JSON.stringify({
20
+ id: "gate-completeness",
21
+ type: "artifact_scan",
22
+ invariant: "All required artifacts exist and are non-empty",
23
+ command: "",
24
+ evidence_requirement: "File presence and section completeness evidence",
25
+ }, null, 2),
26
+ currentAssetRelativePath: "agent-state/MODULES/gates/gate-completeness.json",
27
+ },
28
+ {
29
+ fileName: "gate-operability.json",
30
+ legacyContent: JSON.stringify({
31
+ id: "gate-operability",
32
+ type: "artifact_scan",
33
+ invariant: "Runbooks, ownership, and incident routing are defined for release",
34
+ command: "",
35
+ evidence_requirement: "COMMUNICATION_CHANNELS.md + observability/release artifacts",
36
+ }, null, 2),
37
+ currentAssetRelativePath: "agent-state/MODULES/gates/gate-operability.json",
38
+ },
39
+ {
40
+ fileName: "gate-security.json",
41
+ legacyContent: JSON.stringify({
42
+ id: "gate-security",
43
+ type: "manual_review",
44
+ invariant: "No unmitigated high-severity security or policy risks",
45
+ command: "",
46
+ evidence_requirement: "SECURITY_REPORT.md with explicit mitigations or accepted residual risk",
47
+ }, null, 2),
48
+ currentAssetRelativePath: "agent-state/MODULES/gates/gate-security.json",
49
+ },
50
+ {
51
+ fileName: "gate-correctness.json",
52
+ legacyContent: JSON.stringify({
53
+ id: "gate-correctness",
54
+ type: "executable",
55
+ invariant: "All required tests pass",
56
+ command: "if [ -f ace-mcp-server/package.json ]; then cd ace-mcp-server && npm test --silent; elif [ -f package.json ]; then npm test --silent; else echo 'package.json not found for gate-correctness' && exit 1; fi",
57
+ evidence_requirement: "Command output snippet with exit code 0",
58
+ }, null, 2),
59
+ remove: true,
60
+ },
61
+ {
62
+ fileName: "gate-evaluation.json",
63
+ legacyContent: JSON.stringify({
64
+ id: "gate-evaluation",
65
+ type: "executable",
66
+ invariant: "Core autonomy benchmark suites meet defined pass thresholds",
67
+ command: "bash scripts/ace/eval-harness.sh",
68
+ evidence_requirement: "EVAL_REPORT.md + command output",
69
+ }, null, 2),
70
+ remove: true,
71
+ },
72
+ {
73
+ fileName: "gate-typescript-public-surface.json",
74
+ legacyContent: JSON.stringify({
75
+ id: "gate-typescript-public-surface",
76
+ type: "executable",
77
+ invariant: "Exported MCP tools, resources, prompts, and public status events stay registered, described, schema-backed where required, and covered by the audit gate",
78
+ command: "node --input-type=module -e \"import('./dist/public-surface.js').then(async (m) => { const result = await m.auditPublicSurface({ write_artifact: false }); if (!result.ok) { console.error(result.failures.join('\\n')); process.exit(1); } console.log(JSON.stringify(result.summary)); })\"",
79
+ evidence_requirement: "PUBLIC_SURFACE_REPORT.md + passing public-surface audit",
80
+ }, null, 2),
81
+ remove: true,
82
+ },
83
+ ];
84
+ const LEGACY_STATIC_ASSET_MIGRATIONS = [
85
+ {
86
+ key: "knowledge/agent-state/TEAL_CONFIG.md",
87
+ legacyContent: `# TEAL_CONFIG.md
88
+
89
+ \`\`\`yaml
90
+ teal_version: "1.1"
91
+ timestamp: "2026-03-03T20:45:00Z"
92
+
93
+ modules:
94
+ skeptic:
95
+ version: "1.0.0"
96
+ interface: "QUALITY_GATES.md"
97
+ sidecar: true
98
+
99
+ ops:
100
+ version: "1.0.0"
101
+ outputs: ["STATUS.md", "HANDOFF.json"]
102
+
103
+ astgrep:
104
+ version: "1.0.0"
105
+ inputs: ["TASK.md", "SCOPE.md", "AST_GREP_COMMANDS.md"]
106
+ outputs: ["AST_GREP_INDEX.md", "AST_GREP_INDEX.json"]
107
+
108
+ research:
109
+ version: "1.0.0"
110
+ inputs: ["TASK.md"]
111
+ outputs: ["EVIDENCE_LOG.md"]
112
+
113
+ spec:
114
+ version: "1.0.0"
115
+ depends_on: ["research", "skeptic"]
116
+ outputs: ["SPEC_CONTRACT.json", "INTERFACE_REGISTRY.md"]
117
+
118
+ build:
119
+ version: "1.0.0"
120
+ depends_on: ["spec"]
121
+ parallelizable: true
122
+
123
+ qa:
124
+ version: "1.0.0"
125
+ depends_on: ["build"]
126
+ gates: ["gate-completeness", "gate-correctness", "gate-autonomy"]
127
+
128
+ docs:
129
+ version: "1.0.0"
130
+ depends_on: ["build", "qa"]
131
+
132
+ memory:
133
+ version: "1.0.0"
134
+ sidecar: true
135
+ depends_on: ["ops"]
136
+ outputs: ["MEMORY_INDEX.md"]
137
+
138
+ security:
139
+ version: "1.0.0"
140
+ sidecar: true
141
+ depends_on: ["skeptic", "spec", "build"]
142
+ gates: ["gate-security"]
143
+
144
+ observability:
145
+ version: "1.0.0"
146
+ sidecar: true
147
+ depends_on: ["ops", "qa"]
148
+ outputs: ["OBSERVABILITY_REPORT.md"]
149
+
150
+ eval:
151
+ version: "1.0.0"
152
+ depends_on: ["qa"]
153
+ gates: ["gate-evaluation"]
154
+
155
+ release:
156
+ version: "1.0.0"
157
+ depends_on: ["security", "observability", "eval"]
158
+ gates: ["gate-operability"]
159
+
160
+ pipelines:
161
+ standard:
162
+ - astgrep -> research -> spec -> build -> qa -> docs
163
+ - skeptic (sidecar)
164
+
165
+ high_certainty:
166
+ - astgrep -> spec -> build -> qa
167
+ - security (sidecar)
168
+ - eval -> release
169
+
170
+ high_ambiguity:
171
+ - astgrep -> research -> spec -> build
172
+ - skeptic (continuous)
173
+ - memory (continuous)
174
+ - ops (orchestration)
175
+
176
+ production_promotion:
177
+ - qa -> security -> eval -> observability -> release
178
+
179
+ gates:
180
+ gate-completeness:
181
+ module: qa
182
+ inputs: ["SPEC_CONTRACT.json", "artifact_set"]
183
+
184
+ gate-correctness:
185
+ module: qa
186
+ inputs: ["test_results", "EVIDENCE_LOG.md"]
187
+
188
+ gate-autonomy:
189
+ module: skeptic
190
+ inputs: ["STATUS.md", "HANDOFF.json", "EVIDENCE_LOG.md"]
191
+
192
+ gate-security:
193
+ module: security
194
+ inputs: ["RISKS.md", "SECURITY_REPORT.md"]
195
+
196
+ gate-evaluation:
197
+ module: eval
198
+ inputs: ["EVAL_REPORT.md"]
199
+
200
+ gate-operability:
201
+ module: release
202
+ inputs: ["OBSERVABILITY_REPORT.md", "RELEASE_DECISION.md"]
203
+ \`\`\`
204
+ `,
205
+ currentAssetRelativePath: "agent-state/TEAL_CONFIG.md",
206
+ },
207
+ {
208
+ key: "knowledge/agent-state/MODULES/registry.json",
209
+ legacyContent: JSON.stringify({
210
+ roles: [
211
+ "capability-astgrep",
212
+ "capability-skeptic",
213
+ "capability-ops",
214
+ "capability-research",
215
+ "capability-spec",
216
+ "capability-build",
217
+ "capability-qa",
218
+ "capability-docs",
219
+ "capability-memory",
220
+ "capability-security",
221
+ "capability-observability",
222
+ "capability-eval",
223
+ "capability-release",
224
+ "capability-safety",
225
+ "capability-framework",
226
+ "capability-git",
227
+ ],
228
+ gates: [
229
+ "gate-completeness",
230
+ "gate-correctness",
231
+ "gate-autonomy",
232
+ "gate-security",
233
+ "gate-operability",
234
+ "gate-evaluation",
235
+ "gate-typescript-public-surface",
236
+ ],
237
+ schemas: [
238
+ "STATUS_EVENT.schema.json",
239
+ "HANDOFF.schema.json",
240
+ "ARTIFACT_MANIFEST.schema.json",
241
+ "ACE_RUNTIME_PROFILE.schema.json",
242
+ "WORKSPACE_SESSION_REGISTRY.schema.json",
243
+ "RUNTIME_TOOL_SPEC_REGISTRY.schema.json",
244
+ "RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json",
245
+ "TRACKER_SNAPSHOT.schema.json",
246
+ "VERICIFY_BRIDGE_SNAPSHOT.schema.json",
247
+ "VERICIFY_PROCESS_POST_LOG.schema.json",
248
+ ],
249
+ }, null, 2),
250
+ currentAssetRelativePath: "agent-state/MODULES/registry.json",
251
+ },
252
+ {
253
+ key: "knowledge/agent-state/QUALITY_GATES.md",
254
+ legacyContent: `# QUALITY GATES
255
+
256
+ ## gate-completeness
257
+
258
+ - Invariant: required artifacts exist and are non-empty.
259
+ - Pass condition: all required files are present with required sections.
260
+ - Fail condition: any missing required artifact or empty required section.
261
+ - Evidence: file list + section checks in \`EVIDENCE_LOG.md\`.
262
+
263
+ ## gate-correctness
264
+
265
+ - Invariant: behavior satisfies spec acceptance criteria.
266
+ - Pass condition: mapped tests pass and no unresolved critical failures.
267
+ - Fail condition: failing mapped test, unresolved regression, or invalid provenance.
268
+ - Evidence: test output and verification report pointers.
269
+ `,
270
+ currentAssetRelativePath: "agent-state/QUALITY_GATES.md",
271
+ },
272
+ {
273
+ key: "knowledge/agent-state/INTERFACE_REGISTRY.md",
274
+ legacyContent: `# INTERFACE_REGISTRY
275
+
276
+ ## Runtime Executor Session Contract
277
+
278
+ - \`runtime-executor-sessions.json\` is the authoritative session registry for unattended runtime executor workspaces.
279
+ - It must validate against \`MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json\`.
280
+ - Every session entry must expose \`session_id\`, \`workspace_path\`, \`root_path\`, \`status\`, \`source\`, timestamps, optional hook details, and any \`metadata\` required for routing/debugging.
281
+
282
+ ## Runtime Tool Spec Contract
283
+
284
+ - \`runtime-tool-specs.json\` is the authoritative registry for external runtime tool declarations.
285
+ - It must validate against \`MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json\`.
286
+ - Each tool entry must include deterministic command metadata and explicitly declare environment inputs under \`env_keys\`.
287
+
288
+ ## Workspace Session Registry Contract
289
+
290
+ - \`runtime-workspaces.json\` is the authoritative registry for active ACE workspace sessions.
291
+ - It must validate against \`MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json\`.
292
+ - Entries may optionally include hook health summaries and per-hook status under \`hooks\`.
293
+
294
+ ## Tracker Snapshot Contract
295
+
296
+ - \`tracker-snapshot.json\` is the normalized cache of task/comment data mirrored from provider trackers.
297
+ - It must validate against \`MODULES/schemas/TRACKER_SNAPSHOT.schema.json\`.
298
+ - Scheduler and future executor consumers must use normalized item/comment fields only; provider-specific detail belongs under \`metadata\`.
299
+
300
+ ## Vericify Sidecar Contract
301
+
302
+ - \`vericify/ace-bridge.json\` is an optional sidecar bridge snapshot for Vericify-style read-model consumers.
303
+ - It must validate against \`MODULES/schemas/VERICIFY_BRIDGE_SNAPSHOT.schema.json\`.
304
+ - \`vericify/process-posts.json\` is an optional structured process-post log and must validate against \`MODULES/schemas/VERICIFY_PROCESS_POST_LOG.schema.json\`.
305
+ - Vericify remains optional; ACE must not require the Vericify package to read or write these artifacts.
306
+
307
+ ## Public Surface Gate
308
+
309
+ - \`audit_public_surface\` is the canonical TypeScript public-surface gate for MCP tools, resources, prompts, and registered event names.
310
+ - \`PUBLIC_SURFACE_REPORT.md\` is the durable audit artifact written by that gate.
311
+ - \`MODULES/gates/gate-typescript-public-surface.json\` is the executable gate manifest for CI or operator use.
312
+
313
+ ## Provenance Contract
314
+
315
+ - \`ARTIFACT_MANIFEST.json\` is mandatory and must be schema-valid.
316
+ - Each artifact entry must include \`artifact_id\`, \`artifact_path\`, \`producer_module\`, \`spec_version\`, \`req_ids\`, \`checksum\`, \`confidence_level\`, \`evidence_ref\`, and \`timestamp\`.
317
+ - \`PROVENANCE_LOG.md\` must mirror manifest entries with append-only evidence-linked records.
318
+
319
+ ## Versioning Rules
320
+
321
+ - additive changes: \`minor\`
322
+ - breaking changes: \`major\`
323
+ - bugfix/clarification: \`patch\`
324
+
325
+ ## Compatibility Requirement
326
+
327
+ Producer and consumer compatibility must be documented before release.
328
+ `,
329
+ currentAssetRelativePath: "agent-state/INTERFACE_REGISTRY.md",
330
+ },
331
+ ];
332
+ function normalizeForExactMatch(content) {
333
+ return content.replace(/\r\n/g, "\n").trimEnd();
334
+ }
335
+ function matchesLegacyPackageContent(content, legacyContent) {
336
+ return normalizeForExactMatch(content) === normalizeForExactMatch(legacyContent);
337
+ }
338
+ function matchesCurrentPackageContent(content, currentContent) {
339
+ return normalizeForExactMatch(content) === normalizeForExactMatch(currentContent);
340
+ }
341
+ function readAsset(assetsRoot, relativePath) {
342
+ return readFileSync(join(assetsRoot, relativePath), "utf-8");
343
+ }
344
+ export async function recordPackageGateContractVersion(store) {
345
+ await store.setJSON(PACKAGE_GATE_CONTRACT_VERSION_KEY, PACKAGE_GATE_CONTRACT_VERSION);
346
+ }
347
+ export async function migratePackageGateContract(store, workspaceRoot, assetsRoot) {
348
+ const messages = [];
349
+ let changed = false;
350
+ for (const entry of LEGACY_GATE_MIGRATIONS) {
351
+ const key = `knowledge/gates/${entry.fileName}`;
352
+ const currentContent = entry.currentAssetRelativePath
353
+ ? readAsset(assetsRoot, entry.currentAssetRelativePath)
354
+ : undefined;
355
+ const existing = await store.getBlob(key);
356
+ if (typeof existing === "string") {
357
+ if (matchesLegacyPackageContent(existing, entry.legacyContent)) {
358
+ if (entry.remove) {
359
+ await store.delete(key);
360
+ messages.push(`Removed legacy package gate ${key}.`);
361
+ }
362
+ else if (currentContent) {
363
+ await store.setBlob(key, currentContent);
364
+ messages.push(`Updated legacy package gate ${key}.`);
365
+ }
366
+ changed = true;
367
+ }
368
+ else if (!currentContent || !matchesCurrentPackageContent(existing, currentContent)) {
369
+ messages.push(`Skipped customized gate ${key}; content differs from known package seed.`);
370
+ }
371
+ }
372
+ const projectionPath = join(workspaceRoot, "agent-state", "MODULES", "gates", entry.fileName);
373
+ if (existsSync(projectionPath)) {
374
+ const projectionContent = readFileSync(projectionPath, "utf-8");
375
+ if (matchesLegacyPackageContent(projectionContent, entry.legacyContent)) {
376
+ if (entry.remove) {
377
+ rmSync(projectionPath, { force: true });
378
+ messages.push(`Removed legacy gate projection ${projectionPath}.`);
379
+ }
380
+ else if (currentContent) {
381
+ mkdirSync(join(workspaceRoot, "agent-state", "MODULES", "gates"), { recursive: true });
382
+ writeFileSync(projectionPath, currentContent, "utf-8");
383
+ messages.push(`Updated legacy gate projection ${projectionPath}.`);
384
+ }
385
+ changed = true;
386
+ }
387
+ else if (!currentContent || !matchesCurrentPackageContent(projectionContent, currentContent)) {
388
+ messages.push(`Skipped customized gate projection ${projectionPath}; content differs from known package seed.`);
389
+ }
390
+ }
391
+ }
392
+ for (const entry of LEGACY_STATIC_ASSET_MIGRATIONS) {
393
+ const currentContent = readAsset(assetsRoot, entry.currentAssetRelativePath);
394
+ const existing = await store.getBlob(entry.key);
395
+ if (typeof existing !== "string")
396
+ continue;
397
+ if (matchesLegacyPackageContent(existing, entry.legacyContent)) {
398
+ await store.setBlob(entry.key, currentContent);
399
+ messages.push(`Updated legacy package asset ${entry.key}.`);
400
+ changed = true;
401
+ }
402
+ else if (!matchesCurrentPackageContent(existing, currentContent)) {
403
+ messages.push(`Skipped customized asset ${entry.key}; content differs from known package seed.`);
404
+ }
405
+ }
406
+ const recordedVersion = await store.getJSON(PACKAGE_GATE_CONTRACT_VERSION_KEY);
407
+ if (recordedVersion !== PACKAGE_GATE_CONTRACT_VERSION) {
408
+ await recordPackageGateContractVersion(store);
409
+ changed = true;
410
+ }
411
+ return { changed, messages };
412
+ }
413
+ //# sourceMappingURL=gate-contract-migration.js.map
@@ -199,7 +199,7 @@ export class HostFileMaterializer {
199
199
  codex: "codex.config.toml",
200
200
  vscode: "vscode.mcp.json",
201
201
  copilot: ".mcp.json",
202
- claude: "claude_desktop_config.json",
202
+ claude: "claude_code.mcp.json",
203
203
  cursor: "cursor.mcp.json",
204
204
  antigravity: "antigravity.mcp.json",
205
205
  };
@@ -258,6 +258,14 @@ export class HostFileMaterializer {
258
258
  async materializeMcpBundle() {
259
259
  await this.seedStorePayload();
260
260
  const paths = [];
261
+ // Project-root .mcp.json is the only location Claude Code CLI and GitHub
262
+ // Copilot CLI read for project-scoped MCP servers. Without this, the
263
+ // .mcp-config/ bundle alone is invisible to both — the server never loads.
264
+ paths.push(await this.materializeEntry({
265
+ absPath: this.root(".mcp.json"),
266
+ keyParts: [".mcp.json"],
267
+ fallbackContent: getMcpServerConfigSnippet("copilot"),
268
+ }));
261
269
  for (const entry of [...this.buildMcpBundleEntries(), ...this.buildOptionalHookBundleEntries()]) {
262
270
  paths.push(await this.materializeEntry(entry));
263
271
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { spawnSync } from "node:child_process";
6
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
7
7
  import { dirname, isAbsolute, relative, resolve } from "node:path";
8
8
  import { z } from "zod";
9
9
  import { ACE_TASKS_ROOT_REL, normalizePathForValidation, safeRead, safeWriteAsync, safeWrite, resolveWorkspaceWritePath, resolveStoreFallbackKeysForPath, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
@@ -14,7 +14,7 @@ import { shouldAutoRefreshKanbanForPath, refreshKanbanArtifacts, } from "./kanba
14
14
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
15
15
  import { appendStatusEventSafe } from "./status-events.js";
16
16
  import { safeEditFile, diffContents, applyPatch } from "./safe-edit.js";
17
- import { detectAstgrepCommand, locateAstgrepMatches, runAstgrepQuery, } from "./astgrep-index.js";
17
+ import { detectAstgrepCommand, flattenMetaVariables, locateAstgrepMatches, runAstgrepQuery, } from "./astgrep-index.js";
18
18
  import { readRuntimeProfile, resolveEffectiveSurgicalReadBudget, validateRuntimeProfileContent, } from "./runtime-profile.js";
19
19
  import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
20
20
  function reasonCodeFromError(error) {
@@ -206,8 +206,12 @@ async function compileStructuralEditPlan(input) {
206
206
  },
207
207
  validation_command: input.validationCommand,
208
208
  test_command: input.testCommand,
209
+ allow_multi_match: input.allowMultiMatch,
209
210
  });
210
211
  }
212
+ function renderStructuralRewrite(template, captures) {
213
+ return template.replace(/\$([A-Z][A-Z0-9_]*)/g, (full, name) => captures[name] ?? full);
214
+ }
211
215
  function previewDiffText(diffText) {
212
216
  const lines = diffText.trimEnd().split("\n");
213
217
  const limited = lines.slice(0, 80).join("\n");
@@ -378,6 +382,24 @@ async function buildStructuralPreview(plan) {
378
382
  }),
379
383
  };
380
384
  }
385
+ if (targetMatches.length > 1 && !plan.allow_multi_match) {
386
+ return {
387
+ artifact: await finalize({
388
+ ok: false,
389
+ reason_code: "ambiguous_multi_match_in_file",
390
+ error: `Plan ${plan.plan_id} resolves to ${targetMatches.length} matches in ${plan.target.file}. Pass allow_multi_match=true at compile time to opt in, or narrow the pattern.`,
391
+ target_file: plan.target.file,
392
+ expected_file_hash: plan.target.file_hash,
393
+ current_file_hash: currentFileHash,
394
+ matched_count: targetMatches.length,
395
+ affected_file_count: affectedFiles.length,
396
+ changed_ranges: [],
397
+ diff_summary: "",
398
+ diff_preview: "",
399
+ promotable: false,
400
+ }),
401
+ };
402
+ }
381
403
  const astgrepCmd = detectAstgrepCommand();
382
404
  if (!astgrepCmd) {
383
405
  return {
@@ -403,7 +425,7 @@ async function buildStructuralPreview(plan) {
403
425
  mkdirSync(dirname(stagedOriginal), { recursive: true });
404
426
  writeFileSync(stagedOriginal, currentContent, "utf-8");
405
427
  writeFileSync(stagedRewrite, currentContent, "utf-8");
406
- const rewriteResult = spawnSync(astgrepCmd, ["--pattern", plan.locator.pattern, "--rewrite", plan.rewrite.rewrite_template, "--lang", plan.locator.lang, stagedRewrite, "--update-all"], { encoding: "utf8", cwd: WORKSPACE_ROOT });
428
+ const rewriteResult = spawnSync(astgrepCmd, ["--pattern", plan.locator.pattern, "--rewrite", plan.rewrite.rewrite_template, "--lang", plan.locator.lang, "--json", stagedRewrite], { encoding: "utf8", cwd: WORKSPACE_ROOT });
407
429
  if (rewriteResult.status !== 0) {
408
430
  return {
409
431
  artifact: await finalize({
@@ -423,7 +445,46 @@ async function buildStructuralPreview(plan) {
423
445
  }),
424
446
  };
425
447
  }
426
- const rewrittenContent = readFileSync(stagedRewrite, "utf-8");
448
+ let rawMatches = [];
449
+ try {
450
+ rawMatches = JSON.parse(rewriteResult.stdout || "[]");
451
+ }
452
+ catch {
453
+ rawMatches = [];
454
+ }
455
+ const selectedBytes = plan.target.selected_range?.byteOffset;
456
+ const targeted = rawMatches.find((match) => selectedBytes
457
+ && match.range?.byteOffset?.start === selectedBytes.start
458
+ && match.range?.byteOffset?.end === selectedBytes.end);
459
+ const replacement = targeted && (typeof targeted.replacement === "string"
460
+ ? targeted.replacement
461
+ : renderStructuralRewrite(plan.rewrite.rewrite_template, flattenMetaVariables(targeted.metaVariables)));
462
+ if (!selectedBytes || !targeted || typeof replacement !== "string") {
463
+ return {
464
+ artifact: await finalize({
465
+ ok: false,
466
+ reason_code: "target_byte_range_drift",
467
+ error: `No match at selected byte range [${selectedBytes?.start},${selectedBytes?.end}] for plan ${plan.plan_id}`,
468
+ target_file: plan.target.file,
469
+ expected_file_hash: plan.target.file_hash,
470
+ current_file_hash: currentFileHash,
471
+ matched_count: rawMatches.length,
472
+ affected_file_count: affectedFiles.length,
473
+ changed_ranges: [],
474
+ diff_summary: "",
475
+ diff_preview: "",
476
+ promotable: false,
477
+ staging_path: stagingDir,
478
+ }),
479
+ };
480
+ }
481
+ const originalBuffer = Buffer.from(currentContent, "utf8");
482
+ const rewrittenContent = Buffer.concat([
483
+ originalBuffer.subarray(0, selectedBytes.start ?? 0),
484
+ Buffer.from(replacement, "utf8"),
485
+ originalBuffer.subarray(selectedBytes.end ?? 0),
486
+ ]).toString("utf8");
487
+ writeFileSync(stagedRewrite, rewrittenContent, "utf8");
427
488
  const rewriteHash = contentSha256(rewrittenContent);
428
489
  const diff = diffContents(currentContent, rewrittenContent);
429
490
  const diffProcess = spawnSync("diff", ["-u", "--label", `a/${plan.target.file}`, "--label", `b/${plan.target.file}`, stagedOriginal, stagedRewrite], { encoding: "utf8", cwd: WORKSPACE_ROOT });
@@ -1344,9 +1405,10 @@ export function registerFileTools(server) {
1344
1405
  max_results: z.number().int().min(1).max(200).optional().describe("Max locator matches to keep (default: 50)"),
1345
1406
  desired_change: z.string().describe("Plain-language edit intent; used as rewrite_template when no explicit template is provided"),
1346
1407
  rewrite_template: z.string().optional().describe("Explicit ast-grep rewrite template; preferred when provided"),
1408
+ allow_multi_match: z.boolean().optional().describe("Opt into selecting and rewriting only the chosen match when multiple matches exist in the target file"),
1347
1409
  validation_command: z.string().optional().describe("Shell command to validate before promotion"),
1348
1410
  test_command: z.string().optional().describe("Shell command to run before promotion"),
1349
- }, async ({ match_id, pattern, lang, scope, symbol_hint, max_results, desired_change, rewrite_template, validation_command, test_command, }) => {
1411
+ }, async ({ match_id, pattern, lang, scope, symbol_hint, max_results, desired_change, rewrite_template, allow_multi_match, validation_command, test_command, }) => {
1350
1412
  if (!match_id && (!pattern || !lang)) {
1351
1413
  return jsonResponse({
1352
1414
  ok: false,
@@ -1455,6 +1517,7 @@ export function registerFileTools(server) {
1455
1517
  selectedMatch,
1456
1518
  desiredChange: desired_change,
1457
1519
  rewriteTemplate: rewrite_template,
1520
+ allowMultiMatch: allow_multi_match,
1458
1521
  validationCommand: validation_command,
1459
1522
  testCommand: test_command,
1460
1523
  }).catch((error) => {