@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.1

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 (140) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/completion-gen.d.ts +80 -0
  5. package/dist/types/commands/complete.d.ts +6 -0
  6. package/dist/types/commands/completions.d.ts +13 -0
  7. package/dist/types/commands/setup.d.ts +10 -1
  8. package/dist/types/config/settings-schema.d.ts +170 -10
  9. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  10. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  13. package/dist/types/edit/hashline/index.d.ts +1 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -0
  15. package/dist/types/eval/py/runtime.d.ts +11 -1
  16. package/dist/types/export/html/template.generated.d.ts +1 -1
  17. package/dist/types/main.d.ts +1 -0
  18. package/dist/types/modes/components/index.d.ts +1 -0
  19. package/dist/types/modes/components/segment-track.d.ts +22 -0
  20. package/dist/types/modes/components/welcome.d.ts +21 -0
  21. package/dist/types/modes/interactive-mode.d.ts +3 -2
  22. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  23. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  24. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  25. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  26. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  27. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  28. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  29. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  30. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  31. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  32. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  33. package/dist/types/modes/theme/theme.d.ts +11 -0
  34. package/dist/types/modes/types.d.ts +5 -1
  35. package/dist/types/tiny/device.d.ts +78 -0
  36. package/dist/types/tiny/dtype.d.ts +85 -0
  37. package/dist/types/tiny/models.d.ts +6 -6
  38. package/dist/types/tiny/text.d.ts +15 -0
  39. package/dist/types/tiny/title-client.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +0 -1
  41. package/dist/types/tools/eval.d.ts +1 -1
  42. package/dist/types/tools/index.d.ts +0 -1
  43. package/dist/types/tui/code-cell.d.ts +2 -0
  44. package/dist/types/tui/output-block.d.ts +17 -0
  45. package/package.json +9 -9
  46. package/src/capability/rule-buckets.ts +64 -0
  47. package/src/capability/rule.ts +8 -0
  48. package/src/cli/completion-gen.ts +550 -0
  49. package/src/cli/setup-cli.ts +5 -3
  50. package/src/cli-commands.ts +2 -0
  51. package/src/cli.ts +1 -7
  52. package/src/commands/complete.ts +66 -0
  53. package/src/commands/completions.ts +60 -0
  54. package/src/commands/setup.ts +29 -4
  55. package/src/config/settings-schema.ts +70 -11
  56. package/src/discovery/builtin-defaults.ts +39 -0
  57. package/src/discovery/builtin-rules/index.ts +48 -0
  58. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  59. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  60. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  61. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  62. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  63. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  64. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  65. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  66. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  67. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  68. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  69. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  70. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  71. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  72. package/src/discovery/index.ts +1 -0
  73. package/src/edit/hashline/block-resolver.ts +14 -0
  74. package/src/edit/hashline/diff.ts +4 -1
  75. package/src/edit/hashline/execute.ts +2 -1
  76. package/src/edit/hashline/index.ts +1 -0
  77. package/src/eval/py/kernel.ts +37 -15
  78. package/src/eval/py/runtime.ts +57 -28
  79. package/src/export/html/template.generated.ts +1 -1
  80. package/src/export/html/template.js +0 -12
  81. package/src/export/ttsr.ts +2 -0
  82. package/src/internal-urls/docs-index.generated.ts +7 -8
  83. package/src/main.ts +18 -1
  84. package/src/modes/components/hook-selector.ts +15 -17
  85. package/src/modes/components/index.ts +1 -0
  86. package/src/modes/components/segment-track.ts +52 -0
  87. package/src/modes/components/tips.txt +2 -1
  88. package/src/modes/components/tool-execution.ts +5 -1
  89. package/src/modes/components/welcome.ts +47 -42
  90. package/src/modes/controllers/input-controller.ts +12 -21
  91. package/src/modes/interactive-mode.ts +17 -5
  92. package/src/modes/setup-wizard/index.ts +88 -0
  93. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  94. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  95. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  96. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  97. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  98. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  99. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  100. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  101. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  102. package/src/modes/theme/shimmer.ts +5 -0
  103. package/src/modes/theme/theme.ts +44 -20
  104. package/src/modes/types.ts +6 -1
  105. package/src/prompts/system/orchestrate-notice.md +1 -1
  106. package/src/prompts/tools/read.md +4 -0
  107. package/src/sdk.ts +5 -15
  108. package/src/slash-commands/builtin-registry.ts +8 -0
  109. package/src/tiny/device.ts +117 -0
  110. package/src/tiny/dtype.ts +101 -0
  111. package/src/tiny/models.ts +7 -6
  112. package/src/tiny/text.ts +36 -1
  113. package/src/tiny/title-client.ts +58 -3
  114. package/src/tiny/worker.ts +93 -29
  115. package/src/tools/bash.ts +16 -13
  116. package/src/tools/eval.ts +9 -4
  117. package/src/tools/index.ts +0 -11
  118. package/src/tools/read.ts +1 -0
  119. package/src/tools/renderers.ts +0 -2
  120. package/src/tui/code-cell.ts +6 -1
  121. package/src/tui/output-block.ts +199 -38
  122. package/dist/types/tools/recipe/index.d.ts +0 -46
  123. package/dist/types/tools/recipe/render.d.ts +0 -36
  124. package/dist/types/tools/recipe/runner.d.ts +0 -60
  125. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  126. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  127. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  128. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  129. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  130. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  131. package/src/prompts/tools/recipe.md +0 -16
  132. package/src/tools/recipe/index.ts +0 -81
  133. package/src/tools/recipe/render.ts +0 -19
  134. package/src/tools/recipe/runner.ts +0 -219
  135. package/src/tools/recipe/runners/cargo.ts +0 -131
  136. package/src/tools/recipe/runners/index.ts +0 -8
  137. package/src/tools/recipe/runners/just.ts +0 -73
  138. package/src/tools/recipe/runners/make.ts +0 -101
  139. package/src/tools/recipe/runners/pkg.ts +0 -167
  140. package/src/tools/recipe/runners/task.ts +0 -72
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: Use bare `catch {` when the error binding is unused
3
+ condition: "catch \\(_"
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ ---
6
+
7
+ Use bare `catch {}` when the caught value is unused. An underscore-prefixed binding adds noise and still allocates a local name.
8
+
9
+ ## Replace
10
+
11
+ ```typescript
12
+ // Bad
13
+ try {
14
+ await loadConfig();
15
+ } catch (_err) {
16
+ return null;
17
+ }
18
+
19
+ // Good
20
+ try {
21
+ await loadConfig();
22
+ } catch {
23
+ return null;
24
+ }
25
+ ```
26
+
27
+ ## Keep a real name when used
28
+
29
+ ```typescript
30
+ try {
31
+ await saveConfig();
32
+ } catch (err) {
33
+ logger.error("save failed", { err });
34
+ throw err;
35
+ }
36
+ ```
37
+
38
+ Unused error? Bare `catch`. Used error? Name it for what it carries.
@@ -0,0 +1,42 @@
1
+ ---
2
+ description: "Use `import type`, not `import('pkg').Type` in type positions"
3
+ condition: "import\\("
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ ---
6
+
7
+ Use top-level `import type` declarations for type-only dependencies. NEVER write `import("pkg").Type` inside source annotations.
8
+
9
+ ## Why
10
+
11
+ - Top-level imports expose dependencies immediately.
12
+ - Import sorting and deduplication can manage them.
13
+ - Signatures stay readable and reviewable.
14
+ - Re-exports do not inherit noisy inline paths.
15
+
16
+ ## Avoid
17
+
18
+ ```typescript
19
+ // Bad — inline imports hide dependencies in signatures.
20
+ function run(client: import("some-sdk").Client, input: import("zod/v4").infer<Schema>): Promise<Output>;
21
+
22
+ // Bad — annotations become path dumps.
23
+ const options: import("some-sdk/config").ClientOptions = { ... };
24
+ ```
25
+
26
+ ## Use
27
+
28
+ ```typescript
29
+ import type { Client } from "some-sdk";
30
+ import type { ClientOptions } from "some-sdk/config";
31
+ import type { infer as Infer } from "zod/v4";
32
+
33
+ function run(client: Client, input: Infer<Schema>): Promise<Output>;
34
+ const options: ClientOptions = { ... };
35
+ ```
36
+
37
+ ## Exceptions
38
+
39
+ - Ambient `.d.ts` globals that must not become modules.
40
+ - Generated files whose generator owns import management.
41
+
42
+ In normal `.ts` / `.tsx` source, use `import type`.
@@ -0,0 +1,56 @@
1
+ ---
2
+ description: "Never use `any` in TypeScript annotations or assertions — use `unknown`, generics, or the actual type"
3
+ condition: ": any|as any"
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ ---
6
+
7
+ Never use `: any` or `as any`. They disable type checking exactly where the boundary needs precision.
8
+
9
+ ## Use instead
10
+
11
+ - `unknown` for unvalidated input.
12
+ - A domain type when the shape is known.
13
+ - A generic when the caller supplies the shape.
14
+ - A type guard when runtime checks establish shape.
15
+ - `satisfies` for object literals that must match a contract.
16
+
17
+ ## Parameters and returns
18
+
19
+ ```typescript
20
+ // Bad
21
+ function readId(value: any): any {
22
+ return value.id;
23
+ }
24
+
25
+ // Good — validate unknown input.
26
+ function readId(value: unknown): string | undefined {
27
+ if (value && typeof value === "object" && "id" in value) {
28
+ const candidate = (value as { id: unknown }).id;
29
+ return typeof candidate === "string" ? candidate : undefined;
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Assertions
35
+
36
+ ```typescript
37
+ // Bad
38
+ const root = document.getElementById("root") as any;
39
+ root.innerText = "ready";
40
+
41
+ // Good
42
+ const root = document.getElementById("root") as HTMLElement | null;
43
+ root?.innerText = "ready";
44
+ ```
45
+
46
+ ## Object literals
47
+
48
+ ```typescript
49
+ // Bad
50
+ const config = { port: 3000 } as any as ServerConfig;
51
+
52
+ // Good
53
+ const config = { port: 3000 } satisfies ServerConfig;
54
+ ```
55
+
56
+ If a library boundary truly requires an unchecked cast, use `as unknown as T` with a short reason. Never leave a bare `any`.
@@ -0,0 +1,39 @@
1
+ ---
2
+ description: "Do not use `await import()` — use static imports unless dynamic loading is unavoidable"
3
+ condition: "await import\\("
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ ---
6
+
7
+ Use static imports for modules known at author time. Reach for `await import()` only when the module specifier is genuinely runtime-selected.
8
+
9
+ ## Why
10
+
11
+ - Static imports fail during build, not under load.
12
+ - Bundlers, type checkers, and tree shakers see them.
13
+ - The dependency graph remains reviewable.
14
+ - Consumers keep precise module types without casts.
15
+
16
+ ## Avoid
17
+
18
+ ```typescript
19
+ // Bad — the module path is a literal.
20
+ const { createClient } = await import("some-sdk");
21
+
22
+ // Bad — dynamic import followed by a shape assertion.
23
+ const mod = (await import("./known-module")) as { run?: unknown };
24
+ ```
25
+
26
+ ## Use
27
+
28
+ ```typescript
29
+ import { createClient } from "some-sdk";
30
+ import { run } from "./known-module";
31
+ ```
32
+
33
+ ## Exceptions
34
+
35
+ - Plugin loading from a runtime registry.
36
+ - Platform-specific modules that do not exist everywhere.
37
+ - Test cases that intentionally exercise module loading boundaries.
38
+
39
+ Exception? Add a short comment naming why static import cannot work.
@@ -0,0 +1,45 @@
1
+ ---
2
+ description: "Do not use `ReturnType<typeof fn>` — name the type explicitly"
3
+ condition: "ReturnType<"
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ ---
6
+
7
+ Do not publish contracts through `ReturnType<typeof fn>`. Name the type at the module that owns the value and import that name at consumers.
8
+
9
+ ## Why
10
+
11
+ - Named types document the contract directly.
12
+ - Consumers stop coupling to implementation helpers.
13
+ - JSDoc and changelog notes attach to the exported type.
14
+ - Type errors point at the intended API boundary.
15
+
16
+ ## Avoid
17
+
18
+ ```typescript
19
+ // Bad — opaque and coupled to implementation names.
20
+ type Config = Awaited<ReturnType<typeof loadConfig>>;
21
+ type Message = ReturnType<typeof buildMessage>["message"];
22
+ let service: ReturnType<typeof createService> | undefined;
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```typescript
28
+ // In the module that owns the function:
29
+ export interface LoadedConfig {
30
+ path: string;
31
+ values: Record<string, unknown>;
32
+ }
33
+
34
+ export function loadConfig(path: string): Promise<LoadedConfig> { ... }
35
+
36
+ // At the consumer:
37
+ import type { LoadedConfig } from "./config";
38
+ ```
39
+
40
+ ## Exceptions
41
+
42
+ - Timer handles: `ReturnType<typeof setTimeout>` / `setInterval`.
43
+ - Generic type utilities where the function is a type parameter.
44
+
45
+ Concrete function? Export a concrete type.
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: "Do not extract 1-2 line functions that only wrap an expression — inline them"
3
+ condition: "\\{\\s*return [^;{}\\n]+;?\\s*\\}|\\b(?:const|let|var)\\s+[\\w$]+\\s*=\\s*(\\([^)]*\\)|[a-zA-Z_$][\\w$]*)\\s*=>\\s*[^{\\n]+$"
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ interruptMode: never
6
+ ---
7
+
8
+ Do not extract a function whose whole body is one expression or one `return`. Inline it unless the name creates a durable contract.
9
+
10
+ ## Why
11
+
12
+ - One-line wrappers hide no real behavior.
13
+ - Readers must jump to verify trivial code.
14
+ - The signature freezes a shape too early.
15
+ - Search and type flow work better with inline expressions.
16
+
17
+ ## Avoid
18
+
19
+ ```typescript
20
+ // Bad — pure rename, no behavior added.
21
+ function isEmpty(value: string): boolean {
22
+ return value.length === 0;
23
+ }
24
+
25
+ const getDisplayName = (user: User) => user.profile.displayName;
26
+
27
+ function double(value: number) {
28
+ return value * 2;
29
+ }
30
+
31
+ if (isEmpty(name)) { ... }
32
+ ```
33
+
34
+ ## Use
35
+
36
+ ```typescript
37
+ if (name.length === 0) { ... }
38
+ const displayName = user.profile.displayName;
39
+ const doubled = value * 2;
40
+ ```
41
+
42
+ ## Allowed tiny functions
43
+
44
+ - Three or more call sites need lockstep behavior.
45
+ - Exported name represents a stable domain concept.
46
+ - Callback identity matters.
47
+ - Type guard preserves narrowing.
48
+ - Public API, test seam, or DI boundary needs indirection.
49
+
50
+ If none apply, inline it.
@@ -0,0 +1,65 @@
1
+ ---
2
+ description: Use Promise.withResolvers() instead of new Promise() constructor
3
+ condition: "new Promise\\("
4
+ scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
+ ---
6
+
7
+ Use `Promise.withResolvers()` instead of `new Promise((resolve, reject) => ...)`. It keeps control flow linear and exposes typed resolver functions without callback nesting.
8
+
9
+ ## Basic operation
10
+
11
+ ```typescript
12
+ // Bad
13
+ function delay(ms: number): Promise<void> {
14
+ return new Promise(resolve => {
15
+ setTimeout(resolve, ms);
16
+ });
17
+ }
18
+
19
+ // Good
20
+ function delay(ms: number): Promise<void> {
21
+ const { promise, resolve } = Promise.withResolvers<void>();
22
+ setTimeout(resolve, ms);
23
+ return promise;
24
+ }
25
+ ```
26
+
27
+ ## Event-based completion
28
+
29
+ ```typescript
30
+ // Bad
31
+ function waitForEvent(emitter: EventEmitter, event: string): Promise<unknown> {
32
+ return new Promise((resolve, reject) => {
33
+ emitter.once(event, resolve);
34
+ emitter.once("error", reject);
35
+ });
36
+ }
37
+
38
+ // Good
39
+ function waitForEvent(emitter: EventEmitter, event: string): Promise<unknown> {
40
+ const { promise, resolve, reject } = Promise.withResolvers<unknown>();
41
+ emitter.once(event, resolve);
42
+ emitter.once("error", reject);
43
+ return promise;
44
+ }
45
+ ```
46
+
47
+ ## Stored resolver
48
+
49
+ ```typescript
50
+ class Gate {
51
+ #promise: Promise<void>;
52
+ #resolve: () => void;
53
+
54
+ constructor() {
55
+ const { promise, resolve } = Promise.withResolvers<void>();
56
+ this.#promise = promise;
57
+ this.#resolve = resolve;
58
+ }
59
+
60
+ open(): void { this.#resolve(); }
61
+ wait(): Promise<void> { return this.#promise; }
62
+ }
63
+ ```
64
+
65
+ Use the constructor only when an API specifically requires the executor form.
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: Prefer Record<K, V> for small static literals; use Set/Map for anything dynamic
3
+ condition: "\\bnew\\s+(Set|Map)\\b"
4
+ scope: "tool:edit(**/*.{ts,tsx}), tool:write(**/*.{ts,tsx})"
5
+ interruptMode: never
6
+ ---
7
+
8
+ Use `Record<K, V>` / `Record<K, true>` for small, static string-keyed lookup tables.
9
+
10
+ Use `Set` / `Map` when keys are dynamic, non-string, inserted or deleted at runtime, or when code needs `.size`, `.clear()`, stable insertion order, or iterator APIs.
11
+
12
+ ```typescript
13
+ // Static literal → Record
14
+ const LABEL_BY_KIND: Record<string, string> = {
15
+ text: "Text",
16
+ json: "JSON",
17
+ binary: "Binary",
18
+ };
19
+
20
+ // Dynamic membership → Set
21
+ const seen = new Set<string>();
22
+ for (const item of items) {
23
+ if (seen.has(item.id)) continue;
24
+ seen.add(item.id);
25
+ }
26
+ ```
27
+
28
+ Small fixed table? `Record`. Runtime collection? `Set` / `Map`.
@@ -22,6 +22,7 @@ import "../capability/tool";
22
22
  // Import providers (each registers itself on import)
23
23
  import "./agents-md";
24
24
  import "./builtin";
25
+ import "./builtin-defaults";
25
26
  import "./claude";
26
27
  import "./claude-plugins";
27
28
  import "./cline";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tree-sitter-backed {@link BlockResolver} for the hashline `replace block N:`
3
+ * operator. Bridges the pure hashline seam to the native `blockRangeAt`
4
+ * primitive in `@oh-my-pi/pi-natives`, which infers the language from the file
5
+ * path and returns the 1-indexed line span of the syntactic block beginning on
6
+ * the requested line (or `null` when none can be resolved).
7
+ */
8
+ import type { BlockResolver } from "@oh-my-pi/hashline";
9
+ import { blockRangeAt } from "@oh-my-pi/pi-natives";
10
+
11
+ export const nativeBlockResolver: BlockResolver = ({ path, text, line }) => {
12
+ const range = blockRangeAt({ code: text, path, line });
13
+ return range ? { start: range.startLine, end: range.endLine } : null;
14
+ };
@@ -22,6 +22,7 @@ import {
22
22
  import { resolveToCwd } from "../../tools/path-utils";
23
23
  import { generateDiffString } from "../diff";
24
24
  import { readEditFileText } from "../read-file";
25
+ import { nativeBlockResolver } from "./block-resolver";
25
26
 
26
27
  export interface HashlineDiffOptions {
27
28
  /**
@@ -74,7 +75,9 @@ export async function computeHashlineSectionDiff(
74
75
  const normalized = normalizeToLF(content);
75
76
  const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
76
77
  if (hashError) return { error: hashError };
77
- const result = options.streaming ? section.applyPartialTo(normalized) : section.applyTo(normalized);
78
+ const result = options.streaming
79
+ ? section.applyPartialTo(normalized, nativeBlockResolver)
80
+ : section.applyTo(normalized, nativeBlockResolver);
78
81
  if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
79
82
  return generateDiffString(normalized, result.text);
80
83
  } catch (err) {
@@ -25,6 +25,7 @@ import { outputMeta } from "../../tools/output-meta";
25
25
  import { generateDiffString } from "../diff";
26
26
  import { getFileSnapshotStore } from "../file-snapshot-store";
27
27
  import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
28
+ import { nativeBlockResolver } from "./block-resolver";
28
29
  import { HashlineFilesystem } from "./filesystem";
29
30
  import { type HashlineParams, hashlineEditParamsSchema } from "./params";
30
31
 
@@ -133,7 +134,7 @@ export async function executeHashlineSingle(
133
134
  batchRequest: options.batchRequest,
134
135
  });
135
136
  const snapshots = getFileSnapshotStore(options.session);
136
- const patcher = new Patcher({ fs, snapshots });
137
+ const patcher = new Patcher({ fs, snapshots, blockResolver: nativeBlockResolver });
137
138
 
138
139
  // Single-section fast path: prepare, commit, render.
139
140
  if (patch.sections.length === 1) {
@@ -1,3 +1,4 @@
1
+ export * from "./block-resolver";
1
2
  export * from "./diff";
2
3
  export * from "./execute";
3
4
  export * from "./filesystem";
@@ -17,7 +17,7 @@ import { Settings } from "../../config/settings";
17
17
  import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
18
18
  import { PYTHON_PRELUDE } from "./prelude";
19
19
  import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
20
- import { filterEnv, resolvePythonRuntime } from "./runtime";
20
+ import { enumeratePythonRuntimes, filterEnv, type PythonRuntime, resolvePythonRuntime } from "./runtime";
21
21
 
22
22
  export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
23
23
  export { renderKernelDisplay } from "./display";
@@ -106,6 +106,8 @@ export interface PythonKernelAvailability {
106
106
  ok: boolean;
107
107
  pythonPath?: string;
108
108
  reason?: string;
109
+ /** The probed-working runtime, when one was found. */
110
+ runtime?: PythonRuntime;
109
111
  }
110
112
 
111
113
  function getRemainingTimeMs(deadlineMs?: number): number | undefined {
@@ -134,19 +136,34 @@ export async function checkPythonKernelAvailability(cwd: string): Promise<Python
134
136
  const settings = await Settings.init();
135
137
  const { env } = settings.getShellConfig();
136
138
  const baseEnv = filterEnv(env);
137
- const runtime = resolvePythonRuntime(cwd, baseEnv);
138
- const probe = await $`${runtime.pythonPath} -c "import sys;sys.exit(0)"`
139
- .quiet()
140
- .nothrow()
141
- .cwd(cwd)
142
- .env(runtime.env);
143
- if (probe.exitCode === 0) {
144
- return { ok: true, pythonPath: runtime.pythonPath };
139
+ const runtimes = enumeratePythonRuntimes(cwd, baseEnv);
140
+ if (runtimes.length === 0) {
141
+ return { ok: false, reason: "Python executable not found on PATH" };
142
+ }
143
+ // Probe each candidate in priority order and use the first that actually
144
+ // runs. A managed env left behind by a removed `uv` install can exist on
145
+ // disk yet fail to execute; falling through to the next candidate lets a
146
+ // working system Python take over instead of failing the whole session.
147
+ const failures: string[] = [];
148
+ for (const runtime of runtimes) {
149
+ try {
150
+ const probe = await $`${runtime.pythonPath} -c "import sys;sys.exit(0)"`
151
+ .quiet()
152
+ .nothrow()
153
+ .cwd(cwd)
154
+ .env(runtime.env);
155
+ if (probe.exitCode === 0) {
156
+ return { ok: true, pythonPath: runtime.pythonPath, runtime };
157
+ }
158
+ failures.push(`${runtime.pythonPath} (exit code ${probe.exitCode})`);
159
+ } catch (err) {
160
+ failures.push(`${runtime.pythonPath} (${err instanceof Error ? err.message : String(err)})`);
161
+ }
145
162
  }
146
163
  return {
147
164
  ok: false,
148
- pythonPath: runtime.pythonPath,
149
- reason: `Python interpreter at ${runtime.pythonPath} returned exit code ${probe.exitCode}`,
165
+ pythonPath: runtimes[0].pythonPath,
166
+ reason: `No working Python interpreter found. Tried: ${failures.join("; ")}`,
150
167
  };
151
168
  } catch (err) {
152
169
  return { ok: false, reason: err instanceof Error ? err.message : String(err) };
@@ -207,10 +224,15 @@ export class PythonKernel {
207
224
  throw new Error(availability.reason ?? "Python kernel unavailable");
208
225
  }
209
226
 
210
- const settings = await Settings.init();
211
- const { env: shellEnv } = settings.getShellConfig();
212
- const baseEnv = filterEnv(shellEnv);
213
- const runtime = resolvePythonRuntime(options.cwd, baseEnv);
227
+ // Reuse the interpreter the availability probe selected so the spawned
228
+ // kernel matches what we verified actually runs. The fallback computes a
229
+ // runtime only for the skip-check fast path (test runtime /
230
+ // PI_PYTHON_SKIP_CHECK), where no candidate was probed.
231
+ let runtime = availability.runtime;
232
+ if (!runtime) {
233
+ const { env: shellEnv } = (await Settings.init()).getShellConfig();
234
+ runtime = resolvePythonRuntime(options.cwd, filterEnv(shellEnv));
235
+ }
214
236
  const spawnEnv: Record<string, string> = {};
215
237
  for (const [key, value] of Object.entries(runtime.env)) {
216
238
  if (typeof value === "string") spawnEnv[key] = value;
@@ -162,49 +162,78 @@ export function resolveVenvPath(cwd: string): string | undefined {
162
162
  }
163
163
 
164
164
  /**
165
- * Resolve Python runtime including executable path, environment, and venv detection.
165
+ * Apply a venv-style PATH/VIRTUAL_ENV layout onto a fresh copy of `baseEnv` for
166
+ * the interpreter living in `binDir`.
166
167
  */
167
- export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime {
168
+ function applyVenvEnv(
169
+ baseEnv: Record<string, string | undefined>,
170
+ venvPath: string,
171
+ binDir: string,
172
+ ): Record<string, string | undefined> {
168
173
  const env = { ...baseEnv };
169
- const venvPath = env.VIRTUAL_ENV ?? resolveVenvPath(cwd);
174
+ env.VIRTUAL_ENV = venvPath;
175
+ const pathKey = resolvePathKey(env);
176
+ const currentPath = env[pathKey];
177
+ env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
178
+ return env;
179
+ }
180
+
181
+ function venvBinDir(venvPath: string): string {
182
+ return process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
183
+ }
184
+
185
+ /**
186
+ * Enumerate candidate Python runtimes in priority order: an active/project venv,
187
+ * the managed `~/.omp/python-env`, then the system interpreter on PATH. Every
188
+ * candidate that physically exists is returned so callers can probe each in turn
189
+ * rather than committing to the first — a managed env left behind by a removed
190
+ * `uv` install no longer shadows a working system Python.
191
+ */
192
+ export function enumeratePythonRuntimes(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime[] {
193
+ const runtimes: PythonRuntime[] = [];
194
+ const seen = new Set<string>();
195
+ const push = (runtime: PythonRuntime): void => {
196
+ if (seen.has(runtime.pythonPath)) return;
197
+ seen.add(runtime.pythonPath);
198
+ runtimes.push(runtime);
199
+ };
170
200
 
201
+ const venvPath = baseEnv.VIRTUAL_ENV ?? resolveVenvPath(cwd);
171
202
  if (venvPath) {
172
- env.VIRTUAL_ENV = venvPath;
173
- const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
203
+ const binDir = venvBinDir(venvPath);
174
204
  const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
175
205
  if (fs.existsSync(pythonCandidate)) {
176
- const pathKey = resolvePathKey(env);
177
- const currentPath = env[pathKey];
178
- env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
179
- return {
180
- pythonPath: pythonCandidate,
181
- env,
182
- venvPath,
183
- };
206
+ push({ pythonPath: pythonCandidate, env: applyVenvEnv(baseEnv, venvPath, binDir), venvPath });
184
207
  }
185
208
  }
186
209
 
187
210
  const managed = resolveManagedPythonCandidate();
188
211
  if (fs.existsSync(managed.pythonPath)) {
189
- env.VIRTUAL_ENV = managed.venvPath;
190
- const pathKey = resolvePathKey(env);
191
- const currentPath = env[pathKey];
192
- const managedBin =
193
- process.platform === "win32" ? path.join(managed.venvPath, "Scripts") : path.join(managed.venvPath, "bin");
194
- env[pathKey] = currentPath ? `${managedBin}${path.delimiter}${currentPath}` : managedBin;
195
- return {
212
+ const managedBin = path.dirname(managed.pythonPath);
213
+ push({
196
214
  pythonPath: managed.pythonPath,
197
- env,
215
+ env: applyVenvEnv(baseEnv, managed.venvPath, managedBin),
198
216
  venvPath: managed.venvPath,
199
- };
217
+ });
200
218
  }
201
219
 
202
- const pythonPath = $which("python") ?? $which("python3");
203
- if (!pythonPath) {
220
+ const systemPath = $which("python") ?? $which("python3");
221
+ if (systemPath) {
222
+ push({ pythonPath: systemPath, env: { ...baseEnv } });
223
+ }
224
+
225
+ return runtimes;
226
+ }
227
+
228
+ /**
229
+ * Resolve the highest-priority Python runtime. Prefer {@link enumeratePythonRuntimes}
230
+ * when you can probe candidates; this returns only the first one and throws when
231
+ * no interpreter exists.
232
+ */
233
+ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime {
234
+ const [runtime] = enumeratePythonRuntimes(cwd, baseEnv);
235
+ if (!runtime) {
204
236
  throw new Error("Python executable not found on PATH");
205
237
  }
206
- return {
207
- pythonPath,
208
- env,
209
- };
238
+ return runtime;
210
239
  }