@oh-my-pi/pi-coding-agent 12.5.0 → 12.5.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.5.1] - 2026-02-15
6
+ ### Added
7
+
8
+ - Added `repeatToolDescriptions` setting to render full tool descriptions in the system prompt instead of a tool name list
9
+
5
10
  ## [12.5.0] - 2026-02-15
6
11
  ### Breaking Changes
7
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "12.5.0",
3
+ "version": "12.5.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -84,12 +84,12 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "@mozilla/readability": "0.6.0",
87
- "@oh-my-pi/omp-stats": "12.5.0",
88
- "@oh-my-pi/pi-agent-core": "12.5.0",
89
- "@oh-my-pi/pi-ai": "12.5.0",
90
- "@oh-my-pi/pi-natives": "12.5.0",
91
- "@oh-my-pi/pi-tui": "12.5.0",
92
- "@oh-my-pi/pi-utils": "12.5.0",
87
+ "@oh-my-pi/omp-stats": "12.5.1",
88
+ "@oh-my-pi/pi-agent-core": "12.5.1",
89
+ "@oh-my-pi/pi-ai": "12.5.1",
90
+ "@oh-my-pi/pi-natives": "12.5.1",
91
+ "@oh-my-pi/pi-tui": "12.5.1",
92
+ "@oh-my-pi/pi-utils": "12.5.1",
93
93
  "@sinclair/typebox": "^0.34.48",
94
94
  "@xterm/headless": "^6.0.0",
95
95
  "ajv": "^8.18.0",
@@ -253,6 +253,15 @@ export const SETTINGS_SCHEMA = {
253
253
  description: "Rewrite tool call arguments to normalized format in session history",
254
254
  },
255
255
  },
256
+ repeatToolDescriptions: {
257
+ type: "boolean",
258
+ default: false,
259
+ ui: {
260
+ tab: "agent",
261
+ label: "Repeat tool descriptions",
262
+ description: "Render full tool descriptions in the system prompt instead of a tool name list",
263
+ },
264
+ },
256
265
  readLineNumbers: {
257
266
  type: "boolean",
258
267
  default: false,
@@ -56,11 +56,15 @@ The question is not "does this work?" but "under what conditions? What happens o
56
56
 
57
57
  <tools>
58
58
  ## Available Tools
59
+ {{#if repeatToolDescriptions}}
59
60
  {{#each toolDescriptions}}
60
61
  <tool name="{{name}}">
61
62
  {{description}}
62
63
  </tool>
63
64
  {{/each}}
65
+ {{else}}
66
+ {{#list tools join="\n"}}- {{this}}{{/list}}
67
+ {{/if}}
64
68
 
65
69
  {{#ifAny (includes tools "python") (includes tools "bash")}}
66
70
  ### Precedence: Specialized → Python → Bash
@@ -1,94 +1,85 @@
1
- # Edit (Hash anchored)
1
+ # Edit (Hash Anchored)
2
2
 
3
- Line-addressed edits using hash-verified line references. Read file in hashline mode, then edit by referencing `LINE:HASH` pairs.
3
+ Line-addressed edits using hash-verified line references. Read files in hashline mode, collect exact `LINE:HASH` references, and submit edits that change only the targeted token or expression.
4
+ **CRITICAL: Copy `LINE:HASH` refs verbatim from read output. Use only the anchor prefix (e.g., `{{hashline 42 "const x = 1"}}`), never the trailing source text after `|`.**
4
5
 
5
- <critical>
6
- - Copy `LINE:HASH` refs verbatim from read output never fabricate or guess hashes
7
- - `new_text` (set_line/replace_lines) or `text` (insert_after) contains plain replacement lines only — no `LINE:HASH` prefix, no diff `+` markers
8
- - On hash mismatch: use the updated `LINE:HASH` refs shown by `>>>` directly; only `read` again if you need additional lines/context
9
- - If you already edited a file in this turn, re-read that file before the next edit to it
10
- - For code-change requests, respond with tool calls, not prose
11
- - Edit only requested lines. Do not reformat unrelated code.
12
- - Direction-lock every mutation: replace the exact currently-present token/expression with the intended target token/expression; never reverse the change or "change something nearby".
13
- - `new_text` must differ from the current line content — sending identical content is rejected as a no-op
14
- </critical>
6
+ <workflow>
7
+ 1. Read the target file (`read`) to obtain `LINE:HASH` references
8
+ 2. Collect the exact `LINE:HASH` refs for lines you will change
9
+ 3. Direction-lock each mutation: identify the exact current token/expression the intended replacement
10
+ 4. Submit one `edit` call containing all operations for that file
11
+ 5. If another edit is needed on the same file: re-read first, then edit (hashes change after every edit)
12
+ 6. Respond with tool calls only no prose
13
+ </workflow>
15
14
 
16
- <instruction>
17
- **Workflow:**
18
- 1. Read target file (`read`)
19
- 2. Collect the exact `LINE:HASH` refs you need
20
- 3. Submit one `edit` call with all known operations for that file
21
- 4. If another change on same file is needed later: re-read first, then edit
22
- 5. Direction-lock each operation before submitting (`exact source token/expression on target line` → `intended replacement`) and keep the mutation to one logical locus. Do not output prose; submit only the tool call.
23
- **Atomicity:** All edits in one call are validated against the file as last read — line numbers and hashes refer to the original state, not after earlier edits in the same array. The applicator sorts and applies bottom-up automatically.
24
- **Edit variants:**
25
- - `{ set_line: { anchor: "LINE:HASH", new_text: "..." } }`
26
- - `{ replace_lines: { start_anchor: "LINE:HASH", end_anchor: "LINE:HASH", new_text: "..." } }`
27
- - `{ insert_after: { anchor: "LINE:HASH", text: "..." } }`
28
- - `{ replace: { old_text: "...", new_text: "...", all?: boolean } }` — substr-style fuzzy replace (no LINE:HASH; use when line refs unavailable)
15
+ <operations>
16
+ Four edit variants are available:
17
+ - **`set_line`**: Replace a single line
18
+ `{ set_line: { anchor: "LINE:HASH", new_text: "..." } }`
19
+ `new_text: ""` keeps the line but makes it blank.
20
+ - **`replace_lines`**: Replace a contiguous range (use for deletions with `new_text: ""`)
21
+ `{ replace_lines: { start_anchor: "LINE:HASH", end_anchor: "LINE:HASH", new_text: "..." } }`
22
+ - **`insert_after`**: Add new content after an anchor line
23
+ `{ insert_after: { anchor: "LINE:HASH", text: "..." } }`
24
+ - **`replace`**: Substring-style fuzzy match (when line refs are unavailable)
25
+ `{ replace: { old_text: "...", new_text: "...", all?: boolean } }`
26
+ **Atomicity:** All edits in one call validate against the file as last read. Line numbers and hashes refer to the original state, not post-edit state. The applicator sorts and applies bottom-up automatically.
27
+ </operations>
29
28
 
30
- `new_text: ""` means delete (for `set_line`/`replace_lines`).
31
- </instruction>
29
+ <rules>
30
+ 1. **Scope each operation minimally.** One logical change site per operation. Use separate `set_line` ops for non-adjacent lines instead of a wide `replace_lines` that spans unchanged code.
31
+ 2. **Preserve original formatting exactly.** Copy each line's whitespace, braces, semicolons, trailing commas, and style — then change only the targeted token/expression. Keep `import { foo }` as-is; keep indentation and line breaks as-is.
32
+ 3. **Use `insert_after` for additions.** When adding a field, argument, or import near existing lines, prefer `insert_after` over replacing a neighboring line.
33
+ 4. **Ensure `new_text` differs from current content.** Identical content is rejected as a no-op.
34
+ 5. **Edit only requested lines.** Leave unrelated code untouched.
35
+ 6. **Lock mutation direction.** Replace the exact currently-present token with the intended target. For swaps between two locations, use two `set_line` ops in one call.
36
+ </rules>
32
37
 
33
- <caution>
34
- **Preserve original formatting.** When writing `new_text`/`text`, copy each line's exact whitespace, braces, and style from the read output — then change *only* the targeted token/expression. Do not:
35
- - Restyle braces: `import { foo }` `import {foo}`
36
- - Reflow arguments onto multiple lines or collapse them onto one line
37
- - Change indentation style, trailing commas, or semicolons on lines you replace
38
- - Do NOT use `replace_lines` over a wide span when multiple `set_line` ops would work — wide ranges tempt reformatting everything in between
39
-
40
- If a change spans multiple non-adjacent lines, use separate `set_line` operations for each not a single `replace_lines` that includes unchanged lines in `new_text`.
41
- - Each edit operation must target one logical change site with minimal scope. If a fix requires two locations, use two operations; never span unrelated lines in one `replace_lines`.
42
- - Self-check before submitting: if your edit touches lines unrelated to the stated fix, split or narrow it.
43
- - Do NOT reformat lines you are replacing — preserve exact whitespace, braces (`{ foo }` not `{foo}`), arrow style, and line breaks. Change ONLY the targeted token/expression. Reformatting causes hash verification failure even when the logic is correct.
44
- - For swaps (exchanging content between two locations), use two `set_line` operations in one call — the applicator handles ordering. Do not try to account for line number shifts between operations.
45
- </caution>
46
- <instruction>
47
- **Recovery:**
48
- - Hash mismatch (`>>>` error): copy the updated `LINE:HASH` refs from the error verbatim and retry with the same intended mutation. Do NOT re-read unless you need lines not shown in the error.
49
- - If hash mismatch repeats after applying updated refs, stop blind retries and re-read the relevant region before retrying.
50
- - After a successful edit, always re-read the file before making another edit to the same file (hashes have changed).
51
- - No-op error ("identical content"): your replacement text matches what the file already contains. STOP and re-read the file — you are likely targeting the wrong line or your replacement is not actually different. Do NOT retry with the same content. After 2 consecutive no-op errors on the same line, re-read the entire function/block to understand the current file state.
52
- </instruction>
53
-
54
- <instruction>
55
- **Preflight schema and validation (required):**
56
- - Payload shape is `{"path": string, "edits": [operation, ...]}` with a non-empty `edits` array.
57
- - Each operation contains exactly one variant key: `set_line`, `replace_lines`, `insert_after`, or `replace`.
58
- - Required fields by variant:
59
- - `set_line`: `anchor`, `new_text`
60
- - `replace_lines`: `start_anchor`, `end_anchor`, `new_text`
61
- - `insert_after`: `anchor`, `text` (non-empty)
62
- - `replace`: `old_text`, `new_text` (fuzzy match; `all: true` for replace-all)
63
- - Each `anchor`/`start_anchor`/`end_anchor` ref matches `^\d+:[A-Za-z0-9]+$` (no spaces, no trailing source text).
64
- - `new_text`/`text` preserves original formatting and changes only the direction-locked target locus.
65
- </instruction>
66
-
67
- <input>
68
- - `path`: File path
69
- - `edits`: Array of edit operations (one of the variants above)
70
- </input>
38
+ <recovery>
39
+ **Hash mismatch (`>>>` error):**
40
+ Copy the updated `LINE:HASH` refs from the error output verbatim and retry with the same intended mutation.
41
+ → Re-read only if you need lines not shown in the error.
42
+ If mismatch repeats after applying updated refs, stop and re-read the relevant region.
43
+ **No-op error ("identical content"):**
44
+ → Stop. Re-read the file — you are targeting the wrong line or your replacement is not different.
45
+ After 2 consecutive no-op errors on the same line, re-read the entire function/block.
46
+ </recovery>
71
47
 
48
+ <examples>
72
49
  <example name="replace single line">
73
- edit {"path":"src/app.py","edits":[{"set_line":{"anchor":"{{hashline 2 'x = 42'}}","new_text":" x = 99"}}]}
50
+ set_line: { anchor: "{{hashline 2 " x"}}", new_text: " x = 99" }
74
51
  </example>
75
52
 
76
53
  <example name="replace range">
77
- edit {"path":"src/app.py","edits":[{"replace_lines":{"start_anchor":"{{hashline 5 'old_value = True'}}","end_anchor":"{{hashline 8 'return result'}}","new_text":" combined = True"}}]}
54
+ replace_lines: { start_anchor: "{{hashline 5 "old start line"}}", end_anchor: "{{hashline 8 "old end line"}}", new_text: " combined = True" }
78
55
  </example>
79
56
 
80
57
  <example name="delete lines">
81
- edit {"path":"src/app.py","edits":[{"replace_lines":{"start_anchor":"{{hashline 5 'old_value = True'}}","end_anchor":"{{hashline 6 'unused = None'}}","new_text":""}}]}
58
+ replace_lines: { start_anchor: "{{hashline 5 "line to delete A"}}", end_anchor: "{{hashline 6 "line to delete B"}}", new_text: "" }
82
59
  </example>
83
60
 
84
61
  <example name="insert after">
85
- edit {"path":"src/app.py","edits":[{"insert_after":{"anchor":"{{hashline 3 'def hello'}}","text":" # new comment"}}]}
62
+ insert_after: { anchor: "{{hashline 3 "anchor line content"}}", text: " # new comment" }
86
63
  </example>
87
64
 
88
65
  <example name="multiple edits (bottom-up safe)">
89
- edit {"path":"src/app.py","edits":[{"set_line":{"anchor":"{{hashline 10 'return True'}}","new_text":" return False"}},{"set_line":{"anchor":"{{hashline 3 'def hello'}}","new_text":" x = 42"}}]}
66
+ set_line: { anchor: "{{hashline 10 "old line 10"}}", new_text: " return False" }
67
+ set_line: { anchor: "{{hashline 3 "old line 3"}}", new_text: " x = 42" }
90
68
  </example>
91
69
 
92
70
  <example name="content replace (substr-style, no hashes)">
93
- edit {"path":"src/app.py","edits":[{"replace":{"old_text":"x = 42","new_text":"x = 99"}}]}
94
- </example>
71
+ replace: { old_text: "x = 42", new_text: "x = 99" }
72
+ </example>
73
+ </examples>
74
+
75
+ <validation>
76
+ Before submitting, verify:
77
+ - [ ] Payload shape: `{"path": string, "edits": [operation, ...]}` with non-empty `edits` array
78
+ - [ ] Each operation has exactly one variant key: `set_line` | `replace_lines` | `insert_after` | `replace`
79
+ - [ ] Each anchor is copied exactly from the `LINE:HASH` prefix (no spaces, no trailing source text)
80
+ - [ ] `new_text`/`text` contains plain replacement lines only — no `LINE:HASH` prefixes, no diff `+` markers
81
+ - [ ] Each replacement differs from the current line content
82
+ - [ ] Each operation targets one logical change site with minimal scope
83
+ - [ ] Formatting of replaced lines matches the original exactly, except for the targeted change
84
+ </validation>
85
+ **REMINDER: Copy `LINE:HASH` refs verbatim. Anchors are `LINE:HASH` only — never `LINE:HASH|content`. Preserve exact formatting. Change only the targeted token.**
package/src/sdk.ts CHANGED
@@ -323,6 +323,7 @@ export interface BuildSystemPromptOptions {
323
323
  contextFiles?: Array<{ path: string; content: string }>;
324
324
  cwd?: string;
325
325
  appendPrompt?: string;
326
+ repeatToolDescriptions?: boolean;
326
327
  }
327
328
 
328
329
  /**
@@ -334,6 +335,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
334
335
  skills: options.skills,
335
336
  contextFiles: options.contextFiles,
336
337
  appendSystemPrompt: options.appendPrompt,
338
+ repeatToolDescriptions: options.repeatToolDescriptions,
337
339
  });
338
340
  }
339
341
 
@@ -986,6 +988,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
986
988
  emitEvent: event => cursorEventEmitter?.(event),
987
989
  });
988
990
 
991
+ const repeatToolDescriptions = settings.get("repeatToolDescriptions");
989
992
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
990
993
  toolContextStore.setToolNames(toolNames);
991
994
  const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
@@ -999,6 +1002,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
999
1002
  rules: rulebookRules,
1000
1003
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
1001
1004
  appendSystemPrompt: memoryInstructions,
1005
+ repeatToolDescriptions,
1002
1006
  });
1003
1007
 
1004
1008
  if (options.systemPrompt === undefined) {
@@ -1016,6 +1020,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1016
1020
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
1017
1021
  customPrompt: options.systemPrompt,
1018
1022
  appendSystemPrompt: memoryInstructions,
1023
+ repeatToolDescriptions,
1019
1024
  });
1020
1025
  }
1021
1026
  return options.systemPrompt(defaultPrompt);
@@ -430,6 +430,8 @@ export interface BuildSystemPromptOptions {
430
430
  toolNames?: string[];
431
431
  /** Text to append to system prompt. */
432
432
  appendSystemPrompt?: string;
433
+ /** Repeat full tool descriptions in system prompt. Default: false */
434
+ repeatToolDescriptions?: boolean;
433
435
  /** Skills settings for discovery. */
434
436
  skillsSettings?: SkillsSettings;
435
437
  /** Working directory. Default: getProjectDir() */
@@ -454,6 +456,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
454
456
  customPrompt,
455
457
  tools,
456
458
  appendSystemPrompt,
459
+ repeatToolDescriptions = false,
457
460
  skillsSettings,
458
461
  toolNames,
459
462
  cwd,
@@ -553,6 +556,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
553
556
  return renderPromptTemplate(systemPromptTemplate, {
554
557
  tools: toolNamesArray,
555
558
  toolDescriptions,
559
+ repeatToolDescriptions,
556
560
  environment,
557
561
  systemPromptCustomization: systemPromptCustomization ?? "",
558
562
  contextFiles,