@oh-my-pi/pi-coding-agent 14.9.7 → 14.9.9

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.
package/CHANGELOG.md CHANGED
@@ -1,22 +1,58 @@
1
1
  # Changelog
2
2
 
3
3
  ## [Unreleased]
4
+
5
+ ## [14.9.9] - 2026-05-12
6
+
7
+ ### Added
8
+
9
+ - Added new `task.isolation.mode` values `auto`, `apfs`, `btrfs`, `zfs`, `reflink`, `overlayfs`, `projfs`, `block-clone`, and `rcopy` for native PAL-backed task isolation backends
10
+ - Added automatic PAL-backed isolation backend selection so `task.isolation.mode` uses the host's best-available backend
11
+ - Added input-token and output-token totals to `omp stats --summary`.
12
+
13
+ ### Changed
14
+
15
+ - Changed `task.isolation.enabled=true` migration to map to `task.isolation.mode = "auto"` instead of legacy `worktree` isolation
16
+ - Updated isolation configuration UI labels and descriptions to expose new back-end names (`overlayfs`, `projfs`, etc.) and removed references to deprecated values in guidance text
17
+
18
+ ### Fixed
19
+
20
+ - Fixed worktree delta capture to include previously untracked file state by baselining untracked patches for both snapshots
21
+ - Fixed task isolation startup to try alternate PAL backends when the preferred one is unavailable, allowing successful fallback instead of immediate failure
22
+ - Mapped legacy `task.isolation.mode` values `worktree`, `fuse-overlay`, and `fuse-projfs` to their new equivalents during settings migration to preserve behavior with older configs
23
+
24
+ ## [14.9.8] - 2026-05-12
25
+
4
26
  ### Breaking Changes
5
27
 
6
28
  - Changed the `eval` tool input format to a single-line `*** Cell <lang>:"<title>" [t:<duration>] [rst]` header per cell, replacing the `*** Begin <LANG>` / `*** End <LANG>` envelope and the standalone `*** Title:` / `*** Timeout:` / `*** Reset` directives. The lark grammar enforces a fixed attribute order; the runtime parser remains lenient (alias keys, bare positional tokens, single-quoted titles).
7
29
 
8
30
  ### Added
9
31
 
32
+ - Added `:conflicts` read selector (`read <path>:conflicts`) to return a one-line index of all unresolved merge conflicts with stable `#N` IDs for quick inspection
33
+ - Added bulk conflict resolution with `write({ path: "conflict://*", content })` to resolve all currently registered conflicts across files in one call, expanding `@ours`/`@theirs`/`@base`/`@both` per conflict and returning per-file counts
34
+ - Added `read` support for `conflict://<N>` and `read conflict://<N>/<scope>` to inspect unresolved conflict regions captured by a prior read, including `ours`, `theirs`, and `base` side views with original file line alignment
35
+ - Added shorthand content tokens `@ours`, `@theirs`, `@both`, and `@base` to conflict-resolution writes using `path: "conflict://<N>"` so replacement content can be composed from recorded conflict sections
36
+ - Added conflict count metadata to read results so conflict files now show a warning badge (`⚠ N`) in the read tool UI
10
37
  - Added support for explicit boolean `rst` values (`rst:true`, `rst:false`, `rst:1`, `rst:0`, `rst:yes`, `rst:no`, `rst:on`, `rst:off`) in `*** Cell` headers
38
+ - Added detection of unresolved git merge conflicts in `read` output: each marker block is registered with a session-stable id and surfaced in a footer with `ours`/`theirs` previews. Resolve a block by calling `write({ path: "conflict://<id>", content })` — the tool splices the recorded marker region (markers and all sides) with the supplied content and routes through the normal writethrough (LSP format/diagnostics, fs-cache invalidation).
11
39
 
12
40
  ### Changed
13
41
 
42
+ - Changed `read` conflict warning footers to show `X of Y` unresolved conflicts when a range only captures part of a file and provide a `read <path>:conflicts` hint for the full list
43
+ - Changed conflict scanning in conflict read paths to inspect the whole file (with a 10 MB cap) so totals better reflect hidden conflicts and truncated scans are called out
44
+ - Changed conflict marker scanning during `read` to only register fully formed, column-0 merge-marker blocks, so indented or malformed marker-like lines are no longer treated as conflicts
45
+ - Changed `write` conflict resolution to validate `conflict://` IDs and report clear errors for malformed or unknown conflict URIs
14
46
  - Changed the HTML transcript renderer to parse the new `*** Cell` headers while keeping the older `*** Begin <LANG>` and `===== ... =====` formats renderable for historical sessions.
15
47
  - Changed the `eval` tool parser so a stray non-marker line between cells no longer crashes with `null is not an object (evaluating 'BEGIN_RE.exec(lines[i])[1]')`; stray content is consumed without aborting parsing.
16
48
  - Changed `*** End` to be an optional, undocumented per-cell terminator (kept in the lark to satisfy GPT-trained models' natural terminator habit during constrained sampling).
17
49
 
18
50
  ### Fixed
19
51
 
52
+ - Fixed single-conflict `write` retries to re-locate the recorded conflict block by exact marker content so shifted line numbers from out-of-band edits no longer prevent resolution
53
+ - Fixed `read conflict://*` handling by rejecting wildcard reads with a clear write-only guidance error
54
+ - Fixed conflict resolution to verify the live file still contains recorded `<<<<<<<` and `>>>>>>>` markers before splicing, preventing stale conflict IDs from silently corrupting out-of-band-edited files
55
+ - Fixed `@base` token handling so two-way conflicts without a base section now return a clear error
20
56
  - Improved `*** Cell` header parsing to reject invalid `rst` values with a clear `invalid rst value` error
21
57
 
22
58
  ## [14.9.7] - 2026-05-12
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.9.7",
4
+ "version": "14.9.9",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "14.9.7",
51
- "@oh-my-pi/pi-agent-core": "14.9.7",
52
- "@oh-my-pi/pi-ai": "14.9.7",
53
- "@oh-my-pi/pi-natives": "14.9.7",
54
- "@oh-my-pi/pi-tui": "14.9.7",
55
- "@oh-my-pi/pi-utils": "14.9.7",
50
+ "@oh-my-pi/omp-stats": "14.9.9",
51
+ "@oh-my-pi/pi-agent-core": "14.9.9",
52
+ "@oh-my-pi/pi-ai": "14.9.9",
53
+ "@oh-my-pi/pi-natives": "14.9.9",
54
+ "@oh-my-pi/pi-tui": "14.9.9",
55
+ "@oh-my-pi/pi-utils": "14.9.9",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@sinclair/typebox": "^0.34.49",
58
58
  "@types/turndown": "5.0.6",
@@ -40,6 +40,17 @@ async function main(): Promise<void> {
40
40
  "--root",
41
41
  "../..",
42
42
  "./src/cli.ts",
43
+ // Worker entrypoints. Bun's `--compile` discovers the literal in
44
+ // `new Worker("…", …)` at each spawn site, but only actually
45
+ // emits the worker into the bunfs root when it is listed here as
46
+ // an explicit additional entry. Paths are relative to this
47
+ // script's cwd (packages/coding-agent) and the `--root` above
48
+ // (../..) makes them appear inside the binary at
49
+ // `/$bunfs/root/packages/<pkg>/src/<worker>.js`, which is
50
+ // exactly what the literals at the spawn sites resolve to.
51
+ "../stats/src/sync-worker.ts",
52
+ "./src/tools/browser/tab-worker-entry.ts",
53
+ "./src/eval/js/worker-entry.ts",
43
54
  "--outfile",
44
55
  "dist/omp",
45
56
  ],
@@ -18,10 +18,11 @@ const minifiedCss = css
18
18
  .replace(/\s*([{}:;,])\s*/g, "$1")
19
19
  .trim();
20
20
 
21
- // Inline everything
21
+ // Inline everything; use function replacements so `$'`, `$&`, `$$`, etc. inside
22
+ // the embedded CSS/JS are not interpreted as substitution patterns.
22
23
  const template = html
23
- .replace("<template-css/>", `<style>${minifiedCss}</style>`)
24
- .replace("<template-js/>", `<script>${js}</script>`);
24
+ .replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
25
+ .replace("<template-js/>", () => `<script>${js}</script>`);
25
26
 
26
27
  // Write generated file
27
28
  const output = `// Auto-generated by scripts/generate-template.ts - DO NOT EDIT
@@ -176,6 +176,8 @@ async function printStatsSummary(): Promise<void> {
176
176
  console.log(` Requests: ${formatNumber(overall.totalRequests)} (${formatNumber(overall.failedRequests)} errors)`);
177
177
  console.log(` Error Rate: ${formatPercent(overall.errorRate)}`);
178
178
  console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
179
+ console.log(` Input Tokens: ${formatNumber(overall.totalInputTokens)}`);
180
+ console.log(` Output Tokens: ${formatNumber(overall.totalOutputTokens)}`);
179
181
  console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
180
182
  console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
181
183
  console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`);
package/src/cli.ts CHANGED
@@ -84,8 +84,30 @@ function isSubcommand(first: string | undefined): boolean {
84
84
  return commands.some(e => e.name === first || e.aliases?.includes(first));
85
85
  }
86
86
 
87
+ /**
88
+ * Smoke-test entry. Spawns the stats sync worker, pings it, exits.
89
+ *
90
+ * Purpose: catch the silent worker-load regressions that hit compiled
91
+ * binaries (issues #1011 and #1027). Neither `--version` nor
92
+ * `stats --summary` actually spawns a Worker on a fresh install — the
93
+ * sync path early-returns when no session files exist. This probe is the
94
+ * minimal end-to-end test that proves `new Worker(...)` resolves and the
95
+ * bundled worker module evaluates successfully. Wired into
96
+ * `scripts/install-tests/run-ci.sh` so binary / source-link / tarball
97
+ * installs all exercise it on every CI run.
98
+ */
99
+ async function runSmokeTest(): Promise<void> {
100
+ const { smokeTestSyncWorker } = await import("@oh-my-pi/omp-stats");
101
+ await smokeTestSyncWorker();
102
+ process.stdout.write("smoke-test: ok\n");
103
+ }
104
+
87
105
  /** Run the CLI with the given argv (no `process.argv` prefix). */
88
- export function runCli(argv: string[]): Promise<void> {
106
+ export async function runCli(argv: string[]): Promise<void> {
107
+ if (argv[0] === "--smoke-test") {
108
+ await runSmokeTest();
109
+ return;
110
+ }
89
111
  // --help and --version are handled by run() directly, don't rewrite those.
90
112
  // Everything else that isn't a known subcommand routes to "launch".
91
113
  const first = argv[0];
@@ -2067,25 +2067,46 @@ export const SETTINGS_SCHEMA = {
2067
2067
  // Delegation
2068
2068
  "task.isolation.mode": {
2069
2069
  type: "enum",
2070
- values: ["none", "worktree", "fuse-overlay", "fuse-projfs"] as const,
2070
+ values: [
2071
+ "none",
2072
+ "auto",
2073
+ "apfs",
2074
+ "btrfs",
2075
+ "zfs",
2076
+ "reflink",
2077
+ "overlayfs",
2078
+ "projfs",
2079
+ "block-clone",
2080
+ "rcopy",
2081
+ ] as const,
2071
2082
  default: "none",
2072
2083
  ui: {
2073
2084
  tab: "tasks",
2074
2085
  label: "Isolation Mode",
2075
2086
  description:
2076
- "Isolation mode for subagents (none, git worktree, fuse-overlayfs on Unix, or ProjFS on Windows via fuse-projfs; unsupported modes fall back to worktree)",
2087
+ 'Isolation backend for subagents. "auto" lets the native PAL pick the best available backend (CoW-aware filesystems, then overlayfs/ProjFS, then a git worktree / recursive-copy fallback).',
2077
2088
  options: [
2078
2089
  { value: "none", label: "None", description: "No isolation" },
2079
- { value: "worktree", label: "Worktree", description: "Git worktree isolation" },
2090
+ { value: "auto", label: "Auto", description: "Let the PAL pick the best available backend" },
2091
+ { value: "apfs", label: "APFS", description: "macOS clonefile reflink (APFS)" },
2092
+ { value: "btrfs", label: "btrfs", description: "btrfs subvolume snapshot" },
2093
+ { value: "zfs", label: "ZFS", description: "ZFS snapshot + clone" },
2094
+ { value: "reflink", label: "Reflink", description: "Linux FICLONE per-file reflink" },
2095
+ {
2096
+ value: "overlayfs",
2097
+ label: "Overlayfs",
2098
+ description: "Linux kernel overlay (or fuse-overlayfs fallback)",
2099
+ },
2100
+ { value: "projfs", label: "ProjFS", description: "Windows Projected File System" },
2080
2101
  {
2081
- value: "fuse-overlay",
2082
- label: "Fuse Overlay",
2083
- description: "COW overlay via fuse-overlayfs (Unix only)",
2102
+ value: "block-clone",
2103
+ label: "Block clone",
2104
+ description: "Windows FSCTL_DUPLICATE_EXTENTS_TO_FILE (NTFS/ReFS)",
2084
2105
  },
2085
2106
  {
2086
- value: "fuse-projfs",
2087
- label: "Fuse ProjFS",
2088
- description: "COW overlay via ProjFS (Windows only; falls back to worktree if unavailable)",
2107
+ value: "rcopy",
2108
+ label: "Recursive copy",
2109
+ description: "git worktree if available, otherwise recursive copy",
2089
2110
  },
2090
2111
  ],
2091
2112
  },
@@ -598,11 +598,28 @@ export class Settings {
598
598
  const isolationObj = taskObj?.isolation as Record<string, unknown> | undefined;
599
599
  if (isolationObj && "enabled" in isolationObj) {
600
600
  if (typeof isolationObj.enabled === "boolean") {
601
- isolationObj.mode = isolationObj.enabled ? "worktree" : "none";
601
+ isolationObj.mode = isolationObj.enabled ? "auto" : "none";
602
602
  }
603
603
  delete isolationObj.enabled;
604
604
  }
605
605
 
606
+ // task.isolation.mode: legacy values from before the pi-iso PAL refactor.
607
+ // `worktree` was git worktree → now lives under `rcopy`. `fuse-overlay`
608
+ // and `fuse-projfs` are now the platform-named `overlayfs` / `projfs`
609
+ // kinds; the PAL falls back internally when the chosen one isn't
610
+ // available, so we don't need the old TS-side platform guards.
611
+ if (isolationObj && typeof isolationObj.mode === "string") {
612
+ const legacy: Record<string, string> = {
613
+ worktree: "rcopy",
614
+ "fuse-overlay": "overlayfs",
615
+ "fuse-projfs": "projfs",
616
+ };
617
+ const mapped = legacy[isolationObj.mode as string];
618
+ if (mapped !== undefined) {
619
+ isolationObj.mode = mapped;
620
+ }
621
+ }
622
+
606
623
  // edit.mode: removed "atom" variant is now "hashline"
607
624
  const editObj = raw.edit as Record<string, unknown> | undefined;
608
625
  if (editObj) {
@@ -287,7 +287,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
287
287
  sections = splitHashlineInputs(args.input, { cwd: ctx.cwd, path: args.path });
288
288
  } catch {
289
289
  // Single-section fallback keeps the original error rendering for the
290
- // "haven't typed `@PATH` yet" case.
290
+ // "haven't typed `@@ PATH` yet" case.
291
291
  const result = await computeHashlineDiff({ input: args.input, path: args.path }, ctx.cwd, {
292
292
  autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
293
293
  });
@@ -1,14 +1,13 @@
1
- import { logger, Snowflake } from "@oh-my-pi/pi-utils";
1
+ import { isCompiledBinary, logger, Snowflake } from "@oh-my-pi/pi-utils";
2
2
  import type { ToolSession } from "../../tools";
3
3
  import { ToolAbortError, ToolError } from "../../tools/tool-errors";
4
4
  import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
5
5
  import { WorkerCore } from "./worker-core";
6
- // Imported with `type: "file"` so Bun's bundler statically discovers the worker entry and
7
- // embeds it inside `bun build --compile` single-file binaries. Mirrors the browser tab
8
- // worker setup; see packages/coding-agent/src/tools/browser/tab-supervisor.ts for the
9
- // rationale.
10
- // @ts-expect-error -- Bun file-URL import attribute is not modeled by tsgo.
11
- import jsWorkerEntryUrl from "./worker-entry.ts" with { type: "file" };
6
+ // Worker entry. See `tab-supervisor.ts` for the rationale behind the
7
+ // literal-string + `new URL(import.meta.url)` hybrid: the literal is what
8
+ // Bun's `--compile` bundler discovers, the `new URL` form is what makes dev
9
+ // runs portable across cwds. The worker is registered as an additional
10
+ // `--compile` entrypoint in `scripts/build-binary.ts`.
12
11
  import type {
13
12
  JsDisplayOutput,
14
13
  RunErrorPayload,
@@ -344,7 +343,9 @@ async function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, reason
344
343
 
345
344
  async function spawnJsWorker(): Promise<WorkerHandle> {
346
345
  try {
347
- const worker = new Worker(jsWorkerEntryUrl, { type: "module" });
346
+ const worker = isCompiledBinary()
347
+ ? new Worker("./packages/coding-agent/src/eval/js/worker-entry.ts", { type: "module" })
348
+ : new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" });
348
349
  return wrapBunWorker(worker);
349
350
  } catch (err) {
350
351
  logger.warn("Bun Worker spawn failed; using inline JS eval worker (no sync-loop guard)", {
@@ -103,9 +103,12 @@ async function generateHtml(sessionData: SessionData, themeName?: string): Promi
103
103
  const themeVars = await generateThemeVars(themeName);
104
104
  const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64();
105
105
 
106
- return TEMPLATE.replace("<theme-vars/>", `<style>:root { ${themeVars} }</style>`).replace(
106
+ // Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the
107
+ // substituted CSS/base64 are not interpreted as substitution patterns
108
+ // (see https://mdn.io/String.replace).
109
+ return TEMPLATE.replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`).replace(
107
110
  "{{SESSION_DATA}}",
108
- sessionDataBase64,
111
+ () => sessionDataBase64,
109
112
  );
110
113
  }
111
114