@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
package/README.md CHANGED
@@ -214,6 +214,7 @@ 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
+ | **reasoning** | default | How much the helper sessions think before answering. Local models differ sharply here: some break without reasoning, some waste minutes with it, and some cannot do it at all. The seven **think:** rows below set one level per group of steps — research workers, spec phases, /task-auto planning, /task-plan, the checking gates, the small extraction children, and the implementation turn itself. **default** uses the table pi-task has measured, **on** and **off** force one answer everywhere, and **custom** is whatever the think: rows say (changing any of them switches 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, and is what every cell of the measured table currently holds. The think: rows always show what a step *actually* runs at, not what the stored table says. Three tiers of model exist and pi-task tells them apart: full levels (`reasoning_effort` in the chat template, e.g. Qwen3.8), on/off only (e.g. Qwen3.6, Gemma 4), and none at all. pi silently CLAMPS a level a model does not declare — a level you set can be erased, and an `off` can be clamped back up to `medium` — so a startup warning names the model and the steps it will not honour. It never fires while every step is on inherit. |
217
218
  | **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. |
218
219
  | **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. |
219
220
  | **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. |
@@ -1,4 +1,5 @@
1
1
  import { type SearchProvider } from '../workers/search-types.js';
2
+ import { type GroupSetting, type ReasoningGroup, type ReasoningMode } from './reasoning.js';
2
3
  export interface PiTaskConfig {
3
4
  remote: boolean;
4
5
  autoCommit: boolean;
@@ -134,6 +135,24 @@ export interface PiTaskConfig {
134
135
  * cannot be recovered after the fact.
135
136
  */
136
137
  debugLogs: DebugLogLevel;
138
+ /**
139
+ * Which reasoning profile is in force. See config/reasoning.ts for the four
140
+ * modes and the group vocabulary.
141
+ *
142
+ * DEFAULT `default`, whose table ships all-`inherit` — so installing this
143
+ * version changes no child's argv until the user opts in.
144
+ */
145
+ reasoningMode: ReasoningMode;
146
+ /**
147
+ * The per-group thinking levels, consulted ONLY when
148
+ * `reasoningMode === 'custom'`.
149
+ *
150
+ * Kept populated in every mode so switching to `custom` and back does not
151
+ * lose the user's table — the same reason `commandTimeoutExemptTools`
152
+ * survives the watchdog being turned off. Its sanitizer always returns a
153
+ * complete record, so no consumer needs a per-key fallback.
154
+ */
155
+ reasoningLevels: Record<ReasoningGroup, GroupSetting>;
137
156
  }
138
157
  /** How verbose the `.pi-tasks/*-debug.log` trail is. See {@link PiTaskConfig.debugLogs}. */
139
158
  export type DebugLogLevel = 'off' | 'events' | 'full';
@@ -3,6 +3,7 @@ import * as fsp from 'node:fs/promises';
3
3
  import * as path from 'node:path';
4
4
  import * as os from 'node:os';
5
5
  import { isSearchProvider } from '../workers/search-types.js';
6
+ import { DEFAULT_REASONING_TABLE, sanitizeReasoningLevels, sanitizeReasoningMode } from './reasoning.js';
6
7
  import { DEFAULT_STREAM_INACTIVITY_MS } from '../shared/stream-watchdog.js';
7
8
  /**
8
9
  * The debug-log choices offered by /task-config, in cycle order (quietest →
@@ -104,7 +105,12 @@ export const DEFAULT_CONFIG = {
104
105
  yoloMode: false,
105
106
  // EVENTS: the model chatter is 85% of the bytes and nobody reads it; the
106
107
  // guard/verdict markers are the 15% that explains a failed run.
107
- debugLogs: DEFAULT_DEBUG_LOGS
108
+ debugLogs: DEFAULT_DEBUG_LOGS,
109
+ // DEFAULT: the measured per-group table, which currently holds `inherit` in
110
+ // every cell. Every child's argv is byte-identical to the version before
111
+ // reasoning profiles existed until a cell is filled in by an A/B.
112
+ reasoningMode: 'default',
113
+ reasoningLevels: { ...DEFAULT_REASONING_TABLE }
108
114
  };
109
115
  /**
110
116
  * A hand-edited config can hold anything; keep only string entries so a stray
@@ -155,7 +161,9 @@ export const CONFIG_LOADERS = {
155
161
  requestTimeoutMs: sanitizeRequestTimeoutMs,
156
162
  commandTimeoutExemptTools: sanitizeCommandTimeoutExemptTools,
157
163
  streamInactivityMs: sanitizeStreamInactivityMs,
158
- debugLogs: sanitizeDebugLogs
164
+ debugLogs: sanitizeDebugLogs,
165
+ reasoningMode: sanitizeReasoningMode,
166
+ reasoningLevels: sanitizeReasoningLevels
159
167
  };
160
168
  /**
161
169
  * Turn parsed config JSON into a `PiTaskConfig`. Pure — this is the seam the load
@@ -0,0 +1,10 @@
1
+ import { type ReasoningGroup } from './reasoning.js';
2
+ /**
3
+ * The `['--thinking', level]` fragment for a group, or `[]` when the group is
4
+ * `inherit` and the child should keep falling back to settings.json.
5
+ *
6
+ * This is the ONLY function the argv builders call. They never read config
7
+ * themselves — an argv builder that resolves its own policy is one that cannot
8
+ * be told to do something else, which is how childBaseArgs became universal.
9
+ */
10
+ export declare function groupThinkingArgs(group: ReasoningGroup): string[];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The live-config bridge for reasoning profiles: group in, argv fragment out.
3
+ *
4
+ * Separate from reasoning.ts because that module must stay import-free — see its
5
+ * header. This file is the one hop that reads `getConfig()`, so it imports both
6
+ * and neither imports it back: a tree, not a cycle.
7
+ *
8
+ * Read PER CALL, never cached at module scope, so a /task-config change lands on
9
+ * the next child without a restart — the same contract childBaseArgs states.
10
+ */
11
+ import { getConfig } from './config.js';
12
+ import { resolveReasoning, thinkingArgs } from './reasoning.js';
13
+ /**
14
+ * The `['--thinking', level]` fragment for a group, or `[]` when the group is
15
+ * `inherit` and the child should keep falling back to settings.json.
16
+ *
17
+ * This is the ONLY function the argv builders call. They never read config
18
+ * themselves — an argv builder that resolves its own policy is one that cannot
19
+ * be told to do something else, which is how childBaseArgs became universal.
20
+ */
21
+ export function groupThinkingArgs(group) {
22
+ return thinkingArgs(resolveReasoning(group, getConfig()));
23
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Reasoning profiles — which thinking level each group of model children runs at.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * Every pi-task child inherits `defaultThinkingLevel` from the host's
7
+ * `~/.pi/agent/settings.json`, because `CHILD_BASE_ARGS`
8
+ * (shared/child-process.ts) passes no `--thinking`. That is one level for ~33
9
+ * children whose jobs are nothing alike: a planner that must reason to produce a
10
+ * plan at all, and a label-compressor that emits four words.
11
+ *
12
+ * The control that WAS here — appending Qwen3's `/no_think` soft switch to eight
13
+ * prompts — is measured inert. Captured live against Qwen3.8-27B with server
14
+ * thinking on and `/no_think` still in the prompt: median 17k-char trace anyway,
15
+ * n=25. A harness cannot disable thinking by prompt text; the chat-template
16
+ * kwarg beats it. `--thinking` sets that kwarg, so it is the lever that works.
17
+ *
18
+ * PURE MODULE — no imports with runtime side effects, so `config.ts` can import
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
21
+ * it here would make config.ts ⇄ reasoning.ts a real cycle, and whichever module
22
+ * a caller reached first would decide whether `DEFAULT_REASONING_TABLE` was
23
+ * initialised before `DEFAULT_CONFIG` read it.
24
+ */
25
+ import type { PiTaskConfig } from './config.js';
26
+ /**
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 two that reach the model by different code paths (`refine` via
32
+ * runPhaseChild, `compress-label` via the same) want the same.
33
+ *
34
+ * - `research` the four research workers, plus the ad-hoc `pi-worker` tool
35
+ * - `phase` refine, verify-tooling, grill, compose, critique, compress-label
36
+ * - `planning` /task-auto's clarify / decompose / extract children
37
+ * - `plan` /task-plan's question and answer children
38
+ * - `gate` enforce, verify, lint-fix, final-fix, recommend
39
+ * - `extraction` the --no-tools focused docs/fetch extractors
40
+ * - `implementation` the host-session turn that writes the code (not a child)
41
+ */
42
+ export type ReasoningGroup = 'research' | 'phase' | 'planning' | 'plan' | 'gate' | 'extraction' | 'implementation';
43
+ export declare const REASONING_GROUPS: readonly ReasoningGroup[];
44
+ /**
45
+ * The four profiles offered by /task-config.
46
+ * - `default` the measured per-group table below
47
+ * - `on` one level everywhere ({@link REASONING_ON_LEVEL})
48
+ * - `off` no thinking anywhere
49
+ * - `custom` the user's own per-group table
50
+ */
51
+ export type ReasoningMode = 'default' | 'on' | 'off' | 'custom';
52
+ export declare const REASONING_MODES: readonly ReasoningMode[];
53
+ /**
54
+ * What one group is set to.
55
+ *
56
+ * `inherit` is NOT a pi thinking level — it means EMIT NO FLAG, so the child
57
+ * falls back to `settings.json` exactly as it does today. It is what lets this
58
+ * whole feature ship as a zero-behaviour-change commit: with every cell at
59
+ * `inherit`, every child's argv is byte-identical to the version before it.
60
+ */
61
+ export type GroupSetting = 'inherit' | 'off' | 'minimal' | 'low' | 'medium' | 'high';
62
+ /**
63
+ * The settings offered in /task-config: `inherit` plus pi's OWN cycle
64
+ * (`THINKING_LEVELS` in pi-coding-agent's agent-session).
65
+ *
66
+ * `xhigh` and `max` are DELIBERATELY ABSENT. pi treats an absent
67
+ * `thinkingLevelMap` entry as "supported" for the standard levels but requires a
68
+ * declared entry for the extended two (pi-ai `getSupportedThinkingLevels`), so a
69
+ * model with no map would receive the raw string — and Qwen3.8's chat template
70
+ * answers an unknown effort with HTTP 500, not a clamp. Offering a level that pi's
71
+ * own UI does not is how you ship a `--thinking` that hard-fails on some models.
72
+ * This machine already reaches xhigh through `"high": "xhigh"` in its map.
73
+ */
74
+ export declare const REASONING_SETTINGS: readonly GroupSetting[];
75
+ /**
76
+ * The level mode `on` uses, and the treatment arm of the A/B that fills in
77
+ * {@link DEFAULT_REASONING_TABLE}. ONE constant so the shipped config and the
78
+ * measurement behind it can never drift apart.
79
+ */
80
+ export declare const REASONING_ON_LEVEL: GroupSetting;
81
+ /**
82
+ * The per-group table used by mode `default`.
83
+ *
84
+ * A cell is `inherit` until it has been MEASURED, and then it names a level.
85
+ * Every non-`inherit` cell here must be a live A/B result
86
+ * (scripts/live-reasoning-group-ab.ts, n>=20 per arm, arms `off` vs
87
+ * {@link REASONING_ON_LEVEL}) carrying its date, model, counts and RUNG in the
88
+ * comment beside it. A cell filled in from intuition is WORSE than `inherit`:
89
+ * `inherit` is honest about knowing nothing, while a wrong cell is a
90
+ * measurement nobody took, wearing the authority of a default.
91
+ *
92
+ * THE RUNG IS PART OF THE RESULT, so read it before trusting a cell. The
93
+ * harness returns a two-way verdict — `off` or {@link REASONING_ON_LEVEL},
94
+ * never a tie — down a three-rung ladder: rung 1 a significant quality
95
+ * difference, rung 2 quality level and a significant speed difference, rung 3
96
+ * nothing separated the arms and the cheaper level carries it by a stated
97
+ * prior. A rung-3 cell is a DECISION, not a finding: it says "no reason found
98
+ * to pay for thinking", not "thinking was shown not to help". At n=20/arm the
99
+ * run is only powered for large effects, so rung 3 is the common outcome and
100
+ * an absent effect and an undetected one look identical from here.
101
+ *
102
+ * NOTHING IS ALREADY ON THE RECORD, and the belief that something was is worth
103
+ * writing down. This docstring used to say magicknumbers.md had measured the
104
+ * decompose child "with one knob, `enable_thinking` — off answered 1/10, on
105
+ * answered 8/8", and treated that as the split the A/B had to reproduce.
106
+ *
107
+ * VERIFIED 2026-08-27: the string "8/8" does not appear in magicknumbers.md, and
108
+ * never has (`git log -S`; the phrase was introduced by THIS file's own commit,
109
+ * `0b91f71`). What that page records is
110
+ *
111
+ * "Measured, captured decompose request, REASONING OFF, n=10 per cell:
112
+ * 1/10 → 7/10 (stall detector) → 8/9 (all three)."
113
+ *
114
+ * — a ladder over THREE GUARDS at CONSTANT reasoning off, from `fea7bbb`. Both
115
+ * endpoints are the same arm. `enable_thinking` is not its knob and never was,
116
+ * and the "8/8" is a misread of the ladder's last cell, `8/9`.
117
+ *
118
+ * So no reasoning effect on `planning` had ever been measured, and the 10/10 vs
119
+ * 10/10 read on the current tree was not a contradiction of a prior result — it
120
+ * was the only reading there had ever been. The controls that page DOES record
121
+ * (Qwen3.6-27B 10/10, Gemma4-12B 10/10) are likewise reasoning off, so they say
122
+ * nothing about a cell flipping by model either.
123
+ *
124
+ * The `planning` cell below is the FIRST measured reading of that knob, taken
125
+ * 2026-08-27 on a citation-fidelity axis built for it. It is also the first cell
126
+ * that is not `off`.
127
+ */
128
+ export declare const DEFAULT_REASONING_TABLE: Readonly<Record<ReasoningGroup, GroupSetting>>;
129
+ /**
130
+ * A hand-edited or stale mode must not reach {@link resolveReasoning}'s switch as
131
+ * an unknown string — the `default:` arm would silently absorb it and the user
132
+ * would see "custom" in the file and the default table in behaviour.
133
+ */
134
+ export declare function sanitizeReasoningMode(value: unknown): ReasoningMode;
135
+ /**
136
+ * Always returns a COMPLETE record, never a partial one.
137
+ *
138
+ * A hand-edited file missing a group, or one carrying a group from a future
139
+ * version, must not reach `resolveReasoning` as a hole: `levels[group]` would be
140
+ * `undefined`, and every call site would need its own fallback. Filling the gaps
141
+ * here means the type is true at the only place that constructs the value.
142
+ */
143
+ export declare function sanitizeReasoningLevels(value: unknown): Record<ReasoningGroup, GroupSetting>;
144
+ /**
145
+ * What one group is actually set to, given a config. The ONLY place the four
146
+ * modes are interpreted.
147
+ *
148
+ * `cfg` is required rather than defaulted to `getConfig()` so this module stays
149
+ * import-free (see the header). Callers that want the live config use
150
+ * `groupThinkingArgs` from reasoning-args.ts.
151
+ */
152
+ export declare function resolveReasoning(group: ReasoningGroup, cfg: PiTaskConfig): GroupSetting;
153
+ /**
154
+ * The argv fragment for a setting. `inherit` is the empty fragment — no flag at
155
+ * all — which is what makes an all-`inherit` config byte-identical to the
156
+ * version before this feature existed.
157
+ */
158
+ export declare function thinkingArgs(setting: GroupSetting): string[];
159
+ /** One honest sentence per group, for the /task-config rows. */
160
+ export declare const REASONING_GROUP_HELP: Readonly<Record<ReasoningGroup, string>>;