@mjasnikovs/pi-task 0.15.1 → 0.16.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.
@@ -60,13 +60,34 @@ export type { PhaseDeps };
60
60
  */
61
61
  export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: string, prompt: string): Promise<string>;
62
62
  export declare function formatLoopHint(hit: LoopHit): string;
63
+ /**
64
+ * Terminal hint for the degrade attempt: the model has thrashed through the whole
65
+ * strike budget re-reading files without converging, so we strip its tools and
66
+ * order it to emit the deliverable NOW from what it already has. Used only by
67
+ * read-only analysis phases (refine) whose output is a text rewrite that never
68
+ * strictly required a successful read — far better to ship a best-effort spec
69
+ * than to hard-fail the whole /task-auto run. See countRevisits / LoopExhausted.
70
+ */
71
+ export declare function formatDegradeHint(hit: LoopHit): string;
63
72
  export declare function prependHint(hint: string | null, prompt: string): string;
64
73
  /**
65
74
  * Run a phase child with loop detection. On a detected loop, kill and re-spawn
66
75
  * with a hint that names the offending call. Cap at MAX_LOOP_RESTARTS restarts;
67
76
  * the (MAX_LOOP_RESTARTS+1)th loop throws LoopExhaustedError.
68
77
  */
69
- export declare function runPhaseWithLoopGuard(deps: PhaseDeps, name: string, tools: string, buildPrompt: (loopHint: string | null) => string): Promise<string>;
78
+ export interface LoopGuardOptions {
79
+ /**
80
+ * When the strike budget is exhausted by loops, do NOT fail the phase. Run
81
+ * ONE final attempt with NO tools and a terminal hint ordering the model to
82
+ * emit its output from what it already has. Only safe for phases whose
83
+ * deliverable is a pure text rewrite that never strictly required a read
84
+ * (refine) — a hard-fail there kills the whole /task-auto run for a model
85
+ * that simply over-explored. Research/location phases must NOT enable this:
86
+ * their output depends on real reads, so a no-tools fallback would fabricate.
87
+ */
88
+ degradeOnExhaustion?: boolean;
89
+ }
90
+ export declare function runPhaseWithLoopGuard(deps: PhaseDeps, name: string, tools: string, buildPrompt: (loopHint: string | null) => string, opts?: LoopGuardOptions): Promise<string>;
70
91
  /**
71
92
  * Run a child up to twice; the second attempt gets `emphasized=true` to escalate
72
93
  * the prompt. On success, return the validator's value; on two failures, throw
@@ -160,6 +160,22 @@ export function formatLoopHint(hit) {
160
160
  + `stuck in a loop. Avoid repeating that exact call; if you've already seen its result, `
161
161
  + `work from memory or pick a different angle.]`);
162
162
  }
163
+ /**
164
+ * Terminal hint for the degrade attempt: the model has thrashed through the whole
165
+ * strike budget re-reading files without converging, so we strip its tools and
166
+ * order it to emit the deliverable NOW from what it already has. Used only by
167
+ * read-only analysis phases (refine) whose output is a text rewrite that never
168
+ * strictly required a successful read — far better to ship a best-effort spec
169
+ * than to hard-fail the whole /task-auto run. See countRevisits / LoopExhausted.
170
+ */
171
+ export function formatDegradeHint(hit) {
172
+ return (`[SYSTEM NOTE: You called ${hit.call.name}(${JSON.stringify(hit.call.args)}) `
173
+ + `repeatedly and made no forward progress — you are stuck re-reading the same files. `
174
+ + `You now have NO tools: you cannot read, grep, list, or open anything further. `
175
+ + `STOP exploring. Produce the COMPLETE required output IMMEDIATELY, using only the task `
176
+ + `description and what you have already seen. Emit the full structured result now — do not `
177
+ + `ask to read more, do not explain, just output the final answer.]`);
178
+ }
163
179
  export function prependHint(hint, prompt) {
164
180
  return hint === null ? prompt : `${hint}\n\n${prompt}`;
165
181
  }
@@ -172,12 +188,7 @@ async function appendLoopEvent(cwd, taskId, phase, hit, strike, outcome) {
172
188
  const next = existing ? `${existing}\n${line}` : line;
173
189
  await setTaskSection(cwd, taskId, 'loop events', next);
174
190
  }
175
- /**
176
- * Run a phase child with loop detection. On a detected loop, kill and re-spawn
177
- * with a hint that names the offending call. Cap at MAX_LOOP_RESTARTS restarts;
178
- * the (MAX_LOOP_RESTARTS+1)th loop throws LoopExhaustedError.
179
- */
180
- export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
191
+ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt, opts = {}) {
181
192
  const loopHistory = [];
182
193
  // Carries the correction hint (loop OR leaked-tool-call) into the next strike.
183
194
  let nextHint = null;
@@ -192,9 +203,14 @@ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
192
203
  if (r.loopHit) {
193
204
  const isLastStrike = strike === MAX_LOOP_RESTARTS;
194
205
  loopHistory.push(r.loopHit);
195
- await appendLoopEvent(deps.cwd, deps.taskId, name, r.loopHit, strike + 1, isLastStrike ? 'phase failed' : 'restarted with hint');
196
- if (isLastStrike)
206
+ const lastOutcome = opts.degradeOnExhaustion ? 'degraded no-tools final attempt' : 'phase failed';
207
+ await appendLoopEvent(deps.cwd, deps.taskId, name, r.loopHit, strike + 1, isLastStrike ? lastOutcome : 'restarted with hint');
208
+ if (isLastStrike) {
209
+ if (opts.degradeOnExhaustion) {
210
+ return await runDegradedFinalAttempt(deps, name, buildPrompt, r.loopHit, loopHistory);
211
+ }
197
212
  throw new LoopExhaustedError(name, loopHistory);
213
+ }
198
214
  nextHint = formatLoopHint(r.loopHit);
199
215
  continue;
200
216
  }
@@ -236,6 +252,24 @@ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
236
252
  }
237
253
  throw new LoopExhaustedError(name, loopHistory);
238
254
  }
255
+ /**
256
+ * Final degrade attempt after the loop budget is spent: re-spawn the child with
257
+ * NO tools and a terminal hint, so a model that thrashed re-reading files is
258
+ * forced to emit its deliverable from what it already has. With no tools there
259
+ * are no tool calls, so no loop can recur — the only failure modes left are a
260
+ * dead turn (non-zero exit, model error) or empty output, any of which fall back
261
+ * to the original LoopExhaustedError so the phase still fails honestly when even
262
+ * the degrade produces nothing.
263
+ */
264
+ async function runDegradedFinalAttempt(deps, name, buildPrompt, hit, loopHistory) {
265
+ deps.logDebug?.(`${name}: loop budget exhausted — degrading to a no-tools final attempt`);
266
+ const r = await runChild(deps.cwd, '', // --no-tools: the model cannot read/grep/list, only answer
267
+ buildPrompt(formatDegradeHint(hit)), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn);
268
+ if (r.exitCode !== 0 || r.modelError || r.text.trim().length === 0) {
269
+ throw new LoopExhaustedError(name, loopHistory);
270
+ }
271
+ return r.text;
272
+ }
239
273
  /**
240
274
  * Run a child up to twice; the second attempt gets `emphasized=true` to escalate
241
275
  * the prompt. On success, return the validator's value; on two failures, throw
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Implementation-turn status widget.
3
+ *
4
+ * The phase widget (widget.ts) is disposed at spec-handoff, so the host agent's
5
+ * implementation turn — the longest, most visible part of a run — otherwise shows
6
+ * only pi's bare "⠸ Working…" indicator. This module keeps the SAME rich status
7
+ * block alive across that turn (task id · implementing/elapsed/context bar · ↳ last
8
+ * tool), driven by the host's own live `ctx.getContextUsage()`.
9
+ *
10
+ * It is event-driven rather than poll-wrapped because the two delivery paths differ:
11
+ *
12
+ * • /task (fire-and-forget): the command returns right after `sendUserMessage`,
13
+ * so the host runs the impl turn AFTER our code is gone — only an `agent_start`
14
+ * handler can pick it up. Armed one-shot: `agent_end` disarms it.
15
+ * • /task-auto (awaited): the caller blocks across `waitForIdle` plus any
16
+ * compaction-resume / steer turns. Armed sticky: each `agent_start` re-shows the
17
+ * widget, `agent_end` only hides it between turns, and the caller disarms once
18
+ * the whole implementation phase has settled.
19
+ *
20
+ * Only one task runs at a time (single active task), so a single module-level slot
21
+ * is sufficient.
22
+ */
23
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
24
+ export interface ImplWidgetMeta {
25
+ taskId: string;
26
+ title: string;
27
+ label?: string;
28
+ }
29
+ /**
30
+ * Arm the implementation widget just before the spec is handed off. `oneShot`
31
+ * true (fire-and-forget /task) lets `agent_end` disarm it after the single turn;
32
+ * false (awaited /task-auto) keeps it armed until `disarmImplWidget` is called.
33
+ */
34
+ export declare function armImplWidget(meta: ImplWidgetMeta, opts: {
35
+ oneShot: boolean;
36
+ }): void;
37
+ /** Tear down the widget and clear the armed slot (sticky/awaited path). */
38
+ export declare function disarmImplWidget(): void;
39
+ /** Wire the agent-lifecycle handlers that drive the widget. Call once at setup. */
40
+ export declare function setupImplWidget(pi: ExtensionAPI): void;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Implementation-turn status widget.
3
+ *
4
+ * The phase widget (widget.ts) is disposed at spec-handoff, so the host agent's
5
+ * implementation turn — the longest, most visible part of a run — otherwise shows
6
+ * only pi's bare "⠸ Working…" indicator. This module keeps the SAME rich status
7
+ * block alive across that turn (task id · implementing/elapsed/context bar · ↳ last
8
+ * tool), driven by the host's own live `ctx.getContextUsage()`.
9
+ *
10
+ * It is event-driven rather than poll-wrapped because the two delivery paths differ:
11
+ *
12
+ * • /task (fire-and-forget): the command returns right after `sendUserMessage`,
13
+ * so the host runs the impl turn AFTER our code is gone — only an `agent_start`
14
+ * handler can pick it up. Armed one-shot: `agent_end` disarms it.
15
+ * • /task-auto (awaited): the caller blocks across `waitForIdle` plus any
16
+ * compaction-resume / steer turns. Armed sticky: each `agent_start` re-shows the
17
+ * widget, `agent_end` only hides it between turns, and the caller disarms once
18
+ * the whole implementation phase has settled.
19
+ *
20
+ * Only one task runs at a time (single active task), so a single module-level slot
21
+ * is sufficient.
22
+ */
23
+ import { WIDGET_KEY, WIDGET_REFRESH_MS, buildImplLines } from './widget.js';
24
+ import { setTaskWidget } from '../remote/session-state.js';
25
+ let armed = null;
26
+ let lastLine;
27
+ let timer = null;
28
+ let activeCtx = null;
29
+ /** Map the host's live context usage to the widget snapshot, or undefined when the
30
+ * token count is unknown (e.g. right after compaction, before the next response). */
31
+ function snapshot(ctx) {
32
+ const u = ctx.getContextUsage?.();
33
+ if (!u || u.tokens == null)
34
+ return undefined;
35
+ return { tokens: u.tokens, contextWindow: u.contextWindow, percent: u.percent ?? 0 };
36
+ }
37
+ function render() {
38
+ if (!armed || !activeCtx)
39
+ return;
40
+ const ctx = activeCtx;
41
+ const state = {
42
+ taskId: armed.meta.taskId,
43
+ title: armed.meta.title,
44
+ label: armed.meta.label,
45
+ startedAt: armed.startedAt,
46
+ lastLine,
47
+ contextUsage: snapshot(ctx)
48
+ };
49
+ try {
50
+ ctx.ui.setWidget(WIDGET_KEY, buildImplLines(state, ctx.ui.theme));
51
+ }
52
+ catch {
53
+ /* stale ctx */
54
+ }
55
+ setTaskWidget(buildImplLines(state, undefined)); // un-themed for the wire
56
+ }
57
+ function startTimer() {
58
+ if (timer)
59
+ return;
60
+ render();
61
+ timer = setInterval(render, WIDGET_REFRESH_MS);
62
+ timer.unref?.();
63
+ }
64
+ function stopTimer() {
65
+ if (timer) {
66
+ clearInterval(timer);
67
+ timer = null;
68
+ }
69
+ }
70
+ function clearWidget() {
71
+ try {
72
+ activeCtx?.ui.setWidget(WIDGET_KEY, undefined);
73
+ }
74
+ catch {
75
+ /* stale ctx */
76
+ }
77
+ setTaskWidget(undefined);
78
+ }
79
+ /** One-line summary of a host tool call for the `↳` trailer (best-effort). */
80
+ function formatToolLine(toolName, args) {
81
+ const a = (args ?? {});
82
+ const field = a.path ?? a.file_path ?? a.command ?? a.pattern ?? a.prompt ?? a.query ?? a.url;
83
+ const detail = typeof field === 'string' ? ' ' + field.replace(/\s+/g, ' ').trim() : '';
84
+ return `${toolName}${detail}`;
85
+ }
86
+ /**
87
+ * Arm the implementation widget just before the spec is handed off. `oneShot`
88
+ * true (fire-and-forget /task) lets `agent_end` disarm it after the single turn;
89
+ * false (awaited /task-auto) keeps it armed until `disarmImplWidget` is called.
90
+ */
91
+ export function armImplWidget(meta, opts) {
92
+ armed = { meta, oneShot: opts.oneShot, startedAt: Date.now() };
93
+ lastLine = undefined;
94
+ }
95
+ /** Tear down the widget and clear the armed slot (sticky/awaited path). */
96
+ export function disarmImplWidget() {
97
+ stopTimer();
98
+ clearWidget();
99
+ armed = null;
100
+ activeCtx = null;
101
+ lastLine = undefined;
102
+ }
103
+ /** Wire the agent-lifecycle handlers that drive the widget. Call once at setup. */
104
+ export function setupImplWidget(pi) {
105
+ pi.on('agent_start', (_event, ctx) => {
106
+ if (!armed)
107
+ return;
108
+ activeCtx = ctx;
109
+ lastLine = undefined;
110
+ startTimer();
111
+ });
112
+ pi.on('tool_execution_start', (event, _ctx) => {
113
+ if (!armed)
114
+ return;
115
+ lastLine = formatToolLine(event.toolName, event.args);
116
+ });
117
+ pi.on('agent_end', (_event, _ctx) => {
118
+ if (!armed)
119
+ return;
120
+ stopTimer();
121
+ clearWidget();
122
+ // Fire-and-forget /task: the single impl turn is over, so disarm. Awaited
123
+ // /task-auto leaves the slot armed so the next compaction-resume / steer
124
+ // turn re-shows it; the caller disarms when the phase truly settles.
125
+ if (armed.oneShot) {
126
+ armed = null;
127
+ activeCtx = null;
128
+ lastLine = undefined;
129
+ }
130
+ });
131
+ }
@@ -28,6 +28,11 @@ export declare class TaskRunner {
28
28
  private readonly _onStart;
29
29
  private readonly _planContext;
30
30
  private readonly _fixInstruction;
31
+ /** True when the caller awaits the implementation turn (waitForImplementation):
32
+ * the impl widget stays armed across the whole impl phase (incl. compaction /
33
+ * steer turns) and is disarmed here. False for fire-and-forget /task, where the
34
+ * widget is armed one-shot and its own agent_end disarms it. */
35
+ private readonly _implAwaited;
31
36
  private readonly _abort;
32
37
  private readonly _startedAt;
33
38
  private readonly _widgetState;
@@ -43,7 +48,7 @@ export declare class TaskRunner {
43
48
  */
44
49
  private readonly _timings;
45
50
  private _currentPhaseChildren;
46
- constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string, fixInstruction?: string);
51
+ constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string, fixInstruction?: string, implAwaited?: boolean);
47
52
  get taskId(): string;
48
53
  get signal(): AbortSignal;
49
54
  /** Return the current widget state, or null if not started. */
@@ -23,6 +23,7 @@ import { PHASE_INDEX, PHASE_ORDER, RESUMABLE_STATES } from './task-types.js';
23
23
  import { normaliseTaskId, parseFrontMatter, extractSection } from './task-parsers.js';
24
24
  import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
25
25
  import { startWidget } from './widget.js';
26
+ import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
26
27
  import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
27
28
  import { pushNotify } from '../remote/push.js';
28
29
  import { parseVerifyBlock } from './spec-validation.js';
@@ -54,6 +55,11 @@ export class TaskRunner {
54
55
  _onStart;
55
56
  _planContext;
56
57
  _fixInstruction;
58
+ /** True when the caller awaits the implementation turn (waitForImplementation):
59
+ * the impl widget stays armed across the whole impl phase (incl. compaction /
60
+ * steer turns) and is disarmed here. False for fire-and-forget /task, where the
61
+ * widget is armed one-shot and its own agent_end disarms it. */
62
+ _implAwaited;
57
63
  _abort = new AbortController();
58
64
  _startedAt;
59
65
  _widgetState;
@@ -69,7 +75,7 @@ export class TaskRunner {
69
75
  */
70
76
  _timings = [];
71
77
  _currentPhaseChildren = null;
72
- constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart, planContext, fixInstruction) {
78
+ constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart, planContext, fixInstruction, implAwaited) {
73
79
  this._ctx = ctx;
74
80
  this._cwd = cwd;
75
81
  this._rawPrompt = rawPrompt;
@@ -78,6 +84,7 @@ export class TaskRunner {
78
84
  this._onStart = onStart;
79
85
  this._planContext = planContext;
80
86
  this._fixInstruction = fixInstruction;
87
+ this._implAwaited = implAwaited ?? false;
81
88
  this._startedAt = Date.now();
82
89
  // We'll populate id/title/phase lazily in run().
83
90
  // Placeholder — real values set in run().
@@ -265,13 +272,30 @@ export class TaskRunner {
265
272
  }
266
273
  async _deliverSpec(ctx) {
267
274
  const spec = this._specForDelivery();
275
+ // Keep the rich status block alive across the implementation turn (the phase
276
+ // widget was disposed at handoff). Awaited (/task-auto) stays armed across all
277
+ // sub-turns and is disarmed here; fire-and-forget (/task) arms one-shot and its
278
+ // own agent_end disarms it after the single turn.
279
+ const meta = {
280
+ taskId: this._widgetState.taskId,
281
+ title: this._widgetState.title,
282
+ label: this._widgetState.label
283
+ };
268
284
  if (this._sendSpec) {
269
- await this._sendSpec(spec);
285
+ armImplWidget(meta, { oneShot: !this._implAwaited });
286
+ try {
287
+ await this._sendSpec(spec);
288
+ }
289
+ finally {
290
+ if (this._implAwaited)
291
+ disarmImplWidget();
292
+ }
270
293
  return;
271
294
  }
272
295
  if (!piApi) {
273
296
  throw new Error('extension not initialised (no ExtensionAPI captured)');
274
297
  }
298
+ armImplWidget(meta, { oneShot: true });
275
299
  if (ctx.isIdle()) {
276
300
  piApi.sendUserMessage(spec);
277
301
  }
@@ -494,7 +518,7 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
494
518
  if (!interrupted)
495
519
  implError = implementationError(newCtx);
496
520
  }
497
- }, opts.spawnFn, opts.onStart, opts.planContext, opts.fixInstruction);
521
+ }, opts.spawnFn, opts.onStart, opts.planContext, opts.fixInstruction, opts.waitForImplementation);
498
522
  await runner.run();
499
523
  taskId = runner.taskId;
500
524
  }
@@ -643,6 +667,7 @@ async function handleTaskCancel(_args, ctx) {
643
667
  // ─── Entry point ─────────────────────────────────────────────────────────────
644
668
  export function registerTask(pi) {
645
669
  piApi = pi;
670
+ setupImplWidget(pi);
646
671
  registerBridgeCommand(pi, 'task', {
647
672
  description: 'Start a new task. Usage: /task <prompt>',
648
673
  handler: handleTask
@@ -62,7 +62,15 @@ export function replaceToolingWithVerified(research, verifiedCommands) {
62
62
  return replaced;
63
63
  }
64
64
  // ─── Phase functions ─────────────────────────────────────────────────────────
65
- export const phaseRefine = (deps, raw, planContext) => runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext))));
65
+ export const phaseRefine = (deps, raw, planContext) => runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext))),
66
+ // refine's deliverable is a 4-section text rewrite that never strictly
67
+ // needs a successful read — on a test-writing task against a large
68
+ // existing codebase the model over-explores (re-reads source hunting for
69
+ // the impl) and burns the loop budget. Degrade to a no-tools final
70
+ // attempt instead of hard-failing the whole run. See TASK_0016 (mx5):
71
+ // refine looped 3×/resume forever; the deliverable was always producible
72
+ // from the title + design doc alone.
73
+ { degradeOnExhaustion: true });
66
74
  export async function phaseVerifyTooling(deps, research) {
67
75
  const commands = extractToolingCommands(research);
68
76
  if (!commands || commands.length === 0) {
@@ -63,4 +63,14 @@ export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetT
63
63
  * (returns a no-op disposer) when there's no UI.
64
64
  */
65
65
  export declare function startAutoLoader(ctx: ExtensionCommandContext, getState: () => AutoLoaderState | null): () => void;
66
+ export interface ImplState {
67
+ taskId: string;
68
+ title: string;
69
+ /** Short display label compressed from `title`; falls back to a truncation when absent. */
70
+ label?: string;
71
+ startedAt: number;
72
+ lastLine?: string;
73
+ contextUsage?: ContextSnapshot;
74
+ }
75
+ export declare function buildImplLines(s: ImplState, theme?: WidgetTheme): string[];
66
76
  export declare function flashTerminalWidget(ctx: ExtensionCommandContext, state: Exclude<TaskState, 'pending' | 'in_progress' | 'completed'>, taskId: string, reason: string | undefined): void;
@@ -170,6 +170,21 @@ export function startAutoLoader(ctx, getState) {
170
170
  setTaskWidget(undefined);
171
171
  };
172
172
  }
173
+ export function buildImplLines(s, theme) {
174
+ const elapsed = formatDuration(Date.now() - s.startedAt);
175
+ const head = `${s.taskId} · ${titleForDisplay(s)}`;
176
+ let detail = `implementing · ${elapsed}`;
177
+ if (s.contextUsage) {
178
+ const ctxDetail = formatContextDetail(s.contextUsage, theme);
179
+ if (ctxDetail)
180
+ detail += ` · ${ctxDetail}`;
181
+ }
182
+ const lines = [head, detail];
183
+ const trailer = lastLineTrailer(s.lastLine, theme);
184
+ if (trailer)
185
+ lines.push(trailer);
186
+ return lines;
187
+ }
173
188
  export function flashTerminalWidget(ctx, state, taskId, reason) {
174
189
  if (!ctx.hasUI)
175
190
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",