@ordewell/core 0.10.4 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/{IRunner-D_UkkMbJ.d.ts → IFileSystem-C4_mpfze.d.ts} +159 -15
  2. package/dist/{IRunner-NtTHQgLR.d.mts → IFileSystem-DUgeWWQN.d.mts} +159 -15
  3. package/dist/{ModeResolver-wnTFsDnF.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
  4. package/dist/{ModeResolver-Dy3Ef6p-.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
  5. package/dist/{Task-DZs9eKgg.d.mts → Task-C0o4i3Nn.d.mts} +114 -13
  6. package/dist/{Task-DZs9eKgg.d.ts → Task-C0o4i3Nn.d.ts} +114 -13
  7. package/dist/{chunk-FH6M5QZG.mjs → chunk-DMG4ISUG.mjs} +1008 -49
  8. package/dist/chunk-DMG4ISUG.mjs.map +1 -0
  9. package/dist/{chunk-SM6BV6XS.mjs → chunk-EEE2GBLG.mjs} +116 -27
  10. package/dist/chunk-EEE2GBLG.mjs.map +1 -0
  11. package/dist/chunk-H7QX32BH.mjs +647 -0
  12. package/dist/chunk-H7QX32BH.mjs.map +1 -0
  13. package/dist/{chunk-645FALZX.mjs → chunk-LTHKFBKM.mjs} +4 -4
  14. package/dist/chunk-LTHKFBKM.mjs.map +1 -0
  15. package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
  16. package/dist/chunk-NJGID6BG.mjs.map +1 -0
  17. package/dist/index.d.mts +498 -1689
  18. package/dist/index.d.ts +498 -1689
  19. package/dist/index.js +4108 -2082
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +3380 -1639
  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 +231 -75
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/plan-utils.d.mts +20 -4
  29. package/dist/plan-utils.d.ts +20 -4
  30. package/dist/plan-utils.js +335 -88
  31. package/dist/plan-utils.js.map +1 -1
  32. package/dist/plan-utils.mjs +43 -19
  33. package/dist/{plan-utils-CtKceSsQ.d.mts → runSettingsText-BgOmLxdw.d.mts} +2242 -224
  34. package/dist/{plan-utils-POIz5d8z.d.ts → runSettingsText-BgXvajj2.d.ts} +2242 -224
  35. package/dist/testing.d.mts +5 -3
  36. package/dist/testing.d.ts +5 -3
  37. package/dist/testing.js +7 -0
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +7 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/IFileSystem-_effm8-b.d.ts +0 -78
  43. package/dist/IFileSystem-vyqMdL-x.d.mts +0 -78
  44. package/dist/chunk-645FALZX.mjs.map +0 -1
  45. package/dist/chunk-A57ITQE7.mjs +0 -1378
  46. package/dist/chunk-A57ITQE7.mjs.map +0 -1
  47. package/dist/chunk-FH6M5QZG.mjs.map +0 -1
  48. package/dist/chunk-OYUSADFM.mjs.map +0 -1
  49. package/dist/chunk-SM6BV6XS.mjs.map +0 -1
@@ -1,6 +1,6 @@
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.mjs';
2
- import { R as RunnerModeInfo } from './ModeResolver-wnTFsDnF.mjs';
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-NtTHQgLR.mjs';
1
+ import { ai as Task, a4 as RunnerId, aq as TaskSnapshot, b as AwaitingReason, av as UsageRecord, a5 as RunnerSessionCost, d as DiscoveredModel, af as SkillsService, aa as SkillInfo, Z as RepoGroupLayout, I as IWorktreeIsolation, w as IsolationRun, k as IsolationHandoff, J as PlanIsolation, x as IsolationTaskRecord, al as TaskIsolation, H as IsolationView, S as PrepareIntent, V as PreviousWork, s as IsolationOutcome, X as RepairEvidence, at as TreeSnapshot, aj as TaskDiffRead, ak as TaskDiffResult, r as IsolationMergeResult, ax as Verdict, ap as TaskSkillSnapshot, ao as TaskSkillNotice, ag as SubagentOutcome, L as LegacyPlanState, C as ConversationMessage, ac as SkillLoadNotice, P as PendingPlanTurn, a as AttemptUsage, a3 as RunSettings, O as PlanUsage, Y as ReplyRun, M as ModelUsage, aw as UsageTotals, f as ExecutionRecord, a0 as ResearchStep, K as PlanState, $ as ResearchProgress, _ as ResearchLogEntry, ab as SkillLoad, N as PlanStatus, ah as SupervisorLevel, a1 as ResearchStepOutcome, R as PlannerUsage, am as TaskIsolationState, as as TaskUsageSummary } from './Task-C0o4i3Nn.mjs';
2
+ import { N as IRunnerSession, K as IConfig, aL as TaskMessageFrom, ax as RunnerModelRef, ap as RunnerCommand, au as RunnerMcpServer, aE as StructuredTurnEnd, j as ApprovalDecision, aD as StructuredEvent, ah as QueuedTaskMessage, k as ApprovalKind, q as ApprovalSource, M as IRunner, ay as RunnerRegistry, J as IApproval, aU as ToolOutcome, d as AgentProcessDeps, a as AgentAdapter, a4 as OrdewellMcpServer, L as IFileSystem, aV as UsageLimitSignal, ac as PlannerToolHandler, p as ApprovalRequest, i as ApprovalAnswer, f as AiProvider, a3 as OrchestratorOption, a9 as PlannerBackendId } from './IFileSystem-DUgeWWQN.mjs';
3
+ import { R as RunnerModeInfo } from './ModeResolver-4qNjxcDs.mjs';
4
4
 
5
5
  type NotificationAction = {
6
6
  label: string;
@@ -142,15 +142,15 @@ declare class PlanStore {
142
142
  setForcedPastGate(id: string, dependencies: string[]): void;
143
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
144
  private setStatus;
145
- blockDependents(id: string): void;
146
- unblockDependents(id: string): void;
147
145
  setTaskVerdict(id: string, verdict: Task['verdict']): void;
148
146
  setTaskOutputSummary(id: string, summary: Task['outputSummary']): void;
149
- setTaskRunnerSessionId(id: string, runnerSessionId: string | undefined): void;
147
+ /** The owner is saved with the session it belongs to, and dropped with it. */
148
+ setTaskRunnerSessionId(id: string, runnerSessionId: string | undefined, owner?: RunnerId): void;
149
+ setCarriedFrom(id: string, runner: RunnerId): void;
150
150
  clearCarriedFrom(id: string): void;
151
151
  startAttemptUsage(id: string, attempt: number, runner: string, startedAt: string): void;
152
152
  recordAttemptUsage(id: string, attempt: number, record: UsageRecord, now: string): void;
153
- endAttemptUsage(id: string, attempt: number, now: string): void;
153
+ endAttemptUsage(id: string, attempt: number, now: string, runnerSessionCost?: RunnerSessionCost, sessionId?: string): void;
154
154
  setTaskAttemptSkills(id: string, skills: Task['attemptSkills']): void;
155
155
  /**
156
156
  * Widen the plan's runner set. Retargeting a task onto a runner the plan has
@@ -165,19 +165,90 @@ declare class PlanStore {
165
165
  }
166
166
 
167
167
  /**
168
- * The catalog a model/task-mode edit is checked against — the same discovered
169
- * models and manifest modes the planner was shown in the per-turn catalog
170
- * block (`PlannerConversation.catalogBlock`), so a refusal here can never name something
171
- * as invalid that the planner was never told about, or vice versa.
168
+ * Who writes a task's assignment. It decides what a model the writer names
169
+ * meets where the task's runner does not offer it or the allowlist excludes it
170
+ * (ADR-0003), which runners it may put a task on, and whether a live runner's
171
+ * hold applies (ADR-0041):
172
+ *
173
+ * - `plan`: a whole plan the planner submitted, and every planner task as it
174
+ * lands. A stray model is replaced, and the planner is told what moved.
175
+ * - `edit`: the planner's edit. A stray model is refused, which the planner
176
+ * answers in the same reply.
177
+ * - `user`: a user's direct edit. A stray model is refused, with the reason.
178
+ * - `userAdd`: a task the user adds. A stray model is replaced, and the user
179
+ * is told what moved.
180
+ * - `runner`: a switch made with the runner's own `/model`, written as the
181
+ * user's edit. The runner already runs it, so neither the allowlist nor the
182
+ * live runner's hold applies.
183
+ */
184
+ type AssignmentWriter = 'plan' | 'edit' | 'user' | 'userAdd' | 'runner';
185
+ /**
186
+ * What an assignment is resolved against: the discovered models, the manifest
187
+ * modes and the allowlist, as the session holds them when the write lands —
188
+ * the same catalog the planner's per-turn block shows, so nothing is refused
189
+ * that the planner was never told about.
172
190
  */
173
- interface EditCatalog {
191
+ interface AssignmentCatalog {
192
+ /** A runner absent, or listed with nothing, has models discovery could not find, not none. */
174
193
  modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>;
175
- runnerModes: Partial<Record<RunnerId, RunnerModeInfo[]>>;
176
- /** Raw (unfiltered) allowlist, keyed by runner — the same shape `coerceAssignments` takes. */
194
+ /** Absent when no runner's modes are known: no mode is then derived. */
195
+ runnerModes?: Partial<Record<RunnerId, readonly RunnerModeInfo[]>>;
196
+ /** Raw (unfiltered) allowlist, keyed by runner; read through {@link effectiveAllowlist}. */
177
197
  perRunnerAllowlist?: Partial<Record<RunnerId, string[]>>;
178
- /** Full (true) or Guarded: the mode a task the planner writes lands on unless it is read-only (ADR-0031). Full when absent. */
198
+ /** Full (true) or Guarded: the mode a derived task lands on unless it is read-only (ADR-0001). Full when absent. */
179
199
  autonomousDefault?: boolean;
200
+ /**
201
+ * Whether a task's runner is live: an attempt of it is claimed, running,
202
+ * quiet or waiting on someone. Its model, effort and runner are then that
203
+ * runner's (ADR-0041): the process was started on them, and only the
204
+ * runner's own command moves them. Absent means none is.
205
+ */
206
+ liveRunner?: (taskId: string) => boolean;
207
+ }
208
+ /** What a runner offers a task, as discovery and its manifest have it. */
209
+ interface RunnerCatalog {
210
+ models: DiscoveredModel[];
211
+ modes: RunnerModeInfo[];
212
+ }
213
+ /** A change the resolver made to what was written, named so it is never silent (ADR-0001). */
214
+ interface AssignmentAdjustment {
215
+ taskId: string;
216
+ title: string;
217
+ field: 'assignedModel' | 'thinkingEffort';
218
+ from: string | null;
219
+ to: string | null;
220
+ }
221
+
222
+ /** Plan placement and dependency rules, shared with browser-bundled surfaces. */
223
+ interface TreeTask<T> {
224
+ id: string;
225
+ order: number;
226
+ title: string;
227
+ subtasks?: readonly T[];
228
+ }
229
+ interface DependencyTask<T> extends TreeTask<T> {
230
+ dependencies: readonly string[];
231
+ }
232
+ interface TaskPlacement<T> {
233
+ task: T;
234
+ parent?: T;
235
+ ancestors: readonly T[];
236
+ siblings: readonly T[];
237
+ label: string;
238
+ }
239
+ declare function walkPlan<T extends TreeTask<T>>(tasks: readonly T[]): Generator<TaskPlacement<T>>;
240
+ declare function taskPlacement<T extends TreeTask<T>>(tasks: readonly T[], id: string): TaskPlacement<T> | undefined;
241
+ /** Includes the root: deletion has to detach every edge into the removed tree. */
242
+ declare function subtreeIds<T extends TreeTask<T>>(tasks: readonly T[], id: string): Set<string>;
243
+ /** Pickers offer earlier siblings; API validity is independent of display order. */
244
+ declare function dependencyCandidates<T extends TreeTask<T>>(tasks: readonly T[], taskId?: string): T[];
245
+ /** The surviving tasks whose dependency lists a subtree removal will change. */
246
+ declare function dependentsOf<T extends DependencyTask<T>>(tasks: readonly T[], taskId: string): T[];
247
+ interface DependencyCheck {
248
+ ok: boolean;
249
+ error?: string;
180
250
  }
251
+ declare function canSetDependencies<T extends DependencyTask<T>>(tasks: readonly T[], taskId: string, dependencies: readonly string[]): DependencyCheck;
181
252
 
182
253
  /**
183
254
  * Targeted task edits emitted by the planner conversation (the 'task_ops'
@@ -231,6 +302,8 @@ interface ApplyTaskOpsResult {
231
302
  errors: string[];
232
303
  /** Human-readable summary of what changed, for the chat transcript. */
233
304
  summary: string[];
305
+ /** On ok, what the batch's tasks were adjusted to beyond what the planner wrote, as they stand at its end. */
306
+ adjustments?: AssignmentAdjustment[];
234
307
  /** On ok, the id of the task each batch handle names. */
235
308
  handles?: ReadonlyMap<string, string>;
236
309
  }
@@ -260,22 +333,7 @@ interface TaskRef {
260
333
  title: string;
261
334
  dependencies: string[];
262
335
  }
263
- /** The tasks listing `taskId` as a dependency — exactly what removing it detaches. */
264
- declare function dependentsOf<T extends Pick<TaskRef, 'id' | 'dependencies'>>(tasks: T[], taskId: string): T[];
265
- /**
266
- * The tasks that may become dependencies of `taskId`: those displayed before it.
267
- *
268
- * Offering only earlier tasks is what keeps a hand-edited graph valid without a
269
- * cycle check — dependencies then only ever point backwards in display order,
270
- * the same invariant `applyTaskOps` enforces. Omit `taskId` for a task that does
271
- * not exist yet: it lands last, so every current task is a candidate.
272
- */
273
- declare function dependencyCandidates<T extends Pick<TaskRef, 'id' | 'order'>>(tasks: readonly T[], taskId?: string): T[];
274
- /** Pre-flight check for a hand-edited dependency list: every id exists and comes earlier. */
275
- declare function canSetDependencies<T extends TaskRef>(tasks: readonly T[], taskId: string, dependencies: string[]): {
276
- ok: boolean;
277
- error?: string;
278
- };
336
+
279
337
  /** Pre-flight check for a split: the task exists and is not locked by execution. */
280
338
  declare function canSplitTask(tasks: readonly Task[], taskId: string): {
281
339
  ok: boolean;
@@ -287,7 +345,7 @@ declare function canSplitTask(tasks: readonly Task[], taskId: string): {
287
345
  * completed tasks untouched), or nothing is returned and `errors` explains
288
346
  * why. The caller commits `tasks` on ok.
289
347
  */
290
- declare function applyTaskOps(currentTasks: readonly Task[], newOps: TaskOp[], runners: RunnerId[], catalog?: EditCatalog, held?: readonly TaskOp[]): ApplyTaskOpsResult;
348
+ declare function applyTaskOps(currentTasks: readonly Task[], newOps: TaskOp[], runners: RunnerId[], catalog?: AssignmentCatalog, held?: readonly TaskOp[]): ApplyTaskOpsResult;
291
349
 
292
350
  /**
293
351
  * Task skills end to end (ADR-0024): a skill reaches a runner only by being
@@ -358,6 +416,8 @@ interface IsolationRunListener {
358
416
  blocked(repos: string[]): void;
359
417
  /** An isolated run closed and handed its integration branches over. */
360
418
  handoff(handoff: IsolationHandoff): void;
419
+ /** The run was cleared up and forgotten: nothing was handed over, or it was discarded. */
420
+ closed(): void;
361
421
  /** Already said through the notifications; also for a surface that shows no toasts. */
362
422
  notice(level: IsolationNoticeLevel, message: string): void;
363
423
  }
@@ -724,6 +784,86 @@ declare class Landing {
724
784
  private isGroup;
725
785
  }
726
786
 
787
+ /**
788
+ * Why a task cannot start now: the one start rule (ADR-0034), which the
789
+ * scheduler, the explicit starts, the command seam's refusals, the task row's
790
+ * merge gate and the planner's notes all read. {@link startWaits} lists every
791
+ * one that holds, in this order, so the first is the reason a task waits.
792
+ */
793
+ type StartWait =
794
+ /** Under way, waiting on the user, or ended: only a retry, a continue or a resume starts it again. */
795
+ {
796
+ kind: 'started';
797
+ }
798
+ /** Mark complete is landing its work. */
799
+ | {
800
+ kind: 'completing';
801
+ }
802
+ /** An AI task with no prompt yet: there is nothing to run. */
803
+ | {
804
+ kind: 'no-prompt';
805
+ }
806
+ /** Outside the user's approval record (ADR-0035). */
807
+ | {
808
+ kind: 'unapproved';
809
+ }
810
+ /** Pulled out of the scheduler: cancelled, marked not done, or a spawn that failed. */
811
+ | {
812
+ kind: 'on-hold';
813
+ }
814
+ /** The failed tasks it waits on through its dependencies (ADR-0034). */
815
+ | {
816
+ kind: 'blocked';
817
+ failed: string[];
818
+ }
819
+ /** What its dependencies still hold it back for (see {@link unmetDependencies}). */
820
+ | {
821
+ kind: 'dependencies';
822
+ waiting: string[];
823
+ }
824
+ /** A force start queued behind the dependencies still under way or yet to run, which starts once they finish. */
825
+ | {
826
+ kind: 'queued';
827
+ waiting: string[];
828
+ }
829
+ /** An ops task, and a Merge all is under way (ADR-0020). */
830
+ | {
831
+ kind: 'merging';
832
+ }
833
+ /** Its runner hit its usage limit: nothing more starts on that runner until the reset. */
834
+ | {
835
+ kind: 'usage-limit';
836
+ runner: string;
837
+ }
838
+ /** A planner turn holds new starts until its reply ends (ADR-0029). */
839
+ | {
840
+ kind: 'start-hold';
841
+ }
842
+ /** Its merge gate (ADR-0020): the ancestors whose landed work is not in the user's branch yet. */
843
+ | {
844
+ kind: 'merge-gate';
845
+ unmerged: string[];
846
+ }
847
+ /** A user task: the user does it by hand, and nothing starts it. */
848
+ | {
849
+ kind: 'by-hand';
850
+ };
851
+ /**
852
+ * A start the user forced, which goes past what the scheduler waits for: a
853
+ * hold, a failed dependency (ADR-0034), its runner's usage limit, and the
854
+ * merge gate they confirmed (ADR-0020). Made now, it is the user's own act
855
+ * and goes past the whole gate and a planner turn's start hold. Queued behind
856
+ * its dependencies, it starts later with nobody asking, so it waits for a
857
+ * start hold as the scheduler's starts do, and goes past only the gate it was
858
+ * confirmed past: `pastGate`.
859
+ */
860
+ type ForcedStart = {
861
+ queued: false;
862
+ } | {
863
+ queued: true;
864
+ pastGate: readonly string[];
865
+ };
866
+
727
867
  /**
728
868
  * The variables one workspace's planner and agents run with, on top of the
729
869
  * daemon's own environment (ADR-0016).
@@ -799,6 +939,23 @@ type TaskLogEvent =
799
939
  preTokens?: number;
800
940
  postTokens?: number;
801
941
  }
942
+ /**
943
+ * The runner's main thread moved to another model, and what the plan says
944
+ * once the write-back settled (#106): written from its outcome, not from the
945
+ * runner's event, so the line never claims an edit that did not land.
946
+ * `kept` says why the plan did not follow, when it did not. A log saved
947
+ * before the plan's side was recorded has no `plan`: it says nothing of it.
948
+ */
949
+ | {
950
+ type: 'model_switched';
951
+ from: RunnerModelRef;
952
+ to: RunnerModelRef;
953
+ plan?: {
954
+ model?: RunnerModelRef;
955
+ effort?: string;
956
+ };
957
+ kept?: string;
958
+ }
802
959
  /** What the runner loaded from the user's and the project's setup, as its process started. */
803
960
  | {
804
961
  type: 'runner_loaded';
@@ -807,13 +964,21 @@ type TaskLogEvent =
807
964
  } | {
808
965
  type: 'turn_end';
809
966
  reason: StructuredTurnEnd;
810
- } | {
967
+ }
968
+ /**
969
+ * A piece of the run `runId` of the agent's reply (see `AgentEvent`). A log
970
+ * saved before runs carried an id has none, and opens each later run of a
971
+ * turn with the paragraph break that joined it.
972
+ */
973
+ | {
811
974
  type: 'text_delta';
975
+ runId?: string;
812
976
  text: string;
813
977
  }
814
- /** A complete run of the agent's reply, authoritative over the deltas streamed for it. */
978
+ /** A complete run of the agent's reply, authoritative over the deltas streamed under its `runId`. */
815
979
  | {
816
980
  type: 'text';
981
+ runId?: string;
817
982
  text: string;
818
983
  } | {
819
984
  type: 'thinking_delta';
@@ -940,15 +1105,17 @@ declare function trimToolOutput(output: string): {
940
1105
  };
941
1106
  /**
942
1107
  * 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.
1108
+ * keep: background-agent launches, which the subagent's own events already
1109
+ * show, and a model change, which is logged with what the plan made of it.
944
1110
  */
945
1111
  declare function toTaskLogEvent(event: StructuredEvent): TaskLogEvent | null;
946
1112
  /** A log as a surface is sent it, live or read back: everything but the skills' bodies. */
947
1113
  declare function taskLogForSurface(events: readonly TaskLogEvent[]): TaskLogEvent[];
948
1114
  /**
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.
1115
+ * Merge consecutive deltas of one stream into one event each: of one run of
1116
+ * text, or of one voice's thinking. Deltas arrive a few characters at a time;
1117
+ * merged, a batch is one message on the wire and one line on disk, and the
1118
+ * view folds to the same blocks either way.
952
1119
  */
953
1120
  declare function coalesceTaskLog(events: readonly TaskLogEvent[]): TaskLogEvent[];
954
1121
  /**
@@ -1078,6 +1245,8 @@ type SurfacePlanState = (Omit<Extract<PlanState, {
1078
1245
  });
1079
1246
  type SerializedPlan = {
1080
1247
  runSettings?: LegacyPlanState['runSettings'];
1248
+ /** What waits on the planner's usage limit, and when it goes, so a surface can say so and offer to cancel it. */
1249
+ plannerResend?: LegacyPlanState['plannerResend'];
1081
1250
  tasks: SerializedTask[];
1082
1251
  runners: RunnerId[];
1083
1252
  generatedAt: string;
@@ -1109,8 +1278,9 @@ type PlannerTurnOutcome = 'message' | 'plan' | 'task_ops' | 'stopped' | 'error';
1109
1278
  * `planner_turn_ended` with the same `turnId`; every turn-scoped message in
1110
1279
  * between carries it. The stream is provisional and the settled messages are
1111
1280
  * authoritative:
1112
- * - `planner_message` replaces the streamed text of the turn's final segment —
1113
- * a surface drops what it accumulated and shows the message instead.
1281
+ * - `planner_message` settles the streamed text of each run it carries in that
1282
+ * run's segment, where it streamed — a surface drops what it accumulated
1283
+ * there and shows the run's text instead.
1114
1284
  * - A reply that is a JSON envelope (plan, taskOps, taskQuery) never arrives
1115
1285
  * as `planner_text_delta`; it streams as `plan_token`, the "building plan"
1116
1286
  * display.
@@ -1132,9 +1302,12 @@ type SessionMessage =
1132
1302
  }
1133
1303
  /**
1134
1304
  * The settled reply of a planner turn, emitted by the session once the turn
1135
- * is classified. Authoritative over any `planner_text_delta` of its turn's
1136
- * final segment. `turnId` is absent for replies sent outside a streamed turn.
1305
+ * is classified. `turnId` is absent for replies sent outside a streamed turn.
1137
1306
  * `supervisor`: the supervisor said it in a wake-up turn (ADR-0029).
1307
+ * `runs`: what the reply shows, run by run, each authoritative over its
1308
+ * segment's `planner_text_delta`s; `content` stays the whole reply. Without
1309
+ * runs (a note the conversation wrote, an older daemon) the message is
1310
+ * authoritative over the turn's final segment.
1138
1311
  */
1139
1312
  | {
1140
1313
  type: 'planner_message';
@@ -1142,6 +1315,7 @@ type SessionMessage =
1142
1315
  timestamp: string;
1143
1316
  turnId?: string;
1144
1317
  supervisor?: true;
1318
+ runs?: ReplyRun[];
1145
1319
  }
1146
1320
  /**
1147
1321
  * A planner turn began. Emitted once per turn by whoever runs the turn,
@@ -1334,6 +1508,8 @@ type SessionMessage =
1334
1508
  type: 'isolation_merge';
1335
1509
  result: IsolationMergeResult;
1336
1510
  trigger?: AutoMergeTrigger;
1511
+ } | {
1512
+ type: 'run_closed';
1337
1513
  } | {
1338
1514
  type: 'planner_liveness';
1339
1515
  } | {
@@ -1511,6 +1687,22 @@ interface OrchestratorObserver {
1511
1687
  resumeAt: string | null;
1512
1688
  cancelled?: true;
1513
1689
  }): void;
1690
+ /**
1691
+ * A task's runner now answers on another model (#106). `asked` when a model
1692
+ * command sent through its chat chose it: the user's own choice, which the
1693
+ * plan is to follow. `attemptEnded` when the attempt that runs `sessionId`
1694
+ * is no longer the task's, so the switch says nothing of what runs next.
1695
+ * `effort` only where the runner reported one.
1696
+ */
1697
+ onModelSwitched?(data: {
1698
+ taskId: string;
1699
+ sessionId: string;
1700
+ from: RunnerModelRef;
1701
+ model: RunnerModelRef;
1702
+ effort?: string;
1703
+ asked: boolean;
1704
+ attemptEnded: boolean;
1705
+ }): void;
1514
1706
  /** A live task's runner went silent (ADR-0018's idle timer); paused while it waits on input, a checkpoint or an approval. */
1515
1707
  onTaskIdle?(data: {
1516
1708
  taskId: string;
@@ -1541,6 +1733,8 @@ interface OrchestratorObserver {
1541
1733
  }): void;
1542
1734
  /** An isolated run settled; emitted before `onExecutionComplete`, which surfaces treat as terminal. */
1543
1735
  onIsolationHandoff?(handoff: IsolationHandoff): void;
1736
+ /** The isolated run was cleared up and forgotten, by a Merge all that left nothing to hand over or by a discard. */
1737
+ onRunClosed?(): void;
1544
1738
  /**
1545
1739
  * What a run says about how it isolates — the fallback to the workspace root,
1546
1740
  * shared paths, copies, a stash. Beside the notification channel, which a
@@ -1596,6 +1790,12 @@ interface TaskOrchestratorDeps {
1596
1790
  skillsAt: (roots: readonly string[]) => SkillLookup;
1597
1791
  /** What the task's last saved attempt did, or null when it left no log (ADR-0020). */
1598
1792
  previousAttemptFromLog: (taskId: string) => string | null;
1793
+ /**
1794
+ * The newest attempt the task has a saved log for, 0 for none. An attempt's
1795
+ * log is saved under its number, so a new one is numbered past a log kept
1796
+ * from before the task had ledgers.
1797
+ */
1798
+ lastLoggedAttempt: (taskId: string) => number;
1599
1799
  /**
1600
1800
  * The open run's settings, read as each automatic merge is decided rather
1601
1801
  * than copied when the run opens, so a change to the run under way applies
@@ -1625,12 +1825,13 @@ interface TaskOrchestratorOptions {
1625
1825
  workspaceEnv?: (cwd: string) => Promise<WorkspaceEnv>;
1626
1826
  skillsAt?: (roots: readonly string[]) => SkillLookup;
1627
1827
  previousAttemptFromLog?: (taskId: string) => string | null;
1828
+ lastLoggedAttempt?: (taskId: string) => number;
1628
1829
  runSettings?: () => SchedulerRunSettings | undefined;
1629
1830
  /** Every task is approved unless a Session supplies its record: an orchestrator on its own has no user to ask. */
1630
1831
  isApproved?: (taskId: string) => boolean;
1631
1832
  }
1632
1833
  /**
1633
- * The pure scheduler. Owns execution state (`running`, `planStatus`,
1834
+ * The pure scheduler. Owns execution state (the {@link RunState},
1634
1835
  * `reviewApproved`, the live task attempts, the start hold) and the verifier.
1635
1836
  * All task-shaped state — the plan tree, the flat index, the completed set
1636
1837
  * — lives in {@link PlanStore}, injected at construction. The orchestrator
@@ -1647,8 +1848,8 @@ declare class TaskOrchestrator {
1647
1848
  private output;
1648
1849
  private attempts;
1649
1850
  private verifier;
1650
- private running;
1651
- private planStatus;
1851
+ /** Changed only by start, stop, a plan load, a usage-limit pause, a move off the limit, and {@link settleRun}. */
1852
+ private runState;
1652
1853
  /** Planner turns in flight that new starts wait for (ADR-0029); running attempts carry on. */
1653
1854
  private startHolds;
1654
1855
  /** Bumped by the user's Stop: a start admitted under an older generation never takes its claim. */
@@ -1660,14 +1861,6 @@ declare class TaskOrchestrator {
1660
1861
  private mergesInFlight;
1661
1862
  private retryCounts;
1662
1863
  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
1864
  /** A Merge all is under way: no ops task starts until it ends (ADR-0020). */
1672
1865
  private merging;
1673
1866
  /** The last "waiting for Merge all" notice, so a run paused at a gate says it once, not on every tick. */
@@ -1683,6 +1876,12 @@ declare class TaskOrchestrator {
1683
1876
  * task whose spawn always throws.
1684
1877
  */
1685
1878
  private onHold;
1879
+ /**
1880
+ * Force starts queued behind dependencies the scheduler will not start them
1881
+ * after: outside a run it drives, or past a failed dependency, which it
1882
+ * never forces. Each starts once those still under way have finished.
1883
+ */
1884
+ private queuedStarts;
1686
1885
  /**
1687
1886
  * Tasks Mark complete is landing after it ended their attempt: in progress
1688
1887
  * with no attempt for as long as that takes, and not orphans for it.
@@ -1704,6 +1903,7 @@ declare class TaskOrchestrator {
1704
1903
  private observers;
1705
1904
  private skillsAt;
1706
1905
  private previousAttemptFromLog;
1906
+ private lastLoggedAttempt;
1707
1907
  private runSettings;
1708
1908
  private isApproved;
1709
1909
  constructor(deps: TaskOrchestratorDeps);
@@ -1820,11 +2020,14 @@ declare class TaskOrchestrator {
1820
2020
  admitStart(): StartAdmission;
1821
2021
  /**
1822
2022
  * 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.
2023
+ * admitted and not yet claimed, a force start queued behind its
2024
+ * dependencies, or a task that resumes on its own at its usage limit's
2025
+ * reset. A Stop is owed to it, whether or not anything runs.
1825
2026
  */
1826
2027
  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. */
2028
+ /** A start admitted now that nothing has to count as live work: one that claims before it awaits, or follows on from one that did. */
2029
+ private admittedNow;
2030
+ /** Run `work`, which awaits before it claims, under an admission it holds throughout and claims under. */
1828
2031
  private whileAdmitted;
1829
2032
  /**
1830
2033
  * "Merge all"; a settled run that merged everything is cleared up and
@@ -1849,10 +2052,20 @@ declare class TaskOrchestrator {
1849
2052
  runMergeView(): RunMergeView | null;
1850
2053
  /**
1851
2054
  * 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.
2055
+ * landed work is not in the user's branch yet (ADR-0020). Empty for every
2056
+ * other task. Judged whatever the task's status but `completed` — a retry of
2057
+ * a failed ops task acts on the checkout as a first start does — so a start
2058
+ * the user is about to confirm is told what it would act without.
1854
2059
  */
1855
2060
  getMergeGate(taskId: string): string[];
2061
+ /**
2062
+ * Every reason a task cannot start now, from the one start rule
2063
+ * (ADR-0034); none when it can. `forced` judges a start the user forced.
2064
+ * Takes the task, not its id: the command seam judges one as a held edit
2065
+ * leaves it.
2066
+ */
2067
+ startWaits(task: Readonly<Task>, forced?: ForcedStart): StartWait[];
2068
+ private startRule;
1856
2069
  /** The failed tasks a task not started yet waits on, through its dependencies (ADR-0034); empty when it is not blocked. */
1857
2070
  getBlockedBy(taskId: string): string[];
1858
2071
  loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
@@ -1871,8 +2084,16 @@ declare class TaskOrchestrator {
1871
2084
  * not a retry, and the tasks it blocks wait for that retry (ADR-0034).
1872
2085
  */
1873
2086
  reconcilePlan(newTasks: Task[], planRunners?: RunnerId[]): void;
2087
+ /** Start the full-plan run, or continue one that is held or ended on a failure. */
1874
2088
  start(origin?: RunOrigin): Promise<void>;
1875
- /** A usage-limit pause holds the run: spawning more would spend the same exhausted limit. */
2089
+ /**
2090
+ * A usage-limit pause with a reset to wait for holds the run (ADR-0018,
2091
+ * U1): spawning more would spend the same exhausted limit, and the run
2092
+ * stays open — neither handed over nor merged at its end — however it was
2093
+ * started, so a task started alone keeps its run for the reset too. A pause
2094
+ * no reset will resume holds nothing: it keeps only its own runner's tasks
2095
+ * from starting (`readiness.usage-limit`).
2096
+ */
1876
2097
  private holdOnLimit;
1877
2098
  /**
1878
2099
  * Stop the run. The user's Stop also withdraws every start admitted before
@@ -1932,12 +2153,18 @@ declare class TaskOrchestrator {
1932
2153
  * integration branch.
1933
2154
  */
1934
2155
  private land;
1935
- /** Settle a task by how its landing went; `onLanded` is what the path that landed it adds to completing it. */
2156
+ /**
2157
+ * Settle a task by how its landing went; `onLanded` is what the path that
2158
+ * landed it adds to completing it. `admission` is the start the landed work
2159
+ * came from, which a repair follows on from.
2160
+ */
1936
2161
  private applyLanding;
1937
2162
  /**
1938
2163
  * Work that did not land waits on the user — it never fails the task or
1939
2164
  * blocks its dependents — unless its conflict is owed a repair, which takes
1940
2165
  * the slot the ending attempt freed and never one more than the run allows.
2166
+ * The repair follows on from the start whose work it repairs: a Stop since
2167
+ * that was admitted withdraws it.
1941
2168
  */
1942
2169
  private settleUnlanded;
1943
2170
  private landResolved;
@@ -1960,10 +2187,10 @@ declare class TaskOrchestrator {
1960
2187
  */
1961
2188
  private openFor;
1962
2189
  /**
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.
2190
+ * {@link openFor} for an explicit start under `admission`. A Stop while the
2191
+ * run opened interrupts that opening too, which then opens nothing; the
2192
+ * start says it was stopped rather than going quiet as if a dirty tree were
2193
+ * waiting on the user.
1967
2194
  */
1968
2195
  private openForAdmitted;
1969
2196
  /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
@@ -2018,14 +2245,35 @@ declare class TaskOrchestrator {
2018
2245
  /**
2019
2246
  * Keep a task paused on its runner's usage limit from resuming on its own:
2020
2247
  * 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.
2248
+ * does. What a surface's cancel calls. A run the pause held goes on once
2249
+ * nothing else waits on a reset. False when no resume was scheduled for it.
2250
+ */
2251
+ cancelScheduledResume(taskId: string): Promise<boolean>;
2252
+ /** {@link cancelScheduledResume} without its tick, for a caller that ticks or starts once it is done. */
2253
+ private dropResume;
2254
+ /**
2255
+ * The hold a usage-limit pause put on the run is released once nothing
2256
+ * waits on a reset (ADR-0018, U1) — however the last wait went: the reset,
2257
+ * a cancel, Mark complete or the task leaving the plan. The run is still
2258
+ * open, so the next tick schedules it.
2259
+ */
2260
+ private releaseHold;
2261
+ /**
2262
+ * The runner whose attempt left the task's work, when the task has been
2263
+ * moved off it since: a session cannot cross runners, so whatever starts
2264
+ * the task next is a carry-over of that work, the rule a paused task's move
2265
+ * follows (ADR-0018, U1).
2023
2266
  */
2024
- cancelScheduledResume(taskId: string): boolean;
2267
+ private movedFrom;
2025
2268
  /**
2026
2269
  * Run before writing assignments so cancellation sees the old runner's
2027
2270
  * shared wait. A session cannot cross runners, so the next attempt is a
2028
2271
  * carry-over of the paused attempt's files (ADR-0018, U1).
2272
+ *
2273
+ * The move routes the task around the limit, so a run the pause held goes
2274
+ * on, as a retry would continue it. That run is still open — a hold never
2275
+ * closes one — and every edit that calls this ticks once it is written,
2276
+ * which starts the task.
2029
2277
  */
2030
2278
  releaseMovedResume(taskId: string, changes: Partial<Task>): Partial<Task>;
2031
2279
  /**
@@ -2070,26 +2318,65 @@ declare class TaskOrchestrator {
2070
2318
  /**
2071
2319
  * Retry a task paused on a usage limit, continuing its saved session where
2072
2320
  * it left one (ADR-0018, K1), then release the hold the pause put on the
2073
- * run once nothing else waits on a reset.
2321
+ * run once nothing else waits on a reset. `now` is the Resume now that
2322
+ * asked for it, or for another task on its runner, which sweeps it along.
2074
2323
  */
2075
2324
  private resumeTask;
2076
2325
  /**
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.
2326
+ * A resume nobody asked for cannot be refused to anyone. At the limit's
2327
+ * reset, one that may not claim waits for the user instead — its resume
2328
+ * time dropped, and said. One swept along by a Resume now keeps its resume
2329
+ * time, which its caller arms again: the reset has not come. A Stop says
2330
+ * so itself.
2080
2331
  */
2081
- private mayResumeAtReset;
2332
+ private mayResume;
2082
2333
  /**
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.
2334
+ * Manually start a single AI task right now (the "force start" affordance on
2335
+ * a task card), past a failed dependency (ADR-0034) or a merge gate
2336
+ * (ADR-0020), but never past the claim: it still needs the user's approval,
2337
+ * a free slot, and no Stop since it was asked for, and throws
2338
+ * {@link TaskControlError} saying which it lacks. A dependency still under
2339
+ * way or yet to run is not forced past: the task is released to the
2340
+ * scheduler instead, which starts it once they finish, and the line saying
2341
+ * so is the answer.
2087
2342
  * Reuses the scheduler's own claim so a force-started task gets the same
2088
2343
  * augmented prompt, session tracking, and exit handling — callers must not
2089
2344
  * re-spawn the runner themselves. No-op if the task is unknown, not an AI
2090
2345
  * task, or already running.
2091
2346
  */
2092
- forceStartTask(taskId: string, origin?: RunOrigin): Promise<void>;
2347
+ forceStartTask(taskId: string, origin?: RunOrigin): Promise<string | undefined>;
2348
+ /**
2349
+ * A forced start, from its run opening to its claim. It is judged again once
2350
+ * the run has opened, against the task as it stands then, which is what
2351
+ * the claim reads and runs. A queued one that something holds back again
2352
+ * meanwhile goes back in the queue; one made now that an edit gave
2353
+ * dependencies still under way waits for them as if forced after the edit,
2354
+ * and says so.
2355
+ */
2356
+ private startForced;
2357
+ /**
2358
+ * A force start of a task whose dependencies are still under way or yet to
2359
+ * run: started now, it would build on work that is not there, so it waits
2360
+ * for them in the run, off any hold, and says so. Dependencies are never
2361
+ * bypassed silently. A blocked task is forced past its failure as asked.
2362
+ * What it goes past at the merge gate is the gate the user confirmed now.
2363
+ */
2364
+ private queueBehindDependencies;
2365
+ /**
2366
+ * Starts the queued force starts nothing holds back any more. One the
2367
+ * scheduler now drives the run for, that nothing blocks and that was not
2368
+ * confirmed past a gate, is left to it. One at a gate the user did not
2369
+ * confirm — work that landed after they forced the start — waits for Merge
2370
+ * all, or for another force start, and says so once.
2371
+ */
2372
+ private startQueued;
2373
+ /**
2374
+ * A queued start the start rule holds back, at a gate it was not confirmed
2375
+ * past or otherwise. Taken out of the queue by now, so its dependencies
2376
+ * still under way read as `dependencies` rather than `queued`.
2377
+ */
2378
+ private holdsQueued;
2379
+ private sayQueuedAtGate;
2093
2380
  private refuseOpsDuringMerge;
2094
2381
  /**
2095
2382
  * A force start passes a merge gate (ADR-0020), and the surface confirmed it
@@ -2097,24 +2384,27 @@ declare class TaskOrchestrator {
2097
2384
  * on is not in the user's branch.
2098
2385
  */
2099
2386
  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
2387
  getCompletedCount(): number;
2108
2388
  getTotalCount(): number;
2109
2389
  isAllComplete(): boolean;
2110
2390
  isAnyFailed(): boolean;
2111
2391
  approveReview(origin?: RunOrigin): Promise<void>;
2112
2392
  tick(): Promise<void>;
2113
- /** How a run that ended on failures ends: what it finished, what failed, and what that blocks. */
2393
+ /**
2394
+ * The one step that decides, with nothing at work and nothing the scheduler
2395
+ * can start, whether the run goes on, is held, waits on the user or is over
2396
+ * (ADR-0034). Every surface takes `onExecutionComplete` as the run's end, so
2397
+ * only an end emits it. A run the scheduler is not driving — tasks started
2398
+ * on their own — ends on a verdict; a cancel, Mark complete or failed spawn
2399
+ * just closes it, so the next run decides its own mode rather than inherit
2400
+ * this one's.
2401
+ */
2402
+ private settleRun;
2114
2403
  /** Skipped work is not completed work, so a run with any says so rather than that everything completed. */
2115
2404
  private sayFinished;
2405
+ /** How a run that ended on failures ends: what it finished, what failed, and what that blocks. */
2116
2406
  private sayEndedOnFailure;
2117
- /** Whether the scheduler may start `id` now: it passes every readiness gate and a slot is free. */
2407
+ /** Whether the scheduler may start `id` now: nothing in the start rule holds it back, and a slot is free. */
2118
2408
  private stillReady;
2119
2409
  /**
2120
2410
  * A run paused at merge gates says so, as plainly as a task that waits on
@@ -2123,9 +2413,10 @@ declare class TaskOrchestrator {
2123
2413
  private sayGate;
2124
2414
  /**
2125
2415
  * 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.
2416
+ * since it was admitted (ADR-0029), the task left the plan or is being
2417
+ * marked complete, it is outside the user's approval record (ADR-0035), or
2418
+ * every slot is taken. Read in the same synchronous step as the claim it
2419
+ * guards, so nothing can change in between.
2129
2420
  */
2130
2421
  private claimRefusal;
2131
2422
  /** An explicit start's refusal, before it changes anything: its caller's answer, as a {@link TaskControlError}. */
@@ -2137,7 +2428,10 @@ declare class TaskOrchestrator {
2137
2428
  * start, a retry's, a continue, a resume, a repair. Checked and taken in one
2138
2429
  * synchronous step ({@link claimRefusal}), then marked claimed before
2139
2430
  * 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.
2431
+ * The task is read here, as it stands: a start that awaited first — its run
2432
+ * opening, the user's answer — runs what the plan holds now, not what it
2433
+ * held when it was asked for. A task already claimed is not claimed again:
2434
+ * there is nothing to start.
2141
2435
  */
2142
2436
  private claim;
2143
2437
  private runClaimed;
@@ -2165,6 +2459,8 @@ declare class TaskOrchestrator {
2165
2459
  * A spawn whose attempt ended while it was in flight: kill what it produced
2166
2460
  * and take back the claim it made. Only the claim — whatever ended the
2167
2461
  * attempt may have decided the task since (mark complete, cancel, retry).
2462
+ * A task Mark complete is landing stays in progress until that lands:
2463
+ * back to not started, the scheduler would start it again under the landing.
2168
2464
  */
2169
2465
  private abandonSpawn;
2170
2466
  private tell;
@@ -2194,153 +2490,1828 @@ declare class TaskOrchestrator {
2194
2490
  }
2195
2491
 
2196
2492
  /**
2197
- * Task references: the one grammar for naming a task in a plan, both ways.
2198
- * A label is rendered from a task's order under its parent ("2", "2.1"), and
2199
- * a reference written by anyone — the planner, a tool call, a command — is
2200
- * resolved back against the plan tree. Pure and import-free so a
2201
- * browser-bundled surface can use it through `@ordewell/core/plan-utils`.
2493
+ * The planner's read channel: a `{"taskQuery":{...}}` reply asking Ordewell to
2494
+ * show it what the per-turn plan block deliberately leaves out.
2495
+ *
2496
+ * The plan block carries only short fields, so a planner asked to "add a line
2497
+ * to task 3's prompt" would be rewriting text it has never read. Inlining every
2498
+ * body on every turn was rejected — a twenty-task plan re-sends thousands of
2499
+ * tokens per message — so the planner asks instead, and pays for the detail
2500
+ * only on the turns it uses it.
2501
+ *
2502
+ * This module owns the wire shape and the rendering of the answer; the loop,
2503
+ * the budgets and the injection live in the Session, and the classification
2504
+ * lives in PlanRepair alongside the plan and task-ops envelopes.
2202
2505
  */
2203
- /** The shape a reference resolves over: the plan's task tree, or a surface's projection of it. */
2204
- interface RefTask<T> {
2205
- id: string;
2206
- order: number;
2207
- title: string;
2208
- subtasks?: readonly T[];
2209
- }
2506
+ /** The long fields a query may ask for. Everything else is already in the plan block. */
2507
+ declare const TASK_QUERY_FIELDS: readonly ["description", "prompt", "userSteps", "verdict", "outputSummary", "userStoriesCovered", "skills", "output"];
2508
+ type TaskQueryField = typeof TASK_QUERY_FIELDS[number];
2509
+ /** Lines a `output` read returns when the query sets no count. */
2510
+ declare const OUTPUT_LINES_DEFAULT = 80;
2511
+ /** Hard cap on `outputLines` — the tail read is bounded however the planner asks. */
2512
+ declare const OUTPUT_LINES_MAX = 400;
2210
2513
  /**
2211
- * The parent a label extends. A label is the full path ("3.2.1"), so a parent
2212
- * below the top level carries the label it was itself reached by; without one
2213
- * the parent's order stands for it, which is right for a top-level parent.
2514
+ * Character budget the rendered answer converges to. A running output tail is
2515
+ * the only part with no natural bound — a runner may print for minutes — so
2516
+ * the tail absorbs the clamp; everything else (task bodies, the catalog cap)
2517
+ * is bounded upstream.
2214
2518
  */
2215
- interface RefParent {
2216
- order: number;
2217
- label?: string;
2519
+ declare const TASK_QUERY_ANSWER_MAX_CHARS = 20000;
2520
+ interface TaskQuery {
2521
+ /** Task references to read in full, as {@link resolveTaskRef} reads them. */
2522
+ tasks: string[];
2523
+ /** Narrows what comes back. Omitted means every field in {@link TASK_QUERY_FIELDS}. */
2524
+ fields?: TaskQueryField[];
2525
+ /** Also return the full runner/model catalog with labels, variants and mode descriptions. */
2526
+ catalog: boolean;
2527
+ /** Line count for `output` reads. Clamped at render to [1, {@link OUTPUT_LINES_MAX}]. */
2528
+ outputLines?: number;
2529
+ /** A previous answer's next offset: the output tail then covers only what came after it. */
2530
+ outputSince?: number;
2531
+ }
2532
+ declare function textHasTaskQuery(text: string): boolean;
2533
+ /** Parse a `{"taskQuery":...}` reply. Throws PlanParseError when the JSON is unusable. */
2534
+ declare function parseTaskQueryJson(text: string): TaskQuery;
2535
+ /** Stable identity of a query, so a turn can recognise the planner asking the same thing twice. */
2536
+ declare function taskQuerySignature(query: TaskQuery): string;
2537
+ /** What a full-catalog read is answered from. Models arrive already allowlist-filtered. */
2538
+ interface TaskQueryCatalog {
2539
+ runners: RunnerId[];
2540
+ models: Partial<Record<RunnerId, DiscoveredModel[]>>;
2541
+ modes: Record<RunnerId, RunnerModeInfo[]>;
2542
+ autonomousDefault: boolean;
2218
2543
  }
2219
- /** Dotted path label for a task, e.g. "2.1" for the first subtask of task 2, "2.1.3" one level down. */
2220
- declare function taskOrderLabel(task: {
2221
- order: number;
2222
- }, parent?: RefParent): string;
2223
- /** How a surface refers to a task in prose: "#2", or "#2.1" for a subtask. */
2224
- declare function taskRef(task: {
2225
- order: number;
2226
- }, parent?: RefParent): string;
2227
- /** A task named in full, as a confirmation or a picker title does: "#2 Deploy". */
2228
- declare function titledTaskRef(task: {
2229
- order: number;
2230
- title: string;
2231
- }, parent?: RefParent): string;
2232
2544
  /**
2233
- * The reference {@link taskRef} renders for the task a plan tree holds, by id
2234
- * — its full path, dotted, for a subtask. Undefined when the tree holds no such task.
2545
+ * Appended when the planner has spent its read budget, or asked the identical
2546
+ * question twice. The detail still comes back — refusing it would leave the
2547
+ * planner editing text it has not read, which is the whole failure this
2548
+ * channel exists to prevent — but the turn is told to land.
2235
2549
  */
2236
- declare function taskRefOf<T extends RefTask<T>>(tasks: readonly T[], id: string): string | undefined;
2550
+ declare const TASK_QUERY_ANSWER_OR_OPS: string;
2237
2551
  /**
2238
- * The reference and title {@link titledTaskRef} renders for the task a plan
2239
- * tree holds, by id — its full path, dotted, for a subtask. Undefined when the
2240
- * tree holds no such task.
2552
+ * The protocol as the planner is taught it, owned here beside the parser so the
2553
+ * two cannot drift. Both planner backends get these lines verbatim (ADR-0009):
2554
+ * a harness planner has no tool loop to reach, so the envelope is the channel.
2241
2555
  */
2242
- declare function titledTaskRefOf<T extends RefTask<T>>(tasks: readonly T[], id: string): string | undefined;
2556
+ declare const TASK_QUERY_PROTOCOL: string[];
2557
+ /** The one-line reminder the per-turn plan block carries, so the protocol has a single owner. */
2558
+ declare const TASK_QUERY_REMINDER = "- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {\"taskQuery\":{\"tasks\":[\"<id or #order>\"],\"catalog\":true}} \u2014 it changes nothing, and you then reply again with your ops.";
2243
2559
  /**
2244
- * Resolve a written reference to a task at any depth: an id, then an order
2245
- * label with or without "#" ("#2", "2", "#2.1"), then an exact title.
2246
- *
2247
- * An order label is read against the tree, never a flattened list: subtask
2248
- * orders restart at 1 under each parent, so "#2" always means top-level task 2
2249
- * and a subtask is reachable only by its dotted path ("#2.1", "#2.1.3").
2560
+ * Where an `output` read is answered from: the orchestrator's live capture of
2561
+ * the task's latest attempt. Injected so this module stays planner-side and
2562
+ * never imports the orchestrator.
2250
2563
  */
2251
- declare function resolveTaskRef<T extends RefTask<T>>(tasks: readonly T[], ref: string | number): T | undefined;
2564
+ type LiveOutputLookup = (taskId: string, opts: LiveTailOptions) => LiveTail | null;
2565
+ /**
2566
+ * Render the injected answer to one read. Never persisted — it is context for
2567
+ * the planner's next reply and nothing else — so it is built fresh from live
2568
+ * state on every query rather than cached. `liveOutput` backs the `output`
2569
+ * field; omitting it reads as "nothing captured".
2570
+ */
2571
+ declare function renderTaskQueryAnswer(query: TaskQuery, tasks: readonly Task[], catalog: TaskQueryCatalog, liveOutput?: LiveOutputLookup): string;
2252
2572
 
2253
2573
  /**
2254
- * Pure, no-imports helper so a browser surface can format a conflict's files
2255
- * without pulling `isolationRecord` — and its `path` import — into a webview
2256
- * bundle. Kept out of `isolationRecord.ts` for exactly that reason.
2574
+ * The SSRF guard for anything that leaves the machine on the planner's behalf.
2575
+ * An unguarded `http://169.254.169.254/` would hand a prompt-injected planner
2576
+ * the cloud metadata credentials, so every outbound host is vetted here: IP
2577
+ * literals by range, names by every address DNS answers with.
2257
2578
  */
2258
- declare function capConflictFiles(files: string[], max?: number): string;
2579
+ type ResolveAll = (host: string) => Promise<string[]>;
2259
2580
 
2260
- /** How a run is started, watched and merged: the user's, not a plan's (ADR-0030). */
2261
- interface GlobalRunSettings {
2262
- supervisorLevel: SupervisorLevel;
2263
- mergeAtGatesAutomatically: boolean;
2264
- /** Start a plan when the planner submits it, approved as if the user had pressed Execute. */
2265
- autorun: boolean;
2266
- /** Retries an Act supervisor may make of one task in a run (a loop guard). */
2267
- supervisorRetryLimit: number;
2268
- /** Reads a wake-up turn may make, in place of a user message's caps. */
2269
- wakeUpReadBudget: number;
2581
+ /**
2582
+ * The one decision every planner web read goes through, whichever runner
2583
+ * plans and whichever path carries the bytes (ADR-0027). A fetch names the
2584
+ * URL it reads; a search names its query and the provider that receives it.
2585
+ */
2586
+ type WebRead = {
2587
+ kind: 'fetch';
2588
+ url: string;
2270
2589
  }
2271
- declare const RUN_SETTINGS_DEFAULTS: Readonly<GlobalRunSettings>;
2272
- declare class RunSettingsError extends Error {
2273
- constructor(message: string);
2590
+ /** `provider` keys the grant; `providerLabel`, when given, names it to the person asked. */
2591
+ | {
2592
+ kind: 'search';
2593
+ query: string;
2594
+ provider: string;
2595
+ providerLabel?: string;
2596
+ note?: string;
2597
+ };
2598
+ /** A refusal's `reason` is written for the planner: it says what to do next. */
2599
+ type WebReadDecision = {
2600
+ allowed: true;
2601
+ } | {
2602
+ allowed: false;
2603
+ reason: string;
2604
+ };
2605
+ interface DecideOptions {
2606
+ /**
2607
+ * Research subagents run in the background where nothing can prompt
2608
+ * (ADR-0008, T6). When false, a read that would ask is refused with the gap
2609
+ * to report, rather than raising a prompt nobody can answer.
2610
+ */
2611
+ prompting?: boolean;
2274
2612
  }
2275
- interface UserSettings {
2276
- modelAllowlist?: Record<string, string[]>;
2277
- /** Last model (and its thinking effort) the user chose for each planner backend, keyed by planner backend id. */
2278
- plannerModels?: Record<string, {
2279
- model: string;
2280
- effort?: string;
2281
- }>;
2613
+ interface WebReadPolicyOptions {
2614
+ approval: IApproval;
2615
+ /** The secrets Ordewell holds — provider keys, MCP tokens — read per request. */
2616
+ secrets?: () => Iterable<string>;
2617
+ /** Defaults to `process.env`; its credential-named values are secrets too. */
2618
+ env?: () => Record<string, string | undefined>;
2619
+ /** How many web reads one planner turn may make; read per request, so a settings change applies at once. */
2620
+ budget?: () => number;
2621
+ /** The planner turn in progress; a new one starts a new budget. */
2622
+ turn?: () => string | undefined;
2623
+ /** Override DNS resolution (every answer for a name) in tests. */
2624
+ resolveHost?: ResolveAll;
2625
+ }
2626
+ declare const DEFAULT_WEB_READ_BUDGET = 40;
2627
+ declare class WebReadPolicy {
2628
+ private readonly approval;
2629
+ private readonly resolveHost;
2630
+ private readonly secrets;
2631
+ private readonly env;
2632
+ private readonly budget;
2633
+ private readonly turn;
2634
+ private spent;
2635
+ constructor(opts: WebReadPolicyOptions);
2282
2636
  /**
2283
- * Runners the user picked for planning. Absent — not empty — is what falls
2284
- * back to the environment's defaults; `[]` is a deliberate "none of them".
2637
+ * Every check that needs no network runs first, then the budget and the
2638
+ * approval, and only then is a fetch's host resolved: a lookup is itself a
2639
+ * request, so `<data>.attacker.example` would leave through DNS even for a
2640
+ * read that is then excluded, over budget or declined.
2285
2641
  */
2286
- enabledRunners?: string[];
2287
- /** Full (true) or Guarded (false), ADR-0001; Full until the user chooses. */
2288
- autonomousMode?: boolean;
2289
- supervisorLevel?: SupervisorLevel;
2290
- mergeAtGatesAutomatically?: boolean;
2291
- autorun?: boolean;
2292
- supervisorRetryLimit?: number;
2293
- wakeUpReadBudget?: number;
2642
+ decide(read: WebRead, opts?: DecideOptions): Promise<WebReadDecision>;
2643
+ private subagentRefusal;
2644
+ /**
2645
+ * Whether a search through `provider` would run without asking anyone, for
2646
+ * a runner whose own search raises no request and is set on or off when
2647
+ * its session starts (ADR-0027). A zero budget disables it; no query is
2648
+ * known yet, so secrets and individual reads cannot be checked or counted.
2649
+ */
2650
+ searchesUnasked(provider: string): boolean;
2651
+ /** The last check, the one that leaves the machine: every DNS answer for the host must be public. */
2652
+ private resolved;
2653
+ private refuse;
2654
+ private vetFetch;
2655
+ private vetSearch;
2656
+ private spend;
2657
+ private refund;
2658
+ private carriesSecret;
2294
2659
  }
2295
- /**
2296
- * The model allowlist, remembered planner models and enabled runners are shared
2297
- * across processes by default. `ORDEWELL_SETTINGS_PATH` lets independent runs
2298
- * use separate files: otherwise a write in one run would change another's
2299
- * settings when `getAll()` detects the new mtime.
2300
- */
2301
- declare function getSettingsPath(): string;
2302
- /** The run settings `settings` holds, each one it leaves out at its default. */
2303
- declare function runSettingsOf(settings: UserSettings): GlobalRunSettings;
2304
- declare class SettingsService {
2305
- private filePath;
2306
- private cache;
2307
- private cachedMtimeMs;
2308
- constructor(filePath?: string);
2309
- getAll(): UserSettings;
2310
- private fileMtimeMs;
2311
- getModelAllowlist(runner: string): string[] | undefined;
2312
- setModelAllowlist(runner: string, ids: string[] | undefined): void;
2313
- getPlannerModel(provider: string): {
2314
- model: string;
2315
- effort?: string;
2316
- } | undefined;
2317
- setPlannerModel(provider: string, entry: {
2318
- model: string;
2319
- effort?: string;
2320
- } | undefined): void;
2321
- getEnabledRunners(): string[] | undefined;
2322
- setEnabledRunners(ids: string[]): void;
2660
+
2661
+ interface IWebFetcher {
2323
2662
  /**
2324
- * Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
2325
- * environment wins, so a script or CI run can fix its own without touching
2326
- * the user's file.
2663
+ * The web-read policy's decision (ADR-0027), for a read this fetcher will
2664
+ * carry or one a runner carries natively, such as Claude's `WebFetch`.
2665
+ * `fetch` and `search` decide for themselves; never call both.
2327
2666
  */
2328
- autonomousMode(): boolean;
2329
- /** Whether `ORDEWELL_AUTONOMOUS_MODE` decides the level, whatever this file says. */
2330
- autonomousModeOverridden(): boolean;
2331
- setAutonomousMode(enabled: boolean): void;
2332
- getRunSettings(): GlobalRunSettings;
2333
- /** Checks every value before writing any, so a refused change saves nothing. */
2334
- updateRunSettings(changes: Partial<GlobalRunSettings>): GlobalRunSettings;
2335
- private load;
2336
- private persist;
2667
+ decide(read: WebRead): Promise<WebReadDecision>;
2668
+ /** Whether a runner's own search through `provider` would run unasked; see `WebReadPolicy.searchesUnasked`. */
2669
+ searchesUnasked?(provider: string): boolean;
2670
+ fetch(url: string): Promise<ToolOutcome>;
2671
+ /**
2672
+ * Optional web search. A fetcher without it makes the `web_search` tool
2673
+ * report itself unavailable rather than failing the turn — the same
2674
+ * degradation `fetch` already uses when no fetcher is wired at all.
2675
+ */
2676
+ search?(query: string): Promise<ToolOutcome>;
2677
+ /**
2678
+ * A variant of this fetcher for a research subagent: same policy, cache,
2679
+ * transport and budget, but a read that would ask a person is refused
2680
+ * instead (ADR-0027, ADR-0008 T6). Absent means subagents get no web tools
2681
+ * and the `fetch`/`web_search` tools report themselves unavailable.
2682
+ */
2683
+ forSubagent?(): IWebFetcher;
2337
2684
  }
2338
2685
 
2339
- declare const EMPTY_RUN: RunView;
2340
- declare function reduceRun(view: RunView, input: RunInput): RunView;
2686
+ /**
2687
+ * A person's approvals of project MCP servers, kept across sessions
2688
+ * (ADR-0028). Keyed by repository root and scope, which names the server
2689
+ * and hashes its definition: a changed definition is a new question.
2690
+ */
2691
+ interface ProjectServerApprovals {
2692
+ approved(repoRoot: string, scope: string): boolean;
2693
+ remember(repoRoot: string, scope: string): void;
2694
+ }
2341
2695
 
2342
- /** What the blocked rule reads of a task; structural, so a surface's own task shape answers it too. */
2343
- interface BlockingSubject {
2696
+ interface CliAgentAiServiceDeps extends Partial<AgentProcessDeps> {
2697
+ /** Overrides adapter construction. Tests supply a fake agent; production picks by runner id. */
2698
+ createAdapter?: (runner: string, deps: AgentProcessDeps) => AgentAdapter | null;
2699
+ /** Workspace root the agent explores. Defaults to the host process's cwd. */
2700
+ workspaceRoot?: () => string;
2701
+ /** Where a conversation's planner tools are served (ADR-0022). Absent: no conversation can start. */
2702
+ mcpServer?: OrdewellMcpServer;
2703
+ /** A person's approvals of project MCP servers, kept across sessions (ADR-0028). Defaults to Ordewell's data directory. */
2704
+ projectServerApprovals?: ProjectServerApprovals;
2705
+ }
2706
+ /**
2707
+ * A coding agent driven as Ordewell's planner (ADR-0009).
2708
+ *
2709
+ * The second transport behind `IAiService`, sitting beside `OpenAiService` and
2710
+ * `GeminiService`. It deliberately does **not** extend {@link BaseAiService}:
2711
+ * that class's body is Ordewell executing research tools on a model's behalf,
2712
+ * which is precisely the part a coding agent replaces. What it reuses instead
2713
+ * is everything above the transport — `settleReply` (the empty-reply nudge
2714
+ * and the turn's settling), the plan validator, the `ResearchProgress` events the
2715
+ * four surfaces already render. That is why this backend reaches VS Code, the
2716
+ * web UI, the CLI and the TUI without any of them learning a coding agent is
2717
+ * on the other end.
2718
+ */
2719
+ declare class CliAgentAiService implements IAiService {
2720
+ private config;
2721
+ private readonly runner;
2722
+ private readonly processDeps;
2723
+ private readonly makeAdapter;
2724
+ private readonly workspaceRoot;
2725
+ private environmentSecrets;
2726
+ /** The conversation's session, from its start until the next session boundary. */
2727
+ private session;
2728
+ private readonly mcpServer;
2729
+ private readonly projectServerApprovals;
2730
+ private activeAbort;
2731
+ constructor(config: IConfig, deps?: CliAgentAiServiceDeps);
2732
+ hasActiveConversation(): boolean;
2733
+ /**
2734
+ * False when the running agent process was spawned under a model/effort the
2735
+ * user has since changed in the picker. Unlike a vendor backend, the model
2736
+ * is a spawn-time argument to the agent CLI (`--model`), not a per-request
2737
+ * field — `continueConversation` sends the next turn to whatever process is
2738
+ * already running, so a plain config read here would silently keep planning
2739
+ * on the old model. No conversation yet is vacuously "current".
2740
+ */
2741
+ conversationMatchesConfig(): boolean;
2742
+ /** Always: a conversation whose planner cannot reach the Ordewell MCP server fails to start rather than falling back to the envelopes. */
2743
+ plannerToolsAttached(): boolean;
2744
+ /** The agent's own session id, a resumption hint only — Ordewell's transcript is authoritative (T4). */
2745
+ nativeSessionId(): string | null;
2746
+ heldSecrets(): readonly string[];
2747
+ restart(): void;
2748
+ reset(): void;
2749
+ startConversation(req: ConversationRequest): Promise<ConversationTurn>;
2750
+ continueConversation(userMessage: string, onProgress: (progress: ResearchProgress) => void, signal?: AbortSignal): Promise<ConversationTurn>;
2751
+ private openingMessage;
2752
+ /**
2753
+ * Drive one user message to a settled planner turn through
2754
+ * {@link settleReply}, the loop the API backend settles through too. The
2755
+ * tool rounds belong to the agent now, so one call is one agent turn —
2756
+ * continued while it left subagents running in the background.
2757
+ */
2758
+ private runConversation;
2759
+ /**
2760
+ * Run one agent turn: stream its events into `ResearchProgress`, collect the
2761
+ * reply text, and return the research steps produced. Tool calls are matched
2762
+ * to their results by the agent's own call id — matching by tool name puts
2763
+ * one file's body on another file's row the moment an agent runs two reads
2764
+ * at once, which all three of these do routinely.
2765
+ */
2766
+ /**
2767
+ * A turn, and then — while the agent ended it with subagents still in the
2768
+ * background — a turn asking it to wait for them: whatever they find would
2769
+ * otherwise be said into a closed turn, or, in a one-shot, into a session
2770
+ * already thrown away. Bounded by `waits`, shared across a reply's sends.
2771
+ */
2772
+ private runTurnAwaitingAgents;
2773
+ private runTurn;
2774
+ /**
2775
+ * The only way this service starts an agent, and it takes a planner start by
2776
+ * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
2777
+ * mode from here without changing this signature. A planner that cannot
2778
+ * reach its tools cannot submit a plan, so it is respawned once and never
2779
+ * left to plan without them (ADR-0025).
2780
+ */
2781
+ private startAdapter;
2782
+ private newAdapter;
2783
+ /** Whether the web-read policy lets the runner's own session-wide search run unasked now (ADR-0027). */
2784
+ private nativeSearch;
2785
+ /**
2786
+ * The live agent process, restarted from its own session id if it died
2787
+ * between turns. Resume is a hint: when it fails, the caller's next
2788
+ * `startConversation` reseeds from Ordewell's transcript, which is the same
2789
+ * degradation `restoreChat` already performs on every surface.
2790
+ *
2791
+ * Also restarted when the policy's decision on the runner's own search has
2792
+ * changed since the process was spawned: the running thread would otherwise
2793
+ * keep searching unasked after Full turned Guarded. Codex 0.162 applies
2794
+ * `thread/resume`'s `config.web_search` either way (checked live).
2795
+ */
2796
+ private ensureAdapter;
2797
+ /**
2798
+ * What the planner gets of the repository's config, its runner being
2799
+ * launched without it (ADR-0028): the instruction files, which are text, in
2800
+ * its system prompt, and the project MCP servers it may start. Decided once
2801
+ * per conversation, so a respawn starts the same servers without asking.
2802
+ */
2803
+ private repoConfig;
2804
+ private plannerModel;
2805
+ /**
2806
+ * A plan from one-shot agent sessions: only what a submit_plan call
2807
+ * accepted, never the reply text, so a plan the tool refused cannot come
2808
+ * back in as the JSON the agent echoed. A session that ends without an
2809
+ * accepted call is followed by one corrective session, then fails.
2810
+ */
2811
+ private submittedPlan;
2812
+ /**
2813
+ * A single agent session that answers one prompt and exits. Used by every
2814
+ * non-conversational entry point. A plan-shaped envelope in its text
2815
+ * streams to the plan display, as a vendor planner's one-shot does, and the
2816
+ * prose around it is not streamed at all, since no turn is open to show it in.
2817
+ */
2818
+ private oneShot;
2819
+ researchAndPlan(userDescription: string, runners: RunnerId[], modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>, fs: IFileSystem, onProgress: (progress: ResearchProgress) => void, _fetcher?: IWebFetcher, runnerModes?: Record<RunnerId, RunnerModeInfo[]>, modes?: PlannerModes, signal?: AbortSignal): Promise<{
2820
+ tasks: Task[];
2821
+ researchLog: ResearchLogEntry[];
2822
+ researchResults: string;
2823
+ }>;
2824
+ generatePlanDirect(userDescription: string, runners?: RunnerId[], modelsByRunner?: Partial<Record<RunnerId, DiscoveredModel[]>>, onToken?: (token: string) => void, _fs?: IFileSystem, _fetcher?: IWebFetcher, runnerModes?: Record<RunnerId, RunnerModeInfo[]>, modes?: PlannerModes, signal?: AbortSignal): Promise<Task[]>;
2825
+ }
2826
+
2827
+ /**
2828
+ * The planner tools a session offers its conversation (ADR-0022). A backend
2829
+ * that can inject the Ordewell MCP server into its planner issues a planner
2830
+ * token bound to `sessionId` and these handlers; API planners use the same task read handlers directly in their tool loop.
2831
+ */
2832
+ interface PlannerToolsOffer {
2833
+ sessionId: string;
2834
+ handler: PlannerToolHandler;
2835
+ }
2836
+ /**
2837
+ * Everything the conversation loop needs to start planning (ADR-0002).
2838
+ * The AI service keeps the tool-use message history internally across turns;
2839
+ * the surfaces only exchange user/assistant messages with it.
2840
+ */
2841
+ interface ConversationRequest {
2842
+ goal: string;
2843
+ runners: RunnerId[];
2844
+ modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>;
2845
+ fs: IFileSystem;
2846
+ onProgress: (progress: ResearchProgress) => void;
2847
+ fetcher?: IWebFetcher;
2848
+ runnerModes?: Record<RunnerId, RunnerModeInfo[]>;
2849
+ autonomousDefault?: boolean;
2850
+ /** Where tasks will run (ADR-0013, ADR-0014): in worktrees, the prompt drops file-overlap ordering and describes a repo group. */
2851
+ isolatedExecution?: IsolatedExecution;
2852
+ /** A plan the planner submits starts at once (ADR-0030), so the opening prompt has no outline gate. */
2853
+ autorun?: boolean;
2854
+ signal?: AbortSignal;
2855
+ /**
2856
+ * Persisted dialogue to seed a resumed conversation (session reload). The
2857
+ * turns are injected into the message history verbatim — no LLM call happens
2858
+ * for them; the first call is the turn opened by `initialMessage`.
2859
+ */
2860
+ priorHistory?: ConversationMessage[];
2861
+ /**
2862
+ * The user message that opens this turn. Defaults to `goal` — set it when
2863
+ * resuming so the system prompt keeps the original goal while the turn
2864
+ * carries the user's new message.
2865
+ */
2866
+ initialMessage?: string;
2867
+ plannerTools?: PlannerToolsOffer;
2868
+ /**
2869
+ * The session's approval policy, which a harness planner's own tool
2870
+ * requests are decided against as the API planner's are (ADR-0026). Absent:
2871
+ * nobody can be asked, so they are denied.
2872
+ */
2873
+ approval?: IApproval;
2874
+ /**
2875
+ * The workspace's skill catalog. Every planner is shown the task skills it
2876
+ * may attach; only one with tools is shown the planner skills, which load
2877
+ * through `load_skill` alone (ADR-0024).
2878
+ */
2879
+ skills?: readonly SkillInfo[];
2880
+ }
2881
+ /**
2882
+ * A run of a turn's reply text, as a backend reports it. `narration`: it began
2883
+ * before its call's last tool call, so it stays when a corrective retry
2884
+ * discards what the call replied.
2885
+ */
2886
+ interface TurnRun extends ReplyRun {
2887
+ narration?: true;
2888
+ }
2889
+ /**
2890
+ * One planner turn's outcome. The planner talks to the user (`message`),
2891
+ * commits a full plan (`plan`, a `{tasks:[...]}` JSON object), emits targeted
2892
+ * task edits (`task_ops`, a `{taskOps:[...]}` JSON object), or asks to read
2893
+ * task bodies and the full catalog before editing (`task_query`, a
2894
+ * `{taskQuery:{...}}` JSON object). The Session validates and applies task ops
2895
+ * atomically and answers queries out of its own state; the AI service only
2896
+ * parses them.
2897
+ */
2898
+ type ConversationTurn =
2899
+ /**
2900
+ * `aborted`: the call was stopped, and `text` is only what it had by then.
2901
+ * `failed`: the call ended without a reply (a dead agent, a usage limit),
2902
+ * and `text` says why; `usageLimit`, where the runner also said in a field
2903
+ * whether that was its usage limit (see {@link TurnFailure}).
2904
+ * `runs`, on every kind: the reply's runs the turn's surviving calls
2905
+ * streamed, in order, and last a note written in the planner's place when
2906
+ * `text` is one. An envelope's own run is not among them. Absent from a
2907
+ * backend that does not report runs.
2908
+ */
2909
+ {
2910
+ kind: 'message';
2911
+ text: string;
2912
+ researchLog: ResearchLogEntry[];
2913
+ aborted?: boolean;
2914
+ failed?: boolean;
2915
+ usageLimit?: UsageLimitSignal | false;
2916
+ runs?: TurnRun[];
2917
+ } | {
2918
+ kind: 'plan';
2919
+ tasks: Task[];
2920
+ text: string;
2921
+ researchLog: ResearchLogEntry[];
2922
+ runs?: TurnRun[];
2923
+ } | {
2924
+ kind: 'task_ops';
2925
+ ops: TaskOp[];
2926
+ text: string;
2927
+ researchLog: ResearchLogEntry[];
2928
+ runs?: TurnRun[];
2929
+ } | {
2930
+ kind: 'task_query';
2931
+ query: TaskQuery;
2932
+ text: string;
2933
+ researchLog: ResearchLogEntry[];
2934
+ runs?: TurnRun[];
2935
+ };
2936
+ interface IAiService {
2937
+ /** Credential values from the harness planner's effective environment, for the outbound secret check. */
2938
+ heldSecrets?(): readonly string[];
2939
+ /**
2940
+ * Begin the planner conversation: collect workspace context, run the
2941
+ * research/tool loop, and return the first planner turn. The service
2942
+ * retains the full API message history for subsequent
2943
+ * {@link continueConversation} calls.
2944
+ */
2945
+ startConversation(req: ConversationRequest): Promise<ConversationTurn>;
2946
+ /**
2947
+ * Feed the user's reply into the active conversation and return the next
2948
+ * planner turn. Throws if no conversation is active.
2949
+ */
2950
+ continueConversation(userMessage: string, onProgress: (progress: ResearchProgress) => void, signal?: AbortSignal): Promise<ConversationTurn>;
2951
+ /**
2952
+ * Whether a planner conversation is currently held in memory. A branching
2953
+ * predicate only — never a test for whether there is anything to release.
2954
+ * The harness backend holds an OS process that outlives its conversation, so
2955
+ * guarding {@link reset} with this leaks an agent process; call `reset`
2956
+ * unconditionally instead.
2957
+ */
2958
+ hasActiveConversation(): boolean;
2959
+ /**
2960
+ * Optional: end the planner's live process but keep its own session id, so
2961
+ * the next turn starts a fresh one resumed from it (ADR-0009). What a
2962
+ * supervisor does to a planner that crashed or hung (ADR-0029). Absent where
2963
+ * there is no process to end: the next call is a fresh request anyway.
2964
+ */
2965
+ restart?(): void;
2966
+ /**
2967
+ * Whether the active conversation (if any) still matches the planner
2968
+ * model/effort currently configured. Optional, and true when absent: a
2969
+ * vendor backend re-reads its model from config on every turn, so there is
2970
+ * nothing to drift. A harness planner (ADR-0009) is the exception — its
2971
+ * model is baked into the agent process at spawn — so only
2972
+ * {@link CliAgentAiService} answers this for real. False tells the caller
2973
+ * the same thing an absent conversation would: don't call
2974
+ * `continueConversation`, restart instead so the new model takes effect.
2975
+ */
2976
+ conversationMatchesConfig?(): boolean;
2977
+ /**
2978
+ * Whether the planner reads the catalog and submits plans through Ordewell's
2979
+ * MCP tools (ADR-0022). Optional, and false when absent: only a harness
2980
+ * planner the server attached to has them, and every other planner keeps
2981
+ * the per-turn catalog and the envelopes.
2982
+ */
2983
+ plannerToolsAttached?(): boolean;
2984
+ /**
2985
+ * Optional: prune bulky raw tool output out of the live conversation, in
2986
+ * place, keeping subagent digests. Returns the characters removed. Absent
2987
+ * where the backend holds no tool transcript to prune (a harness planner's
2988
+ * agent owns its own context).
2989
+ */
2990
+ pruneContext?(): number;
2991
+ /**
2992
+ * One-shot research + plan for non-conversational surfaces (CLI `plan --goal`,
2993
+ * web REST). Never asks questions — it plans with what it can find.
2994
+ */
2995
+ researchAndPlan(userDescription: string, runners: RunnerId[], modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>, fs: IFileSystem, onProgress: (progress: ResearchProgress) => void, fetcher?: IWebFetcher, runnerModes?: Record<RunnerId, RunnerModeInfo[]>, modes?: PlannerModes, signal?: AbortSignal): Promise<{
2996
+ tasks: Task[];
2997
+ researchLog: ResearchLogEntry[];
2998
+ researchResults: string;
2999
+ }>;
3000
+ generatePlanDirect(userDescription: string, runners: RunnerId[], modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>, onToken?: (token: string) => void, fs?: IFileSystem, fetcher?: IWebFetcher, runnerModes?: Record<RunnerId, RunnerModeInfo[]>, modes?: PlannerModes, signal?: AbortSignal): Promise<Task[]>;
3001
+ /**
3002
+ * Release everything this service holds: the conversation, any in-flight
3003
+ * turn, and — on the harness backend — the agent process itself. Idempotent
3004
+ * and cheap in every implementation, so callers never gate it.
3005
+ */
3006
+ reset(): void;
3007
+ }
3008
+ /**
3009
+ * The single branch point between planner transports. Two of them talk HTTP to
3010
+ * an LLM vendor; the third (ADR-0009) drives a coding agent already installed
3011
+ * on the machine. Everything downstream — the plan contract, the research-step
3012
+ * stream, the four surfaces — is shared.
3013
+ */
3014
+ declare function createAiService(config: IConfig, deps?: CliAgentAiServiceDeps): IAiService;
3015
+
3016
+ /** The dialogue record a fork carries — what {@link PlannerConversation.clone} returns. */
3017
+ interface ForkedDialogue {
3018
+ conversationHistory: ConversationMessage[];
3019
+ researchLog: ResearchLogEntry[];
3020
+ }
3021
+
3022
+ /** A user's message as sent, and the skills its `/name` tokens load. */
3023
+ interface SkillInvocation {
3024
+ text: string;
3025
+ skills: SkillLoad[];
3026
+ }
3027
+ /**
3028
+ * A path with the home directory written `~`, for a notice or listing. On
3029
+ * Windows the drive letter's case is not significant and the separators are
3030
+ * rendered `/`, so the same skill reads alike wherever the session reloads.
3031
+ */
3032
+ declare function abbreviateHome(file: string, home?: string, platform?: NodeJS.Platform): string;
3033
+ /**
3034
+ * Resolve the `/skill-name` tokens in a message to snapshots of the skills they
3035
+ * name. Pure and exported so surfaces can resolve without a full Session. The text itself is never rewritten.
3036
+ *
3037
+ * Every distinct skill named loads, once, in the order first named. A token
3038
+ * naming no skill is plain text, wherever it stands — a whole message of
3039
+ * `/unknown` included: embedded in a sentence it is as likely to be incidental
3040
+ * text (a path, an example command) as a typo'd invocation. So is one naming
3041
+ * a skill with `user-invocable: false`, which only the model may invoke.
3042
+ */
3043
+ declare function resolveSkillInvocation(text: string, skillsService: Pick<SkillsService, 'findSkill'>, home?: string): SkillInvocation;
3044
+ /**
3045
+ * What the planner receives for a user message: the bodies of the skills it
3046
+ * loaded, then the message as the user wrote it. The one composition for a
3047
+ * live send and for a replay of the transcript, so a resumed conversation
3048
+ * hands the planner what the live one did.
3049
+ */
3050
+ declare function plannerMessage(text: string, loads: readonly SkillLoad[]): string;
3051
+ /**
3052
+ * The transcript as the planner is to see it on a replay: each user message
3053
+ * carries the skills loaded after it, composed by {@link plannerMessage}, and
3054
+ * the load entries themselves are folded away. Sessions saved before skill
3055
+ * loads existed hold the expanded text in the message and replay unchanged.
3056
+ * A failed turn's entry is left out: the planner never said it.
3057
+ */
3058
+ declare function plannerTranscript(history: readonly ConversationMessage[]): ConversationMessage[];
3059
+
3060
+ /**
3061
+ * What a stopped runner says about why it stopped, and what ending an attempt
3062
+ * does to the runner it leaves behind. TaskOrchestrator acts on the answers.
3063
+ */
3064
+
3065
+ /**
3066
+ * A turn's failure as its runner reported it: its words, and `usageLimit`
3067
+ * where its protocol also says in a field whether it was the limit (Codex's
3068
+ * `codexErrorInfo`).
3069
+ */
3070
+ interface TurnFailure {
3071
+ message: string;
3072
+ usageLimit?: UsageLimitSignal | false;
3073
+ }
3074
+
3075
+ /** What a usage limit that ended a planner turn led to. */
3076
+ interface PlannerLimit {
3077
+ /** The line the transcript keeps, drawn as a failure. */
3078
+ text: string;
3079
+ /** When what the turn took goes again on its own; null when it waits for the user's next message. */
3080
+ resendAt: Date | null;
3081
+ /** Why nothing goes again on its own, when nothing does: what the planner did, as in "the planner hit its usage limit and …". */
3082
+ held?: string;
3083
+ }
3084
+
3085
+ /**
3086
+ * What the user allows the plan to be built from right now: the enabled
3087
+ * runners and each runner's model allowlist. Structural, so a surface that
3088
+ * only holds the two lists can ask without a session.
3089
+ */
3090
+ interface AssignmentScope {
3091
+ enabledRunners: readonly string[];
3092
+ allowlist: Readonly<Record<string, readonly string[] | undefined>>;
3093
+ /**
3094
+ * The models discovery has found per runner, where the holder has them. An
3095
+ * allowlist is read against them exactly as the planner's catalog reads it
3096
+ * (ids only another runner lists limit nothing), so a flag never says a
3097
+ * model is off a list the planner sees as no limit.
3098
+ */
3099
+ models?: Readonly<Partial<Record<RunnerId, readonly DiscoveredModel[]>>>;
3100
+ }
3101
+ /** The part of a task an assignment is read from. */
3102
+ interface AssignedTask {
3103
+ type: string;
3104
+ status: string;
3105
+ assignedRunner: string;
3106
+ assignedModel?: {
3107
+ modelId: string;
3108
+ modelLabel?: string;
3109
+ };
3110
+ }
3111
+ type AssignmentIssue = {
3112
+ kind: 'runner-off';
3113
+ runner: string;
3114
+ } | {
3115
+ kind: 'model-off-list';
3116
+ runner: string;
3117
+ modelId: string;
3118
+ modelLabel: string;
3119
+ };
3120
+ /**
3121
+ * Whether a task sits outside the scope it is read against. This only names
3122
+ * the mismatch: such a task still runs (ADR-0003, P1) — the allowlist and the
3123
+ * enabled runners shape what is planned, not what is started — and the user or
3124
+ * the planner may move it. A runner that is off settles the matter before its
3125
+ * model is looked at.
3126
+ */
3127
+ declare function assignmentIssue(task: AssignedTask, scope: AssignmentScope): AssignmentIssue | null;
3128
+ declare function assignmentIssueText(issue: AssignmentIssue): string;
3129
+
3130
+ /**
3131
+ * The planner turn (see CONTEXT.md): the one piece of planner work that holds
3132
+ * the conversation, from its claim to its settled outcome. Its signal is what a
3133
+ * stop aborts.
3134
+ */
3135
+ declare class PlannerTurn {
3136
+ private readonly controller;
3137
+ private dropped;
3138
+ private forced;
3139
+ get signal(): AbortSignal;
3140
+ /** The plan it was answering was dropped under it, so nothing it settles may land. */
3141
+ get abandoned(): boolean;
3142
+ /** A force send stopped it, not the user: the forced turn follows. A turn already stopped stays the user's stop. */
3143
+ get forceSent(): boolean;
3144
+ stop(by?: 'force_send'): void;
3145
+ abandon(): void;
3146
+ }
3147
+ /** What a planner turn may change: the whole plan, or tasks through edits. */
3148
+ type PlanChange = 'plan' | 'edit';
3149
+ /**
3150
+ * A wake-up turn (ADR-0029): a supervisor's turn on wake-up digests alone, no
3151
+ * user message in it. What it may do is its level's, decided by the host.
3152
+ */
3153
+ interface WakeUpTurn {
3154
+ /** Reads the turn may make, in place of a user message's caps. */
3155
+ readBudget: number;
3156
+ /**
3157
+ * Why a whole plan, or a task edit, is refused in this turn, worded for the
3158
+ * model; undefined when its level allows it. Asked when the change is made
3159
+ * and again as it lands, so a level lowered during the turn applies at once.
3160
+ */
3161
+ refused(change: PlanChange): string | undefined;
3162
+ /** Told to the model after the digests and never saved, like the plan block: who it is in this turn, and what it may do. */
3163
+ brief?: string;
3164
+ }
3165
+ /** A read made through a planner tool: refused once the turn's budget is gone, else answered, told to land when the budget asks it to. */
3166
+ type ToolRead<T> = {
3167
+ status: 'refused';
3168
+ } | {
3169
+ status: 'answered';
3170
+ value: T;
3171
+ landNow: boolean;
3172
+ };
3173
+ /** A plan or task edit handed to the open turn over a channel other than its reply (ADR-0022). */
3174
+ type PlannerSubmission = {
3175
+ kind: 'plan';
3176
+ tasks: Task[];
3177
+ } | {
3178
+ kind: 'task_ops';
3179
+ ops: TaskOp[];
3180
+ };
3181
+ /** Everything (re)opening a conversation needs besides the dialogue itself. */
3182
+ type ConversationOpening = Omit<ConversationRequest, 'goal' | 'onProgress' | 'signal' | 'priorHistory' | 'initialMessage'>;
3183
+ /**
3184
+ * What the conversation needs from the session that hosts it. The session keeps
3185
+ * plan state, persistence and scheduling; the conversation reaches them only
3186
+ * through here, so a fake host is enough to drive it.
3187
+ */
3188
+ interface PlannerConversationHost {
3189
+ /** The plan the transcript lives on (`conversationHistory`, `researchLog`). */
3190
+ plan(): LegacyPlanState | null;
3191
+ goal(): string;
3192
+ aiService(): IAiService;
3193
+ onProgress(progress: ResearchProgress): void;
3194
+ /** Fresh discovery, allowlist-filtered the way the system prompt shows it. */
3195
+ opening(runners: RunnerId[]): Promise<ConversationOpening>;
3196
+ /** What the per-turn catalog block and every read draw from, as of now. */
3197
+ catalog(): TaskQueryCatalog;
3198
+ /** Whether autorun is on now (ADR-0030); the planner is told in the turn after it changes. */
3199
+ autorun(): boolean;
3200
+ /** The enabled runners and allowlist as of now; the planner is told in the turn after either changes. */
3201
+ assignmentScope(): AssignmentScope;
3202
+ tasks(): readonly Task[];
3203
+ /**
3204
+ * Why a task cannot start now, from the scheduler's start rule (ADR-0034);
3205
+ * none once nothing holds it back, or while the plan is not yet approved.
3206
+ */
3207
+ startWaits(task: Readonly<Task>): readonly StartWait[];
3208
+ /**
3209
+ * The orchestrator's live capture for a task's latest attempt, backing the
3210
+ * `output` field of a read. Injected the same way as the catalog so the
3211
+ * conversation never reaches into execution state directly.
3212
+ */
3213
+ liveOutput: LiveOutputLookup;
3214
+ isExecuting(): boolean;
3215
+ /** The session's single mutation ritual: op → persist → notify (default: the plan). */
3216
+ mutate(op: () => boolean, notify?: () => void): LegacyPlanState | null;
3217
+ broadcast: SessionBroadcaster;
3218
+ /** `turnId`: the planner turn whose commit this is, when one is. */
3219
+ broadcastPlan(turnId?: string): void;
3220
+ /**
3221
+ * Validate a batch against live state; nothing of it is applied. A
3222
+ * supervisor's batch that trips the oscillation guard is refused, and the
3223
+ * user is told of the trip once, in the supervisor's voice (ADR-0029): a note
3224
+ * saved as it is written, like any of Ordewell's own, which a turn in flight
3225
+ * holds nothing in.
3226
+ */
3227
+ validateOps(ops: TaskOp[]): ApplyTaskOpsResult;
3228
+ /** The workspace's skill catalog under its repo group's layout, which the skills a plan or edit attaches are checked against. */
3229
+ taskSkills(): SkillLookup;
3230
+ /** Tell the user something the transcript does not carry. */
3231
+ notice(level: SessionNotice['level'], message: string): void;
3232
+ /** Load planner-produced tasks: an edit keeps run state, a commit starts over. Returns how many landed. */
3233
+ adoptTasks(tasks: readonly Task[], how: 'edit' | 'commit'): number;
3234
+ capturePrd(text: string): void;
3235
+ /** The planner turn was stopped or abandoned: whatever it is waiting on (a parked approval) will never be answered. */
3236
+ turnAborted(): void;
3237
+ /** A planner turn is about to take the conversation, which nothing holds now. */
3238
+ turnClaiming?(): void;
3239
+ /** A planner turn let the conversation go, so the next one may take it. */
3240
+ turnReleased?(): void;
3241
+ /** A turn ended on the planner's usage limit, which it reported as `said`: what is sent again, and when. */
3242
+ usageLimit(said: TurnFailure): PlannerLimit;
3243
+ }
3244
+ /**
3245
+ * A conversation edit (rewind, fork) the conversation refused because the
3246
+ * request itself is wrong — no such message, nothing to fork. Transport-agnostic
3247
+ * like `PlanEditError`: a route maps it to a status, core carries none.
3248
+ */
3249
+ declare class ConversationEditError extends Error {
3250
+ constructor(message: string);
3251
+ }
3252
+ /**
3253
+ * Refused because a planner turn is in flight. Distinct from a bad request:
3254
+ * the same call succeeds once the turn settles, and the turn would otherwise
3255
+ * append its reply to a transcript that no longer holds the message it
3256
+ * answers.
3257
+ */
3258
+ declare class ConversationBusyError extends ConversationEditError {
3259
+ constructor(operation: string);
3260
+ }
3261
+ /**
3262
+ * A planner turn that settled after the plan it was answering was dropped (a
3263
+ * new plan, another session adopted, the session closed). Nothing it produced
3264
+ * was committed: landing it would write one session's reply into another's
3265
+ * transcript.
3266
+ */
3267
+ declare class PlannerTurnDiscardedError extends Error {
3268
+ constructor(options?: ErrorOptions);
3269
+ }
3270
+ /**
3271
+ * A wake-up turn's backend ended without a reply — a dead agent, a usage
3272
+ * limit — and said why. A user's turn shows that in the chat instead; a
3273
+ * wake-up has nobody waiting on it, so its supervisor is told (ADR-0029).
3274
+ */
3275
+ declare class PlannerTurnFailedError extends Error {
3276
+ constructor(message: string);
3277
+ }
3278
+ /**
3279
+ * A planner turn ended on the planner's usage limit (#84). Nothing of it
3280
+ * lands: the transcript keeps the failure instead, out of the planner's own
3281
+ * dialogue, and the turn's inputs wait to be sent again — at `resendAt`, or
3282
+ * with the next input when it is null.
3283
+ */
3284
+ declare class PlannerUsageLimitError extends PlannerTurnFailedError {
3285
+ readonly resendAt: Date | null;
3286
+ /** Why nothing is sent again on its own, when nothing is. */
3287
+ readonly held?: string;
3288
+ constructor(limit: PlannerLimit);
3289
+ }
3290
+ /**
3291
+ * The user stopped the turn and the backend reported that as a thrown error
3292
+ * rather than a settled reply. Typed so a surface can stay quiet for it without
3293
+ * reading the message: some SDKs' abort errors are plain `Error`s by name, and
3294
+ * a real failure can mention "aborted" too.
3295
+ */
3296
+ declare class PlannerTurnStoppedError extends Error {
3297
+ constructor(options?: ErrorOptions);
3298
+ }
3299
+ /** A user message a rewind can fork the conversation from just before. */
3300
+ interface RewindTarget {
3301
+ /** Position in the transcript — what {@link PlannerConversation.cloneBefore} takes. */
3302
+ index: number;
3303
+ preview: string;
3304
+ /** The whole message — the preview cuts it to one line, and a surface confirming the rewind quotes it. */
3305
+ content: string;
3306
+ timestamp: string;
3307
+ }
3308
+ /** What a rewind forks from, and the message it lands just before. */
3309
+ interface RewoundDialogue {
3310
+ dialogue: ForkedDialogue;
3311
+ rewoundMessage: string;
3312
+ }
3313
+ /** What a compaction left behind. */
3314
+ interface ConversationCompaction {
3315
+ summary: string;
3316
+ /** Transcript entries kept verbatim after the summary entry. */
3317
+ keptMessages: number;
3318
+ }
3319
+ interface ReplyOptions {
3320
+ signal?: AbortSignal;
3321
+ /**
3322
+ * Run inside the mutation that lands the turn's outcome — the edit, the
3323
+ * plan or the reply — so what the caller drops with it (the inputs the turn
3324
+ * answered) leaves disk in the same save.
3325
+ */
3326
+ onCommit?: () => void;
3327
+ /** Wake-up digests delivered with this turn (ADR-0029), oldest first, ahead of the user's message if there is one. */
3328
+ wakeUps?: readonly string[];
3329
+ /** Set when the turn is a wake-up's alone. */
3330
+ wakeUp?: WakeUpTurn;
3331
+ /** The user's message was force sent: the turn replaces the one it stopped (ADR-0023). */
3332
+ forced?: boolean;
3333
+ }
3334
+ /**
3335
+ * The planner conversation (ADR-0002), end to end: the persisted dialogue
3336
+ * record, the live model context behind it, and every turn from the user's
3337
+ * message to a settled, persisted outcome.
3338
+ *
3339
+ * It is the only writer of `conversationHistory` and of the planner's
3340
+ * `researchLog`. The writes land on the host's plan object — which is what the
3341
+ * session persists and broadcasts — and only ever inside the host's mutation
3342
+ * ritual, so each reaches disk in the save made for it. A planner turn writes
3343
+ * nothing until the mutation that lands it ({@link land}): any other save made
3344
+ * while it runs finds nothing of it to persist.
3345
+ *
3346
+ * The live model context (a vendor service's message list, a harness planner's
3347
+ * process and native session id) is disposable: {@link reset} drops it and the
3348
+ * next turn is replayed from the transcript. That is what lets the transcript
3349
+ * be replaced by a summary, or copied into a fork, without the model's memory
3350
+ * drifting from it.
3351
+ */
3352
+ declare class PlannerConversation {
3353
+ private readonly host;
3354
+ private readonly briefing;
3355
+ private live;
3356
+ private openTurn;
3357
+ constructor(host: PlannerConversationHost);
3358
+ get transcript(): readonly ConversationMessage[];
3359
+ /** Whether a planner turn holds the conversation. */
3360
+ get isTurnInFlight(): boolean;
3361
+ /** Stop the planner turn in flight; it settles as stopped. False when none is. */
3362
+ stopTurn(by?: 'force_send'): boolean;
3363
+ /**
3364
+ * Drop the planner turn in flight together with the plan it was answering:
3365
+ * it is stopped, whatever it settles is discarded, and the conversation is
3366
+ * free at once rather than when the abandoned turn finishes unwinding.
3367
+ */
3368
+ abandonTurn(): void;
3369
+ /**
3370
+ * Free the conversation from a turn that did not end when stopped — a hung
3371
+ * wake-up (ADR-0029). Unlike {@link abandonTurn} the plan stays, so the turn
3372
+ * is forgotten as its failure would have been: nothing it held lands, and
3373
+ * what it would have told the planner is told to the next turn.
3374
+ */
3375
+ abandonHungTurn(): void;
3376
+ /**
3377
+ * Let the live turn go. A user turn's end is announced now, and it says
3378
+ * nothing after — not its stream, not its held edit, not its end again —
3379
+ * since it may settle at any time later: the API services restart nothing,
3380
+ * and a hung call settles whenever it settles, while the next turn runs.
3381
+ */
3382
+ private drop;
3383
+ /**
3384
+ * Run planner work that is not a conversation turn — one-shot plan
3385
+ * generation — as the planner turn, so it is stopped, refused and superseded
3386
+ * like one. It starts a fresh plan, so it supersedes the turn in flight.
3387
+ */
3388
+ hold<T>(signal: AbortSignal | undefined, body: (turn: PlannerTurn) => Promise<T>): Promise<T>;
3389
+ /**
3390
+ * Take the conversation for a new planner turn. A caller's own signal stops
3391
+ * the turn too; the relay is detached on release because a caller may reuse
3392
+ * that signal, and a stale listener would stop the next turn with it.
3393
+ */
3394
+ private claim;
3395
+ /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
3396
+ get currentTurnId(): string | undefined;
3397
+ /** The open turn's wake-up rules, when the supervisor is answering it rather than the user's message. */
3398
+ get currentWakeUp(): WakeUpTurn | undefined;
3399
+ /** A skill the planner loaded in the open turn, held with the turn; false when none is open to take it. */
3400
+ recordPlannerSkill(skill: SkillLoad): boolean;
3401
+ /**
3402
+ * Hand the open turn a plan (already validated) or a task edit. The turn
3403
+ * settles with it exactly as with the same plan or ops parsed out of its
3404
+ * reply, and it wins over any envelope that reply carries. A later
3405
+ * submission replaces an earlier one. False when no user turn is open.
3406
+ */
3407
+ submit(submission: PlannerSubmission): boolean;
3408
+ /**
3409
+ * Tell surfaces what the turn's edit would leave, from the same applier that
3410
+ * lands it, so an edit that would not apply shows nothing. It stays shown
3411
+ * from the moment it is handed in until it lands, is refused, or the turn
3412
+ * ends: the backend's reply claims it, but it has not landed by then.
3413
+ */
3414
+ private showHeld;
3415
+ private clearHeld;
3416
+ /**
3417
+ * The edit the open turn already holds, for a further edit in the same reply
3418
+ * to join rather than replace. Empty when it holds nothing; null when it holds
3419
+ * a whole plan, which an edit cannot be layered on.
3420
+ */
3421
+ pendingOps(): TaskOp[] | null;
3422
+ /**
3423
+ * A read the planner makes through a tool. It spends the same per-turn budget
3424
+ * as an envelope read, so a planner cannot read more by switching channels:
3425
+ * past the soft limit, or on a repeated question, the answer still comes but
3426
+ * is told to land the turn; at the hard limit it is refused. `signature`
3427
+ * names the question, for the repeat check.
3428
+ */
3429
+ read<T>(signature: string, answer: () => Promise<T>): Promise<ToolRead<T>>;
3430
+ /** Whether the model still holds this conversation in memory. */
3431
+ get isActive(): boolean;
3432
+ /** Call inside the host's mutation ritual. */
3433
+ append(role: ConversationMessage['role'], content: string, opts?: {
3434
+ timestamp?: string;
3435
+ kind?: ConversationMessage['kind'];
3436
+ runs?: ReplyRun[];
3437
+ actor?: ConversationMessage['actor'];
3438
+ }): void;
3439
+ private appendEntries;
3440
+ /** Swap the whole transcript. The live context no longer matches it, so pair with {@link reset}. */
3441
+ replace(messages: readonly ConversationMessage[]): void;
3442
+ /**
3443
+ * The plan scope this conversation served is gone: drop the live model
3444
+ * context, and what the planner was told outside the transcript in it (see
3445
+ * {@link PlannerBriefing}). Idempotent; the harness backend holds an OS
3446
+ * process here, so callers never gate it on {@link isActive}.
3447
+ */
3448
+ reset(): void;
3449
+ /** A run update for the planner's next turn, whoever opens it (ADR-0029). */
3450
+ tellRunUpdate(text: string): void;
3451
+ /** Drop the live model context alone: the next turn replays from the transcript. */
3452
+ private dropContext;
3453
+ /**
3454
+ * The user messages a rewind may land before. The opening message is the
3455
+ * goal: cutting it leaves a conversation about nothing, which is a new
3456
+ * session, not a rewind. After a compaction the summary entry, always
3457
+ * first, plays the goal's part: what it replaced is gone, so a rewind stops
3458
+ * at it.
3459
+ */
3460
+ rewindTargets(): RewindTarget[];
3461
+ /**
3462
+ * Replace the transcript with a summary of it, keeping the last two
3463
+ * exchanges as they were, and drop the live context so the next message
3464
+ * replays from the shorter record.
3465
+ *
3466
+ * The summary is one hidden turn through whichever planner is configured —
3467
+ * on the live context when it still matches, replayed from the transcript
3468
+ * when not — so the compaction is the same for a vendor API and a harness
3469
+ * agent. Whatever the turn emits besides the summary is discarded, ops
3470
+ * included: it condenses the conversation, never the plan. Nothing is
3471
+ * written until the summary is in hand, so a failed or stopped turn leaves
3472
+ * the transcript exactly as it was.
3473
+ */
3474
+ compact(signal?: AbortSignal): Promise<ConversationCompaction>;
3475
+ private summarize;
3476
+ /**
3477
+ * A copy of the dialogue record for a forked session to carry. Refused
3478
+ * mid-turn: the copy would hold the user's message without the reply to it.
3479
+ * The live context is not part of it — the fork replays from this record on
3480
+ * its first turn, like any adopted session.
3481
+ */
3482
+ clone(): ForkedDialogue;
3483
+ /**
3484
+ * A copy of the dialogue as it stood just before the user message at
3485
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
3486
+ * from — plus that message's full text, for a surface to offer back. This
3487
+ * conversation is not touched. The research trace is cut at the same point
3488
+ * by time: its entries carry no link to the message that caused them.
3489
+ */
3490
+ cloneBefore(index: number): RewoundDialogue;
3491
+ /**
3492
+ * What the skill check of a landed plan or edit warned about, kept in the
3493
+ * transcript so every surface shows it, on reload too, and not only the
3494
+ * planner that may have read it in a tool result. Call inside the host's
3495
+ * mutation ritual, so the broadcast plan carries it.
3496
+ */
3497
+ recordSkillWarnings(warnings: readonly string[], timestamp?: string): void;
3498
+ /**
3499
+ * A call at the command seam (ADR-0029), kept where it happened in the
3500
+ * dialogue: a reload shows it, and a planner replayed from the transcript
3501
+ * knows what was started or stopped, and by whom. Call inside the host's
3502
+ * mutation ritual.
3503
+ */
3504
+ recordExecution(record: ExecutionRecord, text: string): void;
3505
+ /**
3506
+ * Open the conversation on the host's fresh plan: the goal is its first
3507
+ * message. `prepare` (discovery) runs inside the turn, so a stop reaches it.
3508
+ */
3509
+ start(goal: SkillInvocation, prepare: () => Promise<ConversationOpening>, signal?: AbortSignal): Promise<LegacyPlanState>;
3510
+ /**
3511
+ * Every later user message, to a persisted outcome. A turn that throws lands
3512
+ * nothing: what it held is dropped with it, so session memory never drifts
3513
+ * from disk and the UI.
3514
+ */
3515
+ reply(message: string | SkillInvocation, options?: ReplyOptions): Promise<LegacyPlanState>;
3516
+ private replyTurn;
3517
+ /**
3518
+ * A wake-up turn lands only a real answer. Stopped — by the user, or by its
3519
+ * timeout — it lands nothing, not the fragment a backend hands back; a
3520
+ * failure is thrown rather than shown, for the supervisor to retry
3521
+ * (ADR-0029).
3522
+ */
3523
+ private assertWakeUpAnswered;
3524
+ /**
3525
+ * A turn that ended on the planner's usage limit lands nothing: what it held
3526
+ * is dropped, its failure is kept in its place, and the limit's wait is armed
3527
+ * in the same save. Only a turn Ordewell started gets here, so a limit is
3528
+ * never acted on for a planner nobody asked anything of.
3529
+ */
3530
+ private assertNotLimited;
3531
+ /**
3532
+ * A failure kept in the transcript, drawn as one on reload too, and never
3533
+ * replayed to the planner. Stamped after the newest entry: surfaces take
3534
+ * only entries newer than the last they synced.
3535
+ */
3536
+ private recordFailure;
3537
+ private assertIdle;
3538
+ /** Refuse to land a turn that no longer belongs here: it was abandoned, or its plan is no longer the host's. */
3539
+ private assertCurrent;
3540
+ /**
3541
+ * {@link assertCurrent} on the far side of an await that precedes a plan
3542
+ * mutation: the skill check waits on git, and a stop, rewind or session
3543
+ * switch in that window drops the edit instead of landing it on whatever
3544
+ * plan is current by then. A stop counts here because nothing has landed
3545
+ * yet — the user asked for the turn to end before it changed the plan.
3546
+ */
3547
+ private assertStillCurrent;
3548
+ /**
3549
+ * A live conversation is only safe to continue in-place when it also
3550
+ * matches the model/effort configured right now — a harness planner's
3551
+ * running process was spawned with the old one baked in and cannot pick up
3552
+ * a switch (ADR-0009). Otherwise the turn resumes from the transcript.
3553
+ */
3554
+ private canContinueLive;
3555
+ /**
3556
+ * Bracket one user turn with its start and end, under an id minted here:
3557
+ * the turn is where the stream a surface draws begins and ends, and only the
3558
+ * conversation sees all of it — every backend call, read and retry.
3559
+ */
3560
+ private userTurn;
3561
+ private requirePlan;
3562
+ /** The message as typed, then one entry per skill it loaded, sharing its timestamp so a reload keeps them together. */
3563
+ private userEntries;
3564
+ private userPrompt;
3565
+ /** Hold what the turn writes until it lands, each entry where the transcript stands now; see {@link TurnWrites}. */
3566
+ private holdWrites;
3567
+ /**
3568
+ * The one mutation that lands a turn's outcome (ADR-0029). What the turn held
3569
+ * goes in first — each entry where it belongs, ahead of what was written
3570
+ * after it, then the research and the subagents — so the transcript reads in
3571
+ * the order it happened and the inputs leave the plan's pending turns in the
3572
+ * save that carried their answer. `outcome` makes the turn's change to the
3573
+ * plan and returns what the turn said, which is written, then sent in that
3574
+ * order; `announce` follows it on every surface. The turn's briefing is told
3575
+ * unless the planner gave no answer (`unheard`): a failure, or a stop.
3576
+ */
3577
+ private land;
3578
+ private writeHeld;
3579
+ private recordResearch;
3580
+ /**
3581
+ * The opening a replay starts under, read fresh, the live context dropped
3582
+ * first; what it states of autorun and the runners is what the planner is
3583
+ * told by it. A turn abandoned while the opening was read stops here: the
3584
+ * backend may be its successor's by now.
3585
+ */
3586
+ private reopen;
3587
+ /**
3588
+ * Reopen the model context from the transcript — because the in-memory one
3589
+ * is gone (session reload, extension restart), or because it is stale
3590
+ * against the planner config now in effect. No LLM call happens for the
3591
+ * replayed turns; the first call is the one the user's message opens.
3592
+ */
3593
+ private resume;
3594
+ /**
3595
+ * Answer every read the planner emits until it says something else.
3596
+ *
3597
+ * The channel is a text envelope rather than a registered tool because the
3598
+ * protocol has to be identical on both planner backends (ADR-0009): Ordewell
3599
+ * owns a tool loop only in the API case, and a harness planner running as a
3600
+ * coding-agent subprocess can only be reached this way.
3601
+ *
3602
+ * Draining here — outside {@link repairLoop} — is what keeps reads free of
3603
+ * the repair budget. A planner that looks a task up and *then* fumbles its
3604
+ * ops JSON still gets its two corrective retries; charging it for the read
3605
+ * would cost it the chance to fix the edit.
3606
+ */
3607
+ private drainTaskQueries;
3608
+ /**
3609
+ * A call cut off by something other than the turn's own stop failed: what it
3610
+ * hands back is a fragment of an answer, and the turn keeps what it was sent,
3611
+ * as any failure does. Nothing of Ordewell's resets a backend under a turn on
3612
+ * the same plan (see `PlannerBackend`); one that changes the plan abandons the turn.
3613
+ */
3614
+ private failedIfCutOff;
3615
+ /**
3616
+ * The backend's answer, with what was handed in while it ran standing in
3617
+ * for whatever its reply carried. Taken once: a corrective re-send after a
3618
+ * rejected edit must not settle on the same submission again. A stopped
3619
+ * turn commits nothing, the same as a stopped reply with a plan in it.
3620
+ */
3621
+ private claimSubmission;
3622
+ /**
3623
+ * Render one read out of live state. Never persisted to the transcript: the
3624
+ * detail is context for the planner's next reply, and re-sending it on every
3625
+ * later turn is exactly the cost this channel exists to avoid.
3626
+ */
3627
+ private taskQueryAnswer;
3628
+ /**
3629
+ * Drive a planner turn, its reads already drained, to a persisted, broadcast
3630
+ * outcome. Task edits apply atomically; validation failures are fed back to
3631
+ * the model for up to 2 silent retries, then surfaced as a message with the
3632
+ * plan untouched. The first turn and every later turn route through here —
3633
+ * one path, not two.
3634
+ */
3635
+ private settle;
3636
+ /** A name not found at submit only warns: a task the attaching one depends on may create it. */
3637
+ private warnSkills;
3638
+ /** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
3639
+ private applyTaskOps;
3640
+ /** The edit was refused as it landed: what it showed is not coming. */
3641
+ private refusedHeld;
3642
+ /** Commit a settled (non-task_ops) turn: a whole plan, or what it said. */
3643
+ private commit;
3644
+ /**
3645
+ * The runs a reply is saved and sent with (ADR-0017 A1): the narration the
3646
+ * turn kept from calls it did not settle on, then the reply's own. None from
3647
+ * a backend that reports no runs, whose reply then stands as one message.
3648
+ */
3649
+ private shown;
3650
+ /** What a plan or edit turn said beside what it landed: its reply, or an envelope's narration. */
3651
+ private said;
3652
+ /**
3653
+ * The catalog the planner may actually draw from — model ids and task-mode
3654
+ * ids, per runner in the plan — emitted on EVERY turn, before any plan exists
3655
+ * or after. The system prompt shows this once at conversation start; a long
3656
+ * clarifying conversation outlives that single showing and the planner
3657
+ * starts misquoting it, so this re-states it per turn instead.
3658
+ *
3659
+ * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
3660
+ * hard bound on every turn, and reads the plan's runners live, so a runner
3661
+ * admitted mid-session by a retarget is shown like every other. A planner
3662
+ * with Ordewell's tools gets a reminder to read the catalog instead.
3663
+ */
3664
+ private catalogBlock;
3665
+ /**
3666
+ * The "you are here" block for post-plan chat: current tasks with stable
3667
+ * references, plus the read and edit protocols. Injected per turn (never
3668
+ * persisted) so the model always sees live statuses — including which tasks
3669
+ * are locked by a running execution.
3670
+ */
3671
+ private planContextBlock;
3672
+ /** One line per task, subtasks indented under their parent, with stable references — null until the plan has tasks. */
3673
+ private currentPlanLines;
3674
+ }
3675
+
3676
+ /**
3677
+ * Task references: the one grammar for naming a task in a plan, both ways.
3678
+ * A label is rendered from a task's order under its parent ("2", "2.1"), and
3679
+ * a reference written by anyone — the planner, a tool call, a command — is
3680
+ * resolved back against the plan tree. Pure so a
3681
+ * browser-bundled surface can use it through `@ordewell/core/plan-utils`.
3682
+ */
3683
+ /** The shape a reference resolves over: the plan's task tree, or a surface's projection of it. */
3684
+ type RefTask<T> = TreeTask<T>;
3685
+ /**
3686
+ * The parent a label extends. A label is the full path ("3.2.1"), so a parent
3687
+ * below the top level carries the label it was itself reached by; without one
3688
+ * the parent's order stands for it, which is right for a top-level parent.
3689
+ */
3690
+ interface RefParent {
3691
+ order: number;
3692
+ label?: string;
3693
+ }
3694
+ /** Dotted path label for a task, e.g. "2.1" for the first subtask of task 2, "2.1.3" one level down. */
3695
+ declare function taskOrderLabel(task: {
3696
+ order: number;
3697
+ }, parent?: RefParent): string;
3698
+ /** How a surface refers to a task in prose: "#2", or "#2.1" for a subtask. */
3699
+ declare function taskRef(task: {
3700
+ order: number;
3701
+ }, parent?: RefParent): string;
3702
+ /** A task named in full, as a confirmation or a picker title does: "#2 Deploy". */
3703
+ declare function titledTaskRef(task: {
3704
+ order: number;
3705
+ title: string;
3706
+ }, parent?: RefParent): string;
3707
+ /**
3708
+ * The reference {@link taskRef} renders for the task a plan tree holds, by id
3709
+ * — its full path, dotted, for a subtask. Undefined when the tree holds no such task.
3710
+ */
3711
+ declare function taskRefOf<T extends RefTask<T>>(tasks: readonly T[], id: string): string | undefined;
3712
+ /**
3713
+ * The reference and title {@link titledTaskRef} renders for the task a plan
3714
+ * tree holds, by id — its full path, dotted, for a subtask. Undefined when the
3715
+ * tree holds no such task.
3716
+ */
3717
+ declare function titledTaskRefOf<T extends RefTask<T>>(tasks: readonly T[], id: string): string | undefined;
3718
+ /**
3719
+ * Resolve a written reference to a task at any depth: an id, then an order
3720
+ * label with or without "#" ("#2", "2", "#2.1"), then an exact title.
3721
+ *
3722
+ * An order label is read against the tree, never a flattened list: subtask
3723
+ * orders restart at 1 under each parent, so "#2" always means top-level task 2
3724
+ * and a subtask is reachable only by its dotted path ("#2.1", "#2.1.3").
3725
+ */
3726
+ declare function resolveTaskRef<T extends RefTask<T>>(tasks: readonly T[], ref: string | number): T | undefined;
3727
+ /**
3728
+ * A reference as the user types one: {@link resolveTaskRef}, then a unique
3729
+ * prefix of a task's id at any depth. Only the user's: a model's reference
3730
+ * that names no task is refused, never matched to the one task a fragment of
3731
+ * it happens to fit.
3732
+ */
3733
+ declare function resolveUserTaskRef<T extends RefTask<T>>(tasks: readonly T[], ref: string | number): T | undefined;
3734
+
3735
+ interface SessionMeta {
3736
+ id: string;
3737
+ goal: string;
3738
+ runners: RunnerId[];
3739
+ taskCount: number;
3740
+ status: PlanStatus;
3741
+ createdAt: string;
3742
+ updatedAt: string;
3743
+ }
3744
+ interface SessionData {
3745
+ meta: SessionMeta;
3746
+ /** The full plan state — including conversationHistory, prdMarkdown, and queuedMessages. */
3747
+ plan: LegacyPlanState;
3748
+ }
3749
+
3750
+ /**
3751
+ * The bridge between "core needs an answer" and "a human somewhere is looking
3752
+ * at a UI". Core cannot prompt: the human may be at a browser, a TUI, a CLI
3753
+ * stream, or a VS Code webview, and on the web server they are on the far end
3754
+ * of a socket. So the Session parks a promise here, announces the request
3755
+ * through the normal broadcast seam, and every surface answers through the
3756
+ * same `resolve(id, granted)` call.
3757
+ *
3758
+ * Timeouts are load-bearing rather than defensive: a planner turn that blocks
3759
+ * forever on an unanswered prompt would hang the whole research loop with no
3760
+ * visible cause. On expiry the request resolves to denied and the model gets a
3761
+ * normal, actionable tool result. A task runner's request is the exception
3762
+ * (ADR-0018, A1): the runner is parked on it rather than a research loop, and
3763
+ * it waits for a person — or the supervisor — however long that takes.
3764
+ */
3765
+ interface PendingApproval {
3766
+ id: string;
3767
+ request: ApprovalRequest;
3768
+ createdAt: string;
3769
+ }
3770
+ interface PendingApprovalsOptions {
3771
+ /** Denies and resolves after this long with no answer. Default 5 minutes. */
3772
+ timeoutMs?: number;
3773
+ /** Announce a new request to the surfaces. */
3774
+ onRequest?: (pending: PendingApproval) => void;
3775
+ /** Announce that a request is no longer actionable (answered or expired). */
3776
+ onSettled?: (id: string, granted: boolean, settled: {
3777
+ request: ApprovalRequest;
3778
+ decision: ApprovalDecision;
3779
+ }) => void;
3780
+ }
3781
+ interface AskOptions {
3782
+ /**
3783
+ * An id the caller already announced the request under. One still in use is
3784
+ * refused — denied at once — rather than overwriting the request that has it.
3785
+ */
3786
+ id?: string;
3787
+ /** Wait for an answer however long it takes. */
3788
+ noTimeout?: boolean;
3789
+ /**
3790
+ * Given the answer as the request settles, before anything awaiting the
3791
+ * promise runs: a caller about to tear down what asked (a runner being
3792
+ * stopped) must deliver the answer before it goes.
3793
+ */
3794
+ onDecision?: (decision: ApprovalDecision) => void;
3795
+ }
3796
+ declare class PendingApprovals {
3797
+ private readonly opts;
3798
+ private readonly entries;
3799
+ private readonly listeners;
3800
+ constructor(opts?: PendingApprovalsOptions);
3801
+ /** Hear each new request once it is announced — the supervisor wakes on a runner's (ADR-0029). */
3802
+ listen(listener: (pending: PendingApproval) => void): () => void;
3803
+ /** Park a request and return the promise the approval policy awaits. */
3804
+ ask(request: ApprovalRequest, options?: AskOptions): Promise<boolean>;
3805
+ /** Park a request whose answer is more than yes or no. */
3806
+ decide(request: ApprovalRequest, options?: AskOptions): Promise<ApprovalDecision>;
3807
+ /**
3808
+ * Answer one request. Returns false when the id is unknown or already
3809
+ * settled. "Allow for this task" on a request that did not offer it is a
3810
+ * plain allow: no grant is made that the requester never proposed.
3811
+ */
3812
+ resolve(id: string, answer: ApprovalAnswer): boolean;
3813
+ /** Everything still awaiting an answer — replayed to a surface that connects late. */
3814
+ outstanding(): PendingApproval[];
3815
+ /**
3816
+ * Deny everything in flight, or only the requests `which` picks. Called on
3817
+ * abort and on session reset.
3818
+ */
3819
+ clear(which?: (request: ApprovalRequest) => boolean, note?: string): void;
3820
+ }
3821
+
3822
+ /**
3823
+ * Pure, no-imports helper so a browser surface can format a conflict's files
3824
+ * without pulling `isolationRecord` — and its `path` import — into a webview
3825
+ * bundle. Kept out of `isolationRecord.ts` for exactly that reason.
3826
+ */
3827
+ declare function capConflictFiles(files: string[], max?: number): string;
3828
+
3829
+ /** How a run is started, watched and merged: the user's, not a plan's (ADR-0030). */
3830
+ interface GlobalRunSettings {
3831
+ supervisorLevel: SupervisorLevel;
3832
+ mergeAtGatesAutomatically: boolean;
3833
+ /** Start a plan when the planner submits it, approved as if the user had pressed Execute. */
3834
+ autorun: boolean;
3835
+ /** Retries an Act supervisor may make of one task in a run (a loop guard). */
3836
+ supervisorRetryLimit: number;
3837
+ /** Reads a wake-up turn may make, in place of a user message's caps. */
3838
+ wakeUpReadBudget: number;
3839
+ }
3840
+ declare const RUN_SETTINGS_DEFAULTS: Readonly<GlobalRunSettings>;
3841
+ declare class RunSettingsError extends Error {
3842
+ constructor(message: string);
3843
+ }
3844
+ interface UserSettings {
3845
+ modelAllowlist?: Record<string, string[]>;
3846
+ /** Last model (and its thinking effort) the user chose for each planner backend, keyed by planner backend id. */
3847
+ plannerModels?: Record<string, {
3848
+ model: string;
3849
+ effort?: string;
3850
+ }>;
3851
+ /**
3852
+ * Runners the user picked for planning. Absent — not empty — is what falls
3853
+ * back to the environment's defaults; `[]` is a deliberate "none of them".
3854
+ */
3855
+ enabledRunners?: string[];
3856
+ /** Full (true) or Guarded (false), ADR-0001; Full until the user chooses. */
3857
+ autonomousMode?: boolean;
3858
+ supervisorLevel?: SupervisorLevel;
3859
+ mergeAtGatesAutomatically?: boolean;
3860
+ autorun?: boolean;
3861
+ supervisorRetryLimit?: number;
3862
+ wakeUpReadBudget?: number;
3863
+ }
3864
+ /**
3865
+ * The model allowlist, remembered planner models and enabled runners are shared
3866
+ * across processes by default. `ORDEWELL_SETTINGS_PATH` lets independent runs
3867
+ * use separate files: otherwise a write in one run would change another's
3868
+ * settings when `getAll()` detects the new mtime.
3869
+ */
3870
+ declare function getSettingsPath(): string;
3871
+ /** The run settings `settings` holds, each one it leaves out at its default. */
3872
+ declare function runSettingsOf(settings: UserSettings): GlobalRunSettings;
3873
+ declare class SettingsService {
3874
+ private filePath;
3875
+ private cache;
3876
+ private cachedStamp;
3877
+ constructor(filePath?: string);
3878
+ getAll(): UserSettings;
3879
+ /**
3880
+ * Every write replaces the file, so its inode changes with it; the mtime
3881
+ * alone cannot tell two writes apart on a filesystem whose timestamps are
3882
+ * coarse, and a write that shares the last one's tick stayed unseen.
3883
+ */
3884
+ private fileStamp;
3885
+ /**
3886
+ * What a change is applied to: the file as it is now, never a copy held since
3887
+ * an earlier read. Another process may have saved in between, and writing
3888
+ * the whole file back would silently undo it.
3889
+ */
3890
+ private current;
3891
+ /**
3892
+ * Calls `listener` when another process (or this one) saves the file. The
3893
+ * directory is watched, not the file: a write that swaps the file in leaves
3894
+ * a watch on the old one pointing at nothing.
3895
+ */
3896
+ watch(listener: () => void): {
3897
+ dispose(): void;
3898
+ };
3899
+ getModelAllowlist(runner: string): string[] | undefined;
3900
+ setModelAllowlist(runner: string, ids: string[] | undefined): void;
3901
+ getPlannerModel(provider: string): {
3902
+ model: string;
3903
+ effort?: string;
3904
+ } | undefined;
3905
+ setPlannerModel(provider: string, entry: {
3906
+ model: string;
3907
+ effort?: string;
3908
+ } | undefined): void;
3909
+ getEnabledRunners(): string[] | undefined;
3910
+ setEnabledRunners(ids: string[]): void;
3911
+ /**
3912
+ * Change the list from what the file holds at this moment: the change is
3913
+ * handed the saved list (undefined when the user never chose) and its result
3914
+ * is saved in the same call. A list worked out from an earlier read would
3915
+ * save over what another surface set since. It may be handed the list again,
3916
+ * when another process saved before this one could (see {@link save}).
3917
+ */
3918
+ updateEnabledRunners(change: (saved: string[] | undefined) => string[]): string[];
3919
+ /** Save `ids` as the user's choice unless one is saved already, judged on the file as it is now. */
3920
+ setEnabledRunnersIfUnset(ids: string[]): boolean;
3921
+ /**
3922
+ * Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
3923
+ * environment wins, so a script or CI run can fix its own without touching
3924
+ * the user's file.
3925
+ */
3926
+ autonomousMode(): boolean;
3927
+ /** Whether `ORDEWELL_AUTONOMOUS_MODE` decides the level, whatever this file says. */
3928
+ autonomousModeOverridden(): boolean;
3929
+ setAutonomousMode(enabled: boolean): void;
3930
+ getRunSettings(): GlobalRunSettings;
3931
+ /** Checks every value before writing any, so a refused change saves nothing. */
3932
+ updateRunSettings(changes: Partial<GlobalRunSettings>): GlobalRunSettings;
3933
+ private load;
3934
+ /**
3935
+ * Makes `change` to the file as it is now, and saves it. Another process can
3936
+ * save between that read and this write; the file is looked at again just
3937
+ * before ours replaces it, and when it moved the change is made afresh from
3938
+ * what that process saved, so neither save undoes the other. A file that
3939
+ * keeps moving is written over after {@link SAVE_TRIES} tries rather than
3940
+ * never. The look and the swap are one step under the file's lock
3941
+ * ({@link withFileLock}). A change that changes nothing writes nothing.
3942
+ */
3943
+ private save;
3944
+ /** False, saving nothing, when the file is no longer the one stamped `expected`. */
3945
+ private persist;
3946
+ }
3947
+
3948
+ /** What the preflight reads: the planner choice and whatever it needs to be reachable. */
3949
+ type PlannerReadinessConfig = Pick<IConfig, 'aiProvider' | 'planningModel' | 'openaiCompatibleBaseUrl' | 'getProviderApiKey'>;
3950
+ /**
3951
+ * Why the selected planner cannot plan yet, as data. Each surface words it with
3952
+ * its own command spelling (`describePlannerNotReady`), so a daemon, a chat and
3953
+ * a shell script can all say the same thing without sharing a syntax.
3954
+ */
3955
+ type PlannerNotReady = {
3956
+ code: 'no_key';
3957
+ provider: AiProvider;
3958
+ label: string;
3959
+ keyEnvVar: string;
3960
+ } | {
3961
+ code: 'no_base_url';
3962
+ provider: AiProvider;
3963
+ label: string;
3964
+ baseUrlEnvVar: string;
3965
+ } | {
3966
+ code: 'no_model';
3967
+ provider: AiProvider;
3968
+ label: string;
3969
+ };
3970
+ /**
3971
+ * What the selected planner still needs before it can plan, or null when it is
3972
+ * ready. There are two ways in (ADR-0009), and only one of them involves a
3973
+ * credential: a harness planner drives a coding agent already installed on the
3974
+ * machine, authenticated by the user's own subscription, so demanding an API
3975
+ * key of it turns a working setup into a wall. Its model is optional too: an
3976
+ * unset id means "the agent's own default".
3977
+ */
3978
+ declare function plannerNotReady(config: PlannerReadinessConfig): PlannerNotReady | null;
3979
+ /**
3980
+ * `plannerNotReady` judged on a host's own config — the one way every host
3981
+ * builds the preflight's input, so none can hand it a key the selected
3982
+ * planner's transport will not read (a stored OpenRouter key passing for an
3983
+ * OpenAI planner, which then failed inside the transport). Every planner turn
3984
+ * judges through this too, so none can let through what another refuses.
3985
+ */
3986
+ declare function plannerNotReadyFor(config: Pick<IConfig, 'aiProvider' | 'orchestratorModel' | 'planningModel' | 'openaiCompatibleBaseUrl' | 'getProviderApiKey'>): PlannerNotReady | null;
3987
+ /** How one surface spells the commands a not-ready message points at. */
3988
+ interface PlannerCommandSyntax {
3989
+ pickPlanner: string;
3990
+ setKey(provider: AiProvider): string;
3991
+ setModel: string;
3992
+ }
3993
+ declare const TERMINAL_UI_COMMANDS: PlannerCommandSyntax;
3994
+ /** The VS Code chat shares the terminal's slash commands, except that its `/model` needs `set`. */
3995
+ declare const CHAT_COMMANDS: PlannerCommandSyntax;
3996
+ declare const HEADLESS_CLI_COMMANDS: PlannerCommandSyntax;
3997
+ declare function describePlannerNotReady(reason: PlannerNotReady, commands: PlannerCommandSyntax): string;
3998
+ /**
3999
+ * A planner turn refused before it began. Carries the reason as data so a
4000
+ * client that spells commands differently (the headless CLI) can re-word it;
4001
+ * the message is the terminal UI's spelling, which is what most clients show.
4002
+ */
4003
+ declare class PlannerNotReadyError extends Error {
4004
+ readonly reason: PlannerNotReady;
4005
+ constructor(reason: PlannerNotReady);
4006
+ }
4007
+
4008
+ /** What the user last picked for one planner backend. */
4009
+ interface PlannerModelChoice {
4010
+ model: string;
4011
+ effort?: string;
4012
+ }
4013
+ /**
4014
+ * A candidate model as this planner's catalog lists it. Deliberately plain
4015
+ * data: a harness planner's catalog is its runner's own model list and a
4016
+ * vendor's is a picker list (ADR-0009), and the rule below is the same for
4017
+ * both — so the caller normalizes, and this module never reaches for a
4018
+ * ModelResolver, a config or the network.
4019
+ */
4020
+ interface PlannerModelCandidate {
4021
+ id: string;
4022
+ variants?: {
4023
+ id: string;
4024
+ }[];
4025
+ }
4026
+ interface PlannerModelRecall {
4027
+ model: string;
4028
+ effort: string;
4029
+ source: 'remembered' | 'catalog-default' | 'none';
4030
+ }
4031
+ /** The slice of `SettingsService` this needs, so tests can pass a plain object. */
4032
+ interface PlannerModelStore {
4033
+ getPlannerModel(provider: string): PlannerModelChoice | undefined;
4034
+ setPlannerModel(provider: string, entry: PlannerModelChoice | undefined): void;
4035
+ }
4036
+ /**
4037
+ * Remembers, per planner backend, the model the user last chose for it.
4038
+ *
4039
+ * Switching the planner has to clear the model — an id is scoped to the agent
4040
+ * that listed it, so an OpenRouter slug handed to Claude Code points it at
4041
+ * something it cannot spawn. That clear is correct and stays; what was missing
4042
+ * is that switching *back* then forced a re-pick of a model the user had
4043
+ * already chosen. The rule itself is `selectPlanner`'s; this is the storage
4044
+ * the surfaces that keep their memory in the settings file hand it.
4045
+ */
4046
+ declare class PlannerModelMemory {
4047
+ private store;
4048
+ constructor(store: PlannerModelStore);
4049
+ /** Record what the user picked for `provider`; a blank model forgets it instead. */
4050
+ remember(provider: string, model: string, effort?: string): void;
4051
+ getPlannerModel(provider: string): PlannerModelChoice | undefined;
4052
+ /**
4053
+ * What the model and effort should become when `provider` takes over the
4054
+ * planner, given the models it actually serves. The `source` is what the
4055
+ * three surfaces word their notice from — restored, defaulted, or nothing.
4056
+ */
4057
+ recall(provider: string, catalog: PlannerModelCandidate[]): PlannerModelRecall;
4058
+ }
4059
+
4060
+ /**
4061
+ * The daemon contract: every HTTP body the local daemon sends, in one place.
4062
+ *
4063
+ * The daemon's routes build their bodies against these types and the CLI's
4064
+ * `ApiClient` reads them back, so a field added on one side is a compile error
4065
+ * on the other. Before this there were three hand-written copies — the routes'
4066
+ * inline `c.json`, the client's per-method generics, and the TUI's slice of the
4067
+ * client — and they had drifted (the client declared a `headless` field the
4068
+ * daemon never sent, and typed `GET /sessions/:id`'s plan as the wrong shape).
4069
+ *
4070
+ * Types only. Core is the one package both the daemon and its clients already
4071
+ * depend on; importing from the daemon would pull its runtime into the CLI.
4072
+ */
4073
+ /**
4074
+ * Why a request was refused, as a stable code a client switches on instead of
4075
+ * reading the message. Absent on a refusal nobody needs to tell apart — a
4076
+ * missing field, say — where the message is all there is.
4077
+ */
4078
+ type DaemonErrorCode = 'session_not_found' | 'task_not_found' | 'no_plan' | 'already_executing' | 'workspace_not_found' | 'workspace_not_a_project' | 'conversation_busy'
4079
+ /** The user stopped the planner turn: the turn ended as asked, so a client stays quiet. */
4080
+ | 'planner_turn_stopped'
4081
+ /** The planner turn settled after its plan was dropped; nothing it produced was kept. */
4082
+ | 'planner_turn_discarded'
4083
+ /** The selected planner cannot plan yet (no key, no base URL, no model); `plannerNotReady` says which. */
4084
+ | 'planner_not_ready'
4085
+ /** The request asked for something the plan, task or conversation does not allow. */
4086
+ | 'refused' | 'checkpoint_not_waiting' | 'approval_not_outstanding';
4087
+ /** The body of every non-2xx answer a route gives. */
4088
+ interface ErrorBody {
4089
+ error: string;
4090
+ code?: DaemonErrorCode;
4091
+ /** Set with `workspace_not_a_project`: the directory the daemon refused, so a client can offer to initialize it. */
4092
+ workspace?: string;
4093
+ /** Set with `planner_not_ready`: the reason as data, so a client can word it in its own commands. */
4094
+ plannerNotReady?: PlannerNotReady;
4095
+ }
4096
+ /**
4097
+ * What the command seam said of a control the user pressed (ADR-0029), for the
4098
+ * surface to show as it is, so each words a control alike. Absent from a daemon
4099
+ * older than the field, and from a call that never reached the seam.
4100
+ */
4101
+ interface Worded {
4102
+ message?: string;
4103
+ }
4104
+ interface OkResponse extends Worded {
4105
+ ok: boolean;
4106
+ }
4107
+ /** A task start's answer: `queued` says why it did not start now, when the task waits for its dependencies instead. */
4108
+ interface TaskStartResponse extends OkResponse {
4109
+ queued?: string;
4110
+ }
4111
+ /**
4112
+ * A plan as the planner routes return it: the session's plan state, not the
4113
+ * `SerializedPlan` a websocket `plan_generated` carries. The two differ —
4114
+ * this one holds raw tasks and the dialogue's bookkeeping — and a client that
4115
+ * reads either reads only the fields it needs. Skill bodies stay in the
4116
+ * session: loads and attempt snapshots arrive as notices.
4117
+ */
4118
+ interface PlanResponse {
4119
+ plan: SurfacePlan;
4120
+ }
4121
+ /** `POST /plans/:id/review/approve`: the plan the user signed off, as it now runs. */
4122
+ interface ApproveReviewResponse extends PlanResponse, Worded {
4123
+ }
4124
+ /** `POST /plans/:id/resolve-conflict`: `null` when the session had no conflict to hand a resolver task. */
4125
+ interface ResolveConflictResponse {
4126
+ plan: SurfacePlan | null;
4127
+ }
4128
+ /** What `GET /api/models` returns, and what the pool's provider discovery produces. */
4129
+ interface ModelsResponse {
4130
+ models: DiscoveredModel[];
4131
+ modelsByRunner: Record<string, DiscoveredModel[]>;
4132
+ /** Each runner's manifest modes, so a surface can offer a per-task mode picker. */
4133
+ modesByRunner: Record<string, RunnerModeInfo[]>;
4134
+ orchestratorModel: string;
4135
+ providers: AiProvider[];
4136
+ /** Full cross-provider catalog for the orchestrator (planner) model picker. */
4137
+ orchestratorModels: OrchestratorOption[];
4138
+ /** Per-provider catalog-fetch failures, keyed by provider id. */
4139
+ providerErrors: Record<string, string>;
4140
+ }
4141
+ /** One-shot planning answers in the phase-tagged shape a saved session is read back in. */
4142
+ interface GeneratePlanResponse extends Pick<ModelsResponse, 'models' | 'modelsByRunner'> {
4143
+ plan: SurfacePlanState;
4144
+ /** The host notes the plan's transcript records, a skill check's warnings among them, which the phase-tagged plan does not carry. */
4145
+ notes?: string[];
4146
+ }
4147
+ interface ExecuteResponse extends Worded {
4148
+ status: 'running';
4149
+ }
4150
+ interface StopResponse extends Worded {
4151
+ status: 'stopped';
4152
+ }
4153
+ interface CancelPlanningResponse {
4154
+ cancelled: boolean;
4155
+ }
4156
+ /** `POST …/resume/cancel` (cancelled is true: a task with no resume scheduled is refused) and `POST …/converse/resend/cancel` (false when nothing waited). */
4157
+ interface CancelResumeResponse extends Worded {
4158
+ cancelled: boolean;
4159
+ }
4160
+ interface TaskMessageResponse {
4161
+ id: string;
4162
+ }
4163
+ interface ForceSendResponse extends Worded {
4164
+ sent: boolean;
4165
+ }
4166
+ interface RemoveMessageResponse extends Worded {
4167
+ removed: boolean;
4168
+ }
4169
+ interface MergeGateEntry {
4170
+ id: string;
4171
+ order: number;
4172
+ title: string;
4173
+ }
4174
+ interface MergeGateResponse {
4175
+ mergeGate: MergeGateEntry[];
4176
+ }
4177
+ interface IsolationDiffResponse {
4178
+ diff: string;
4179
+ }
4180
+ /** A merge that conflicts or is blocked is an outcome, not a refusal, so it answers 200 with the result. */
4181
+ type IsolationMergeResponse = IsolationMergeResult;
4182
+ /** A fork the daemon has already adopted. */
4183
+ interface ConversationForkResponse {
4184
+ sessionId: string;
4185
+ goal: string;
4186
+ plan: SurfacePlan;
4187
+ }
4188
+ /** A fork made by a rewind, with the full text of the message it was made just before. */
4189
+ interface ConversationRewindResponse extends ConversationForkResponse {
4190
+ rewoundMessage: string;
4191
+ }
4192
+ interface RewindTargetsResponse {
4193
+ targets: RewindTarget[];
4194
+ }
4195
+ interface ConversationCompactResponse extends ConversationCompaction, PlanResponse {
4196
+ }
4197
+ interface PrdResponse {
4198
+ prdMarkdown: string;
4199
+ }
4200
+ interface RunnerEntry {
4201
+ id: string;
4202
+ name: string;
4203
+ enabled: boolean;
4204
+ }
4205
+ interface RunnersResponse {
4206
+ runners: RunnerEntry[];
4207
+ orchestratorModel: string;
4208
+ }
4209
+ /** Sessions: a saved one comes off its file, a live one from the pool's store. */
4210
+ interface SessionResponse {
4211
+ meta: SessionMeta;
4212
+ plan: SurfacePlanState;
4213
+ /**
4214
+ * The live session's run as `Session.runSnapshot()` holds it, isolation
4215
+ * marks and handoff included. Absent for a session nobody holds: the saved
4216
+ * file has no run to speak of, and a surface does not rebuild one from it.
4217
+ */
4218
+ run?: RunView;
4219
+ }
4220
+ interface AdoptSessionResponse extends OkResponse {
4221
+ plan: SurfacePlan;
4222
+ goal: string;
4223
+ /** The adopted session's run, as {@link SessionResponse.run}. */
4224
+ run?: RunView;
4225
+ }
4226
+ interface TaskLogAttemptsResponse {
4227
+ /** Captured with the synchronous listing, including an empty listing. */
4228
+ position?: StreamStamp;
4229
+ attempts: number[];
4230
+ }
4231
+ interface TaskLogResponse {
4232
+ /** Captured with the synchronous log read; live batches after it are not in events. */
4233
+ position?: StreamStamp;
4234
+ attempt: number;
4235
+ events: TaskLogEvent[];
4236
+ }
4237
+ type SessionListResponse = SessionMeta[];
4238
+ interface SettingsResponse {
4239
+ orchestratorModel: string;
4240
+ /** Who plans (ADR-0009) — a vendor id, or a runner serving as a harness planner. */
4241
+ aiProvider: PlannerBackendId;
4242
+ plannerThinkingEffort: string;
4243
+ maxParallel: number;
4244
+ modelAllowlist: UserSettings['modelAllowlist'];
4245
+ plannerModels: UserSettings['plannerModels'];
4246
+ /** The user's run settings (ADR-0030), each at its default until set. */
4247
+ run: GlobalRunSettings;
4248
+ }
4249
+ /** What `PATCH /api/settings` answers: the settings as they now stand, plus what the write itself decided. */
4250
+ interface SettingsUpdateResponse extends SettingsResponse {
4251
+ /**
4252
+ * Which model a planner switch landed on and why — remembered for that
4253
+ * backend, its catalog default, or none. Present only when the write changed
4254
+ * the planner; absent for every other write, where nothing was recalled.
4255
+ */
4256
+ switchRecall?: PlannerModelRecall;
4257
+ /** Env keys the daemon refused to apply, so a client does not persist what the daemon would not. */
4258
+ rejectedEnvKeys?: string[];
4259
+ }
4260
+ interface CommandDescriptor {
4261
+ name: string;
4262
+ description: string;
4263
+ }
4264
+ interface CommandsResponse {
4265
+ commands: CommandDescriptor[];
4266
+ }
4267
+ interface WorkspacesResponse {
4268
+ workspaces: string[];
4269
+ }
4270
+ interface ApprovalsResponse {
4271
+ pending: PendingApproval[];
4272
+ approvedScopes: string[];
4273
+ }
4274
+ interface ApprovalAnswerResponse extends OkResponse {
4275
+ granted: boolean;
4276
+ decision: ApprovalDecision['decision'];
4277
+ }
4278
+ /**
4279
+ * Where a frame stands in its session's broadcast stream. A client holding
4280
+ * several sockets on one session gets every broadcast on each, and a frame's
4281
+ * content is no identity: two clears of a held edit are the same JSON. `seq`
4282
+ * rises by one per broadcast of the session for as long as the daemon runs,
4283
+ * whichever Session object holds the id; `stream` names the daemon process,
4284
+ * so a restarted daemon's numbering is never compared with the last one's.
4285
+ */
4286
+ interface StreamStamp {
4287
+ stream: string;
4288
+ seq: number;
4289
+ }
4290
+ /** A broadcast as the session's socket carries it. Unstamped from a daemon older than the stamp. */
4291
+ type SessionFrame = (SessionMessage | SessionNotice) & Partial<StreamStamp>;
4292
+ type ApprovalRequestMessage = Extract<SessionMessage, {
4293
+ type: 'approval_request';
4294
+ }>;
4295
+ /**
4296
+ * What a session's socket sends right after `connected`: its run as it stands,
4297
+ * so a client that joins mid-run has the run view before the next update —
4298
+ * the held edit's preview included — and the planner's approval requests
4299
+ * still waiting, which no later frame would announce again. `seq` is the last
4300
+ * broadcast it accounts for. Every frame after it is a {@link SessionFrame}.
4301
+ */
4302
+ interface RunSnapshotFrame extends Partial<StreamStamp> {
4303
+ type: 'run_snapshot';
4304
+ sessionId: string;
4305
+ view: RunView;
4306
+ /** Absent from a daemon older than the field: nothing is known, not "none waiting". */
4307
+ approvals?: ApprovalRequestMessage[];
4308
+ }
4309
+
4310
+ declare const EMPTY_RUN: RunView;
4311
+ declare function reduceRun(view: RunView, input: RunInput): RunView;
4312
+
4313
+ /** What the blocked rule reads of a task; structural, so a surface's own task shape answers it too. */
4314
+ interface BlockingSubject {
2344
4315
  id: string;
2345
4316
  status: string;
2346
4317
  dependencies: readonly string[];
@@ -2610,10 +4581,10 @@ declare function reduceConversation(view: ConversationView, input: ConversationI
2610
4581
 
2611
4582
  /**
2612
4583
  * The view a saved session reopens with: what its transcript and research log
2613
- * kept, in the order it happened — messages, notices and the compaction
2614
- * summary, plan markers, tool calls with each subagent's calls nested under
2615
- * it, and the token line. Reasoning and streamed text were never saved, so a
2616
- * reload has none.
4584
+ * kept, in the order it happened — messages, each reply run by run where it
4585
+ * began, notices and the compaction summary, plan markers, tool calls with
4586
+ * each subagent's calls nested under it, and the token line. Reasoning and
4587
+ * streamed text that never settled were never saved, so a reload has none.
2617
4588
  *
2618
4589
  * The research log interleaves with the transcript by time. Entries older than
2619
4590
  * a compaction's first kept message went with the turns it condensed.
@@ -2638,8 +4609,9 @@ interface TaskLogView extends BlockList {
2638
4609
  /** What the runner reported over the attempt, subagents included. */
2639
4610
  readonly usage?: PlannerUsage;
2640
4611
  /**
2641
- * The block each run of deltas streams into, keyed by stream, so the run's
2642
- * authoritative copy replaces it instead of repeating it below.
4612
+ * The block each stream of deltas writes into — a run of text by its id,
4613
+ * thinking by whose it is — so the stream's authoritative copy replaces it
4614
+ * instead of repeating it below.
2643
4615
  */
2644
4616
  readonly streams: Readonly<Record<string, string>>;
2645
4617
  /** The turn's latest agent text block, which a failure that repeats it takes the place of. */
@@ -2806,6 +4778,40 @@ declare function markdownTokens(text: string): readonly MarkdownToken[];
2806
4778
  /** A model's text as HTML, safe to set as a webview element's innerHTML. */
2807
4779
  declare function markdownHtml(text: string): string;
2808
4780
 
4781
+ interface TaskLogBatch {
4782
+ attempt: number;
4783
+ events: TaskLogEvent[];
4784
+ position?: StreamStamp;
4785
+ }
4786
+ /** A saved attempt and the live batches that follow it (ADR-0018 P1). */
4787
+ interface TaskLogFollow {
4788
+ view: TaskLogView;
4789
+ attempts: number[];
4790
+ attempt: number;
4791
+ pending: TaskLogBatch[];
4792
+ loaded: boolean;
4793
+ followLatest: boolean;
4794
+ /** The outstanding pick; an older attempt read cannot replace it. */
4795
+ loadingAttempt?: number;
4796
+ /** The saved read's cutoff, also rejecting socket copies delivered after the read. */
4797
+ position?: StreamStamp;
4798
+ }
4799
+ type TaskLogFollowInput = {
4800
+ type: 'opened';
4801
+ } | ({
4802
+ type: 'loaded';
4803
+ attempts?: number[];
4804
+ abandoned?: boolean;
4805
+ } & TaskLogBatch) | ({
4806
+ type: 'live';
4807
+ } & TaskLogBatch) | {
4808
+ type: 'picked';
4809
+ attempt: number;
4810
+ };
4811
+ declare const EMPTY_TASK_LOG_FOLLOW: TaskLogFollow;
4812
+ /** Join by stream position, never by event content or socket arrival time. */
4813
+ declare function followTaskLog(state: TaskLogFollow, input: TaskLogFollowInput): TaskLogFollow;
4814
+
2809
4815
  /**
2810
4816
  * What a row's status means to a reader, before any surface picks a word,
2811
4817
  * icon or colour for it. `quiet` is a running task whose runner has printed
@@ -2837,7 +4843,7 @@ declare function skippedLabel(reason: string | null): string;
2837
4843
  * for them (ADR-0035). Not a runner approval: nothing has started, so no
2838
4844
  * runner is asking anything.
2839
4845
  */
2840
- declare const WAITS_FOR_APPROVAL_LABEL = "Waiting for your approval to start";
4846
+ declare const WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
2841
4847
  /** Runner requests a task waits on (ADR-0018, A1); null when none do. */
2842
4848
  declare function approvalLabel(count: number | undefined): string | null;
2843
4849
  /**
@@ -2914,6 +4920,11 @@ interface TaskRowSubject {
2914
4920
  mergeGate?: readonly string[];
2915
4921
  forcedPastGate?: readonly string[];
2916
4922
  attemptUsage?: readonly AttemptUsage[];
4923
+ assignedRunner?: string;
4924
+ assignedModel?: {
4925
+ modelId: string;
4926
+ modelLabel?: string;
4927
+ };
2917
4928
  }
2918
4929
  interface TaskRowIsolation {
2919
4930
  state: TaskIsolationState;
@@ -2955,12 +4966,14 @@ interface TaskRowContext {
2955
4966
  }[];
2956
4967
  /** What a resume time is read against; the clock when absent. */
2957
4968
  now?: Date;
4969
+ /** The enabled runners and allowlist an assignment is read against; without it no assignment is flagged. */
4970
+ runnerScope?: AssignmentScope;
2958
4971
  }
2959
4972
  /** Whether the row stands for an AI task, a manual one, or an ops task (ADR-0020). */
2960
4973
  type TaskRowKind = 'ai' | 'user' | 'ops';
2961
- interface TaskRowUsage extends UsageLine {
4974
+ /** The task's whole spend (see `taskUsageSummary`); `attempt` is the latest one, 0 before any reported. */
4975
+ interface TaskRowUsage extends Pick<TaskUsageSummary, 'totals' | 'contextFill'> {
2962
4976
  attempt: number;
2963
- wallTimeMs?: number;
2964
4977
  }
2965
4978
  interface TaskRowView {
2966
4979
  usage: TaskRowUsage | null;
@@ -2978,6 +4991,10 @@ interface TaskRowView {
2978
4991
  approvals: string | null;
2979
4992
  /** That it waits for the user's approval to start, in sentence case; null once it started, or when it needs none. */
2980
4993
  waitsForApproval: string | null;
4994
+ /** Why the task's runner or model is one the user has since moved away from, in sentence case; null when it is not. The task still runs. */
4995
+ assignmentFlag: string | null;
4996
+ /** What the flag means for this task as it stands (a failed one runs again only if retried); set exactly when `assignmentFlag` is. */
4997
+ assignmentConsequence: string | null;
2981
4998
  /** The task's mode is tagged autonomous: it runs without permission prompts. */
2982
4999
  autonomous: boolean;
2983
5000
  /** The repos its isolated work changed, by path; empty for a group of one. */
@@ -3040,9 +5057,10 @@ declare function titledRefs(ids: readonly string[], tasks: readonly Named[]): st
3040
5057
  * and a bare "Remove task?" would hide that.
3041
5058
  */
3042
5059
  declare function dependentsNotice(dependents: readonly {
5060
+ id?: string;
3043
5061
  order: number;
3044
5062
  title: string;
3045
- }[]): string | null;
5063
+ }[], tasks?: readonly Named[]): string | null;
3046
5064
  /**
3047
5065
  * What a start past a merge gate is asked with (ADR-0020): the work the task
3048
5066
  * would act without, named. `subject` is how the asking surface refers to the
@@ -3075,4 +5093,4 @@ declare function parseSwitch(arg: string): boolean | null;
3075
5093
  /** A whole number at least `min`, as typed; null for anything else. */
3076
5094
  declare function parseCount(arg: string | undefined, min: number): number | null;
3077
5095
 
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 };
5096
+ export { type ForceSendResponse as $, ACT_RISK as A, type BlockingSubject as B, CHAT_COMMANDS as C, type ConversationCompactResponse as D, type ConversationCompaction as E, ConversationEditError as F, type ConversationForkResponse as G, type ConversationInput as H, type ConversationRequest as I, type ConversationRewindResponse as J, type ConversationTurn as K, type ConversationView as L, DEFAULT_WEB_READ_BUDGET as M, type DaemonErrorCode as N, type DecideOptions as O, type DiffRow as P, type DiffRowKind as Q, type DiffStat as R, type DisplayBlock as S, EMPTY_CONVERSATION as T, EMPTY_HOLD as U, EMPTY_RUN as V, EMPTY_TASK_LOG as W, EMPTY_TASK_LOG_FOLLOW as X, type ErrorBody as Y, type ExecuteResponse as Z, type ExecutionSummary as _, AUTORUN_RISK as a, type RewindTargetsResponse as a$, type GatedConversation as a0, type GeneratePlanResponse as a1, type GlobalRunSettings as a2, HEADLESS_CLI_COMMANDS as a3, type HeldEdit as a4, type HeldEditMark as a5, type IAiService as a6, type INotification as a7, type IWebFetcher as a8, type IsolationDiffResponse as a9, type PlanResponse as aA, type PlanSnapshot as aB, PlanStore as aC, type PlannerCommandSyntax as aD, PlannerConversation as aE, type PlannerConversationHost as aF, type PlannerModelCandidate as aG, type PlannerModelChoice as aH, PlannerModelMemory as aI, type PlannerModelRecall as aJ, type PlannerModelStore as aK, type PlannerModes as aL, type PlannerNotReady as aM, PlannerNotReadyError as aN, type PlannerReadinessConfig as aO, type PlannerSubmission as aP, PlannerTurnDiscardedError as aQ, PlannerTurnStoppedError as aR, PlannerUsageLimitError as aS, type PrdResponse as aT, type PromptHold as aU, RUN_SETTINGS_DEFAULTS as aV, type RefTask as aW, type RemoveMessageResponse as aX, type ResolveAll as aY, type ResolveConflictResponse as aZ, type RewindTarget as a_, type IsolationMergeResponse as aa, IsolationRunController as ab, type LiveOutputLookup as ac, type LiveTail as ad, type LiveTailOptions as ae, type LocalEntry as af, type MarkdownToken as ag, type MergeGateEntry as ah, type MergeGateResponse as ai, type MessageBlock as aj, type MessageRole as ak, type ModelsResponse as al, NOT_STARTED as am, NO_TURN as an, type NewTaskFields as ao, type NotificationAction as ap, OUTPUT_LINES_DEFAULT as aq, OUTPUT_LINES_MAX as ar, type OkResponse as as, type OrchestratorObserver as at, type OutputPreview as au, type PendingApproval as av, PendingApprovals as aw, type PendingApprovalsOptions as ax, type PlanBlock as ay, type PlanMarkerStatus as az, type AdoptSessionResponse as b, type TaskPlacement as b$, type RunInput as b0, type RunMergeView as b1, RunSettingsError as b2, type RunSnapshotFrame as b3, type RunTask as b4, type RunTaskStatus as b5, type RunView as b6, type RunnerCatalog as b7, type RunnerLoadedBlock as b8, type RunnersResponse as b9, type SubagentChild as bA, type SubagentStatus as bB, type SurfacePlan as bC, type SurfacePlanState as bD, type SurfaceTask as bE, TASK_QUERY_ANSWER_MAX_CHARS as bF, TASK_QUERY_ANSWER_OR_OPS as bG, TASK_QUERY_FIELDS as bH, TASK_QUERY_PROTOCOL as bI, TASK_QUERY_REMINDER as bJ, TERMINAL_UI_COMMANDS as bK, type TakenPrompt as bL, type TaskAttemptSnapshot as bM, TaskControlError as bN, type TaskLogAttemptsResponse as bO, type TaskLogBatch as bP, type TaskLogEvent as bQ, type TaskLogFollow as bR, type TaskLogFollowInput as bS, type TaskLogResponse as bT, type TaskLogView as bU, type TaskMessageResponse as bV, type TaskOp as bW, TaskOrchestrator as bX, type TaskOrchestratorDeps as bY, type TaskOrchestratorOptions as bZ, type TaskOutputSource as b_, STOPPED_BEFORE_START as ba, SUPERVISOR_LEVEL_LABEL as bb, type SerializedConversationMessage as bc, type SerializedPendingTurn as bd, type SerializedPlan as be, type SerializedTask as bf, type SerializedTaskStatus as bg, type SessionBroadcaster as bh, type SessionData as bi, type SessionFrame as bj, type SessionListResponse as bk, type SessionMessage as bl, type SessionMeta as bm, type SessionNotice as bn, type SessionResponse as bo, type SettingsResponse as bp, SettingsService as bq, type SettingsUpdateResponse as br, type SkillInvocation as bs, type SkillLoadBlock as bt, type SkillToken as bu, type StartAdmission as bv, type StartWait as bw, type StopResponse as bx, type StreamStamp as by, type SubagentBlock as bz, type ApplyTaskOpsResult as c, loadedSkillTokens as c$, type TaskQuery as c0, type TaskQueryCatalog as c1, type TaskQueryField as c2, type TaskRef as c3, type TaskRowAction as c4, type TaskRowContext as c5, type TaskRowIsolation as c6, type TaskRowKind as c7, type TaskRowPlacement as c8, type TaskRowSubject as c9, awaitingLabel as cA, blockedByLabel as cB, blockingFailures as cC, canMergeTasks as cD, canSetDependencies as cE, canSplitTask as cF, capConflictFiles as cG, classifyOutcome as cH, coalesceTaskLog as cI, createAiService as cJ, dependencyCandidates as cK, dependentsNotice as cL, dependentsOf as cM, describePlannerNotReady as cN, diffRows as cO, diffStat as cP, diffSummary as cQ, digestTaskLog as cR, drainNext as cS, executionSummary as cT, followTaskLog as cU, followTurn as cV, fromTranscript as cW, getSettingsPath as cX, hasHiddenDetail as cY, holdPrompt as cZ, isTaskRunning as c_, type TaskRowUsage as ca, type TaskRowView as cb, type TaskStartResponse as cc, type TaskStatusKind as cd, type ThinkingDisplayBlock as ce, type ToolBlock as cf, type ToolHeadline as cg, type ToolStatus as ch, type TreeTask as ci, type TurnGate as cj, type UsageBlock as ck, type UserSettings as cl, WAITS_FOR_APPROVAL_LABEL as cm, type WebRead as cn, type WebReadDecision as co, WebReadPolicy as cp, type Worded as cq, type WorkspacesResponse as cr, abbreviateHome as cs, aheadOfDraft as ct, applyTaskOps as cu, approvalLabel as cv, assignmentIssue as cw, assignmentIssueText as cx, automergeSummary as cy, autorunSummary as cz, type ApprovalAnswerResponse as d, unsendAll as d$, markAction as d0, markRequestFor as d1, markdownHtml as d2, markdownTokens as d3, namedRepos as d4, newTaskFields as d5, opsEditable as d6, outputLines as d7, outputPreview as d8, parseCount as d9, skippedLabel as dA, stopTurn as dB, subtreeIds as dC, summarizeToolCall as dD, supervisorSummary as dE, surfacePlan as dF, surfacePlanState as dG, takeAt as dH, taskLogForSurface as dI, taskOrderLabel as dJ, taskPlacement as dK, taskQuerySignature as dL, taskRef as dM, taskRefOf as dN, taskRowActions as dO, taskRowView as dP, taskStartedNotice as dQ, taskStatusKind as dR, textHasTaskOps as dS, textHasTaskQuery as dT, titledRefs as dU, titledTaskRef as dV, titledTaskRefOf as dW, toTaskLogEvent as dX, toolHeadline as dY, trimToolOutput as dZ, truncateCheckpointSummary as d_, parseSwitch as da, parseTaskOpsJson as db, parseTaskQueryJson as dc, pastGateConfirmation as dd, plannerMessage as de, plannerNotReady as df, plannerNotReadyFor as dg, plannerTranscript as dh, reconcileApprovals as di, reduceConversation as dj, reduceRun as dk, reduceTaskLog as dl, renderTaskQueryAnswer as dm, replayTaskLog as dn, resolveSkillInvocation as dp, resolveTaskRef as dq, resolveUserTaskRef as dr, resumeLabel as ds, resumeTime as dt, runSettingsOf as du, runnerToolSubject as dv, serializePlan as dw, serializeTask as dx, serializeTaskStatus as dy, skillTokens as dz, type ApprovalBlock as e, unsendLatest as e0, walkPlan as e1, type ApprovalRequestMessage as f, type ApprovalStatus as g, type ApprovalsResponse as h, type ApproveReviewResponse as i, type AskOptions as j, type AssignmentCatalog as k, type AssignmentIssue as l, type AssignmentScope as m, type AssignmentWriter as n, type AutoMergeTrigger as o, CHECKPOINT_TRUNCATE_LENGTH as p, type CancelPlanningResponse as q, type CancelResumeResponse as r, CliAgentAiService as s, type CliAgentAiServiceDeps as t, type CommandBlock as u, type CommandDescriptor as v, type CommandStatus as w, type CommandsResponse as x, type CompactionBlock as y, ConversationBusyError as z };