@lebronj/pi-suite 0.1.19 → 0.1.20

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/README.md CHANGED
@@ -15,12 +15,12 @@ pi install npm:pi-web-access
15
15
  Or use the bootstrap script to install Pi, configure the team OpenAI-compatible endpoint, install this suite, and set up Bun + qmd for memory search:
16
16
 
17
17
  ```bash
18
- curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.17.tgz | tar -xzO package/scripts/bootstrap.sh | bash
18
+ curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.20.tgz | tar -xzO package/scripts/bootstrap.sh | bash
19
19
  ```
20
20
 
21
21
  ## What Is Included
22
22
 
23
- - Local extensions: autogoal, goal mode, pet, prompt URL widget, snake, TPS notifications.
23
+ - Local extensions: autogoal, goal mode, update_plan, pet, prompt URL widget, snake, TPS notifications.
24
24
  - Prompts: changelog audit, issue analysis, PR review, review workflow, commit workflow, wrap workflow.
25
25
  - Skills: provider checklist, Pi capability reference, image-to-editable-PPT workflow.
26
26
  - Vendored package: `@jhp/pi-memory`, including qmd search, external curator service, memory/skill-draft versioning, scoped Multica agent roots, review reminders, and local memory/skill self-evolution queues.
@@ -75,6 +75,17 @@ Behavior:
75
75
  - Subagents are optional and budgeted; worker subagents must use worktree isolation.
76
76
  - Run artifacts are written under `~/.pi/agent/workflow-runs/autogoal-<run-id>/`.
77
77
 
78
+ ## Update Plan
79
+
80
+ The `update_plan` tool gives Pi a Codex-style visible execution checklist for non-trivial tasks. It supports `init`, `start`, `done`, `drop`, `rm`, `append`, and `note`, shows active plan progress in the UI, and injects guidance to use it for 3+ step tasks or user-provided checklists.
81
+
82
+ Useful commands:
83
+
84
+ ```bash
85
+ /plan-status
86
+ /plan-clear
87
+ ```
88
+
78
89
  ## Team Model Setup
79
90
 
80
91
  The bootstrap script can be run with `curl | bash`: it reads the API key from the terminal instead of stdin. It asks for an API key and writes:
@@ -0,0 +1,364 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { Type } from "typebox";
5
+
6
+ type PlanStatus = "pending" | "in_progress" | "completed" | "abandoned";
7
+
8
+ interface PlanItem {
9
+ content: string;
10
+ status: PlanStatus;
11
+ notes?: string[];
12
+ }
13
+
14
+ interface PlanPhase {
15
+ name: string;
16
+ items: PlanItem[];
17
+ }
18
+
19
+ interface PlanDetails {
20
+ op: string;
21
+ phases: PlanPhase[];
22
+ error?: string;
23
+ }
24
+
25
+ const PLAN_TOOL_NAME = "update_plan";
26
+ const PLAN_STATE_TYPE = "update-plan-state";
27
+ const WIDGET_KEY = "update-plan";
28
+
29
+ const PlanOp = StringEnum(["list", "init", "start", "done", "drop", "rm", "append", "note"] as const);
30
+
31
+ const PlanOperation = Type.Object({
32
+ op: PlanOp,
33
+ list: Type.Optional(
34
+ Type.Array(
35
+ Type.Object({
36
+ phase: Type.String({ description: "Phase name" }),
37
+ items: Type.Array(Type.String({ description: "Task content" }), { minItems: 1 }),
38
+ }),
39
+ ),
40
+ ),
41
+ task: Type.Optional(Type.String({ description: "Exact task content" })),
42
+ phase: Type.Optional(Type.String({ description: "Exact phase name" })),
43
+ items: Type.Optional(Type.Array(Type.String({ description: "Task content" }), { minItems: 1 })),
44
+ text: Type.Optional(Type.String({ description: "Note text" })),
45
+ });
46
+
47
+ const UpdatePlanParams = Type.Object({
48
+ ops: Type.Array(PlanOperation, { minItems: 1, description: "Ordered plan operations" }),
49
+ });
50
+
51
+ type PlanOperationInput = {
52
+ op: "list" | "init" | "start" | "done" | "drop" | "rm" | "append" | "note";
53
+ list?: Array<{ phase: string; items: string[] }>;
54
+ task?: string;
55
+ phase?: string;
56
+ items?: string[];
57
+ text?: string;
58
+ };
59
+
60
+ type UpdatePlanInput = { ops: PlanOperationInput[] };
61
+
62
+ function cloneItem(item: PlanItem): PlanItem {
63
+ return {
64
+ content: item.content,
65
+ status: item.status,
66
+ ...(item.notes && item.notes.length > 0 ? { notes: [...item.notes] } : {}),
67
+ };
68
+ }
69
+
70
+ function clonePhases(phases: PlanPhase[]): PlanPhase[] {
71
+ return phases.map((phase) => ({ name: phase.name, items: phase.items.map(cloneItem) }));
72
+ }
73
+
74
+ function countItems(phases: PlanPhase[]): { total: number; done: number; open: number } {
75
+ let total = 0;
76
+ let done = 0;
77
+ let open = 0;
78
+ for (const phase of phases) {
79
+ for (const item of phase.items) {
80
+ total++;
81
+ if (item.status === "completed") done++;
82
+ if (item.status === "pending" || item.status === "in_progress") open++;
83
+ }
84
+ }
85
+ return { total, done, open };
86
+ }
87
+
88
+ function normalizeOpenTask(phases: PlanPhase[]): void {
89
+ const items = phases.flatMap((phase) => phase.items);
90
+ const active = items.filter((item) => item.status === "in_progress");
91
+ for (const item of active.slice(1)) {
92
+ item.status = "pending";
93
+ }
94
+ if (active.length > 0) return;
95
+ const next = items.find((item) => item.status === "pending");
96
+ if (next) next.status = "in_progress";
97
+ }
98
+
99
+ function findPhase(phases: PlanPhase[], name: string | undefined): PlanPhase | undefined {
100
+ if (!name) return undefined;
101
+ return phases.find((phase) => phase.name === name);
102
+ }
103
+
104
+ function findTask(phases: PlanPhase[], content: string | undefined): { phase: PlanPhase; item: PlanItem } | undefined {
105
+ if (!content) return undefined;
106
+ for (const phase of phases) {
107
+ const item = phase.items.find((candidate) => candidate.content === content);
108
+ if (item) return { phase, item };
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ function renderTextPlan(phases: PlanPhase[]): string {
114
+ if (phases.length === 0) return "No active plan.";
115
+ const lines: string[] = [];
116
+ for (const phase of phases) {
117
+ lines.push(`${phase.name}:`);
118
+ for (const item of phase.items) {
119
+ const marker = item.status === "completed" ? "[x]" : item.status === "abandoned" ? "[-]" : item.status === "in_progress" ? "[>]" : "[ ]";
120
+ lines.push(` ${marker} ${item.content}`);
121
+ if (item.status === "in_progress" && item.notes && item.notes.length > 0) {
122
+ for (const note of item.notes.slice(-2)) lines.push(` note: ${note}`);
123
+ }
124
+ }
125
+ }
126
+ const counts = countItems(phases);
127
+ lines.push(``);
128
+ lines.push(`${counts.done}/${counts.total} completed, ${counts.open} open`);
129
+ return lines.join("\n");
130
+ }
131
+
132
+ function renderWidgetLines(ctx: ExtensionContext, phases: PlanPhase[]): string[] | undefined {
133
+ if (phases.length === 0) return undefined;
134
+ const th = ctx.ui.theme;
135
+ const counts = countItems(phases);
136
+ const lines: string[] = [th.fg("accent", `Plan ${counts.done}/${counts.total}`)];
137
+ let shown = 0;
138
+ let hidden = 0;
139
+ for (const phase of phases) {
140
+ const openItems = phase.items.filter((item) => item.status === "pending" || item.status === "in_progress");
141
+ if (openItems.length === 0) continue;
142
+ if (shown >= 6) {
143
+ hidden += openItems.length;
144
+ continue;
145
+ }
146
+ lines.push(th.fg("muted", phase.name));
147
+ for (const item of openItems) {
148
+ if (shown >= 6) {
149
+ hidden++;
150
+ continue;
151
+ }
152
+ const marker = item.status === "in_progress" ? th.fg("accent", "[>]") : th.fg("muted", "[ ]");
153
+ lines.push(`${marker} ${item.content}`);
154
+ shown++;
155
+ }
156
+ }
157
+ if (hidden > 0) lines.push(th.fg("dim", `... ${hidden} more`));
158
+ if (counts.open === 0) lines.push(th.fg("success", "All plan items are closed."));
159
+ return lines;
160
+ }
161
+
162
+ function updatePlanUi(ctx: ExtensionContext, phases: PlanPhase[]): void {
163
+ if (!ctx.hasUI) return;
164
+ ctx.ui.setWidget(WIDGET_KEY, renderWidgetLines(ctx, phases));
165
+ const counts = countItems(phases);
166
+ ctx.ui.setStatus(WIDGET_KEY, counts.total > 0 ? ctx.ui.theme.fg("accent", `plan ${counts.done}/${counts.total}`) : undefined);
167
+ }
168
+
169
+ function applyOperation(phases: PlanPhase[], op: PlanOperationInput): string | undefined {
170
+ switch (op.op) {
171
+ case "list":
172
+ return undefined;
173
+ case "init": {
174
+ if (!op.list || op.list.length === 0) return "init requires list";
175
+ phases.splice(
176
+ 0,
177
+ phases.length,
178
+ ...op.list.map((phase) => ({
179
+ name: phase.phase,
180
+ items: phase.items.map((content, index) => ({ content, status: index === 0 && phases.length === 0 ? "in_progress" : "pending" as PlanStatus })),
181
+ })),
182
+ );
183
+ normalizeOpenTask(phases);
184
+ return undefined;
185
+ }
186
+ case "append": {
187
+ if (!op.phase) return "append requires phase";
188
+ if (!op.items || op.items.length === 0) return "append requires items";
189
+ let phase = findPhase(phases, op.phase);
190
+ if (!phase) {
191
+ phase = { name: op.phase, items: [] };
192
+ phases.push(phase);
193
+ }
194
+ phase.items.push(...op.items.map((content) => ({ content, status: "pending" as PlanStatus })));
195
+ normalizeOpenTask(phases);
196
+ return undefined;
197
+ }
198
+ case "start": {
199
+ const hit = findTask(phases, op.task);
200
+ if (!hit) return "start requires an existing task";
201
+ for (const phase of phases) {
202
+ for (const item of phase.items) {
203
+ if (item.status === "in_progress") item.status = "pending";
204
+ }
205
+ }
206
+ hit.item.status = "in_progress";
207
+ return undefined;
208
+ }
209
+ case "done":
210
+ case "drop": {
211
+ const status: PlanStatus = op.op === "done" ? "completed" : "abandoned";
212
+ if (op.task) {
213
+ const hit = findTask(phases, op.task);
214
+ if (!hit) return `${op.op} requires an existing task`;
215
+ hit.item.status = status;
216
+ } else if (op.phase) {
217
+ const phase = findPhase(phases, op.phase);
218
+ if (!phase) return `${op.op} requires an existing phase`;
219
+ for (const item of phase.items) item.status = status;
220
+ } else {
221
+ return `${op.op} requires task or phase`;
222
+ }
223
+ normalizeOpenTask(phases);
224
+ return undefined;
225
+ }
226
+ case "rm": {
227
+ if (!op.task && !op.phase) {
228
+ phases.splice(0, phases.length);
229
+ return undefined;
230
+ }
231
+ if (op.task) {
232
+ const hit = findTask(phases, op.task);
233
+ if (!hit) return "rm requires an existing task";
234
+ hit.phase.items = hit.phase.items.filter((item) => item !== hit.item);
235
+ } else if (op.phase) {
236
+ const index = phases.findIndex((phase) => phase.name === op.phase);
237
+ if (index < 0) return "rm requires an existing phase";
238
+ phases.splice(index, 1);
239
+ }
240
+ for (let i = phases.length - 1; i >= 0; i--) {
241
+ if (phases[i].items.length === 0) phases.splice(i, 1);
242
+ }
243
+ normalizeOpenTask(phases);
244
+ return undefined;
245
+ }
246
+ case "note": {
247
+ if (!op.text) return "note requires text";
248
+ const hit = findTask(phases, op.task);
249
+ if (!hit) return "note requires an existing task";
250
+ hit.item.notes = [...(hit.item.notes ?? []), op.text];
251
+ return undefined;
252
+ }
253
+ }
254
+ }
255
+
256
+ export default function updatePlanExtension(pi: ExtensionAPI): void {
257
+ let phases: PlanPhase[] = [];
258
+
259
+ function restoreFromEntries(ctx: ExtensionContext): void {
260
+ phases = [];
261
+ for (const entry of ctx.sessionManager.getBranch()) {
262
+ if (entry.type === "custom" && entry.customType === PLAN_STATE_TYPE) {
263
+ const data = entry.data as { phases?: PlanPhase[] } | undefined;
264
+ if (data?.phases) phases = clonePhases(data.phases);
265
+ continue;
266
+ }
267
+ if (entry.type !== "message") continue;
268
+ const message = entry.message as { role?: string; toolName?: string; details?: unknown; isError?: boolean };
269
+ if (message.role !== "toolResult" || message.toolName !== PLAN_TOOL_NAME || message.isError) continue;
270
+ const details = message.details as PlanDetails | undefined;
271
+ if (details?.phases) phases = clonePhases(details.phases);
272
+ }
273
+ updatePlanUi(ctx, phases);
274
+ }
275
+
276
+ pi.on("session_start", async (_event, ctx) => restoreFromEntries(ctx));
277
+ pi.on("session_tree", async (_event, ctx) => restoreFromEntries(ctx));
278
+
279
+ pi.on("before_agent_start", async () => {
280
+ if (!pi.getActiveTools().includes(PLAN_TOOL_NAME)) return;
281
+ return {
282
+ message: {
283
+ customType: "update-plan-guidance",
284
+ content: `<update_plan_guidance>\nFor non-trivial tasks with 3+ distinct steps, or when the user provides a checklist/plan, call update_plan before implementation. Keep exactly one open task in_progress, update it immediately after each completed step, and do not use update_plan for trivial one-step requests.\n</update_plan_guidance>`,
285
+ display: false,
286
+ },
287
+ };
288
+ });
289
+
290
+ pi.registerTool({
291
+ name: PLAN_TOOL_NAME,
292
+ label: "Update Plan",
293
+ description:
294
+ "Maintain a visible execution plan. Use for multi-step tasks: init the plan, mark exactly one task in_progress, and mark tasks done/drop as work proceeds.",
295
+ promptSnippet: "Maintain a visible execution plan for non-trivial multi-step tasks.",
296
+ promptGuidelines: [
297
+ "For tasks with 3+ distinct steps, or when the user provides a checklist, call update_plan with init before doing the work.",
298
+ "Keep exactly one open task in_progress; mark tasks done immediately after completing them.",
299
+ "Do not use update_plan for trivial single-step requests.",
300
+ ],
301
+ parameters: UpdatePlanParams,
302
+ executionMode: "sequential",
303
+ async execute(_toolCallId, params: UpdatePlanInput, _signal, _onUpdate, ctx) {
304
+ let error: string | undefined;
305
+ for (const op of params.ops) {
306
+ error = applyOperation(phases, op);
307
+ if (error) break;
308
+ }
309
+ updatePlanUi(ctx, phases);
310
+ const lastOp = params.ops[params.ops.length - 1]?.op ?? "list";
311
+ const text = error ? `Error: ${error}` : renderTextPlan(phases);
312
+ return {
313
+ content: [{ type: "text", text }],
314
+ details: { op: lastOp, phases: clonePhases(phases), error } satisfies PlanDetails,
315
+ isError: error ? true : undefined,
316
+ };
317
+ },
318
+ renderCall(args: UpdatePlanInput, theme: Theme) {
319
+ const ops = args.ops?.map((op) => op.op).join("+") || "list";
320
+ return new Text(`${theme.fg("toolTitle", theme.bold("update_plan"))} ${theme.fg("muted", ops)}`, 0, 0);
321
+ },
322
+ renderResult(result, { expanded }, theme: Theme) {
323
+ const details = result.details as PlanDetails | undefined;
324
+ if (!details) {
325
+ const block = result.content?.find((item) => item.type === "text");
326
+ return new Text(block?.text ?? "", 0, 0);
327
+ }
328
+ if (details.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
329
+ const snapshot = details.phases ?? [];
330
+ if (snapshot.length === 0) return new Text(theme.fg("dim", "No active plan"), 0, 0);
331
+ const counts = countItems(snapshot);
332
+ const lines = [theme.fg("accent", `Plan ${counts.done}/${counts.total}`)];
333
+ for (const phase of snapshot) {
334
+ lines.push(theme.fg("muted", phase.name));
335
+ const items = expanded ? phase.items : phase.items.slice(0, 5);
336
+ for (const item of items) {
337
+ const marker = item.status === "completed" ? theme.fg("success", "[x]") : item.status === "abandoned" ? theme.fg("dim", "[-]") : item.status === "in_progress" ? theme.fg("accent", "[>]") : theme.fg("muted", "[ ]");
338
+ const text = item.status === "completed" || item.status === "abandoned" ? theme.fg("dim", item.content) : item.content;
339
+ lines.push(`${marker} ${text}`);
340
+ }
341
+ if (!expanded && phase.items.length > items.length) lines.push(theme.fg("dim", `... ${phase.items.length - items.length} more`));
342
+ }
343
+ return new Text(lines.join("\n"), 0, 0);
344
+ },
345
+ });
346
+
347
+ pi.registerCommand("plan-status", {
348
+ description: "Show the current update_plan state",
349
+ handler: async (_args, ctx) => {
350
+ restoreFromEntries(ctx);
351
+ ctx.ui.notify(renderTextPlan(phases), "info");
352
+ },
353
+ });
354
+
355
+ pi.registerCommand("plan-clear", {
356
+ description: "Clear the current update_plan state",
357
+ handler: async (_args, ctx) => {
358
+ phases = [];
359
+ pi.appendEntry(PLAN_STATE_TYPE, { phases: [] });
360
+ updatePlanUi(ctx, phases);
361
+ ctx.ui.notify("Plan cleared.", "info");
362
+ },
363
+ });
364
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lebronj/pi-suite",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "JHP's Pi extension suite for team coding workflows",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -88,10 +88,11 @@ Curator and learning behavior:
88
88
  - Expired temporary memories go to `REVIEW.md`, not automatic deletion.
89
89
  - Quotas reset when `month` or `reset` rolls over.
90
90
  - Mutations are audited to `audit/curator.jsonl`.
91
- - Session shutdown may extract conservative learning candidates into `REVIEW.md`; they are not injected as normal memory and are not auto-enabled.
91
+ - Session shutdown may extract conservative learning candidates into `REVIEW.md`; `/new` and `/fork` also run a lightweight structured-evidence pass.
92
92
  - `memory_curate` scans yesterday's daily log once per content hash into review candidates, then curator lifecycle and proposal rules process those candidates.
93
- - Repeated candidates can become proposed memory promotions or proposed disabled skill drafts after `memory_curate`.
94
- - Approval is explicit by default: memory proposals write to memory stores; skill proposals write disabled drafts under the resolved skill draft root.
93
+ - Repeated or high-confidence candidates can become proposed memory promotions or proposed disabled skill drafts after `memory_curate`.
94
+ - Skill-worthy `failure -> edit/action -> validation success` patterns become high-confidence skill candidates from structured tool evidence.
95
+ - Skill drafts default to `auto-draft`: high-quality proposals write disabled drafts under the resolved skill draft root, but memory proposals still require approval.
95
96
  - Draft and generated skills stay disabled until `memory_skill_enable` copies their full directories into `skills/enabled`; enabled skills are injected as `<available_skills>` metadata for the current agent.
96
97
  - Pi session start can show one pending-review hint; disable with `PI_MEMORY_REVIEW_STARTUP_HINT=0`.
97
98
  - Local multi-agent self-evolution supports one Local Curator Manager registry/dirty-root API for many agent roots, plus a manager service that runs `manager-scan` every 6 hours and exits quickly when no root is dirty.
@@ -143,12 +144,16 @@ Useful memory environment variables:
143
144
  - `PI_MEMORY_NO_SEARCH=1`: disable per-turn search injection.
144
145
  - `PI_MEMORY_SUMMARIZE_TRANSITIONS=1`: also summarize lifecycle transitions such as `/reload`.
145
146
  - `PI_MEMORY_LEARNING`: `off`, `review`, or `auto-review`.
146
- - `PI_MEMORY_SKILL_DRAFTS`: `off` or `review`.
147
+ - `PI_MEMORY_SKILL_DRAFTS`: `off`, `propose`/`review`, or `auto-draft` (default).
148
+ - `PI_MEMORY_SKILL_SEEN_THRESHOLD`: repeated medium-confidence skill candidate threshold; default `2`.
147
149
  - `PI_MEMORY_AUTO_APPROVE_MEMORY=1`: automatically approve newly created memory proposals.
148
150
  - `PI_MEMORY_AUTO_APPROVE_SKILL_DRAFTS=1`: automatically create newly proposed disabled skill drafts.
149
151
  - `PI_MEMORY_CURATOR_STARTUP_HINT=0`: hide the disabled-curator startup hint.
150
152
  - `PI_MEMORY_REVIEW_STARTUP_HINT=0`: hide pending review proposal startup hints.
151
153
  - `PI_MEMORY_REMOTE_URL` and `PI_MEMORY_REMOTE_TOKEN`: enable Multica candidate/profile/feedback upload and current-agent delivery pull.
154
+ - Local CLI Pi can bind to Multica by setting `MULTICA_WORKSPACE_ID`, `MULTICA_AGENT_ID`, `PI_MEMORY_REMOTE_URL`, and `PI_MEMORY_REMOTE_TOKEN`; it then uses the same agent root/sync loop as a Multica-wrapped Pi agent.
155
+ - `PI_MEMORY_AUTO_SYNC=1`: best-effort automatic pull on session start and upload on session shutdown; narrower aliases are `PI_MEMORY_AUTO_SYNC_PULL_ON_START=1`, `PI_MEMORY_AUTO_SYNC_PULL=1`, `PI_MEMORY_AUTO_SYNC_UPLOAD_ON_SHUTDOWN=1`, and `PI_MEMORY_AUTO_SYNC_UPLOAD=1`.
156
+ - `PI_MEMORY_AUTO_SYNC_PULL_LIMIT`: maximum automatic delivery pull count; default `20`.
152
157
  - `PI_EVOLUTION_ENABLED=0`: disable snapshot + git versioning.
153
158
  - `PI_EVOLUTION_DIR`: override evolution repo directory; default `~/.pi/agent/evolution`.
154
159
  - `PI_EVOLUTION_REMOTE`: optional personal private Git remote; unset by default.
@@ -221,6 +226,16 @@ Goal mode is provided by `goal-mode.ts`.
221
226
  - The agent must verify current files/checks before calling `goal({ op: "complete" })`.
222
227
  - Useful commands: `/goal show`, `/goal pause`, `/goal resume`, `/goal drop`, `/goal budget <tokens|off>`, `/goal auto on`, `/goal auto off`.
223
228
 
229
+ ## Update Plan
230
+
231
+ `update-plan.ts` provides a lightweight Codex-style execution checklist without changing Pi core.
232
+
233
+ - Tool: `update_plan`.
234
+ - Use it for non-trivial tasks with 3+ distinct steps, user-provided checklists, or explicit plan/progress tracking requests.
235
+ - Operations: `list`, `init`, `start`, `done`, `drop`, `rm`, `append`, and `note`.
236
+ - It keeps phased task state in session history, auto-promotes the next pending task to `in_progress`, and shows widget/footer progress when active.
237
+ - Commands: `/plan-status` shows the current plan; `/plan-clear` clears it.
238
+
224
239
  ## Pet Companion
225
240
 
226
241
  The pet extension provides a small terminal companion and durable profile.
@@ -233,6 +248,7 @@ The pet extension provides a small terminal companion and durable profile.
233
248
 
234
249
  ## UI And Utility Extensions
235
250
 
251
+ - `update-plan.ts`: registers `update_plan`, `/plan-status`, and `/plan-clear` for visible per-session execution planning.
236
252
  - `prompt-url-widget.ts`: detects PR/issue prompt templates, fetches GitHub metadata with `gh`, shows a widget, and names the session when possible.
237
253
  - `snake.ts`: `/snake` opens a TUI snake game; `Esc` pauses/saves, `q` quits, arrows/WASD move.
238
254
  - `tps.ts`: after each assistant run, shows tokens-per-second and token usage details.