@mjasnikovs/pi-task 0.2.2 → 0.4.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/README.md +13 -0
- package/dist/index.js +2 -0
- package/dist/remote/bridge.d.ts +78 -0
- package/dist/remote/bridge.js +184 -0
- package/dist/remote/broadcast.d.ts +6 -0
- package/dist/remote/broadcast.js +27 -0
- package/dist/remote/events.d.ts +3 -0
- package/dist/remote/events.js +81 -0
- package/dist/remote/history.d.ts +22 -0
- package/dist/remote/history.js +25 -0
- package/dist/remote/protocol.d.ts +42 -0
- package/dist/remote/protocol.js +13 -0
- package/dist/remote/qr.d.ts +1 -0
- package/dist/remote/qr.js +5 -0
- package/dist/remote/register.d.ts +2 -0
- package/dist/remote/register.js +149 -0
- package/dist/remote/server.d.ts +31 -0
- package/dist/remote/server.js +126 -0
- package/dist/remote/state.d.ts +2 -0
- package/dist/remote/state.js +7 -0
- package/dist/remote/ui.d.ts +1 -0
- package/dist/remote/ui.js +660 -0
- package/dist/task/auto-orchestrator.js +32 -5
- package/dist/task/orchestrator.js +10 -4
- package/dist/task/phases.js +9 -2
- package/dist/task/widget.js +15 -2
- package/package.json +16 -10
|
@@ -15,6 +15,7 @@ import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, check
|
|
|
15
15
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter } from './task-io.js';
|
|
16
16
|
import { gitCommitAll } from './auto-commit.js';
|
|
17
17
|
import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
|
|
18
|
+
import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
|
|
18
19
|
import { startAutoLoader } from './widget.js';
|
|
19
20
|
// Matches pi's @-file completion token (a path after @, until whitespace).
|
|
20
21
|
const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
|
|
@@ -55,6 +56,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
55
56
|
// with the model's recommended default pre-filled (Enter to accept, type to
|
|
56
57
|
// override); we never auto-answer. The model emits NONE when nothing remains.
|
|
57
58
|
const theme = ctx.ui.theme;
|
|
59
|
+
const ui = new SessionUI(ctx);
|
|
58
60
|
// Inline any @file spec the user referenced so clarify/decompose reason over
|
|
59
61
|
// the real content, not a one-line "Implement @file" that reads as trivial.
|
|
60
62
|
const featureForModel = await expandFeatureMentions(cwd, feature);
|
|
@@ -74,7 +76,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
74
76
|
const title = suggested ?
|
|
75
77
|
`${shownQ}\n${theme.fg('muted', 'Recommended:')}\n\n${renderInlineMarkdown(suggested, theme)}\n\n${theme.fg('muted', 'press Enter to accept')}`
|
|
76
78
|
: `${shownQ}\n${theme.fg('muted', '(no recommendation — please answer)')}`;
|
|
77
|
-
const a = await
|
|
79
|
+
const a = await ui.ask({
|
|
80
|
+
localTitle: title,
|
|
81
|
+
question: plainQ,
|
|
82
|
+
recommended: plainSuggested,
|
|
83
|
+
allowSkip: plainSuggested === undefined
|
|
84
|
+
});
|
|
78
85
|
if (a === undefined) {
|
|
79
86
|
ctx.ui.notify('/task-auto cancelled.', 'warning');
|
|
80
87
|
return null;
|
|
@@ -175,6 +182,7 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
175
182
|
}
|
|
176
183
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
177
184
|
let cancelRequested = false;
|
|
185
|
+
let autoRunning = false;
|
|
178
186
|
export function requestAutoCancel() {
|
|
179
187
|
cancelRequested = true;
|
|
180
188
|
}
|
|
@@ -242,6 +250,7 @@ async function handleTaskAuto(args, ctx) {
|
|
|
242
250
|
ctx.ui.notify('Describe the feature after /task-auto (use @ for file completion).', 'info');
|
|
243
251
|
return;
|
|
244
252
|
}
|
|
253
|
+
autoRunning = true;
|
|
245
254
|
const abort = new AbortController();
|
|
246
255
|
const deps = defaultDeps(ctx, cwd, abort.signal, deriveTitle(raw));
|
|
247
256
|
let id;
|
|
@@ -249,6 +258,7 @@ async function handleTaskAuto(args, ctx) {
|
|
|
249
258
|
id = await planAuto(ctx, cwd, raw, deps);
|
|
250
259
|
}
|
|
251
260
|
catch (err) {
|
|
261
|
+
autoRunning = false;
|
|
252
262
|
const msg = err instanceof Error ? err.message : String(err);
|
|
253
263
|
if (msg === USER_CANCELLED) {
|
|
254
264
|
ctx.ui.notify('/task-auto cancelled.', 'warning');
|
|
@@ -257,9 +267,20 @@ async function handleTaskAuto(args, ctx) {
|
|
|
257
267
|
ctx.ui.notify(`/task-auto planning failed: ${msg}`, 'error');
|
|
258
268
|
return;
|
|
259
269
|
}
|
|
260
|
-
if (!id)
|
|
270
|
+
if (!id) {
|
|
271
|
+
autoRunning = false;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Check for a cancel that was requested during the planning phase before the
|
|
275
|
+
// loop resets the flag.
|
|
276
|
+
if (cancelRequested) {
|
|
277
|
+
cancelRequested = false;
|
|
278
|
+
autoRunning = false;
|
|
279
|
+
ctx.ui.notify('/task-auto cancelled.', 'warning');
|
|
261
280
|
return;
|
|
281
|
+
}
|
|
262
282
|
await runAutoLoop(ctx, cwd, id, deps);
|
|
283
|
+
autoRunning = false;
|
|
263
284
|
}
|
|
264
285
|
async function handleTaskAutoResume(_args, ctx) {
|
|
265
286
|
await ctx.waitForIdle();
|
|
@@ -271,27 +292,33 @@ async function handleTaskAutoResume(_args, ctx) {
|
|
|
271
292
|
}
|
|
272
293
|
ctx.ui.notify(`Resuming ${id}…`, 'info');
|
|
273
294
|
await updateTaskFrontMatter(cwd, id, { state: 'in_progress' });
|
|
295
|
+
autoRunning = true;
|
|
274
296
|
const abort = new AbortController();
|
|
275
297
|
// Resume only runs the loop (runTask); no planning children, so the loader
|
|
276
298
|
// title is unused here — pass the id for clarity if that ever changes.
|
|
277
299
|
await runAutoLoop(ctx, cwd, id, defaultDeps(ctx, cwd, abort.signal, id));
|
|
300
|
+
autoRunning = false;
|
|
278
301
|
}
|
|
279
302
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
280
303
|
async function handleTaskAutoCancel(_args, ctx) {
|
|
304
|
+
if (!autoRunning) {
|
|
305
|
+
ctx.ui.notify('No /task-auto loop is running.', 'info');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
281
308
|
requestAutoCancel();
|
|
282
309
|
ctx.ui.notify('Stopping /task-auto after the current task…', 'warning');
|
|
283
310
|
}
|
|
284
311
|
// ─── Registration ────────────────────────────────────────────────────────────
|
|
285
312
|
export function registerTaskAuto(pi) {
|
|
286
|
-
pi
|
|
313
|
+
registerBridgeCommand(pi, 'task-auto', {
|
|
287
314
|
description: 'Plan a feature into tasks and run them. Usage: /task-auto <feature>',
|
|
288
315
|
handler: handleTaskAuto
|
|
289
316
|
});
|
|
290
|
-
pi
|
|
317
|
+
registerBridgeCommand(pi, 'task-auto-resume', {
|
|
291
318
|
description: 'Resume the active /task-auto run.',
|
|
292
319
|
handler: handleTaskAutoResume
|
|
293
320
|
});
|
|
294
|
-
pi
|
|
321
|
+
registerBridgeCommand(pi, 'task-auto-cancel', {
|
|
295
322
|
description: 'Stop the running /task-auto loop after the current task.',
|
|
296
323
|
handler: handleTaskAutoCancel
|
|
297
324
|
});
|
|
@@ -21,6 +21,7 @@ import { PHASES, postCommitPhase } from './phases.js';
|
|
|
21
21
|
import { handleFailure } from './failure-classifier.js';
|
|
22
22
|
import { PHASE_INDEX, PHASE_ORDER, allocateTaskId, ensureTasksDir, normaliseTaskId, parseFrontMatter, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile, extractSection, RESUMABLE_STATES } from './task-file.js';
|
|
23
23
|
import { startWidget, WIDGET_KEY } from './widget.js';
|
|
24
|
+
import { publishViewer, publishNotify, registerBridgeCommand } from '../remote/bridge.js';
|
|
24
25
|
import { parseVerifyBlock } from './parsers.js';
|
|
25
26
|
import { formatTimings } from './timings.js';
|
|
26
27
|
// ─── Module-level state ──────────────────────────────────────────────────────
|
|
@@ -341,6 +342,7 @@ async function handleTaskList(_args, ctx) {
|
|
|
341
342
|
if (lines.length === 0)
|
|
342
343
|
lines.push('(no tasks in .pi-tasks/)');
|
|
343
344
|
lines.push('', 'resume: /task-resume <id> (eligible: in_progress, pending, cancelled, failed)');
|
|
345
|
+
publishViewer('Tasks', lines.join('\n'));
|
|
344
346
|
await ctx.ui.editor('Tasks', lines.join('\n'));
|
|
345
347
|
}
|
|
346
348
|
async function handleTaskResume(args, ctx) {
|
|
@@ -354,6 +356,7 @@ async function handleTaskResume(args, ctx) {
|
|
|
354
356
|
}
|
|
355
357
|
catch {
|
|
356
358
|
ctx.ui.notify(`${id} not found in .pi-tasks/`, 'error');
|
|
359
|
+
publishNotify(`${id} not found in .pi-tasks/`, 'error');
|
|
357
360
|
return;
|
|
358
361
|
}
|
|
359
362
|
}
|
|
@@ -382,6 +385,7 @@ async function handleTaskResume(args, ctx) {
|
|
|
382
385
|
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
383
386
|
if (candidates.length === 0) {
|
|
384
387
|
ctx.ui.notify('No resumable tasks.', 'info');
|
|
388
|
+
publishNotify('No resumable tasks.', 'info');
|
|
385
389
|
return;
|
|
386
390
|
}
|
|
387
391
|
id = candidates[0].id;
|
|
@@ -393,27 +397,29 @@ async function handleTaskResume(args, ctx) {
|
|
|
393
397
|
async function handleTaskCancel(_args, ctx) {
|
|
394
398
|
if (!activeTask) {
|
|
395
399
|
ctx.ui.notify('No task is running.', 'info');
|
|
400
|
+
publishNotify('No task is running.', 'info');
|
|
396
401
|
return;
|
|
397
402
|
}
|
|
398
403
|
activeTask.cancel();
|
|
399
404
|
ctx.ui.notify(`Cancelling ${activeTask.taskId}…`, 'warning');
|
|
405
|
+
publishNotify(`Cancelling ${activeTask.taskId}…`, 'warning');
|
|
400
406
|
}
|
|
401
407
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
402
408
|
export function registerTask(pi) {
|
|
403
409
|
piApi = pi;
|
|
404
|
-
pi
|
|
410
|
+
registerBridgeCommand(pi, 'task', {
|
|
405
411
|
description: 'Start a new task. Usage: /task <prompt>',
|
|
406
412
|
handler: handleTask
|
|
407
413
|
});
|
|
408
|
-
pi
|
|
414
|
+
registerBridgeCommand(pi, 'task-list', {
|
|
409
415
|
description: 'List tasks in this project.',
|
|
410
416
|
handler: handleTaskList
|
|
411
417
|
});
|
|
412
|
-
pi
|
|
418
|
+
registerBridgeCommand(pi, 'task-resume', {
|
|
413
419
|
description: 'Resume a task. Usage: /task-resume [id]',
|
|
414
420
|
handler: handleTaskResume
|
|
415
421
|
});
|
|
416
|
-
pi
|
|
422
|
+
registerBridgeCommand(pi, 'task-cancel', {
|
|
417
423
|
description: 'Cancel the currently running task.',
|
|
418
424
|
handler: handleTaskCancel
|
|
419
425
|
});
|
package/dist/task/phases.js
CHANGED
|
@@ -15,6 +15,7 @@ import { setTaskSection, updateTaskFrontMatter } from './task-file.js';
|
|
|
15
15
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
16
16
|
import { parseVerifyBlock, parseGrillQuestions, parseAutoAnswer, parseVerifyToolingOutput, validateSpecShape, stripSpecPreamble, deriveTitle, isCritiqueClean } from './parsers.js';
|
|
17
17
|
import { runPhaseChild, runPhaseWithLoopGuard, runWithEmphasisRetry, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
18
|
+
import { SessionUI } from '../remote/bridge.js';
|
|
18
19
|
// ─── Re-export constants from their home modules ────────────────────────────
|
|
19
20
|
export { MAX_GRILL_QUESTIONS };
|
|
20
21
|
// ─── Tooling helpers ─────────────────────────────────────────────────────────
|
|
@@ -332,6 +333,7 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
332
333
|
// or surfaced as a pre-filled recommendation. The model emits NONE when
|
|
333
334
|
// nothing ambiguous remains. Kept in sync with /task-auto's clarify dialog.
|
|
334
335
|
const theme = ctx.ui.theme;
|
|
336
|
+
const ui = new SessionUI(ctx);
|
|
335
337
|
const out = []; // human-facing Q&A transcript (with auto-worker debug lines)
|
|
336
338
|
const qa = []; // compact Q&A fed back into the next question
|
|
337
339
|
// Open-ended: keep asking until the model emits NONE or the user dismisses.
|
|
@@ -361,11 +363,16 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
361
363
|
}
|
|
362
364
|
else {
|
|
363
365
|
const plainSuggested = auto.suggested === undefined ? undefined : stripInlineMarkdown(auto.suggested);
|
|
364
|
-
const
|
|
366
|
+
const localTitle = auto.suggested ?
|
|
365
367
|
`${shownQ}\n${theme.fg('muted', 'Recommended:')}\n\n${renderInlineMarkdown(auto.suggested, theme)}\n\n${theme.fg('muted', 'press Enter to accept')}`
|
|
366
368
|
: `${shownQ}\n${theme.fg('muted', '(no recommendation — please answer)')}`;
|
|
367
369
|
widgetState.lastLine = `awaiting Q${n + 1}`;
|
|
368
|
-
const a = await
|
|
370
|
+
const a = await ui.ask({
|
|
371
|
+
localTitle,
|
|
372
|
+
question: plainQ,
|
|
373
|
+
recommended: plainSuggested,
|
|
374
|
+
allowSkip: plainSuggested === undefined
|
|
375
|
+
});
|
|
369
376
|
if (a === undefined)
|
|
370
377
|
throw new Error(USER_CANCELLED);
|
|
371
378
|
const typed = a.trim();
|
package/dist/task/widget.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* context usage, and the latest child-process line.
|
|
6
6
|
*/
|
|
7
7
|
import { PHASE_INDEX, PHASE_ORDER } from './task-file.js';
|
|
8
|
+
import { publishWidget } from '../remote/bridge.js';
|
|
8
9
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
9
10
|
export const WIDGET_KEY = 'pi-tasks';
|
|
10
11
|
export const AUTO_WIDGET_KEY = 'pi-task-auto';
|
|
@@ -92,12 +93,15 @@ export function startWidget(ctx, getState) {
|
|
|
92
93
|
return () => { };
|
|
93
94
|
const render = () => {
|
|
94
95
|
const s = getState();
|
|
96
|
+
const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
|
|
97
|
+
const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire
|
|
95
98
|
try {
|
|
96
|
-
ctx.ui.setWidget(WIDGET_KEY,
|
|
99
|
+
ctx.ui.setWidget(WIDGET_KEY, lines);
|
|
97
100
|
}
|
|
98
101
|
catch {
|
|
99
102
|
/* stale ctx */
|
|
100
103
|
}
|
|
104
|
+
publishWidget(WIDGET_KEY, plain);
|
|
101
105
|
};
|
|
102
106
|
render();
|
|
103
107
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
@@ -129,12 +133,15 @@ export function startAutoLoader(ctx, getState) {
|
|
|
129
133
|
return () => { };
|
|
130
134
|
const render = () => {
|
|
131
135
|
const s = getState();
|
|
136
|
+
const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
|
|
137
|
+
const plain = s ? buildAutoLoaderLines(s, undefined) : undefined;
|
|
132
138
|
try {
|
|
133
|
-
ctx.ui.setWidget(AUTO_WIDGET_KEY,
|
|
139
|
+
ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
|
|
134
140
|
}
|
|
135
141
|
catch {
|
|
136
142
|
/* stale ctx */
|
|
137
143
|
}
|
|
144
|
+
publishWidget(AUTO_WIDGET_KEY, plain);
|
|
138
145
|
};
|
|
139
146
|
render();
|
|
140
147
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
@@ -147,6 +154,7 @@ export function startAutoLoader(ctx, getState) {
|
|
|
147
154
|
catch {
|
|
148
155
|
/* stale ctx */
|
|
149
156
|
}
|
|
157
|
+
publishWidget(AUTO_WIDGET_KEY, undefined);
|
|
150
158
|
};
|
|
151
159
|
}
|
|
152
160
|
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
@@ -163,8 +171,12 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
|
163
171
|
line = theme.fg('error', `✘ ${taskId} failed${reason ? ': ' + reason : ''}`);
|
|
164
172
|
clearMs = FAIL_CLEAR_MS;
|
|
165
173
|
}
|
|
174
|
+
const plainLine = state === 'cancelled' ?
|
|
175
|
+
`⚠ ${taskId} cancelled`
|
|
176
|
+
: `✘ ${taskId} failed${reason ? ': ' + reason : ''}`;
|
|
166
177
|
try {
|
|
167
178
|
ctx.ui.setWidget(WIDGET_KEY, [line]);
|
|
179
|
+
publishWidget(WIDGET_KEY, [plainLine]);
|
|
168
180
|
}
|
|
169
181
|
catch {
|
|
170
182
|
/* stale ctx */
|
|
@@ -172,6 +184,7 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
|
172
184
|
setTimeout(() => {
|
|
173
185
|
try {
|
|
174
186
|
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
187
|
+
publishWidget(WIDGET_KEY, undefined);
|
|
175
188
|
}
|
|
176
189
|
catch {
|
|
177
190
|
/* stale ctx */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Deterministic spec-orchestration for local models, with bundled web/docs/fetch/worker subagent tools.",
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -17,20 +17,24 @@
|
|
|
17
17
|
"prepublishOnly": "bun run build"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
21
|
-
"@earendil-works/pi-agent-core": "0.
|
|
22
|
-
"@earendil-works/pi-tui": "0.
|
|
20
|
+
"@earendil-works/pi-coding-agent": "0.78.1",
|
|
21
|
+
"@earendil-works/pi-agent-core": "0.78.1",
|
|
22
|
+
"@earendil-works/pi-tui": "0.78.1"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@mozilla/readability": "^0.6.0",
|
|
26
26
|
"@sinclair/typebox": "0.34.49",
|
|
27
27
|
"jsdom": "^29.1.1",
|
|
28
|
-
"
|
|
28
|
+
"qrcode": "^1.5.4",
|
|
29
|
+
"turndown": "^7.2.4",
|
|
30
|
+
"ws": "^8.18.0"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
32
|
-
"@earendil-works/pi-agent-core": "0.
|
|
33
|
-
"@earendil-works/pi-tui": "0.
|
|
33
|
+
"@earendil-works/pi-coding-agent": "0.78.1",
|
|
34
|
+
"@earendil-works/pi-agent-core": "0.78.1",
|
|
35
|
+
"@earendil-works/pi-tui": "0.78.1",
|
|
36
|
+
"@types/qrcode": "^1.5.5",
|
|
37
|
+
"@types/ws": "^8.5.14",
|
|
34
38
|
"@eslint/js": "10.0.1",
|
|
35
39
|
"@sinclair/typebox": "0.34.49",
|
|
36
40
|
"@types/bun": "1.3.12",
|
|
@@ -48,7 +52,9 @@
|
|
|
48
52
|
"pi-extension",
|
|
49
53
|
"coding-agent",
|
|
50
54
|
"task-orchestration",
|
|
51
|
-
"local-llm"
|
|
55
|
+
"local-llm",
|
|
56
|
+
"remote",
|
|
57
|
+
"web"
|
|
52
58
|
],
|
|
53
59
|
"repository": {
|
|
54
60
|
"type": "git",
|