@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli.js +3785 -3695
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +13 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  35. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  37. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  38. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  39. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  40. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  41. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  42. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  43. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  44. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  45. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  46. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  47. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  48. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  49. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  50. package/dist/types/modes/interactive-mode.d.ts +10 -1
  51. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  52. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  53. package/dist/types/modes/theme/theme.d.ts +1 -1
  54. package/dist/types/modes/types.d.ts +9 -1
  55. package/dist/types/sdk.d.ts +2 -2
  56. package/dist/types/session/agent-session.d.ts +8 -1
  57. package/dist/types/session/blob-store.d.ts +4 -0
  58. package/dist/types/session/session-manager.d.ts +13 -0
  59. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  60. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  61. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  62. package/dist/types/ssh/connection-manager.d.ts +2 -0
  63. package/dist/types/ssh/file-transfer.d.ts +79 -0
  64. package/dist/types/ssh/utils.d.ts +6 -0
  65. package/dist/types/system-prompt.d.ts +3 -0
  66. package/dist/types/task/executor.d.ts +16 -0
  67. package/dist/types/tiny/text.d.ts +1 -1
  68. package/dist/types/tools/builtin-names.d.ts +5 -1
  69. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  70. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  71. package/dist/types/tools/index.d.ts +3 -3
  72. package/dist/types/tools/path-utils.d.ts +29 -0
  73. package/dist/types/tools/read.d.ts +2 -2
  74. package/dist/types/tools/render-utils.d.ts +8 -0
  75. package/dist/types/tools/renderers.d.ts +11 -0
  76. package/dist/types/tools/ssh.d.ts +2 -0
  77. package/dist/types/tools/todo.d.ts +0 -16
  78. package/dist/types/tools/write.d.ts +2 -2
  79. package/dist/types/utils/active-repo-context.d.ts +8 -0
  80. package/dist/types/utils/image-resize.d.ts +1 -0
  81. package/dist/types/utils/markit-cache.d.ts +23 -0
  82. package/dist/types/utils/markit.d.ts +5 -1
  83. package/dist/types/utils/prompt-path.d.ts +1 -0
  84. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  85. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  86. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  87. package/dist/types/web/search/providers/xai.d.ts +13 -0
  88. package/dist/types/web/search/types.d.ts +18 -2
  89. package/package.json +30 -15
  90. package/scripts/build-binary.ts +9 -9
  91. package/scripts/bundle-dist.ts +2 -2
  92. package/src/advisor/__tests__/advisor.test.ts +61 -4
  93. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  94. package/src/advisor/advise-tool.ts +1 -1
  95. package/src/advisor/emission-guard.ts +172 -0
  96. package/src/advisor/index.ts +1 -0
  97. package/src/advisor/runtime.ts +11 -0
  98. package/src/advisor/watchdog.ts +27 -1
  99. package/src/cli/args.ts +5 -2
  100. package/src/cli/auth-broker-cli.ts +17 -0
  101. package/src/cli/config-cli.ts +4 -0
  102. package/src/cli/flag-tables.ts +7 -4
  103. package/src/cli/gallery-cli.ts +14 -2
  104. package/src/cli/gallery-fixtures/edit.ts +60 -0
  105. package/src/cli/gallery-fixtures/fs.ts +17 -17
  106. package/src/cli/gallery-fixtures/search.ts +4 -4
  107. package/src/cli/usage-cli.ts +20 -1
  108. package/src/cli/web-search-cli.ts +1 -1
  109. package/src/collab/display-name.ts +13 -0
  110. package/src/collab/guest.ts +5 -1
  111. package/src/collab/host.ts +2 -13
  112. package/src/commands/worktree.ts +6 -0
  113. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  114. package/src/config/model-discovery.ts +77 -8
  115. package/src/config/model-registry.ts +89 -11
  116. package/src/config/model-resolver.ts +39 -1
  117. package/src/config/models-config-schema.ts +41 -5
  118. package/src/config/models-config.ts +4 -1
  119. package/src/config/settings-schema.ts +85 -27
  120. package/src/config/settings.ts +172 -1
  121. package/src/cursor.ts +1 -1
  122. package/src/dap/config.ts +152 -8
  123. package/src/dap/session.ts +1 -1
  124. package/src/discovery/claude-plugins.ts +3 -2
  125. package/src/discovery/helpers.ts +3 -1
  126. package/src/edit/hashline/diff.ts +14 -3
  127. package/src/edit/hashline/execute.ts +42 -6
  128. package/src/edit/hashline/filesystem.ts +38 -2
  129. package/src/edit/index.ts +1 -0
  130. package/src/edit/modes/patch.ts +11 -1
  131. package/src/edit/renderer.ts +140 -13
  132. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  133. package/src/eval/agent-bridge.ts +26 -3
  134. package/src/eval/js/tool-bridge.ts +2 -2
  135. package/src/export/html/index.ts +1 -1
  136. package/src/export/html/template.js +3 -1
  137. package/src/export/html/tool-views.generated.js +20 -20
  138. package/src/extensibility/extensions/types.ts +22 -22
  139. package/src/extensibility/hooks/types.ts +11 -11
  140. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  141. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  142. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  143. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  144. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  145. package/src/internal-urls/docs-index.generated.txt +2 -2
  146. package/src/internal-urls/docs-index.ts +2 -3
  147. package/src/internal-urls/index.ts +1 -0
  148. package/src/internal-urls/registry-helpers.ts +19 -4
  149. package/src/internal-urls/router.ts +5 -3
  150. package/src/internal-urls/ssh-protocol.ts +367 -0
  151. package/src/internal-urls/types.ts +19 -2
  152. package/src/irc/bus.ts +11 -3
  153. package/src/mcp/tool-bridge.ts +32 -2
  154. package/src/memories/index.ts +1 -1
  155. package/src/modes/acp/acp-agent.ts +14 -1
  156. package/src/modes/acp/acp-event-mapper.ts +91 -27
  157. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  158. package/src/modes/components/agent-hub.ts +3 -0
  159. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  160. package/src/modes/components/custom-editor.ts +90 -5
  161. package/src/modes/components/move-overlay.ts +282 -0
  162. package/src/modes/components/plan-review-overlay.ts +35 -35
  163. package/src/modes/components/plugin-selector.ts +6 -1
  164. package/src/modes/components/queue-mode-selector.ts +6 -1
  165. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  166. package/src/modes/components/session-selector.ts +11 -10
  167. package/src/modes/components/settings-defs.ts +14 -1
  168. package/src/modes/components/settings-selector.ts +196 -29
  169. package/src/modes/components/show-images-selector.ts +6 -1
  170. package/src/modes/components/status-line/component.ts +108 -28
  171. package/src/modes/components/status-line/segments.ts +5 -1
  172. package/src/modes/components/status-line/types.ts +2 -0
  173. package/src/modes/components/theme-selector.ts +6 -1
  174. package/src/modes/components/thinking-selector.ts +6 -1
  175. package/src/modes/components/tool-execution.ts +25 -9
  176. package/src/modes/components/tree-selector.ts +5 -5
  177. package/src/modes/controllers/command-controller.ts +140 -47
  178. package/src/modes/controllers/event-controller.ts +59 -3
  179. package/src/modes/controllers/input-controller.ts +68 -2
  180. package/src/modes/controllers/selector-controller.ts +5 -2
  181. package/src/modes/controllers/streaming-reveal.ts +17 -0
  182. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  183. package/src/modes/interactive-mode.ts +116 -50
  184. package/src/modes/internal-url-autocomplete.ts +17 -2
  185. package/src/modes/prompt-action-autocomplete.ts +3 -1
  186. package/src/modes/running-subagent-badge.ts +13 -0
  187. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  188. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  189. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  190. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  191. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  192. package/src/modes/theme/theme.ts +9 -1
  193. package/src/modes/types.ts +9 -1
  194. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  195. package/src/priority.json +15 -0
  196. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  197. package/src/prompts/advisor/context-files.md +8 -0
  198. package/src/prompts/advisor/system.md +21 -7
  199. package/src/prompts/agents/designer.md +1 -1
  200. package/src/prompts/agents/explore.md +1 -1
  201. package/src/prompts/agents/librarian.md +2 -2
  202. package/src/prompts/agents/plan.md +2 -2
  203. package/src/prompts/agents/reviewer.md +1 -1
  204. package/src/prompts/agents/task.md +2 -2
  205. package/src/prompts/system/active-repo-context.md +4 -0
  206. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  207. package/src/prompts/system/plan-mode-active.md +3 -3
  208. package/src/prompts/system/project-prompt.md +2 -2
  209. package/src/prompts/system/system-prompt.md +6 -6
  210. package/src/prompts/tools/bash.md +2 -2
  211. package/src/prompts/tools/checkpoint.md +1 -1
  212. package/src/prompts/tools/{find.md → glob.md} +1 -1
  213. package/src/prompts/tools/{search.md → grep.md} +3 -3
  214. package/src/prompts/tools/read.md +4 -2
  215. package/src/sdk.ts +65 -25
  216. package/src/session/agent-session.ts +519 -132
  217. package/src/session/blob-store.ts +24 -0
  218. package/src/session/session-history-format.ts +2 -2
  219. package/src/session/session-manager.ts +46 -0
  220. package/src/slash-commands/builtin-registry.ts +138 -20
  221. package/src/slash-commands/helpers/usage-report.ts +23 -2
  222. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  223. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  224. package/src/ssh/connection-manager.ts +12 -15
  225. package/src/ssh/file-transfer.ts +209 -0
  226. package/src/ssh/utils.ts +24 -0
  227. package/src/system-prompt.ts +56 -28
  228. package/src/task/executor.ts +96 -36
  229. package/src/task/index.ts +3 -3
  230. package/src/task/render.ts +14 -13
  231. package/src/task/worktree.ts +38 -1
  232. package/src/tiny/text.ts +49 -4
  233. package/src/tiny/worker.ts +3 -3
  234. package/src/tools/bash.ts +2 -2
  235. package/src/tools/builtin-names.ts +26 -2
  236. package/src/tools/{find.ts → glob.ts} +48 -42
  237. package/src/tools/{search.ts → grep.ts} +311 -129
  238. package/src/tools/index.ts +13 -14
  239. package/src/tools/irc.ts +6 -2
  240. package/src/tools/path-utils.ts +64 -1
  241. package/src/tools/read.ts +35 -6
  242. package/src/tools/render-utils.ts +14 -0
  243. package/src/tools/renderers.ts +15 -4
  244. package/src/tools/ssh.ts +17 -2
  245. package/src/tools/todo.ts +4 -26
  246. package/src/tools/write.ts +25 -9
  247. package/src/utils/active-repo-context.ts +143 -0
  248. package/src/utils/image-resize.ts +88 -7
  249. package/src/utils/lang-from-path.ts +3 -3
  250. package/src/utils/markit-cache.ts +166 -0
  251. package/src/utils/markit.ts +86 -18
  252. package/src/utils/prompt-path.ts +3 -0
  253. package/src/utils/title-generator.ts +1 -1
  254. package/src/web/search/provider.ts +54 -34
  255. package/src/web/search/providers/duckduckgo.ts +140 -0
  256. package/src/web/search/providers/firecrawl.ts +144 -0
  257. package/src/web/search/providers/tinyfish.ts +159 -0
  258. package/src/web/search/providers/xai.ts +292 -0
  259. package/src/web/search/providers/zai.ts +142 -56
  260. package/src/web/search/types.ts +6 -2
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Per-session policy gate for advisor `advise()` calls.
3
+ *
4
+ * The advisor system prompt tells the watcher model:
5
+ *
6
+ * > at most one `advise` per update
7
+ * > NEVER repeat advice you already gave, and NEVER send the same advice twice
8
+ *
9
+ * Real advisor models violate this. Issue #3520 captured a session where
10
+ * `__advisor.jsonl` recorded 309 `advise` calls covering 92 unique notes —
11
+ * 114× `Stop.`, 52× `No issue; continue.`, 41× `Done.` — flooding the primary
12
+ * transcript with `<advisory severity="blocker">Stop.</advisory>` after the
13
+ * task was already complete. The fix is to make the rules load-bearing in code
14
+ * instead of prose: silently drop duplicates, content-free self-talk, and
15
+ * over-budget calls at the `enqueueAdvice` boundary so the primary stays
16
+ * clean even when the advisor misbehaves.
17
+ *
18
+ * The gate is intentionally invisible to the advisor model — `AdviseTool`
19
+ * still returns `Recorded.` for a suppressed call. Surfacing "suppressed"
20
+ * back into advisor context risks the model rephrasing the same useless note
21
+ * to bypass the dedupe ("Stop.", then "Halt." then "Stop now.").
22
+ */
23
+
24
+ /**
25
+ * Case-insensitive, punctuation-folded normalization. Collapses every run of
26
+ * non-letter / non-digit characters into a single space and trims, so
27
+ * `"Stop."`, `"*Stop*"`, and `" stop "` all key to `stop`, while
28
+ * `"No issue; continue."` keys to `no issue continue`.
29
+ *
30
+ * Exported for tests.
31
+ */
32
+ export function normalizeAdvisorNote(note: string): string {
33
+ return note
34
+ .toLowerCase()
35
+ .normalize("NFKC")
36
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
37
+ .trim();
38
+ }
39
+
40
+ /**
41
+ * Normalized phrases the advisor occasionally emits that carry no concrete
42
+ * actionable content. Each must be the output of {@link normalizeAdvisorNote}
43
+ * so a single membership check covers every punctuation/casing variant
44
+ * (`"Stop."`, `"stop"`, `"STOP!"`).
45
+ *
46
+ * The list is conservative — only short, content-free filler the reporter
47
+ * observed driving primary-transcript pollution. A genuine `blocker` like
48
+ * `"Stop: 'await' missing on writeStream.end() will lose buffered writes."`
49
+ * does not match.
50
+ */
51
+ const SUPPRESSED_NORMALIZED_PHRASES: Record<string, true> = {
52
+ // Self-stop noise — telling the agent to "stop" without a reason is useless.
53
+ stop: true,
54
+ "stop here": true,
55
+ "stop now": true,
56
+ halt: true,
57
+ abort: true,
58
+ // Completion self-talk — the agent already finished the task.
59
+ done: true,
60
+ "task done": true,
61
+ "task complete": true,
62
+ complete: true,
63
+ finished: true,
64
+ ok: true,
65
+ okay: true,
66
+ "ok done": true,
67
+ // "Nothing to flag" — silence is the correct expression of "no concerns".
68
+ "no issue": true,
69
+ "no issues": true,
70
+ "no issue continue": true,
71
+ "no concerns": true,
72
+ "no concern": true,
73
+ "nothing to add": true,
74
+ "nothing to flag": true,
75
+ "nothing to report": true,
76
+ "no notes": true,
77
+ "no further input": true,
78
+ "no further input needed": true,
79
+ "no further input required": true,
80
+ "no further watcher input": true,
81
+ "no further watcher input needed": true,
82
+ "no further advice": true,
83
+ "no further advice needed": true,
84
+ // Endorsements — equivalent to silence.
85
+ lgtm: true,
86
+ "looks good": true,
87
+ "all good": true,
88
+ "agent is on track": true,
89
+ "agent on track": true,
90
+ "on track": true,
91
+ continue: true,
92
+ "carry on": true,
93
+ };
94
+
95
+ /**
96
+ * Bounds the dedupe history. Sessions with very long advisor activity could
97
+ * otherwise grow the set without bound. The reporter's pathological session
98
+ * had 92 unique notes; 4096 leaves headroom while staying tiny (≤ ~256 KB of
99
+ * normalized strings even at long max).
100
+ */
101
+ const DEFAULT_HISTORY_CAPACITY = 4096;
102
+
103
+ /**
104
+ * Decides whether an advisor `advise()` call should reach the primary agent.
105
+ *
106
+ * Enforces — in this order — the noise filter, session-scoped exact-text
107
+ * dedupe (FIFO-evicted at {@link DEFAULT_HISTORY_CAPACITY}), and a per-update
108
+ * rate limit of one accepted note per advisor model prompt. Suppressed calls
109
+ * never consume the per-update budget — a noise call doesn't burn the slot
110
+ * for a real concern that follows in the same update.
111
+ *
112
+ * Reset on advisor reset (compaction, session switch, `/new`) via
113
+ * {@link reset}. Per-update gate is cleared at the start of every advisor
114
+ * `agent.prompt()` cycle via {@link beginUpdate}.
115
+ */
116
+ export class AdvisorEmissionGuard {
117
+ #seen = new Set<string>();
118
+ /** Insertion-order log to drive FIFO eviction without an extra Map. */
119
+ #seenOrder: string[] = [];
120
+ #consumedThisUpdate = false;
121
+ readonly #capacity: number;
122
+
123
+ constructor(opts: { capacity?: number } = {}) {
124
+ this.#capacity = opts.capacity ?? DEFAULT_HISTORY_CAPACITY;
125
+ }
126
+
127
+ /**
128
+ * Drop all dedupe and per-update state. Called from
129
+ * `AgentSession#resetAdvisorSessionState()` whenever the advisor runtime is
130
+ * reset — same boundary as `yieldQueue.clear("advisor")`, so a re-primed
131
+ * advisor can re-raise old issues (the primary transcript was rewritten).
132
+ */
133
+ reset(): void {
134
+ this.#seen.clear();
135
+ this.#seenOrder.length = 0;
136
+ this.#consumedThisUpdate = false;
137
+ }
138
+
139
+ /**
140
+ * Clear the per-update rate-limit gate. Called by `AdvisorRuntime` right
141
+ * before each `agent.prompt(batch)` invocation so the next advisor model
142
+ * cycle starts with a fresh budget of one advise.
143
+ */
144
+ beginUpdate(): void {
145
+ this.#consumedThisUpdate = false;
146
+ }
147
+
148
+ /**
149
+ * Whether the proposed note should reach the primary. On `true` the gate
150
+ * has already recorded the note (consumed the per-update budget and added
151
+ * it to the dedupe history) — caller delivers the note. On `false` the
152
+ * caller drops it.
153
+ *
154
+ * Empty / whitespace-only notes are suppressed; the model's
155
+ * tool-args contract still requires a non-empty string but defense-in-depth.
156
+ */
157
+ accept(note: string): boolean {
158
+ const key = normalizeAdvisorNote(note);
159
+ if (!key) return false;
160
+ if (SUPPRESSED_NORMALIZED_PHRASES[key]) return false;
161
+ if (this.#seen.has(key)) return false;
162
+ if (this.#consumedThisUpdate) return false;
163
+ this.#consumedThisUpdate = true;
164
+ this.#seen.add(key);
165
+ this.#seenOrder.push(key);
166
+ if (this.#seenOrder.length > this.#capacity) {
167
+ const stale = this.#seenOrder.shift();
168
+ if (stale !== undefined) this.#seen.delete(stale);
169
+ }
170
+ return true;
171
+ }
172
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./advise-tool";
2
+ export * from "./emission-guard";
2
3
  export * from "./runtime";
3
4
  export * from "./transcript-recorder";
4
5
  export * from "./watchdog";
@@ -30,6 +30,13 @@ export interface AdvisorRuntimeHost {
30
30
  * the primary's next compaction triggers {@link AdvisorRuntime.reset}).
31
31
  */
32
32
  maintainContext?(incomingTokens: number): Promise<boolean>;
33
+ /**
34
+ * Called immediately before each `agent.prompt(batch)` cycle. Lets the host
35
+ * clear per-update advisor state — currently the one-advise-per-update gate
36
+ * in {@link AdvisorEmissionGuard}, which the host owns because it is the
37
+ * one that routes `advise()` results back to the primary.
38
+ */
39
+ beginAdvisorUpdate?(): void;
33
40
  }
34
41
 
35
42
  interface PendingDelta {
@@ -275,6 +282,10 @@ export class AdvisorRuntime {
275
282
 
276
283
  let success = false;
277
284
  try {
285
+ // Reset the host's per-update advisor state (one-advise-per-update
286
+ // gate) before each model cycle, so the new batch starts with a
287
+ // fresh budget. Dedupe history persists across cycles.
288
+ this.host.beginAdvisorUpdate?.();
278
289
  await this.agent.prompt(batch);
279
290
  success = true;
280
291
  this.#consecutiveFailures = 0;
@@ -1,8 +1,34 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
3
+ import { getAgentDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
4
4
  import { expandAtImports } from "../discovery/at-imports";
5
+ import activeRepoWatchdogTemplate from "../prompts/advisor/active-repo-watchdog.md" with { type: "text" };
6
+ import contextFilesTemplate from "../prompts/advisor/context-files.md" with { type: "text" };
7
+ import type { ActiveRepoContext } from "../utils/active-repo-context";
5
8
  import { repo } from "../utils/git";
9
+ import { normalizePromptPath } from "../utils/prompt-path";
10
+
11
+ export function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoContext): string {
12
+ return prompt
13
+ .render(activeRepoWatchdogTemplate, {
14
+ relativeRepoRoot: normalizePromptPath(activeRepoContext.relativeRepoRoot),
15
+ })
16
+ .trim();
17
+ }
18
+
19
+ /**
20
+ * Render the project context files (AGENTS.md and the like) into a block for the
21
+ * advisor's system prompt, mirroring how the primary agent receives them. Gives
22
+ * the read-only reviewer the user's standing project instructions so it can hold
23
+ * the driving agent to them instead of advising against project conventions it
24
+ * cannot otherwise see. Returns undefined when there are no context files.
25
+ */
26
+ export function formatAdvisorContextPrompt(
27
+ contextFiles: ReadonlyArray<{ path: string; content: string }>,
28
+ ): string | undefined {
29
+ if (contextFiles.length === 0) return undefined;
30
+ return prompt.render(contextFilesTemplate, { contextFiles }).trim() || undefined;
31
+ }
6
32
 
7
33
  /**
8
34
  * Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
package/src/cli/args.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
5
5
  import chalk from "chalk";
6
6
  import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
7
- import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
7
+ import { BUILTIN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names";
8
8
  import {
9
9
  OPTIONAL_FLAGS,
10
10
  OPTIONAL_VALUE_FLAGS,
@@ -90,6 +90,7 @@ const PARSE_DEPS: ParseDeps = {
90
90
  logger,
91
91
  parseThinking: parseCliThinkingLevel,
92
92
  builtinToolNames: BUILTIN_TOOL_NAMES,
93
+ normalizeToolNames,
93
94
  thinkingEfforts: CLI_THINKING_LEVELS,
94
95
  };
95
96
 
@@ -310,6 +311,8 @@ export function getExtraHelpText(): string {
310
311
  PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback)
311
312
  PERPLEXITY_COOKIES - Perplexity web search (session cookie)
312
313
  TAVILY_API_KEY - Tavily web search
314
+ TINYFISH_API_KEY - TinyFish web search
315
+ FIRECRAWL_API_KEY - Firecrawl web search
313
316
  ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY)
314
317
  ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY)
315
318
 
@@ -330,7 +333,7 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
330
333
  edit - Edit files with find/replace
331
334
  write - Write files (creates/overwrites)
332
335
  grep - Search file contents
333
- find - Find files by glob pattern
336
+ glob - Find files by glob pattern
334
337
  lsp - Language server protocol (code intelligence)
335
338
  python - Execute Python code (requires: ${APP_NAME} setup python)
336
339
  notebook - Edit Jupyter notebooks
@@ -28,6 +28,7 @@ import {
28
28
  type OAuthCredential,
29
29
  type OAuthProvider,
30
30
  type OAuthProviderInfo,
31
+ PASTE_CODE_LOGIN_PROVIDERS,
31
32
  PROVIDER_REGISTRY,
32
33
  SqliteAuthCredentialStore,
33
34
  } from "@oh-my-pi/pi-ai";
@@ -211,6 +212,15 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
211
212
  const storage = new AuthStorage(store);
212
213
  await storage.reload();
213
214
  try {
215
+ // Only paste-code providers (fixed non-loopback redirect, e.g. GitLab Duo
216
+ // Agent's vscode:// URI) get the manual paste fallback. An explicit
217
+ // `onManualCodeInput` is honored for ANY provider (the storage escape hatch),
218
+ // so for loopback providers we must not pass it: it would make
219
+ // `OAuthCallbackFlow` race a readline prompt against the HTTP callback and, if
220
+ // the callback wins, leave that prompt outstanding (dirty/blocked terminal).
221
+ // `AuthStorage.login` independently refuses to synthesize the default prompt
222
+ // for non-paste-code providers, so this is defense-in-depth on the same gate.
223
+ const usesManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(provider);
214
224
  await storage.login(provider, {
215
225
  onAuth({ url, instructions }) {
216
226
  process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
@@ -223,6 +233,13 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
223
233
  onPrompt(p) {
224
234
  return ask(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
225
235
  },
236
+ ...(usesManualInput
237
+ ? {
238
+ onManualCodeInput() {
239
+ return ask("Paste the authorization code (or full redirect URL):");
240
+ },
241
+ }
242
+ : undefined),
226
243
  });
227
244
  process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
228
245
  } finally {
@@ -16,6 +16,7 @@ import {
16
16
  Settings,
17
17
  type SettingValue,
18
18
  settings,
19
+ validateProviderMaxInFlightRequests,
19
20
  } from "../config/settings";
20
21
  import { SETTINGS_SCHEMA } from "../config/settings-schema";
21
22
  import { theme } from "../modes/theme/theme";
@@ -218,6 +219,9 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
218
219
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
219
220
  throw new Error(`Invalid record JSON: ${rawValue}`);
220
221
  }
222
+ if (path === "providers.maxInFlightRequests") {
223
+ parsed = validateProviderMaxInFlightRequests(parsed);
224
+ }
221
225
  parsedValue = parsed;
222
226
  break;
223
227
  }
@@ -46,6 +46,7 @@ export interface ParseDeps {
46
46
  logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
47
47
  parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
48
48
  builtinToolNames: readonly string[];
49
+ normalizeToolNames: (values: Iterable<string>) => string[];
49
50
  thinkingEfforts: readonly string[];
50
51
  }
51
52
 
@@ -147,10 +148,12 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
147
148
  result.models = value.split(",").map(s => s.trim());
148
149
  },
149
150
  "--tools": (result, value, deps) => {
150
- const names = value
151
- .split(",")
152
- .map(s => s.trim().toLowerCase())
153
- .filter(Boolean);
151
+ const names = deps.normalizeToolNames(
152
+ value
153
+ .split(",")
154
+ .map(s => s.trim())
155
+ .filter(Boolean),
156
+ );
154
157
  const valid: string[] = [];
155
158
  for (const name of names) {
156
159
  if (deps.builtinToolNames.includes(name)) {
@@ -138,14 +138,26 @@ export async function renderGalleryState(
138
138
  return await fixture.renderState(state, width, expanded);
139
139
  }
140
140
 
141
- const tool = fakeToolFor(name, fixture);
141
+ // A non-customRendered fixture may borrow another tool's built-in renderer
142
+ // (e.g. `edit_delete` → `edit`): drive the component under that real tool
143
+ // name so the sample exercises the exact production branch, not the
144
+ // custom-tool one (which tints/pads non-framed result rows).
145
+ const componentName = fixture.customRendered ? name : (fixture.renderer ?? name);
146
+ const tool = fakeToolFor(componentName, fixture);
142
147
  const streamingArgs = state === "streaming" ? (fixture.streamingArgs ?? fixture.args) : fixture.args;
143
148
  // The component only calls `requestRender`/`requestComponentRender` (via
144
149
  // its loader) during a static render; `imageBudget` is consulted solely
145
150
  // when images render, which the gallery disables. A cast avoids
146
151
  // constructing a real terminal.
147
152
  const ui = { requestRender() {}, requestComponentRender() {} } as unknown as TUI;
148
- const component = new ToolExecutionComponent(name, streamingArgs, { showImages: false }, tool, ui, getProjectDir());
153
+ const component = new ToolExecutionComponent(
154
+ componentName,
155
+ streamingArgs,
156
+ { showImages: false },
157
+ tool,
158
+ ui,
159
+ getProjectDir(),
160
+ );
149
161
  component.setExpanded(expanded);
150
162
 
151
163
  if (state !== "streaming") {
@@ -62,6 +62,66 @@ export const editFixtures: Record<string, GalleryFixture> = {
62
62
  },
63
63
  },
64
64
 
65
+ edit_delete: {
66
+ label: "Delete",
67
+ // The registry has no `edit_delete` key, so `renderer: "edit"` routes this
68
+ // fixture through the real built-in edit renderer (see the harness in
69
+ // `gallery-cli`), keeping the sample identical to a production delete.
70
+ renderer: "edit",
71
+ streamingArgs: { file_path: "scripts/prune-changelogs.ts", op: "delete" },
72
+ args: { file_path: "scripts/prune-changelogs.ts", op: "delete" },
73
+ result: {
74
+ content: [{ type: "text", text: "Deleted scripts/prune-changelogs.ts" }],
75
+ details: {
76
+ op: "delete",
77
+ path: "scripts/prune-changelogs.ts",
78
+ diff: "",
79
+ oldText: "#!/usr/bin/env bun\n// obsolete changelog pruning helper\n",
80
+ },
81
+ },
82
+ errorResult: {
83
+ content: [{ type: "text", text: "Edit failed: scripts/prune-changelogs.ts not found" }],
84
+ isError: true,
85
+ details: {
86
+ op: "delete",
87
+ path: "scripts/prune-changelogs.ts",
88
+ diff: "",
89
+ errorText: "Cannot delete scripts/prune-changelogs.ts: the file does not exist.",
90
+ },
91
+ },
92
+ },
93
+
94
+ edit_move: {
95
+ label: "Move",
96
+ renderer: "edit",
97
+ streamingArgs: { file_path: "scripts/prune-changelogs.ts", rename: "scripts/archived/prune-changelogs.ts" },
98
+ args: { file_path: "scripts/prune-changelogs.ts", rename: "scripts/archived/prune-changelogs.ts" },
99
+ result: {
100
+ content: [{ type: "text", text: "Moved scripts/prune-changelogs.ts to scripts/archived/prune-changelogs.ts" }],
101
+ details: {
102
+ op: "update",
103
+ path: "scripts/archived/prune-changelogs.ts",
104
+ move: "scripts/archived/prune-changelogs.ts",
105
+ sourcePath: "scripts/prune-changelogs.ts",
106
+ diff: "",
107
+ },
108
+ },
109
+ errorResult: {
110
+ content: [
111
+ { type: "text", text: "Edit failed: destination scripts/archived/prune-changelogs.ts already exists" },
112
+ ],
113
+ isError: true,
114
+ details: {
115
+ op: "update",
116
+ path: "scripts/archived/prune-changelogs.ts",
117
+ move: "scripts/archived/prune-changelogs.ts",
118
+ sourcePath: "scripts/prune-changelogs.ts",
119
+ diff: "",
120
+ errorText: "MV destination scripts/archived/prune-changelogs.ts already exists.",
121
+ },
122
+ },
123
+ },
124
+
65
125
  apply_patch: {
66
126
  label: "Apply Patch",
67
127
  editMode: "apply_patch",
@@ -1,17 +1,17 @@
1
1
  // biome-ignore-all lint/suspicious/noTemplateCurlyInString: sample source-code strings (read fixtures) intentionally contain literal ${...}.
2
- // Gallery fixtures for the filesystem tools (read, write, find).
2
+ // Gallery fixtures for the filesystem tools (read, write, glob).
3
3
  import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
4
4
  import type { GalleryFixture, GalleryFixtureState, GalleryResult } from "./types";
5
5
 
6
6
  const readSnippet = [
7
- "export const findToolRenderer = {",
7
+ "export const globToolRenderer = {",
8
8
  "\tinline: true,",
9
- "\trenderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
9
+ "\trenderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
10
10
  "\t\tconst meta: string[] = [];",
11
11
  "\t\tif (args.limit !== undefined) meta.push(`limit:${args.limit}`);",
12
12
  "",
13
13
  "\t\tconst text = renderStatusLine(",
14
- '\t\t\t{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },',
14
+ '\t\t\t{ icon: "pending", title: "Glob", description: formatGlobRenderPaths(args.paths) || "*", meta },',
15
15
  "\t\t\tuiTheme,",
16
16
  "\t\t);",
17
17
  "\t\treturn new Text(text, 0, 0);",
@@ -99,22 +99,22 @@ export const fsFixtures: Record<string, GalleryFixture> = {
99
99
  read: {
100
100
  label: "Read",
101
101
  // Streaming: path still being typed, selector not yet appended.
102
- streamingArgs: { path: "packages/coding-agent/src/tools/find" },
103
- args: { path: "packages/coding-agent/src/tools/find.ts:437-448" },
102
+ streamingArgs: { path: "packages/coding-agent/src/tools/glob" },
103
+ args: { path: "packages/coding-agent/src/tools/glob.ts:437-448" },
104
104
  result: {
105
105
  content: [
106
106
  {
107
107
  type: "text",
108
108
  text: [
109
- "[packages/coding-agent/src/tools/find.ts#E48E]",
110
- "437:export const findToolRenderer = {",
109
+ "[packages/coding-agent/src/tools/glob.ts#E48E]",
110
+ "437:export const globToolRenderer = {",
111
111
  "438:\tinline: true,",
112
- "439:\trenderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
112
+ "439:\trenderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
113
113
  "440:\t\tconst meta: string[] = [];",
114
114
  "441:\t\tif (args.limit !== undefined) meta.push(`limit:${args.limit}`);",
115
115
  "442:",
116
116
  "443:\t\tconst text = renderStatusLine(",
117
- '444:\t\t\t{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },',
117
+ '444:\t\t\t{ icon: "pending", title: "Glob", description: formatGlobRenderPaths(args.paths) || "*", meta },',
118
118
  "445:\t\t\tuiTheme,",
119
119
  "446:\t\t);",
120
120
  "447:\t\treturn new Text(text, 0, 0);",
@@ -124,7 +124,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
124
124
  ],
125
125
  details: {
126
126
  kind: "file",
127
- resolvedPath: "/Users/dev/Projects/pi/packages/coding-agent/src/tools/find.ts",
127
+ resolvedPath: "/Users/dev/Projects/pi/packages/coding-agent/src/tools/glob.ts",
128
128
  contentType: "text/typescript",
129
129
  displayContent: { text: readSnippet, startLine: 437 },
130
130
  },
@@ -134,7 +134,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
134
134
  content: [
135
135
  {
136
136
  type: "text",
137
- text: "Error: ENOENT: no such file or directory, open 'packages/coding-agent/src/tools/find.ts'",
137
+ text: "Error: ENOENT: no such file or directory, open 'packages/coding-agent/src/tools/glob.ts'",
138
138
  },
139
139
  ],
140
140
  },
@@ -179,8 +179,8 @@ export const fsFixtures: Record<string, GalleryFixture> = {
179
179
  },
180
180
  },
181
181
 
182
- find: {
183
- label: "Find",
182
+ glob: {
183
+ label: "Glob",
184
184
  // Streaming: glob half-typed, no limit yet.
185
185
  streamingArgs: { paths: ["packages/coding-agent/src/tools/*-render"] },
186
186
  args: { paths: ["packages/coding-agent/src/**/*.test.ts"], limit: 50 },
@@ -191,7 +191,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
191
191
  text: [
192
192
  "packages/coding-agent/src/tools/read.test.ts",
193
193
  "packages/coding-agent/src/tools/write.test.ts",
194
- "packages/coding-agent/src/tools/find.test.ts",
194
+ "packages/coding-agent/src/tools/glob.test.ts",
195
195
  "packages/coding-agent/src/cli/gallery-cli.test.ts",
196
196
  "packages/coding-agent/src/edit/edit.test.ts",
197
197
  ].join("\n"),
@@ -205,7 +205,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
205
205
  files: [
206
206
  "packages/coding-agent/src/cli/gallery-cli.test.ts",
207
207
  "packages/coding-agent/src/edit/edit.test.ts",
208
- "packages/coding-agent/src/tools/find.test.ts",
208
+ "packages/coding-agent/src/tools/glob.test.ts",
209
209
  "packages/coding-agent/src/tools/read.test.ts",
210
210
  "packages/coding-agent/src/tools/write.test.ts",
211
211
  ],
@@ -213,7 +213,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
213
213
  },
214
214
  errorResult: {
215
215
  isError: true,
216
- content: [{ type: "text", text: "Find failed: invalid glob pattern '[unclosed'." }],
216
+ content: [{ type: "text", text: "Glob failed: invalid glob pattern '[unclosed'." }],
217
217
  details: { error: "invalid glob pattern '[unclosed'" },
218
218
  },
219
219
  },
@@ -1,9 +1,9 @@
1
- /** Gallery fixtures for the search tools (search, search_tool_bm25, ast_grep). */
1
+ /** Gallery fixtures for the search tools (grep, search_tool_bm25, ast_grep). */
2
2
  import type { GalleryFixture } from "./types";
3
3
 
4
4
  export const searchFixtures: Record<string, GalleryFixture> = {
5
- search: {
6
- label: "Search",
5
+ grep: {
6
+ label: "Grep",
7
7
  streamingArgs: {
8
8
  pattern: "useState",
9
9
  },
@@ -101,7 +101,7 @@ export const searchFixtures: Record<string, GalleryFixture> = {
101
101
  limit: 5,
102
102
  total_tools: 142,
103
103
  activated_tools: ["docling_extract_tables", "docling_convert", "pdf_read_text"],
104
- active_selected_tools: ["read", "search", "edit", "bash"],
104
+ active_selected_tools: ["read", "grep", "edit", "bash"],
105
105
  tools: [
106
106
  {
107
107
  name: "docling_extract_tables",
@@ -326,7 +326,26 @@ function formatAccountHeader(
326
326
  const planType = report.metadata?.planType;
327
327
  if (typeof planType === "string" && planType) header += chalk.dim(` · plan: ${planType}`);
328
328
  const savedResets = report.resetCredits?.availableCount ?? 0;
329
- if (savedResets > 0) header += chalk.cyan(` · ✦ ${savedResets} saved reset${savedResets === 1 ? "" : "s"}`);
329
+ if (savedResets > 0) {
330
+ header += chalk.cyan(` · ✦ ${savedResets} saved reset${savedResets === 1 ? "" : "s"}`);
331
+ const credits = report.resetCredits?.credits;
332
+ if (credits) {
333
+ const expiries = credits
334
+ .filter(c => c.expiresAt)
335
+ .map(c => ({ date: c.expiresAt!, ms: Date.parse(c.expiresAt!) }))
336
+ .filter(c => !Number.isNaN(c.ms))
337
+ .sort((a, b) => a.ms - b.ms);
338
+ const upcoming = expiries.find(c => c.ms > nowMs);
339
+ if (upcoming) {
340
+ header += chalk.dim(
341
+ ` · soonest expires in ${formatDuration(upcoming.ms - nowMs)} (${upcoming.date.slice(0, 10)})`,
342
+ );
343
+ } else {
344
+ const lastExpired = expiries.at(-1);
345
+ if (lastExpired) header += chalk.dim(` · expired (${lastExpired.date.slice(0, 10)})`);
346
+ }
347
+ }
348
+ }
330
349
  if (report.fetchedAt && nowMs - report.fetchedAt > 90_000) {
331
350
  header += chalk.dim(` · fetched ${formatDuration(nowMs - report.fetchedAt)} ago`);
332
351
  }
@@ -120,7 +120,7 @@ ${chalk.bold("Arguments:")}
120
120
 
121
121
  ${chalk.bold("Options:")}
122
122
  --provider <name> Provider: ${PROVIDERS.join(", ")}
123
- --recency <value> Recency filter (Brave/Perplexity): ${RECENCY_OPTIONS.join(", ")}
123
+ --recency <value> Recency filter (when supported): ${RECENCY_OPTIONS.join(", ")}
124
124
  -l, --limit <n> Max results to return
125
125
  --compact Render condensed output
126
126
  -h, --help Show this help
@@ -0,0 +1,13 @@
1
+ import * as os from "node:os";
2
+ import type { InteractiveModeContext } from "../modes/types";
3
+
4
+ /** Display name for this process's user in collab sessions. */
5
+ export function collabDisplayName(ctx: InteractiveModeContext): string {
6
+ const configured = (ctx.settings.get("collab.displayName") ?? "").trim();
7
+ if (configured) return configured;
8
+ try {
9
+ return os.userInfo().username;
10
+ } catch {
11
+ return "anonymous";
12
+ }
13
+ }