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

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 (187) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/cli.js +3621 -3579
  3. package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +8 -4
  5. package/dist/types/advisor/config.d.ts +88 -0
  6. package/dist/types/advisor/index.d.ts +1 -0
  7. package/dist/types/advisor/runtime.d.ts +15 -1
  8. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  9. package/dist/types/advisor/watchdog.d.ts +20 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/config/model-roles.d.ts +1 -1
  12. package/dist/types/config/settings-schema.d.ts +113 -12
  13. package/dist/types/debug/log-viewer.d.ts +1 -0
  14. package/dist/types/debug/raw-sse.d.ts +1 -0
  15. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/edit/index.d.ts +18 -0
  18. package/dist/types/edit/streaming.d.ts +30 -0
  19. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  20. package/dist/types/extensibility/shared-events.d.ts +1 -0
  21. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  22. package/dist/types/extensibility/utils.d.ts +12 -0
  23. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  24. package/dist/types/mcp/transports/index.d.ts +1 -0
  25. package/dist/types/mcp/transports/sse.d.ts +20 -0
  26. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  27. package/dist/types/modes/components/index.d.ts +1 -0
  28. package/dist/types/modes/components/model-selector.d.ts +9 -1
  29. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/component.d.ts +30 -3
  31. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/interactive-mode.d.ts +10 -4
  34. package/dist/types/modes/skill-command.d.ts +32 -0
  35. package/dist/types/modes/types.d.ts +7 -2
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +84 -12
  38. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  39. package/dist/types/session/messages.d.ts +32 -7
  40. package/dist/types/session/messages.test.d.ts +1 -0
  41. package/dist/types/session/session-entries.d.ts +31 -3
  42. package/dist/types/session/session-history-format.d.ts +6 -0
  43. package/dist/types/session/session-loader.d.ts +9 -1
  44. package/dist/types/session/session-manager.d.ts +6 -4
  45. package/dist/types/session/session-storage.d.ts +11 -0
  46. package/dist/types/session/session-title-slot.d.ts +19 -0
  47. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  48. package/dist/types/session/turn-persistence.d.ts +88 -0
  49. package/dist/types/ssh/connection-manager.d.ts +47 -0
  50. package/dist/types/ssh/utils.d.ts +16 -0
  51. package/dist/types/task/executor.d.ts +3 -16
  52. package/dist/types/task/render.d.ts +0 -5
  53. package/dist/types/task/renderer.d.ts +13 -0
  54. package/dist/types/task/types.d.ts +16 -0
  55. package/dist/types/task/yield-assembly.d.ts +28 -0
  56. package/dist/types/tiny/text.d.ts +8 -0
  57. package/dist/types/tools/render-utils.d.ts +2 -0
  58. package/dist/types/tools/review.d.ts +6 -4
  59. package/dist/types/tools/ssh.d.ts +1 -1
  60. package/dist/types/tools/todo.d.ts +6 -0
  61. package/dist/types/tools/yield.d.ts +8 -3
  62. package/dist/types/utils/thinking-display.d.ts +4 -0
  63. package/package.json +12 -12
  64. package/src/advisor/__tests__/advisor.test.ts +438 -10
  65. package/src/advisor/__tests__/config.test.ts +173 -0
  66. package/src/advisor/advise-tool.ts +11 -6
  67. package/src/advisor/config.ts +256 -0
  68. package/src/advisor/index.ts +1 -0
  69. package/src/advisor/runtime.ts +77 -4
  70. package/src/advisor/transcript-recorder.ts +25 -2
  71. package/src/advisor/watchdog.ts +57 -31
  72. package/src/auto-thinking/classifier.ts +2 -2
  73. package/src/autoresearch/index.ts +7 -2
  74. package/src/cli/gc-cli.ts +17 -10
  75. package/src/collab/guest.ts +43 -7
  76. package/src/config/model-registry.ts +80 -18
  77. package/src/config/model-resolver.ts +5 -1
  78. package/src/config/model-roles.ts +3 -3
  79. package/src/config/settings-schema.ts +107 -8
  80. package/src/debug/index.ts +32 -7
  81. package/src/debug/log-viewer.ts +111 -53
  82. package/src/debug/raw-sse.ts +68 -48
  83. package/src/discovery/codex.ts +13 -5
  84. package/src/discovery/omp-extension-roots.ts +38 -13
  85. package/src/edit/hashline/diff.ts +57 -4
  86. package/src/edit/index.ts +21 -0
  87. package/src/edit/streaming.ts +170 -0
  88. package/src/eval/js/shared/local-module-loader.ts +23 -1
  89. package/src/export/html/template.js +13 -7
  90. package/src/extensibility/custom-tools/types.ts +1 -0
  91. package/src/extensibility/extensions/loader.ts +5 -3
  92. package/src/extensibility/extensions/wrapper.ts +9 -3
  93. package/src/extensibility/hooks/loader.ts +3 -3
  94. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  95. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  96. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  97. package/src/extensibility/plugins/manager.ts +76 -5
  98. package/src/extensibility/shared-events.ts +1 -0
  99. package/src/extensibility/tool-event-input.ts +23 -0
  100. package/src/extensibility/utils.ts +74 -0
  101. package/src/internal-urls/docs-index.generated.txt +1 -1
  102. package/src/mcp/client.ts +3 -1
  103. package/src/mcp/manager.ts +12 -5
  104. package/src/mcp/oauth-discovery.ts +5 -29
  105. package/src/mcp/transports/http.ts +3 -1
  106. package/src/mcp/transports/index.ts +1 -0
  107. package/src/mcp/transports/sse.ts +377 -0
  108. package/src/memories/index.ts +15 -6
  109. package/src/mnemopi/backend.ts +2 -2
  110. package/src/modes/acp/acp-agent.ts +1 -1
  111. package/src/modes/components/advisor-config.ts +555 -0
  112. package/src/modes/components/advisor-message.ts +9 -2
  113. package/src/modes/components/agent-hub.ts +9 -4
  114. package/src/modes/components/assistant-message.ts +5 -5
  115. package/src/modes/components/index.ts +2 -0
  116. package/src/modes/components/model-selector.ts +79 -48
  117. package/src/modes/components/settings-selector.ts +1 -0
  118. package/src/modes/components/status-line/component.ts +145 -14
  119. package/src/modes/components/status-line/segments.ts +47 -22
  120. package/src/modes/components/status-line/types.ts +13 -1
  121. package/src/modes/components/tool-execution.ts +47 -6
  122. package/src/modes/controllers/command-controller.ts +23 -2
  123. package/src/modes/controllers/event-controller.ts +114 -11
  124. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  125. package/src/modes/controllers/input-controller.ts +61 -61
  126. package/src/modes/controllers/selector-controller.ts +100 -9
  127. package/src/modes/interactive-mode.ts +65 -10
  128. package/src/modes/print-mode.ts +1 -1
  129. package/src/modes/skill-command.ts +116 -0
  130. package/src/modes/types.ts +7 -2
  131. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  132. package/src/modes/utils/ui-helpers.ts +46 -27
  133. package/src/prompts/agents/reviewer.md +11 -10
  134. package/src/prompts/review-custom-request.md +1 -2
  135. package/src/prompts/review-request.md +1 -2
  136. package/src/prompts/system/interrupted-thinking.md +7 -0
  137. package/src/prompts/system/recap-user.md +9 -0
  138. package/src/prompts/system/subagent-system-prompt.md +8 -5
  139. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  140. package/src/prompts/system/system-prompt.md +0 -1
  141. package/src/prompts/tools/irc.md +2 -2
  142. package/src/prompts/tools/read.md +2 -2
  143. package/src/sdk.ts +40 -50
  144. package/src/session/agent-session.ts +1139 -600
  145. package/src/session/indexed-session-storage.ts +86 -13
  146. package/src/session/messages.test.ts +125 -0
  147. package/src/session/messages.ts +192 -21
  148. package/src/session/redis-session-storage.ts +49 -2
  149. package/src/session/session-entries.ts +39 -2
  150. package/src/session/session-history-format.ts +29 -2
  151. package/src/session/session-listing.ts +54 -24
  152. package/src/session/session-loader.ts +66 -3
  153. package/src/session/session-manager.ts +113 -19
  154. package/src/session/session-persistence.ts +96 -3
  155. package/src/session/session-storage.ts +36 -0
  156. package/src/session/session-title-slot.ts +141 -0
  157. package/src/session/settings-stream-fn.ts +49 -0
  158. package/src/session/sql-session-storage.ts +71 -11
  159. package/src/session/turn-persistence.ts +142 -0
  160. package/src/session/unexpected-stop-classifier.ts +2 -2
  161. package/src/slash-commands/builtin-registry.ts +16 -3
  162. package/src/slash-commands/helpers/mcp.ts +2 -1
  163. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  164. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  165. package/src/ssh/connection-manager.ts +139 -12
  166. package/src/ssh/file-transfer.ts +23 -18
  167. package/src/ssh/ssh-executor.ts +2 -13
  168. package/src/ssh/utils.ts +19 -0
  169. package/src/task/executor.ts +21 -23
  170. package/src/task/render.ts +162 -20
  171. package/src/task/renderer.ts +14 -0
  172. package/src/task/types.ts +17 -0
  173. package/src/task/yield-assembly.ts +207 -0
  174. package/src/tiny/models.ts +8 -6
  175. package/src/tiny/text.ts +37 -7
  176. package/src/tools/ask.ts +55 -4
  177. package/src/tools/image-gen.ts +2 -1
  178. package/src/tools/render-utils.ts +2 -0
  179. package/src/tools/renderers.ts +8 -2
  180. package/src/tools/review.ts +17 -7
  181. package/src/tools/ssh.ts +8 -4
  182. package/src/tools/todo.ts +17 -1
  183. package/src/tools/tts.ts +2 -1
  184. package/src/tools/yield.ts +140 -31
  185. package/src/utils/thinking-display.ts +15 -0
  186. package/src/utils/title-generator.ts +1 -1
  187. package/src/web/search/providers/tavily.ts +36 -19
@@ -6,6 +6,8 @@ import type { Theme } from "../modes/theme/theme";
6
6
  import type { ToolSession } from "../sdk";
7
7
  import type { SessionEntry } from "../session/session-entries";
8
8
  export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
9
+ /** Operation names accepted by the todo tool and echoed in successful result details. */
10
+ export type TodoOperation = "init" | "start" | "done" | "rm" | "drop" | "append" | "view";
9
11
  export interface TodoItem {
10
12
  content: string;
11
13
  status: TodoStatus;
@@ -19,6 +21,8 @@ export interface TodoCompletionTransition {
19
21
  content: string;
20
22
  }
21
23
  export interface TodoToolDetails {
24
+ /** Operation that produced this snapshot; absent on legacy transcript entries. */
25
+ op?: TodoOperation;
22
26
  phases: TodoPhase[];
23
27
  storage: "session" | "memory";
24
28
  completedTasks?: TodoCompletionTransition[];
@@ -35,6 +39,8 @@ declare const todoSchema: import("arktype/internal/variants/object.ts").ObjectTy
35
39
  }, {}>;
36
40
  type TodoParams = TodoSchema;
37
41
  type TodoSchema = typeof todoSchema.infer;
42
+ /** Return the active todo task, preferring an in-progress item over the first pending item. */
43
+ export declare function nextActionableTask(phases: readonly TodoPhase[]): TodoItem | undefined;
38
44
  export declare const USER_TODO_EDIT_CUSTOM_TYPE = "user_todo_edit";
39
45
  export declare function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPhase[];
40
46
  /**
@@ -1,15 +1,20 @@
1
1
  /**
2
- * Submit result tool for structured subagent output.
2
+ * Result submission tool for subagent output.
3
3
  *
4
- * Subagents must call this tool to finish and return structured JSON output.
4
+ * Subagents can call this tool incrementally or terminally depending on `type`.
5
5
  */
6
6
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { TSchema } from "@oh-my-pi/pi-ai/types";
8
8
  import type { ToolSession } from ".";
9
9
  export interface YieldDetails {
10
- data: unknown;
10
+ /** Successful result payload, or omitted when `useLastTurn` requests last-turn extraction. */
11
+ data?: unknown;
11
12
  status: "success" | "aborted";
12
13
  error?: string;
14
+ /** Optional result section/classification supplied by the yield caller. */
15
+ type?: string | string[];
16
+ /** True when the caller intentionally omitted success data so the executor uses the last assistant turn. */
17
+ useLastTurn?: boolean;
13
18
  /**
14
19
  * Set when the yield tool exhausted its in-tool schema-retry budget
15
20
  * (MAX_SCHEMA_RETRIES) and accepted the data anyway. Surfaced so the
@@ -1,3 +1,7 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
1
2
  export declare function canonicalizeMessage(text: string | null | undefined): string;
2
3
  export declare function formatThinkingForDisplay(text: string, proseOnly: boolean): string;
4
+ /** Whether a formatted thinking block has non-placeholder content worth rendering. */
3
5
  export declare function hasDisplayableThinking(text: string | null | undefined, formattedText: string | null | undefined): boolean;
6
+ /** Whether an assistant message contains thinking content the TUI can reveal. */
7
+ export declare function messageHasDisplayableThinking(message: AgentMessage, proseOnly: boolean): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.2.1",
4
+ "version": "16.2.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -55,17 +55,17 @@
55
55
  "@agentclientprotocol/sdk": "0.25.0",
56
56
  "@babel/parser": "^7.29.7",
57
57
  "@mozilla/readability": "^0.6.0",
58
- "@oh-my-pi/hashline": "16.2.1",
59
- "@oh-my-pi/omp-stats": "16.2.1",
60
- "@oh-my-pi/pi-agent-core": "16.2.1",
61
- "@oh-my-pi/pi-ai": "16.2.1",
62
- "@oh-my-pi/pi-catalog": "16.2.1",
63
- "@oh-my-pi/pi-mnemopi": "16.2.1",
64
- "@oh-my-pi/pi-natives": "16.2.1",
65
- "@oh-my-pi/pi-tui": "16.2.1",
66
- "@oh-my-pi/pi-utils": "16.2.1",
67
- "@oh-my-pi/pi-wire": "16.2.1",
68
- "@oh-my-pi/snapcompact": "16.2.1",
58
+ "@oh-my-pi/hashline": "16.2.3",
59
+ "@oh-my-pi/omp-stats": "16.2.3",
60
+ "@oh-my-pi/pi-agent-core": "16.2.3",
61
+ "@oh-my-pi/pi-ai": "16.2.3",
62
+ "@oh-my-pi/pi-catalog": "16.2.3",
63
+ "@oh-my-pi/pi-mnemopi": "16.2.3",
64
+ "@oh-my-pi/pi-natives": "16.2.3",
65
+ "@oh-my-pi/pi-tui": "16.2.3",
66
+ "@oh-my-pi/pi-utils": "16.2.3",
67
+ "@oh-my-pi/pi-wire": "16.2.3",
68
+ "@oh-my-pi/snapcompact": "16.2.3",
69
69
  "@opentelemetry/api": "^1.9.1",
70
70
  "@opentelemetry/context-async-hooks": "^2.7.1",
71
71
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1,25 +1,33 @@
1
1
  import { describe, expect, it, vi } from "bun:test";
2
2
  import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
3
+ import type { TUI } from "@oh-my-pi/pi-tui";
3
4
  import { type } from "arktype";
5
+ import type { ModelRegistry } from "../../config/model-registry";
6
+ import type { Settings } from "../../config/settings";
7
+ import { type AdvisorConfigDeps, AdvisorConfigOverlayComponent } from "../../modes/components/advisor-config";
4
8
  import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
5
- import { getThemeByName } from "../../modes/theme/theme";
9
+ import { getThemeByName, setThemeInstance } from "../../modes/theme/theme";
6
10
  import advisorSystemPrompt from "../../prompts/advisor/system.md" with { type: "text" };
7
11
  import { SecretObfuscator } from "../../secrets/obfuscator";
8
12
  import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
9
13
  import { YieldQueue } from "../../session/yield-queue";
14
+ import { BUILTIN_TOOL_NAMES } from "../../tools/builtin-names";
10
15
  import {
11
- ADVISOR_READONLY_TOOL_NAMES,
16
+ ADVISOR_DEFAULT_TOOL_NAMES,
12
17
  AdviseTool,
13
18
  type AdvisorAgent,
14
19
  type AdvisorNote,
15
20
  AdvisorRuntime,
16
21
  type AdvisorRuntimeHost,
22
+ advisorTranscriptFilename,
17
23
  deriveAdvisorTelemetry,
18
24
  formatAdvisorBatchContent,
19
25
  formatAdvisorContextPrompt,
20
26
  isAdvisorInterruptImmuneTurnActive,
27
+ isAdvisorTranscriptName,
21
28
  isInterruptingSeverity,
22
29
  resolveAdvisorDeliveryChannel,
30
+ type WatchdogConfigDoc,
23
31
  } from "..";
24
32
 
25
33
  describe("advisor", () => {
@@ -158,6 +166,57 @@ describe("advisor", () => {
158
166
  });
159
167
  });
160
168
 
169
+ describe("formatSessionHistoryMarkdown expandEditDiffs", () => {
170
+ const diff = "--- a/foo.ts\n+++ b/foo.ts\n@@ -1,2 +1,2 @@\n-const x = 1;\n+const x = 2;";
171
+ const editCall = {
172
+ role: "assistant",
173
+ content: [{ type: "toolCall", id: "c1", name: "edit", arguments: { path: "foo.ts" } }],
174
+ timestamp: 1,
175
+ } as unknown as AgentMessage;
176
+ const editResult = {
177
+ role: "toolResult",
178
+ toolCallId: "c1",
179
+ toolName: "edit",
180
+ content: "ok",
181
+ details: { diff },
182
+ timestamp: 2,
183
+ } as unknown as AgentMessage;
184
+
185
+ it("appends the full diff in a fenced block when expandEditDiffs is set", () => {
186
+ const md = formatSessionHistoryMarkdown([editCall, editResult], {
187
+ expandEditDiffs: true,
188
+ watchedRoles: true,
189
+ });
190
+ expect(md).toContain("```diff");
191
+ expect(md).toContain("-const x = 1;");
192
+ expect(md).toContain("+const x = 2;");
193
+ });
194
+
195
+ it("omits the diff body without the flag", () => {
196
+ const md = formatSessionHistoryMarkdown([editCall, editResult], { watchedRoles: true });
197
+ expect(md).not.toContain("```diff");
198
+ expect(md).not.toContain("+const x = 2;");
199
+ });
200
+
201
+ it("widens the fence past backtick runs in the diff body", () => {
202
+ const fenced = "--- a/readme.md\n+++ b/readme.md\n@@ -1 +1 @@\n-```\n+```ts\n+code\n+```";
203
+ const result = {
204
+ role: "toolResult",
205
+ toolCallId: "c1",
206
+ toolName: "edit",
207
+ content: "ok",
208
+ details: { diff: fenced },
209
+ timestamp: 2,
210
+ } as unknown as AgentMessage;
211
+ const md = formatSessionHistoryMarkdown([editCall, result], {
212
+ expandEditDiffs: true,
213
+ watchedRoles: true,
214
+ });
215
+ // The body contains a ``` run, so the wrapping fence widens to 4 backticks.
216
+ expect(md).toContain("````diff");
217
+ });
218
+ });
219
+
161
220
  describe("advisor yield-queue dispatcher", () => {
162
221
  it("batches advice notes into one custom message", async () => {
163
222
  const injected: AgentMessage[] = [];
@@ -373,6 +432,18 @@ describe("advisor", () => {
373
432
  // Exactly one severity attribute (only the blocker note carries one).
374
433
  expect(content.split('severity="').length - 1).toBe(1);
375
434
  });
435
+
436
+ it("emits an advisor attribute only for named advisors, escaping the name", () => {
437
+ const content = formatAdvisorBatchContent([
438
+ { note: "named note", advisor: 'Arch "X"' },
439
+ { note: "default note" },
440
+ ]);
441
+ // Named advisor: attribute present, double quote escaped for attribute context.
442
+ expect(content).toContain('advisor="Arch "X""');
443
+ // A note with no source (the legacy/default advisor) carries no advisor attribute.
444
+ expect(content.split('advisor="').length - 1).toBe(1);
445
+ expect(content).toContain("default note");
446
+ });
376
447
  });
377
448
 
378
449
  describe("deriveAdvisorTelemetry", () => {
@@ -662,6 +733,46 @@ describe("advisor", () => {
662
733
  expect(promptInputs[0]).not.toContain(secret);
663
734
  });
664
735
 
736
+ it("surfaces edit diff details but redacts secrets inside the diff", async () => {
737
+ const secret = "DIFF_SECRET_TOKEN_123";
738
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
739
+ const placeholder = obfuscator.obfuscate(secret);
740
+ const promptInputs: string[] = [];
741
+ const agent = makeAgent(promptInputs);
742
+ const diff = `--- a/config.ts\n+++ b/config.ts\n@@ -1 +1 @@\n-const token = "old";\n+const token = "${secret}";`;
743
+ const messages: AgentMessage[] = [
744
+ {
745
+ role: "assistant",
746
+ content: [{ type: "toolCall", id: "c1", name: "edit", arguments: { path: "config.ts" } }],
747
+ timestamp: 1,
748
+ } as unknown as AgentMessage,
749
+ {
750
+ role: "toolResult",
751
+ toolCallId: "c1",
752
+ toolName: "edit",
753
+ content: "ok",
754
+ details: { diff },
755
+ timestamp: 2,
756
+ } as unknown as AgentMessage,
757
+ ];
758
+ const host: AdvisorRuntimeHost = {
759
+ snapshotMessages: () => messages,
760
+ enqueueAdvice: () => {},
761
+ obfuscator,
762
+ };
763
+ const runtime = new AdvisorRuntime(agent, host);
764
+
765
+ runtime.onTurnEnd();
766
+ await Promise.resolve();
767
+
768
+ expect(promptInputs).toHaveLength(1);
769
+ // The diff is surfaced to the advisor (expandEditDiffs) ...
770
+ expect(promptInputs[0]).toContain("+const token =");
771
+ // ... but a secret living inside details.diff is obfuscated (details now walked).
772
+ expect(promptInputs[0]).toContain(placeholder);
773
+ expect(promptInputs[0]).not.toContain(secret);
774
+ });
775
+
665
776
  it("expands plan-mode context once, then collapses an unchanged re-injection", async () => {
666
777
  const promptInputs: string[] = [];
667
778
  const agent = makeAgent(promptInputs);
@@ -1005,6 +1116,202 @@ describe("advisor", () => {
1005
1116
  expect(runtime.backlog).toBe(0);
1006
1117
  });
1007
1118
 
1119
+ it("notifies the host once when consecutive prompt failures make the advisor unavailable", async () => {
1120
+ const promptInputs: string[] = [];
1121
+ const failures: unknown[] = [];
1122
+ let shouldFail = true;
1123
+ const agent: AdvisorAgent = {
1124
+ prompt: async input => {
1125
+ promptInputs.push(input);
1126
+ if (shouldFail) {
1127
+ throw new Error("404 No endpoints available matching your guardrail restrictions and data policy.");
1128
+ }
1129
+ },
1130
+ abort: () => {},
1131
+ reset: () => {},
1132
+ state: { messages: [] },
1133
+ };
1134
+ const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
1135
+ const host: AdvisorRuntimeHost = {
1136
+ snapshotMessages: () => messages,
1137
+ enqueueAdvice: () => {},
1138
+ notifyFailure: error => failures.push(error),
1139
+ };
1140
+ const runtime = new AdvisorRuntime(agent, host, 0);
1141
+
1142
+ runtime.onTurnEnd(messages);
1143
+ await Bun.sleep(0);
1144
+ await Bun.sleep(0);
1145
+ await Bun.sleep(0);
1146
+
1147
+ expect(promptInputs).toHaveLength(3);
1148
+ expect(failures).toHaveLength(1);
1149
+ const failure = failures[0];
1150
+ expect(failure).toBeInstanceOf(Error);
1151
+ if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
1152
+ expect(failure.message).toContain("No endpoints available");
1153
+
1154
+ messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
1155
+ runtime.onTurnEnd(messages);
1156
+ await Bun.sleep(0);
1157
+ await Bun.sleep(0);
1158
+ await Bun.sleep(0);
1159
+
1160
+ expect(promptInputs).toHaveLength(6);
1161
+ expect(failures).toHaveLength(1);
1162
+
1163
+ shouldFail = false;
1164
+ messages.push({ role: "user", content: "ccc", timestamp: 3 } as AgentMessage);
1165
+ runtime.onTurnEnd(messages);
1166
+ await Bun.sleep(0);
1167
+ expect(failures).toHaveLength(1);
1168
+
1169
+ shouldFail = true;
1170
+ messages.push({ role: "user", content: "ddd", timestamp: 4 } as AgentMessage);
1171
+ runtime.onTurnEnd(messages);
1172
+ await Bun.sleep(0);
1173
+ await Bun.sleep(0);
1174
+ await Bun.sleep(0);
1175
+
1176
+ expect(failures).toHaveLength(2);
1177
+ });
1178
+
1179
+ it("treats a clean prompt resolution with state.error as a failed turn (real Agent contract)", async () => {
1180
+ // `Agent.#runLoop` catches provider/stream failures internally — it resolves
1181
+ // `prompt()` cleanly and stores the message on `state.error` (e.g. the
1182
+ // OpenRouter ZDR `404 No endpoints available` case from #3635). The runtime
1183
+ // must surface that as a failed turn even though the awaited promise did
1184
+ // not reject.
1185
+ const promptInputs: string[] = [];
1186
+ const failures: unknown[] = [];
1187
+ const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
1188
+ let shouldFail = true;
1189
+ const agent: AdvisorAgent = {
1190
+ prompt: async input => {
1191
+ promptInputs.push(input);
1192
+ state.error = shouldFail
1193
+ ? "404 No endpoints available matching your guardrail restrictions and data policy."
1194
+ : undefined;
1195
+ },
1196
+ abort: () => {},
1197
+ reset: () => {
1198
+ state.error = undefined;
1199
+ },
1200
+ state,
1201
+ };
1202
+ const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
1203
+ const host: AdvisorRuntimeHost = {
1204
+ snapshotMessages: () => messages,
1205
+ enqueueAdvice: () => {},
1206
+ notifyFailure: error => failures.push(error),
1207
+ };
1208
+ const runtime = new AdvisorRuntime(agent, host, 0);
1209
+
1210
+ runtime.onTurnEnd(messages);
1211
+ await Bun.sleep(0);
1212
+ await Bun.sleep(0);
1213
+ await Bun.sleep(0);
1214
+
1215
+ expect(promptInputs).toHaveLength(3);
1216
+ expect(failures).toHaveLength(1);
1217
+ const failure = failures[0];
1218
+ if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
1219
+ expect(failure.message).toContain("No endpoints available");
1220
+ expect(runtime.backlog).toBe(0);
1221
+
1222
+ shouldFail = false;
1223
+ messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
1224
+ runtime.onTurnEnd(messages);
1225
+ await Bun.sleep(0);
1226
+ expect(failures).toHaveLength(1);
1227
+
1228
+ shouldFail = true;
1229
+ messages.push({ role: "user", content: "ccc", timestamp: 3 } as AgentMessage);
1230
+ runtime.onTurnEnd(messages);
1231
+ await Bun.sleep(0);
1232
+ await Bun.sleep(0);
1233
+ await Bun.sleep(0);
1234
+
1235
+ expect(failures).toHaveLength(2);
1236
+ });
1237
+
1238
+ it("rolls advisor state back after each failed prompt so retries don't replay duplicate turns", async () => {
1239
+ // The real `Agent` appends the user batch + a synthetic `stopReason: "error"`
1240
+ // assistant turn before `state.error` is read. Without rollback, the runtime's
1241
+ // retry/drop path would replay the failed batch on top of those orphans,
1242
+ // duplicating session-update user turns and leaking dropped failures into the
1243
+ // next successful run's context.
1244
+ const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
1245
+ const rollbackCalls: number[] = [];
1246
+ const lengthsBeforePrompt: number[] = [];
1247
+ let shouldFail = true;
1248
+ const agent: AdvisorAgent = {
1249
+ prompt: async input => {
1250
+ lengthsBeforePrompt.push(state.messages.length);
1251
+ state.messages.push({ role: "user", content: input, timestamp: Date.now() } as AgentMessage);
1252
+ if (shouldFail) {
1253
+ state.messages.push({
1254
+ role: "assistant",
1255
+ content: [{ type: "text", text: "" }],
1256
+ stopReason: "error",
1257
+ errorMessage: "404 No endpoints available",
1258
+ timestamp: Date.now(),
1259
+ } as unknown as AgentMessage);
1260
+ state.error = "404 No endpoints available";
1261
+ } else {
1262
+ state.messages.push({
1263
+ role: "assistant",
1264
+ content: [{ type: "text", text: "ok" }],
1265
+ timestamp: Date.now(),
1266
+ } as unknown as AgentMessage);
1267
+ state.error = undefined;
1268
+ }
1269
+ },
1270
+ abort: () => {},
1271
+ reset: () => {
1272
+ state.messages.length = 0;
1273
+ state.error = undefined;
1274
+ },
1275
+ rollbackTo: count => {
1276
+ rollbackCalls.push(count);
1277
+ if (count < state.messages.length) state.messages.length = count;
1278
+ state.error = undefined;
1279
+ },
1280
+ state,
1281
+ };
1282
+ const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
1283
+ const host: AdvisorRuntimeHost = {
1284
+ snapshotMessages: () => messages,
1285
+ enqueueAdvice: () => {},
1286
+ };
1287
+ const runtime = new AdvisorRuntime(agent, host, 0);
1288
+
1289
+ runtime.onTurnEnd(messages);
1290
+ await Bun.sleep(0);
1291
+ await Bun.sleep(0);
1292
+ await Bun.sleep(0);
1293
+
1294
+ // Three failed prompts each rolled back to the empty baseline, so every retry
1295
+ // saw a clean state.messages instead of stacked failed turns.
1296
+ expect(lengthsBeforePrompt).toEqual([0, 0, 0]);
1297
+ expect(rollbackCalls).toEqual([0, 0, 0]);
1298
+ // The drop-after-3 path also left state.messages empty — no orphan failed
1299
+ // turns leak into the next successful run's context.
1300
+ expect(state.messages).toHaveLength(0);
1301
+ expect(state.error).toBeUndefined();
1302
+
1303
+ // A subsequent successful run starts from the clean baseline and is NOT
1304
+ // rolled back.
1305
+ shouldFail = false;
1306
+ messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
1307
+ runtime.onTurnEnd(messages);
1308
+ await Bun.sleep(0);
1309
+
1310
+ expect(lengthsBeforePrompt[lengthsBeforePrompt.length - 1]).toBe(0);
1311
+ expect(rollbackCalls).toHaveLength(3);
1312
+ expect(state.messages).toHaveLength(2);
1313
+ });
1314
+
1008
1315
  it("drops the in-flight batch when a reset aborts the advisor prompt", async () => {
1009
1316
  const promptInputs: string[] = [];
1010
1317
  const { promise: firstPromptStarted, resolve: startFirstPrompt } = Promise.withResolvers<void>();
@@ -1063,14 +1370,18 @@ describe("advisor", () => {
1063
1370
  });
1064
1371
  });
1065
1372
 
1066
- describe("read-only tool allowlist", () => {
1067
- it("selects only the investigation tools from a mixed toolset", () => {
1068
- const toolset = ["read", "edit", "grep", "bash", "glob", "write", "advise"];
1069
- const selected = toolset.filter(name => ADVISOR_READONLY_TOOL_NAMES.has(name));
1070
- expect(selected).toEqual(["read", "grep", "glob"]);
1071
- expect(ADVISOR_READONLY_TOOL_NAMES.has("edit")).toBe(false);
1072
- expect(ADVISOR_READONLY_TOOL_NAMES.has("bash")).toBe(false);
1073
- expect(ADVISOR_READONLY_TOOL_NAMES.has("write")).toBe(false);
1373
+ describe("advisor default tools", () => {
1374
+ it("defaults to read/grep/glob, a subset of the full grantable tool pool", () => {
1375
+ expect([...ADVISOR_DEFAULT_TOOL_NAMES]).toEqual(["read", "grep", "glob"]);
1376
+ // The advisor is a full agent now: every built tool is grantable (no hard
1377
+ // read-only restriction), including mutating ones like edit/bash/write.
1378
+ const builtin = new Set<string>(BUILTIN_TOOL_NAMES);
1379
+ for (const name of ["read", "grep", "glob", "edit", "bash", "write"]) {
1380
+ expect(builtin.has(name)).toBe(true);
1381
+ }
1382
+ for (const name of ADVISOR_DEFAULT_TOOL_NAMES) {
1383
+ expect(builtin.has(name)).toBe(true);
1384
+ }
1074
1385
  });
1075
1386
  });
1076
1387
 
@@ -1093,6 +1404,26 @@ describe("advisor", () => {
1093
1404
  expect(text).toContain("watch the empty case");
1094
1405
  });
1095
1406
 
1407
+ it("prefixes the note with a named-advisor label, but not for the default advisor", async () => {
1408
+ const uiTheme = await getThemeByName("dark");
1409
+ if (!uiTheme) throw new Error("theme unavailable");
1410
+ const card = createAdvisorMessageCard(
1411
+ {
1412
+ notes: [
1413
+ { note: "module boundary leak", severity: "concern", advisor: "Architecture" },
1414
+ { note: "default-advisor note", advisor: "default" },
1415
+ ],
1416
+ },
1417
+ () => true,
1418
+ uiTheme,
1419
+ );
1420
+ const text = strip(card.render(80));
1421
+ expect(text).toContain("[Architecture]");
1422
+ expect(text).toContain("module boundary leak");
1423
+ // The implicit "default" advisor stays unlabeled.
1424
+ expect(text).not.toContain("[default]");
1425
+ });
1426
+
1096
1427
  it("collapses to the first notes with an overflow hint", async () => {
1097
1428
  const uiTheme = await getThemeByName("dark");
1098
1429
  if (!uiTheme) throw new Error("theme unavailable");
@@ -1231,4 +1562,101 @@ describe("advisor", () => {
1231
1562
  }
1232
1563
  });
1233
1564
  });
1565
+ describe("advisor transcript filenames", () => {
1566
+ it("derives default and named transcript filenames", () => {
1567
+ expect(advisorTranscriptFilename("")).toBe("__advisor.jsonl");
1568
+ expect(advisorTranscriptFilename("arch")).toBe("__advisor.arch.jsonl");
1569
+ });
1570
+
1571
+ it("recognizes default and named advisor transcripts, and nothing else", () => {
1572
+ expect(isAdvisorTranscriptName("__advisor.jsonl")).toBe(true);
1573
+ expect(isAdvisorTranscriptName("__advisor.arch.jsonl")).toBe(true);
1574
+ expect(isAdvisorTranscriptName("__advisor-2.jsonl")).toBe(false);
1575
+ expect(isAdvisorTranscriptName("Foo.jsonl")).toBe(false);
1576
+ expect(isAdvisorTranscriptName("__advisor.arch.bak")).toBe(false);
1577
+ });
1578
+ });
1579
+
1580
+ describe("AdvisorConfigOverlayComponent", () => {
1581
+ const deps = {
1582
+ modelRegistry: { getCanonicalModelSelections: () => [] } as unknown as ModelRegistry,
1583
+ settings: {} as unknown as Settings,
1584
+ scopedModels: [],
1585
+ availableToolNames: ["read", "grep", "glob", "lsp", "web_search"],
1586
+ };
1587
+ const callbacks = {
1588
+ loadDoc: async () => ({ advisors: [] }),
1589
+ save: async () => {},
1590
+ close: () => {},
1591
+ requestRender: () => {},
1592
+ notify: () => {},
1593
+ };
1594
+ const strip = (lines: readonly string[]): string => lines.join("\n").replace(/\x1b\[[0-9;]*m/g, "");
1595
+ const make = (doc: WatchdogConfigDoc, extra?: Partial<AdvisorConfigDeps>): AdvisorConfigOverlayComponent =>
1596
+ new AdvisorConfigOverlayComponent({} as unknown as TUI, { ...deps, ...extra }, "project", doc, callbacks);
1597
+ const fullHeight = Math.max(14, process.stdout.rows || 40);
1598
+
1599
+ it("paints a full-screen split frame: roster sidebar + selected-advisor preview", async () => {
1600
+ const uiTheme = await getThemeByName("dark");
1601
+ if (!uiTheme) throw new Error("theme unavailable");
1602
+ setThemeInstance(uiTheme);
1603
+ const overlay = make({
1604
+ instructions: "shared baseline",
1605
+ advisors: [
1606
+ { name: "Architecture", model: "x-ai/grok-code-fast:high" },
1607
+ { name: "Security", tools: ["read", "web_search"] },
1608
+ ],
1609
+ });
1610
+ const frame = overlay.render(200);
1611
+ // Fills the screen top-to-bottom (the fix for the bottom-anchored frame
1612
+ // whose offset broke mouse hit-testing and wasted the upper space).
1613
+ expect(frame.length).toBe(fullHeight);
1614
+ const text = strip(frame);
1615
+ expect(text).toContain("Advisor configuration");
1616
+ expect(text).toContain("project");
1617
+ expect(text).toContain("Architecture");
1618
+ expect(text).toContain("Security");
1619
+ expect(text).toContain("+ Add advisor");
1620
+ expect(text).toContain("Save & apply");
1621
+ // Right preview reflects the highlighted (first) advisor.
1622
+ expect(text).toContain("x-ai/grok-code-fast:high");
1623
+ expect(text).toContain("read, grep, glob (default)");
1624
+ });
1625
+
1626
+ it("moves the preview with keyboard selection and preserves an explicit tool set", async () => {
1627
+ const uiTheme = await getThemeByName("dark");
1628
+ if (!uiTheme) throw new Error("theme unavailable");
1629
+ setThemeInstance(uiTheme);
1630
+ const overlay = make({
1631
+ advisors: [{ name: "Architecture" }, { name: "Security", tools: ["read", "web_search"] }],
1632
+ });
1633
+ overlay.render(200);
1634
+ overlay.handleInput("\x1b[B"); // arrow down → highlight Security
1635
+ expect(strip(overlay.render(200))).toContain("read, web_search");
1636
+ });
1637
+
1638
+ it("opens an advisor's detail editor on a left click in the sidebar", async () => {
1639
+ const uiTheme = await getThemeByName("dark");
1640
+ if (!uiTheme) throw new Error("theme unavailable");
1641
+ setThemeInstance(uiTheme);
1642
+ const overlay = make({ advisors: [{ name: "Architecture" }, { name: "Security" }] });
1643
+ // Render once so the frame geometry is recorded; the first advisor sits on
1644
+ // the first body row (0-based screen row 1 → SGR 1-based row 2).
1645
+ overlay.render(120);
1646
+ overlay.handleInput("\x1b[<0;4;2M"); // left-button press, col 4, row 2
1647
+ const text = strip(overlay.render(120));
1648
+ expect(text).toContain("Editing");
1649
+ expect(text).toContain("Architecture");
1650
+ });
1651
+
1652
+ it("seeds a visible default advisor (labeled with the role model) when the config is empty", async () => {
1653
+ const uiTheme = await getThemeByName("dark");
1654
+ if (!uiTheme) throw new Error("theme unavailable");
1655
+ setThemeInstance(uiTheme);
1656
+ const overlay = make({ advisors: [] }, { defaultModelLabel: "anthropic/claude-opus" });
1657
+ const text = strip(overlay.render(200));
1658
+ expect(text).toContain("default");
1659
+ expect(text).toContain("anthropic/claude-opus");
1660
+ });
1661
+ });
1234
1662
  });