@mjasnikovs/pi-task 0.13.12 → 0.13.14
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/dist/remote/events.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { setAgentIdle } from './state.js';
|
|
2
|
-
import { pushNotify } from './push.js';
|
|
3
2
|
import { publishNotify } from './bridge.js';
|
|
4
3
|
import { agentStart, appendText, textEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
|
|
5
4
|
/** Mirror pi agent events into the authoritative SessionState. Each handler
|
|
@@ -22,10 +21,11 @@ export function setupEvents(pi) {
|
|
|
22
21
|
if (errorMessage || ae.reason === 'error') {
|
|
23
22
|
const message = errorMessage || 'Request failed';
|
|
24
23
|
addError(message);
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
24
|
+
// No push here: pi-task only notifies for the two cases the user
|
|
25
|
+
// cares about — needing their input (the grill/clarify dialog, see
|
|
26
|
+
// bridge.ask) and a top-level /task or /task-auto run finishing
|
|
27
|
+
// (orchestrator/auto-orchestrator). A push on every host agent
|
|
28
|
+
// error — most of them outside any task — is just noise.
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
});
|
|
@@ -44,7 +44,12 @@ export function setupEvents(pi) {
|
|
|
44
44
|
pi.on('agent_end', (_event, ctx) => {
|
|
45
45
|
setAgentIdle(true);
|
|
46
46
|
agentEnd(ctx.getContextUsage());
|
|
47
|
-
|
|
47
|
+
// Deliberately no push: agent_end fires on EVERY host turn — every chat
|
|
48
|
+
// reply, every internal phase turn inside /task, and every internal /task
|
|
49
|
+
// run inside /task-auto — so a "Task finished" push here floods the device.
|
|
50
|
+
// The real "a run finished" push is fired from the top-level command
|
|
51
|
+
// handlers instead (orchestrator handleTask/handleTaskResume, and
|
|
52
|
+
// auto-orchestrator's runAutoLoop), which never fire for internal runs.
|
|
48
53
|
});
|
|
49
54
|
pi.on('input', (event, _ctx) => {
|
|
50
55
|
if (event.source === 'interactive' && typeof event.text === 'string') {
|
|
@@ -16,6 +16,7 @@ import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath } from
|
|
|
16
16
|
import { gitCommitAll } from './auto-commit.js';
|
|
17
17
|
import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
|
|
18
18
|
import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
|
|
19
|
+
import { pushNotify } from '../remote/push.js';
|
|
19
20
|
import { getConfig } from '../config/config.js';
|
|
20
21
|
import { startAutoLoader } from './widget.js';
|
|
21
22
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
@@ -106,7 +107,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
106
107
|
})
|
|
107
108
|
});
|
|
108
109
|
if (a === undefined) {
|
|
109
|
-
ctx
|
|
110
|
+
announceDone(ctx, '/task-auto cancelled.', 'warning');
|
|
110
111
|
return null;
|
|
111
112
|
}
|
|
112
113
|
const typed = a.trim();
|
|
@@ -139,7 +140,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
139
140
|
const listRaw = await deps.runChild('auto-decompose', 'read', AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications));
|
|
140
141
|
const titles = parseDecomposeList(listRaw);
|
|
141
142
|
if (titles.length === 0) {
|
|
142
|
-
ctx
|
|
143
|
+
announceDone(ctx, '/task-auto: no tasks produced from the feature.', 'warning');
|
|
143
144
|
return null;
|
|
144
145
|
}
|
|
145
146
|
// persist
|
|
@@ -220,6 +221,17 @@ let autoRunning = false;
|
|
|
220
221
|
export function requestAutoCancel() {
|
|
221
222
|
cancelRequested = true;
|
|
222
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Announce a terminal /task-auto-overall outcome both in the terminal and to
|
|
226
|
+
* subscribed devices. The push body reuses the exact terminal message, so a
|
|
227
|
+
* backgrounded phone learns the same thing the TUI shows. Used ONLY at the
|
|
228
|
+
* overall run's terminal points — never per internal task (those go through
|
|
229
|
+
* runSingleTask without notifyFinish, so they stay silent).
|
|
230
|
+
*/
|
|
231
|
+
function announceDone(ctx, msg, level) {
|
|
232
|
+
ctx.ui.notify(msg, level);
|
|
233
|
+
void pushNotify('Task finished', msg, 'pi-end').catch(() => { });
|
|
234
|
+
}
|
|
223
235
|
export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
224
236
|
cancelRequested = false;
|
|
225
237
|
// Each task runs in its own fresh session (deps.runTask → ctx.newSession),
|
|
@@ -230,7 +242,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
230
242
|
try {
|
|
231
243
|
for (;;) {
|
|
232
244
|
if (cancelRequested) {
|
|
233
|
-
active
|
|
245
|
+
announceDone(active, `${id} cancelled — resume with /task-auto-resume.`, 'warning');
|
|
234
246
|
return;
|
|
235
247
|
}
|
|
236
248
|
const { body } = await readTaskFile(cwd, id);
|
|
@@ -238,7 +250,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
238
250
|
const next = entries.find(e => !e.done);
|
|
239
251
|
if (!next) {
|
|
240
252
|
await updateTaskFrontMatter(cwd, id, { state: 'completed' });
|
|
241
|
-
active
|
|
253
|
+
announceDone(active, `${id} complete — all ${entries.length} tasks done.`, 'info');
|
|
242
254
|
return;
|
|
243
255
|
}
|
|
244
256
|
active.ui.notify(`${id}: task ${next.index + 1}/${entries.length} — ${next.title}`, 'info');
|
|
@@ -277,7 +289,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
277
289
|
});
|
|
278
290
|
active = res.ctx ?? active;
|
|
279
291
|
if (res.sessionCancelled) {
|
|
280
|
-
active
|
|
292
|
+
announceDone(active, `${id} paused — could not start a session. Run /task-auto-resume to retry.`, 'warning');
|
|
281
293
|
return;
|
|
282
294
|
}
|
|
283
295
|
if (res.interrupted) {
|
|
@@ -287,12 +299,12 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
287
299
|
// this task's spec to finish it. (A plain ESC that the user
|
|
288
300
|
// follows with steering text never reaches here — that loops on
|
|
289
301
|
// the same task inside runSingleTask until a turn completes.)
|
|
290
|
-
active
|
|
302
|
+
announceDone(active, `${id} paused at "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
291
303
|
return;
|
|
292
304
|
}
|
|
293
305
|
if (!res.ok) {
|
|
294
306
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
295
|
-
active
|
|
307
|
+
announceDone(active, `${id} stopped at "${next.title}" — fix and run /task-auto-resume.`, 'error');
|
|
296
308
|
return;
|
|
297
309
|
}
|
|
298
310
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
@@ -319,11 +331,11 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
319
331
|
// mirroring the in-loop per-task failure path.
|
|
320
332
|
const msg = err instanceof Error ? err.message : String(err);
|
|
321
333
|
if (msg === USER_CANCELLED) {
|
|
322
|
-
active
|
|
334
|
+
announceDone(active, `${id} cancelled — resume with /task-auto-resume.`, 'warning');
|
|
323
335
|
return;
|
|
324
336
|
}
|
|
325
337
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' }).catch(() => { });
|
|
326
|
-
active
|
|
338
|
+
announceDone(active, `${id} stopped: ${msg} — fix and run /task-auto-resume.`, 'error');
|
|
327
339
|
}
|
|
328
340
|
finally {
|
|
329
341
|
cancelRequested = false;
|
|
@@ -350,10 +362,10 @@ async function handleTaskAuto(args, ctx) {
|
|
|
350
362
|
autoRunning = false;
|
|
351
363
|
const msg = err instanceof Error ? err.message : String(err);
|
|
352
364
|
if (msg === USER_CANCELLED) {
|
|
353
|
-
ctx
|
|
365
|
+
announceDone(ctx, '/task-auto cancelled.', 'warning');
|
|
354
366
|
return;
|
|
355
367
|
}
|
|
356
|
-
ctx
|
|
368
|
+
announceDone(ctx, `/task-auto planning failed: ${msg}`, 'error');
|
|
357
369
|
return;
|
|
358
370
|
}
|
|
359
371
|
if (!id) {
|
|
@@ -365,7 +377,7 @@ async function handleTaskAuto(args, ctx) {
|
|
|
365
377
|
if (cancelRequested) {
|
|
366
378
|
cancelRequested = false;
|
|
367
379
|
autoRunning = false;
|
|
368
|
-
ctx
|
|
380
|
+
announceDone(ctx, '/task-auto cancelled.', 'warning');
|
|
369
381
|
return;
|
|
370
382
|
}
|
|
371
383
|
await runAutoLoop(ctx, cwd, id, deps);
|
|
@@ -34,18 +34,33 @@ fork the breakdown). Account for the answers so far:
|
|
|
34
34
|
real-time vs polling transport, search, deployment).
|
|
35
35
|
- Skip anything /task will naturally resolve per-task during its own research.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
YOU MUST propose a default answer for the question — every question you emit
|
|
38
|
+
carries exactly one SUGGESTED line. Never omit it, never leave it blank, never
|
|
39
|
+
refuse. Infer the most sensible, concrete, decisive default from the repo, the
|
|
40
|
+
referenced docs, and any stated philosophy or constraints; it is shown to the
|
|
41
|
+
user as a recommendation they accept or override. When the question is a genuine
|
|
42
|
+
binary "A or B?" fork, also give the single best alternative as an ALT line;
|
|
43
|
+
otherwise emit only the one SUGGESTED.
|
|
42
44
|
|
|
43
45
|
OUTPUT FORMAT (exact):
|
|
44
46
|
- One clarifying question as a single numbered line: "1. ...".
|
|
45
|
-
- On the NEXT line (never inline), a line that begins with "SUGGESTED: <your recommended default>".
|
|
47
|
+
- On the NEXT line (never inline), a line that begins with "SUGGESTED: <your recommended default>". This line is REQUIRED for every question.
|
|
46
48
|
- ONLY for a binary "A or B?" fork, on the line after that, a line beginning with "ALT: <the alternative option>". Omit the ALT line entirely for open-ended questions.
|
|
47
49
|
- Put the core question in **bold**, followed by a short one-line rationale in plain prose. Backticks around code/identifiers are fine. Avoid other markdown (headings, bullet lists, links).
|
|
48
|
-
- Only when the spec already pins down every choice that would change the task breakdown — nothing decision-changing is left to ask — output exactly
|
|
50
|
+
- Only when the spec already pins down every choice that would change the task breakdown — nothing decision-changing is left to ask — output exactly the single token NONE on its own line (and no SUGGESTED line).
|
|
51
|
+
|
|
52
|
+
EXAMPLES (format only — your wording will differ):
|
|
53
|
+
|
|
54
|
+
Open-ended question:
|
|
55
|
+
1. **Where should uploaded files be stored?** This forks whether an early storage-abstraction task is needed.
|
|
56
|
+
SUGGESTED: store on local disk under ./uploads, served by the existing static handler
|
|
57
|
+
|
|
58
|
+
Binary "A or B?" fork:
|
|
59
|
+
1. **Should the Zod schemas live in a shared module imported by both server and client, or stay server-only with manual client types?** This decides whether a "shared schema" task must land before any route or API-client work.
|
|
60
|
+
SUGGESTED: a shared schema module imported by both, so \`hc()\` gets typed RPC for free
|
|
61
|
+
ALT: server-only schemas with hand-written client types
|
|
62
|
+
|
|
63
|
+
No question remains:
|
|
49
64
|
NONE`;
|
|
50
65
|
/**
|
|
51
66
|
* Decompose: output a markdown checkbox list of task titles (one line each).
|
|
@@ -77,6 +77,13 @@ export interface RunSingleTaskOptions {
|
|
|
77
77
|
* steer loop is testable without a real dialog.
|
|
78
78
|
*/
|
|
79
79
|
promptSteer?: (ctx: ExtensionCommandContext) => Promise<string | undefined>;
|
|
80
|
+
/**
|
|
81
|
+
* Push a "Task finished" notification to subscribed devices when this run
|
|
82
|
+
* reaches a terminal state (completed / failed / cancelled). Set only by the
|
|
83
|
+
* top-level /task and /task-resume command handlers — NOT by /task-auto's
|
|
84
|
+
* internal per-task runs, which must stay silent. Default false.
|
|
85
|
+
*/
|
|
86
|
+
notifyFinish?: boolean;
|
|
80
87
|
}
|
|
81
88
|
export interface RunSingleTaskResult {
|
|
82
89
|
taskId: string;
|
|
@@ -24,6 +24,7 @@ import { normaliseTaskId, parseFrontMatter, extractSection } from './task-parser
|
|
|
24
24
|
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
25
25
|
import { startWidget } from './widget.js';
|
|
26
26
|
import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
|
|
27
|
+
import { pushNotify } from '../remote/push.js';
|
|
27
28
|
import { parseVerifyBlock } from './spec-validation.js';
|
|
28
29
|
import { formatTimings } from './timings.js';
|
|
29
30
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
@@ -342,18 +343,29 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
342
343
|
});
|
|
343
344
|
if (result.cancelled) {
|
|
344
345
|
// No replacement happened — the original ctx is still live.
|
|
346
|
+
if (opts.notifyFinish) {
|
|
347
|
+
void pushNotify('Task finished', `${taskId || 'Task'} cancelled — could not start a session.`, 'pi-end').catch(() => { });
|
|
348
|
+
}
|
|
345
349
|
return { taskId, ok: false, sessionCancelled: true, ctx };
|
|
346
350
|
}
|
|
347
351
|
let ok = false;
|
|
352
|
+
let state;
|
|
348
353
|
if (taskId) {
|
|
349
354
|
try {
|
|
350
355
|
const { frontMatter } = await readTaskFile(cwd, taskId);
|
|
351
|
-
|
|
356
|
+
state = frontMatter.state;
|
|
357
|
+
ok = state === 'completed';
|
|
352
358
|
}
|
|
353
359
|
catch {
|
|
354
360
|
ok = false;
|
|
355
361
|
}
|
|
356
362
|
}
|
|
363
|
+
if (opts.notifyFinish) {
|
|
364
|
+
// One push per top-level /task or /task-resume, on any terminal end. The
|
|
365
|
+
// file state is the source of truth: 'completed' on success, 'failed' /
|
|
366
|
+
// 'cancelled' otherwise; an unreadable/absent file falls back to 'ended'.
|
|
367
|
+
void pushNotify('Task finished', `${taskId || 'Task'} ${state ?? 'ended'}.`, 'pi-end').catch(() => { });
|
|
368
|
+
}
|
|
357
369
|
return { taskId, ok, sessionCancelled: false, ctx: freshCtx, interrupted };
|
|
358
370
|
}
|
|
359
371
|
// ─── Command handlers ────────────────────────────────────────────────────────
|
|
@@ -366,7 +378,7 @@ async function handleTask(args, ctx) {
|
|
|
366
378
|
ctx.ui.notify('Type your prompt after /task (use @ for file completion).', 'info');
|
|
367
379
|
return;
|
|
368
380
|
}
|
|
369
|
-
const { sessionCancelled } = await runSingleTask(ctx, cwd, raw);
|
|
381
|
+
const { sessionCancelled } = await runSingleTask(ctx, cwd, raw, { notifyFinish: true });
|
|
370
382
|
if (sessionCancelled) {
|
|
371
383
|
ctx.ui.notify('Could not start a fresh session for /task.', 'warning');
|
|
372
384
|
}
|
|
@@ -449,7 +461,7 @@ async function handleTaskResume(args, ctx) {
|
|
|
449
461
|
}
|
|
450
462
|
id = candidates[0].id;
|
|
451
463
|
}
|
|
452
|
-
const { sessionCancelled } = await runSingleTask(ctx, cwd, '', { resumeId: id });
|
|
464
|
+
const { sessionCancelled } = await runSingleTask(ctx, cwd, '', { resumeId: id, notifyFinish: true });
|
|
453
465
|
if (sessionCancelled) {
|
|
454
466
|
ctx.ui.notify('Could not start a fresh session for /task-resume.', 'warning');
|
|
455
467
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.14",
|
|
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",
|