@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
@@ -0,0 +1,213 @@
1
+ import type { ToolChoice } from "@oh-my-pi/pi-ai";
2
+
3
+ // ── Callback types ──────────────────────────────────────────────────────────
4
+
5
+ export interface ResolveInfo {
6
+ /** The ToolChoice that was served to the LLM. */
7
+ choice: ToolChoice;
8
+ }
9
+
10
+ export interface RejectInfo {
11
+ /** The ToolChoice that was yielded but never (or unsuccessfully) served. */
12
+ choice: ToolChoice;
13
+ reason: "aborted" | "error" | "cleared" | "removed";
14
+ }
15
+
16
+ /** "requeue" replays the lost yield next turn; "drop" (or void/undefined) discards it. */
17
+ export type RejectOutcome = "requeue" | "drop";
18
+
19
+ export interface DirectiveCallbacks {
20
+ /** Fires when the yield was served (LLM call completed). The directive is consumed. */
21
+ onResolved?: (info: ResolveInfo) => void;
22
+ /**
23
+ * Fires when the yield is being discarded. Return "requeue" to replay the
24
+ * same value at the head of the queue for the next turn. Default: "drop".
25
+ */
26
+ onRejected?: (info: RejectInfo) => RejectOutcome | undefined;
27
+ /**
28
+ * Handler invoked when the model actually calls the forced tool. The queue
29
+ * directive carries the real execution logic; the tool's own execute() is
30
+ * bypassed. Returns the tool result directly.
31
+ */
32
+ onInvoked?: (input: unknown) => Promise<unknown> | unknown;
33
+ }
34
+
35
+ // ── Directive ───────────────────────────────────────────────────────────────
36
+
37
+ export interface ToolChoiceDirective {
38
+ generator: Iterator<ToolChoice>;
39
+ /** Stable label for targeted removal and debugging (e.g. "user-force"). */
40
+ label: string;
41
+ callbacks: DirectiveCallbacks;
42
+ }
43
+
44
+ export interface PushOptions {
45
+ /** Prepend to head instead of appending to tail. Default: false. */
46
+ now?: boolean;
47
+ label?: string;
48
+ /** Lifecycle callbacks for this directive. */
49
+ onResolved?: DirectiveCallbacks["onResolved"];
50
+ onRejected?: DirectiveCallbacks["onRejected"];
51
+ onInvoked?: DirectiveCallbacks["onInvoked"];
52
+ }
53
+
54
+ // ── Generators ──────────────────────────────────────────────────────────────
55
+
56
+ export function* onceGen(choice: ToolChoice): Generator<ToolChoice, void, unknown> {
57
+ yield choice;
58
+ }
59
+
60
+ // ── In-flight state ─────────────────────────────────────────────────────────
61
+
62
+ interface InFlight {
63
+ directive: ToolChoiceDirective;
64
+ yielded: ToolChoice;
65
+ }
66
+
67
+ // ── Queue ───────────────────────────────────────────────────────────────────
68
+
69
+ export class ToolChoiceQueue {
70
+ #queue: ToolChoiceDirective[] = [];
71
+ #inFlight: InFlight | undefined;
72
+ /**
73
+ * Label of the directive whose last yield was resolved this turn.
74
+ * Consumers (e.g. todo reminder suppression) read via consumeLastServedLabel().
75
+ */
76
+ #lastResolvedLabel: string | undefined;
77
+
78
+ // ── Push ──────────────────────────────────────────────────────────────
79
+
80
+ pushOnce(choice: ToolChoice, options?: PushOptions): void {
81
+ this.push(onceGen(choice), options);
82
+ }
83
+
84
+ pushSequence(choices: ToolChoice[], options?: PushOptions): void {
85
+ this.push(choices, options);
86
+ }
87
+
88
+ push(generator: Iterable<ToolChoice>, options?: PushOptions): void {
89
+ const directive: ToolChoiceDirective = {
90
+ generator: generator[Symbol.iterator](),
91
+ label: options?.label ?? "anonymous",
92
+ callbacks: {
93
+ onResolved: options?.onResolved,
94
+ onRejected: options?.onRejected,
95
+ onInvoked: options?.onInvoked,
96
+ },
97
+ };
98
+ if (options?.now) {
99
+ this.#queue.unshift(directive);
100
+ } else {
101
+ this.#queue.push(directive);
102
+ }
103
+ }
104
+
105
+ // ── Consume ───────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Advance the head directive and return its next yield. Records the value
109
+ * as in-flight until resolve() or reject() is called.
110
+ */
111
+ nextToolChoice(): ToolChoice | undefined {
112
+ while (this.#queue.length > 0) {
113
+ const head = this.#queue[0]!;
114
+ const result = head.generator.next();
115
+ if (result.done) {
116
+ this.#queue.shift();
117
+ continue;
118
+ }
119
+ this.#inFlight = { directive: head, yielded: result.value };
120
+ return result.value;
121
+ }
122
+ return undefined;
123
+ }
124
+
125
+ // ── Lifecycle ─────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * The in-flight yield was served — the LLM call completed normally.
129
+ * Fires onResolved, then clears in-flight state. The directive's generator
130
+ * remains in the queue if it has more values to yield.
131
+ */
132
+ resolve(): void {
133
+ const inFlight = this.#inFlight;
134
+ this.#inFlight = undefined;
135
+ if (!inFlight) return;
136
+
137
+ this.#lastResolvedLabel = inFlight.directive.label;
138
+ inFlight.directive.callbacks.onResolved?.({ choice: inFlight.yielded });
139
+ }
140
+
141
+ /**
142
+ * The in-flight yield was not served, or the turn aborted/errored.
143
+ * Fires onRejected to let the caller decide: "requeue" replays the exact
144
+ * lost value at the head of the queue; anything else drops it.
145
+ */
146
+ reject(reason: RejectInfo["reason"]): void {
147
+ const inFlight = this.#inFlight;
148
+ this.#inFlight = undefined;
149
+ if (!inFlight) return;
150
+
151
+ const outcome = inFlight.directive.callbacks.onRejected?.({
152
+ choice: inFlight.yielded,
153
+ reason,
154
+ });
155
+
156
+ if (outcome === "requeue") {
157
+ // Re-queue only the lost yield, not the rest of the sequence. Carry forward
158
+ // onInvoked and onRejected so the replayed yield still executes correctly
159
+ // and can requeue itself again if the next turn also aborts.
160
+ this.#queue.unshift({
161
+ generator: onceGen(inFlight.yielded),
162
+ label: `${inFlight.directive.label}-requeued`,
163
+ callbacks: {
164
+ onInvoked: inFlight.directive.callbacks.onInvoked,
165
+ onRejected: inFlight.directive.callbacks.onRejected,
166
+ },
167
+ });
168
+ }
169
+ }
170
+
171
+ /** True if there is an in-flight yield that hasn't been resolved or rejected. */
172
+ get hasInFlight(): boolean {
173
+ return this.#inFlight !== undefined;
174
+ }
175
+
176
+ /** Peek the in-flight directive's onInvoked handler, if any. */
177
+ peekInFlightInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
178
+ return this.#inFlight?.directive.callbacks.onInvoked;
179
+ }
180
+
181
+ // ── Cleanup ───────────────────────────────────────────────────────────
182
+
183
+ /** Remove all directives with the given label. Rejects in-flight if it matches. */
184
+ removeByLabel(label: string): void {
185
+ if (this.#inFlight?.directive.label === label) {
186
+ this.reject("removed");
187
+ }
188
+ this.#queue = this.#queue.filter(d => d.label !== label);
189
+ }
190
+
191
+ /** Empty the queue and reject any in-flight yield. */
192
+ clear(): void {
193
+ if (this.#inFlight) {
194
+ this.reject("cleared");
195
+ }
196
+ this.#queue = [];
197
+ this.#lastResolvedLabel = undefined;
198
+ }
199
+
200
+ // ── Observation ───────────────────────────────────────────────────────
201
+
202
+ /** Return the label of the most recently resolved directive, then clear it. */
203
+ consumeLastServedLabel(): string | undefined {
204
+ const label = this.#lastResolvedLabel;
205
+ this.#lastResolvedLabel = undefined;
206
+ return label;
207
+ }
208
+
209
+ /** For tests/debug: labels of currently queued directives in order. */
210
+ inspect(): readonly string[] {
211
+ return this.#queue.map(d => d.label);
212
+ }
213
+ }
@@ -55,7 +55,15 @@ interface ParsedBuiltinSlashCommand {
55
55
  interface BuiltinSlashCommandSpec extends BuiltinSlashCommand {
56
56
  aliases?: string[];
57
57
  allowArgs?: boolean;
58
- handle: (command: ParsedBuiltinSlashCommand, runtime: BuiltinSlashCommandRuntime) => Promise<void> | void;
58
+ /**
59
+ * Handle the command. Return a string to pass remaining text through as prompt input.
60
+ * Return void/undefined to consume the input entirely.
61
+ */
62
+ handle: (
63
+ command: ParsedBuiltinSlashCommand,
64
+ runtime: BuiltinSlashCommandRuntime,
65
+ // biome-ignore lint/suspicious/noConfusingVoidType: void needed so handlers returning nothing are assignable
66
+ ) => Promise<string | undefined> | string | void;
59
67
  }
60
68
 
61
69
  export interface BuiltinSlashCommandRuntime {
@@ -69,7 +77,11 @@ function parseBuiltinSlashCommand(text: string): ParsedBuiltinSlashCommand | nul
69
77
  if (!body) return null;
70
78
 
71
79
  const firstWhitespace = body.search(/\s/);
72
- if (firstWhitespace === -1) {
80
+ const firstColon = body.indexOf(":");
81
+ const firstSeparator =
82
+ firstWhitespace === -1 ? firstColon : firstColon === -1 ? firstWhitespace : Math.min(firstWhitespace, firstColon);
83
+
84
+ if (firstSeparator === -1) {
73
85
  return {
74
86
  name: body,
75
87
  args: "",
@@ -78,8 +90,8 @@ function parseBuiltinSlashCommand(text: string): ParsedBuiltinSlashCommand | nul
78
90
  }
79
91
 
80
92
  return {
81
- name: body.slice(0, firstWhitespace),
82
- args: body.slice(firstWhitespace).trim(),
93
+ name: body.slice(0, firstSeparator),
94
+ args: body.slice(firstSeparator + 1).trim(),
83
95
  text,
84
96
  };
85
97
  }
@@ -888,6 +900,37 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
888
900
  runtime.ctx.editor.setText("");
889
901
  },
890
902
  },
903
+ {
904
+ name: "force",
905
+ description: "Force next turn to use a specific tool",
906
+ inlineHint: "<tool-name> [prompt]",
907
+ allowArgs: true,
908
+ handle: (command, runtime) => {
909
+ const spaceIdx = command.args.indexOf(" ");
910
+ const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
911
+ const prompt = spaceIdx === -1 ? "" : command.args.slice(spaceIdx + 1).trim();
912
+
913
+ if (!toolName) {
914
+ runtime.ctx.showError("Usage: /force:<tool-name> [prompt]");
915
+ runtime.ctx.editor.setText("");
916
+ return;
917
+ }
918
+
919
+ try {
920
+ runtime.ctx.session.setForcedToolChoice(toolName);
921
+ runtime.ctx.showStatus(`Next turn forced to use ${toolName}.`);
922
+ } catch (error) {
923
+ runtime.ctx.showError(error instanceof Error ? error.message : String(error));
924
+ runtime.ctx.editor.setText("");
925
+ return;
926
+ }
927
+
928
+ runtime.ctx.editor.setText("");
929
+
930
+ // If a prompt was provided, pass it through as input
931
+ if (prompt) return prompt;
932
+ },
933
+ },
891
934
  {
892
935
  name: "quit",
893
936
  description: "Quit the application",
@@ -916,9 +959,14 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
916
959
  /**
917
960
  * Execute a builtin slash command when it matches known command syntax.
918
961
  *
919
- * Returns true when a builtin command consumed the input; false otherwise.
962
+ * Returns `false` when no builtin matched. Returns `true` when a command consumed
963
+ * the input entirely. Returns a `string` when the command was handled but remaining
964
+ * text should be sent as a prompt.
920
965
  */
921
- export async function executeBuiltinSlashCommand(text: string, runtime: BuiltinSlashCommandRuntime): Promise<boolean> {
966
+ export async function executeBuiltinSlashCommand(
967
+ text: string,
968
+ runtime: BuiltinSlashCommandRuntime,
969
+ ): Promise<string | boolean> {
922
970
  const parsed = parseBuiltinSlashCommand(text);
923
971
  if (!parsed) return false;
924
972
 
@@ -928,6 +976,6 @@ export async function executeBuiltinSlashCommand(text: string, runtime: BuiltinS
928
976
  return false;
929
977
  }
930
978
 
931
- await command.handle(parsed, runtime);
932
- return true;
979
+ const remaining = await command.handle(parsed, runtime);
980
+ return remaining ?? true;
933
981
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
3
+ import { $which, getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
4
4
  import { $ } from "bun";
5
5
  import { buildSshTarget, sanitizeHostName } from "./utils";
6
6
 
@@ -106,7 +106,7 @@ async function runSshCaptureSync(args: string[]): Promise<{ exitCode: number | n
106
106
  }
107
107
 
108
108
  function ensureSshBinary(): void {
109
- if (!Bun.which("ssh")) {
109
+ if (!$which("ssh")) {
110
110
  throw new Error("ssh binary not found on PATH");
111
111
  }
112
112
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
3
+ import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
4
4
  import { $ } from "bun";
5
5
  import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
6
6
  import { buildSshTarget, sanitizeHostName } from "./utils";
@@ -60,24 +60,24 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
60
60
  }
61
61
 
62
62
  async function unmountPath(path: string): Promise<boolean> {
63
- const fusermount = Bun.which("fusermount") ?? Bun.which("fusermount3");
63
+ const fusermount = $which("fusermount") ?? $which("fusermount3");
64
64
  if (fusermount) {
65
65
  const result = await $`${fusermount} -u ${path}`.quiet().nothrow();
66
66
  if (result.exitCode === 0) return true;
67
67
  }
68
68
 
69
- const umount = Bun.which("umount");
69
+ const umount = $which("umount");
70
70
  if (!umount) return false;
71
71
  const result = await $`${umount} ${path}`.quiet().nothrow();
72
72
  return result.exitCode === 0;
73
73
  }
74
74
 
75
75
  export function hasSshfs(): boolean {
76
- return Bun.which("sshfs") !== null;
76
+ return $which("sshfs") !== null;
77
77
  }
78
78
 
79
79
  export async function isMounted(path: string): Promise<boolean> {
80
- const mountpoint = Bun.which("mountpoint");
80
+ const mountpoint = $which("mountpoint");
81
81
  if (!mountpoint) return false;
82
82
  const result = await $`${mountpoint} -q ${path}`.quiet().nothrow();
83
83
  return result.exitCode === 0;
@@ -1,4 +1,4 @@
1
- import { logger } from "@oh-my-pi/pi-utils";
1
+ import { $which, logger } from "@oh-my-pi/pi-utils";
2
2
  import { $ } from "bun";
3
3
  import { resolvePython } from "./transcriber";
4
4
 
@@ -15,9 +15,9 @@ export interface EnsureOptions {
15
15
  // ── Recording tool ─────────────────────────────────────────────────
16
16
 
17
17
  async function ensureRecordingTool(options?: EnsureOptions): Promise<void> {
18
- if (Bun.which("sox")) return;
19
- if (Bun.which("ffmpeg")) return;
20
- if (process.platform === "linux" && Bun.which("arecord")) return;
18
+ if ($which("sox")) return;
19
+ if ($which("ffmpeg")) return;
20
+ if (process.platform === "linux" && $which("arecord")) return;
21
21
 
22
22
  // Windows: PowerShell mciSendString is always available as fallback
23
23
  if (process.platform === "win32") {
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { logger, Snowflake } from "@oh-my-pi/pi-utils";
4
+ import { $which, logger, Snowflake } from "@oh-my-pi/pi-utils";
5
5
  import { $ } from "bun";
6
6
 
7
7
  export interface RecordingHandle {
@@ -15,9 +15,9 @@ const isWindows = process.platform === "win32";
15
15
  */
16
16
  export function detectRecordingTools(): string[] {
17
17
  const tools: string[] = [];
18
- if (Bun.which("sox")) tools.push("sox");
19
- if (Bun.which("ffmpeg")) tools.push("ffmpeg");
20
- if (!isWindows && Bun.which("arecord")) tools.push("arecord");
18
+ if ($which("sox")) tools.push("sox");
19
+ if ($which("ffmpeg")) tools.push("ffmpeg");
20
+ if (!isWindows && $which("arecord")) tools.push("arecord");
21
21
  if (isWindows) tools.push("powershell");
22
22
  return tools;
23
23
  }
@@ -1,4 +1,4 @@
1
- import { logger } from "@oh-my-pi/pi-utils";
1
+ import { $which, logger } from "@oh-my-pi/pi-utils";
2
2
  import transcribeScript from "./transcribe.py" with { type: "text" };
3
3
 
4
4
  export interface TranscribeOptions {
@@ -14,7 +14,7 @@ const TRANSCRIBE_TIMEOUT_MS = 120_000;
14
14
  */
15
15
  export function resolvePython(): string | null {
16
16
  for (const cmd of ["python", "py", "python3"]) {
17
- if (Bun.which(cmd)) return cmd;
17
+ if ($which(cmd)) return cmd;
18
18
  }
19
19
  return null;
20
20
  }
@@ -6,17 +6,15 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
9
- import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
9
+ import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
10
10
  import { $ } from "bun";
11
11
  import { contextFileCapability } from "./capability/context-file";
12
12
  import { systemPromptCapability } from "./capability/system-prompt";
13
- import { renderPromptTemplate } from "./config/prompt-templates";
14
13
  import type { SkillsSettings } from "./config/settings";
15
14
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
16
15
  import { loadSkills, type Skill } from "./extensibility/skills";
17
16
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
18
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
19
- import { formatPromptContent } from "./utils/prompt-format";
20
18
 
21
19
  interface AlwaysApplyRule {
22
20
  name: string;
@@ -25,7 +23,7 @@ interface AlwaysApplyRule {
25
23
  }
26
24
 
27
25
  function normalizePromptBlock(content: string): string {
28
- return formatPromptContent(content, { renderPhase: "post-render" }).trim();
26
+ return prompt.format(content, { renderPhase: "post-render" }).trim();
29
27
  }
30
28
 
31
29
  function splitComparablePromptBlocks(content: string | null | undefined): string[] {
@@ -267,14 +265,16 @@ async function saveGpuCache(info: GpuCache): Promise<void> {
267
265
  }
268
266
 
269
267
  async function getCachedGpu(): Promise<string | undefined> {
270
- const cached = await logger.timeAsync("getCachedGpu:loadGpuCache", loadGpuCache);
268
+ const cached = await logger.time("getCachedGpu:loadGpuCache", loadGpuCache);
271
269
  if (cached) return cached.gpu;
272
- const gpu = await logger.timeAsync("getCachedGpu:getGpuModel", getGpuModel);
273
- if (gpu) await logger.timeAsync("getCachedGpu:saveGpuCache", saveGpuCache, { gpu });
270
+ const gpu = await logger.time("getCachedGpu:getGpuModel", getGpuModel);
271
+ if (gpu) {
272
+ await logger.time("getCachedGpu:saveGpuCache", saveGpuCache, { gpu });
273
+ }
274
274
  return gpu ?? undefined;
275
275
  }
276
276
  async function getEnvironmentInfo(): Promise<Array<{ label: string; value: string }>> {
277
- const gpu = await logger.timeAsync("getEnvironmentInfo:getCachedGpu", getCachedGpu);
277
+ const gpu = await getCachedGpu();
278
278
  const cpus = os.cpus();
279
279
  const entries: Array<{ label: string; value: string | undefined }> = [
280
280
  { label: "OS", value: `${os.platform()} ${os.release()}` },
@@ -463,13 +463,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
463
463
  const resolvedCwd = cwd ?? getProjectDir();
464
464
 
465
465
  const prepPromise = (() => {
466
- const systemPromptCustomizationPromise = logger.timeAsync("loadSystemPromptFiles", loadSystemPromptFiles, {
466
+ const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
467
467
  cwd: resolvedCwd,
468
468
  });
469
469
  const contextFilesPromise = providedContextFiles
470
470
  ? Promise.resolve(providedContextFiles)
471
- : logger.timeAsync("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
472
- const agentsMdSearchPromise = logger.timeAsync("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
471
+ : logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
472
+ const agentsMdSearchPromise = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
473
473
  const skillsPromise: Promise<Skill[]> =
474
474
  providedSkills !== undefined
475
475
  ? Promise.resolve(providedSkills)
@@ -583,7 +583,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
583
583
  const promptSources = [effectiveSystemPromptCustomization, resolvedCustomPrompt, resolvedAppendPrompt];
584
584
  const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
585
585
 
586
- const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
586
+ const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
587
587
  const data = {
588
588
  systemPromptCustomization: effectiveSystemPromptCustomization,
589
589
  customPrompt: resolvedCustomPrompt,
@@ -608,5 +608,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
608
608
  eagerTasks,
609
609
  secretsEnabled,
610
610
  };
611
- return renderPromptTemplate(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
611
+ let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
612
+
613
+ // When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
614
+ if (toolNames.includes("report_tool_issue")) {
615
+ rendered +=
616
+ "\n\n<critical>\nThe `report_tool_issue` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `report_tool_issue` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>";
617
+ }
618
+
619
+ return rendered;
612
620
  }
@@ -4,18 +4,18 @@
4
4
  * Agents are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
6
  import { Effort } from "@oh-my-pi/pi-ai";
7
- import { renderPromptTemplate } from "../config/prompt-templates";
7
+ import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
8
8
  import { parseAgentFields } from "../discovery/helpers";
9
9
  import designerMd from "../prompts/agents/designer.md" with { type: "text" };
10
10
  import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
11
11
  // Embed agent markdown files at build time
12
12
  import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
13
13
  import librarianMd from "../prompts/agents/librarian.md" with { type: "text" };
14
- import oracleMd from "../prompts/agents/oracle.md" with { type: "text" };
14
+
15
15
  import planMd from "../prompts/agents/plan.md" with { type: "text" };
16
16
  import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
17
17
  import taskMd from "../prompts/agents/task.md" with { type: "text" };
18
- import { parseFrontmatter } from "../utils/frontmatter";
18
+
19
19
  import type { AgentDefinition, AgentSource } from "./types";
20
20
 
21
21
  interface AgentFrontmatter {
@@ -35,9 +35,9 @@ interface EmbeddedAgentDef {
35
35
  }
36
36
 
37
37
  function buildAgentContent(def: EmbeddedAgentDef): string {
38
- const body = renderPromptTemplate(def.template);
38
+ const body = prompt.render(def.template);
39
39
  if (!def.frontmatter) return body;
40
- return renderPromptTemplate(agentFrontmatterTemplate, { ...def.frontmatter, body });
40
+ return prompt.render(agentFrontmatterTemplate, { ...def.frontmatter, body });
41
41
  }
42
42
 
43
43
  const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
@@ -45,7 +45,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
45
45
  { fileName: "plan.md", template: planMd },
46
46
  { fileName: "designer.md", template: designerMd },
47
47
  { fileName: "reviewer.md", template: reviewerMd },
48
- { fileName: "oracle.md", template: oracleMd },
49
48
  { fileName: "librarian.md", template: librarianMd },
50
49
  {
51
50
  fileName: "task.md",
@@ -4,16 +4,13 @@
4
4
  * Commands are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
6
  import * as path from "node:path";
7
+ import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
7
8
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
8
- import { renderPromptTemplate } from "../config/prompt-templates";
9
9
  import { loadCapability } from "../discovery";
10
10
  // Embed command markdown files at build time
11
11
  import initMd from "../prompts/agents/init.md" with { type: "text" };
12
- import { parseFrontmatter } from "../utils/frontmatter";
13
12
 
14
- const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
15
- { name: "init.md", content: renderPromptTemplate(initMd) },
16
- ];
13
+ const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
17
14
 
18
15
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
19
16
 
@@ -6,12 +6,12 @@
6
6
  import path from "node:path";
7
7
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { SearchDb } from "@oh-my-pi/pi-natives";
9
- import { logger, untilAborted } from "@oh-my-pi/pi-utils";
9
+ import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import type { TSchema } from "@sinclair/typebox";
11
11
  import Ajv, { type ValidateFunction } from "ajv";
12
12
  import { ModelRegistry } from "../config/model-registry";
13
13
  import { resolveModelOverride } from "../config/model-resolver";
14
- import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
14
+ import type { PromptTemplate } from "../config/prompt-templates";
15
15
  import { Settings } from "../config/settings";
16
16
  import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
17
17
  import type { CustomTool } from "../extensibility/custom-tools/types";
@@ -965,7 +965,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
965
965
  skills: options.skills,
966
966
  promptTemplates: options.promptTemplates,
967
967
  systemPrompt: defaultPrompt =>
968
- renderPromptTemplate(subagentSystemPromptTemplate, {
968
+ prompt.render(subagentSystemPromptTemplate, {
969
969
  base: defaultPrompt,
970
970
  agent: agent.systemPrompt,
971
971
  worktree: worktree ?? "",
@@ -1106,7 +1106,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1106
1106
  while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
1107
1107
  try {
1108
1108
  retryCount++;
1109
- const reminder = renderPromptTemplate(submitReminderTemplate, {
1109
+ const reminder = prompt.render(submitReminderTemplate, {
1110
1110
  retryCount,
1111
1111
  maxRetries: MAX_SUBMIT_RESULT_RETRIES,
1112
1112
  });
package/src/task/index.ts CHANGED
@@ -17,10 +17,9 @@ import * as os from "node:os";
17
17
  import path from "node:path";
18
18
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
19
  import type { Usage } from "@oh-my-pi/pi-ai";
20
- import { $env, Snowflake } from "@oh-my-pi/pi-utils";
20
+ import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
21
21
  import type { ToolSession } from "..";
22
22
  import { resolveAgentModelPatterns } from "../config/model-resolver";
23
- import { renderPromptTemplate } from "../config/prompt-templates";
24
23
  import type { Theme } from "../modes/theme/theme";
25
24
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
26
25
  import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
@@ -136,7 +135,7 @@ function renderDescription(
136
135
  disabledAgents: string[],
137
136
  ): string {
138
137
  const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
139
- return renderPromptTemplate(taskDescriptionTemplate, {
138
+ return prompt.render(taskDescriptionTemplate, {
140
139
  agents: filteredAgents,
141
140
  MAX_CONCURRENCY: maxConcurrency,
142
141
  isolationEnabled,
@@ -1157,7 +1156,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
1157
1156
 
1158
1157
  const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
1159
1158
  const backendSummaryPrefix = isolationBackendWarning ? `\n\n${isolationBackendWarning}` : "";
1160
- const summary = renderPromptTemplate(taskSummaryTemplate, {
1159
+ const summary = prompt.render(taskSummaryTemplate, {
1161
1160
  successCount,
1162
1161
  totalCount: results.length,
1163
1162
  cancelledCount,
@@ -1,4 +1,4 @@
1
- import { renderPromptTemplate } from "../config/prompt-templates";
1
+ import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
3
3
  import type { TaskItem } from "./types";
4
4
 
@@ -25,7 +25,7 @@ export function renderTemplate(context: string | undefined, task: TaskItem): Ren
25
25
  return { task: assignment || context!, assignment: assignment || context!, id, description };
26
26
  }
27
27
  return {
28
- task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
28
+ task: prompt.render(subagentUserPromptTemplate, { context, assignment }),
29
29
  assignment,
30
30
  id,
31
31
  description,