@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.
- 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 +70 -0
- package/dist/config/option-picker.js +113 -0
- package/dist/config/reasoning.d.ts +26 -64
- package/dist/config/reasoning.js +31 -115
- package/dist/config/register.d.ts +144 -24
- package/dist/config/register.js +345 -56
- package/dist/index.js +2 -0
- package/dist/remote/push.js +1 -7
- 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/child-runner.d.ts +19 -16
- package/dist/task/child-runner.js +64 -36
- package/dist/task/context-usage.d.ts +46 -0
- package/dist/task/context-usage.js +41 -0
- 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-hold.d.ts +118 -0
- package/dist/task/implementation-hold.js +165 -0
- 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 +36 -4
- package/dist/task/phases.js +2 -2
- 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 +4 -3
- package/dist/workers/focused-extractor.js +5 -4
- 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 +7 -7
- package/dist/workers/pi-worker-core.js +4 -3
- 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/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
package/README.md
CHANGED
|
@@ -214,7 +214,8 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
214
214
|
| **command timeout** | 15 min | Wall-clock ceiling on a **single** tool execution. Local models routinely run a command that never returns (a hung build, a dev server, a check with no timeout) and the run wedges until you abort by hand — pi's bash tool has an optional timeout with no default, so this is the missing one. One knob, two surfaces: in the main session the overrun call is cancelled (killing the tool's whole process tree) plus a reminder turn; in the verify/fix gate children the child is killed and re-spawned with a hint, halving the ceiling on repeat hangs. Choices: 5/10/15/30 min or **off** — off unguards both surfaces, gates included. |
|
|
215
215
|
| **stuck reply retry** | 10 min | Inactivity ceiling on the **model stream**. A hung or silently-dropped stream throws nothing at all, so neither the connection-error retry (it needs a reported error) nor the **command timeout** (tool calls only) nor the dead-backend stall guard (a reachable endpoint reads as proof of life) can see it — an mx5 run lost ~2.9h to three of them while the model server stayed healthy. Measured as time since the **last stream event of any kind**, so a slow model emitting one token every 30s is never touched, and it pauses while a tool runs. On expiry the main session aborts the turn (through the same channel the command watchdog uses) and posts a resume reminder; a child is killed and routed into the existing connection-error retry. Choices: 5/10/20/30 min or **off**. Keep it generous on local backends — prompt processing on a large context legitimately emits nothing for minutes. |
|
|
216
216
|
| **yolo mode** | off | **Unattended runs.** Wherever pi-task would stop and ask, it takes the option already marked RECOMMENDED, stamps the artifact `(YOLO)` so an audit can tell a machine decided, and shows no prompt at all — clarify/grill answers, the verify-FAIL picker (auto-**Accept**, recorded as a yolo debt), and the final-gate picker (autofix while the budget lasts, then leave the run FAILED). A question with no recommendation is **skipped**, never invented. For throwaway/test projects nobody is watching; a real run should decide these itself. |
|
|
217
|
-
| **
|
|
217
|
+
| **profile** | default | How much the helper sessions think, in one word, for every step at once. Local models differ sharply here: some break without reasoning, some waste minutes with it, and some cannot do it at all. **default** uses the per-step table pi-task has measured, **on** and **off** force one answer everywhere and ignore that table, and **custom** is whatever the step rows say — changing any of them switches this to custom. A step on **inherit** passes no flag at all, so it uses whatever thinking level pi itself is set to, which is what every step did before this setting existed. |
|
|
218
|
+
| **steps: …** | all `inherit` | One row per group of steps, carrying BOTH dials: the model those children run on and the level they think at, shown as `level · model`. Enter walks a two-step picker — model first, then level — and **the level step offers only what that model declares, opening on the one that will actually run**. That is the whole point of the merge: pi silently CLAMPS a level a model cannot do (a level you set can be erased, and an `off` can be clamped back up to `medium`), so instead of discovering that later you watch the cursor land on the level you are really getting. Models are offered from `pi.modelRegistry.getAvailable()` and stored as the canonical `provider/id` that pi's own `--model` takes. **inherit** on the model half emits no flag, so an all-inherit table is byte-identical to a build without this feature; that is the shipped default, because which models exist is a property of your machine and nothing here can be measured for you. A stored model this machine cannot resolve is never erased (you may have set it on another machine): the flag is dropped, the step runs on pi's default, and a startup hint names the step. Two need care — a provider registered by a host **extension** needs that extension enabled under **ext: …** or those children exit 1; and **implementation** is not free, because it is *your* session moved for the turn and moved back, and a model switch re-bills the whole prompt as a cache miss, twice per task. |
|
|
218
219
|
| **debug logs** | events | How much of a run is written to `.pi-tasks/*-debug.log`. **`events`** keeps decisions and guard actions — which phase ran, why a worker was retried, what the git-state guard restored, what a write-capable child changed on disk, why a gate returned FAIL — a few lines per task. **`full`** adds every line the child model emitted and every tool result; that's ~85% of the bytes (a real 247 KB `verify-debug.log` is 1315 lines, 521 of them tool dumps) and is what you want while actively debugging. **`off`** writes nothing. Nothing in pi-task ever reads these files back, so the setting cannot change how a run behaves — only whether you can explain it afterwards, and a log not written can't be recovered later. |
|
|
219
220
|
| **watch: …** | all on | One toggle per tool in the live session, deciding whether **command timeout** applies to it. The list is discovered from `pi.getAllTools()` when the menu opens — built-ins first, then each extension's tools with the owning entry-point path in the description — so nothing is typed by hand and an uninstalled tool just stops being listed. Turn one **off** only for a tool that already owns a longer bounded, cancellable contract of its own (the guard exists because pi's `bash` has an optional timeout with *no* default — that reasoning doesn't transfer to a tool that has one). Two things to know before you do: a genuine hang in an unwatched tool is caught by nothing, since **stuck reply retry** is paused for the whole time any tool runs; and an unwatched tool is still killed as collateral if a *watched* sibling in the same turn overruns, because pi runs sibling tool calls concurrently and the abort ends the whole turn. Stored as exemptions, so the default and every tool pi-task has never seen stay guarded. |
|
|
220
221
|
| **ext: …** | all off | One toggle per installed host `pi` extension, loading it into every child session by explicit path. Children otherwise run with extensions off, so a provider registered by an extension (e.g. `pi-lmstudio`) doesn't exist in them and they can't resolve the default model. Children also inherit the extension's tools and hooks, so only enable ones you trust. The list is strictly additive (discovery stays off), and an entry whose file is gone is skipped at spawn time, never fatal. |
|
package/dist/config/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SearchProvider } from '../workers/search-types.js';
|
|
2
|
-
import { type GroupSetting, type
|
|
2
|
+
import { type GroupSetting, type ChildGroup, type ReasoningMode } from './reasoning.js';
|
|
3
3
|
export interface PiTaskConfig {
|
|
4
4
|
remote: boolean;
|
|
5
5
|
autoCommit: boolean;
|
|
@@ -155,7 +155,21 @@ export interface PiTaskConfig {
|
|
|
155
155
|
* survives the watchdog being turned off. Its sanitizer always returns a
|
|
156
156
|
* complete record, so no consumer needs a per-key fallback.
|
|
157
157
|
*/
|
|
158
|
-
reasoningLevels: Record<
|
|
158
|
+
reasoningLevels: Record<ChildGroup, GroupSetting>;
|
|
159
|
+
/**
|
|
160
|
+
* The per-group MODEL, as a `provider/id` string, or `inherit` to emit no
|
|
161
|
+
* `--model` and let the child resolve pi's saved default as it always has.
|
|
162
|
+
*
|
|
163
|
+
* DEFAULT: every cell `inherit`, which makes every child's argv
|
|
164
|
+
* byte-identical to a build without this field. There is no mode enum and no
|
|
165
|
+
* shipped table — a model id is machine-local, so there is nothing to ship.
|
|
166
|
+
*
|
|
167
|
+
* Its sanitizer checks SHAPE, never existence: a spec naming a model this
|
|
168
|
+
* machine cannot resolve survives, because the alternative is deleting a
|
|
169
|
+
* setting the user made on their other machine. The once-per-session
|
|
170
|
+
* resolution check names it instead.
|
|
171
|
+
*/
|
|
172
|
+
groupModels: Record<ChildGroup, string>;
|
|
159
173
|
}
|
|
160
174
|
/** How verbose the `.pi-tasks/*-debug.log` trail is. See {@link PiTaskConfig.debugLogs}. */
|
|
161
175
|
export type DebugLogLevel = 'off' | 'events' | 'full';
|
package/dist/config/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import { isSearchProvider } from '../workers/search-types.js';
|
|
6
6
|
import { DEFAULT_REASONING_TABLE, sanitizeReasoningLevels, sanitizeReasoningMode } from './reasoning.js';
|
|
7
|
+
import { DEFAULT_GROUP_MODELS, sanitizeGroupModels } from './group-models.js';
|
|
7
8
|
import { DEFAULT_STREAM_INACTIVITY_MS } from '../shared/stream-watchdog.js';
|
|
8
9
|
/**
|
|
9
10
|
* The debug-log choices offered by /task-config, in cycle order (quietest →
|
|
@@ -108,7 +109,10 @@ export const DEFAULT_CONFIG = {
|
|
|
108
109
|
// DEFAULT: the shipped per-group table. A cell left at `inherit` emits no
|
|
109
110
|
// --thinking flag, so that child keeps the host's own level.
|
|
110
111
|
reasoningMode: 'default',
|
|
111
|
-
reasoningLevels: { ...DEFAULT_REASONING_TABLE }
|
|
112
|
+
reasoningLevels: { ...DEFAULT_REASONING_TABLE },
|
|
113
|
+
// INHERIT everywhere. Nothing is measured here and nothing can be: which
|
|
114
|
+
// models exist is a property of the machine, not of pi-task.
|
|
115
|
+
groupModels: { ...DEFAULT_GROUP_MODELS }
|
|
112
116
|
};
|
|
113
117
|
/**
|
|
114
118
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
@@ -154,7 +158,8 @@ export const CONFIG_LOADERS = {
|
|
|
154
158
|
streamInactivityMs: sanitizeStreamInactivityMs,
|
|
155
159
|
debugLogs: sanitizeDebugLogs,
|
|
156
160
|
reasoningMode: sanitizeReasoningMode,
|
|
157
|
-
reasoningLevels: sanitizeReasoningLevels
|
|
161
|
+
reasoningLevels: sanitizeReasoningLevels,
|
|
162
|
+
groupModels: sanitizeGroupModels
|
|
158
163
|
};
|
|
159
164
|
/**
|
|
160
165
|
* Turn parsed config JSON into a `PiTaskConfig`. Pure, so every hostile-value
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live-config bridge for per-group child settings: group in, argv fragment out.
|
|
3
|
+
*
|
|
4
|
+
* Separate from reasoning.ts and group-models.ts because those must take no
|
|
5
|
+
* import with a runtime side effect — see their headers. The `getConfig()` read
|
|
6
|
+
* lives here instead: this file imports them and nothing in config/ imports it
|
|
7
|
+
* back, so the graph stays a tree.
|
|
8
|
+
*
|
|
9
|
+
* Read PER CALL, never cached at module scope, so a /task-config change lands on
|
|
10
|
+
* the next child without a restart. Same contract `childBaseArgs` keeps.
|
|
11
|
+
*/
|
|
12
|
+
import { type PiTaskConfig } from './config.js';
|
|
13
|
+
import type { ChildGroup } from './groups.js';
|
|
14
|
+
export declare function setUnusableSpecs(specs: Iterable<string>): void;
|
|
15
|
+
export declare function isSpecUsable(spec: string): boolean;
|
|
16
|
+
export declare function setGroupWindows(windows: Readonly<Partial<Record<ChildGroup, number>>>): void;
|
|
17
|
+
/** The group's own window, or `undefined` for "caller keeps its fallback". */
|
|
18
|
+
export declare function groupWindow(group: ChildGroup): number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The `['--model', spec]` fragment for a group, or `[]` for `inherit` and for a
|
|
21
|
+
* spec this session proved unresolvable.
|
|
22
|
+
*
|
|
23
|
+
* Dropping the flag is not the same failure class as passing it. A spec naming a
|
|
24
|
+
* model that is gone, whose PROVIDER still has other models, does not make pi
|
|
25
|
+
* exit — `buildFallbackModel` invents a synthetic model id, forces
|
|
26
|
+
* `reasoning: true` onto it, inherits the provider's default baseUrl and answers
|
|
27
|
+
* at exit 0. Dropping the flag runs the same child the user got last week and
|
|
28
|
+
* says so out loud; passing it runs a model nobody chose and says nothing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function groupModelArgs(group: ChildGroup, cfg?: PiTaskConfig): string[];
|
|
31
|
+
/**
|
|
32
|
+
* The `['--thinking', level]` fragment for a group, or `[]` when the group is
|
|
33
|
+
* `inherit` and the child should keep falling back to settings.json.
|
|
34
|
+
*
|
|
35
|
+
* Still exported on its own: the host-session turn (implementation-hold.ts) and
|
|
36
|
+
* the settings UI (register.ts) need the level rather than a whole fragment.
|
|
37
|
+
*/
|
|
38
|
+
export declare function groupThinkingArgs(group: ChildGroup, cfg?: PiTaskConfig): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Everything one group contributes to a child's argv, in one fragment.
|
|
41
|
+
*
|
|
42
|
+
* Every argv builder calls THIS rather than composing the two halves itself.
|
|
43
|
+
* That is why there is one carried field (`groupArgs`) rather than a `model`
|
|
44
|
+
* beside a `thinking`: a second required field would double the obligation on
|
|
45
|
+
* every future wiring, and a fragment assembled by hand is a doubled
|
|
46
|
+
* `--thinking` waiting to happen.
|
|
47
|
+
*
|
|
48
|
+
* ORDER: `--model` then `--thinking`. pi's parser is a flat loop, so it does not
|
|
49
|
+
* care. A human diffing two runs does — the identity of the child first, then
|
|
50
|
+
* the dial on it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function groupChildArgs(group: ChildGroup, cfg?: PiTaskConfig): string[];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live-config bridge for per-group child settings: group in, argv fragment out.
|
|
3
|
+
*
|
|
4
|
+
* Separate from reasoning.ts and group-models.ts because those must take no
|
|
5
|
+
* import with a runtime side effect — see their headers. The `getConfig()` read
|
|
6
|
+
* lives here instead: this file imports them and nothing in config/ imports it
|
|
7
|
+
* back, so the graph stays a tree.
|
|
8
|
+
*
|
|
9
|
+
* Read PER CALL, never cached at module scope, so a /task-config change lands on
|
|
10
|
+
* the next child without a restart. Same contract `childBaseArgs` keeps.
|
|
11
|
+
*/
|
|
12
|
+
import { getConfig } from './config.js';
|
|
13
|
+
import { MODEL_INHERIT, modelArgs } from './group-models.js';
|
|
14
|
+
import { resolveReasoning, thinkingArgs } from './reasoning.js';
|
|
15
|
+
/**
|
|
16
|
+
* Specs this session has proven a child cannot resolve.
|
|
17
|
+
*
|
|
18
|
+
* WHY A SESSION-SCOPED SET AND NOT A LOOKUP
|
|
19
|
+
* -----------------------------------------
|
|
20
|
+
* The honest question is "can a `--no-extensions` child resolve this spec?", and
|
|
21
|
+
* only `ctx.modelRegistry` can answer it. Five of the six argv producers have no
|
|
22
|
+
* `ctx` — `pi-worker`, `pi-worker-docs`, `docs-core`, `fetch-core` and
|
|
23
|
+
* `child-runner` — so the answer cannot be fetched where it is needed. It also
|
|
24
|
+
* cannot be read from disk: models.json and models-store.json are only part of
|
|
25
|
+
* the catalogue, since pi-ai ships built-in model lists for 39 providers, and
|
|
26
|
+
* this project does not depend on pi-ai.
|
|
27
|
+
*
|
|
28
|
+
* So it is answered ONCE, at session_start, where ctx exists and every task is
|
|
29
|
+
* still in the future, and the verdict is left here.
|
|
30
|
+
*
|
|
31
|
+
* EMPTY MEANS EMIT. A host that never fires session_start therefore behaves
|
|
32
|
+
* exactly as it does today — the failure direction is "pi decides", never "we
|
|
33
|
+
* silently dropped a flag nobody checked".
|
|
34
|
+
*/
|
|
35
|
+
let unusableSpecs = new Set();
|
|
36
|
+
export function setUnusableSpecs(specs) {
|
|
37
|
+
unusableSpecs = new Set(specs);
|
|
38
|
+
}
|
|
39
|
+
export function isSpecUsable(spec) {
|
|
40
|
+
return !unusableSpecs.has(spec);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The context window of each group's model, resolved in the SAME session pass
|
|
44
|
+
* that filled {@link setUnusableSpecs}.
|
|
45
|
+
*
|
|
46
|
+
* It lives here for the same reason that set does — `child-runner` and the
|
|
47
|
+
* workers have no `ctx`, so they cannot ask a registry — and it is filled by the
|
|
48
|
+
* same walk, so the two can never disagree about which model a group runs on.
|
|
49
|
+
*
|
|
50
|
+
* The number drives `StallDetector`'s churn rule, where the two error directions
|
|
51
|
+
* are NOT symmetric: too large fires late (degraded, and the no-new-ground rule
|
|
52
|
+
* still covers it), too small fires early and KILLS A HEALTHY CHILD. So an
|
|
53
|
+
* absent answer means "use the parent's", never a guess.
|
|
54
|
+
*/
|
|
55
|
+
let groupWindows = {};
|
|
56
|
+
export function setGroupWindows(windows) {
|
|
57
|
+
groupWindows = { ...windows };
|
|
58
|
+
}
|
|
59
|
+
/** The group's own window, or `undefined` for "caller keeps its fallback". */
|
|
60
|
+
export function groupWindow(group) {
|
|
61
|
+
const w = groupWindows[group];
|
|
62
|
+
return w !== undefined && w > 0 ? w : undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The `['--model', spec]` fragment for a group, or `[]` for `inherit` and for a
|
|
66
|
+
* spec this session proved unresolvable.
|
|
67
|
+
*
|
|
68
|
+
* Dropping the flag is not the same failure class as passing it. A spec naming a
|
|
69
|
+
* model that is gone, whose PROVIDER still has other models, does not make pi
|
|
70
|
+
* exit — `buildFallbackModel` invents a synthetic model id, forces
|
|
71
|
+
* `reasoning: true` onto it, inherits the provider's default baseUrl and answers
|
|
72
|
+
* at exit 0. Dropping the flag runs the same child the user got last week and
|
|
73
|
+
* says so out loud; passing it runs a model nobody chose and says nothing.
|
|
74
|
+
*/
|
|
75
|
+
export function groupModelArgs(group, cfg) {
|
|
76
|
+
const spec = (cfg ?? getConfig()).groupModels[group];
|
|
77
|
+
if (spec === undefined || spec === MODEL_INHERIT)
|
|
78
|
+
return [];
|
|
79
|
+
return isSpecUsable(spec) ? modelArgs(spec) : [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The `['--thinking', level]` fragment for a group, or `[]` when the group is
|
|
83
|
+
* `inherit` and the child should keep falling back to settings.json.
|
|
84
|
+
*
|
|
85
|
+
* Still exported on its own: the host-session turn (implementation-hold.ts) and
|
|
86
|
+
* the settings UI (register.ts) need the level rather than a whole fragment.
|
|
87
|
+
*/
|
|
88
|
+
export function groupThinkingArgs(group, cfg) {
|
|
89
|
+
// The default is evaluated HERE, per call. Hoisting the read to module scope
|
|
90
|
+
// would leave every test green, so the optional parameter is what makes the
|
|
91
|
+
// per-call contract assertable.
|
|
92
|
+
return thinkingArgs(resolveReasoning(group, cfg ?? getConfig()));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Everything one group contributes to a child's argv, in one fragment.
|
|
96
|
+
*
|
|
97
|
+
* Every argv builder calls THIS rather than composing the two halves itself.
|
|
98
|
+
* That is why there is one carried field (`groupArgs`) rather than a `model`
|
|
99
|
+
* beside a `thinking`: a second required field would double the obligation on
|
|
100
|
+
* every future wiring, and a fragment assembled by hand is a doubled
|
|
101
|
+
* `--thinking` waiting to happen.
|
|
102
|
+
*
|
|
103
|
+
* ORDER: `--model` then `--thinking`. pi's parser is a flat loop, so it does not
|
|
104
|
+
* care. A human diffing two runs does — the identity of the child first, then
|
|
105
|
+
* the dial on it.
|
|
106
|
+
*/
|
|
107
|
+
export function groupChildArgs(group, cfg) {
|
|
108
|
+
const live = cfg ?? getConfig();
|
|
109
|
+
return [...groupModelArgs(group, live), ...groupThinkingArgs(group, live)];
|
|
110
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model each child group runs on — the second table keyed on ChildGroup.
|
|
3
|
+
*
|
|
4
|
+
* PURE MODULE, like reasoning.ts and groups.ts: config.ts imports this during
|
|
5
|
+
* its own evaluation, so nothing here may have a runtime side effect.
|
|
6
|
+
*
|
|
7
|
+
* WHY THERE IS NO MODE ENUM
|
|
8
|
+
* -------------------------
|
|
9
|
+
* reasoningMode has four modes because there is a measured, shipped table worth
|
|
10
|
+
* a one-word "use the project's numbers", plus meaningful global on/off. Neither
|
|
11
|
+
* exists here. A model id is machine-local, so there can be no shipped table,
|
|
12
|
+
* and "all models on" is not a sentence. A mode enum would be four states of
|
|
13
|
+
* which three are unreachable.
|
|
14
|
+
*
|
|
15
|
+
* WHY THE CELL IS A STRING AND NOT A STRUCT
|
|
16
|
+
* -----------------------------------------
|
|
17
|
+
* `provider/id` is exactly what pi's `--model` takes, so the stored value goes
|
|
18
|
+
* to argv untouched. The round-trip property in config-items.test.ts needs the
|
|
19
|
+
* stored value recoverable verbatim from the offered label; a struct forces a
|
|
20
|
+
* format/parse pair that can drift.
|
|
21
|
+
*/
|
|
22
|
+
import { type ChildGroup } from './groups.js';
|
|
23
|
+
/**
|
|
24
|
+
* The cell value meaning "emit no `--model`". Identical in spirit to `inherit`
|
|
25
|
+
* in the reasoning table, and identical in effect: an all-`inherit` table makes
|
|
26
|
+
* every child's argv byte-identical to a build without this feature.
|
|
27
|
+
*
|
|
28
|
+
* It is not a legal `provider/id` — pi rejects an unslashed pattern that matches
|
|
29
|
+
* no model — so it cannot collide with a real spec.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MODEL_INHERIT = "inherit";
|
|
32
|
+
/** Every cell on `inherit`. The shipped default, and deliberately not a table. */
|
|
33
|
+
export declare const DEFAULT_GROUP_MODELS: Readonly<Record<ChildGroup, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* SHAPE ONLY, never existence.
|
|
36
|
+
*
|
|
37
|
+
* A config written on machine A must survive a session on machine B whose
|
|
38
|
+
* provider extension failed to load, or whose models.json is a different file
|
|
39
|
+
* entirely. Erasing a spec because this machine cannot resolve it would punish
|
|
40
|
+
* the two-machine user and lose a setting they never changed. Whether a spec
|
|
41
|
+
* resolves is asked once per session, where a warning can name it — not here,
|
|
42
|
+
* where the only available answer is deletion.
|
|
43
|
+
*
|
|
44
|
+
* So this rejects exactly what could RESHAPE argv, and nothing else:
|
|
45
|
+
* - a non-string, which cannot be an argv token at all
|
|
46
|
+
* - empty or whitespace-only, which pi would read as a missing value and
|
|
47
|
+
* consume the following flag as the model pattern
|
|
48
|
+
* - a leading `-`, which pi's flat parser reads as the next FLAG
|
|
49
|
+
* - embedded whitespace, which is one token here and two on a shell round-trip
|
|
50
|
+
*/
|
|
51
|
+
export declare function isModelSpec(value: unknown): value is string;
|
|
52
|
+
export declare function sanitizeGroupModels(value: unknown): Record<ChildGroup, string>;
|
|
53
|
+
/**
|
|
54
|
+
* The `['--model', spec]` fragment for a resolved cell, or `[]` for `inherit`.
|
|
55
|
+
*
|
|
56
|
+
* NEVER `--provider`. Adding it is what turns pi's loud "model not found, exit
|
|
57
|
+
* 1" into `buildFallbackModel`'s silent synthetic model at exit 0 — the branch
|
|
58
|
+
* is `if (provider)` in pi's model-resolver, and a canonical `provider/id`
|
|
59
|
+
* reaches it through inference only when that provider already has a model the
|
|
60
|
+
* child can see. One flag keeps the loud failure loud.
|
|
61
|
+
*
|
|
62
|
+
* No `:thinking` suffix either. We emit an explicit `--thinking`, which pi
|
|
63
|
+
* prefers over the suffix, so a suffix would be a second dead source of truth
|
|
64
|
+
* for the same dial.
|
|
65
|
+
*/
|
|
66
|
+
export declare function modelArgs(spec: string): string[];
|
|
67
|
+
/**
|
|
68
|
+
* The spec a built argv fragment actually carries, or `undefined` for a child
|
|
69
|
+
* that will resolve pi's saved default.
|
|
70
|
+
*
|
|
71
|
+
* Read BACK OUT of the argv rather than threaded alongside it, deliberately. The
|
|
72
|
+
* argv is what the child runs; a second field carrying "the model we meant"
|
|
73
|
+
* could disagree with it after any future edit, and the one consumer — the
|
|
74
|
+
* dead-backend probe — must ask about the endpoint the child will really use.
|
|
75
|
+
*/
|
|
76
|
+
export declare function modelSpecFromArgs(args: readonly string[]): string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Split a stored spec on its FIRST slash.
|
|
79
|
+
*
|
|
80
|
+
* `openrouter/z-ai/glm-4.6` is provider `openrouter` and id `z-ai/glm-4.6`:
|
|
81
|
+
* OpenRouter-style ids contain slashes of their own, and splitting on the last
|
|
82
|
+
* one would invent the provider `openrouter/z-ai`. Returns `undefined` for a
|
|
83
|
+
* spec with no slash, which `inherit` is and a canonical spec never is.
|
|
84
|
+
*/
|
|
85
|
+
export declare function splitSpec(spec: string): {
|
|
86
|
+
provider: string;
|
|
87
|
+
id: string;
|
|
88
|
+
} | undefined;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model each child group runs on — the second table keyed on ChildGroup.
|
|
3
|
+
*
|
|
4
|
+
* PURE MODULE, like reasoning.ts and groups.ts: config.ts imports this during
|
|
5
|
+
* its own evaluation, so nothing here may have a runtime side effect.
|
|
6
|
+
*
|
|
7
|
+
* WHY THERE IS NO MODE ENUM
|
|
8
|
+
* -------------------------
|
|
9
|
+
* reasoningMode has four modes because there is a measured, shipped table worth
|
|
10
|
+
* a one-word "use the project's numbers", plus meaningful global on/off. Neither
|
|
11
|
+
* exists here. A model id is machine-local, so there can be no shipped table,
|
|
12
|
+
* and "all models on" is not a sentence. A mode enum would be four states of
|
|
13
|
+
* which three are unreachable.
|
|
14
|
+
*
|
|
15
|
+
* WHY THE CELL IS A STRING AND NOT A STRUCT
|
|
16
|
+
* -----------------------------------------
|
|
17
|
+
* `provider/id` is exactly what pi's `--model` takes, so the stored value goes
|
|
18
|
+
* to argv untouched. The round-trip property in config-items.test.ts needs the
|
|
19
|
+
* stored value recoverable verbatim from the offered label; a struct forces a
|
|
20
|
+
* format/parse pair that can drift.
|
|
21
|
+
*/
|
|
22
|
+
import { sanitizeGroupRecord } from './groups.js';
|
|
23
|
+
/**
|
|
24
|
+
* The cell value meaning "emit no `--model`". Identical in spirit to `inherit`
|
|
25
|
+
* in the reasoning table, and identical in effect: an all-`inherit` table makes
|
|
26
|
+
* every child's argv byte-identical to a build without this feature.
|
|
27
|
+
*
|
|
28
|
+
* It is not a legal `provider/id` — pi rejects an unslashed pattern that matches
|
|
29
|
+
* no model — so it cannot collide with a real spec.
|
|
30
|
+
*/
|
|
31
|
+
export const MODEL_INHERIT = 'inherit';
|
|
32
|
+
/** Every cell on `inherit`. The shipped default, and deliberately not a table. */
|
|
33
|
+
export const DEFAULT_GROUP_MODELS = {
|
|
34
|
+
research: MODEL_INHERIT,
|
|
35
|
+
'research:files': MODEL_INHERIT,
|
|
36
|
+
'research:apis': MODEL_INHERIT,
|
|
37
|
+
'research:context': MODEL_INHERIT,
|
|
38
|
+
'research:tooling': MODEL_INHERIT,
|
|
39
|
+
phase: MODEL_INHERIT,
|
|
40
|
+
planning: MODEL_INHERIT,
|
|
41
|
+
plan: MODEL_INHERIT,
|
|
42
|
+
gate: MODEL_INHERIT,
|
|
43
|
+
extraction: MODEL_INHERIT,
|
|
44
|
+
implementation: MODEL_INHERIT
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* SHAPE ONLY, never existence.
|
|
48
|
+
*
|
|
49
|
+
* A config written on machine A must survive a session on machine B whose
|
|
50
|
+
* provider extension failed to load, or whose models.json is a different file
|
|
51
|
+
* entirely. Erasing a spec because this machine cannot resolve it would punish
|
|
52
|
+
* the two-machine user and lose a setting they never changed. Whether a spec
|
|
53
|
+
* resolves is asked once per session, where a warning can name it — not here,
|
|
54
|
+
* where the only available answer is deletion.
|
|
55
|
+
*
|
|
56
|
+
* So this rejects exactly what could RESHAPE argv, and nothing else:
|
|
57
|
+
* - a non-string, which cannot be an argv token at all
|
|
58
|
+
* - empty or whitespace-only, which pi would read as a missing value and
|
|
59
|
+
* consume the following flag as the model pattern
|
|
60
|
+
* - a leading `-`, which pi's flat parser reads as the next FLAG
|
|
61
|
+
* - embedded whitespace, which is one token here and two on a shell round-trip
|
|
62
|
+
*/
|
|
63
|
+
export function isModelSpec(value) {
|
|
64
|
+
if (typeof value !== 'string')
|
|
65
|
+
return false;
|
|
66
|
+
if (value.trim() !== value || value.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
if (value.startsWith('-'))
|
|
69
|
+
return false;
|
|
70
|
+
return !/\s/.test(value);
|
|
71
|
+
}
|
|
72
|
+
export function sanitizeGroupModels(value) {
|
|
73
|
+
return sanitizeGroupRecord(value, isModelSpec, () => MODEL_INHERIT);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The `['--model', spec]` fragment for a resolved cell, or `[]` for `inherit`.
|
|
77
|
+
*
|
|
78
|
+
* NEVER `--provider`. Adding it is what turns pi's loud "model not found, exit
|
|
79
|
+
* 1" into `buildFallbackModel`'s silent synthetic model at exit 0 — the branch
|
|
80
|
+
* is `if (provider)` in pi's model-resolver, and a canonical `provider/id`
|
|
81
|
+
* reaches it through inference only when that provider already has a model the
|
|
82
|
+
* child can see. One flag keeps the loud failure loud.
|
|
83
|
+
*
|
|
84
|
+
* No `:thinking` suffix either. We emit an explicit `--thinking`, which pi
|
|
85
|
+
* prefers over the suffix, so a suffix would be a second dead source of truth
|
|
86
|
+
* for the same dial.
|
|
87
|
+
*/
|
|
88
|
+
export function modelArgs(spec) {
|
|
89
|
+
return spec === MODEL_INHERIT ? [] : ['--model', spec];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The spec a built argv fragment actually carries, or `undefined` for a child
|
|
93
|
+
* that will resolve pi's saved default.
|
|
94
|
+
*
|
|
95
|
+
* Read BACK OUT of the argv rather than threaded alongside it, deliberately. The
|
|
96
|
+
* argv is what the child runs; a second field carrying "the model we meant"
|
|
97
|
+
* could disagree with it after any future edit, and the one consumer — the
|
|
98
|
+
* dead-backend probe — must ask about the endpoint the child will really use.
|
|
99
|
+
*/
|
|
100
|
+
export function modelSpecFromArgs(args) {
|
|
101
|
+
const i = args.indexOf('--model');
|
|
102
|
+
return i === -1 ? undefined : args[i + 1];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Split a stored spec on its FIRST slash.
|
|
106
|
+
*
|
|
107
|
+
* `openrouter/z-ai/glm-4.6` is provider `openrouter` and id `z-ai/glm-4.6`:
|
|
108
|
+
* OpenRouter-style ids contain slashes of their own, and splitting on the last
|
|
109
|
+
* one would invent the provider `openrouter/z-ai`. Returns `undefined` for a
|
|
110
|
+
* spec with no slash, which `inherit` is and a canonical spec never is.
|
|
111
|
+
*/
|
|
112
|
+
export function splitSpec(spec) {
|
|
113
|
+
const i = spec.indexOf('/');
|
|
114
|
+
if (i <= 0 || i === spec.length - 1)
|
|
115
|
+
return undefined;
|
|
116
|
+
return { provider: spec.slice(0, i), id: spec.slice(i + 1) };
|
|
117
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child roster — which children exist, and which of them share one setting.
|
|
3
|
+
*
|
|
4
|
+
* Split out of reasoning.ts because the roster is not about reasoning. Two
|
|
5
|
+
* tables are keyed on it now (thinking level, and model), and a third would be
|
|
6
|
+
* no more surprising. A roster named after one of its consumers reads as though
|
|
7
|
+
* adding a second consumer were a special case.
|
|
8
|
+
*
|
|
9
|
+
* PURE MODULE, for the same reason reasoning.ts is: config.ts imports both
|
|
10
|
+
* during its own module evaluation, so neither may take an import with a runtime
|
|
11
|
+
* side effect.
|
|
12
|
+
*
|
|
13
|
+
* Grouped by JOB, not by spawn mechanism — two children that both go through
|
|
14
|
+
* `runWorker` (a research worker and a verify gate) want different amounts of
|
|
15
|
+
* thinking, while `refine` in phases.ts and `compress-label` in title-label.ts
|
|
16
|
+
* want the same amount and share the `phase` cell.
|
|
17
|
+
*
|
|
18
|
+
* - `research` the ad-hoc `pi-worker` subagent tool, and the fallback the four
|
|
19
|
+
* research workers use when their own cell is unset
|
|
20
|
+
* - `research:files` / `research:apis` / `research:context` / `research:tooling`
|
|
21
|
+
* one cell per research worker, so a level can be paid for in
|
|
22
|
+
* one worker without paying for it in the other three
|
|
23
|
+
* - `phase` refine, verify-tooling, grill, compose, critique, compress-label
|
|
24
|
+
* - `planning` /task-auto's planning children — clarify, decompose, and the
|
|
25
|
+
* extract/coverage passes around them
|
|
26
|
+
* - `plan` /task-plan's question and answer children
|
|
27
|
+
* - `gate` enforce, verify, lint-fix, final-fix, recommend
|
|
28
|
+
* - `extraction` the --no-tools focused docs/fetch extractors
|
|
29
|
+
* - `implementation` the host-session turn that writes the code (not a child)
|
|
30
|
+
*/
|
|
31
|
+
export type ChildGroup = 'research' | 'research:files' | 'research:apis' | 'research:context' | 'research:tooling' | 'phase' | 'planning' | 'plan' | 'gate' | 'extraction' | 'implementation';
|
|
32
|
+
export declare const CHILD_GROUPS: readonly ChildGroup[];
|
|
33
|
+
/**
|
|
34
|
+
* Which group each NAMED child belongs to.
|
|
35
|
+
*
|
|
36
|
+
* The name is the one `runChild` is called with, so this is exhaustive over the
|
|
37
|
+
* children that carry one. `config/groups.test.ts` scans the source for those
|
|
38
|
+
* call sites and fails if a name is missing here. A defaulting lookup would let a
|
|
39
|
+
* phase added later opt itself out of the tables without anyone deciding to.
|
|
40
|
+
*
|
|
41
|
+
* The gate and extraction groups are NOT here: those children reach the model
|
|
42
|
+
* through `groupChildArgs('gate' | 'extraction')` at call sites with no name in
|
|
43
|
+
* scope (gate-deps.ts, fetch-core.ts, docs-core.ts, pi-worker-docs.ts). The four
|
|
44
|
+
* research workers do have a name — their `spec.label` — so they are here.
|
|
45
|
+
*/
|
|
46
|
+
export declare const GROUP_BY_CHILD: Readonly<Record<string, ChildGroup>>;
|
|
47
|
+
/**
|
|
48
|
+
* The group a named child belongs to.
|
|
49
|
+
*
|
|
50
|
+
* Returns `undefined` for a name the table does not know, and the CALLER decides
|
|
51
|
+
* what that means. `runPhaseChild` treats it as `inherit` — a child that reaches
|
|
52
|
+
* the model with today's argv is always safe — while the test treats it as a
|
|
53
|
+
* failure. That split is deliberate: the guard belongs at build time, where
|
|
54
|
+
* someone can fix it, not at run time, where it would abort a user's task over a
|
|
55
|
+
* missing table row.
|
|
56
|
+
*/
|
|
57
|
+
export declare function groupForChild(name: string): ChildGroup | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* For a `research:*` group, the group a stored config falls back to when its own
|
|
60
|
+
* key is missing. Every other group maps to `undefined`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const RESEARCH_SUBGROUP_PARENT: Readonly<Partial<Record<ChildGroup, ChildGroup>>>;
|
|
63
|
+
/**
|
|
64
|
+
* Always returns a COMPLETE record, never a partial one, for any per-group table.
|
|
65
|
+
*
|
|
66
|
+
* A hand-edited file missing a group, or one carrying a group from a future
|
|
67
|
+
* version, must not reach a resolver as a hole: `table[group]` would be
|
|
68
|
+
* `undefined`, and every call site would need its own fallback. Filling the gaps
|
|
69
|
+
* here means the type is true at the only place that constructs the value.
|
|
70
|
+
*
|
|
71
|
+
* `fallbackFor` is a function of the GROUP, not a value. The reasoning table
|
|
72
|
+
* falls back per group to its measured default; the model table falls back to
|
|
73
|
+
* the constant `inherit`. A scalar parameter cannot express the first, which is
|
|
74
|
+
* the one that already ships.
|
|
75
|
+
*/
|
|
76
|
+
export declare function sanitizeGroupRecord<T>(value: unknown, valid: (v: unknown) => v is T, fallbackFor: (group: ChildGroup) => T): Record<ChildGroup, T>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const CHILD_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
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Which group each NAMED child belongs to.
|
|
16
|
+
*
|
|
17
|
+
* The name is the one `runChild` is called with, so this is exhaustive over the
|
|
18
|
+
* children that carry one. `config/groups.test.ts` scans the source for those
|
|
19
|
+
* call sites and fails if a name is missing here. A defaulting lookup would let a
|
|
20
|
+
* phase added later opt itself out of the tables without anyone deciding to.
|
|
21
|
+
*
|
|
22
|
+
* The gate and extraction groups are NOT here: those children reach the model
|
|
23
|
+
* through `groupChildArgs('gate' | 'extraction')` at call sites with no name in
|
|
24
|
+
* scope (gate-deps.ts, fetch-core.ts, docs-core.ts, pi-worker-docs.ts). The four
|
|
25
|
+
* research workers do have a name — their `spec.label` — so they are here.
|
|
26
|
+
*/
|
|
27
|
+
export const GROUP_BY_CHILD = {
|
|
28
|
+
// ── phase: task/phases.ts + task/title-label.ts ──────────────────────────
|
|
29
|
+
refine: 'phase',
|
|
30
|
+
'verify-tooling': 'phase',
|
|
31
|
+
'grill-auto': 'phase',
|
|
32
|
+
'grill-gen': 'phase',
|
|
33
|
+
compose: 'phase',
|
|
34
|
+
critique: 'phase',
|
|
35
|
+
'critique-triage': 'phase',
|
|
36
|
+
'compress-label': 'phase',
|
|
37
|
+
// ── planning: task/auto-orchestrator.ts ──────────────────────────────────
|
|
38
|
+
'clarify-triage': 'planning',
|
|
39
|
+
'auto-clarify': 'planning',
|
|
40
|
+
'auto-decompose': 'planning',
|
|
41
|
+
'requirement-extract': 'planning',
|
|
42
|
+
'decompose-coverage': 'planning',
|
|
43
|
+
'coverage-map': 'planning',
|
|
44
|
+
'contract-extract': 'planning',
|
|
45
|
+
'launch-extract': 'planning',
|
|
46
|
+
// ── plan: task/plan-orchestrator.ts ──────────────────────────────────────
|
|
47
|
+
'plan-question': 'plan',
|
|
48
|
+
'plan-answer': 'plan',
|
|
49
|
+
// ── research: task/phases.ts `workerSpecs`, keyed on the spec's LABEL ─────
|
|
50
|
+
'worker:files': 'research:files',
|
|
51
|
+
'worker:apis': 'research:apis',
|
|
52
|
+
'worker:context': 'research:context',
|
|
53
|
+
'worker:tooling': 'research:tooling'
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The group a named child belongs to.
|
|
57
|
+
*
|
|
58
|
+
* Returns `undefined` for a name the table does not know, and the CALLER decides
|
|
59
|
+
* what that means. `runPhaseChild` treats it as `inherit` — a child that reaches
|
|
60
|
+
* the model with today's argv is always safe — while the test treats it as a
|
|
61
|
+
* failure. That split is deliberate: the guard belongs at build time, where
|
|
62
|
+
* someone can fix it, not at run time, where it would abort a user's task over a
|
|
63
|
+
* missing table row.
|
|
64
|
+
*/
|
|
65
|
+
export function groupForChild(name) {
|
|
66
|
+
return GROUP_BY_CHILD[name];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* For a `research:*` group, the group a stored config falls back to when its own
|
|
70
|
+
* key is missing. Every other group maps to `undefined`.
|
|
71
|
+
*/
|
|
72
|
+
export const RESEARCH_SUBGROUP_PARENT = {
|
|
73
|
+
'research:files': 'research',
|
|
74
|
+
'research:apis': 'research',
|
|
75
|
+
'research:context': 'research',
|
|
76
|
+
'research:tooling': 'research'
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Always returns a COMPLETE record, never a partial one, for any per-group table.
|
|
80
|
+
*
|
|
81
|
+
* A hand-edited file missing a group, or one carrying a group from a future
|
|
82
|
+
* version, must not reach a resolver as a hole: `table[group]` would be
|
|
83
|
+
* `undefined`, and every call site would need its own fallback. Filling the gaps
|
|
84
|
+
* here means the type is true at the only place that constructs the value.
|
|
85
|
+
*
|
|
86
|
+
* `fallbackFor` is a function of the GROUP, not a value. The reasoning table
|
|
87
|
+
* falls back per group to its measured default; the model table falls back to
|
|
88
|
+
* the constant `inherit`. A scalar parameter cannot express the first, which is
|
|
89
|
+
* the one that already ships.
|
|
90
|
+
*/
|
|
91
|
+
export function sanitizeGroupRecord(value, valid, fallbackFor) {
|
|
92
|
+
const stored = typeof value === 'object' && value !== null && !Array.isArray(value) ?
|
|
93
|
+
value
|
|
94
|
+
: {};
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const group of CHILD_GROUPS) {
|
|
97
|
+
const own = stored[group];
|
|
98
|
+
if (valid(own)) {
|
|
99
|
+
out[group] = own;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// A `research:*` key the stored config never had falls back to its
|
|
103
|
+
// parent `research` value, so a config written before the split keeps
|
|
104
|
+
// meaning what it meant.
|
|
105
|
+
const parent = RESEARCH_SUBGROUP_PARENT[group];
|
|
106
|
+
const inherited = parent === undefined ? undefined : stored[parent];
|
|
107
|
+
out[group] = valid(inherited) ? inherited : fallbackFor(group);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|