@oh-my-pi/pi-coding-agent 15.8.2 → 15.8.3

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
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.8.3] - 2026-06-03
6
+ ### Fixed
7
+
8
+ - Fixed Jujutsu workspace detection failing in non-default workspaces created by `jj workspace add`, whose `.jj/repo` is a FILE pointing at the shared repo dir rather than a directory. Detection now matches jj's own criterion (`.jj/repo` present, file or dir) instead of requiring a `.jj/repo/store` directory, and `jj.repo.resolve`'s `storeDir` follows the file indirection to the shared store.
9
+
10
+ ### Changed
11
+
12
+ - Changed the `todo-write` prompt to require initializing every item from a user-supplied multi-step plan as an individual todo task before execution
13
+ - Changed context compaction (prune/shake) to protect reads of the active plan file the same way it already protects `skill://` reads, so the plan stays intact through automatic and manual compaction. Both the canonical `local://PLAN.md` alias and the session's current plan reference path (e.g. a titled `local://<title>.md` after approval) are kept, tolerating read selectors and `local:/` scheme spelling.
14
+
5
15
  ## [15.8.2] - 2026-06-03
6
16
 
7
17
  ### Added
@@ -9241,4 +9251,4 @@ Initial public release.
9241
9251
  - Git branch display in footer
9242
9252
  - Message queueing during streaming responses
9243
9253
  - OAuth integration for Gmail and Google Calendar access
9244
- - HTML export with syntax highlighting and collapsible sections
9254
+ - HTML export with syntax highlighting and collapsible sections
@@ -0,0 +1,12 @@
1
+ import { type ProtectedToolContext } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
2
+ /**
3
+ * Build a compaction protection matcher that keeps `read` results for the active
4
+ * plan file intact through prune/shake — the plan analog of skill-read
5
+ * protection. Matches both the canonical `local://PLAN.md` alias and the
6
+ * session's current plan reference path (e.g. a titled `local://<title>.md`), so
7
+ * the plan survives compaction whether the agent reads it by alias or by title.
8
+ *
9
+ * `getPlanReferencePath` is evaluated at match time so a mid-session retitle
10
+ * (plan approval renames `PLAN.md` → `<title>.md`) is honored immediately.
11
+ */
12
+ export declare function createPlanReadMatcher(getPlanReferencePath: () => string): (context: ProtectedToolContext) => boolean;
@@ -11,7 +11,7 @@ export interface JjCommandResult {
11
11
  export interface JjRepository {
12
12
  /** Root directory containing the `.jj` workspace metadata. */
13
13
  repoRoot: string;
14
- /** Path to the workspace store directory used to verify a real JJ checkout. */
14
+ /** Path to the shared workspace store directory, resolved through `.jj/repo`'s file indirection for non-default workspaces. */
15
15
  storeDir: string;
16
16
  }
17
17
  /** Options for `jj diff` invocations. */
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": "15.8.2",
4
+ "version": "15.8.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,14 +47,14 @@
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.8.2",
51
- "@oh-my-pi/omp-stats": "15.8.2",
52
- "@oh-my-pi/pi-agent-core": "15.8.2",
53
- "@oh-my-pi/pi-ai": "15.8.2",
54
- "@oh-my-pi/pi-mnemopi": "15.8.2",
55
- "@oh-my-pi/pi-natives": "15.8.2",
56
- "@oh-my-pi/pi-tui": "15.8.2",
57
- "@oh-my-pi/pi-utils": "15.8.2",
50
+ "@oh-my-pi/hashline": "15.8.3",
51
+ "@oh-my-pi/omp-stats": "15.8.3",
52
+ "@oh-my-pi/pi-agent-core": "15.8.3",
53
+ "@oh-my-pi/pi-ai": "15.8.3",
54
+ "@oh-my-pi/pi-mnemopi": "15.8.3",
55
+ "@oh-my-pi/pi-natives": "15.8.3",
56
+ "@oh-my-pi/pi-tui": "15.8.3",
57
+ "@oh-my-pi/pi-utils": "15.8.3",
58
58
  "@puppeteer/browsers": "^3.0.4",
59
59
  "@types/turndown": "5.0.6",
60
60
  "@xterm/headless": "^6.0.0",
@@ -0,0 +1,31 @@
1
+ import { getReadToolPath, type ProtectedToolContext } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
2
+ import { normalizeLocalScheme } from "../tools/path-utils";
3
+
4
+ /** Canonical plan alias every session's `local://` root resolves. */
5
+ const LOCAL_PLAN_ALIAS = "local://PLAN.md";
6
+
7
+ /** True when `readPath` targets `planTarget`, ignoring `local:/` vs `local://`
8
+ * scheme spelling and any trailing read selector (`:1-50`, `:raw`, …). */
9
+ function readTargetsPlan(readPath: string, planTarget: string): boolean {
10
+ const read = normalizeLocalScheme(readPath);
11
+ const target = normalizeLocalScheme(planTarget);
12
+ return read === target || read.startsWith(`${target}:`);
13
+ }
14
+
15
+ /**
16
+ * Build a compaction protection matcher that keeps `read` results for the active
17
+ * plan file intact through prune/shake — the plan analog of skill-read
18
+ * protection. Matches both the canonical `local://PLAN.md` alias and the
19
+ * session's current plan reference path (e.g. a titled `local://<title>.md`), so
20
+ * the plan survives compaction whether the agent reads it by alias or by title.
21
+ *
22
+ * `getPlanReferencePath` is evaluated at match time so a mid-session retitle
23
+ * (plan approval renames `PLAN.md` → `<title>.md`) is honored immediately.
24
+ */
25
+ export function createPlanReadMatcher(getPlanReferencePath: () => string): (context: ProtectedToolContext) => boolean {
26
+ return (context: ProtectedToolContext) => {
27
+ const path = getReadToolPath(context);
28
+ if (path === undefined) return false;
29
+ return readTargetsPlan(path, LOCAL_PLAN_ALIAS) || readTargetsPlan(path, getPlanReferencePath());
30
+ };
31
+ }
@@ -48,3 +48,11 @@ Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, an
48
48
  # Append tasks to a phase
49
49
  `{"ops":[{"op":"append","phase":"Auth","items":["Handle retries","Run tests"]}]}`
50
50
  </examples>
51
+
52
+ <critical>
53
+ When the user hands you a multi-step plan — a phased todo, a numbered or bulleted checklist, or "N bugs/items/tasks" to work through:
54
+ - You MUST `init` the list with EVERY item as its own task before doing the work.
55
+ - Enumerate all of them;
56
+ - NEVER summarize the plan into fewer tasks, sample "the important ones", drop items, or rely on memory to track the rest.
57
+ The entire point is to remember every one.
58
+ </critical>
@@ -55,6 +55,7 @@ import {
55
55
  shouldCompact,
56
56
  } from "@oh-my-pi/pi-agent-core/compaction";
57
57
  import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@oh-my-pi/pi-agent-core/compaction/pruning";
58
+ import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
58
59
  import type {
59
60
  AssistantMessage,
60
61
  Context,
@@ -163,6 +164,7 @@ import { parseTurnBudget } from "../modes/turn-budget";
163
164
  import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
164
165
  import { computeNonMessageTokens } from "../modes/utils/context-usage";
165
166
  import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
167
+ import { createPlanReadMatcher } from "../plan-mode/plan-protection";
166
168
  import type { PlanModeState } from "../plan-mode/state";
167
169
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
168
170
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
@@ -5658,9 +5660,20 @@ export class AgentSession {
5658
5660
  // Compaction
5659
5661
  // =========================================================================
5660
5662
 
5663
+ /**
5664
+ * Append plan-read protection to a prune/shake config so the active plan
5665
+ * file survives compaction alongside skill reads (the config defaults
5666
+ * already carry skill protection). The matcher reads the current plan
5667
+ * reference path at match time, so retitled plans are covered.
5668
+ */
5669
+ #withPlanProtection<T extends { protectedTools: ProtectedToolMatcher[] }>(config: T): T {
5670
+ const planMatcher = createPlanReadMatcher(() => this.#planReferencePath);
5671
+ return { ...config, protectedTools: [...config.protectedTools, planMatcher] };
5672
+ }
5673
+
5661
5674
  async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
5662
5675
  const branchEntries = this.sessionManager.getBranch();
5663
- const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
5676
+ const result = pruneToolOutputs(branchEntries, this.#withPlanProtection(DEFAULT_PRUNE_CONFIG));
5664
5677
  if (result.prunedCount === 0) {
5665
5678
  return undefined;
5666
5679
  }
@@ -5741,7 +5754,7 @@ export class AgentSession {
5741
5754
  return { mode, toolResultsDropped: 0, blocksDropped: 0, imagesDropped: removed, tokensFreed: 0 };
5742
5755
  }
5743
5756
 
5744
- const config = opts.config ?? AGGRESSIVE_SHAKE_CONFIG;
5757
+ const config = this.#withPlanProtection(opts.config ?? AGGRESSIVE_SHAKE_CONFIG);
5745
5758
  const regions = collectShakeRegions(this.sessionManager.getBranch(), config);
5746
5759
  if (regions.length === 0) {
5747
5760
  return { mode, toolResultsDropped: 0, blocksDropped: 0, tokensFreed: 0 };
package/src/utils/jj.ts CHANGED
@@ -21,7 +21,7 @@ export interface JjCommandResult {
21
21
  export interface JjRepository {
22
22
  /** Root directory containing the `.jj` workspace metadata. */
23
23
  repoRoot: string;
24
- /** Path to the workspace store directory used to verify a real JJ checkout. */
24
+ /** Path to the shared workspace store directory, resolved through `.jj/repo`'s file indirection for non-default workspaces. */
25
25
  storeDir: string;
26
26
  }
27
27
 
@@ -146,8 +146,14 @@ const WORKSPACE_ROOT_CACHE_MAX_ENTRIES = 256;
146
146
  const workspaceRootCache = new LRUCache<string, WorkspaceRootCacheEntry>({ max: WORKSPACE_ROOT_CACHE_MAX_ENTRIES });
147
147
 
148
148
  async function hasJjWorkspaceMetadata(dir: string): Promise<boolean> {
149
+ // jj marks a directory as a workspace via `.jj/repo`. In the default workspace
150
+ // it is a directory (containing `store/`, …); in a workspace created by
151
+ // `jj workspace add` it is a FILE whose contents point at the shared repo dir
152
+ // of the default workspace. Either form is a real workspace, so match on
153
+ // `.jj/repo` presence rather than the inner `store/` directory.
149
154
  try {
150
- return (await fs.stat(path.join(dir, ".jj", "repo", "store"))).isDirectory();
155
+ await fs.stat(path.join(dir, ".jj", "repo"));
156
+ return true;
151
157
  } catch {
152
158
  return false;
153
159
  }
@@ -173,10 +179,27 @@ async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
173
179
  return undefined;
174
180
  }
175
181
 
176
- function repositoryFromRoot(root: string): JjRepository {
182
+ /**
183
+ * Resolve the `.jj/repo` directory backing a workspace root, following the file
184
+ * indirection used by non-default workspaces. `jj workspace add` writes a FILE at
185
+ * `.jj/repo` whose contents are a path — relative to `.jj` — to the shared repo
186
+ * directory of the default workspace; the default workspace keeps `.jj/repo` as a
187
+ * directory.
188
+ */
189
+ async function resolveRepoDir(root: string): Promise<string> {
190
+ const jjDir = path.join(root, ".jj");
191
+ const repoPath = path.join(jjDir, "repo");
192
+ if ((await fs.stat(repoPath)).isFile()) {
193
+ const target = (await fs.readFile(repoPath, "utf8")).trim();
194
+ return path.resolve(jjDir, target);
195
+ }
196
+ return repoPath;
197
+ }
198
+
199
+ async function repositoryFromRoot(root: string): Promise<JjRepository> {
177
200
  return {
178
201
  repoRoot: root,
179
- storeDir: path.join(root, ".jj", "repo", "store"),
202
+ storeDir: path.join(await resolveRepoDir(root), "store"),
180
203
  };
181
204
  }
182
205
 
@@ -215,7 +238,7 @@ export const repo = {
215
238
  /** Full Jujutsu workspace metadata. */
216
239
  async resolve(cwd: string): Promise<JjRepository | null> {
217
240
  const root = await repo.root(cwd);
218
- return root ? repositoryFromRoot(root) : null;
241
+ return root ? await repositoryFromRoot(root) : null;
219
242
  },
220
243
 
221
244
  /** Check whether `cwd` is inside a Jujutsu repository. */