@oh-my-pi/pi-coding-agent 17.2.6 → 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 (162) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/{CHANGELOG-gs76k6wc.md → CHANGELOG-1p8yvde7.md} +17 -1
  3. package/dist/cli.js +3645 -3689
  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/commit/agentic/tools/analyze-file.d.ts +5 -2
  10. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +5 -2
  11. package/dist/types/commit/agentic/tools/git-hunk.d.ts +6 -2
  12. package/dist/types/commit/agentic/tools/git-overview.d.ts +5 -2
  13. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +29 -7
  14. package/dist/types/commit/agentic/tools/propose-commit.d.ts +14 -4
  15. package/dist/types/commit/agentic/tools/recent-commits.d.ts +4 -2
  16. package/dist/types/commit/agentic/tools/schemas.d.ts +7 -3
  17. package/dist/types/commit/agentic/tools/split-commit.d.ts +37 -10
  18. package/dist/types/commit/changelog/generate.d.ts +13 -3
  19. package/dist/types/commit/shared-llm.d.ts +13 -4
  20. package/dist/types/config/config-file.d.ts +1 -1
  21. package/dist/types/config/models-config-schema-bundle.d.ts +1992 -981
  22. package/dist/types/config/models-config-schema.d.ts +1201 -580
  23. package/dist/types/config/models-config.d.ts +355 -371
  24. package/dist/types/edit/hashline/params.d.ts +4 -2
  25. package/dist/types/edit/modes/apply-patch.d.ts +4 -2
  26. package/dist/types/edit/modes/patch.d.ts +16 -5
  27. package/dist/types/edit/modes/replace.d.ts +8 -3
  28. package/dist/types/eval/bridge-timeout.d.ts +5 -2
  29. package/dist/types/eval/completion-bridge.d.ts +13 -0
  30. package/dist/types/eval/py/tool-bridge.d.ts +12 -0
  31. package/dist/types/extensibility/custom-commands/types.d.ts +4 -4
  32. package/dist/types/extensibility/custom-tools/types.d.ts +4 -4
  33. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  34. package/dist/types/extensibility/hooks/types.d.ts +4 -4
  35. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  36. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  37. package/dist/types/extensibility/legacy-typebox.d.ts +8 -0
  38. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +6 -13
  39. package/dist/types/goals/tools/goal-tool.d.ts +12 -4
  40. package/dist/types/lsp/index.d.ts +15 -5
  41. package/dist/types/lsp/types.d.ts +15 -5
  42. package/dist/types/security/contracts/schemas.d.ts +985 -409
  43. package/dist/types/security/publication.d.ts +92 -34
  44. package/dist/types/session/agent-session-types.d.ts +1 -1
  45. package/dist/types/session/session-entries.d.ts +1 -1
  46. package/dist/types/session/session-manager.d.ts +1 -1
  47. package/dist/types/task/types.d.ts +63 -18
  48. package/dist/types/tools/ask.d.ts +34 -8
  49. package/dist/types/tools/ast-edit.d.ts +16 -4
  50. package/dist/types/tools/ast-grep.d.ts +12 -4
  51. package/dist/types/tools/bash.d.ts +20 -11
  52. package/dist/types/tools/browser.d.ts +64 -20
  53. package/dist/types/tools/checkpoint.d.ts +16 -8
  54. package/dist/types/tools/computer.d.ts +1 -1
  55. package/dist/types/tools/debug.d.ts +118 -48
  56. package/dist/types/tools/eval.d.ts +11 -5
  57. package/dist/types/tools/gh.d.ts +74 -26
  58. package/dist/types/tools/glob.d.ts +16 -6
  59. package/dist/types/tools/grep.d.ts +18 -6
  60. package/dist/types/tools/hub/index.d.ts +116 -46
  61. package/dist/types/tools/image-gen.d.ts +25 -8
  62. package/dist/types/tools/inspect-image.d.ts +10 -4
  63. package/dist/types/tools/learn.d.ts +26 -8
  64. package/dist/types/tools/manage-skill.d.ts +16 -6
  65. package/dist/types/tools/memory-edit.d.ts +18 -6
  66. package/dist/types/tools/memory-recall.d.ts +8 -4
  67. package/dist/types/tools/memory-reflect.d.ts +10 -4
  68. package/dist/types/tools/memory-retain.d.ts +14 -4
  69. package/dist/types/tools/read.d.ts +8 -4
  70. package/dist/types/tools/security-scan.d.ts +82 -30
  71. package/dist/types/tools/todo.d.ts +30 -10
  72. package/dist/types/tools/tts.d.ts +12 -5
  73. package/dist/types/tools/vibe.d.ts +42 -18
  74. package/dist/types/tools/write.d.ts +10 -4
  75. package/dist/types/web/search/index.d.ts +20 -6
  76. package/package.json +13 -13
  77. package/scripts/legacy-pi-virtual-module.ts +2 -2
  78. package/src/advisor/advise-tool.ts +1 -1
  79. package/src/advisor/config.ts +1 -1
  80. package/src/autoresearch/tools/init-experiment.ts +1 -2
  81. package/src/autoresearch/tools/log-experiment.ts +1 -2
  82. package/src/autoresearch/tools/run-experiment.ts +1 -1
  83. package/src/autoresearch/tools/update-notes.ts +1 -1
  84. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  85. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  86. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  87. package/src/commit/agentic/tools/git-overview.ts +1 -1
  88. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  89. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  90. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  91. package/src/commit/agentic/tools/schemas.ts +1 -1
  92. package/src/commit/agentic/tools/split-commit.ts +1 -1
  93. package/src/commit/analysis/summary.ts +1 -1
  94. package/src/commit/changelog/generate.ts +1 -1
  95. package/src/config/config-file.ts +2 -2
  96. package/src/config/models-config-schema-bundle.ts +1 -8
  97. package/src/discovery/codex.ts +22 -9
  98. package/src/edit/hashline/params.ts +1 -1
  99. package/src/edit/modes/apply-patch.ts +1 -1
  100. package/src/edit/modes/patch.ts +1 -1
  101. package/src/edit/modes/replace.ts +2 -1
  102. package/src/eval/agent-bridge.ts +4 -2
  103. package/src/eval/bridge-timeout.ts +5 -2
  104. package/src/eval/completion-bridge.ts +3 -2
  105. package/src/eval/executor-base.ts +20 -1
  106. package/src/eval/js/context-manager.ts +94 -6
  107. package/src/eval/py/tool-bridge.ts +29 -9
  108. package/src/extensibility/custom-commands/loader.ts +3 -3
  109. package/src/extensibility/custom-commands/types.ts +4 -4
  110. package/src/extensibility/custom-tools/loader.ts +3 -3
  111. package/src/extensibility/custom-tools/types.ts +5 -4
  112. package/src/extensibility/extensions/loader.ts +3 -3
  113. package/src/extensibility/extensions/types.ts +5 -4
  114. package/src/extensibility/hooks/loader.ts +3 -3
  115. package/src/extensibility/hooks/types.ts +4 -4
  116. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  117. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  118. package/src/extensibility/legacy-typebox.ts +101 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -22
  120. package/src/goals/tools/goal-tool.ts +1 -1
  121. package/src/lsp/types.ts +1 -1
  122. package/src/modes/theme/theme.ts +3 -3
  123. package/src/security/contracts/schemas.ts +1 -5
  124. package/src/security/contracts/validation.ts +1 -1
  125. package/src/security/publication.ts +1 -1
  126. package/src/session/agent-session-types.ts +1 -1
  127. package/src/session/agent-session.ts +1 -2
  128. package/src/session/session-context.ts +3 -3
  129. package/src/session/session-entries.ts +1 -1
  130. package/src/session/session-manager.ts +1 -1
  131. package/src/slash-commands/builtin-registry.ts +3 -4
  132. package/src/task/types.ts +1 -1
  133. package/src/tools/ask.ts +1 -1
  134. package/src/tools/ast-edit.ts +1 -1
  135. package/src/tools/ast-grep.ts +1 -1
  136. package/src/tools/bash.ts +61 -3
  137. package/src/tools/browser.ts +1 -1
  138. package/src/tools/checkpoint.ts +1 -1
  139. package/src/tools/computer.ts +1 -1
  140. package/src/tools/debug.ts +1 -1
  141. package/src/tools/eval.ts +1 -1
  142. package/src/tools/gh.ts +1 -2
  143. package/src/tools/glob.ts +1 -1
  144. package/src/tools/grep.ts +1 -1
  145. package/src/tools/hub/index.ts +1 -1
  146. package/src/tools/image-gen.ts +1 -1
  147. package/src/tools/inspect-image.ts +1 -1
  148. package/src/tools/learn.ts +1 -1
  149. package/src/tools/manage-skill.ts +1 -1
  150. package/src/tools/memory-edit.ts +1 -1
  151. package/src/tools/memory-recall.ts +1 -1
  152. package/src/tools/memory-reflect.ts +1 -1
  153. package/src/tools/memory-retain.ts +1 -1
  154. package/src/tools/read.ts +1 -1
  155. package/src/tools/security-scan.ts +1 -1
  156. package/src/tools/todo.ts +1 -1
  157. package/src/tools/tts.ts +1 -1
  158. package/src/tools/vibe.ts +2 -1
  159. package/src/tools/write.ts +1 -1
  160. package/src/web/search/index.ts +2 -1
  161. package/dist/types/extensibility/typebox.d.ts +0 -181
  162. package/src/extensibility/typebox.ts +0 -958
@@ -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,
@@ -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[]",
@@ -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);
@@ -13,7 +13,19 @@ import { callSessionTool, type JsStatusEvent } from "../js/tool-bridge";
13
13
 
14
14
  export interface PyToolBridgeEntry {
15
15
  toolSession: ToolSession;
16
+ /**
17
+ * Turn-cancel handed to the tool implementation. Raw and never deferred, so
18
+ * delegated work — above all the subagents `agent()` spawns — stops at once.
19
+ */
16
20
  signal?: AbortSignal;
21
+ /**
22
+ * Kernel-side abort, held back while a critical `agent()` phase (isolation
23
+ * worktree setup, merge/cherry-pick) is in flight. Decides only when the host
24
+ * may stop waiting on a call and let the kernel unwind; it is never given to
25
+ * a tool. Keeping these separate is what stops a cancel from settling the
26
+ * cell on top of a still-running, abort-insensitive merge.
27
+ */
28
+ shieldedSignal?: AbortSignal;
17
29
  emitStatus?: (event: JsStatusEvent) => void;
18
30
  abortRequested?: () => boolean;
19
31
  }
@@ -32,16 +44,24 @@ const registrations = new Map<string, PyToolBridgeEntry>();
32
44
  let serverPromise: Promise<BridgeServer> | null = null;
33
45
 
34
46
  /**
35
- * Forward a bridge call to {@link callSessionTool} while respecting eval abort
36
- * shielding.
47
+ * Forward a bridge call to {@link callSessionTool}, failing fast once the cell
48
+ * has been interrupted.
37
49
  *
38
50
  * Python invokes this bridge with blocking `urllib` requests from worker threads
39
- * (each `agent()` / `tool.*` call). The base executor defers the registered
40
- * signal while a bridge call is already paused so in-flight subagents can finish
41
- * and persist output instead of being orphaned. Once an abort has been requested,
42
- * later bridge calls are rejected before starting; once the shielded signal
43
- * finally aborts, this handler still resolves the HTTP request promptly so the
44
- * kernel can unwind without being hard-killed.
51
+ * (each `agent()` / `tool.*` call). Two different aborts meet here:
52
+ *
53
+ * - {@link PyToolBridgeEntry.signal} goes to the tool, so a turn cancel tears
54
+ * down delegated work subagents included instead of leaving it running
55
+ * past the cell.
56
+ * - {@link PyToolBridgeEntry.shieldedSignal} decides when we may stop waiting.
57
+ * It is deferred across a critical `agent()` phase, so a cancel landing
58
+ * mid-merge cannot return early and let the cell settle while an
59
+ * abort-insensitive cherry-pick is still rewriting the repo.
60
+ *
61
+ * Calls arriving after an abort are rejected before starting. Otherwise the
62
+ * usual path is that the tool observes its own abort and rejects; the race only
63
+ * matters for tools that ignore the signal, keeping the kernel unwinding
64
+ * promptly instead of being hard-killed.
45
65
  */
46
66
  async function callSessionToolPromptOnAbort(name: string, args: unknown, entry: PyToolBridgeEntry): Promise<unknown> {
47
67
  if (entry.abortRequested?.()) {
@@ -52,7 +72,7 @@ async function callSessionToolPromptOnAbort(name: string, args: unknown, entry:
52
72
  signal: entry.signal,
53
73
  emitStatus: entry.emitStatus,
54
74
  });
55
- const signal = entry.signal;
75
+ const signal = entry.shieldedSignal ?? entry.signal;
56
76
  if (!signal) return await call;
57
77
  if (signal.aborted) {
58
78
  void call.catch(() => {});
@@ -6,14 +6,14 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
+ import * as arktype from "@oh-my-pi/omptype";
10
+ import * as zodModule from "@oh-my-pi/omptype/zod";
9
11
  import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
10
- import * as arktype from "arktype";
11
- import * as zodModule from "zod/v4";
12
12
  import { getConfigDirs } from "../../config";
13
13
  import { execCommand } from "../../exec/exec";
14
14
  // Runtime self-reference: dereference this namespace only inside loader functions to keep the index.ts cycle safe.
15
15
  import * as PiCodingAgent from "../../index";
16
- import * as typebox from "../typebox";
16
+ import * as typebox from "../legacy-typebox";
17
17
  import { GreenCommand } from "./bundled/ci-green";
18
18
  import { ReviewCommand } from "./bundled/review";
19
19
  import type {
@@ -5,11 +5,11 @@
5
5
  * Unlike markdown commands which expand to prompts, custom commands can execute
6
6
  * arbitrary logic with full access to the hook context.
7
7
  */
8
- import type * as arktype from "arktype";
9
- import type * as zod from "zod/v4";
8
+ import type * as arktype from "@oh-my-pi/omptype";
9
+ import type * as TypeBox from "@oh-my-pi/omptype/typebox";
10
+ import type * as zod from "@oh-my-pi/omptype/zod";
10
11
  import type { ExecOptions, ExecResult, HookCommandContext } from "../../extensibility/hooks/types";
11
12
  import type * as PiCodingAgent from "../../index";
12
- import type * as TypeBox from "../typebox";
13
13
 
14
14
  // Re-export for custom commands to use
15
15
  export type { ExecOptions, ExecResult, HookCommandContext };
@@ -27,7 +27,7 @@ export interface CustomCommandAPI {
27
27
  typebox: typeof TypeBox;
28
28
  /** Injected arktype module for validation in custom commands. */
29
29
  arktype: typeof arktype;
30
- /** Injected zod/v4 module for canonical command validation. */
30
+ /** Injected omptype-backed zod facade for command validation. */
31
31
  zod: typeof zod;
32
32
  /** Injected pi-coding-agent exports */
33
33
  pi: typeof PiCodingAgent;
@@ -5,10 +5,10 @@
5
5
  * CustomToolAPI to avoid import resolution issues with custom tools loaded from user directories.
6
6
  */
7
7
  import * as path from "node:path";
8
+ import { type } from "@oh-my-pi/omptype";
9
+ import * as zodModule from "@oh-my-pi/omptype/zod";
8
10
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
9
11
  import { logger } from "@oh-my-pi/pi-utils";
10
- import { type } from "arktype";
11
- import * as zodModule from "zod/v4";
12
12
  import { toolCapability } from "../../capability/tool";
13
13
  import { type CustomTool, loadCapability } from "../../discovery";
14
14
  import type { ExecOptions } from "../../exec/exec";
@@ -17,7 +17,7 @@ import type { HookUIContext } from "../../extensibility/hooks/types";
17
17
  import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
18
18
  // Runtime self-reference: dereference this namespace only inside loader functions to keep the index.ts cycle safe.
19
19
  import * as PiCodingAgent from "../../index";
20
- import * as typebox from "../typebox";
20
+ import * as typebox from "../legacy-typebox";
21
21
  import { createNoOpUIContext, resolvePath, withHostGuard } from "../utils";
22
22
  import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
23
23
 
@@ -4,6 +4,10 @@
4
4
  * Custom tools are TypeScript modules that define additional tools for the agent.
5
5
  * They can provide custom rendering for tool calls and results in the TUI.
6
6
  */
7
+
8
+ import type { type as ArkType } from "@oh-my-pi/omptype";
9
+ import type * as TypeBox from "@oh-my-pi/omptype/typebox";
10
+ import type * as zod from "@oh-my-pi/omptype/zod";
7
11
  import type {
8
12
  AgentToolResult,
9
13
  AgentToolUpdateCallback,
@@ -16,8 +20,6 @@ import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
16
20
  import type { FetchImpl, Model, Static, TSchema } from "@oh-my-pi/pi-ai";
17
21
  import type { Component } from "@oh-my-pi/pi-tui";
18
22
  import type { logger as PiLogger } from "@oh-my-pi/pi-utils";
19
- import type { type as ArkType } from "arktype";
20
- import type * as zod from "zod/v4";
21
23
  import type { Rule } from "../../capability/rule";
22
24
  import type { ModelRegistry } from "../../config/model-registry";
23
25
  import type { Settings } from "../../config/settings";
@@ -29,7 +31,6 @@ import type { Theme } from "../../modes/theme/theme";
29
31
  import type { ReadonlySessionManager } from "../../session/session-manager";
30
32
  import type { TodoItem } from "../../tools/todo";
31
33
  import type { RecoveredRetryError } from "../shared-events";
32
- import type * as TypeBox from "../typebox";
33
34
 
34
35
  /** Alias for clarity */
35
36
  export type CustomToolUIContext = HookUIContext;
@@ -69,7 +70,7 @@ export interface CustomToolAPI {
69
70
  typebox: typeof TypeBox;
70
71
  /** Injected arktype module for arktype-authored custom tools. */
71
72
  arktype: typeof ArkType;
72
- /** Injected zod/v4 module for canonical parameter schemas. */
73
+ /** Injected omptype-backed zod facade for parameter schemas. */
73
74
  zod: typeof zod;
74
75
  /** Injected pi-coding-agent exports */
75
76
  pi: typeof PiCodingAgent;
@@ -4,6 +4,8 @@
4
4
  import type * as fs1 from "node:fs";
5
5
  import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
+ import { Type } from "@oh-my-pi/omptype";
8
+ import * as zodModule from "@oh-my-pi/omptype/zod";
7
9
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
10
  import type {
9
11
  ImageContent,
@@ -16,8 +18,6 @@ import type {
16
18
  } from "@oh-my-pi/pi-ai";
17
19
  import type { KeyId } from "@oh-my-pi/pi-tui";
18
20
  import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
19
- import { Type } from "arktype";
20
- import * as zodModule from "zod/v4";
21
21
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
22
22
  import { type Hook, hookCapability } from "../../capability/hook";
23
23
  import { isServiceTierFamily, isServiceTierForFamily } from "../../config/service-tier";
@@ -29,9 +29,9 @@ import { execCommand } from "../../exec/exec";
29
29
  import * as PiCodingAgent from "../../index";
30
30
  import type { CustomMessagePayload } from "../../session/messages";
31
31
  import { EventBus } from "../../utils/event-bus";
32
+ import * as TypeBox from "../legacy-typebox";
32
33
  import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/legacy-pi-compat";
33
34
  import { getAllPluginExtensionPaths } from "../plugins/loader";
34
- import * as TypeBox from "../typebox";
35
35
 
36
36
  import { resolvePath, withHostGuard } from "../utils";
37
37
  import type {