@mjasnikovs/pi-task 0.38.17 → 0.38.19

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.
@@ -215,5 +215,16 @@ export declare const CONFIG_LOADERS: {
215
215
  * into the config object.
216
216
  */
217
217
  export declare function loadConfig(raw: unknown): PiTaskConfig;
218
+ /**
219
+ * Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
220
+ *
221
+ * The override exists because this module loads the real file at import time,
222
+ * which made every test that reads a config value depend on the developer's own
223
+ * `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
224
+ * 12 tests here while CI (no config file at all) stayed green. The test preload
225
+ * points this at a path under the tmp dir that never exists, so tests always see
226
+ * DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
227
+ */
228
+ export declare const CONFIG_PATH_ENV = "PI_TASK_CONFIG_PATH";
218
229
  export declare function getConfig(): PiTaskConfig;
219
230
  export declare function saveConfig(config: PiTaskConfig): Promise<void>;
@@ -182,7 +182,19 @@ export function loadConfig(raw) {
182
182
  }
183
183
  return out;
184
184
  }
185
- const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
185
+ /**
186
+ * Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
187
+ *
188
+ * The override exists because this module loads the real file at import time,
189
+ * which made every test that reads a config value depend on the developer's own
190
+ * `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
191
+ * 12 tests here while CI (no config file at all) stayed green. The test preload
192
+ * points this at a path under the tmp dir that never exists, so tests always see
193
+ * DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
194
+ */
195
+ export const CONFIG_PATH_ENV = 'PI_TASK_CONFIG_PATH';
196
+ const CONFIG_PATH = process.env[CONFIG_PATH_ENV]?.trim()
197
+ || path.join(os.homedir(), '.config', 'pi-task', 'config.json');
186
198
  const _g = globalThis;
187
199
  if (!_g.__piTaskConfig) {
188
200
  _g.__piTaskConfig = { config: { ...DEFAULT_CONFIG }, loaded: false };
@@ -115,6 +115,25 @@ export interface RunChildJsonEventsOptions {
115
115
  mode: 'json-events';
116
116
  onLine?: (line: string) => void;
117
117
  onContextUsage?: (snapshot: ContextSnapshot) => void;
118
+ /**
119
+ * The child's context window in tokens, supplied BY THE CALLER — pi's event
120
+ * stream does not carry one (GitHub issue #16).
121
+ *
122
+ * Verified against the published tarballs of @earendil-works/pi-coding-agent
123
+ * and @earendil-works/pi-agent-core at 0.80.2 and 0.84.2, and against pi's
124
+ * own docs/json.md: the wire union is session / agent_* / turn_* / message_*
125
+ * / tool_execution_* / queue_update / compaction_* / auto_retry_*, the
126
+ * session header is {type,version,id,timestamp,cwd,parentSession}, and no
127
+ * member of either carries a window or even a model id. `contextUsage`
128
+ * exists ONLY as the in-process `ctx.getContextUsage()` extension API, which
129
+ * a `--mode json` child never speaks back to its parent.
130
+ *
131
+ * The parent therefore has to say. Children are spawned without `-m`
132
+ * (CHILD_BASE_ARGS), so they resolve the same default model the parent runs
133
+ * and the parent session's window is the honest answer. 0 / omitted keeps
134
+ * the old behaviour: report the token count with no window.
135
+ */
136
+ contextWindow?: number;
118
137
  onToolCall?: (call: ToolCall) => LoopHit | null;
119
138
  /**
120
139
  * Fires when a tool call finishes, carrying its RESULT (mx5 run 10 item 6: the
@@ -88,15 +88,12 @@ export class JsonEventSink {
88
88
  handleEvent(evt) {
89
89
  const opts = this.opts;
90
90
  const t = typeof evt.type === 'string' ? evt.type : '';
91
- if (t === 'context_usage' && opts.onContextUsage) {
92
- const tokens = Number(evt.tokens ?? 0);
93
- const contextWindow = Number(evt.contextWindow ?? 0);
94
- const percent = Number(evt.percent ?? 0);
95
- if (tokens > 0 || contextWindow > 0) {
96
- opts.onContextUsage({ tokens, contextWindow, percent });
97
- }
98
- return;
99
- }
91
+ // `message_end` is the ONLY context readout pi gives a `--mode json`
92
+ // child's parent. There used to be a `context_usage` branch above this
93
+ // one that was preferred over it; no released pi has ever emitted such
94
+ // an event (see `contextWindow` on RunChildJsonEventsOptions), so it was
95
+ // unreachable, and its presence is what made the zero window below look
96
+ // like a harmless fallback rather than the only path. Issue #16.
100
97
  if (t === 'message_end' && opts.onContextUsage) {
101
98
  const msg = evt.message;
102
99
  if (msg?.role === 'assistant') {
@@ -107,7 +104,9 @@ export class JsonEventSink {
107
104
  + Number(usage.cacheWrite ?? 0)
108
105
  + Number(usage.output ?? 0);
109
106
  if (tokens > 0) {
110
- opts.onContextUsage({ tokens, contextWindow: 0, percent: 0 });
107
+ const cw = Math.max(0, Number(opts.contextWindow ?? 0));
108
+ const percent = cw > 0 ? Math.min(100, (tokens / cw) * 100) : 0;
109
+ opts.onContextUsage({ tokens, contextWindow: cw, percent });
111
110
  }
112
111
  }
113
112
  }
@@ -82,13 +82,30 @@ extensions?: readonly string[],
82
82
  * needs the size of what actually entered the child's context, which the
83
83
  * CALL alone does not carry (task/stall-detector.ts).
84
84
  */
85
- onToolResult?: (text: string, isError: boolean) => void): Promise<PhaseRunResult>;
85
+ onToolResult?: (text: string, isError: boolean) => void,
86
+ /**
87
+ * The child's context window in tokens. Nothing in pi's event stream reports
88
+ * one (issue #16), so the parent hands its own down — children carry no `-m`
89
+ * and resolve the same default model. 0 / omitted = unknown, as before.
90
+ */
91
+ contextWindow?: number): Promise<PhaseRunResult>;
86
92
  interface PhaseDeps {
87
93
  cwd: string;
88
94
  taskId: string;
89
95
  signal: AbortSignal;
90
96
  onChildOutput?: (line: string) => void;
91
97
  onContextUsage?: (snapshot: ContextSnapshot) => void;
98
+ /**
99
+ * The parent session's context window in tokens, handed down to every child.
100
+ *
101
+ * pi's `--mode json` stream reports token counts but no window (issue #16),
102
+ * so without this the gauge shows a bare number and — worse — the
103
+ * StallDetector's CONTEXT CHURN rule, which is gated on a positive window,
104
+ * can never fire. Children are spawned without `-m` (CHILD_BASE_ARGS) and so
105
+ * run the parent's own default model; its window is the honest value.
106
+ * Absent = unknown, and both consumers degrade exactly as they did before.
107
+ */
108
+ contextWindow?: number;
92
109
  /**
93
110
  * Record a sub-step duration under the currently running top-level phase.
94
111
  * The orchestrator rebinds this between phases so each call lands in the
@@ -172,7 +172,13 @@ extensions,
172
172
  * needs the size of what actually entered the child's context, which the
173
173
  * CALL alone does not carry (task/stall-detector.ts).
174
174
  */
175
- onToolResult) {
175
+ onToolResult,
176
+ /**
177
+ * The child's context window in tokens. Nothing in pi's event stream reports
178
+ * one (issue #16), so the parent hands its own down — children carry no `-m`
179
+ * and resolve the same default model. 0 / omitted = unknown, as before.
180
+ */
181
+ contextWindow) {
176
182
  const invocation = getPiInvocation(childArgs(tools, extensions), prompt);
177
183
  let loopHit;
178
184
  const result = await runChildUnified(spawnFn ?? spawn, invocation, cwd, signal, {
@@ -184,6 +190,7 @@ onToolResult) {
184
190
  streamInactivityMs: getConfig().streamInactivityMs,
185
191
  onLine,
186
192
  onContextUsage,
193
+ ...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
187
194
  onToolResult: onToolResult ? r => onToolResult(r.text, r.isError) : undefined,
188
195
  onToolCall: call => {
189
196
  if (!onToolCall)
@@ -316,13 +323,21 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
316
323
  throw new Error(USER_CANCELLED);
317
324
  const detector = new LoopDetector(LOOP_WINDOW, LOOP_THRESHOLD);
318
325
  const stall = new StallDetector();
326
+ // Arm the churn rule BEFORE the first tool call. The window used to reach
327
+ // the detector only through a context snapshot, and pi's stream never
328
+ // carries one, so it was always 0 and rule 2 never fired (issue #16). The
329
+ // parent knows the value at spawn time — say it then, not later.
330
+ stall.noteContext(deps.contextWindow ?? 0);
319
331
  const clock = phaseTimeout(deps.signal, budgetMs);
320
332
  let r;
321
333
  try {
322
334
  r = await runChild(deps.cwd, tools, prependHint(hint, prompt), clock.signal, deps.onChildOutput, snapshot => {
335
+ // Real window or nothing: noteContext ignores 0, and until
336
+ // deps.contextWindow existed 0 was all it ever saw, which
337
+ // left the churn rule permanently disarmed (issue #16).
323
338
  stall.noteContext(snapshot.contextWindow);
324
339
  deps.onContextUsage?.(snapshot);
325
- }, call => detector.record(call) ?? stall.record(call), deps.spawn, deps.childExtensions, (text, isError) => stall.noteResult(text, isError));
340
+ }, call => detector.record(call) ?? stall.record(call), deps.spawn, deps.childExtensions, (text, isError) => stall.noteResult(text, isError), deps.contextWindow);
326
341
  }
327
342
  finally {
328
343
  clock.cleanup();
@@ -430,7 +445,7 @@ async function appendLoopEvent(cwd, taskId, phase, hit, strike, outcome) {
430
445
  async function runDegradedFinalAttempt(deps, name, prompt, hit, loopHistory) {
431
446
  deps.logDebug?.(`${name}: loop budget exhausted — degrading to a no-tools final attempt`);
432
447
  const r = await runChild(deps.cwd, '', // --no-tools: the model cannot read/grep/list, only answer
433
- prependHint(formatDegradeHint(hit), prompt), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn);
448
+ prependHint(formatDegradeHint(hit), prompt), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn, undefined, undefined, deps.contextWindow);
434
449
  if (r.exitCode !== 0 || r.modelError || r.text.trim().length === 0) {
435
450
  throw new LoopExhaustedError(name, loopHistory);
436
451
  }
@@ -34,10 +34,15 @@ export declare class ChildStatus {
34
34
  private readonly _parentContextWindow;
35
35
  private readonly _startLoader;
36
36
  constructor(deps: ChildStatusDeps);
37
+ /**
38
+ * The parent session's window — the value handed DOWN to each child so its
39
+ * own readout carries one, and the last fallback when a child reports none.
40
+ */
41
+ get parentContextWindow(): number;
37
42
  /** The child's latest stream line. Bind as `onChildOutput` / `onLine`. */
38
43
  onLine(line: string): void;
39
44
  /**
40
- * Fold a raw context_usage snapshot into the gauge: the child's own window,
45
+ * Fold a raw context snapshot into the gauge: the child's own window,
41
46
  * else the last known one, else the parent's (`resolveContextUsage`).
42
47
  */
43
48
  onContextUsage(snapshot: ContextSnapshot): void;
@@ -91,5 +96,8 @@ export declare function runPlanningChild(opts: {
91
96
  prompt: string;
92
97
  loader: PlanningChildLoader;
93
98
  }): Promise<string>;
94
- /** Wire a `ChildStatus` as a phase child's stream callbacks. */
95
- export declare function statusCallbacks(status: ChildStatus): Pick<PhaseDeps, 'onChildOutput' | 'onContextUsage'>;
99
+ /**
100
+ * Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
101
+ * child must be TOLD, since pi's event stream never reports one (issue #16).
102
+ */
103
+ export declare function statusCallbacks(status: ChildStatus): Pick<PhaseDeps, 'onChildOutput' | 'onContextUsage' | 'contextWindow'>;
@@ -30,12 +30,19 @@ export class ChildStatus {
30
30
  this._parentContextWindow = deps.parentContextWindow;
31
31
  this._startLoader = deps.startLoader ?? startAutoLoader;
32
32
  }
33
+ /**
34
+ * The parent session's window — the value handed DOWN to each child so its
35
+ * own readout carries one, and the last fallback when a child reports none.
36
+ */
37
+ get parentContextWindow() {
38
+ return this._parentContextWindow;
39
+ }
33
40
  /** The child's latest stream line. Bind as `onChildOutput` / `onLine`. */
34
41
  onLine(line) {
35
42
  this._lastLine = line;
36
43
  }
37
44
  /**
38
- * Fold a raw context_usage snapshot into the gauge: the child's own window,
45
+ * Fold a raw context snapshot into the gauge: the child's own window,
39
46
  * else the last known one, else the parent's (`resolveContextUsage`).
40
47
  */
41
48
  onContextUsage(snapshot) {
@@ -90,10 +97,14 @@ export async function runPlanningChild(opts) {
90
97
  startedAt
91
98
  }), () => runPhaseChild(phaseDeps, name, tools, prompt));
92
99
  }
93
- /** Wire a `ChildStatus` as a phase child's stream callbacks. */
100
+ /**
101
+ * Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
102
+ * child must be TOLD, since pi's event stream never reports one (issue #16).
103
+ */
94
104
  export function statusCallbacks(status) {
95
105
  return {
96
106
  onChildOutput: line => status.onLine(line),
97
- onContextUsage: snapshot => status.onContextUsage(snapshot)
107
+ onContextUsage: snapshot => status.onContextUsage(snapshot),
108
+ contextWindow: status.parentContextWindow
98
109
  };
99
110
  }
@@ -9,7 +9,7 @@ import type { ContextSnapshot } from '../shared/child-process.js';
9
9
  /** The parent session's context window, or 0 when the model doesn't expose it. */
10
10
  export declare function getParentContextWindow(ctx: ExtensionCommandContext): number;
11
11
  /**
12
- * Fold a raw context_usage snapshot into a display snapshot: prefer the child's
12
+ * Fold a raw context snapshot into a display snapshot: prefer the child's
13
13
  * own contextWindow, else the last known one, else the parent session's; then
14
14
  * derive percent against it — falling back to the child's reported percent when
15
15
  * no window is known at all.
@@ -9,7 +9,7 @@ export function getParentContextWindow(ctx) {
9
9
  return (ctx.model?.contextWindow ?? 0);
10
10
  }
11
11
  /**
12
- * Fold a raw context_usage snapshot into a display snapshot: prefer the child's
12
+ * Fold a raw context snapshot into a display snapshot: prefer the child's
13
13
  * own contextWindow, else the last known one, else the parent session's; then
14
14
  * derive percent against it — falling back to the child's reported percent when
15
15
  * no window is known at all.
@@ -119,7 +119,10 @@ export function makeGateChild(deps) {
119
119
  + deps.truncateToolResult(text), 'stream')
120
120
  }
121
121
  : {}),
122
- onContextUsage: snapshot => deps.status.onContextUsage(snapshot)
122
+ onContextUsage: snapshot => deps.status.onContextUsage(snapshot),
123
+ // The gate child has to be TOLD its window: nothing in pi's
124
+ // event stream reports one (issue #16).
125
+ contextWindow: deps.status.parentContextWindow
123
126
  });
124
127
  }
125
128
  finally {
@@ -122,6 +122,10 @@ export class TaskRunner {
122
122
  onChildOutput: (line) => {
123
123
  this._widgetState.lastLine = line;
124
124
  },
125
+ // Handed DOWN so the child's own snapshot carries a window and the
126
+ // StallDetector's churn rule can arm; resolveContextUsage below stays
127
+ // as the fallback for a child that still reports none (issue #16).
128
+ contextWindow: parentContextWindow,
125
129
  onContextUsage: snapshot => {
126
130
  this._widgetState.contextUsage = resolveContextUsage(snapshot, this._widgetState.contextUsage, parentContextWindow);
127
131
  },
@@ -38,6 +38,11 @@
38
38
  * rule 1 alone would not have caught it. The bound scales with the model's
39
39
  * OWN window, so a 1M-context model gets a 1M-context allowance.
40
40
  *
41
+ * The window is supplied by the PARENT at spawn (`PhaseDeps.contextWindow`),
42
+ * not read off the child's event stream: pi emits no context event at all,
43
+ * which is why this rule sat disarmed from the day it was written until
44
+ * GitHub issue #16.
45
+ *
41
46
  * Neither rule can fire on a child that is thinking rather than calling tools:
42
47
  * that case is bounded by the model's max tokens (server-enforced) and by the
43
48
  * stream watchdog if the stream goes silent. Between the three there is no
@@ -38,6 +38,11 @@
38
38
  * rule 1 alone would not have caught it. The bound scales with the model's
39
39
  * OWN window, so a 1M-context model gets a 1M-context allowance.
40
40
  *
41
+ * The window is supplied by the PARENT at spawn (`PhaseDeps.contextWindow`),
42
+ * not read off the child's event stream: pi emits no context event at all,
43
+ * which is why this rule sat disarmed from the day it was written until
44
+ * GitHub issue #16.
45
+ *
41
46
  * Neither rule can fire on a child that is thinking rather than calling tools:
42
47
  * that case is bounded by the model's max tokens (server-enforced) and by the
43
48
  * stream watchdog if the stream goes silent. Between the three there is no
@@ -115,21 +115,35 @@ export function buildWidgetData(s) {
115
115
  export function startWidget(ctx, getState) {
116
116
  if (!ctx.hasUI)
117
117
  return () => { };
118
+ // `ctx.ui` THROWS once the ctx goes stale (/reload, session replacement), so
119
+ // the theme read belongs INSIDE the guard, not one line above it. render()
120
+ // runs from a timer, where an unguarded throw is an uncaughtException that
121
+ // kills the whole pi process — and a swallowed one would throw again on
122
+ // every tick, so a stale ctx latches and stops the timer. Issue #15.
123
+ let stale = false;
118
124
  const render = () => {
125
+ if (stale)
126
+ return;
119
127
  const s = getState();
120
- const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
121
- const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire
128
+ const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire; needs no ctx
122
129
  try {
130
+ const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
123
131
  ctx.ui.setWidget(WIDGET_KEY, lines);
124
132
  }
125
133
  catch {
126
- /* stale ctx */
134
+ stale = true;
135
+ clearInterval(timer);
136
+ return;
127
137
  }
128
138
  setTaskWidget(plain, s ? buildWidgetData(s) : null);
129
139
  };
130
- render();
140
+ // The timer is created BEFORE the first render so `timer` is always bound
141
+ // when render's catch reaches for it: a ctx already stale on the very first
142
+ // paint now stops the loop there, instead of arming an interval that wakes
143
+ // up and returns early forever.
131
144
  const timer = setInterval(render, WIDGET_REFRESH_MS);
132
145
  timer.unref?.();
146
+ render();
133
147
  return () => {
134
148
  clearInterval(timer);
135
149
  try {
@@ -192,21 +206,32 @@ export function buildAutoLoaderData(s) {
192
206
  export function startAutoLoader(ctx, getState) {
193
207
  if (!ctx.hasUI)
194
208
  return () => { };
209
+ // Same staleness hazard as startWidget: the theme read must sit inside the
210
+ // guard, and a stale ctx stops the timer rather than throwing every tick.
211
+ let stale = false;
195
212
  const render = () => {
213
+ if (stale)
214
+ return;
196
215
  const s = getState();
197
- const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
198
- const plain = s ? buildAutoLoaderLines(s, undefined) : undefined;
216
+ const plain = s ? buildAutoLoaderLines(s, undefined) : undefined; // needs no ctx
199
217
  try {
218
+ const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
200
219
  ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
201
220
  }
202
221
  catch {
203
- /* stale ctx */
222
+ stale = true;
223
+ clearInterval(timer);
224
+ return;
204
225
  }
205
226
  setTaskWidget(plain, s ? buildAutoLoaderData(s) : null);
206
227
  };
207
- render();
228
+ // The timer is created BEFORE the first render so `timer` is always bound
229
+ // when render's catch reaches for it: a ctx already stale on the very first
230
+ // paint now stops the loop there, instead of arming an interval that wakes
231
+ // up and returns early forever.
208
232
  const timer = setInterval(render, WIDGET_REFRESH_MS);
209
233
  timer.unref?.();
234
+ render();
210
235
  return () => {
211
236
  clearInterval(timer);
212
237
  try {
@@ -249,7 +274,14 @@ export function buildImplData(s) {
249
274
  export function flashTerminalWidget(ctx, state, taskId, reason) {
250
275
  if (!ctx.hasUI)
251
276
  return;
252
- const theme = ctx.ui.theme;
277
+ // Same staleness hazard as the render timers: bail rather than throw.
278
+ let theme;
279
+ try {
280
+ theme = ctx.ui.theme;
281
+ }
282
+ catch {
283
+ return;
284
+ }
253
285
  let line;
254
286
  let clearMs;
255
287
  if (state === 'cancelled') {
@@ -62,12 +62,19 @@ export interface RunWorkerInput {
62
62
  toolCallId?: string;
63
63
  }) => void;
64
64
  /**
65
- * Called for each context_usage snapshot the child emits (same `--mode json`
66
- * stream the phase children parse). Lets a caller's status widget show the
67
- * tokens/window progress bar for the worker exactly like the phase widget,
68
- * instead of omitting it.
65
+ * Called for each context snapshot derived from the child's `message_end`
66
+ * events (same `--mode json` stream the phase children parse). Lets a
67
+ * caller's status widget show the tokens/window progress bar for the worker
68
+ * exactly like the phase widget, instead of omitting it.
69
69
  */
70
70
  onContextUsage?: (snapshot: ContextSnapshot) => void;
71
+ /**
72
+ * The worker child's context window in tokens. pi's event stream carries no
73
+ * window (issue #16), so a caller that wants a progress bar rather than a
74
+ * bare token count has to supply the parent session's — which is the child's
75
+ * too, since workers are spawned without `-m`.
76
+ */
77
+ contextWindow?: number;
71
78
  /**
72
79
  * Per-worker wall-clock timeout in ms. Defaults to RESEARCH_WORKER_TIMEOUT_MS.
73
80
  * Pass 0 to disable the timeout entirely (run until the child exits on its
@@ -549,7 +549,10 @@ export async function runWorker(input) {
549
549
  cmdWatch?.onEnd(r.toolCallId);
550
550
  input.onToolResult?.(r);
551
551
  },
552
- onContextUsage: input.onContextUsage
552
+ onContextUsage: input.onContextUsage,
553
+ ...(input.contextWindow && input.contextWindow > 0 ?
554
+ { contextWindow: input.contextWindow }
555
+ : {})
553
556
  }, input.spawn);
554
557
  }
555
558
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.17",
3
+ "version": "0.38.19",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,9 +32,9 @@
32
32
  "ws": "8.21.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@earendil-works/pi-agent-core": "0.80.2",
36
- "@earendil-works/pi-coding-agent": "0.80.2",
37
- "@earendil-works/pi-tui": "0.80.2",
35
+ "@earendil-works/pi-agent-core": "0.84.2",
36
+ "@earendil-works/pi-coding-agent": "0.84.2",
37
+ "@earendil-works/pi-tui": "0.84.2",
38
38
  "@eslint/js": "10.0.1",
39
39
  "@types/bun": "1.3.12",
40
40
  "@types/qrcode": "1.5.6",