@mjasnikovs/pi-task 0.38.11 → 0.38.12

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 (55) hide show
  1. package/README.md +7 -3
  2. package/dist/shared/child-process.d.ts +8 -0
  3. package/dist/shared/command-watchdog.d.ts +1 -1
  4. package/dist/shared/command-watchdog.js +1 -1
  5. package/dist/task/accept-debt.d.ts +47 -0
  6. package/dist/task/accept-debt.js +127 -28
  7. package/dist/task/auto-orchestrator.js +91 -114
  8. package/dist/task/child-runner.d.ts +39 -25
  9. package/dist/task/child-runner.js +59 -31
  10. package/dist/task/child-status.d.ts +95 -0
  11. package/dist/task/child-status.js +99 -0
  12. package/dist/task/command-run.d.ts +36 -0
  13. package/dist/task/command-run.js +48 -1
  14. package/dist/task/command-watchdog.js +1 -1
  15. package/dist/task/context-usage.d.ts +4 -3
  16. package/dist/task/context-usage.js +4 -3
  17. package/dist/task/contracts.js +18 -35
  18. package/dist/task/deep-render-check.d.ts +47 -0
  19. package/dist/task/deep-render-check.js +110 -65
  20. package/dist/task/env-notes.d.ts +3 -3
  21. package/dist/task/env-notes.js +24 -35
  22. package/dist/task/final-gate-fix.d.ts +1 -1
  23. package/dist/task/final-gate-fix.js +1 -1
  24. package/dist/task/final-gate.d.ts +5 -151
  25. package/dist/task/final-gate.js +81 -379
  26. package/dist/task/gate-child.d.ts +8 -10
  27. package/dist/task/gate-child.js +15 -19
  28. package/dist/task/gate-deps.d.ts +29 -0
  29. package/dist/task/gate-deps.js +192 -206
  30. package/dist/task/gate-tally.d.ts +189 -0
  31. package/dist/task/gate-tally.js +249 -0
  32. package/dist/task/implementation-turn.d.ts +201 -0
  33. package/dist/task/implementation-turn.js +263 -0
  34. package/dist/task/launch-contract.js +27 -43
  35. package/dist/task/ledger.d.ts +38 -0
  36. package/dist/task/ledger.js +83 -0
  37. package/dist/task/loop-detector.d.ts +14 -8
  38. package/dist/task/loop-detector.js +36 -12
  39. package/dist/task/orchestrator.d.ts +61 -126
  40. package/dist/task/orchestrator.js +67 -294
  41. package/dist/task/plan-orchestrator.js +34 -33
  42. package/dist/task/requirements.d.ts +1 -1
  43. package/dist/task/requirements.js +50 -66
  44. package/dist/task/root-cause-repair.js +20 -32
  45. package/dist/task/run-bracket.d.ts +75 -0
  46. package/dist/task/run-bracket.js +41 -0
  47. package/dist/task/stall-detector.d.ts +110 -0
  48. package/dist/task/stall-detector.js +159 -0
  49. package/dist/task/verify-work.d.ts +53 -67
  50. package/dist/task/verify-work.js +15 -11
  51. package/dist/workers/single-read-extension.d.ts +1 -1
  52. package/dist/workers/single-read-extension.js +5 -4
  53. package/dist/workers/single-read-guard.d.ts +32 -10
  54. package/dist/workers/single-read-guard.js +67 -16
  55. package/package.json +1 -1
@@ -19,7 +19,58 @@ import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-c
19
19
  import { type WidgetState } from './widget.js';
20
20
  import { type RunTaskFn } from './gate-deps.js';
21
21
  import { type GateDeps } from './task-gates.js';
22
+ import { type PhaseDeps } from './child-runner.js';
22
23
  import type { SpawnFn } from '../shared/child-process.js';
24
+ import { type SuperviseOptions } from './implementation-turn.js';
25
+ /**
26
+ * Everything one TaskRunner needs, as one object. The runner is the shared core
27
+ * under `runSingleTask` (and so under /task-auto's per-task loop), so this is the
28
+ * shape both of those construct; `RunSingleTaskOptions` extends the injectable
29
+ * subset and adds what only the wrapper reads.
30
+ */
31
+ export interface TaskRunnerOptions {
32
+ ctx: ExtensionCommandContext;
33
+ cwd: string;
34
+ rawPrompt: string;
35
+ /** Resume an existing task by ID instead of starting a new one. */
36
+ resumeId?: string;
37
+ /** Deliver the finished spec to the main session. Absent → nothing is sent. */
38
+ sendSpec?: (spec: string) => Promise<void>;
39
+ /** Test seam: spawn function forwarded into `PhaseDeps.spawn`. Keep for tests
40
+ * that drive the Error-triage ladder or a real process. */
41
+ spawnFn?: SpawnFn;
42
+ /**
43
+ * Test seam: `PhaseDeps.runChild(name, tools, prompt)`. Present → every phase
44
+ * child is answered by name, with none of the ladder's guards. Use this when
45
+ * the child is a premise of the test, not its subject.
46
+ */
47
+ runChild?: PhaseDeps['runChild'];
48
+ /** Called with the resolved task id once its file exists, before any phase
49
+ * work. Lets callers record the id (e.g. stamp the /task-auto entry) so an
50
+ * interrupted run can be resumed instead of restarted. */
51
+ onStart?: (taskId: string) => void | Promise<void>;
52
+ /**
53
+ * Scope fence naming the sibling steps of a /task-auto plan. Forwarded into
54
+ * the refine phase so a single decomposed step bounds its slice instead of
55
+ * re-expanding the whole referenced spec doc. Set only by /task-auto's loop;
56
+ * a bare /task leaves it undefined and the refine prompt is unchanged.
57
+ */
58
+ planContext?: string;
59
+ /**
60
+ * Marks this run as a verify-FAIL re-attempt. When set (only by /task-auto's
61
+ * autofix path, with `resumeId` pointing at the already-composed task), the
62
+ * text — the verify gate's failure reason plus any guidance the user typed —
63
+ * is prepended to the delivered spec as a RE-ATTEMPT banner, so the
64
+ * implementer fixes the specific failure and re-satisfies the VERIFY block
65
+ * rather than blindly redoing the task. Empty/undefined on a first attempt.
66
+ */
67
+ fixInstruction?: string;
68
+ /** True when the caller awaits the implementation turn (waitForImplementation):
69
+ * the impl widget stays armed across the whole impl phase (incl. compaction /
70
+ * steer turns) and is disarmed here. False for fire-and-forget /task, where the
71
+ * widget is armed one-shot and its own agent_end disarms it. */
72
+ implAwaited?: boolean;
73
+ }
23
74
  /** Encapsulates the full lifecycle of a single pi-task run. */
24
75
  export declare class TaskRunner {
25
76
  private readonly _ctx;
@@ -30,10 +81,7 @@ export declare class TaskRunner {
30
81
  private readonly _onStart;
31
82
  private readonly _planContext;
32
83
  private readonly _fixInstruction;
33
- /** True when the caller awaits the implementation turn (waitForImplementation):
34
- * the impl widget stays armed across the whole impl phase (incl. compaction /
35
- * steer turns) and is disarmed here. False for fire-and-forget /task, where the
36
- * widget is armed one-shot and its own agent_end disarms it. */
84
+ /** See {@link TaskRunnerOptions.implAwaited}. */
37
85
  private readonly _implAwaited;
38
86
  private readonly _abort;
39
87
  private readonly _startedAt;
@@ -50,15 +98,20 @@ export declare class TaskRunner {
50
98
  */
51
99
  private readonly _timings;
52
100
  private _currentPhaseChildren;
53
- constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string, fixInstruction?: string, implAwaited?: boolean);
101
+ constructor(opts: TaskRunnerOptions);
54
102
  get taskId(): string;
55
103
  get signal(): AbortSignal;
56
104
  /** Return the current widget state, or null if not started. */
57
105
  status(): WidgetState | null;
58
106
  /** Cancel the running task by aborting the signal. */
59
107
  cancel(): void;
60
- /** Execute the full task lifecycle. */
108
+ /** Execute the full task lifecycle. Mid-run input holds instead of starting
109
+ * a competing turn for the whole of it, and the terminal interception is
110
+ * armed for the same window (`withRun`); nested inside `runGatedTask` or
111
+ * the `/task-auto` loop the bracket refcounts, so this changes nothing there
112
+ * and covers the fire-and-forget `runSingleTask` path on its own. */
61
113
  run(): Promise<void>;
114
+ private _run;
62
115
  /** Stop the phase widget — clearing both the terminal and remote surfaces —
63
116
  * exactly once. Nulling the disposer makes repeat calls no-ops, so the
64
117
  * failure flash that handleFailure sets after the catch isn't wiped by the
@@ -76,18 +129,10 @@ export declare class TaskRunner {
76
129
  */
77
130
  private _specForDelivery;
78
131
  }
79
- export interface RunSingleTaskOptions {
132
+ export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId' | 'spawnFn' | 'runChild' | 'onStart' | 'planContext' | 'fixInstruction'> {
80
133
  /** Await the session going idle after the spec is delivered, so the caller
81
134
  * blocks until the agent has implemented it. Default false. */
82
135
  waitForImplementation?: boolean;
83
- /** Resume an existing task by ID instead of starting a new one. */
84
- resumeId?: string;
85
- /** Test seam: spawn function forwarded to TaskRunner. */
86
- spawnFn?: SpawnFn;
87
- /** Called with the resolved task id once its file exists, before any phase
88
- * work. Lets callers record the id (e.g. stamp the /task-auto entry) so an
89
- * interrupted run can be resumed instead of restarted. */
90
- onStart?: (taskId: string) => void | Promise<void>;
91
136
  /**
92
137
  * Ask the user for a steering message after they interrupt (ESC) the
93
138
  * implementation turn. Return text to continue the same task as another turn,
@@ -96,7 +141,7 @@ export interface RunSingleTaskOptions {
96
141
  * raced against a remote browser card); injectable so the steer loop is
97
142
  * testable without a real dialog.
98
143
  */
99
- promptSteer?: (ctx: ExtensionCommandContext) => Promise<string | undefined>;
144
+ promptSteer?: SuperviseOptions['promptSteer'];
100
145
  /**
101
146
  * Push a "Task finished" notification to subscribed devices when this run
102
147
  * reaches a terminal state (completed / failed / cancelled). Set only by the
@@ -104,33 +149,7 @@ export interface RunSingleTaskOptions {
104
149
  * internal per-task runs, which must stay silent. Default false.
105
150
  */
106
151
  notifyFinish?: boolean;
107
- /**
108
- * Scope fence naming the sibling steps of a /task-auto plan. Forwarded into
109
- * the refine phase so a single decomposed step bounds its slice instead of
110
- * re-expanding the whole referenced spec doc. Set only by /task-auto's loop;
111
- * a bare /task leaves it undefined and the refine prompt is unchanged.
112
- */
113
- planContext?: string;
114
- /**
115
- * Marks this run as a verify-FAIL re-attempt. When set (only by /task-auto's
116
- * autofix path, with `resumeId` pointing at the already-composed task), the
117
- * text — the verify gate's failure reason plus any guidance the user typed —
118
- * is prepended to the delivered spec as a RE-ATTEMPT banner, so the
119
- * implementer fixes the specific failure and re-satisfies the VERIFY block
120
- * rather than blindly redoing the task. Empty/undefined on a first attempt.
121
- */
122
- fixInstruction?: string;
123
152
  }
124
- /**
125
- * The slice of the replacement-session context the steer loop needs.
126
- * `sendUserMessage` lives on ReplacedSessionContext (not the base command ctx,
127
- * and not re-exported from the package), so we narrow to just what we call.
128
- */
129
- export type SteerCtx = ExtensionCommandContext & {
130
- sendUserMessage(content: string, options?: {
131
- deliverAs?: 'steer' | 'followUp';
132
- }): Promise<void>;
133
- };
134
153
  export interface RunSingleTaskResult {
135
154
  taskId: string;
136
155
  ok: boolean;
@@ -163,90 +182,6 @@ export interface RunSingleTaskResult {
163
182
  */
164
183
  reason?: string;
165
184
  }
166
- /**
167
- * True when the implementation turn went idle right after a context compaction —
168
- * the most recent entry in the branch is a `compaction` boundary sitting after the
169
- * last assistant message.
170
- *
171
- * A *threshold* auto-compaction (the runtime's "context is getting large" path)
172
- * compacts and then deliberately does NOT auto-continue: it returns to idle and
173
- * expects a manual continue (`_runAutoCompaction("threshold", false)` →
174
- * `hasQueuedMessages()` is false → the agent loop stops). Our implementation wait
175
- * resolves at exactly that idle. Without this check it reads as "the model
176
- * finished" (the last assistant message is a normal `stop`, not `aborted`/`error`),
177
- * so the run jumps straight to the verify gate and abandons a half-done task at the
178
- * compaction boundary — the failure this detector closes.
179
- *
180
- * Position-based, not timestamp-based: the runtime APPENDS the compaction entry to
181
- * the tail of the branch after the assistant message that triggered it
182
- * (`appendCompaction` → `_appendEntry` push), so a `compaction` after the last
183
- * assistant message means we are parked on a compaction with no continuation. A
184
- * genuinely finished turn ends on an assistant message with no trailing compaction;
185
- * an *overflow* compaction self-retries, so it never leaves us idle here.
186
- */
187
- export declare function endedAtCompactionBoundary(ctx: ExtensionCommandContext): boolean;
188
- /**
189
- * Nudge that resumes an implementation turn the runtime parked at a compaction
190
- * boundary. It must let a turn that was genuinely finished (then tipped over the
191
- * threshold by its own final message) confirm completion without inventing busywork
192
- * — we cannot tell "paused mid-task by compaction" from "finished, then compacted"
193
- * from the boundary alone, so the wording lets a done turn end in one line.
194
- */
195
- export declare const CONTINUE_AFTER_COMPACTION: string;
196
- /**
197
- * Safety cap on compaction-driven resumes for a single implementation turn. Each
198
- * resume follows a real compaction (which only fires after the model produced a
199
- * turn large enough to cross the threshold), so a legitimately large task may
200
- * resume a handful of times; the cap exists only to stop a pathological loop from
201
- * auto-sending forever with no user in the loop. Hitting it stops resuming and lets
202
- * the verify gate / `/task-auto-resume` catch any leftover incompleteness.
203
- */
204
- export declare const MAX_COMPACTION_RESUMES = 20;
205
- /**
206
- * Resume an implementation turn that went idle at a threshold-compaction boundary.
207
- * The runtime compacts and parks at idle without auto-continuing; we send a
208
- * continue and wait again, repeating across successive compactions until the turn
209
- * ends on a real assistant message (genuine completion). A user ESC takes priority
210
- * (it is not a compaction boundary, and `wasInterrupted` guards the loop so the
211
- * steer loop handles it), and the safety cap bounds a runaway. Returns the number
212
- * of resumes performed (0 when the turn did not end on a compaction).
213
- */
214
- export declare function resumeAcrossCompactions(ctx: SteerCtx): Promise<number>;
215
- /**
216
- * Timing knobs for the watchdog-abort guard in {@link steerUntilDone}, injectable
217
- * so tests exercise the grace expiry without a 10-second wait. `graceMs` bounds
218
- * how long the loop waits for the watchdog's follow-up to be DELIVERED (not to
219
- * finish — its turn may legitimately run for minutes afterwards); delivery is
220
- * normally near-instant, so the grace only expires on a stale flag.
221
- */
222
- export interface SteerWatchdogDeps {
223
- consume: () => boolean;
224
- graceMs: number;
225
- pollMs: number;
226
- }
227
- /**
228
- * After the implementation turn settles, honour a user ESC by letting them steer.
229
- *
230
- * `waitForIdle` resolves both on natural completion AND on an ESC (which aborts
231
- * the turn → idle). When the last turn was aborted, the host's main input loop is
232
- * blocked inside our command handler, so a message typed in the editor would only
233
- * queue, never run (interactive-mode routes idle input through onInputCallback,
234
- * which is unset while we hold the loop). We therefore solicit the steering text
235
- * ourselves and feed it back as another turn via sendUserMessage — which runs to
236
- * completion when the session is idle. Repeat until a turn finishes uninterrupted.
237
- *
238
- * A WATCHDOG abort also ends the turn with stopReason 'aborted' — indistinguishable
239
- * from a human ESC by the session entries alone at that instant. The watchdog
240
- * queues its own recovery follow-up, so prompting there would show a steering
241
- * dialog to an empty room and wedge an unattended run on the race. The one-shot
242
- * flag (set synchronously before the abort) routes that case to
243
- * {@link awaitWatchdogFollowUp} instead; a stale flag degrades to a bounded wait
244
- * followed by the ordinary prompt, never to a suppressed one.
245
- *
246
- * Returns true when the user declined to steer (empty/cancelled) and the run
247
- * should pause; false when the implementation completed (steered or not).
248
- */
249
- export declare function steerUntilDone(ctx: SteerCtx, promptSteer?: (ctx: ExtensionCommandContext) => Promise<string | undefined>, watchdog?: Partial<SteerWatchdogDeps>): Promise<boolean>;
250
185
  /**
251
186
  * Run one prompt through the full single-task pipeline in a fresh session and
252
187
  * deliver its spec. With waitForImplementation, block until the agent finishes