@mjasnikovs/pi-task 0.39.0 → 0.39.1

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 (49) hide show
  1. package/dist/config/group-args.d.ts +24 -9
  2. package/dist/config/group-args.js +38 -28
  3. package/dist/config/register.d.ts +0 -9
  4. package/dist/config/register.js +17 -62
  5. package/dist/shared/child-process.d.ts +34 -32
  6. package/dist/shared/child-process.js +44 -58
  7. package/dist/shared/command-watchdog.d.ts +12 -4
  8. package/dist/shared/command-watchdog.js +6 -7
  9. package/dist/shared/connection-error.d.ts +7 -0
  10. package/dist/shared/connection-error.js +65 -0
  11. package/dist/shared/model-endpoint.d.ts +12 -24
  12. package/dist/shared/model-endpoint.js +32 -82
  13. package/dist/shared/model-resolve.d.ts +105 -0
  14. package/dist/shared/model-resolve.js +97 -0
  15. package/dist/shared/reasoning-capability.d.ts +20 -0
  16. package/dist/shared/reasoning-capability.js +32 -1
  17. package/dist/shared/stall-probe.d.ts +51 -0
  18. package/dist/shared/stall-probe.js +79 -0
  19. package/dist/task/child-runner.d.ts +76 -278
  20. package/dist/task/child-runner.js +186 -722
  21. package/dist/task/context-usage.js +2 -7
  22. package/dist/task/failure-classifier.js +53 -81
  23. package/dist/task/gate-child.js +1 -1
  24. package/dist/task/impl-widget.d.ts +2 -0
  25. package/dist/task/impl-widget.js +4 -0
  26. package/dist/task/implementation-hold.d.ts +11 -0
  27. package/dist/task/implementation-hold.js +20 -0
  28. package/dist/task/implementation-scope.d.ts +24 -0
  29. package/dist/task/implementation-scope.js +34 -0
  30. package/dist/task/loop-detector.d.ts +13 -5
  31. package/dist/task/loop-detector.js +11 -5
  32. package/dist/task/model-hold-stash.js +4 -14
  33. package/dist/task/orchestrator.d.ts +1 -8
  34. package/dist/task/orchestrator.js +11 -34
  35. package/dist/task/phases.js +2 -2
  36. package/dist/task/stall-detector.d.ts +1 -1
  37. package/dist/task/stall-detector.js +1 -1
  38. package/dist/workers/model-warning.d.ts +4 -16
  39. package/dist/workers/model-warning.js +14 -70
  40. package/dist/workers/pi-worker-core.d.ts +65 -20
  41. package/dist/workers/pi-worker-core.js +109 -50
  42. package/dist/workers/reasoning-warning.js +2 -24
  43. package/dist/workers/worker-failure.d.ts +2 -0
  44. package/dist/workers/worker-failure.js +2 -1
  45. package/dist/workers/worker-kill.d.ts +30 -11
  46. package/dist/workers/worker-kill.js +68 -20
  47. package/dist/workers/worker-profiles.d.ts +20 -0
  48. package/dist/workers/worker-profiles.js +22 -9
  49. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Dead-backend stall probe — the third child guard machine, beside the stream and
3
+ * command watchdogs.
4
+ *
5
+ * The failure it serves: the model server dies mid-child and the child hangs
6
+ * MUTE. pi's own connection handling runs from a catch, so a request that never
7
+ * answers never reaches it. Silence alone is not evidence — prompt processing
8
+ * legitimately emits nothing for minutes — so only "no output for `afterMs` AND
9
+ * the endpoint does not answer a probe" counts as a dead backend.
10
+ *
11
+ * Liveness is OUTPUT PROGRESS: any chunk resets the window. A reachable probe
12
+ * also resets it, so the next probe is a full window away rather than every
13
+ * tick, and a probe that itself throws proves nothing and is treated as
14
+ * reachable. The machine takes its clock and scheduler as deps for the same
15
+ * reason the two watchdogs do: so it can be driven by a fake in a unit test
16
+ * instead of only through a real spawn.
17
+ */
18
+ /**
19
+ * Half the window, clamped. Never tighter than 50ms, never looser than 15s: a
20
+ * probe is one network call, so it must not be re-issued every tick, and a
21
+ * window of minutes must still be noticed within seconds of elapsing.
22
+ */
23
+ export function stallPollIntervalMs(afterMs) {
24
+ return Math.max(50, Math.min(afterMs / 2, 15_000));
25
+ }
26
+ export class StallProbe {
27
+ deps;
28
+ timer;
29
+ lastActivity = 0;
30
+ probing = false;
31
+ dead = false;
32
+ constructor(deps) {
33
+ this.deps = deps;
34
+ }
35
+ start() {
36
+ if (this.timer !== undefined)
37
+ return;
38
+ this.lastActivity = this.deps.now();
39
+ this.timer = this.deps.schedule(() => void this.check(), stallPollIntervalMs(this.deps.afterMs));
40
+ }
41
+ /** Any output from the child: the window starts over. */
42
+ note() {
43
+ this.lastActivity = this.deps.now();
44
+ }
45
+ stop() {
46
+ if (this.timer !== undefined)
47
+ this.deps.cancel(this.timer);
48
+ this.timer = undefined;
49
+ }
50
+ /** @internal Exposed for the poll callback and tests. */
51
+ async check() {
52
+ if (this.probing || this.dead)
53
+ return;
54
+ if (this.deps.now() - this.lastActivity < this.deps.afterMs)
55
+ return;
56
+ this.probing = true;
57
+ let reachable;
58
+ try {
59
+ reachable = await this.deps.probe();
60
+ }
61
+ catch {
62
+ reachable = true;
63
+ }
64
+ this.probing = false;
65
+ if (reachable) {
66
+ this.lastActivity = this.deps.now();
67
+ return;
68
+ }
69
+ this.dead = true;
70
+ this.stop();
71
+ this.deps.onDead();
72
+ }
73
+ }
74
+ /** Real-clock poll deps. REF'd for the reason stream-watchdog.ts records. */
75
+ export const realStallTimerDeps = {
76
+ now: () => Date.now(),
77
+ schedule: (fn, ms) => setInterval(fn, ms),
78
+ cancel: handle => clearInterval(handle)
79
+ };
@@ -1,204 +1,66 @@
1
1
  /**
2
- * Child process runner for the pi-task orchestrator.
2
+ * The phase children's adapter over the one attempt loop.
3
3
  *
4
- * Thin wrapper layer over the unified `runChild` in `shared/child-process.ts`.
5
- * Provides JSON event-stream parsing, loop detection, and context-usage tracking
6
- * for phase-level child pi invocations.
7
- */
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';
4
+ * `runWorker` (workers/pi-worker-core.ts) owns every guard a model child runs
5
+ * under and every restart it may be granted; the `phase` row of WORKER_PROFILES
6
+ * says which. What is left here is what a PHASE child is that a research worker
7
+ * is not: it is named, its name picks its group, its failure is THROWN as a
8
+ * typed error the pipeline switches on, and its loop kills leave a trail in the
9
+ * task file. Nothing here re-decides how a child may die.
10
+ */
11
+ import { type RunWorkerInput, type RunWorkerResult } from '../workers/pi-worker-core.js';
12
+ import { type WorkerFailure } from '../workers/worker-failure.js';
13
+ import type { SpawnFn, ContextSnapshot, LoopHit } from '../shared/child-process.js';
11
14
  import type { DebugLine } from './debug-log.js';
12
- import type { RunWorkerInput, RunWorkerResult } from '../workers/pi-worker-core.js';
13
15
  import type { docsRaw, docsFocused } from '../workers/docs-core.js';
14
16
  import type { fetchRaw, fetchFocused } from '../workers/fetch-core.js';
15
17
  import type { npmVersionLookup } from '../workers/npm-version.js';
16
18
  import type { SearchCoreInput, SearchCoreResult } from '../workers/search-core.js';
19
+ export declare const USER_CANCELLED = "__user_cancelled__";
17
20
  /**
18
- * Optional wall-clock bound on ONE spawn of a phase child. DEFAULT: OFF.
19
- *
20
- * WHY OFF, AND NOT A NUMBER. A wall clock on a model child measures the
21
- * MODEL'S SPEED, not its health. The same planning child that answers well in
22
- * seconds on one backend takes many minutes on another, or on the same backend
23
- * with thinking turned on — so any cap generous enough to be safe is too loose
24
- * to catch anything, and any cap tight enough to catch a runaway kills healthy
25
- * work. Assume a model that emits one token per second and the number has no
26
- * defensible value at all.
27
- *
28
- * The runaway it was there to catch — a child forward-paging through its whole
29
- * context window, past the loop detector, never going to return — is caught by
30
- * StallDetector (stall-detector.ts) instead. That bounds NON-PROGRESS and
31
- * CONTEXT CHURN, both properties of the pathology itself, so neither has to be
32
- * re-tuned for a slower model or a bigger repo.
33
- *
34
- * The value and the plumbing stay for a caller that genuinely wants a hard stop
35
- * (tests inject a short one), but nothing sets it in production. Pass
36
- * `timeoutMs` explicitly to arm it.
37
- */
38
- export declare const PHASE_CHILD_TIMEOUT_MS = 0;
39
- /**
40
- * Restart hint after a phase child burns its whole wall-clock budget. It
41
- * diagnoses over-exploration, which is what the cap actually catches — the same
42
- * job WORKER_TIMEOUT_HINT does for research workers.
43
- */
44
- export declare const PHASE_TIMEOUT_HINT: string;
45
- /** Thrown when a phase child spends its whole restart budget hitting the cap. */
46
- export declare class PhaseTimeoutError extends Error {
47
- readonly childName: string;
48
- readonly budgetMs: number;
49
- readonly attempts: number;
50
- constructor(childName: string, budgetMs: number, attempts: number);
51
- }
52
- /**
53
- * The terminal error for a guard kill, or null when the child was not killed.
21
+ * Why a phase child did not answer.
54
22
  *
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;
23
+ * The worker roster's kills, plus the two outcomes `worker-failure.ts`
24
+ * deliberately leaves to the consumer: a reported model error, and an empty
25
+ * answer. For a phase child both ARE failures, so they join the union here.
26
+ * `loop` carries how many strikes it took, which is what the notice reports.
27
+ */
28
+ export type ChildFailure = Exclude<WorkerFailure, {
29
+ kind: 'loop';
30
+ }> | {
31
+ kind: 'loop';
32
+ hit: LoopHit;
33
+ strikes: number;
34
+ } | {
35
+ kind: 'model-error';
36
+ cause: string;
37
+ } | {
38
+ kind: 'empty-answer';
39
+ };
62
40
  /**
63
- * The dead-backend probe killed a phase child on its LAST attempt.
41
+ * ONE error class for every way a phase child fails, carrying the cause as data.
64
42
  *
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.
43
+ * Six classes plus a string sentinel used to say the same nine things, and
44
+ * `classifyFailure` rebuilt the ladder by `instanceof` and then fell through
45
+ * to sniffing the message the tell that the vocabulary was leaking. A catch
46
+ * site asks `isFatalChildCause`; the notice switches on `failure.kind`.
69
47
  */
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);
48
+ export declare class ChildFailureError extends Error {
49
+ readonly phase: string;
50
+ readonly failure: ChildFailure;
51
+ readonly stderr: string;
52
+ constructor(phase: string, failure: ChildFailure, stderr?: string);
83
53
  }
84
54
  /**
85
55
  * Causes a best-effort `catch` must NOT absorb.
86
56
  *
87
57
  * 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.
58
+ * catches are for. A dead backend and a user cancel are different in kind: the
59
+ * run is over either way, and swallowing them ships a half-built spec while
60
+ * every later phase dies against the same dead server, or turns an ESC into
61
+ * silent progress. Which kills are fatal is the roster's column, not a list here.
93
62
  */
94
63
  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
- */
99
- export declare function isConnectionError(cause: string): boolean;
100
- /** Exponential backoff before a connection-error retry: 500ms, 1s, 2s, …, so a
101
- * brief saturation window can drain before we re-issue the request. */
102
- export declare function connectionRetryBackoffMs(attempt: number): number;
103
- export interface PhaseRunResult {
104
- text: string;
105
- exitCode: number;
106
- stderr: string;
107
- loopHit?: LoopHit;
108
- /** Set when the assistant text contains an unexecuted, leaked tool call. */
109
- leakedToolCall?: string;
110
- /** Set when the child's final turn failed with stopReason "error" (model/provider failure). */
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;
124
- }
125
- export declare function childArgs(tools: string, extensions?: readonly string[],
126
- /**
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.
135
- */
136
- groupArgs?: readonly string[]): string[];
137
- export declare const USER_CANCELLED = "__user_cancelled__";
138
- /**
139
- * Run a child pi process with JSON event-stream output, loop detection, and
140
- * context-usage tracking. This is the typed convenience wrapper used by
141
- * phase-level code.
142
- */
143
- /**
144
- * One child-pi invocation, as a value.
145
- *
146
- * WHY A RECORD RATHER THAN POSITIONALS. With this many optional parameters of
147
- * the same type, a caller reaching a late one must write bare `undefined`s to get
148
- * there, and one that miscounts silently lands the wrong value in the wrong slot.
149
- * The failure that shape produces here is a child spawned with the RAW signal
150
- * instead of the wall-clocked one, escaping a guard its siblings run under, with
151
- * nothing to catch it. Named fields make adjacent optionals of the same type
152
- * impossible to swap without a type error.
153
- */
154
- export interface ChildRun {
155
- cwd: string;
156
- /** `''` means `--no-tools`. See childArgs. */
157
- tools: string;
158
- prompt: string;
159
- signal: AbortSignal;
160
- onLine?: (line: string) => void;
161
- onContextUsage?: (snapshot: ContextSnapshot) => void;
162
- onToolCall?: (call: ToolCall) => LoopHit | null;
163
- spawn?: SpawnFn;
164
- /** Internal `-e` extension paths for in-run guards (see childArgs). */
165
- extensions?: readonly string[];
166
- /**
167
- * Every finished tool call's result text. The StallDetector's churn rule
168
- * needs the size of what actually entered the child's context, which the
169
- * CALL alone does not carry (task/stall-detector.ts).
170
- */
171
- onToolResult?: (text: string, isError: boolean) => void;
172
- /**
173
- * The child's context window in tokens. Nothing in pi's `--mode json` stream
174
- * reports one — a real capture carries token counts and a model id, but no key
175
- * naming a window — so the parent hands its own down. Children carry no `-m`
176
- * (CHILD_BASE_ARGS) and resolve the same default model, which is what makes
177
- * the parent's window the honest value. 0 or omitted = unknown.
178
- */
179
- contextWindow?: number;
180
- /**
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.
189
- */
190
- commandCeilingMs?: number;
191
- }
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>;
202
64
  export interface PhaseDeps {
203
65
  cwd: string;
204
66
  taskId: string;
@@ -206,14 +68,13 @@ export interface PhaseDeps {
206
68
  onChildOutput?: (line: string) => void;
207
69
  onContextUsage?: (snapshot: ContextSnapshot) => void;
208
70
  /**
209
- * The parent session's context window in tokens, handed down to every child.
71
+ * The parent session's context window in tokens, handed down to every child
72
+ * whose group resolved no window of its own.
210
73
  *
211
74
  * pi's `--mode json` stream reports token counts but no window, so without
212
75
  * this the gauge shows a bare number and — worse — the StallDetector's CONTEXT
213
76
  * CHURN rule can never fire: it opens with `if (this.contextWindow <= 0)
214
- * return false`. Children are spawned without `-m` (CHILD_BASE_ARGS) and so
215
- * run the parent's own default model, which is what makes its window the
216
- * honest value. Absent = unknown, and both consumers degrade.
77
+ * return false`. Absent = unknown, and both consumers degrade.
217
78
  */
218
79
  contextWindow?: number;
219
80
  /**
@@ -235,10 +96,9 @@ export interface PhaseDeps {
235
96
  */
236
97
  childExtensions?: readonly string[];
237
98
  /**
238
- * Wall-clock budget for ONE spawn of this child, in ms. Defaults to
239
- * PHASE_CHILD_TIMEOUT_MS; `0` disables the cap. Mirrors runWorker's
240
- * `timeoutMs` input, which is the same backstop one layer down
241
- * (workers/pi-worker-core.ts). Tests inject a short budget.
99
+ * Wall-clock budget for ONE spawn of this child, in ms the `phase`
100
+ * profile's `worker-timeout` row. Nothing sets it in production; the row
101
+ * says why. Tests inject a short budget.
242
102
  */
243
103
  timeoutMs?: number;
244
104
  /**
@@ -256,9 +116,9 @@ export interface PhaseDeps {
256
116
  sleepFor?: (ms: number) => Promise<void>;
257
117
  /**
258
118
  * Run ONE named Child pi and return its assistant text — the seam every phase
259
- * child goes through. Absent (production) → the real wrappers run, with the
260
- * loop detector, the wall-clock budget and the Error-triage ladder. Present →
261
- * the substitute answers directly and NONE of those guards run.
119
+ * child goes through. Absent (production) → the real loop runs, with every
120
+ * guard the `phase` profile names. Present → the substitute answers directly
121
+ * and NONE of those guards run.
262
122
  *
263
123
  * The child's NAME is the first parameter because the name is what a caller
264
124
  * branches on and what a test wants to assert. Discarded before it reaches
@@ -266,8 +126,8 @@ export interface PhaseDeps {
266
126
  * by matching prompt PROSE against prompts.ts — which makes prompt copy
267
127
  * load-bearing test infrastructure in a codebase that rewords prompts.
268
128
  *
269
- * `spawn` stays: the ladder's OWN tests must drive a real process to exercise
270
- * the rungs. This seam is for callers to whom the child is a premise.
129
+ * `spawn` stays: the loop's OWN tests drive a real process to exercise the
130
+ * rungs. This seam is for callers to whom the child is a premise.
271
131
  */
272
132
  runChild?: (name: string, tools: string, prompt: string) => Promise<string>;
273
133
  /**
@@ -316,29 +176,27 @@ export interface PhaseDeps {
316
176
  */
317
177
  export type PhaseSeams = Omit<PhaseDeps, 'cwd' | 'taskId' | 'signal' | 'onChildOutput' | 'onContextUsage' | 'contextWindow' | 'recordSubStep'>;
318
178
  /**
319
- * Run a child pi and return its assistant text. Throws if exit code != 0.
320
- *
321
- * If the child leaks a tool call as plain text (wrong dialect — never executed),
322
- * re-prompt with a correction hint up to MAX_LEAK_RETRIES times; if it keeps
323
- * leaking, throw LeakedToolCallError rather than returning the unexecuted call.
324
- * Empty completions and connection-class model errors share that same budget —
325
- * see triageChildResult, which decides every one of those cases.
326
- *
327
- * THREE RUNAWAY GUARDS ride the same budget, because this is the runner every
328
- * /task-auto planning child goes through (clarify, decompose, coverage,
329
- * contract-extract), and an unguarded planning child can burn a whole run:
330
- * • a LoopDetector, so an identical repeated tool call is killed and
331
- * re-prompted instead of being allowed to fill the context window;
332
- * • a StallDetector, the backstop for the varied-args thrash the loop
333
- * detector's short window cannot see — a child that keeps calling tools with
334
- * different arguments, learns nothing, and is never going to return. It bounds
335
- * consecutive no-new-ground calls and total context churn, NOT elapsed time;
336
- * • PHASE_CHILD_TIMEOUT_MS, a hard wall clock, OFF by default: a healthy
337
- * reasoning-on planning child and a runaway one occupy the same range of
338
- * elapsed times, so no threshold separates them. See its comment.
339
- * All three are checked BEFORE the triage ladder: we killed the child, so its
340
- * exit status describes our SIGTERM and says nothing about its verdict.
179
+ * The two things a phase child can disagree about. Everything else the
180
+ * guards, the budgets, the loop trail — is the one loop's and the `phase` row's.
341
181
  */
182
+ export interface PhaseChildOptions {
183
+ /**
184
+ * The wrapper's own word in the debug log for "we are going round again".
185
+ * The debug trail of a real run is read by a human who knows which phases
186
+ * restart and which retry.
187
+ */
188
+ verb?: 'retry' | 'restart';
189
+ /**
190
+ * When the strike budget is exhausted by loops, do NOT fail the phase. Run
191
+ * ONE final attempt with NO tools and a terminal hint ordering the model to
192
+ * emit its output from what it already has. Only safe for phases whose
193
+ * deliverable is a pure text rewrite that never strictly required a read
194
+ * (refine) — a hard-fail there kills the whole /task-auto run for a model
195
+ * that simply over-explored. Research/location phases must NOT enable this:
196
+ * their output depends on real reads, so a no-tools fallback would fabricate.
197
+ */
198
+ degradeOnExhaustion?: boolean;
199
+ }
342
200
  /**
343
201
  * The group fragment for a named child, or `[]` when the name is unmapped.
344
202
  *
@@ -350,42 +208,13 @@ export type PhaseSeams = Omit<PhaseDeps, 'cwd' | 'taskId' | 'signal' | 'onChildO
350
208
  */
351
209
  export declare function groupArgsForChild(name: string): string[];
352
210
  export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: string, prompt: string, opts?: PhaseChildOptions): Promise<string>;
353
- export declare function formatLoopHint(hit: LoopHit): string;
354
211
  /**
355
212
  * Terminal hint for the degrade attempt: the model has thrashed through the whole
356
213
  * strike budget re-reading files without converging, so we strip its tools and
357
- * order it to emit the deliverable NOW from what it already has. Used only by
358
- * read-only analysis phases (refine) whose output is a text rewrite that never
359
- * strictly required a successful read — far better to ship a best-effort spec
360
- * than to hard-fail the whole /task-auto run. See countRevisits / LoopExhausted.
214
+ * order it to emit the deliverable NOW from what it already has.
361
215
  */
362
216
  export declare function formatDegradeHint(hit: LoopHit): string;
363
217
  export declare function prependHint(hint: string | null, prompt: string): string;
364
- /**
365
- * The two things a phase child can disagree about. Everything else — the loop
366
- * and stall detectors, the wall clock, the loop trail, the triage ladder and its
367
- * budget — is the one loop's. These two are the only differences observable from
368
- * outside it.
369
- */
370
- export interface PhaseChildOptions {
371
- /**
372
- * The wrapper's own word in the debug log for "we are going round again".
373
- * An option rather than one word because it is the single externally visible
374
- * difference between the two loops this collapsed, and the debug trail of a
375
- * real run is read by a human who knows which phases restart and which retry.
376
- */
377
- verb?: 'retry' | 'restart';
378
- /**
379
- * When the strike budget is exhausted by loops, do NOT fail the phase. Run
380
- * ONE final attempt with NO tools and a terminal hint ordering the model to
381
- * emit its output from what it already has. Only safe for phases whose
382
- * deliverable is a pure text rewrite that never strictly required a read
383
- * (refine) — a hard-fail there kills the whole /task-auto run for a model
384
- * that simply over-explored. Research/location phases must NOT enable this:
385
- * their output depends on real reads, so a no-tools fallback would fabricate.
386
- */
387
- degradeOnExhaustion?: boolean;
388
- }
389
218
  /**
390
219
  * Run a child up to twice; the second attempt gets `emphasized=true` to escalate
391
220
  * the prompt. On success, return the validator's value; on two failures, throw
@@ -398,34 +227,3 @@ export declare function runWithEmphasisRetry<T>(deps: PhaseDeps, name: string, t
398
227
  ok: false;
399
228
  problem: string;
400
229
  }, onFail: (problem: string) => Error): Promise<T>;
401
- export declare class LoopExhaustedError extends Error {
402
- readonly phase: string;
403
- readonly history: LoopHit[];
404
- constructor(phase: string, history: LoopHit[]);
405
- }
406
- /**
407
- * Thrown when a phase child's final turn failed with stopReason "error" — the
408
- * model/provider died (local model disconnect, fetch failed, socket hang up,
409
- * provider 5xx) after pi exhausted its own internal retries. pi reports this as
410
- * an agent_end with empty assistant text, which would otherwise surface as the
411
- * misleading "produced no output"; this names the real cause instead.
412
- *
413
- * Fail-fast: not retried at the pi-task layer. pi already retried the retryable
414
- * cases; re-spawning a fresh child against the same dead endpoint only burns
415
- * time and buries the real error. Restart the model/provider, then resume.
416
- */
417
- export declare class ModelError extends Error {
418
- readonly phase: string;
419
- readonly cause: string;
420
- constructor(phase: string, cause: string);
421
- }
422
- /**
423
- * Thrown when a phase child repeatedly wrote a tool call as plain text (a markup
424
- * dialect pi's harness didn't parse) instead of invoking it. The call never ran,
425
- * so the phase output is untrustworthy — fail loudly rather than check it off.
426
- */
427
- export declare class LeakedToolCallError extends Error {
428
- readonly phase: string;
429
- readonly marker: string;
430
- constructor(phase: string, marker: string);
431
- }