@cad0p/pi-tree-navigator 0.1.0 → 0.1.1-20260809.0

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,41 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.1] - 2026-07-31
6
+
7
+ <!-- USER-EDITABLE SECTION START -->
8
+ Patch release: restores the mid-loop context refresh on pi ≥0.80.3. No behavior change on pi ≤0.80.2.
9
+
10
+ **The bug (pi ≥0.80.3):** pi 0.80.3 added `AgentSession._installAgentNextTurnRefresh()`, which installs pi's own `agent.prepareNextTurnWithContext` in the constructor, and pi-agent-core's `Agent.createLoopConfig` now prefers that field over `agent.prepareNextTurn`. Since this extension only wrapped `prepareNextTurn`, its mid-loop context replacement was dead code: after a `rewind`, the branch summary landed correctly, but every remaining turn of the same loop still sent the full pre-rewind context to the API, and the footer's context-% re-anchored on that stale usage (jumping back up right after the rewind). Rewinds only actually saved context on the *next* user prompt.
11
+
12
+ **The fix:** `installPrepareNextTurn` now wraps both hook fields with the same marker/`__prior` chaining discipline. On pi ≥0.80.3 the `prepareNextTurnWithContext` wrapper chains pi's own (keeping its per-turn `systemPrompt`/`tools`/`model`/`thinkingLevel` refreshes) and overrides only `messages`; on pi ≤0.80.2 the new field is never read and `prepareNextTurn` does the work as before.
13
+
14
+ Verified live on pi 0.83.0 (persisted session): after a rewind at 31.5% context, the footer stays at ~1.6% for the rest of the loop (previously bounced back to ~33.5%), and the post-rewind API call goes out with ~3.7k tokens instead of ~80.5k.
15
+ <!-- USER-EDITABLE SECTION END -->
16
+
17
+ ### 🚀 Features
18
+
19
+ - Discriminated-union schema makes summaryFocus required at the wire level ([#1](https://github.com/cad0p/pi-tree-navigator/pull/1))
20
+
21
+ ### 🐛 Bug Fixes
22
+
23
+ - Revert discriminated-union parameters — Kiro rejects non-object root schemas ([#2](https://github.com/cad0p/pi-tree-navigator/pull/2))
24
+ - Wrap prepareNextTurnWithContext — in-loop context refresh dead since pi 0.80.3 ([#8](https://github.com/cad0p/pi-tree-navigator/pull/8))
25
+
26
+ ### 🚜 Refactor
27
+
28
+ - Nest extension under extensions/navigate-tree/ per pi-napkin convention
29
+
30
+ ### 📚 Documentation
31
+
32
+ - Promote npm install and publishing ([#4](https://github.com/cad0p/pi-tree-navigator/pull/4))
33
+
34
+ ### ⚙️ Miscellaneous Tasks
35
+
36
+ - Release-grade cleanup for v0.1.0 ([#3](https://github.com/cad0p/pi-tree-navigator/pull/3))
37
+ - Switch from bun to node + pnpm for local dev and CI ([#6](https://github.com/cad0p/pi-tree-navigator/pull/6))
38
+
39
+
5
40
  ## [0.1.0] - 2026-05-25
6
41
 
7
42
  <!-- USER-EDITABLE SECTION START -->
package/README.md CHANGED
@@ -6,28 +6,31 @@ Lets a pi agent anchor named milestones in its own conversation, then collapse w
6
6
 
7
7
  ## Install
8
8
 
9
+ Stable npm release:
10
+
9
11
  ```bash
10
- pi install git:github.com/cad0p/pi-tree-navigator
12
+ pi install npm:@cad0p/pi-tree-navigator
11
13
  ```
12
14
 
13
- > **Status:** v0.1.0 is not yet on the npm registry (pending OIDC trusted-publisher setup). Install from the git source for now.
15
+ Pre-release npm snapshots from `main` are published with the `next` dist-tag:
14
16
 
15
- <details>
16
- <summary>Once published / pre-release installs</summary>
17
+ ```bash
18
+ pi install npm:@cad0p/pi-tree-navigator@next
19
+ ```
17
20
 
18
- - Stable (once published to npm):
21
+ You can also install directly from the git source when testing unreleased branches:
19
22
 
20
- ```bash
21
- pi install npm:@cad0p/pi-tree-navigator
22
- ```
23
+ ```bash
24
+ pi install git:github.com/cad0p/pi-tree-navigator
25
+ ```
23
26
 
24
- - Pre-release (calver snapshots from `main`, published to npm `@next` on every push):
27
+ ## Publishing
25
28
 
26
- ```bash
27
- pi install npm:@cad0p/pi-tree-navigator@next
28
- ```
29
+ This repo uses [`cad0p/semver-calver-release`](https://github.com/cad0p/semver-calver-release)'s npm-package workflow:
29
30
 
30
- </details>
31
+ - Pushes to `main` compute the next hybrid SemVer + CalVer version, tag a GitHub prerelease, and publish to npm with the `next` dist-tag.
32
+ - Curated release PRs from `release/from-v*` branches bump the base `package.json` version and publish stable npm releases.
33
+ - npm publishing uses GitHub OIDC / npm trusted publishing via `.github/workflows/release.yml` (`id-token: write`) and `publishConfig.access: public`.
31
34
 
32
35
  ### Requirements
33
36
 
@@ -36,7 +39,7 @@ pi install git:github.com/cad0p/pi-tree-navigator
36
39
  - `@earendil-works/pi-coding-agent >=0.74.0`
37
40
  - `@earendil-works/pi-agent-core >=0.74.0`
38
41
  - `typebox ^1.0.0` (used to declare the tool's parameter schema; bundled with pi but listed explicitly so a standalone install resolves correctly).
39
- - The reflection bootstrap depends on five plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, and `agent.prepareNextTurn`. Verified against pi 0.75.x.
42
+ - The reflection bootstrap depends on six plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, `agent.prepareNextTurn` (pi ≤0.80.2), and `agent.prepareNextTurnWithContext` (preferred by pi ≥0.80.3). Verified against pi 0.75.5 and pi 0.83.0.
40
43
 
41
44
  ## What you get
42
45
 
@@ -90,7 +93,7 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
90
93
 
91
94
  ## Limitations
92
95
 
93
- - **Brittle to pi version bumps.** The fix uses five independent reflection points on internals that aren't part of pi's public API: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, and `agent.prepareNextTurn`. If a future pi release renames any of these, switches them to private (`#`) fields, or restructures the class hierarchy, this breaks. The extension fails loudly: `anchor` still works, `rewind` reports `⚠ reflection bootstrap missing — the rewind landed on disk but the next assistant turn may still see the pre-rewind context. Run \`/reload\` (or restart pi) to recover.`, and you'd see context corruption return on the next prompt.
96
+ - **Brittle to pi version bumps.** The fix uses six independent reflection points on internals that aren't part of pi's public API: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, `agent.prepareNextTurn`, and `agent.prepareNextTurnWithContext`. This is not hypothetical: pi 0.80.3 added `prepareNextTurnWithContext` and made the loop prefer it, silently dead-ending the `prepareNextTurn`-only hook until v0.1.1. If a future pi release renames any of these, switches them to private (`#`) fields, or restructures the class hierarchy, this breaks. The extension fails loudly: `anchor` still works, `rewind` reports `⚠ reflection bootstrap missing — the rewind landed on disk but the next assistant turn may still see the pre-rewind context. Run \`/reload\` (or restart pi) to recover.`, and you'd see context corruption return on the next prompt.
94
97
 
95
98
  - **Anchor early in the turn.** Whatever's in `agent.state.messages` *before* the `anchor` tool call stays in the kept chain. Everything after gets summarized. Anchor at the *start* of a stage for maximum context savings.
96
99
 
@@ -107,13 +110,13 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
107
110
  ## Development
108
111
 
109
112
  ```bash
110
- bun install
111
- bun test # helpers + dispatch / reflection bootstrap / salvage path
112
- bunx biome check extensions/
113
- bunx tsc --noEmit
113
+ pnpm install
114
+ pnpm test # helpers + dispatch / reflection bootstrap / salvage path
115
+ pnpm run lint # biome check extensions/
116
+ pnpm run typecheck # tsc --noEmit
114
117
  ```
115
118
 
116
- Tests cover `extensions/navigate-tree/helpers.ts` (pure helpers in `helpers.test.ts`) and `extensions/navigate-tree/index.ts` (action dispatch, schema shape, synthetic-assistant injection, reflection bootstrap, salvage path — in `index.test.ts`). The `summarize` factory option injects a stub for `generateBranchSummary` so no real LLM call fires during rewind tests. Additional manual e2e validation against pi 0.75.x is recommended for any pi version bump (the reflection bootstrap depends on internal field shapes).
119
+ Tests cover `extensions/navigate-tree/helpers.ts` (pure helpers in `helpers.test.ts`) and `extensions/navigate-tree/index.ts` (action dispatch, schema shape, synthetic-assistant injection, reflection bootstrap, salvage path — in `index.test.ts`). The `summarize` factory option injects a stub for `generateBranchSummary` so no real LLM call fires during rewind tests. Additional manual e2e validation against the current pi release (0.83.x at time of writing) is recommended for any pi version bump (the reflection bootstrap depends on internal field shapes).
117
120
 
118
121
  ## License
119
122
 
@@ -22,18 +22,20 @@
22
22
  * this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
23
23
  *
24
24
  * Risks of the reflection approach:
25
- * • If pi switches any of the five fields this extension reads —
25
+ * • If pi switches any of the six fields this extension reads —
26
26
  * `AgentSession.prototype.prompt`, `agent.state.messages`,
27
- * `agent.state.systemPrompt`, `agent.state.tools`, or
28
- * `agent.prepareNextTurn` — to ES `#` private fields, this breaks
29
- * fundamentally.
27
+ * `agent.state.systemPrompt`, `agent.state.tools`,
28
+ * `agent.prepareNextTurn`, or `agent.prepareNextTurnWithContext` —
29
+ * to ES `#` private fields, this breaks fundamentally.
30
30
  * • If pi renames or restructures any of these fields, this breaks.
31
31
  * • Patches `AgentSession.prototype.prompt` globally on import; not
32
32
  * reversible without a process restart; affects every session in
33
33
  * the pi process, including sessions that never call
34
34
  * `navigate_tree`.
35
35
  *
36
- * Verified against pi 0.75.5.
36
+ * Verified against pi 0.75.5 (`prepareNextTurn` path) and pi 0.83.0
37
+ * (`prepareNextTurnWithContext` path — required since pi 0.80.3, see
38
+ * `installPrepareNextTurn`).
37
39
  */
38
40
 
39
41
  import { estimateContextTokens } from "@earendil-works/pi-agent-core";
@@ -116,6 +118,7 @@ const LIST_PCT_COL_WIDTH = 5;
116
118
  // hint column visible without truncating common names.
117
119
  const LIST_LABEL_COL_WIDTH = 28;
118
120
  const PNT_MARKER = Symbol.for("navigate-tree.pnt-installed");
121
+ const PNTWC_MARKER = Symbol.for("navigate-tree.pntwc-installed");
119
122
  const ORIG_PROMPT_KEY = Symbol.for("navigate-tree.orig-prompt");
120
123
 
121
124
  // Two warnings: list-site (read-only path; warns about the next turn's
@@ -144,6 +147,10 @@ interface PiInternals {
144
147
  tools: unknown[];
145
148
  };
146
149
  prepareNextTurn?: unknown;
150
+ // Preferred over `prepareNextTurn` by `Agent.createLoopConfig`
151
+ // when set; pi's own AgentSession always sets it (via
152
+ // `_installAgentNextTurnRefresh`) since pi-coding-agent 0.80.3.
153
+ prepareNextTurnWithContext?: unknown;
147
154
  };
148
155
  sessionManager: SessionManager;
149
156
  }
@@ -163,14 +170,22 @@ type PntResult = {
163
170
  thinkingLevel?: unknown;
164
171
  };
165
172
  // pi 0.75.5 invokes `agent.prepareNextTurn(signal)` from
166
- // `Agent.createLoopConfig` — a single AbortSignal argument. This differs
167
- // from the documented `AgentLoopConfig.prepareNextTurn(context: PrepareNextTurnContext)`
168
- // shape, which `Agent` is bridging. We accept whatever pi passes and forward
169
- // it verbatim to the prior wrapper so we don't fight a future signature
170
- // alignment. Verified against pi-coding-agent 0.75.5; revisit if the call
171
- // site changes.
173
+ // `Agent.createLoopConfig` — a single AbortSignal argument. Since pi
174
+ // 0.80.3 the loop instead invokes
175
+ // `agent.prepareNextTurnWithContext(nextTurnContext, signal)` (and only
176
+ // falls back to `prepareNextTurn` when the WithContext field is unset).
177
+ // Both differ from the documented
178
+ // `AgentLoopConfig.prepareNextTurn(context: PrepareNextTurnContext)`
179
+ // shape, which `Agent` is bridging. We accept whatever pi passes and
180
+ // forward it verbatim to the prior wrapper so we don't fight a future
181
+ // signature alignment. Verified against pi-coding-agent 0.75.5 and
182
+ // 0.83.0; revisit if the call site changes.
172
183
  type PntFn = (...args: unknown[]) => Promise<PntResult> | PntResult;
173
- type MarkedPntFn = PntFn & { [PNT_MARKER]?: boolean; __prior?: PntFn };
184
+ type MarkedPntFn = PntFn & {
185
+ [PNT_MARKER]?: boolean;
186
+ [PNTWC_MARKER]?: boolean;
187
+ __prior?: PntFn;
188
+ };
174
189
 
175
190
  // =============================================================================
176
191
  // Reflection bootstrap & in-loop refresh
@@ -218,17 +233,32 @@ function patchAgentSessionPrototype(): void {
218
233
  }
219
234
 
220
235
  /**
221
- * Wire `agent.prepareNextTurn` so the in-flight agent loop refreshes its
222
- * context from sessionManager between turns within the same prompt() call.
223
- * Without this, the loop snapshots agent.state.messages once at prompt start
224
- * and pushes new messages onto its own array — a rewind issued mid-loop
225
- * doesn't reduce the next API call's size until the user sends a new prompt.
236
+ * Wire the in-flight agent loop to refresh its context from sessionManager
237
+ * between turns within the same prompt() call. Without this, the loop
238
+ * snapshots agent.state.messages once at prompt start and pushes new
239
+ * messages onto its own array — a rewind issued mid-loop doesn't reduce
240
+ * the next API call's size until the user sends a new prompt.
241
+ *
242
+ * Two hook fields, by pi version:
243
+ * • pi ≤0.80.2: `Agent.createLoopConfig` reads `agent.prepareNextTurn`.
244
+ * • pi ≥0.80.3: AgentSession's constructor installs its own
245
+ * `agent.prepareNextTurnWithContext` (`_installAgentNextTurnRefresh`),
246
+ * and `createLoopConfig` prefers that field over `prepareNextTurn`.
247
+ * Pi's wrapper refreshes systemPrompt/tools/model/thinkingLevel per
248
+ * turn but leaves `messages` stale — so wrapping only
249
+ * `prepareNextTurn` silently dead-ends (footer % and the wire context
250
+ * stay pre-rewind for the rest of the loop).
251
+ *
252
+ * We install on BOTH fields, each with the same marker/__prior chaining
253
+ * discipline: on new pi the WithContext wrapper chains pi's own (keeping
254
+ * its per-turn refreshes) and overrides only `messages`; on old pi the
255
+ * WithContext field is never read and `prepareNextTurn` does the work.
226
256
  *
227
- * The Agent class's `createLoopConfig` dereferences `this.prepareNextTurn`
228
- * at the closure call site, so the value here is read at every turn boundary.
229
- * But it gates the closure on `this.prepareNextTurn` being truthy at config
230
- * creation — so we have to set this BEFORE prompt() runs, hence wiring it
231
- * from inside the prompt patch.
257
+ * The Agent class's `createLoopConfig` dereferences both fields at the
258
+ * closure call site, so the values here are read at every turn boundary.
259
+ * But it gates the closure on either field being truthy at config
260
+ * creation — so we have to set this BEFORE prompt() runs, hence wiring
261
+ * it from inside the prompt patch.
232
262
  */
233
263
  function installPrepareNextTurn(session: AgentSession): void {
234
264
  const internals = asInternals(session);
@@ -237,15 +267,22 @@ function installPrepareNextTurn(session: AgentSession): void {
237
267
 
238
268
  const sm = internals.sessionManager;
239
269
 
240
- // If the existing prepareNextTurn was installed by a previous load of THIS
241
- // extension, recover the chain it captured (its `__prior`) so we don't
242
- // strand other extensions' closures across /reload. Preserve any other
243
- // extension's prepareNextTurn so we compose with them.
270
+ // If the existing hooks were installed by a previous load of THIS
271
+ // extension, recover the chains they captured (their `__prior`) so we
272
+ // don't strand other extensions' closures across /reload. Preserve any
273
+ // other extension's (or pi's own) hooks so we compose with them.
244
274
  const existing = agent.prepareNextTurn as MarkedPntFn | undefined;
245
275
  const prior: PntFn | undefined =
246
276
  typeof existing === "function" && existing[PNT_MARKER]
247
277
  ? existing.__prior
248
278
  : (existing as PntFn | undefined);
279
+ const existingWc = agent.prepareNextTurnWithContext as
280
+ | MarkedPntFn
281
+ | undefined;
282
+ const priorWc: PntFn | undefined =
283
+ typeof existingWc === "function" && existingWc[PNTWC_MARKER]
284
+ ? existingWc.__prior
285
+ : (existingWc as PntFn | undefined);
249
286
 
250
287
  const next: MarkedPntFn = async (...args: unknown[]) => {
251
288
  let priorResult: PntResult | undefined;
@@ -273,6 +310,40 @@ function installPrepareNextTurn(session: AgentSession): void {
273
310
  next[PNT_MARKER] = true;
274
311
  next.__prior = prior;
275
312
  agent.prepareNextTurn = next;
313
+
314
+ const nextWc: MarkedPntFn = async (...args: unknown[]) => {
315
+ // On pi ≥0.80.3 the loop calls this as (nextTurnContext, signal),
316
+ // where nextTurnContext = { message, toolResults, context,
317
+ // newMessages }. Forward args verbatim to the prior (pi's own
318
+ // wrapper expects exactly this shape).
319
+ const turn = args[0] as { context?: PntResult["context"] } | undefined;
320
+ let priorResult: PntResult | undefined;
321
+ if (typeof priorWc === "function") {
322
+ priorResult = await priorWc(...args);
323
+ }
324
+ // Same wholesale-replacement rationale as the prepareNextTurn
325
+ // wrapper above. When no prior result exists, fall back to the
326
+ // turn's live context (mirrors pi's own wrapper, which does
327
+ // `previousSnapshot?.context ?? turn.context`) so systemPrompt and
328
+ // tools are never dropped. `messages` is owned by this wrapper —
329
+ // this override is the entire point of the hook on pi ≥0.80.3, whose
330
+ // own wrapper refreshes every other field but leaves messages stale.
331
+ const priorContext = priorResult?.context ?? turn?.context;
332
+ return {
333
+ ...priorResult,
334
+ context: {
335
+ ...priorContext,
336
+ systemPrompt: priorContext?.systemPrompt ?? agent.state.systemPrompt,
337
+ tools: priorContext?.tools ?? agent.state.tools,
338
+ messages: sm.buildSessionContext().messages,
339
+ },
340
+ model: priorResult?.model,
341
+ thinkingLevel: priorResult?.thinkingLevel,
342
+ };
343
+ };
344
+ nextWc[PNTWC_MARKER] = true;
345
+ nextWc.__prior = priorWc;
346
+ agent.prepareNextTurnWithContext = nextWc;
276
347
  }
277
348
 
278
349
  function findOwningSession(sm: SessionManager): AgentSession | null {
@@ -934,8 +1005,11 @@ export const __testHooks = {
934
1005
  installPrepareNextTurn,
935
1006
  refreshAgentMessages,
936
1007
  captureSession,
937
- /** Symbol used to mark the wrapper installed by `installPrepareNextTurn`. */
1008
+ /** Symbols used to mark the wrappers installed by `installPrepareNextTurn`
1009
+ * (`PNT_MARKER` on `agent.prepareNextTurn`, `PNTWC_MARKER` on
1010
+ * `agent.prepareNextTurnWithContext` — the field pi ≥0.80.3 prefers). */
938
1011
  PNT_MARKER,
1012
+ PNTWC_MARKER,
939
1013
  /** Read-only view of captured-session ref count for reaping assertions. */
940
1014
  sessionRefCount(): number {
941
1015
  return sessionInstances.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cad0p/pi-tree-navigator",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-20260809.0",
4
4
  "description": "agent-callable session tree navigation for pi: anchor named milestones, rewind work into a branch summary, free up context for long autonomous sessions",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -36,14 +36,14 @@
36
36
  ]
37
37
  },
38
38
  "scripts": {
39
- "lint": "bunx biome check extensions/",
40
- "lint:fix": "bunx biome check --write extensions/",
41
- "typecheck": "bunx tsc --noEmit",
42
- "test": "bun test"
39
+ "lint": "biome check extensions/",
40
+ "lint:fix": "biome check --write extensions/",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "node --test extensions/**/*.test.ts"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.3.14",
46
- "@types/bun": "^1.3.0",
46
+ "@types/node": "^22.0.0",
47
47
  "typescript": "^5.8.0"
48
48
  },
49
49
  "peerDependencies": {