@mjasnikovs/pi-task 0.38.30 → 0.38.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +2 -1
  2. package/dist/config/config.d.ts +16 -2
  3. package/dist/config/config.js +7 -2
  4. package/dist/config/group-args.d.ts +52 -0
  5. package/dist/config/group-args.js +110 -0
  6. package/dist/config/group-models.d.ts +88 -0
  7. package/dist/config/group-models.js +117 -0
  8. package/dist/config/groups.d.ts +76 -0
  9. package/dist/config/groups.js +110 -0
  10. package/dist/config/option-picker.d.ts +42 -0
  11. package/dist/config/option-picker.js +73 -0
  12. package/dist/config/reasoning.d.ts +22 -63
  13. package/dist/config/reasoning.js +37 -108
  14. package/dist/config/register.d.ts +98 -12
  15. package/dist/config/register.js +228 -23
  16. package/dist/index.js +4 -0
  17. package/dist/remote/push.js +1 -7
  18. package/dist/shared/command-watchdog.d.ts +63 -0
  19. package/dist/shared/command-watchdog.js +87 -0
  20. package/dist/shared/data-home.d.ts +8 -0
  21. package/dist/shared/data-home.js +14 -0
  22. package/dist/shared/model-endpoint.d.ts +53 -0
  23. package/dist/shared/model-endpoint.js +98 -2
  24. package/dist/shared/reasoning-capability.d.ts +25 -5
  25. package/dist/shared/reasoning-capability.js +18 -9
  26. package/dist/task/auto-orchestrator.js +14 -3
  27. package/dist/task/child-runner.d.ts +92 -15
  28. package/dist/task/child-runner.js +303 -66
  29. package/dist/task/context-usage.d.ts +46 -0
  30. package/dist/task/context-usage.js +41 -0
  31. package/dist/task/failure-classifier.js +24 -1
  32. package/dist/task/gate-child.d.ts +15 -4
  33. package/dist/task/gate-child.js +2 -2
  34. package/dist/task/gate-deps.js +7 -2
  35. package/dist/task/implementation-guards.d.ts +26 -0
  36. package/dist/task/implementation-guards.js +177 -0
  37. package/dist/task/implementation-hold.d.ts +118 -0
  38. package/dist/task/implementation-hold.js +165 -0
  39. package/dist/task/implementation-turn.d.ts +5 -0
  40. package/dist/task/implementation-turn.js +12 -1
  41. package/dist/task/loop-detector.d.ts +18 -0
  42. package/dist/task/loop-detector.js +22 -2
  43. package/dist/task/model-hold-stash.d.ts +43 -0
  44. package/dist/task/model-hold-stash.js +70 -0
  45. package/dist/task/orchestrator.d.ts +18 -5
  46. package/dist/task/orchestrator.js +63 -6
  47. package/dist/task/phases.js +18 -5
  48. package/dist/task/research-worker.d.ts +2 -2
  49. package/dist/task/research-worker.js +1 -1
  50. package/dist/workers/docs-core.js +2 -2
  51. package/dist/workers/docs-lookup.d.ts +4 -3
  52. package/dist/workers/docs-lookup.js +1 -1
  53. package/dist/workers/fetch-core.js +2 -2
  54. package/dist/workers/focused-extractor.d.ts +6 -4
  55. package/dist/workers/focused-extractor.js +17 -5
  56. package/dist/workers/index.js +2 -0
  57. package/dist/workers/model-warning.d.ts +69 -0
  58. package/dist/workers/model-warning.js +113 -0
  59. package/dist/workers/pi-worker-core.d.ts +9 -38
  60. package/dist/workers/pi-worker-core.js +8 -86
  61. package/dist/workers/pi-worker-docs.js +2 -2
  62. package/dist/workers/pi-worker.js +4 -4
  63. package/dist/workers/reasoning-warning.d.ts +17 -9
  64. package/dist/workers/reasoning-warning.js +69 -22
  65. package/dist/workers/single-read-guard.d.ts +6 -6
  66. package/dist/workers/single-read-guard.js +8 -8
  67. package/dist/workers/worker-profiles.d.ts +11 -3
  68. package/dist/workers/worker-profiles.js +33 -1
  69. package/package.json +1 -1
  70. package/dist/config/reasoning-args.d.ts +0 -23
  71. package/dist/config/reasoning-args.js +0 -28
  72. package/dist/task/implementation-thinking.d.ts +0 -56
  73. package/dist/task/implementation-thinking.js +0 -32
@@ -10,10 +10,15 @@
10
10
  * while it runs, so only "no output AND the endpoint does not answer" counts as
11
11
  * a dead backend.
12
12
  *
13
- * Discovery is generic — the custom providers pi is configured with, read from
14
- * models.json `providers.*.baseUrl`, with no provider or server name hardcoded.
13
+ * Discovery is generic — no provider or server name is hardcoded anywhere here.
15
14
  * No discoverable endpoint means nothing to probe, and the guard then NEVER
16
15
  * kills: a child on a backend we cannot see gets the benefit of the doubt.
16
+ *
17
+ * `childModelEndpoints` is what the guards call; `discoverModelEndpoints` is its
18
+ * last resort. The difference is the point. Discovery answers "every endpoint on
19
+ * this machine", and `probeModelEndpoints` ORs over it, so one healthy provider
20
+ * says "reachable" for a child stranded on a dead one. Resolving the child's OWN
21
+ * model first turns that OR into a single exact question.
17
22
  */
18
23
  import * as fs from 'node:fs';
19
24
  import * as os from 'node:os';
@@ -33,6 +38,97 @@ export function discoverModelEndpoints(agentDir = path.join(os.homedir(), '.pi',
33
38
  return [];
34
39
  }
35
40
  }
41
+ /** A `provider/id` spec as a ref, splitting on the FIRST slash. */
42
+ function specRef(spec) {
43
+ if (spec === undefined)
44
+ return undefined;
45
+ const i = spec.indexOf('/');
46
+ if (i <= 0 || i === spec.length - 1)
47
+ return undefined;
48
+ return { provider: spec.slice(0, i), id: spec.slice(i + 1) };
49
+ }
50
+ export function defaultModelRef(agentDir = path.join(os.homedir(), '.pi', 'agent')) {
51
+ try {
52
+ const j = JSON.parse(fs.readFileSync(path.join(agentDir, 'settings.json'), 'utf8'));
53
+ const provider = j.defaultProvider;
54
+ const id = j.defaultModel;
55
+ if (typeof provider !== 'string' || provider === '')
56
+ return undefined;
57
+ if (typeof id !== 'string' || id === '')
58
+ return undefined;
59
+ return { provider, id };
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /**
66
+ * The base URL one model is served from, or `undefined` for "not configured on
67
+ * this machine".
68
+ *
69
+ * Two files, two shapes, and the difference is not cosmetic. `models.json` is
70
+ * hand-written and hangs `baseUrl` off the PROVIDER; `models-store.json` is the
71
+ * cached remote catalogue and hangs it off each MODEL — its provider objects are
72
+ * `{models, checkedAt, lastModified, etag}` with no `baseUrl` key at all. A
73
+ * reader that knows only the first shape sees one endpoint on a machine that has
74
+ * fifteen.
75
+ *
76
+ * `undefined` is the honest answer for a model pi serves from `@earendil-works/
77
+ * pi-ai`'s built-in catalogue, whose URLs live in that package rather than on
78
+ * disk. We do not import it — see the header of this file — so we say we do not
79
+ * know, and the caller declines to probe rather than probing something else.
80
+ */
81
+ export function modelBaseUrl(ref, agentDir = path.join(os.homedir(), '.pi', 'agent')) {
82
+ const url = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
83
+ try {
84
+ const j = JSON.parse(fs.readFileSync(path.join(agentDir, 'models.json'), 'utf8'));
85
+ const p = j.providers?.[ref.provider];
86
+ if (p) {
87
+ const own = p.models?.find(m => m.id === ref.id);
88
+ return url(own?.baseUrl) ?? url(p.baseUrl);
89
+ }
90
+ }
91
+ catch {
92
+ /* fall through to the store */
93
+ }
94
+ try {
95
+ const j = JSON.parse(fs.readFileSync(path.join(agentDir, 'models-store.json'), 'utf8'));
96
+ return url(j[ref.provider]?.models?.find(m => m.id === ref.id)?.baseUrl);
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ }
102
+ /**
103
+ * What to probe on behalf of one child — the endpoint that child's own model
104
+ * uses, not every endpoint on the machine.
105
+ *
106
+ * The bug this closes is `probeModelEndpoints`'s `.some(Boolean)`: with a live
107
+ * cloud provider and a dead local one, the OR answers "reachable" and the stall
108
+ * guard is disarmed for a child that will never speak again. Handing it ONE url
109
+ * makes the OR a no-op and the verdict exact.
110
+ *
111
+ * `spec` is the child's OWN `provider/id`, as carried by its argv. `undefined`
112
+ * means the child carries no `--model` and will resolve pi's saved default,
113
+ * which is then the right thing to probe. Reading the saved default for a child
114
+ * that IS pinned asks about the wrong server in BOTH directions: it can kill a
115
+ * child whose own backend is healthy, and it can leave the guard disarmed for
116
+ * one whose backend is dead.
117
+ *
118
+ * Two escapes, both toward never killing:
119
+ * - model known, endpoint not on disk (a built-in provider) → `[]`, which
120
+ * `probeModelEndpoints` reads as reachable. We cannot see that server, so we
121
+ * do not get a vote. Probing some OTHER provider's url instead would import a
122
+ * false positive, which is the one thing today's blind OR never does.
123
+ * - no readable default → today's behaviour, unchanged.
124
+ */
125
+ export function childModelEndpoints(spec, agentDir = path.join(os.homedir(), '.pi', 'agent')) {
126
+ const ref = specRef(spec) ?? defaultModelRef(agentDir);
127
+ if (!ref)
128
+ return discoverModelEndpoints(agentDir);
129
+ const url = modelBaseUrl(ref, agentDir);
130
+ return url === undefined ? [] : [url];
131
+ }
36
132
  /**
37
133
  * true → at least one endpoint ANSWERED. Any HTTP status counts, because the
38
134
  * question is liveness, not correctness: probing a path that 404s still returns
@@ -30,7 +30,7 @@
30
30
  * line-for-line identical to it, ladder included. Nothing here imports pi-ai, so
31
31
  * an upstream change will not fail a test — the two have to be re-compared.
32
32
  */
33
- import { type ReasoningGroup, type GroupSetting } from '../config/reasoning.js';
33
+ import { type ChildGroup, type GroupSetting } from '../config/reasoning.js';
34
34
  /**
35
35
  * pi's own level ladder, in order — the same seven names, in the same sequence,
36
36
  * as `EXTENDED_THINKING_LEVELS` in pi-ai's models.js. The order IS the algorithm:
@@ -67,16 +67,36 @@ export declare function supportedThinkingLevels(model: ReasoningModelFacts): Lad
67
67
  * the model supports it — which is what makes the inequality a mismatch test.
68
68
  */
69
69
  export declare function clampToModel(model: ReasoningModelFacts, level: LadderLevel): LadderLevel;
70
- /** One group whose configured setting the connected model will not honour. */
70
+ /** One group whose configured setting the model it runs on will not honour. */
71
71
  export interface ReasoningMismatch {
72
- group: ReasoningGroup;
72
+ group: ChildGroup;
73
+ /**
74
+ * The model THIS GROUP runs on. Per-item, not per-warning, because groups no
75
+ * longer share one model: a line that opens `model "X" will not run …` while
76
+ * listing groups that run on Y is itself a lie about what it checked.
77
+ */
78
+ modelName: string;
73
79
  /** What /task-config says. Never `inherit` — an inherited group asks for nothing. */
74
80
  wanted: LadderLevel;
75
81
  /** What pi will send instead. */
76
82
  actual: LadderLevel;
77
83
  }
84
+ /** What a group runs on, as much of it as this check needs. */
85
+ export interface GroupModelFacts extends ReasoningModelFacts {
86
+ /** What to call it in the warning. */
87
+ name: string;
88
+ /** Where it is served from, for the `/props` probe. Absent ⇒ not probeable. */
89
+ baseUrl?: string;
90
+ }
78
91
  /**
79
- * Every group whose setting the model will silently change.
92
+ * Every group whose setting the model IT RUNS ON will silently change.
93
+ *
94
+ * `modelFor` is a FUNCTION rather than a `Record`, for two reasons: a record
95
+ * would build eleven identical entries for the overwhelmingly common
96
+ * all-`inherit` case, and a function is drivable from a test with two literals.
97
+ * It answers `undefined` for a group whose model cannot be resolved — nothing is
98
+ * reported for those, because the run degrades to the session default and the
99
+ * separate model hint is what names them.
80
100
  *
81
101
  * `inherit` groups are skipped entirely, because an inherited group asks for
82
102
  * nothing. That is not the same as a quiet default: the shipped table is mostly
@@ -91,4 +111,4 @@ export interface ReasoningMismatch {
91
111
  * comparison. Warning about one direction while staying silent about the other
92
112
  * would ship this feature unable to see its own failure mode.
93
113
  */
94
- export declare function reasoningMismatches(model: ReasoningModelFacts | undefined, levels: Readonly<Record<ReasoningGroup, GroupSetting>>): ReasoningMismatch[];
114
+ export declare function reasoningMismatches(modelFor: (group: ChildGroup) => GroupModelFacts | undefined, levels: Readonly<Record<ChildGroup, GroupSetting>>): ReasoningMismatch[];
@@ -30,7 +30,7 @@
30
30
  * line-for-line identical to it, ladder included. Nothing here imports pi-ai, so
31
31
  * an upstream change will not fail a test — the two have to be re-compared.
32
32
  */
33
- import { REASONING_GROUPS } from '../config/reasoning.js';
33
+ import { CHILD_GROUPS } from '../config/reasoning.js';
34
34
  /**
35
35
  * pi's own level ladder, in order — the same seven names, in the same sequence,
36
36
  * as `EXTENDED_THINKING_LEVELS` in pi-ai's models.js. The order IS the algorithm:
@@ -87,7 +87,14 @@ export function clampToModel(model, level) {
87
87
  return available[0] ?? 'off';
88
88
  }
89
89
  /**
90
- * Every group whose setting the model will silently change.
90
+ * Every group whose setting the model IT RUNS ON will silently change.
91
+ *
92
+ * `modelFor` is a FUNCTION rather than a `Record`, for two reasons: a record
93
+ * would build eleven identical entries for the overwhelmingly common
94
+ * all-`inherit` case, and a function is drivable from a test with two literals.
95
+ * It answers `undefined` for a group whose model cannot be resolved — nothing is
96
+ * reported for those, because the run degrades to the session default and the
97
+ * separate model hint is what names them.
91
98
  *
92
99
  * `inherit` groups are skipped entirely, because an inherited group asks for
93
100
  * nothing. That is not the same as a quiet default: the shipped table is mostly
@@ -102,19 +109,21 @@ export function clampToModel(model, level) {
102
109
  * comparison. Warning about one direction while staying silent about the other
103
110
  * would ship this feature unable to see its own failure mode.
104
111
  */
105
- export function reasoningMismatches(model, levels) {
106
- // No model resolved yet (session still starting, or none selected): say
107
- // nothing. A warning naming no model is noise, not information.
108
- if (!model)
109
- return [];
112
+ export function reasoningMismatches(modelFor, levels) {
110
113
  const out = [];
111
- for (const group of REASONING_GROUPS) {
114
+ for (const group of CHILD_GROUPS) {
112
115
  const wanted = levels[group];
113
116
  if (wanted === 'inherit')
114
117
  continue;
118
+ // No model resolved for this group (session still starting, none
119
+ // selected, or a spec this machine cannot resolve): say nothing. A
120
+ // warning naming no model is noise, not information.
121
+ const model = modelFor(group);
122
+ if (!model)
123
+ continue;
115
124
  const actual = clampToModel(model, wanted);
116
125
  if (actual !== wanted)
117
- out.push({ group, wanted, actual });
126
+ out.push({ group, modelName: model.name, wanted, actual });
118
127
  }
119
128
  return out;
120
129
  }
@@ -22,7 +22,7 @@ import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairT
22
22
  import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
23
23
  import { readTextFile } from '../shared/fs-text.js';
24
24
  import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
25
- import { prependHint, USER_CANCELLED } from './child-runner.js';
25
+ import { isFatalChildCause, prependHint, USER_CANCELLED } from './child-runner.js';
26
26
  import { requestCancel, resetCancel, isCancelRequested, cancelCheckpoint } from './cancel-points.js';
27
27
  import { withRun, announceTerminal } from './run-bracket.js';
28
28
  import { refineExistingFilesBlock, SINGLE_READ_EXTENSION_PATH } from './phases.js';
@@ -493,8 +493,15 @@ export async function orientFeature(cwd, feature, deps) {
493
493
  reqEntries = capRequirements(reqEntries, passages, featureForModel);
494
494
  logPlanDebug(cwd, `requirement extraction: ${reqEntries.length} grounded requirement(s) kept`);
495
495
  }
496
- catch {
497
- // best-effort channel
496
+ catch (e) {
497
+ // Best-effort covers a child that answered badly. It must NOT cover a user
498
+ // ESC or a dead backend: planning would continue on an EMPTY ledger, moving
499
+ // the granularity floor and shipping a degraded plan instead of a cancel or
500
+ // a failure. Same rule as verify-resolution.ts.
501
+ if (isFatalChildCause(e))
502
+ throw e;
503
+ // Best-effort, but not silent: nothing else records a guard kill here.
504
+ logPlanDebug(cwd, `requirement extraction: skipped — ${e.message}`);
498
505
  }
499
506
  // Granularity floor: without it the plan's task COUNT is set by an
500
507
  // auto-resolved clarify line the user never sees, so the same spec and the same
@@ -1101,8 +1108,12 @@ function defaultDeps(ctx, cwd, signal, title) {
1101
1108
  const status = new ChildStatus({ parentContextWindow });
1102
1109
  const phaseDeps = {
1103
1110
  cwd,
1111
+ // No task file, so appendLoopEvent swallows its ENOENT. Its docblock
1112
+ // allows that because "the kill is already reported through the debug
1113
+ // log" — which is why logDebug below is not optional here.
1104
1114
  taskId: '',
1105
1115
  signal,
1116
+ logDebug: msg => logPlanDebug(cwd, msg),
1106
1117
  // IN-RUN thrash guard for the planning children: without it a decompose
1107
1118
  // child can re-read its design document until it fills the whole context
1108
1119
  // window, and never return. Every planning child
@@ -6,15 +6,14 @@
6
6
  * for phase-level child pi invocations.
7
7
  */
8
8
  import { type SpawnFn, type ContextSnapshot, type ToolCall, type LoopHit } from '../shared/child-process.js';
9
+ import { type CommandKill } from '../shared/command-watchdog.js';
10
+ import { type WorkerGuardPolicy } from '../workers/worker-profiles.js';
9
11
  import type { DebugLine } from './debug-log.js';
10
12
  import type { RunWorkerInput, RunWorkerResult } from '../workers/pi-worker-core.js';
11
13
  import type { docsRaw, docsFocused } from '../workers/docs-core.js';
12
14
  import type { fetchRaw, fetchFocused } from '../workers/fetch-core.js';
13
15
  import type { npmVersionLookup } from '../workers/npm-version.js';
14
16
  import type { SearchCoreInput, SearchCoreResult } from '../workers/search-core.js';
15
- export declare const LOOP_WINDOW = 20;
16
- export declare const LOOP_THRESHOLD = 5;
17
- export declare const MAX_LOOP_RESTARTS = 2;
18
17
  /**
19
18
  * Optional wall-clock bound on ONE spawn of a phase child. DEFAULT: OFF.
20
19
  *
@@ -50,6 +49,53 @@ export declare class PhaseTimeoutError extends Error {
50
49
  readonly attempts: number;
51
50
  constructor(childName: string, budgetMs: number, attempts: number);
52
51
  }
52
+ /**
53
+ * The terminal error for a guard kill, or null when the child was not killed.
54
+ *
55
+ * Both spawn paths must ask. A kill reports `exitCode: 0` (child-process.ts uses
56
+ * `code ?? 0`, and a signal gives null), so a path that tests the exit code
57
+ * instead returns the truncated text as the phase's answer.
58
+ */
59
+ export declare function guardKillError(name: string, r: PhaseRunResult, opts?: {
60
+ finalAttempt?: boolean;
61
+ }): Error | null;
62
+ /**
63
+ * The dead-backend probe killed a phase child on its LAST attempt.
64
+ *
65
+ * Reaching this means every attempt found no endpoint answering, not one. The
66
+ * single-probe verdict is not trusted on its own, because one sample cannot tell
67
+ * a dead server from a blip. Three failed probes cost ~15s; one wrong verdict
68
+ * costs the run.
69
+ */
70
+ export declare class BackendDownError extends Error {
71
+ readonly childName: string;
72
+ constructor(childName: string);
73
+ }
74
+ /**
75
+ * A phase child spent every attempt on a command that never returned. Its own
76
+ * class because the fix is in the SPEC, not the model's exploration: a VERIFY
77
+ * block naming an unbounded `dev` command re-hangs every attempt.
78
+ */
79
+ export declare class CommandTimeoutError extends Error {
80
+ readonly childName: string;
81
+ readonly kill: CommandKill;
82
+ constructor(childName: string, kill: CommandKill);
83
+ }
84
+ /**
85
+ * Causes a best-effort `catch` must NOT absorb.
86
+ *
87
+ * A phase child that merely answered badly should degrade — that is what those
88
+ * catches are for. These two are different in kind: the run is over either way,
89
+ * and swallowing them ships a half-built spec while every later phase dies
90
+ * against the same dead backend, or turns a user's ESC into silent progress.
91
+ * `failure-classifier.ts` has a verdict for both; a catch that eats them makes it
92
+ * unreachable.
93
+ */
94
+ export declare function isFatalChildCause(e: unknown): boolean;
95
+ /**
96
+ * Retry budget is three attempts at 500ms/1s/2s — three requests over 3.5s, which
97
+ * is not a storm even against a throttle. pi's own ladder is three at 2s/4s/8s.
98
+ */
53
99
  export declare function isConnectionError(cause: string): boolean;
54
100
  /** Exponential backoff before a connection-error retry: 500ms, 1s, 2s, …, so a
55
101
  * brief saturation window can drain before we re-issue the request. */
@@ -63,15 +109,31 @@ export interface PhaseRunResult {
63
109
  leakedToolCall?: string;
64
110
  /** Set when the child's final turn failed with stopReason "error" (model/provider failure). */
65
111
  modelError?: string;
112
+ /**
113
+ * Set when the per-command watchdog killed the child: one tool call outran
114
+ * `requestTimeoutMs`. RESTARTABLE (worker-kill.ts) — a hung command is a
115
+ * mistake the next attempt can be told not to repeat.
116
+ */
117
+ commandKill?: CommandKill;
118
+ /**
119
+ * Set when the dead-backend probe killed the child: no output for the stall
120
+ * window AND the model endpoint unreachable. NOT restartable (worker-kill.ts):
121
+ * re-spawning against a backend that is down buys nothing.
122
+ */
123
+ stalled?: boolean;
66
124
  }
67
125
  export declare function childArgs(tools: string, extensions?: readonly string[],
68
126
  /**
69
- * An already-resolved `['--thinking', level]`, or `[]` for "emit no flag".
70
- * Resolved by the CALLER, never here: the level is a property of the child's
71
- * ROLE, and this function is handed tools and extensions, not a name.
72
- * Omitted ⇒ byte-identical argv to the version before reasoning profiles.
127
+ * This child's group fragment: `--model` then `--thinking`, either half
128
+ * possibly absent. Resolved by the CALLER, never here both are properties
129
+ * of the child's ROLE, and this function is handed tools and extensions, not
130
+ * a name. Omitted ⇒ byte-identical argv to the version before group profiles.
131
+ *
132
+ * ONE field rather than a `model` beside a `thinking`, because nothing may
133
+ * compose the two halves by hand: `groupChildArgs` is the only producer, so a
134
+ * doubled `--thinking` is unreachable rather than merely unlikely.
73
135
  */
74
- thinking?: readonly string[]): string[];
136
+ groupArgs?: readonly string[]): string[];
75
137
  export declare const USER_CANCELLED = "__user_cancelled__";
76
138
  /**
77
139
  * Run a child pi process with JSON event-stream output, loop detection, and
@@ -116,12 +178,27 @@ export interface ChildRun {
116
178
  */
117
179
  contextWindow?: number;
118
180
  /**
119
- * The resolved `['--thinking', level]` fragment for this child's reasoning
120
- * group, or `[]`/omitted to inherit the session default as before.
181
+ * The resolved argv fragment for this child's group — `--model` then
182
+ * `--thinking` or `[]`/omitted to inherit both defaults as before.
183
+ */
184
+ groupArgs?: readonly string[];
185
+ /**
186
+ * This attempt's per-command ceiling, already halved for prior hangs by the
187
+ * caller's strike loop. Omitted -> the `phase` row's full configured ceiling,
188
+ * which is the right value for a single-attempt caller.
121
189
  */
122
- thinking?: readonly string[];
190
+ commandCeilingMs?: number;
123
191
  }
124
- export declare function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow, thinking }: ChildRun): Promise<PhaseRunResult>;
192
+ /**
193
+ * The `phase` row of WORKER_PROFILES, resolved with this machine's config.
194
+ *
195
+ * Read here rather than at module load so a /task-config change reaches the next
196
+ * child, the same contract childBaseArgs already keeps. Both spawn paths in this
197
+ * file go through it, so the degraded final attempt cannot drift from the ordinary
198
+ * one — the mislabel class runDegradedFinalAttempt's own comment warns about.
199
+ */
200
+ export declare function phasePolicy(): WorkerGuardPolicy;
201
+ export declare function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow, groupArgs, commandCeilingMs }: ChildRun): Promise<PhaseRunResult>;
125
202
  export interface PhaseDeps {
126
203
  cwd: string;
127
204
  taskId: string;
@@ -263,15 +340,15 @@ export type PhaseSeams = Omit<PhaseDeps, 'cwd' | 'taskId' | 'signal' | 'onChildO
263
340
  * exit status describes our SIGTERM and says nothing about its verdict.
264
341
  */
265
342
  /**
266
- * The `--thinking` fragment for a named child, or `[]` when the name is unmapped.
343
+ * The group fragment for a named child, or `[]` when the name is unmapped.
267
344
  *
268
345
  * An unmapped name INHERITS rather than throwing: a child that reaches the model
269
346
  * with today's argv is always safe, and aborting a user's task over a missing
270
347
  * table row would be a worse failure than the one it reports. The guard that
271
- * makes the table complete is `reasoning-groups.test.ts`, which fails the BUILD —
348
+ * makes the table complete is `config/groups.test.ts`, which fails the BUILD —
272
349
  * where someone can actually fix it.
273
350
  */
274
- export declare function thinkingForChild(name: string): string[];
351
+ export declare function groupArgsForChild(name: string): string[];
275
352
  export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: string, prompt: string, opts?: PhaseChildOptions): Promise<string>;
276
353
  export declare function formatLoopHint(hit: LoopHit): string;
277
354
  /**