@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 +11 -1
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/utils/jj.d.ts +1 -1
- package/package.json +9 -9
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/prompts/tools/todo-write.md +8 -0
- package/src/session/agent-session.ts +15 -2
- package/src/utils/jj.ts +28 -5
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;
|
package/dist/types/utils/jj.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.8.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.8.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.8.
|
|
54
|
-
"@oh-my-pi/pi-mnemopi": "15.8.
|
|
55
|
-
"@oh-my-pi/pi-natives": "15.8.
|
|
56
|
-
"@oh-my-pi/pi-tui": "15.8.
|
|
57
|
-
"@oh-my-pi/pi-utils": "15.8.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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. */
|