@mjasnikovs/pi-task 0.38.30 → 0.38.32
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 -1
- package/dist/config/config.d.ts +16 -2
- package/dist/config/config.js +7 -2
- package/dist/config/group-args.d.ts +52 -0
- package/dist/config/group-args.js +110 -0
- package/dist/config/group-models.d.ts +88 -0
- package/dist/config/group-models.js +117 -0
- package/dist/config/groups.d.ts +76 -0
- package/dist/config/groups.js +110 -0
- package/dist/config/option-picker.d.ts +42 -0
- package/dist/config/option-picker.js +73 -0
- package/dist/config/reasoning.d.ts +22 -63
- package/dist/config/reasoning.js +37 -108
- package/dist/config/register.d.ts +98 -12
- package/dist/config/register.js +228 -23
- package/dist/index.js +4 -0
- package/dist/remote/push.js +1 -7
- package/dist/shared/command-watchdog.d.ts +63 -0
- package/dist/shared/command-watchdog.js +87 -0
- package/dist/shared/data-home.d.ts +8 -0
- package/dist/shared/data-home.js +14 -0
- package/dist/shared/model-endpoint.d.ts +53 -0
- package/dist/shared/model-endpoint.js +98 -2
- package/dist/shared/reasoning-capability.d.ts +25 -5
- package/dist/shared/reasoning-capability.js +18 -9
- package/dist/task/auto-orchestrator.js +14 -3
- package/dist/task/child-runner.d.ts +92 -15
- package/dist/task/child-runner.js +303 -66
- package/dist/task/context-usage.d.ts +46 -0
- package/dist/task/context-usage.js +41 -0
- package/dist/task/failure-classifier.js +24 -1
- package/dist/task/gate-child.d.ts +15 -4
- package/dist/task/gate-child.js +2 -2
- package/dist/task/gate-deps.js +7 -2
- package/dist/task/implementation-guards.d.ts +26 -0
- package/dist/task/implementation-guards.js +177 -0
- package/dist/task/implementation-hold.d.ts +118 -0
- package/dist/task/implementation-hold.js +165 -0
- package/dist/task/implementation-turn.d.ts +5 -0
- package/dist/task/implementation-turn.js +12 -1
- package/dist/task/loop-detector.d.ts +18 -0
- package/dist/task/loop-detector.js +22 -2
- package/dist/task/model-hold-stash.d.ts +43 -0
- package/dist/task/model-hold-stash.js +70 -0
- package/dist/task/orchestrator.d.ts +18 -5
- package/dist/task/orchestrator.js +63 -6
- package/dist/task/phases.js +18 -5
- package/dist/task/research-worker.d.ts +2 -2
- package/dist/task/research-worker.js +1 -1
- package/dist/workers/docs-core.js +2 -2
- package/dist/workers/docs-lookup.d.ts +4 -3
- package/dist/workers/docs-lookup.js +1 -1
- package/dist/workers/fetch-core.js +2 -2
- package/dist/workers/focused-extractor.d.ts +6 -4
- package/dist/workers/focused-extractor.js +17 -5
- package/dist/workers/index.js +2 -0
- package/dist/workers/model-warning.d.ts +69 -0
- package/dist/workers/model-warning.js +113 -0
- package/dist/workers/pi-worker-core.d.ts +9 -38
- package/dist/workers/pi-worker-core.js +8 -86
- package/dist/workers/pi-worker-docs.js +2 -2
- package/dist/workers/pi-worker.js +4 -4
- package/dist/workers/reasoning-warning.d.ts +17 -9
- package/dist/workers/reasoning-warning.js +69 -22
- package/dist/workers/single-read-guard.d.ts +6 -6
- package/dist/workers/single-read-guard.js +8 -8
- package/dist/workers/worker-profiles.d.ts +11 -3
- package/dist/workers/worker-profiles.js +33 -1
- package/package.json +1 -1
- package/dist/config/reasoning-args.d.ts +0 -23
- package/dist/config/reasoning-args.js +0 -28
- package/dist/task/implementation-thinking.d.ts +0 -56
- package/dist/task/implementation-thinking.js +0 -32
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The submenu a /task-config row opens on Enter, when cycling is the wrong verb.
|
|
3
|
+
*
|
|
4
|
+
* WHY A SUBMENU AND NOT `ctx.ui.select`
|
|
5
|
+
* -------------------------------------
|
|
6
|
+
* The settings panel already lives inside `ctx.ui.custom` with `overlay: true`.
|
|
7
|
+
* Opening a second dialog from inside a component's input handler races two
|
|
8
|
+
* overlays for focus. `SettingsList` already solves this: it renders the submenu
|
|
9
|
+
* in place of the list and delegates input to it. And its `done(v)` calls
|
|
10
|
+
* `onChange(id, v)` with exactly the returned value while `done(undefined)`
|
|
11
|
+
* writes nothing — the same one-value contract every cycling row uses, so the
|
|
12
|
+
* panel's dispatch needs no branch and a model list of 200 costs one write
|
|
13
|
+
* instead of 200.
|
|
14
|
+
*
|
|
15
|
+
* WHY NOT A BARE `SelectList`
|
|
16
|
+
* ---------------------------
|
|
17
|
+
* `SelectList.handleInput` matches up, down, confirm and cancel and DROPS every
|
|
18
|
+
* other key, so it can never fill its own `setFilter`. `Container` has no
|
|
19
|
+
* `handleInput` at all — it composes `render` and nothing else. pi's own model
|
|
20
|
+
* picker (`modes/interactive/components/model-selector.js`) hand-rolls exactly
|
|
21
|
+
* this: an `Input`, a filtered list, and a `handleInput` that routes between
|
|
22
|
+
* them. The three components that DO use `SelectList` bare — theme, thinking,
|
|
23
|
+
* show-images — are short fixed lists with no filter. A model list is not.
|
|
24
|
+
*/
|
|
25
|
+
import { Container } from '@earendil-works/pi-tui';
|
|
26
|
+
import type { SelectItem } from '@earendil-works/pi-tui';
|
|
27
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
28
|
+
type Theme = ExtensionCommandContext['ui']['theme'];
|
|
29
|
+
/**
|
|
30
|
+
* A filterable one-of picker.
|
|
31
|
+
*
|
|
32
|
+
* `currentValue` may match NO option — that is the vanished-model case, where
|
|
33
|
+
* the stored spec is still shown by the row and must still be openable. It
|
|
34
|
+
* simply starts at the top rather than refusing to render.
|
|
35
|
+
*/
|
|
36
|
+
export declare class OptionPicker extends Container {
|
|
37
|
+
private readonly input;
|
|
38
|
+
private readonly list;
|
|
39
|
+
constructor(options: readonly SelectItem[], currentValue: string, theme: Theme, done: (value?: string) => void);
|
|
40
|
+
handleInput(data: string): void;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The submenu a /task-config row opens on Enter, when cycling is the wrong verb.
|
|
3
|
+
*
|
|
4
|
+
* WHY A SUBMENU AND NOT `ctx.ui.select`
|
|
5
|
+
* -------------------------------------
|
|
6
|
+
* The settings panel already lives inside `ctx.ui.custom` with `overlay: true`.
|
|
7
|
+
* Opening a second dialog from inside a component's input handler races two
|
|
8
|
+
* overlays for focus. `SettingsList` already solves this: it renders the submenu
|
|
9
|
+
* in place of the list and delegates input to it. And its `done(v)` calls
|
|
10
|
+
* `onChange(id, v)` with exactly the returned value while `done(undefined)`
|
|
11
|
+
* writes nothing — the same one-value contract every cycling row uses, so the
|
|
12
|
+
* panel's dispatch needs no branch and a model list of 200 costs one write
|
|
13
|
+
* instead of 200.
|
|
14
|
+
*
|
|
15
|
+
* WHY NOT A BARE `SelectList`
|
|
16
|
+
* ---------------------------
|
|
17
|
+
* `SelectList.handleInput` matches up, down, confirm and cancel and DROPS every
|
|
18
|
+
* other key, so it can never fill its own `setFilter`. `Container` has no
|
|
19
|
+
* `handleInput` at all — it composes `render` and nothing else. pi's own model
|
|
20
|
+
* picker (`modes/interactive/components/model-selector.js`) hand-rolls exactly
|
|
21
|
+
* this: an `Input`, a filtered list, and a `handleInput` that routes between
|
|
22
|
+
* them. The three components that DO use `SelectList` bare — theme, thinking,
|
|
23
|
+
* show-images — are short fixed lists with no filter. A model list is not.
|
|
24
|
+
*/
|
|
25
|
+
import { Container, getKeybindings, Input, SelectList } from '@earendil-works/pi-tui';
|
|
26
|
+
/** Rows visible before the list scrolls. Matches the panel's own MAX_VISIBLE. */
|
|
27
|
+
const PICKER_VISIBLE = 11;
|
|
28
|
+
function selectTheme(theme) {
|
|
29
|
+
return {
|
|
30
|
+
selectedPrefix: text => theme.fg('accent', text),
|
|
31
|
+
selectedText: text => theme.fg('accent', theme.bold(text)),
|
|
32
|
+
description: text => theme.fg('muted', text),
|
|
33
|
+
scrollInfo: text => theme.fg('dim', text),
|
|
34
|
+
noMatch: text => theme.fg('dim', text)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A filterable one-of picker.
|
|
39
|
+
*
|
|
40
|
+
* `currentValue` may match NO option — that is the vanished-model case, where
|
|
41
|
+
* the stored spec is still shown by the row and must still be openable. It
|
|
42
|
+
* simply starts at the top rather than refusing to render.
|
|
43
|
+
*/
|
|
44
|
+
export class OptionPicker extends Container {
|
|
45
|
+
input = new Input();
|
|
46
|
+
list;
|
|
47
|
+
constructor(options, currentValue, theme, done) {
|
|
48
|
+
super();
|
|
49
|
+
this.list = new SelectList([...options], PICKER_VISIBLE, selectTheme(theme));
|
|
50
|
+
const at = options.findIndex(o => o.value === currentValue);
|
|
51
|
+
if (at !== -1)
|
|
52
|
+
this.list.setSelectedIndex(at);
|
|
53
|
+
this.list.onSelect = item => done(item.value);
|
|
54
|
+
this.list.onCancel = () => done(undefined);
|
|
55
|
+
this.addChild(this.input);
|
|
56
|
+
this.addChild(this.list);
|
|
57
|
+
}
|
|
58
|
+
handleInput(data) {
|
|
59
|
+
const kb = getKeybindings();
|
|
60
|
+
const forList = kb.matches(data, 'tui.select.up')
|
|
61
|
+
|| kb.matches(data, 'tui.select.down')
|
|
62
|
+
|| kb.matches(data, 'tui.select.confirm')
|
|
63
|
+
|| kb.matches(data, 'tui.select.cancel');
|
|
64
|
+
if (forList) {
|
|
65
|
+
this.list.handleInput(data);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Everything else is typing. The list re-filters on every keystroke,
|
|
69
|
+
// which is what `setFilter` is for and what nothing else calls.
|
|
70
|
+
this.input.handleInput(data);
|
|
71
|
+
this.list.setFilter(this.input.getValue());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -17,35 +17,21 @@
|
|
|
17
17
|
*
|
|
18
18
|
* PURE MODULE — no imports with runtime side effects, so `config.ts` can import
|
|
19
19
|
* the table and the sanitizers during its own module evaluation. `getConfig()`
|
|
20
|
-
* lives one hop away in `
|
|
20
|
+
* lives one hop away in `group-args.ts` for exactly this reason: importing
|
|
21
21
|
* it here would make config.ts ⇄ reasoning.ts a real cycle, and whichever module
|
|
22
22
|
* a caller reached first would decide whether `DEFAULT_REASONING_TABLE` was
|
|
23
23
|
* initialised before `DEFAULT_CONFIG` read it.
|
|
24
24
|
*/
|
|
25
25
|
import type { PiTaskConfig } from './config.js';
|
|
26
|
+
import { CHILD_GROUPS, type ChildGroup } from './groups.js';
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
* want the same amount and share the `phase` cell.
|
|
33
|
-
*
|
|
34
|
-
* - `research` the ad-hoc `pi-worker` subagent tool, and the fallback the four
|
|
35
|
-
* research workers use when their own cell is unset
|
|
36
|
-
* - `research:files` / `research:apis` / `research:context` / `research:tooling`
|
|
37
|
-
* one cell per research worker, so a level can be paid for in
|
|
38
|
-
* one worker without paying for it in the other three
|
|
39
|
-
* - `phase` refine, verify-tooling, grill, compose, critique, compress-label
|
|
40
|
-
* - `planning` /task-auto's planning children — clarify, decompose, and the
|
|
41
|
-
* extract/coverage passes around them
|
|
42
|
-
* - `plan` /task-plan's question and answer children
|
|
43
|
-
* - `gate` enforce, verify, lint-fix, final-fix, recommend
|
|
44
|
-
* - `extraction` the --no-tools focused docs/fetch extractors
|
|
45
|
-
* - `implementation` the host-session turn that writes the code (not a child)
|
|
28
|
+
* Re-exported because this module's whole public surface — the default table,
|
|
29
|
+
* `resolveReasoning`, `effectiveReasoning` — is typed on them, so an importer of
|
|
30
|
+
* those already needs them. The roster itself lives in groups.ts; `GROUP_BY_CHILD`
|
|
31
|
+
* and `groupForChild` are deliberately NOT re-exported, so a caller that wants
|
|
32
|
+
* the roster reaches for the roster.
|
|
46
33
|
*/
|
|
47
|
-
export
|
|
48
|
-
export declare const REASONING_GROUPS: readonly ReasoningGroup[];
|
|
34
|
+
export { CHILD_GROUPS, type ChildGroup };
|
|
49
35
|
/**
|
|
50
36
|
* The four profiles offered by /task-config.
|
|
51
37
|
* - `default` the per-group table below
|
|
@@ -82,44 +68,9 @@ export declare const REASONING_ON_LEVEL: GroupSetting;
|
|
|
82
68
|
* host's level. Any other value is a decision this project made for that
|
|
83
69
|
* group's job, and `/task-config` mode `custom` overrides all of it.
|
|
84
70
|
*/
|
|
85
|
-
export declare const DEFAULT_REASONING_TABLE: Readonly<Record<
|
|
71
|
+
export declare const DEFAULT_REASONING_TABLE: Readonly<Record<ChildGroup, GroupSetting>>;
|
|
86
72
|
/** A stored mode, or `default` when the value is not one. */
|
|
87
73
|
export declare function sanitizeReasoningMode(value: unknown): ReasoningMode;
|
|
88
|
-
/**
|
|
89
|
-
* Child NAME → reasoning group, for every child spawned under a name:
|
|
90
|
-
* `runPhaseChild`, `runPlanningChild`, and the research workers' `spec.label`.
|
|
91
|
-
*
|
|
92
|
-
* WHY KEYED ON THE NAME
|
|
93
|
-
* ---------------------
|
|
94
|
-
* The name is the only identifier in scope at all three spawn paths (phases,
|
|
95
|
-
* /task-auto planning, /task-plan), it is what the loader and the debug trail
|
|
96
|
-
* already print, and it is the one thing a reader can check against the phase
|
|
97
|
-
* list without following the call graph. Threading a group parameter through
|
|
98
|
-
* `PhaseDeps` / `AutoDeps` instead would touch both orchestrators' dep bags to
|
|
99
|
-
* express something the call site already says out loud.
|
|
100
|
-
*
|
|
101
|
-
* AN UNMAPPED NAME IS A BUILD FAILURE, not a silent `inherit`.
|
|
102
|
-
* `reasoning-groups.test.ts` scans every literal child name under `src/task`
|
|
103
|
-
* and fails if it is missing here. A defaulting lookup would let a phase added
|
|
104
|
-
* later opt itself out of the table without anyone deciding to.
|
|
105
|
-
*
|
|
106
|
-
* The gate and extraction groups are NOT here: those children reach the model
|
|
107
|
-
* through `groupThinkingArgs('gate' | 'extraction')` at call sites with no name
|
|
108
|
-
* in scope (gate-deps.ts, fetch-core.ts, docs-core.ts, pi-worker-docs.ts). The
|
|
109
|
-
* four research workers do have a name — their `spec.label` — so they are here.
|
|
110
|
-
*/
|
|
111
|
-
export declare const REASONING_GROUP_BY_CHILD: Readonly<Record<string, ReasoningGroup>>;
|
|
112
|
-
/**
|
|
113
|
-
* The group a named child belongs to.
|
|
114
|
-
*
|
|
115
|
-
* Returns `undefined` for a name the table does not know, and the CALLER decides
|
|
116
|
-
* what that means. `runPhaseChild` treats it as `inherit` — a child that reaches
|
|
117
|
-
* the model with today's argv is always safe — while the test treats it as a
|
|
118
|
-
* failure. That split is deliberate: the guard belongs at build time, where
|
|
119
|
-
* someone can fix it, not at run time, where it would abort a user's task over a
|
|
120
|
-
* missing table row.
|
|
121
|
-
*/
|
|
122
|
-
export declare function reasoningGroupForChild(name: string): ReasoningGroup | undefined;
|
|
123
74
|
/**
|
|
124
75
|
* Always returns a COMPLETE record, never a partial one.
|
|
125
76
|
*
|
|
@@ -128,16 +79,16 @@ export declare function reasoningGroupForChild(name: string): ReasoningGroup | u
|
|
|
128
79
|
* `undefined`, and every call site would need its own fallback. Filling the gaps
|
|
129
80
|
* here means the type is true at the only place that constructs the value.
|
|
130
81
|
*/
|
|
131
|
-
export declare function sanitizeReasoningLevels(value: unknown): Record<
|
|
82
|
+
export declare function sanitizeReasoningLevels(value: unknown): Record<ChildGroup, GroupSetting>;
|
|
132
83
|
/**
|
|
133
84
|
* What one group is actually set to, given a config. The ONLY place the four
|
|
134
85
|
* modes are interpreted.
|
|
135
86
|
*
|
|
136
87
|
* `cfg` is required rather than defaulted to `getConfig()` so this module stays
|
|
137
88
|
* import-free (see the header). Callers that want the live config use
|
|
138
|
-
* `groupThinkingArgs` from
|
|
89
|
+
* `groupThinkingArgs` from group-args.ts.
|
|
139
90
|
*/
|
|
140
|
-
export declare function resolveReasoning(group:
|
|
91
|
+
export declare function resolveReasoning(group: ChildGroup, cfg: PiTaskConfig): GroupSetting;
|
|
141
92
|
/**
|
|
142
93
|
* The WHOLE table, as this config will actually run it.
|
|
143
94
|
*
|
|
@@ -146,7 +97,7 @@ export declare function resolveReasoning(group: ReasoningGroup, cfg: PiTaskConfi
|
|
|
146
97
|
* asks the single-group question instead, through `resolveReasoning` — which is
|
|
147
98
|
* the only place the four modes are interpreted.
|
|
148
99
|
*/
|
|
149
|
-
export declare function effectiveReasoning(cfg: PiTaskConfig): Record<
|
|
100
|
+
export declare function effectiveReasoning(cfg: PiTaskConfig): Record<ChildGroup, GroupSetting>;
|
|
150
101
|
/**
|
|
151
102
|
* The argv fragment for a setting. `inherit` is the empty fragment — no flag at
|
|
152
103
|
* all — which is what makes an all-`inherit` config byte-identical to the
|
|
@@ -154,4 +105,12 @@ export declare function effectiveReasoning(cfg: PiTaskConfig): Record<ReasoningG
|
|
|
154
105
|
*/
|
|
155
106
|
export declare function thinkingArgs(setting: GroupSetting): string[];
|
|
156
107
|
/** One honest sentence per group, for the /task-config rows. */
|
|
157
|
-
export declare const REASONING_GROUP_HELP: Readonly<Record<
|
|
108
|
+
export declare const REASONING_GROUP_HELP: Readonly<Record<ChildGroup, string>>;
|
|
109
|
+
/**
|
|
110
|
+
* One honest sentence per model row.
|
|
111
|
+
*
|
|
112
|
+
* They are NOT the reasoning help reworded. A model cell answers a different
|
|
113
|
+
* question — which machine does this work — and one of them costs money every
|
|
114
|
+
* turn rather than once per change, which is a thing the row has to say.
|
|
115
|
+
*/
|
|
116
|
+
export declare const MODEL_GROUP_HELP: Readonly<Record<ChildGroup, string>>;
|
package/dist/config/reasoning.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'gate',
|
|
11
|
-
'extraction',
|
|
12
|
-
'implementation'
|
|
13
|
-
];
|
|
1
|
+
import { CHILD_GROUPS, sanitizeGroupRecord } from './groups.js';
|
|
2
|
+
/**
|
|
3
|
+
* Re-exported because this module's whole public surface — the default table,
|
|
4
|
+
* `resolveReasoning`, `effectiveReasoning` — is typed on them, so an importer of
|
|
5
|
+
* those already needs them. The roster itself lives in groups.ts; `GROUP_BY_CHILD`
|
|
6
|
+
* and `groupForChild` are deliberately NOT re-exported, so a caller that wants
|
|
7
|
+
* the roster reaches for the roster.
|
|
8
|
+
*/
|
|
9
|
+
export { CHILD_GROUPS };
|
|
14
10
|
export const REASONING_MODES = ['default', 'on', 'off', 'custom'];
|
|
15
11
|
/**
|
|
16
12
|
* The settings offered in /task-config: `inherit` plus the standard part of pi's
|
|
@@ -56,80 +52,6 @@ export const DEFAULT_REASONING_TABLE = {
|
|
|
56
52
|
export function sanitizeReasoningMode(value) {
|
|
57
53
|
return REASONING_MODES.includes(value) ? value : 'default';
|
|
58
54
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Child NAME → reasoning group, for every child spawned under a name:
|
|
61
|
-
* `runPhaseChild`, `runPlanningChild`, and the research workers' `spec.label`.
|
|
62
|
-
*
|
|
63
|
-
* WHY KEYED ON THE NAME
|
|
64
|
-
* ---------------------
|
|
65
|
-
* The name is the only identifier in scope at all three spawn paths (phases,
|
|
66
|
-
* /task-auto planning, /task-plan), it is what the loader and the debug trail
|
|
67
|
-
* already print, and it is the one thing a reader can check against the phase
|
|
68
|
-
* list without following the call graph. Threading a group parameter through
|
|
69
|
-
* `PhaseDeps` / `AutoDeps` instead would touch both orchestrators' dep bags to
|
|
70
|
-
* express something the call site already says out loud.
|
|
71
|
-
*
|
|
72
|
-
* AN UNMAPPED NAME IS A BUILD FAILURE, not a silent `inherit`.
|
|
73
|
-
* `reasoning-groups.test.ts` scans every literal child name under `src/task`
|
|
74
|
-
* and fails if it is missing here. A defaulting lookup would let a phase added
|
|
75
|
-
* later opt itself out of the table without anyone deciding to.
|
|
76
|
-
*
|
|
77
|
-
* The gate and extraction groups are NOT here: those children reach the model
|
|
78
|
-
* through `groupThinkingArgs('gate' | 'extraction')` at call sites with no name
|
|
79
|
-
* in scope (gate-deps.ts, fetch-core.ts, docs-core.ts, pi-worker-docs.ts). The
|
|
80
|
-
* four research workers do have a name — their `spec.label` — so they are here.
|
|
81
|
-
*/
|
|
82
|
-
export const REASONING_GROUP_BY_CHILD = {
|
|
83
|
-
// ── phase: task/phases.ts + task/title-label.ts ──────────────────────────
|
|
84
|
-
refine: 'phase',
|
|
85
|
-
'verify-tooling': 'phase',
|
|
86
|
-
'grill-auto': 'phase',
|
|
87
|
-
'grill-gen': 'phase',
|
|
88
|
-
compose: 'phase',
|
|
89
|
-
critique: 'phase',
|
|
90
|
-
'critique-triage': 'phase',
|
|
91
|
-
'compress-label': 'phase',
|
|
92
|
-
// ── planning: task/auto-orchestrator.ts ──────────────────────────────────
|
|
93
|
-
'clarify-triage': 'planning',
|
|
94
|
-
'auto-clarify': 'planning',
|
|
95
|
-
'auto-decompose': 'planning',
|
|
96
|
-
'requirement-extract': 'planning',
|
|
97
|
-
'decompose-coverage': 'planning',
|
|
98
|
-
'coverage-map': 'planning',
|
|
99
|
-
'contract-extract': 'planning',
|
|
100
|
-
'launch-extract': 'planning',
|
|
101
|
-
// ── plan: task/plan-orchestrator.ts ──────────────────────────────────────
|
|
102
|
-
'plan-question': 'plan',
|
|
103
|
-
'plan-answer': 'plan',
|
|
104
|
-
// ── research: task/phases.ts `workerSpecs`, keyed on the spec's LABEL ─────
|
|
105
|
-
'worker:files': 'research:files',
|
|
106
|
-
'worker:apis': 'research:apis',
|
|
107
|
-
'worker:context': 'research:context',
|
|
108
|
-
'worker:tooling': 'research:tooling'
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* The group a named child belongs to.
|
|
112
|
-
*
|
|
113
|
-
* Returns `undefined` for a name the table does not know, and the CALLER decides
|
|
114
|
-
* what that means. `runPhaseChild` treats it as `inherit` — a child that reaches
|
|
115
|
-
* the model with today's argv is always safe — while the test treats it as a
|
|
116
|
-
* failure. That split is deliberate: the guard belongs at build time, where
|
|
117
|
-
* someone can fix it, not at run time, where it would abort a user's task over a
|
|
118
|
-
* missing table row.
|
|
119
|
-
*/
|
|
120
|
-
export function reasoningGroupForChild(name) {
|
|
121
|
-
return REASONING_GROUP_BY_CHILD[name];
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* For a `research:*` group, the group a stored config falls back to when its own
|
|
125
|
-
* key is missing. Every other group maps to `undefined`.
|
|
126
|
-
*/
|
|
127
|
-
const RESEARCH_SUBGROUP_PARENT = {
|
|
128
|
-
'research:files': 'research',
|
|
129
|
-
'research:apis': 'research',
|
|
130
|
-
'research:context': 'research',
|
|
131
|
-
'research:tooling': 'research'
|
|
132
|
-
};
|
|
133
55
|
/**
|
|
134
56
|
* Always returns a COMPLETE record, never a partial one.
|
|
135
57
|
*
|
|
@@ -139,25 +61,7 @@ const RESEARCH_SUBGROUP_PARENT = {
|
|
|
139
61
|
* here means the type is true at the only place that constructs the value.
|
|
140
62
|
*/
|
|
141
63
|
export function sanitizeReasoningLevels(value) {
|
|
142
|
-
|
|
143
|
-
value
|
|
144
|
-
: {};
|
|
145
|
-
const valid = (v) => REASONING_SETTINGS.includes(v);
|
|
146
|
-
const out = {};
|
|
147
|
-
for (const group of REASONING_GROUPS) {
|
|
148
|
-
const stored_ = stored[group];
|
|
149
|
-
if (valid(stored_)) {
|
|
150
|
-
out[group] = stored_;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
// A `research:*` key the stored config never had falls back to its
|
|
154
|
-
// parent `research` value, so a config written before the split keeps
|
|
155
|
-
// meaning what it meant.
|
|
156
|
-
const parent = RESEARCH_SUBGROUP_PARENT[group];
|
|
157
|
-
out[group] =
|
|
158
|
-
parent && valid(stored[parent]) ? stored[parent] : DEFAULT_REASONING_TABLE[group];
|
|
159
|
-
}
|
|
160
|
-
return out;
|
|
64
|
+
return sanitizeGroupRecord(value, (v) => REASONING_SETTINGS.includes(v), group => DEFAULT_REASONING_TABLE[group]);
|
|
161
65
|
}
|
|
162
66
|
/**
|
|
163
67
|
* What one group is actually set to, given a config. The ONLY place the four
|
|
@@ -165,7 +69,7 @@ export function sanitizeReasoningLevels(value) {
|
|
|
165
69
|
*
|
|
166
70
|
* `cfg` is required rather than defaulted to `getConfig()` so this module stays
|
|
167
71
|
* import-free (see the header). Callers that want the live config use
|
|
168
|
-
* `groupThinkingArgs` from
|
|
72
|
+
* `groupThinkingArgs` from group-args.ts.
|
|
169
73
|
*/
|
|
170
74
|
export function resolveReasoning(group, cfg) {
|
|
171
75
|
switch (cfg.reasoningMode) {
|
|
@@ -189,7 +93,7 @@ export function resolveReasoning(group, cfg) {
|
|
|
189
93
|
*/
|
|
190
94
|
export function effectiveReasoning(cfg) {
|
|
191
95
|
const out = {};
|
|
192
|
-
for (const group of
|
|
96
|
+
for (const group of CHILD_GROUPS)
|
|
193
97
|
out[group] = resolveReasoning(group, cfg);
|
|
194
98
|
return out;
|
|
195
99
|
}
|
|
@@ -224,3 +128,28 @@ export const REASONING_GROUP_HELP = {
|
|
|
224
128
|
implementation: 'The main session turn that actually writes the code. Changing this briefly '
|
|
225
129
|
+ "changes pi's own thinking level, and puts it back afterwards."
|
|
226
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* One honest sentence per model row.
|
|
133
|
+
*
|
|
134
|
+
* They are NOT the reasoning help reworded. A model cell answers a different
|
|
135
|
+
* question — which machine does this work — and one of them costs money every
|
|
136
|
+
* turn rather than once per change, which is a thing the row has to say.
|
|
137
|
+
*/
|
|
138
|
+
export const MODEL_GROUP_HELP = {
|
|
139
|
+
research: 'The pi-worker subagent tool, and the fallback for any research worker below. '
|
|
140
|
+
+ 'Long read-only loops: a cheap fast model pays off here.',
|
|
141
|
+
'research:files': 'Research worker 1 of 4: maps which files the task will touch.',
|
|
142
|
+
'research:apis': 'Research worker 2 of 4: the symbols and signatures the task must call.',
|
|
143
|
+
'research:context': 'Research worker 3 of 4: how the project is put together.',
|
|
144
|
+
'research:tooling': 'Research worker 4 of 4: the commands that build, test and run it.',
|
|
145
|
+
phase: 'Refining your request, generating and answering the clarifying questions, '
|
|
146
|
+
+ 'writing the spec, and critiquing it.',
|
|
147
|
+
planning: "/task-auto's planners: splitting a design document into tasks.",
|
|
148
|
+
plan: "/task-plan's interactive question-and-answer children.",
|
|
149
|
+
gate: 'The checks that run after code is written: verify, enforce, lint-fix, autofix.',
|
|
150
|
+
extraction: 'The small no-tools children that pull one answer out of a page or a docs chunk.',
|
|
151
|
+
implementation: 'The main session turn that writes the code — YOUR session, switched for the turn and '
|
|
152
|
+
+ 'switched back. Unlike every row above, this one is not free: a model switch re-bills '
|
|
153
|
+
+ 'the whole prompt as a cache miss, twice per task. Leave it on inherit unless you '
|
|
154
|
+
+ 'want a different model than the one you are reading this in.'
|
|
155
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { SettingsList } from '@earendil-works/pi-tui';
|
|
3
|
-
import type { Component } from '@earendil-works/pi-tui';
|
|
3
|
+
import type { Component, SelectItem } from '@earendil-works/pi-tui';
|
|
4
|
+
import { type ReasoningModelFacts } from '../shared/reasoning-capability.js';
|
|
4
5
|
import { type PiTaskConfig } from './config.js';
|
|
5
6
|
import { type InstalledExtension } from './extension-list.js';
|
|
6
7
|
import { type GuardableTool } from './tool-list.js';
|
|
7
|
-
import { type
|
|
8
|
+
import { type GroupSetting, type ChildGroup } from './reasoning.js';
|
|
8
9
|
type Theme = ExtensionCommandContext['ui']['theme'];
|
|
9
10
|
/**
|
|
10
11
|
* Frames a child component (the settings list) in a rounded border with a title
|
|
@@ -73,6 +74,17 @@ export interface ConfigItem {
|
|
|
73
74
|
description: string;
|
|
74
75
|
/** Offered values. Omitted for a boolean, which is always on/off. */
|
|
75
76
|
values?: string[];
|
|
77
|
+
/**
|
|
78
|
+
* Rows whose choice is a LIST, not a cycle. Present ⇒ Enter opens a picker
|
|
79
|
+
* instead of stepping `values`.
|
|
80
|
+
*
|
|
81
|
+
* A FUNCTION of the draft config, evaluated at Enter-time, because what a row
|
|
82
|
+
* may offer can depend on another row the user changed while the panel was
|
|
83
|
+
* open: a group's thinking options narrow to what its chosen model declares.
|
|
84
|
+
* `values` stays the STATIC complete vocabulary, because that is what the
|
|
85
|
+
* round-trip property in config-items.test.ts quantifies over.
|
|
86
|
+
*/
|
|
87
|
+
submenu?: (cfg: PiTaskConfig) => SelectItem[];
|
|
76
88
|
/** What the panel shows for the current value. */
|
|
77
89
|
format: (cfg: PiTaskConfig) => string;
|
|
78
90
|
/** Write the chosen label back. A value it does not recognise is ignored. */
|
|
@@ -92,7 +104,7 @@ export interface ConfigItem {
|
|
|
92
104
|
* exemptions from it) end up separated by rows that have nothing to do with
|
|
93
105
|
* them. The headers are inert rows: no `values`, so Enter does nothing on them.
|
|
94
106
|
*/
|
|
95
|
-
export type Section = 'session' | 'checks' | 'research' | 'reasoning' | 'timeouts' | 'unattended' | 'logging' | 'extensions';
|
|
107
|
+
export type Section = 'session' | 'checks' | 'research' | 'models' | 'reasoning' | 'timeouts' | 'unattended' | 'logging' | 'extensions';
|
|
96
108
|
/** Section order, and the label each header renders. */
|
|
97
109
|
export declare const SECTIONS: ReadonlyArray<{
|
|
98
110
|
key: Section;
|
|
@@ -100,6 +112,18 @@ export declare const SECTIONS: ReadonlyArray<{
|
|
|
100
112
|
}>;
|
|
101
113
|
/** Marks a header row, so onChange can ignore one and tests can find them. */
|
|
102
114
|
export declare const SECTION_ID_PREFIX = "section:";
|
|
115
|
+
/**
|
|
116
|
+
* Is this row scenery rather than a setting?
|
|
117
|
+
*
|
|
118
|
+
* Reads the ID, not `values`. Those two agreed only while every real row cycled
|
|
119
|
+
* a list: a picker row has a `submenu` and may carry `values` purely for the
|
|
120
|
+
* round-trip contract, so "no values" stopped meaning "not a row". Both
|
|
121
|
+
* `sectionHeader` and `sectionGap` already stamp the prefix, so this is exact
|
|
122
|
+
* rather than a heuristic, and all three consumers ask the same question.
|
|
123
|
+
*/
|
|
124
|
+
export declare const isSectionRow: (item: {
|
|
125
|
+
id: string;
|
|
126
|
+
}) => boolean;
|
|
103
127
|
/**
|
|
104
128
|
* Every setting rendered by /task-config, in display order.
|
|
105
129
|
*
|
|
@@ -125,13 +149,60 @@ export declare function applyToolToggle(exempt: readonly string[], toolName: str
|
|
|
125
149
|
* So a child is drawn as a tree branch under its parent and loses the repeated
|
|
126
150
|
* `think: research:` prefix, the same text at the head of four consecutive
|
|
127
151
|
* lines. `└─` on the last child, `├─` on the rest, decided from the group's
|
|
128
|
-
* position in {@link
|
|
152
|
+
* position in {@link CHILD_GROUPS} rather than a hand-kept list — adding a
|
|
129
153
|
* fifth worker moves the corner on its own.
|
|
130
154
|
*
|
|
131
155
|
* Leading spaces survive: SettingsList pads the label right, never trims it.
|
|
132
156
|
*/
|
|
133
|
-
export declare function reasoningRowLabel(group:
|
|
134
|
-
|
|
157
|
+
export declare function reasoningRowLabel(group: ChildGroup): string;
|
|
158
|
+
/**
|
|
159
|
+
* What this machine can offer a model row, and what each offer can DO.
|
|
160
|
+
*
|
|
161
|
+
* `facts` is what lets the thinking rows narrow: a group pinned to a
|
|
162
|
+
* `reasoning: false` model may only be offered `off`. It answers `undefined` for
|
|
163
|
+
* a spec it cannot resolve — a vanished model, or `inherit` before a session
|
|
164
|
+
* exists — and every consumer reads that as "offer everything", which is exactly
|
|
165
|
+
* today's behaviour.
|
|
166
|
+
*/
|
|
167
|
+
export interface ModelCatalog {
|
|
168
|
+
/** Offerable `provider/id` specs, without `inherit`. */
|
|
169
|
+
specs: readonly string[];
|
|
170
|
+
/** Human note per spec, e.g. an extension-provided provider's warning. */
|
|
171
|
+
note?: (spec: string) => string | undefined;
|
|
172
|
+
facts: (spec: string) => ReasoningModelFacts | undefined;
|
|
173
|
+
}
|
|
174
|
+
/** No registry reachable. Every row still renders; nothing narrows. */
|
|
175
|
+
export declare const EMPTY_CATALOG: ModelCatalog;
|
|
176
|
+
export declare function modelItems(catalog: ModelCatalog): ConfigItem[];
|
|
177
|
+
/**
|
|
178
|
+
* Write a group's model, and RE-CLAMP its thinking cell in the same write.
|
|
179
|
+
*
|
|
180
|
+
* Narrowing the picker does nothing about a level already stored from before the
|
|
181
|
+
* model was chosen: on its own it would freeze a lie into a cell it has just
|
|
182
|
+
* made unconfigurable. `syncRows` re-renders every row, so the user sees the
|
|
183
|
+
* level move.
|
|
184
|
+
*
|
|
185
|
+
* The clamp reads `resolveReasoning`, NOT `cfg.reasoningLevels[group]`. In mode
|
|
186
|
+
* `on` or `off` the stored table is ignored entirely, so a user in `on` who
|
|
187
|
+
* picks a non-reasoning model has a stored cell that says nothing and an
|
|
188
|
+
* effective `medium` the model will erase — and comparing the stored cell would
|
|
189
|
+
* see no clamp and stay silent about a real lie.
|
|
190
|
+
*
|
|
191
|
+
* Guarded on the clamp actually MOVING something, because `applyReasoningLevel`
|
|
192
|
+
* flips the whole table to `custom`, and picking a fully-capable model must not
|
|
193
|
+
* do that as a side effect.
|
|
194
|
+
*/
|
|
195
|
+
export declare function applyGroupModel(cfg: PiTaskConfig, group: ChildGroup, chosen: string, catalog: ModelCatalog): void;
|
|
196
|
+
/**
|
|
197
|
+
* The levels a row may offer, given the model that row's group will run on.
|
|
198
|
+
*
|
|
199
|
+
* The INTERSECTION with `REASONING_SETTINGS`, not `supportedThinkingLevels`
|
|
200
|
+
* directly: that returns the whole ladder including `xhigh` and `max`, which
|
|
201
|
+
* this menu excludes on purpose (see reasoning.ts) because pi's own UI may not
|
|
202
|
+
* offer them. A model declaring `xhigh` must not smuggle it in here.
|
|
203
|
+
*/
|
|
204
|
+
export declare function offeredLevels(facts: ReasoningModelFacts | undefined): GroupSetting[];
|
|
205
|
+
export declare function reasoningItems(catalog?: ModelCatalog): ConfigItem[];
|
|
135
206
|
/**
|
|
136
207
|
* Apply one group row's new value.
|
|
137
208
|
*
|
|
@@ -142,7 +213,7 @@ export declare function reasoningItems(): ConfigItem[];
|
|
|
142
213
|
* it, nudging `research` while in `off` would silently return every other group
|
|
143
214
|
* to whatever the stored table happened to hold.
|
|
144
215
|
*/
|
|
145
|
-
export declare function applyReasoningLevel(cfg: PiTaskConfig, group:
|
|
216
|
+
export declare function applyReasoningLevel(cfg: PiTaskConfig, group: ChildGroup, chosen: string): void;
|
|
146
217
|
/**
|
|
147
218
|
* Tallest body the settings list can render, so {@link BorderedBox} can pad
|
|
148
219
|
* every frame to it and hold the border still. Mirrors SettingsList's own
|
|
@@ -157,11 +228,26 @@ export type PanelItem = {
|
|
|
157
228
|
description: string;
|
|
158
229
|
currentValue: string;
|
|
159
230
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
231
|
+
* The values a cycling row steps through. A section header carries none.
|
|
232
|
+
*
|
|
233
|
+
* It is NOT what marks a header any more — {@link isSectionRow} reads the id
|
|
234
|
+
* for that. A model row legitimately has both a `values` list and a
|
|
235
|
+
* `submenu`, so "no values" and "not a row" stopped being the same fact the
|
|
236
|
+
* moment pickers existed.
|
|
163
237
|
*/
|
|
164
238
|
values?: string[];
|
|
239
|
+
/**
|
|
240
|
+
* Enter-time options for a picker row, already closed over the draft config
|
|
241
|
+
* by {@link renderRows}.
|
|
242
|
+
*
|
|
243
|
+
* Deliberately NOT called `submenu`: pi-tui's `SettingItem.submenu` is a
|
|
244
|
+
* COMPONENT FACTORY, and a PanelItem is handed to `SettingsList` directly by
|
|
245
|
+
* tests and by the headless path. Two different things under one name would
|
|
246
|
+
* make PanelItem stop being assignable to SettingItem, for no gain.
|
|
247
|
+
* {@link createSettingsPanel} is where a theme exists, so it is where these
|
|
248
|
+
* options become a component.
|
|
249
|
+
*/
|
|
250
|
+
submenuOptions?: () => SelectItem[];
|
|
165
251
|
/**
|
|
166
252
|
* What the headless one-line rendering calls this row, when `label` reads
|
|
167
253
|
* only in the panel. A tree branch means nothing on a line of `|`-joined
|
|
@@ -198,11 +284,11 @@ onChange: (id: string, newValue: string, list: SettingsList) => void, onCancel:
|
|
|
198
284
|
* Fixed rows come before discovered ones within a section, so a freshly
|
|
199
285
|
* installed extension appends rather than reshuffling the menu.
|
|
200
286
|
*/
|
|
201
|
-
export declare function configRows(installed: InstalledExtension[], tools?: readonly GuardableTool[]): ConfigItem[];
|
|
287
|
+
export declare function configRows(installed: InstalledExtension[], tools?: readonly GuardableTool[], catalog?: ModelCatalog): ConfigItem[];
|
|
202
288
|
/** Render `rows` for the current config, grouped under their section headers. */
|
|
203
289
|
export declare function renderRows(cfg: PiTaskConfig, rows: readonly ConfigItem[]): PanelItem[];
|
|
204
290
|
/** The full settings row list for the current config, in menu order. */
|
|
205
|
-
export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[], tools?: readonly GuardableTool[]): PanelItem[];
|
|
291
|
+
export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[], tools?: readonly GuardableTool[], catalog?: ModelCatalog): PanelItem[];
|
|
206
292
|
/**
|
|
207
293
|
* Re-ask every row what it now displays, and write the answers back.
|
|
208
294
|
*
|