@ordewell/core 0.10.1 → 0.10.3

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 (45) hide show
  1. package/dist/{IFileSystem-Bj6fy1cG.d.mts → IFileSystem-B8Nkgg4e.d.mts} +1 -1
  2. package/dist/{IFileSystem-BCJBHFwB.d.ts → IFileSystem-DCd-rkeH.d.ts} +1 -1
  3. package/dist/{IRunner-hWAJC4hg.d.mts → IRunner-CXdYyIZo.d.mts} +213 -202
  4. package/dist/{IRunner-C7_SSpzG.d.ts → IRunner-QBdYcatb.d.ts} +213 -202
  5. package/dist/{ModeResolver-m1d8Cb-j.d.mts → ModeResolver-BOU90Nn-.d.ts} +1 -1
  6. package/dist/{ModeResolver-u5ZhRC8x.d.ts → ModeResolver-geqjKkLY.d.mts} +1 -1
  7. package/dist/{Task-CxwnCJLi.d.mts → Task-CRVIqSfy.d.mts} +58 -11
  8. package/dist/{Task-CxwnCJLi.d.ts → Task-CRVIqSfy.d.ts} +58 -11
  9. package/dist/{chunk-DNFT4SQG.mjs → chunk-645FALZX.mjs} +5 -5
  10. package/dist/chunk-645FALZX.mjs.map +1 -0
  11. package/dist/{chunk-DJAPDO6E.mjs → chunk-A57ITQE7.mjs} +41 -21
  12. package/dist/chunk-A57ITQE7.mjs.map +1 -0
  13. package/dist/{chunk-CH5IGWFS.mjs → chunk-FH6M5QZG.mjs} +168 -122
  14. package/dist/chunk-FH6M5QZG.mjs.map +1 -0
  15. package/dist/{chunk-2IIRSBEQ.mjs → chunk-SM6BV6XS.mjs} +20 -4
  16. package/dist/chunk-SM6BV6XS.mjs.map +1 -0
  17. package/dist/index.d.mts +454 -63
  18. package/dist/index.d.ts +454 -63
  19. package/dist/index.js +19575 -18631
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +17991 -17137
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/parsing.d.mts +2 -2
  24. package/dist/parsing.d.ts +2 -2
  25. package/dist/parsing.js +15 -14
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/{plan-utils-CKXdD1AD.d.mts → plan-utils-KU1Rse82.d.mts} +1322 -1146
  29. package/dist/{plan-utils-B6yJdpgi.d.ts → plan-utils-oC8bO4pN.d.ts} +1322 -1146
  30. package/dist/plan-utils.d.mts +4 -4
  31. package/dist/plan-utils.d.ts +4 -4
  32. package/dist/plan-utils.js +60 -0
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +11 -3
  35. package/dist/testing.d.mts +3 -3
  36. package/dist/testing.d.ts +3 -3
  37. package/dist/testing.js +3 -1
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +4 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +5 -1
  42. package/dist/chunk-2IIRSBEQ.mjs.map +0 -1
  43. package/dist/chunk-CH5IGWFS.mjs.map +0 -1
  44. package/dist/chunk-DJAPDO6E.mjs.map +0 -1
  45. package/dist/chunk-DNFT4SQG.mjs.map +0 -1
@@ -1,6 +1,17 @@
1
- import { $ as RunnerId, c as DiscoveredModel, ac as Task, a9 as SkillsService, a4 as SkillInfo, U as RepoGroupLayout, aj as TaskSkillSnapshot, ai as TaskSkillNotice, aa as SubagentOutcome, ao as UsageRecord, ak as TaskSnapshot, a as AwaitingReason, I as IWorktreeIsolation, v as IsolationRun, j as IsolationHandoff, H as PlanIsolation, w as IsolationTaskRecord, af as TaskIsolation, F as IsolationView, r as IsolationOutcome, T as RepairEvidence, am as TreeSnapshot, ad as TaskDiffRead, ae as TaskDiffResult, q as IsolationMergeResult, aq as Verdict, _ as RunSettings, L as LegacyPlanState, C as ConversationMessage, a6 as SkillLoadNotice, P as PendingPlanTurn, A as AttemptUsage, M as ModelUsage, ap as UsageTotals, N as PlanUsage, e as ExecutionRecord, X as ResearchStep, J as PlanState, ab as SupervisorLevel, Y as ResearchStepOutcome, V as ResearchLogEntry, O as PlannerUsage, ag as TaskIsolationState, an as UsageLine } from './Task-CxwnCJLi.js';
2
- import { R as RunnerModeInfo } from './ModeResolver-u5ZhRC8x.js';
3
- import { B as IRunnerSession, aB as TaskMessageFrom, ah as RunnerCommand, am as RunnerMcpServer, au as StructuredTurnEnd, j as ApprovalDecision, at as StructuredEvent, y as IConfig, z as IRunner, ap as RunnerRegistry, aa as QueuedTaskMessage, k as ApprovalKind, q as ApprovalSource } from './IRunner-C7_SSpzG.js';
1
+ import { af as Task, a2 as RunnerId, an as TaskSnapshot, b as AwaitingReason, ar as UsageRecord, d as DiscoveredModel, ac as SkillsService, a7 as SkillInfo, X as RepoGroupLayout, I as IWorktreeIsolation, w as IsolationRun, k as IsolationHandoff, J as PlanIsolation, x as IsolationTaskRecord, ai as TaskIsolation, H as IsolationView, R as PrepareIntent, U as PreviousWork, s as IsolationOutcome, W as RepairEvidence, ap as TreeSnapshot, ag as TaskDiffRead, ah as TaskDiffResult, r as IsolationMergeResult, at as Verdict, am as TaskSkillSnapshot, al as TaskSkillNotice, ad as SubagentOutcome, L as LegacyPlanState, C as ConversationMessage, a9 as SkillLoadNotice, P as PendingPlanTurn, a as AttemptUsage, M as ModelUsage, as as UsageTotals, O as PlanUsage, f as ExecutionRecord, _ as ResearchStep, a1 as RunSettings, K as PlanState, ae as SupervisorLevel, $ as ResearchStepOutcome, Y as ResearchLogEntry, Q as PlannerUsage, aj as TaskIsolationState, aq as UsageLine } from './Task-CRVIqSfy.js';
2
+ import { R as RunnerModeInfo } from './ModeResolver-BOU90Nn-.js';
3
+ import { B as IRunnerSession, y as IConfig, aC as TaskMessageFrom, ai as RunnerCommand, an as RunnerMcpServer, av as StructuredTurnEnd, j as ApprovalDecision, au as StructuredEvent, ab as QueuedTaskMessage, k as ApprovalKind, q as ApprovalSource, z as IRunner, aq as RunnerRegistry } from './IRunner-QBdYcatb.js';
4
+
5
+ type NotificationAction = {
6
+ label: string;
7
+ value: string;
8
+ };
9
+ interface INotification {
10
+ info(msg: string): void;
11
+ warn(msg: string): void;
12
+ error(msg: string): void;
13
+ confirm(msg: string, options: string[]): Promise<string | undefined>;
14
+ }
4
15
 
5
16
  interface LiveTailOptions {
6
17
  maxLines: number;
@@ -33,6 +44,126 @@ interface TaskOutputSource {
33
44
  liveTail(taskId: string, opts: LiveTailOptions): LiveTail | null;
34
45
  }
35
46
 
47
+ /**
48
+ * The deep module owning all plan-shaped state. Holds `planTasks` (the ordered
49
+ * tree the user edits), the flattened `allTasks` view and the `taskMap` index.
50
+ * The orchestrator calls `markCompleted`/`markFailed`/`markInProgress`/`retry`
51
+ * to update task status — it never mutates task state directly.
52
+ *
53
+ * Completion and failure are read from `task.status` and nothing else, so
54
+ * `isCompleted`, `completedCount`, `isAllComplete` and the scheduler's
55
+ * dependency checks cannot disagree. The store owns its task objects: `load`
56
+ * copies what it is given, the getters hand out readonly views, and
57
+ * `snapshot` is the copy a caller may keep or write to disk.
58
+ *
59
+ * `rebuild` is the internal seam that keeps the flat views in sync with the
60
+ * tree.
61
+ *
62
+ * `planRunners` lives here because it's part of the plan's identity (the runner
63
+ * set, carried on the plan). `validateAssignedRunners` is pure store logic.
64
+ */
65
+ declare class PlanStore {
66
+ private _planTasks;
67
+ private _allTasks;
68
+ private _taskMap;
69
+ /** Task id → the top-level task it hangs under, itself for a top-level task. */
70
+ private _rootMap;
71
+ private _planRunners;
72
+ private _onMutate;
73
+ private _onTree;
74
+ private _executionLog;
75
+ /** Hook called after every structural mutation (add/remove/update/resetForRun). */
76
+ set onMutate(cb: (() => void) | null);
77
+ /**
78
+ * Called with every task id, subtasks included, each time the tree is
79
+ * rebuilt — on every route a task enters or leaves the plan, loads and
80
+ * reconciled edits included, which `onMutate` does not hear.
81
+ */
82
+ set onTree(cb: ((ids: ReadonlySet<string>) => void) | null);
83
+ get planTasks(): ReadonlyArray<Readonly<Task>>;
84
+ get allTasks(): ReadonlyArray<Readonly<Task>>;
85
+ get planRunners(): RunnerId[];
86
+ get completedCount(): number;
87
+ get failedCount(): number;
88
+ isAllComplete(): boolean;
89
+ isAnyFailed(): boolean;
90
+ isCompleted(id: string): boolean;
91
+ isSkipped(id: string): boolean;
92
+ /** Nothing is left to do: every task completed or was skipped. */
93
+ isFinished(): boolean;
94
+ isFailed(id: string): boolean;
95
+ get(taskId: string): Readonly<Task> | undefined;
96
+ /**
97
+ * Whether a task runs as an ops task (ADR-0020). Only a top-level AI task
98
+ * can be one; a subtask runs with the top-level task it hangs under.
99
+ */
100
+ isOps(taskId: string): boolean;
101
+ /** A copy of the task tree, detached from the store: later status changes do not reach it. */
102
+ snapshot(): Task[];
103
+ getExecutionLog(): ReadonlyArray<TaskSnapshot>;
104
+ appendToLog(snapshot: TaskSnapshot): void;
105
+ /**
106
+ * Drop a task's archived snapshot. Un-marking a completion has to erase the
107
+ * "finished" record too — dependent tasks are prompted from the log, so a
108
+ * left-behind snapshot would keep feeding them a result that no longer exists.
109
+ */
110
+ removeFromLog(taskId: string): void;
111
+ clearLog(): void;
112
+ private notifyMutate;
113
+ private countStatus;
114
+ load(tasks: ReadonlyArray<Readonly<Task>>, runners: readonly RunnerId[]): void;
115
+ add(partial: Partial<Task>): Task;
116
+ remove(taskId: string): void;
117
+ update(taskId: string, changes: Partial<Task>): Task | undefined;
118
+ /**
119
+ * Run preparation as one named op: flip every AI task to 'approved'. By
120
+ * default completed tasks keep their status so a reloaded half-finished plan
121
+ * resumes the remainder instead of re-running work that already succeeded,
122
+ * and skipped ones stay skipped: a new run is not a retry of them.
123
+ * `preserveCompleted: false` is for a freshly committed plan, where
124
+ * everything starts over. `keepScheduledResumes` leaves a task waiting on
125
+ * its runner's usage-limit reset waiting: an edit adopted between runs is
126
+ * not the user starting over.
127
+ */
128
+ resetForRun(opts?: {
129
+ preserveCompleted?: boolean;
130
+ keepScheduledResumes?: boolean;
131
+ }): void;
132
+ markCompleted(id: string): void;
133
+ markFailed(id: string): void;
134
+ markSkipped(id: string, reason?: string): void;
135
+ markInProgress(id: string): void;
136
+ markAwaitingUser(id: string, reason?: AwaitingReason): void;
137
+ /** When a task paused on a usage limit resumes on its own; undefined leaves it to a manual retry. */
138
+ setTaskResumeAt(id: string, resumeAt: string | undefined): void;
139
+ markPending(id: string): void;
140
+ retry(id: string): void;
141
+ /** Kept on the task: it was force-started past its merge gate, before these ancestors' work was merged (ADR-0020). */
142
+ setForcedPastGate(id: string, dependencies: string[]): void;
143
+ /** A reason outlives nothing: any status but `awaiting_user` drops it, and the resume time with it; any status drops a skip's reason. */
144
+ private setStatus;
145
+ blockDependents(id: string): void;
146
+ unblockDependents(id: string): void;
147
+ setTaskVerdict(id: string, verdict: Task['verdict']): void;
148
+ setTaskOutputSummary(id: string, summary: Task['outputSummary']): void;
149
+ setTaskRunnerSessionId(id: string, runnerSessionId: string | undefined): void;
150
+ clearCarriedFrom(id: string): void;
151
+ startAttemptUsage(id: string, attempt: number, runner: string, startedAt: string): void;
152
+ recordAttemptUsage(id: string, attempt: number, record: UsageRecord, now: string): void;
153
+ endAttemptUsage(id: string, attempt: number, now: string): void;
154
+ setTaskAttemptSkills(id: string, skills: Task['attemptSkills']): void;
155
+ /**
156
+ * Widen the plan's runner set. Retargeting a task onto a runner the plan has
157
+ * not used before has to land here as well as on the plan state, or
158
+ * {@link resolveTaskRunner} reads the new runner as foreign and spawns the
159
+ * plan's first one instead.
160
+ */
161
+ admitRunner(runner: RunnerId): void;
162
+ resolveTaskRunner(task: Readonly<Task>): RunnerId;
163
+ private rebuild;
164
+ private validateAssignedRunners;
165
+ }
166
+
36
167
  /**
37
168
  * The catalog a model/task-mode edit is checked against — the same discovered
38
169
  * models and manifest modes the planner was shown in the per-turn catalog
@@ -100,6 +231,8 @@ interface ApplyTaskOpsResult {
100
231
  errors: string[];
101
232
  /** Human-readable summary of what changed, for the chat transcript. */
102
233
  summary: string[];
234
+ /** On ok, the id of the task each batch handle names. */
235
+ handles?: ReadonlyMap<string, string>;
103
236
  }
104
237
  /**
105
238
  * Pre-flight check for a merge: would collapsing the selected tasks into one
@@ -200,450 +333,119 @@ interface PlannerModes {
200
333
  taskSkills?: SkillLookup;
201
334
  }
202
335
 
203
- /**
204
- * One entry of a structured task's log (ADR-0018, P1): what a runner did,
205
- * normalized and serializable, streamed live as `task_log` and appended to
206
- * the attempt's `.jsonl` file. The live view and the reload fold the same
207
- * events through `reduceTaskLog`, so the two cannot disagree.
208
- *
209
- * Readers skip a type they do not know, which is how later events join
210
- * without a format version.
211
- */
212
- type TaskLogEvent =
213
- /**
214
- * The task skills this attempt's prompt carried, as read at its spawn
215
- * (ADR-0024): first in its log. The file keeps their bodies; a surface is
216
- * sent notices ({@link taskLogForSurface}).
217
- */
218
- {
219
- type: 'task_skills';
220
- skills: TaskSkillSnapshot[] | TaskSkillNotice[];
336
+ type IsolationNoticeLevel = 'info' | 'warn' | 'error';
337
+ /** How an attempt that may run in a worktree gets one: fresh, the kept one for a repair, or restoring the previous attempt's work. */
338
+ type AttemptIntent = 'fresh' | 'repair' | PrepareIntent;
339
+ interface AttemptWorkspace {
340
+ cwd: string;
341
+ worktree: boolean;
342
+ /** Set for a `restore` in a worktree; absent in the checkout, where the previous attempt's edits remain. */
343
+ previousWork?: PreviousWork;
221
344
  }
222
345
  /**
223
- * A turn began by delivering `message`; `messageId` names it when it had been
224
- * queued, `forced` when it was force sent. `from` on this and the other
225
- * message events marks one Ordewell sent rather than the user.
346
+ * What the controller reports. It never schedules or emits on its own: the
347
+ * orchestrator turns these into observer events and decides what runs next.
226
348
  */
227
- | {
228
- type: 'turn_start';
229
- message: string;
230
- messageId?: string;
231
- forced?: boolean;
232
- from?: TaskMessageFrom;
349
+ interface IsolationRunListener {
350
+ /**
351
+ * The run record changed and should be persisted with the plan. `detached`
352
+ * is set instead when the record that changed is no longer the plan's own —
353
+ * a release that outlived the plan load that replaced its run — and belongs
354
+ * to the session that was left.
355
+ */
356
+ changed(detached?: IsolationRun): void;
357
+ /** A run did not start: `repos` of the group have tracked changes. */
358
+ blocked(repos: string[]): void;
359
+ /** An isolated run closed and handed its integration branches over. */
360
+ handoff(handoff: IsolationHandoff): void;
361
+ /** Already said through the notifications; also for a surface that shows no toasts. */
362
+ notice(level: IsolationNoticeLevel, message: string): void;
363
+ }
364
+ interface IsolationRunControllerDeps {
365
+ isolation: IWorktreeIsolation;
366
+ config: IConfig;
367
+ notifications: INotification;
368
+ workspaceRoot: () => string;
369
+ listener: IsolationRunListener;
370
+ /**
371
+ * The tasks no clean-up of the whole run may take a worktree from: an
372
+ * attempt is running them, or the plan has them in progress or waiting on
373
+ * the user mid-attempt.
374
+ */
375
+ liveTasks: () => ReadonlySet<string>;
233
376
  }
234
377
  /**
235
- * A turn began by running the runner's own command `/name args` (#66): the
236
- * `turn_start` of a command turn. `messageId` names it when it had been queued.
378
+ * The lifecycle of a plan's isolation run (ADR-0013, ADR-0014, ADR-0020),
379
+ * between the scheduler and the git layer: deciding, at a run's first change
380
+ * task, whether it executes in worktrees, a blocked run and how it goes on,
381
+ * each attempt's working directory, releasing worktrees, which landed work the
382
+ * user has merged, the tree check of an ops task, the handoff when the run
383
+ * closes, and what the user does with it afterwards. The record it keeps
384
+ * outlives one run — a resumed plan continues it.
237
385
  */
238
- | {
239
- type: 'command';
240
- name: string;
241
- args?: string;
242
- messageId?: string;
243
- }
244
- /** What the running command printed, for one that runs without the model. */
245
- | {
246
- type: 'command_output';
247
- text: string;
248
- }
249
- /** The runner compacted its own context, asked to or on its own. */
250
- | {
251
- type: 'compaction';
252
- trigger: 'manual' | 'auto';
253
- preTokens?: number;
254
- postTokens?: number;
255
- }
256
- /** What the runner loaded from the user's and the project's setup, as its process started. */
257
- | {
258
- type: 'runner_loaded';
259
- commands: RunnerCommand[];
260
- mcpServers: RunnerMcpServer[];
261
- } | {
262
- type: 'turn_end';
263
- reason: StructuredTurnEnd;
264
- } | {
265
- type: 'text_delta';
266
- text: string;
267
- }
268
- /** A complete run of the agent's reply, authoritative over the deltas streamed for it. */
269
- | {
270
- type: 'text';
271
- text: string;
272
- } | {
273
- type: 'thinking_delta';
274
- text: string;
275
- subagentId?: string;
276
- } | {
277
- type: 'thinking';
278
- text: string;
279
- subagentId?: string;
280
- }
281
- /** `args` is the call's arguments as JSON, exactly as the runner announced them. */
282
- | {
283
- type: 'tool_call';
284
- id: string;
285
- name: string;
286
- args: string;
287
- subagentId?: string;
288
- }
289
- /** `omittedLines` is set when {@link trimToolOutput} cut the middle of `output`. */
290
- | {
291
- type: 'tool_result';
292
- id: string;
293
- output: string;
294
- success: boolean;
295
- omittedLines?: number;
296
- subagentId?: string;
297
- } | {
298
- type: 'subagent_started';
299
- subagentId: string;
300
- brief: string;
301
- model?: string;
302
- } | {
303
- type: 'subagent_finished';
304
- subagentId: string;
305
- outcome: SubagentOutcome;
306
- digest: string;
307
- } | {
308
- type: 'usage';
309
- record: UsageRecord;
310
- }
311
- /**
312
- * Waiting in Ordewell's queue — again, for a message the runner was handed
313
- * and let go of (ADR-0023, D4). `forced` moves it ahead of the rest (F2).
314
- */
315
- | {
316
- type: 'message_queued';
317
- messageId: string;
318
- text: string;
319
- forced?: boolean;
320
- from?: TaskMessageFrom;
321
- } | {
322
- type: 'message_removed';
323
- messageId: string;
324
- } | {
325
- type: 'message_undelivered';
326
- messageId: string;
327
- text: string;
328
- from?: TaskMessageFrom;
329
- }
330
- /** The runner accepted a queued message into its running turn; it can no longer be taken back. */
331
- | {
332
- type: 'message_handed_over';
333
- messageId: string;
334
- }
335
- /**
336
- * The model read a message inside its running turn (ADR-0023, Q2), logged
337
- * where it read it. A message that opened a turn is that turn's `turn_start`.
338
- */
339
- | {
340
- type: 'message_delivered';
341
- messageId: string;
342
- text: string;
343
- from?: TaskMessageFrom;
344
- }
345
- /** The agent's own words for a failed turn. */
346
- | {
347
- type: 'error';
348
- message: string;
349
- }
350
- /**
351
- * The runner asked to use a tool its mode does not cover (ADR-0018, A1).
352
- * `approvalId` is what the answer is given under; `args` is the call's
353
- * arguments as JSON; `allowForTask` says the runner offered its own
354
- * session-scoped grant. `decided` is set when the request was never left
355
- * open — an Ordewell tool or an auto-approving mode — so a surface does not
356
- * offer to answer what is already answered (it is still followed by the
357
- * matching `approval_decided`, which a reader may rely on).
358
- */
359
- | {
360
- type: 'approval_requested';
361
- approvalId: string;
362
- tool: string;
363
- args: string;
364
- allowForTask: boolean;
365
- toolCallId?: string;
366
- decided?: ApprovalDecision['decision'];
367
- } | {
368
- type: 'approval_decided';
369
- approvalId: string;
370
- decision: ApprovalDecision['decision'];
371
- note?: string;
372
- }
373
- /** The request went unanswered: the runner withdrew it, or its process ended. */
374
- | {
375
- type: 'approval_withdrawn';
376
- approvalId: string;
377
- }
378
- /**
379
- * The runner's process ended. A call it had not reported back on is not
380
- * running any more: a task's runner is ended the moment its `task_complete`
381
- * lands, often before it reports that call's result.
382
- */
383
- | {
384
- type: 'runner_ended';
385
- };
386
- /**
387
- * Keep a tool result's head and tail. A build log or a whole file read can
388
- * run to megabytes, and every attempt's file keeps it for good; the ends are
389
- * where a reader looks — the command's start, and how it finished.
390
- */
391
- declare function trimToolOutput(output: string): {
392
- output: string;
393
- omittedLines?: number;
394
- };
395
- /**
396
- * The log entry for one structured event, or null for what the log does not
397
- * keep: background-agent launches, which the subagent's own events already show.
398
- */
399
- declare function toTaskLogEvent(event: StructuredEvent): TaskLogEvent | null;
400
- /** A log as a surface is sent it, live or read back: everything but the skills' bodies. */
401
- declare function taskLogForSurface(events: readonly TaskLogEvent[]): TaskLogEvent[];
402
- /**
403
- * Merge runs of deltas into one event each. Deltas arrive a few characters at
404
- * a time; merged, a batch is one message on the wire and one line on disk,
405
- * and the view folds to the same blocks either way.
406
- */
407
- declare function coalesceTaskLog(events: readonly TaskLogEvent[]): TaskLogEvent[];
408
- /**
409
- * What an attempt did to the world, for the retry of a task whose effects
410
- * outlive it (ADR-0020): one line per tool call and how it ended.
411
- */
412
- declare function digestTaskLog(events: readonly TaskLogEvent[], maxCalls: number): string;
413
-
414
- type NotificationAction = {
415
- label: string;
416
- value: string;
417
- };
418
- interface INotification {
419
- info(msg: string): void;
420
- warn(msg: string): void;
421
- error(msg: string): void;
422
- confirm(msg: string, options: string[]): Promise<string | undefined>;
423
- }
424
-
425
- /**
426
- * The deep module owning all plan-shaped state. Holds `planTasks` (the ordered
427
- * tree the user edits), the flattened `allTasks` view and the `taskMap` index.
428
- * The orchestrator calls `markCompleted`/`markFailed`/`markInProgress`/`retry`
429
- * to update task status — it never mutates task state directly.
430
- *
431
- * Completion and failure are read from `task.status` and nothing else, so
432
- * `isCompleted`, `completedCount`, `isAllComplete` and the scheduler's
433
- * dependency checks cannot disagree. The store owns its task objects: `load`
434
- * copies what it is given, the getters hand out readonly views, and
435
- * `snapshot` is the copy a caller may keep or write to disk.
436
- *
437
- * `rebuild` is the internal seam that keeps the flat views in sync with the
438
- * tree.
439
- *
440
- * `planRunners` lives here because it's part of the plan's identity (the runner
441
- * set, carried on the plan). `validateAssignedRunners` is pure store logic.
442
- */
443
- declare class PlanStore {
444
- private _planTasks;
445
- private _allTasks;
446
- private _taskMap;
447
- /** Task id → the top-level task it hangs under, itself for a top-level task. */
448
- private _rootMap;
449
- private _planRunners;
450
- private _onMutate;
451
- private _executionLog;
452
- /** Hook called after every structural mutation (add/remove/update/resetForRun). */
453
- set onMutate(cb: (() => void) | null);
454
- get planTasks(): ReadonlyArray<Readonly<Task>>;
455
- get allTasks(): ReadonlyArray<Readonly<Task>>;
456
- get planRunners(): RunnerId[];
457
- get completedCount(): number;
458
- get failedCount(): number;
459
- isAllComplete(): boolean;
460
- isAnyFailed(): boolean;
461
- isCompleted(id: string): boolean;
462
- isSkipped(id: string): boolean;
463
- /** Nothing is left to do: every task completed or was skipped. */
464
- isFinished(): boolean;
465
- isFailed(id: string): boolean;
466
- get(taskId: string): Readonly<Task> | undefined;
386
+ declare class IsolationRunController {
387
+ private readonly isolation;
388
+ private readonly config;
389
+ private readonly notifications;
390
+ private readonly workspaceRoot;
391
+ private readonly listener;
392
+ private readonly liveTasks;
393
+ private run;
394
+ /** Copied paths already reported for the current run: every task gets the same copies. */
395
+ private reportedCopies;
396
+ private reportedSetupFailures;
467
397
  /**
468
- * Whether a task runs as an ops task (ADR-0020). Only a top-level AI task
469
- * can be one; a subtask runs with the top-level task it hangs under.
398
+ * How the open run executes; null while no run is open, `undecided` until
399
+ * its first change task asks. A run is one Execute-Plan or one manual task
400
+ * run, from its start until it settles or is stopped.
470
401
  */
471
- isOps(taskId: string): boolean;
472
- /** A copy of the task tree, detached from the store: later status changes do not reach it. */
473
- snapshot(): Task[];
474
- getExecutionLog(): ReadonlyArray<TaskSnapshot>;
475
- appendToLog(snapshot: TaskSnapshot): void;
402
+ private mode;
403
+ /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
404
+ private resolvers;
405
+ private opening;
406
+ /** Bumped by an interrupt, so an activation in flight across it does not open the run it ended. */
407
+ private activations;
408
+ /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
409
+ private blockedStart;
410
+ /** The dirty repos behind {@link blockedStart}, for the stash notice. */
411
+ private blockedRepos;
412
+ constructor(deps: IsolationRunControllerDeps);
413
+ /** The plan's run record, open or not; null when the plan has not isolated. */
414
+ get current(): IsolationRun | null;
415
+ get isOpen(): boolean;
416
+ /** The open run has decided whether it isolates. */
417
+ get decided(): boolean;
418
+ /** The open run executes in worktrees. */
419
+ get isolating(): boolean;
420
+ /** A run is waiting on the user to stash or to go on without isolation. */
421
+ get blocked(): boolean;
422
+ /** What the plan persists of isolated execution; null when no run ever isolated. */
423
+ get planIsolation(): PlanIsolation | null;
424
+ /** The one guard of every action on the run: a plan that never isolated is a request refused, not a fault. */
425
+ requireRun(): IsolationRun;
426
+ /** A task's record in the open run, only while that run isolates. */
427
+ openRecord(taskId: string): IsolationTaskRecord | undefined;
428
+ /** Where a task's isolated work stands; null when the plan has no isolation run to speak of. */
429
+ taskIsolation(taskId: string): TaskIsolation | null;
430
+ view(): IsolationView | null;
476
431
  /**
477
- * Drop a task's archived snapshot. Un-marking a completion has to erase the
478
- * "finished" record too — dependent tasks are prompted from the log, so a
479
- * left-behind snapshot would keep feeding them a result that no longer exists.
432
+ * Take over a plan's persisted isolation, or none for a plan that has not
433
+ * isolated yet. Whatever a crashed process left behind for the run — a
434
+ * worktree still marked active, a directory no record owns — is pruned,
435
+ * while kept, failed and conflicted worktrees stay for the user.
436
+ *
437
+ * Deliberately not reported as a change: adopting is not a change to
438
+ * persist, and a host that adopts without persisting (VS Code's restore)
439
+ * would otherwise write a new session file on every reload.
480
440
  */
481
- removeFromLog(taskId: string): void;
482
- clearLog(): void;
483
- private notifyMutate;
484
- private countStatus;
485
- load(tasks: ReadonlyArray<Readonly<Task>>, runners: readonly RunnerId[]): void;
486
- add(partial: Partial<Task>): Task;
487
- remove(taskId: string): void;
488
- update(taskId: string, changes: Partial<Task>): Task | undefined;
441
+ adopt(state: PlanIsolation | null): Promise<void>;
442
+ /** Open a run if none is, leaving whether it isolates to its first change task. */
443
+ open(): void;
489
444
  /**
490
- * Run preparation as one named op: flip every AI task to 'approved'. By
491
- * default completed tasks keep their status so a reloaded half-finished plan
492
- * resumes the remainder instead of re-running work that already succeeded,
493
- * and skipped ones stay skipped: a new run is not a retry of them.
494
- * `preserveCompleted: false` is for a freshly committed plan, where
495
- * everything starts over. `keepScheduledResumes` leaves a task waiting on
496
- * its runner's usage-limit reset waiting: an edit adopted between runs is
497
- * not the user starting over.
498
- */
499
- resetForRun(opts?: {
500
- preserveCompleted?: boolean;
501
- keepScheduledResumes?: boolean;
502
- }): void;
503
- markCompleted(id: string): void;
504
- markFailed(id: string): void;
505
- markSkipped(id: string, reason?: string): void;
506
- markInProgress(id: string): void;
507
- markAwaitingUser(id: string, reason?: AwaitingReason): void;
508
- /** When a task paused on a usage limit resumes on its own; undefined leaves it to a manual retry. */
509
- setTaskResumeAt(id: string, resumeAt: string | undefined): void;
510
- markPending(id: string): void;
511
- retry(id: string): void;
512
- /** Kept on the task: it was force-started past its merge gate, before these dependencies' work was merged (ADR-0020). */
513
- setForcedPastGate(id: string, dependencies: string[]): void;
514
- /** A reason outlives nothing: any status but `awaiting_user` drops it, and the resume time with it; any status drops a skip's reason. */
515
- private setStatus;
516
- blockDependents(id: string): void;
517
- unblockDependents(id: string): void;
518
- setTaskVerdict(id: string, verdict: Task['verdict']): void;
519
- setTaskOutputSummary(id: string, summary: Task['outputSummary']): void;
520
- setTaskRunnerSessionId(id: string, runnerSessionId: string | undefined): void;
521
- startAttemptUsage(id: string, attempt: number, runner: string, startedAt: string): void;
522
- recordAttemptUsage(id: string, attempt: number, record: UsageRecord, now: string): void;
523
- endAttemptUsage(id: string, attempt: number, now: string): void;
524
- setTaskAttemptSkills(id: string, skills: Task['attemptSkills']): void;
525
- /**
526
- * Widen the plan's runner set. Retargeting a task onto a runner the plan has
527
- * not used before has to land here as well as on the plan state, or
528
- * {@link resolveTaskRunner} reads the new runner as foreign and spawns the
529
- * plan's first one instead.
530
- */
531
- admitRunner(runner: RunnerId): void;
532
- resolveTaskRunner(task: Readonly<Task>): RunnerId;
533
- private rebuild;
534
- private validateAssignedRunners;
535
- }
536
-
537
- type IsolationNoticeLevel = 'info' | 'warn' | 'error';
538
- interface AttemptWorkspace {
539
- cwd: string;
540
- worktree: boolean;
541
- continuationMessage?: string;
542
- }
543
- /**
544
- * What the controller reports. It never schedules or emits on its own: the
545
- * orchestrator turns these into observer events and decides what runs next.
546
- */
547
- interface IsolationRunListener {
548
- /**
549
- * The run record changed and should be persisted with the plan. `detached`
550
- * is set instead when the record that changed is no longer the plan's own —
551
- * a release that outlived the plan load that replaced its run — and belongs
552
- * to the session that was left.
553
- */
554
- changed(detached?: IsolationRun): void;
555
- /** A run did not start: `repos` of the group have tracked changes. */
556
- blocked(repos: string[]): void;
557
- /** An isolated run closed and handed its integration branches over. */
558
- handoff(handoff: IsolationHandoff): void;
559
- /** Already said through the notifications; also for a surface that shows no toasts. */
560
- notice(level: IsolationNoticeLevel, message: string): void;
561
- }
562
- interface IsolationRunControllerDeps {
563
- isolation: IWorktreeIsolation;
564
- config: IConfig;
565
- notifications: INotification;
566
- workspaceRoot: () => string;
567
- listener: IsolationRunListener;
568
- /**
569
- * The tasks no clean-up of the whole run may take a worktree from: an
570
- * attempt is running them, or the plan has them in progress or waiting on
571
- * the user mid-attempt.
572
- */
573
- liveTasks: () => ReadonlySet<string>;
574
- }
575
- /**
576
- * The lifecycle of a plan's isolation run (ADR-0013, ADR-0014, ADR-0020),
577
- * between the scheduler and the git layer: deciding, at a run's first change
578
- * task, whether it executes in worktrees, a blocked run and how it goes on,
579
- * each attempt's working directory, releasing worktrees, which landed work the
580
- * user has merged, the tree check of an ops task, the handoff when the run
581
- * closes, and what the user does with it afterwards. The record it keeps
582
- * outlives one run — a resumed plan continues it.
583
- */
584
- declare class IsolationRunController {
585
- private readonly isolation;
586
- private readonly config;
587
- private readonly notifications;
588
- private readonly workspaceRoot;
589
- private readonly listener;
590
- private readonly liveTasks;
591
- private run;
592
- /** Copied paths already reported for the current run: every task gets the same copies. */
593
- private reportedCopies;
594
- private reportedSetupFailures;
595
- /**
596
- * How the open run executes; null while no run is open, `undecided` until
597
- * its first change task asks. A run is one Execute-Plan or one manual task
598
- * run, from its start until it settles or is stopped.
599
- */
600
- private mode;
601
- /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
602
- private resolvers;
603
- private opening;
604
- /** Bumped by an interrupt, so an activation in flight across it does not open the run it ended. */
605
- private activations;
606
- /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
607
- private blockedStart;
608
- /** The dirty repos behind {@link blockedStart}, for the stash notice. */
609
- private blockedRepos;
610
- constructor(deps: IsolationRunControllerDeps);
611
- /** The plan's run record, open or not; null when the plan has not isolated. */
612
- get current(): IsolationRun | null;
613
- get isOpen(): boolean;
614
- /** The open run has decided whether it isolates. */
615
- get decided(): boolean;
616
- /** The open run executes in worktrees. */
617
- get isolating(): boolean;
618
- /** A run is waiting on the user to stash or to go on without isolation. */
619
- get blocked(): boolean;
620
- /** What the plan persists of isolated execution; null when no run ever isolated. */
621
- get planIsolation(): PlanIsolation | null;
622
- /** The one guard of every action on the run: a plan that never isolated is a request refused, not a fault. */
623
- requireRun(): IsolationRun;
624
- /** A task's record in the open run, only while that run isolates. */
625
- openRecord(taskId: string): IsolationTaskRecord | undefined;
626
- /** Where a task's isolated work stands; null when the plan has no isolation run to speak of. */
627
- taskIsolation(taskId: string): TaskIsolation | null;
628
- view(): IsolationView | null;
629
- /**
630
- * Take over a plan's persisted isolation, or none for a plan that has not
631
- * isolated yet. Whatever a crashed process left behind for the run — a
632
- * worktree still marked active, a directory no record owns — is pruned,
633
- * while kept, failed and conflicted worktrees stay for the user.
634
- *
635
- * Deliberately not reported as a change: adopting is not a change to
636
- * persist, and a host that adopts without persisting (VS Code's restore)
637
- * would otherwise write a new session file on every reload.
638
- */
639
- adopt(state: PlanIsolation | null): Promise<void>;
640
- /** Open a run if none is, leaving whether it isolates to its first change task. */
641
- open(): void;
642
- /**
643
- * Decide once per run, as its first change task starts, whether it executes
644
- * in worktrees (ADR-0020): an ops task needs none, so a run of only ops
645
- * tasks never asks. `resume` is what a dirty tree parks until the user
646
- * chooses how to go on. Resolves false while the run waits on that choice.
445
+ * Decide once per run, as its first change task starts, whether it executes
446
+ * in worktrees (ADR-0020): an ops task needs none, so a run of only ops
447
+ * tasks never asks. `resume` is what a dirty tree parks until the user
448
+ * chooses how to go on. Resolves false while the run waits on that choice.
647
449
  */
648
450
  decide(resume: () => Promise<void>): Promise<boolean>;
649
451
  /**
@@ -680,7 +482,7 @@ declare class IsolationRunController {
680
482
  * the attempt is committed as running, so a spawn abandoned or failed after
681
483
  * this settles is not misreported as a change that stuck.
682
484
  */
683
- attemptCwd(task: Task, intent: 'fresh' | 'repair' | 'continuation'): Promise<AttemptWorkspace>;
485
+ attemptCwd(task: Task, intent: AttemptIntent): Promise<AttemptWorkspace>;
684
486
  /** The workspace root, for an attempt that runs outside any worktree. */
685
487
  workspaceCwd(task: Task): Promise<{
686
488
  cwd: string;
@@ -944,729 +746,278 @@ interface WorkspaceEnv {
944
746
  trackedEnvFile: string | null;
945
747
  }
946
748
 
947
- /** What ran an automatic Merge all: a task reaching its merge gate, or the run settling with landed work unmerged. */
948
- type AutoMergeTrigger = {
949
- kind: 'gate';
950
- taskId: string;
951
- } | {
952
- kind: 'run_end';
953
- };
954
749
  /**
955
- * Who opened a run (ADR-0029): a call the user made, or one made for them; a
956
- * call of the supervisor's own; or a usage-limit reset resuming what it paused.
957
- * Only the user's starts a run's supervision afresh.
750
+ * One entry of a structured task's log (ADR-0018, P1): what a runner did,
751
+ * normalized and serializable, streamed live as `task_log` and appended to
752
+ * the attempt's `.jsonl` file. The live view and the reload fold the same
753
+ * events through `reduceTaskLog`, so the two cannot disagree.
754
+ *
755
+ * Readers skip a type they do not know, which is how later events join
756
+ * without a format version.
958
757
  */
959
- type RunOrigin = 'user' | 'supervisor' | 'limit-reset';
960
- /** What the scheduler reads of the open run's settings, at each decision it makes from them. */
961
- type SchedulerRunSettings = Pick<RunSettings, 'mergeAtGatesAutomatically'>;
758
+ type TaskLogEvent =
962
759
  /**
963
- * The one notification channel out of the orchestrator. Everything that used
964
- * to travel over a separate callback (onRefresh) is an observer event; the
965
- * Session subscribes once and turns these into SessionMessages.
760
+ * The task skills this attempt's prompt carried, as read at its spawn
761
+ * (ADR-0024): first in its log. The file keeps their bodies; a surface is
762
+ * sent notices ({@link taskLogForSurface}).
966
763
  */
967
- interface OrchestratorObserver {
968
- /** Any task-shaped state changed (store mutation, checkpoint, retry, …). */
969
- onTaskChanged?(): void;
970
- /**
971
- * A run reached an outcome that stands until the user acts — completed,
972
- * failed, or awaiting the user — on its own, not as the answer to a call.
973
- * Emitted before the `onTaskChanged` that announces it, so a listener can
974
- * save the outcome before any surface is told.
975
- */
976
- onTaskSettled?(data: {
977
- taskId: string;
978
- }): void;
979
- onRunStarted?(data: {
980
- origin: RunOrigin;
981
- }): void;
982
- /** An automatic Merge all settled, at a gate or at the run's end; failures are attention events for a supervisor. */
983
- onAutoMerge?(data: {
984
- trigger: AutoMergeTrigger;
985
- result: IsolationMergeResult;
986
- }): void;
987
- onTick?(): void;
988
- onUsageChanged?(): void;
989
- onExecutionComplete?(): void;
990
- /**
991
- * A task paused on its runner's usage limit, or its resume changed:
992
- * `resumeAt` is when it resumes on its own (ISO time), null when it waits
993
- * for a manual retry — the reset could not be read, the resume was
994
- * cancelled, or the resumed attempt hit the limit again at once.
995
- * `cancelled` marks the resume being cancelled, which is no new pause.
996
- */
997
- onUsageLimitWait?(data: {
998
- taskId: string;
999
- runner: string;
1000
- resumeAt: string | null;
1001
- cancelled?: true;
1002
- }): void;
1003
- /** A live task's runner went silent (ADR-0018's idle timer); paused while it waits on input, a checkpoint or an approval. */
1004
- onTaskIdle?(data: {
1005
- taskId: string;
1006
- idleSince: string;
1007
- }): void;
1008
- onReviewNeeded?(data: {
1009
- tasks: Task[];
1010
- planRunners: RunnerId[];
1011
- }): void;
1012
- onReviewApproved?(data: {
1013
- tasks: Task[];
1014
- }): void;
1015
- onCheckpoint?(data: {
1016
- taskId: string;
1017
- taskTitle: string;
1018
- summary: string;
1019
- }): void;
1020
- /**
1021
- * The isolation run record changed and should be persisted with the plan.
1022
- * `detached` is the record of a run another plan has replaced since — a
1023
- * release that outlived a plan load — for the session it belongs to.
1024
- */
1025
- onIsolationChanged?(detached?: IsolationRun): void;
1026
- /** A run did not start: the tree is dirty, and the user picks stash or no isolation. `repos` names the dirty repos of a group. */
1027
- onIsolationBlocked?(data: {
1028
- reason: 'dirty';
1029
- repos: string[];
1030
- }): void;
1031
- /** An isolated run settled; emitted before `onExecutionComplete`, which surfaces treat as terminal. */
1032
- onIsolationHandoff?(handoff: IsolationHandoff): void;
1033
- /**
1034
- * What a run says about how it isolates — the fallback to the workspace root,
1035
- * shared paths, copies, a stash. Beside the notification channel, which a
1036
- * daemon may leave unwired, so a surface without toasts can still show it.
1037
- */
1038
- onIsolationNotice?(data: {
1039
- level: 'info' | 'warn' | 'error';
1040
- message: string;
1041
- }): void;
764
+ {
765
+ type: 'task_skills';
766
+ skills: TaskSkillSnapshot[] | TaskSkillNotice[];
1042
767
  }
1043
768
  /**
1044
- * A message or interrupt a task cannot take: it is not running, or it is
1045
- * waiting on something a message does not answer.
1046
- * The request is wrong, not the orchestrator, so a surface says why.
769
+ * A turn began by delivering `message`; `messageId` names it when it had been
770
+ * queued, `forced` when it was force sent. `from` on this and the other
771
+ * message events marks one Ordewell sent rather than the user.
1047
772
  */
1048
- declare class TaskControlError extends Error {
1049
- constructor(message: string);
1050
- }
1051
- type AttemptPhase = 'starting' | 'running' | 'integrating';
1052
- /** Read-only view of a task's live attempt. */
1053
- interface TaskAttemptSnapshot {
1054
- taskId: string;
1055
- attempt: number;
1056
- phase: AttemptPhase;
1057
- sessionId: string | null;
1058
- runner: string;
1059
- cwd: string | null;
1060
- startedAt: string;
1061
- skills: readonly TaskSkillSnapshot[];
773
+ | {
774
+ type: 'turn_start';
775
+ message: string;
776
+ messageId?: string;
777
+ forced?: boolean;
778
+ from?: TaskMessageFrom;
1062
779
  }
1063
780
  /**
1064
- * The wiring a {@link TaskOrchestrator} runs on, every collaborator resolved.
1065
- * {@link TaskOrchestrator.compose} is the only production caller; tests build
1066
- * one through it too. The constructor makes nothing itself, so there is exactly
1067
- * one place defaults live.
781
+ * A turn began by running the runner's own command `/name args` (#66): the
782
+ * `turn_start` of a command turn. `messageId` names it when it had been queued.
1068
783
  */
1069
- interface TaskOrchestratorDeps {
1070
- config: IConfig;
1071
- notifications: INotification;
1072
- runner: IRunner;
1073
- store: PlanStore;
1074
- output: TaskOutputSource;
1075
- /** The plan's isolation run and the open run's lifecycle (ADR-0013). */
1076
- runs: IsolationRunController;
1077
- /** A passed attempt's work onto the integration branch, and any conflict repair. */
1078
- landing: Landing;
1079
- registry: RunnerRegistry | null;
1080
- /** The workspace root, read at call time — VS Code can change it. */
1081
- workspaceRoot: () => string;
1082
- /** The workspace's own variables for a task's cwd (ADR-0016). */
1083
- workspaceEnv: (cwd: string) => Promise<WorkspaceEnv>;
1084
- /** The skill catalog over workspace roots, as {@link workspaceSkillRoots} orders them — read at each spawn. */
1085
- skillsAt: (roots: readonly string[]) => SkillLookup;
1086
- /** What the task's last saved attempt did, or null when it left no log (ADR-0020). */
1087
- previousAttemptFromLog: (taskId: string) => string | null;
1088
- /**
1089
- * The open run's settings, read as each automatic merge is decided rather
1090
- * than copied when the run opens, so a change to the run under way applies
1091
- * at once (ADR-0030); absent before any run.
1092
- */
1093
- runSettings: () => SchedulerRunSettings | undefined;
784
+ | {
785
+ type: 'command';
786
+ name: string;
787
+ args?: string;
788
+ messageId?: string;
789
+ }
790
+ /** What the running command printed, for one that runs without the model. */
791
+ | {
792
+ type: 'command_output';
793
+ text: string;
794
+ }
795
+ /** The runner compacted its own context, asked to or on its own. */
796
+ | {
797
+ type: 'compaction';
798
+ trigger: 'manual' | 'auto';
799
+ preTokens?: number;
800
+ postTokens?: number;
801
+ }
802
+ /** What the runner loaded from the user's and the project's setup, as its process started. */
803
+ | {
804
+ type: 'runner_loaded';
805
+ commands: RunnerCommand[];
806
+ mcpServers: RunnerMcpServer[];
807
+ } | {
808
+ type: 'turn_end';
809
+ reason: StructuredTurnEnd;
810
+ } | {
811
+ type: 'text_delta';
812
+ text: string;
813
+ }
814
+ /** A complete run of the agent's reply, authoritative over the deltas streamed for it. */
815
+ | {
816
+ type: 'text';
817
+ text: string;
818
+ } | {
819
+ type: 'thinking_delta';
820
+ text: string;
821
+ subagentId?: string;
822
+ } | {
823
+ type: 'thinking';
824
+ text: string;
825
+ subagentId?: string;
826
+ }
827
+ /** `args` is the call's arguments as JSON, exactly as the runner announced them. */
828
+ | {
829
+ type: 'tool_call';
830
+ id: string;
831
+ name: string;
832
+ args: string;
833
+ subagentId?: string;
834
+ }
835
+ /** `omittedLines` is set when {@link trimToolOutput} cut the middle of `output`. */
836
+ | {
837
+ type: 'tool_result';
838
+ id: string;
839
+ output: string;
840
+ success: boolean;
841
+ omittedLines?: number;
842
+ subagentId?: string;
843
+ } | {
844
+ type: 'subagent_started';
845
+ subagentId: string;
846
+ brief: string;
847
+ model?: string;
848
+ } | {
849
+ type: 'subagent_finished';
850
+ subagentId: string;
851
+ outcome: SubagentOutcome;
852
+ digest: string;
853
+ } | {
854
+ type: 'usage';
855
+ record: UsageRecord;
1094
856
  }
1095
857
  /**
1096
- * What a caller supplies to {@link TaskOrchestrator.compose}; every optional
1097
- * one gets its default there. This is what a host or test chooses, not the
1098
- * resolved wiring the orchestrator itself runs on.
858
+ * Waiting in Ordewell's queue — again, for a message the runner was handed
859
+ * and let go of (ADR-0023, D4). `forced` moves it ahead of the rest (F2).
1099
860
  */
1100
- interface TaskOrchestratorOptions {
1101
- config: IConfig;
1102
- notifications: INotification;
1103
- runner: IRunner;
1104
- store?: PlanStore;
1105
- output?: TaskOutputSource;
1106
- isolation?: IWorktreeIsolation;
1107
- registry?: RunnerRegistry | null;
1108
- workspaceRoot?: () => string;
1109
- workspaceEnv?: (cwd: string) => Promise<WorkspaceEnv>;
1110
- skillsAt?: (roots: readonly string[]) => SkillLookup;
1111
- previousAttemptFromLog?: (taskId: string) => string | null;
1112
- runSettings?: () => SchedulerRunSettings | undefined;
861
+ | {
862
+ type: 'message_queued';
863
+ messageId: string;
864
+ text: string;
865
+ forced?: boolean;
866
+ from?: TaskMessageFrom;
867
+ } | {
868
+ type: 'message_removed';
869
+ messageId: string;
870
+ } | {
871
+ type: 'message_undelivered';
872
+ messageId: string;
873
+ text: string;
874
+ from?: TaskMessageFrom;
875
+ }
876
+ /** The runner accepted a queued message into its running turn; it can no longer be taken back. */
877
+ | {
878
+ type: 'message_handed_over';
879
+ messageId: string;
1113
880
  }
1114
881
  /**
1115
- * The pure scheduler. Owns execution state (`running`, `planStatus`,
1116
- * `reviewApproved`, the live task attempts, the start hold) and the verifier.
1117
- * All task-shaped state — the plan tree, the flat index, the completed set
1118
- * — lives in {@link PlanStore}, injected at construction. The orchestrator
1119
- * calls `store.markCompleted(id)` / `store.markFailed(id)` instead of mutating
1120
- * task state directly. A task completes only after the runner reports it done
1121
- * through `task_complete` on its attempt's token; process exit without that
1122
- * evidence is a visible failure and does not unblock dependent work.
882
+ * The model read a message inside its running turn (ADR-0023, Q2), logged
883
+ * where it read it. A message that opened a turn is that turn's `turn_start`.
1123
884
  */
1124
- declare class TaskOrchestrator {
1125
- private config;
1126
- private notifications;
1127
- private runner;
1128
- private store;
1129
- private output;
1130
- private attempts;
1131
- private verifier;
1132
- private running;
1133
- private planStatus;
1134
- /** Planner turns in flight that new starts wait for (ADR-0029); running attempts carry on. */
1135
- private startHolds;
1136
- private reviewApproved;
1137
- private autoMergeBlocked;
1138
- private mergesInFlight;
1139
- private retryCounts;
1140
- private spawnCounts;
1141
- /** A full-plan run a usage-limit pause holds: no new start until the reset resumes it. */
1142
- private heldOnLimit;
1143
- /**
1144
- * A full-plan run that ended with failed tasks and the tasks they block
1145
- * (ADR-0034). A retry of a failed task continues it, so what it blocked
1146
- * starts once it passes — without this the retried task ran alone.
1147
- */
1148
- private endedOnFailure;
1149
- /** A Merge all is under way: no ops task starts until it ends (ADR-0020). */
1150
- private merging;
1151
- /** The last "waiting for Merge all" notice, so a run paused at a gate says it once, not on every tick. */
1152
- private gateNotice;
1153
- /** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
1154
- private workspaceEnv;
1155
- /** What the workspace env has already warned about, so a run says it once, not per task. */
1156
- private envWarnings;
1157
- /**
1158
- * Tasks pulled out of auto-scheduling (user-cancelled or failed to spawn).
1159
- * They stay 'pending' — "not executed" — but the scheduler skips them until
1160
- * the user retries or force-starts, which would otherwise loop forever on a
1161
- * task whose spawn always throws.
1162
- */
1163
- private onHold;
1164
- /**
1165
- * Tasks Mark complete is landing after it ended their attempt: in progress
1166
- * with no attempt for as long as that takes, and not orphans for it.
1167
- */
1168
- private completing;
1169
- /** One wait per runner for its usage limit to reset; the tasks it resumes, and when, are on the plan (#84). */
1170
- private limitWaits;
1171
- /**
1172
- * The plan's isolation run and the open run's lifecycle (ADR-0013); see
1173
- * {@link IsolationRunController}. Read and acted on there directly, except
1174
- * Merge all: that goes through {@link mergeRun}, which keeps it apart from
1175
- * ops tasks and starts the tasks its merge lets through.
1176
- */
1177
- readonly runs: IsolationRunController;
1178
- /** A passed attempt's work onto the integration branch, and the conflict repair a landing may need. */
1179
- private landing;
1180
- private registry;
1181
- private workspaceRootFn;
1182
- private observers;
1183
- private skillsAt;
1184
- private previousAttemptFromLog;
1185
- private runSettings;
1186
- constructor(deps: TaskOrchestratorDeps);
1187
- /**
1188
- * The composition root: resolves every optional collaborator and wires the
1189
- * isolation listener that the constructor cannot, because it needs the
1190
- * instance's own (private) emit seam. Nothing else in the
1191
- * codebase constructs an orchestrator, so this is the one place defaults
1192
- * live — a host or test supplies what it cares about and gets the rest.
1193
- */
1194
- static compose(options: TaskOrchestratorOptions): TaskOrchestrator;
1195
- /** Advisory silence timestamp for a task's live runner, or null if not idle. */
1196
- getIdleSince(taskId: string): string | null;
1197
- /**
1198
- * What an ops retry is told about the attempt before. The saved log is read
1199
- * whether or not the session was reloaded, so both behave alike; the output
1200
- * buffer is the fallback for a runner that has no log. A reload clears the
1201
- * attempt count along with the buffer, so a retry made since is what says
1202
- * there was an attempt to report on.
1203
- */
1204
- private opsPreviousAttempt;
1205
- /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
1206
- getLiveOutput(taskId: string, opts: LiveTailOptions): LiveTail | null;
1207
- get storeInstance(): PlanStore;
1208
- /**
1209
- * The variables a task's agent gets from its workspace. What cannot be
1210
- * applied is said once — silently starting without them is how agents ran
1211
- * under the wrong account when an edited `.envrc` was left unallowed.
1212
- */
1213
- private envForTask;
1214
- approveCheckpoint(taskId: string): void;
1215
- rejectCheckpoint(taskId: string, reason?: string): void;
1216
- /** What the task's waiting checkpoint asks, whole; undefined once it settles or when none waits. */
1217
- getCheckpointQuestion(taskId: string): string | undefined;
1218
- awaitsCheckpoint(taskId: string): boolean;
1219
- /**
1220
- * Send a structured task a user message (ADR-0018, M1). Mid-turn it queues
1221
- * behind the turn; a task waiting for input takes it at once and is back in
1222
- * progress. Returns the message's id, for {@link removeQueuedTaskMessage}.
1223
- */
1224
- sendTaskMessage(taskId: string, text: string): string;
1225
- /**
1226
- * Force send (ADR-0023, F1–F3): interrupt the running turn and deliver the
1227
- * message as the next one, ahead of anything queued, without the task
1228
- * waiting for input in between. A task already waiting for input just takes it.
1229
- */
1230
- forceSendTaskMessage(taskId: string, text: string): string;
1231
- /**
1232
- * Run the task runner's own command `/name args` (#66) as a turn of its
1233
- * own, waiting behind a running turn as a message would. Ordewell carries
1234
- * it; what it does is the runner's business (#24).
1235
- */
1236
- invokeTaskCommand(taskId: string, name: string, args?: string): string;
1237
- private messageTask;
1238
- /** Force send a message not yet delivered, queued or handed over; false once it was delivered. */
1239
- forceSendQueuedTaskMessage(taskId: string, id: string): boolean;
1240
- /** Take back a message still queued behind a turn; false once it was delivered. */
1241
- removeQueuedTaskMessage(taskId: string, id: string): boolean;
1242
- /** Stop a structured task's running turn; it then waits for input like any turn that ends without a `task_complete` call. */
1243
- interruptTask(taskId: string): Promise<void>;
1244
- /** What is waiting for a structured task's turn to end; empty for any other task. */
1245
- getQueuedTaskMessages(taskId: string): QueuedTaskMessage[];
1246
- private runnerSession;
1247
- /**
1248
- * A structured turn that ended without a `task_complete` call (ADR-0018, W1). No
1249
- * verdict is guessed: the task waits for input, unless a checkpoint in the
1250
- * same turn already has it waiting, or a queued message went straight out —
1251
- * the session never passes through idle then, so nothing flickers. The first
1252
- * turn of an attempt to end that way, cleanly, gets the completion reminder
1253
- * instead: weaker models often finish the work and report in prose.
1254
- */
1255
- private onTurnEnd;
1256
- subscribe(observer: OrchestratorObserver): () => void;
1257
- /**
1258
- * Isolated per observer: one that throws must not take down the scheduler
1259
- * or stop the remaining observers from hearing the event.
1260
- */
1261
- private emit;
1262
- get isRunning(): boolean;
1263
- /**
1264
- * A runner is executing *right now*. Narrower than {@link isRunning}, which
1265
- * also covers the armed-but-idle scheduler `tick()` deliberately leaves
1266
- * behind when a plan is paused on a user task, a checkpoint or a hold —
1267
- * nothing is executing then, so nothing is reading the plan mid-mutation.
1268
- */
1269
- get hasLiveWork(): boolean;
1270
- get isReviewApproved(): boolean;
1271
- /** The scheduler is driving the plan: ready tasks start on their own as slots free. Narrower than {@link isRunning}. */
1272
- get isScheduling(): boolean;
1273
- /**
1274
- * A full-plan run with nothing starting that something still resumes: a
1275
- * usage limit's reset, or a retry of a failed task its blocked tasks wait on.
1276
- */
1277
- get awaitsResume(): boolean;
1278
- get status(): 'approved' | 'running' | 'completed';
1279
- get activeTaskIds(): string[];
1280
- /** Task id → session id of every attempt whose runner is up; a spawn in flight has no session yet. */
1281
- get activeSessionMap(): Map<string, string>;
1282
- getAttempt(taskId: string): TaskAttemptSnapshot | undefined;
1283
- /** A planner turn holds new starts: what is ready starts when it releases them. */
1284
- get startsHeld(): boolean;
1285
- /**
1286
- * Hold the scheduler's new starts while a planner turn decides what runs
1287
- * next, so its edits do not race a task that would otherwise start under
1288
- * it. Running attempts are untouched. The release is idempotent and starts
1289
- * nothing itself: whoever releases ticks, which starts what became ready.
1290
- */
1291
- holdStarts(): () => void;
1292
- /**
1293
- * "Merge all"; a settled run that merged everything is cleared up and
1294
- * forgotten. During a run it merges what has landed and the run goes on:
1295
- * the tasks waiting at a merge gate for that work then start (ADR-0020).
1296
- * Never alongside ops work, either way round ({@link mergeExcludes}).
1297
- */
1298
- mergeRun(): Promise<IsolationMergeResult>;
1299
- private mergeLanded;
1300
- /**
1301
- * Close the settled run. With merge at gates automatically on, landed work
1302
- * not merged yet is merged first, through the gate's path, so no surface
1303
- * hears of a handoff for a run about to merge; one that stopped is handed
1304
- * over as before.
1305
- */
1306
- private closeRun;
1307
- /**
1308
- * The run as a surface offers Merge all mid-run (ADR-0020): what has landed
1309
- * and is not merged, and whether the run is paused at its gates with nothing
1310
- * else running. Null once everything landed is merged.
1311
- */
1312
- runMergeView(): RunMergeView | null;
1313
- /**
1314
- * The dependencies an ops or user task waits on at its merge gate: those
1315
- * whose landed work is not in the user's branch yet (ADR-0020). Empty once
1316
- * the task has started, and for every other task.
1317
- */
1318
- getMergeGate(taskId: string): string[];
1319
- /** The failed tasks a task not started yet waits on, through its dependencies (ADR-0034); empty when it is not blocked. */
1320
- getBlockedBy(taskId: string): string[];
1321
- loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
1322
- /**
1323
- * Adopt an edited plan without letting go of the run in progress. Everything
1324
- * the scheduler owns — live sessions, holds, retry counts, the verifier and
1325
- * the review approval — survives, because `loadPlan` clears all of it and a
1326
- * mid-run edit is not a new run. Only the tasks change.
1327
- *
1328
- * The ids with live sessions are defended: one dropped from the edited plan is
1329
- * carried over (its verdict still has to land somewhere), and one whose status
1330
- * the snapshot predates is put back to `in_progress` — adopting the snapshot's
1331
- * "pending" would offer the scheduler work a runner is already doing.
1332
- *
1333
- * A failed task stays failed, unlike a plan load's fresh chance: an edit is
1334
- * not a retry, and the tasks it blocks wait for that retry (ADR-0034).
1335
- */
1336
- reconcilePlan(newTasks: Task[], planRunners?: RunnerId[]): void;
1337
- start(origin?: RunOrigin): Promise<void>;
1338
- /** A usage-limit pause holds the run: spawning more would spend the same exhausted limit. */
1339
- private holdOnLimit;
1340
- stop(): void;
1341
- /** Name the tasks a stop took down, so none is mistaken for one still at work. */
1342
- private sayStopped;
1343
- /**
1344
- * A task in progress with no attempt has nothing running it: a stop, a plan
1345
- * load or anything else that ended its attempt left its status behind. It
1346
- * is put back to not started, and said, so it can be retried rather than
1347
- * read as running — or, in an isolated run, count as live work forever.
1348
- */
1349
- private recoverOrphans;
885
+ | {
886
+ type: 'message_delivered';
887
+ messageId: string;
888
+ text: string;
889
+ from?: TaskMessageFrom;
890
+ }
891
+ /** The agent's own words for a failed turn. */
892
+ | {
893
+ type: 'error';
894
+ message: string;
895
+ }
896
+ /**
897
+ * The runner asked to use a tool its mode does not cover (ADR-0018, A1).
898
+ * `approvalId` is what the answer is given under; `args` is the call's
899
+ * arguments as JSON; `allowForTask` says the runner offered its own
900
+ * session-scoped grant. `decided` is set when the request was never left
901
+ * open — an Ordewell tool or an auto-approving mode — so a surface does not
902
+ * offer to answer what is already answered (it is still followed by the
903
+ * matching `approval_decided`, which a reader may rely on).
904
+ */
905
+ | {
906
+ type: 'approval_requested';
907
+ approvalId: string;
908
+ tool: string;
909
+ args: string;
910
+ allowForTask: boolean;
911
+ toolCallId?: string;
912
+ decided?: ApprovalDecision['decision'];
913
+ } | {
914
+ type: 'approval_decided';
915
+ approvalId: string;
916
+ decision: ApprovalDecision['decision'];
917
+ note?: string;
918
+ }
919
+ /** The request went unanswered: the runner withdrew it, or its process ended. */
920
+ | {
921
+ type: 'approval_withdrawn';
922
+ approvalId: string;
923
+ }
924
+ /**
925
+ * The runner's process ended. A call it had not reported back on is not
926
+ * running any more: a task's runner is ended the moment its `task_complete`
927
+ * lands, often before it reports that call's result.
928
+ */
929
+ | {
930
+ type: 'runner_ended';
931
+ };
932
+ /**
933
+ * Keep a tool result's head and tail. A build log or a whole file read can
934
+ * run to megabytes, and every attempt's file keeps it for good; the ends are
935
+ * where a reader looks — the command's start, and how it finished.
936
+ */
937
+ declare function trimToolOutput(output: string): {
938
+ output: string;
939
+ omittedLines?: number;
940
+ };
941
+ /**
942
+ * The log entry for one structured event, or null for what the log does not
943
+ * keep: background-agent launches, which the subagent's own events already show.
944
+ */
945
+ declare function toTaskLogEvent(event: StructuredEvent): TaskLogEvent | null;
946
+ /** A log as a surface is sent it, live or read back: everything but the skills' bodies. */
947
+ declare function taskLogForSurface(events: readonly TaskLogEvent[]): TaskLogEvent[];
948
+ /**
949
+ * Merge runs of deltas into one event each. Deltas arrive a few characters at
950
+ * a time; merged, a batch is one message on the wire and one line on disk,
951
+ * and the view folds to the same blocks either way.
952
+ */
953
+ declare function coalesceTaskLog(events: readonly TaskLogEvent[]): TaskLogEvent[];
954
+ /**
955
+ * What an attempt did to the world, for the retry of a task whose effects
956
+ * outlive it (ADR-0020): one line per tool call and how it ended.
957
+ */
958
+ declare function digestTaskLog(events: readonly TaskLogEvent[], maxCalls: number): string;
959
+
960
+ type SerializedTaskStatus = {
961
+ id: string;
962
+ status: Task['status'];
963
+ verdict: {
964
+ outcome: 'pass' | 'fail';
965
+ reason: string;
966
+ checks: Verdict['checks'];
967
+ } | null;
968
+ /** Advisory silence timestamp from VerdictEngine — not part of task status semantics. */
969
+ idleSince?: string | null;
970
+ /** Absent unless the plan has an isolation run, so a shared-root plan's updates are unchanged. */
971
+ isolation?: TaskIsolation;
972
+ /** What an `awaiting_user` task waits on, when it was saved (ADR-0018, W1). */
973
+ awaitingReason?: AwaitingReason;
974
+ /** When a task paused on a usage limit resumes on its own (ISO time); absent when it waits for a manual retry. */
975
+ resumeAt?: string;
976
+ /** Why a `skipped` task was skipped; absent when no reason was given, and on any other status. */
977
+ skipReason?: string;
978
+ /** The whole question of the checkpoint the task waits at; absent when it waits at none. */
979
+ checkpoint?: string;
980
+ /** Messages waiting for a structured task's turn to end, oldest first; absent when there are none. */
981
+ queued?: QueuedTaskMessage[];
982
+ /** Set when the task can be continued in its saved runner session (ADR-0018, K1); the id itself stays in the daemon. */
983
+ continuable?: true;
1350
984
  /**
1351
- * Task ids still live, whose worktrees no clean-up of the whole run may
1352
- * take: an attempt is running them, or the plan has them in progress or
1353
- * waiting on the user mid-attempt — at a checkpoint, or for input. One
1354
- * waiting on the user's decision instead (a conflict, a usage limit, files
1355
- * an ops task changed) is not: no runner is at work on it, and a clean-up
1356
- * keeps what it holds on a branch before its worktree goes.
985
+ * How many of a structured task's runner requests wait for an answer
986
+ * (ADR-0018, A1) — "waiting for approval", which leaves `status` alone.
987
+ * Absent when none do.
1357
988
  */
1358
- private liveTaskIds;
989
+ awaitingApproval?: number;
1359
990
  /**
1360
- * Whether the run may close with its handoff: no task is live. A handoff
1361
- * tells the user the run has finished, and a Merge all after it clears the
1362
- * run up, so neither may happen under a task the plan still shows at work.
991
+ * The ancestors an ops or user task waits on at its merge gate: their
992
+ * work has landed but is not merged into the user's branch (ADR-0020).
993
+ * Absent when it waits for no Merge all.
1363
994
  */
1364
- private get settled();
995
+ mergeGate?: string[];
996
+ /** The ancestors, by title, a force start went past the merge gate of (ADR-0020). */
997
+ forcedPastGate?: string[];
1365
998
  /**
1366
- * The boundary a verdict crosses: nothing awaits the verifier's listener, so
1367
- * a throw from reading the output, landing, releasing a worktree or the tick
1368
- * after would be an unhandled rejection — and an attempt left integrating is
1369
- * live work forever. One that never settled fails (a repair waits on the
1370
- * user, as its conflict did) and blocks only its dependents; either way the
1371
- * run says why and goes on.
999
+ * Added after the user approved the plan and not approved since, so the
1000
+ * scheduler leaves it for them (ADR-0035). Absent for any other task.
1372
1001
  */
1373
- private deliverVerdict;
1374
- /** Work nothing awaits: a rejection is said, not left unhandled. */
1375
- private unawaited;
1376
- private onVerdict;
1377
- private afterVerdict;
1378
- /**
1379
- * A repair's verdict never replaces the verdict the task's own work earned,
1380
- * and a repair that does not land leaves the task waiting on the user —
1381
- * never a failed task, so never one that blocks its dependents.
1382
- */
1383
- private settleRepair;
1384
- /**
1385
- * Hand an attempt's verdict to Landing. The attempt stays live while that
1386
- * settles — it may wait on the module's merge queue — so a cancel, retry or
1387
- * stop in that window still wins, and waits for the merge before tearing the
1388
- * worktree down; nothing counts the task as done before its work is on the
1389
- * integration branch.
1390
- */
1391
- private land;
1392
- /** Settle a task by how its landing went; `onLanded` is what the path that landed it adds to completing it. */
1393
- private applyLanding;
1394
- /**
1395
- * Work that did not land waits on the user — it never fails the task or
1396
- * blocks its dependents — unless its conflict is owed a repair, which takes
1397
- * the slot the ending attempt freed and never one more than the run allows.
1398
- */
1399
- private settleUnlanded;
1400
- private landResolved;
1401
- getReadyTasks(): Task[];
1402
- private readiness;
1403
- private mergesAutomatically;
1404
- /**
1405
- * The user changed the run settings under an open run (ADR-0030). The
1406
- * scheduler reads them at each decision it makes from them, so a merge gate
1407
- * already reached is merged now rather than at the next event. Fire-and-forget
1408
- * — no caller waits on the change — and its failure is said through
1409
- * {@link unawaited}, never left unhandled.
1410
- */
1411
- runSettingsChanged(): void;
1412
- private openRun;
1413
- /**
1414
- * Open the run a task starts in, and decide whether it isolates if the
1415
- * task's next attempt needs that ({@link decidesIsolation}). False while a
1416
- * dirty tree waits on the user; `resume` is what their choice replays.
1417
- */
1418
- private openFor;
1419
- /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
1420
- private nextAttemptKind;
1421
- /**
1422
- * Cancel a running (or scheduled) task: kill its session and return it to
1423
- * 'pending' — "not executed". The task is put on hold so the scheduler
1424
- * doesn't immediately restart it; Retry / Force Start release the hold.
1425
- *
1426
- * The attempt's worktree is kept, as a stopped or failed one is: a runner is
1427
- * often cancelled because it looked stuck after doing the work, and Mark
1428
- * complete can still land that work. The next attempt replaces it.
1429
- */
1430
- cancelTask(taskId: string): Promise<void>;
1431
- private cancelAttempt;
1432
- /**
1433
- * Let go of a task that is leaving the plan. A live runner is cancelled
1434
- * as {@link cancelTask} does, but its worktree goes: no task is left to land
1435
- * it into. A spawn still in flight just loses its attempt,
1436
- * which is what makes {@link startTask} kill the session it is about to
1437
- * receive. The id's cross-attempt bookkeeping goes too — a hold or retry
1438
- * count kept for a task that no longer exists would be inherited by nothing.
1439
- */
1440
- releaseTask(taskId: string): Promise<void>;
1441
- markTaskComplete(taskId: string): Promise<void>;
1442
- /**
1443
- * Undo a completion: return the task to "not executed" — pending, verdict and
1444
- * summary dropped, archive entry removed. It is put on hold like a cancel, so
1445
- * a running plan does not immediately re-spawn the work the user just
1446
- * un-marked; Retry / Force Start / Run release the hold. Dependents fall back
1447
- * to waiting on their own, because the scheduler gates on `isCompleted`.
1448
- */
1449
- markTaskIncomplete(taskId: string): Promise<void>;
1450
- /**
1451
- * Skip: end a task as `skipped`, with no verdict and nothing of its work
1452
- * landed. Its worktree is kept, as a failed attempt's is, so the user can
1453
- * still look at it or Mark complete it later. Its dependents go ahead
1454
- * without it, and a run that ended on a failure continues for them, as a
1455
- * retry would continue it. A running task is stopped before it is skipped.
1456
- */
1457
- skipTask(taskId: string, reason?: string, origin?: RunOrigin): Promise<void>;
1458
- private logAndArchive;
1459
- retryTask(taskId: string, origin?: RunOrigin): Promise<void>;
1460
- /**
1461
- * Continue a finished structured task in its saved runner session (ADR-0018,
1462
- * K1): a retry whose first turn is the user's message, sent to the session
1463
- * the task ended in rather than a fresh one given its prompt again. The
1464
- * worktree keeps the same path, where the runner keeps its sessions, and
1465
- * restores the previous work so that session's memory matches its files.
1466
- */
1467
- continueTask(taskId: string, message: string, origin?: RunOrigin): Promise<void>;
1468
- /**
1469
- * Keep a task paused on its runner's usage limit from resuming on its own:
1470
- * it waits for a manual retry, as one whose reset time could not be read
1471
- * does. What a surface's cancel calls, and what moving the task to another
1472
- * runner has to call first. False when no resume was scheduled for it.
1473
- */
1474
- cancelScheduledResume(taskId: string): boolean;
1475
- /** Run before writing assignments so cancellation sees the old runner's shared wait. */
1476
- releaseMovedResume(taskId: string, changes: Partial<Task>): Partial<Task>;
1477
- /**
1478
- * A task's attempt stopped on its runner's usage limit, without a
1479
- * completion call. Failing would paint a red X on a task that only has to
1480
- * wait, and spawning more tasks would spend the same exhausted limit, so the
1481
- * task pauses and the run holds. When the runner said when its limit
1482
- * resets, the task joins its runner's wait and resumes on its own then
1483
- * (#84); otherwise, or when it was resumed a moment ago and hit the limit
1484
- * again, it waits for the user to retry it.
1485
- */
1486
- private pauseOnLimit;
1487
- /**
1488
- * The limit arrived as the turn's error with the runner still up: the
1489
- * attempt ends here, as its verdict would have ended it, so a resume is a
1490
- * continue in a fresh process (ADR-0018, L1/K1).
1491
- */
1492
- private stopOnLimit;
1493
- /** The tasks waiting on `runner`'s reset to resume on their own. */
1494
- private waitingOn;
1495
- /**
1496
- * Arm `runner`'s one wait for the latest resume time among its tasks, and
1497
- * put that time on each of them, so they resume together.
1498
- */
1499
- private armLimitWait;
1500
- /**
1501
- * The waits a loaded plan still owes. A resume time that passed while
1502
- * nothing was running — the daemon was down, say — resumes at once.
1503
- */
1504
- private rearmLimitWaits;
1505
- /**
1506
- * Resume a task paused on its runner's usage limit now, in its saved
1507
- * session, once the user says the limit has reset early. The limit is the
1508
- * runner's account's, so every task waiting on that runner's reset resumes
1509
- * with it, as at the reset itself. `message` follows the reset notice in the
1510
- * same continuation. A resumed attempt that hits the limit again at once
1511
- * waits for the user, as an automatic resume's does: that is what keeps a
1512
- * caller from resuming it round and round.
1513
- */
1514
- resumeNow(taskId: string, origin?: RunOrigin, message?: string): Promise<void>;
1515
- private resumeAfterLimit;
1516
- /**
1517
- * Retry a task paused on a usage limit, continuing its saved session where
1518
- * it left one (ADR-0018, K1), then release the hold the pause put on the
1519
- * run once nothing else waits on a reset.
1520
- */
1521
- private resumeTask;
1522
- /**
1523
- * Manually start a single AI task right now, bypassing dependency/readiness
1524
- * gating (the "force start" affordance on a task card). Reuses the scheduler's
1525
- * own startTask so a force-started task gets the same augmented prompt, session
1526
- * tracking, and exit handling — callers must not re-spawn the runner themselves.
1527
- * No-op if the task is unknown, not an AI task, or already running.
1528
- */
1529
- forceStartTask(taskId: string, origin?: RunOrigin): Promise<void>;
1530
- private refuseOpsDuringMerge;
1531
- /**
1532
- * A force start passes a merge gate (ADR-0020), and the surface confirmed it
1533
- * first; the choice is kept on the task and said, because the work it acts
1534
- * on is not in the user's branch.
1535
- */
1536
- private notePastGate;
1537
- /**
1538
- * Run exactly one task outside full-plan scheduling. The active/starting
1539
- * session still contributes to {@link hasLiveWork} so every surface exposes
1540
- * Stop and disables Execute Plan, but onVerdict cannot auto-schedule other
1541
- * tasks because the plan scheduler's `running` flag remains false.
1542
- */
1543
- runTask(taskId: string): Promise<void>;
1544
- getCompletedCount(): number;
1545
- getTotalCount(): number;
1546
- isAllComplete(): boolean;
1547
- isAnyFailed(): boolean;
1548
- approveReview(origin?: RunOrigin): Promise<void>;
1549
- tick(): Promise<void>;
1550
- /** How a run that ended on failures ends: what it finished, what failed, and what that blocks. */
1551
- /** Skipped work is not completed work, so a run with any says so rather than that everything completed. */
1552
- private sayFinished;
1553
- private sayEndedOnFailure;
1554
- /** Whether the scheduler may start `id` now: it passes every readiness gate and a slot is free. */
1555
- private stillReady;
1556
- /**
1557
- * A run paused at merge gates says so, as plainly as a task that waits on
1558
- * the user: which tasks wait, and that Merge all lets them go on (ADR-0020).
1559
- */
1560
- private sayGate;
1561
- private startTask;
1562
- /**
1563
- * The fallible half of starting a task: resolving its cwd/worktree and
1564
- * spawning the runner. Resolves false when the attempt never committed —
1565
- * abandoned because it was ended from under it, or a failed spawn already
1566
- * unwound (both leave `startTask` with nothing further to announce).
1567
- */
1568
- private spawnAttempt;
1569
- /** For a skill the main checkout has, the folder a worktree only gets once it is committed. */
1570
- private uncommittedSkillFolder;
1571
- /**
1572
- * The roots whose `.ordewell/skills/` an attempt at `cwd` reads: in a repo
1573
- * group, `cwd` holds each repo — a worktree of it when isolated, the
1574
- * workspace's own checkout when not — and no folder of its own (ADR-0014).
1575
- */
1576
- private skillRoots;
1577
- /**
1578
- * A continue whose runner never announced the session it was told to
1579
- * resume. Nothing was continued, and nothing fresh was started in its place.
1580
- */
1581
- private unresumed;
1582
- /**
1583
- * A spawn whose attempt ended while it was in flight: kill what it produced
1584
- * and take back the claim it made. Only the claim — whatever ended the
1585
- * attempt may have decided the task since (mark complete, cancel, retry).
1586
- */
1587
- private abandonSpawn;
1588
- private tell;
1589
- private say;
1590
- /**
1591
- * Replay the start a dirty tree turned away. `stash` puts the user's tracked
1592
- * changes on the git stash first, so the run isolates; `shared` runs this one
1593
- * run in the workspace root, knowingly.
1594
- */
1595
- continueBlockedRun(how: 'stash' | 'shared'): Promise<void>;
1596
- /**
1597
- * The one way an attempt ends. Dropping the record is itself what
1598
- * invalidates a spawn still in flight ({@link startTask} kills the session
1599
- * it receives for an attempt that is no longer current).
1600
- *
1601
- * A user interruption also bumps the verifier's generation *before* stopping
1602
- * the runner: a runner may fire onExit synchronously from
1603
- * stop(), and if that exit reaches VerdictEngine under the still-valid
1604
- * generation it delivers a verdict that marks the task 'completed' for one
1605
- * tick — long enough for the scheduler to start a dependent task. Stop and
1606
- * load reset the whole verifier themselves.
1607
- */
1608
- private endAttempt;
1609
- /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
1610
- private saveNativeSession;
1611
- private endAllAttempts;
1002
+ waitsForApproval?: true;
1003
+ attemptUsage?: AttemptUsage[];
1004
+ };
1005
+ /**
1006
+ * Landed work not yet in the user's branch. `paused` means the running plan
1007
+ * waits at a merge gate with no attempts active.
1008
+ */
1009
+ interface RunMergeView {
1010
+ waitingAtGate: boolean;
1011
+ paused: boolean;
1012
+ repos: IsolationHandoff['repos'];
1013
+ landed: IsolationHandoff['landed'];
1612
1014
  }
1613
-
1614
- type SerializedTaskStatus = {
1015
+ type SerializedTask = {
1615
1016
  id: string;
1616
- status: Task['status'];
1617
- verdict: {
1618
- outcome: 'pass' | 'fail';
1619
- reason: string;
1620
- checks: Verdict['checks'];
1621
- } | null;
1622
- /** Advisory silence timestamp from VerdictEngine — not part of task status semantics. */
1623
- idleSince?: string | null;
1624
- /** Absent unless the plan has an isolation run, so a shared-root plan's updates are unchanged. */
1625
- isolation?: TaskIsolation;
1626
- /** What an `awaiting_user` task waits on, when it was saved (ADR-0018, W1). */
1627
- awaitingReason?: AwaitingReason;
1628
- /** When a task paused on a usage limit resumes on its own (ISO time); absent when it waits for a manual retry. */
1629
- resumeAt?: string;
1630
- /** Why a `skipped` task was skipped; absent when no reason was given, and on any other status. */
1631
- skipReason?: string;
1632
- /** The whole question of the checkpoint the task waits at; absent when it waits at none. */
1633
- checkpoint?: string;
1634
- /** Messages waiting for a structured task's turn to end, oldest first; absent when there are none. */
1635
- queued?: QueuedTaskMessage[];
1636
- /** Set when the task can be continued in its saved runner session (ADR-0018, K1); the id itself stays in the daemon. */
1637
- continuable?: true;
1638
- /**
1639
- * How many of a structured task's runner requests wait for an answer
1640
- * (ADR-0018, A1) — "waiting for approval", which leaves `status` alone.
1641
- * Absent when none do.
1642
- */
1643
- awaitingApproval?: number;
1644
- /**
1645
- * The dependencies an ops or user task waits on at its merge gate: their
1646
- * work has landed but is not merged into the user's branch (ADR-0020).
1647
- * Absent when it waits for no Merge all.
1648
- */
1649
- mergeGate?: string[];
1650
- /** The dependencies, by title, a force start went past the merge gate of (ADR-0020). */
1651
- forcedPastGate?: string[];
1652
- attemptUsage?: AttemptUsage[];
1653
- };
1654
- /**
1655
- * Landed work not yet in the user's branch. `paused` means the running plan
1656
- * waits at a merge gate with no attempts active.
1657
- */
1658
- interface RunMergeView {
1659
- waitingAtGate: boolean;
1660
- paused: boolean;
1661
- repos: IsolationHandoff['repos'];
1662
- landed: IsolationHandoff['landed'];
1663
- }
1664
- type SerializedTask = {
1665
- id: string;
1666
- order: number;
1667
- title: string;
1668
- type: string;
1669
- /** Without it a plan broadcast after a run reads as every task pending. */
1017
+ order: number;
1018
+ title: string;
1019
+ type: string;
1020
+ /** Without it a plan broadcast after a run reads as every task pending. */
1670
1021
  status: Task['status'];
1671
1022
  description: string;
1672
1023
  dependencies: string[];
@@ -1733,6 +1084,22 @@ type SerializedPlan = {
1733
1084
  conversationHistory?: SerializedConversationMessage[];
1734
1085
  prdMarkdown?: string;
1735
1086
  };
1087
+ /** What a held edit does to a task once it lands. */
1088
+ type HeldEditMark = 'added' | 'changed' | 'removed';
1089
+ /**
1090
+ * The plan as the open planner turn's edit would leave it, in plan order with
1091
+ * the removed tasks where they were; flat, a subtask after its parent. `label`
1092
+ * is the task's `#2.1` reference; `mark` is absent for a task the edit leaves
1093
+ * alone. Proposed, not approved: the tasks need the user's go-ahead to run.
1094
+ */
1095
+ interface HeldEdit {
1096
+ turnId: string;
1097
+ tasks: Array<{
1098
+ label: string;
1099
+ mark?: HeldEditMark;
1100
+ task: SerializedTask;
1101
+ }>;
1102
+ }
1736
1103
  /** How a planner turn ended: the reply kind it settled on, a user stop, or a failure. */
1737
1104
  type PlannerTurnOutcome = 'message' | 'plan' | 'task_ops' | 'stopped' | 'error';
1738
1105
  /**
@@ -1798,6 +1165,16 @@ type SessionMessage =
1798
1165
  turnId: string;
1799
1166
  skill: SkillLoadNotice;
1800
1167
  }
1168
+ /**
1169
+ * What the open planner turn's task edit would leave, sent whenever the
1170
+ * edit it holds changes (ADR-0029: it lands as the reply ends). `held` is
1171
+ * null once nothing is held: the edit landed, was refused as it landed, or
1172
+ * the turn ended without it. The plan itself is untouched until it lands.
1173
+ */
1174
+ | {
1175
+ type: 'plan_edit_held';
1176
+ held: HeldEdit | null;
1177
+ }
1801
1178
  /**
1802
1179
  * A planner turn is over; nothing more carries its `turnId`. Emitted exactly
1803
1180
  * once per `planner_turn_started`, stop and failure included, after the
@@ -2007,7 +1384,7 @@ type PlanSnapshot = {
2007
1384
  plan: SurfacePlan | SurfacePlanState;
2008
1385
  };
2009
1386
  /** What only the status stream says about a task; a plan refresh keeps these as they were. */
2010
- type RunTaskStatus = Partial<Pick<SerializedTaskStatus, 'verdict' | 'idleSince' | 'isolation' | 'checkpoint' | 'queued' | 'continuable' | 'awaitingApproval' | 'mergeGate' | 'forcedPastGate'>>;
1387
+ type RunTaskStatus = Partial<Pick<SerializedTaskStatus, 'verdict' | 'idleSince' | 'isolation' | 'checkpoint' | 'queued' | 'continuable' | 'awaitingApproval' | 'mergeGate' | 'forcedPastGate' | 'waitsForApproval'>>;
2011
1388
  /** A task in the run view: the plan's fields, then the status stream's. */
2012
1389
  type RunTask = Omit<SerializedTask, 'subtasks'> & RunTaskStatus & {
2013
1390
  subtasks: RunTask[];
@@ -2025,6 +1402,8 @@ interface RunView {
2025
1402
  usage?: PlanUsage;
2026
1403
  handoff: IsolationHandoff | null;
2027
1404
  mergeResult: IsolationMergeResult | null;
1405
+ /** The edit the open planner turn holds, as it would leave the plan; absent when it holds none. */
1406
+ heldEdit?: HeldEdit;
2028
1407
  }
2029
1408
  type RunInput = SessionMessage | PlanSnapshot;
2030
1409
  /**
@@ -2040,7 +1419,7 @@ type SessionNotice = {
2040
1419
  };
2041
1420
  type SessionBroadcaster = (msg: SessionMessage) => void;
2042
1421
  declare function serializeTask(t: SurfaceTask): SerializedTask;
2043
- declare function serializeTaskStatus(t: Task, idleSince?: string | null, isolation?: TaskIsolation | null, queued?: readonly QueuedTaskMessage[], awaitingApproval?: number, mergeGate?: readonly string[], checkpoint?: string): SerializedTaskStatus;
1422
+ declare function serializeTaskStatus(t: Task, idleSince?: string | null, isolation?: TaskIsolation | null, queued?: readonly QueuedTaskMessage[], awaitingApproval?: number, mergeGate?: readonly string[], checkpoint?: string, waitsForApproval?: boolean): SerializedTaskStatus;
2044
1423
  /** The one place a plan's skill bodies come off before it leaves the session: every wire path that carries plan state goes through here. */
2045
1424
  declare function surfacePlan({ queuedMessages: _carriedOverAtLoad, ...plan }: LegacyPlanState): SurfacePlan;
2046
1425
  declare function surfacePlanState(state: PlanState): SurfacePlanState;
@@ -2057,10 +1436,762 @@ interface ExecutionSummary {
2057
1436
  blocked: number;
2058
1437
  skipped: number;
2059
1438
  }
2060
- declare function executionSummary(tasks: readonly Task[]): ExecutionSummary;
2061
- declare const CHECKPOINT_TRUNCATE_LENGTH = 120;
2062
- /** Truncate a checkpoint summary to a single line of at most CHECKPOINT_TRUNCATE_LENGTH chars. */
2063
- declare function truncateCheckpointSummary(summary: string): string;
1439
+ declare function executionSummary(tasks: readonly Task[]): ExecutionSummary;
1440
+ declare const CHECKPOINT_TRUNCATE_LENGTH = 120;
1441
+ /** Truncate a checkpoint summary to a single line of at most CHECKPOINT_TRUNCATE_LENGTH chars. */
1442
+ declare function truncateCheckpointSummary(summary: string): string;
1443
+
1444
+ /** What ran an automatic Merge all: a task reaching its merge gate, or the run settling with landed work unmerged. */
1445
+ type AutoMergeTrigger = {
1446
+ kind: 'gate';
1447
+ taskId: string;
1448
+ } | {
1449
+ kind: 'run_end';
1450
+ };
1451
+ /**
1452
+ * Who opened a run (ADR-0029): a call the user made, or one made for them; a
1453
+ * call of the supervisor's own; or a usage-limit reset resuming what it paused.
1454
+ * Only the user's starts a run's supervision afresh.
1455
+ */
1456
+ type RunOrigin = 'user' | 'supervisor' | 'limit-reset';
1457
+ /** What the scheduler reads of the open run's settings, at each decision it makes from them. */
1458
+ type SchedulerRunSettings = Pick<RunSettings, 'mergeAtGatesAutomatically'>;
1459
+ /**
1460
+ * A start someone asked for, from the moment it was admitted until it has
1461
+ * taken its claim or been let go (ADR-0029): a start waiting on the user's
1462
+ * answer, on a held edit, or on the run to open. Stop counts it as live work,
1463
+ * and a Stop since it was admitted refuses its claim.
1464
+ */
1465
+ interface StartAdmission {
1466
+ /** No Stop came since it was admitted. */
1467
+ current(): boolean;
1468
+ /** It claimed, or will not: Stop no longer counts it. Idempotent. */
1469
+ release(): void;
1470
+ }
1471
+ /** What a start the user's Stop withdrew answers, wherever it was waiting. */
1472
+ declare const STOPPED_BEFORE_START = "Not done: stopped before it could start. The run was stopped meanwhile; start it again if it is still wanted.";
1473
+ /**
1474
+ * The one notification channel out of the orchestrator. Everything that used
1475
+ * to travel over a separate callback (onRefresh) is an observer event; the
1476
+ * Session subscribes once and turns these into SessionMessages.
1477
+ */
1478
+ interface OrchestratorObserver {
1479
+ /** Any task-shaped state changed (store mutation, checkpoint, retry, …). */
1480
+ onTaskChanged?(): void;
1481
+ /**
1482
+ * A run reached an outcome that stands until the user acts — completed,
1483
+ * failed, or awaiting the user — on its own, not as the answer to a call.
1484
+ * Emitted before the `onTaskChanged` that announces it, so a listener can
1485
+ * save the outcome before any surface is told.
1486
+ */
1487
+ onTaskSettled?(data: {
1488
+ taskId: string;
1489
+ }): void;
1490
+ onRunStarted?(data: {
1491
+ origin: RunOrigin;
1492
+ }): void;
1493
+ /** An automatic Merge all settled, at a gate or at the run's end; failures are attention events for a supervisor. */
1494
+ onAutoMerge?(data: {
1495
+ trigger: AutoMergeTrigger;
1496
+ result: IsolationMergeResult;
1497
+ }): void;
1498
+ onTick?(): void;
1499
+ onUsageChanged?(): void;
1500
+ onExecutionComplete?(): void;
1501
+ /**
1502
+ * A task paused on its runner's usage limit, or its resume changed:
1503
+ * `resumeAt` is when it resumes on its own (ISO time), null when it waits
1504
+ * for a manual retry — the reset could not be read, the resume was
1505
+ * cancelled, or the resumed attempt hit the limit again at once.
1506
+ * `cancelled` marks the resume being cancelled, which is no new pause.
1507
+ */
1508
+ onUsageLimitWait?(data: {
1509
+ taskId: string;
1510
+ runner: string;
1511
+ resumeAt: string | null;
1512
+ cancelled?: true;
1513
+ }): void;
1514
+ /** A live task's runner went silent (ADR-0018's idle timer); paused while it waits on input, a checkpoint or an approval. */
1515
+ onTaskIdle?(data: {
1516
+ taskId: string;
1517
+ idleSince: string;
1518
+ }): void;
1519
+ onReviewNeeded?(data: {
1520
+ tasks: Task[];
1521
+ planRunners: RunnerId[];
1522
+ }): void;
1523
+ onReviewApproved?(data: {
1524
+ tasks: Task[];
1525
+ }): void;
1526
+ onCheckpoint?(data: {
1527
+ taskId: string;
1528
+ taskTitle: string;
1529
+ summary: string;
1530
+ }): void;
1531
+ /**
1532
+ * The isolation run record changed and should be persisted with the plan.
1533
+ * `detached` is the record of a run another plan has replaced since — a
1534
+ * release that outlived a plan load — for the session it belongs to.
1535
+ */
1536
+ onIsolationChanged?(detached?: IsolationRun): void;
1537
+ /** A run did not start: the tree is dirty, and the user picks stash or no isolation. `repos` names the dirty repos of a group. */
1538
+ onIsolationBlocked?(data: {
1539
+ reason: 'dirty';
1540
+ repos: string[];
1541
+ }): void;
1542
+ /** An isolated run settled; emitted before `onExecutionComplete`, which surfaces treat as terminal. */
1543
+ onIsolationHandoff?(handoff: IsolationHandoff): void;
1544
+ /**
1545
+ * What a run says about how it isolates — the fallback to the workspace root,
1546
+ * shared paths, copies, a stash. Beside the notification channel, which a
1547
+ * daemon may leave unwired, so a surface without toasts can still show it.
1548
+ */
1549
+ onIsolationNotice?(data: {
1550
+ level: 'info' | 'warn' | 'error';
1551
+ message: string;
1552
+ }): void;
1553
+ }
1554
+ /**
1555
+ * A message or interrupt a task cannot take: it is not running, or it is
1556
+ * waiting on something a message does not answer.
1557
+ * The request is wrong, not the orchestrator, so a surface says why.
1558
+ */
1559
+ declare class TaskControlError extends Error {
1560
+ constructor(message: string);
1561
+ }
1562
+ type AttemptPhase = 'starting' | 'running' | 'integrating';
1563
+ /** Read-only view of a task's live attempt. */
1564
+ interface TaskAttemptSnapshot {
1565
+ taskId: string;
1566
+ attempt: number;
1567
+ phase: AttemptPhase;
1568
+ sessionId: string | null;
1569
+ runner: string;
1570
+ cwd: string | null;
1571
+ startedAt: string;
1572
+ skills: readonly TaskSkillSnapshot[];
1573
+ }
1574
+ /**
1575
+ * The wiring a {@link TaskOrchestrator} runs on, every collaborator resolved.
1576
+ * {@link TaskOrchestrator.compose} is the only production caller; tests build
1577
+ * one through it too. The constructor makes nothing itself, so there is exactly
1578
+ * one place defaults live.
1579
+ */
1580
+ interface TaskOrchestratorDeps {
1581
+ config: IConfig;
1582
+ notifications: INotification;
1583
+ runner: IRunner;
1584
+ store: PlanStore;
1585
+ output: TaskOutputSource;
1586
+ /** The plan's isolation run and the open run's lifecycle (ADR-0013). */
1587
+ runs: IsolationRunController;
1588
+ /** A passed attempt's work onto the integration branch, and any conflict repair. */
1589
+ landing: Landing;
1590
+ registry: RunnerRegistry | null;
1591
+ /** The workspace root, read at call time — VS Code can change it. */
1592
+ workspaceRoot: () => string;
1593
+ /** The workspace's own variables for a task's cwd (ADR-0016). */
1594
+ workspaceEnv: (cwd: string) => Promise<WorkspaceEnv>;
1595
+ /** The skill catalog over workspace roots, as {@link workspaceSkillRoots} orders them — read at each spawn. */
1596
+ skillsAt: (roots: readonly string[]) => SkillLookup;
1597
+ /** What the task's last saved attempt did, or null when it left no log (ADR-0020). */
1598
+ previousAttemptFromLog: (taskId: string) => string | null;
1599
+ /**
1600
+ * The open run's settings, read as each automatic merge is decided rather
1601
+ * than copied when the run opens, so a change to the run under way applies
1602
+ * at once (ADR-0030); absent before any run.
1603
+ */
1604
+ runSettings: () => SchedulerRunSettings | undefined;
1605
+ /**
1606
+ * The user's approval record, read at each start (ADR-0035): the Session
1607
+ * owns it, and the scheduler starts nothing outside it.
1608
+ */
1609
+ isApproved: (taskId: string) => boolean;
1610
+ }
1611
+ /**
1612
+ * What a caller supplies to {@link TaskOrchestrator.compose}; every optional
1613
+ * one gets its default there. This is what a host or test chooses, not the
1614
+ * resolved wiring the orchestrator itself runs on.
1615
+ */
1616
+ interface TaskOrchestratorOptions {
1617
+ config: IConfig;
1618
+ notifications: INotification;
1619
+ runner: IRunner;
1620
+ store?: PlanStore;
1621
+ output?: TaskOutputSource;
1622
+ isolation?: IWorktreeIsolation;
1623
+ registry?: RunnerRegistry | null;
1624
+ workspaceRoot?: () => string;
1625
+ workspaceEnv?: (cwd: string) => Promise<WorkspaceEnv>;
1626
+ skillsAt?: (roots: readonly string[]) => SkillLookup;
1627
+ previousAttemptFromLog?: (taskId: string) => string | null;
1628
+ runSettings?: () => SchedulerRunSettings | undefined;
1629
+ /** Every task is approved unless a Session supplies its record: an orchestrator on its own has no user to ask. */
1630
+ isApproved?: (taskId: string) => boolean;
1631
+ }
1632
+ /**
1633
+ * The pure scheduler. Owns execution state (`running`, `planStatus`,
1634
+ * `reviewApproved`, the live task attempts, the start hold) and the verifier.
1635
+ * All task-shaped state — the plan tree, the flat index, the completed set
1636
+ * — lives in {@link PlanStore}, injected at construction. The orchestrator
1637
+ * calls `store.markCompleted(id)` / `store.markFailed(id)` instead of mutating
1638
+ * task state directly. A task completes only after the runner reports it done
1639
+ * through `task_complete` on its attempt's token; process exit without that
1640
+ * evidence is a visible failure and does not unblock dependent work.
1641
+ */
1642
+ declare class TaskOrchestrator {
1643
+ private config;
1644
+ private notifications;
1645
+ private runner;
1646
+ private store;
1647
+ private output;
1648
+ private attempts;
1649
+ private verifier;
1650
+ private running;
1651
+ private planStatus;
1652
+ /** Planner turns in flight that new starts wait for (ADR-0029); running attempts carry on. */
1653
+ private startHolds;
1654
+ /** Bumped by the user's Stop: a start admitted under an older generation never takes its claim. */
1655
+ private stopGeneration;
1656
+ /** Starts admitted and not yet claimed or let go; see {@link StartAdmission}. */
1657
+ private admissions;
1658
+ private reviewApproved;
1659
+ private autoMergeBlocked;
1660
+ private mergesInFlight;
1661
+ private retryCounts;
1662
+ private spawnCounts;
1663
+ /** A full-plan run a usage-limit pause holds: no new start until the reset resumes it. */
1664
+ private heldOnLimit;
1665
+ /**
1666
+ * A full-plan run that ended with failed tasks and the tasks they block
1667
+ * (ADR-0034). A retry of a failed task continues it, so what it blocked
1668
+ * starts once it passes — without this the retried task ran alone.
1669
+ */
1670
+ private endedOnFailure;
1671
+ /** A Merge all is under way: no ops task starts until it ends (ADR-0020). */
1672
+ private merging;
1673
+ /** The last "waiting for Merge all" notice, so a run paused at a gate says it once, not on every tick. */
1674
+ private gateNotice;
1675
+ /** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
1676
+ private workspaceEnv;
1677
+ /** What the workspace env has already warned about, so a run says it once, not per task. */
1678
+ private envWarnings;
1679
+ /**
1680
+ * Tasks pulled out of auto-scheduling (user-cancelled or failed to spawn).
1681
+ * They stay 'pending' — "not executed" — but the scheduler skips them until
1682
+ * the user retries or force-starts, which would otherwise loop forever on a
1683
+ * task whose spawn always throws.
1684
+ */
1685
+ private onHold;
1686
+ /**
1687
+ * Tasks Mark complete is landing after it ended their attempt: in progress
1688
+ * with no attempt for as long as that takes, and not orphans for it.
1689
+ */
1690
+ private completing;
1691
+ /** One wait per runner for its usage limit to reset; the tasks it resumes, and when, are on the plan (#84). */
1692
+ private limitWaits;
1693
+ /**
1694
+ * The plan's isolation run and the open run's lifecycle (ADR-0013); see
1695
+ * {@link IsolationRunController}. Read and acted on there directly, except
1696
+ * Merge all: that goes through {@link mergeRun}, which keeps it apart from
1697
+ * ops tasks and starts the tasks its merge lets through.
1698
+ */
1699
+ readonly runs: IsolationRunController;
1700
+ /** A passed attempt's work onto the integration branch, and the conflict repair a landing may need. */
1701
+ private landing;
1702
+ private registry;
1703
+ private workspaceRootFn;
1704
+ private observers;
1705
+ private skillsAt;
1706
+ private previousAttemptFromLog;
1707
+ private runSettings;
1708
+ private isApproved;
1709
+ constructor(deps: TaskOrchestratorDeps);
1710
+ /**
1711
+ * The composition root: resolves every optional collaborator and wires the
1712
+ * isolation listener that the constructor cannot, because it needs the
1713
+ * instance's own (private) emit seam. Nothing else in the
1714
+ * codebase constructs an orchestrator, so this is the one place defaults
1715
+ * live — a host or test supplies what it cares about and gets the rest.
1716
+ */
1717
+ static compose(options: TaskOrchestratorOptions): TaskOrchestrator;
1718
+ /** Advisory silence timestamp for a task's live runner, or null if not idle. */
1719
+ getIdleSince(taskId: string): string | null;
1720
+ /**
1721
+ * What an ops retry is told about the attempt before. The saved log is read
1722
+ * whether or not the session was reloaded, so both behave alike; the output
1723
+ * buffer is the fallback for a runner that has no log. A reload clears the
1724
+ * attempt count along with the buffer, so a retry made since is what says
1725
+ * there was an attempt to report on.
1726
+ */
1727
+ private opsPreviousAttempt;
1728
+ /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
1729
+ getLiveOutput(taskId: string, opts: LiveTailOptions): LiveTail | null;
1730
+ get storeInstance(): PlanStore;
1731
+ /**
1732
+ * The variables a task's agent gets from its workspace. What cannot be
1733
+ * applied is said once — silently starting without them is how agents ran
1734
+ * under the wrong account when an edited `.envrc` was left unallowed.
1735
+ */
1736
+ private envForTask;
1737
+ approveCheckpoint(taskId: string): void;
1738
+ rejectCheckpoint(taskId: string, reason?: string): void;
1739
+ /** What the task's waiting checkpoint asks, whole; undefined once it settles or when none waits. */
1740
+ getCheckpointQuestion(taskId: string): string | undefined;
1741
+ awaitsCheckpoint(taskId: string): boolean;
1742
+ /**
1743
+ * Send a structured task a user message (ADR-0018, M1). Mid-turn it queues
1744
+ * behind the turn; a task waiting for input takes it at once and is back in
1745
+ * progress. Returns the message's id, for {@link removeQueuedTaskMessage}.
1746
+ */
1747
+ sendTaskMessage(taskId: string, text: string): string;
1748
+ /**
1749
+ * Force send (ADR-0023, F1–F3): interrupt the running turn and deliver the
1750
+ * message as the next one, ahead of anything queued, without the task
1751
+ * waiting for input in between. A task already waiting for input just takes it.
1752
+ */
1753
+ forceSendTaskMessage(taskId: string, text: string): string;
1754
+ /**
1755
+ * Run the task runner's own command `/name args` (#66) as a turn of its
1756
+ * own, waiting behind a running turn as a message would. Ordewell carries
1757
+ * it; what it does is the runner's business (#24).
1758
+ */
1759
+ invokeTaskCommand(taskId: string, name: string, args?: string): string;
1760
+ private messageTask;
1761
+ /** Force send a message not yet delivered, queued or handed over; false once it was delivered. */
1762
+ forceSendQueuedTaskMessage(taskId: string, id: string): boolean;
1763
+ /** Take back a message still queued behind a turn; false once it was delivered. */
1764
+ removeQueuedTaskMessage(taskId: string, id: string): boolean;
1765
+ /** Stop a structured task's running turn; it then waits for input like any turn that ends without a `task_complete` call. */
1766
+ interruptTask(taskId: string): Promise<void>;
1767
+ /** What is waiting for a structured task's turn to end; empty for any other task. */
1768
+ getQueuedTaskMessages(taskId: string): QueuedTaskMessage[];
1769
+ private runnerSession;
1770
+ /**
1771
+ * A structured turn that ended without a `task_complete` call (ADR-0018, W1). No
1772
+ * verdict is guessed: the task waits for input, unless a checkpoint in the
1773
+ * same turn already has it waiting, or a queued message went straight out —
1774
+ * the session never passes through idle then, so nothing flickers. The first
1775
+ * turn of an attempt to end that way, cleanly, gets the completion reminder
1776
+ * instead: weaker models often finish the work and report in prose.
1777
+ */
1778
+ private onTurnEnd;
1779
+ subscribe(observer: OrchestratorObserver): () => void;
1780
+ /**
1781
+ * Isolated per observer: one that throws must not take down the scheduler
1782
+ * or stop the remaining observers from hearing the event.
1783
+ */
1784
+ private emit;
1785
+ get isRunning(): boolean;
1786
+ /**
1787
+ * A runner is executing *right now*. Narrower than {@link isRunning}, which
1788
+ * also covers the armed-but-idle scheduler `tick()` deliberately leaves
1789
+ * behind when a plan is paused on a user task, a checkpoint or a hold —
1790
+ * nothing is executing then, so nothing is reading the plan mid-mutation.
1791
+ */
1792
+ get hasLiveWork(): boolean;
1793
+ get isReviewApproved(): boolean;
1794
+ /** The scheduler is driving the plan: ready tasks start on their own as slots free. Narrower than {@link isRunning}. */
1795
+ get isScheduling(): boolean;
1796
+ /**
1797
+ * A full-plan run with nothing starting that something still resumes: a
1798
+ * usage limit's reset, or a retry of a failed task its blocked tasks wait on.
1799
+ */
1800
+ get awaitsResume(): boolean;
1801
+ get status(): 'approved' | 'running' | 'completed';
1802
+ get activeTaskIds(): string[];
1803
+ /** Task id → session id of every attempt whose runner is up; a spawn in flight has no session yet. */
1804
+ get activeSessionMap(): Map<string, string>;
1805
+ getAttempt(taskId: string): TaskAttemptSnapshot | undefined;
1806
+ /** A planner turn holds new starts: what is ready starts when it releases them. */
1807
+ get startsHeld(): boolean;
1808
+ /**
1809
+ * Hold the scheduler's new starts while a planner turn decides what runs
1810
+ * next, so its edits do not race a task that would otherwise start under
1811
+ * it. Running attempts are untouched. The release is idempotent and starts
1812
+ * nothing itself: whoever releases ticks, which starts what became ready.
1813
+ */
1814
+ holdStarts(): () => void;
1815
+ /**
1816
+ * Admit a start that will wait before it claims — on the user's answer, a
1817
+ * held edit, the run opening. Until it is released, Stop counts it as live
1818
+ * work; once a Stop comes it is no longer current, and its claim is refused.
1819
+ */
1820
+ admitStart(): StartAdmission;
1821
+ /**
1822
+ * Something can still start that nobody has to ask for again: a start
1823
+ * admitted and not yet claimed, or a task that resumes on its own at its
1824
+ * usage limit's reset. A Stop is owed to it, whether or not anything runs.
1825
+ */
1826
+ get startsPending(): boolean;
1827
+ /** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
1828
+ private whileAdmitted;
1829
+ /**
1830
+ * "Merge all"; a settled run that merged everything is cleared up and
1831
+ * forgotten. During a run it merges what has landed and the run goes on:
1832
+ * the tasks waiting at a merge gate for that work then start (ADR-0020).
1833
+ * Never alongside ops work, either way round ({@link mergeExcludes}).
1834
+ */
1835
+ mergeRun(): Promise<IsolationMergeResult>;
1836
+ private mergeLanded;
1837
+ /**
1838
+ * Close the settled run. With merge at gates automatically on, landed work
1839
+ * not merged yet is merged first, through the gate's path, so no surface
1840
+ * hears of a handoff for a run about to merge; one that stopped is handed
1841
+ * over as before.
1842
+ */
1843
+ private closeRun;
1844
+ /**
1845
+ * The run as a surface offers Merge all mid-run (ADR-0020): what has landed
1846
+ * and is not merged, and whether the run is paused at its gates with nothing
1847
+ * else running. Null once everything landed is merged.
1848
+ */
1849
+ runMergeView(): RunMergeView | null;
1850
+ /**
1851
+ * The ancestors an ops or user task waits on at its merge gate: those whose
1852
+ * landed work is not in the user's branch yet (ADR-0020). Empty once the
1853
+ * task has started, and for every other task.
1854
+ */
1855
+ getMergeGate(taskId: string): string[];
1856
+ /** The failed tasks a task not started yet waits on, through its dependencies (ADR-0034); empty when it is not blocked. */
1857
+ getBlockedBy(taskId: string): string[];
1858
+ loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
1859
+ /**
1860
+ * Adopt an edited plan without letting go of the run in progress. Everything
1861
+ * the scheduler owns — live sessions, holds, retry counts, the verifier and
1862
+ * the review approval — survives, because `loadPlan` clears all of it and a
1863
+ * mid-run edit is not a new run. Only the tasks change.
1864
+ *
1865
+ * The ids with live sessions are defended: one dropped from the edited plan is
1866
+ * carried over (its verdict still has to land somewhere), and one whose status
1867
+ * the snapshot predates is put back to `in_progress` — adopting the snapshot's
1868
+ * "pending" would offer the scheduler work a runner is already doing.
1869
+ *
1870
+ * A failed task stays failed, unlike a plan load's fresh chance: an edit is
1871
+ * not a retry, and the tasks it blocks wait for that retry (ADR-0034).
1872
+ */
1873
+ reconcilePlan(newTasks: Task[], planRunners?: RunnerId[]): void;
1874
+ start(origin?: RunOrigin): Promise<void>;
1875
+ /** A usage-limit pause holds the run: spawning more would spend the same exhausted limit. */
1876
+ private holdOnLimit;
1877
+ /**
1878
+ * Stop the run. The user's Stop also withdraws every start admitted before
1879
+ * it and not yet claimed (ADR-0029); a `restart` only disarms an idle
1880
+ * scheduler for the run about to start, which must not undo starts asked
1881
+ * for meanwhile.
1882
+ */
1883
+ stop(reason?: 'user' | 'restart'): void;
1884
+ /** Name the tasks a stop took down, so none is mistaken for one still at work. */
1885
+ private sayStopped;
1886
+ /**
1887
+ * A task in progress with no attempt has nothing running it: a stop, a plan
1888
+ * load or anything else that ended its attempt left its status behind. It
1889
+ * is put back to not started, and said, so it can be retried rather than
1890
+ * read as running — or, in an isolated run, count as live work forever.
1891
+ */
1892
+ private recoverOrphans;
1893
+ /**
1894
+ * Task ids still live, whose worktrees no clean-up of the whole run may
1895
+ * take: an attempt is running them, or the plan has them in progress or
1896
+ * waiting on the user mid-attempt — at a checkpoint, or for input. One
1897
+ * waiting on the user's decision instead (a conflict, a usage limit, files
1898
+ * an ops task changed) is not: no runner is at work on it, and a clean-up
1899
+ * keeps what it holds on a branch before its worktree goes.
1900
+ */
1901
+ private liveTaskIds;
1902
+ /**
1903
+ * Whether the run may close with its handoff: no task is live. A handoff
1904
+ * tells the user the run has finished, and a Merge all after it clears the
1905
+ * run up, so neither may happen under a task the plan still shows at work.
1906
+ */
1907
+ private get settled();
1908
+ /**
1909
+ * The boundary a verdict crosses: nothing awaits the verifier's listener, so
1910
+ * a throw from reading the output, landing, releasing a worktree or the tick
1911
+ * after would be an unhandled rejection — and an attempt left integrating is
1912
+ * live work forever. One that never settled fails (a repair waits on the
1913
+ * user, as its conflict did) and blocks only its dependents; either way the
1914
+ * run says why and goes on.
1915
+ */
1916
+ private deliverVerdict;
1917
+ /** Work nothing awaits: a rejection is said, not left unhandled. */
1918
+ private unawaited;
1919
+ private onVerdict;
1920
+ private afterVerdict;
1921
+ /**
1922
+ * A repair's verdict never replaces the verdict the task's own work earned,
1923
+ * and a repair that does not land leaves the task waiting on the user —
1924
+ * never a failed task, so never one that blocks its dependents.
1925
+ */
1926
+ private settleRepair;
1927
+ /**
1928
+ * Hand an attempt's verdict to Landing. The attempt stays live while that
1929
+ * settles — it may wait on the module's merge queue — so a cancel, retry or
1930
+ * stop in that window still wins, and waits for the merge before tearing the
1931
+ * worktree down; nothing counts the task as done before its work is on the
1932
+ * integration branch.
1933
+ */
1934
+ private land;
1935
+ /** Settle a task by how its landing went; `onLanded` is what the path that landed it adds to completing it. */
1936
+ private applyLanding;
1937
+ /**
1938
+ * Work that did not land waits on the user — it never fails the task or
1939
+ * blocks its dependents — unless its conflict is owed a repair, which takes
1940
+ * the slot the ending attempt freed and never one more than the run allows.
1941
+ */
1942
+ private settleUnlanded;
1943
+ private landResolved;
1944
+ getReadyTasks(): Task[];
1945
+ private readiness;
1946
+ private mergesAutomatically;
1947
+ /**
1948
+ * The user changed the run settings under an open run (ADR-0030). The
1949
+ * scheduler reads them at each decision it makes from them, so a merge gate
1950
+ * already reached is merged now rather than at the next event. Fire-and-forget
1951
+ * — no caller waits on the change — and its failure is said through
1952
+ * {@link unawaited}, never left unhandled.
1953
+ */
1954
+ runSettingsChanged(): void;
1955
+ private openRun;
1956
+ /**
1957
+ * Open the run a task starts in, and decide whether it isolates if the
1958
+ * task's next attempt needs that ({@link decidesIsolation}). False while a
1959
+ * dirty tree waits on the user; `resume` is what their choice replays.
1960
+ */
1961
+ private openFor;
1962
+ /**
1963
+ * {@link openFor} for an explicit start admitted at `admittedAt`. A Stop
1964
+ * while the run opened interrupts that opening too, which then opens
1965
+ * nothing; the start says it was stopped rather than going quiet as if a
1966
+ * dirty tree were waiting on the user.
1967
+ */
1968
+ private openForAdmitted;
1969
+ /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
1970
+ private nextAttemptKind;
1971
+ /**
1972
+ * Cancel a running (or scheduled) task: kill its session and return it to
1973
+ * 'pending' — "not executed". The task is put on hold so the scheduler
1974
+ * doesn't immediately restart it; Retry / Force Start release the hold.
1975
+ *
1976
+ * The attempt's worktree is kept, as a stopped or failed one is: a runner is
1977
+ * often cancelled because it looked stuck after doing the work, and Mark
1978
+ * complete can still land that work. The next attempt replaces it.
1979
+ */
1980
+ cancelTask(taskId: string): Promise<void>;
1981
+ private cancelAttempt;
1982
+ /**
1983
+ * Let go of a task that is leaving the plan. A live runner is cancelled
1984
+ * as {@link cancelTask} does, but its worktree goes: no task is left to land
1985
+ * it into. A spawn still in flight just loses its attempt,
1986
+ * which is what makes {@link runClaimed} kill the session it is about to
1987
+ * receive. The id's cross-attempt bookkeeping goes too — a hold or retry
1988
+ * count kept for a task that no longer exists would be inherited by nothing.
1989
+ */
1990
+ releaseTask(taskId: string): Promise<void>;
1991
+ markTaskComplete(taskId: string): Promise<void>;
1992
+ /**
1993
+ * Undo a completion: return the task to "not executed" — pending, verdict and
1994
+ * summary dropped, archive entry removed. It is put on hold like a cancel, so
1995
+ * a running plan does not immediately re-spawn the work the user just
1996
+ * un-marked; Retry / Force Start / Run release the hold. Dependents fall back
1997
+ * to waiting on their own, because the scheduler gates on `isCompleted`.
1998
+ */
1999
+ markTaskIncomplete(taskId: string): Promise<void>;
2000
+ /**
2001
+ * Skip: end a task as `skipped`, with no verdict and nothing of its work
2002
+ * landed. Its worktree is kept, as a failed attempt's is, so the user can
2003
+ * still look at it or Mark complete it later. Its dependents go ahead
2004
+ * without it once the work it waited on is done, and a run that ended on a
2005
+ * failure continues for them, as a retry would continue it. A running task is stopped before it is skipped.
2006
+ */
2007
+ skipTask(taskId: string, reason?: string, origin?: RunOrigin): Promise<void>;
2008
+ private logAndArchive;
2009
+ retryTask(taskId: string, origin?: RunOrigin): Promise<void>;
2010
+ /**
2011
+ * Continue a finished structured task in its saved runner session (ADR-0018,
2012
+ * K1): a retry whose first turn is the user's message, sent to the session
2013
+ * the task ended in rather than a fresh one given its prompt again. The
2014
+ * worktree keeps the same path, where the runner keeps its sessions, and
2015
+ * restores the previous work so that session's memory matches its files.
2016
+ */
2017
+ continueTask(taskId: string, message: string, origin?: RunOrigin): Promise<void>;
2018
+ /**
2019
+ * Keep a task paused on its runner's usage limit from resuming on its own:
2020
+ * it waits for a manual retry, as one whose reset time could not be read
2021
+ * does. What a surface's cancel calls, and what moving the task to another
2022
+ * runner has to call first. False when no resume was scheduled for it.
2023
+ */
2024
+ cancelScheduledResume(taskId: string): boolean;
2025
+ /**
2026
+ * Run before writing assignments so cancellation sees the old runner's
2027
+ * shared wait. A session cannot cross runners, so the next attempt is a
2028
+ * carry-over of the paused attempt's files (ADR-0018, U1).
2029
+ */
2030
+ releaseMovedResume(taskId: string, changes: Partial<Task>): Partial<Task>;
2031
+ /**
2032
+ * A task's attempt stopped on its runner's usage limit, without a
2033
+ * completion call. Failing would paint a red X on a task that only has to
2034
+ * wait, and spawning more tasks would spend the same exhausted limit, so the
2035
+ * task pauses and the run holds. When the runner said when its limit
2036
+ * resets, the task joins its runner's wait and resumes on its own then
2037
+ * (#84); otherwise, or when it was resumed a moment ago and hit the limit
2038
+ * again, it waits for the user to retry it.
2039
+ */
2040
+ private pauseOnLimit;
2041
+ /**
2042
+ * The limit arrived as the turn's error with the runner still up: the
2043
+ * attempt ends here, as its verdict would have ended it, so a resume is a
2044
+ * continue in a fresh process (ADR-0018, L1/K1).
2045
+ */
2046
+ private stopOnLimit;
2047
+ /** The tasks waiting on `runner`'s reset to resume on their own. */
2048
+ private waitingOn;
2049
+ /**
2050
+ * Arm `runner`'s one wait for the latest resume time among its tasks, and
2051
+ * put that time on each of them, so they resume together.
2052
+ */
2053
+ private armLimitWait;
2054
+ /**
2055
+ * The waits a loaded plan still owes. A resume time that passed while
2056
+ * nothing was running — the daemon was down, say — resumes at once.
2057
+ */
2058
+ private rearmLimitWaits;
2059
+ /**
2060
+ * Resume a task paused on its runner's usage limit now, in its saved
2061
+ * session, once the user says the limit has reset early. The limit is the
2062
+ * runner's account's, so every task waiting on that runner's reset resumes
2063
+ * with it, as at the reset itself. `message` follows the reset notice in the
2064
+ * same continuation. A resumed attempt that hits the limit again at once
2065
+ * waits for the user, as an automatic resume's does: that is what keeps a
2066
+ * caller from resuming it round and round.
2067
+ */
2068
+ resumeNow(taskId: string, origin?: RunOrigin, message?: string): Promise<void>;
2069
+ private resumeAfterLimit;
2070
+ /**
2071
+ * Retry a task paused on a usage limit, continuing its saved session where
2072
+ * it left one (ADR-0018, K1), then release the hold the pause put on the
2073
+ * run once nothing else waits on a reset.
2074
+ */
2075
+ private resumeTask;
2076
+ /**
2077
+ * A resume at the limit's reset that nobody waits on cannot be refused to
2078
+ * anyone, so one that may not claim waits for the user instead — its resume
2079
+ * time dropped, and said — unless a Stop is why, which said so itself.
2080
+ */
2081
+ private mayResumeAtReset;
2082
+ /**
2083
+ * Manually start a single AI task right now, bypassing dependency/readiness
2084
+ * gating (the "force start" affordance on a task card) but never the claim:
2085
+ * it still needs the user's approval, a free slot, and no Stop since it was
2086
+ * asked for, and throws {@link TaskControlError} saying which it lacks.
2087
+ * Reuses the scheduler's own claim so a force-started task gets the same
2088
+ * augmented prompt, session tracking, and exit handling — callers must not
2089
+ * re-spawn the runner themselves. No-op if the task is unknown, not an AI
2090
+ * task, or already running.
2091
+ */
2092
+ forceStartTask(taskId: string, origin?: RunOrigin): Promise<void>;
2093
+ private refuseOpsDuringMerge;
2094
+ /**
2095
+ * A force start passes a merge gate (ADR-0020), and the surface confirmed it
2096
+ * first; the choice is kept on the task and said, because the work it acts
2097
+ * on is not in the user's branch.
2098
+ */
2099
+ private notePastGate;
2100
+ /**
2101
+ * Run exactly one task outside full-plan scheduling. The active/starting
2102
+ * session still contributes to {@link hasLiveWork} so every surface exposes
2103
+ * Stop and disables Execute Plan, but onVerdict cannot auto-schedule other
2104
+ * tasks because the plan scheduler's `running` flag remains false.
2105
+ */
2106
+ runTask(taskId: string): Promise<void>;
2107
+ getCompletedCount(): number;
2108
+ getTotalCount(): number;
2109
+ isAllComplete(): boolean;
2110
+ isAnyFailed(): boolean;
2111
+ approveReview(origin?: RunOrigin): Promise<void>;
2112
+ tick(): Promise<void>;
2113
+ /** How a run that ended on failures ends: what it finished, what failed, and what that blocks. */
2114
+ /** Skipped work is not completed work, so a run with any says so rather than that everything completed. */
2115
+ private sayFinished;
2116
+ private sayEndedOnFailure;
2117
+ /** Whether the scheduler may start `id` now: it passes every readiness gate and a slot is free. */
2118
+ private stillReady;
2119
+ /**
2120
+ * A run paused at merge gates says so, as plainly as a task that waits on
2121
+ * the user: which tasks wait, and that Merge all lets them go on (ADR-0020).
2122
+ */
2123
+ private sayGate;
2124
+ /**
2125
+ * Why a start may not take its claim now, or null when it may: a Stop came
2126
+ * since it was admitted (ADR-0029), the task is outside the user's approval
2127
+ * record (ADR-0035), or every slot is taken. Read in the same synchronous
2128
+ * step as the claim it guards, so nothing can change in between.
2129
+ */
2130
+ private claimRefusal;
2131
+ /** An explicit start's refusal, before it changes anything: its caller's answer, as a {@link TaskControlError}. */
2132
+ private refuseClaim;
2133
+ /** An explicit start's claim, once {@link refuseClaim} let it through: its spawn, or the refusal as a rejection. */
2134
+ private claimed;
2135
+ /**
2136
+ * The one claim every attempt start passes — the scheduler's, a run or force
2137
+ * start, a retry's, a continue, a resume, a repair. Checked and taken in one
2138
+ * synchronous step ({@link claimRefusal}), then marked claimed before
2139
+ * anything is awaited, so no other start can take the same task or slot.
2140
+ * A task already claimed is not claimed again: there is nothing to start.
2141
+ */
2142
+ private claim;
2143
+ private runClaimed;
2144
+ /**
2145
+ * The fallible half of starting a task: resolving its cwd/worktree and
2146
+ * spawning the runner. Resolves false when the attempt never committed —
2147
+ * abandoned because it was ended from under it, or a failed spawn already
2148
+ * unwound (both leave `runClaimed` with nothing further to announce).
2149
+ */
2150
+ private spawnAttempt;
2151
+ /** For a skill the main checkout has, the folder a worktree only gets once it is committed. */
2152
+ private uncommittedSkillFolder;
2153
+ /**
2154
+ * The roots whose `.ordewell/skills/` an attempt at `cwd` reads: in a repo
2155
+ * group, `cwd` holds each repo — a worktree of it when isolated, the
2156
+ * workspace's own checkout when not — and no folder of its own (ADR-0014).
2157
+ */
2158
+ private skillRoots;
2159
+ /**
2160
+ * A continue whose runner never announced the session it was told to
2161
+ * resume. Nothing was continued, and nothing fresh was started in its place.
2162
+ */
2163
+ private unresumed;
2164
+ /**
2165
+ * A spawn whose attempt ended while it was in flight: kill what it produced
2166
+ * and take back the claim it made. Only the claim — whatever ended the
2167
+ * attempt may have decided the task since (mark complete, cancel, retry).
2168
+ */
2169
+ private abandonSpawn;
2170
+ private tell;
2171
+ private say;
2172
+ /**
2173
+ * Replay the start a dirty tree turned away. `stash` puts the user's tracked
2174
+ * changes on the git stash first, so the run isolates; `shared` runs this one
2175
+ * run in the workspace root, knowingly.
2176
+ */
2177
+ continueBlockedRun(how: 'stash' | 'shared'): Promise<void>;
2178
+ /**
2179
+ * The one way an attempt ends. Dropping the record is itself what
2180
+ * invalidates a spawn still in flight ({@link runClaimed} kills the session
2181
+ * it receives for an attempt that is no longer current).
2182
+ *
2183
+ * A user interruption also bumps the verifier's generation *before* stopping
2184
+ * the runner: a runner may fire onExit synchronously from
2185
+ * stop(), and if that exit reaches VerdictEngine under the still-valid
2186
+ * generation it delivers a verdict that marks the task 'completed' for one
2187
+ * tick — long enough for the scheduler to start a dependent task. Stop and
2188
+ * load reset the whole verifier themselves.
2189
+ */
2190
+ private endAttempt;
2191
+ /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
2192
+ private saveNativeSession;
2193
+ private endAllAttempts;
2194
+ }
2064
2195
 
2065
2196
  /**
2066
2197
  * Task references: the one grammar for naming a task in a plan, both ways.
@@ -2143,7 +2274,7 @@ declare class RunSettingsError extends Error {
2143
2274
  }
2144
2275
  interface UserSettings {
2145
2276
  modelAllowlist?: Record<string, string[]>;
2146
- /** Last model (and its thinking effort) the user chose for each planner backend, keyed by AiProvider id. */
2277
+ /** Last model (and its thinking effort) the user chose for each planner backend, keyed by planner backend id. */
2147
2278
  plannerModels?: Record<string, {
2148
2279
  model: string;
2149
2280
  effort?: string;
@@ -2457,6 +2588,18 @@ interface ConversationView extends BlockList {
2457
2588
  }[];
2458
2589
  }
2459
2590
  declare const EMPTY_CONVERSATION: ConversationView;
2591
+ type Message<T extends SessionMessage['type']> = Extract<SessionMessage, {
2592
+ type: T;
2593
+ }>;
2594
+ /**
2595
+ * Bring the view's approval cards in line with the planner's requests the
2596
+ * session says are still waiting — for a surface that was not listening the
2597
+ * whole time: one that reconnected, reloaded, or just attached. A pending card
2598
+ * the session no longer has was answered or expired meanwhile; this surface
2599
+ * never heard how, so the card goes rather than guessing an answer. A waiting
2600
+ * request with no card gets one, answerable as the live one would have been.
2601
+ */
2602
+ declare function reconcileApprovals(view: ConversationView, waiting: readonly Message<'approval_request'>[]): ConversationView;
2460
2603
  /**
2461
2604
  * Fold one input into the view. Pure and incremental: blocks the input does
2462
2605
  * not touch keep their identity, and an input that changes nothing returns
@@ -2640,6 +2783,29 @@ declare function skillTokens(text: string): SkillToken[];
2640
2783
  /** The tokens in a sent message that loaded one of `loaded` — what a surface highlights in the user's bubble. */
2641
2784
  declare function loadedSkillTokens(text: string, loaded: readonly string[]): SkillToken[];
2642
2785
 
2786
+ /**
2787
+ * One markdown-it token, as far as a surface reads it. Declared here rather
2788
+ * than re-exporting markdown-it's class so a surface depends on core alone.
2789
+ */
2790
+ interface MarkdownToken {
2791
+ readonly type: string;
2792
+ readonly tag: string;
2793
+ readonly nesting: 1 | 0 | -1;
2794
+ readonly level: number;
2795
+ readonly content: string;
2796
+ readonly markup: string;
2797
+ readonly info: string;
2798
+ readonly block: boolean;
2799
+ readonly hidden: boolean;
2800
+ readonly map: readonly [number, number] | null;
2801
+ readonly attrs: readonly (readonly [string, string])[] | null;
2802
+ readonly children: readonly MarkdownToken[] | null;
2803
+ }
2804
+ /** The parsed blocks of a model's text, for a surface that paints them itself (the TUI). */
2805
+ declare function markdownTokens(text: string): readonly MarkdownToken[];
2806
+ /** A model's text as HTML, safe to set as a webview element's innerHTML. */
2807
+ declare function markdownHtml(text: string): string;
2808
+
2643
2809
  /**
2644
2810
  * What a row's status means to a reader, before any surface picks a word,
2645
2811
  * icon or colour for it. `quiet` is a running task whose runner has printed
@@ -2666,6 +2832,12 @@ declare function awaitingLabel(task: {
2666
2832
  declare function blockedByLabel(refs: readonly string[]): string;
2667
2833
  /** "Skipped: no GPU on this host", or "Skipped" when no reason was given. */
2668
2834
  declare function skippedLabel(reason: string | null): string;
2835
+ /**
2836
+ * A task added after the user approved the plan, which the scheduler leaves
2837
+ * for them (ADR-0035). Not a runner approval: nothing has started, so no
2838
+ * runner is asking anything.
2839
+ */
2840
+ declare const WAITS_FOR_APPROVAL_LABEL = "Waiting for your approval to start";
2669
2841
  /** Runner requests a task waits on (ADR-0018, A1); null when none do. */
2670
2842
  declare function approvalLabel(count: number | undefined): string | null;
2671
2843
  /**
@@ -2736,6 +2908,8 @@ interface TaskRowSubject {
2736
2908
  /** Its last attempt left a runner session to continue (ADR-0018, K1). */
2737
2909
  continuable?: boolean;
2738
2910
  awaitingApproval?: number;
2911
+ /** Added after the user approved the plan, and not approved since (ADR-0035). */
2912
+ waitsForApproval?: boolean;
2739
2913
  isolation?: TaskRowIsolation | null;
2740
2914
  mergeGate?: readonly string[];
2741
2915
  forcedPastGate?: readonly string[];
@@ -2802,6 +2976,8 @@ interface TaskRowView {
2802
2976
  resumes: string | null;
2803
2977
  /** Runner requests waiting on the user, in sentence case; null when none do. */
2804
2978
  approvals: string | null;
2979
+ /** That it waits for the user's approval to start, in sentence case; null once it started, or when it needs none. */
2980
+ waitsForApproval: string | null;
2805
2981
  /** The task's mode is tagged autonomous: it runs without permission prompts. */
2806
2982
  autonomous: boolean;
2807
2983
  /** The repos its isolated work changed, by path; empty for a group of one. */
@@ -2824,7 +3000,7 @@ interface TaskRowView {
2824
3000
  files: string;
2825
3001
  landed: boolean;
2826
3002
  } | null;
2827
- /** The dependencies, by reference, whose work is not merged into the user's branch yet. */
3003
+ /** The ancestors, by reference, whose work is not merged into the user's branch yet. */
2828
3004
  mergeGate: string[] | null;
2829
3005
  /** The failed tasks, by reference, a task not started waits on through its dependencies (ADR-0034); null when none. */
2830
3006
  blockedBy: string[] | null;
@@ -2832,7 +3008,7 @@ interface TaskRowView {
2832
3008
  skipReason: string | null;
2833
3009
  /** An ops task changed tracked files in the user's checkout and nothing was committed. */
2834
3010
  opsChangedFiles: boolean;
2835
- /** The dependencies, by title, a force start went past the merge gate of. */
3011
+ /** The ancestors, by title, a force start went past the merge gate of. */
2836
3012
  forcedPastGate: readonly string[] | null;
2837
3013
  /** The task's dependencies, by reference. */
2838
3014
  dependencies: string[];
@@ -2899,4 +3075,4 @@ declare function parseSwitch(arg: string): boolean | null;
2899
3075
  /** A whole number at least `min`, as typed; null for anything else. */
2900
3076
  declare function parseCount(arg: string | undefined, min: number): number | null;
2901
3077
 
2902
- export { type RunnerLoadedBlock as $, ACT_RISK as A, type BlockingSubject as B, CHECKPOINT_TRUNCATE_LENGTH as C, type DiffRow as D, EMPTY_CONVERSATION as E, type NotificationAction as F, type GatedConversation as G, type OutputPreview as H, type INotification as I, type PlanMarkerStatus as J, type PlanSnapshot as K, type LiveTail as L, type MessageBlock as M, NOT_STARTED as N, type OrchestratorObserver as O, type PlanBlock as P, PlanStore as Q, type PlannerModes as R, type PromptHold as S, RUN_SETTINGS_DEFAULTS as T, type RefTask as U, type RunInput as V, type RunMergeView as W, RunSettingsError as X, type RunTask as Y, type RunTaskStatus as Z, type RunView as _, AUTORUN_RISK as a, diffRows as a$, SUPERVISOR_LEVEL_LABEL as a0, type SerializedConversationMessage as a1, type SerializedPendingTurn as a2, type SerializedPlan as a3, type SerializedTask as a4, type SerializedTaskStatus as a5, type SessionBroadcaster as a6, type SessionMessage as a7, type SessionNotice as a8, SettingsService as a9, type TaskRowUsage as aA, type TaskRowView as aB, type TaskStatusKind as aC, type ThinkingDisplayBlock as aD, type ToolBlock as aE, type ToolHeadline as aF, type ToolStatus as aG, type TurnGate as aH, type UsageBlock as aI, type UserSettings as aJ, aheadOfDraft as aK, applyTaskOps as aL, approvalLabel as aM, automergeSummary as aN, autorunSummary as aO, awaitingLabel as aP, blockedByLabel as aQ, blockingFailures as aR, canMergeTasks as aS, canSetDependencies as aT, canSplitTask as aU, capConflictFiles as aV, classifyOutcome as aW, coalesceTaskLog as aX, dependencyCandidates as aY, dependentsNotice as aZ, dependentsOf as a_, type SkillLoadBlock as aa, type SkillLookup as ab, type SkillToken as ac, type SubagentBlock as ad, type SubagentChild as ae, type SubagentStatus as af, type SurfacePlan as ag, type SurfacePlanState as ah, type SurfaceTask as ai, type TakenPrompt as aj, type TaskAttemptSnapshot as ak, TaskControlError as al, type TaskLogEvent as am, type TaskLogView as an, type TaskOp as ao, TaskOrchestrator as ap, type TaskOrchestratorDeps as aq, type TaskOrchestratorOptions as ar, type TaskOutputSource as as, type TaskRef as at, type TaskRowAction as au, type TaskRowContext as av, type TaskRowIsolation as aw, type TaskRowKind as ax, type TaskRowPlacement as ay, type TaskRowSubject as az, type ApplyTaskOpsResult as b, diffStat as b0, diffSummary as b1, digestTaskLog as b2, drainNext as b3, executionSummary as b4, followTurn as b5, fromTranscript as b6, getSettingsPath as b7, hasHiddenDetail as b8, holdPrompt as b9, skippedLabel as bA, stopTurn as bB, summarizeToolCall as bC, supervisorSummary as bD, surfacePlan as bE, surfacePlanState as bF, takeAt as bG, taskLogForSurface as bH, taskOrderLabel as bI, taskRef as bJ, taskRefOf as bK, taskRowActions as bL, taskRowView as bM, taskStartedNotice as bN, taskStatusKind as bO, textHasTaskOps as bP, titledRefs as bQ, titledTaskRef as bR, titledTaskRefOf as bS, toTaskLogEvent as bT, toolHeadline as bU, trimToolOutput as bV, truncateCheckpointSummary as bW, unsendAll as bX, unsendLatest as bY, isTaskRunning as ba, loadedSkillTokens as bb, markAction as bc, markRequestFor as bd, namedRepos as be, newTaskFields as bf, opsEditable as bg, outputLines as bh, outputPreview as bi, parseCount as bj, parseSwitch as bk, parseTaskOpsJson as bl, pastGateConfirmation as bm, reduceConversation as bn, reduceRun as bo, reduceTaskLog as bp, replayTaskLog as bq, resolveTaskRef as br, resumeLabel as bs, resumeTime as bt, runSettingsOf as bu, runnerToolSubject as bv, serializePlan as bw, serializeTask as bx, serializeTaskStatus as by, skillTokens as bz, type ApprovalBlock as c, type ApprovalStatus as d, type AutoMergeTrigger as e, type CommandBlock as f, type CommandStatus as g, type CompactionBlock as h, type ConversationInput as i, type ConversationView as j, type DiffRowKind as k, type DiffStat as l, type DisplayBlock as m, EMPTY_HOLD as n, EMPTY_RUN as o, EMPTY_TASK_LOG as p, type EditCatalog as q, type ExecutionSummary as r, type GlobalRunSettings as s, type IsolatedExecution as t, IsolationRunController as u, type LiveTailOptions as v, type LocalEntry as w, type MessageRole as x, NO_TURN as y, type NewTaskFields as z };
3078
+ export { type RunTask as $, ACT_RISK as A, type BlockingSubject as B, CHECKPOINT_TRUNCATE_LENGTH as C, type DiffRow as D, EMPTY_CONVERSATION as E, NO_TURN as F, type GatedConversation as G, type HeldEdit as H, type INotification as I, type NewTaskFields as J, type NotificationAction as K, type LiveTail as L, type MarkdownToken as M, NOT_STARTED as N, type OrchestratorObserver as O, type OutputPreview as P, type PlanBlock as Q, type PlanMarkerStatus as R, type PlanSnapshot as S, PlanStore as T, type PlannerModes as U, type PromptHold as V, RUN_SETTINGS_DEFAULTS as W, type RefTask as X, type RunInput as Y, type RunMergeView as Z, RunSettingsError as _, AUTORUN_RISK as a, capConflictFiles as a$, type RunTaskStatus as a0, type RunView as a1, type RunnerLoadedBlock as a2, STOPPED_BEFORE_START as a3, SUPERVISOR_LEVEL_LABEL as a4, type SerializedConversationMessage as a5, type SerializedPendingTurn as a6, type SerializedPlan as a7, type SerializedTask as a8, type SerializedTaskStatus as a9, type TaskRowContext as aA, type TaskRowIsolation as aB, type TaskRowKind as aC, type TaskRowPlacement as aD, type TaskRowSubject as aE, type TaskRowUsage as aF, type TaskRowView as aG, type TaskStatusKind as aH, type ThinkingDisplayBlock as aI, type ToolBlock as aJ, type ToolHeadline as aK, type ToolStatus as aL, type TurnGate as aM, type UsageBlock as aN, type UserSettings as aO, WAITS_FOR_APPROVAL_LABEL as aP, aheadOfDraft as aQ, applyTaskOps as aR, approvalLabel as aS, automergeSummary as aT, autorunSummary as aU, awaitingLabel as aV, blockedByLabel as aW, blockingFailures as aX, canMergeTasks as aY, canSetDependencies as aZ, canSplitTask as a_, type SessionBroadcaster as aa, type SessionMessage as ab, type SessionNotice as ac, SettingsService as ad, type SkillLoadBlock as ae, type SkillLookup as af, type SkillToken as ag, type StartAdmission as ah, type SubagentBlock as ai, type SubagentChild as aj, type SubagentStatus as ak, type SurfacePlan as al, type SurfacePlanState as am, type SurfaceTask as an, type TakenPrompt as ao, type TaskAttemptSnapshot as ap, TaskControlError as aq, type TaskLogEvent as ar, type TaskLogView as as, type TaskOp as at, TaskOrchestrator as au, type TaskOrchestratorDeps as av, type TaskOrchestratorOptions as aw, type TaskOutputSource as ax, type TaskRef as ay, type TaskRowAction as az, type ApplyTaskOpsResult as b, titledTaskRefOf as b$, classifyOutcome as b0, coalesceTaskLog as b1, dependencyCandidates as b2, dependentsNotice as b3, dependentsOf as b4, diffRows as b5, diffStat as b6, diffSummary as b7, digestTaskLog as b8, drainNext as b9, resolveTaskRef as bA, resumeLabel as bB, resumeTime as bC, runSettingsOf as bD, runnerToolSubject as bE, serializePlan as bF, serializeTask as bG, serializeTaskStatus as bH, skillTokens as bI, skippedLabel as bJ, stopTurn as bK, summarizeToolCall as bL, supervisorSummary as bM, surfacePlan as bN, surfacePlanState as bO, takeAt as bP, taskLogForSurface as bQ, taskOrderLabel as bR, taskRef as bS, taskRefOf as bT, taskRowActions as bU, taskRowView as bV, taskStartedNotice as bW, taskStatusKind as bX, textHasTaskOps as bY, titledRefs as bZ, titledTaskRef as b_, executionSummary as ba, followTurn as bb, fromTranscript as bc, getSettingsPath as bd, hasHiddenDetail as be, holdPrompt as bf, isTaskRunning as bg, loadedSkillTokens as bh, markAction as bi, markRequestFor as bj, markdownHtml as bk, markdownTokens as bl, namedRepos as bm, newTaskFields as bn, opsEditable as bo, outputLines as bp, outputPreview as bq, parseCount as br, parseSwitch as bs, parseTaskOpsJson as bt, pastGateConfirmation as bu, reconcileApprovals as bv, reduceConversation as bw, reduceRun as bx, reduceTaskLog as by, replayTaskLog as bz, type ApprovalBlock as c, toTaskLogEvent as c0, toolHeadline as c1, trimToolOutput as c2, truncateCheckpointSummary as c3, unsendAll as c4, unsendLatest as c5, type ApprovalStatus as d, type AutoMergeTrigger as e, type CommandBlock as f, type CommandStatus as g, type CompactionBlock as h, type ConversationInput as i, type ConversationView as j, type DiffRowKind as k, type DiffStat as l, type DisplayBlock as m, EMPTY_HOLD as n, EMPTY_RUN as o, EMPTY_TASK_LOG as p, type EditCatalog as q, type ExecutionSummary as r, type GlobalRunSettings as s, type HeldEditMark as t, type IsolatedExecution as u, IsolationRunController as v, type LiveTailOptions as w, type LocalEntry as x, type MessageBlock as y, type MessageRole as z };