@oh-my-pi/pi-coding-agent 17.2.5 → 17.2.7

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 (201) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/{CHANGELOG-q2w43aw3.md → CHANGELOG-1p8yvde7.md} +35 -1
  3. package/dist/cli.js +3340 -3339
  4. package/dist/types/advisor/advise-tool.d.ts +10 -4
  5. package/dist/types/autoresearch/tools/init-experiment.d.ts +19 -7
  6. package/dist/types/autoresearch/tools/log-experiment.d.ts +19 -11
  7. package/dist/types/autoresearch/tools/run-experiment.d.ts +4 -2
  8. package/dist/types/autoresearch/tools/update-notes.d.ts +5 -2
  9. package/dist/types/cli/gc-cli.d.ts +1 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +5 -2
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +5 -2
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +6 -2
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +5 -2
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +29 -7
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +14 -4
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +4 -2
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +7 -3
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +37 -10
  19. package/dist/types/commit/changelog/generate.d.ts +13 -3
  20. package/dist/types/commit/shared-llm.d.ts +13 -4
  21. package/dist/types/config/config-file.d.ts +1 -1
  22. package/dist/types/config/models-config-schema-bundle.d.ts +1992 -981
  23. package/dist/types/config/models-config-schema.d.ts +1201 -580
  24. package/dist/types/config/models-config.d.ts +355 -371
  25. package/dist/types/edit/hashline/params.d.ts +4 -2
  26. package/dist/types/edit/modes/apply-patch.d.ts +4 -2
  27. package/dist/types/edit/modes/patch.d.ts +16 -5
  28. package/dist/types/edit/modes/replace.d.ts +8 -3
  29. package/dist/types/eval/bridge-timeout.d.ts +5 -2
  30. package/dist/types/eval/completion-bridge.d.ts +13 -0
  31. package/dist/types/eval/py/tool-bridge.d.ts +12 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +4 -4
  33. package/dist/types/extensibility/custom-tools/types.d.ts +4 -4
  34. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  35. package/dist/types/extensibility/hooks/types.d.ts +4 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  38. package/dist/types/extensibility/legacy-typebox.d.ts +8 -0
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +6 -13
  40. package/dist/types/goals/tools/goal-tool.d.ts +12 -4
  41. package/dist/types/launch/client.d.ts +9 -1
  42. package/dist/types/launch/protocol.d.ts +17 -0
  43. package/dist/types/lsp/index.d.ts +15 -5
  44. package/dist/types/lsp/types.d.ts +15 -5
  45. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +4 -1
  49. package/dist/types/modes/types.d.ts +3 -1
  50. package/dist/types/security/contracts/schemas.d.ts +1115 -537
  51. package/dist/types/security/publication.d.ts +92 -34
  52. package/dist/types/session/agent-session-types.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +26 -2
  54. package/dist/types/session/launch-completion.d.ts +10 -0
  55. package/dist/types/session/session-entries.d.ts +15 -1
  56. package/dist/types/session/session-manager.d.ts +7 -0
  57. package/dist/types/session/yield-queue.d.ts +6 -1
  58. package/dist/types/task/types.d.ts +63 -18
  59. package/dist/types/tools/ask.d.ts +34 -8
  60. package/dist/types/tools/ast-edit.d.ts +16 -4
  61. package/dist/types/tools/ast-grep.d.ts +12 -4
  62. package/dist/types/tools/bash.d.ts +20 -11
  63. package/dist/types/tools/browser.d.ts +64 -20
  64. package/dist/types/tools/checkpoint.d.ts +16 -8
  65. package/dist/types/tools/computer.d.ts +1 -1
  66. package/dist/types/tools/debug.d.ts +118 -48
  67. package/dist/types/tools/eval.d.ts +11 -5
  68. package/dist/types/tools/gh.d.ts +74 -26
  69. package/dist/types/tools/glob.d.ts +16 -6
  70. package/dist/types/tools/grep.d.ts +18 -6
  71. package/dist/types/tools/hub/index.d.ts +116 -46
  72. package/dist/types/tools/image-gen.d.ts +25 -8
  73. package/dist/types/tools/index.d.ts +9 -0
  74. package/dist/types/tools/inspect-image.d.ts +10 -4
  75. package/dist/types/tools/learn.d.ts +26 -8
  76. package/dist/types/tools/manage-skill.d.ts +16 -6
  77. package/dist/types/tools/memory-edit.d.ts +18 -6
  78. package/dist/types/tools/memory-recall.d.ts +8 -4
  79. package/dist/types/tools/memory-reflect.d.ts +10 -4
  80. package/dist/types/tools/memory-retain.d.ts +14 -4
  81. package/dist/types/tools/read.d.ts +8 -4
  82. package/dist/types/tools/security-scan.d.ts +82 -30
  83. package/dist/types/tools/todo.d.ts +30 -10
  84. package/dist/types/tools/tts.d.ts +12 -5
  85. package/dist/types/tools/vibe.d.ts +42 -18
  86. package/dist/types/tools/write.d.ts +10 -4
  87. package/dist/types/web/search/index.d.ts +20 -6
  88. package/package.json +13 -13
  89. package/scripts/legacy-pi-virtual-module.ts +2 -2
  90. package/src/advisor/advise-tool.ts +1 -1
  91. package/src/advisor/config.ts +1 -1
  92. package/src/autoresearch/tools/init-experiment.ts +1 -2
  93. package/src/autoresearch/tools/log-experiment.ts +1 -2
  94. package/src/autoresearch/tools/run-experiment.ts +1 -1
  95. package/src/autoresearch/tools/update-notes.ts +1 -1
  96. package/src/cli/gc-cli.ts +641 -21
  97. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  98. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  99. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  100. package/src/commit/agentic/tools/git-overview.ts +1 -1
  101. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  102. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  103. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  104. package/src/commit/agentic/tools/schemas.ts +1 -1
  105. package/src/commit/agentic/tools/split-commit.ts +1 -1
  106. package/src/commit/analysis/summary.ts +1 -1
  107. package/src/commit/changelog/generate.ts +1 -1
  108. package/src/config/config-file.ts +2 -2
  109. package/src/config/model-discovery.ts +14 -14
  110. package/src/config/model-registry.ts +10 -8
  111. package/src/config/models-config-schema-bundle.ts +1 -8
  112. package/src/config/settings.ts +1 -1
  113. package/src/discovery/codex.ts +22 -9
  114. package/src/edit/hashline/params.ts +1 -1
  115. package/src/edit/modes/apply-patch.ts +1 -1
  116. package/src/edit/modes/patch.ts +1 -1
  117. package/src/edit/modes/replace.ts +2 -1
  118. package/src/eval/agent-bridge.ts +4 -2
  119. package/src/eval/bridge-timeout.ts +5 -2
  120. package/src/eval/completion-bridge.ts +3 -2
  121. package/src/eval/executor-base.ts +20 -1
  122. package/src/eval/js/context-manager.ts +94 -6
  123. package/src/eval/py/tool-bridge.ts +29 -9
  124. package/src/export/html/index.ts +10 -3
  125. package/src/export/share.ts +4 -0
  126. package/src/extensibility/custom-commands/loader.ts +3 -3
  127. package/src/extensibility/custom-commands/types.ts +4 -4
  128. package/src/extensibility/custom-tools/loader.ts +3 -3
  129. package/src/extensibility/custom-tools/types.ts +5 -4
  130. package/src/extensibility/extensions/loader.ts +3 -3
  131. package/src/extensibility/extensions/types.ts +5 -4
  132. package/src/extensibility/hooks/loader.ts +3 -3
  133. package/src/extensibility/hooks/types.ts +4 -4
  134. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  135. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  136. package/src/extensibility/legacy-typebox.ts +101 -0
  137. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -22
  138. package/src/goals/tools/goal-tool.ts +1 -1
  139. package/src/launch/broker.ts +222 -6
  140. package/src/launch/client.ts +161 -9
  141. package/src/launch/protocol.ts +52 -0
  142. package/src/lsp/types.ts +1 -1
  143. package/src/main.ts +13 -6
  144. package/src/mcp/config-writer.ts +1 -1
  145. package/src/modes/components/btw-panel.ts +41 -4
  146. package/src/modes/controllers/btw-controller.ts +55 -7
  147. package/src/modes/controllers/command-controller.ts +31 -0
  148. package/src/modes/controllers/input-controller.ts +24 -7
  149. package/src/modes/interactive-mode.ts +16 -2
  150. package/src/modes/theme/theme.ts +3 -3
  151. package/src/modes/types.ts +8 -1
  152. package/src/prompts/session/launch-completion.md +1 -0
  153. package/src/sdk.ts +15 -0
  154. package/src/security/contracts/schemas.ts +202 -184
  155. package/src/security/contracts/validation.ts +6 -2
  156. package/src/security/publication.ts +1 -1
  157. package/src/security/store.ts +2 -2
  158. package/src/session/agent-session-types.ts +6 -0
  159. package/src/session/agent-session.ts +190 -14
  160. package/src/session/launch-completion.ts +37 -0
  161. package/src/session/session-context.ts +26 -0
  162. package/src/session/session-entries.ts +17 -1
  163. package/src/session/session-manager.ts +21 -0
  164. package/src/session/yield-queue.ts +121 -16
  165. package/src/slash-commands/builtin-registry.ts +10 -1
  166. package/src/task/types.ts +1 -1
  167. package/src/tools/ask.ts +1 -1
  168. package/src/tools/ast-edit.ts +1 -1
  169. package/src/tools/ast-grep.ts +1 -1
  170. package/src/tools/bash.ts +61 -3
  171. package/src/tools/browser/cmux/cmux-tab.ts +92 -4
  172. package/src/tools/browser.ts +1 -1
  173. package/src/tools/checkpoint.ts +1 -1
  174. package/src/tools/computer.ts +1 -1
  175. package/src/tools/debug.ts +1 -1
  176. package/src/tools/eval.ts +1 -1
  177. package/src/tools/gh.ts +1 -2
  178. package/src/tools/glob.ts +1 -1
  179. package/src/tools/grep.ts +1 -1
  180. package/src/tools/hub/index.ts +1 -1
  181. package/src/tools/hub/launch.ts +122 -6
  182. package/src/tools/image-gen.ts +1 -1
  183. package/src/tools/index.ts +9 -0
  184. package/src/tools/inspect-image.ts +1 -1
  185. package/src/tools/learn.ts +1 -1
  186. package/src/tools/manage-skill.ts +1 -1
  187. package/src/tools/memory-edit.ts +1 -1
  188. package/src/tools/memory-recall.ts +1 -1
  189. package/src/tools/memory-reflect.ts +1 -1
  190. package/src/tools/memory-retain.ts +1 -1
  191. package/src/tools/read.ts +1 -1
  192. package/src/tools/security-scan.ts +1 -1
  193. package/src/tools/todo.ts +1 -1
  194. package/src/tools/tts.ts +1 -1
  195. package/src/tools/vibe.ts +2 -1
  196. package/src/tools/write.ts +1 -1
  197. package/src/web/search/index.ts +2 -1
  198. package/dist/types/config/file-lock.d.ts +0 -29
  199. package/dist/types/extensibility/typebox.d.ts +0 -181
  200. package/src/config/file-lock.ts +0 -164
  201. package/src/extensibility/typebox.ts +0 -958
@@ -1,5 +1,5 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import { prompt } from "@oh-my-pi/pi-utils";
2
- import { type } from "arktype";
3
3
  import analyzeFilePrompt from "../../../commit/agentic/prompts/analyze-file.md" with { type: "text" };
4
4
  import type { CommitAgentState } from "../../../commit/agentic/state";
5
5
  import type { NumstatEntry } from "../../../commit/types";
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
4
  import * as git from "../../../utils/git";
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { DiffHunk, FileHunks } from "../../../commit/types";
3
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
4
  import * as git from "../../../utils/git";
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agentic/state";
3
3
  import { extractScopeCandidates } from "../../../commit/analysis/scope";
4
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import { CHANGELOG_CATEGORIES, type ChangelogCategory } from "../../../commit/types";
4
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import {
4
4
  capDetails,
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
3
3
  import * as git from "../../../utils/git";
4
4
 
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
 
3
3
  export const commitTypeSchema = type(
4
4
  "'feat' | 'fix' | 'refactor' | 'perf' | 'docs' | 'test' | 'build' | 'ci' | 'chore' | 'style' | 'revert'",
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import type { CommitAgentState, SplitCommitGroup, SplitCommitPlan } from "../../../commit/agentic/state";
3
3
  import { computeDependencyOrder } from "../../../commit/agentic/topo-sort";
4
4
  import {
@@ -1,8 +1,8 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
3
  import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
4
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
5
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { type } from "arktype";
6
6
  import summarySystemPrompt from "../../commit/prompts/summary-system.md" with { type: "text" };
7
7
  import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
8
8
  import type { CommitSummary } from "../../commit/types";
@@ -1,8 +1,8 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
3
  import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
4
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
5
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { type } from "arktype";
6
6
  import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
7
7
  import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
8
8
  import type { ChangelogGenerationResult } from "../../commit/types";
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { OmpErrors, type Type } from "@oh-my-pi/omptype";
3
4
  import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
4
- import { ArkErrors, type Type } from "arktype";
5
5
  import { JSONC, YAML } from "bun";
6
6
 
7
7
  /** Minimal subset of the AJV ConfigSchemaError shape this module actually relies on. */
@@ -252,7 +252,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
252
252
  }
253
253
 
254
254
  const checked = this.schema(parsed);
255
- if (checked instanceof ArkErrors) {
255
+ if (checked instanceof OmpErrors) {
256
256
  const schemaErrors: ConfigSchemaError[] = checked.map(error => ({
257
257
  instancePath: error.path.length === 0 ? "root" : error.path.join("."),
258
258
  message: error.problem,
@@ -38,25 +38,25 @@ export const DISCOVERY_DEFAULT_CONTEXT_WINDOW = OPENAI_COMPAT_DISCOVERY_DEFAULT_
38
38
  export const DISCOVERY_DEFAULT_MAX_TOKENS = OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS;
39
39
 
40
40
  /**
41
- * Run `fn` with an abort signal that fires after `timeoutMs`, clearing the
42
- * backing timer the instant the operation settles.
41
+ * Run `fn` with a hard deadline while also signalling cooperative transports
42
+ * to abort. The independent rejection keeps discovery bounded when a runtime
43
+ * leaves its fetch promise pending after `AbortSignal.abort()` (observed with
44
+ * Windows localhost probes).
43
45
  *
44
- * Unlike the built-in abort-signal timeout API, the timer never outlives the
45
- * request: on the success path it is cancelled before `fn` resolves, so the
46
- * signal is never aborted and no pending callback lingers on the heap. A leaked
47
- * abort-signal timeout (e.g. discovery against a mocked fetch that resolves
48
- * instantly) fires seconds later and sets its abort `reason` — which crashed
49
- * Bun's concurrent GC while it marked the signal's wrapped reason during an
50
- * unrelated allocation (`JSAbortSignal::visitAdditionalChildren`).
46
+ * The backing timer is cleared as soon as `fn` settles, unlike
47
+ * `AbortSignal.timeout()`, whose delayed reason previously crashed Bun's
48
+ * concurrent GC during an unrelated allocation.
51
49
  */
52
50
  async function withTimeoutSignal<T>(timeoutMs: number, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
53
51
  const controller = new AbortController();
54
- const timer = setTimeout(
55
- () => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
56
- timeoutMs,
57
- );
52
+ const timeout = Promise.withResolvers<never>();
53
+ const timer = setTimeout(() => {
54
+ const error = new DOMException("The operation timed out.", "TimeoutError");
55
+ controller.abort(error);
56
+ timeout.reject(error);
57
+ }, timeoutMs);
58
58
  try {
59
- return await fn(controller.signal);
59
+ return await Promise.race([fn(controller.signal), timeout.promise]);
60
60
  } finally {
61
61
  clearTimeout(timer);
62
62
  }
@@ -68,6 +68,7 @@ import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
68
68
  import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
69
69
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
70
70
  import { setCodexAttestationProvider } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
71
+ import { getProviderDefinition } from "@oh-my-pi/pi-ai/registry";
71
72
  import {
72
73
  getBundledModelReferenceIndex,
73
74
  inheritReferenceThinking,
@@ -2011,14 +2012,15 @@ export class ModelRegistry {
2011
2012
  this.#providerOverrides.has(descriptor.providerId) ||
2012
2013
  this.#keylessProviders.has(descriptor.providerId));
2013
2014
  if (isAuthenticated(apiKey) || descriptor.allowUnauthenticated || hasExplicitVllmConfig) {
2014
- const discoveryBaseUrl = this.#descriptorBaseUrl(descriptor.providerId);
2015
- options.push(
2016
- descriptor.createModelManagerOptions({
2017
- apiKey: isDiscoveryBearerApiKey(apiKey) ? apiKey : undefined,
2018
- baseUrl: discoveryBaseUrl,
2019
- fetch: this.#fetch,
2020
- }),
2021
- );
2015
+ const discoveryConfig = {
2016
+ apiKey: isDiscoveryBearerApiKey(apiKey) ? apiKey : undefined,
2017
+ baseUrl: this.#descriptorBaseUrl(descriptor.providerId),
2018
+ fetch: this.#fetch,
2019
+ };
2020
+ const preparedConfig =
2021
+ getProviderDefinition(descriptor.providerId)?.prepareModelDiscovery?.(discoveryConfig) ??
2022
+ discoveryConfig;
2023
+ options.push(descriptor.createModelManagerOptions(preparedConfig));
2022
2024
  }
2023
2025
  }
2024
2026
 
@@ -1,14 +1,7 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import { once } from "@oh-my-pi/pi-utils";
2
- import { scope } from "arktype";
3
3
 
4
4
  export const getModelsConfigSchemaBundle = once(() => {
5
- // Config schemas validate at most a handful of times per process (on config
6
- // load), so the eager JIT codegen ArkType runs at definition time is pure
7
- // startup tax. A local jitless scope skips that codegen and falls back to
8
- // interpreted traversal — ~65% cheaper to construct, validation correctness
9
- // unchanged. (No `name`: duplicate module instances would collide.)
10
- const { type } = scope({}, { jitless: true });
11
-
12
5
  const OpenRouterRoutingSchema = type({
13
6
  "only?": "string[]",
14
7
  "order?": "string[]",
@@ -30,6 +30,7 @@ import {
30
30
  setWorktreesDir,
31
31
  toError,
32
32
  } from "@oh-my-pi/pi-utils";
33
+ import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
33
34
  import { JSONC, YAML } from "bun";
34
35
  import { invalidate as invalidateCapabilityFsCache } from "../capability/fs";
35
36
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
@@ -41,7 +42,6 @@ import { AUTO_IMAGE_PROVIDER_ORDER, isImageProviderId } from "../tools/image-pro
41
42
  import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
42
43
  import { INSPECT_IMAGE_MODES } from "../utils/inspect-image-mode";
43
44
  import { isSearchProviderId, SEARCH_PROVIDER_ORDER } from "../web/search/types";
44
- import { withFileLock } from "./file-lock";
45
45
  import {
46
46
  type BashInterceptorRule,
47
47
  type GroupPrefix,
@@ -87,23 +87,28 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
87
87
  ]);
88
88
 
89
89
  const items: MCPServer[] = [];
90
- if (userConfig) {
91
- const servers = extractMCPServersFromToml(userConfig, path.dirname(userConfigPath));
92
- for (const [name, config] of Object.entries(servers)) {
90
+ // Capability dedupe is first-wins, including suppressed items claiming their
91
+ // key. Load project entries first so a project `enabled = false` keeps a
92
+ // same-named user server disabled.
93
+ if (projectConfig) {
94
+ const servers = extractMCPServersFromToml(projectConfig, path.dirname(projectConfigPath));
95
+ for (const name in servers) {
96
+ const config = servers[name];
93
97
  items.push({
94
98
  name,
95
99
  ...config,
96
- _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
100
+ _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
97
101
  });
98
102
  }
99
103
  }
100
- if (projectConfig) {
101
- const servers = extractMCPServersFromToml(projectConfig, path.dirname(projectConfigPath));
102
- for (const [name, config] of Object.entries(servers)) {
104
+ if (userConfig) {
105
+ const servers = extractMCPServersFromToml(userConfig, path.dirname(userConfigPath));
106
+ for (const name in servers) {
107
+ const config = servers[name];
103
108
  items.push({
104
109
  name,
105
110
  ...config,
106
- _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
111
+ _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
107
112
  });
108
113
  }
109
114
  }
@@ -125,6 +130,7 @@ async function loadTomlConfig(_ctx: LoadContext, path: string): Promise<Record<s
125
130
 
126
131
  /** Codex MCP server config format (from config.toml) */
127
132
  interface CodexMCPConfig {
133
+ enabled?: boolean;
128
134
  command?: string;
129
135
  args?: string[];
130
136
  env?: Record<string, string>;
@@ -152,13 +158,20 @@ function extractMCPServersFromToml(
152
158
  const codexServers = toml.mcp_servers as Record<string, CodexMCPConfig>;
153
159
  const result: Record<string, Partial<MCPServer>> = {};
154
160
 
155
- for (const [name, config] of Object.entries(codexServers)) {
161
+ for (const name in codexServers) {
162
+ const config = codexServers[name];
156
163
  // Root relative cwd/command against the Codex config directory. Codex
157
164
  // spawns the process with the resolved cwd, so a relative command is
158
165
  // resolved by the OS from there — pass "cwd" so e.g. cwd="server",
159
166
  // command="./bin/mcp" resolves to <configDir>/server/bin/mcp.
160
167
  const rooted = resolvePluginStdioPaths({ command: config.command, cwd: config.cwd }, configDir, "cwd");
161
168
  const server: Partial<MCPServer> = {
169
+ // Carry `enabled: false` through rather than dropping the entry: the
170
+ // central MCP loader (`loadAllMCPConfigs`) suppresses disabled servers
171
+ // so they still claim their dedupe key (keeping a same-named,
172
+ // lower-priority source disabled) and remain overridable via the user
173
+ // force-enable allowlist. Dropping here would defeat both.
174
+ ...(config.enabled === false && { enabled: false }),
162
175
  ...(rooted.command !== undefined && { command: rooted.command }),
163
176
  args: config.args,
164
177
  url: config.url,
@@ -3,7 +3,7 @@
3
3
  * deliberately permissive (allows extra keys) so providers can attach extra
4
4
  * keys without rejection; only `input` is required.
5
5
  */
6
- import { type } from "arktype";
6
+ import { type } from "@oh-my-pi/omptype";
7
7
 
8
8
  export const hashlineEditParamsSchema = type({
9
9
  input: "string",
@@ -8,7 +8,7 @@
8
8
  * the `patch` mode.
9
9
  */
10
10
 
11
- import { type } from "arktype";
11
+ import { type } from "@oh-my-pi/omptype";
12
12
  import { parseApplyPatch, parseApplyPatchStreaming } from "../apply-patch/parser";
13
13
  import { ApplyPatchError } from "../diff";
14
14
  import type { PatchEditEntry } from "./patch";
@@ -7,9 +7,9 @@
7
7
 
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
+ import { type } from "@oh-my-pi/omptype";
10
11
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
11
12
  import { isEnoent } from "@oh-my-pi/pi-utils";
12
- import { type } from "arktype";
13
13
  import {
14
14
  type FileDiagnosticsResult,
15
15
  flushLspWritethroughBatch,
@@ -4,8 +4,9 @@
4
4
  * Provides both character-level and line-level fuzzy matching with progressive
5
5
  * fallback strategies for finding text in files.
6
6
  */
7
+
8
+ import { type } from "@oh-my-pi/omptype";
7
9
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
8
- import { type } from "arktype";
9
10
  import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
10
11
  import type { ToolSession } from "../../tools";
11
12
  import { routeWriteThroughBridge } from "../../tools/acp-bridge";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Host-side handler for the eval `agent()` helper.
3
3
  */
4
- import { type } from "arktype";
4
+ import { type } from "@oh-my-pi/omptype";
5
5
  import {
6
6
  buildStructuredSubagentRecoveryHint,
7
7
  runStructuredSubagent,
@@ -155,7 +155,9 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
155
155
  ...(isolation ? { isolation } : {}),
156
156
  ...(parsed.handle ? { retainArtifacts: true } : {}),
157
157
  keepAlive: false,
158
- maxRuntimeMs: 0,
158
+ // `maxRuntimeMs` is intentionally omitted: the executor then inherits
159
+ // `task.maxRuntimeMs`, matching the task tool. Pinning it to 0 here
160
+ // silently overrode the user's wall-clock cap for eval fan-outs.
159
161
  shareEvalSession: false,
160
162
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
161
163
  ...(options.emitStatus
@@ -29,8 +29,11 @@ export function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean {
29
29
  /** Optional behavior for a timeout pause around a host bridge call. */
30
30
  export interface BridgeTimeoutPauseOptions {
31
31
  /**
32
- * Marks the pause as an `agent()` call whose already-started work must finish
33
- * before an external eval abort reaches the kernel.
32
+ * Holds an external eval abort back from the *kernel* until this bridge call
33
+ * settles, so the runtime is never torn down mid-phase (`agent()` isolation
34
+ * worktree setup and merge/cherry-pick). It does not shield the delegated
35
+ * work itself: the bridge hands subagents the caller's real signal, so a
36
+ * turn cancel still stops them immediately.
34
37
  */
35
38
  deferExternalAbort?: boolean;
36
39
  }
@@ -11,10 +11,11 @@
11
11
  * The call is oneshot and toolless from the model's perspective — pure text
12
12
  * in, text (or, with `schema`, a structured object) out.
13
13
  */
14
+
15
+ import { type } from "@oh-my-pi/omptype";
14
16
  import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
15
17
  import { type Api, Effort, type Model, type Tool } from "@oh-my-pi/pi-ai";
16
18
  import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
17
- import { type } from "arktype";
18
19
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
19
20
 
20
21
  import {
@@ -46,7 +47,7 @@ const completionArgsSchema = type({
46
47
  prompt: "string>0",
47
48
  "model?": "'smol'|'default'|'slow'",
48
49
  "system?": "string",
49
- "schema?": "Record<string,unknown>",
50
+ "schema?": { "[string]": "unknown" },
50
51
  });
51
52
 
52
53
  export interface EvalCompletionBridgeOptions {
@@ -158,6 +158,15 @@ export async function waitForPromiseWithCancellation<T>(
158
158
  return await resultPromise;
159
159
  }
160
160
 
161
+ /**
162
+ * Derived abort signal for the kernel, holding back an external abort while a
163
+ * bridge call marked `deferExternalAbort` is in flight.
164
+ *
165
+ * Scope is deliberately narrow: only `kernel.execute` consumes
166
+ * {@link BridgeAbortShield.signal}. Work dispatched through the tool bridge
167
+ * keeps the caller's real signal so a turn cancel reaches spawned subagents
168
+ * immediately — deferral protects the runtime, never the delegated work.
169
+ */
161
170
  interface BridgeAbortShield {
162
171
  signal: AbortSignal | undefined;
163
172
  abortRequested: boolean;
@@ -428,11 +437,21 @@ export async function executeWithKernelBase<
428
437
  const emitStatus: (event: JsStatusEvent) => void =
429
438
  options?.emitStatus ?? (event => collectDisplay({ type: "status", event }));
430
439
  const runId = `${runIdPrefix}-${crypto.randomUUID()}`;
440
+ // Two aborts cross the bridge, and conflating them is what let a cancelled
441
+ // turn keep working. Delegated work (above all the subagents `agent()`
442
+ // spawns) gets the caller's real signal so it dies with the turn — shielding
443
+ // it here made Python/Ruby/Julia fan-outs outlive a cancel indefinitely,
444
+ // while JS cells, which never route through this shield, stopped fine. The
445
+ // shielded signal only governs how long the host waits on a call, holding
446
+ // the cell open across a critical phase (isolation worktree setup,
447
+ // merge/cherry-pick) so a cancel can't settle it on top of a half-applied
448
+ // git operation.
431
449
  const unregisterBridge =
432
450
  options?.toolSession && options?.bridgeSessionId
433
451
  ? registerPyToolBridge(options.bridgeSessionId, runId, {
434
452
  toolSession: options.toolSession,
435
- signal: abortShield.signal,
453
+ signal: options.signal,
454
+ shieldedSignal: abortShield.signal,
436
455
  emitStatus,
437
456
  abortRequested: () => {
438
457
  return abortShield.abortRequested;
@@ -8,6 +8,7 @@ import {
8
8
  import type { ToolSession } from "../../tools";
9
9
  import { ToolAbortError, ToolError } from "../../tools/tool-errors";
10
10
  import { safeSend as safeSendIpc } from "../../utils/ipc";
11
+ import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../bridge-timeout";
11
12
  import { attachSessionOwner, resolveOwnerScopedSessionKey, type SessionOwners } from "../executor-base";
12
13
  import { shouldDetachKernel } from "../py/spawn-options";
13
14
  import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
@@ -48,6 +49,26 @@ interface PendingRun {
48
49
  resolve(value: { value: unknown }): void;
49
50
  reject(error: Error): void;
50
51
  toolCalls: Map<string, AbortController>;
52
+ /**
53
+ * Host calls currently inside a `deferExternalAbort` phase — `agent()`
54
+ * isolation worktree setup and merge/cherry-pick, which ignore their abort
55
+ * once started. Settling the run while one is live would return the cell on
56
+ * top of a git operation still rewriting the repo, so the abort path drains
57
+ * this first. Mirrors the Python bridge's shielded-signal contract.
58
+ */
59
+ deferDepth: number;
60
+ /** Resolves once {@link deferDepth} falls back to zero. */
61
+ deferDrained?: PromiseWithResolvers<void>;
62
+ /** Set once the turn was cancelled; blocks new bridge calls during the drain. */
63
+ aborted: boolean;
64
+ /**
65
+ * A worker `result` withheld because the cell still has bridge calls in
66
+ * flight. `#runOne` reports a finished run without awaiting its pending
67
+ * tools, so a floated or caught `agent()` would otherwise settle the run —
68
+ * tearing down the abort listener — while the subagent kept going with
69
+ * nothing left able to cancel it. Delivered once the last call drains.
70
+ */
71
+ heldResult?: Extract<WorkerOutbound, { type: "result" }>;
51
72
  settled: boolean;
52
73
  }
53
74
 
@@ -267,6 +288,8 @@ async function runOnce(
267
288
  resolve,
268
289
  reject,
269
290
  toolCalls: new Map(),
291
+ deferDepth: 0,
292
+ aborted: false,
270
293
  settled: false,
271
294
  };
272
295
  session.pending.set(runId, pending);
@@ -274,9 +297,21 @@ async function runOnce(
274
297
  const onAbort = (): void => {
275
298
  const reason = options.runState.signal?.reason;
276
299
  const abortError = reasonToError(reason, "Execution aborted");
277
- // Cancel any in-flight tool calls first.
300
+ // Stop delegated work at once — this is what kills spawned subagents —
301
+ // and refuse further bridge calls so the drain below stays bounded to
302
+ // phases that had already started.
303
+ pending.aborted = true;
278
304
  for (const ctrl of pending.toolCalls.values()) ctrl.abort(abortError);
279
- // Hard-kill the worker only way to interrupt synchronous user code.
305
+ // A critical host phase ignores its abort once started (isolation
306
+ // worktree setup, merge/cherry-pick). Killing the worker now would
307
+ // settle the cell on top of a git operation still in progress, so wait
308
+ // for it. Hard-kill is still the only way to interrupt synchronous user
309
+ // code, hence it stays the terminal step either way.
310
+ const drained = pending.deferDepth > 0 ? pending.deferDrained?.promise : undefined;
311
+ if (drained) {
312
+ void drained.then(() => killSessionFor(session, abortError, { force: true }));
313
+ return;
314
+ }
280
315
  void killSessionFor(session, abortError, { force: true });
281
316
  };
282
317
 
@@ -458,6 +493,25 @@ function handleSessionMessage(session: JsSession, msg: WorkerOutbound): void {
458
493
  }
459
494
  }
460
495
 
496
+ /**
497
+ * Maintain {@link PendingRun.deferDepth} from the bridge's pause/resume status
498
+ * events so an abort can wait out a critical `agent()` phase instead of
499
+ * settling the cell over a half-applied merge.
500
+ */
501
+ function trackDeferPhase(pending: PendingRun, event: JsStatusEvent): void {
502
+ if (event.deferExternalAbort !== true) return;
503
+ if (event.op === EVAL_TIMEOUT_PAUSE_OP) {
504
+ pending.deferDepth++;
505
+ pending.deferDrained ??= Promise.withResolvers<void>();
506
+ return;
507
+ }
508
+ if (event.op !== EVAL_TIMEOUT_RESUME_OP || pending.deferDepth === 0) return;
509
+ pending.deferDepth--;
510
+ if (pending.deferDepth > 0) return;
511
+ pending.deferDrained?.resolve();
512
+ pending.deferDrained = undefined;
513
+ }
514
+
461
515
  async function handleToolCall(session: JsSession, msg: Extract<WorkerOutbound, { type: "tool-call" }>): Promise<void> {
462
516
  const pending = session.pending.get(msg.runId);
463
517
  if (!pending) {
@@ -468,26 +522,42 @@ async function handleToolCall(session: JsSession, msg: Extract<WorkerOutbound, {
468
522
  });
469
523
  return;
470
524
  }
525
+ if (pending.aborted) {
526
+ safeSend(session, {
527
+ type: "tool-reply",
528
+ id: msg.id,
529
+ reply: { ok: false, error: { message: "Run was interrupted" } },
530
+ });
531
+ return;
532
+ }
471
533
  const ctrl = new AbortController();
472
534
  pending.toolCalls.set(msg.id, ctrl);
473
535
  try {
474
536
  const value = await callSessionTool(msg.name, msg.args, {
475
537
  session: pending.toolSession,
476
538
  signal: ctrl.signal,
477
- emitStatus: (event: JsStatusEvent) => pending.runState.onDisplay?.({ type: "status", event }),
539
+ emitStatus: (event: JsStatusEvent) => {
540
+ trackDeferPhase(pending, event);
541
+ pending.runState.onDisplay?.({ type: "status", event });
542
+ },
478
543
  });
479
544
  safeSend(session, { type: "tool-reply", id: msg.id, reply: { ok: true, value } });
480
545
  } catch (error) {
481
546
  safeSend(session, { type: "tool-reply", id: msg.id, reply: { ok: false, error: toErrorPayload(error) } });
482
547
  } finally {
483
548
  pending.toolCalls.delete(msg.id);
549
+ // Last call of a run whose worker result was withheld: settle it now.
550
+ const held = pending.heldResult;
551
+ if (held && !pending.settled && !pending.aborted && pending.toolCalls.size === 0) {
552
+ finishPending(pending, held);
553
+ }
484
554
  }
485
555
  }
486
556
 
487
- function settlePending(session: JsSession, msg: Extract<WorkerOutbound, { type: "result" }>): void {
488
- const pending = session.pending.get(msg.runId);
489
- if (!pending || pending.settled) return;
557
+ /** Deliver a worker `result` to the waiting {@link runOnce}. */
558
+ function finishPending(pending: PendingRun, msg: Extract<WorkerOutbound, { type: "result" }>): void {
490
559
  pending.settled = true;
560
+ pending.heldResult = undefined;
491
561
  if (msg.ok) {
492
562
  pending.resolve({ value: undefined });
493
563
  return;
@@ -495,6 +565,24 @@ function settlePending(session: JsSession, msg: Extract<WorkerOutbound, { type:
495
565
  pending.reject(errorFromPayload(msg.error));
496
566
  }
497
567
 
568
+ function settlePending(session: JsSession, msg: Extract<WorkerOutbound, { type: "result" }>): void {
569
+ const pending = session.pending.get(msg.runId);
570
+ if (!pending || pending.settled) return;
571
+ // Once the turn is cancelled the scheduled kill is the sole settler, so a
572
+ // late worker result can't cut the abort drain short.
573
+ if (pending.aborted) return;
574
+ // A cell owns every bridge call it starts. The worker finishes a run without
575
+ // awaiting its outstanding tool calls, so `agent(...)` that is floated or
576
+ // caught would settle the run here — `runOnce` then drops the abort listener
577
+ // and the pending entry, leaving the subagent running with nothing able to
578
+ // cancel it. Hold the result until the last call drains.
579
+ if (pending.toolCalls.size > 0) {
580
+ pending.heldResult = msg;
581
+ return;
582
+ }
583
+ finishPending(pending, msg);
584
+ }
585
+
498
586
  async function killSessionFor(session: JsSession, error: Error, options: { force: boolean }): Promise<void> {
499
587
  if (sessions.get(session.sessionKey) === session) {
500
588
  sessions.delete(session.sessionKey);