@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6

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 (143) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/cli.js +3616 -3676
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/exec/bash-executor.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  15. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
  17. package/dist/types/launch/spawn-options.d.ts +10 -0
  18. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  19. package/dist/types/modes/components/status-line/component.d.ts +10 -3
  20. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  21. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  22. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  23. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  24. package/dist/types/modes/components/usage-row.d.ts +1 -1
  25. package/dist/types/modes/interactive-mode.d.ts +2 -0
  26. package/dist/types/modes/print-mode.d.ts +4 -0
  27. package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
  28. package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
  29. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  30. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  31. package/dist/types/modes/setup-wizard/index.d.ts +1 -1
  32. package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
  33. package/dist/types/modes/types.d.ts +2 -0
  34. package/dist/types/sdk.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +42 -6
  36. package/dist/types/session/messages.d.ts +6 -0
  37. package/dist/types/session/session-paths.d.ts +21 -4
  38. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  39. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  40. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  41. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  42. package/dist/types/tools/gh.d.ts +5 -3
  43. package/dist/types/tools/hub/index.d.ts +1 -1
  44. package/dist/types/tools/hub/jobs.d.ts +1 -0
  45. package/dist/types/tools/hub/types.d.ts +2 -0
  46. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  47. package/dist/types/tools/xdev.d.ts +5 -3
  48. package/dist/types/utils/git.d.ts +33 -0
  49. package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
  50. package/dist/types/vibe/runtime.d.ts +23 -0
  51. package/dist/types/web/search/providers/codex.d.ts +1 -1
  52. package/package.json +12 -12
  53. package/src/advisor/__tests__/advisor.test.ts +150 -0
  54. package/src/advisor/advise-tool.ts +4 -0
  55. package/src/advisor/runtime.ts +38 -14
  56. package/src/async/job-manager.ts +3 -0
  57. package/src/cli/bench-cli.ts +8 -2
  58. package/src/cli/dry-balance-cli.ts +1 -0
  59. package/src/config/model-registry.ts +89 -8
  60. package/src/config/model-resolver.ts +78 -14
  61. package/src/config/models-config-schema.ts +1 -0
  62. package/src/config/settings.ts +3 -1
  63. package/src/dap/client.ts +168 -1
  64. package/src/dap/config.ts +51 -1
  65. package/src/dap/session.ts +575 -234
  66. package/src/dap/types.ts +6 -5
  67. package/src/discovery/agents.ts +2 -2
  68. package/src/exec/bash-executor.ts +68 -8
  69. package/src/extensibility/extensions/load-errors.ts +13 -0
  70. package/src/extensibility/extensions/runner.ts +6 -4
  71. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  72. package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
  73. package/src/launch/broker.ts +34 -31
  74. package/src/launch/client.ts +7 -1
  75. package/src/launch/spawn-options.test.ts +31 -0
  76. package/src/launch/spawn-options.ts +17 -0
  77. package/src/lsp/types.ts +5 -1
  78. package/src/main.ts +25 -4
  79. package/src/mcp/transports/stdio.test.ts +9 -1
  80. package/src/modes/components/ask-dialog.ts +137 -73
  81. package/src/modes/components/chat-transcript-builder.ts +10 -1
  82. package/src/modes/components/status-line/component.ts +57 -3
  83. package/src/modes/components/status-line/segments.ts +20 -3
  84. package/src/modes/components/status-line/types.ts +3 -1
  85. package/src/modes/components/tool-execution.ts +5 -0
  86. package/src/modes/components/transcript-container.ts +23 -122
  87. package/src/modes/components/tree-selector.ts +10 -4
  88. package/src/modes/components/usage-row.ts +17 -1
  89. package/src/modes/controllers/command-controller.ts +41 -1
  90. package/src/modes/controllers/event-controller.ts +7 -3
  91. package/src/modes/controllers/input-controller.ts +1 -1
  92. package/src/modes/controllers/selector-controller.ts +29 -8
  93. package/src/modes/interactive-mode.ts +102 -60
  94. package/src/modes/noninteractive-dispose.test.ts +14 -1
  95. package/src/modes/print-mode.ts +81 -9
  96. package/src/modes/rpc/rpc-client.ts +76 -35
  97. package/src/modes/rpc/rpc-frame.ts +156 -0
  98. package/src/modes/rpc/rpc-input.ts +38 -0
  99. package/src/modes/rpc/rpc-mode.ts +11 -4
  100. package/src/modes/setup-wizard/index.ts +2 -0
  101. package/src/modes/setup-wizard/scenes/model.ts +134 -0
  102. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  103. package/src/modes/types.ts +2 -0
  104. package/src/modes/utils/ui-helpers.ts +9 -3
  105. package/src/prompts/system/workflow-notice.md +89 -74
  106. package/src/prompts/tools/browser.md +3 -2
  107. package/src/prompts/tools/debug.md +2 -7
  108. package/src/prompts/tools/github.md +6 -1
  109. package/src/prompts/tools/hub.md +4 -2
  110. package/src/prompts/tools/task-async-contract.md +1 -0
  111. package/src/prompts/tools/task.md +9 -2
  112. package/src/sdk.ts +38 -6
  113. package/src/session/agent-session.ts +484 -188
  114. package/src/session/messages.test.ts +91 -0
  115. package/src/session/messages.ts +248 -110
  116. package/src/session/session-manager.ts +34 -3
  117. package/src/session/session-paths.ts +38 -9
  118. package/src/slash-commands/builtin-registry.ts +1 -0
  119. package/src/task/executor.ts +104 -33
  120. package/src/task/index.ts +46 -9
  121. package/src/task/worktree.ts +10 -0
  122. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  123. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  124. package/src/tools/browser/cmux/socket-client.ts +139 -3
  125. package/src/tools/browser/run-cancellation.ts +4 -0
  126. package/src/tools/browser/tab-protocol.ts +2 -0
  127. package/src/tools/browser/tab-supervisor.ts +21 -11
  128. package/src/tools/browser/tab-worker.ts +199 -33
  129. package/src/tools/debug.ts +3 -0
  130. package/src/tools/gh.ts +40 -2
  131. package/src/tools/hub/index.ts +4 -1
  132. package/src/tools/hub/jobs.ts +42 -1
  133. package/src/tools/hub/types.ts +2 -0
  134. package/src/tools/tool-timeouts.ts +1 -1
  135. package/src/tools/xdev.ts +11 -4
  136. package/src/tools/yield.ts +4 -1
  137. package/src/utils/git.ts +237 -0
  138. package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
  139. package/src/vibe/__tests__/token-rate.test.ts +96 -0
  140. package/src/vibe/runtime.ts +50 -0
  141. package/src/web/search/index.ts +9 -5
  142. package/src/web/search/providers/codex.ts +195 -99
  143. /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
@@ -13,7 +13,7 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
  import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
16
- import { Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type StreamFn, ThinkingLevel, type ToolChoiceDirective } from "@oh-my-pi/pi-agent-core";
16
+ import { Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type AgentToolResult, type StreamFn, ThinkingLevel, type ToolChoiceDirective } from "@oh-my-pi/pi-agent-core";
17
17
  import { type CompactionResult, type ShakeConfig } from "@oh-my-pi/pi-agent-core/compaction";
18
18
  import type { AssistantMessage, Context, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget, ServiceTier, ServiceTierByFamily, ServiceTierFamily, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
19
19
  import { Effort } from "@oh-my-pi/pi-ai";
@@ -41,6 +41,7 @@ import type { Goal, GoalModeState } from "../goals/state.js";
41
41
  import type { HindsightSessionState } from "../hindsight/state.js";
42
42
  import { type IrcMessage } from "../irc/bus.js";
43
43
  import { type MnemopiSessionState } from "../mnemopi/state.js";
44
+ import { type PlanApprovalDetails } from "../plan-mode/approved-plan.js";
44
45
  import type { PlanModeState } from "../plan-mode/state.js";
45
46
  import { type SecretObfuscator } from "../secrets/obfuscator.js";
46
47
  import { type ConfiguredThinkingLevel } from "../thinking.js";
@@ -542,6 +543,8 @@ export declare class AgentSession {
542
543
  * is already armed and waiting.
543
544
  */
544
545
  armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void;
546
+ /** Validate the active plan artifact and shape an `xd://propose` result for review-mode hosts. */
547
+ preparePlanForReview(title: string): Promise<AgentToolResult<PlanApprovalDetails>>;
545
548
  constructor(config: AgentSessionConfig);
546
549
  /** Model registry for API key resolution and model discovery */
547
550
  get modelRegistry(): ModelRegistry;
@@ -657,10 +660,26 @@ export declare class AgentSession {
657
660
  get isAborting(): boolean;
658
661
  /** Wait until streaming and deferred recovery work are fully settled. */
659
662
  waitForIdle(): Promise<void>;
663
+ /**
664
+ * Prevent advisor notes from starting hidden primary turns while a headless
665
+ * caller prints and drains the final primary response.
666
+ */
667
+ prepareForHeadlessAdvisorDrain(): void;
668
+ /**
669
+ * Wait for active advisor reviews and their emitted card events before a
670
+ * headless caller disposes the session. Returns `false` and logs work disposal
671
+ * will abandon when the shared deadline expires or an advisor fails.
672
+ */
673
+ waitForAdvisorCatchup(timeoutMs: number): Promise<boolean>;
660
674
  drainAsyncJobDeliveriesForAcp(options?: {
661
675
  timeoutMs?: number;
662
676
  }): Promise<boolean>;
663
- /** Most recent assistant message in agent state. */
677
+ /**
678
+ * Most recent settled assistant message. A classifier-refusal turn pruned
679
+ * from active context at settle is still reported until the next run
680
+ * starts, so terminal-outcome consumers (print mode, task executor) see
681
+ * the refusal error rather than the previous turn — or nothing.
682
+ */
664
683
  getLastAssistantMessage(): AssistantMessage | undefined;
665
684
  /** Current effective system prompt blocks (includes any per-turn extension modifications) */
666
685
  get systemPrompt(): string[];
@@ -714,6 +733,21 @@ export declare class AgentSession {
714
733
  * Changes take effect before the next model call.
715
734
  */
716
735
  setActiveToolsByName(toolNames: string[]): Promise<void>;
736
+ /**
737
+ * Restore an enabled tool set with its exact top-level versus `xd://` partition.
738
+ *
739
+ * Both inputs are required because {@link setActiveToolsByName} only receives the
740
+ * enabled name list and classifies mounts from the *current* `#mountedXdevToolNames`.
741
+ * Rollback/restore callers must pass the snapshotted mounted subset so names that
742
+ * were top-level stay pinned (`#runtimeSelectedToolNames`) and names that were under
743
+ * `xd://` remain mount-eligible, even when the live mount set has drifted.
744
+ *
745
+ * Names outside `mountedToolNames` are pinned top-level for this application;
746
+ * names in the mounted subset remain eligible for xdev mounting. Delegates the
747
+ * actual apply through `#applyActiveToolsByName` and restores the prior runtime
748
+ * selection if that apply throws.
749
+ */
750
+ setActiveToolPresentation(toolNames: string[], mountedToolNames: string[]): Promise<void>;
717
751
  /** Rebuild the base system prompt using the current active tool set. */
718
752
  refreshBaseSystemPrompt(): Promise<void>;
719
753
  /**
@@ -738,6 +772,8 @@ export declare class AgentSession {
738
772
  * to avoid racing against the delivery turn.
739
773
  */
740
774
  get hasPostPromptWork(): boolean;
775
+ /** Register post-prompt work in tests without driving a full agent turn. */
776
+ trackPostPromptTaskForTests(task: Promise<unknown>): void;
741
777
  /** All messages including custom types like BashExecutionMessage */
742
778
  get messages(): AgentMessage[];
743
779
  /** Latest image attachments addressable by tools as `Image #N` or `attachment://N`. */
@@ -1036,10 +1072,10 @@ export declare class AgentSession {
1036
1072
  */
1037
1073
  getAvailableModels(): Model[];
1038
1074
  /**
1039
- * Set the thinking level. `auto` enables per-turn classification; the selector
1040
- * itself is never written to the session log, but resolved concrete levels are
1041
- * persisted when real user turns are classified so resumed sessions keep the
1042
- * last resolved effort instead of reverting to pending auto.
1075
+ * Set the thinking level. `auto` enables per-turn classification. Entering
1076
+ * auto writes its provisional level plus `configured: "auto"` immediately,
1077
+ * giving external readers an authoritative selection receipt before the next
1078
+ * user turn. Later classifications persist only changed concrete resolutions.
1043
1079
  */
1044
1080
  setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist?: boolean): void;
1045
1081
  /**
@@ -250,5 +250,11 @@ export declare function sanitizeRehydratedOpenAIResponsesAssistantMessage(messag
250
250
  * - Agent's transormToLlm option (for prompt calls and queued messages)
251
251
  * - Compaction's generateSummary (for summarization)
252
252
  * - Custom extensions and tools
253
+ *
254
+ * Settled history converts once and is reused per message identity: an
255
+ * append-only turn on the same array re-pays only the new suffix, and an
256
+ * unchanged re-convert of the same array hands back the same outer `Message[]`.
257
+ * Owner mutations (prune/shake/strip-images) invalidate the affected message
258
+ * through the shared registry before the next pass.
253
259
  */
254
260
  export declare function convertToLlm(messages: AgentMessage[]): Message[];
@@ -10,16 +10,33 @@ export declare function computeDefaultSessionDir(cwd: string, storage: SessionSt
10
10
  * Write a breadcrumb linking the current terminal to a session file.
11
11
  * The breadcrumb contains the cwd and session path so --continue can
12
12
  * find "this terminal's last session" even when running concurrent instances.
13
+ *
14
+ * `fresh` marks a `/new` (or freshly-minted) session boundary whose JSONL is
15
+ * not yet materialized (new-session persistence is lazy until assistant output
16
+ * exists). A fresh breadcrumb is honored by {@link readTerminalBreadcrumbEntry}
17
+ * even when its target file is still absent, so relaunch/auto-resume reopens the
18
+ * post-`/new` session instead of falling back to the pre-`/new` transcript. Once
19
+ * the session materializes the caller rewrites the breadcrumb with `fresh:false`
20
+ * so a later external delete is still treated as a genuinely stale crumb.
13
21
  */
14
- export declare function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void;
22
+ export declare function writeTerminalBreadcrumb(cwd: string, sessionFile: string, fresh?: boolean): void;
15
23
  export interface TerminalBreadcrumb {
16
24
  cwd: string;
17
25
  sessionFile: string;
26
+ /** The recorded session file exists on disk right now. */
27
+ exists: boolean;
28
+ /** Recorded as a `/new` fresh-session boundary whose JSONL may not exist yet. */
29
+ fresh: boolean;
18
30
  }
19
31
  /**
20
32
  * Read the raw terminal breadcrumb for the current terminal.
21
- * Returns the recorded cwd + session file (verified to exist) regardless of
22
- * whether the recorded cwd still matches the current one. Callers decide how
23
- * to interpret a cwd mismatch (e.g. a moved/renamed worktree).
33
+ * Returns the recorded cwd + session file regardless of whether the recorded
34
+ * cwd still matches the current one. Callers decide how to interpret a cwd
35
+ * mismatch (e.g. a moved/renamed worktree).
36
+ *
37
+ * A missing target file yields `null` UNLESS the breadcrumb is a `fresh`
38
+ * boundary — a lazy `/new` session whose JSONL was never written — in which case
39
+ * the entry is returned with `exists:false` so the caller can distinguish it
40
+ * from a genuinely stale/deleted breadcrumb.
24
41
  */
25
42
  export declare function readTerminalBreadcrumbEntry(): Promise<TerminalBreadcrumb | null>;
@@ -19,13 +19,23 @@ export declare function captureAriaSnapshot(page: Page, root: ElementHandle | nu
19
19
  */
20
20
  export declare function resolveAriaRefHandle(page: Page, ref: string): Promise<ElementHandle | null>;
21
21
  /**
22
- * Recognize the explicit `[ref=eN]` selector forms and return the bare ref id,
23
- * else null. Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, and
24
- * `ariaref/e5` — lets `tab.click("aria-ref=e5")` etc. act on snapshot refs. A
25
- * bare `e5` is intentionally NOT a ref selector: the cmux backend already uses
26
- * bare `eN`/`@eN` for its own observe ids, so requiring the prefix keeps action
27
- * selectors meaning the same thing on both backends. (`tab.ref("e5")` still
28
- * accepts a bare id directly.)
22
+ * Guard the selector funnels: `tab.click`/`type`/`fill`/`waitFor*`/`scrollIntoView`
23
+ * take string selectors only, but user `run` code routinely passes the ElementHandle
24
+ * from `tab.id(n)`/`tab.ref(...)` (or an un-awaited Promise of one) straight in.
25
+ * Without this the value reaches `.trim()`/`.startsWith()` and throws the opaque,
26
+ * minified `A.trim is not a function` instead of a recovery-naming ToolError.
27
+ */
28
+ export declare function assertSelectorString(selector: unknown): asserts selector is string;
29
+ /**
30
+ * Recognize a snapshot-ref selector and return the bare ref id, else null.
31
+ * Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, `ariaref/e5`,
32
+ * and bare `e5`/`@e5`: agents copy ids straight out of the snapshot YAML
33
+ * (`[ref=e5]`), so `tab.click("e5")` must act on the ref instead of falling
34
+ * through to a CSS tag selector that can never match. Bare ids are safe to
35
+ * claim here — an eN tag name is not real HTML, and the tab-worker backend's
36
+ * observe ids are numeric (`tab.id(7)`), so refs are its only eN namespace.
37
+ * (The cmux backend parses selectors itself and routes bare `eN` to its own
38
+ * observe ids; either way `eN` means "the id from the last page dump".)
29
39
  */
30
40
  export declare function parseAriaRefSelector(selector: string): string | null;
31
41
  /**
@@ -9,6 +9,8 @@ export declare class CmuxSocketClient {
9
9
  constructor(opts: {
10
10
  socketPath: string;
11
11
  password?: string;
12
+ relayId?: string;
13
+ relayToken?: string;
12
14
  });
13
15
  connect(): Promise<void>;
14
16
  request(method: string, params: Record<string, unknown>, opts?: {
@@ -115,6 +115,8 @@ export interface RunErrorPayload {
115
115
  stack?: string;
116
116
  isToolError: boolean;
117
117
  isAbort: boolean;
118
+ /** The worker could not restore tab-scoped browser state and must be recycled. */
119
+ recoverTab?: boolean;
118
120
  }
119
121
  export type WorkerOutbound = {
120
122
  type: "ready";
@@ -27,6 +27,8 @@ export interface OpTimeouts {
27
27
  }
28
28
  /** Resolve the per-op fail-fast ceilings for a given cell budget. */
29
29
  export declare function resolveOpTimeouts(cellTimeoutMs: number): OpTimeouts;
30
+ /** Queue a wheel event without treating a delayed renderer acknowledgement as dispatch failure. */
31
+ export declare function dispatchScroll(dispatch: () => Promise<void>, ackTimeoutMs?: number): Promise<void>;
30
32
  /**
31
33
  * Effective timeout for a wait helper (`waitFor*`). A positive explicit `{ timeout }` is
32
34
  * honored but clamped to the cell budget so it still fails fast + named; raising the tool
@@ -7,9 +7,10 @@ import type { OutputMeta } from "./output-meta.js";
7
7
  /** Runs `gh --json` for issue data, retrying without optional stateReason on older gh releases. */
8
8
  export declare function githubIssueJsonWithStateReasonFallback<T>(cwd: string, args: readonly string[], signal: AbortSignal | undefined, options?: git.GhCommandOptions): Promise<T>;
9
9
  declare const githubSchema: import("arktype/internal/variants/object.ts").ObjectType<{
10
- op: "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
10
+ op: "file_read" | "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
11
11
  repo?: string | undefined;
12
12
  branch?: string | undefined;
13
+ path?: string | undefined;
13
14
  pr?: string | string[] | undefined;
14
15
  force?: boolean | undefined;
15
16
  forceWithLease?: boolean | undefined;
@@ -219,14 +220,15 @@ export declare class GithubTool implements AgentTool<typeof githubSchema, GhTool
219
220
  private readonly session;
220
221
  readonly name = "github";
221
222
  readonly approval: (args: unknown) => ToolApprovalDecision;
222
- readonly summary = "Interact with GitHub issues, pull requests, and repositories";
223
+ readonly summary = "Interact with GitHub repositories, files, pull requests, and Actions";
223
224
  readonly loadMode = "discoverable";
224
225
  readonly label = "GitHub";
225
226
  readonly description: string;
226
227
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
227
- op: "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
228
+ op: "file_read" | "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
228
229
  repo?: string | undefined;
229
230
  branch?: string | undefined;
231
+ path?: string | undefined;
230
232
  pr?: string | string[] | undefined;
231
233
  force?: boolean | undefined;
232
234
  forceWithLease?: boolean | undefined;
@@ -117,7 +117,7 @@ export declare class HubTool implements AgentTool<typeof hubSchema, HubDetails>
117
117
  timeout?: number | undefined;
118
118
  }, {}>;
119
119
  readonly strict = true;
120
- readonly interruptible = true;
120
+ readonly interruptible: (params: Partial<HubParams>) => boolean;
121
121
  readonly loadMode = "essential";
122
122
  readonly examples: readonly ToolExample<typeof hubSchema.infer>[];
123
123
  constructor(session: ToolSession);
@@ -44,6 +44,7 @@ interface TrackedJobLike {
44
44
  status: string;
45
45
  label: string;
46
46
  startTime: number;
47
+ latestDetails?: Record<string, unknown>;
47
48
  resultText?: string;
48
49
  errorText?: string;
49
50
  }
@@ -30,6 +30,8 @@ export interface JobSnapshot {
30
30
  status: "running" | "completed" | "failed" | "cancelled";
31
31
  label: string;
32
32
  durationMs: number;
33
+ /** Effective task model selector, including an explicit reasoning suffix when configured. */
34
+ resolvedModel?: string;
33
35
  resultText?: string;
34
36
  errorText?: string;
35
37
  }
@@ -35,7 +35,7 @@ export declare const TOOL_TIMEOUTS: {
35
35
  readonly lsp: {
36
36
  readonly default: 20;
37
37
  readonly min: 5;
38
- readonly max: 60;
38
+ readonly max: 300;
39
39
  };
40
40
  readonly debug: {
41
41
  readonly default: 30;
@@ -32,9 +32,11 @@ import type { ToolRenderer } from "./renderers.js";
32
32
  /**
33
33
  * Discoverable built-ins that must stay top-level even when xdev mounting is
34
34
  * active: `todo` feeds the todo prelude/prewalk machinery, `ask` is the
35
- * model's user-interaction affordance, and `grep` is the redirect target of
36
- * the bash interceptor rules each loses its harness integration if hidden
37
- * behind dispatch.
35
+ * model's user-interaction affordance, `grep` is the redirect target of the
36
+ * bash interceptor rules, and `web_search` is invoked directly by most models
37
+ * (which have no notion of the `xd://` protocol) so hiding it behind dispatch
38
+ * makes it unreachable in practice (issue #5973) — each loses its harness
39
+ * integration or usability if hidden behind dispatch.
38
40
  */
39
41
  export declare const XDEV_KEEP_TOP_LEVEL: Record<string, true>;
40
42
  /**
@@ -219,6 +219,39 @@ export declare function fetch(cwd: string, remote: string, source: string, targe
219
219
  export declare function readTree(cwd: string, treeish: string, options?: Pick<CommandOptions, "env" | "signal">): Promise<void>;
220
220
  /** Write the current index as a tree and return its object id. */
221
221
  export declare function writeTree(cwd: string, options?: Pick<CommandOptions, "env" | "signal">): Promise<string>;
222
+ /** Outcome of {@link detachGitDir}. */
223
+ export type DetachGitDirResult =
224
+ /** `worktreeRoot` had no `.git`; nothing to detach. */
225
+ "no-git"
226
+ /** `.git` already resolves to an independent object DB — left untouched. */
227
+ | "independent"
228
+ /** Detached into a standalone repo borrowing `sourceCommonDir`'s objects. */
229
+ | "detached";
230
+ /**
231
+ * Sever a copied/mounted working tree from the git metadata it shares with a
232
+ * source checkout, turning it into a standalone repository that borrows the
233
+ * source object database through `objects/info/alternates`.
234
+ *
235
+ * Isolation backends (reflink/apfs/btrfs/rcopy…) materialise `merged` by
236
+ * copying `worktreeRoot` byte-for-byte. When `worktreeRoot` is a **linked git
237
+ * worktree** its `.git` is a pointer file (`gitdir: …/worktrees/<name>`), so
238
+ * the copy still resolves HEAD/index/refs through the source repo — a task's
239
+ * `git checkout`/`commit` inside the isolation then mutates the *parent*
240
+ * checkout. The rcopy `git worktree add` path leaks the other way: task
241
+ * branches land in the shared ref namespace and stack on each other.
242
+ *
243
+ * After detaching, the working tree keeps its files verbatim while:
244
+ * - HEAD, refs, and the index are frozen to the snapshot at call time;
245
+ * - all commits/branches the task creates stay private to the isolation;
246
+ * - objects resolve against `sourceCommonDir` via alternates, so history reads
247
+ * and later `git fetch <merged>` object transfer keep working;
248
+ * - the source checkout's HEAD, branch, index, and working tree are untouched.
249
+ *
250
+ * A full-copy `.git` (non-worktree source) already owns its object DB and is
251
+ * returned as `"independent"` without modification. `worktreeRoot` without a
252
+ * `.git` yields `"no-git"`.
253
+ */
254
+ export declare function detachGitDir(worktreeRoot: string, sourceCommonDir: string): Promise<DetachGitDirResult>;
222
255
  /** Run `git show` on a revision. */
223
256
  export declare const show: ((cwd: string, revision: string, options?: {
224
257
  format?: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -83,6 +83,17 @@ export declare class VibeSessionRegistry {
83
83
  static global(): VibeSessionRegistry;
84
84
  /** Reset the global registry. Test-only. */
85
85
  static resetGlobalForTests(): void;
86
+ /**
87
+ * Insert a bare worker record without the spawn/job machinery. Test-only —
88
+ * lets {@link aggregateVibeWorkerTokensPerSecond} be exercised against a
89
+ * fake roster + AgentRegistry session without driving a real turn.
90
+ */
91
+ registerRecordForTests(record: {
92
+ id: string;
93
+ cli?: VibeCli;
94
+ ownerId: string;
95
+ state?: VibeSessionState;
96
+ }): void;
86
97
  listIds(owner: string): string[];
87
98
  /**
88
99
  * Live screen snapshots for rich rendering (the "TV wall"): one entry per
@@ -122,3 +133,15 @@ export declare class VibeSessionRegistry {
122
133
  /** Kill every session belonging to `owner` (vibe-mode exit / teardown). Returns the number killed. */
123
134
  killAll(owner: string, manager?: AsyncJobManager): Promise<number>;
124
135
  }
136
+ /**
137
+ * Aggregate tok/s across every live vibe worker session owned by `ownerId`.
138
+ * Returns null when no workers are streaming (so callers can fall back to
139
+ * their own rate unchanged). The director is often idle while workers stream,
140
+ * so without this aggregation the status-line tok/s badge would show a stale
141
+ * value while parallel work is actively generating tokens.
142
+ *
143
+ * Reads each worker's last assistant message via {@link calculateTokensPerSecond}
144
+ * — the same leaf calculator the main status line uses — so worker rates are
145
+ * computed identically to the main session's rate.
146
+ */
147
+ export declare function aggregateVibeWorkerTokensPerSecond(ownerId: string): number | null;
@@ -25,7 +25,7 @@ export interface CodexSearchParams {
25
25
  */
26
26
  export declare function searchCodex(params: SearchParams): Promise<SearchResponse>;
27
27
  /**
28
- * Checks if Codex web search is available.
28
+ * Checks whether Codex web search has an API key or OAuth credential.
29
29
  */
30
30
  export declare function hasCodexSearch(authStorage: AuthStorage): Promise<boolean>;
31
31
  /** Search provider for OpenAI Codex web search. */
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": "17.0.4",
4
+ "version": "17.0.6",
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",
@@ -52,17 +52,17 @@
52
52
  "@agentclientprotocol/sdk": "1.2.1",
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "17.0.4",
56
- "@oh-my-pi/omp-stats": "17.0.4",
57
- "@oh-my-pi/pi-agent-core": "17.0.4",
58
- "@oh-my-pi/pi-ai": "17.0.4",
59
- "@oh-my-pi/pi-catalog": "17.0.4",
60
- "@oh-my-pi/pi-mnemopi": "17.0.4",
61
- "@oh-my-pi/pi-natives": "17.0.4",
62
- "@oh-my-pi/pi-tui": "17.0.4",
63
- "@oh-my-pi/pi-utils": "17.0.4",
64
- "@oh-my-pi/pi-wire": "17.0.4",
65
- "@oh-my-pi/snapcompact": "17.0.4",
55
+ "@oh-my-pi/hashline": "17.0.6",
56
+ "@oh-my-pi/omp-stats": "17.0.6",
57
+ "@oh-my-pi/pi-agent-core": "17.0.6",
58
+ "@oh-my-pi/pi-ai": "17.0.6",
59
+ "@oh-my-pi/pi-catalog": "17.0.6",
60
+ "@oh-my-pi/pi-mnemopi": "17.0.6",
61
+ "@oh-my-pi/pi-natives": "17.0.6",
62
+ "@oh-my-pi/pi-tui": "17.0.6",
63
+ "@oh-my-pi/pi-utils": "17.0.6",
64
+ "@oh-my-pi/pi-wire": "17.0.6",
65
+ "@oh-my-pi/snapcompact": "17.0.6",
66
66
  "@opentelemetry/api": "^1.9.1",
67
67
  "@opentelemetry/api-logs": "^0.220.0",
68
68
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "bun:test";
2
2
  import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
3
3
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
4
  import * as AIError from "@oh-my-pi/pi-ai/error";
5
+ import { kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
5
6
  import type { TUI } from "@oh-my-pi/pi-tui";
6
7
  import { type } from "arktype";
7
8
  import type { ModelRegistry } from "../../config/model-registry";
@@ -519,6 +520,58 @@ describe("advisor", () => {
519
520
  expect(message.content).toBe(originalContent);
520
521
  });
521
522
 
523
+ it("keeps advise when Cursor emits exec-resolved native tools outside the grant (issue #5900)", () => {
524
+ const message = {
525
+ role: "assistant",
526
+ content: [
527
+ { type: "text", text: "Investigating the networking design." },
528
+ {
529
+ type: "toolCall",
530
+ id: "tc-grep",
531
+ name: "grep",
532
+ arguments: { pattern: "backoff" },
533
+ [kCursorExecResolved]: true,
534
+ },
535
+ {
536
+ type: "toolCall",
537
+ id: "tc-bash",
538
+ name: "bash",
539
+ arguments: { command: "ls" },
540
+ [kCursorExecResolved]: true,
541
+ },
542
+ {
543
+ type: "toolCall",
544
+ id: "tc-advise",
545
+ name: "advise",
546
+ arguments: { note: "The retry backoff looks unbounded." },
547
+ },
548
+ ],
549
+ stopReason: "toolUse",
550
+ } as unknown as AssistantMessage;
551
+ const originalContent = message.content;
552
+
553
+ // Grant is `advise` only (WATCHDOG.yml `tools: []`). The native grep/bash
554
+ // frames already ran server-side through the advisor-scoped bridge, which
555
+ // rejected them in-band; they must not discard the legitimate advise.
556
+ expect(quarantineAdvisorUnsafeOutput(message, new Set(["advise"]))).toBeUndefined();
557
+ expect(message.stopReason).toBe("toolUse");
558
+ expect(message.content).toBe(originalContent);
559
+ expect(JSON.stringify(message)).toContain("unbounded");
560
+ });
561
+
562
+ it("still quarantines an ungranted native tool that was not exec-resolved", () => {
563
+ const message = {
564
+ role: "assistant",
565
+ content: [{ type: "toolCall", id: "tc-bash", name: "bash", arguments: { command: "ls" } }],
566
+ stopReason: "toolUse",
567
+ } as unknown as AssistantMessage;
568
+
569
+ expect(quarantineAdvisorUnsafeOutput(message, new Set(["advise"]))).toBe(
570
+ "Advisor response quarantined: requested unavailable tool bash",
571
+ );
572
+ expect(message.stopReason).toBe("error");
573
+ });
574
+
522
575
  it("sanitizes destructive advise notes even when advise is an allowed tool", () => {
523
576
  const message = {
524
577
  role: "assistant",
@@ -865,6 +918,65 @@ describe("advisor", () => {
865
918
  expect(promptInputs[1]).toContain("second");
866
919
  });
867
920
 
921
+ it("waits for an in-flight review within the catch-up deadline", async () => {
922
+ const promptStarted = Promise.withResolvers<void>();
923
+ const releasePrompt = Promise.withResolvers<void>();
924
+ const messages: AgentMessage[] = [{ role: "user", content: "first", timestamp: 1 } as AgentMessage];
925
+ const agent: AdvisorAgent = {
926
+ prompt: async () => {
927
+ promptStarted.resolve();
928
+ await releasePrompt.promise;
929
+ },
930
+ abort: () => {},
931
+ reset: () => {},
932
+ state: { messages: [] },
933
+ };
934
+ const runtime = new AdvisorRuntime(agent, {
935
+ snapshotMessages: () => messages,
936
+ enqueueAdvice: () => {},
937
+ });
938
+
939
+ runtime.onTurnEnd();
940
+ await promptStarted.promise;
941
+ let settled = false;
942
+ const catchup = runtime.waitForCatchup(1000, 1).then(caughtUp => {
943
+ settled = true;
944
+ return caughtUp;
945
+ });
946
+ await Promise.resolve();
947
+ expect(settled).toBe(false);
948
+
949
+ releasePrompt.resolve();
950
+ expect(await catchup).toBe(true);
951
+ });
952
+
953
+ it("reports an in-flight review that exceeds the catch-up deadline", async () => {
954
+ const promptStarted = Promise.withResolvers<void>();
955
+ const releasePrompt = Promise.withResolvers<void>();
956
+ const messages: AgentMessage[] = [{ role: "user", content: "first", timestamp: 1 } as AgentMessage];
957
+ const agent: AdvisorAgent = {
958
+ prompt: async () => {
959
+ promptStarted.resolve();
960
+ await releasePrompt.promise;
961
+ },
962
+ abort: () => {},
963
+ reset: () => {},
964
+ state: { messages: [] },
965
+ };
966
+ const runtime = new AdvisorRuntime(agent, {
967
+ snapshotMessages: () => messages,
968
+ enqueueAdvice: () => {},
969
+ });
970
+
971
+ runtime.onTurnEnd();
972
+ await promptStarted.promise;
973
+ expect(await runtime.waitForCatchup(20, 1)).toBe(false);
974
+ expect(runtime.backlog).toBe(1);
975
+
976
+ releasePrompt.resolve();
977
+ await settleUntil(() => runtime.backlog === 0);
978
+ });
979
+
868
980
  it("preserves the next user turn when an accepted empty stop is pruned", async () => {
869
981
  const promptInputs: string[] = [];
870
982
  const agent = makeAgent(promptInputs);
@@ -3779,6 +3891,44 @@ describe("advisor", () => {
3779
3891
  // or it strands and #drainStrandedQueuedMessages auto-resumes it. Do not swap
3780
3892
  // the call site back to session `isStreaming`.
3781
3893
  describe("resolveAdvisorDeliveryChannel", () => {
3894
+ it("preserves every severity when a headless drain forbids primary turns", () => {
3895
+ for (const severity of [undefined, "nit", "concern", "blocker"] as const) {
3896
+ expect(
3897
+ resolveAdvisorDeliveryChannel({
3898
+ severity,
3899
+ autoResumeSuppressed: false,
3900
+ streaming: false,
3901
+ aborting: false,
3902
+ terminalAnswerNoQueuedWork: true,
3903
+ preserveOnly: true,
3904
+ }),
3905
+ ).toBe("preserve");
3906
+ }
3907
+ });
3908
+
3909
+ it("keeps live headless advice on normal delivery channels until the primary finishes", () => {
3910
+ expect(
3911
+ resolveAdvisorDeliveryChannel({
3912
+ severity: "nit",
3913
+ autoResumeSuppressed: false,
3914
+ streaming: true,
3915
+ aborting: false,
3916
+ preserveOnly: true,
3917
+ }),
3918
+ ).toBe("aside");
3919
+ for (const severity of ["concern", "blocker"] as const) {
3920
+ expect(
3921
+ resolveAdvisorDeliveryChannel({
3922
+ severity,
3923
+ autoResumeSuppressed: false,
3924
+ streaming: true,
3925
+ aborting: false,
3926
+ preserveOnly: true,
3927
+ }),
3928
+ ).toBe("steer");
3929
+ }
3930
+ });
3931
+
3782
3932
  it("routes a non-interrupting nit to the aside queue regardless of state", () => {
3783
3933
  expect(
3784
3934
  resolveAdvisorDeliveryChannel({
@@ -101,6 +101,8 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
101
101
  /**
102
102
  * Decide how one advisor note reaches the primary agent.
103
103
  *
104
+ * - A `preserveOnly` caller records every note that arrives while the primary
105
+ * is idle as a visible card and never starts a new primary turn.
104
106
  * - A non-interrupting `nit` always rides the non-interrupting aside queue.
105
107
  * - An interrupting `concern`/`blocker` is normally steered into the agent: into
106
108
  * the live turn while one is streaming, or (when idle) a triggered turn so the
@@ -129,7 +131,9 @@ export function resolveAdvisorDeliveryChannel(opts: {
129
131
  aborting: boolean;
130
132
  terminalAnswerNoQueuedWork?: boolean;
131
133
  interruptImmuneTurnActive?: boolean;
134
+ preserveOnly?: boolean;
132
135
  }): AdvisorDeliveryChannel {
136
+ if (opts.preserveOnly && !opts.streaming) return "preserve";
133
137
  if (!isInterruptingSeverity(opts.severity)) return "aside";
134
138
  if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
135
139
  if (opts.terminalAnswerNoQueuedWork && opts.severity !== "blocker" && !opts.streaming && !opts.aborting)