@oh-my-pi/pi-coding-agent 16.2.2 → 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 (150) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli.js +3624 -3568
  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/transcript-recorder.d.ts +13 -2
  8. package/dist/types/advisor/watchdog.d.ts +20 -0
  9. package/dist/types/collab/guest.d.ts +29 -0
  10. package/dist/types/config/settings-schema.d.ts +81 -0
  11. package/dist/types/debug/log-viewer.d.ts +1 -0
  12. package/dist/types/debug/raw-sse.d.ts +1 -0
  13. package/dist/types/edit/hashline/diff.d.ts +0 -11
  14. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  15. package/dist/types/extensibility/utils.d.ts +12 -0
  16. package/dist/types/mcp/transports/index.d.ts +1 -0
  17. package/dist/types/mcp/transports/sse.d.ts +20 -0
  18. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  19. package/dist/types/modes/components/index.d.ts +1 -0
  20. package/dist/types/modes/components/model-selector.d.ts +9 -1
  21. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  22. package/dist/types/modes/components/status-line/component.d.ts +30 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  24. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  25. package/dist/types/modes/interactive-mode.d.ts +10 -4
  26. package/dist/types/modes/skill-command.d.ts +32 -0
  27. package/dist/types/modes/types.d.ts +7 -2
  28. package/dist/types/session/agent-session.d.ts +58 -10
  29. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  30. package/dist/types/session/messages.d.ts +26 -0
  31. package/dist/types/session/messages.test.d.ts +1 -0
  32. package/dist/types/session/session-entries.d.ts +31 -3
  33. package/dist/types/session/session-history-format.d.ts +6 -0
  34. package/dist/types/session/session-loader.d.ts +9 -1
  35. package/dist/types/session/session-manager.d.ts +6 -4
  36. package/dist/types/session/session-storage.d.ts +11 -0
  37. package/dist/types/session/session-title-slot.d.ts +19 -0
  38. package/dist/types/ssh/connection-manager.d.ts +47 -0
  39. package/dist/types/ssh/utils.d.ts +16 -0
  40. package/dist/types/task/executor.d.ts +3 -16
  41. package/dist/types/task/render.d.ts +0 -5
  42. package/dist/types/task/renderer.d.ts +13 -0
  43. package/dist/types/task/types.d.ts +16 -0
  44. package/dist/types/task/yield-assembly.d.ts +28 -0
  45. package/dist/types/tiny/text.d.ts +8 -0
  46. package/dist/types/tools/render-utils.d.ts +2 -0
  47. package/dist/types/tools/review.d.ts +6 -4
  48. package/dist/types/tools/ssh.d.ts +1 -1
  49. package/dist/types/tools/todo.d.ts +6 -0
  50. package/dist/types/tools/yield.d.ts +8 -3
  51. package/dist/types/utils/thinking-display.d.ts +4 -0
  52. package/package.json +12 -12
  53. package/src/advisor/__tests__/advisor.test.ts +242 -10
  54. package/src/advisor/__tests__/config.test.ts +173 -0
  55. package/src/advisor/advise-tool.ts +11 -6
  56. package/src/advisor/config.ts +256 -0
  57. package/src/advisor/index.ts +1 -0
  58. package/src/advisor/runtime.ts +12 -2
  59. package/src/advisor/transcript-recorder.ts +25 -2
  60. package/src/advisor/watchdog.ts +57 -31
  61. package/src/autoresearch/index.ts +7 -2
  62. package/src/cli/gc-cli.ts +17 -10
  63. package/src/collab/guest.ts +43 -7
  64. package/src/config/model-registry.ts +80 -18
  65. package/src/config/settings-schema.ts +77 -0
  66. package/src/debug/index.ts +32 -7
  67. package/src/debug/log-viewer.ts +111 -53
  68. package/src/debug/raw-sse.ts +68 -48
  69. package/src/discovery/codex.ts +13 -5
  70. package/src/edit/hashline/diff.ts +57 -4
  71. package/src/eval/js/shared/local-module-loader.ts +23 -1
  72. package/src/export/html/template.js +13 -7
  73. package/src/extensibility/extensions/loader.ts +5 -3
  74. package/src/extensibility/extensions/wrapper.ts +9 -3
  75. package/src/extensibility/hooks/loader.ts +3 -3
  76. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  77. package/src/extensibility/plugins/manager.ts +2 -1
  78. package/src/extensibility/tool-event-input.ts +23 -0
  79. package/src/extensibility/utils.ts +74 -0
  80. package/src/internal-urls/docs-index.generated.txt +1 -1
  81. package/src/mcp/client.ts +3 -1
  82. package/src/mcp/manager.ts +12 -5
  83. package/src/mcp/transports/index.ts +1 -0
  84. package/src/mcp/transports/sse.ts +377 -0
  85. package/src/memories/index.ts +15 -6
  86. package/src/modes/components/advisor-config.ts +555 -0
  87. package/src/modes/components/advisor-message.ts +9 -2
  88. package/src/modes/components/agent-hub.ts +9 -4
  89. package/src/modes/components/index.ts +2 -0
  90. package/src/modes/components/model-selector.ts +79 -48
  91. package/src/modes/components/settings-selector.ts +1 -0
  92. package/src/modes/components/status-line/component.ts +144 -5
  93. package/src/modes/components/status-line/segments.ts +46 -21
  94. package/src/modes/components/status-line/types.ts +13 -1
  95. package/src/modes/components/tool-execution.ts +47 -6
  96. package/src/modes/controllers/command-controller.ts +23 -2
  97. package/src/modes/controllers/event-controller.ts +106 -0
  98. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  99. package/src/modes/controllers/input-controller.ts +61 -61
  100. package/src/modes/controllers/selector-controller.ts +100 -9
  101. package/src/modes/interactive-mode.ts +65 -5
  102. package/src/modes/skill-command.ts +116 -0
  103. package/src/modes/types.ts +7 -2
  104. package/src/modes/utils/ui-helpers.ts +41 -23
  105. package/src/prompts/agents/reviewer.md +11 -10
  106. package/src/prompts/review-custom-request.md +1 -2
  107. package/src/prompts/review-request.md +1 -2
  108. package/src/prompts/system/interrupted-thinking.md +7 -0
  109. package/src/prompts/system/recap-user.md +9 -0
  110. package/src/prompts/system/subagent-system-prompt.md +8 -5
  111. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  112. package/src/prompts/system/system-prompt.md +0 -1
  113. package/src/prompts/tools/irc.md +2 -2
  114. package/src/prompts/tools/read.md +2 -2
  115. package/src/sdk.ts +28 -24
  116. package/src/session/agent-session.ts +867 -428
  117. package/src/session/indexed-session-storage.ts +86 -13
  118. package/src/session/messages.test.ts +125 -0
  119. package/src/session/messages.ts +172 -9
  120. package/src/session/redis-session-storage.ts +49 -2
  121. package/src/session/session-entries.ts +39 -2
  122. package/src/session/session-history-format.ts +29 -2
  123. package/src/session/session-listing.ts +54 -24
  124. package/src/session/session-loader.ts +66 -3
  125. package/src/session/session-manager.ts +113 -19
  126. package/src/session/session-persistence.ts +95 -1
  127. package/src/session/session-storage.ts +36 -0
  128. package/src/session/session-title-slot.ts +141 -0
  129. package/src/session/sql-session-storage.ts +71 -11
  130. package/src/slash-commands/builtin-registry.ts +16 -3
  131. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  132. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  133. package/src/ssh/connection-manager.ts +139 -12
  134. package/src/ssh/file-transfer.ts +23 -18
  135. package/src/ssh/ssh-executor.ts +2 -13
  136. package/src/ssh/utils.ts +19 -0
  137. package/src/task/executor.ts +21 -23
  138. package/src/task/render.ts +162 -20
  139. package/src/task/renderer.ts +14 -0
  140. package/src/task/types.ts +17 -0
  141. package/src/task/yield-assembly.ts +207 -0
  142. package/src/tiny/text.ts +23 -0
  143. package/src/tools/ask.ts +55 -4
  144. package/src/tools/render-utils.ts +2 -0
  145. package/src/tools/renderers.ts +8 -2
  146. package/src/tools/review.ts +17 -7
  147. package/src/tools/ssh.ts +8 -4
  148. package/src/tools/todo.ts +17 -1
  149. package/src/tools/yield.ts +140 -31
  150. package/src/utils/thinking-display.ts +15 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Task tool renderer export.
3
+ *
4
+ * Separated from render.ts to avoid circular dependency issues with
5
+ * tools/renderers.ts. This module has no side effects and can be safely
6
+ * imported without triggering the subprocessToolRegistry registration.
7
+ */
8
+ import { renderCall, renderResult } from "./render";
9
+ export declare const taskToolRenderer: {
10
+ readonly renderCall: typeof renderCall;
11
+ readonly renderResult: typeof renderResult;
12
+ readonly mergeCallAndResult: true;
13
+ };
@@ -206,6 +206,22 @@ export interface AgentDefinition {
206
206
  source: AgentSource;
207
207
  filePath?: string;
208
208
  }
209
+ /** Details extracted from a subagent `yield` tool call for final-result assembly and task rendering. */
210
+ export interface YieldItem {
211
+ data?: unknown;
212
+ status?: "success" | "aborted";
213
+ error?: string;
214
+ /** A string label is terminal; a non-empty array of labels is incremental. */
215
+ type?: string | string[];
216
+ /** Resolve this yield's payload from the latest durable assistant text instead of `data`. */
217
+ useLastTurn?: boolean;
218
+ /**
219
+ * Set by the in-tool yield validator when it exhausted its retry budget and
220
+ * accepted schema-invalid data anyway. The executor preserves that override
221
+ * during post-mortem validation.
222
+ */
223
+ schemaOverridden?: boolean;
224
+ }
209
225
  /** Progress tracking for a single agent */
210
226
  export interface AgentProgress {
211
227
  index: number;
@@ -0,0 +1,28 @@
1
+ import type { YieldItem } from "./types";
2
+ /** Outcome of folding a run's yield calls into one payload, with provenance flags. */
3
+ interface AssembledYieldResult {
4
+ data: unknown;
5
+ schemaOverridden: boolean;
6
+ rawText: boolean;
7
+ missingData: boolean;
8
+ }
9
+ /**
10
+ * Top-level output-schema property names declared as arrays (JTD `elements` →
11
+ * JSON `type: "array"`). An incremental yield section for such a label
12
+ * accumulates into a list even when the agent emits exactly one — otherwise a
13
+ * single `type: ["findings"]` yield would assemble as a bare object and fail
14
+ * array-typed schema validation.
15
+ */
16
+ export declare function arrayValuedLabels(outputSchema: unknown): ReadonlySet<string>;
17
+ /**
18
+ * Assemble typed yield calls into the final payload consumed by schema validation.
19
+ *
20
+ * A non-empty array `type` contributes an incremental section and never decides
21
+ * termination by itself. A string `type` with omitted `data` makes the last
22
+ * assistant turn the raw terminal result. Other string-typed yields contribute
23
+ * the terminal labelled section. Untyped terminal yields keep the historical
24
+ * "last yield wins" behavior unless no terminal yield exists, in which case
25
+ * accumulated typed sections finalize on idle.
26
+ */
27
+ export declare function assembleYieldResult(yieldItems: YieldItem[], lastAssistantText?: string, arrayLabels?: ReadonlySet<string>): AssembledYieldResult | undefined;
28
+ export {};
@@ -16,6 +16,14 @@ export declare function stripCodeBlocks(message: string): string;
16
16
  /** Prepare a raw user message for titling: drop code blocks, then bound length. */
17
17
  export declare function prepareTitleInput(message: string): string;
18
18
  export declare function formatTitleUserMessage(message: string): string;
19
+ /** Single recent conversation turn supplied to title refresh after replanning. */
20
+ export interface TitleConversationTurn {
21
+ role: "user" | "assistant";
22
+ text?: string;
23
+ thinking?: string;
24
+ }
25
+ /** Format recent user/assistant context for title generation after a todo replan. */
26
+ export declare function formatTitleConversationContext(turns: readonly TitleConversationTurn[]): string;
19
27
  /**
20
28
  * True when a first user message is too low-signal to title (greeting, ack,
21
29
  * bare number, or empty once code/punctuation/emoji are stripped).
@@ -45,6 +45,8 @@ export declare const TRUNCATE_LENGTHS: {
45
45
  readonly LINE: 110;
46
46
  /** Very short (task previews, badges) */
47
47
  readonly SHORT: 40;
48
+ /** Idle recap status line (~40-word LLM reply) */
49
+ readonly RECAP: 280;
48
50
  };
49
51
  /** Human-readable key currently bound to tool-output expansion, e.g. `Ctrl+O`. */
50
52
  export declare function expandKeyHint(): string;
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Review tools - report_finding for structured code review.
2
+ * Legacy hidden review-finding tool for agents that have not migrated to
3
+ * incremental `yield` sections.
3
4
  *
4
- * Used by the reviewer agent to report findings in a structured way.
5
- * Hidden by default - only enabled when explicitly listed in agent's tools.
6
- * Reviewers finish via `yield` tool with SubmitReviewDetails schema.
5
+ * Hidden by default - only enabled when explicitly listed in an agent's tools.
6
+ * Reviewers now finish via incremental `yield`; this tool remains for
7
+ * compatibility with older or custom review agents.
7
8
  */
8
9
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
9
10
  import type { Theme, ThemeColor } from "../modes/theme/theme";
@@ -15,6 +16,7 @@ export interface FindingPriorityInfo {
15
16
  color: ThemeColor;
16
17
  }
17
18
  export declare const PRIORITY_LABELS: FindingPriority[];
19
+ export declare function isFindingPriority(value: unknown): value is FindingPriority;
18
20
  export declare function getPriorityInfo(priority: FindingPriority): FindingPriorityInfo;
19
21
  declare const ReportFindingParams: import("arktype/internal/variants/object.ts").ObjectType<{
20
22
  title: string;
@@ -67,7 +67,7 @@ export declare const sshToolRenderer: {
67
67
  renderContext?: SshRenderContext;
68
68
  }, uiTheme: Theme, args?: SshRenderArgs): Component;
69
69
  mergeCallAndResult: boolean;
70
- provisionalPendingPreview: string;
70
+ provisionalPendingPreview: boolean;
71
71
  provisionalPartialResult: boolean;
72
72
  };
73
73
  export {};
@@ -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.2",
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.2",
59
- "@oh-my-pi/omp-stats": "16.2.2",
60
- "@oh-my-pi/pi-agent-core": "16.2.2",
61
- "@oh-my-pi/pi-ai": "16.2.2",
62
- "@oh-my-pi/pi-catalog": "16.2.2",
63
- "@oh-my-pi/pi-mnemopi": "16.2.2",
64
- "@oh-my-pi/pi-natives": "16.2.2",
65
- "@oh-my-pi/pi-tui": "16.2.2",
66
- "@oh-my-pi/pi-utils": "16.2.2",
67
- "@oh-my-pi/pi-wire": "16.2.2",
68
- "@oh-my-pi/snapcompact": "16.2.2",
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 &quot;X&quot;"');
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);
@@ -1259,14 +1370,18 @@ describe("advisor", () => {
1259
1370
  });
1260
1371
  });
1261
1372
 
1262
- describe("read-only tool allowlist", () => {
1263
- it("selects only the investigation tools from a mixed toolset", () => {
1264
- const toolset = ["read", "edit", "grep", "bash", "glob", "write", "advise"];
1265
- const selected = toolset.filter(name => ADVISOR_READONLY_TOOL_NAMES.has(name));
1266
- expect(selected).toEqual(["read", "grep", "glob"]);
1267
- expect(ADVISOR_READONLY_TOOL_NAMES.has("edit")).toBe(false);
1268
- expect(ADVISOR_READONLY_TOOL_NAMES.has("bash")).toBe(false);
1269
- 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
+ }
1270
1385
  });
1271
1386
  });
1272
1387
 
@@ -1289,6 +1404,26 @@ describe("advisor", () => {
1289
1404
  expect(text).toContain("watch the empty case");
1290
1405
  });
1291
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
+
1292
1427
  it("collapses to the first notes with an overflow hint", async () => {
1293
1428
  const uiTheme = await getThemeByName("dark");
1294
1429
  if (!uiTheme) throw new Error("theme unavailable");
@@ -1427,4 +1562,101 @@ describe("advisor", () => {
1427
1562
  }
1428
1563
  });
1429
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
+ });
1430
1662
  });