@mjasnikovs/pi-task 0.13.7 → 0.13.9
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/config/register.js +1 -1
- package/dist/remote/bridge.d.ts +22 -0
- package/dist/remote/bridge.js +29 -3
- package/dist/remote/register.js +7 -3
- package/dist/remote/server.d.ts +4 -2
- package/dist/remote/server.js +7 -3
- package/dist/remote/tailscale.d.ts +8 -2
- package/dist/remote/tailscale.js +13 -6
- package/dist/task/auto-orchestrator.js +48 -7
- package/dist/task/auto-prompts.d.ts +4 -3
- package/dist/task/auto-prompts.js +9 -6
- package/dist/task/parsers.d.ts +3 -0
- package/dist/task/parsers.js +31 -3
- package/dist/task/phases.js +22 -10
- package/dist/workers/html-clean.js +7 -4
- package/package.json +1 -1
package/dist/config/register.js
CHANGED
|
@@ -11,7 +11,7 @@ const ITEMS = [
|
|
|
11
11
|
{
|
|
12
12
|
id: 'autoCommit',
|
|
13
13
|
label: 'auto-commit',
|
|
14
|
-
description: 'git commit
|
|
14
|
+
description: 'git commit around each /task-auto sub-task (checkpoint before, snapshot after)'
|
|
15
15
|
}
|
|
16
16
|
];
|
|
17
17
|
function makeTheme(theme) {
|
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -28,6 +28,19 @@ export interface AskSpec {
|
|
|
28
28
|
recommended2?: string;
|
|
29
29
|
/** Whether the browser card shows a Skip button (answers with empty string). */
|
|
30
30
|
allowSkip: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* When set, the local TUI shows a select() picker of these entries instead of
|
|
33
|
+
* a bare text input — one option per line, arrow-key navigable. Each entry's
|
|
34
|
+
* `label` is what the picker displays; its `value` is what ask() resolves to
|
|
35
|
+
* when chosen. A built-in "type a different answer" entry is appended that
|
|
36
|
+
* falls back to a text input, preserving the free-text override. Used for the
|
|
37
|
+
* binary A/B grill/clarify fork. Remote browsers ignore this and keep
|
|
38
|
+
* rendering recommended/recommended2 as buttons.
|
|
39
|
+
*/
|
|
40
|
+
options?: {
|
|
41
|
+
label: string;
|
|
42
|
+
value: string;
|
|
43
|
+
}[];
|
|
31
44
|
}
|
|
32
45
|
/** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
|
|
33
46
|
export declare class SessionUI {
|
|
@@ -38,6 +51,15 @@ export declare class SessionUI {
|
|
|
38
51
|
get hasUI(): boolean;
|
|
39
52
|
/** Race the local input against a remote answer; first to settle wins. */
|
|
40
53
|
ask(spec: AskSpec): Promise<string | undefined>;
|
|
54
|
+
/**
|
|
55
|
+
* The local-TUI half of ask(). With `spec.options` it renders a select()
|
|
56
|
+
* picker (each option on its own line) plus a trailing "type a different
|
|
57
|
+
* answer" entry that drops to a text input; the chosen entry's `value` is
|
|
58
|
+
* returned. Without options it falls back to a single text input. Cancelling
|
|
59
|
+
* either dialog (or an abort when the remote wins the race) resolves to
|
|
60
|
+
* undefined.
|
|
61
|
+
*/
|
|
62
|
+
private askLocal;
|
|
41
63
|
}
|
|
42
64
|
export declare function publishNotify(message: string, level: 'info' | 'warning' | 'error'): void;
|
|
43
65
|
export declare function publishViewer(title: string, text: string): void;
|
package/dist/remote/bridge.js
CHANGED
|
@@ -25,6 +25,9 @@ export function answerPrompt(id, value) {
|
|
|
25
25
|
b.pending.delete(id);
|
|
26
26
|
settle(value);
|
|
27
27
|
}
|
|
28
|
+
/** Trailing picker entry that drops to a free-text input — the local mirror of
|
|
29
|
+
* the remote card's "✎ Manual answer" button. */
|
|
30
|
+
const TYPE_OWN_LABEL = 'Type a different answer…';
|
|
28
31
|
/** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
|
|
29
32
|
export class SessionUI {
|
|
30
33
|
ctx;
|
|
@@ -63,9 +66,7 @@ export class SessionUI {
|
|
|
63
66
|
// Local: resolves to a value/undefined, or undefined on abort. Swallow
|
|
64
67
|
// the rejection some implementations throw on abort so it never leaks.
|
|
65
68
|
const local = this.ctx.hasUI ?
|
|
66
|
-
this.
|
|
67
|
-
.input(spec.localTitle, spec.recommended, { signal: ac.signal })
|
|
68
|
-
.catch(() => undefined)
|
|
69
|
+
this.askLocal(spec, ac.signal).catch(() => undefined)
|
|
69
70
|
: new Promise(() => { });
|
|
70
71
|
try {
|
|
71
72
|
const winner = await Promise.race([
|
|
@@ -81,6 +82,31 @@ export class SessionUI {
|
|
|
81
82
|
clearPrompt(id);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The local-TUI half of ask(). With `spec.options` it renders a select()
|
|
87
|
+
* picker (each option on its own line) plus a trailing "type a different
|
|
88
|
+
* answer" entry that drops to a text input; the chosen entry's `value` is
|
|
89
|
+
* returned. Without options it falls back to a single text input. Cancelling
|
|
90
|
+
* either dialog (or an abort when the remote wins the race) resolves to
|
|
91
|
+
* undefined.
|
|
92
|
+
*/
|
|
93
|
+
async askLocal(spec, signal) {
|
|
94
|
+
const opts = spec.options;
|
|
95
|
+
if (opts && opts.length > 0) {
|
|
96
|
+
const labels = opts.map(o => o.label);
|
|
97
|
+
const choice = await this.ctx.ui.select(spec.localTitle, [...labels, TYPE_OWN_LABEL], {
|
|
98
|
+
signal
|
|
99
|
+
});
|
|
100
|
+
if (choice === undefined)
|
|
101
|
+
return undefined; // cancelled / aborted
|
|
102
|
+
if (choice === TYPE_OWN_LABEL) {
|
|
103
|
+
return this.ctx.ui.input(spec.localTitle, undefined, { signal });
|
|
104
|
+
}
|
|
105
|
+
const hit = opts.find(o => o.label === choice);
|
|
106
|
+
return hit ? hit.value : choice;
|
|
107
|
+
}
|
|
108
|
+
return this.ctx.ui.input(spec.localTitle, spec.recommended, { signal });
|
|
109
|
+
}
|
|
84
110
|
}
|
|
85
111
|
export function publishNotify(message, level) {
|
|
86
112
|
getBridge().broadcast({ type: 'notify', message, level });
|
package/dist/remote/register.js
CHANGED
|
@@ -5,7 +5,7 @@ import { reset, addUserTurn } from './session-state.js';
|
|
|
5
5
|
import { html } from './ui.js';
|
|
6
6
|
import { qrLines } from './qr.js';
|
|
7
7
|
import { startServer, formatAddresses } from './server.js';
|
|
8
|
-
import { ensureTailscaleServe, teardownTailscaleServe, planRemoteUrls } from './tailscale.js';
|
|
8
|
+
import { ensureTailscaleServe, teardownTailscaleServe, planRemoteUrls, hostFromResult } from './tailscale.js';
|
|
9
9
|
import { isAgentIdle } from './state.js';
|
|
10
10
|
const _g = globalThis;
|
|
11
11
|
if (!_g.__piRemote)
|
|
@@ -105,10 +105,14 @@ export function registerRemote(pi) {
|
|
|
105
105
|
const server = await ensureServer();
|
|
106
106
|
const httpPrimary = `http://${server.ip}:${server.port}`;
|
|
107
107
|
const result = S.serveResult ?? { state: 'unavailable' };
|
|
108
|
-
const plan = planRemoteUrls(httpPrimary, result);
|
|
108
|
+
const plan = planRemoteUrls(httpPrimary, result, server.port);
|
|
109
109
|
const primaryUrl = plan.primaryUrl;
|
|
110
110
|
const qr = await qrLines(primaryUrl);
|
|
111
|
-
const
|
|
111
|
+
const tsHost = hostFromResult(result);
|
|
112
|
+
const addrs = [
|
|
113
|
+
...plan.urlLines,
|
|
114
|
+
...formatAddresses(server.ips, server.port, tsHost)
|
|
115
|
+
];
|
|
112
116
|
const labelW = addrs.reduce((m, a) => Math.max(m, a.label.length), 0);
|
|
113
117
|
const addrLines = [
|
|
114
118
|
...addrs.map(a => (a.label ? `${a.label.padEnd(labelW)} ${a.url}` : a.url)),
|
package/dist/remote/server.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export interface ServerHandle {
|
|
|
24
24
|
type MessageCallback = (text: string) => void;
|
|
25
25
|
export declare function getLocalIPs(nets?: NodeJS.Dict<import("node:os").NetworkInterfaceInfo[]>): LocalIPs;
|
|
26
26
|
/** Build the labeled URL lines shown under the QR code. Both Tailscale and LAN
|
|
27
|
-
* when present; a single unlabeled primary URL when neither resolves.
|
|
28
|
-
|
|
27
|
+
* when present; a single unlabeled primary URL when neither resolves. The
|
|
28
|
+
* Tailscale line uses the MagicDNS host when known (resolves to the same node,
|
|
29
|
+
* but is what SSH and webpush certs need), falling back to the raw IP. */
|
|
30
|
+
export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: string): AddressLine[];
|
|
29
31
|
export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string): Promise<ServerHandle>;
|
|
30
32
|
export {};
|
package/dist/remote/server.js
CHANGED
|
@@ -33,10 +33,14 @@ export function getLocalIPs(nets = networkInterfaces()) {
|
|
|
33
33
|
return { tailscale, lan, primary: tailscale ?? lan ?? '127.0.0.1' };
|
|
34
34
|
}
|
|
35
35
|
/** Build the labeled URL lines shown under the QR code. Both Tailscale and LAN
|
|
36
|
-
* when present; a single unlabeled primary URL when neither resolves.
|
|
37
|
-
|
|
36
|
+
* when present; a single unlabeled primary URL when neither resolves. The
|
|
37
|
+
* Tailscale line uses the MagicDNS host when known (resolves to the same node,
|
|
38
|
+
* but is what SSH and webpush certs need), falling back to the raw IP. */
|
|
39
|
+
export function formatAddresses(ips, port, tsHost) {
|
|
38
40
|
const out = [];
|
|
39
|
-
if (
|
|
41
|
+
if (tsHost)
|
|
42
|
+
out.push({ label: 'Tailscale', url: `http://${tsHost}:${port}` });
|
|
43
|
+
else if (ips.tailscale)
|
|
40
44
|
out.push({ label: 'Tailscale', url: `http://${ips.tailscale}:${port}` });
|
|
41
45
|
if (ips.lan)
|
|
42
46
|
out.push({ label: 'LAN', url: `http://${ips.lan}:${port}` });
|
|
@@ -9,6 +9,7 @@ export interface Run {
|
|
|
9
9
|
export type ServeResult = {
|
|
10
10
|
state: 'served';
|
|
11
11
|
url: string;
|
|
12
|
+
host: string;
|
|
12
13
|
} | {
|
|
13
14
|
state: 'foreign-conflict';
|
|
14
15
|
host: string;
|
|
@@ -18,6 +19,8 @@ export type ServeResult = {
|
|
|
18
19
|
} | {
|
|
19
20
|
state: 'unavailable';
|
|
20
21
|
};
|
|
22
|
+
/** The MagicDNS host of a serve result, or undefined when the daemon had no name. */
|
|
23
|
+
export declare function hostFromResult(result: ServeResult): string | undefined;
|
|
21
24
|
export interface RemoteUrlPlan {
|
|
22
25
|
/** URL to encode in the QR and announce; the https one when serve is live. */
|
|
23
26
|
primaryUrl: string;
|
|
@@ -41,5 +44,8 @@ export declare function ensureTailscaleServe(port: number, run?: Run): Promise<S
|
|
|
41
44
|
* Best-effort; callers should additionally `.catch()` since it may run during
|
|
42
45
|
* shutdown. */
|
|
43
46
|
export declare function teardownTailscaleServe(port: number, run?: Run): Promise<void>;
|
|
44
|
-
/** Pure: pick the primary URL and any hint lines from a
|
|
45
|
-
|
|
47
|
+
/** Pure: pick the primary URL (what the QR encodes) and any hint lines from a
|
|
48
|
+
* serve result. Prefers the MagicDNS host over the raw IP whenever it's known —
|
|
49
|
+
* the https URL when serve is live, else http://<host>:<port> — so the QR and
|
|
50
|
+
* the announced URL carry the tailnet name (needed for SSH and webpush certs). */
|
|
51
|
+
export declare function planRemoteUrls(httpPrimary: string, result: ServeResult, port: number): RemoteUrlPlan;
|
package/dist/remote/tailscale.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
/** The MagicDNS host of a serve result, or undefined when the daemon had no name. */
|
|
3
|
+
export function hostFromResult(result) {
|
|
4
|
+
return result.state === 'unavailable' ? undefined : result.host;
|
|
5
|
+
}
|
|
2
6
|
const defaultRun = (cmd, args) => new Promise(resolve => {
|
|
3
7
|
execFile(cmd, args, { timeout: 5000 }, (err, stdout, stderr) => {
|
|
4
8
|
const exitCode = err && typeof err.code === 'number' ?
|
|
@@ -54,7 +58,7 @@ export async function ensureTailscaleServe(port, run = defaultRun) {
|
|
|
54
58
|
const serve = await run('tailscale', ['serve', 'status', '--json']);
|
|
55
59
|
const target = serve443Target(serve.stdout);
|
|
56
60
|
if (target === ours)
|
|
57
|
-
return { state: 'served', url: `https://${host}
|
|
61
|
+
return { state: 'served', url: `https://${host}`, host };
|
|
58
62
|
if (target !== undefined)
|
|
59
63
|
return { state: 'foreign-conflict', host };
|
|
60
64
|
// No :443 handler yet — check cert capability before trying to create one.
|
|
@@ -64,7 +68,7 @@ export async function ensureTailscaleServe(port, run = defaultRun) {
|
|
|
64
68
|
}
|
|
65
69
|
const set = await run('tailscale', ['serve', '--bg', '--https=443', ours]);
|
|
66
70
|
if (set.exitCode === 0)
|
|
67
|
-
return { state: 'served', url: `https://${host}
|
|
71
|
+
return { state: 'served', url: `https://${host}`, host };
|
|
68
72
|
return { state: 'certs-disabled', host };
|
|
69
73
|
}
|
|
70
74
|
/** Tear down the :443 serve handler ONLY if it currently points at our port.
|
|
@@ -76,8 +80,11 @@ export async function teardownTailscaleServe(port, run = defaultRun) {
|
|
|
76
80
|
await run('tailscale', ['serve', '--https=443', 'off']);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
/** Pure: pick the primary URL and any hint lines from a
|
|
80
|
-
|
|
83
|
+
/** Pure: pick the primary URL (what the QR encodes) and any hint lines from a
|
|
84
|
+
* serve result. Prefers the MagicDNS host over the raw IP whenever it's known —
|
|
85
|
+
* the https URL when serve is live, else http://<host>:<port> — so the QR and
|
|
86
|
+
* the announced URL carry the tailnet name (needed for SSH and webpush certs). */
|
|
87
|
+
export function planRemoteUrls(httpPrimary, result, port) {
|
|
81
88
|
switch (result.state) {
|
|
82
89
|
case 'served':
|
|
83
90
|
return {
|
|
@@ -87,7 +94,7 @@ export function planRemoteUrls(httpPrimary, result) {
|
|
|
87
94
|
};
|
|
88
95
|
case 'foreign-conflict':
|
|
89
96
|
return {
|
|
90
|
-
primaryUrl:
|
|
97
|
+
primaryUrl: `http://${result.host}:${port}`,
|
|
91
98
|
urlLines: [],
|
|
92
99
|
hintLines: [
|
|
93
100
|
'HTTPS: port 443 is already used by another tailscale serve config; not touching it.',
|
|
@@ -96,7 +103,7 @@ export function planRemoteUrls(httpPrimary, result) {
|
|
|
96
103
|
};
|
|
97
104
|
case 'certs-disabled':
|
|
98
105
|
return {
|
|
99
|
-
primaryUrl:
|
|
106
|
+
primaryUrl: `http://${result.host}:${port}`,
|
|
100
107
|
urlLines: [],
|
|
101
108
|
hintLines: [
|
|
102
109
|
'HTTPS (for phone notifications): enable HTTPS in the Tailscale admin console, then restart the remote.'
|
|
@@ -55,8 +55,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
55
55
|
// clarify — sequential & adaptive: ask one question at a time, feeding every
|
|
56
56
|
// answer back into the next call so later questions react to earlier ones
|
|
57
57
|
// (e.g. a framework choice reshapes what gets asked). Each question is shown
|
|
58
|
-
//
|
|
59
|
-
//
|
|
58
|
+
// exactly like /task's grill dialog: a binary fork offers two options (A/B),
|
|
59
|
+
// otherwise the model's recommendation is shown as the input placeholder and
|
|
60
|
+
// in the title. Nothing is pre-filled into the editor — submitting an empty
|
|
61
|
+
// field is what accepts the recommendation (see the typed.length === 0 branch
|
|
62
|
+
// below); typing overrides it. We never auto-answer; the model emits NONE when
|
|
63
|
+
// nothing remains.
|
|
60
64
|
const theme = ctx.ui.theme;
|
|
61
65
|
const ui = new SessionUI(ctx);
|
|
62
66
|
// Inline any @file spec the user referenced so clarify/decompose reason over
|
|
@@ -69,26 +73,46 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
69
73
|
const parsed = parseClarifyList(qRaw);
|
|
70
74
|
if (parsed.length === 0)
|
|
71
75
|
break; // NONE / nothing left to ask
|
|
72
|
-
const { question, suggested } = parsed[0];
|
|
76
|
+
const { question, suggested, alt } = parsed[0];
|
|
73
77
|
// Render markdown (bold/code) for the displayed prompt; keep plain text
|
|
74
78
|
// for the editable default and the persisted file.
|
|
75
79
|
const shownQ = renderInlineMarkdown(question, theme);
|
|
76
80
|
const plainQ = stripInlineMarkdown(question);
|
|
77
81
|
const plainSuggested = suggested === undefined ? undefined : stripInlineMarkdown(suggested);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const plainAlt = alt === undefined ? undefined : stripInlineMarkdown(alt);
|
|
83
|
+
// Identical to /task's grill dialog: a binary fork becomes a select()
|
|
84
|
+
// picker locally — each option on its own line, labelled A/B; a single
|
|
85
|
+
// recommendation rides under the question as the input default; an open
|
|
86
|
+
// question shows the bare prompt. No verbose "Recommended:" /
|
|
87
|
+
// "press Enter to accept" scaffolding.
|
|
88
|
+
const twoOption = plainSuggested !== undefined && plainAlt !== undefined;
|
|
89
|
+
const title = !twoOption && plainSuggested ?
|
|
90
|
+
`${shownQ}\n${renderInlineMarkdown(suggested, theme)}`
|
|
91
|
+
: shownQ;
|
|
81
92
|
const a = await ui.ask({
|
|
82
93
|
localTitle: title,
|
|
83
94
|
question: plainQ,
|
|
84
95
|
recommended: plainSuggested,
|
|
85
|
-
|
|
96
|
+
...(plainAlt !== undefined && { recommended2: plainAlt }),
|
|
97
|
+
allowSkip: plainSuggested === undefined && plainAlt === undefined,
|
|
98
|
+
...(twoOption && {
|
|
99
|
+
options: [
|
|
100
|
+
{
|
|
101
|
+
label: `A: ${renderInlineMarkdown(suggested, theme)}`,
|
|
102
|
+
value: plainSuggested
|
|
103
|
+
},
|
|
104
|
+
{ label: `B: ${renderInlineMarkdown(alt, theme)}`, value: plainAlt }
|
|
105
|
+
]
|
|
106
|
+
})
|
|
86
107
|
});
|
|
87
108
|
if (a === undefined) {
|
|
88
109
|
ctx.ui.notify('/task-auto cancelled.', 'warning');
|
|
89
110
|
return null;
|
|
90
111
|
}
|
|
91
112
|
const typed = a.trim();
|
|
113
|
+
// The local picker resolves to the chosen option's full value, but a
|
|
114
|
+
// remote user (or the picker's free-text fallback) may still type a bare
|
|
115
|
+
// "A"/"B" — map those back to the option's full text. Mirrors phaseGrill.
|
|
92
116
|
let answer;
|
|
93
117
|
if (typed.length === 0 && plainSuggested) {
|
|
94
118
|
answer = `${plainSuggested} (accepted recommendation)`;
|
|
@@ -96,6 +120,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
96
120
|
else if (typed.length === 0) {
|
|
97
121
|
answer = '(skipped)';
|
|
98
122
|
}
|
|
123
|
+
else if (twoOption && /^a[.)]?$/i.test(typed)) {
|
|
124
|
+
answer = plainSuggested;
|
|
125
|
+
}
|
|
126
|
+
else if (twoOption && /^b[.)]?$/i.test(typed)) {
|
|
127
|
+
answer = plainAlt;
|
|
128
|
+
}
|
|
99
129
|
else {
|
|
100
130
|
answer = typed;
|
|
101
131
|
}
|
|
@@ -230,6 +260,17 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
230
260
|
resumeId = undefined;
|
|
231
261
|
}
|
|
232
262
|
}
|
|
263
|
+
// Before starting, fold any uncommitted work into its own checkpoint
|
|
264
|
+
// commit so a dirty tree at the start of the run — or edits left behind
|
|
265
|
+
// by an interrupted/failed task — land separately instead of being swept
|
|
266
|
+
// into this task's snapshot. Best-effort and a no-op on a clean tree
|
|
267
|
+
// (gitCommitAll commits nothing), so it only ever produces a commit when
|
|
268
|
+
// there is stray work; the matching post-task commit below is the "after"
|
|
269
|
+
// half. Only the success path is announced to keep the common no-op quiet.
|
|
270
|
+
const checkpoint = await deps.commit(cwd, `chore: checkpoint before "${next.title}"`);
|
|
271
|
+
if (checkpoint.committed) {
|
|
272
|
+
active.ui.notify(`${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
273
|
+
}
|
|
233
274
|
const res = await deps.runTask(active, cwd, next.title, {
|
|
234
275
|
resumeId,
|
|
235
276
|
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
7
|
-
* single numbered question followed by a "SUGGESTED: <default>" line,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
8
|
+
* "ALT: <alternative>" line for binary "A or B?" forks, or the literal token NONE
|
|
9
|
+
* when no clarification remains. priorQA carries the questions already answered so
|
|
10
|
+
* each next question adapts to them.
|
|
10
11
|
*/
|
|
11
12
|
export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) => string;
|
|
12
13
|
/**
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
7
|
-
* single numbered question followed by a "SUGGESTED: <default>" line,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
8
|
+
* "ALT: <alternative>" line for binary "A or B?" forks, or the literal token NONE
|
|
9
|
+
* when no clarification remains. priorQA carries the questions already answered so
|
|
10
|
+
* each next question adapts to them.
|
|
10
11
|
*/
|
|
11
12
|
export const AUTO_CLARIFY_PROMPT = (feature, priorQA) => `You are planning how to split a feature into separate implementation tasks, one clarifying question at a time.
|
|
12
13
|
|
|
@@ -33,14 +34,16 @@ fork the breakdown). Account for the answers so far:
|
|
|
33
34
|
real-time vs polling transport, search, deployment).
|
|
34
35
|
- Skip anything /task will naturally resolve per-task during its own research.
|
|
35
36
|
|
|
36
|
-
Also propose the
|
|
37
|
-
|
|
37
|
+
Also propose the most sensible default answer for this question, inferred from
|
|
38
|
+
the repo, the referenced docs, and any stated philosophy or constraints —
|
|
38
39
|
concrete and decisive, shown to the user as a recommendation they can accept or
|
|
39
|
-
override.
|
|
40
|
+
override. When the question is a genuine binary "A or B?" fork, also give the
|
|
41
|
+
single best alternative as a second option; otherwise offer only the one default.
|
|
40
42
|
|
|
41
43
|
OUTPUT FORMAT (exact):
|
|
42
44
|
- One clarifying question as a single numbered line: "1. ...".
|
|
43
45
|
- On the NEXT line (never inline), a line that begins with "SUGGESTED: <your recommended default>".
|
|
46
|
+
- 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.
|
|
44
47
|
- 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).
|
|
45
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:
|
|
46
49
|
NONE`;
|
package/dist/task/parsers.d.ts
CHANGED
|
@@ -17,9 +17,12 @@ export type AutoAnswer = {
|
|
|
17
17
|
export interface ClarifyQuestion {
|
|
18
18
|
question: string;
|
|
19
19
|
suggested?: string;
|
|
20
|
+
/** Secondary option for a binary "A or B?" fork; mirrors grill's ALT line. */
|
|
21
|
+
alt?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare const GRILL_LINE_RE: RegExp;
|
|
22
24
|
export declare const SUGGESTED_LINE_RE: RegExp;
|
|
25
|
+
export declare const ALT_LINE_RE: RegExp;
|
|
23
26
|
export declare function parseGrillQuestions(raw: string): string[];
|
|
24
27
|
export declare function parseClarifyList(raw: string): ClarifyQuestion[];
|
|
25
28
|
export declare function autoAnswerHasTag(raw: string): boolean;
|
package/dist/task/parsers.js
CHANGED
|
@@ -7,6 +7,7 @@ import { MAX_GRILL_QUESTIONS } from './phases.js';
|
|
|
7
7
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
8
8
|
export const GRILL_LINE_RE = /^\s*\d+[.)]\s+(.+)$/;
|
|
9
9
|
export const SUGGESTED_LINE_RE = /^\s*SUGGESTED:\s*(.*)$/i;
|
|
10
|
+
export const ALT_LINE_RE = /^\s*ALT:\s*(.*)$/i;
|
|
10
11
|
// ─── Grill questions parser ──────────────────────────────────────────────────
|
|
11
12
|
// The grill-gen prompt instructs the worker to emit the literal token `NONE`
|
|
12
13
|
// when it has zero questions, so the runner's empty-output guard can still
|
|
@@ -30,14 +31,32 @@ export function parseGrillQuestions(raw) {
|
|
|
30
31
|
// (e.g. "1. ...so this must be resolved. SUGGESTED: use polling.") rather than
|
|
31
32
|
// on its own line.
|
|
32
33
|
const INLINE_SUGGESTED_RE = /\bSUGGESTED:\s*/i;
|
|
33
|
-
|
|
34
|
+
const INLINE_ALT_RE = /\bALT:\s*/i;
|
|
35
|
+
/**
|
|
36
|
+
* Split a question line's text into the question, any inline SUGGESTED default,
|
|
37
|
+
* and any inline ALT secondary option (the model may write both on one line:
|
|
38
|
+
* "1. A or B? SUGGESTED: A ALT: B").
|
|
39
|
+
*/
|
|
34
40
|
function splitInlineSuggested(text) {
|
|
35
41
|
const m = INLINE_SUGGESTED_RE.exec(text);
|
|
36
42
|
if (!m)
|
|
37
43
|
return { question: text.trim() };
|
|
38
44
|
const question = text.slice(0, m.index).trim();
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
let rest = text.slice(m.index + m[0].length);
|
|
46
|
+
let alt;
|
|
47
|
+
const altM = INLINE_ALT_RE.exec(rest);
|
|
48
|
+
if (altM) {
|
|
49
|
+
const altText = rest.slice(altM.index + altM[0].length).trim();
|
|
50
|
+
if (altText.length > 0)
|
|
51
|
+
alt = altText;
|
|
52
|
+
rest = rest.slice(0, altM.index);
|
|
53
|
+
}
|
|
54
|
+
const suggested = rest.trim();
|
|
55
|
+
return {
|
|
56
|
+
question,
|
|
57
|
+
...(suggested.length > 0 && { suggested }),
|
|
58
|
+
...(alt !== undefined && { alt })
|
|
59
|
+
};
|
|
41
60
|
}
|
|
42
61
|
// Parses the /task-auto clarify output: a numbered question list where each
|
|
43
62
|
// question carries a "SUGGESTED: <default>" recommendation — either on its own
|
|
@@ -67,6 +86,15 @@ export function parseClarifyList(raw) {
|
|
|
67
86
|
if (suggested.length > 0 && last.suggested === undefined) {
|
|
68
87
|
last.suggested = suggested;
|
|
69
88
|
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const altLine = ALT_LINE_RE.exec(line);
|
|
92
|
+
if (altLine && out.length > 0) {
|
|
93
|
+
const alt = altLine[1].trim();
|
|
94
|
+
const last = out[out.length - 1];
|
|
95
|
+
if (alt.length > 0 && last.alt === undefined) {
|
|
96
|
+
last.alt = alt;
|
|
97
|
+
}
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
return out;
|
package/dist/task/phases.js
CHANGED
|
@@ -326,10 +326,13 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
326
326
|
else {
|
|
327
327
|
const plainSuggested = auto.suggested === undefined ? undefined : stripInlineMarkdown(auto.suggested);
|
|
328
328
|
const plainAlt = auto.alt === undefined ? undefined : stripInlineMarkdown(auto.alt);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
// A binary fork (suggested + alt) becomes a select() picker locally —
|
|
330
|
+
// each option on its own line, labelled A/B. A single recommendation
|
|
331
|
+
// rides along under the question as the input default; an open
|
|
332
|
+
// question shows the bare prompt.
|
|
333
|
+
const twoOption = plainSuggested !== undefined && plainAlt !== undefined;
|
|
334
|
+
const localTitle = !twoOption && plainSuggested ?
|
|
335
|
+
`${shownQ}\n${renderInlineMarkdown(auto.suggested, theme)}`
|
|
333
336
|
: shownQ;
|
|
334
337
|
widgetState.lastLine = `awaiting Q${n + 1}`;
|
|
335
338
|
const a = await ui.ask({
|
|
@@ -337,16 +340,25 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
337
340
|
question: plainQ,
|
|
338
341
|
recommended: plainSuggested,
|
|
339
342
|
recommended2: plainAlt,
|
|
340
|
-
allowSkip: plainSuggested === undefined && plainAlt === undefined
|
|
343
|
+
allowSkip: plainSuggested === undefined && plainAlt === undefined,
|
|
344
|
+
...(twoOption && {
|
|
345
|
+
options: [
|
|
346
|
+
{
|
|
347
|
+
label: `A: ${renderInlineMarkdown(auto.suggested, theme)}`,
|
|
348
|
+
value: plainSuggested
|
|
349
|
+
},
|
|
350
|
+
{ label: `B: ${renderInlineMarkdown(auto.alt, theme)}`, value: plainAlt }
|
|
351
|
+
]
|
|
352
|
+
})
|
|
341
353
|
});
|
|
342
354
|
if (a === undefined)
|
|
343
355
|
throw new Error(USER_CANCELLED);
|
|
344
356
|
const typed = a.trim();
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// leaves the next grill-gen call a
|
|
349
|
-
|
|
357
|
+
// The local picker resolves to the chosen option's full value, but a
|
|
358
|
+
// remote user (or the picker's free-text fallback) may still type a
|
|
359
|
+
// bare "A"/"B" — map those back to the option's full text, since
|
|
360
|
+
// storing the literal letter leaves the next grill-gen call a
|
|
361
|
+
// dangling reference it can't decode.
|
|
350
362
|
if (typed.length === 0 && plainSuggested) {
|
|
351
363
|
answer = plainSuggested;
|
|
352
364
|
}
|
|
@@ -11,21 +11,22 @@ const turndown = new TurndownService({
|
|
|
11
11
|
});
|
|
12
12
|
export function cleanHtml(html, baseUrl) {
|
|
13
13
|
const { document } = parseHTML(html);
|
|
14
|
+
const doc = document;
|
|
14
15
|
const reader = new Readability(document);
|
|
15
16
|
const parsed = reader.parse();
|
|
16
17
|
if (parsed && parsed.content) {
|
|
17
18
|
return {
|
|
18
|
-
title: parsed.title ||
|
|
19
|
+
title: parsed.title || doc.title || new URL(baseUrl).hostname,
|
|
19
20
|
markdown: turndown.turndown(parsed.content).trim(),
|
|
20
21
|
finalUrl: baseUrl
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
// Fallback: turndown the body
|
|
24
|
-
const body =
|
|
25
|
+
const body = doc.body;
|
|
25
26
|
const bodyHtml = body ? body.innerHTML : '';
|
|
26
27
|
const markdown = turndown.turndown(bodyHtml).trim();
|
|
27
28
|
return {
|
|
28
|
-
title:
|
|
29
|
+
title: doc.title || new URL(baseUrl).hostname,
|
|
29
30
|
markdown,
|
|
30
31
|
finalUrl: baseUrl
|
|
31
32
|
};
|
|
@@ -147,7 +148,9 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
147
148
|
let bytesRead = 0;
|
|
148
149
|
try {
|
|
149
150
|
while (true) {
|
|
150
|
-
|
|
151
|
+
// response.body's stream type doesn't resolve here, so the chunk
|
|
152
|
+
// surfaces as `any`; pin it to the Uint8Array the reader yields.
|
|
153
|
+
const { value, done } = (await reader.read());
|
|
151
154
|
if (done)
|
|
152
155
|
break;
|
|
153
156
|
if (value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
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",
|