@mjasnikovs/pi-task 0.38.22 → 0.38.24
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 +2 -2
- package/dist/config/reasoning-args.d.ts +12 -1
- package/dist/config/reasoning-args.js +5 -2
- package/dist/config/reasoning.d.ts +60 -2
- package/dist/config/reasoning.js +344 -37
- package/dist/config/register.d.ts +76 -32
- package/dist/config/register.js +124 -82
- package/dist/shared/reasoning-capability.d.ts +2 -5
- package/dist/shared/reasoning-capability.js +31 -4
- package/dist/task/auto-orchestrator.d.ts +2 -0
- package/dist/task/auto-orchestrator.js +28 -41
- package/dist/task/child-runner.d.ts +89 -24
- package/dist/task/child-runner.js +67 -46
- package/dist/task/orchestrator.d.ts +14 -20
- package/dist/task/orchestrator.js +12 -9
- package/dist/task/phases.d.ts +16 -23
- package/dist/task/phases.js +47 -452
- package/dist/task/question-dialog.d.ts +56 -0
- package/dist/task/question-dialog.js +53 -0
- package/dist/task/research-worker.d.ts +180 -0
- package/dist/task/research-worker.js +432 -0
- package/dist/workers/brave-warning.js +4 -30
- package/dist/workers/docs-core.d.ts +8 -4
- package/dist/workers/docs-core.js +30 -21
- package/dist/workers/docs-lookup.d.ts +72 -0
- package/dist/workers/docs-lookup.js +53 -0
- package/dist/workers/docs-project.d.ts +9 -0
- package/dist/workers/docs-project.js +15 -0
- package/dist/workers/pi-worker-core.d.ts +87 -1
- package/dist/workers/pi-worker-core.js +3 -7
- package/dist/workers/pi-worker-docs.js +27 -31
- package/dist/workers/reasoning-warning.d.ts +10 -16
- package/dist/workers/reasoning-warning.js +25 -57
- package/dist/workers/session-hint.d.ts +37 -0
- package/dist/workers/session-hint.js +82 -0
- package/dist/workers/worker-failure.d.ts +34 -0
- package/dist/workers/worker-failure.js +27 -16
- package/dist/workers/worker-kill.d.ts +84 -0
- package/dist/workers/worker-kill.js +124 -0
- package/package.json +1 -1
- package/dist/task/reasoning-groups.d.ts +0 -36
- package/dist/task/reasoning-groups.js +0 -36
|
@@ -59,7 +59,18 @@ declare class BorderedBox implements Component {
|
|
|
59
59
|
* because both read `format`.
|
|
60
60
|
*/
|
|
61
61
|
export interface ConfigItem {
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The row's id. A `keyof PiTaskConfig` for a fixed setting; a prefixed
|
|
64
|
+
* string (`reason:`, `tool:`, `ext:`) for a DISCOVERED one.
|
|
65
|
+
*
|
|
66
|
+
* It is `string`, not `keyof PiTaskConfig`, and that is what lets the three
|
|
67
|
+
* dynamic families BE rows instead of bypassing them. While the id was
|
|
68
|
+
* narrow, each family re-invented both directions by hand — a builder, an
|
|
69
|
+
* apply function, and an arm in a four-way prefix ladder 300 lines away —
|
|
70
|
+
* and the round-trip property `config-items.test.ts` runs over `ITEMS`
|
|
71
|
+
* covered none of them.
|
|
72
|
+
*/
|
|
73
|
+
id: string;
|
|
63
74
|
/**
|
|
64
75
|
* Which titled block of the menu this row sits under. Rows are grouped by
|
|
65
76
|
* section in {@link panelItems}, in the order the sections first appear in
|
|
@@ -75,6 +86,11 @@ export interface ConfigItem {
|
|
|
75
86
|
format: (cfg: PiTaskConfig) => string;
|
|
76
87
|
/** Write the chosen label back. A value it does not recognise is ignored. */
|
|
77
88
|
apply: (cfg: PiTaskConfig, chosen: string) => void;
|
|
89
|
+
/**
|
|
90
|
+
* What the headless one-line rendering calls this row, when `label` reads
|
|
91
|
+
* only in the panel. See {@link PanelItem.headlessLabel}.
|
|
92
|
+
*/
|
|
93
|
+
headlessLabel?: string;
|
|
78
94
|
}
|
|
79
95
|
/**
|
|
80
96
|
* The titled blocks the settings menu is divided into.
|
|
@@ -102,31 +118,31 @@ export declare const SECTION_ID_PREFIX = "section:";
|
|
|
102
118
|
* either of the two ladders this replaced.
|
|
103
119
|
*/
|
|
104
120
|
export declare const ITEMS: ConfigItem[];
|
|
105
|
-
export declare function extensionItems(extensions: InstalledExtension[]
|
|
106
|
-
id: string;
|
|
107
|
-
label: string;
|
|
108
|
-
description: string;
|
|
109
|
-
currentValue: string;
|
|
110
|
-
values: string[];
|
|
111
|
-
}[];
|
|
121
|
+
export declare function extensionItems(extensions: InstalledExtension[]): ConfigItem[];
|
|
112
122
|
/** Apply an extension toggle to the config's whitelist (idempotent both ways). */
|
|
113
123
|
export declare function applyExtensionToggle(whitelist: readonly string[], entryPath: string, on: boolean): string[];
|
|
114
|
-
export declare function toolItems(tools: readonly GuardableTool[]
|
|
115
|
-
id: string;
|
|
116
|
-
label: string;
|
|
117
|
-
description: string;
|
|
118
|
-
currentValue: string;
|
|
119
|
-
values: string[];
|
|
120
|
-
}[];
|
|
124
|
+
export declare function toolItems(tools: readonly GuardableTool[]): ConfigItem[];
|
|
121
125
|
/** Apply a per-tool watchdog toggle to the exemption list (idempotent both ways). */
|
|
122
126
|
export declare function applyToolToggle(exempt: readonly string[], toolName: string, watched: boolean): string[];
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The label for one `think:` row.
|
|
129
|
+
*
|
|
130
|
+
* A group whose name carries a colon is a CHILD of the group before the colon —
|
|
131
|
+
* `research:files` is one of the four workers `research` fans out to. Rendered
|
|
132
|
+
* flat, those four read as four more peers of `research`, and the menu's one
|
|
133
|
+
* genuine hierarchy is invisible: the four are the only rows in the list whose
|
|
134
|
+
* parent is also a row.
|
|
135
|
+
*
|
|
136
|
+
* So a child is drawn as a tree branch under its parent and loses the repeated
|
|
137
|
+
* `think: research:` prefix, which is 15 columns of the same text on four
|
|
138
|
+
* consecutive lines. `└─` on the last child, `├─` on the rest, decided from the
|
|
139
|
+
* group's position in {@link REASONING_GROUPS} rather than a hand-kept list —
|
|
140
|
+
* adding a fifth worker moves the corner on its own.
|
|
141
|
+
*
|
|
142
|
+
* Leading spaces survive: SettingsList pads the label right, never trims it.
|
|
143
|
+
*/
|
|
144
|
+
export declare function reasoningRowLabel(group: ReasoningGroup): string;
|
|
145
|
+
export declare function reasoningItems(): ConfigItem[];
|
|
130
146
|
/**
|
|
131
147
|
* Apply one group row's new value.
|
|
132
148
|
*
|
|
@@ -137,16 +153,6 @@ export declare function reasoningItems(cfg: PiTaskConfig): {
|
|
|
137
153
|
* it, nudging `research` while in `off` would silently return the other six to
|
|
138
154
|
* whatever the stored table happened to hold.
|
|
139
155
|
*/
|
|
140
|
-
/**
|
|
141
|
-
* Write every `think:` row's displayed value back from the config.
|
|
142
|
-
*
|
|
143
|
-
* Called after ANY change, not just a reasoning one, because the mode row and
|
|
144
|
-
* the seven group rows are one control split across eight lines: cycling
|
|
145
|
-
* `reasoning` to `off` changes what all seven of them run at, and cycling one
|
|
146
|
-
* group row flips the mode, which changes the other six. A row showing a level
|
|
147
|
-
* the run will not use is worse than no row.
|
|
148
|
-
*/
|
|
149
|
-
export declare function refreshReasoningRows(cfg: PiTaskConfig, list: SettingsList): void;
|
|
150
156
|
export declare function applyReasoningLevel(cfg: PiTaskConfig, group: ReasoningGroup, chosen: string): void;
|
|
151
157
|
/**
|
|
152
158
|
* Tallest body the settings list can render, so {@link BorderedBox} can pad
|
|
@@ -167,6 +173,14 @@ export type PanelItem = {
|
|
|
167
173
|
* — the header does not need its own branch anywhere.
|
|
168
174
|
*/
|
|
169
175
|
values?: string[];
|
|
176
|
+
/**
|
|
177
|
+
* What the headless one-line rendering calls this row, when `label` reads
|
|
178
|
+
* only in the panel. A tree branch means nothing on a line of `|`-joined
|
|
179
|
+
* rows: `├─ files` there names no parent, where `think: research:files`
|
|
180
|
+
* does. Set by the reasoning rows; every other row leaves it off and its
|
|
181
|
+
* `label` is used.
|
|
182
|
+
*/
|
|
183
|
+
headlessLabel?: string;
|
|
170
184
|
};
|
|
171
185
|
/**
|
|
172
186
|
* Builds the framed settings panel. Split out of the command handler so the
|
|
@@ -184,7 +198,37 @@ export declare function createSettingsPanel(items: PanelItem[], theme: Theme,
|
|
|
184
198
|
* beside seven rows still claiming `inherit` — which is what it did.
|
|
185
199
|
*/
|
|
186
200
|
onChange: (id: string, newValue: string, list: SettingsList) => void, onCancel: () => void): BorderedBox;
|
|
201
|
+
/**
|
|
202
|
+
* Every settings row for this session, fixed and DISCOVERED, in menu order.
|
|
203
|
+
*
|
|
204
|
+
* One list of `ConfigItem`s — so a row's display, its write-back and its section
|
|
205
|
+
* are one object, the dispatch is a lookup by id, and the round-trip properties
|
|
206
|
+
* in `config-items.test.ts` cover the reasoning, tool and extension families
|
|
207
|
+
* that used to bypass the row type entirely.
|
|
208
|
+
*
|
|
209
|
+
* Fixed rows come before discovered ones within a section, which is the order
|
|
210
|
+
* the hand-written `extra` map produced.
|
|
211
|
+
*/
|
|
212
|
+
export declare function configRows(installed: InstalledExtension[], tools?: readonly GuardableTool[]): ConfigItem[];
|
|
213
|
+
/** Render `rows` for the current config, grouped under their section headers. */
|
|
214
|
+
export declare function renderRows(cfg: PiTaskConfig, rows: readonly ConfigItem[]): PanelItem[];
|
|
187
215
|
/** The full settings row list for the current config, in menu order. */
|
|
188
216
|
export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[], tools?: readonly GuardableTool[]): PanelItem[];
|
|
217
|
+
/**
|
|
218
|
+
* Re-ask every row what it now displays, and write the answers back.
|
|
219
|
+
*
|
|
220
|
+
* A row's `currentValue` in the live list is a snapshot taken when the panel was
|
|
221
|
+
* built, and rows describe each other: cycling `reasoning` to `off` changes what
|
|
222
|
+
* all seven `think:` rows run at, and cycling one group row flips the mode,
|
|
223
|
+
* which changes the other six.
|
|
224
|
+
*
|
|
225
|
+
* This runs after ANY change, over EVERY row. Its predecessor,
|
|
226
|
+
* `refreshReasoningRows`, ran after any change too — but hard-coded the seven
|
|
227
|
+
* reasoning ids plus `reasoningMode` and touched none of the other ~30 rows, so
|
|
228
|
+
* the next cross-row dependency would have needed a fifth function. Re-reading a
|
|
229
|
+
* `format` costs nothing, which is why there is still no list of "changes that
|
|
230
|
+
* need a refresh" to keep correct.
|
|
231
|
+
*/
|
|
232
|
+
export declare function syncRows(cfg: PiTaskConfig, rows: readonly ConfigItem[], list: SettingsList): void;
|
|
189
233
|
export declare function registerConfig(pi: ExtensionAPI): void;
|
|
190
234
|
export {};
|
package/dist/config/register.js
CHANGED
|
@@ -5,7 +5,7 @@ import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../w
|
|
|
5
5
|
import { COMMAND_TIMEOUT_OPTIONS, DEBUG_LOG_OPTIONS, getConfig, sanitizeDebugLogs, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
|
|
6
6
|
import { listInstalledExtensions } from './extension-list.js';
|
|
7
7
|
import { listGuardableTools } from './tool-list.js';
|
|
8
|
-
import { REASONING_GROUPS, REASONING_MODES, sanitizeReasoningMode, REASONING_GROUP_HELP, REASONING_SETTINGS, resolveReasoning } from './reasoning.js';
|
|
8
|
+
import { REASONING_GROUPS, REASONING_MODES, sanitizeReasoningMode, REASONING_GROUP_HELP, REASONING_SETTINGS, effectiveReasoning, resolveReasoning } from './reasoning.js';
|
|
9
9
|
// Version in the title so a bug report or screenshot says which build it came
|
|
10
10
|
// from without anyone having to go look it up.
|
|
11
11
|
const CONFIG_TITLE = `pi-task ${readPkgVersion()} settings`;
|
|
@@ -253,16 +253,22 @@ export const ITEMS = [
|
|
|
253
253
|
* can tell an extension toggle from a PiTaskConfig field.
|
|
254
254
|
*/
|
|
255
255
|
const EXT_ID_PREFIX = 'ext:';
|
|
256
|
-
export function extensionItems(extensions
|
|
256
|
+
export function extensionItems(extensions) {
|
|
257
257
|
return extensions.map(e => ({
|
|
258
258
|
id: EXT_ID_PREFIX + e.path,
|
|
259
|
+
section: 'extensions',
|
|
259
260
|
label: `ext: ${e.label}`,
|
|
260
261
|
description: `Load this ${e.origin} extension in the helper sessions pi-task spawns. They run `
|
|
261
262
|
+ 'with extensions off by default, so turn this on when the extension provides the '
|
|
262
263
|
+ 'model they need (pi-lmstudio, for example). They also inherit its tools and '
|
|
263
264
|
+ `hooks, so only enable ones you trust. ${e.path}`,
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
values: ['on', 'off'],
|
|
266
|
+
format: cfg => (cfg.extensionWhitelist.includes(e.path) ? 'on' : 'off'),
|
|
267
|
+
apply: (cfg, chosen) => {
|
|
268
|
+
if (chosen !== 'on' && chosen !== 'off')
|
|
269
|
+
return;
|
|
270
|
+
cfg.extensionWhitelist = applyExtensionToggle(cfg.extensionWhitelist, e.path, chosen === 'on');
|
|
271
|
+
}
|
|
266
272
|
}));
|
|
267
273
|
}
|
|
268
274
|
/** Apply an extension toggle to the config's whitelist (idempotent both ways). */
|
|
@@ -280,18 +286,24 @@ export function applyExtensionToggle(whitelist, entryPath, on) {
|
|
|
280
286
|
* the config matches nothing.
|
|
281
287
|
*/
|
|
282
288
|
const TOOL_ID_PREFIX = 'tool:';
|
|
283
|
-
export function toolItems(tools
|
|
289
|
+
export function toolItems(tools) {
|
|
284
290
|
return tools.map(t => ({
|
|
285
291
|
id: TOOL_ID_PREFIX + t.name,
|
|
292
|
+
section: 'timeouts',
|
|
286
293
|
label: `watch: ${t.name}`,
|
|
287
294
|
description: `Apply the command timeout to this tool. Leave it on unless the tool runs its own `
|
|
288
295
|
+ `bounded, cancellable work for longer than the timeout — turning it off means a `
|
|
289
296
|
+ `genuine hang in this tool will never be caught, and nothing else is watching `
|
|
290
297
|
+ `while a tool runs. ${t.origin}`,
|
|
298
|
+
values: ['on', 'off'],
|
|
291
299
|
// Stored inverted: the config records the EXEMPTIONS, so an empty list
|
|
292
300
|
// (and any tool pi-task has never heard of) stays guarded by default.
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
format: cfg => (cfg.commandTimeoutExemptTools.includes(t.name) ? 'off' : 'on'),
|
|
302
|
+
apply: (cfg, chosen) => {
|
|
303
|
+
if (chosen !== 'on' && chosen !== 'off')
|
|
304
|
+
return;
|
|
305
|
+
cfg.commandTimeoutExemptTools = applyToolToggle(cfg.commandTimeoutExemptTools, t.name, chosen === 'on');
|
|
306
|
+
}
|
|
295
307
|
}));
|
|
296
308
|
}
|
|
297
309
|
/** Apply a per-tool watchdog toggle to the exemption list (idempotent both ways). */
|
|
@@ -314,16 +326,45 @@ export function applyToolToggle(exempt, toolName, watched) {
|
|
|
314
326
|
* whole point of having measured it.
|
|
315
327
|
*/
|
|
316
328
|
const REASON_ID_PREFIX = 'reason:';
|
|
317
|
-
|
|
329
|
+
/**
|
|
330
|
+
* The label for one `think:` row.
|
|
331
|
+
*
|
|
332
|
+
* A group whose name carries a colon is a CHILD of the group before the colon —
|
|
333
|
+
* `research:files` is one of the four workers `research` fans out to. Rendered
|
|
334
|
+
* flat, those four read as four more peers of `research`, and the menu's one
|
|
335
|
+
* genuine hierarchy is invisible: the four are the only rows in the list whose
|
|
336
|
+
* parent is also a row.
|
|
337
|
+
*
|
|
338
|
+
* So a child is drawn as a tree branch under its parent and loses the repeated
|
|
339
|
+
* `think: research:` prefix, which is 15 columns of the same text on four
|
|
340
|
+
* consecutive lines. `└─` on the last child, `├─` on the rest, decided from the
|
|
341
|
+
* group's position in {@link REASONING_GROUPS} rather than a hand-kept list —
|
|
342
|
+
* adding a fifth worker moves the corner on its own.
|
|
343
|
+
*
|
|
344
|
+
* Leading spaces survive: SettingsList pads the label right, never trims it.
|
|
345
|
+
*/
|
|
346
|
+
export function reasoningRowLabel(group) {
|
|
347
|
+
const colon = group.indexOf(':');
|
|
348
|
+
if (colon < 0)
|
|
349
|
+
return `think: ${group}`;
|
|
350
|
+
const parent = group.slice(0, colon);
|
|
351
|
+
const nextIsSibling = REASONING_GROUPS[REASONING_GROUPS.indexOf(group) + 1]?.startsWith(`${parent}:`);
|
|
352
|
+
return ` ${nextIsSibling ? '├─' : '└─'} ${group.slice(colon + 1)}`;
|
|
353
|
+
}
|
|
354
|
+
export function reasoningItems() {
|
|
318
355
|
return REASONING_GROUPS.map(group => ({
|
|
319
356
|
id: REASON_ID_PREFIX + group,
|
|
320
|
-
|
|
357
|
+
section: 'reasoning',
|
|
358
|
+
label: reasoningRowLabel(group),
|
|
359
|
+
headlessLabel: `think: ${group}`,
|
|
321
360
|
description: REASONING_GROUP_HELP[group],
|
|
361
|
+
values: [...REASONING_SETTINGS],
|
|
322
362
|
// The EFFECTIVE level, not cfg.reasoningLevels[group]: in default/on/off
|
|
323
363
|
// the stored table is not what runs, and a row that shows a value the
|
|
324
|
-
// run does not use is worse than no row.
|
|
325
|
-
|
|
326
|
-
|
|
364
|
+
// run does not use is worse than no row. As a FUNCTION rather than a
|
|
365
|
+
// snapshot, so `syncRows` can re-ask after any change.
|
|
366
|
+
format: cfg => resolveReasoning(group, cfg),
|
|
367
|
+
apply: (cfg, chosen) => applyReasoningLevel(cfg, group, chosen)
|
|
327
368
|
}));
|
|
328
369
|
}
|
|
329
370
|
/**
|
|
@@ -336,29 +377,13 @@ export function reasoningItems(cfg) {
|
|
|
336
377
|
* it, nudging `research` while in `off` would silently return the other six to
|
|
337
378
|
* whatever the stored table happened to hold.
|
|
338
379
|
*/
|
|
339
|
-
/**
|
|
340
|
-
* Write every `think:` row's displayed value back from the config.
|
|
341
|
-
*
|
|
342
|
-
* Called after ANY change, not just a reasoning one, because the mode row and
|
|
343
|
-
* the seven group rows are one control split across eight lines: cycling
|
|
344
|
-
* `reasoning` to `off` changes what all seven of them run at, and cycling one
|
|
345
|
-
* group row flips the mode, which changes the other six. A row showing a level
|
|
346
|
-
* the run will not use is worse than no row.
|
|
347
|
-
*/
|
|
348
|
-
export function refreshReasoningRows(cfg, list) {
|
|
349
|
-
for (const group of REASONING_GROUPS) {
|
|
350
|
-
list.updateValue(REASON_ID_PREFIX + group, resolveReasoning(group, cfg));
|
|
351
|
-
}
|
|
352
|
-
list.updateValue('reasoningMode', cfg.reasoningMode);
|
|
353
|
-
}
|
|
354
380
|
export function applyReasoningLevel(cfg, group, chosen) {
|
|
355
381
|
if (!REASONING_SETTINGS.includes(chosen))
|
|
356
382
|
return;
|
|
357
383
|
if (cfg.reasoningMode !== 'custom') {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
cfg.reasoningLevels = seeded;
|
|
384
|
+
// Freeze the table exactly as it runs today, then switch to custom, so
|
|
385
|
+
// opening one row cannot silently move the other six.
|
|
386
|
+
cfg.reasoningLevels = effectiveReasoning(cfg);
|
|
362
387
|
cfg.reasoningMode = 'custom';
|
|
363
388
|
}
|
|
364
389
|
cfg.reasoningLevels = { ...cfg.reasoningLevels, [group]: chosen };
|
|
@@ -492,38 +517,70 @@ onChange, onCancel) {
|
|
|
492
517
|
const list = new SettingsList(items, MAX_VISIBLE, makeTheme(theme, label => headerLabels.has(label.trimEnd())), (id, newValue) => onChange(id, newValue, list), onCancel);
|
|
493
518
|
return new BorderedBox(new SkipInertRows(list, items.map(i => (i.values?.length ?? 0) > 0)), CONFIG_TITLE, s => theme.fg('borderMuted', s), s => theme.fg('accent', theme.bold(s)), settingsBodyHeight(items.map(i => i.description), MAX_VISIBLE, OVERLAY_WIDTH - 8));
|
|
494
519
|
}
|
|
495
|
-
/**
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Every settings row for this session, fixed and DISCOVERED, in menu order.
|
|
522
|
+
*
|
|
523
|
+
* One list of `ConfigItem`s — so a row's display, its write-back and its section
|
|
524
|
+
* are one object, the dispatch is a lookup by id, and the round-trip properties
|
|
525
|
+
* in `config-items.test.ts` cover the reasoning, tool and extension families
|
|
526
|
+
* that used to bypass the row type entirely.
|
|
527
|
+
*
|
|
528
|
+
* Fixed rows come before discovered ones within a section, which is the order
|
|
529
|
+
* the hand-written `extra` map produced.
|
|
530
|
+
*/
|
|
531
|
+
export function configRows(installed, tools = []) {
|
|
532
|
+
// The discovered rows carry a section like every other row — the per-tool
|
|
533
|
+
// watchdog exemptions under `timeouts` (they are exemptions FROM that
|
|
534
|
+
// timeout), and the per-extension toggles under their own heading.
|
|
535
|
+
return [...ITEMS, ...reasoningItems(), ...toolItems(tools), ...extensionItems(installed)];
|
|
536
|
+
}
|
|
537
|
+
/** Render `rows` for the current config, grouped under their section headers. */
|
|
538
|
+
export function renderRows(cfg, rows) {
|
|
505
539
|
const out = [];
|
|
506
540
|
for (const { key, title } of SECTIONS) {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
...(
|
|
516
|
-
|
|
541
|
+
const inSection = rows
|
|
542
|
+
.filter(i => i.section === key)
|
|
543
|
+
.map(i => ({
|
|
544
|
+
id: i.id,
|
|
545
|
+
label: i.label,
|
|
546
|
+
description: i.description,
|
|
547
|
+
currentValue: i.format(cfg),
|
|
548
|
+
values: i.values ?? ['on', 'off'],
|
|
549
|
+
...(i.headlessLabel === undefined ? {} : { headlessLabel: i.headlessLabel })
|
|
550
|
+
}));
|
|
517
551
|
// An empty section prints no header. `extensions` has no fixed rows at
|
|
518
552
|
// all, so with nothing installed the heading would otherwise sit alone.
|
|
519
|
-
if (
|
|
553
|
+
if (inSection.length === 0)
|
|
520
554
|
continue;
|
|
521
555
|
if (out.length > 0)
|
|
522
556
|
out.push(sectionGap(title));
|
|
523
|
-
out.push(sectionHeader(title), ...
|
|
557
|
+
out.push(sectionHeader(title), ...inSection);
|
|
524
558
|
}
|
|
525
559
|
return out;
|
|
526
560
|
}
|
|
561
|
+
/** The full settings row list for the current config, in menu order. */
|
|
562
|
+
export function panelItems(cfg, installed, tools = []) {
|
|
563
|
+
return renderRows(cfg, configRows(installed, tools));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Re-ask every row what it now displays, and write the answers back.
|
|
567
|
+
*
|
|
568
|
+
* A row's `currentValue` in the live list is a snapshot taken when the panel was
|
|
569
|
+
* built, and rows describe each other: cycling `reasoning` to `off` changes what
|
|
570
|
+
* all seven `think:` rows run at, and cycling one group row flips the mode,
|
|
571
|
+
* which changes the other six.
|
|
572
|
+
*
|
|
573
|
+
* This runs after ANY change, over EVERY row. Its predecessor,
|
|
574
|
+
* `refreshReasoningRows`, ran after any change too — but hard-coded the seven
|
|
575
|
+
* reasoning ids plus `reasoningMode` and touched none of the other ~30 rows, so
|
|
576
|
+
* the next cross-row dependency would have needed a fifth function. Re-reading a
|
|
577
|
+
* `format` costs nothing, which is why there is still no list of "changes that
|
|
578
|
+
* need a refresh" to keep correct.
|
|
579
|
+
*/
|
|
580
|
+
export function syncRows(cfg, rows, list) {
|
|
581
|
+
for (const row of rows)
|
|
582
|
+
list.updateValue(row.id, row.format(cfg));
|
|
583
|
+
}
|
|
527
584
|
async function handleTaskConfig(_args, ctx, getTools = () => []) {
|
|
528
585
|
const cfg = {
|
|
529
586
|
...getConfig(),
|
|
@@ -555,38 +612,23 @@ async function handleTaskConfig(_args, ctx, getTools = () => []) {
|
|
|
555
612
|
.filter(i => i.label !== '')
|
|
556
613
|
.map(i => i.values === undefined ?
|
|
557
614
|
`[${i.label.trim()}]`
|
|
558
|
-
: `${i.label.padEnd(22)} ${i.currentValue}`);
|
|
615
|
+
: `${(i.headlessLabel ?? i.label).padEnd(22)} ${i.currentValue}`);
|
|
559
616
|
ctx.ui.notify(lines.join(' | '), 'info');
|
|
560
617
|
return;
|
|
561
618
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
// Every setting parses its own value. There is no generic
|
|
578
|
-
// fallback: the ladder this replaces ended in one that
|
|
579
|
-
// wrote `newValue === 'on'` into whatever field it was
|
|
580
|
-
// handed, so a new enum setting silently became a boolean
|
|
581
|
-
// until someone noticed.
|
|
582
|
-
ITEMS.find(item => item.id === id)?.apply(cfg, newValue);
|
|
583
|
-
}
|
|
584
|
-
// The reasoning rows describe each other, so they are
|
|
585
|
-
// re-read from the config after every change — including
|
|
586
|
-
// changes to unrelated rows, which costs nothing and means
|
|
587
|
-
// there is no list of "changes that need a refresh" to keep
|
|
588
|
-
// correct.
|
|
589
|
-
refreshReasoningRows(cfg, list);
|
|
619
|
+
// Built ONCE and shared by the renderer, the dispatch and the refresh, so
|
|
620
|
+
// all three necessarily agree about which rows exist.
|
|
621
|
+
const rows = configRows(installed, tools);
|
|
622
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => createSettingsPanel(renderRows(cfg, rows), theme, (id, newValue, list) => {
|
|
623
|
+
// Every row parses its own value. There is no generic
|
|
624
|
+
// fallback and no prefix ladder: the ladder this replaces
|
|
625
|
+
// ended in one that wrote `newValue === 'on'` into whatever
|
|
626
|
+
// field it was handed, so a new enum setting silently became
|
|
627
|
+
// a boolean until someone noticed. A header row carries no
|
|
628
|
+
// `values`, so SettingsList never cycles it and it matches
|
|
629
|
+
// no row here anyway.
|
|
630
|
+
rows.find(row => row.id === id)?.apply(cfg, newValue);
|
|
631
|
+
syncRows(cfg, rows, list);
|
|
590
632
|
saveConfig(cfg).catch(() => { });
|
|
591
633
|
}, () => done(undefined)), { overlay: true, overlayOptions: { width: OVERLAY_WIDTH } });
|
|
592
634
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* transitive package to get twenty lines of arithmetic. SOURCE OF TRUTH is that
|
|
25
25
|
* module; `reasoning-capability.test.ts` is where a change upstream shows up.
|
|
26
26
|
*/
|
|
27
|
-
import type
|
|
27
|
+
import { type ReasoningGroup, type GroupSetting } from '../config/reasoning.js';
|
|
28
28
|
/**
|
|
29
29
|
* pi's own level ladder, in order. The order is the whole algorithm: an
|
|
30
30
|
* unsupported level is resolved by walking UP first, then down.
|
|
@@ -80,7 +80,4 @@ export interface ReasoningMismatch {
|
|
|
80
80
|
* about one direction while staying silent about the other would ship this
|
|
81
81
|
* feature with its own measured failure mode unreported.
|
|
82
82
|
*/
|
|
83
|
-
export declare function reasoningMismatches(model: ReasoningModelFacts | undefined,
|
|
84
|
-
group: ReasoningGroup;
|
|
85
|
-
setting: GroupSetting;
|
|
86
|
-
}>): ReasoningMismatch[];
|
|
83
|
+
export declare function reasoningMismatches(model: ReasoningModelFacts | undefined, levels: Readonly<Record<ReasoningGroup, GroupSetting>>): ReasoningMismatch[];
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What pi will ACTUALLY send, given a model and a requested thinking level.
|
|
3
|
+
*
|
|
4
|
+
* WHY A LOCAL COPY OF PI'S CLAMP
|
|
5
|
+
* ------------------------------
|
|
6
|
+
* pi never reports that it ignored or downgraded a level. Measured live against
|
|
7
|
+
* this machine's llama-server, with a proxy capturing the request body:
|
|
8
|
+
*
|
|
9
|
+
* 1. a model with `reasoning: false` + `--thinking medium`
|
|
10
|
+
* → the body carries NO reasoning field at all. No error, no warning.
|
|
11
|
+
* 2. `thinkingLevelMap: {off: null, ...}` + `--thinking off`
|
|
12
|
+
* → silently clamped UP to `medium`. Thinking stays on.
|
|
13
|
+
* 3. `--thinking low` where `low: null`
|
|
14
|
+
* → silently clamped to `medium`.
|
|
15
|
+
*
|
|
16
|
+
* All three are the same arithmetic, and it is pure: `getSupportedThinkingLevels`
|
|
17
|
+
* / `clampThinkingLevel` in @earendil-works/pi-ai's models module. Reproducing it
|
|
18
|
+
* lets one predicate — `clampToModel(m, wanted) !== wanted` — catch all three
|
|
19
|
+
* host-side, before a single request is sent.
|
|
20
|
+
*
|
|
21
|
+
* Reimplemented rather than imported because `@earendil-works/pi-ai` is neither a
|
|
22
|
+
* dependency nor a peerDependency of pi-task: it is present only because
|
|
23
|
+
* pi-coding-agent hoists it, so importing it would take a hard dependency on a
|
|
24
|
+
* transitive package to get twenty lines of arithmetic. SOURCE OF TRUTH is that
|
|
25
|
+
* module; `reasoning-capability.test.ts` is where a change upstream shows up.
|
|
26
|
+
*/
|
|
27
|
+
import { REASONING_GROUPS } from '../config/reasoning.js';
|
|
1
28
|
/**
|
|
2
29
|
* pi's own level ladder, in order. The order is the whole algorithm: an
|
|
3
30
|
* unsupported level is resolved by walking UP first, then down.
|
|
@@ -64,16 +91,16 @@ export function clampToModel(model, level) {
|
|
|
64
91
|
* about one direction while staying silent about the other would ship this
|
|
65
92
|
* feature with its own measured failure mode unreported.
|
|
66
93
|
*/
|
|
67
|
-
export function reasoningMismatches(model,
|
|
94
|
+
export function reasoningMismatches(model, levels) {
|
|
68
95
|
// No model resolved yet (session still starting, or none selected): say
|
|
69
96
|
// nothing. A warning naming no model is noise, not information.
|
|
70
97
|
if (!model)
|
|
71
98
|
return [];
|
|
72
99
|
const out = [];
|
|
73
|
-
for (const
|
|
74
|
-
|
|
100
|
+
for (const group of REASONING_GROUPS) {
|
|
101
|
+
const wanted = levels[group];
|
|
102
|
+
if (wanted === 'inherit')
|
|
75
103
|
continue;
|
|
76
|
-
const wanted = setting;
|
|
77
104
|
const actual = clampToModel(model, wanted);
|
|
78
105
|
if (actual !== wanted)
|
|
79
106
|
out.push({ group, wanted, actual });
|
|
@@ -29,6 +29,8 @@ export interface AutoDeps extends GateDeps, FinalGateStageDeps {
|
|
|
29
29
|
*/
|
|
30
30
|
stashRef?: (cwd: string) => Promise<string | null>;
|
|
31
31
|
}
|
|
32
|
+
/** Wait for every plan-debug line written so far to reach disk. Tests only. */
|
|
33
|
+
export declare function flushPlanDebug(): Promise<unknown>;
|
|
32
34
|
/**
|
|
33
35
|
* Expand any @file references in the feature text by appending each referenced
|
|
34
36
|
* file's contents, so the planning children (clarify, decompose) always see the
|
|
@@ -47,7 +47,7 @@ import { granularityFloor, granularitySplitHint, isPlanShapeQuestion, isTooCoars
|
|
|
47
47
|
import { mandatesTestsInSameChange, rewriteBatchTestPlan } from './batch-test-task.js';
|
|
48
48
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, writeOwnedRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
49
49
|
import { groundedCoverage } from './coverage-loop.js';
|
|
50
|
-
import {
|
|
50
|
+
import { settleQuestion } from './question-dialog.js';
|
|
51
51
|
import { TERMINAL_OUTCOMES, formatAt, formatWhy } from './terminal-outcome.js';
|
|
52
52
|
import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, danglingCarryText } from './artifact-closure.js';
|
|
53
53
|
import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, keepGroundedScripts, appendDeclaredScripts } from './launch-contract.js';
|
|
@@ -149,12 +149,28 @@ function mentionPath(token) {
|
|
|
149
149
|
* default level. It is also the only channel the plan phase has: it runs before
|
|
150
150
|
* any task file, hence any `TASK_NNNN-debug.log`, exists.
|
|
151
151
|
*/
|
|
152
|
+
/**
|
|
153
|
+
* Every plan-debug write not yet on disk, chained.
|
|
154
|
+
*
|
|
155
|
+
* Fire-and-forget is right for production — a plan must never wait on its own
|
|
156
|
+
* trail — but it leaves nothing to synchronise on, and the twelve tests that
|
|
157
|
+
* read `plan-debug.log` back were racing the append that writes it. They failed
|
|
158
|
+
* intermittently on ENOENT, ~4ms in, at a rate that moved with how many other
|
|
159
|
+
* files the suite was running beside them. Chaining also serialises concurrent
|
|
160
|
+
* appends, which is what keeps a line whole.
|
|
161
|
+
*/
|
|
162
|
+
let planDebugChain = Promise.resolve();
|
|
163
|
+
/** Wait for every plan-debug line written so far to reach disk. Tests only. */
|
|
164
|
+
export function flushPlanDebug() {
|
|
165
|
+
return planDebugChain;
|
|
166
|
+
}
|
|
152
167
|
function logPlanDebug(cwd, msg) {
|
|
153
168
|
if (!shouldLogDebug('event', debugLogLevel()))
|
|
154
169
|
return;
|
|
155
170
|
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
156
171
|
const dir = tasksDir(cwd);
|
|
157
|
-
|
|
172
|
+
planDebugChain = planDebugChain
|
|
173
|
+
.then(() => fsp.mkdir(dir, { recursive: true }))
|
|
158
174
|
.then(() => fsp.appendFile(path.join(dir, 'plan-debug.log'), line))
|
|
159
175
|
.catch(() => { });
|
|
160
176
|
}
|
|
@@ -559,56 +575,27 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
559
575
|
transcript.add('auto-resolved', plainQ, autoResolved);
|
|
560
576
|
continue;
|
|
561
577
|
}
|
|
562
|
-
const plainSuggested = suggested === undefined ? undefined : stripInlineMarkdown(suggested);
|
|
563
|
-
const plainAlt = alt === undefined ? undefined : stripInlineMarkdown(alt);
|
|
564
578
|
// YOLO: take the recommended option (index 0 / the green card) without ever
|
|
565
579
|
// building the prompt. Clarify has no anti-synthesis channel — it runs before
|
|
566
580
|
// any research — so the only step-aside here is a question that carries no
|
|
567
581
|
// recommendation to take; that one is skipped rather than guessed.
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
});
|
|
572
|
-
if (yolo !== null) {
|
|
573
|
-
if (yolo.kind === 'answer')
|
|
574
|
-
transcript.add('yolo', plainQ, yolo.answer);
|
|
575
|
-
else
|
|
576
|
-
transcript.add('yolo-skip', plainQ, `(skipped — ${yolo.note})`);
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
// The picker cards and the reply mapping are shared with /task's grill
|
|
580
|
-
// phase and the plan session (question-dialog.ts) — all three used to
|
|
581
|
-
// write them out, and had drifted.
|
|
582
|
-
const pending = {
|
|
582
|
+
const outcome = await settleQuestion({
|
|
583
|
+
ui,
|
|
584
|
+
transcript,
|
|
583
585
|
plain: plainQ,
|
|
584
586
|
shown: shownQ,
|
|
585
|
-
...(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
alt:
|
|
591
|
-
shownAlt: renderInlineMarkdown(alt, theme)
|
|
587
|
+
...(suggested !== undefined && { suggested }),
|
|
588
|
+
...(alt !== undefined && { alt }),
|
|
589
|
+
render: md => renderInlineMarkdown(md, theme),
|
|
590
|
+
yolo: yoloPickAnswer(isYoloMode(), {
|
|
591
|
+
...(suggested !== undefined && { suggested: stripInlineMarkdown(suggested) }),
|
|
592
|
+
...(alt !== undefined && { alt: stripInlineMarkdown(alt) })
|
|
592
593
|
})
|
|
593
|
-
};
|
|
594
|
-
const options = buildOptionCards(pending);
|
|
595
|
-
const a = await ui.ask({
|
|
596
|
-
localTitle: shownQ,
|
|
597
|
-
displayQuestion: shownQ,
|
|
598
|
-
question: plainQ,
|
|
599
|
-
recommended: plainSuggested,
|
|
600
|
-
...(plainAlt !== undefined && { recommended2: plainAlt }),
|
|
601
|
-
allowSkip: plainSuggested === undefined && plainAlt === undefined,
|
|
602
|
-
...(options && { options })
|
|
603
594
|
});
|
|
604
|
-
if (
|
|
595
|
+
if (outcome === 'cancelled') {
|
|
605
596
|
announceDone(ctx, '/task-auto cancelled.', 'warning');
|
|
606
597
|
return null;
|
|
607
598
|
}
|
|
608
|
-
// An accept covers both routes to it: submitting empty, and pressing the
|
|
609
|
-
// single green card. The suffix is the policy's, not this call site's.
|
|
610
|
-
const resolved = resolveAnswer(pending, a);
|
|
611
|
-
transcript.add(resolved.source === 'accepted' ? 'accepted' : 'typed', plainQ, resolved.answer);
|
|
612
599
|
}
|
|
613
600
|
if (transcript.length === 0) {
|
|
614
601
|
ctx.ui.notify('No clarifying questions needed — planning tasks…', 'info');
|