@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +777 -0
  3. package/docs/ADAPTER-CONTRACT.md +1260 -0
  4. package/docs/ARCHITECTURE.md +846 -0
  5. package/docs/CLI-QUICKREF.md +827 -0
  6. package/docs/COMPETITIVE-ANALYSIS.md +307 -0
  7. package/docs/INSTALL.md +403 -0
  8. package/docs/INTEGRATION-CLAUDE.md +126 -0
  9. package/docs/INTEGRATION-MCP.md +196 -0
  10. package/docs/INTEGRATION-OPENCODE.md +136 -0
  11. package/docs/INTEGRATION-PI.md +195 -0
  12. package/package.json +77 -0
  13. package/scripts/build-site.sh +39 -0
  14. package/scripts/doctor.sh +218 -0
  15. package/scripts/gen-cli-quickref.ts +232 -0
  16. package/scripts/install-cli.sh +60 -0
  17. package/scripts/install.sh +466 -0
  18. package/scripts/roadmap-lint.ts +200 -0
  19. package/scripts/uninstall.sh +202 -0
  20. package/src/cli-node.cjs +51 -0
  21. package/src/cli.ts +209 -0
  22. package/src/commands/ast.ts +255 -0
  23. package/src/commands/diff.ts +98 -0
  24. package/src/commands/edit.ts +93 -0
  25. package/src/commands/hash.ts +64 -0
  26. package/src/commands/intent.ts +68 -0
  27. package/src/commands/maintenance.ts +191 -0
  28. package/src/commands/mcp.ts +28 -0
  29. package/src/commands/provenance.ts +111 -0
  30. package/src/commands/read.ts +117 -0
  31. package/src/commands/route.ts +42 -0
  32. package/src/commands/shared.ts +65 -0
  33. package/src/commands/telemetry.ts +126 -0
  34. package/src/commands/verify.ts +61 -0
  35. package/src/core/ast-edit.ts +2357 -0
  36. package/src/core/batch-edit.ts +185 -0
  37. package/src/core/config.ts +189 -0
  38. package/src/core/diff-engine.ts +474 -0
  39. package/src/core/doctor.ts +303 -0
  40. package/src/core/encoding.ts +116 -0
  41. package/src/core/envelope.ts +163 -0
  42. package/src/core/exit-codes.ts +198 -0
  43. package/src/core/format.ts +339 -0
  44. package/src/core/grep.ts +180 -0
  45. package/src/core/hash-edit.ts +416 -0
  46. package/src/core/index.ts +155 -0
  47. package/src/core/intent.ts +584 -0
  48. package/src/core/locking.ts +292 -0
  49. package/src/core/module-system.ts +142 -0
  50. package/src/core/operations.ts +557 -0
  51. package/src/core/output.ts +122 -0
  52. package/src/core/path-normalize.ts +61 -0
  53. package/src/core/paths.ts +326 -0
  54. package/src/core/plan-executor.ts +437 -0
  55. package/src/core/platform.ts +132 -0
  56. package/src/core/provenance.ts +214 -0
  57. package/src/core/read.ts +111 -0
  58. package/src/core/redact.ts +98 -0
  59. package/src/core/resolve-content.ts +12 -0
  60. package/src/core/router.ts +463 -0
  61. package/src/core/snapshot.ts +346 -0
  62. package/src/core/telemetry.ts +838 -0
  63. package/src/core/utils.ts +7 -0
  64. package/src/core/verify-baseline.ts +186 -0
  65. package/src/core/verify-scope.ts +282 -0
  66. package/src/core/verify.ts +753 -0
  67. package/src/mcp/server.ts +325 -0
  68. package/templates/claude-section.md +12 -0
  69. package/templates/opencode-agent.md +106 -0
  70. package/templates/opencode-skill.md +241 -0
  71. package/templates/pi-extension.ts +288 -0
  72. package/templates/pi-skill.md +123 -0
  73. 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