@tduyng/pi-edit-path 0.0.1 → 0.0.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/README.md CHANGED
@@ -1,34 +1,44 @@
1
1
  # @tduyng/pi-edit-path
2
2
 
3
- Two halves of one edit path. `then_run` fuses the validation command into the edit that needs it, and the edit hint names the region on disk a failed edit meant, so the retry lands without a re-read.
3
+ Fix the edit path instead of walking it twice. Fuse the check into the edit that needs it, and name the region on disk a failed edit meant, so the retry lands without a re-read.
4
+
5
+ ## What it costs
6
+
7
+ `then_run` adds two guideline bullets to the `edit` and `write` prompts, and the edit hint adds nothing at all: no tool, no schema, no prompt text. It reads the file pi already named in its own error.
8
+
9
+ What it saves is a round trip per check and a read per failed edit. A rejected edit that comes back with the closest region on disk costs one retry instead of a re-read plus a retry.
4
10
 
5
11
  ## then_run
6
12
 
7
- `then_run` is an optional parameter on `edit` and `write`: an object with `command` and `timeout` (seconds), or the command as a plain string. It runs after the mutation succeeds and is skipped if the mutation fails.
13
+ `then_run` is an optional parameter on `edit` and `write`: an object with `command` and `timeout` in seconds, or the command as a plain string. It runs after the mutation succeeds and is skipped when the mutation fails.
8
14
 
9
- A string carrying an object literal, `then_run: '{command: "npm test"}'`, is read for its `command` and `timeout` rather than handed to bash, which used to run the braces and answer `command not found`. A string with no key in it stays the command. A literal naming no command is reported as a `then_run` failure after the mutation has landed, so the model is told the file changed and what the argument should have been.
15
+ ```text
16
+ edit(path: "src/retry.ts", edits: [...], then_run: {command: "bun test src/retry", timeout: 120})
17
+ ```
18
+
19
+ The tool definitions are extended, not replaced. The built-in `edit` and `write` schemas keep their properties and gain an optional `then_run`, and each working directory gets its own cached definition.
10
20
 
11
- Success and failure use sentinel markers. On success the command output is appended to the mutation result preceded by `[then_run:succeeded]`. On failure the mutation output is reported first, followed by `[then_run:failed]` and the error message, all wrapped in a single thrown error so the mutation result is not lost.
21
+ Result markers are sentinels. On success the command output is appended after `[then_run:succeeded]`. On failure the mutation output is reported first, then `[then_run:failed]` and the error, all inside one thrown error, so the mutation result is never lost.
12
22
 
13
- The tool definitions are extended, not replaced: the built-in `edit` and `write` schemas keep their properties and gain an optional `then_run`. Each working directory gets its own cached tool definition.
23
+ A string carrying an object literal, `then_run: '{command: "npm test"}'`, is read for its `command` and `timeout` instead of being handed to bash, which used to run the braces and answer `command not found`. A literal naming no command is reported as a `then_run` failure after the mutation has landed, so the model is told the file changed and what the argument should have been.
14
24
 
15
- Two guidelines ride on the tool prompts. Pass `then_run` when the check follows from the same file the edit or write just changed. Use a separate `bash` call when the check does not follow from the edit, spans files not yet written, or when the model needs to read the edit result before deciding what to run.
25
+ Pass `then_run` when the check follows from the file the edit just changed. Use a separate `bash` call when the check spans files not yet written, or when you need to read the edit result before deciding what to run.
16
26
 
17
27
  ## The edit hint
18
28
 
19
- On a `tool_result` where pi's own edit could not find the old text or matched it more than once, the hint appends what the read was for: the closest region as it reads on disk now, with line numbers and current text, or the line of every match when the old text matches more than once. The retry copies old text from those lines.
29
+ When pi's own edit cannot find the old text, or finds it more than once, the hint appends what the read was for: the closest region as it reads on disk now, with line numbers and current text, or the line of every match when the old text matched more than once. The retry copies old text out of those lines.
20
30
 
21
- The hint reports, it never repairs. A miss that survives pi's fuzzy matching is a real mismatch, and silently applying a guess there would write code nobody asked for.
31
+ The hint reports and never repairs. A miss that survives pi's fuzzy matching is a real mismatch, and silently applying a guess would write code nobody asked for.
22
32
 
23
- A region is found by line agreement, not by character count. Every distinctive line of old text (at least four characters, so a lone `}` never votes) is looked up in the file as pi's own matcher would read it, and the window of oldText's height holding the most of them wins. Under half agreeing means no region is shown, because a weak guess invites an edit against the wrong lines.
33
+ A region is found by line agreement, not by character count. Every distinctive line of the old text (at least four characters, so a lone `}` never votes) is looked up in the file as pi's own matcher would read it, and the window of oldText's height holding the most of them wins. Under half agreeing means no region is shown, because a weak guess invites an edit against the wrong lines.
24
34
 
25
- Agreement alone hides the commonest cause. On a miss where every distinctive line of a doc comment was present in the file, the model had re-wrapped a paragraph at a different column and written one word from memory. A line-for-line search cannot see a re-wrap, so the lines that are in no way on disk are listed by name next to the region.
35
+ Agreement alone hides the commonest cause. On a miss where every distinctive line of a doc comment was present, the model had re-wrapped a paragraph at a different column and written one word from memory. A line-for-line search cannot see a re-wrap, so the lines that are in no way on disk are listed by name next to the region.
26
36
 
27
37
  ## Cost
28
38
 
29
- The hint adds no tool, no schema and no prompt text. It reads the file pi already named in the error. A file over 2MB is not read. `then_run` costs two guideline bullets on the `edit` and `write` prompts and saves one provider round trip per fused check.
30
-
31
- `PI_HINT_EDIT_HINT=0` disables the hint. It accepts `false`, `no`, and `off` too.
39
+ - The hint reads one file that pi already named, and refuses anything over 2 MB.
40
+ - `then_run` costs two guideline bullets on the `edit` and `write` prompts and saves one provider round trip per fused check.
41
+ - `PI_HINT_EDIT_HINT=0` disables the hint, and accepts `false`, `no`, and `off`.
32
42
 
33
43
  ## Install
34
44
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tduyng/pi-edit-path",
3
- "version": "0.0.1",
4
- "description": "Two halves of one edit path: fuse the validation command into the edit that needs it, and name the region on disk a failed edit meant so the retry lands without a re-read.",
3
+ "version": "0.0.3",
4
+ "description": "Fuse the validation command into the edit that needs it, and name the region on disk a failed edit meant.",
5
5
  "keywords": [
6
6
  "pi",
7
7
  "pi-package",
@@ -62,7 +62,7 @@ export function coerceThenRun(value: ThenRun | string | undefined): ThenRun | un
62
62
  function commandFrom(literal: string): ThenRun {
63
63
  const trimmed = literal.trim()
64
64
  if (trimmed === '') throw new Error(NO_COMMAND)
65
- if (!/^[{[]?\s*"?[A-Za-z_]\w*"?\s*:/.test(trimmed)) return { command: trimmed }
65
+ if (!/^[{[]/.test(trimmed)) return { command: trimmed }
66
66
  const command = literalCommand(trimmed)
67
67
  if (command === undefined) throw new Error(NO_COMMAND)
68
68
  const ran: ThenRun = { command }
@@ -79,11 +79,13 @@ function literalCommand(literal: string): string | undefined {
79
79
  parsed = undefined
80
80
  }
81
81
  if (isCommandObject(parsed)) return parsed.command.trim()
82
- const match = /"?command"?\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,:}]+))/.exec(literal)
82
+ const match = /"?command"?\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,}]*))/.exec(literal)
83
83
  const command = (match?.[1] ?? match?.[2] ?? match?.[3] ?? '').trim()
84
84
  return command === '' ? undefined : command
85
85
  }
86
86
 
87
+ const MAX_OUTPUT_CHARS = 12_000
88
+
87
89
  type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject
88
90
 
89
91
  interface JsonObject {
@@ -124,7 +126,11 @@ export function resultText(result: AgentToolResult<unknown>): string {
124
126
  }
125
127
 
126
128
  export function fuseResults<T>(mutation: AgentToolResult<T>, ran: AgentToolResult<unknown>): AgentToolResult<T> {
127
- const output = resultText(ran)
129
+ const raw = resultText(ran)
130
+ const output =
131
+ raw.length > MAX_OUTPUT_CHARS
132
+ ? `${raw.slice(-MAX_OUTPUT_CHARS)}\n[then_run output truncated to the last ${MAX_OUTPUT_CHARS} characters]`
133
+ : raw
128
134
  return {
129
135
  ...mutation,
130
136
  content: [...mutation.content, { type: 'text' as const, text: output ? `${SUCCEEDED}\n${output}` : SUCCEEDED }]
@@ -183,7 +189,10 @@ export default function actionFusionExtension(pi: ExtensionAPI): void {
183
189
  edits: t.array(t.object({ oldText: t.string(), newText: t.string() })),
184
190
  then_run: thenRunSchema
185
191
  }),
186
- prepareArguments: (input) => (isJsonObject(input) ? normalizeEditInput(input) : input) as EditArgs,
192
+ prepareArguments: (input) => {
193
+ const prepared = editBuiltin.prepareArguments?.(input) ?? input
194
+ return (isJsonObject(prepared) ? normalizeEditInput(prepared) : prepared) as EditArgs
195
+ },
187
196
  async execute(toolCallId, input: EditArgs, signal, onUpdate, ctx) {
188
197
  const { then_run, ...mutation } = input as typeof input & { then_run?: ThenRun }
189
198
  const result = await editFor(ctx.cwd).execute(toolCallId, mutation, signal, onUpdate, ctx)