@mjasnikovs/pi-task 0.38.31 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -1
  2. package/dist/config/config.d.ts +16 -2
  3. package/dist/config/config.js +7 -2
  4. package/dist/config/group-args.d.ts +52 -0
  5. package/dist/config/group-args.js +110 -0
  6. package/dist/config/group-models.d.ts +88 -0
  7. package/dist/config/group-models.js +117 -0
  8. package/dist/config/groups.d.ts +76 -0
  9. package/dist/config/groups.js +110 -0
  10. package/dist/config/option-picker.d.ts +70 -0
  11. package/dist/config/option-picker.js +113 -0
  12. package/dist/config/reasoning.d.ts +26 -64
  13. package/dist/config/reasoning.js +31 -115
  14. package/dist/config/register.d.ts +144 -24
  15. package/dist/config/register.js +345 -56
  16. package/dist/index.js +2 -0
  17. package/dist/remote/push.js +1 -7
  18. package/dist/shared/data-home.d.ts +8 -0
  19. package/dist/shared/data-home.js +14 -0
  20. package/dist/shared/model-endpoint.d.ts +53 -0
  21. package/dist/shared/model-endpoint.js +98 -2
  22. package/dist/shared/reasoning-capability.d.ts +25 -5
  23. package/dist/shared/reasoning-capability.js +18 -9
  24. package/dist/task/child-runner.d.ts +19 -16
  25. package/dist/task/child-runner.js +64 -36
  26. package/dist/task/context-usage.d.ts +46 -0
  27. package/dist/task/context-usage.js +41 -0
  28. package/dist/task/gate-child.d.ts +15 -4
  29. package/dist/task/gate-child.js +2 -2
  30. package/dist/task/gate-deps.js +7 -2
  31. package/dist/task/implementation-hold.d.ts +118 -0
  32. package/dist/task/implementation-hold.js +165 -0
  33. package/dist/task/model-hold-stash.d.ts +43 -0
  34. package/dist/task/model-hold-stash.js +70 -0
  35. package/dist/task/orchestrator.d.ts +18 -5
  36. package/dist/task/orchestrator.js +36 -4
  37. package/dist/task/phases.js +2 -2
  38. package/dist/task/research-worker.d.ts +2 -2
  39. package/dist/task/research-worker.js +1 -1
  40. package/dist/workers/docs-core.js +2 -2
  41. package/dist/workers/docs-lookup.d.ts +4 -3
  42. package/dist/workers/docs-lookup.js +1 -1
  43. package/dist/workers/fetch-core.js +2 -2
  44. package/dist/workers/focused-extractor.d.ts +4 -3
  45. package/dist/workers/focused-extractor.js +5 -4
  46. package/dist/workers/index.js +2 -0
  47. package/dist/workers/model-warning.d.ts +69 -0
  48. package/dist/workers/model-warning.js +113 -0
  49. package/dist/workers/pi-worker-core.d.ts +7 -7
  50. package/dist/workers/pi-worker-core.js +4 -3
  51. package/dist/workers/pi-worker-docs.js +2 -2
  52. package/dist/workers/pi-worker.js +4 -4
  53. package/dist/workers/reasoning-warning.d.ts +17 -9
  54. package/dist/workers/reasoning-warning.js +69 -22
  55. package/package.json +1 -1
  56. package/dist/config/reasoning-args.d.ts +0 -23
  57. package/dist/config/reasoning-args.js +0 -28
  58. package/dist/task/implementation-thinking.d.ts +0 -56
  59. package/dist/task/implementation-thinking.js +0 -32
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The two-step picker a /task-config step row opens on Enter.
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 pair chosen out of two long lists costs
13
+ * ONE write.
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
+ /** What the second stage offers, once the first has been answered. */
30
+ export interface PairStage {
31
+ options: readonly SelectItem[];
32
+ /**
33
+ * The option to open on — the value that will ACTUALLY run.
34
+ *
35
+ * Supplied by the caller rather than derived here, because deciding it is
36
+ * the clamp, and the clamp belongs to the config layer that owns the model's
37
+ * declared ladder. It is what makes the auto-switch visible: pick a model
38
+ * that cannot do `medium` and this stage opens on `off`.
39
+ */
40
+ preselect: string;
41
+ }
42
+ export interface PairOptions {
43
+ first: readonly SelectItem[];
44
+ second: (firstValue: string) => PairStage;
45
+ /** Which half of the stored value stage one opens on. */
46
+ firstOf: (value: string) => string;
47
+ /** How the two answers become the one string the row stores. */
48
+ join: (first: string, second: string) => string;
49
+ }
50
+ /**
51
+ * Model, then thinking level, as one choice.
52
+ *
53
+ * Two stages rather than one flat list of every legal pair: the pair space is
54
+ * models × levels, and the common edit is "change the model, keep the level" —
55
+ * a flat list would make that re-pick both every time.
56
+ *
57
+ * Escape at EITHER stage cancels the whole thing. Going back one step would be
58
+ * friendlier, but a TUI select owns up, down, enter and escape and has no fifth
59
+ * key to spare, and a half-answered pair must never reach `done`.
60
+ */
61
+ export declare class PairPicker extends Container {
62
+ private readonly options;
63
+ private readonly theme;
64
+ private readonly done;
65
+ private stage;
66
+ constructor(options: PairOptions, currentValue: string, theme: Theme, done: (value?: string) => void);
67
+ private openSecond;
68
+ handleInput(data: string): void;
69
+ }
70
+ export {};
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The two-step picker a /task-config step row opens on Enter.
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 pair chosen out of two long lists costs
13
+ * ONE write.
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 list with a text filter above it.
39
+ *
40
+ * Split out because both stages are one of these and the routing is the part
41
+ * that is easy to get wrong: `SelectList` owns four keys and drops the rest, so
42
+ * everything else has to be handed to the `Input` and the filter re-applied.
43
+ */
44
+ class FilterList extends Container {
45
+ onPick;
46
+ onCancel;
47
+ input = new Input();
48
+ list;
49
+ constructor(options, preselect, theme, onPick, onCancel) {
50
+ super();
51
+ this.onPick = onPick;
52
+ this.onCancel = onCancel;
53
+ this.list = new SelectList([...options], PICKER_VISIBLE, selectTheme(theme));
54
+ // A `preselect` matching no option is normal, not an error: a row can
55
+ // hold a spec the catalog no longer offers, and it must still open.
56
+ const at = options.findIndex(o => o.value === preselect);
57
+ if (at !== -1)
58
+ this.list.setSelectedIndex(at);
59
+ this.list.onSelect = item => this.onPick(item.value);
60
+ this.list.onCancel = () => this.onCancel();
61
+ this.addChild(this.input);
62
+ this.addChild(this.list);
63
+ }
64
+ handleInput(data) {
65
+ const kb = getKeybindings();
66
+ const forList = kb.matches(data, 'tui.select.up')
67
+ || kb.matches(data, 'tui.select.down')
68
+ || kb.matches(data, 'tui.select.confirm')
69
+ || kb.matches(data, 'tui.select.cancel');
70
+ if (forList) {
71
+ this.list.handleInput(data);
72
+ return;
73
+ }
74
+ // Everything else is typing. The list re-filters on every keystroke,
75
+ // which is what `setFilter` is for and what nothing else calls.
76
+ this.input.handleInput(data);
77
+ this.list.setFilter(this.input.getValue());
78
+ }
79
+ }
80
+ /**
81
+ * Model, then thinking level, as one choice.
82
+ *
83
+ * Two stages rather than one flat list of every legal pair: the pair space is
84
+ * models × levels, and the common edit is "change the model, keep the level" —
85
+ * a flat list would make that re-pick both every time.
86
+ *
87
+ * Escape at EITHER stage cancels the whole thing. Going back one step would be
88
+ * friendlier, but a TUI select owns up, down, enter and escape and has no fifth
89
+ * key to spare, and a half-answered pair must never reach `done`.
90
+ */
91
+ export class PairPicker extends Container {
92
+ options;
93
+ theme;
94
+ done;
95
+ stage;
96
+ constructor(options, currentValue, theme, done) {
97
+ super();
98
+ this.options = options;
99
+ this.theme = theme;
100
+ this.done = done;
101
+ this.stage = new FilterList(options.first, options.firstOf(currentValue), theme, first => this.openSecond(first), () => done(undefined));
102
+ this.addChild(this.stage);
103
+ }
104
+ openSecond(first) {
105
+ const { options: second, preselect } = this.options.second(first);
106
+ this.stage = new FilterList(second, preselect, this.theme, level => this.done(this.options.join(first, level)), () => this.done(undefined));
107
+ this.clear();
108
+ this.addChild(this.stage);
109
+ }
110
+ handleInput(data) {
111
+ this.stage.handleInput(data);
112
+ }
113
+ }
@@ -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 `reasoning-args.ts` for exactly this reason: importing
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
- * The child roles that share one reasoning setting.
28
- *
29
- * Grouped by JOB, not by spawn mechanism two children that both go through
30
- * `runWorker` (a research worker and a verify gate) want different amounts of
31
- * thinking, while `refine` in phases.ts and `compress-label` in title-label.ts
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 type ReasoningGroup = 'research' | 'research:files' | 'research:apis' | 'research:context' | 'research:tooling' | 'phase' | 'planning' | 'plan' | 'gate' | 'extraction' | 'implementation';
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<ReasoningGroup, GroupSetting>>;
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<ReasoningGroup, GroupSetting>;
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 reasoning-args.ts.
89
+ * `groupThinkingArgs` from group-args.ts.
139
90
  */
140
- export declare function resolveReasoning(group: ReasoningGroup, cfg: PiTaskConfig): GroupSetting;
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,12 +97,23 @@ 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<ReasoningGroup, GroupSetting>;
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
153
104
  * version before this feature existed.
154
105
  */
155
106
  export declare function thinkingArgs(setting: GroupSetting): string[];
156
- /** One honest sentence per group, for the /task-config rows. */
157
- export declare const REASONING_GROUP_HELP: Readonly<Record<ReasoningGroup, string>>;
107
+ /**
108
+ * One honest sentence per step, for the merged /task-config rows.
109
+ *
110
+ * ONE table, not the model help and the reasoning help concatenated. The panel
111
+ * sizes every frame from its tallest description (`settingsBodyHeight`), so
112
+ * gluing two paragraphs together for `implementation` would permanently add
113
+ * about six rows to the whole menu — for a row most users never open.
114
+ *
115
+ * What survives the merge is the part that is not guessable: which children the
116
+ * step actually spawns, what was MEASURED about its thinking level, and the one
117
+ * cost that is paid per turn rather than per change.
118
+ */
119
+ export declare const STEP_GROUP_HELP: Readonly<Record<ChildGroup, string>>;
@@ -1,16 +1,12 @@
1
- export const REASONING_GROUPS = [
2
- 'research',
3
- 'research:files',
4
- 'research:apis',
5
- 'research:context',
6
- 'research:tooling',
7
- 'phase',
8
- 'planning',
9
- 'plan',
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
- const stored = typeof value === 'object' && value !== null && !Array.isArray(value) ?
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 reasoning-args.ts.
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 REASONING_GROUPS)
96
+ for (const group of CHILD_GROUPS)
193
97
  out[group] = resolveReasoning(group, cfg);
194
98
  return out;
195
99
  }
@@ -201,12 +105,23 @@ export function effectiveReasoning(cfg) {
201
105
  export function thinkingArgs(setting) {
202
106
  return setting === 'inherit' ? [] : ['--thinking', setting];
203
107
  }
204
- /** One honest sentence per group, for the /task-config rows. */
205
- export const REASONING_GROUP_HELP = {
206
- research: 'The pi-worker subagent tool, and the fallback for any research worker below '
207
- + 'whose own level is unset. Read-only exploration loops.',
108
+ /**
109
+ * One honest sentence per step, for the merged /task-config rows.
110
+ *
111
+ * ONE table, not the model help and the reasoning help concatenated. The panel
112
+ * sizes every frame from its tallest description (`settingsBodyHeight`), so
113
+ * gluing two paragraphs together for `implementation` would permanently add
114
+ * about six rows to the whole menu — for a row most users never open.
115
+ *
116
+ * What survives the merge is the part that is not guessable: which children the
117
+ * step actually spawns, what was MEASURED about its thinking level, and the one
118
+ * cost that is paid per turn rather than per change.
119
+ */
120
+ export const STEP_GROUP_HELP = {
121
+ research: 'The pi-worker subagent tool, and the fallback for any research worker below. '
122
+ + 'Long read-only loops, where a cheap fast model pays off.',
208
123
  'research:files': 'Research worker 1 of 4: maps which files the task will touch. Read-heavy. '
209
- + 'Measured: the two arms tie, so it runs without thinking.',
124
+ + 'Measured: the two thinking arms tie, so it runs without.',
210
125
  'research:apis': 'Research worker 2 of 4: the symbols and signatures the task must call. '
211
126
  + 'Read-heavy, docs- and search-capable.',
212
127
  'research:context': 'Research worker 3 of 4: how the project is put together. One of the two that '
@@ -221,6 +136,7 @@ export const REASONING_GROUP_HELP = {
221
136
  gate: 'The checks that run after code is written: verify, enforce, lint-fix, autofix.',
222
137
  extraction: 'The small no-tools children that pull one answer out of a fetched page or '
223
138
  + 'a docs chunk.',
224
- implementation: 'The main session turn that actually writes the code. Changing this briefly '
225
- + "changes pi's own thinking level, and puts it back afterwards."
139
+ implementation: 'The main session turn that writes the code YOUR session, moved for the turn and '
140
+ + 'moved back. Unlike every step above, the model half here is not free: a switch '
141
+ + 're-bills the whole prompt as a cache miss, twice per task.'
226
142
  };