@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.2

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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. package/src/utils/prompt-format.ts +0 -170
@@ -1,12 +1,17 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { getProjectDir, getProjectPromptsDir, getPromptsDir, logger } from "@oh-my-pi/pi-utils";
4
- import Handlebars from "handlebars";
5
- import { computeLineHash } from "../patch/hashline";
3
+ import { type ChunkAnchorStyle, formatAnchor } from "@oh-my-pi/pi-natives";
4
+ import {
5
+ getProjectDir,
6
+ getProjectPromptsDir,
7
+ getPromptsDir,
8
+ logger,
9
+ parseFrontmatter,
10
+ prompt,
11
+ } from "@oh-my-pi/pi-utils";
12
+ import { computeLineHash } from "../edit/line-hash";
6
13
  import { jtdToTypeScript } from "../tools/jtd-to-typescript";
7
14
  import { parseCommandArgs, substituteArgs } from "../utils/command-args";
8
- import { parseFrontmatter } from "../utils/frontmatter";
9
- import { formatPromptContent } from "../utils/prompt-format";
10
15
 
11
16
  /**
12
17
  * Represents a prompt template loaded from a markdown file
@@ -18,215 +23,7 @@ export interface PromptTemplate {
18
23
  source: string; // e.g., "(user)", "(project)", "(project:frontend)"
19
24
  }
20
25
 
21
- export interface TemplateContext extends Record<string, unknown> {
22
- args?: string[];
23
- ARGUMENTS?: string;
24
- arguments?: string;
25
- }
26
-
27
- const handlebars = Handlebars.create();
28
-
29
- handlebars.registerHelper("arg", function (this: TemplateContext, index: number | string): string {
30
- const args = this.args ?? [];
31
- const parsedIndex = typeof index === "number" ? index : Number.parseInt(index, 10);
32
- if (!Number.isFinite(parsedIndex)) return "";
33
- const zeroBased = parsedIndex - 1;
34
- if (zeroBased < 0) return "";
35
- return args[zeroBased] ?? "";
36
- });
37
-
38
- /**
39
- * {{#list items prefix="- " suffix="" join="\n"}}{{this}}{{/list}}
40
- * Renders an array with customizable prefix, suffix, and join separator.
41
- * Note: Use \n in join for newlines (will be unescaped automatically).
42
- */
43
- handlebars.registerHelper(
44
- "list",
45
- function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
46
- if (!Array.isArray(context) || context.length === 0) return "";
47
- const prefix = (options.hash.prefix as string) ?? "";
48
- const suffix = (options.hash.suffix as string) ?? "";
49
- const rawSeparator = (options.hash.join as string) ?? "\n";
50
- const separator = rawSeparator.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
51
- return context.map(item => `${prefix}${options.fn(item)}${suffix}`).join(separator);
52
- },
53
- );
54
-
55
- /**
56
- * {{join array ", "}}
57
- * Joins an array with a separator (default: ", ").
58
- */
59
- handlebars.registerHelper("join", (context: unknown[], separator?: unknown): string => {
60
- if (!Array.isArray(context)) return "";
61
- const sep = typeof separator === "string" ? separator : ", ";
62
- return context.join(sep);
63
- });
64
-
65
- /**
66
- * {{default value "fallback"}}
67
- * Returns the value if truthy, otherwise returns the fallback.
68
- */
69
- handlebars.registerHelper("default", (value: unknown, defaultValue: unknown): unknown => value || defaultValue);
70
-
71
- /**
72
- * {{pluralize count "item" "items"}}
73
- * Returns "1 item" or "5 items" based on count.
74
- */
75
- handlebars.registerHelper(
76
- "pluralize",
77
- (count: number, singular: string, plural: string): string => `${count} ${count === 1 ? singular : plural}`,
78
- );
79
-
80
- /**
81
- * {{#when value "==" compare}}...{{else}}...{{/when}}
82
- * Conditional block with comparison operators: ==, ===, !=, !==, >, <, >=, <=
83
- */
84
- handlebars.registerHelper(
85
- "when",
86
- function (this: unknown, lhs: unknown, operator: string, rhs: unknown, options: Handlebars.HelperOptions): string {
87
- const ops: Record<string, (a: unknown, b: unknown) => boolean> = {
88
- "==": (a, b) => a === b,
89
- "===": (a, b) => a === b,
90
- "!=": (a, b) => a !== b,
91
- "!==": (a, b) => a !== b,
92
- ">": (a, b) => (a as number) > (b as number),
93
- "<": (a, b) => (a as number) < (b as number),
94
- ">=": (a, b) => (a as number) >= (b as number),
95
- "<=": (a, b) => (a as number) <= (b as number),
96
- };
97
- const fn = ops[operator];
98
- if (!fn) return options.inverse(this);
99
- return fn(lhs, rhs) ? options.fn(this) : options.inverse(this);
100
- },
101
- );
102
-
103
- /**
104
- * {{#ifAny a b c}}...{{else}}...{{/ifAny}}
105
- * True if any argument is truthy.
106
- */
107
- handlebars.registerHelper("ifAny", function (this: unknown, ...args: unknown[]): string {
108
- const options = args.pop() as Handlebars.HelperOptions;
109
- return args.some(Boolean) ? options.fn(this) : options.inverse(this);
110
- });
111
-
112
- /**
113
- * {{#ifAll a b c}}...{{else}}...{{/ifAll}}
114
- * True if all arguments are truthy.
115
- */
116
- handlebars.registerHelper("ifAll", function (this: unknown, ...args: unknown[]): string {
117
- const options = args.pop() as Handlebars.HelperOptions;
118
- return args.every(Boolean) ? options.fn(this) : options.inverse(this);
119
- });
120
-
121
- /**
122
- * {{#table rows headers="Col1|Col2"}}{{col1}}|{{col2}}{{/table}}
123
- * Generates a markdown table from an array of objects.
124
- */
125
- handlebars.registerHelper(
126
- "table",
127
- function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
128
- if (!Array.isArray(context) || context.length === 0) return "";
129
- const headersStr = options.hash.headers as string | undefined;
130
- const headers = headersStr?.split("|") ?? [];
131
- const separator = headers.map(() => "---").join(" | ");
132
- const headerRow = headers.length > 0 ? `| ${headers.join(" | ")} |\n| ${separator} |\n` : "";
133
- const rows = context.map(item => `| ${options.fn(item).trim()} |`).join("\n");
134
- return headerRow + rows;
135
- },
136
- );
137
-
138
- /**
139
- * {{#codeblock lang="diff"}}...{{/codeblock}}
140
- * Wraps content in a fenced code block.
141
- */
142
- handlebars.registerHelper("codeblock", function (this: unknown, options: Handlebars.HelperOptions): string {
143
- const lang = (options.hash.lang as string) ?? "";
144
- const content = options.fn(this).trim();
145
- return `\`\`\`${lang}\n${content}\n\`\`\``;
146
- });
147
-
148
- /**
149
- * {{#xml "tag"}}content{{/xml}}
150
- * Wraps content in XML-style tags. Returns empty string if content is empty.
151
- */
152
- handlebars.registerHelper("xml", function (this: unknown, tag: string, options: Handlebars.HelperOptions): string {
153
- const content = options.fn(this).trim();
154
- if (!content) return "";
155
- return `<${tag}>\n${content}\n</${tag}>`;
156
- });
157
-
158
- /**
159
- * {{escapeXml value}}
160
- * Escapes XML special characters: & < > "
161
- */
162
- handlebars.registerHelper("escapeXml", (value: unknown): string => {
163
- if (value == null) return "";
164
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
165
- });
166
-
167
- /**
168
- * {{len array}}
169
- * Returns the length of an array or string.
170
- */
171
- handlebars.registerHelper("len", (value: unknown): number => {
172
- if (Array.isArray(value)) return value.length;
173
- if (typeof value === "string") return value.length;
174
- return 0;
175
- });
176
-
177
- /**
178
- * {{add a b}}
179
- * Adds two numbers.
180
- */
181
- handlebars.registerHelper("add", (a: number, b: number): number => (a ?? 0) + (b ?? 0));
182
-
183
- /**
184
- * {{sub a b}}
185
- * Subtracts b from a.
186
- */
187
- handlebars.registerHelper("sub", (a: number, b: number): number => (a ?? 0) - (b ?? 0));
188
-
189
- /**
190
- * {{#has collection item}}...{{else}}...{{/has}}
191
- * Checks if an array includes an item or if a Set/Map has a key.
192
- */
193
- handlebars.registerHelper(
194
- "has",
195
- function (this: unknown, collection: unknown, item: unknown, options: Handlebars.HelperOptions): string {
196
- let found = false;
197
- if (Array.isArray(collection)) {
198
- found = collection.includes(item);
199
- } else if (collection instanceof Set) {
200
- found = collection.has(item);
201
- } else if (collection instanceof Map) {
202
- found = collection.has(item);
203
- } else if (collection && typeof collection === "object") {
204
- if (typeof item === "string" || typeof item === "number" || typeof item === "symbol") {
205
- found = item in collection;
206
- }
207
- }
208
- return found ? options.fn(this) : options.inverse(this);
209
- },
210
- );
211
-
212
- /**
213
- * {{includes array item}}
214
- * Returns true if array includes item. For use in other helpers.
215
- */
216
- handlebars.registerHelper("includes", (collection: unknown, item: unknown): boolean => {
217
- if (Array.isArray(collection)) return collection.includes(item);
218
- if (collection instanceof Set) return collection.has(item);
219
- if (collection instanceof Map) return collection.has(item);
220
- return false;
221
- });
222
-
223
- /**
224
- * {{not value}}
225
- * Returns logical NOT of value. For use in subexpressions.
226
- */
227
- handlebars.registerHelper("not", (value: unknown): boolean => !value);
228
-
229
- handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => {
26
+ prompt.registerHelper("jtdToTypeScript", (schema: unknown): string => {
230
27
  try {
231
28
  return jtdToTypeScript(schema);
232
29
  } catch {
@@ -234,8 +31,6 @@ handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => {
234
31
  }
235
32
  });
236
33
 
237
- handlebars.registerHelper("jsonStringify", (value: unknown): string => JSON.stringify(value));
238
-
239
34
  /**
240
35
  * Renders a section separator:
241
36
  *
@@ -247,7 +42,7 @@ export function sectionSeparator(name: string): string {
247
42
  return `\n\n═══════════${name}═══════════\n`;
248
43
  }
249
44
 
250
- handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
45
+ prompt.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
251
46
 
252
47
  function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
253
48
  const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
@@ -261,7 +56,7 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
261
56
  * {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
262
57
  * Returns `"lineNum#hash"` using the actual hash algorithm.
263
58
  */
264
- handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string => {
59
+ prompt.registerHelper("href", (lineNum: unknown, content: unknown): string => {
265
60
  const { ref } = formatHashlineRef(lineNum, content);
266
61
  return JSON.stringify(ref);
267
62
  });
@@ -270,11 +65,40 @@ handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string =
270
65
  * {{hline lineNum "content"}} — format a full read-style line with prefix.
271
66
  * Returns `"lineNum#hash:content"`.
272
67
  */
273
- handlebars.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
68
+ prompt.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
274
69
  const { ref, text } = formatHashlineRef(lineNum, content);
275
70
  return `${ref}:${text}`;
276
71
  });
277
72
 
73
+ /**
74
+ * {{anchor name checksum}} — render a branch anchor tag using the current anchor style.
75
+ * Style is resolved from the template context (`anchorStyle`) or defaults to "full".
76
+ */
77
+ prompt.registerHelper("anchor", function (this: prompt.TemplateContext, name: string, checksum: string): string {
78
+ const style = (this.anchorStyle as ChunkAnchorStyle) ?? "full";
79
+ return formatAnchor(name, checksum, style);
80
+ });
81
+
82
+ /**
83
+ * {{sel "parent_Name.child_Name"}} — render a chunk path for `sel` fields in examples.
84
+ * In `full` style the path is returned as-is (`class_Server.fn_start`).
85
+ * In `kind` style each segment is trimmed to its kind prefix (`class.fn`).
86
+ * In `bare` style the path is omitted (the model uses only `crc` to identify chunks).
87
+ */
88
+ prompt.registerHelper("sel", function (this: prompt.TemplateContext, chunkPath: string): string {
89
+ const style = (this.anchorStyle as ChunkAnchorStyle) ?? "full";
90
+ if (style === "full") return chunkPath;
91
+ if (style === "bare") return "";
92
+ // kind: trim each segment to its kind prefix (before the first `_`)
93
+ return chunkPath
94
+ .split(".")
95
+ .map(seg => {
96
+ const idx = seg.indexOf("_");
97
+ return idx === -1 ? seg : seg.slice(0, idx);
98
+ })
99
+ .join(".");
100
+ });
101
+
278
102
  const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
279
103
  const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/;
280
104
 
@@ -297,12 +121,6 @@ export function appendInlineArgsFallback(
297
121
  return `${rendered}\n\n${argsText}`;
298
122
  }
299
123
 
300
- export function renderPromptTemplate(template: string, context: TemplateContext = {}): string {
301
- const compiled = handlebars.compile(template, { noEscape: true, strict: false });
302
- const rendered = compiled(context ?? {});
303
- return formatPromptContent(rendered, { renderPhase: "post-render" });
304
- }
305
-
306
124
  /**
307
125
  * Recursively scan a directory for .md files (and symlinks to .md files) and load them as prompt templates
308
126
  */
@@ -429,7 +247,7 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
429
247
  const argsText = args.join(" ");
430
248
  const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(template.content);
431
249
  const substituted = substituteArgs(template.content, args);
432
- const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
250
+ const rendered = prompt.render(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
433
251
  return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
434
252
  }
435
253
 
@@ -55,8 +55,10 @@ async function executeCommand(commandConfig: string): Promise<string | undefined
55
55
  async function runShellCommand(command: string, timeoutMs: number): Promise<string | undefined> {
56
56
  try {
57
57
  let output = "";
58
- const result = await executeShell({ command, timeoutMs }, chunk => {
59
- output += chunk;
58
+ const result = await executeShell({ command, timeoutMs }, (err, chunk) => {
59
+ if (!err) {
60
+ output += chunk;
61
+ }
60
62
  });
61
63
  if (result.timedOut || result.exitCode !== 0) {
62
64
  return undefined;
@@ -949,12 +949,12 @@ export const SETTINGS_SCHEMA = {
949
949
  // Edit tool
950
950
  "edit.mode": {
951
951
  type: "enum",
952
- values: ["replace", "patch", "hashline"] as const,
952
+ values: ["replace", "patch", "hashline", "chunk"] as const,
953
953
  default: "hashline",
954
954
  ui: {
955
955
  tab: "editing",
956
956
  label: "Edit Mode",
957
- description: "Select the edit tool variant (replace, patch, or hashline)",
957
+ description: "Select the edit tool variant (replace, patch, hashline, or chunk)",
958
958
  },
959
959
  },
960
960
 
@@ -1030,6 +1030,38 @@ export const SETTINGS_SCHEMA = {
1030
1030
  },
1031
1031
  },
1032
1032
 
1033
+ "read.prosechunks": {
1034
+ type: "boolean",
1035
+ default: false,
1036
+ ui: {
1037
+ tab: "editing",
1038
+ label: "Prose Chunks",
1039
+ description: "Enable chunk rendering for prose files in chunk edit mode",
1040
+ },
1041
+ },
1042
+
1043
+ "read.explorechunks": {
1044
+ type: "boolean",
1045
+ default: false,
1046
+ ui: {
1047
+ tab: "editing",
1048
+ label: "Explore Chunks",
1049
+ description: "Show chunk tree without checksums for read-only agents like explore",
1050
+ },
1051
+ },
1052
+
1053
+ "read.anchorstyle": {
1054
+ type: "enum",
1055
+ values: ["full", "kind", "bare"],
1056
+ default: "full",
1057
+ ui: {
1058
+ tab: "editing",
1059
+ label: "Anchor Style",
1060
+ description: "Render chunk anchors with full names, kind prefixes, or checksum-only tags",
1061
+ submenu: true,
1062
+ },
1063
+ },
1064
+
1033
1065
  // LSP
1034
1066
  "lsp.enabled": {
1035
1067
  type: "boolean",
@@ -1214,6 +1246,16 @@ export const SETTINGS_SCHEMA = {
1214
1246
  },
1215
1247
  },
1216
1248
 
1249
+ "debug.enabled": {
1250
+ type: "boolean",
1251
+ default: true,
1252
+ ui: {
1253
+ tab: "tools",
1254
+ label: "Debug",
1255
+ description: "Enable the debug tool for DAP-based debugging",
1256
+ },
1257
+ },
1258
+
1217
1259
  "calc.enabled": {
1218
1260
  type: "boolean",
1219
1261
  default: false,
@@ -1642,6 +1684,16 @@ export const SETTINGS_SCHEMA = {
1642
1684
 
1643
1685
  "commit.changelogMaxDiffChars": { type: "number", default: 120000 },
1644
1686
 
1687
+ "dev.autoqa": {
1688
+ type: "boolean",
1689
+ default: false,
1690
+ ui: {
1691
+ tab: "tools",
1692
+ label: "Auto QA",
1693
+ description: "Enable automated tool issue reporting (report_tool_issue) for all agents",
1694
+ },
1695
+ },
1696
+
1645
1697
  "thinkingBudgets.minimal": { type: "number", default: 1024 },
1646
1698
 
1647
1699
  "thinkingBudgets.low": { type: "number", default: 2048 },
@@ -13,22 +13,15 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import {
17
- getAgentDbPath,
18
- getAgentDir,
19
- getProjectDir,
20
- isEnoent,
21
- logger,
22
- procmgr,
23
- setDefaultTabWidth,
24
- } from "@oh-my-pi/pi-utils";
16
+ import { setDefaultTabWidth } from "@oh-my-pi/pi-natives";
17
+ import { getAgentDbPath, getAgentDir, getProjectDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
25
18
  import { YAML } from "bun";
26
19
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
27
20
  import type { ModelRole } from "../config/model-registry";
28
21
  import { loadCapability } from "../discovery";
29
22
  import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
30
- import { type EditMode, normalizeEditMode } from "../patch";
31
23
  import { AgentStorage } from "../session/agent-storage";
24
+ import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
32
25
  import { withFileLock } from "./file-lock";
33
26
  import {
34
27
  type BashInterceptorRule,
@@ -68,15 +61,6 @@ export interface SettingsOptions {
68
61
  // Path Utilities
69
62
  // ═══════════════════════════════════════════════════════════════════════════
70
63
 
71
- /**
72
- * Parse a dotted path into segments.
73
- * "compaction.enabled" → ["compaction", "enabled"]
74
- * "theme.dark" → ["theme", "dark"]
75
- */
76
- function parsePath(path: string): string[] {
77
- return path.split(".");
78
- }
79
-
80
64
  /**
81
65
  * Get a nested value from an object by path segments.
82
66
  */
@@ -144,7 +128,7 @@ export class Settings {
144
128
 
145
129
  if (options.overrides) {
146
130
  for (const [key, value] of Object.entries(options.overrides)) {
147
- setByPath(this.#overrides, parsePath(key), value);
131
+ setByPath(this.#overrides, key.split("."), value);
148
132
  }
149
133
  }
150
134
  }
@@ -207,7 +191,7 @@ export class Settings {
207
191
  * Returns the merged value from global + project + overrides, or the default.
208
192
  */
209
193
  get<P extends SettingPath>(path: P): SettingValue<P> {
210
- const segments = parsePath(path);
194
+ const segments = path.split(".");
211
195
  const value = getByPath(this.#merged, segments);
212
196
  if (value !== undefined) {
213
197
  return value as SettingValue<P>;
@@ -222,7 +206,7 @@ export class Settings {
222
206
  */
223
207
  set<P extends SettingPath>(path: P, value: SettingValue<P>): void {
224
208
  const prev = this.get(path);
225
- const segments = parsePath(path);
209
+ const segments = path.split(".");
226
210
  setByPath(this.#global, segments, value);
227
211
  this.#modified.add(path);
228
212
  this.#rebuildMerged();
@@ -239,7 +223,7 @@ export class Settings {
239
223
  * Apply runtime overrides (not persisted).
240
224
  */
241
225
  override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
242
- const segments = parsePath(path);
226
+ const segments = path.split(".");
243
227
  setByPath(this.#overrides, segments, value);
244
228
  this.#rebuildMerged();
245
229
  }
@@ -248,7 +232,7 @@ export class Settings {
248
232
  * Clear a runtime override.
249
233
  */
250
234
  clearOverride(path: SettingPath): void {
251
- const segments = parsePath(path);
235
+ const segments = path.split(".");
252
236
  let current = this.#overrides;
253
237
  for (let i = 0; i < segments.length - 1; i++) {
254
238
  const segment = segments[i];
@@ -276,6 +260,21 @@ export class Settings {
276
260
  }
277
261
  }
278
262
 
263
+ async cloneForCwd(cwd: string): Promise<Settings> {
264
+ const cloned = new Settings({
265
+ cwd,
266
+ agentDir: this.#agentDir,
267
+ inMemory: !this.#persist,
268
+ });
269
+ cloned.#storage = this.#storage;
270
+ cloned.#global = structuredClone(this.#global);
271
+ cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
272
+ cloned.#overrides = structuredClone(this.#overrides);
273
+ cloned.#rebuildMerged();
274
+ cloned.#fireAllHooks();
275
+ return cloned;
276
+ }
277
+
279
278
  // ─────────────────────────────────────────────────────────────────────────
280
279
  // Accessors
281
280
  // ─────────────────────────────────────────────────────────────────────────
@@ -320,7 +319,7 @@ export class Settings {
320
319
 
321
320
  /**
322
321
  * Get the edit variant for a specific model.
323
- * Returns "patch", "replace", "hashline", or null (use global default).
322
+ * Returns "patch", "replace", "hashline", "chunk", or null (use global default).
324
323
  */
325
324
  getEditVariantForModel(model: string | undefined): EditMode | null {
326
325
  if (!model) return null;
@@ -562,7 +561,7 @@ export class Settings {
562
561
 
563
562
  // Apply only our modified paths
564
563
  for (const modPath of modifiedPaths) {
565
- const segments = parsePath(modPath);
564
+ const segments = modPath.split(".");
566
565
  const value = getByPath(this.#global, segments);
567
566
  setByPath(current, segments, value);
568
567
  }