@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.
- package/CHANGELOG.md +19 -1
- package/README.md +21 -13
- package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
- package/assets/agent-state/EVIDENCE_LOG.md +1 -1
- package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
- package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
- package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
- package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
- package/assets/agent-state/STATUS.md +2 -2
- package/assets/agent-state/runtime-tool-specs.json +70 -2
- package/assets/instructions/ACE_Coder.instructions.md +13 -0
- package/assets/instructions/ACE_UI.instructions.md +11 -0
- package/assets/scripts/ace-hook-dispatch.mjs +70 -6
- package/assets/scripts/render-mcp-configs.sh +19 -5
- package/dist/ace-context.js +91 -11
- package/dist/ace-internal-tools.d.ts +3 -1
- package/dist/ace-internal-tools.js +10 -2
- package/dist/ace-server-instructions.js +3 -3
- package/dist/ace-state-resolver.js +5 -3
- package/dist/agent-runtime/role-adapters.d.ts +18 -1
- package/dist/agent-runtime/role-adapters.js +49 -5
- package/dist/astgrep-index.d.ts +57 -1
- package/dist/astgrep-index.js +140 -4
- package/dist/cli.js +232 -35
- package/dist/discovery-runtime-wrappers.d.ts +108 -0
- package/dist/discovery-runtime-wrappers.js +615 -0
- package/dist/handoff-registry.js +5 -5
- package/dist/helpers/artifacts.d.ts +19 -0
- package/dist/helpers/artifacts.js +152 -0
- package/dist/helpers/bootstrap.d.ts +24 -0
- package/dist/helpers/bootstrap.js +894 -0
- package/dist/helpers/constants.d.ts +53 -0
- package/dist/helpers/constants.js +295 -0
- package/dist/helpers/drift.d.ts +13 -0
- package/dist/helpers/drift.js +45 -0
- package/dist/helpers/path-utils.d.ts +24 -0
- package/dist/helpers/path-utils.js +123 -0
- package/dist/helpers/store-resolution.d.ts +19 -0
- package/dist/helpers/store-resolution.js +305 -0
- package/dist/helpers/workspace-root.d.ts +3 -0
- package/dist/helpers/workspace-root.js +80 -0
- package/dist/helpers.d.ts +8 -125
- package/dist/helpers.js +8 -1768
- package/dist/job-scheduler.js +33 -7
- package/dist/json-sanitizer.d.ts +16 -0
- package/dist/json-sanitizer.js +26 -0
- package/dist/local-model-policy.d.ts +27 -0
- package/dist/local-model-policy.js +84 -0
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +33 -21
- package/dist/model-bridge.d.ts +13 -1
- package/dist/model-bridge.js +410 -23
- package/dist/orchestrator-supervisor.d.ts +56 -0
- package/dist/orchestrator-supervisor.js +179 -1
- package/dist/plan-proposal.d.ts +115 -0
- package/dist/plan-proposal.js +1073 -0
- package/dist/run-ledger.js +3 -3
- package/dist/runtime-command.d.ts +8 -0
- package/dist/runtime-command.js +38 -6
- package/dist/runtime-executor.d.ts +20 -1
- package/dist/runtime-executor.js +737 -172
- package/dist/runtime-profile.d.ts +32 -0
- package/dist/runtime-profile.js +89 -13
- package/dist/runtime-tool-specs.d.ts +39 -0
- package/dist/runtime-tool-specs.js +144 -28
- package/dist/safe-edit.d.ts +7 -0
- package/dist/safe-edit.js +163 -37
- package/dist/schemas.js +48 -1
- package/dist/server.js +51 -0
- package/dist/shared.d.ts +3 -2
- package/dist/shared.js +2 -0
- package/dist/status-events.js +9 -6
- package/dist/store/ace-packed-store.d.ts +3 -2
- package/dist/store/ace-packed-store.js +188 -110
- package/dist/store/bootstrap-store.d.ts +2 -1
- package/dist/store/bootstrap-store.js +102 -83
- package/dist/store/cache-workspace.js +11 -5
- package/dist/store/materializers/context-snapshot-materializer.js +6 -2
- package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
- package/dist/store/materializers/hook-context-materializer.js +11 -21
- package/dist/store/materializers/host-file-materializer.js +6 -0
- package/dist/store/materializers/projection-manager.d.ts +0 -1
- package/dist/store/materializers/projection-manager.js +5 -13
- package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
- package/dist/store/materializers/vericify-projector.d.ts +7 -7
- package/dist/store/materializers/vericify-projector.js +11 -11
- package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
- package/dist/store/repositories/local-model-runtime-repository.js +242 -6
- package/dist/store/repositories/vericify-repository.d.ts +1 -1
- package/dist/store/skills-install.d.ts +4 -0
- package/dist/store/skills-install.js +21 -12
- package/dist/store/state-reader.d.ts +2 -0
- package/dist/store/state-reader.js +20 -0
- package/dist/store/store-artifacts.d.ts +7 -0
- package/dist/store/store-artifacts.js +27 -1
- package/dist/store/store-authority-audit.d.ts +18 -1
- package/dist/store/store-authority-audit.js +115 -5
- package/dist/store/store-snapshot.d.ts +3 -0
- package/dist/store/store-snapshot.js +22 -2
- package/dist/store/workspace-store-paths.d.ts +39 -0
- package/dist/store/workspace-store-paths.js +94 -0
- package/dist/store/write-coordinator.d.ts +65 -0
- package/dist/store/write-coordinator.js +386 -0
- package/dist/todo-state.js +5 -5
- package/dist/tools-agent.d.ts +20 -0
- package/dist/tools-agent.js +789 -25
- package/dist/tools-discovery.js +136 -1
- package/dist/tools-files.d.ts +7 -0
- package/dist/tools-files.js +1002 -11
- package/dist/tools-framework.js +105 -66
- package/dist/tools-handoff.js +2 -2
- package/dist/tools-lifecycle.js +4 -4
- package/dist/tools-memory.js +6 -6
- package/dist/tools-todo.js +2 -2
- package/dist/tracker-adapters.d.ts +1 -1
- package/dist/tracker-adapters.js +13 -18
- package/dist/tracker-sync.js +5 -3
- package/dist/tui/agent-runner.js +3 -1
- package/dist/tui/chat.js +103 -7
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.js +43 -0
- package/dist/tui/index.js +10 -1
- package/dist/tui/layout.d.ts +20 -0
- package/dist/tui/layout.js +31 -1
- package/dist/tui/local-model-contract.d.ts +6 -2
- package/dist/tui/local-model-contract.js +16 -3
- package/dist/tui/ollama.d.ts +8 -1
- package/dist/tui/ollama.js +53 -12
- package/dist/tui/openai-compatible.d.ts +13 -0
- package/dist/tui/openai-compatible.js +305 -5
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +35 -11
- package/dist/vericify-bridge.d.ts +6 -1
- package/dist/vericify-bridge.js +27 -3
- package/dist/workspace-manager.d.ts +30 -3
- package/dist/workspace-manager.js +257 -27
- package/package.json +1 -2
- package/dist/internal-tool-runtime.d.ts +0 -21
- package/dist/internal-tool-runtime.js +0 -136
- package/dist/store/workspace-snapshot.d.ts +0 -26
- package/dist/store/workspace-snapshot.js +0 -107
package/dist/tools-files.js
CHANGED
|
@@ -1,18 +1,466 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File operation tool registrations + new safe-edit and diff tools.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
6
8
|
import { z } from "zod";
|
|
7
|
-
import { ACE_TASKS_ROOT_REL, normalizePathForValidation, safeRead, safeWrite, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
|
|
8
|
-
import { looksLikeSwarmHandoffPath } from "./shared.js";
|
|
9
|
+
import { ACE_TASKS_ROOT_REL, normalizePathForValidation, safeRead, safeWriteAsync, safeWrite, resolveWorkspaceWritePath, resolveStoreFallbackKeysForPath, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
|
|
10
|
+
import { isInside, looksLikeSwarmHandoffPath, normalizeRelPath } from "./shared.js";
|
|
9
11
|
import { validateAgentStateHandoffPayload, validateArtifactManifestPayload, validateProvenanceLogContent, validateStatusEventsNdjsonContent, validateSwarmHandoffPayload, validateTealConfigContent, validateRuntimeExecutorSessionRegistryPayload, validateRuntimeToolSpecRegistryPayload, validateTrackerSnapshotPayload, validateVericifyBridgeSnapshotPayload, validateVericifyProcessPostLogPayload, validateWorkspaceSessionRegistryPayload, lintHandoffPayload, } from "./schemas.js";
|
|
10
12
|
import { syncTodoStateSafe } from "./todo-state.js";
|
|
11
|
-
import { validateRuntimeProfileContent } from "./runtime-profile.js";
|
|
12
13
|
import { shouldAutoRefreshKanbanForPath, refreshKanbanArtifacts, } from "./kanban.js";
|
|
13
14
|
import { appendRunLedgerEntrySafe } from "./run-ledger.js";
|
|
14
15
|
import { appendStatusEventSafe } from "./status-events.js";
|
|
15
|
-
import { safeEditFile, diffContents } from "./safe-edit.js";
|
|
16
|
+
import { safeEditFile, diffContents, applyPatch } from "./safe-edit.js";
|
|
17
|
+
import { detectAstgrepCommand, locateAstgrepMatches, runAstgrepQuery, } from "./astgrep-index.js";
|
|
18
|
+
import { readRuntimeProfile, resolveEffectiveSurgicalReadBudget, validateRuntimeProfileContent, } from "./runtime-profile.js";
|
|
19
|
+
import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
|
|
20
|
+
function reasonCodeFromError(error) {
|
|
21
|
+
return typeof error === "object" && error !== null && "reason_code" in error
|
|
22
|
+
? String(error.reason_code)
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
function errorMessage(error) {
|
|
26
|
+
return error instanceof Error ? error.message : String(error);
|
|
27
|
+
}
|
|
28
|
+
async function appendWriteWorkspaceRejection(input) {
|
|
29
|
+
await appendRunLedgerEntrySafe({
|
|
30
|
+
tool: "write_workspace_file",
|
|
31
|
+
category: "regression",
|
|
32
|
+
message: input.message,
|
|
33
|
+
artifacts: [],
|
|
34
|
+
metadata: {
|
|
35
|
+
reason_code: input.reason_code,
|
|
36
|
+
path: input.path,
|
|
37
|
+
workspace_path: input.workspace_path,
|
|
38
|
+
},
|
|
39
|
+
}).catch(() => undefined);
|
|
40
|
+
await appendStatusEventSafe({
|
|
41
|
+
source_module: "capability-safety",
|
|
42
|
+
event_type: "WORKSPACE_WRITE_REJECTED",
|
|
43
|
+
status: "blocked",
|
|
44
|
+
summary: input.message,
|
|
45
|
+
objective_id: "workspace-write-safety",
|
|
46
|
+
payload: {
|
|
47
|
+
reason_code: input.reason_code,
|
|
48
|
+
path: input.path,
|
|
49
|
+
workspace_path: input.workspace_path,
|
|
50
|
+
},
|
|
51
|
+
}).catch(() => undefined);
|
|
52
|
+
}
|
|
53
|
+
export function planAstgrepRewriteTargets(files) {
|
|
54
|
+
const affected = [...new Set(files.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
55
|
+
if (affected.length > 1) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
affected_files: affected,
|
|
59
|
+
error: `astgrep_rewrite refuses multi-file rewrites (${affected.length} files): ${affected.slice(0, 5).join(", ")}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
affected_files: affected,
|
|
65
|
+
target_file: affected[0],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function appendAstgrepRewriteTransition(sessionId, to, reason, evidenceRefs) {
|
|
69
|
+
if (!sessionId)
|
|
70
|
+
return;
|
|
71
|
+
await withLocalModelRuntimeRepository(WORKSPACE_ROOT, async (repo) => {
|
|
72
|
+
await repo.appendTransitionRecord({
|
|
73
|
+
subject_kind: "plan_step",
|
|
74
|
+
subject_id: `${sessionId}/astgrep_rewrite`,
|
|
75
|
+
from: "staged",
|
|
76
|
+
to,
|
|
77
|
+
reason,
|
|
78
|
+
reason_code: to === "promoted" ? "surgical_rewrite" : "surgical_rewrite_failed",
|
|
79
|
+
evidence_refs: evidenceRefs,
|
|
80
|
+
});
|
|
81
|
+
}).catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
function astgrepFileToWorkspaceRel(file) {
|
|
84
|
+
const abs = isAbsolute(file) ? file : resolve(WORKSPACE_ROOT, file);
|
|
85
|
+
if (!isInside(WORKSPACE_ROOT, abs))
|
|
86
|
+
return undefined;
|
|
87
|
+
return normalizeRelPath(relative(WORKSPACE_ROOT, abs));
|
|
88
|
+
}
|
|
89
|
+
const STRUCTURAL_EDIT_ARTIFACTS_REL = "agent-state/structural-edits";
|
|
90
|
+
function contentSha256(content) {
|
|
91
|
+
return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
92
|
+
}
|
|
93
|
+
function structuralEditArtifactRelPath(prefix, id) {
|
|
94
|
+
return `${STRUCTURAL_EDIT_ARTIFACTS_REL}/${prefix}-${id}.json`;
|
|
95
|
+
}
|
|
96
|
+
async function writeJsonArtifact(artifact) {
|
|
97
|
+
await safeWriteAsync(artifact.artifact_path, JSON.stringify(artifact, null, 2));
|
|
98
|
+
return artifact;
|
|
99
|
+
}
|
|
100
|
+
function readJsonArtifact(relPath) {
|
|
101
|
+
const raw = safeRead(relPath);
|
|
102
|
+
if (raw.startsWith("[FILE NOT FOUND]") || raw.startsWith("[ACCESS DENIED]"))
|
|
103
|
+
return undefined;
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function jsonResponse(payload, isError = false) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
114
|
+
...(isError ? { isError: true } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function storeLocateArtifact(input, result) {
|
|
118
|
+
const locatorId = randomUUID();
|
|
119
|
+
return writeJsonArtifact({
|
|
120
|
+
version: 1,
|
|
121
|
+
kind: "astgrep_locate",
|
|
122
|
+
locator_id: locatorId,
|
|
123
|
+
created_at: new Date().toISOString(),
|
|
124
|
+
artifact_path: structuralEditArtifactRelPath("locate", locatorId),
|
|
125
|
+
input,
|
|
126
|
+
astgrep_command: result.astgrep_command,
|
|
127
|
+
total_matches: result.total_matches,
|
|
128
|
+
matches: result.matches,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function listStructuralEditArtifacts(prefix) {
|
|
132
|
+
const dir = wsPath(STRUCTURAL_EDIT_ARTIFACTS_REL);
|
|
133
|
+
if (!existsSync(dir))
|
|
134
|
+
return [];
|
|
135
|
+
return readdirSync(dir)
|
|
136
|
+
.filter((name) => name.startsWith(`${prefix}-`) && name.endsWith(".json"))
|
|
137
|
+
.sort((a, b) => b.localeCompare(a))
|
|
138
|
+
.map((name) => normalizeRelPath(relative(WORKSPACE_ROOT, resolve(dir, name))));
|
|
139
|
+
}
|
|
140
|
+
function findStoredMatch(matchId) {
|
|
141
|
+
for (const relPath of listStructuralEditArtifacts("locate")) {
|
|
142
|
+
const artifact = readJsonArtifact(relPath);
|
|
143
|
+
if (!artifact)
|
|
144
|
+
continue;
|
|
145
|
+
const match = artifact.matches.find((candidate) => candidate.match_id === matchId);
|
|
146
|
+
if (match)
|
|
147
|
+
return { artifact, match };
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
function loadStructuralEditPlan(planId) {
|
|
152
|
+
return readJsonArtifact(structuralEditArtifactRelPath("plan", planId));
|
|
153
|
+
}
|
|
154
|
+
function planScopeFromMatches(matches) {
|
|
155
|
+
return [...new Set(matches.map((match) => match.file))].sort((a, b) => a.localeCompare(b));
|
|
156
|
+
}
|
|
157
|
+
function rewriteCaptureRefs(rewriteTemplate) {
|
|
158
|
+
const refs = new Set();
|
|
159
|
+
const regex = /\$([A-Z][A-Z0-9_]*)/g;
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = regex.exec(rewriteTemplate)) !== null) {
|
|
162
|
+
refs.add(match[1]);
|
|
163
|
+
}
|
|
164
|
+
return [...refs].sort((a, b) => a.localeCompare(b));
|
|
165
|
+
}
|
|
166
|
+
function missingRewriteCaptures(rewriteTemplate, captures) {
|
|
167
|
+
const available = new Set(Object.keys(captures ?? {}));
|
|
168
|
+
return rewriteCaptureRefs(rewriteTemplate).filter((ref) => !available.has(ref));
|
|
169
|
+
}
|
|
170
|
+
async function compileStructuralEditPlan(input) {
|
|
171
|
+
const scopedMatches = input.selectedMatch
|
|
172
|
+
? input.locateArtifact.matches.filter((match) => match.file === input.selectedMatch?.file)
|
|
173
|
+
: input.locateArtifact.matches;
|
|
174
|
+
const targetFile = input.selectedMatch?.file ?? scopedMatches[0]?.file ?? "";
|
|
175
|
+
const selectedMatch = input.selectedMatch ?? scopedMatches.find((match) => match.file === targetFile) ?? scopedMatches[0];
|
|
176
|
+
const rewriteTemplate = input.rewriteTemplate ?? input.desiredChange;
|
|
177
|
+
const missingCaptures = missingRewriteCaptures(rewriteTemplate, selectedMatch?.captures);
|
|
178
|
+
if (missingCaptures.length > 0) {
|
|
179
|
+
throw new Error(`bad_capture: rewrite references missing capture(s): ${missingCaptures.join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
const planId = randomUUID();
|
|
182
|
+
return writeJsonArtifact({
|
|
183
|
+
version: 1,
|
|
184
|
+
kind: "astgrep_edit_plan",
|
|
185
|
+
plan_id: planId,
|
|
186
|
+
created_at: new Date().toISOString(),
|
|
187
|
+
artifact_path: structuralEditArtifactRelPath("plan", planId),
|
|
188
|
+
locator_artifact_path: input.locateArtifact.artifact_path,
|
|
189
|
+
locator: input.locator,
|
|
190
|
+
target: {
|
|
191
|
+
file: targetFile,
|
|
192
|
+
file_hash: selectedMatch?.file_hash ?? "sha256:none",
|
|
193
|
+
selected_match_id: selectedMatch?.match_id,
|
|
194
|
+
selected_range: selectedMatch?.range,
|
|
195
|
+
selected_text_preview: selectedMatch?.text_preview,
|
|
196
|
+
captures: selectedMatch?.captures,
|
|
197
|
+
node_kind: selectedMatch?.node_kind,
|
|
198
|
+
match_count_in_file: scopedMatches.length,
|
|
199
|
+
scope_match_count: input.locateArtifact.matches.length,
|
|
200
|
+
scope_affected_files: planScopeFromMatches(input.locateArtifact.matches),
|
|
201
|
+
},
|
|
202
|
+
rewrite: {
|
|
203
|
+
desired_change: input.desiredChange,
|
|
204
|
+
rewrite_template: rewriteTemplate,
|
|
205
|
+
rewrite_source: input.rewriteTemplate ? "rewrite_template" : "desired_change",
|
|
206
|
+
},
|
|
207
|
+
validation_command: input.validationCommand,
|
|
208
|
+
test_command: input.testCommand,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function previewDiffText(diffText) {
|
|
212
|
+
const lines = diffText.trimEnd().split("\n");
|
|
213
|
+
const limited = lines.slice(0, 80).join("\n");
|
|
214
|
+
const bounded = limited.slice(0, 4000);
|
|
215
|
+
return bounded.length < diffText.length ? `${bounded}\n…` : bounded;
|
|
216
|
+
}
|
|
217
|
+
function parseChangedRanges(diffText) {
|
|
218
|
+
const ranges = [];
|
|
219
|
+
const regex = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm;
|
|
220
|
+
let match;
|
|
221
|
+
while ((match = regex.exec(diffText)) !== null) {
|
|
222
|
+
const originalStart = Number(match[1] ?? "0");
|
|
223
|
+
const originalLength = Number(match[2] ?? "1");
|
|
224
|
+
const updatedStart = Number(match[3] ?? "0");
|
|
225
|
+
const updatedLength = Number(match[4] ?? "1");
|
|
226
|
+
ranges.push({
|
|
227
|
+
original_start_line: originalStart,
|
|
228
|
+
original_end_line: Math.max(originalStart, originalStart + Math.max(originalLength, 1) - 1),
|
|
229
|
+
updated_start_line: updatedStart,
|
|
230
|
+
updated_end_line: Math.max(updatedStart, updatedStart + Math.max(updatedLength, 1) - 1),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return ranges;
|
|
234
|
+
}
|
|
235
|
+
async function buildStructuralPreview(plan) {
|
|
236
|
+
const previewId = randomUUID();
|
|
237
|
+
const artifactPath = structuralEditArtifactRelPath("preview", previewId);
|
|
238
|
+
const finalize = (artifact) => writeJsonArtifact({
|
|
239
|
+
version: 1,
|
|
240
|
+
kind: "astgrep_preview",
|
|
241
|
+
preview_id: previewId,
|
|
242
|
+
created_at: new Date().toISOString(),
|
|
243
|
+
artifact_path: artifactPath,
|
|
244
|
+
plan_id: plan.plan_id,
|
|
245
|
+
...artifact,
|
|
246
|
+
});
|
|
247
|
+
const currentContent = safeRead(plan.target.file);
|
|
248
|
+
if (currentContent.startsWith("[FILE NOT FOUND]") || currentContent.startsWith("[ACCESS DENIED]")) {
|
|
249
|
+
return {
|
|
250
|
+
artifact: await finalize({
|
|
251
|
+
ok: false,
|
|
252
|
+
reason_code: "target_unreadable",
|
|
253
|
+
error: `Cannot read ${plan.target.file}`,
|
|
254
|
+
target_file: plan.target.file,
|
|
255
|
+
expected_file_hash: plan.target.file_hash,
|
|
256
|
+
matched_count: 0,
|
|
257
|
+
affected_file_count: 0,
|
|
258
|
+
changed_ranges: [],
|
|
259
|
+
diff_summary: "",
|
|
260
|
+
diff_preview: "",
|
|
261
|
+
promotable: false,
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const currentFileHash = contentSha256(currentContent);
|
|
266
|
+
if (currentFileHash !== plan.target.file_hash) {
|
|
267
|
+
return {
|
|
268
|
+
artifact: await finalize({
|
|
269
|
+
ok: false,
|
|
270
|
+
reason_code: "stale_hash",
|
|
271
|
+
error: `Plan ${plan.plan_id} is stale for ${plan.target.file}`,
|
|
272
|
+
target_file: plan.target.file,
|
|
273
|
+
expected_file_hash: plan.target.file_hash,
|
|
274
|
+
current_file_hash: currentFileHash,
|
|
275
|
+
matched_count: 0,
|
|
276
|
+
affected_file_count: 0,
|
|
277
|
+
changed_ranges: [],
|
|
278
|
+
diff_summary: "",
|
|
279
|
+
diff_preview: "",
|
|
280
|
+
promotable: false,
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let locateResult;
|
|
285
|
+
try {
|
|
286
|
+
locateResult = locateAstgrepMatches(plan.locator);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
artifact: await finalize({
|
|
291
|
+
ok: false,
|
|
292
|
+
reason_code: "scope_escape",
|
|
293
|
+
error: error instanceof Error ? error.message : String(error),
|
|
294
|
+
target_file: plan.target.file,
|
|
295
|
+
expected_file_hash: plan.target.file_hash,
|
|
296
|
+
current_file_hash: currentFileHash,
|
|
297
|
+
matched_count: 0,
|
|
298
|
+
affected_file_count: 0,
|
|
299
|
+
changed_ranges: [],
|
|
300
|
+
diff_summary: "",
|
|
301
|
+
diff_preview: "",
|
|
302
|
+
promotable: false,
|
|
303
|
+
}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (!locateResult.ok) {
|
|
307
|
+
return {
|
|
308
|
+
artifact: await finalize({
|
|
309
|
+
ok: false,
|
|
310
|
+
reason_code: "locate_failed",
|
|
311
|
+
error: locateResult.error ?? "Failed to locate structural matches",
|
|
312
|
+
target_file: plan.target.file,
|
|
313
|
+
expected_file_hash: plan.target.file_hash,
|
|
314
|
+
current_file_hash: currentFileHash,
|
|
315
|
+
matched_count: 0,
|
|
316
|
+
affected_file_count: 0,
|
|
317
|
+
changed_ranges: [],
|
|
318
|
+
diff_summary: "",
|
|
319
|
+
diff_preview: "",
|
|
320
|
+
promotable: false,
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (locateResult.matches.length === 0) {
|
|
325
|
+
return {
|
|
326
|
+
artifact: await finalize({
|
|
327
|
+
ok: false,
|
|
328
|
+
reason_code: "no_matches",
|
|
329
|
+
error: `No matches found for plan ${plan.plan_id}`,
|
|
330
|
+
target_file: plan.target.file,
|
|
331
|
+
expected_file_hash: plan.target.file_hash,
|
|
332
|
+
current_file_hash: currentFileHash,
|
|
333
|
+
matched_count: 0,
|
|
334
|
+
affected_file_count: 0,
|
|
335
|
+
changed_ranges: [],
|
|
336
|
+
diff_summary: "",
|
|
337
|
+
diff_preview: "",
|
|
338
|
+
promotable: false,
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const affectedFiles = planScopeFromMatches(locateResult.matches);
|
|
343
|
+
const targetPlan = planAstgrepRewriteTargets(affectedFiles);
|
|
344
|
+
if (!targetPlan.ok) {
|
|
345
|
+
return {
|
|
346
|
+
artifact: await finalize({
|
|
347
|
+
ok: false,
|
|
348
|
+
reason_code: "ambiguous_multi_file",
|
|
349
|
+
error: targetPlan.error ?? "astgrep_rewrite refused a multi-file preview",
|
|
350
|
+
target_file: plan.target.file,
|
|
351
|
+
expected_file_hash: plan.target.file_hash,
|
|
352
|
+
current_file_hash: currentFileHash,
|
|
353
|
+
matched_count: locateResult.matches.length,
|
|
354
|
+
affected_file_count: affectedFiles.length,
|
|
355
|
+
changed_ranges: [],
|
|
356
|
+
diff_summary: "",
|
|
357
|
+
diff_preview: "",
|
|
358
|
+
promotable: false,
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const targetMatches = locateResult.matches.filter((match) => match.file === plan.target.file);
|
|
363
|
+
if (targetMatches.length === 0) {
|
|
364
|
+
return {
|
|
365
|
+
artifact: await finalize({
|
|
366
|
+
ok: false,
|
|
367
|
+
reason_code: "target_mismatch",
|
|
368
|
+
error: `Plan ${plan.plan_id} no longer resolves ${plan.target.file}`,
|
|
369
|
+
target_file: plan.target.file,
|
|
370
|
+
expected_file_hash: plan.target.file_hash,
|
|
371
|
+
current_file_hash: currentFileHash,
|
|
372
|
+
matched_count: locateResult.matches.length,
|
|
373
|
+
affected_file_count: affectedFiles.length,
|
|
374
|
+
changed_ranges: [],
|
|
375
|
+
diff_summary: "",
|
|
376
|
+
diff_preview: "",
|
|
377
|
+
promotable: false,
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const astgrepCmd = detectAstgrepCommand();
|
|
382
|
+
if (!astgrepCmd) {
|
|
383
|
+
return {
|
|
384
|
+
artifact: await finalize({
|
|
385
|
+
ok: false,
|
|
386
|
+
reason_code: "astgrep_unavailable",
|
|
387
|
+
error: "ast-grep command not available",
|
|
388
|
+
target_file: plan.target.file,
|
|
389
|
+
expected_file_hash: plan.target.file_hash,
|
|
390
|
+
current_file_hash: currentFileHash,
|
|
391
|
+
matched_count: locateResult.matches.length,
|
|
392
|
+
affected_file_count: affectedFiles.length,
|
|
393
|
+
changed_ranges: [],
|
|
394
|
+
diff_summary: "",
|
|
395
|
+
diff_preview: "",
|
|
396
|
+
promotable: false,
|
|
397
|
+
}),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const stagingDir = wsPath(".ace-staging", `structural-edit-${plan.plan_id}-${Date.now()}`);
|
|
401
|
+
const stagedOriginal = resolve(stagingDir, `original__${plan.target.file.replace(/\//g, "__")}`);
|
|
402
|
+
const stagedRewrite = resolve(stagingDir, `rewrite__${plan.target.file.replace(/\//g, "__")}`);
|
|
403
|
+
mkdirSync(dirname(stagedOriginal), { recursive: true });
|
|
404
|
+
writeFileSync(stagedOriginal, currentContent, "utf-8");
|
|
405
|
+
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 });
|
|
407
|
+
if (rewriteResult.status !== 0) {
|
|
408
|
+
return {
|
|
409
|
+
artifact: await finalize({
|
|
410
|
+
ok: false,
|
|
411
|
+
reason_code: "rewrite_failed",
|
|
412
|
+
error: rewriteResult.stderr || rewriteResult.stdout || "ast-grep rewrite failed",
|
|
413
|
+
target_file: plan.target.file,
|
|
414
|
+
expected_file_hash: plan.target.file_hash,
|
|
415
|
+
current_file_hash: currentFileHash,
|
|
416
|
+
matched_count: locateResult.matches.length,
|
|
417
|
+
affected_file_count: affectedFiles.length,
|
|
418
|
+
changed_ranges: [],
|
|
419
|
+
diff_summary: "",
|
|
420
|
+
diff_preview: "",
|
|
421
|
+
promotable: false,
|
|
422
|
+
staging_path: stagingDir,
|
|
423
|
+
}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const rewrittenContent = readFileSync(stagedRewrite, "utf-8");
|
|
427
|
+
const rewriteHash = contentSha256(rewrittenContent);
|
|
428
|
+
const diff = diffContents(currentContent, rewrittenContent);
|
|
429
|
+
const diffProcess = spawnSync("diff", ["-u", "--label", `a/${plan.target.file}`, "--label", `b/${plan.target.file}`, stagedOriginal, stagedRewrite], { encoding: "utf8", cwd: WORKSPACE_ROOT });
|
|
430
|
+
const diffText = diffProcess.status === 1
|
|
431
|
+
? diffProcess.stdout
|
|
432
|
+
: diffProcess.status === 0
|
|
433
|
+
? ""
|
|
434
|
+
: diff.diff_summary;
|
|
435
|
+
return {
|
|
436
|
+
artifact: await finalize({
|
|
437
|
+
ok: true,
|
|
438
|
+
target_file: plan.target.file,
|
|
439
|
+
expected_file_hash: plan.target.file_hash,
|
|
440
|
+
current_file_hash: currentFileHash,
|
|
441
|
+
rewritten_file_hash: rewriteHash,
|
|
442
|
+
matched_count: targetMatches.length,
|
|
443
|
+
affected_file_count: affectedFiles.length,
|
|
444
|
+
changed_ranges: parseChangedRanges(diffText),
|
|
445
|
+
diff_summary: diff.diff_summary,
|
|
446
|
+
diff_preview: diffText ? previewDiffText(diffText) : diff.diff_summary,
|
|
447
|
+
promotable: diff.has_diff,
|
|
448
|
+
staging_path: stagingDir,
|
|
449
|
+
}),
|
|
450
|
+
rewritten_content: rewrittenContent,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async function resolveReadFileLinesBudget() {
|
|
454
|
+
let modelClass;
|
|
455
|
+
const statuses = await withLocalModelRuntimeRepository(WORKSPACE_ROOT, async (repo) => repo.listRuntimeStatuses()).catch(() => undefined);
|
|
456
|
+
modelClass = statuses?.[0]?.model_class;
|
|
457
|
+
const profile = readRuntimeProfile();
|
|
458
|
+
const resolved = resolveEffectiveSurgicalReadBudget(profile, modelClass);
|
|
459
|
+
return {
|
|
460
|
+
...resolved,
|
|
461
|
+
source: modelClass ? "runtime-status" : "runtime-profile",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
16
464
|
export function registerFileTools(server) {
|
|
17
465
|
// ── Append-only artifact protection ───────────────────────────────
|
|
18
466
|
// These files must only grow; overwrites from the generic write tool
|
|
@@ -26,17 +474,70 @@ export function registerFileTools(server) {
|
|
|
26
474
|
"agent-state/PROVENANCE_LOG.md",
|
|
27
475
|
]);
|
|
28
476
|
// ── Core file operations ──────────────────────────────────────────
|
|
29
|
-
server.tool("read_workspace_file", "Read any workspace file by relative path", {
|
|
477
|
+
server.tool("read_workspace_file", "Read any workspace file by relative path. Prefer outline_file for discovery, read_file_lines for known regions, astgrep_query for symbol lookup. For small_local runs, this is the fallback after surgical reads are insufficient. Cost: heavy for large files.", {
|
|
30
478
|
path: z.string().describe("Relative path from workspace root"),
|
|
31
479
|
}, async ({ path }) => ({
|
|
32
480
|
content: [{ type: "text", text: safeRead(path) }],
|
|
33
481
|
}));
|
|
34
|
-
server.tool("write_workspace_file", "Write or update a workspace file", {
|
|
482
|
+
server.tool("write_workspace_file", "Write or update a workspace file. Prefer apply_patch for targeted edits, astgrep_rewrite for structural rewrites. Cost: heavy.", {
|
|
35
483
|
path: z.string().describe("Relative path from workspace root"),
|
|
36
484
|
content: z.string().describe("File content"),
|
|
37
|
-
|
|
485
|
+
workspace_path: z
|
|
486
|
+
.string()
|
|
487
|
+
.optional()
|
|
488
|
+
.describe("Required workspace root for model/runtime writes"),
|
|
489
|
+
}, async ({ path, content, workspace_path }) => {
|
|
490
|
+
if (!workspace_path?.trim()) {
|
|
491
|
+
const message = "write_workspace_file requires workspace_path for workspace-root write safety.";
|
|
492
|
+
await appendWriteWorkspaceRejection({
|
|
493
|
+
reason_code: "missing_workspace_path",
|
|
494
|
+
message,
|
|
495
|
+
path,
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
isError: true,
|
|
499
|
+
content: [
|
|
500
|
+
{
|
|
501
|
+
type: "text",
|
|
502
|
+
text: JSON.stringify({
|
|
503
|
+
ok: false,
|
|
504
|
+
reason_code: "missing_workspace_path",
|
|
505
|
+
message,
|
|
506
|
+
}, null, 2),
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const workspaceRoot = resolve(workspace_path);
|
|
38
512
|
const normalizedPath = normalizePathForValidation(path);
|
|
39
513
|
const validationNotes = [];
|
|
514
|
+
let targetPath;
|
|
515
|
+
try {
|
|
516
|
+
targetPath = resolveWorkspaceWritePath(path, workspaceRoot);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
const reasonCode = reasonCodeFromError(error) ?? "path_escape";
|
|
520
|
+
const message = errorMessage(error);
|
|
521
|
+
await appendWriteWorkspaceRejection({
|
|
522
|
+
reason_code: reasonCode,
|
|
523
|
+
message,
|
|
524
|
+
path,
|
|
525
|
+
workspace_path,
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
isError: true,
|
|
529
|
+
content: [
|
|
530
|
+
{
|
|
531
|
+
type: "text",
|
|
532
|
+
text: JSON.stringify({
|
|
533
|
+
ok: false,
|
|
534
|
+
reason_code: reasonCode,
|
|
535
|
+
message,
|
|
536
|
+
}, null, 2),
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
40
541
|
// ── Append-only guard ───────────────────────────────────────────
|
|
41
542
|
if (APPEND_ONLY_PATHS.has(normalizedPath)) {
|
|
42
543
|
const existing = safeRead(path);
|
|
@@ -475,8 +976,21 @@ export function registerFileTools(server) {
|
|
|
475
976
|
const synced = await syncTodoStateSafe(content);
|
|
476
977
|
todoStateSuffix = `\nTODO state synced: ${synced.path}`;
|
|
477
978
|
}
|
|
979
|
+
const storeFallbackKeys = resolveStoreFallbackKeysForPath(normalizedPath);
|
|
980
|
+
const useAsyncStoreAdmission = storeFallbackKeys.length > 0 && workspaceRoot === WORKSPACE_ROOT;
|
|
478
981
|
// Keep the file as a materialized projection after the canonical store update.
|
|
479
|
-
|
|
982
|
+
let abs;
|
|
983
|
+
if (useAsyncStoreAdmission) {
|
|
984
|
+
abs = await safeWriteAsync(path, content);
|
|
985
|
+
}
|
|
986
|
+
else if (workspaceRoot === WORKSPACE_ROOT) {
|
|
987
|
+
abs = safeWrite(path, content);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
991
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
992
|
+
abs = targetPath;
|
|
993
|
+
}
|
|
480
994
|
let kanbanSuffix = "";
|
|
481
995
|
const shouldRefreshKanban = shouldAutoRefreshKanbanForPath(normalizedPath);
|
|
482
996
|
if (shouldRefreshKanban) {
|
|
@@ -544,7 +1058,7 @@ export function registerFileTools(server) {
|
|
|
544
1058
|
}
|
|
545
1059
|
});
|
|
546
1060
|
// ── Safe-edit (new P0 tool) ───────────────────────────────────────
|
|
547
|
-
server.tool("safe_edit_file", "Edit a file using copy→validate→swap pattern. Automatically rolls back if validation or tests fail.", {
|
|
1061
|
+
server.tool("safe_edit_file", "Edit a file using copy→validate→swap pattern. Automatically rolls back if validation or tests fail. Prefer apply_patch for small targeted edits. Cost: heavy for large files.", {
|
|
548
1062
|
path: z.string().describe("Workspace-relative file path to edit"),
|
|
549
1063
|
content: z.string().describe("New file content"),
|
|
550
1064
|
validation_command: z
|
|
@@ -676,5 +1190,482 @@ export function registerFileTools(server) {
|
|
|
676
1190
|
],
|
|
677
1191
|
};
|
|
678
1192
|
});
|
|
1193
|
+
// ── Surgical read / query / patch tools ──────────────────────────
|
|
1194
|
+
server.tool("read_file_lines", "Read a specific line range from a workspace file. Prefer this over read_workspace_file for large files. Cost: moderate.", {
|
|
1195
|
+
path: z.string().describe("Workspace-relative file path"),
|
|
1196
|
+
start_line: z.number().int().min(1).describe("First line to read (1-indexed)"),
|
|
1197
|
+
end_line: z.number().int().min(1).describe("Last line to read (inclusive)"),
|
|
1198
|
+
symbol_anchor: z.string().optional().describe("Optional symbol name hint for context (informational only)"),
|
|
1199
|
+
}, async ({ path: relPath, start_line, end_line, symbol_anchor }) => {
|
|
1200
|
+
const budget = await resolveReadFileLinesBudget();
|
|
1201
|
+
const requestedLines = end_line - start_line + 1;
|
|
1202
|
+
const budgetText = budget.read_file_lines_max_lines === null
|
|
1203
|
+
? "unbounded"
|
|
1204
|
+
: String(budget.read_file_lines_max_lines);
|
|
1205
|
+
const headerLines = [
|
|
1206
|
+
`# Budget: model_class=${budget.model_class}; read_file_lines_max_lines=${budgetText}`,
|
|
1207
|
+
`# Budget source: ${budget.source}`,
|
|
1208
|
+
];
|
|
1209
|
+
if (budget.read_file_lines_max_lines !== null &&
|
|
1210
|
+
requestedLines > budget.read_file_lines_max_lines) {
|
|
1211
|
+
return {
|
|
1212
|
+
content: [
|
|
1213
|
+
{
|
|
1214
|
+
type: "text",
|
|
1215
|
+
text: [
|
|
1216
|
+
...headerLines,
|
|
1217
|
+
`Error: requested range (${requestedLines} lines) exceeds ${budget.model_class} budget of ${budgetText} lines.`,
|
|
1218
|
+
"Use outline_file for discovery or astgrep_query for symbol lookup.",
|
|
1219
|
+
].join("\n"),
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
isError: true,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
const content = safeRead(relPath);
|
|
1226
|
+
const lines = content.split("\n");
|
|
1227
|
+
const slice = lines.slice(start_line - 1, end_line);
|
|
1228
|
+
const result = slice.map((l, i) => `${start_line + i}: ${l}`).join("\n");
|
|
1229
|
+
return {
|
|
1230
|
+
content: [
|
|
1231
|
+
{
|
|
1232
|
+
type: "text",
|
|
1233
|
+
text: [
|
|
1234
|
+
...headerLines,
|
|
1235
|
+
`# ${relPath} (lines ${start_line}–${end_line})`,
|
|
1236
|
+
symbol_anchor ? `# Symbol context: ${symbol_anchor}` : "",
|
|
1237
|
+
"",
|
|
1238
|
+
result,
|
|
1239
|
+
]
|
|
1240
|
+
.filter(Boolean)
|
|
1241
|
+
.join("\n"),
|
|
1242
|
+
},
|
|
1243
|
+
],
|
|
1244
|
+
};
|
|
1245
|
+
});
|
|
1246
|
+
server.tool("outline_file", "Return symbol signatures and top-level structure of a file without bodies. Use for discovery before reading specific regions. Cost: cheap.", {
|
|
1247
|
+
path: z.string().describe("Workspace-relative file path"),
|
|
1248
|
+
}, async ({ path: relPath }) => {
|
|
1249
|
+
const content = safeRead(relPath);
|
|
1250
|
+
const lines = content.split("\n");
|
|
1251
|
+
const outlinePatterns = [
|
|
1252
|
+
/^export\s+(async\s+)?function\s+\w+/,
|
|
1253
|
+
/^export\s+(class|interface|type|enum|const|abstract)\s+\w+/,
|
|
1254
|
+
/^(export\s+)?default\s+/,
|
|
1255
|
+
/^\s+(async\s+)?(\w+\s+)?\w+\s*\([^)]*\)\s*[:{]/,
|
|
1256
|
+
/^(function|class|interface|type|enum|const|let|var)\s+\w+/,
|
|
1257
|
+
];
|
|
1258
|
+
const outline = [];
|
|
1259
|
+
lines.forEach((line, idx) => {
|
|
1260
|
+
if (outlinePatterns.some(p => p.test(line))) {
|
|
1261
|
+
outline.push(`${idx + 1}: ${line.trimEnd()}`);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
return {
|
|
1265
|
+
content: [{
|
|
1266
|
+
type: "text",
|
|
1267
|
+
text: [
|
|
1268
|
+
`# Outline: ${relPath} (${lines.length} lines total)`,
|
|
1269
|
+
"",
|
|
1270
|
+
outline.length > 0 ? outline.join("\n") : "(no symbols found)",
|
|
1271
|
+
].join("\n"),
|
|
1272
|
+
}],
|
|
1273
|
+
};
|
|
1274
|
+
});
|
|
1275
|
+
server.tool("astgrep_query", "Search for a structural pattern in workspace files using ast-grep. Returns matching ranges without reading whole files. Cost: cheap.", {
|
|
1276
|
+
pattern: z.string().describe("ast-grep pattern, e.g. 'export function $NAME($$$)'"),
|
|
1277
|
+
lang: z.string().describe("Language: ts, py, rust, go, js"),
|
|
1278
|
+
scope: z.string().optional().describe("Workspace-relative directory to search (default: src)"),
|
|
1279
|
+
max_results: z.number().int().min(1).max(200).optional().describe("Max results to return (default: 50)"),
|
|
1280
|
+
}, async ({ pattern, lang, scope, max_results }) => {
|
|
1281
|
+
const root = wsPath(scope ?? "src");
|
|
1282
|
+
const matches = runAstgrepQuery(pattern, lang, [root]);
|
|
1283
|
+
const limited = matches.slice(0, max_results ?? 50);
|
|
1284
|
+
const lines = limited.map((m) => `${m.file}:${m.line}: ${m.text}`);
|
|
1285
|
+
return {
|
|
1286
|
+
content: [{
|
|
1287
|
+
type: "text",
|
|
1288
|
+
text: [
|
|
1289
|
+
`# astgrep_query: ${pattern} (${lang})`,
|
|
1290
|
+
`# Found ${matches.length} matches, showing ${limited.length}`,
|
|
1291
|
+
"",
|
|
1292
|
+
...lines,
|
|
1293
|
+
].join("\n"),
|
|
1294
|
+
}],
|
|
1295
|
+
};
|
|
1296
|
+
});
|
|
1297
|
+
server.tool("astgrep_locate", "Locate structural matches and persist a reusable locator artifact with match IDs, ranges, previews, captures, and file hashes. Cost: cheap.", {
|
|
1298
|
+
pattern: z.string().describe("ast-grep pattern to match"),
|
|
1299
|
+
lang: z.string().describe("Language: ts, py, rust, go, js"),
|
|
1300
|
+
scope: z.string().optional().describe("Workspace-relative directory (default: src)"),
|
|
1301
|
+
symbol_hint: z.string().optional().describe("Optional symbol name or text hint used to narrow or prioritize matches"),
|
|
1302
|
+
max_results: z.number().int().min(1).max(200).optional().describe("Max results to persist/return (default: 50)"),
|
|
1303
|
+
}, async ({ pattern, lang, scope, symbol_hint, max_results }) => {
|
|
1304
|
+
const locatorInput = {
|
|
1305
|
+
pattern,
|
|
1306
|
+
lang,
|
|
1307
|
+
scope: scope ?? "src",
|
|
1308
|
+
symbol_hint,
|
|
1309
|
+
max_results,
|
|
1310
|
+
};
|
|
1311
|
+
try {
|
|
1312
|
+
const result = locateAstgrepMatches(locatorInput);
|
|
1313
|
+
const artifact = await storeLocateArtifact(locatorInput, result);
|
|
1314
|
+
return jsonResponse({
|
|
1315
|
+
ok: result.ok,
|
|
1316
|
+
locator_id: artifact.locator_id,
|
|
1317
|
+
artifact_path: artifact.artifact_path,
|
|
1318
|
+
pattern,
|
|
1319
|
+
lang,
|
|
1320
|
+
scope: locatorInput.scope,
|
|
1321
|
+
symbol_hint,
|
|
1322
|
+
total_matches: artifact.total_matches,
|
|
1323
|
+
matches: artifact.matches.map(({ matched_text: _matchedText, ...match }) => match),
|
|
1324
|
+
error: result.error,
|
|
1325
|
+
}, !result.ok);
|
|
1326
|
+
}
|
|
1327
|
+
catch (error) {
|
|
1328
|
+
return jsonResponse({
|
|
1329
|
+
ok: false,
|
|
1330
|
+
reason_code: "scope_escape",
|
|
1331
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1332
|
+
pattern,
|
|
1333
|
+
lang,
|
|
1334
|
+
scope: locatorInput.scope,
|
|
1335
|
+
}, true);
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
server.tool("compile_structural_edit", "Compile a one-file structural edit plan from a selected match ID or locator input and persist the plan artifact without touching files. Cost: moderate.", {
|
|
1339
|
+
match_id: z.string().optional().describe("Previously returned astgrep_locate match ID"),
|
|
1340
|
+
pattern: z.string().optional().describe("ast-grep pattern to locate when no match_id is provided"),
|
|
1341
|
+
lang: z.string().optional().describe("Language: ts, py, rust, go, js when using locator input"),
|
|
1342
|
+
scope: z.string().optional().describe("Workspace-relative directory (default: src) when using locator input"),
|
|
1343
|
+
symbol_hint: z.string().optional().describe("Optional symbol name or text hint for locator input"),
|
|
1344
|
+
max_results: z.number().int().min(1).max(200).optional().describe("Max locator matches to keep (default: 50)"),
|
|
1345
|
+
desired_change: z.string().describe("Plain-language edit intent; used as rewrite_template when no explicit template is provided"),
|
|
1346
|
+
rewrite_template: z.string().optional().describe("Explicit ast-grep rewrite template; preferred when provided"),
|
|
1347
|
+
validation_command: z.string().optional().describe("Shell command to validate before promotion"),
|
|
1348
|
+
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, }) => {
|
|
1350
|
+
if (!match_id && (!pattern || !lang)) {
|
|
1351
|
+
return jsonResponse({
|
|
1352
|
+
ok: false,
|
|
1353
|
+
reason_code: "locator_required",
|
|
1354
|
+
error: "compile_structural_edit requires either match_id or pattern+lang locator input",
|
|
1355
|
+
}, true);
|
|
1356
|
+
}
|
|
1357
|
+
let locateArtifact;
|
|
1358
|
+
let selectedMatch;
|
|
1359
|
+
let locator;
|
|
1360
|
+
if (match_id) {
|
|
1361
|
+
const stored = findStoredMatch(match_id);
|
|
1362
|
+
if (!stored) {
|
|
1363
|
+
return jsonResponse({
|
|
1364
|
+
ok: false,
|
|
1365
|
+
reason_code: "match_not_found",
|
|
1366
|
+
error: `No persisted locator match found for ${match_id}`,
|
|
1367
|
+
match_id,
|
|
1368
|
+
}, true);
|
|
1369
|
+
}
|
|
1370
|
+
const narrowedMatches = stored.artifact.matches.filter((match) => match.file === stored.match.file);
|
|
1371
|
+
locator = {
|
|
1372
|
+
...stored.artifact.input,
|
|
1373
|
+
scope: stored.match.file,
|
|
1374
|
+
};
|
|
1375
|
+
locateArtifact = {
|
|
1376
|
+
...stored.artifact,
|
|
1377
|
+
input: locator,
|
|
1378
|
+
matches: narrowedMatches,
|
|
1379
|
+
total_matches: narrowedMatches.length,
|
|
1380
|
+
};
|
|
1381
|
+
selectedMatch = stored.match;
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
locator = {
|
|
1385
|
+
pattern: pattern,
|
|
1386
|
+
lang: lang,
|
|
1387
|
+
scope: scope ?? "src",
|
|
1388
|
+
symbol_hint,
|
|
1389
|
+
max_results,
|
|
1390
|
+
};
|
|
1391
|
+
try {
|
|
1392
|
+
const locateResult = locateAstgrepMatches(locator);
|
|
1393
|
+
locateArtifact = await storeLocateArtifact(locator, locateResult);
|
|
1394
|
+
if (!locateResult.ok) {
|
|
1395
|
+
return jsonResponse({
|
|
1396
|
+
ok: false,
|
|
1397
|
+
reason_code: "locate_failed",
|
|
1398
|
+
error: locateResult.error ?? "Failed to locate structural matches",
|
|
1399
|
+
locator_id: locateArtifact.locator_id,
|
|
1400
|
+
artifact_path: locateArtifact.artifact_path,
|
|
1401
|
+
}, true);
|
|
1402
|
+
}
|
|
1403
|
+
if (locateArtifact.matches.length === 0) {
|
|
1404
|
+
return jsonResponse({
|
|
1405
|
+
ok: false,
|
|
1406
|
+
reason_code: "no_matches",
|
|
1407
|
+
error: "No matches found for locator input",
|
|
1408
|
+
locator_id: locateArtifact.locator_id,
|
|
1409
|
+
artifact_path: locateArtifact.artifact_path,
|
|
1410
|
+
}, true);
|
|
1411
|
+
}
|
|
1412
|
+
selectedMatch = locateArtifact.matches[0];
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
return jsonResponse({
|
|
1416
|
+
ok: false,
|
|
1417
|
+
reason_code: "scope_escape",
|
|
1418
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1419
|
+
}, true);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (!locateArtifact) {
|
|
1423
|
+
return jsonResponse({
|
|
1424
|
+
ok: false,
|
|
1425
|
+
reason_code: "locator_missing",
|
|
1426
|
+
error: "Structural locator artifact was not available",
|
|
1427
|
+
}, true);
|
|
1428
|
+
}
|
|
1429
|
+
const scopedMatches = selectedMatch
|
|
1430
|
+
? locateArtifact.matches.filter((match) => match.file === selectedMatch?.file)
|
|
1431
|
+
: locateArtifact.matches;
|
|
1432
|
+
const scopeFiles = planScopeFromMatches(locateArtifact.matches);
|
|
1433
|
+
if (scopeFiles.length > 1) {
|
|
1434
|
+
return jsonResponse({
|
|
1435
|
+
ok: false,
|
|
1436
|
+
reason_code: "ambiguous_multi_file",
|
|
1437
|
+
error: `compile_structural_edit refuses multi-file locators (${scopeFiles.length} files)`,
|
|
1438
|
+
locator_id: locateArtifact.locator_id,
|
|
1439
|
+
artifact_path: locateArtifact.artifact_path,
|
|
1440
|
+
affected_files: scopeFiles,
|
|
1441
|
+
}, true);
|
|
1442
|
+
}
|
|
1443
|
+
if (scopedMatches.length === 0) {
|
|
1444
|
+
return jsonResponse({
|
|
1445
|
+
ok: false,
|
|
1446
|
+
reason_code: "no_matches",
|
|
1447
|
+
error: "No matches remained in the selected file for plan compilation",
|
|
1448
|
+
locator_id: locateArtifact.locator_id,
|
|
1449
|
+
artifact_path: locateArtifact.artifact_path,
|
|
1450
|
+
}, true);
|
|
1451
|
+
}
|
|
1452
|
+
const plan = await compileStructuralEditPlan({
|
|
1453
|
+
locator,
|
|
1454
|
+
locateArtifact,
|
|
1455
|
+
selectedMatch,
|
|
1456
|
+
desiredChange: desired_change,
|
|
1457
|
+
rewriteTemplate: rewrite_template,
|
|
1458
|
+
validationCommand: validation_command,
|
|
1459
|
+
testCommand: test_command,
|
|
1460
|
+
}).catch((error) => {
|
|
1461
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1462
|
+
return {
|
|
1463
|
+
error: message,
|
|
1464
|
+
reason_code: message.startsWith("bad_capture:") ? "bad_capture" : "plan_compile_failed",
|
|
1465
|
+
};
|
|
1466
|
+
});
|
|
1467
|
+
if ("error" in plan) {
|
|
1468
|
+
return jsonResponse({
|
|
1469
|
+
ok: false,
|
|
1470
|
+
reason_code: plan.reason_code,
|
|
1471
|
+
error: plan.error,
|
|
1472
|
+
locator_id: locateArtifact.locator_id,
|
|
1473
|
+
artifact_path: locateArtifact.artifact_path,
|
|
1474
|
+
}, true);
|
|
1475
|
+
}
|
|
1476
|
+
return jsonResponse(plan);
|
|
1477
|
+
});
|
|
1478
|
+
server.tool("preview_structural_edit", "Stage a compiled structural edit plan, return bounded diff and changed ranges, and refuse stale-hash or multi-file previews. Cost: heavy.", {
|
|
1479
|
+
plan_id: z.string().describe("Compiled structural edit plan id"),
|
|
1480
|
+
}, async ({ plan_id }) => {
|
|
1481
|
+
const plan = loadStructuralEditPlan(plan_id);
|
|
1482
|
+
if (!plan) {
|
|
1483
|
+
return jsonResponse({
|
|
1484
|
+
ok: false,
|
|
1485
|
+
reason_code: "plan_not_found",
|
|
1486
|
+
error: `No structural edit plan found for ${plan_id}`,
|
|
1487
|
+
plan_id,
|
|
1488
|
+
}, true);
|
|
1489
|
+
}
|
|
1490
|
+
const preview = (await buildStructuralPreview(plan)).artifact;
|
|
1491
|
+
return jsonResponse(preview, !preview.ok);
|
|
1492
|
+
});
|
|
1493
|
+
server.tool("astgrep_rewrite", "Apply a structural rewrite to one workspace file using a compiled plan_id or direct ast-grep pattern + replacement. Multi-file rewrites are refused; staged through safe_edit_file. Emits transition-compatible result details. Cost: heavy.", {
|
|
1494
|
+
plan_id: z.string().optional().describe("Compiled structural edit plan id; preferred over direct pattern+rewrite"),
|
|
1495
|
+
pattern: z.string().optional().describe("ast-grep pattern to match"),
|
|
1496
|
+
rewrite: z.string().optional().describe("Replacement template (use $NAME etc. from pattern)"),
|
|
1497
|
+
lang: z.string().optional().describe("Language: ts, py, rust, go, js"),
|
|
1498
|
+
scope: z.string().optional().describe("Workspace-relative directory (default: src)"),
|
|
1499
|
+
confirm_multi_file: z.boolean().optional().describe("Deprecated; multi-file rewrites are always refused"),
|
|
1500
|
+
validation_command: z.string().optional().describe("Shell command to validate the staged rewrite before promotion"),
|
|
1501
|
+
test_command: z.string().optional().describe("Shell command to test the staged rewrite before promotion"),
|
|
1502
|
+
session_id: z.string().optional().describe("Optional runtime session id for transition-record emission"),
|
|
1503
|
+
}, async ({ plan_id, pattern, rewrite, lang, scope, validation_command, test_command, session_id }) => {
|
|
1504
|
+
let compiledPlan;
|
|
1505
|
+
if (plan_id) {
|
|
1506
|
+
compiledPlan = loadStructuralEditPlan(plan_id);
|
|
1507
|
+
if (!compiledPlan) {
|
|
1508
|
+
return {
|
|
1509
|
+
content: [{ type: "text", text: `astgrep_rewrite failed: no structural edit plan found for ${plan_id}` }],
|
|
1510
|
+
isError: true,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
if (!pattern || !rewrite || !lang) {
|
|
1516
|
+
return {
|
|
1517
|
+
content: [{ type: "text", text: "astgrep_rewrite requires plan_id or direct pattern+rewrite+lang input." }],
|
|
1518
|
+
isError: true,
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const locatorInput = {
|
|
1522
|
+
pattern,
|
|
1523
|
+
lang,
|
|
1524
|
+
scope: scope ?? "src",
|
|
1525
|
+
max_results: 200,
|
|
1526
|
+
};
|
|
1527
|
+
let locateResult;
|
|
1528
|
+
try {
|
|
1529
|
+
locateResult = locateAstgrepMatches(locatorInput);
|
|
1530
|
+
}
|
|
1531
|
+
catch {
|
|
1532
|
+
return {
|
|
1533
|
+
content: [{ type: "text", text: `astgrep_rewrite failed: scope escapes workspace root (${scope})` }],
|
|
1534
|
+
isError: true,
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
const locateArtifact = await storeLocateArtifact(locatorInput, locateResult);
|
|
1538
|
+
if (!locateResult.ok) {
|
|
1539
|
+
return {
|
|
1540
|
+
content: [{ type: "text", text: `astgrep_rewrite failed: ${locateResult.error ?? "locator failed"}` }],
|
|
1541
|
+
isError: true,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
if (locateArtifact.matches.length === 0) {
|
|
1545
|
+
return {
|
|
1546
|
+
content: [{ type: "text", text: "No matches found for pattern. No files modified." }],
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
const scopeFiles = planScopeFromMatches(locateArtifact.matches);
|
|
1550
|
+
const targetPlan = planAstgrepRewriteTargets(scopeFiles);
|
|
1551
|
+
if (!targetPlan.ok) {
|
|
1552
|
+
await appendAstgrepRewriteTransition(session_id, "refused", targetPlan.error ?? "astgrep_rewrite refused the requested rewrite", [locateArtifact.artifact_path, ...targetPlan.affected_files]);
|
|
1553
|
+
return {
|
|
1554
|
+
content: [{
|
|
1555
|
+
type: "text",
|
|
1556
|
+
text: targetPlan.error ?? "astgrep_rewrite refused the requested rewrite",
|
|
1557
|
+
}],
|
|
1558
|
+
isError: true,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
const directPlan = await compileStructuralEditPlan({
|
|
1562
|
+
locator: locatorInput,
|
|
1563
|
+
locateArtifact,
|
|
1564
|
+
selectedMatch: locateArtifact.matches[0],
|
|
1565
|
+
desiredChange: rewrite,
|
|
1566
|
+
rewriteTemplate: rewrite,
|
|
1567
|
+
validationCommand: validation_command,
|
|
1568
|
+
testCommand: test_command,
|
|
1569
|
+
}).catch((error) => ({
|
|
1570
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1571
|
+
}));
|
|
1572
|
+
if ("error" in directPlan) {
|
|
1573
|
+
return {
|
|
1574
|
+
content: [{ type: "text", text: `astgrep_rewrite failed: ${directPlan.error}` }],
|
|
1575
|
+
isError: true,
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
compiledPlan = directPlan;
|
|
1579
|
+
}
|
|
1580
|
+
const preview = await buildStructuralPreview(compiledPlan);
|
|
1581
|
+
if (!preview.artifact.ok || !preview.artifact.promotable || !preview.rewritten_content) {
|
|
1582
|
+
await appendAstgrepRewriteTransition(session_id, "refused", preview.artifact.error ?? "astgrep_rewrite preview refused promotion", [compiledPlan.artifact_path, preview.artifact.artifact_path, compiledPlan.target.file]);
|
|
1583
|
+
return {
|
|
1584
|
+
content: [{
|
|
1585
|
+
type: "text",
|
|
1586
|
+
text: [
|
|
1587
|
+
"astgrep_rewrite failed before promotion",
|
|
1588
|
+
`Plan: ${compiledPlan.artifact_path}`,
|
|
1589
|
+
`Preview: ${preview.artifact.artifact_path}`,
|
|
1590
|
+
`Path: ${compiledPlan.target.file}`,
|
|
1591
|
+
`Reason: ${preview.artifact.error ?? "preview was not promotable"}`,
|
|
1592
|
+
].join("\n"),
|
|
1593
|
+
}],
|
|
1594
|
+
isError: true,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
const safeResult = safeEditFile({
|
|
1598
|
+
path: compiledPlan.target.file,
|
|
1599
|
+
content: preview.rewritten_content,
|
|
1600
|
+
validation_command: validation_command ?? compiledPlan.validation_command,
|
|
1601
|
+
test_command: test_command ?? compiledPlan.test_command,
|
|
1602
|
+
});
|
|
1603
|
+
if (!safeResult.ok) {
|
|
1604
|
+
await appendAstgrepRewriteTransition(session_id, "failed", `astgrep_rewrite failed before promotion for ${compiledPlan.target.file}: ${safeResult.error ?? "unknown error"}`, [compiledPlan.artifact_path, preview.artifact.artifact_path, compiledPlan.target.file]);
|
|
1605
|
+
return {
|
|
1606
|
+
content: [{
|
|
1607
|
+
type: "text",
|
|
1608
|
+
text: [
|
|
1609
|
+
`astgrep_rewrite failed before promotion`,
|
|
1610
|
+
`Plan: ${compiledPlan.artifact_path}`,
|
|
1611
|
+
`Preview: ${preview.artifact.artifact_path}`,
|
|
1612
|
+
`Path: ${compiledPlan.target.file}`,
|
|
1613
|
+
`Error: ${safeResult.error ?? "unknown error"}`,
|
|
1614
|
+
safeResult.validation_passed !== undefined ? `Validation: ${safeResult.validation_passed ? "passed" : "FAILED"}` : "Validation: not requested",
|
|
1615
|
+
safeResult.validation_output ? `Validation output:\n${safeResult.validation_output}` : "",
|
|
1616
|
+
safeResult.test_passed !== undefined ? `Tests: ${safeResult.test_passed ? "passed" : "FAILED"}` : "Tests: not requested",
|
|
1617
|
+
safeResult.test_output ? `Test output:\n${safeResult.test_output}` : "",
|
|
1618
|
+
`Staging: ${safeResult.staging_path || preview.artifact.staging_path || ""}`,
|
|
1619
|
+
].filter(Boolean).join("\n"),
|
|
1620
|
+
}],
|
|
1621
|
+
isError: true,
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
await appendAstgrepRewriteTransition(session_id, "promoted", `astgrep_rewrite promoted staged rewrite for ${compiledPlan.target.file}`, [compiledPlan.artifact_path, preview.artifact.artifact_path, compiledPlan.target.file]);
|
|
1625
|
+
return {
|
|
1626
|
+
content: [{
|
|
1627
|
+
type: "text",
|
|
1628
|
+
text: [
|
|
1629
|
+
`# astgrep_rewrite completed`,
|
|
1630
|
+
`Plan: ${compiledPlan.artifact_path}`,
|
|
1631
|
+
`Preview: ${preview.artifact.artifact_path}`,
|
|
1632
|
+
`Pattern: ${compiledPlan.locator.pattern}`,
|
|
1633
|
+
`Rewrite: ${compiledPlan.rewrite.rewrite_template}`,
|
|
1634
|
+
`Files affected: ${preview.artifact.affected_file_count}`,
|
|
1635
|
+
`Path: ${compiledPlan.target.file}`,
|
|
1636
|
+
`Hash: ${safeResult.original_hash} → ${safeResult.new_hash}`,
|
|
1637
|
+
safeResult.validation_passed !== undefined ? `Validation: ${safeResult.validation_passed ? "passed" : "failed"}` : "Validation: not requested",
|
|
1638
|
+
safeResult.test_passed !== undefined ? `Tests: ${safeResult.test_passed ? "passed" : "failed"}` : "Tests: not requested",
|
|
1639
|
+
`Staging: ${safeResult.staging_path}`,
|
|
1640
|
+
].filter(Boolean).join("\n"),
|
|
1641
|
+
}],
|
|
1642
|
+
};
|
|
1643
|
+
});
|
|
1644
|
+
server.tool("apply_patch", "Apply a unified diff patch to a workspace file. Applies the patch to staged content, runs optional validation/tests, and promotes to the target only on success. Cost: moderate.", {
|
|
1645
|
+
path: z.string().describe("Workspace-relative file path to patch"),
|
|
1646
|
+
patch: z.string().describe("Unified diff patch (output of diff -u or similar)"),
|
|
1647
|
+
validation_command: z.string().optional().describe("Shell command to validate after applying (e.g. 'npx tsc --noEmit')"),
|
|
1648
|
+
test_command: z.string().optional().describe("Shell command to run tests after applying"),
|
|
1649
|
+
}, async ({ path: relPath, patch, validation_command, test_command }) => {
|
|
1650
|
+
const result = applyPatch({ path: relPath, patch, validation_command, test_command });
|
|
1651
|
+
if (!result.ok) {
|
|
1652
|
+
return {
|
|
1653
|
+
content: [{ type: "text", text: `apply_patch failed: ${result.error ?? "unknown error"}\n${result.validation_output ?? ""}` }],
|
|
1654
|
+
isError: true,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
content: [{
|
|
1659
|
+
type: "text",
|
|
1660
|
+
text: [
|
|
1661
|
+
`# apply_patch: ${relPath}`,
|
|
1662
|
+
`Status: success`,
|
|
1663
|
+
result.validation_passed !== undefined ? `Validation: ${result.validation_passed ? "passed" : "failed"}` : "",
|
|
1664
|
+
result.test_passed !== undefined ? `Tests: ${result.test_passed ? "passed" : "failed"}` : "",
|
|
1665
|
+
`Staging: ${result.staging_path}`,
|
|
1666
|
+
].filter(Boolean).join("\n"),
|
|
1667
|
+
}],
|
|
1668
|
+
};
|
|
1669
|
+
});
|
|
679
1670
|
}
|
|
680
1671
|
//# sourceMappingURL=tools-files.js.map
|