@ordewell/core 0.10.2 → 0.10.4

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-CyTIoiSn.d.ts → IFileSystem-_effm8-b.d.ts} +1 -1
  2. package/dist/{IFileSystem-j0naIoAQ.d.mts → IFileSystem-vyqMdL-x.d.mts} +1 -1
  3. package/dist/{IRunner---PfQVdc.d.mts → IRunner-D_UkkMbJ.d.ts} +202 -196
  4. package/dist/{IRunner-WwL-Bjqq.d.ts → IRunner-NtTHQgLR.d.mts} +202 -196
  5. package/dist/{ModeResolver-DrMU-5lw.d.ts → ModeResolver-Dy3Ef6p-.d.ts} +1 -1
  6. package/dist/{ModeResolver-CjQ7zqTN.d.mts → ModeResolver-wnTFsDnF.d.mts} +1 -1
  7. package/dist/{Task-AfA1m4qW.d.mts → Task-DZs9eKgg.d.mts} +25 -6
  8. package/dist/{Task-AfA1m4qW.d.ts → Task-DZs9eKgg.d.ts} +25 -6
  9. package/dist/{chunk-4FEARWTS.mjs → chunk-645FALZX.mjs} +5 -5
  10. package/dist/chunk-645FALZX.mjs.map +1 -0
  11. package/dist/{chunk-3LNZDBCX.mjs → chunk-A57ITQE7.mjs} +35 -16
  12. package/dist/chunk-A57ITQE7.mjs.map +1 -0
  13. package/dist/{chunk-UQGR3UCA.mjs → chunk-FH6M5QZG.mjs} +136 -122
  14. package/dist/chunk-FH6M5QZG.mjs.map +1 -0
  15. package/dist/{chunk-CIL3BQIF.mjs → chunk-SM6BV6XS.mjs} +17 -3
  16. package/dist/chunk-SM6BV6XS.mjs.map +1 -0
  17. package/dist/index.d.mts +427 -40
  18. package/dist/index.d.ts +427 -40
  19. package/dist/index.js +2589 -1498
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +10799 -9764
  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 +12 -12
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/{plan-utils-DL7yOuJs.d.mts → plan-utils-CtKceSsQ.d.mts} +1292 -1147
  29. package/dist/{plan-utils-gpeqV-Lp.d.ts → plan-utils-POIz5d8z.d.ts} +1292 -1147
  30. package/dist/plan-utils.d.mts +4 -4
  31. package/dist/plan-utils.d.ts +4 -4
  32. package/dist/plan-utils.js +16 -0
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +7 -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 +1 -1
  42. package/dist/chunk-3LNZDBCX.mjs.map +0 -1
  43. package/dist/chunk-4FEARWTS.mjs.map +0 -1
  44. package/dist/chunk-CIL3BQIF.mjs.map +0 -1
  45. package/dist/chunk-UQGR3UCA.mjs.map +0 -1
@@ -1,6 +1,17 @@
1
- import { a1 as RunnerId, c as DiscoveredModel, ae as Task, ab as SkillsService, a6 as SkillInfo, W as RepoGroupLayout, al as TaskSkillSnapshot, ak as TaskSkillNotice, ac as SubagentOutcome, aq as UsageRecord, am as TaskSnapshot, a as AwaitingReason, I as IWorktreeIsolation, v as IsolationRun, j as IsolationHandoff, H as PlanIsolation, w as IsolationTaskRecord, ah as TaskIsolation, F as IsolationView, Q as PrepareIntent, T as PreviousWork, r as IsolationOutcome, V as RepairEvidence, ao as TreeSnapshot, af as TaskDiffRead, ag as TaskDiffResult, q as IsolationMergeResult, as as Verdict, a0 as RunSettings, L as LegacyPlanState, C as ConversationMessage, a8 as SkillLoadNotice, P as PendingPlanTurn, A as AttemptUsage, M as ModelUsage, ar as UsageTotals, N as PlanUsage, e as ExecutionRecord, Z as ResearchStep, J as PlanState, ad as SupervisorLevel, _ as ResearchStepOutcome, X as ResearchLogEntry, O as PlannerUsage, ai as TaskIsolationState, ap as UsageLine } from './Task-AfA1m4qW.js';
2
- import { R as RunnerModeInfo } from './ModeResolver-DrMU-5lw.js';
3
- import { B as IRunnerSession, aC as TaskMessageFrom, ai as RunnerCommand, an as RunnerMcpServer, av as StructuredTurnEnd, j as ApprovalDecision, au as StructuredEvent, y as IConfig, z as IRunner, aq as RunnerRegistry, ab as QueuedTaskMessage, k as ApprovalKind, q as ApprovalSource } from './IRunner-WwL-Bjqq.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-DZs9eKgg.js';
2
+ import { R as RunnerModeInfo } from './ModeResolver-Dy3Ef6p-.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-D_UkkMbJ.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,454 +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
- clearCarriedFrom(id: string): void;
522
- startAttemptUsage(id: string, attempt: number, runner: string, startedAt: string): void;
523
- recordAttemptUsage(id: string, attempt: number, record: UsageRecord, now: string): void;
524
- endAttemptUsage(id: string, attempt: number, now: string): void;
525
- setTaskAttemptSkills(id: string, skills: Task['attemptSkills']): void;
526
- /**
527
- * Widen the plan's runner set. Retargeting a task onto a runner the plan has
528
- * not used before has to land here as well as on the plan state, or
529
- * {@link resolveTaskRunner} reads the new runner as foreign and spawns the
530
- * plan's first one instead.
531
- */
532
- admitRunner(runner: RunnerId): void;
533
- resolveTaskRunner(task: Readonly<Task>): RunnerId;
534
- private rebuild;
535
- private validateAssignedRunners;
536
- }
537
-
538
- type IsolationNoticeLevel = 'info' | 'warn' | 'error';
539
- /** 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. */
540
- type AttemptIntent = 'fresh' | 'repair' | PrepareIntent;
541
- interface AttemptWorkspace {
542
- cwd: string;
543
- worktree: boolean;
544
- /** Set for a `restore` in a worktree; absent in the checkout, where the previous attempt's edits remain. */
545
- previousWork?: PreviousWork;
546
- }
547
- /**
548
- * What the controller reports. It never schedules or emits on its own: the
549
- * orchestrator turns these into observer events and decides what runs next.
550
- */
551
- interface IsolationRunListener {
552
- /**
553
- * The run record changed and should be persisted with the plan. `detached`
554
- * is set instead when the record that changed is no longer the plan's own —
555
- * a release that outlived the plan load that replaced its run — and belongs
556
- * to the session that was left.
557
- */
558
- changed(detached?: IsolationRun): void;
559
- /** A run did not start: `repos` of the group have tracked changes. */
560
- blocked(repos: string[]): void;
561
- /** An isolated run closed and handed its integration branches over. */
562
- handoff(handoff: IsolationHandoff): void;
563
- /** Already said through the notifications; also for a surface that shows no toasts. */
564
- notice(level: IsolationNoticeLevel, message: string): void;
565
- }
566
- interface IsolationRunControllerDeps {
567
- isolation: IWorktreeIsolation;
568
- config: IConfig;
569
- notifications: INotification;
570
- workspaceRoot: () => string;
571
- listener: IsolationRunListener;
572
- /**
573
- * The tasks no clean-up of the whole run may take a worktree from: an
574
- * attempt is running them, or the plan has them in progress or waiting on
575
- * the user mid-attempt.
576
- */
577
- liveTasks: () => ReadonlySet<string>;
578
- }
579
- /**
580
- * The lifecycle of a plan's isolation run (ADR-0013, ADR-0014, ADR-0020),
581
- * between the scheduler and the git layer: deciding, at a run's first change
582
- * task, whether it executes in worktrees, a blocked run and how it goes on,
583
- * each attempt's working directory, releasing worktrees, which landed work the
584
- * user has merged, the tree check of an ops task, the handoff when the run
585
- * closes, and what the user does with it afterwards. The record it keeps
586
- * outlives one run — a resumed plan continues it.
587
- */
588
- declare class IsolationRunController {
589
- private readonly isolation;
590
- private readonly config;
591
- private readonly notifications;
592
- private readonly workspaceRoot;
593
- private readonly listener;
594
- private readonly liveTasks;
595
- private run;
596
- /** Copied paths already reported for the current run: every task gets the same copies. */
597
- private reportedCopies;
598
- private reportedSetupFailures;
599
- /**
600
- * How the open run executes; null while no run is open, `undecided` until
601
- * its first change task asks. A run is one Execute-Plan or one manual task
602
- * run, from its start until it settles or is stopped.
603
- */
604
- private mode;
605
- /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
606
- private resolvers;
607
- private opening;
608
- /** Bumped by an interrupt, so an activation in flight across it does not open the run it ended. */
609
- private activations;
610
- /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
611
- private blockedStart;
612
- /** The dirty repos behind {@link blockedStart}, for the stash notice. */
613
- private blockedRepos;
614
- constructor(deps: IsolationRunControllerDeps);
615
- /** The plan's run record, open or not; null when the plan has not isolated. */
616
- get current(): IsolationRun | null;
617
- get isOpen(): boolean;
618
- /** The open run has decided whether it isolates. */
619
- get decided(): boolean;
620
- /** The open run executes in worktrees. */
621
- get isolating(): boolean;
622
- /** A run is waiting on the user to stash or to go on without isolation. */
623
- get blocked(): boolean;
624
- /** What the plan persists of isolated execution; null when no run ever isolated. */
625
- get planIsolation(): PlanIsolation | null;
626
- /** The one guard of every action on the run: a plan that never isolated is a request refused, not a fault. */
627
- requireRun(): IsolationRun;
628
- /** A task's record in the open run, only while that run isolates. */
629
- openRecord(taskId: string): IsolationTaskRecord | undefined;
630
- /** Where a task's isolated work stands; null when the plan has no isolation run to speak of. */
631
- taskIsolation(taskId: string): TaskIsolation | null;
632
- view(): IsolationView | null;
633
- /**
634
- * Take over a plan's persisted isolation, or none for a plan that has not
635
- * isolated yet. Whatever a crashed process left behind for the run — a
636
- * worktree still marked active, a directory no record owns — is pruned,
637
- * while kept, failed and conflicted worktrees stay for the user.
638
- *
639
- * Deliberately not reported as a change: adopting is not a change to
640
- * persist, and a host that adopts without persisting (VS Code's restore)
641
- * would otherwise write a new session file on every reload.
642
- */
643
- adopt(state: PlanIsolation | null): Promise<void>;
644
- /** Open a run if none is, leaving whether it isolates to its first change task. */
645
- open(): void;
646
- /**
647
- * Decide once per run, as its first change task starts, whether it executes
648
- * in worktrees (ADR-0020): an ops task needs none, so a run of only ops
649
- * tasks never asks. `resume` is what a dirty tree parks until the user
650
- * 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.
651
449
  */
652
450
  decide(resume: () => Promise<void>): Promise<boolean>;
653
451
  /**
@@ -948,728 +746,273 @@ interface WorkspaceEnv {
948
746
  trackedEnvFile: string | null;
949
747
  }
950
748
 
951
- /** What ran an automatic Merge all: a task reaching its merge gate, or the run settling with landed work unmerged. */
952
- type AutoMergeTrigger = {
953
- kind: 'gate';
954
- taskId: string;
955
- } | {
956
- kind: 'run_end';
957
- };
958
749
  /**
959
- * Who opened a run (ADR-0029): a call the user made, or one made for them; a
960
- * call of the supervisor's own; or a usage-limit reset resuming what it paused.
961
- * 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.
962
757
  */
963
- type RunOrigin = 'user' | 'supervisor' | 'limit-reset';
964
- /** What the scheduler reads of the open run's settings, at each decision it makes from them. */
965
- type SchedulerRunSettings = Pick<RunSettings, 'mergeAtGatesAutomatically'>;
758
+ type TaskLogEvent =
966
759
  /**
967
- * The one notification channel out of the orchestrator. Everything that used
968
- * to travel over a separate callback (onRefresh) is an observer event; the
969
- * 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}).
970
763
  */
971
- interface OrchestratorObserver {
972
- /** Any task-shaped state changed (store mutation, checkpoint, retry, …). */
973
- onTaskChanged?(): void;
974
- /**
975
- * A run reached an outcome that stands until the user acts — completed,
976
- * failed, or awaiting the user — on its own, not as the answer to a call.
977
- * Emitted before the `onTaskChanged` that announces it, so a listener can
978
- * save the outcome before any surface is told.
979
- */
980
- onTaskSettled?(data: {
981
- taskId: string;
982
- }): void;
983
- onRunStarted?(data: {
984
- origin: RunOrigin;
985
- }): void;
986
- /** An automatic Merge all settled, at a gate or at the run's end; failures are attention events for a supervisor. */
987
- onAutoMerge?(data: {
988
- trigger: AutoMergeTrigger;
989
- result: IsolationMergeResult;
990
- }): void;
991
- onTick?(): void;
992
- onUsageChanged?(): void;
993
- onExecutionComplete?(): void;
994
- /**
995
- * A task paused on its runner's usage limit, or its resume changed:
996
- * `resumeAt` is when it resumes on its own (ISO time), null when it waits
997
- * for a manual retry — the reset could not be read, the resume was
998
- * cancelled, or the resumed attempt hit the limit again at once.
999
- * `cancelled` marks the resume being cancelled, which is no new pause.
1000
- */
1001
- onUsageLimitWait?(data: {
1002
- taskId: string;
1003
- runner: string;
1004
- resumeAt: string | null;
1005
- cancelled?: true;
1006
- }): void;
1007
- /** A live task's runner went silent (ADR-0018's idle timer); paused while it waits on input, a checkpoint or an approval. */
1008
- onTaskIdle?(data: {
1009
- taskId: string;
1010
- idleSince: string;
1011
- }): void;
1012
- onReviewNeeded?(data: {
1013
- tasks: Task[];
1014
- planRunners: RunnerId[];
1015
- }): void;
1016
- onReviewApproved?(data: {
1017
- tasks: Task[];
1018
- }): void;
1019
- onCheckpoint?(data: {
1020
- taskId: string;
1021
- taskTitle: string;
1022
- summary: string;
1023
- }): void;
1024
- /**
1025
- * The isolation run record changed and should be persisted with the plan.
1026
- * `detached` is the record of a run another plan has replaced since — a
1027
- * release that outlived a plan load — for the session it belongs to.
1028
- */
1029
- onIsolationChanged?(detached?: IsolationRun): void;
1030
- /** 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. */
1031
- onIsolationBlocked?(data: {
1032
- reason: 'dirty';
1033
- repos: string[];
1034
- }): void;
1035
- /** An isolated run settled; emitted before `onExecutionComplete`, which surfaces treat as terminal. */
1036
- onIsolationHandoff?(handoff: IsolationHandoff): void;
1037
- /**
1038
- * What a run says about how it isolates — the fallback to the workspace root,
1039
- * shared paths, copies, a stash. Beside the notification channel, which a
1040
- * daemon may leave unwired, so a surface without toasts can still show it.
1041
- */
1042
- onIsolationNotice?(data: {
1043
- level: 'info' | 'warn' | 'error';
1044
- message: string;
1045
- }): void;
764
+ {
765
+ type: 'task_skills';
766
+ skills: TaskSkillSnapshot[] | TaskSkillNotice[];
1046
767
  }
1047
768
  /**
1048
- * A message or interrupt a task cannot take: it is not running, or it is
1049
- * waiting on something a message does not answer.
1050
- * 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.
1051
772
  */
1052
- declare class TaskControlError extends Error {
1053
- constructor(message: string);
773
+ | {
774
+ type: 'turn_start';
775
+ message: string;
776
+ messageId?: string;
777
+ forced?: boolean;
778
+ from?: TaskMessageFrom;
1054
779
  }
1055
- type AttemptPhase = 'starting' | 'running' | 'integrating';
1056
- /** Read-only view of a task's live attempt. */
1057
- interface TaskAttemptSnapshot {
1058
- taskId: string;
1059
- attempt: number;
1060
- phase: AttemptPhase;
1061
- sessionId: string | null;
1062
- runner: string;
1063
- cwd: string | null;
1064
- startedAt: string;
1065
- skills: readonly TaskSkillSnapshot[];
780
+ /**
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.
783
+ */
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;
1066
856
  }
1067
857
  /**
1068
- * The wiring a {@link TaskOrchestrator} runs on, every collaborator resolved.
1069
- * {@link TaskOrchestrator.compose} is the only production caller; tests build
1070
- * one through it too. The constructor makes nothing itself, so there is exactly
1071
- * one place defaults live.
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).
1072
860
  */
1073
- interface TaskOrchestratorDeps {
1074
- config: IConfig;
1075
- notifications: INotification;
1076
- runner: IRunner;
1077
- store: PlanStore;
1078
- output: TaskOutputSource;
1079
- /** The plan's isolation run and the open run's lifecycle (ADR-0013). */
1080
- runs: IsolationRunController;
1081
- /** A passed attempt's work onto the integration branch, and any conflict repair. */
1082
- landing: Landing;
1083
- registry: RunnerRegistry | null;
1084
- /** The workspace root, read at call time — VS Code can change it. */
1085
- workspaceRoot: () => string;
1086
- /** The workspace's own variables for a task's cwd (ADR-0016). */
1087
- workspaceEnv: (cwd: string) => Promise<WorkspaceEnv>;
1088
- /** The skill catalog over workspace roots, as {@link workspaceSkillRoots} orders them — read at each spawn. */
1089
- skillsAt: (roots: readonly string[]) => SkillLookup;
1090
- /** What the task's last saved attempt did, or null when it left no log (ADR-0020). */
1091
- previousAttemptFromLog: (taskId: string) => string | null;
1092
- /**
1093
- * The open run's settings, read as each automatic merge is decided rather
1094
- * than copied when the run opens, so a change to the run under way applies
1095
- * at once (ADR-0030); absent before any run.
1096
- */
1097
- 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;
1098
880
  }
1099
881
  /**
1100
- * What a caller supplies to {@link TaskOrchestrator.compose}; every optional
1101
- * one gets its default there. This is what a host or test chooses, not the
1102
- * resolved wiring the orchestrator itself runs on.
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`.
1103
884
  */
1104
- interface TaskOrchestratorOptions {
1105
- config: IConfig;
1106
- notifications: INotification;
1107
- runner: IRunner;
1108
- store?: PlanStore;
1109
- output?: TaskOutputSource;
1110
- isolation?: IWorktreeIsolation;
1111
- registry?: RunnerRegistry | null;
1112
- workspaceRoot?: () => string;
1113
- workspaceEnv?: (cwd: string) => Promise<WorkspaceEnv>;
1114
- skillsAt?: (roots: readonly string[]) => SkillLookup;
1115
- previousAttemptFromLog?: (taskId: string) => string | null;
1116
- runSettings?: () => SchedulerRunSettings | undefined;
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;
1117
895
  }
1118
896
  /**
1119
- * The pure scheduler. Owns execution state (`running`, `planStatus`,
1120
- * `reviewApproved`, the live task attempts, the start hold) and the verifier.
1121
- * All task-shaped state — the plan tree, the flat index, the completed set
1122
- * — lives in {@link PlanStore}, injected at construction. The orchestrator
1123
- * calls `store.markCompleted(id)` / `store.markFailed(id)` instead of mutating
1124
- * task state directly. A task completes only after the runner reports it done
1125
- * through `task_complete` on its attempt's token; process exit without that
1126
- * evidence is a visible failure and does not unblock dependent work.
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).
1127
904
  */
1128
- declare class TaskOrchestrator {
1129
- private config;
1130
- private notifications;
1131
- private runner;
1132
- private store;
1133
- private output;
1134
- private attempts;
1135
- private verifier;
1136
- private running;
1137
- private planStatus;
1138
- /** Planner turns in flight that new starts wait for (ADR-0029); running attempts carry on. */
1139
- private startHolds;
1140
- private reviewApproved;
1141
- private autoMergeBlocked;
1142
- private mergesInFlight;
1143
- private retryCounts;
1144
- private spawnCounts;
1145
- /** A full-plan run a usage-limit pause holds: no new start until the reset resumes it. */
1146
- private heldOnLimit;
1147
- /**
1148
- * A full-plan run that ended with failed tasks and the tasks they block
1149
- * (ADR-0034). A retry of a failed task continues it, so what it blocked
1150
- * starts once it passes — without this the retried task ran alone.
1151
- */
1152
- private endedOnFailure;
1153
- /** A Merge all is under way: no ops task starts until it ends (ADR-0020). */
1154
- private merging;
1155
- /** The last "waiting for Merge all" notice, so a run paused at a gate says it once, not on every tick. */
1156
- private gateNotice;
1157
- /** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
1158
- private workspaceEnv;
1159
- /** What the workspace env has already warned about, so a run says it once, not per task. */
1160
- private envWarnings;
1161
- /**
1162
- * Tasks pulled out of auto-scheduling (user-cancelled or failed to spawn).
1163
- * They stay 'pending' — "not executed" — but the scheduler skips them until
1164
- * the user retries or force-starts, which would otherwise loop forever on a
1165
- * task whose spawn always throws.
1166
- */
1167
- private onHold;
1168
- /**
1169
- * Tasks Mark complete is landing after it ended their attempt: in progress
1170
- * with no attempt for as long as that takes, and not orphans for it.
1171
- */
1172
- private completing;
1173
- /** One wait per runner for its usage limit to reset; the tasks it resumes, and when, are on the plan (#84). */
1174
- private limitWaits;
1175
- /**
1176
- * The plan's isolation run and the open run's lifecycle (ADR-0013); see
1177
- * {@link IsolationRunController}. Read and acted on there directly, except
1178
- * Merge all: that goes through {@link mergeRun}, which keeps it apart from
1179
- * ops tasks and starts the tasks its merge lets through.
1180
- */
1181
- readonly runs: IsolationRunController;
1182
- /** A passed attempt's work onto the integration branch, and the conflict repair a landing may need. */
1183
- private landing;
1184
- private registry;
1185
- private workspaceRootFn;
1186
- private observers;
1187
- private skillsAt;
1188
- private previousAttemptFromLog;
1189
- private runSettings;
1190
- constructor(deps: TaskOrchestratorDeps);
1191
- /**
1192
- * The composition root: resolves every optional collaborator and wires the
1193
- * isolation listener that the constructor cannot, because it needs the
1194
- * instance's own (private) emit seam. Nothing else in the
1195
- * codebase constructs an orchestrator, so this is the one place defaults
1196
- * live — a host or test supplies what it cares about and gets the rest.
1197
- */
1198
- static compose(options: TaskOrchestratorOptions): TaskOrchestrator;
1199
- /** Advisory silence timestamp for a task's live runner, or null if not idle. */
1200
- getIdleSince(taskId: string): string | null;
1201
- /**
1202
- * What an ops retry is told about the attempt before. The saved log is read
1203
- * whether or not the session was reloaded, so both behave alike; the output
1204
- * buffer is the fallback for a runner that has no log. A reload clears the
1205
- * attempt count along with the buffer, so a retry made since is what says
1206
- * there was an attempt to report on.
1207
- */
1208
- private opsPreviousAttempt;
1209
- /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
1210
- getLiveOutput(taskId: string, opts: LiveTailOptions): LiveTail | null;
1211
- get storeInstance(): PlanStore;
1212
- /**
1213
- * The variables a task's agent gets from its workspace. What cannot be
1214
- * applied is said once — silently starting without them is how agents ran
1215
- * under the wrong account when an edited `.envrc` was left unallowed.
1216
- */
1217
- private envForTask;
1218
- approveCheckpoint(taskId: string): void;
1219
- rejectCheckpoint(taskId: string, reason?: string): void;
1220
- /** What the task's waiting checkpoint asks, whole; undefined once it settles or when none waits. */
1221
- getCheckpointQuestion(taskId: string): string | undefined;
1222
- awaitsCheckpoint(taskId: string): boolean;
1223
- /**
1224
- * Send a structured task a user message (ADR-0018, M1). Mid-turn it queues
1225
- * behind the turn; a task waiting for input takes it at once and is back in
1226
- * progress. Returns the message's id, for {@link removeQueuedTaskMessage}.
1227
- */
1228
- sendTaskMessage(taskId: string, text: string): string;
1229
- /**
1230
- * Force send (ADR-0023, F1–F3): interrupt the running turn and deliver the
1231
- * message as the next one, ahead of anything queued, without the task
1232
- * waiting for input in between. A task already waiting for input just takes it.
1233
- */
1234
- forceSendTaskMessage(taskId: string, text: string): string;
1235
- /**
1236
- * Run the task runner's own command `/name args` (#66) as a turn of its
1237
- * own, waiting behind a running turn as a message would. Ordewell carries
1238
- * it; what it does is the runner's business (#24).
1239
- */
1240
- invokeTaskCommand(taskId: string, name: string, args?: string): string;
1241
- private messageTask;
1242
- /** Force send a message not yet delivered, queued or handed over; false once it was delivered. */
1243
- forceSendQueuedTaskMessage(taskId: string, id: string): boolean;
1244
- /** Take back a message still queued behind a turn; false once it was delivered. */
1245
- removeQueuedTaskMessage(taskId: string, id: string): boolean;
1246
- /** Stop a structured task's running turn; it then waits for input like any turn that ends without a `task_complete` call. */
1247
- interruptTask(taskId: string): Promise<void>;
1248
- /** What is waiting for a structured task's turn to end; empty for any other task. */
1249
- getQueuedTaskMessages(taskId: string): QueuedTaskMessage[];
1250
- private runnerSession;
1251
- /**
1252
- * A structured turn that ended without a `task_complete` call (ADR-0018, W1). No
1253
- * verdict is guessed: the task waits for input, unless a checkpoint in the
1254
- * same turn already has it waiting, or a queued message went straight out —
1255
- * the session never passes through idle then, so nothing flickers. The first
1256
- * turn of an attempt to end that way, cleanly, gets the completion reminder
1257
- * instead: weaker models often finish the work and report in prose.
1258
- */
1259
- private onTurnEnd;
1260
- subscribe(observer: OrchestratorObserver): () => void;
1261
- /**
1262
- * Isolated per observer: one that throws must not take down the scheduler
1263
- * or stop the remaining observers from hearing the event.
1264
- */
1265
- private emit;
1266
- get isRunning(): boolean;
1267
- /**
1268
- * A runner is executing *right now*. Narrower than {@link isRunning}, which
1269
- * also covers the armed-but-idle scheduler `tick()` deliberately leaves
1270
- * behind when a plan is paused on a user task, a checkpoint or a hold —
1271
- * nothing is executing then, so nothing is reading the plan mid-mutation.
1272
- */
1273
- get hasLiveWork(): boolean;
1274
- get isReviewApproved(): boolean;
1275
- /** The scheduler is driving the plan: ready tasks start on their own as slots free. Narrower than {@link isRunning}. */
1276
- get isScheduling(): boolean;
1277
- /**
1278
- * A full-plan run with nothing starting that something still resumes: a
1279
- * usage limit's reset, or a retry of a failed task its blocked tasks wait on.
1280
- */
1281
- get awaitsResume(): boolean;
1282
- get status(): 'approved' | 'running' | 'completed';
1283
- get activeTaskIds(): string[];
1284
- /** Task id → session id of every attempt whose runner is up; a spawn in flight has no session yet. */
1285
- get activeSessionMap(): Map<string, string>;
1286
- getAttempt(taskId: string): TaskAttemptSnapshot | undefined;
1287
- /** A planner turn holds new starts: what is ready starts when it releases them. */
1288
- get startsHeld(): boolean;
1289
- /**
1290
- * Hold the scheduler's new starts while a planner turn decides what runs
1291
- * next, so its edits do not race a task that would otherwise start under
1292
- * it. Running attempts are untouched. The release is idempotent and starts
1293
- * nothing itself: whoever releases ticks, which starts what became ready.
1294
- */
1295
- holdStarts(): () => void;
1296
- /**
1297
- * "Merge all"; a settled run that merged everything is cleared up and
1298
- * forgotten. During a run it merges what has landed and the run goes on:
1299
- * the tasks waiting at a merge gate for that work then start (ADR-0020).
1300
- * Never alongside ops work, either way round ({@link mergeExcludes}).
1301
- */
1302
- mergeRun(): Promise<IsolationMergeResult>;
1303
- private mergeLanded;
1304
- /**
1305
- * Close the settled run. With merge at gates automatically on, landed work
1306
- * not merged yet is merged first, through the gate's path, so no surface
1307
- * hears of a handoff for a run about to merge; one that stopped is handed
1308
- * over as before.
1309
- */
1310
- private closeRun;
1311
- /**
1312
- * The run as a surface offers Merge all mid-run (ADR-0020): what has landed
1313
- * and is not merged, and whether the run is paused at its gates with nothing
1314
- * else running. Null once everything landed is merged.
1315
- */
1316
- runMergeView(): RunMergeView | null;
1317
- /**
1318
- * The dependencies an ops or user task waits on at its merge gate: those
1319
- * whose landed work is not in the user's branch yet (ADR-0020). Empty once
1320
- * the task has started, and for every other task.
1321
- */
1322
- getMergeGate(taskId: string): string[];
1323
- /** The failed tasks a task not started yet waits on, through its dependencies (ADR-0034); empty when it is not blocked. */
1324
- getBlockedBy(taskId: string): string[];
1325
- loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
1326
- /**
1327
- * Adopt an edited plan without letting go of the run in progress. Everything
1328
- * the scheduler owns — live sessions, holds, retry counts, the verifier and
1329
- * the review approval — survives, because `loadPlan` clears all of it and a
1330
- * mid-run edit is not a new run. Only the tasks change.
1331
- *
1332
- * The ids with live sessions are defended: one dropped from the edited plan is
1333
- * carried over (its verdict still has to land somewhere), and one whose status
1334
- * the snapshot predates is put back to `in_progress` — adopting the snapshot's
1335
- * "pending" would offer the scheduler work a runner is already doing.
1336
- *
1337
- * A failed task stays failed, unlike a plan load's fresh chance: an edit is
1338
- * not a retry, and the tasks it blocks wait for that retry (ADR-0034).
1339
- */
1340
- reconcilePlan(newTasks: Task[], planRunners?: RunnerId[]): void;
1341
- start(origin?: RunOrigin): Promise<void>;
1342
- /** A usage-limit pause holds the run: spawning more would spend the same exhausted limit. */
1343
- private holdOnLimit;
1344
- stop(): void;
1345
- /** Name the tasks a stop took down, so none is mistaken for one still at work. */
1346
- private sayStopped;
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;
1347
984
  /**
1348
- * A task in progress with no attempt has nothing running it: a stop, a plan
1349
- * load or anything else that ended its attempt left its status behind. It
1350
- * is put back to not started, and said, so it can be retried rather than
1351
- * read as running — or, in an isolated run, count as live work forever.
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.
1352
988
  */
1353
- private recoverOrphans;
989
+ awaitingApproval?: number;
1354
990
  /**
1355
- * Task ids still live, whose worktrees no clean-up of the whole run may
1356
- * take: an attempt is running them, or the plan has them in progress or
1357
- * waiting on the user mid-attempt — at a checkpoint, or for input. One
1358
- * waiting on the user's decision instead (a conflict, a usage limit, files
1359
- * an ops task changed) is not: no runner is at work on it, and a clean-up
1360
- * keeps what it holds on a branch before its worktree goes.
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.
1361
994
  */
1362
- private liveTaskIds;
995
+ mergeGate?: string[];
996
+ /** The ancestors, by title, a force start went past the merge gate of (ADR-0020). */
997
+ forcedPastGate?: string[];
1363
998
  /**
1364
- * Whether the run may close with its handoff: no task is live. A handoff
1365
- * tells the user the run has finished, and a Merge all after it clears the
1366
- * run up, so neither may happen under a task the plan still shows at work.
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.
1367
1001
  */
1368
- private get settled();
1369
- /**
1370
- * The boundary a verdict crosses: nothing awaits the verifier's listener, so
1371
- * a throw from reading the output, landing, releasing a worktree or the tick
1372
- * after would be an unhandled rejection — and an attempt left integrating is
1373
- * live work forever. One that never settled fails (a repair waits on the
1374
- * user, as its conflict did) and blocks only its dependents; either way the
1375
- * run says why and goes on.
1376
- */
1377
- private deliverVerdict;
1378
- /** Work nothing awaits: a rejection is said, not left unhandled. */
1379
- private unawaited;
1380
- private onVerdict;
1381
- private afterVerdict;
1382
- /**
1383
- * A repair's verdict never replaces the verdict the task's own work earned,
1384
- * and a repair that does not land leaves the task waiting on the user —
1385
- * never a failed task, so never one that blocks its dependents.
1386
- */
1387
- private settleRepair;
1388
- /**
1389
- * Hand an attempt's verdict to Landing. The attempt stays live while that
1390
- * settles — it may wait on the module's merge queue — so a cancel, retry or
1391
- * stop in that window still wins, and waits for the merge before tearing the
1392
- * worktree down; nothing counts the task as done before its work is on the
1393
- * integration branch.
1394
- */
1395
- private land;
1396
- /** Settle a task by how its landing went; `onLanded` is what the path that landed it adds to completing it. */
1397
- private applyLanding;
1398
- /**
1399
- * Work that did not land waits on the user — it never fails the task or
1400
- * blocks its dependents — unless its conflict is owed a repair, which takes
1401
- * the slot the ending attempt freed and never one more than the run allows.
1402
- */
1403
- private settleUnlanded;
1404
- private landResolved;
1405
- getReadyTasks(): Task[];
1406
- private readiness;
1407
- private mergesAutomatically;
1408
- /**
1409
- * The user changed the run settings under an open run (ADR-0030). The
1410
- * scheduler reads them at each decision it makes from them, so a merge gate
1411
- * already reached is merged now rather than at the next event. Fire-and-forget
1412
- * — no caller waits on the change — and its failure is said through
1413
- * {@link unawaited}, never left unhandled.
1414
- */
1415
- runSettingsChanged(): void;
1416
- private openRun;
1417
- /**
1418
- * Open the run a task starts in, and decide whether it isolates if the
1419
- * task's next attempt needs that ({@link decidesIsolation}). False while a
1420
- * dirty tree waits on the user; `resume` is what their choice replays.
1421
- */
1422
- private openFor;
1423
- /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
1424
- private nextAttemptKind;
1425
- /**
1426
- * Cancel a running (or scheduled) task: kill its session and return it to
1427
- * 'pending' — "not executed". The task is put on hold so the scheduler
1428
- * doesn't immediately restart it; Retry / Force Start release the hold.
1429
- *
1430
- * The attempt's worktree is kept, as a stopped or failed one is: a runner is
1431
- * often cancelled because it looked stuck after doing the work, and Mark
1432
- * complete can still land that work. The next attempt replaces it.
1433
- */
1434
- cancelTask(taskId: string): Promise<void>;
1435
- private cancelAttempt;
1436
- /**
1437
- * Let go of a task that is leaving the plan. A live runner is cancelled
1438
- * as {@link cancelTask} does, but its worktree goes: no task is left to land
1439
- * it into. A spawn still in flight just loses its attempt,
1440
- * which is what makes {@link startTask} kill the session it is about to
1441
- * receive. The id's cross-attempt bookkeeping goes too — a hold or retry
1442
- * count kept for a task that no longer exists would be inherited by nothing.
1443
- */
1444
- releaseTask(taskId: string): Promise<void>;
1445
- markTaskComplete(taskId: string): Promise<void>;
1446
- /**
1447
- * Undo a completion: return the task to "not executed" — pending, verdict and
1448
- * summary dropped, archive entry removed. It is put on hold like a cancel, so
1449
- * a running plan does not immediately re-spawn the work the user just
1450
- * un-marked; Retry / Force Start / Run release the hold. Dependents fall back
1451
- * to waiting on their own, because the scheduler gates on `isCompleted`.
1452
- */
1453
- markTaskIncomplete(taskId: string): Promise<void>;
1454
- /**
1455
- * Skip: end a task as `skipped`, with no verdict and nothing of its work
1456
- * landed. Its worktree is kept, as a failed attempt's is, so the user can
1457
- * still look at it or Mark complete it later. Its dependents go ahead
1458
- * without it, and a run that ended on a failure continues for them, as a
1459
- * retry would continue it. A running task is stopped before it is skipped.
1460
- */
1461
- skipTask(taskId: string, reason?: string, origin?: RunOrigin): Promise<void>;
1462
- private logAndArchive;
1463
- retryTask(taskId: string, origin?: RunOrigin): Promise<void>;
1464
- /**
1465
- * Continue a finished structured task in its saved runner session (ADR-0018,
1466
- * K1): a retry whose first turn is the user's message, sent to the session
1467
- * the task ended in rather than a fresh one given its prompt again. The
1468
- * worktree keeps the same path, where the runner keeps its sessions, and
1469
- * restores the previous work so that session's memory matches its files.
1470
- */
1471
- continueTask(taskId: string, message: string, origin?: RunOrigin): Promise<void>;
1472
- /**
1473
- * Keep a task paused on its runner's usage limit from resuming on its own:
1474
- * it waits for a manual retry, as one whose reset time could not be read
1475
- * does. What a surface's cancel calls, and what moving the task to another
1476
- * runner has to call first. False when no resume was scheduled for it.
1477
- */
1478
- cancelScheduledResume(taskId: string): boolean;
1479
- /**
1480
- * Run before writing assignments so cancellation sees the old runner's
1481
- * shared wait. A session cannot cross runners, so the next attempt is a
1482
- * carry-over of the paused attempt's files (ADR-0018, U1).
1483
- */
1484
- releaseMovedResume(taskId: string, changes: Partial<Task>): Partial<Task>;
1485
- /**
1486
- * A task's attempt stopped on its runner's usage limit, without a
1487
- * completion call. Failing would paint a red X on a task that only has to
1488
- * wait, and spawning more tasks would spend the same exhausted limit, so the
1489
- * task pauses and the run holds. When the runner said when its limit
1490
- * resets, the task joins its runner's wait and resumes on its own then
1491
- * (#84); otherwise, or when it was resumed a moment ago and hit the limit
1492
- * again, it waits for the user to retry it.
1493
- */
1494
- private pauseOnLimit;
1495
- /**
1496
- * The limit arrived as the turn's error with the runner still up: the
1497
- * attempt ends here, as its verdict would have ended it, so a resume is a
1498
- * continue in a fresh process (ADR-0018, L1/K1).
1499
- */
1500
- private stopOnLimit;
1501
- /** The tasks waiting on `runner`'s reset to resume on their own. */
1502
- private waitingOn;
1503
- /**
1504
- * Arm `runner`'s one wait for the latest resume time among its tasks, and
1505
- * put that time on each of them, so they resume together.
1506
- */
1507
- private armLimitWait;
1508
- /**
1509
- * The waits a loaded plan still owes. A resume time that passed while
1510
- * nothing was running — the daemon was down, say — resumes at once.
1511
- */
1512
- private rearmLimitWaits;
1513
- /**
1514
- * Resume a task paused on its runner's usage limit now, in its saved
1515
- * session, once the user says the limit has reset early. The limit is the
1516
- * runner's account's, so every task waiting on that runner's reset resumes
1517
- * with it, as at the reset itself. `message` follows the reset notice in the
1518
- * same continuation. A resumed attempt that hits the limit again at once
1519
- * waits for the user, as an automatic resume's does: that is what keeps a
1520
- * caller from resuming it round and round.
1521
- */
1522
- resumeNow(taskId: string, origin?: RunOrigin, message?: string): Promise<void>;
1523
- private resumeAfterLimit;
1524
- /**
1525
- * Retry a task paused on a usage limit, continuing its saved session where
1526
- * it left one (ADR-0018, K1), then release the hold the pause put on the
1527
- * run once nothing else waits on a reset.
1528
- */
1529
- private resumeTask;
1530
- /**
1531
- * Manually start a single AI task right now, bypassing dependency/readiness
1532
- * gating (the "force start" affordance on a task card). Reuses the scheduler's
1533
- * own startTask so a force-started task gets the same augmented prompt, session
1534
- * tracking, and exit handling — callers must not re-spawn the runner themselves.
1535
- * No-op if the task is unknown, not an AI task, or already running.
1536
- */
1537
- forceStartTask(taskId: string, origin?: RunOrigin): Promise<void>;
1538
- private refuseOpsDuringMerge;
1539
- /**
1540
- * A force start passes a merge gate (ADR-0020), and the surface confirmed it
1541
- * first; the choice is kept on the task and said, because the work it acts
1542
- * on is not in the user's branch.
1543
- */
1544
- private notePastGate;
1545
- /**
1546
- * Run exactly one task outside full-plan scheduling. The active/starting
1547
- * session still contributes to {@link hasLiveWork} so every surface exposes
1548
- * Stop and disables Execute Plan, but onVerdict cannot auto-schedule other
1549
- * tasks because the plan scheduler's `running` flag remains false.
1550
- */
1551
- runTask(taskId: string): Promise<void>;
1552
- getCompletedCount(): number;
1553
- getTotalCount(): number;
1554
- isAllComplete(): boolean;
1555
- isAnyFailed(): boolean;
1556
- approveReview(origin?: RunOrigin): Promise<void>;
1557
- tick(): Promise<void>;
1558
- /** How a run that ended on failures ends: what it finished, what failed, and what that blocks. */
1559
- /** Skipped work is not completed work, so a run with any says so rather than that everything completed. */
1560
- private sayFinished;
1561
- private sayEndedOnFailure;
1562
- /** Whether the scheduler may start `id` now: it passes every readiness gate and a slot is free. */
1563
- private stillReady;
1564
- /**
1565
- * A run paused at merge gates says so, as plainly as a task that waits on
1566
- * the user: which tasks wait, and that Merge all lets them go on (ADR-0020).
1567
- */
1568
- private sayGate;
1569
- private startTask;
1570
- /**
1571
- * The fallible half of starting a task: resolving its cwd/worktree and
1572
- * spawning the runner. Resolves false when the attempt never committed —
1573
- * abandoned because it was ended from under it, or a failed spawn already
1574
- * unwound (both leave `startTask` with nothing further to announce).
1575
- */
1576
- private spawnAttempt;
1577
- /** For a skill the main checkout has, the folder a worktree only gets once it is committed. */
1578
- private uncommittedSkillFolder;
1579
- /**
1580
- * The roots whose `.ordewell/skills/` an attempt at `cwd` reads: in a repo
1581
- * group, `cwd` holds each repo — a worktree of it when isolated, the
1582
- * workspace's own checkout when not — and no folder of its own (ADR-0014).
1583
- */
1584
- private skillRoots;
1585
- /**
1586
- * A continue whose runner never announced the session it was told to
1587
- * resume. Nothing was continued, and nothing fresh was started in its place.
1588
- */
1589
- private unresumed;
1590
- /**
1591
- * A spawn whose attempt ended while it was in flight: kill what it produced
1592
- * and take back the claim it made. Only the claim — whatever ended the
1593
- * attempt may have decided the task since (mark complete, cancel, retry).
1594
- */
1595
- private abandonSpawn;
1596
- private tell;
1597
- private say;
1598
- /**
1599
- * Replay the start a dirty tree turned away. `stash` puts the user's tracked
1600
- * changes on the git stash first, so the run isolates; `shared` runs this one
1601
- * run in the workspace root, knowingly.
1602
- */
1603
- continueBlockedRun(how: 'stash' | 'shared'): Promise<void>;
1604
- /**
1605
- * The one way an attempt ends. Dropping the record is itself what
1606
- * invalidates a spawn still in flight ({@link startTask} kills the session
1607
- * it receives for an attempt that is no longer current).
1608
- *
1609
- * A user interruption also bumps the verifier's generation *before* stopping
1610
- * the runner: a runner may fire onExit synchronously from
1611
- * stop(), and if that exit reaches VerdictEngine under the still-valid
1612
- * generation it delivers a verdict that marks the task 'completed' for one
1613
- * tick — long enough for the scheduler to start a dependent task. Stop and
1614
- * load reset the whole verifier themselves.
1615
- */
1616
- private endAttempt;
1617
- /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
1618
- private saveNativeSession;
1619
- 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'];
1620
1014
  }
1621
-
1622
- type SerializedTaskStatus = {
1623
- id: string;
1624
- status: Task['status'];
1625
- verdict: {
1626
- outcome: 'pass' | 'fail';
1627
- reason: string;
1628
- checks: Verdict['checks'];
1629
- } | null;
1630
- /** Advisory silence timestamp from VerdictEngine — not part of task status semantics. */
1631
- idleSince?: string | null;
1632
- /** Absent unless the plan has an isolation run, so a shared-root plan's updates are unchanged. */
1633
- isolation?: TaskIsolation;
1634
- /** What an `awaiting_user` task waits on, when it was saved (ADR-0018, W1). */
1635
- awaitingReason?: AwaitingReason;
1636
- /** When a task paused on a usage limit resumes on its own (ISO time); absent when it waits for a manual retry. */
1637
- resumeAt?: string;
1638
- /** Why a `skipped` task was skipped; absent when no reason was given, and on any other status. */
1639
- skipReason?: string;
1640
- /** The whole question of the checkpoint the task waits at; absent when it waits at none. */
1641
- checkpoint?: string;
1642
- /** Messages waiting for a structured task's turn to end, oldest first; absent when there are none. */
1643
- queued?: QueuedTaskMessage[];
1644
- /** Set when the task can be continued in its saved runner session (ADR-0018, K1); the id itself stays in the daemon. */
1645
- continuable?: true;
1646
- /**
1647
- * How many of a structured task's runner requests wait for an answer
1648
- * (ADR-0018, A1) — "waiting for approval", which leaves `status` alone.
1649
- * Absent when none do.
1650
- */
1651
- awaitingApproval?: number;
1652
- /**
1653
- * The dependencies an ops or user task waits on at its merge gate: their
1654
- * work has landed but is not merged into the user's branch (ADR-0020).
1655
- * Absent when it waits for no Merge all.
1656
- */
1657
- mergeGate?: string[];
1658
- /** The dependencies, by title, a force start went past the merge gate of (ADR-0020). */
1659
- forcedPastGate?: string[];
1660
- attemptUsage?: AttemptUsage[];
1661
- };
1662
- /**
1663
- * Landed work not yet in the user's branch. `paused` means the running plan
1664
- * waits at a merge gate with no attempts active.
1665
- */
1666
- interface RunMergeView {
1667
- waitingAtGate: boolean;
1668
- paused: boolean;
1669
- repos: IsolationHandoff['repos'];
1670
- landed: IsolationHandoff['landed'];
1671
- }
1672
- type SerializedTask = {
1015
+ type SerializedTask = {
1673
1016
  id: string;
1674
1017
  order: number;
1675
1018
  title: string;
@@ -1741,6 +1084,22 @@ type SerializedPlan = {
1741
1084
  conversationHistory?: SerializedConversationMessage[];
1742
1085
  prdMarkdown?: string;
1743
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
+ }
1744
1103
  /** How a planner turn ended: the reply kind it settled on, a user stop, or a failure. */
1745
1104
  type PlannerTurnOutcome = 'message' | 'plan' | 'task_ops' | 'stopped' | 'error';
1746
1105
  /**
@@ -1806,6 +1165,16 @@ type SessionMessage =
1806
1165
  turnId: string;
1807
1166
  skill: SkillLoadNotice;
1808
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
+ }
1809
1178
  /**
1810
1179
  * A planner turn is over; nothing more carries its `turnId`. Emitted exactly
1811
1180
  * once per `planner_turn_started`, stop and failure included, after the
@@ -2015,7 +1384,7 @@ type PlanSnapshot = {
2015
1384
  plan: SurfacePlan | SurfacePlanState;
2016
1385
  };
2017
1386
  /** What only the status stream says about a task; a plan refresh keeps these as they were. */
2018
- 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'>>;
2019
1388
  /** A task in the run view: the plan's fields, then the status stream's. */
2020
1389
  type RunTask = Omit<SerializedTask, 'subtasks'> & RunTaskStatus & {
2021
1390
  subtasks: RunTask[];
@@ -2033,6 +1402,8 @@ interface RunView {
2033
1402
  usage?: PlanUsage;
2034
1403
  handoff: IsolationHandoff | null;
2035
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;
2036
1407
  }
2037
1408
  type RunInput = SessionMessage | PlanSnapshot;
2038
1409
  /**
@@ -2048,7 +1419,7 @@ type SessionNotice = {
2048
1419
  };
2049
1420
  type SessionBroadcaster = (msg: SessionMessage) => void;
2050
1421
  declare function serializeTask(t: SurfaceTask): SerializedTask;
2051
- 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;
2052
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. */
2053
1424
  declare function surfacePlan({ queuedMessages: _carriedOverAtLoad, ...plan }: LegacyPlanState): SurfacePlan;
2054
1425
  declare function surfacePlanState(state: PlanState): SurfacePlanState;
@@ -2065,10 +1436,762 @@ interface ExecutionSummary {
2065
1436
  blocked: number;
2066
1437
  skipped: number;
2067
1438
  }
2068
- declare function executionSummary(tasks: readonly Task[]): ExecutionSummary;
2069
- declare const CHECKPOINT_TRUNCATE_LENGTH = 120;
2070
- /** Truncate a checkpoint summary to a single line of at most CHECKPOINT_TRUNCATE_LENGTH chars. */
2071
- 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
+ }
2072
2195
 
2073
2196
  /**
2074
2197
  * Task references: the one grammar for naming a task in a plan, both ways.
@@ -2465,6 +2588,18 @@ interface ConversationView extends BlockList {
2465
2588
  }[];
2466
2589
  }
2467
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;
2468
2603
  /**
2469
2604
  * Fold one input into the view. Pure and incremental: blocks the input does
2470
2605
  * not touch keep their identity, and an input that changes nothing returns
@@ -2697,6 +2832,12 @@ declare function awaitingLabel(task: {
2697
2832
  declare function blockedByLabel(refs: readonly string[]): string;
2698
2833
  /** "Skipped: no GPU on this host", or "Skipped" when no reason was given. */
2699
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";
2700
2841
  /** Runner requests a task waits on (ADR-0018, A1); null when none do. */
2701
2842
  declare function approvalLabel(count: number | undefined): string | null;
2702
2843
  /**
@@ -2767,6 +2908,8 @@ interface TaskRowSubject {
2767
2908
  /** Its last attempt left a runner session to continue (ADR-0018, K1). */
2768
2909
  continuable?: boolean;
2769
2910
  awaitingApproval?: number;
2911
+ /** Added after the user approved the plan, and not approved since (ADR-0035). */
2912
+ waitsForApproval?: boolean;
2770
2913
  isolation?: TaskRowIsolation | null;
2771
2914
  mergeGate?: readonly string[];
2772
2915
  forcedPastGate?: readonly string[];
@@ -2833,6 +2976,8 @@ interface TaskRowView {
2833
2976
  resumes: string | null;
2834
2977
  /** Runner requests waiting on the user, in sentence case; null when none do. */
2835
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;
2836
2981
  /** The task's mode is tagged autonomous: it runs without permission prompts. */
2837
2982
  autonomous: boolean;
2838
2983
  /** The repos its isolated work changed, by path; empty for a group of one. */
@@ -2855,7 +3000,7 @@ interface TaskRowView {
2855
3000
  files: string;
2856
3001
  landed: boolean;
2857
3002
  } | null;
2858
- /** 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. */
2859
3004
  mergeGate: string[] | null;
2860
3005
  /** The failed tasks, by reference, a task not started waits on through its dependencies (ADR-0034); null when none. */
2861
3006
  blockedBy: string[] | null;
@@ -2863,7 +3008,7 @@ interface TaskRowView {
2863
3008
  skipReason: string | null;
2864
3009
  /** An ops task changed tracked files in the user's checkout and nothing was committed. */
2865
3010
  opsChangedFiles: boolean;
2866
- /** 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. */
2867
3012
  forcedPastGate: readonly string[] | null;
2868
3013
  /** The task's dependencies, by reference. */
2869
3014
  dependencies: string[];
@@ -2930,4 +3075,4 @@ declare function parseSwitch(arg: string): boolean | null;
2930
3075
  /** A whole number at least `min`, as typed; null for anything else. */
2931
3076
  declare function parseCount(arg: string | undefined, min: number): number | null;
2932
3077
 
2933
- export { type RunView as $, ACT_RISK as A, type BlockingSubject as B, CHECKPOINT_TRUNCATE_LENGTH as C, type DiffRow as D, EMPTY_CONVERSATION as E, type NewTaskFields as F, type GatedConversation as G, type NotificationAction as H, type INotification as I, type OutputPreview as J, type PlanMarkerStatus as K, type LiveTail as L, type MarkdownToken as M, NOT_STARTED as N, type OrchestratorObserver as O, type PlanBlock as P, type PlanSnapshot as Q, PlanStore as R, type PlannerModes as S, type PromptHold as T, RUN_SETTINGS_DEFAULTS as U, type RefTask as V, type RunInput as W, type RunMergeView as X, RunSettingsError as Y, type RunTask as Z, type RunTaskStatus as _, AUTORUN_RISK as a, dependentsOf as a$, type RunnerLoadedBlock as a0, SUPERVISOR_LEVEL_LABEL as a1, type SerializedConversationMessage as a2, type SerializedPendingTurn as a3, type SerializedPlan as a4, type SerializedTask as a5, type SerializedTaskStatus as a6, type SessionBroadcaster as a7, type SessionMessage as a8, type SessionNotice as a9, type TaskRowSubject as aA, type TaskRowUsage as aB, type TaskRowView as aC, type TaskStatusKind as aD, type ThinkingDisplayBlock as aE, type ToolBlock as aF, type ToolHeadline as aG, type ToolStatus as aH, type TurnGate as aI, type UsageBlock as aJ, type UserSettings as aK, aheadOfDraft as aL, applyTaskOps as aM, approvalLabel as aN, automergeSummary as aO, autorunSummary as aP, awaitingLabel as aQ, blockedByLabel as aR, blockingFailures as aS, canMergeTasks as aT, canSetDependencies as aU, canSplitTask as aV, capConflictFiles as aW, classifyOutcome as aX, coalesceTaskLog as aY, dependencyCandidates as aZ, dependentsNotice as a_, SettingsService as aa, type SkillLoadBlock as ab, type SkillLookup as ac, type SkillToken as ad, type SubagentBlock as ae, type SubagentChild as af, type SubagentStatus as ag, type SurfacePlan as ah, type SurfacePlanState as ai, type SurfaceTask as aj, type TakenPrompt as ak, type TaskAttemptSnapshot as al, TaskControlError as am, type TaskLogEvent as an, type TaskLogView as ao, type TaskOp as ap, TaskOrchestrator as aq, type TaskOrchestratorDeps as ar, type TaskOrchestratorOptions as as, type TaskOutputSource as at, type TaskRef as au, type TaskRowAction as av, type TaskRowContext as aw, type TaskRowIsolation as ax, type TaskRowKind as ay, type TaskRowPlacement as az, type ApplyTaskOpsResult as b, unsendLatest as b$, diffRows as b0, diffStat as b1, diffSummary as b2, digestTaskLog as b3, drainNext as b4, executionSummary as b5, followTurn as b6, fromTranscript as b7, getSettingsPath as b8, hasHiddenDetail as b9, serializeTask as bA, serializeTaskStatus as bB, skillTokens as bC, skippedLabel as bD, stopTurn as bE, summarizeToolCall as bF, supervisorSummary as bG, surfacePlan as bH, surfacePlanState as bI, takeAt as bJ, taskLogForSurface as bK, taskOrderLabel as bL, taskRef as bM, taskRefOf as bN, taskRowActions as bO, taskRowView as bP, taskStartedNotice as bQ, taskStatusKind as bR, textHasTaskOps as bS, titledRefs as bT, titledTaskRef as bU, titledTaskRefOf as bV, toTaskLogEvent as bW, toolHeadline as bX, trimToolOutput as bY, truncateCheckpointSummary as bZ, unsendAll as b_, holdPrompt as ba, isTaskRunning as bb, loadedSkillTokens as bc, markAction as bd, markRequestFor as be, markdownHtml as bf, markdownTokens as bg, namedRepos as bh, newTaskFields as bi, opsEditable as bj, outputLines as bk, outputPreview as bl, parseCount as bm, parseSwitch as bn, parseTaskOpsJson as bo, pastGateConfirmation as bp, reduceConversation as bq, reduceRun as br, reduceTaskLog as bs, replayTaskLog as bt, resolveTaskRef as bu, resumeLabel as bv, resumeTime as bw, runSettingsOf as bx, runnerToolSubject as by, serializePlan 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 MessageBlock as x, type MessageRole as y, NO_TURN 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 };