@mjasnikovs/pi-task 0.38.19 → 0.38.20

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 (44) hide show
  1. package/README.md +1 -0
  2. package/dist/config/config.d.ts +19 -0
  3. package/dist/config/config.js +10 -2
  4. package/dist/config/reasoning-args.d.ts +10 -0
  5. package/dist/config/reasoning-args.js +23 -0
  6. package/dist/config/reasoning.d.ts +160 -0
  7. package/dist/config/reasoning.js +530 -0
  8. package/dist/config/register.d.ts +72 -2
  9. package/dist/config/register.js +182 -33
  10. package/dist/shared/model-endpoint.d.ts +34 -0
  11. package/dist/shared/model-endpoint.js +36 -0
  12. package/dist/shared/reasoning-capability.d.ts +86 -0
  13. package/dist/shared/reasoning-capability.js +82 -0
  14. package/dist/task/child-runner.d.ts +19 -26
  15. package/dist/task/child-runner.js +48 -6
  16. package/dist/task/decompose-fidelity.d.ts +21 -8
  17. package/dist/task/decompose-fidelity.js +98 -17
  18. package/dist/task/gate-child.d.ts +10 -0
  19. package/dist/task/gate-child.js +1 -0
  20. package/dist/task/gate-deps.js +4 -0
  21. package/dist/task/implementation-thinking.d.ts +54 -0
  22. package/dist/task/implementation-thinking.js +33 -0
  23. package/dist/task/orchestrator.d.ts +7 -0
  24. package/dist/task/orchestrator.js +42 -14
  25. package/dist/task/phases.js +47 -24
  26. package/dist/task/prompts.d.ts +0 -23
  27. package/dist/task/prompts.js +0 -25
  28. package/dist/task/reasoning-groups.d.ts +36 -0
  29. package/dist/task/reasoning-groups.js +36 -0
  30. package/dist/task/spec-validation.d.ts +28 -0
  31. package/dist/task/spec-validation.js +44 -0
  32. package/dist/task/title-label.js +2 -2
  33. package/dist/workers/docs-core.js +4 -0
  34. package/dist/workers/fetch-core.js +4 -0
  35. package/dist/workers/focused-extractor.d.ts +12 -1
  36. package/dist/workers/focused-extractor.js +6 -2
  37. package/dist/workers/index.js +2 -0
  38. package/dist/workers/pi-worker-core.d.ts +22 -0
  39. package/dist/workers/pi-worker-core.js +24 -6
  40. package/dist/workers/pi-worker-docs.js +4 -0
  41. package/dist/workers/pi-worker.js +11 -1
  42. package/dist/workers/reasoning-warning.d.ts +64 -0
  43. package/dist/workers/reasoning-warning.js +142 -0
  44. package/package.json +1 -1
@@ -0,0 +1,530 @@
1
+ export const REASONING_GROUPS = [
2
+ 'research',
3
+ 'phase',
4
+ 'planning',
5
+ 'plan',
6
+ 'gate',
7
+ 'extraction',
8
+ 'implementation'
9
+ ];
10
+ export const REASONING_MODES = ['default', 'on', 'off', 'custom'];
11
+ /**
12
+ * The settings offered in /task-config: `inherit` plus pi's OWN cycle
13
+ * (`THINKING_LEVELS` in pi-coding-agent's agent-session).
14
+ *
15
+ * `xhigh` and `max` are DELIBERATELY ABSENT. pi treats an absent
16
+ * `thinkingLevelMap` entry as "supported" for the standard levels but requires a
17
+ * declared entry for the extended two (pi-ai `getSupportedThinkingLevels`), so a
18
+ * model with no map would receive the raw string — and Qwen3.8's chat template
19
+ * answers an unknown effort with HTTP 500, not a clamp. Offering a level that pi's
20
+ * own UI does not is how you ship a `--thinking` that hard-fails on some models.
21
+ * This machine already reaches xhigh through `"high": "xhigh"` in its map.
22
+ */
23
+ export const REASONING_SETTINGS = [
24
+ 'inherit',
25
+ 'off',
26
+ 'minimal',
27
+ 'low',
28
+ 'medium',
29
+ 'high'
30
+ ];
31
+ /**
32
+ * The level mode `on` uses, and the treatment arm of the A/B that fills in
33
+ * {@link DEFAULT_REASONING_TABLE}. ONE constant so the shipped config and the
34
+ * measurement behind it can never drift apart.
35
+ */
36
+ export const REASONING_ON_LEVEL = 'medium';
37
+ /**
38
+ * The per-group table used by mode `default`.
39
+ *
40
+ * A cell is `inherit` until it has been MEASURED, and then it names a level.
41
+ * Every non-`inherit` cell here must be a live A/B result
42
+ * (scripts/live-reasoning-group-ab.ts, n>=20 per arm, arms `off` vs
43
+ * {@link REASONING_ON_LEVEL}) carrying its date, model, counts and RUNG in the
44
+ * comment beside it. A cell filled in from intuition is WORSE than `inherit`:
45
+ * `inherit` is honest about knowing nothing, while a wrong cell is a
46
+ * measurement nobody took, wearing the authority of a default.
47
+ *
48
+ * THE RUNG IS PART OF THE RESULT, so read it before trusting a cell. The
49
+ * harness returns a two-way verdict — `off` or {@link REASONING_ON_LEVEL},
50
+ * never a tie — down a three-rung ladder: rung 1 a significant quality
51
+ * difference, rung 2 quality level and a significant speed difference, rung 3
52
+ * nothing separated the arms and the cheaper level carries it by a stated
53
+ * prior. A rung-3 cell is a DECISION, not a finding: it says "no reason found
54
+ * to pay for thinking", not "thinking was shown not to help". At n=20/arm the
55
+ * run is only powered for large effects, so rung 3 is the common outcome and
56
+ * an absent effect and an undetected one look identical from here.
57
+ *
58
+ * NOTHING IS ALREADY ON THE RECORD, and the belief that something was is worth
59
+ * writing down. This docstring used to say magicknumbers.md had measured the
60
+ * decompose child "with one knob, `enable_thinking` — off answered 1/10, on
61
+ * answered 8/8", and treated that as the split the A/B had to reproduce.
62
+ *
63
+ * VERIFIED 2026-08-27: the string "8/8" does not appear in magicknumbers.md, and
64
+ * never has (`git log -S`; the phrase was introduced by THIS file's own commit,
65
+ * `0b91f71`). What that page records is
66
+ *
67
+ * "Measured, captured decompose request, REASONING OFF, n=10 per cell:
68
+ * 1/10 → 7/10 (stall detector) → 8/9 (all three)."
69
+ *
70
+ * — a ladder over THREE GUARDS at CONSTANT reasoning off, from `fea7bbb`. Both
71
+ * endpoints are the same arm. `enable_thinking` is not its knob and never was,
72
+ * and the "8/8" is a misread of the ladder's last cell, `8/9`.
73
+ *
74
+ * So no reasoning effect on `planning` had ever been measured, and the 10/10 vs
75
+ * 10/10 read on the current tree was not a contradiction of a prior result — it
76
+ * was the only reading there had ever been. The controls that page DOES record
77
+ * (Qwen3.6-27B 10/10, Gemma4-12B 10/10) are likewise reasoning off, so they say
78
+ * nothing about a cell flipping by model either.
79
+ *
80
+ * The `planning` cell below is the FIRST measured reading of that knob, taken
81
+ * 2026-08-27 on a citation-fidelity axis built for it. It is also the first cell
82
+ * that is not `off`.
83
+ */
84
+ export const DEFAULT_REASONING_TABLE = {
85
+ // A/B 2026-08-26, Qwen3.8-27B-NVFP4-MTP-VERY-HIGH.gguf (llama.cpp
86
+ // b10620-0f3b51e03), scripts/live-reasoning-group-ab.ts, n=12/arm over 12
87
+ // distinct mx5 tasks. off 10/12 vs medium 11/12 (p=1.0000); neither arm
88
+ // failed to answer. Wall clock, PAIRED by stimulus over the 10 pairs usable
89
+ // in both arms: p=0.7090, mean off 79.4s vs medium 59.3s.
90
+ // off real+edited-named 95% CI [0.55, 0.95].
91
+ // RUNG 3 — a DECISION, not a finding. Nothing separated the arms on either
92
+ // axis, and off carries it by the stated prior: thinking that buys nothing
93
+ // measurable is not worth its tokens. At 12 pairs an absent effect and an
94
+ // undetected one look the same from here. Ledger: ledger-research.jsonl.
95
+ //
96
+ // THE AXIS IS THE CONJUNCTION, built for this run: every path the answer
97
+ // names is real AND every pre-existing file the task edited is named
98
+ // (`filesAnswered`, scripts/reasoning-ab-files-truth.ts). Recall truth is
99
+ // the shipped tree, not the recorded answer. Screened offline: restricted
100
+ // to pre-existing files the recorded answers score 86.6% (71/82 paths, 29
101
+ // of 38 tasks perfect); counting CREATED files instead drops them to 49.0%
102
+ // and the CHECK loses, so a file that does not exist yet is not truth.
103
+ // It is NOT saturated here — 10/12 and 11/12, with genuine judgement errors
104
+ // in both arms — so the clock was allowed to decide and declined to.
105
+ //
106
+ // THE STIMULI ARE HALF THE AXIS. The superseded precision-only run
107
+ // (ledger-research.PRECISION-ONLY-10rep.jsonl, `--axis precision` to
108
+ // reproduce) tied 10/10 on TASK_0002..0012, the greenfield head of the mx5
109
+ // run — SEVEN OF THOSE TEN EDIT NO PRE-EXISTING FILE AT ALL, so it measured
110
+ // recall where recall does not exist. `filesRecallStimuli` screens for it;
111
+ // these 12 tasks each edit >=2 pre-existing files. That ledger CANNOT be
112
+ // rescored onto this axis and the rescorer abstains rather than return a
113
+ // number.
114
+ //
115
+ // INSTRUMENT NOTE, because this cell read `off 8/12` before it. `filesPaths`
116
+ // split an entry on TWO spaces, which is what the prompt specifies and what
117
+ // all 470 recorded paths use. Three live `off` trials wrote
118
+ // `src/client/main.tsx: App root` — one space after a colon — so the
119
+ // description was taken as part of the path and TASK_0053 scored 0/19 with
120
+ // all 19 paths real. Fixed by `COLON_ENTRY`; rescored from stored text with
121
+ // no GPU, 2 of 24 trials changed side, and the known-good answer is
122
+ // unmoved (53/56 whole-corpus precision, identical path counts). This is
123
+ // [[ab-scorer-must-see-the-same-input]] a second time in one function.
124
+ //
125
+ // THE CONFOUND THE PRECISION RUN EXPOSED IS STILL VISIBLE and still points
126
+ // the other way: off named 212 real paths to medium's 153, while medium
127
+ // named 27/27 edited files to off's 26/27. Off says more and is slower for
128
+ // it. The conjunction was built so that neither half can be gamed alone,
129
+ // and on these numbers it separates neither arm.
130
+ //
131
+ // AXIS RE-AUDITED 2026-08-27, after planning's adjudicator turned out to
132
+ // have four bugs. One more was found here and it is the SAME-INPUT rule
133
+ // broken a third time: `filesPaths` read the child's whole raw turn, while
134
+ // the SCREEN sliced the recorded `## research` down to its FILES block
135
+ // precisely because APIS is symbols and would score as 100% invented. One
136
+ // `off` trial emitted its own `APIS` heading despite the prompt's "No other
137
+ // sections", and three symbols under it — `cn` and two `--*` token lists —
138
+ // were counted as invented paths, failing a trial whose FILES block was
139
+ // 20/20 real. The slice now lives in `filesPaths`, where both callers reach
140
+ // it. The cell is UNCHANGED: that trial still fails, on a genuine recall
141
+ // miss (`src/client/types.d.ts`), and the counts are identical.
142
+ // The other two failures were checked by hand and are genuine — both arms
143
+ // put `AdminPage.spec.tsx` / `.story.tsx` under `src/client/routes/` when
144
+ // they ship under `src/client/pages/`, a wrong-directory prediction made
145
+ // from a real sibling.
146
+ //
147
+ // MEASURED UNDER THE SERVER'S GLOBAL SAMPLER, which is the THINKING preset,
148
+ // so the `off` arm decodes on sampling tuned for the `on` arm. That is the
149
+ // regime this machine really runs pi-task in, so the result is
150
+ // ecologically valid — it is NOT a clean comparison.
151
+ research: 'off',
152
+ // NOT MEASURED. DECIDED BY PRIOR, 2026-08-27 — the same prior that carries
153
+ // every rung-3 cell in this table: thinking that buys nothing measurable is
154
+ // not worth its tokens. `off`.
155
+ //
156
+ // WHY A DECISION AND NOT `inherit`. All eight phase children run UNATTENDED
157
+ // — refine, grill-gen, grill-auto, verify-tooling, compose, critique,
158
+ // critique-triage, compress-label. Nobody is watching to turn a knob, so
159
+ // `inherit` does not defer to a judgement, it defers to whatever
160
+ // ~/.pi/agent/settings.json happens to hold. That is the one case this
161
+ // table exists to remove. (`plan` is the opposite case and keeps `inherit`
162
+ // for exactly the opposite reason — see that cell.)
163
+ //
164
+ // WHY `off` RATHER THAN `medium`. The one cell decided on QUALITY is
165
+ // extraction at rung 1, where off beat medium 20/20 vs 15/20 and medium's
166
+ // failure mode was STITCHING — concatenating non-contiguous passages into
167
+ // something that reads verbatim. refine, compose and critique all emit prose
168
+ // that downstream phases consume as fact, which is the same hazard.
169
+ // `planning` went `medium`, and it is the nearest neighbour by job, but it
170
+ // is NOT evidence that thinking helps this class: off led that cell on
171
+ // quality too, 28/30 vs 26/30, and lost only the clock, because it wrote
172
+ // more. Nothing measured anywhere in this table shows thinking improving a
173
+ // synthesis child's output.
174
+ //
175
+ // WHY IT COULD NOT BE MEASURED. Three candidate axes were each scored
176
+ // against refine's OWN RECORDED OUTPUT and rejected before any GPU:
177
+ // `validateRefineShape` scores 55/56 — saturated; "EXTERNAL-DEPENDENCIES
178
+ // names a real package" finds 4 distinct packages across all 56 tasks — no
179
+ // signal; "every backticked path exists" scored 56.2%, 6/56 perfect — a bar
180
+ // the known-good answer could not clear.
181
+ //
182
+ // THAT THIRD AXIS WAS RE-AUDITED 2026-08-27, because planning's citation
183
+ // axis was rejected the same way at 59.2% and reached 97.1% once four
184
+ // adjudicator bugs were fixed. THE CHECK WAS INDEED LOSING.
185
+ // scripts/phase-path-axis-audit.ts walks the ladder:
186
+ //
187
+ // NAIVE 60.1% of paths, 5/55 tasks perfect
188
+ // CATEGORY-CLEAN 83.8% of paths, 34/52 tasks perfect
189
+ // FINAL-TREE 93.4% of paths, 43/52 tasks perfect
190
+ //
191
+ // CATEGORY-CLEAN drops the spans that were never repo paths: npm specifiers
192
+ // (`@hono/zod-validator`, `hono/client`), doc URLs, MIME types, dotted code
193
+ // expressions (`c.var.user` — nine of them), bare filenames with no
194
+ // directory, and `./`/`../` import specifiers. FINAL-TREE additionally counts
195
+ // a path real if it exists anywhere in the tree the RUN shipped, which stops
196
+ // marking a CORRECT PREDICTION wrong — 16 of the 27 remaining misses are
197
+ // files a later task really creates.
198
+ //
199
+ // AND IT IS STILL NOT A USABLE AXIS. At 93.4% the eleven residual misses are
200
+ // almost all PATH-PREFIX ELISIONS — `server/index.ts` for
201
+ // `src/server/index.ts`, `client/main.tsx` for `src/client/main.tsx`.
202
+ // Loosen enough to accept a suffix match and it saturates; keep them and the
203
+ // A/B measures whether refine writes the `src/` prefix. Formatting, not
204
+ // reasoning. DO NOT RE-DERIVE THIS.
205
+ //
206
+ // ledger-phase.VOID-wrong-scorer.jsonl stores no output text, so its apparent
207
+ // off 2/14 vs medium 6/14 is an artefact that cannot be rescored. DO NOT READ
208
+ // A RESULT INTO IT.
209
+ //
210
+ // TO REPLACE THIS WITH A MEASUREMENT: find a bar refine's own recorded output
211
+ // clears while still leaving headroom, and that is not a property of its
212
+ // formatting. Four candidates are now dead; a fifth needs a different kind of
213
+ // truth — an EXECUTION, the way gate and implementation are scored, not a
214
+ // property of the text.
215
+ phase: 'off',
216
+ // A/B 2026-08-27, Qwen3.8-27B-NVFP4-MTP-VERY-HIGH.gguf (llama.cpp
217
+ // b10620-0f3b51e03), scripts/live-reasoning-group-ab.ts, n=30/arm over the
218
+ // committed mx5 fixture replayed 30 times. Ledger: ledger-planning.jsonl.
219
+ //
220
+ // THE FIRST CELL THAT IS NOT `off`, and the only one where the two axes
221
+ // point in opposite directions. Read both.
222
+ //
223
+ // off 28/30 faithful plans vs medium 26/30 (p=0.6707); neither arm ever
224
+ // failed to terminate. Wall clock, UNPAIRED — one fixture, so there are no
225
+ // distinct stimuli to pair by and `pairByStimulus` refuses rather than
226
+ // invent a pairing: mean off 207.3s vs medium 143.6s, p=0.0241.
227
+ // off faithful-plan 95% CI [0.79, 0.98].
228
+ // RUNG 2 — quality is level on an axis with headroom, and medium is 1.4x
229
+ // faster at equal quality. The ladder is symmetric: `off` losing the clock
230
+ // is not privileged over `medium` losing it.
231
+ //
232
+ // THE CLOCK CAVEAT, because the direction is the opposite of every other
233
+ // cell. off's mean is tail-driven — medians are 157.6s vs 148.2s, ~6% apart
234
+ // — and off emits a LONGER plan for the same title count (median 6840 vs
235
+ // 4988 chars, 13 titles both arms). Per character off is the faster arm,
236
+ // 23.0 vs 29.7 ms/char. So what the clock records here is "off writes more",
237
+ // not "thinking is free". Production waits for the whole plan, so the cost
238
+ // is real; it is not evidence that thinking decodes faster.
239
+ //
240
+ // THE AXIS IS `planningPlanFaithful`, built for this run:
241
+ // a plan is FAITHFUL when it lists >= 2 titles AND EVERY source clause it
242
+ // emitted — counted in the RAW TITLE — comes back grounded.
243
+ // It is production's own adjudicator (`extractTitleSource`) with no model in
244
+ // the loop, and the source doc is a committed fixture, so every stored trial
245
+ // stays rescorable with no corpus:
246
+ // bun run scripts/rescore-reasoning-ledger.ts \
247
+ // ab-grouplab/ledger-planning.jsonl planning --from-text
248
+ // Counting the clauses in the RAW title is load-bearing: a malformed clause
249
+ // (a missing closing quote — measured live, 4 trials) stops the peel, and a
250
+ // scorer that counted only what it peeled reads everything before the break
251
+ // as a clean sweep.
252
+ //
253
+ // THE OLD AXIS WAS A SHAPE CHECK. `parseDecomposeList >= 2` read 10/10 in
254
+ // both arms at n=10/arm — saturated, the same death as gate's and phase's
255
+ // first scorers. ledger-planning.SHAPE-AXIS-no-extension-10rep.jsonl is that
256
+ // run; it is a PRIOR, NOT A REPLICATE, because it also ran a different child
257
+ // (no single-read extension). Do not pool it.
258
+ //
259
+ // THE ADJUDICATOR HAD TO BE FIXED FOUR TIMES BEFORE IT COULD JUDGE, and
260
+ // every fix was found by auditing failures row by row, not by reading code:
261
+ // 1. GREEDY REGEX across multi-clause titles (25% of real titles carry
262
+ // more than one clause) — two real citations became one superstring.
263
+ // 2. MARKDOWN EMPHASIS/LIST MARKERS counted as content.
264
+ // 3. CODE BACKTICKS counted as content — the larger half of (2), and
265
+ // found only after this run. A code span renders as bare text, so
266
+ // `Invites — create/validate/redeem, /join/:token page.` is a verbatim
267
+ // copy of a line the file stores with backticks. Screening EVERY spec
268
+ // line of the fixture in its rendered form: 107/216 grounded before,
269
+ // 216/216 after, floor 0/216.
270
+ // 4. BACKSLASH-ESCAPED QUOTES. The clause is double-quoted, so a spec line
271
+ // containing a double quote comes back as `\"`. The backslash is the
272
+ // delimiter's artefact, not content.
273
+ // The live run scored off 21/30 vs medium 24/30 and printed `medium`; fixes
274
+ // 3 and 4 moved 9 of 60 trials and the QUALITY ORDER REVERSED, to off 28/30
275
+ // vs medium 26/30. The verdict survives only because the clock decides at
276
+ // rung 2. scripts/decompose-fidelity-screen.ts is the standing screen that
277
+ // would have caught 3 and 4 before the GPU ran.
278
+ //
279
+ // THE REMAINING 6 FAILURES ARE GENUINE, checked by hand: 4 malformed clauses
280
+ // (missing closing quote or bracket), 1 word substituted in a real line
281
+ // ("invokes" for "invites"), 1 single-quoted where the doc has double.
282
+ //
283
+ // THE HARNESS SPAWNS PRODUCTION'S CHILD. `phaseDeps()` passed no
284
+ // `childExtensions`, so auto-decompose ran WITHOUT the single-read guard
285
+ // production hands every planning child (auto-orchestrator.ts). Fixed before
286
+ // this run; `loadableSingleReadExtension()` maps src->dist and ABSTAINS if
287
+ // neither exists, so `bun run build` is a precondition.
288
+ //
289
+ // THE TARGET THIS CELL ONCE CHASED DOES NOT EXIST. "off ~1/10 vs on ~8/8
290
+ // from magicknumbers.md" was cited here and in this file's header as the
291
+ // split a positive control had to reproduce. Verified 2026-08-27: that page
292
+ // has no "8/8" in it and never did, and its `1/10 -> 7/10 -> 8/9` ladder is
293
+ // labelled REASONING OFF, n=10 per cell — three GUARDS from `fea7bbb`, one
294
+ // arm throughout. See the header.
295
+ //
296
+ // A pre-`fea7bbb` control tree WAS built and verified anyway
297
+ // (ab-grouplab/make-preguard-tree.sh + preguard-probe.ts, all four guard
298
+ // behaviours rolled back and asserted both ways). It is the only planning
299
+ // regime known to have headroom at reasoning off — 1/10 there — but it is a
300
+ // regime we deleted, so a cell decided in it would not be writable here.
301
+ // Left unrun on purpose.
302
+ //
303
+ // MEASURED UNDER THE SERVER'S GLOBAL SAMPLER, which is the THINKING preset.
304
+ // Here that cuts AGAINST the written cell rather than for it: the `off` arm
305
+ // decoded on sampling tuned for `medium` and still led on quality, so a
306
+ // clean comparison could only widen off's quality margin — which the clock
307
+ // then has to overcome. Ecologically valid, not clean.
308
+ planning: 'medium',
309
+ // `inherit` ON PURPOSE, and this is the one cell where that is an ANSWER
310
+ // rather than an absence of one.
311
+ //
312
+ // /task-plan is INTERACTIVE. The user is sitting in the loop, reading each
313
+ // question and steering the next one, and how much thinking that wants is
314
+ // theirs to judge per session — a quick sketch and a hard architectural
315
+ // plan are the same command. Every other group in this table runs
316
+ // unattended, where nobody is there to turn a knob and the table has to
317
+ // decide. Here there is, and it should not be overridden.
318
+ //
319
+ // `inherit` means the child gets whatever `~/.pi/agent/settings.json` holds
320
+ // — that is the general objection to it, and here it is exactly the point:
321
+ // the setting the user chose is the setting the user gets.
322
+ //
323
+ // It is also unmeasured: /task-plan has never executed inside the A/B
324
+ // corpus, so there is no recorded child turn to replay. But that is not why
325
+ // the cell reads `inherit`. Recording a run and measuring the group would
326
+ // not change it.
327
+ plan: 'inherit',
328
+ // A/B 2026-08-26, Qwen3.8-27B-NVFP4-MTP-VERY-HIGH.gguf (llama.cpp
329
+ // b10620-0f3b51e03), scripts/live-reasoning-group-ab.ts, n=30/arm.
330
+ //
331
+ // SCORED AGAINST THE TREE, NOT THE ANSWER'S SHAPE. Ten mx5 tasks were
332
+ // screened by EXECUTING each one's own VERIFY with no model in the loop:
333
+ // it fails on the before-tree and passes on the after-tree. So the child
334
+ // faces 20 (task, tree) pairs whose correct verdict is a fact — FAIL before,
335
+ // PASS after — balanced, so always-PASS and always-FAIL each score exactly
336
+ // 50%. The verdict is read by production's own `parseVerifyVerdict`.
337
+ // UNOBSERVED scores WRONG: the harness just executed the evidence, so
338
+ // declining to look is a failure to do the job.
339
+ //
340
+ // off 28/30 correct verdicts vs medium 28/30 (p=1.0000); neither arm ever
341
+ // failed to emit a verdict. Wall clock, PAIRED by stimulus over the 27
342
+ // pairs usable in both arms: off is faster in 23 of them, geometric mean
343
+ // 0.61x, p=0.0019. off correct-verdict 95% CI [0.79, 0.98].
344
+ // RUNG 2 — quality is level on an axis with headroom, and off is 1.6x
345
+ // faster at equal correctness. Not a prior: the axis recorded four genuine
346
+ // judgement errors across the run, so it could have separated the arms and
347
+ // did not.
348
+ //
349
+ // TWO INSTRUMENT NOTES, because this cell read differently before both.
350
+ // (1) The clock test was UNPAIRED on a matched design. Every stimulus runs
351
+ // once per arm, and the stimulus dominates: the same child is ~25s on a
352
+ // before-tree and 100-500s on an after-tree. Pooling buried the arm
353
+ // effect. On these numbers the unpaired test reads p=0.3408 and the
354
+ // paired one p=0.0019. Fixed in `pairedPermutationP`.
355
+ // (2) The 20-rep run alone scored 20/20 vs 20/20 — SATURATED, and the
356
+ // ladder now refuses rung 2 there. The 10-rep run over the same 20
357
+ // stimuli, same fingerprint, same scorer, scored 8/10 vs 8/10. Pooled
358
+ // (ledger-gate.POOLED-10rep+20rep.jsonl, pairs keyed by run so the
359
+ // pairing stays within-run) the axis has headroom and n=30/arm.
360
+ // Superseded ledgers kept beside it: VOID-synthetic-prompt (hand-written
361
+ // prompt, prose-matching scorer) and VOID-saturated-shape-axis.
362
+ //
363
+ // AXIS RE-AUDITED 2026-08-27, all four mismatches read by hand. The PARSER
364
+ // is clean — `verdictWord` extracted the stated word correctly every time.
365
+ // THE TRUTH IS NOT, on one stimulus. TASK_0009/after is `PASS` because its
366
+ // own VERIFY script passes there, and BOTH arms answered FAIL with the same
367
+ // checkable reason: the acceptance list requires phone validation to REJECT
368
+ // `+1234567`, while the spec's own mandated regex `/^\+[1-9]\d{6,14}$/`
369
+ // accepts it (`+` `1` then six digits, and the range starts at six), and the
370
+ // shipped test omits the case. Verified by reading the regex, not by
371
+ // trusting the model. So "the task's VERIFY passes" is not the same fact as
372
+ // "the acceptance criteria are met", and on that stimulus the child was
373
+ // right and the axis was wrong.
374
+ // THE CELL IS UNCHANGED: both arms fail it identically, so dropping it
375
+ // leaves off 28/29 vs medium 28/29 — still level, still rung 2. Recorded
376
+ // because the next axis built on executed VERIFY should expect this gap.
377
+ gate: 'off',
378
+ // A/B 2026-08-26, Qwen3.8-27B-NVFP4-MTP-VERY-HIGH.gguf (llama.cpp
379
+ // b10620-0f3b51e03), scripts/live-reasoning-group-ab.ts, n=20/arm over 20
380
+ // recorded docs queries across 10 packages. off 20/20 usable vs medium
381
+ // 15/20 (p=0.0471); neither arm failed to answer. off usable 95% CI
382
+ // [0.84, 1.00]. Wall clock, PAIRED over the 15 stimuli usable in both arms:
383
+ // off 5.2s vs medium 11.1s, p=0.0010.
384
+ // RUNG 1 — the ONLY cell in this table decided by a quality difference, and
385
+ // the clock agrees with it rather than carrying it. Ledger:
386
+ // ledger-extraction.jsonl.
387
+ //
388
+ // THE PROMPT IS PRODUCTION'S. The harness used to hand-write it, because
389
+ // the two production builders frame content as a named npm package's docs
390
+ // or an anchored web page and the material it had — a recorded `## research`
391
+ // section — is neither. The fix was the right MATERIAL, not a better frame:
392
+ // `.pi-tasks/research-cache.json` records 190 distinct `pi-worker-docs`
393
+ // (package, query) pairs the run really asked, over 31 packages installed in
394
+ // the corpus copy, and all 190 replay through `docsRaw` + `buildPrompt` with
395
+ // no model and no network. See scripts/reasoning-ab-extraction-truth.ts.
396
+ //
397
+ // THE AXIS IS THE CONJUNCTION: a non-empty answer AND a citation that is
398
+ // really in the content the child was shown. Production gates on the first
399
+ // half only and carries `excerptVerified` as metadata, so this is a HARDER
400
+ // bar than production's own — legitimate for an A/B, and necessary because
401
+ // the shape half is the ceiling that returned 10/10 in both arms for gate,
402
+ // research and planning. SCREENED offline over all 190 recorded answers
403
+ // before any GPU:
404
+ // production's own excerptVerified 189/190 — clears the bar
405
+ // every backticked SPAN in the content 51/190 — the CHECK loses
406
+ // code-shaped identifiers only 123/190 — the CHECK loses
407
+ // Both grounding rules die the way phase's three did, for a nameable reason:
408
+ // real type names the model correctly knows — `ResponseInit`,
409
+ // `ArrayBufferView`, `DataTransfer` — are simply not in the retrieved
410
+ // chunks, so a correct answer is marked wrong.
411
+ //
412
+ // WHAT MEDIUM ACTUALLY DOES WRONG, audited row by row rather than trusted.
413
+ // All five failures are genuine and none is a normaliser gap: re-checked
414
+ // with whitespace and case squashed out, ZERO of them appear in the content.
415
+ // The failure mode is STITCHING — the arm concatenates non-contiguous
416
+ // passages into one quote block that reads as verbatim and is not. One
417
+ // matched 457 of its 522 squashed characters before diverging; another glued
418
+ // three separate `ts fences and a trailing `export type` line. It is not
419
+ // inventing the package, it is inventing the CONTIGUITY, which is exactly
420
+ // what a citation asserts.
421
+ //
422
+ // A RESCORE OF THIS GROUP RE-RETRIEVES, and that is a trap the ledger now
423
+ // guards. Two trials that verified in the container failed when rescored on
424
+ // the host, both quoting a real bun declaration (`@deprecated Prefer
425
+ // {@link Bun.sql}`) the host's newer bun does not ship — enough to move the
426
+ // cell from rung 1 to rung 2 on an artefact of WHERE the rescorer ran.
427
+ // Retrieval is deterministic within one environment and NOT across two, so
428
+ // the row now carries `verifyHash` and rescore-reasoning-ledger.ts ABSTAINS
429
+ // on a mismatch. This ledger predates the field, so the live judgements —
430
+ // made against exactly the bytes each child was shown — are authoritative.
431
+ //
432
+ // MEASURED UNDER THE SERVER'S GLOBAL SAMPLER, which is the THINKING preset,
433
+ // so the `off` arm decodes on sampling tuned for the `on` arm. That is the
434
+ // regime this machine really runs pi-task in, so the result is
435
+ // ecologically valid — it is NOT a clean comparison. Here it makes the
436
+ // finding CONSERVATIVE: the arm that wins is the one running on the other
437
+ // arm's sampler.
438
+ extraction: 'off',
439
+ // A/B 2026-08-25, Qwen3.8-27B-NVFP4-MTP-VERY-HIGH.gguf (llama.cpp
440
+ // b10618-1efd800e9), scripts/live-implementation-thinking-ab.ts, n=20/arm
441
+ // over 20 distinct mx5 specs, scored by each task's OWN recorded VERIFY
442
+ // block against the tree the turn produced. off 12/20 pass vs medium 12/20
443
+ // (p=1.0000); turn died 6/20 both arms. off pass 95% CI [0.39, 0.78].
444
+ // RUNG 2, and it was read as rung 3 until 2026-08-26. The quality axes are
445
+ // identical to the trial, so the clock decides — and the clock test was
446
+ // UNPAIRED on a design that runs each spec once per arm. Pooled, the mean
447
+ // wall clock of passing turns is off 237s vs medium 298s, p=0.6150. Paired
448
+ // by spec over the 12 specs that pass in BOTH arms: off faster in 9,
449
+ // geometric mean 0.55x, p=0.0166.
450
+ // Quality is still the widest CI in the table — at 12/20 the true pass rate
451
+ // is anywhere from 39% to 78%, so an ordinary quality difference would have
452
+ // been invisible — but 12/20 is not a ceiling, so the clock is allowed to
453
+ // carry the cell. The value is unchanged; only its standing improved, from
454
+ // a stated prior to a measured win. Rescored from the original ledger on 2026-08-25 when the harness
455
+ // moved to a forced two-way verdict; the trials are unchanged.
456
+ implementation: 'off'
457
+ };
458
+ /**
459
+ * A hand-edited or stale mode must not reach {@link resolveReasoning}'s switch as
460
+ * an unknown string — the `default:` arm would silently absorb it and the user
461
+ * would see "custom" in the file and the default table in behaviour.
462
+ */
463
+ export function sanitizeReasoningMode(value) {
464
+ return REASONING_MODES.includes(value) ? value : 'default';
465
+ }
466
+ /**
467
+ * Always returns a COMPLETE record, never a partial one.
468
+ *
469
+ * A hand-edited file missing a group, or one carrying a group from a future
470
+ * version, must not reach `resolveReasoning` as a hole: `levels[group]` would be
471
+ * `undefined`, and every call site would need its own fallback. Filling the gaps
472
+ * here means the type is true at the only place that constructs the value.
473
+ */
474
+ export function sanitizeReasoningLevels(value) {
475
+ const stored = typeof value === 'object' && value !== null && !Array.isArray(value) ?
476
+ value
477
+ : {};
478
+ const out = {};
479
+ for (const group of REASONING_GROUPS) {
480
+ const stored_ = stored[group];
481
+ out[group] =
482
+ REASONING_SETTINGS.includes(stored_) ?
483
+ stored_
484
+ : DEFAULT_REASONING_TABLE[group];
485
+ }
486
+ return out;
487
+ }
488
+ /**
489
+ * What one group is actually set to, given a config. The ONLY place the four
490
+ * modes are interpreted.
491
+ *
492
+ * `cfg` is required rather than defaulted to `getConfig()` so this module stays
493
+ * import-free (see the header). Callers that want the live config use
494
+ * `groupThinkingArgs` from reasoning-args.ts.
495
+ */
496
+ export function resolveReasoning(group, cfg) {
497
+ switch (cfg.reasoningMode) {
498
+ case 'off':
499
+ return 'off';
500
+ case 'on':
501
+ return REASONING_ON_LEVEL;
502
+ case 'custom':
503
+ return cfg.reasoningLevels[group];
504
+ default:
505
+ return DEFAULT_REASONING_TABLE[group];
506
+ }
507
+ }
508
+ /**
509
+ * The argv fragment for a setting. `inherit` is the empty fragment — no flag at
510
+ * all — which is what makes an all-`inherit` config byte-identical to the
511
+ * version before this feature existed.
512
+ */
513
+ export function thinkingArgs(setting) {
514
+ return setting === 'inherit' ? [] : ['--thinking', setting];
515
+ }
516
+ /** One honest sentence per group, for the /task-config rows. */
517
+ export const REASONING_GROUP_HELP = {
518
+ research: 'The four research workers that read the codebase before a spec is written, '
519
+ + 'and the pi-worker subagent tool. Read-only exploration loops.',
520
+ phase: 'Refining your request, generating and answering the clarifying questions, '
521
+ + 'writing the spec, and critiquing it.',
522
+ planning: "/task-auto's planners: splitting a design document into tasks and extracting "
523
+ + 'its requirements. The most reasoning-hungry step measured so far.',
524
+ plan: "/task-plan's interactive question-and-answer children.",
525
+ gate: 'The checks that run after code is written: verify, enforce, lint-fix, autofix.',
526
+ extraction: 'The small no-tools children that pull one answer out of a fetched page or '
527
+ + 'a docs chunk.',
528
+ implementation: 'The main session turn that actually writes the code. Changing this briefly '
529
+ + "changes pi's own thinking level, and puts it back afterwards."
530
+ };
@@ -1,8 +1,10 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
+ import { SettingsList } from '@earendil-works/pi-tui';
2
3
  import type { Component } from '@earendil-works/pi-tui';
3
4
  import { type PiTaskConfig } from './config.js';
4
5
  import { type InstalledExtension } from './extension-list.js';
5
6
  import { type GuardableTool } from './tool-list.js';
7
+ import { type ReasoningGroup } from './reasoning.js';
6
8
  type Theme = ExtensionCommandContext['ui']['theme'];
7
9
  /**
8
10
  * Frames a child component (the settings list) in a rounded border with a title
@@ -58,6 +60,13 @@ declare class BorderedBox implements Component {
58
60
  */
59
61
  export interface ConfigItem {
60
62
  id: keyof PiTaskConfig;
63
+ /**
64
+ * Which titled block of the menu this row sits under. Rows are grouped by
65
+ * section in {@link panelItems}, in the order the sections first appear in
66
+ * {@link ITEMS} — so moving a row between sections is a one-word edit and
67
+ * the header follows it.
68
+ */
69
+ section: Section;
61
70
  label: string;
62
71
  description: string;
63
72
  /** Offered values. Omitted for a boolean, which is always on/off. */
@@ -67,6 +76,24 @@ export interface ConfigItem {
67
76
  /** Write the chosen label back. A value it does not recognise is ignored. */
68
77
  apply: (cfg: PiTaskConfig, chosen: string) => void;
69
78
  }
79
+ /**
80
+ * The titled blocks the settings menu is divided into.
81
+ *
82
+ * A flat list of ~30 rows — twelve settings, seven reasoning groups, one row per
83
+ * live tool and one per installed extension — reads as a wall, and the rows that
84
+ * belong together (a mode and the seven groups it controls; a timeout and the
85
+ * per-tool exemptions from it) end up separated by rows that have nothing to do
86
+ * with them. The headers are inert rows: no `values`, so Enter does nothing on
87
+ * them.
88
+ */
89
+ export type Section = 'session' | 'checks' | 'research' | 'reasoning' | 'timeouts' | 'unattended' | 'logging' | 'extensions';
90
+ /** Section order, and the label each header renders. */
91
+ export declare const SECTIONS: ReadonlyArray<{
92
+ key: Section;
93
+ title: string;
94
+ }>;
95
+ /** Marks a header row, so onChange can ignore one and tests can find them. */
96
+ export declare const SECTION_ID_PREFIX = "section:";
70
97
  /**
71
98
  * Every setting rendered by /task-config, in display order.
72
99
  *
@@ -93,6 +120,34 @@ export declare function toolItems(tools: readonly GuardableTool[], exempt: reado
93
120
  }[];
94
121
  /** Apply a per-tool watchdog toggle to the exemption list (idempotent both ways). */
95
122
  export declare function applyToolToggle(exempt: readonly string[], toolName: string, watched: boolean): string[];
123
+ export declare function reasoningItems(cfg: PiTaskConfig): {
124
+ id: string;
125
+ label: string;
126
+ description: string;
127
+ currentValue: string;
128
+ values: string[];
129
+ }[];
130
+ /**
131
+ * Apply one group row's new value.
132
+ *
133
+ * Setting any group necessarily means "custom" — there is nowhere else to store
134
+ * a per-group choice. The seeding step is what stops that from being a trap: on
135
+ * the way out of `default`/`on`/`off` every OTHER group is first pinned to the
136
+ * level it was already running at, so changing one row changes one row. Without
137
+ * it, nudging `research` while in `off` would silently return the other six to
138
+ * whatever the stored table happened to hold.
139
+ */
140
+ /**
141
+ * Write every `think:` row's displayed value back from the config.
142
+ *
143
+ * Called after ANY change, not just a reasoning one, because the mode row and
144
+ * the seven group rows are one control split across eight lines: cycling
145
+ * `reasoning` to `off` changes what all seven of them run at, and cycling one
146
+ * group row flips the mode, which changes the other six. A row showing a level
147
+ * the run will not use is worse than no row.
148
+ */
149
+ export declare function refreshReasoningRows(cfg: PiTaskConfig, list: SettingsList): void;
150
+ export declare function applyReasoningLevel(cfg: PiTaskConfig, group: ReasoningGroup, chosen: string): void;
96
151
  /**
97
152
  * Tallest body the settings list can render, so {@link BorderedBox} can pad
98
153
  * every frame to it and hold the border still. Mirrors SettingsList's own
@@ -106,14 +161,29 @@ export type PanelItem = {
106
161
  label: string;
107
162
  description: string;
108
163
  currentValue: string;
109
- values: string[];
164
+ /**
165
+ * Omitted ONLY by a section header. SettingsList cycles a row on Enter when
166
+ * this is a non-empty array, so leaving it off is what makes a header inert
167
+ * — the header does not need its own branch anywhere.
168
+ */
169
+ values?: string[];
110
170
  };
111
171
  /**
112
172
  * Builds the framed settings panel. Split out of the command handler so the
113
173
  * exact component the overlay shows can be rendered to a string in a test or a
114
174
  * preview script, rather than only being inspectable by opening the TUI.
115
175
  */
116
- export declare function createSettingsPanel(items: PanelItem[], theme: Theme, onChange: (id: string, newValue: string) => void, onCancel: () => void): BorderedBox;
176
+ export declare function createSettingsPanel(items: PanelItem[], theme: Theme,
177
+ /**
178
+ * Called with the row's id, its new value, and the LIST ITSELF.
179
+ *
180
+ * The list is handed back because some rows change what OTHER rows display:
181
+ * flipping `reasoning` to off means all seven `think:` rows now run at off,
182
+ * and a row's `currentValue` is a snapshot taken when the panel was built.
183
+ * Without a way to write the others back, the menu shows `reasoning off`
184
+ * beside seven rows still claiming `inherit` — which is what it did.
185
+ */
186
+ onChange: (id: string, newValue: string, list: SettingsList) => void, onCancel: () => void): BorderedBox;
117
187
  /** The full settings row list for the current config, in menu order. */
118
188
  export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[], tools?: readonly GuardableTool[]): PanelItem[];
119
189
  export declare function registerConfig(pi: ExtensionAPI): void;