@oh-my-pi/pi-coding-agent 14.5.7 → 14.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +7 -7
  3. package/src/config/model-registry.ts +23 -1
  4. package/src/config/settings-schema.ts +23 -0
  5. package/src/edit/modes/atom.lark +7 -5
  6. package/src/edit/modes/atom.ts +462 -56
  7. package/src/edit/modes/hashline.ts +21 -1
  8. package/src/lsp/index.ts +2 -4
  9. package/src/lsp/render.ts +0 -3
  10. package/src/lsp/types.ts +1 -4
  11. package/src/lsp/utils.ts +18 -14
  12. package/src/modes/components/settings-defs.ts +10 -0
  13. package/src/modes/controllers/command-controller.ts +17 -0
  14. package/src/modes/controllers/event-controller.ts +14 -9
  15. package/src/modes/controllers/input-controller.ts +13 -1
  16. package/src/modes/interactive-mode.ts +44 -23
  17. package/src/modes/types.ts +5 -2
  18. package/src/modes/utils/context-usage.ts +294 -0
  19. package/src/prompts/tools/atom.md +99 -44
  20. package/src/prompts/tools/exit-plan-mode.md +5 -39
  21. package/src/prompts/tools/lsp.md +2 -3
  22. package/src/prompts/tools/recipe.md +16 -0
  23. package/src/prompts/tools/task.md +34 -147
  24. package/src/prompts/tools/todo-write.md +22 -64
  25. package/src/session/compaction/compaction.ts +35 -22
  26. package/src/session/session-dump-format.ts +1 -0
  27. package/src/slash-commands/builtin-registry.ts +12 -5
  28. package/src/tools/bash.ts +149 -115
  29. package/src/tools/debug.ts +57 -70
  30. package/src/tools/index.ts +11 -0
  31. package/src/tools/recipe/index.ts +80 -0
  32. package/src/tools/recipe/render.ts +19 -0
  33. package/src/tools/recipe/runner.ts +219 -0
  34. package/src/tools/recipe/runners/cargo.ts +131 -0
  35. package/src/tools/recipe/runners/index.ts +8 -0
  36. package/src/tools/recipe/runners/just.ts +73 -0
  37. package/src/tools/recipe/runners/make.ts +101 -0
  38. package/src/tools/recipe/runners/pkg.ts +165 -0
  39. package/src/tools/recipe/runners/task.ts +72 -0
  40. package/src/tools/renderers.ts +2 -0
@@ -0,0 +1,294 @@
1
+ import type { Model } from "@oh-my-pi/pi-ai";
2
+ import { countTokens } from "@oh-my-pi/pi-natives";
3
+ import { formatNumber } from "@oh-my-pi/pi-utils";
4
+ import type { Skill } from "../../extensibility/skills";
5
+ import type { AgentSession } from "../../session/agent-session";
6
+ import type { CompactionSettings } from "../../session/compaction";
7
+ import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "../../session/compaction";
8
+ import type { Tool } from "../../tools";
9
+ import type { theme as Theme } from "../theme/theme";
10
+
11
+ const GRID_COLS = 20;
12
+ const GRID_ROWS = 10;
13
+ const GRID_CELLS = GRID_COLS * GRID_ROWS;
14
+ const GRID_GUTTER = " ";
15
+
16
+ const CELL_FILLED = "⛁";
17
+ const CELL_FILLED_MESSAGES = "⛃";
18
+ const CELL_FREE = "⛶";
19
+ const CELL_BUFFER = "⛝";
20
+
21
+ type CategoryId = "systemPrompt" | "systemTools" | "skills" | "messages";
22
+
23
+ interface CategoryInfo {
24
+ id: CategoryId;
25
+ label: string;
26
+ tokens: number;
27
+ color: "accent" | "warning" | "success" | "userMessageText";
28
+ glyph: string;
29
+ }
30
+
31
+ export interface ContextBreakdown {
32
+ model: Model | undefined;
33
+ contextWindow: number;
34
+ categories: CategoryInfo[];
35
+ usedTokens: number;
36
+ autoCompactBufferTokens: number;
37
+ freeTokens: number;
38
+ }
39
+
40
+ function estimateSkillsTokens(skills: readonly Skill[]): number {
41
+ const fragments: string[] = [];
42
+ for (const skill of skills) {
43
+ // "- name: description\n" wire framing tokenizes ~identically to the
44
+ // concatenated form, so encode each piece separately and sum.
45
+ fragments.push(skill.name, skill.description);
46
+ }
47
+ return countTokens(fragments);
48
+ }
49
+
50
+ function estimateToolSchemaTokens(tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>): number {
51
+ const fragments: string[] = [];
52
+ for (const tool of tools) {
53
+ fragments.push(tool.name, tool.description);
54
+ try {
55
+ fragments.push(JSON.stringify(tool.parameters ?? {}));
56
+ } catch {
57
+ // Schema may contain functions or cycles; ignore.
58
+ }
59
+ }
60
+ return countTokens(fragments);
61
+ }
62
+
63
+ function estimateMessagesTokens(session: AgentSession): number {
64
+ let total = 0;
65
+ for (const message of session.messages) {
66
+ total += estimateTokens(message);
67
+ }
68
+ return total;
69
+ }
70
+
71
+ /**
72
+ * Compute a breakdown of estimated context usage by category for the active
73
+ * session and model.
74
+ */
75
+ export function computeContextBreakdown(session: AgentSession): ContextBreakdown {
76
+ const model = session.model;
77
+ const contextWindow = model?.contextWindow ?? 0;
78
+
79
+ const skillsTokens = estimateSkillsTokens(session.skills);
80
+ const toolsTokens = estimateToolSchemaTokens(session.agent.state.tools);
81
+ const messagesTokens = estimateMessagesTokens(session);
82
+
83
+ // The rendered system prompt already contains the skill descriptions and the
84
+ // markdown tool descriptions. To present a non-overlapping breakdown:
85
+ // System prompt = total system prompt text - skills section (tool descriptions stay)
86
+ // Tools = JSON tool schema sent separately on the wire
87
+ // Skills = the skill list embedded in the system prompt
88
+ // Messages = conversation messages
89
+ const systemPromptTextTokens = countTokens(session.systemPrompt);
90
+ const systemPromptTokens = Math.max(0, systemPromptTextTokens - skillsTokens);
91
+
92
+ const categories: CategoryInfo[] = [
93
+ { id: "systemPrompt", label: "System prompt", tokens: systemPromptTokens, color: "accent", glyph: CELL_FILLED },
94
+ { id: "systemTools", label: "System tools", tokens: toolsTokens, color: "warning", glyph: CELL_FILLED },
95
+ { id: "skills", label: "Skills", tokens: skillsTokens, color: "success", glyph: CELL_FILLED },
96
+ {
97
+ id: "messages",
98
+ label: "Messages",
99
+ tokens: messagesTokens,
100
+ color: "userMessageText",
101
+ glyph: CELL_FILLED_MESSAGES,
102
+ },
103
+ ];
104
+
105
+ const usedTokens = categories.reduce((sum, c) => sum + c.tokens, 0);
106
+
107
+ let autoCompactBufferTokens = 0;
108
+ if (contextWindow > 0) {
109
+ const compactionSettings = session.settings.getGroup("compaction") as CompactionSettings;
110
+ if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
111
+ const threshold = resolveThresholdTokens(contextWindow, compactionSettings);
112
+ autoCompactBufferTokens = Math.max(0, contextWindow - threshold);
113
+ } else {
114
+ autoCompactBufferTokens = 0;
115
+ }
116
+ // Even when fully disabled, fall back to a sensible reserve floor for display.
117
+ if (autoCompactBufferTokens === 0 && compactionSettings.enabled) {
118
+ autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings);
119
+ }
120
+ }
121
+ autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - usedTokens));
122
+
123
+ const freeTokens = Math.max(0, contextWindow - usedTokens - autoCompactBufferTokens);
124
+
125
+ return {
126
+ model,
127
+ contextWindow,
128
+ categories,
129
+ usedTokens,
130
+ autoCompactBufferTokens,
131
+ freeTokens,
132
+ };
133
+ }
134
+
135
+ interface CellSpec {
136
+ glyph: string;
137
+ color: "accent" | "warning" | "success" | "userMessageText" | "muted" | "dim";
138
+ }
139
+
140
+ function planCells(breakdown: ContextBreakdown): CellSpec[] {
141
+ const cells: CellSpec[] = [];
142
+ const window = breakdown.contextWindow;
143
+
144
+ if (window <= 0) {
145
+ for (let i = 0; i < GRID_CELLS; i++) {
146
+ cells.push({ glyph: CELL_FREE, color: "dim" });
147
+ }
148
+ return cells;
149
+ }
150
+
151
+ const tokensPerCell = window / GRID_CELLS;
152
+
153
+ const ratioCells = (tokens: number): number => {
154
+ if (tokens <= 0) return 0;
155
+ return Math.max(1, Math.round(tokens / tokensPerCell));
156
+ };
157
+
158
+ const categoryCounts = breakdown.categories.map(category => ({
159
+ category,
160
+ count: ratioCells(category.tokens),
161
+ }));
162
+
163
+ let bufferCount = ratioCells(breakdown.autoCompactBufferTokens);
164
+
165
+ let usedCount = categoryCounts.reduce((sum, c) => sum + c.count, 0);
166
+
167
+ // Prevent the visualization from over-running the grid.
168
+ const maxUsable = GRID_CELLS - bufferCount;
169
+ if (usedCount > maxUsable) {
170
+ // Scale categories proportionally down to fit.
171
+ let overflow = usedCount - maxUsable;
172
+ // Trim from the largest categories first to preserve visibility for small ones.
173
+ const order = [...categoryCounts].sort((a, b) => b.count - a.count);
174
+ for (const entry of order) {
175
+ while (overflow > 0 && entry.count > 1) {
176
+ entry.count -= 1;
177
+ overflow -= 1;
178
+ }
179
+ }
180
+ usedCount = categoryCounts.reduce((sum, c) => sum + c.count, 0);
181
+ if (usedCount + bufferCount > GRID_CELLS) {
182
+ bufferCount = Math.max(0, GRID_CELLS - usedCount);
183
+ }
184
+ }
185
+
186
+ for (const { category, count } of categoryCounts) {
187
+ for (let i = 0; i < count; i++) {
188
+ cells.push({ glyph: category.glyph, color: category.color });
189
+ }
190
+ }
191
+
192
+ const freeCount = Math.max(0, GRID_CELLS - cells.length - bufferCount);
193
+ for (let i = 0; i < freeCount; i++) {
194
+ cells.push({ glyph: CELL_FREE, color: "dim" });
195
+ }
196
+ for (let i = 0; i < bufferCount; i++) {
197
+ cells.push({ glyph: CELL_BUFFER, color: "warning" });
198
+ }
199
+
200
+ // Pad to exactly GRID_CELLS in case rounding undershot.
201
+ while (cells.length < GRID_CELLS) {
202
+ cells.push({ glyph: CELL_FREE, color: "dim" });
203
+ }
204
+ return cells.slice(0, GRID_CELLS);
205
+ }
206
+
207
+ function percentString(part: number, whole: number, fractionDigits = 1): string {
208
+ if (whole <= 0) return "0%";
209
+ const pct = (part / whole) * 100;
210
+ if (pct > 0 && pct < 0.05) return "<0.1%";
211
+ return `${pct.toFixed(fractionDigits)}%`;
212
+ }
213
+
214
+ function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): string[] {
215
+ const lines: string[] = [];
216
+ const { model, contextWindow, categories, usedTokens, autoCompactBufferTokens, freeTokens } = breakdown;
217
+
218
+ const modelName = model?.name ?? model?.id ?? "no model";
219
+ const modelId = model?.id ?? "unknown";
220
+ const windowLabel = formatNumber(contextWindow).toLowerCase();
221
+
222
+ lines.push(theme.bold(`${modelName}`) + theme.fg("dim", ` (${windowLabel} context)`));
223
+ lines.push(theme.fg("muted", `${modelId}[${windowLabel}]`));
224
+ lines.push(
225
+ `${theme.bold(formatNumber(usedTokens))}${theme.fg("dim", `/${windowLabel} tokens`)}` +
226
+ theme.fg("muted", ` (${percentString(usedTokens, contextWindow)})`),
227
+ );
228
+ lines.push("");
229
+ lines.push(theme.fg("muted", "Estimated usage by category"));
230
+
231
+ for (const category of categories) {
232
+ const dot = theme.fg(category.color, category.glyph);
233
+ const label = category.label;
234
+ const tokens = formatNumber(category.tokens);
235
+ const pct = percentString(category.tokens, contextWindow);
236
+ lines.push(`${dot} ${label}: ${theme.bold(tokens)} ${theme.fg("dim", `tokens (${pct})`)}`);
237
+ }
238
+
239
+ const freeDot = theme.fg("dim", CELL_FREE);
240
+ lines.push(
241
+ `${freeDot} Free space: ${theme.bold(formatNumber(freeTokens))} ${theme.fg("dim", `(${percentString(freeTokens, contextWindow)})`)}`,
242
+ );
243
+
244
+ if (autoCompactBufferTokens > 0) {
245
+ const bufferDot = theme.fg("warning", CELL_BUFFER);
246
+ lines.push(
247
+ `${bufferDot} Autocompact buffer: ${theme.bold(formatNumber(autoCompactBufferTokens))} ${theme.fg(
248
+ "dim",
249
+ `tokens (${percentString(autoCompactBufferTokens, contextWindow)})`,
250
+ )}`,
251
+ );
252
+ }
253
+
254
+ return lines;
255
+ }
256
+
257
+ /**
258
+ * Render a colorful context-usage panel as ANSI text. Output is a series of
259
+ * lines pairing the grid (left) with the legend (right).
260
+ */
261
+ export function renderContextUsage(breakdown: ContextBreakdown, theme: typeof Theme): string {
262
+ if (breakdown.contextWindow <= 0) {
263
+ return theme.fg("muted", "Context usage is unavailable: no model is selected for this session.");
264
+ }
265
+
266
+ const cells = planCells(breakdown);
267
+ const legend = buildLegendLines(breakdown, theme);
268
+
269
+ const totalLines = Math.max(GRID_ROWS, legend.length);
270
+ const lines: string[] = [];
271
+
272
+ for (let row = 0; row < totalLines; row++) {
273
+ let gridSegment = "";
274
+ if (row < GRID_ROWS) {
275
+ const rowCells: string[] = [];
276
+ for (let col = 0; col < GRID_COLS; col++) {
277
+ const cell = cells[row * GRID_COLS + col];
278
+ rowCells.push(theme.fg(cell.color, cell.glyph));
279
+ }
280
+ gridSegment = rowCells.join(" ");
281
+ } else {
282
+ // Pad with blanks the same visible width as a grid row so legend lines
283
+ // past the grid stay aligned with their column.
284
+ const blank = " ".repeat(GRID_COLS * 2 - 1);
285
+ gridSegment = blank;
286
+ }
287
+
288
+ const legendSegment = legend[row] ?? "";
289
+ const line = legendSegment.length > 0 ? `${gridSegment}${GRID_GUTTER}${legendSegment}` : gridSegment;
290
+ lines.push(line);
291
+ }
292
+
293
+ return lines.join("\n");
294
+ }
@@ -1,26 +1,45 @@
1
- Your patch language is a compact, file-oriented edit format.
1
+ Your patch language is a compact, line-anchored edit format.
2
2
 
3
- When emitting a patch, the first non-blank line **MUST** be `---PATH`.
4
- A Lid is the anchor emitted in read/grep etc. (line number + id, e.g. `5th`).
3
+ A patch contains one or more file sections. The first non-blank line of every section **MUST** be `---PATH`.
4
+ A "Lid" is a per-line anchor emitted by `read`, `grep`, etc. `<lineNumber><2-letter-hash>`, e.g. `5th`, `123ab`. You **MUST** copy a Lid verbatim from the latest output for the file you're editing.
5
+
6
+ This format is purely textual. The tool has NO awareness of language, indentation, brackets, fences, or table widths. You are responsible for emitting valid syntax in your replacements/insertions.
5
7
 
6
8
  <ops>
7
- ---PATH start editing PATH with cursor at EOF
8
- !rm delete PATH
9
- !mv X move file to X
10
- $ move cursor to BOF
11
- ^ move cursor to EOF
12
- @Lid move cursor after Lid
13
- +X insert X at the cursor; `+` alone inserts a blank line
14
- Lid=X replace whole line with X; `Lid=` blanks it out
15
- -Lid delete line (repeat for multi)
9
+ ---PATH start a section editing PATH; cursor begins at EOF
10
+ ^ move cursor to BOF (before line 1)
11
+ $ move cursor to EOF (after the last line)
12
+ @Lid move cursor to AFTER the anchored line (does not modify the file)
13
+ ^Lid move cursor to BEFORE the anchored line (does not modify the file)
14
+ +TEXT insert one line containing TEXT at the cursor
15
+ + insert one blank line at the cursor
16
+ Lid=TEXT replace the anchored line with TEXT
17
+ LidA..LidB=TEXT replace the range with one line; following `\TEXT` lines append literal lines to the replacement
18
+ \TEXT append literal TEXT to the active replacement (after `Lid=…` or `LidA..LidB=…`)
19
+ \ append a blank line to the active replacement
20
+ Lid= blank the anchored line's content but KEEP the line (results in an empty line, NOT a removed line; use `-Lid` to remove)
21
+ -Lid delete the anchored line (repeat for multi-line delete)
22
+ -LidA..LidB delete the contiguous line range LidA..LidB (inclusive)
23
+ !rm delete the section's PATH (**MUST** be the only op in the section)
24
+ !mv DEST rename the section's PATH to DEST (**MUST** be the only op in the section)
16
25
  </ops>
17
26
 
18
27
  <rules>
19
- - You may have multiple `---PATH` sections to edit multiple files at once.
20
- - Ops starting with `$` / `^` / `@Lid` do not alter lines; you must still issue an op like `+` afterwards.
21
- - Consecutive `+X` ops insert consecutive lines.
22
- - `Lid=X` replaces the whole line. X must be the complete new line, not a fragment.
23
- - To rewrite multiple adjacent lines, delete each with `-Lid` then emit the new content as `+TEXT` lines. Do not stack `Lid=X` over a contiguous range it requires the new block to match the old line count and silently corrupts the file when they differ.
28
+ - Cursor-only ops (`^`, `$`, `@Lid`, `^Lid`) reposition without modifying. To insert anything you **MUST** follow them with `+TEXT` (or `+` for a blank).
29
+ - TEXT in `+TEXT`, `Lid=TEXT`, and `\TEXT` is literal line content, INCLUDING leading whitespace. You **MUST NOT** trim or re-indent it.
30
+ - Consecutive `+TEXT` ops produce consecutive lines in the order written. You **MUST NOT** separate them with a stray `+` unless you intend to insert a blank line.
31
+ - `Lid=TEXT` rewrites ONE line. To rewrite K adjacent lines, you **MUST** use `LidA..LidB=FIRST_LINE` followed immediately by `\NEXT_LINE` continuation lines (canonical form for any block replacement). You **MUST** use bare `\` for blank replacement lines.
32
+ - You **MUST** prefix every replacement continuation line with `\`, especially when the replacement line starts with edit syntax characters such as `#`, `+`, `-`, `@`, `$`, `^`, `!`, or a Lid-shaped token.
33
+ - `\TEXT` **MUST** appear only immediately after an active `Lid=…` or `LidA..LidB=…` replacement. It **MUST NOT** be used as a general insert operator.
34
+ - A `\TEXT` line **MUST** be the immediate continuation of a `Lid=…` or `LidA..LidB=…` op on the line above (or another `\` line rooted in one). If the line above is `+TEXT`, a bare Lid, a cursor op, or whitespace, the `\` is invalid and the tool will not interpret it as part of a replacement.
35
+ - The legacy `-LidA..LidB` + `+TEXT…` block-rewrite form also works.
36
+ - To insert ABOVE a line, you **MUST** use `^Lid` then `+TEXT`. To insert above line 1, you **MUST** use `^` (BOF) then `+TEXT`. To insert below a line, you **MUST** use `@Lid` then `+TEXT`.
37
+ - Multiple `---PATH` sections **MAY** appear in one input; each section is applied in order.
38
+ - `!rm` / `!mv DEST` **MUST NOT** be combined with line edits in the same section.
39
+ - Lids contain a content hash. If a line has changed since you read it, the tool rejects the edit and shows the current content; you **MUST** re-read and retry with fresh Lids. Small drift (≤5 lines) where the original hash still matches a nearby line auto-rebases with a warning. Larger shifts may show a hash-only candidate, but two-letter hashes collide; verify surrounding content or re-read before using it.
40
+ - After `+TEXT` (or `+`) the cursor advances past the inserted line, so consecutive `+TEXT` ops stack in order. After `Lid=TEXT` the cursor sits on the modified anchor; after `-Lid` it sits on the slot the deleted line vacated. You **MUST** use a fresh `@Lid` / `^Lid` / `^` / `$` to reposition.
41
+ - The tool is syntax-blind: it will not check brackets, indentation, table column counts, or fence integrity. You **MUST** verify indentation-sensitive or structured files after editing (Python, Markdown tables/fences).
42
+ - A section whose PATH does not yet exist creates the file from your `+TEXT` lines (use `^` or `$` then `+TEXT…`). No separate "create file" op is needed.
24
43
  </rules>
25
44
 
26
45
  <case file="a.ts">
@@ -33,11 +52,11 @@ Lid=X replace whole line with X; `Lid=` blanks it out
33
52
  </case>
34
53
 
35
54
  <examples>
36
- # Replace line
55
+ # Replace one line (preserve the leading tab from the original)
37
56
  ---a.ts
38
57
  {{hrefr 5}}= return clean.trim().toUpperCase();
39
58
 
40
- # Rewrite multiple adjacent lines (delete the old, insert the new)
59
+ # Rewrite multiple adjacent lines (delete each, then insert new content)
41
60
  ---a.ts
42
61
  -{{hrefr 3}}
43
62
  -{{hrefr 4}}
@@ -47,48 +66,84 @@ Lid=X replace whole line with X; `Lid=` blanks it out
47
66
  + return (name || DEF).trim().toUpperCase();
48
67
  +}
49
68
 
50
- # Append after
69
+ # Same rewrite using a range (equivalent to four `-Lid` lines)
51
70
  ---a.ts
52
- @{{hrefr 4}}
53
- + const suffix = "";
71
+ -{{hrefr 3}}..{{hrefr 6}}
72
+ +export function label(name: string): string {
73
+ + return (name || DEF).trim().toUpperCase();
74
+ +}
54
75
 
55
- # Delete a line
76
+ # Replace a contiguous range with one line (range-replace shorthand)
56
77
  ---a.ts
57
- -{{hrefr 2}}
78
+ {{hrefr 3}}..{{hrefr 6}}=export const label = (name: string) => (name || DEF).trim().toUpperCase();
58
79
 
59
- # Prepend and append
80
+ # Replace a contiguous range with multiple lines (continuation form)
60
81
  ---a.ts
61
- $
82
+ {{hrefr 3}}..{{hrefr 6}}=export function label(name: string): string {
83
+ \ return (name || DEF).trim().toUpperCase();
84
+ \}
85
+
86
+ # Replace a block with a longer multi-line block, including blank lines (canonical form for refactors)
87
+ ---a.ts
88
+ {{hrefr 3}}..{{hrefr 6}}=/** Format a display label, falling back to DEF when empty. */
89
+ \export function label(name: string): string {
90
+ \ const clean = (name || DEF).trim();
91
+ \
92
+ \ if (clean.length === 0) return DEF;
93
+ \ return clean.toUpperCase();
94
+ \}
95
+
96
+ # Insert ABOVE a line
97
+ ---a.ts
98
+ ^{{hrefr 5}}
99
+ + const debug = false;
100
+
101
+ # Insert BELOW a line
102
+ ---a.ts
103
+ @{{hrefr 4}}
104
+ + const debug = false;
105
+
106
+ # Insert above the first line (use BOF)
107
+ ---a.ts
108
+ ^
62
109
  +// Copyright (c) 2026
63
110
  +
64
- ^
111
+
112
+ # Append at end of file
113
+ ---a.ts
114
+ $
65
115
  +export { DEF };
66
116
 
67
- # File ops
117
+ # Delete a single line
68
118
  ---a.ts
69
- !rm
70
- ---b.ts
71
- !mv a.ts
119
+ -{{hrefr 2}}
72
120
 
73
- # Wrong: `@Lid=TEXT` is not replacement syntax
121
+ # Delete the file (no other ops in the section)
74
122
  ---a.ts
75
- @{{hrefr 5}}= return clean.trim().toUpperCase();
123
+ !rm
76
124
 
77
- # Wrong: do not split `Lid=TEXT` across lines
125
+ # Rename a file
78
126
  ---a.ts
79
- {{hrefr 5}}=
80
- return clean.trim().toUpperCase();
127
+ !mv b.ts
81
128
 
82
- # Wrong: do not replace by deleting then adding
129
+ # Multi-file edit in one input
83
130
  ---a.ts
84
- -{{hrefr 5}}
85
- +{{hrefr 5}}= return clean.trim().toUpperCase();
131
+ {{hrefr 1}}=const DEF = "user";
132
+ ---other.ts
133
+ $
134
+ +// new footer
86
135
  </examples>
87
136
 
88
137
  <critical>
89
- - Copy Lids **EXACTLY** from prior tool output. Never guess, shorten, or omit the letters.
90
- - Only emit lines that change. Never repeat unchanged context anchors imply it.
91
- - This is **NOT** unified diff. Never send `@@`, `-OLD` / `+NEW` pairs, or unchanged context.
92
- - Never split `Lid=TEXT` across two physical lines.
93
- - Never stack `Lid=X` over a contiguous range. Use `-Lid`+`+TEXT` for block rewrites.
138
+ - You **MUST** copy Lids EXACTLY from the latest read/grep output. You **MUST NOT** guess, shorten, drop letters, or invent line numbers.
139
+ - Current/added preview lines include fresh `LINE+hash|content` anchors. Removed preview lines show deleted content and **MUST NOT** be reused as anchors.
140
+ - You **MUST** emit only lines that change. You **MUST NOT** echo unchanged context; the anchor implies position.
141
+ - You **MUST NOT** write `Lid=<sameTextThatIsAlreadyOnThatLine>`; the tool reports a no-op (no change applied). Emit `Lid=TEXT` only when TEXT differs.
142
+ - A line of the form `Lid|content` (a Lid, then `|`, then text, with NO leading `+`/`-`/`^`/`@`/`\`/`=`/`..`) is **FORBIDDEN**. That shape only appears in `read`/`grep` output as an anchor for *you*; it is never an edit op. If you copy a `Lid|content` line verbatim from a read into a patch, you have made an error — every edit op must start with `+`, `-`, `^`, `@`, `\`, `$`, `!`, or a Lid immediately followed by `=` or `..`.
143
+ - To replace a contiguous block with new content, the canonical form is `LidA..LidB=FIRST_LINE` + `\NEXT_LINE…`. You **MUST NOT** write the old block and then the new block — that is unified-diff thinking and the tool does not understand it. If you find yourself emitting pre-image lines (with or without operators) before your new content, STOP and rewrite the section as a single range-replace.
144
+ - TEXT after `=`, `+`, or `\` includes leading whitespace verbatim. You **MUST NOT** trim or re-indent it.
145
+ - This is NOT unified diff. You **MUST NOT** write `@@` headers, `-OLD`/`+NEW` pairs, context lines, or `+Lid|…` (bad: `+5th|new text`; good: `5th=new text`).
146
+ - You **MUST NOT** split `Lid=TEXT` across two physical lines.
147
+ - For a contiguous range replacement, you **MAY** use either `Lid=FIRST_LINE` + `\NEXT_LINE…` (extends one anchor) or `LidA..LidB=FIRST_LINE` + `\NEXT_LINE…` (collapses an existing range), or fall back to `-LidA..LidB` + `+TEXT…` (delete + insert).
148
+ - The tool is syntax-blind. Indentation, brackets, fences, table widths — you remain responsible.
94
149
  </critical>
@@ -1,40 +1,6 @@
1
- Signals plan completion, requests user approval, and provides the final plan title for handoff.
1
+ Submits a finalized implementation plan for user approval.
2
2
 
3
- <conditions>
4
- Use when:
5
- - Plan written to `local://PLAN.md`
6
- - No unresolved questions about requirements or approach
7
- - Ready for user review and approval
8
- </conditions>
9
-
10
- <instruction>
11
- - You **MUST** write plan to plan file BEFORE calling this tool
12
- - Tool reads plan from file—does not take plan content as parameter
13
- - You **MUST** provide a `title` argument for the final plan artifact (example: `WP_MIGRATION_PLAN`)
14
- - `.md` is optional in `title`; it is appended automatically when omitted
15
- - User sees plan contents when reviewing
16
- </instruction>
17
-
18
- <output>
19
- Presents plan to user for approval. If approved, plan mode exits with full tool access restored and the plan is renamed to `local://<title>.md`.
20
- </output>
21
-
22
- <examples>
23
- # Ready
24
- Plan complete at local://PLAN.md, no open questions.
25
- → Call `exit_plan_mode` with `{ "title": "WP_MIGRATION_PLAN" }`
26
- # Unclear
27
- Unsure about auth method (OAuth vs JWT).
28
- → Use `ask` first to clarify, then call `exit_plan_mode`
29
- </examples>
30
-
31
- <avoid>
32
- - **MUST NOT** call before plan is written to file
33
- - **MUST NOT** omit `title`
34
- - **MUST NOT** use `ask` to request plan approval (this tool does that)
35
- - **MUST NOT** call after pure research tasks (no implementation planned)
36
- </avoid>
37
-
38
- <critical>
39
- You **MUST** only use when planning implementation steps. Research tasks (searching, reading, understanding) do not need this tool.
40
- </critical>
3
+ Write the plan to `local://PLAN.md` first, then call this with `title` (e.g. `WP_MIGRATION_PLAN`); on approval the file is renamed to `local://<title>.md` and full tool access is restored.
4
+ - Use only after planning implementation steps; not for pure research.
5
+ - **MUST NOT** call before the plan file exists.
6
+ - **MUST NOT** use `ask` to request plan approval — this tool does that.
@@ -17,8 +17,7 @@ Interacts with Language Server Protocol servers for code intelligence.
17
17
  <parameters>
18
18
  - `file`: File path, glob pattern (e.g. `src/**/*.ts`), or `"*"` for workspace scope. Globs are expanded locally before dispatch. `"*"` routes `diagnostics`/`symbols`/`reload` to their workspace-wide form.
19
19
  - `line`: 1-indexed line number for position-based actions
20
- - `symbol`: Substring on the target line used to resolve column automatically
21
- - `occurrence`: 1-indexed match index when `symbol` appears multiple times on the same line
20
+ - `symbol`: Substring on the target line used to resolve column automatically. Append `#N` to pick the Nth occurrence on that line (1-indexed; default 1) — e.g. `foo#2` selects the second `foo`.
22
21
  - `query`: Symbol search query, code-action kind filter (list mode), or code-action selector (apply mode)
23
22
  - `new_name`: Required for rename
24
23
  - `apply`: Apply edits for rename/code_actions (default true for rename, list mode for code_actions unless explicitly true)
@@ -29,7 +28,7 @@ Interacts with Language Server Protocol servers for code intelligence.
29
28
  - Requires running LSP server for target language
30
29
  - Some operations require file to be saved to disk
31
30
  - Glob expansion samples up to 20 files per request; use `file: "*"` for broader coverage
32
- - When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `occurrence` values return an explicit error instead of silently falling back
31
+ - When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `#N` occurrence selectors return an explicit error instead of silently falling back
33
32
  </caution>
34
33
 
35
34
  <critical>
@@ -0,0 +1,16 @@
1
+ Run a recipe / script / target from the project's task runners.
2
+
3
+ <instruction>
4
+ - `op` is a single string: task name plus any args, e.g. `{op: "test"}` or `{op: "build --release"}`.
5
+ - In monorepos, package and Cargo target tasks are namespaced with `/`, e.g. `{op: "pkg-a/test"}` or `{op: "crate/bin/server"}`.
6
+ {{#if hasMultipleRunners}}- When the same task name exists in more than one runner, prefix with the runner id, e.g. `{op: "{{ambiguityExampleRunner}}:{{ambiguityExampleTask}}"}`. The available runner ids are: {{#each runners}}`{{id}}`{{#unless @last}}, {{/unless}}{{/each}}.
7
+ {{/if}}- Runs in the session's cwd. Output and exit code are returned in the same shape as `bash`.
8
+ </instruction>
9
+
10
+ {{#each runners}}
11
+ <runner id="{{id}}" label="{{label}}" command="{{commandPrefix}}">
12
+ {{#each tasks}}
13
+ - `{{name}}{{#if paramSig}} {{paramSig}}{{/if}}`{{#if doc}} — {{doc}}{{/if}}{{#if command}} (`{{command}}`{{#if cwd}} in `{{cwd}}`{{/if}}){{/if}}
14
+ {{/each}}
15
+ </runner>
16
+ {{/each}}