@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,1260 @@
1
+ # HashPilot — Adapter Contract
2
+
3
+ This document defines the machine-readable contract that coding agents use to interact with HashPilot. All commands are invoked via the `hashpilot` CLI and return JSON on stdout.
4
+
5
+ ## Response envelope (apiVersion 1)
6
+
7
+ Every command writes the same top-level shape. Schema: [`schema/hashpilot-envelope.schema.json`](../schema/hashpilot-envelope.schema.json).
8
+
9
+ ```json
10
+ {
11
+ "apiVersion": "1",
12
+ "ok": true,
13
+ "command": "telemetry show",
14
+ "data": { "...": "the per-command payload documented below" },
15
+ "error": null,
16
+ "warnings": []
17
+ }
18
+ ```
19
+
20
+ | Field | Meaning |
21
+ |-------|---------|
22
+ | `apiVersion` | Envelope version. `"1"` today; bumped only if the envelope's own shape breaks. |
23
+ | `ok` | True exactly when the exit code is 0. `ok` and `$?` never disagree — check either, not both. |
24
+ | `command` | Space-separated subcommand path, e.g. `"telemetry show"`. Over MCP it is the tool name, e.g. `"replace_hash"`. |
25
+ | `data` | The per-command payload. **Every example below shows what goes here, not the top level.** |
26
+ | `error` | `null` when `ok`; otherwise `{ code, message, recovery?, details? }`. `code` is an `ErrorCode` — branch on it, never on `message`. |
27
+ | `warnings` | Non-fatal notices, each `{ code, message, ... }`. Codes: `ROUTE_FALLBACK` (the edit was downgraded to a less safe route), `ANCHOR_RELOCATED` (the anchor moved; the edit landed elsewhere), `TELEMETRY_LOG_CORRUPT` (malformed log lines were skipped), `VERIFY_NO_CHECKS` (`verify-changes` ran no check at all, so nothing was verified). |
28
+
29
+ **Breaking change in v3.0.0 (#18, #56).** Through v2.x each command returned its own
30
+ shape at the top level — some a bare array, some an object — so an adapter had to
31
+ special-case the command it had just run and had no field to detect a contract change
32
+ with. Migration is mechanical: read `.data` where you used to read the root, and `.error.code`
33
+ where you used to read `.errorCode`.
34
+
35
+ Two commands keep a raw, unwrapped mode for piping into other tools:
36
+ `diff generate --raw` (the diff text alone) and `telemetry export --ndjson` (one compact
37
+ event per line). `--human` output on `provenance query` and `telemetry` is text, not JSON,
38
+ and is unaffected.
39
+
40
+ ## Parse-validity gate
41
+
42
+ Every AST operation refuses a file that does not already parse, and reparses its own
43
+ output before the write. Two failure shapes an adapter should expect:
44
+
45
+ | Situation | `error.code` | Exit | Meaning |
46
+ |-----------|--------------|------|---------|
47
+ | Input has a syntax error | `PARSE_ERROR` | 2 | No edit was attempted. The message carries `line:column` and the offending node. |
48
+ | Edit would corrupt a clean file | `PARSE_ERROR` | 2 | Nothing was written. The file on disk is unchanged. |
49
+
50
+ Neither is retryable by re-reading — fix the source, or pass the global
51
+ `--allow-parse-errors` flag to waive the *pre*-check when editing a knowingly broken
52
+ file. The post-edit check is never waived for AST edits, because it also catches bugs
53
+ in HashPilot's own offset arithmetic. `replace-hash` honors the flag for its
54
+ post-check, since a hash edit may legitimately be the thing that repairs a broken file.
55
+
56
+ Hash and diff edits get the post-check too, whenever a parser exists for the language.
57
+ Languages with no tree-sitter grammar (and `.d.ts`) are never gated.
58
+
59
+ There is no file-size limit on AST edits. Through v3.0.0 sources over 32KB threw
60
+ inside the tree-sitter binding and fell back to the diff route.
61
+
62
+ ## Atomic writes and undo
63
+
64
+ Every write is temp-file + `fsync` + `rename`, so a crash mid-edit leaves the original
65
+ file byte-identical rather than truncated. Before each write the original bytes are
66
+ snapshotted under `~/.agentic-tools/snapshots/`, keyed by a changeSet ID minted once
67
+ per CLI invocation.
68
+
69
+ | Command | Success shape | Failure |
70
+ |---------|---------------|---------|
71
+ | `changesets [--limit N]` | `data.changeSets: [{changeSetId, timestamp, files[]}]`, newest first | — |
72
+ | `undo <id>` / `undo --last` | `data: {success, changeSetId, files[], message}` | `HASH_MISMATCH`, exit 3, when a file changed after the edit |
73
+
74
+ `undo` never partially clobbers: a file that fails its check is left exactly as found
75
+ and reported in `data.files[]` with a `reason`. `--force` overrides the check;
76
+ `--dry-run` reports without touching disk. An undo is not itself snapshotted, so
77
+ `undo --last` cannot ping-pong between two states.
78
+
79
+ ## Output Format (B16)
80
+
81
+ The CLI supports two output modes controlled by the global `--format` flag:
82
+
83
+ ```
84
+ --format <json|text>
85
+ ```
86
+
87
+ | Precedence | Condition | Format |
88
+ |------------|-----------|--------|
89
+ | 1. explicit `--format <fmt>` | `hashpilot <cmd> --format text` | text |
90
+ | 2. `--json` (deprecated) | `hashpilot <cmd> --json` | json (emits stderr: `[deprecation]`) |
91
+ | 3. `$CI` truthy | CI runner environment | json |
92
+ | 4. stdout is a TTY | interactive shell | text |
93
+ | 5. default | piped/redirected | json |
94
+
95
+ **`--json` is a hidden deprecated alias for `--format json`.** The deprecation
96
+ notice is emitted once per invocation to stderr. It will be removed in the next
97
+ minor version.
98
+
99
+ **All error output is always JSON.** The `finish()` function only uses the text
100
+ renderer for successful results (`success !== false`). Error/usage envelopes
101
+ remain the canonical apiVersion 1 payload regardless of format mode, because an
102
+ agent parsing a non-zero exit code always expects structured data.
103
+
104
+ **Text renderers** are per-command. A command without a registered renderer falls
105
+ back to a compact key/value dump. Diagnostics, progress messages, and the
106
+ deprecation warning go to **stderr** — never stdout.
107
+
108
+ ## Command Reference
109
+
110
+ ### Configuration
111
+
112
+ HashPilot is configured via config files and environment variables, merged with the following priority (highest wins):
113
+
114
+ 1. `HASHPILOT_ROUTE_POLICY` env var (JSON string)
115
+ 2. CLI `--config <path>` override
116
+ 3. Project `.hashpilot.json` in current working directory
117
+ 4. Global `~/.config/hashpilot/config.json`
118
+ 5. Defaults (telemetry enabled, no route policy)
119
+
120
+ **Config file schema (`config.json` / `.hashpilot.json`):**
121
+ ```json
122
+ {
123
+ "routePolicy": {
124
+ "languageOverrides": { "python": "hash" },
125
+ "operationOverrides": { "add-import": "diff" },
126
+ "conflictResolution": "operation"
127
+ },
128
+ "telemetry": {
129
+ "enabled": true
130
+ }
131
+ }
132
+ ```
133
+
134
+ **`routePolicy.languageOverrides`** — Force a specific route (ast/hash/diff) for files matching a given language key (language ID for supported AST languages, file extension otherwise).
135
+
136
+ **`routePolicy.operationOverrides`** — Force a specific route for a given operation name (e.g., `"rename-symbol"`, `"add-import"`, `"replace-hash"`).
137
+
138
+ **`routePolicy.conflictResolution`** — When both language and operation overrides match: `"language"` (language wins), `"operation"` (operation wins, default), or `"strictest"` (lowest-precedence route wins: diff < hash < ast).
139
+
140
+ **`telemetry.enabled`** — Set to `false` to disable telemetry recording (default: `true`).
141
+
142
+ **Environment variables:**
143
+ - `HASHPILOT_ROUTE_POLICY` — JSON string overriding route policy. Example: `'{"languageOverrides":{"python":"hash"}}'`
144
+
145
+ ---
146
+
147
+ ### provenance tracking (optional on all write commands)
148
+
149
+ The following options are available on all write operations (`replace-hash`, `ast rename-symbol`, `ast replace-body`, `ast add-import`, `ast remove-import`, `ast insert-before`, `ast insert-after`, `diff apply`, `batch`):
150
+
151
+ | Option | Description |
152
+ |--------|-------------|
153
+ | `--actor <name>` | Agent identity for provenance tracking (e.g. `"claude-opus-4.7"`) |
154
+ | `--task-id <id>` | Task/issue reference (e.g. `"ISSUE-142"`, `"GH#123"`) |
155
+ | `--reason <text>` | Human-readable reason for the edit |
156
+
157
+ These are recorded alongside telemetry and queryable via `provenance query`.
158
+
159
+ ---
160
+
161
+ ### read-many
162
+
163
+ Read multiple files with content hashes.
164
+
165
+ **Invocation:**
166
+ ```
167
+ hashpilot read-many <file1> [file2] ...
168
+ ```
169
+
170
+ **Output:**
171
+ ```json
172
+ [
173
+ {
174
+ "path": "/abs/path/to/file.ts",
175
+ "content": "full file content",
176
+ "hash": "12-char-sha256-prefix",
177
+ "lines": 42,
178
+ "error": null
179
+ }
180
+ ]
181
+ ```
182
+
183
+ **Use case:** Batch file reads to minimize round trips. Use `hash` for subsequent `replace-hash` calls.
184
+
185
+ ---
186
+
187
+ ### read-hash
188
+
189
+ Read a specific line with its hash and surrounding context.
190
+
191
+ **Invocation:**
192
+ ```
193
+ hashpilot read-hash <file> <line-number> [-c <context-lines>]
194
+ ```
195
+
196
+ **Output:**
197
+ ```json
198
+ {
199
+ "path": "/abs/path/to/file.ts",
200
+ "line": 10,
201
+ "content": " const x = foo();",
202
+ "lineHash": "12-char-hash",
203
+ "contextHash": "12-char-hash",
204
+ "contextBefore": ["line 7", "line 8", "line 9"],
205
+ "contextAfter": ["line 11", "line 12", "line 13"],
206
+ "error": null
207
+ }
208
+ ```
209
+
210
+ **Use case:** Verify exact line content before editing. Use `contextHash` to anchor edits precisely.
211
+
212
+ Both hashes are 12 hex characters — the same width `replace-hash` computes, so a
213
+ hash returned by `read-hash` can be passed straight back as an anchor. (Through
214
+ v1.5.3 `lineHash` was 8 characters and every such round-trip failed with
215
+ `STALE_ANCHOR` — [#60](../../issues/60).)
216
+
217
+ ---
218
+
219
+ ### grep-many
220
+
221
+ Search a regex pattern across paths.
222
+
223
+ **Invocation:**
224
+ ```
225
+ hashpilot grep-many <pattern> <path1> [path2] ... [-i] [--file-pattern <glob>] [--max-results <n>]
226
+ ```
227
+
228
+ **Output:**
229
+ ```json
230
+ {
231
+ "pattern": "function\\s+\\w+",
232
+ "results": [
233
+ {
234
+ "path": "/abs/path/file.ts",
235
+ "line": 5,
236
+ "column": 1,
237
+ "content": "function hello() {",
238
+ "match": "function\\s+\\w+"
239
+ }
240
+ ],
241
+ "error": null,
242
+ "elapsed_ms": 12
243
+ }
244
+ ```
245
+
246
+ `content` is the matched line verbatim — nothing is stripped, including a line
247
+ that itself begins with `12:`. `column` is the 1-indexed offset of the match
248
+ within `content`, computed in process because grep does not report columns; it
249
+ falls back to `1` for a POSIX pattern JavaScript's regex engine cannot compile
250
+ ([#105](../../issues/105)). `match` echoes the pattern, not the matched text.
251
+
252
+ ---
253
+
254
+ ### symbol-lookup-many
255
+
256
+ Look up symbol definitions across paths.
257
+
258
+ **Invocation:**
259
+ ```
260
+ hashpilot symbol-lookup-many <path1> [path2] ... --names name1,name2
261
+ ```
262
+
263
+ **Output:**
264
+ ```json
265
+ [
266
+ {
267
+ "name": "hello",
268
+ "path": "/abs/path/file.ts",
269
+ "line": 5,
270
+ "kind": "function"
271
+ }
272
+ ]
273
+ ```
274
+
275
+ ---
276
+
277
+ ### replace-hash
278
+
279
+ Replace file content identified by hash anchor.
280
+
281
+ **Invocation:**
282
+ ```
283
+ hashpilot replace-hash <file> <old-hash> <new-content> [--range start:end] [--dry-run]
284
+ ```
285
+
286
+ - `<new-content>` can be `@filepath` to read from a file
287
+ - `--range` is 1-indexed, inclusive start and exclusive end
288
+ - Provenance options: `--actor`, `--task-id`, `--reason`
289
+
290
+ **Stale-anchor recovery (relocation only).** If the anchor hash no longer matches
291
+ the requested range, the tool tries to *relocate* the anchor: it slides a window
292
+ the same height as the range over the file and looks for content whose hash
293
+ equals `<old-hash>`.
294
+
295
+ - Exactly one match → the edit applies there. `stale: true`, `retries: 1`, and
296
+ `relocatedTo: {start, end}` reports where it landed.
297
+ - More than one match → `AMBIGUOUS_ANCHOR`. The file is not touched.
298
+ - No match → `STALE_ANCHOR`. The file is not touched.
299
+
300
+ **Breaking change:** recovery no longer applies to a whole-file anchor (no
301
+ `--range`). Previously a mismatch there caused `<new-content>` to replace the
302
+ entire file, silently discarding whatever changed since the read. That path now
303
+ fails with `STALE_ANCHOR` — re-read the file and retry with the fresh hash.
304
+
305
+ Recovery can be disabled with `--no-recovery` (or `recovery: "off"` in the API),
306
+ which turns any mismatch into an immediate `STALE_ANCHOR`.
307
+
308
+ **Which hash is `newHash`.** On success it is the hash of the **content that was
309
+ written** — the range, not the file — so `{newHash, newRange}` chains straight
310
+ into the next call's `{oldHash, range}` with no intervening read. It used to be
311
+ the whole-file hash on the success path while the `STALE_ANCHOR` paths returned
312
+ the range hash, so one field carried two incompatible meanings and the value an
313
+ agent naturally chained on could never match ([#101](../../issues/101)). The
314
+ whole-file hash after the edit is now `fileHash`, which is reported for
315
+ information and is **not** an anchor. On a whole-file edit (no `--range`) the
316
+ region is the file, so the two agree. A replacement with a different line count
317
+ moves the region, which is why `newRange` is returned alongside — reuse the
318
+ original range and the next edit anchors on the wrong lines.
319
+
320
+ **Range validation:** `--range` bounds must be integers, `1 <= start <= end`, and
321
+ `end` no greater than the file's last line. Anything else is `INVALID_ARGUMENT`
322
+ with no write attempted.
323
+
324
+ **Output (success):**
325
+ ```json
326
+ {
327
+ "path": "/abs/path/file.ts",
328
+ "success": true,
329
+ "oldHash": "abc123def456",
330
+ "newHash": "789ghi012jkl",
331
+ "fileHash": "fedcba987654",
332
+ "newRange": { "start": 10, "end": 12 },
333
+ "linesChanged": 3,
334
+ "stale": false,
335
+ "retries": 0,
336
+ "message": "Replaced 5 lines with 3 lines (range 10-15)",
337
+ "diff": "- 10 | old line\n+ 10 | new line\n 11 | unchanged"
338
+ }
339
+ ```
340
+
341
+ **Output (relocated):**
342
+ ```json
343
+ {
344
+ "path": "/abs/path/file.ts",
345
+ "success": true,
346
+ "oldHash": "abc123def456",
347
+ "newHash": "789ghi012jkl",
348
+ "fileHash": "fedcba987654",
349
+ "newRange": { "start": 12, "end": 14 },
350
+ "linesChanged": 3,
351
+ "stale": true,
352
+ "retries": 1,
353
+ "relocatedTo": { "start": 12, "end": 17 },
354
+ "message": "Replaced 5 lines with 3 lines (anchor relocated to 12-17)",
355
+ "diff": "- 12 | old line\n+ 12 | new line\n 13 | unchanged"
356
+ }
357
+ ```
358
+
359
+ **Output (anchor could not be relocated):**
360
+ ```json
361
+ {
362
+ "path": "/abs/path/file.ts",
363
+ "success": false,
364
+ "stale": true,
365
+ "retries": 0,
366
+ "errorCode": "STALE_ANCHOR",
367
+ "message": "Anchor abc123def456 no longer matches and could not be relocated. Re-read the file and retry."
368
+ }
369
+ ```
370
+
371
+ An agent seeing `STALE_ANCHOR` or `AMBIGUOUS_ANCHOR` (exit code `3`) should
372
+ re-read the file and retry rather than give up.
373
+
374
+ ---
375
+
376
+ ### ast capabilities
377
+
378
+ Show all supported AST languages, operations per language, and known limitations.
379
+
380
+ **Invocation:**
381
+ ```
382
+ hashpilot ast capabilities
383
+ ```
384
+
385
+ **Output:**
386
+ ```json
387
+ [
388
+ {
389
+ "lang": "go",
390
+ "extensions": [".go"],
391
+ "operations": ["find-symbols", "rename-symbol", "replace-body", "add-import", "remove-import", "insert-before", "insert-after"],
392
+ "limitations": ["add-import with no existing imports inserts after `package` clause"]
393
+ }
394
+ ]
395
+ ```
396
+
397
+ ---
398
+
399
+ ### ast find-symbols
400
+
401
+ List symbols in a file.
402
+
403
+ **Invocation:**
404
+ ```
405
+ hashpilot ast find-symbols <file>
406
+ ```
407
+
408
+ **Output:**
409
+ ```json
410
+ {
411
+ "symbols": [
412
+ {
413
+ "name": "hello",
414
+ "kind": "function_declaration",
415
+ "startRow": 0,
416
+ "endRow": 2,
417
+ "startCol": 0,
418
+ "endCol": 1,
419
+ "startLine": 1,
420
+ "endLine": 3,
421
+ "startColumn": 1,
422
+ "endColumn": 2
423
+ }
424
+ ],
425
+ "truncated": false
426
+ }
427
+ ```
428
+
429
+ `truncated` is `true` when the walk stopped at the shared runaway depth guard
430
+ (`MAX_AST_DEPTH`, 200) with subtrees unvisited, and the envelope carries a
431
+ matching `SEARCH_TRUNCATED` warning. An incomplete search never reports
432
+ `SYMBOL_NOT_FOUND`: `insert-parameter` returns `SEARCH_TRUNCATED` (exit 2)
433
+ instead, because "I stopped looking" is not "it is not there".
434
+
435
+ **Line and column indexing.** Two conventions are reported side by side:
436
+
437
+ | Fields | Base | Use |
438
+ |--------|------|-----|
439
+ | `startLine`, `endLine`, `startColumn`, `endColumn` | 1-indexed | **Prefer these.** They match the `range` accepted by the hash tier, the `line` argument to `read-hash`, and editor jump-to-line. |
440
+ | `startRow`, `endRow`, `startCol`, `endCol` | 0-indexed | Raw tree-sitter coordinates. Retained for backward compatibility. |
441
+
442
+ Passing a `startRow` where a `range` is expected targets the line **above** the
443
+ symbol. That is not always an error: if the neighbouring line's content hash
444
+ happens to match the anchor you supply, the edit applies silently to the wrong
445
+ line (#99).
446
+
447
+ ---
448
+
449
+ ### ast rename-symbol
450
+
451
+ Rename all references to a symbol.
452
+
453
+ **Invocation:**
454
+ ```
455
+ hashpilot ast rename-symbol <file> <old-name> <new-name> [--dry-run] [--include-source] [--actor <name>] [--task-id <id>] [--reason <text>]
456
+ ```
457
+
458
+ **Output:**
459
+ ```json
460
+ {
461
+ "success": true,
462
+ "path": "/abs/path/file.ts",
463
+ "operation": "rename-symbol",
464
+ "changes": 5,
465
+ "message": "Renamed 5 occurrences of 'oldName' to 'newName'"
466
+ }
467
+ ```
468
+
469
+ **Dry-run output** (every `ast` command, plus `route-edit` and `batch`): the same
470
+ object with a unified `diff` of the changed hunks and `sourceOmitted: true` in
471
+ place of `newSource`. Pass `--include-source` (MCP: `includeSource: true`) to get
472
+ `newSource` back instead ([#98](../../issues/98)).
473
+
474
+ ```json
475
+ {
476
+ "success": true,
477
+ "path": "/abs/path/file.ts",
478
+ "operation": "rename-symbol",
479
+ "changes": 5,
480
+ "message": "Renamed 5 occurrences of 'oldName' to 'newName'",
481
+ "diff": "--- a/file.ts\n+++ b/file.ts\n@@ -12,7 +12,7 @@\n- oldName();\n+ newName();\n",
482
+ "sourceOmitted": true
483
+ }
484
+ ```
485
+
486
+ ---
487
+
488
+ ### ast replace-body
489
+
490
+ Replace a function/method body.
491
+
492
+ **Invocation:**
493
+ ```
494
+ hashpilot ast replace-body <file> <symbol-name> <new-body> [--dry-run] [--include-source] [--actor <name>] [--task-id <id>] [--reason <text>]
495
+ ```
496
+
497
+ `<new-body>` can be `@filepath` to read from a file.
498
+
499
+ **Output:**
500
+ ```json
501
+ {
502
+ "success": true,
503
+ "path": "/abs/path/file.ts",
504
+ "operation": "replace-body",
505
+ "changes": 1,
506
+ "message": "Replaced body of 'myFunction'"
507
+ }
508
+ ```
509
+
510
+ ---
511
+
512
+ ### ast add-import
513
+
514
+ Add an import statement.
515
+
516
+ **Invocation:**
517
+ ```
518
+ hashpilot ast add-import <file> <import-spec> [--dry-run] [--include-source] [--actor <name>] [--task-id <id>] [--reason <text>]
519
+ ```
520
+
521
+ `<import-spec>` examples: `'{ Foo } from ./bar'`, `'* as React from react'`
522
+
523
+ ---
524
+
525
+ ### ast remove-import
526
+
527
+ Remove an import line.
528
+
529
+ **Invocation:**
530
+ ```
531
+ hashpilot ast remove-import <file> <import-spec> [--dry-run] [--include-source] [--actor <name>] [--task-id <id>] [--reason <text>]
532
+ ```
533
+
534
+ ---
535
+
536
+ ### ast insert-before / insert-after
537
+
538
+ Insert content before or after a named symbol.
539
+
540
+ **Invocation:**
541
+ ```
542
+ hashpilot ast insert-before <file> <symbol-name> <content> [--dry-run] [--include-source] [--actor <name>] [--task-id <id>] [--reason <text>]
543
+ hashpilot ast insert-after <file> <symbol-name> <content> [--dry-run] [--include-source] [--actor <name>] [--task-id <id>] [--reason <text>]
544
+ ```
545
+
546
+ **Anchor selection:** only statement- and declaration-level nodes anchor an
547
+ insertion (functions, classes, interfaces, type aliases, enums, methods, fields,
548
+ and the declaration a `const`/`type`/`var` declarator belongs to). A name that
549
+ resolves only to a parameter, an import specifier, a type parameter, or an
550
+ object key is refused with `SYMBOL_NOT_FOUND` and a message naming the node type
551
+ found — inserting there would splice a statement into an expression. A name that
552
+ resolves to more than one legal anchor is refused with `AMBIGUOUS_SYMBOL` and
553
+ every candidate listed as `<node-type> at line <n>`.
554
+
555
+ **Indentation:** inserted content lands on its own line, indented to match the
556
+ anchor. Multi-line content keeps its internal relative indentation.
557
+
558
+ ---
559
+
560
+ ### diff generate
561
+
562
+ Generate a unified diff between old and new content.
563
+
564
+ **Invocation:**
565
+ ```
566
+ hashpilot diff generate <file> <old-content> <new-content> [-c <context-lines>]
567
+ ```
568
+
569
+ `<old-content>` and `<new-content>` can be `@filepath` to read from files.
570
+
571
+ **Output:** Unified diff text (not JSON). Prints `"(no changes)"` if inputs are identical.
572
+
573
+ ---
574
+
575
+ ### diff apply
576
+
577
+ Apply a unified diff patch to a file.
578
+
579
+ **Invocation:**
580
+ ```
581
+ hashpilot diff apply <file> [--patch <file>] [--dry-run] [-f <fuzzy>] [--actor <name>] [--task-id <id>] [--reason <text>]
582
+ ```
583
+
584
+ - `--patch <file>` — patch file to apply (use `-` for stdin)
585
+ - `-f, --fuzzy <n>` — fuzzy match tolerance (default 3)
586
+
587
+ **Output:**
588
+ ```json
589
+ {
590
+ "success": true,
591
+ "hunksApplied": 1,
592
+ "hunksFailed": 0,
593
+ "message": "Applied 1 hunk(s)",
594
+ "newSource": "...",
595
+ "placements": [{ "expectedAt": 12, "appliedAt": 12, "offset": 0 }],
596
+ "fuzzyPlacements": []
597
+ }
598
+ ```
599
+
600
+ `placements` carries one entry per applied hunk, in patch order: `expectedAt` is the
601
+ 1-indexed line the patch recorded (adjusted for earlier hunks), `appliedAt` is where it
602
+ landed, and `offset` is the difference. `fuzzyPlacements` is the subset with a non-zero
603
+ `offset` — the hunks that slid — and when it is non-empty `message` says so.
604
+
605
+ If the hunk context matches more than once inside the `--fuzzy` window, the patch is
606
+ refused (`success: false`, no write) with a message naming every candidate line.
607
+
608
+ ---
609
+
610
+ ### verify-changes
611
+
612
+ Run formatter, linter, typechecker, and tests on changed files. Supports auto-detection from project config files.
613
+
614
+ **Invocation:**
615
+ ```
616
+ hashpilot verify-changes <file1> [file2] ... [--formatter <cmd>] [--linter <cmd>] [--typecheck <cmd>] [--test-filter <pattern>] [--test-runner <runner>] [--auto-detect] [--no-scope-tests] [--revert-on-failure] [--timeout <ms>] [--use-baseline] [--record-baseline] [--formatter-args ...] [--linter-args ...] [--test-args ...]
617
+ ```
618
+
619
+ **Options:**
620
+ - `--formatter <cmd>` — formatter command (e.g. `prettier --write`)
621
+ - `--linter <cmd>` — linter command (e.g. `eslint`, `biome lint`)
622
+ - `--typecheck <cmd>` — type checker command (e.g. `tsc --noEmit`)
623
+ - `--test-filter <pattern>` — filter tests by name pattern
624
+ - `--test-runner <runner>` — explicit test runner (`bun test`, `vitest`, `jest`, `pytest`, `go test`, `cargo test`)
625
+ - `--auto-detect` — auto-detect tools from `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`
626
+ - `--no-scope-tests` — run the whole test suite instead of only the tests related to the changed files (default: scoped to the changed files)
627
+ - `--revert-on-failure` — restore originals if any check *fails* (a timeout never triggers a revert)
628
+ - `--timeout <ms>` — per-check timeout (default 30000)
629
+ - `--use-baseline` — subtract a pre-edit baseline so only tests this edit *newly* broke count; requires an earlier `--record-baseline` at the same commit
630
+ - `--record-baseline` — record which tests currently fail and return **without running verification** — run it *before* editing so `--use-baseline` can later subtract pre-existing failures
631
+
632
+ **Output:**
633
+ ```json
634
+ {
635
+ "files": ["/abs/path/file.ts"],
636
+ "formatter": { "passed": true, "output": "..." },
637
+ "linter": { "passed": true, "output": "..." },
638
+ "tests": { "passed": true, "output": "...", "timedOut": false, "truncated": false },
639
+ "typecheck": { "passed": true, "output": "..." },
640
+ "overall": "pass",
641
+ "checksRun": ["formatter", "linter", "typecheck", "tests"],
642
+ "testScope": { "cmd": "bun test", "args": ["/abs/path/file.test.ts"], "scoped": true, "reason": "bun test restricted to 1 related test file(s)" },
643
+ "baseline": { "source": "cache", "comparable": true, "preExisting": [], "newFailures": [], "reason": "all failures were already failing at \u2026" },
644
+ "elapsed_ms": 120,
645
+ "fileHashes": { "/abs/path/file.ts": "abc123def456" },
646
+ "detected": { "formatter": "prettier --write", "testRunner": "vitest" },
647
+ "revertedFiles": ["/abs/path/file.ts"]
648
+ }
649
+ ```
650
+
651
+ `overall` is `"pass"`, `"fail"`, `"timeout"`, or `"skipped"`. `"timeout"` means a check
652
+ hit its `--timeout` without reaching a verdict; it is *not* a
653
+ failure and never triggers `--revert-on-failure` — a slow suite must
654
+ not destroy correct work.
655
+
656
+ `"skipped"` means **no check ran at all**, so nothing was verified. Every check
657
+ is opt-in, so a call that names none (and does not pass `--auto-detect`) reaches
658
+ this state; it used to report `"pass"`, because "all checks passed" is vacuously
659
+ true over an empty check set ([#106](../../issues/106)). It carries
660
+ `errorCode: "VERIFY_NO_CHECKS"` (exit `4`), a `VERIFY_NO_CHECKS` warning naming
661
+ the recovery, and never triggers `--revert-on-failure`. `checksRun` lists which
662
+ checks actually ran and is present on every result, so coverage never has to be
663
+ inferred from which optional keys happen to be absent.
664
+
665
+ Per-run fields: `timedOut` (which checks timed out, present only
666
+ when `overall` is `"timeout"`), `errorCode` (`VERIFY_TIMEOUT` on a
667
+ timeout, `VERIFY_NO_CHECKS` when nothing ran, `VERIFY_FAILED` on a real failure, `undefined` on a pass
668
+ — both failures map to exit code `4`), `testScope` (how the test run
669
+ was scoped; `scoped: false` means the whole suite ran and why), and
670
+ `baseline` (the `--use-baseline` comparison: `comparable: false` means
671
+ no usable baseline existed, so every failure counts; `newFailures`
672
+ is the list that flips the verdict). Each tool object may also carry
673
+ `timedOut` and `truncated` (captured output hit the 256 KB cap). When
674
+ `--revert-on-failure` is set, `revertedFiles` lists files restored to
675
+ their original state; it is absent on a pass and on a timeout.
676
+
677
+ **`--record-baseline` output** (a different shape — no checks are run):
678
+ ```json
679
+ {
680
+ "recorded": true,
681
+ "reason": "recorded 1 pre-existing failure(s) at a1b2c3d4",
682
+ "commit": "a1b2c3d4...",
683
+ "runner": "bun test",
684
+ "failures": ["hp_preexisting"],
685
+ "cached": false
686
+ }
687
+ ```
688
+
689
+ `recorded: false` with `cached: true` means a baseline already exists for
690
+ this commit + runner + test scope and was left alone. `recorded: false`
691
+ without `cached` means no baseline could be taken — no git repo, no test
692
+ runner, or the baseline run itself timed out. A timed-out baseline is
693
+ never written: it would record "nothing was failing" and then mark every
694
+ real pre-existing failure as new. `failures: null` means the runner's
695
+ output could not be parsed into test names, which makes later
696
+ comparisons `comparable: false` rather than wrong.
697
+
698
+ ---
699
+
700
+ ### route-edit
701
+
702
+ Auto-routed structured edit through AST → Hash → Diff pipeline. One command dispatches to the best available route.
703
+
704
+ **Invocation:**
705
+ ```
706
+ hashpilot route-edit <file> <operation> [options...]
707
+ ```
708
+
709
+ **Operations:** `rename-symbol`, `replace-body`, `add-import`, `remove-import`, `insert-before`, `insert-after`, `replace-hash`, `replace-content`
710
+
711
+ **Key options:**
712
+ - `--method <route>` — force a specific route (`ast`, `hash`, `diff`)
713
+ - `--policy <json>` — inline RoutePolicy JSON for testing
714
+ - `--dry-run` — preview without writing. The result carries a unified `diff` of the changed hunks and `sourceOmitted: true`, **not** the whole post-edit file ([#98](../../issues/98)): a preview exists so a caller can decide whether to commit the edit, and dumping the file made deciding cost more context than editing.
715
+ - `--include-source` — on a dry run, return the full post-edit text as `newSource` instead of the diff. Costs one whole file of context; use it only when you genuinely need the text.
716
+ - All provenance options: `--actor`, `--task-id`, `--reason`
717
+ - AST-specific: `--symbol`, `--old-name`, `--new-name`, `--new-body`, `--import-spec`, `--content`
718
+ - Hash-specific: `--old-hash`, `--new-content`, `--range`
719
+ - Diff-specific: `--old-content`, `--new-content`
720
+
721
+ **Output:** Same as the underlying route operation.
722
+
723
+ ---
724
+
725
+ ### batch
726
+
727
+ Apply the same edit to multiple files in parallel (or serial with `--serial`).
728
+
729
+ **Invocation:**
730
+ ```
731
+ hashpilot batch <operation> <files...> [options...]
732
+ ```
733
+
734
+ Accepts the same options as `route-edit`, plus `--serial` for sequential execution.
735
+
736
+ **Output:**
737
+ ```json
738
+ {
739
+ "results": [ ... ],
740
+ "summary": {
741
+ "total": 5,
742
+ "succeeded": 3,
743
+ "failed": 1,
744
+ "conflicts": 1,
745
+ "elapsed_ms": 1234
746
+ }
747
+ }
748
+ ```
749
+
750
+ `conflicts` counts files that failed with a stale-anchor or lock conflict — a
751
+ concurrent writer landed, so the edit is **retryable after re-reading the file**.
752
+ These are counted separately and are *not* included in `failed`, which covers
753
+ non-retryable errors. `total == succeeded + failed + conflicts`. Top-level
754
+ `success` is true only when both `failed` and `conflicts` are zero, so an adapter
755
+ that only checks `failed` will report success on a batch that partly conflicted.
756
+
757
+ When the batch cannot acquire the advisory locks for its files up front, every
758
+ entry in `results` is reported with `"route": null`, `"routeReason": "lock
759
+ timeout"`, and `errorCode: "LOCK_TIMEOUT"` — no route was ever chosen, so there
760
+ is no route name to report. These entries carry `"stale": true` and count toward
761
+ `conflicts`, not `failed`. `route` is a string on every other path; an adapter
762
+ that indexes route names must tolerate `null` here.
763
+
764
+ ---
765
+
766
+ ### intent
767
+
768
+ Execute an editing intent — one command, full blast radius. Parses a structured intent, discovers symbol definitions and references, generates an edit plan, and executes it.
769
+
770
+ **Invocation:**
771
+ ```
772
+ hashpilot intent '<json>' [--project-root <dir>] [--dry-run] [--yes] [--no-verify] [--no-revert] [--timeout <ms>] [--actor <name>] [--task-id <id>] [--reason <text>] [--context <text>]
773
+ ```
774
+
775
+ **Intent format (JSON):**
776
+ ```json
777
+ {"operation":"add-parameter","symbol":"myFunction","param":{"name":"x","type":"string","default":"\"hello\""}}
778
+ ```
779
+
780
+ **Supported operations:** `add-parameter`, `rename-exported-symbol`
781
+
782
+ `remove-parameter` is **not implemented** and is rejected with
783
+ `UNSUPPORTED_OPERATION` (exit code `1`). It was previously accepted but produced
784
+ a plan whose call-site steps searched for a literal `/* TODO: remove arg */`
785
+ string that never matches. Remove a parameter with `ast replace-body` on the
786
+ signature plus `diff apply` at each call site.
787
+
788
+ **Output:**
789
+ ```json
790
+ {
791
+ "success": true,
792
+ "plan": { "intent": {...}, "definition": {...}, "impactSummary": "...", "unresolved": [], "reconciliation": { "resolved": 3, "unresolved": 0, "ambiguous": 0 } },
793
+ "execution": { "steps": [...], "summary": {...}, "verification": {...} }
794
+ }
795
+ ```
796
+
797
+ **Rollback outcome (`execution.reverted`, `execution.unrevertedFiles`)**
798
+
799
+ A plan is rolled back when any step fails **or** verification fails. Two fields
800
+ report how that went, and an agent must read both:
801
+
802
+ | Fields | Meaning | What to do |
803
+ |--------|---------|------------|
804
+ | `reverted: false`, no `unrevertedFiles` | No rollback was needed or requested | Nothing |
805
+ | `reverted: true` | Every impacted file was restored to its pre-plan content | Safe to retry the plan |
806
+ | `reverted: false` + `unrevertedFiles: [...]` | **The rollback itself failed.** The listed files still hold edits that were supposed to be undone | **Stop.** Restore those files before any retry. Exit code is `5` with `errorCode: ROLLBACK_INCOMPLETE` |
807
+
808
+ `reverted: true` is never reported over a partial restore — if even one file
809
+ could not be written back it appears in `unrevertedFiles` and `reverted` is
810
+ `false`.
811
+
812
+ **Why the rollback happened (`execution.revertReason`).**
813
+ When `reverted: true`, a new field says *why* — one of:
814
+
815
+ | `revertReason` | Meaning |
816
+ |----------------|---------|
817
+ | `"verification-failed"` | Every step applied, but a check reported `overall: "fail"` |
818
+ | `"step-failed"` | An edit could not be applied, so a step failed |
819
+
820
+ It is **absent whenever nothing was reverted** (`reverted: false`). This is the
821
+ core of [#10](../../issues/10) (B13): the result used to say *that* a plan was
822
+ reverted but not *why*, so an agent could not distinguish a red verification
823
+ (fix the failing check and retry) from a broken plan (a step could not apply). A
824
+ verification **timeout** is its own verdict — `overall: "timeout"`, exit `4`,
825
+ `errorCode: VERIFY_TIMEOUT` — and **never reverts the edit, so it yields no
826
+ `revertReason`**.
827
+
828
+ **Verification is skipped when a step fails.** The tree is half-applied at that
829
+ point, so a suite run over it would report failures caused by the incomplete
830
+ edit rather than by the change itself. `verification` is then absent and the
831
+ exit code is `2` (edit failed), not `4`.
832
+
833
+ **Partial plans (`plan.unresolved`)**
834
+
835
+ The planner never invents source text. When part of an intent cannot be
836
+ computed — `add-parameter` with no `param.default`, so there is no argument to
837
+ pass at the call sites — it reports the gap instead of writing a placeholder
838
+ comment into your files ([#16](../../issues/16)):
839
+
840
+ ```json
841
+ {
842
+ "file": "/abs/path/app.py",
843
+ "operation": "insert-call-arg",
844
+ "reason": "no default given for 'flag', so the argument to pass at each call site cannot be computed",
845
+ "resolution": "Re-run with \"param\": {\"name\": \"flag\", \"default\": \"<value>\"}, or edit the call sites in app.py yourself with `diff apply`."
846
+ }
847
+ ```
848
+
849
+ A plan with a non-empty `unresolved` is **refused rather than half-applied**:
850
+ `error.code` is `UNSUPPORTED_OPERATION`, exit code `1`, and nothing is written.
851
+ Supply `param.default` (the fix in almost every case) or pass `--yes` to apply
852
+ only the steps that could be computed — the unresolved call sites stay
853
+ untouched and are still listed in `plan.unresolved`.
854
+
855
+ **Reference reconciliation (#15)**
856
+
857
+ Reference discovery was upgraded from regex `grep -w` + heuristic `isDefinitionLine` to per-language tree-sitter queries. The `plan` object now carries an optional `reconciliation` field that reports *what* reference discovery could and could not see:
858
+
859
+ ```json
860
+ "reconciliation": { "resolved": 3, "unresolved": 1, "ambiguous": 0 }
861
+ ```
862
+
863
+ | Field | Meaning |
864
+ |-------|---------|
865
+ | `resolved` | Count of genuine call/reference sites found in files HashPilot parses |
866
+ | `unresolved` | Count of files that mention the target symbol but are in a language HashPilot does **not** parse (e.g. `*.rb`). Each contributes one entry to `plan.unresolved` |
867
+ | `ambiguous` | Count of files that both reference and *bind* the target name more than once — HashPilot cannot tell which module's symbol. Each contributes one entry to `plan.unresolved` |
868
+
869
+ When `unresolved` or `ambiguous` > 0 the plan is **refused** via the same `plan.unresolved` guard as partial plans. Pass `--yes` to proceed with only the resolved references; the unparSED/ambiguous files are listed but not touched. `reconciliation` is absent when `generatePlan` is called without it.
870
+
871
+ ---
872
+
873
+ ### provenance query
874
+
875
+ Show edit history for a file — like `git blame` for agent edits.
876
+
877
+ **Invocation:**
878
+ ```
879
+ hashpilot provenance query <file> [<line-number>] [--human] [--fuzzy] [--limit <n>]
880
+ ```
881
+
882
+ - `--human` — human-readable table format
883
+ - `--fuzzy` — include edits without diff data in line-filtered queries
884
+
885
+ **Output (JSON):**
886
+ ```json
887
+ [
888
+ {
889
+ "timestamp": "2026-05-19T00:00:00.000Z",
890
+ "actor": "agent-name",
891
+ "taskId": "ISSUE-142",
892
+ "reason": "Rename function per spec",
893
+ "operation": "rename-symbol",
894
+ "route": "ast",
895
+ "success": true,
896
+ "diff": "@@ -10,3 +10,3 @@\n-oldFunc\n+newFunc"
897
+ }
898
+ ]
899
+ ```
900
+
901
+ ---
902
+
903
+ ### provenance changeset
904
+
905
+ Show all edits belonging to a changeSet (multi-step edit group).
906
+
907
+ **Invocation:**
908
+ ```
909
+ hashpilot provenance changeset <changeSetId> [--human]
910
+ ```
911
+
912
+ ---
913
+
914
+ ### telemetry
915
+
916
+ View or manage telemetry.
917
+
918
+ **Invocation:**
919
+ ```
920
+ hashpilot telemetry show [-n <limit>]
921
+ hashpilot telemetry summary
922
+ hashpilot telemetry health [-w <days>] [--trend]
923
+ hashpilot telemetry sessions
924
+ hashpilot telemetry export [--from <date>] [--to <date>] [--session <id>]
925
+ hashpilot telemetry prune [--older-than <days>]
926
+ hashpilot telemetry clear
927
+ ```
928
+
929
+ **`telemetry show`** — Show recent telemetry events (default 20).
930
+
931
+ **`telemetry summary`** — Aggregate counts by route:operation with success rate and average timing.
932
+
933
+ **`telemetry sessions`** — List session-level summaries (event count, error rate, duration).
934
+
935
+ **`telemetry export`** — Export events as NDJSON with optional date range or session ID filter.
936
+
937
+ **`telemetry prune`** — Delete rotated telemetry files older than N days (default 30).
938
+
939
+ **Event schema:**
940
+
941
+ ### route
942
+
943
+ Show which edit route would be chosen, with detailed explanation including policy matches.
944
+
945
+ **Invocation:**
946
+ ```
947
+ hashpilot route <file> <operation> [--policy <json>] [--no-default-config]
948
+ ```
949
+
950
+ **`--policy <json>`** — inline policy JSON for testing override behavior.
951
+
952
+ **`--no-default-config`** — ignore config file policies.
953
+
954
+ **Output:**
955
+ ```json
956
+ {
957
+ "file": "src/foo.ts",
958
+ "operation": "rename-symbol",
959
+ "language": "typescript",
960
+ "route": "ast",
961
+ "explanation": {
962
+ "route": "ast",
963
+ "reasons": ["Language 'typescript' supports AST operations"],
964
+ "policyApplied": false
965
+ }
966
+ }
967
+ ```
968
+
969
+ **Output with policy override:**
970
+ ```json
971
+ {
972
+ "file": "src/foo.py",
973
+ "operation": "rename-symbol",
974
+ "language": "python",
975
+ "route": "hash",
976
+ "explanation": {
977
+ "route": "hash",
978
+ "reasons": ["Policy language override for 'python' forces route 'hash'"],
979
+ "policyApplied": true,
980
+ "policySource": "language"
981
+ }
982
+ }
983
+ ```
984
+
985
+ ---
986
+
987
+ ### config
988
+
989
+ Show the current HashPilot configuration after merging global, project, CLI, and env overrides.
990
+
991
+ **Invocation:**
992
+ ```
993
+ hashpilot config [--config <path>]
994
+ ```
995
+
996
+ **Output:**
997
+ ```json
998
+ {
999
+ "routePolicy": {
1000
+ "languageOverrides": { "python": "hash" },
1001
+ "operationOverrides": { "add-import": "diff" }
1002
+ },
1003
+ "telemetry": { "enabled": true }
1004
+ }
1005
+ ```
1006
+
1007
+ ---
1008
+
1009
+ ### doctor
1010
+
1011
+ Verify the full user-scope HashPilot installation. Checks core files, CLI on PATH, config, and all adapter integrations.
1012
+
1013
+ **Invocation:**
1014
+ ```
1015
+ hashpilot doctor # human-readable summary (default)
1016
+ hashpilot doctor --json # machine-readable JSON envelope
1017
+ ```
1018
+
1019
+ **Output (JSON, with `--json`):**
1020
+ ```json
1021
+ {
1022
+ "checks": [
1023
+ { "name": "core-directory", "status": "pass", "message": "Found: /home/user/.agentic-tools/structured-editing" },
1024
+ { "name": "cli-executable", "status": "pass", "message": "CLI works: 0.1.0" },
1025
+ { "name": "claude-integration", "status": "pass", "message": "HashPilot section found in CLAUDE.md" },
1026
+ { "name": "config-file", "status": "skip", "message": "No config file — using defaults" }
1027
+ ],
1028
+ "healthy": true,
1029
+ "timestamp": "2026-04-26T00:00:00.000Z",
1030
+ "version": "0.1.0"
1031
+ }
1032
+ ```
1033
+
1034
+ **Status values:**
1035
+ - `pass` — check passed
1036
+ - `fail` — action required
1037
+ - `warn` — non-blocking issue
1038
+ - `skip` — component not applicable
1039
+
1040
+ **Exit code:** always `0`. `doctor` reports installation state — read `healthy` and the per-check `status` values from the JSON (or the human summary) rather than the exit code. A non-healthy report still exits 0 so a doctor run never fails a build for reporting an incomplete install.
1041
+
1042
+ A standalone version is also available: `scripts/doctor.sh` (works without CLI on PATH).
1043
+
1044
+ ---
1045
+
1046
+ ### upgrade
1047
+
1048
+ Upgrade HashPilot to the latest version from GitHub. Downloads and runs `scripts/install.sh` from the specified release channel.
1049
+
1050
+ **Invocation:**
1051
+ ```
1052
+ hashpilot upgrade [--channel <channel>] [--target <dir>] [--keep-telemetry] [--force] [--dry-run]
1053
+ ```
1054
+
1055
+ | Flag | Meaning |
1056
+ |------|---------|
1057
+ | `--channel <channel>` | Release channel (default: `main`) |
1058
+ | `--target <dir>` | Install target directory (default: `~/.agentic-tools`) |
1059
+ | `--keep-telemetry` | Preserve existing telemetry on upgrade |
1060
+ | `--force` | Skip confirmation prompt |
1061
+ | `--dry-run` | Show what would be done without executing |
1062
+
1063
+ **Exit code:** `0` on success, `70` on failure.
1064
+
1065
+ ### uninstall
1066
+
1067
+ Remove HashPilot and all its components from the system. Downloads and runs `scripts/uninstall.sh`.
1068
+
1069
+ **Invocation:**
1070
+ ```
1071
+ hashpilot uninstall [--keep-config] [--force] [--dry-run] [--target <dir>]
1072
+ ```
1073
+
1074
+ | Flag | Meaning |
1075
+ |------|---------|
1076
+ | `--keep-config` | Preserve config and telemetry data |
1077
+ | `--force` | Skip confirmation prompt (auto-detected when piped) |
1078
+ | `--dry-run` | Show what would be removed without deleting anything |
1079
+ | `--target <dir>` | Install target directory (default: `~/.agentic-tools`) |
1080
+
1081
+ **Output (dry-run):** JSON object with `components` array listing what would be removed (or preserved with `--keep-config`).
1082
+
1083
+ **Exit code:** `0` on success, `70` on failure.
1084
+
1085
+
1086
+ ### telemetry
1087
+
1088
+ View or manage telemetry.
1089
+
1090
+ **Invocation:**
1091
+ ```
1092
+ hashpilot telemetry show [-n <limit>]
1093
+ hashpilot telemetry summary
1094
+ hashpilot telemetry clear
1095
+ ```
1096
+
1097
+ **Event schema:**
1098
+ ```json
1099
+ {
1100
+ "timestamp": "2025-01-01T00:00:00.000Z",
1101
+ "operation": "replace-hash",
1102
+ "route": "hash",
1103
+ "file": "/abs/path/file.ts",
1104
+ "files_count": 1,
1105
+ "language": "typescript",
1106
+ "success": true,
1107
+ "fallback_reason": null,
1108
+ "retries": 0,
1109
+ "verification_result": "pass",
1110
+ "elapsed_ms": 5
1111
+ }
1112
+ ```
1113
+
1114
+ **Fields added in Phase 7:**
1115
+ - `language` — detected language for AST/hash operations (e.g., `"typescript"`, `"python"`, `"go"`)
1116
+ - `retries` — number of auto-retries performed (1 if auto-recovered from stale anchor, 0 otherwise)
1117
+
1118
+ ### telemetry health
1119
+
1120
+ Show an operational health report with per-language stats, failure breakdowns, and threshold warnings.
1121
+
1122
+ **Invocation:**
1123
+ ```
1124
+ hashpilot telemetry health [-w <days>] [--trend]
1125
+ ```
1126
+
1127
+ - `-w, --window <days>` — time window in days (default 7)
1128
+ - `-t, --trend` — compare current window to the previous window of the same length, reporting deltas and regressions
1129
+
1130
+ Default window is 7 days.
1131
+
1132
+ **Output:**
1133
+ ```json
1134
+ {
1135
+ "totalEvents": 203,
1136
+ "windowDays": 7,
1137
+ "routeDistribution": {
1138
+ "ast": { "count": 93, "success": 82 },
1139
+ "verify": { "count": 72, "success": 50 },
1140
+ "read": { "count": 19, "success": 19 },
1141
+ "hash": { "count": 19, "success": 14 }
1142
+ },
1143
+ "fallbackFrequency": { "stale-anchor": 5 },
1144
+ "staleAnchors": { "total": 6, "recovered": 1, "failed": 5 },
1145
+ "perLanguage": {
1146
+ "rust": { "operations": 21, "failures": 5 },
1147
+ "python": { "operations": 10, "failures": 1 }
1148
+ },
1149
+ "verifyFailures": { "total": 22, "byCheck": { "formatter": 6 } },
1150
+ "topFallbackCauses": [{ "reason": "stale-anchor", "count": 5 }],
1151
+ "diskBytes": 4823194,
1152
+ "warnings": [
1153
+ "Stale-anchor rate 43% exceeds threshold of 10%"
1154
+ ]
1155
+ }
1156
+ ```
1157
+
1158
+ **Thresholds** (trigger `warnings` when exceeded):
1159
+ - Stale-anchor rate > 10% of replace-hash calls
1160
+ - Fallback-to-diff rate > 10% of all events
1161
+ - Verify-changes failure rate > 20%
1162
+ - Per-language failure rate > 30% (when >= 3 operations)
1163
+ - Telemetry store on disk > 100 MB
1164
+
1165
+ `diskBytes` is the total size of the telemetry store — the active log, every rotated log, and the payload objects. It is a point-in-time property of the store, so on `--trend` output it is populated on `current` and always `0` on `previous`.
1166
+
1167
+ ### telemetry health --trend
1168
+
1169
+ Compare the current window against the previous window of the same length.
1170
+
1171
+ **Output:**
1172
+ ```json
1173
+ {
1174
+ "current": { "...": "standard HealthReport for current window" },
1175
+ "previous": { "...": "standard HealthReport for preceding window" },
1176
+ "changes": {
1177
+ "totalEventsDelta": 15,
1178
+ "errorRateDelta": -2.3,
1179
+ "staleAnchorDelta": 1,
1180
+ "verifyFailureDelta": 0,
1181
+ "newWarnings": ["Stale-anchor rate 43% exceeds threshold of 10%"],
1182
+ "resolvedWarnings": ["Verify-changes failure rate 25% exceeds threshold of 20%"],
1183
+ "languageRegressions": ["rust (10% → 40% failure rate)"]
1184
+ }
1185
+ }
1186
+ ```
1187
+
1188
+ ---
1189
+
1190
+ ## Routing Priority
1191
+
1192
+ 1. **AST** — If the file's language is supported (TypeScript, TSX, JavaScript, Python, Go, Rust) and the operation is AST-compatible (rename, replace-body, add/remove import, insert)
1193
+ 2. **Hash** — If the operation provides hash-anchored content identification
1194
+ 3. **Diff** — Fallback for unsupported operations
1195
+
1196
+ ## Error Handling
1197
+
1198
+ All commands return JSON with:
1199
+ - `success: false` on operation failure
1200
+ - `error` field on file-level failures
1201
+ - `errorCode` — a stable machine-readable code (see below)
1202
+ - `stale: true` on hash mismatch; `relocatedTo` when the anchor was relocated
1203
+ - `message` with human-readable description
1204
+
1205
+ **Error codes:** `PARSE_ERROR`, `SYMBOL_NOT_FOUND`, `STALE_ANCHOR`,
1206
+ `AMBIGUOUS_ANCHOR`, `AMBIGUOUS_SYMBOL`, `HASH_MISMATCH`, `INVALID_ARGUMENT`,
1207
+ `PATH_DENIED`,
1208
+ `UNSUPPORTED_OPERATION`, `FILE_NOT_FOUND`, `READ_FAILED`, `WRITE_FAILED`,
1209
+ `VERIFY_FAILED`, `VERIFY_TIMEOUT`, `MODULE_SYSTEM_MISMATCH`.
1210
+
1211
+ `AMBIGUOUS_SYMBOL` is returned by `ast rename-symbol` when the target name
1212
+ binds more than one symbol in the file — a shadowed local, a foreign
1213
+ `import`, or a duplicate top-level declaration (it maps to exit code `2`,
1214
+ the `SYMBOL_NOT_FOUND` edit-failure band). The file is **not** touched. The
1215
+ error `message` lists the contending binding sites, each as `line <N> (kind)`
1216
+ where `kind` is the declaration type (`variable_declarator`,
1217
+ `function_declaration`, `function_definition`, `class_declaration`,
1218
+ `type_alias_declaration`, `interface_declaration`, `enum_declaration`,
1219
+ `import`, or `parameter`). `rename-symbol` is file-scoped and binding-aware
1220
+ by design: it renames a symbol and its references within the target file only,
1221
+ and refuses a file-wide rename that would clobber an unintended binding.
1222
+ Disambiguate by scoping the rename to the intended binding, or rename each
1223
+ declaration separately.
1224
+
1225
+ `MODULE_SYSTEM_MISMATCH` is returned by `ast add-import` on a JavaScript file
1226
+ when the requested import cannot be written into that file's module system (it
1227
+ maps to exit code `2`, the edit-failure band). The file is **not** touched. Three
1228
+ cases produce it: the file mixes `require` and `import` and has no `.cjs`/`.mjs`
1229
+ extension or `package.json` `type` field to settle which system Node will use; the
1230
+ spec combines a default binding with named ones, which has no single `require`
1231
+ declaration; or the spec is `type`-only, which has no runtime form. `recovery`
1232
+ names the concrete next step in each case. Emitting ESM syntax into a CommonJS
1233
+ file is not an option an adapter should offer as a retry: the result parses — so
1234
+ the parse-validity gate passes it — and then fails to load at runtime (#139).
1235
+
1236
+ Otherwise `add-import` handles the translation itself: the spec is always written
1237
+ in ESM form (`'{ join } from "path"'`) and becomes `const { join } =
1238
+ require("path");` in a CommonJS file. Adapters must not switch the spec syntax
1239
+ based on the target file.
1240
+
1241
+ `READ_FAILED` means the file exists but could not be read (permissions, a
1242
+ directory in its place, a device error) — distinct from `FILE_NOT_FOUND`.
1243
+ Telemetry queries raise it rather than reporting a broken log as an empty one.
1244
+
1245
+ ## Exit Codes
1246
+
1247
+ Branch on the exit code, not on stderr text.
1248
+
1249
+ | Code | Meaning | What an agent should do |
1250
+ |------|---------|-------------------------|
1251
+ | `0` | Success | Continue |
1252
+ | `1` | Usage error — bad arguments, denied path, unsupported operation | Fix the invocation; do not retry as-is |
1253
+ | `2` | Edit failed — the operation ran but could not be applied | Try another route or report |
1254
+ | `3` | Stale anchor / precondition failed | **Retryable:** re-read the file and reissue with the fresh hash |
1255
+ | `4` | Verification failed, timed out, **or never ran** (`overall: "skipped"`, `errorCode: VERIFY_NO_CHECKS` — request a check or pass `--auto-detect`) — the edit applied but the suite did not pass, or hit its `--timeout` (`overall:`"timeout"`, `errorCode:`VERIFY_TIMEOUT`) | Inspect the verify output. A *timeout* is not a failure: do not retry it and it never reverts the edit. A real failure *may* have been reverted |
1256
+ | `5` | I/O error — file not found, unreadable, or write failed. Also an **incomplete rollback** (`errorCode: ROLLBACK_INCOMPLETE`) | Check the path and permissions. On `ROLLBACK_INCOMPLETE`, **stop and inspect** — read `unrevertedFiles` and restore them before retrying anything |
1257
+ | `70` | Internal error | Report a bug |
1258
+
1259
+ Batch commands return the worst code across all items; an all-success batch
1260
+ returns `0`.