@bigknoxy/hashpilot 4.6.3
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/LICENSE +21 -0
- package/README.md +777 -0
- package/docs/ADAPTER-CONTRACT.md +1260 -0
- package/docs/ARCHITECTURE.md +846 -0
- package/docs/CLI-QUICKREF.md +827 -0
- package/docs/COMPETITIVE-ANALYSIS.md +307 -0
- package/docs/INSTALL.md +403 -0
- package/docs/INTEGRATION-CLAUDE.md +126 -0
- package/docs/INTEGRATION-MCP.md +196 -0
- package/docs/INTEGRATION-OPENCODE.md +136 -0
- package/docs/INTEGRATION-PI.md +195 -0
- package/package.json +77 -0
- package/scripts/build-site.sh +39 -0
- package/scripts/doctor.sh +218 -0
- package/scripts/gen-cli-quickref.ts +232 -0
- package/scripts/install-cli.sh +60 -0
- package/scripts/install.sh +466 -0
- package/scripts/roadmap-lint.ts +200 -0
- package/scripts/uninstall.sh +202 -0
- package/src/cli-node.cjs +51 -0
- package/src/cli.ts +209 -0
- package/src/commands/ast.ts +255 -0
- package/src/commands/diff.ts +98 -0
- package/src/commands/edit.ts +93 -0
- package/src/commands/hash.ts +64 -0
- package/src/commands/intent.ts +68 -0
- package/src/commands/maintenance.ts +191 -0
- package/src/commands/mcp.ts +28 -0
- package/src/commands/provenance.ts +111 -0
- package/src/commands/read.ts +117 -0
- package/src/commands/route.ts +42 -0
- package/src/commands/shared.ts +65 -0
- package/src/commands/telemetry.ts +126 -0
- package/src/commands/verify.ts +61 -0
- package/src/core/ast-edit.ts +2357 -0
- package/src/core/batch-edit.ts +185 -0
- package/src/core/config.ts +189 -0
- package/src/core/diff-engine.ts +474 -0
- package/src/core/doctor.ts +303 -0
- package/src/core/encoding.ts +116 -0
- package/src/core/envelope.ts +163 -0
- package/src/core/exit-codes.ts +198 -0
- package/src/core/format.ts +339 -0
- package/src/core/grep.ts +180 -0
- package/src/core/hash-edit.ts +416 -0
- package/src/core/index.ts +155 -0
- package/src/core/intent.ts +584 -0
- package/src/core/locking.ts +292 -0
- package/src/core/module-system.ts +142 -0
- package/src/core/operations.ts +557 -0
- package/src/core/output.ts +122 -0
- package/src/core/path-normalize.ts +61 -0
- package/src/core/paths.ts +326 -0
- package/src/core/plan-executor.ts +437 -0
- package/src/core/platform.ts +132 -0
- package/src/core/provenance.ts +214 -0
- package/src/core/read.ts +111 -0
- package/src/core/redact.ts +98 -0
- package/src/core/resolve-content.ts +12 -0
- package/src/core/router.ts +463 -0
- package/src/core/snapshot.ts +346 -0
- package/src/core/telemetry.ts +838 -0
- package/src/core/utils.ts +7 -0
- package/src/core/verify-baseline.ts +186 -0
- package/src/core/verify-scope.ts +282 -0
- package/src/core/verify.ts +753 -0
- package/src/mcp/server.ts +325 -0
- package/templates/claude-section.md +12 -0
- package/templates/opencode-agent.md +106 -0
- package/templates/opencode-skill.md +241 -0
- package/templates/pi-extension.ts +288 -0
- package/templates/pi-skill.md +123 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server over stdio (#25).
|
|
3
|
+
*
|
|
4
|
+
* The protocol surface is hand-written rather than taken from an SDK: MCP's
|
|
5
|
+
* stdio transport is newline-delimited JSON-RPC 2.0 and the four methods we
|
|
6
|
+
* need are small, while a dependency here would land in every install of a tool
|
|
7
|
+
* whose whole pitch is that it drops into any agent without ceremony.
|
|
8
|
+
*
|
|
9
|
+
* Tools are not declared here. They come from `OPERATIONS` in
|
|
10
|
+
* `src/core/operations.ts`, the same list the CLI is checked against, so the
|
|
11
|
+
* MCP surface cannot quietly drift from the documented commands.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { OPERATIONS, getOperation, inputSchemaFor } from "../core/operations";
|
|
15
|
+
import { API_VERSION, setCommand, takeWarnings } from "../core/envelope";
|
|
16
|
+
// Single source of truth for the version. Bun inlines this JSON import at
|
|
17
|
+
// build time, so any bundle carries the real published version instead of a
|
|
18
|
+
// stale env-var fallback (#156).
|
|
19
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
20
|
+
|
|
21
|
+
/** The package version, echoed back in `initialize`'s `serverInfo`. */
|
|
22
|
+
const SERVER_VERSION: string = pkg.version;
|
|
23
|
+
|
|
24
|
+
/** The MCP revision we implement. Echoed back in `initialize`. */
|
|
25
|
+
export const PROTOCOL_VERSION = "2024-11-05";
|
|
26
|
+
|
|
27
|
+
/* ── JSON-RPC types ──────────────────────────────────────────────────── */
|
|
28
|
+
|
|
29
|
+
export interface JsonRpcRequest {
|
|
30
|
+
jsonrpc: "2.0";
|
|
31
|
+
/** Absent for notifications, which take no response. */
|
|
32
|
+
id?: string | number;
|
|
33
|
+
method: string;
|
|
34
|
+
params?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface JsonRpcResponse {
|
|
38
|
+
jsonrpc: "2.0";
|
|
39
|
+
id: string | number;
|
|
40
|
+
result?: unknown;
|
|
41
|
+
error?: { code: number; message: string; data?: unknown };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** JSON-RPC reserved codes. Tool failures do NOT use these — see `callTool`. */
|
|
45
|
+
const PARSE_ERROR = -32700;
|
|
46
|
+
const INVALID_REQUEST = -32600;
|
|
47
|
+
const METHOD_NOT_FOUND = -32601;
|
|
48
|
+
const INTERNAL_ERROR = -32603;
|
|
49
|
+
|
|
50
|
+
/* ── Method handlers ─────────────────────────────────────────────────── */
|
|
51
|
+
|
|
52
|
+
function listTools() {
|
|
53
|
+
return {
|
|
54
|
+
tools: OPERATIONS.map((op) => ({
|
|
55
|
+
name: op.name,
|
|
56
|
+
description: `${op.summary}\n\n${op.description}`,
|
|
57
|
+
inputSchema: inputSchemaFor(op),
|
|
58
|
+
annotations: {
|
|
59
|
+
readOnlyHint: !op.mutates,
|
|
60
|
+
destructiveHint: op.mutates,
|
|
61
|
+
title: op.summary,
|
|
62
|
+
},
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reject a call whose arguments cannot satisfy the operation before running it.
|
|
69
|
+
* Only presence and coarse shape are checked; the handlers coerce the rest.
|
|
70
|
+
* Returns a message, or null when the arguments are acceptable.
|
|
71
|
+
*/
|
|
72
|
+
function validateArgs(op: ReturnType<typeof getOperation>, args: Record<string, unknown>): string | null {
|
|
73
|
+
if (!op) return null;
|
|
74
|
+
const missing = op.params
|
|
75
|
+
.filter((p) => p.required)
|
|
76
|
+
.filter((p) => {
|
|
77
|
+
const v = args[p.name];
|
|
78
|
+
// An empty string is a legitimate value (a deletion, #40); only
|
|
79
|
+
// absence is missing.
|
|
80
|
+
return v === undefined || v === null;
|
|
81
|
+
})
|
|
82
|
+
.map((p) => p.name);
|
|
83
|
+
if (missing.length) return `missing required parameter(s): ${missing.join(", ")}`;
|
|
84
|
+
|
|
85
|
+
for (const p of op.params) {
|
|
86
|
+
const v = args[p.name];
|
|
87
|
+
if (v === undefined || v === null) continue;
|
|
88
|
+
if (p.type === "string[]" && !Array.isArray(v) && typeof v !== "string") {
|
|
89
|
+
return `parameter "${p.name}" must be an array of strings`;
|
|
90
|
+
}
|
|
91
|
+
// `Number("")` and `Number([])` are both 0, so a NaN test alone lets an
|
|
92
|
+
// empty string or an array through as a number. The handler then coerces it
|
|
93
|
+
// back to undefined and the call returns a green result for a nonsense
|
|
94
|
+
// argument — the same silent-success failure `findFailure` exists to stop.
|
|
95
|
+
if (p.type === "number" && !isNumeric(v)) {
|
|
96
|
+
return `parameter "${p.name}" must be a number`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** True only for a real number, or a string that is entirely one. */
|
|
103
|
+
function isNumeric(v: unknown): boolean {
|
|
104
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
105
|
+
if (typeof v !== "string" || v.trim() === "") return false;
|
|
106
|
+
return Number.isFinite(Number(v));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run one tool call.
|
|
111
|
+
*
|
|
112
|
+
* A failed *edit* is reported as `isError: true` on a successful JSON-RPC
|
|
113
|
+
* result, not as a JSON-RPC error: the distinction MCP draws is that protocol
|
|
114
|
+
* errors are the host's problem while tool errors are the model's to read and
|
|
115
|
+
* act on. The payload carries the envelope's `code` and `recovery` verbatim,
|
|
116
|
+
* which is the whole point — a stale anchor should reach the model as
|
|
117
|
+
* "re-read the file and retry", not as a stack trace.
|
|
118
|
+
*/
|
|
119
|
+
export async function callTool(name: string, rawArgs: unknown): Promise<Record<string, unknown>> {
|
|
120
|
+
// Scopes the envelope's `command` and clears any warnings left by a prior
|
|
121
|
+
// call, so `warnings` reports only what this tool call produced.
|
|
122
|
+
setCommand(name);
|
|
123
|
+
const op = getOperation(name);
|
|
124
|
+
if (!op) {
|
|
125
|
+
return errorResult(name, "UNKNOWN_TOOL", `No such tool: ${name}`, "Call tools/list to see the available tools.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const args = (rawArgs && typeof rawArgs === "object" ? rawArgs : {}) as Record<string, unknown>;
|
|
129
|
+
const invalid = validateArgs(op, args);
|
|
130
|
+
if (invalid) {
|
|
131
|
+
return errorResult(name, "INVALID_ARGUMENTS", invalid, "Check the tool's inputSchema and call it again.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const data = await op.handler(args);
|
|
136
|
+
const failure = findFailure(data);
|
|
137
|
+
if (failure) {
|
|
138
|
+
return errorResult(
|
|
139
|
+
name,
|
|
140
|
+
String(failure.errorCode || failure.code || "EDIT_FAILED"),
|
|
141
|
+
String(failure.error || failure.message || "the edit did not apply"),
|
|
142
|
+
typeof failure.recovery === "string" ? failure.recovery : undefined,
|
|
143
|
+
data as Record<string, unknown>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return okResult(name, data);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
149
|
+
const code = (err as { errorCode?: string })?.errorCode || "INTERNAL_ERROR";
|
|
150
|
+
return errorResult(name, code, message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Find the payload that reports failure, or null when the call succeeded.
|
|
156
|
+
*
|
|
157
|
+
* `routeEdit` reports routing at the top level and the edit's own outcome one
|
|
158
|
+
* level down under `result`, so a check of only the outer `success` reads a
|
|
159
|
+
* failed edit as a success — which would hand the model a green result for an
|
|
160
|
+
* edit that never applied. Both levels are inspected, innermost first, because
|
|
161
|
+
* the inner object carries the specific `errorCode` worth surfacing.
|
|
162
|
+
*/
|
|
163
|
+
function findFailure(data: unknown): Record<string, unknown> | null {
|
|
164
|
+
if (!data || typeof data !== "object") return null;
|
|
165
|
+
const d = data as Record<string, unknown>;
|
|
166
|
+
const inner = findFailure(d.result);
|
|
167
|
+
if (inner) return inner;
|
|
168
|
+
return d.success === false ? d : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* MCP results carry text content. The text is the JSON envelope: models read it
|
|
173
|
+
* directly, and a host that wants structure gets the same object back under
|
|
174
|
+
* `structuredContent`.
|
|
175
|
+
*/
|
|
176
|
+
function okResult(command: string, data: unknown): Record<string, unknown> {
|
|
177
|
+
// The full five-field envelope, identical to the CLI's: an adapter written
|
|
178
|
+
// against docs/ADAPTER-CONTRACT.md must not have to special-case MCP (#104).
|
|
179
|
+
const payload = {
|
|
180
|
+
apiVersion: API_VERSION,
|
|
181
|
+
ok: true,
|
|
182
|
+
command,
|
|
183
|
+
data: data ?? null,
|
|
184
|
+
error: null,
|
|
185
|
+
warnings: takeWarnings(),
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
189
|
+
structuredContent: payload,
|
|
190
|
+
isError: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function errorResult(
|
|
195
|
+
command: string,
|
|
196
|
+
code: string,
|
|
197
|
+
message: string,
|
|
198
|
+
recovery?: string,
|
|
199
|
+
details?: unknown
|
|
200
|
+
): Record<string, unknown> {
|
|
201
|
+
const payload = {
|
|
202
|
+
apiVersion: API_VERSION,
|
|
203
|
+
ok: false,
|
|
204
|
+
command,
|
|
205
|
+
data: details ?? null,
|
|
206
|
+
error: { code, message, ...(recovery ? { recovery } : {}), ...(details ? { details } : {}) },
|
|
207
|
+
warnings: takeWarnings(),
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
211
|
+
structuredContent: payload,
|
|
212
|
+
isError: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Dispatch one request. Returns null for notifications, which JSON-RPC forbids
|
|
218
|
+
* answering — `notifications/initialized` is the one every host sends.
|
|
219
|
+
*/
|
|
220
|
+
export async function handleRequest(req: JsonRpcRequest): Promise<JsonRpcResponse | null> {
|
|
221
|
+
const isNotification = req.id === undefined || req.id === null;
|
|
222
|
+
const id = req.id as string | number;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
switch (req.method) {
|
|
226
|
+
case "initialize":
|
|
227
|
+
return isNotification ? null : {
|
|
228
|
+
jsonrpc: "2.0",
|
|
229
|
+
id,
|
|
230
|
+
result: {
|
|
231
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
232
|
+
capabilities: { tools: { listChanged: false } },
|
|
233
|
+
serverInfo: { name: "hashpilot", version: SERVER_VERSION },
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
case "notifications/initialized":
|
|
238
|
+
case "notifications/cancelled":
|
|
239
|
+
return null;
|
|
240
|
+
|
|
241
|
+
case "ping":
|
|
242
|
+
return isNotification ? null : { jsonrpc: "2.0", id, result: {} };
|
|
243
|
+
|
|
244
|
+
case "tools/list":
|
|
245
|
+
return isNotification ? null : { jsonrpc: "2.0", id, result: listTools() };
|
|
246
|
+
|
|
247
|
+
case "tools/call": {
|
|
248
|
+
const name = String(req.params?.name || "");
|
|
249
|
+
const result = await callTool(name, req.params?.arguments);
|
|
250
|
+
return isNotification ? null : { jsonrpc: "2.0", id, result };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
default:
|
|
254
|
+
if (isNotification) return null;
|
|
255
|
+
return {
|
|
256
|
+
jsonrpc: "2.0",
|
|
257
|
+
id,
|
|
258
|
+
error: { code: METHOD_NOT_FOUND, message: `Method not found: ${req.method}` },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (isNotification) return null;
|
|
263
|
+
return {
|
|
264
|
+
jsonrpc: "2.0",
|
|
265
|
+
id,
|
|
266
|
+
error: { code: INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err) },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Parse and dispatch one newline-delimited message. Returns the line to write back, or null. */
|
|
272
|
+
export async function handleLine(line: string): Promise<string | null> {
|
|
273
|
+
const trimmed = line.trim();
|
|
274
|
+
if (!trimmed) return null;
|
|
275
|
+
|
|
276
|
+
let req: JsonRpcRequest;
|
|
277
|
+
try {
|
|
278
|
+
req = JSON.parse(trimmed);
|
|
279
|
+
} catch {
|
|
280
|
+
// No id is recoverable from unparseable input, so per JSON-RPC the id is null.
|
|
281
|
+
return JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Invalid JSON" } });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!req || typeof req.method !== "string") {
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
jsonrpc: "2.0",
|
|
287
|
+
id: req?.id ?? null,
|
|
288
|
+
error: { code: INVALID_REQUEST, message: "Missing method" },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const res = await handleRequest(req);
|
|
293
|
+
return res ? JSON.stringify(res) : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Run the stdio loop until stdin closes.
|
|
298
|
+
*
|
|
299
|
+
* Requests are handled strictly in order. Concurrency would be free wall-clock
|
|
300
|
+
* here, but two edits to one file interleaving inside one process would
|
|
301
|
+
* deadlock on the advisory lock (it is not re-entrant), and an agent's edits
|
|
302
|
+
* are usually sequentially dependent anyway.
|
|
303
|
+
*
|
|
304
|
+
* Nothing may be written to stdout but protocol frames — a stray log line
|
|
305
|
+
* corrupts the stream — so diagnostics go to stderr.
|
|
306
|
+
*/
|
|
307
|
+
export async function runStdioServer(): Promise<void> {
|
|
308
|
+
let buffer = "";
|
|
309
|
+
const decoder = new TextDecoder();
|
|
310
|
+
|
|
311
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
312
|
+
buffer += decoder.decode(chunk as Uint8Array, { stream: true });
|
|
313
|
+
let nl: number;
|
|
314
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
315
|
+
const line = buffer.slice(0, nl);
|
|
316
|
+
buffer = buffer.slice(nl + 1);
|
|
317
|
+
const out = await handleLine(line);
|
|
318
|
+
if (out !== null) process.stdout.write(out + "\n");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// A final frame with no trailing newline still deserves an answer.
|
|
323
|
+
const out = await handleLine(buffer);
|
|
324
|
+
if (out !== null) process.stdout.write(out + "\n");
|
|
325
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## Install HashPilot
|
|
2
|
+
`curl -fsSL https://raw.githubusercontent.com/bigknoxy/HashPilot/main/scripts/install.sh | bash` — installs `hashpilot` CLI on PATH; injects the section below into `~/.claude/CLAUDE.md`.
|
|
3
|
+
|
|
4
|
+
## HashPilot Claude — Structured Editing Integration
|
|
5
|
+
Active at user scope; `hashpilot` is on PATH.
|
|
6
|
+
- **Preferred:** `claude mcp add hashpilot -- hashpilot mcp --stdio`. Tools mirror the CLI 1:1 (`rename_symbol`, `replace_hash`, `route_edit`, `verify_changes`, plus read/search tools); JSON avoids shell-quoting. Other hosts: `docs/INTEGRATION-MCP.md`. CLI below is the fallback when MCP is unavailable.
|
|
7
|
+
- **Use when:** editing existing files, renaming symbols, replacing function bodies, managing imports, batch reading, verifying changes. **Skip when:** creating/deleting/moving files, one-off edits, other filesystem ops — use direct Edit/Write.
|
|
8
|
+
- **Edit hierarchy** (top preferred): `hashpilot ast <subcommand>` (syntax-aware, best) → `hashpilot replace-hash` (hash-anchored, safe) → direct Edit/Write (fallback only).
|
|
9
|
+
- **Batched ops:** `/hashpilot-read <paths>` (→ `read-many`), `/hashpilot-search <pattern>` (→ `grep-many`), `/hashpilot-verify [files]` (→ `verify-changes`).
|
|
10
|
+
- **Introspection:** `hashpilot route <file> <op> [--policy <json>]`; `hashpilot config`.
|
|
11
|
+
- **Output** (apiVersion 1): `{ apiVersion, ok, command, data, error, warnings }` — payload in `data`; failures carry `error.code` (+`error.recovery` when actionable); `ok` matches exit code; `warnings` covers route fallbacks/relocated anchors/corrupt telemetry. See `docs/ADAPTER-CONTRACT.md`.
|
|
12
|
+
- **Status/control:** `/hashpilot-status`; `HASHPILOT_DISABLE=1` bypasses HashPilot entirely.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: HashPilot
|
|
3
|
+
description: Structured editing agent that uses HashPilot's hash-anchored and AST-aware editing to make precise, low-retry file changes. Prefers syntax-aware edits for TypeScript/TSX, hash-anchored edits for everything else, and verifies changes after editing.
|
|
4
|
+
model: opencode/big-pickle
|
|
5
|
+
small_model: github-copilot/gpt-5-mini
|
|
6
|
+
mode: subagent
|
|
7
|
+
temperature: 0.1
|
|
8
|
+
tools:
|
|
9
|
+
bash: true
|
|
10
|
+
write: true
|
|
11
|
+
edit: true
|
|
12
|
+
read: true
|
|
13
|
+
grep: true
|
|
14
|
+
glob: true
|
|
15
|
+
list: true
|
|
16
|
+
patch: true
|
|
17
|
+
todowrite: true
|
|
18
|
+
todoread: true
|
|
19
|
+
permissions:
|
|
20
|
+
edit: allow
|
|
21
|
+
bash: allow
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
You are the HashPilot editing agent. Make precise, minimal file edits using hashpilot commands. Never guess line numbers.
|
|
25
|
+
|
|
26
|
+
## When to use this agent (delegate here)
|
|
27
|
+
|
|
28
|
+
- Editing existing files in any language (TS/JS/Python/Go/Rust → AST; others → hash)
|
|
29
|
+
- Renaming symbols, replacing function bodies, or managing imports
|
|
30
|
+
- Making batch edits across multiple files
|
|
31
|
+
- When precision matters (avoiding line-counting errors)
|
|
32
|
+
- When you need stale-anchor detection (conflict safety)
|
|
33
|
+
|
|
34
|
+
## When NOT to use this agent (do it yourself)
|
|
35
|
+
|
|
36
|
+
- Creating new files → use write/edit directly
|
|
37
|
+
- Deleting files or directories → use bash
|
|
38
|
+
- Renaming/moving files → use bash
|
|
39
|
+
- Simple single-line edits in non-critical files → just edit directly
|
|
40
|
+
- Exploratory single-file reads → read directly (less overhead)
|
|
41
|
+
- File system operations (cp, mv, rm) → use bash
|
|
42
|
+
|
|
43
|
+
## Route hierarchy (always follow this order)
|
|
44
|
+
|
|
45
|
+
1. **AST** — symbol ops in .ts/.tsx/.js/.py/.go/.rs (rename, replace-body, add/remove-import, insert)
|
|
46
|
+
2. **Hash** — replace-hash for content changes in any file
|
|
47
|
+
3. **Diff** — search+replace fallback for unsupported cases
|
|
48
|
+
|
|
49
|
+
## Decision-first: determine your approach
|
|
50
|
+
|
|
51
|
+
**Q1: What file type?** .ts/.tsx/.js/.py/.go/.rs → `ast`. Everything else → `replace-hash`.
|
|
52
|
+
|
|
53
|
+
**Q2: Should I search first?** Need symbol defs? → `grep-many`. Need references? → `symbol-lookup-many`. Need structure? → `ast find-symbols`.
|
|
54
|
+
|
|
55
|
+
**Q3: How to read?** Single file → `read-many <file>`. Multiple → `read-many <f1> <f2> ...`. Targeted line → `read-hash <file> <line>`.
|
|
56
|
+
|
|
57
|
+
**Q4: How to edit?** Symbol ops → `ast <op> <file> <args>`. Content replace → `replace-hash <file> <hash> <new> [--range s:e]`. Fallback → diff.
|
|
58
|
+
|
|
59
|
+
**Q5: Verify?** Always: `verify-changes <files...> [--formatter] [--linter] [--test-filter]`.
|
|
60
|
+
|
|
61
|
+
## Workflows
|
|
62
|
+
|
|
63
|
+
### For TypeScript/TSX/JS/Python/Go/Rust (AST route)
|
|
64
|
+
1. `grep-many <pattern> <paths>` — find all references (skip if trivial)
|
|
65
|
+
2. `read-many <file>` — get hash for safety
|
|
66
|
+
3. `ast find-symbols <file>` — confirm exact symbol name
|
|
67
|
+
4. `ast <operation> <file> <args>` — make precise edit
|
|
68
|
+
5. `verify-changes <file>` — confirm correctness
|
|
69
|
+
|
|
70
|
+
### For all other files (hash route)
|
|
71
|
+
1. `read-many <file>` — get content hash
|
|
72
|
+
2. `replace-hash <file> <hash> <new-content> [--range s:e]` — edit
|
|
73
|
+
3. On `stale: true`: re-read, get fresh hash, retry step 2
|
|
74
|
+
4. `verify-changes <file>` — confirm
|
|
75
|
+
|
|
76
|
+
### Multi-file refactor
|
|
77
|
+
1. `grep-many <pattern> src/` — find all affected files
|
|
78
|
+
2. `read-many <file1> <file2> ...` — batch read all with hashes
|
|
79
|
+
3. Edit each file (ast or replace-hash per file type)
|
|
80
|
+
4. `verify-changes <file1> <file2> ...` — confirm all
|
|
81
|
+
|
|
82
|
+
## Error handling
|
|
83
|
+
|
|
84
|
+
| Error | Action |
|
|
85
|
+
|-------|--------|
|
|
86
|
+
| `stale: true` | Re-read file, get fresh hash, retry replace-hash |
|
|
87
|
+
| Symbol not found | Run `find-symbols` to verify; check spelling |
|
|
88
|
+
| Parse error | Fix syntax first, then retry AST operation |
|
|
89
|
+
| Verify failure | Fix errors, re-verify |
|
|
90
|
+
| Content appears N times | Provide more context to disambiguate |
|
|
91
|
+
|
|
92
|
+
## Anti-patterns (avoid these)
|
|
93
|
+
|
|
94
|
+
- ❌ Guess line numbers or hashes — always read first
|
|
95
|
+
- ❌ Ignore `stale: true` — re-read and retry
|
|
96
|
+
- ❌ Skip verify-changes — always confirm edits pass
|
|
97
|
+
- ❌ Use replace-hash when AST available — use AST for symbol ops
|
|
98
|
+
- ❌ Edit without understanding — use find-symbols or grep-many first
|
|
99
|
+
|
|
100
|
+
## Key principles
|
|
101
|
+
1. **Prefer AST** for symbol-level edits in supported languages
|
|
102
|
+
2. **Prefer hash** for content changes in any file
|
|
103
|
+
3. **Batch reads** — use read-many for multiple files at once
|
|
104
|
+
4. **Read before write** — always get current hash
|
|
105
|
+
5. **Verify after write** — always run verify-changes
|
|
106
|
+
6. **Recover gracefully** — stale → re-read, not-found → check symbols
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hashpilot
|
|
3
|
+
description: HashPilot structured editing core for coding agents. Provides hash-anchored editing (replace-hash), syntax-aware AST editing via tree-sitter (TypeScript, TSX, JavaScript, Python, Go, Rust), batched read/search, verification bundling, and telemetry. Use when editing files precisely to reduce retries and token waste.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HashPilot — Structured Editing for Coding Agents
|
|
7
|
+
|
|
8
|
+
## Install HashPilot
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
curl -fsSL https://raw.githubusercontent.com/bigknoxy/HashPilot/main/scripts/install.sh | bash
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This installs the `hashpilot` CLI and registers the OpenCode skill + subagent.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
HashPilot is a global, tool-agnostic structured editing system that improves coding-agent efficiency by preferring syntax-aware edits when possible, hash-anchored edits otherwise, and providing verification batching.
|
|
19
|
+
|
|
20
|
+
### Preferred: use the MCP server
|
|
21
|
+
|
|
22
|
+
If your host speaks MCP, register HashPilot once and call its tools directly
|
|
23
|
+
instead of shelling out:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{ "mcpServers": { "hashpilot": { "command": "hashpilot", "args": ["mcp", "--stdio"] } } }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The MCP tools mirror the CLI one-for-one (`rename_symbol`, `replace_hash`,
|
|
30
|
+
`route_edit`, `verify_changes`, plus the read and search tools), and multi-line
|
|
31
|
+
content with quotes or backticks rides inside JSON rather than through shell
|
|
32
|
+
quoting. Per-host setup lives in `docs/INTEGRATION-MCP.md`.
|
|
33
|
+
|
|
34
|
+
The CLI commands below remain fully supported and are the fallback when MCP is
|
|
35
|
+
not available.
|
|
36
|
+
|
|
37
|
+
## When to Use This Skill
|
|
38
|
+
|
|
39
|
+
- **Editing supported languages**: Use AST commands for precise symbol-level edits (TypeScript, TSX, JavaScript, Python, Go, Rust)
|
|
40
|
+
- **Editing any file with hash anchoring**: Use `replace-hash` to avoid line-counting errors
|
|
41
|
+
- **Reading multiple files**: Use `read-many` to batch reads with hashes
|
|
42
|
+
- **Searching across codebases**: Use `grep-many` for structured search results
|
|
43
|
+
- **Verifying changes**: Use `verify-changes` to bundle formatter + linter + tests
|
|
44
|
+
|
|
45
|
+
## When NOT to Use This Skill
|
|
46
|
+
|
|
47
|
+
- **Creating new files**: Use direct write/edit instead (hashpilot edits existing files)
|
|
48
|
+
- **Deleting files/directories**: Use raw bash (`rm`, `rmdir`)
|
|
49
|
+
- **Renaming/moving files**: Use raw bash (`mv`, `git mv`)
|
|
50
|
+
- **Simple single-line edits in non-critical files**: Direct edit is cheaper and just as safe
|
|
51
|
+
- **Exploratory single-file reads**: Raw read has no overhead
|
|
52
|
+
- **File system operations** (copy, move, delete, symlink): Use bash
|
|
53
|
+
|
|
54
|
+
## Prerequisites
|
|
55
|
+
|
|
56
|
+
HashPilot must be installed at `~/.agentic-tools/structured-editing/` with the CLI at `~/.agentic-tools/bin/hashpilot`.
|
|
57
|
+
|
|
58
|
+
Verify installation:
|
|
59
|
+
```bash
|
|
60
|
+
hashpilot --version
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If not installed, see `~/.agentic-tools/structured-editing/docs/INSTALL.md`.
|
|
64
|
+
|
|
65
|
+
## Routing Strategy
|
|
66
|
+
|
|
67
|
+
Always prefer the highest-confidence route:
|
|
68
|
+
|
|
69
|
+
1. **AST route** — For supported languages (TypeScript, TSX, JavaScript, Python, Go, Rust) with symbol-level operations (rename, replace-body, add/remove import, insert before/after)
|
|
70
|
+
2. **Hash route** — For all other edits where you have a content hash
|
|
71
|
+
3. **Diff route** — Fallback for unsupported operations
|
|
72
|
+
|
|
73
|
+
Check routing: `hashpilot route <file> <operation>`
|
|
74
|
+
|
|
75
|
+
## Core Commands
|
|
76
|
+
|
|
77
|
+
### read-many — Batch read files with hashes
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
hashpilot read-many <file1> [file2] ...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Returns the standard envelope; `data` is an array of `{ path, content, hash, lines }`.
|
|
84
|
+
Store `hash` for subsequent `replace-hash` calls.
|
|
85
|
+
|
|
86
|
+
**Usage pattern**: Read all relevant files at once, use hashes for editing.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Read multiple files
|
|
90
|
+
result=$(hashpilot read-many src/api.ts src/utils.ts src/config.ts)
|
|
91
|
+
# Extract hash for later editing
|
|
92
|
+
hash=$(echo "$result" | jq -r '.data[] | select(.path | contains("api.ts")) | .hash')
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### read-hash — Read line with context hash
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
hashpilot read-hash <file> <line-number> [-c <context-lines>]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Returns `lineHash`, `contextHash`, `contextBefore`, `contextAfter`. Use `contextHash` for anchoring edits to specific line ranges.
|
|
102
|
+
|
|
103
|
+
### grep-many — Search across paths
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
hashpilot grep-many <pattern> <paths...> [-i] [--file-pattern <glob>] [--max-results <n>]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### symbol-lookup-many — Find symbol definitions
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
hashpilot symbol-lookup-many <paths...> --names name1,name2
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### replace-hash — Hash-anchored content replacement
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
hashpilot replace-hash <file> <old-hash> <new-content> [--range start:end] [--dry-run]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Critical**: If result shows `"stale": true`, the file changed since the hash was computed. Re-read the file and retry with the new hash.
|
|
122
|
+
|
|
123
|
+
**Range format**: `--range 5:10` means lines 5 through 10 (1-indexed, end exclusive).
|
|
124
|
+
|
|
125
|
+
**File input**: Use `@filepath` to read new content from a file instead of inline.
|
|
126
|
+
|
|
127
|
+
## AST Commands (TypeScript, TSX, JavaScript, Python, Go, Rust)
|
|
128
|
+
|
|
129
|
+
### find-symbols — List all symbols
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
hashpilot ast find-symbols <file>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Returns array of `{name, kind, startRow, endRow, startCol, endCol}`.
|
|
136
|
+
|
|
137
|
+
### rename-symbol — Rename all references
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
hashpilot ast rename-symbol <file> <old-name> <new-name> [--dry-run]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### replace-body — Replace function/method body
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
hashpilot ast replace-body <file> <symbol-name> <new-body> [--dry-run]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Body can be `@filepath` to read from a file.
|
|
150
|
+
|
|
151
|
+
### add-import — Add import statement
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
hashpilot ast add-import <file> '<import-spec>' [--dry-run]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
- `hashpilot ast add-import src/app.ts '{ Router } from express'`
|
|
159
|
+
- `hashpilot ast add-import src/app.ts '* as React from react'`
|
|
160
|
+
|
|
161
|
+
### remove-import — Remove import line
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
hashpilot ast remove-import <file> '<import-spec>' [--dry-run]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### insert-before / insert-after — Insert content relative to a symbol
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
hashpilot ast insert-before <file> <symbol-name> <content> [--dry-run]
|
|
171
|
+
hashpilot ast insert-after <file> <symbol-name> <content> [--dry-run]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## verify-changes — Bundle formatter + linter + tests
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
hashpilot verify-changes <files...> [--formatter <cmd>] [--linter <cmd>] [--test-filter <pattern>]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Returns `{overall: "pass"|"fail"|"partial", formatter, linter, tests, fileHashes}`.
|
|
181
|
+
|
|
182
|
+
## Telemetry
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
hashpilot telemetry show [-n <limit>]
|
|
186
|
+
hashpilot telemetry summary
|
|
187
|
+
hashpilot telemetry clear
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Workflow Patterns
|
|
191
|
+
|
|
192
|
+
### Pattern 1: TypeScript symbol rename
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# 1. Find the symbol
|
|
196
|
+
hashpilot ast find-symbols src/api.ts
|
|
197
|
+
# 2. Rename
|
|
198
|
+
hashpilot ast rename-symbol src/api.ts oldName newName
|
|
199
|
+
# 3. Verify
|
|
200
|
+
hashpilot verify-changes src/api.ts --formatter prettier --linter eslint
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pattern 2: Hash-anchored edit (any file)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# 1. Read file with hash
|
|
207
|
+
data=$(hashpilot read-many config.yaml)
|
|
208
|
+
hash=$(echo "$data" | jq -r '.[0].hash')
|
|
209
|
+
# 2. Edit with hash anchor
|
|
210
|
+
hashpilot replace-hash config.yaml "$hash" "new: content"
|
|
211
|
+
# 3. On stale hash: re-read and retry
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Pattern 3: Batch read + selective edit
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# 1. Batch read
|
|
218
|
+
hashpilot read-many src/a.ts src/b.ts src/c.ts
|
|
219
|
+
# 2. Extract hash for target file
|
|
220
|
+
# 3. Replace targeted range
|
|
221
|
+
hashpilot replace-hash src/b.ts "$hash" "replacement content" --range 10:15
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Error Recovery
|
|
225
|
+
|
|
226
|
+
| Error | Cause | Recovery |
|
|
227
|
+
|-------|-------|----------|
|
|
228
|
+
| `stale: true` | File changed since hash computed | Re-read file, get new hash, retry |
|
|
229
|
+
| `success: false` | Symbol not found, parse error, etc. | Check file content, verify symbol name |
|
|
230
|
+
| Parse error | Invalid source code | Fix syntax errors first |
|
|
231
|
+
| `error` field | File not found, permissions | Check file path and permissions |
|
|
232
|
+
|
|
233
|
+
## Telemetry Event Schema
|
|
234
|
+
|
|
235
|
+
Every operation logs:
|
|
236
|
+
- `operation`: Command name
|
|
237
|
+
- `route`: `ast`, `hash`, `diff`, `read`, `grep`, `verify`
|
|
238
|
+
- `success`: Boolean
|
|
239
|
+
- `fallback_reason`: Why a lower route was chosen
|
|
240
|
+
- `elapsed_ms`: Duration
|
|
241
|
+
- `file`, `files_count`, `lines_read`: Scope info
|