@oh-my-pi/pi-coding-agent 13.18.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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  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 +116 -30
  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 +123 -178
  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 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. 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()}` },
@@ -432,6 +432,8 @@ export interface BuildSystemPromptOptions {
432
432
  eagerTasks?: boolean;
433
433
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
434
434
  alwaysApplyRules?: AlwaysApplyRule[];
435
+ /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
436
+ secretsEnabled?: boolean;
435
437
  }
436
438
 
437
439
  /** Build the system prompt with tools, guidelines, and context */
@@ -456,17 +458,18 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
456
458
  mcpDiscoveryMode = false,
457
459
  mcpDiscoveryServerSummaries = [],
458
460
  eagerTasks = false,
461
+ secretsEnabled = false,
459
462
  } = options;
460
463
  const resolvedCwd = cwd ?? getProjectDir();
461
464
 
462
465
  const prepPromise = (() => {
463
- const systemPromptCustomizationPromise = logger.timeAsync("loadSystemPromptFiles", loadSystemPromptFiles, {
466
+ const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
464
467
  cwd: resolvedCwd,
465
468
  });
466
469
  const contextFilesPromise = providedContextFiles
467
470
  ? Promise.resolve(providedContextFiles)
468
- : logger.timeAsync("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
469
- const agentsMdSearchPromise = logger.timeAsync("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
471
+ : logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
472
+ const agentsMdSearchPromise = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
470
473
  const skillsPromise: Promise<Skill[]> =
471
474
  providedSkills !== undefined
472
475
  ? Promise.resolve(providedSkills)
@@ -580,7 +583,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
580
583
  const promptSources = [effectiveSystemPromptCustomization, resolvedCustomPrompt, resolvedAppendPrompt];
581
584
  const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
582
585
 
583
- const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
586
+ const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
584
587
  const data = {
585
588
  systemPromptCustomization: effectiveSystemPromptCustomization,
586
589
  customPrompt: resolvedCustomPrompt,
@@ -603,6 +606,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
603
606
  hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
604
607
  mcpDiscoveryServerSummaries,
605
608
  eagerTasks,
609
+ secretsEnabled,
606
610
  };
607
- 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;
608
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