@bacnh85/pi-subagent 0.19.2 → 0.20.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
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.0 (2026-09-07)
4
+
5
+ ### Added
6
+
7
+ - **Auto-review** (`subagent.autoReview: true` in settings.json, default off) —
8
+ after a user-initiated turn that made ≥3 file-mutation tool calls
9
+ (`edit`/`write`/`apply_patch`/`str_replace_editor`) in an interactive (TUI)
10
+ session, the read-only `reviewer` agent is dispatched automatically as a
11
+ background task reviewing the current uncommitted diff of the files the turn
12
+ touched (new/untracked files are read directly); its findings wake the parent
13
+ via the normal background follow-up turn, so an independent review happens
14
+ after every real coding turn without asking. Precedence: global
15
+ settings.json → trusted repo `.pi/settings.json` overlay. Guards keep it
16
+ bounded: turns woken by auto-injected messages are
17
+ skipped (custom wake-ups by role, and pi-advisor blocker/concern steers by
18
+ their fixed `Advisor review (` content prefixes since those are plain user
19
+ messages), max 3 dispatches per session, never while another background task
20
+ runs, cursor tracked while the setting is off so enabling mid-session never
21
+ replays history, `session_start` (startup and reload) reseeds it, and a
22
+ fresh session's first coding turn is reviewed from entry zero. Subagent
23
+ catalog prompt now also states when NOT to delegate (single-file small
24
+ edits, quick greps → inline).
25
+
26
+ ## 0.19.3 (2026-09-05)
27
+
28
+ - Widen Pi SDK peer range to `>=0.80.0 <0.86.0` and bump devDep to `^0.85.0` for Pi 0.85.0 compatibility (no breaking changes; peer cap widening only).
29
+
3
30
  ## 0.19.2 (2026-09-02)
4
31
 
5
32
  ### Fixed
package/README.md CHANGED
@@ -66,6 +66,31 @@ subagent({ operation: "cancel", taskId: "bg-..." }) // abort a running task
66
66
 
67
67
  You will be notified on completion — do not poll or sleep.
68
68
 
69
+ ## Auto-review
70
+
71
+ Opt-in workflow automation (`subagent.autoReview: true` in settings.json,
72
+ default off): after a **user-initiated** turn that made ≥3 file-mutation tool
73
+ calls (`edit`, `write`, `apply_patch`, `str_replace_editor`) in an interactive
74
+ (TUI) session, the read-only `reviewer` agent is dispatched automatically as a
75
+ background task reviewing the current uncommitted diff of the files the turn
76
+ touched (new/untracked files are read directly by the reviewer). Its findings
77
+ arrive as a background follow-up turn, so an independent review follows every
78
+ real coding turn without asking. Precedence: global settings.json → trusted
79
+ repo `.pi/settings.json` overlay.
80
+
81
+ Guards keep it bounded:
82
+
83
+ - Turns woken by auto-injected messages never trigger a review — custom
84
+ wake-ups (`pi-subagent-complete`) by role, and pi-advisor blocker/concern
85
+ steers by their fixed `Advisor review (` content prefixes (those are plain
86
+ user messages) — so review→fix→review ping-pong can't start.
87
+ - Max 3 auto-reviews per session; never while another background task runs.
88
+ - The cursor tracks the transcript tail while the setting is off, so enabling
89
+ mid-session never replays accumulated history; `session_start` (startup and
90
+ reload) reseeds it, and a fresh session's first coding turn is reviewed from
91
+ entry zero. Reviewer timeout is 10 minutes; failures are non-blocking. Set
92
+ `PI_SUBAGENT_AUTOREVIEW_DEBUG=1` to trace dispatch decisions on stderr.
93
+
69
94
  ## History
70
95
 
71
96
  Every completed task (foreground and background) is recorded to
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Auto-review: after a user-initiated turn that mutated files, dispatch the
3
+ * read-only `reviewer` agent on the turn's diff as a background task. Its
4
+ * findings wake the parent via the normal background follow-up turn.
5
+ *
6
+ * Enabled via `subagent.autoReview: true` in settings.json (default off).
7
+ * Guards keep it quiet: user-initiated turns only (auto-injected wake-ups are
8
+ * excluded — see ADVISOR_PREFIXES), a per-session dispatch cap, and never two
9
+ * background tasks at once.
10
+ */
11
+
12
+ import { execFile } from "node:child_process";
13
+ import { join } from "node:path";
14
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
+ import { readSubagentSection, readSubagentSectionFrom } from "./roles.ts";
16
+ import { discoverAgents } from "./agents.ts";
17
+ import { runNamedAgent } from "./service.ts";
18
+ import { startBackgroundTask, type BackgroundDeps } from "./background.ts";
19
+ import type { threadStore as ThreadStoreType } from "./threads.ts";
20
+ import type { SubAgentResult } from "./runner.ts";
21
+
22
+ /** File-mutating tool names counted toward the dispatch threshold. */
23
+ const MUTATION_TOOLS = new Set(["edit", "write", "apply_patch", "str_replace_editor"]);
24
+ /** Minimum mutation tool calls in one turn before a review is worth dispatching. */
25
+ export const MIN_MUTATIONS = 3;
26
+ /** Per-session dispatch cap — bounds model cost and any review→fix→review loop. */
27
+ export const MAX_PER_SESSION = 3;
28
+ /** Reviewer inactivity timeout for the bounded diff review (ms). */
29
+ export const REVIEW_TIMEOUT_MS = 10 * 60 * 1_000;
30
+ /** Max diff text embedded in the task (reviewer can `read` files for more). */
31
+ const MAX_EMBED_BYTES = 24 * 1024;
32
+
33
+ // pi-advisor steers blockers/concerns via pi.sendUserMessage — plain user-role
34
+ // messages with NO customType (fixed templates in pi-advisor's lib/watcher.ts).
35
+ // A turn woken by them is not user-initiated and must not re-trigger review.
36
+ const ADVISOR_PREFIXES = [
37
+ "Advisor review (nit",
38
+ "Advisor review (concern",
39
+ "Advisor review (blocker",
40
+ ];
41
+
42
+ /** Effective `subagent.autoReview`: layered ctx settings (when the SDK exposes
43
+ * them) → trusted repo `.pi/settings.json` → global settings.json. Mirrors
44
+ * readSubagentRoles precedence. */
45
+ export function readAutoReviewEnabled(ctx?: ExtensionContext, globalSection: Record<string, unknown> = readSubagentSection()): boolean {
46
+ let enabled = globalSection.autoReview === true;
47
+ try {
48
+ if (ctx?.isProjectTrusted?.()) {
49
+ const project = readSubagentSectionFrom(join(ctx.cwd, ".pi", "settings.json"));
50
+ if (typeof project.autoReview === "boolean") enabled = project.autoReview;
51
+ }
52
+ } catch { /* untrusted ctx or unreadable file — global only */ }
53
+ const layered = (ctx as unknown as { settings?: { subagent?: { autoReview?: unknown } } } | undefined)?.settings?.subagent?.autoReview;
54
+ if (typeof layered === "boolean") return layered;
55
+ return enabled;
56
+ }
57
+
58
+ export interface AutoReviewState {
59
+ /** Last transcript entry id classified; undefined until the first settle reseeds it. */
60
+ cursor: string | undefined;
61
+ /** Auto-reviews dispatched this session. */
62
+ dispatched: number;
63
+ }
64
+
65
+ export function createAutoReviewState(): AutoReviewState {
66
+ return { cursor: undefined, dispatched: 0 };
67
+ }
68
+
69
+ export interface TurnAnalysis {
70
+ mutationCount: number;
71
+ files: string[];
72
+ /** True only when a real user prompt (not an advisor steer / custom wake-up) drove the turn. */
73
+ userInitiated: boolean;
74
+ cursor: string | undefined;
75
+ }
76
+
77
+ function userEntryText(entry: any): string {
78
+ const content = entry?.message?.content;
79
+ if (!Array.isArray(content)) return "";
80
+ return content
81
+ .filter((part: any) => part?.type === "text")
82
+ .map((part: any) => String(part?.text ?? ""))
83
+ .join("\n");
84
+ }
85
+
86
+ function extractMutatedPaths(part: any): string[] {
87
+ const args = part?.arguments;
88
+ if (typeof args?.path === "string" && args.path) return [args.path];
89
+ if (typeof args?.file_path === "string" && args.file_path) return [args.file_path];
90
+ if (typeof args?.patch === "string") {
91
+ return [...args.patch.matchAll(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/gm)].map((m) => m[1]!.trim());
92
+ }
93
+ return [];
94
+ }
95
+
96
+ /**
97
+ * Classify one settled turn's entries (those after `sinceId`): mutation tool
98
+ * calls, changed files, and whether a genuine user prompt drove the turn.
99
+ * Cursor entry itself is excluded (pi-advisor toolCallCount pattern).
100
+ */
101
+ export function analyzeTurn(entries: any[], sinceId: string | undefined): TurnAnalysis {
102
+ // Flip counting at the cursor entry but classify from the NEXT entry on —
103
+ // strictly-after semantics, matching pi-advisor's toolCallCount.
104
+ let counting = sinceId === undefined;
105
+ let mutationCount = 0;
106
+ const files = new Set<string>();
107
+ let userInitiated = false;
108
+ let cursor = sinceId;
109
+ for (const entry of entries) {
110
+ if (counting) {
111
+ if (typeof entry?.id === "string") cursor = entry.id;
112
+ if (entry?.type === "message") {
113
+ const role = entry.message?.role;
114
+ if (role === "user") {
115
+ const text = userEntryText(entry);
116
+ if (text && !ADVISOR_PREFIXES.some((p) => text.startsWith(p))) userInitiated = true;
117
+ } else if (role === "assistant" && Array.isArray(entry.message?.content)) {
118
+ for (const part of entry.message.content) {
119
+ if (part?.type !== "toolCall" || !MUTATION_TOOLS.has(part?.name)) continue;
120
+ mutationCount++;
121
+ for (const file of extractMutatedPaths(part)) files.add(file);
122
+ }
123
+ }
124
+ }
125
+ } else if (entry?.id === sinceId) {
126
+ counting = true;
127
+ }
128
+ }
129
+ return { mutationCount, files: [...files], userInitiated, cursor };
130
+ }
131
+
132
+ /** First-call reseed: point the cursor at the transcript tail so a mid-session
133
+ * enable never replays history (pi-advisor reseedCursor pattern). */
134
+ export function latestEntryId(entries: any[]): string | undefined {
135
+ for (let i = entries.length - 1; i >= 0; i--) {
136
+ if (typeof entries[i]?.id === "string") return entries[i].id;
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ function git(cwd: string, args: string[], maxBytes: number): Promise<string> {
142
+ return new Promise((resolve) => {
143
+ execFile("git", ["-C", cwd, ...args], {
144
+ timeout: 10_000,
145
+ maxBuffer: 4 * 1024 * 1024,
146
+ }, (err, stdout) => {
147
+ if (err) return resolve("");
148
+ const out = String(stdout);
149
+ resolve(out.length > maxBytes ? out.slice(0, maxBytes) + "\n… (truncated)" : out);
150
+ });
151
+ });
152
+ }
153
+
154
+ /** Scoped diff + porcelain status for the turn's changed files. Never rejects. */
155
+ export async function captureDiff(cwd: string, files: string[]): Promise<{ diff: string; status: string }> {
156
+ return {
157
+ diff: files.length > 0 ? await git(cwd, ["diff", "HEAD", "--", ...files], MAX_EMBED_BYTES) : "",
158
+ status: await git(cwd, ["status", "--porcelain"], 4_096),
159
+ };
160
+ }
161
+
162
+ export function buildReviewTask(files: string[], diff: string, status: string): string {
163
+ return [
164
+ "Review this change for correctness, security, regressions, and missing tests. Only the files below are in scope — do not attempt a broader review.",
165
+ "",
166
+ "Changed files:",
167
+ ...files.map((file) => `- ${file}`),
168
+ "",
169
+ status ? `Git status:\n${status}\n` : "",
170
+ diff ? `Diff (may be truncated):\n${diff}` : "No tracked diff captured — read the changed files directly.",
171
+ "",
172
+ "Output contract (hard limits):",
173
+ "- Max 5 findings, one line each: `path:line — issue — evidence`.",
174
+ "- No praise, no style noise, no restating the diff.",
175
+ "- If nothing warrants a finding, reply with exactly: REVIEW: CLEAN",
176
+ "- Max 30 lines total.",
177
+ ]
178
+ .filter((line) => line !== "")
179
+ .join("\n");
180
+ }
181
+
182
+ /**
183
+ * runOne adapter so the auto-review dispatch can reuse startBackgroundTask
184
+ * (threads, history, status/cancel, completion delivery) without the tool
185
+ * path's execute-scoped runOne. Always read-only; reviewer needs no bash.
186
+ */
187
+ export function makeHookRunOne(bundledAgentsDir: string, ctx: ExtensionContext): BackgroundDeps["runOne"] {
188
+ return async (agentName, task, _cwd, signal, timeoutMs, onProgress, onActivity) => {
189
+ const agent = discoverAgents(ctx.cwd, "user", bundledAgentsDir).agents.find((a) => a.name === agentName);
190
+ if (!agent) {
191
+ const errorResult: SubAgentResult = {
192
+ agent: agentName,
193
+ task,
194
+ exitCode: 1,
195
+ status: "error",
196
+ stopReason: "error",
197
+ messages: [],
198
+ stderr: `Unknown agent: "${agentName}".`,
199
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
200
+ errorMessage: `Unknown agent: "${agentName}"`,
201
+ };
202
+ return errorResult;
203
+ }
204
+ // ponytail: hook dispatches are always read-only regardless of the agent file
205
+ return await runNamedAgent({
206
+ agent: { ...agent, sandbox: "read-only" },
207
+ task,
208
+ cwd: ctx.cwd,
209
+ ctx,
210
+ timeout: timeoutMs,
211
+ signal,
212
+ readOnly: true,
213
+ onMessage: onProgress,
214
+ onProgress: onActivity,
215
+ });
216
+ };
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // agent_settled glue (extracted for testability — dispatch/isRunning injectable)
221
+ // ---------------------------------------------------------------------------
222
+
223
+ export interface AutoReviewSettleDeps {
224
+ pi: ExtensionAPI;
225
+ ctx: ExtensionContext;
226
+ state: AutoReviewState;
227
+ bundledAgentsDir: string;
228
+ threadStore: typeof ThreadStoreType;
229
+ /** Theme color for the reviewer thread (index.ts owns the name→color map). */
230
+ agentColor: string | undefined;
231
+ /** startBackgroundTask; injectable so tests can capture dispatches. */
232
+ dispatch: typeof startBackgroundTask;
233
+ /** True when any background task is running; injectable for tests. */
234
+ isRunning: () => boolean;
235
+ }
236
+
237
+ /** One agent_settled tick: analyze the settled turn and maybe dispatch the reviewer. Returns true when a review was dispatched. */
238
+ export async function handleAutoReviewSettle(deps: AutoReviewSettleDeps): Promise<boolean> {
239
+ const { ctx, state } = deps;
240
+ const debug = process.env.PI_SUBAGENT_AUTOREVIEW_DEBUG === "1";
241
+ try {
242
+ const entries = ctx.sessionManager.getEntries() as any[];
243
+ if (!readAutoReviewEnabled(ctx)) {
244
+ // Keep the cursor fresh while disabled so enabling mid-session never
245
+ // replays accumulated history as one pseudo-turn.
246
+ state.cursor = latestEntryId(entries);
247
+ return false;
248
+ }
249
+ // Headless one-shot modes can't surface the follow-up turn and shouldn't
250
+ // linger on a detached reviewer — track the cursor, dispatch nothing.
251
+ if (ctx.mode !== "tui") {
252
+ state.cursor = latestEntryId(entries);
253
+ return false;
254
+ }
255
+ const analysis = analyzeTurn(entries, state.cursor);
256
+ // Write back the transcript tail unconditionally: if the cursor entry was
257
+ // dropped (compaction/pruning), counting never flips and analyzeTurn would
258
+ // return the stale id forever — a silent permanent disable (pi-advisor
259
+ // reseeds to latestEntryId for the same reason).
260
+ state.cursor = latestEntryId(entries);
261
+ if (debug) console.error(`[auto-review] mutations=${analysis.mutationCount} userInitiated=${analysis.userInitiated} dispatched=${state.dispatched}`);
262
+ if (!analysis.userInitiated) return false; // advisor steers / custom wake-ups must not loop
263
+ if (analysis.mutationCount < MIN_MUTATIONS) return false; // trivial edits stay un-reviewed
264
+ if (analysis.files.length === 0) return false; // delete-only/mutation-without-path turns have nothing to scope
265
+ if (state.dispatched >= MAX_PER_SESSION) return false;
266
+ if (deps.isRunning()) return false;
267
+ const { diff, status } = await captureDiff(ctx.cwd, analysis.files);
268
+ deps.dispatch({
269
+ agent: "reviewer",
270
+ task: buildReviewTask(analysis.files, diff, status),
271
+ timeout: REVIEW_TIMEOUT_MS,
272
+ agentColor: deps.agentColor,
273
+ deps: { pi: deps.pi, ctx, runOne: makeHookRunOne(deps.bundledAgentsDir, ctx), threadStore: deps.threadStore },
274
+ });
275
+ state.dispatched++;
276
+ if (debug) console.error("[auto-review] reviewer dispatched (background)");
277
+ return true;
278
+ } catch {
279
+ // Auto-review must never break the main loop (same contract as pi-advisor's watcher).
280
+ return false;
281
+ }
282
+ }
@@ -64,6 +64,11 @@ import { type SubagentThread, threadStore } from "./threads.ts";
64
64
  import { SUBAGENT_REQUEST_EVENT, runNamedAgent, type SubagentRunRequest } from "./service.ts";
65
65
  import { resolveModel, runWithModelFallback } from "./model.ts";
66
66
  import { DEFAULT_ROLES, describeAgentModels, readSubagentRoles, readSubagentRolesGlobal, resolveAgentModelChain, type RolesConfig } from "./roles.ts";
67
+ import {
68
+ createAutoReviewState,
69
+ handleAutoReviewSettle,
70
+ latestEntryId,
71
+ } from "./auto-review.ts";
67
72
  import { ThreadViewer, type ThreadViewerCallbacks } from "./thread-viewer.ts";
68
73
  import { createTaskWidgetController, renderLiveThreadLine, type TaskWidgetController } from "./widget.ts";
69
74
  import {
@@ -185,6 +190,7 @@ interface SubagentDetails {
185
190
 
186
191
  export default function (pi: ExtensionAPI) {
187
192
  let currentCtx: ExtensionContext | undefined;
193
+ let autoReviewState = createAutoReviewState();
188
194
 
189
195
  // Live progress widget — fed by threadStore subscriptions (per SDK event).
190
196
  const widget: TaskWidgetController = createTaskWidgetController(
@@ -198,6 +204,12 @@ export default function (pi: ExtensionAPI) {
198
204
  if (event.reason === "reload") invalidateAgentCache();
199
205
  threadStore.clear();
200
206
  trustedProjectAgentDirs.clear();
207
+ autoReviewState = createAutoReviewState();
208
+ // Reseed before any turn runs so the session's FIRST coding turn is
209
+ // reviewed too (reseed-on-first-settle would silently consume it).
210
+ try {
211
+ if (ctx) autoReviewState.cursor = latestEntryId(ctx.sessionManager.getEntries() as any[]);
212
+ } catch { /* no session yet — analyzeTurn handles undefined cursor */ }
201
213
  // Clear any widget from a prior session.
202
214
  widget.clearWidgetIfIdle();
203
215
  // Mark prior-session running tasks as interrupted (we can't resume them),
@@ -252,6 +264,7 @@ export default function (pi: ExtensionAPI) {
252
264
  "Prefer **scout** and **tester** for cheap routine work. " +
253
265
  "Prefer **worker** or **general-purpose** for normal coding. " +
254
266
  "Prefer **planner** and **reviewer** for consequential reasoning. " +
267
+ "Delegate only when isolation/parallelism/specialization pays off — do NOT delegate single-file small edits or quick greps; do those inline. " +
255
268
  "Modes: single, parallel (max 8 tasks, 4 concurrent), chain.",
256
269
  };
257
270
  });
@@ -298,6 +311,24 @@ export default function (pi: ExtensionAPI) {
298
311
  });
299
312
  });
300
313
 
314
+ // Auto-review: after a user-initiated turn that mutated files, dispatch the
315
+ // read-only reviewer on the current uncommitted diff of the turn's touched
316
+ // files as a background task. Config: `subagent.autoReview`.
317
+ pi.on("agent_settled", async (_event, ctx) => {
318
+ if (!ctx) return;
319
+ const dispatched = await handleAutoReviewSettle({
320
+ pi,
321
+ ctx,
322
+ state: autoReviewState,
323
+ bundledAgentsDir,
324
+ threadStore,
325
+ agentColor: agentToThemeColor("reviewer"),
326
+ dispatch: startBackgroundTask,
327
+ isRunning: () => getAllBackgroundTasks().some((t) => t.status === "running"),
328
+ });
329
+ if (dispatched && ctx.mode === "tui") widget.ensureWidget(ctx);
330
+ });
331
+
301
332
  // Register renderer for background-task completion (follow-up turn).
302
333
  pi.registerMessageRenderer?.("pi-subagent-complete", (message, _opts, theme) => {
303
334
  const d = (message.details ?? {}) as {
@@ -140,6 +140,16 @@ export function readSubagentRoles(ctx?: ExtensionContext): RolesConfig {
140
140
  return cfg;
141
141
  }
142
142
 
143
+ /** Read the `subagent` section of the user's global settings.json (empty when absent). */
144
+ export function readSubagentSection(): Record<string, unknown> {
145
+ return readSubagentSectionFrom(join(agentDir(), "settings.json"));
146
+ }
147
+
148
+ /** Read the `subagent` section of an arbitrary settings.json (empty when absent). */
149
+ export function readSubagentSectionFrom(settingsPath: string): Record<string, unknown> {
150
+ return subagentSection(readJson(settingsPath));
151
+ }
152
+
143
153
  /** Global-only variant used by the panel so a save never persists repo
144
154
  * `.pi/settings.json` overlay values into the user's global settings. */
145
155
  export function readSubagentRolesGlobal(): RolesConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-subagent",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "CHANGELOG.md",
33
33
  "agents/",
34
34
  "extensions/index.ts",
35
+ "extensions/auto-review.ts",
35
36
  "extensions/agents.ts",
36
37
  "extensions/model.ts",
37
38
  "extensions/roles.ts",
@@ -62,20 +63,20 @@
62
63
  "check": "npm run typecheck && npm test"
63
64
  },
64
65
  "peerDependencies": {
65
- "@earendil-works/pi-agent-core": ">=0.80.0 <0.85.0",
66
- "@earendil-works/pi-ai": ">=0.80.0 <0.85.0",
67
- "@earendil-works/pi-coding-agent": ">=0.80.0 <0.85.0",
68
- "@earendil-works/pi-tui": ">=0.80.0 <0.85.0",
66
+ "@earendil-works/pi-agent-core": ">=0.80.0 <0.86.0",
67
+ "@earendil-works/pi-ai": ">=0.80.0 <0.86.0",
68
+ "@earendil-works/pi-coding-agent": ">=0.80.0 <0.86.0",
69
+ "@earendil-works/pi-tui": ">=0.80.0 <0.86.0",
69
70
  "typebox": ">=1.3.0 <2.0.0"
70
71
  },
71
72
  "dependencies": {
72
73
  "@bacnh85/pi-config-panel": "^0.1.0"
73
74
  },
74
75
  "devDependencies": {
75
- "@earendil-works/pi-agent-core": "^0.84.0",
76
- "@earendil-works/pi-ai": "^0.84.0",
77
- "@earendil-works/pi-coding-agent": "^0.84.3",
78
- "@earendil-works/pi-tui": "^0.84.3",
76
+ "@earendil-works/pi-agent-core": "^0.85.0",
77
+ "@earendil-works/pi-ai": "^0.85.0",
78
+ "@earendil-works/pi-coding-agent": "^0.85.0",
79
+ "@earendil-works/pi-tui": "^0.85.0",
79
80
  "@types/mocha": "^10.0.10",
80
81
  "@types/node": "^20.19.43",
81
82
  "mocha": "^11.8.0",