@mariozechner/pi-coding-agent 0.27.8 → 0.27.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/docs/hooks-v2.md CHANGED
@@ -1,314 +1,385 @@
1
- # Hooks v2: Commands + Context Control
1
+ # Hooks v2: Context Control + Commands
2
2
 
3
- Extends hooks with slash commands and context manipulation primitives.
3
+ Issue: #289
4
4
 
5
- ## Goals
5
+ ## Motivation
6
6
 
7
- 1. Hooks can register slash commands (`/pop`, `/pr`, `/test`)
8
- 2. Hooks can save custom session entries
9
- 3. Hooks can transform context before it goes to LLM
10
- 4. All handlers get unified baseline access to state
7
+ Enable features like session stacking (`/pop`) as hooks, not core code. Core provides primitives, hooks implement features.
11
8
 
12
- Benchmark: `/pop` (session stacking) implementable entirely as a hook.
9
+ ## Primitives
13
10
 
14
- ## API Extensions
11
+ | Primitive | Purpose |
12
+ |-----------|---------|
13
+ | `ctx.saveEntry({type, ...})` | Persist custom entry to session |
14
+ | `pi.on("context", handler)` | Transform messages before LLM |
15
+ | `ctx.rebuildContext()` | Trigger context rebuild |
16
+ | `pi.command(name, opts)` | Register slash command |
15
17
 
16
- ### Commands
18
+ ## Extended HookEventContext
17
19
 
18
20
  ```typescript
19
- pi.command("pop", {
20
- description: "Pop to previous turn",
21
- handler: async (ctx) => {
22
- // ctx has full access (see Unified Context below)
23
- const selected = await ctx.ui.select("Pop to:", options);
24
- // ...
25
- return { status: "Done" }; // show status
26
- return "prompt text"; // send to agent
27
- return; // do nothing
28
- }
29
- });
30
- ```
21
+ interface HookEventContext {
22
+ // Existing
23
+ exec, ui, hasUI, cwd, sessionFile
24
+
25
+ // State (read-only)
26
+ model: Model<any> | null;
27
+ thinkingLevel: ThinkingLevel;
28
+ entries: readonly SessionEntry[];
31
29
 
32
- ### Custom Entries
30
+ // Utilities
31
+ findModel(provider: string, id: string): Model<any> | null;
32
+ availableModels(): Promise<Model<any>[]>;
33
+ resolveApiKey(model: Model<any>): Promise<string | undefined>;
33
34
 
34
- ```typescript
35
- // Save arbitrary entry to session
36
- await ctx.saveEntry({
37
- type: "stack_pop", // custom type, ignored by core
38
- backToIndex: 5,
39
- summary: "...",
40
- timestamp: Date.now()
41
- });
35
+ // Mutation
36
+ saveEntry(entry: { type: string; [k: string]: unknown }): Promise<void>;
37
+ rebuildContext(): Promise<void>;
38
+ }
39
+
40
+ interface ContextMessage {
41
+ message: AppMessage;
42
+ entryIndex: number | null; // null = synthetic
43
+ }
44
+
45
+ interface ContextEvent {
46
+ type: "context";
47
+ entries: readonly SessionEntry[];
48
+ messages: ContextMessage[];
49
+ }
42
50
  ```
43
51
 
44
- ### Context Transform
52
+ Commands also get: `args`, `argsRaw`, `signal`, `setModel()`, `setThinkingLevel()`.
53
+
54
+ ## Stacking: Design
55
+
56
+ ### Entry Format
45
57
 
46
58
  ```typescript
47
- // Fires when building context for LLM
48
- pi.on("context", (event, ctx) => {
49
- // event.entries: all session entries (including custom types)
50
- // event.messages: core-computed messages (after compaction)
51
-
52
- // Return modified messages, or undefined to keep default
53
- return { messages: transformed };
54
- });
59
+ interface StackPopEntry {
60
+ type: "stack_pop";
61
+ backToIndex: number;
62
+ summary: string;
63
+ prePopSummary?: string; // when crossing compaction
64
+ timestamp: number;
65
+ }
55
66
  ```
56
67
 
57
- Multiple `context` handlers chain: each receives previous handler's output.
68
+ ### Crossing Compaction
58
69
 
59
- ### Rebuild Trigger
70
+ Entries are never deleted. Raw data always available.
71
+
72
+ When `backToIndex < compaction.firstKeptEntryIndex`:
73
+ 1. Read raw entries `[0, backToIndex)` → summarize → `prePopSummary`
74
+ 2. Read raw entries `[backToIndex, now)` → summarize → `summary`
75
+
76
+ ### Context Algorithm: Later Wins
77
+
78
+ Assign sequential IDs to ranges. On overlap, highest ID wins.
60
79
 
61
- ```typescript
62
- // Force context rebuild (after saving entries)
63
- await ctx.rebuildContext();
64
80
  ```
81
+ Compaction at 40: range [0, 30) id=0
82
+ StackPop at 50, backTo=20, prePopSummary: ranges [0, 20) id=1, [20, 50) id=2
65
83
 
66
- ## Unified Context
84
+ Index 0-19: id=0 and id=1 cover → id=1 wins (prePopSummary)
85
+ Index 20-29: id=0 and id=2 cover → id=2 wins (popSummary)
86
+ Index 30-49: id=2 covers → id=2 (already emitted at 20)
87
+ Index 50+: no coverage → include as messages
88
+ ```
67
89
 
68
- All handlers receive:
90
+ ## Complex Scenario Trace
69
91
 
70
- ```typescript
71
- interface HookEventContext {
72
- // Existing
73
- exec(cmd: string, args: string[], opts?): Promise<ExecResult>;
74
- ui: { select, confirm, input, notify };
75
- hasUI: boolean;
76
- cwd: string;
77
- sessionFile: string | null;
78
-
79
- // New: State (read-only)
80
- model: Model<any> | null;
81
- thinkingLevel: ThinkingLevel;
82
- entries: readonly SessionEntry[];
83
- messages: readonly AppMessage[];
92
+ ```
93
+ Initial: [msg1, msg2, msg3, msg4, msg5]
94
+ idx: 1, 2, 3, 4, 5
95
+
96
+ Compaction triggers:
97
+ [msg1-5, compaction{firstKept:4, summary:C1}]
98
+ idx: 1-5, 6
99
+ Context: [C1, msg4, msg5]
100
+
101
+ User continues:
102
+ [..., compaction, msg4, msg5, msg6, msg7]
103
+ idx: 6, 4*, 5*, 7, 8 (* kept from before)
84
104
 
85
- // New: Utilities
86
- findModel(provider: string, id: string): Model<any> | null;
87
- availableModels(): Promise<Model<any>[]>;
88
- resolveApiKey(model: Model<any>): Promise<string | undefined>;
105
+ User does /pop to msg2 (index 2):
106
+ - backTo=2 < firstKept=4 crossing!
107
+ - prePopSummary: summarize raw [0,2) → P1
108
+ - summary: summarize raw [2,8) S1
109
+ - save: stack_pop{backTo:2, summary:S1, prePopSummary:P1} at index 9
110
+
111
+ Ranges:
112
+ compaction [0,4) id=0
113
+ prePopSummary [0,2) id=1
114
+ popSummary [2,9) id=2
115
+
116
+ Context build:
117
+ idx 0: covered by id=0,1 → id=1 wins, emit P1
118
+ idx 1: covered by id=0,1 → id=1 (already emitted)
119
+ idx 2: covered by id=0,2 → id=2 wins, emit S1
120
+ idx 3-8: covered by id=0 or id=2 → id=2 (already emitted)
121
+ idx 9: stack_pop entry, skip
122
+ idx 10+: not covered, include as messages
123
+
124
+ Result: [P1, S1, msg10+]
125
+
126
+ User continues, another compaction:
127
+ [..., stack_pop, msg10, msg11, msg12, compaction{firstKept:11, summary:C2}]
128
+ idx: 9, 10, 11, 12, 13
129
+
130
+ Ranges:
131
+ compaction@6 [0,4) id=0
132
+ prePopSummary [0,2) id=1
133
+ popSummary [2,9) id=2
134
+ compaction@13 [0,11) id=3 ← this now covers previous ranges!
135
+
136
+ Context build:
137
+ idx 0-10: covered by multiple, id=3 wins → emit C2 at idx 0
138
+ idx 11+: include as messages
139
+
140
+ Result: [C2, msg11, msg12]
89
141
 
90
- // New: Mutation (commands only? or all?)
91
- saveEntry(entry: { type: string; [k: string]: unknown }): Promise<void>;
92
- rebuildContext(): Promise<void>;
93
- }
142
+ C2's summary text includes info from P1 and S1 (they were in context when C2 was generated).
94
143
  ```
95
144
 
96
- Commands additionally get:
97
- - `args: string[]`, `argsRaw: string`
98
- - `setModel()`, `setThinkingLevel()` (state mutation)
145
+ The "later wins" rule naturally handles all cases.
99
146
 
100
- ## Benchmark: Stacking as Hook
147
+ ## Core Changes
148
+
149
+ | File | Change |
150
+ |------|--------|
151
+ | `session-manager.ts` | `saveEntry()`, `buildSessionContext()` returns `ContextMessage[]` |
152
+ | `hooks/types.ts` | `ContextEvent`, `ContextMessage`, extended context, command types |
153
+ | `hooks/loader.ts` | Track commands |
154
+ | `hooks/runner.ts` | `setStateCallbacks()`, `emitContext()`, command methods |
155
+ | `agent-session.ts` | `saveEntry()`, `rebuildContext()`, state callbacks |
156
+ | `interactive-mode.ts` | Command handling, autocomplete |
157
+
158
+ ## Stacking Hook: Complete Implementation
101
159
 
102
160
  ```typescript
161
+ import { complete } from "@mariozechner/pi-ai";
162
+ import type { HookAPI, AppMessage, SessionEntry, ContextMessage } from "@mariozechner/pi-coding-agent/hooks";
163
+
103
164
  export default function(pi: HookAPI) {
104
- // Command: /pop
105
165
  pi.command("pop", {
106
- description: "Pop to previous turn, summarizing substack",
166
+ description: "Pop to previous turn, summarizing work",
107
167
  handler: async (ctx) => {
108
- // 1. Build turn list from entries
109
- const turns = ctx.entries
168
+ const entries = ctx.entries as SessionEntry[];
169
+
170
+ // Get user turns
171
+ const turns = entries
110
172
  .map((e, i) => ({ e, i }))
111
- .filter(({ e }) => e.type === "message" && e.message.role === "user")
112
- .map(({ e, i }) => ({ index: i, text: e.message.content.slice(0, 50) }));
173
+ .filter(({ e }) => e.type === "message" && (e as any).message.role === "user")
174
+ .map(({ e, i }) => ({ idx: i, text: preview((e as any).message) }));
175
+
176
+ if (turns.length < 2) return { status: "Need at least 2 turns" };
113
177
 
114
- if (!turns.length) return { status: "No turns to pop" };
178
+ // Select target (skip last turn - that's current)
179
+ const options = turns.slice(0, -1).map(t => `[${t.idx}] ${t.text}`);
180
+ const selected = ctx.args[0]
181
+ ? options.find(o => o.startsWith(`[${ctx.args[0]}]`))
182
+ : await ctx.ui.select("Pop to:", options);
115
183
 
116
- // 2. User selects
117
- const selected = await ctx.ui.select("Pop to:", turns.map(t => t.text));
118
184
  if (!selected) return;
119
- const backTo = turns.find(t => t.text === selected)!.index;
185
+ const backTo = parseInt(selected.match(/\[(\d+)\]/)![1]);
186
+
187
+ // Check compaction crossing
188
+ const compactions = entries.filter(e => e.type === "compaction") as any[];
189
+ const latestCompaction = compactions[compactions.length - 1];
190
+ const crossing = latestCompaction && backTo < latestCompaction.firstKeptEntryIndex;
120
191
 
121
- // 3. Summarize entries from backTo to now
122
- const toSummarize = ctx.entries.slice(backTo)
123
- .filter(e => e.type === "message")
124
- .map(e => e.message);
125
- const summary = await generateSummary(toSummarize, ctx);
192
+ // Generate summaries
193
+ let prePopSummary: string | undefined;
194
+ if (crossing) {
195
+ ctx.ui.notify("Crossing compaction, generating pre-pop summary...", "info");
196
+ const preMsgs = getMessages(entries.slice(0, backTo));
197
+ prePopSummary = await summarize(preMsgs, ctx, "context before this work");
198
+ }
199
+
200
+ const popMsgs = getMessages(entries.slice(backTo));
201
+ const summary = await summarize(popMsgs, ctx, "completed work");
126
202
 
127
- // 4. Save custom entry
203
+ // Save and rebuild
128
204
  await ctx.saveEntry({
129
205
  type: "stack_pop",
130
206
  backToIndex: backTo,
131
207
  summary,
132
- timestamp: Date.now()
208
+ prePopSummary,
133
209
  });
134
210
 
135
- // 5. Rebuild
136
211
  await ctx.rebuildContext();
137
- return { status: "Popped stack" };
212
+ return { status: `Popped to turn ${backTo}` };
138
213
  }
139
214
  });
140
-
141
- // Context transform: apply stack pops
215
+
142
216
  pi.on("context", (event, ctx) => {
143
- const pops = event.entries.filter(e => e.type === "stack_pop");
144
- if (!pops.length) return; // use default
217
+ const hasPops = event.entries.some(e => e.type === "stack_pop");
218
+ if (!hasPops) return;
145
219
 
146
- // Build exclusion set
147
- const excluded = new Set<number>();
148
- const summaryAt = new Map<number, string>();
220
+ // Collect ranges with IDs
221
+ let rangeId = 0;
222
+ const ranges: Array<{from: number; to: number; summary: string; id: number}> = [];
149
223
 
150
- for (const pop of pops) {
151
- const popIdx = event.entries.indexOf(pop);
152
- for (let i = pop.backToIndex; i <= popIdx; i++) excluded.add(i);
153
- summaryAt.set(pop.backToIndex, pop.summary);
224
+ for (let i = 0; i < event.entries.length; i++) {
225
+ const e = event.entries[i] as any;
226
+ if (e.type === "compaction") {
227
+ ranges.push({ from: 0, to: e.firstKeptEntryIndex, summary: e.summary, id: rangeId++ });
228
+ }
229
+ if (e.type === "stack_pop") {
230
+ if (e.prePopSummary) {
231
+ ranges.push({ from: 0, to: e.backToIndex, summary: e.prePopSummary, id: rangeId++ });
232
+ }
233
+ ranges.push({ from: e.backToIndex, to: i, summary: e.summary, id: rangeId++ });
234
+ }
154
235
  }
155
236
 
156
- // Build filtered messages
157
- const messages: AppMessage[] = [];
237
+ // Build messages
238
+ const messages: ContextMessage[] = [];
239
+ const emitted = new Set<number>();
240
+
158
241
  for (let i = 0; i < event.entries.length; i++) {
159
- if (excluded.has(i)) continue;
242
+ const covering = ranges.filter(r => r.from <= i && i < r.to);
160
243
 
161
- if (summaryAt.has(i)) {
162
- messages.push({
163
- role: "user",
164
- content: `[Subtask completed]\n\n${summaryAt.get(i)}`,
165
- timestamp: Date.now()
166
- });
244
+ if (covering.length) {
245
+ const winner = covering.reduce((a, b) => a.id > b.id ? a : b);
246
+ if (i === winner.from && !emitted.has(winner.id)) {
247
+ messages.push({
248
+ message: { role: "user", content: `[Summary]\n\n${winner.summary}`, timestamp: Date.now() } as AppMessage,
249
+ entryIndex: null
250
+ });
251
+ emitted.add(winner.id);
252
+ }
253
+ continue;
167
254
  }
168
255
 
169
256
  const e = event.entries[i];
170
- if (e.type === "message") messages.push(e.message);
257
+ if (e.type === "message") {
258
+ messages.push({ message: (e as any).message, entryIndex: i });
259
+ }
171
260
  }
172
261
 
173
262
  return { messages };
174
263
  });
175
264
  }
176
265
 
177
- async function generateSummary(messages, ctx) {
266
+ function getMessages(entries: SessionEntry[]): AppMessage[] {
267
+ return entries.filter(e => e.type === "message").map(e => (e as any).message);
268
+ }
269
+
270
+ function preview(msg: AppMessage): string {
271
+ const text = typeof msg.content === "string" ? msg.content
272
+ : (msg.content as any[]).filter(c => c.type === "text").map(c => c.text).join(" ");
273
+ return text.slice(0, 40) + (text.length > 40 ? "..." : "");
274
+ }
275
+
276
+ async function summarize(msgs: AppMessage[], ctx: any, purpose: string): Promise<string> {
178
277
  const apiKey = await ctx.resolveApiKey(ctx.model);
179
- // Call LLM for summary...
278
+ const resp = await complete(ctx.model, {
279
+ messages: [...msgs, { role: "user", content: `Summarize as "${purpose}". Be concise.`, timestamp: Date.now() }]
280
+ }, { apiKey, maxTokens: 2000, signal: ctx.signal });
281
+ return resp.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n");
180
282
  }
181
283
  ```
182
284
 
183
- ## Core Changes Required
285
+ ## Edge Cases
184
286
 
185
- ### session-manager.ts
287
+ ### Session Resumed Without Hook
186
288
 
187
- ```typescript
188
- // Allow saving arbitrary entries
189
- saveEntry(entry: { type: string; [k: string]: unknown }): void {
190
- if (!entry.type) throw new Error("Entry must have type");
191
- this.inMemoryEntries.push(entry);
192
- this._persist(entry);
193
- }
289
+ User has stacking hook, does `/pop`, saves `stack_pop` entry. Later removes hook and resumes session.
194
290
 
195
- // buildSessionContext ignores unknown types (existing behavior works)
196
- ```
291
+ **What happens:**
292
+ 1. Core loads all entries (including `stack_pop`)
293
+ 2. Core's `buildSessionContext()` ignores unknown types, returns compaction + message entries
294
+ 3. `context` event fires, but no handler processes `stack_pop`
295
+ 4. Core's messages pass through unchanged
296
+
297
+ **Result:** Messages that were "popped" return to context. The pop is effectively undone.
197
298
 
198
- ### hooks/types.ts
299
+ **Why this is OK:**
300
+ - Session file is intact, no data lost
301
+ - If compaction happened after pop, the compaction summary captured the popped state
302
+ - User removed the hook, so hook's behavior (hiding messages) is gone
303
+ - User can re-add hook to restore stacking behavior
199
304
 
305
+ **Mitigation:** Could warn on session load if unknown entry types found:
200
306
  ```typescript
201
- // New event
202
- interface ContextEvent {
203
- type: "context";
204
- entries: readonly SessionEntry[];
205
- messages: AppMessage[];
307
+ // In session load
308
+ const unknownTypes = entries
309
+ .map(e => e.type)
310
+ .filter(t => !knownTypes.has(t));
311
+ if (unknownTypes.length) {
312
+ console.warn(`Session has entries of unknown types: ${unknownTypes.join(", ")}`);
206
313
  }
314
+ ```
207
315
 
208
- // Extended base context (see Unified Context above)
316
+ ### Hook Added to Existing Session
209
317
 
210
- // Command types
211
- interface CommandOptions {
212
- description?: string;
213
- handler: (ctx: CommandContext) => Promise<CommandResult | void>;
214
- }
318
+ User has old session without stacking. Adds stacking hook, does `/pop`.
215
319
 
216
- type CommandResult =
217
- | string
218
- | { prompt: string; attachments?: Attachment[] }
219
- | { status: string };
220
- ```
320
+ **What happens:**
321
+ 1. Hook saves `stack_pop` entry
322
+ 2. `context` event fires, hook processes it
323
+ 3. Works normally
221
324
 
222
- ### hooks/loader.ts
325
+ No issue. Hook processes entries it recognizes, ignores others.
223
326
 
224
- ```typescript
225
- // Track registered commands
226
- interface LoadedHook {
227
- path: string;
228
- handlers: Map<string, Handler[]>;
229
- commands: Map<string, CommandOptions>; // NEW
230
- }
327
+ ### Multiple Hooks with Different Entry Types
231
328
 
232
- // createHookAPI adds command() method
233
- ```
329
+ Hook A handles `type_a` entries, Hook B handles `type_b` entries.
234
330
 
235
- ### hooks/runner.ts
331
+ **What happens:**
332
+ 1. `context` event chains through both hooks
333
+ 2. Each hook checks for its entry types, passes through if none found
334
+ 3. Each hook's transforms are applied in order
236
335
 
237
- ```typescript
238
- class HookRunner {
239
- // State callbacks (set by AgentSession)
240
- setStateCallbacks(cb: StateCallbacks): void;
241
-
242
- // Command invocation
243
- getCommands(): Map<string, CommandOptions>;
244
- invokeCommand(name: string, argsRaw: string): Promise<CommandResult | void>;
245
-
246
- // Context event with chaining
247
- async emitContext(entries, messages): Promise<AppMessage[]> {
248
- let result = messages;
249
- for (const hook of this.hooks) {
250
- const handlers = hook.handlers.get("context");
251
- for (const h of handlers ?? []) {
252
- const out = await h({ entries, messages: result }, this.createContext());
253
- if (out?.messages) result = out.messages;
254
- }
255
- }
256
- return result;
257
- }
258
- }
259
- ```
336
+ **Best practice:** Hooks should:
337
+ - Only process their own entry types
338
+ - Return `undefined` (pass through) if no relevant entries
339
+ - Use prefixed type names: `myhook_pop`, `myhook_prune`
260
340
 
261
- ### agent-session.ts
341
+ ### Conflicting Hooks
262
342
 
263
- ```typescript
264
- // Expose saveEntry
265
- async saveEntry(entry): Promise<void> {
266
- this.sessionManager.saveEntry(entry);
267
- }
343
+ Two hooks both try to handle the same entry type (e.g., both handle `compaction`).
268
344
 
269
- // Rebuild context
270
- async rebuildContext(): Promise<void> {
271
- const base = this.sessionManager.buildSessionContext();
272
- const entries = this.sessionManager.getEntries();
273
- const messages = await this._hookRunner.emitContext(entries, base.messages);
274
- this.agent.replaceMessages(messages);
275
- }
345
+ **What happens:**
346
+ - Later hook (project > global) wins in the chain
347
+ - Earlier hook's transform is overwritten
276
348
 
277
- // Fire context event during normal context building too
278
- ```
349
+ **Mitigation:**
350
+ - Core entry types (`compaction`, `message`, etc.) should not be overridden by hooks
351
+ - Hooks should use unique prefixed type names
352
+ - Document which types are "reserved"
279
353
 
280
- ### interactive-mode.ts
354
+ ### Session with Future Entry Types
281
355
 
282
- ```typescript
283
- // In setupEditorSubmitHandler, check hook commands
284
- const commands = this.session.hookRunner?.getCommands();
285
- if (commands?.has(commandName)) {
286
- const result = await this.session.invokeCommand(commandName, argsRaw);
287
- // Handle result...
288
- return;
289
- }
356
+ User downgrades pi version, session has entry types from newer version.
290
357
 
291
- // Add hook commands to autocomplete
292
- ```
358
+ **What happens:**
359
+ - Same as "hook removed" - unknown types ignored
360
+ - Core handles what it knows, hooks handle what they know
293
361
 
294
- ## Open Questions
362
+ **Session file is forward-compatible:** Unknown entries are preserved in file, just not processed.
295
363
 
296
- 1. **Mutation in all handlers or commands only?**
297
- - `saveEntry`/`rebuildContext` in all handlers = more power, more footguns
298
- - Commands only = safer, but limits hook creativity
299
- - Recommendation: start with commands only
364
+ ## Implementation Phases
300
365
 
301
- 2. **Context event timing**
302
- - Fire on every prompt? Or only when explicitly rebuilt?
303
- - Need to fire on session load too
304
- - Recommendation: fire whenever agent.replaceMessages is called
366
+ | Phase | Scope | LOC |
367
+ |-------|-------|-----|
368
+ | v2.0 | `saveEntry`, `context` event, `rebuildContext`, extended context | ~150 |
369
+ | v2.1 | `pi.command()`, TUI integration, autocomplete | ~200 |
370
+ | v2.2 | Example hooks, documentation | ~300 |
305
371
 
306
- 3. **Compaction interaction**
307
- - Core compaction runs first, then `context` event
308
- - Hooks can post-process compacted output
309
- - Future: compaction itself could become a replaceable hook
372
+ ## Implementation Order
310
373
 
311
- 4. **Multiple context handlers**
312
- - Chain in load order (global → project)
313
- - Each sees previous output
314
- - No explicit priority system (KISS)
374
+ 1. `ContextMessage` type, update `buildSessionContext()` return type
375
+ 2. `saveEntry()` in session-manager
376
+ 3. `context` event in runner with chaining
377
+ 4. State callbacks interface and wiring
378
+ 5. `rebuildContext()` in agent-session
379
+ 6. Manual test with simple hook
380
+ 7. Command registration in loader
381
+ 8. Command invocation in runner
382
+ 9. TUI command handling + autocomplete
383
+ 10. Stacking example hook
384
+ 11. Pruning example hook
385
+ 12. Update hooks.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.27.8",
3
+ "version": "0.27.9",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "npm run clean && npm run build"
40
40
  },
41
41
  "dependencies": {
42
- "@mariozechner/pi-agent-core": "^0.27.8",
43
- "@mariozechner/pi-ai": "^0.27.8",
44
- "@mariozechner/pi-tui": "^0.27.8",
42
+ "@mariozechner/pi-agent-core": "^0.27.9",
43
+ "@mariozechner/pi-ai": "^0.27.9",
44
+ "@mariozechner/pi-tui": "^0.27.9",
45
45
  "chalk": "^5.5.0",
46
46
  "cli-highlight": "^2.1.11",
47
47
  "diff": "^8.0.2",