@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,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`.
|