@ordewell/core 0.10.5 → 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 (44) hide show
  1. package/dist/{IRunner-Crkt3tHD.d.ts → IFileSystem-C4_mpfze.d.ts} +97 -14
  2. package/dist/{IRunner-lMQIa5Wc.d.mts → IFileSystem-DUgeWWQN.d.mts} +97 -14
  3. package/dist/{ModeResolver-B4hhABNf.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
  4. package/dist/{ModeResolver-CUR5jCSU.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
  5. package/dist/{Task-D4QHjdGv.d.mts → Task-C0o4i3Nn.d.mts} +23 -4
  6. package/dist/{Task-D4QHjdGv.d.ts → Task-C0o4i3Nn.d.ts} +23 -4
  7. package/dist/{chunk-6LEA6WAM.mjs → chunk-DMG4ISUG.mjs} +923 -45
  8. package/dist/chunk-DMG4ISUG.mjs.map +1 -0
  9. package/dist/{chunk-RT4NQ2BZ.mjs → chunk-EEE2GBLG.mjs} +114 -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-F3KIX7WR.mjs → chunk-LTHKFBKM.mjs} +4 -4
  14. package/dist/chunk-LTHKFBKM.mjs.map +1 -0
  15. package/dist/index.d.mts +278 -1701
  16. package/dist/index.d.ts +278 -1701
  17. package/dist/index.js +2729 -1903
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +2171 -1523
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/parsing.d.mts +2 -2
  22. package/dist/parsing.d.ts +2 -2
  23. package/dist/parsing.js +231 -75
  24. package/dist/parsing.js.map +1 -1
  25. package/dist/parsing.mjs +3 -3
  26. package/dist/plan-utils.d.mts +20 -4
  27. package/dist/plan-utils.d.ts +20 -4
  28. package/dist/plan-utils.js +255 -97
  29. package/dist/plan-utils.js.map +1 -1
  30. package/dist/plan-utils.mjs +33 -19
  31. package/dist/{plan-utils-wI8MuDBC.d.mts → runSettingsText-BgOmLxdw.d.mts} +1941 -108
  32. package/dist/{plan-utils-BVJCzNvm.d.ts → runSettingsText-BgXvajj2.d.ts} +1941 -108
  33. package/dist/testing.d.mts +2 -3
  34. package/dist/testing.d.ts +2 -3
  35. package/dist/testing.js.map +1 -1
  36. package/dist/testing.mjs +1 -1
  37. package/package.json +1 -1
  38. package/dist/IFileSystem-ClBdp59f.d.ts +0 -78
  39. package/dist/IFileSystem-Db34maxC.d.mts +0 -78
  40. package/dist/chunk-4QLFM73A.mjs +0 -1407
  41. package/dist/chunk-4QLFM73A.mjs.map +0 -1
  42. package/dist/chunk-6LEA6WAM.mjs.map +0 -1
  43. package/dist/chunk-F3KIX7WR.mjs.map +0 -1
  44. package/dist/chunk-RT4NQ2BZ.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
- import { ah as Task, a3 as RunnerId, ap as TaskSnapshot, b as AwaitingReason, au as UsageRecord, a4 as RunnerSessionCost, d as DiscoveredModel, ae as SkillsService, a9 as SkillInfo, Y as RepoGroupLayout, I as IWorktreeIsolation, w as IsolationRun, k as IsolationHandoff, J as PlanIsolation, x as IsolationTaskRecord, ak as TaskIsolation, H as IsolationView, S as PrepareIntent, V as PreviousWork, s as IsolationOutcome, X as RepairEvidence, as as TreeSnapshot, ai as TaskDiffRead, aj as TaskDiffResult, r as IsolationMergeResult, aw as Verdict, ao as TaskSkillSnapshot, an as TaskSkillNotice, af as SubagentOutcome, L as LegacyPlanState, C as ConversationMessage, ab as SkillLoadNotice, P as PendingPlanTurn, a as AttemptUsage, M as ModelUsage, av as UsageTotals, O as PlanUsage, f as ExecutionRecord, $ as ResearchStep, a2 as RunSettings, K as PlanState, ag as SupervisorLevel, a0 as ResearchStepOutcome, Z as ResearchLogEntry, R as PlannerUsage, al as TaskIsolationState, ar as TaskUsageSummary } from './Task-D4QHjdGv.mjs';
2
- import { R as RunnerModeInfo } from './ModeResolver-B4hhABNf.mjs';
3
- import { B as IRunnerSession, y as IConfig, aD as TaskMessageFrom, aq as RunnerModelRef, ai as RunnerCommand, an as RunnerMcpServer, aw as StructuredTurnEnd, j as ApprovalDecision, av as StructuredEvent, ab as QueuedTaskMessage, k as ApprovalKind, q as ApprovalSource, z as IRunner, ar as RunnerRegistry } from './IRunner-lMQIa5Wc.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,8 +142,6 @@ 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
147
  /** The owner is saved with the session it belongs to, and dropped with it. */
@@ -167,26 +165,90 @@ declare class PlanStore {
167
165
  }
168
166
 
169
167
  /**
170
- * The catalog a model/task-mode edit is checked against — the same discovered
171
- * models and manifest modes the planner was shown in the per-turn catalog
172
- * block (`PlannerConversation.catalogBlock`), so a refusal here can never name something
173
- * 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.
174
190
  */
175
- interface EditCatalog {
191
+ interface AssignmentCatalog {
192
+ /** A runner absent, or listed with nothing, has models discovery could not find, not none. */
176
193
  modelsByRunner: Partial<Record<RunnerId, DiscoveredModel[]>>;
177
- runnerModes: Partial<Record<RunnerId, RunnerModeInfo[]>>;
178
- /** 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}. */
179
197
  perRunnerAllowlist?: Partial<Record<RunnerId, string[]>>;
180
- /** 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. */
181
199
  autonomousDefault?: boolean;
182
200
  /**
183
- * Whether a task's runner is up now. Its model is then the runner's (#106):
184
- * the process was started on one and only the runner's own command moves it,
185
- * so an edit from outside would leave the plan naming what is not running.
186
- * Absent means none is.
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.
187
205
  */
188
206
  liveRunner?: (taskId: string) => boolean;
189
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;
250
+ }
251
+ declare function canSetDependencies<T extends DependencyTask<T>>(tasks: readonly T[], taskId: string, dependencies: readonly string[]): DependencyCheck;
190
252
 
191
253
  /**
192
254
  * Targeted task edits emitted by the planner conversation (the 'task_ops'
@@ -240,6 +302,8 @@ interface ApplyTaskOpsResult {
240
302
  errors: string[];
241
303
  /** Human-readable summary of what changed, for the chat transcript. */
242
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[];
243
307
  /** On ok, the id of the task each batch handle names. */
244
308
  handles?: ReadonlyMap<string, string>;
245
309
  }
@@ -269,22 +333,7 @@ interface TaskRef {
269
333
  title: string;
270
334
  dependencies: string[];
271
335
  }
272
- /** The tasks listing `taskId` as a dependency — exactly what removing it detaches. */
273
- declare function dependentsOf<T extends Pick<TaskRef, 'id' | 'dependencies'>>(tasks: T[], taskId: string): T[];
274
- /**
275
- * The tasks that may become dependencies of `taskId`: those displayed before it.
276
- *
277
- * Offering only earlier tasks is what keeps a hand-edited graph valid without a
278
- * cycle check — dependencies then only ever point backwards in display order,
279
- * the same invariant `applyTaskOps` enforces. Omit `taskId` for a task that does
280
- * not exist yet: it lands last, so every current task is a candidate.
281
- */
282
- declare function dependencyCandidates<T extends Pick<TaskRef, 'id' | 'order'>>(tasks: readonly T[], taskId?: string): T[];
283
- /** Pre-flight check for a hand-edited dependency list: every id exists and comes earlier. */
284
- declare function canSetDependencies<T extends TaskRef>(tasks: readonly T[], taskId: string, dependencies: string[]): {
285
- ok: boolean;
286
- error?: string;
287
- };
336
+
288
337
  /** Pre-flight check for a split: the task exists and is not locked by execution. */
289
338
  declare function canSplitTask(tasks: readonly Task[], taskId: string): {
290
339
  ok: boolean;
@@ -296,7 +345,7 @@ declare function canSplitTask(tasks: readonly Task[], taskId: string): {
296
345
  * completed tasks untouched), or nothing is returned and `errors` explains
297
346
  * why. The caller commits `tasks` on ok.
298
347
  */
299
- 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;
300
349
 
301
350
  /**
302
351
  * Task skills end to end (ADR-0024): a skill reaches a runner only by being
@@ -367,6 +416,8 @@ interface IsolationRunListener {
367
416
  blocked(repos: string[]): void;
368
417
  /** An isolated run closed and handed its integration branches over. */
369
418
  handoff(handoff: IsolationHandoff): void;
419
+ /** The run was cleared up and forgotten: nothing was handed over, or it was discarded. */
420
+ closed(): void;
370
421
  /** Already said through the notifications; also for a surface that shows no toasts. */
371
422
  notice(level: IsolationNoticeLevel, message: string): void;
372
423
  }
@@ -733,6 +784,86 @@ declare class Landing {
733
784
  private isGroup;
734
785
  }
735
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
+
736
867
  /**
737
868
  * The variables one workspace's planner and agents run with, on top of the
738
869
  * daemon's own environment (ADR-0016).
@@ -833,13 +964,21 @@ type TaskLogEvent =
833
964
  } | {
834
965
  type: 'turn_end';
835
966
  reason: StructuredTurnEnd;
836
- } | {
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
+ | {
837
974
  type: 'text_delta';
975
+ runId?: string;
838
976
  text: string;
839
977
  }
840
- /** 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`. */
841
979
  | {
842
980
  type: 'text';
981
+ runId?: string;
843
982
  text: string;
844
983
  } | {
845
984
  type: 'thinking_delta';
@@ -973,9 +1112,10 @@ declare function toTaskLogEvent(event: StructuredEvent): TaskLogEvent | null;
973
1112
  /** A log as a surface is sent it, live or read back: everything but the skills' bodies. */
974
1113
  declare function taskLogForSurface(events: readonly TaskLogEvent[]): TaskLogEvent[];
975
1114
  /**
976
- * Merge runs of deltas into one event each. Deltas arrive a few characters at
977
- * a time; merged, a batch is one message on the wire and one line on disk,
978
- * 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.
979
1119
  */
980
1120
  declare function coalesceTaskLog(events: readonly TaskLogEvent[]): TaskLogEvent[];
981
1121
  /**
@@ -1138,8 +1278,9 @@ type PlannerTurnOutcome = 'message' | 'plan' | 'task_ops' | 'stopped' | 'error';
1138
1278
  * `planner_turn_ended` with the same `turnId`; every turn-scoped message in
1139
1279
  * between carries it. The stream is provisional and the settled messages are
1140
1280
  * authoritative:
1141
- * - `planner_message` replaces the streamed text of the turn's final segment —
1142
- * 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.
1143
1284
  * - A reply that is a JSON envelope (plan, taskOps, taskQuery) never arrives
1144
1285
  * as `planner_text_delta`; it streams as `plan_token`, the "building plan"
1145
1286
  * display.
@@ -1161,9 +1302,12 @@ type SessionMessage =
1161
1302
  }
1162
1303
  /**
1163
1304
  * The settled reply of a planner turn, emitted by the session once the turn
1164
- * is classified. Authoritative over any `planner_text_delta` of its turn's
1165
- * 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.
1166
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.
1167
1311
  */
1168
1312
  | {
1169
1313
  type: 'planner_message';
@@ -1171,6 +1315,7 @@ type SessionMessage =
1171
1315
  timestamp: string;
1172
1316
  turnId?: string;
1173
1317
  supervisor?: true;
1318
+ runs?: ReplyRun[];
1174
1319
  }
1175
1320
  /**
1176
1321
  * A planner turn began. Emitted once per turn by whoever runs the turn,
@@ -1363,6 +1508,8 @@ type SessionMessage =
1363
1508
  type: 'isolation_merge';
1364
1509
  result: IsolationMergeResult;
1365
1510
  trigger?: AutoMergeTrigger;
1511
+ } | {
1512
+ type: 'run_closed';
1366
1513
  } | {
1367
1514
  type: 'planner_liveness';
1368
1515
  } | {
@@ -1586,6 +1733,8 @@ interface OrchestratorObserver {
1586
1733
  }): void;
1587
1734
  /** An isolated run settled; emitted before `onExecutionComplete`, which surfaces treat as terminal. */
1588
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;
1589
1738
  /**
1590
1739
  * What a run says about how it isolates — the fallback to the workspace root,
1591
1740
  * shared paths, copies, a stash. Beside the notification channel, which a
@@ -1682,7 +1831,7 @@ interface TaskOrchestratorOptions {
1682
1831
  isApproved?: (taskId: string) => boolean;
1683
1832
  }
1684
1833
  /**
1685
- * The pure scheduler. Owns execution state (`running`, `planStatus`,
1834
+ * The pure scheduler. Owns execution state (the {@link RunState},
1686
1835
  * `reviewApproved`, the live task attempts, the start hold) and the verifier.
1687
1836
  * All task-shaped state — the plan tree, the flat index, the completed set
1688
1837
  * — lives in {@link PlanStore}, injected at construction. The orchestrator
@@ -1699,8 +1848,8 @@ declare class TaskOrchestrator {
1699
1848
  private output;
1700
1849
  private attempts;
1701
1850
  private verifier;
1702
- private running;
1703
- 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;
1704
1853
  /** Planner turns in flight that new starts wait for (ADR-0029); running attempts carry on. */
1705
1854
  private startHolds;
1706
1855
  /** Bumped by the user's Stop: a start admitted under an older generation never takes its claim. */
@@ -1712,14 +1861,6 @@ declare class TaskOrchestrator {
1712
1861
  private mergesInFlight;
1713
1862
  private retryCounts;
1714
1863
  private spawnCounts;
1715
- /** A full-plan run a usage-limit pause holds: no new start until the reset resumes it. */
1716
- private heldOnLimit;
1717
- /**
1718
- * A full-plan run that ended with failed tasks and the tasks they block
1719
- * (ADR-0034). A retry of a failed task continues it, so what it blocked
1720
- * starts once it passes — without this the retried task ran alone.
1721
- */
1722
- private endedOnFailure;
1723
1864
  /** A Merge all is under way: no ops task starts until it ends (ADR-0020). */
1724
1865
  private merging;
1725
1866
  /** The last "waiting for Merge all" notice, so a run paused at a gate says it once, not on every tick. */
@@ -1884,7 +2025,9 @@ declare class TaskOrchestrator {
1884
2025
  * reset. A Stop is owed to it, whether or not anything runs.
1885
2026
  */
1886
2027
  get startsPending(): boolean;
1887
- /** 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. */
1888
2031
  private whileAdmitted;
1889
2032
  /**
1890
2033
  * "Merge all"; a settled run that merged everything is cleared up and
@@ -1909,10 +2052,20 @@ declare class TaskOrchestrator {
1909
2052
  runMergeView(): RunMergeView | null;
1910
2053
  /**
1911
2054
  * The ancestors an ops or user task waits on at its merge gate: those whose
1912
- * landed work is not in the user's branch yet (ADR-0020). Empty once the
1913
- * 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.
1914
2059
  */
1915
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;
1916
2069
  /** The failed tasks a task not started yet waits on, through its dependencies (ADR-0034); empty when it is not blocked. */
1917
2070
  getBlockedBy(taskId: string): string[];
1918
2071
  loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
@@ -1931,8 +2084,16 @@ declare class TaskOrchestrator {
1931
2084
  * not a retry, and the tasks it blocks wait for that retry (ADR-0034).
1932
2085
  */
1933
2086
  reconcilePlan(newTasks: Task[], planRunners?: RunnerId[]): void;
2087
+ /** Start the full-plan run, or continue one that is held or ended on a failure. */
1934
2088
  start(origin?: RunOrigin): Promise<void>;
1935
- /** 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
+ */
1936
2097
  private holdOnLimit;
1937
2098
  /**
1938
2099
  * Stop the run. The user's Stop also withdraws every start admitted before
@@ -1992,12 +2153,18 @@ declare class TaskOrchestrator {
1992
2153
  * integration branch.
1993
2154
  */
1994
2155
  private land;
1995
- /** 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
+ */
1996
2161
  private applyLanding;
1997
2162
  /**
1998
2163
  * Work that did not land waits on the user — it never fails the task or
1999
2164
  * blocks its dependents — unless its conflict is owed a repair, which takes
2000
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.
2001
2168
  */
2002
2169
  private settleUnlanded;
2003
2170
  private landResolved;
@@ -2020,10 +2187,10 @@ declare class TaskOrchestrator {
2020
2187
  */
2021
2188
  private openFor;
2022
2189
  /**
2023
- * {@link openFor} for an explicit start admitted at `admittedAt`. A Stop
2024
- * while the run opened interrupts that opening too, which then opens
2025
- * nothing; the start says it was stopped rather than going quiet as if a
2026
- * 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.
2027
2194
  */
2028
2195
  private openForAdmitted;
2029
2196
  /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
@@ -2078,10 +2245,19 @@ declare class TaskOrchestrator {
2078
2245
  /**
2079
2246
  * Keep a task paused on its runner's usage limit from resuming on its own:
2080
2247
  * it waits for a manual retry, as one whose reset time could not be read
2081
- * does. What a surface's cancel calls, and what moving the task to another
2082
- * 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.
2083
2259
  */
2084
- cancelScheduledResume(taskId: string): boolean;
2260
+ private releaseHold;
2085
2261
  /**
2086
2262
  * The runner whose attempt left the task's work, when the task has been
2087
2263
  * moved off it since: a session cannot cross runners, so whatever starts
@@ -2093,6 +2269,11 @@ declare class TaskOrchestrator {
2093
2269
  * Run before writing assignments so cancellation sees the old runner's
2094
2270
  * shared wait. A session cannot cross runners, so the next attempt is a
2095
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.
2096
2277
  */
2097
2278
  releaseMovedResume(taskId: string, changes: Partial<Task>): Partial<Task>;
2098
2279
  /**
@@ -2137,15 +2318,18 @@ declare class TaskOrchestrator {
2137
2318
  /**
2138
2319
  * Retry a task paused on a usage limit, continuing its saved session where
2139
2320
  * it left one (ADR-0018, K1), then release the hold the pause put on the
2140
- * 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.
2141
2323
  */
2142
2324
  private resumeTask;
2143
2325
  /**
2144
- * A resume at the limit's reset that nobody waits on cannot be refused to
2145
- * anyone, so one that may not claim waits for the user instead — its resume
2146
- * 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.
2147
2331
  */
2148
- private mayResumeAtReset;
2332
+ private mayResume;
2149
2333
  /**
2150
2334
  * Manually start a single AI task right now (the "force start" affordance on
2151
2335
  * a task card), past a failed dependency (ADR-0034) or a merge gate
@@ -2161,24 +2345,38 @@ declare class TaskOrchestrator {
2161
2345
  * task, or already running.
2162
2346
  */
2163
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;
2164
2357
  /**
2165
2358
  * A force start of a task whose dependencies are still under way or yet to
2166
2359
  * run: started now, it would build on work that is not there, so it waits
2167
2360
  * for them in the run, off any hold, and says so. Dependencies are never
2168
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.
2169
2363
  */
2170
- private queuedBehindDependencies;
2364
+ private queueBehindDependencies;
2171
2365
  /**
2172
- * A task's unmet dependencies, less the failed or blocked ones a force start
2173
- * goes past. A blocked one is gone past for its failure only: the work under
2174
- * way or yet to run that it waits on is still waited for, through it.
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.
2175
2371
  */
2176
- private underWayBefore;
2372
+ private startQueued;
2177
2373
  /**
2178
- * Starts the queued force starts whose dependencies have now finished. One
2179
- * the scheduler now drives the run for, and nothing blocks, is left to it.
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`.
2180
2377
  */
2181
- private startQueued;
2378
+ private holdsQueued;
2379
+ private sayQueuedAtGate;
2182
2380
  private refuseOpsDuringMerge;
2183
2381
  /**
2184
2382
  * A force start passes a merge gate (ADR-0020), and the surface confirmed it
@@ -2186,24 +2384,27 @@ declare class TaskOrchestrator {
2186
2384
  * on is not in the user's branch.
2187
2385
  */
2188
2386
  private notePastGate;
2189
- /**
2190
- * Run exactly one task outside full-plan scheduling. The active/starting
2191
- * session still contributes to {@link hasLiveWork} so every surface exposes
2192
- * Stop and disables Execute Plan, but onVerdict cannot auto-schedule other
2193
- * tasks because the plan scheduler's `running` flag remains false.
2194
- */
2195
- runTask(taskId: string): Promise<void>;
2196
2387
  getCompletedCount(): number;
2197
2388
  getTotalCount(): number;
2198
2389
  isAllComplete(): boolean;
2199
2390
  isAnyFailed(): boolean;
2200
2391
  approveReview(origin?: RunOrigin): Promise<void>;
2201
2392
  tick(): Promise<void>;
2202
- /** 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;
2203
2403
  /** Skipped work is not completed work, so a run with any says so rather than that everything completed. */
2204
2404
  private sayFinished;
2405
+ /** How a run that ended on failures ends: what it finished, what failed, and what that blocks. */
2205
2406
  private sayEndedOnFailure;
2206
- /** 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. */
2207
2408
  private stillReady;
2208
2409
  /**
2209
2410
  * A run paused at merge gates says so, as plainly as a task that waits on
@@ -2212,9 +2413,10 @@ declare class TaskOrchestrator {
2212
2413
  private sayGate;
2213
2414
  /**
2214
2415
  * Why a start may not take its claim now, or null when it may: a Stop came
2215
- * since it was admitted (ADR-0029), the task is outside the user's approval
2216
- * record (ADR-0035), or every slot is taken. Read in the same synchronous
2217
- * 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.
2218
2420
  */
2219
2421
  private claimRefusal;
2220
2422
  /** An explicit start's refusal, before it changes anything: its caller's answer, as a {@link TaskControlError}. */
@@ -2226,7 +2428,10 @@ declare class TaskOrchestrator {
2226
2428
  * start, a retry's, a continue, a resume, a repair. Checked and taken in one
2227
2429
  * synchronous step ({@link claimRefusal}), then marked claimed before
2228
2430
  * anything is awaited, so no other start can take the same task or slot.
2229
- * 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.
2230
2435
  */
2231
2436
  private claim;
2232
2437
  private runClaimed;
@@ -2254,6 +2459,8 @@ declare class TaskOrchestrator {
2254
2459
  * A spawn whose attempt ended while it was in flight: kill what it produced
2255
2460
  * and take back the claim it made. Only the claim — whatever ended the
2256
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.
2257
2464
  */
2258
2465
  private abandonSpawn;
2259
2466
  private tell;
@@ -2282,6 +2489,599 @@ declare class TaskOrchestrator {
2282
2489
  private endAllAttempts;
2283
2490
  }
2284
2491
 
2492
+ /**
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.
2505
+ */
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;
2513
+ /**
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.
2518
+ */
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;
2543
+ }
2544
+ /**
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.
2549
+ */
2550
+ declare const TASK_QUERY_ANSWER_OR_OPS: string;
2551
+ /**
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.
2555
+ */
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.";
2559
+ /**
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.
2563
+ */
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;
2572
+
2573
+ /**
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.
2578
+ */
2579
+ type ResolveAll = (host: string) => Promise<string[]>;
2580
+
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;
2589
+ }
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;
2612
+ }
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);
2636
+ /**
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.
2641
+ */
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;
2659
+ }
2660
+
2661
+ interface IWebFetcher {
2662
+ /**
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.
2666
+ */
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;
2684
+ }
2685
+
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
+ }
2695
+
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
+
2285
3085
  /**
2286
3086
  * What the user allows the plan to be built from right now: the enabled
2287
3087
  * runners and each runner's model allowlist. Structural, so a surface that
@@ -2328,19 +3128,560 @@ declare function assignmentIssue(task: AssignedTask, scope: AssignmentScope): As
2328
3128
  declare function assignmentIssueText(issue: AssignmentIssue): string;
2329
3129
 
2330
3130
  /**
2331
- * Task references: the one grammar for naming a task in a plan, both ways.
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.
2332
3678
  * A label is rendered from a task's order under its parent ("2", "2.1"), and
2333
3679
  * a reference written by anyone — the planner, a tool call, a command — is
2334
- * resolved back against the plan tree. Pure and import-free so a
3680
+ * resolved back against the plan tree. Pure so a
2335
3681
  * browser-bundled surface can use it through `@ordewell/core/plan-utils`.
2336
3682
  */
2337
3683
  /** The shape a reference resolves over: the plan's task tree, or a surface's projection of it. */
2338
- interface RefTask<T> {
2339
- id: string;
2340
- order: number;
2341
- title: string;
2342
- subtasks?: readonly T[];
2343
- }
3684
+ type RefTask<T> = TreeTask<T>;
2344
3685
  /**
2345
3686
  * The parent a label extends. A label is the full path ("3.2.1"), so a parent
2346
3687
  * below the top level carries the label it was itself reached by; without one
@@ -2383,6 +3724,100 @@ declare function titledTaskRefOf<T extends RefTask<T>>(tasks: readonly T[], id:
2383
3724
  * and a subtask is reachable only by its dotted path ("#2.1", "#2.1.3").
2384
3725
  */
2385
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
+ }
2386
3821
 
2387
3822
  /**
2388
3823
  * Pure, no-imports helper so a browser surface can format a conflict's files
@@ -2510,6 +3945,368 @@ declare class SettingsService {
2510
3945
  private persist;
2511
3946
  }
2512
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
+
2513
4310
  declare const EMPTY_RUN: RunView;
2514
4311
  declare function reduceRun(view: RunView, input: RunInput): RunView;
2515
4312
 
@@ -2784,10 +4581,10 @@ declare function reduceConversation(view: ConversationView, input: ConversationI
2784
4581
 
2785
4582
  /**
2786
4583
  * The view a saved session reopens with: what its transcript and research log
2787
- * kept, in the order it happened — messages, notices and the compaction
2788
- * summary, plan markers, tool calls with each subagent's calls nested under
2789
- * it, and the token line. Reasoning and streamed text were never saved, so a
2790
- * 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.
2791
4588
  *
2792
4589
  * The research log interleaves with the transcript by time. Entries older than
2793
4590
  * a compaction's first kept message went with the turns it condensed.
@@ -2812,8 +4609,9 @@ interface TaskLogView extends BlockList {
2812
4609
  /** What the runner reported over the attempt, subagents included. */
2813
4610
  readonly usage?: PlannerUsage;
2814
4611
  /**
2815
- * The block each run of deltas streams into, keyed by stream, so the run's
2816
- * 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.
2817
4615
  */
2818
4616
  readonly streams: Readonly<Record<string, string>>;
2819
4617
  /** The turn's latest agent text block, which a failure that repeats it takes the place of. */
@@ -2980,6 +4778,40 @@ declare function markdownTokens(text: string): readonly MarkdownToken[];
2980
4778
  /** A model's text as HTML, safe to set as a webview element's innerHTML. */
2981
4779
  declare function markdownHtml(text: string): string;
2982
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
+
2983
4815
  /**
2984
4816
  * What a row's status means to a reader, before any surface picks a word,
2985
4817
  * icon or colour for it. `quiet` is a running task whose runner has printed
@@ -3225,9 +5057,10 @@ declare function titledRefs(ids: readonly string[], tasks: readonly Named[]): st
3225
5057
  * and a bare "Remove task?" would hide that.
3226
5058
  */
3227
5059
  declare function dependentsNotice(dependents: readonly {
5060
+ id?: string;
3228
5061
  order: number;
3229
5062
  title: string;
3230
- }[]): string | null;
5063
+ }[], tasks?: readonly Named[]): string | null;
3231
5064
  /**
3232
5065
  * What a start past a merge gate is asked with (ADR-0020): the work the task
3233
5066
  * would act without, named. `subject` is how the asking surface refers to the
@@ -3260,4 +5093,4 @@ declare function parseSwitch(arg: string): boolean | null;
3260
5093
  /** A whole number at least `min`, as typed; null for anything else. */
3261
5094
  declare function parseCount(arg: string | undefined, min: number): number | null;
3262
5095
 
3263
- export { type RunMergeView as $, ACT_RISK as A, type BlockingSubject as B, CHECKPOINT_TRUNCATE_LENGTH as C, type DiffRow as D, EMPTY_CONVERSATION as E, type MessageBlock as F, type GatedConversation as G, type HeldEdit as H, type INotification as I, type MessageRole as J, NO_TURN as K, type LiveTail as L, type MarkdownToken as M, NOT_STARTED as N, type NewTaskFields as O, type NotificationAction as P, type OrchestratorObserver as Q, type OutputPreview as R, type PlanBlock as S, type PlanMarkerStatus as T, type PlanSnapshot as U, PlanStore as V, type PlannerModes as W, type PromptHold as X, RUN_SETTINGS_DEFAULTS as Y, type RefTask as Z, type RunInput as _, AUTORUN_RISK as a, blockingFailures as a$, RunSettingsError as a0, type RunTask as a1, type RunTaskStatus as a2, type RunView as a3, type RunnerLoadedBlock as a4, STOPPED_BEFORE_START as a5, SUPERVISOR_LEVEL_LABEL as a6, type SerializedConversationMessage as a7, type SerializedPendingTurn as a8, type SerializedPlan as a9, type TaskRef as aA, type TaskRowAction as aB, type TaskRowContext as aC, type TaskRowIsolation as aD, type TaskRowKind as aE, type TaskRowPlacement as aF, type TaskRowSubject as aG, type TaskRowUsage as aH, type TaskRowView as aI, type TaskStatusKind as aJ, type ThinkingDisplayBlock as aK, type ToolBlock as aL, type ToolHeadline as aM, type ToolStatus as aN, type TurnGate as aO, type UsageBlock as aP, type UserSettings as aQ, WAITS_FOR_APPROVAL_LABEL as aR, aheadOfDraft as aS, applyTaskOps as aT, approvalLabel as aU, assignmentIssue as aV, assignmentIssueText as aW, automergeSummary as aX, autorunSummary as aY, awaitingLabel as aZ, blockedByLabel as a_, type SerializedTask as aa, type SerializedTaskStatus as ab, type SessionBroadcaster as ac, type SessionMessage as ad, type SessionNotice as ae, SettingsService as af, type SkillLoadBlock as ag, type SkillLookup as ah, type SkillToken as ai, type StartAdmission as aj, type SubagentBlock as ak, type SubagentChild as al, type SubagentStatus as am, type SurfacePlan as an, type SurfacePlanState as ao, type SurfaceTask as ap, type TakenPrompt as aq, type TaskAttemptSnapshot as ar, TaskControlError as as, type TaskLogEvent as at, type TaskLogView as au, type TaskOp as av, TaskOrchestrator as aw, type TaskOrchestratorDeps as ax, type TaskOrchestratorOptions as ay, type TaskOutputSource as az, type ApplyTaskOpsResult as b, taskStatusKind as b$, canMergeTasks as b0, canSetDependencies as b1, canSplitTask as b2, capConflictFiles as b3, classifyOutcome as b4, coalesceTaskLog as b5, dependencyCandidates as b6, dependentsNotice as b7, dependentsOf as b8, diffRows as b9, reduceConversation as bA, reduceRun as bB, reduceTaskLog as bC, replayTaskLog as bD, resolveTaskRef as bE, resumeLabel as bF, resumeTime as bG, runSettingsOf as bH, runnerToolSubject as bI, serializePlan as bJ, serializeTask as bK, serializeTaskStatus as bL, skillTokens as bM, skippedLabel as bN, stopTurn as bO, summarizeToolCall as bP, supervisorSummary as bQ, surfacePlan as bR, surfacePlanState as bS, takeAt as bT, taskLogForSurface as bU, taskOrderLabel as bV, taskRef as bW, taskRefOf as bX, taskRowActions as bY, taskRowView as bZ, taskStartedNotice as b_, diffStat as ba, diffSummary as bb, digestTaskLog as bc, drainNext as bd, executionSummary as be, followTurn as bf, fromTranscript as bg, getSettingsPath as bh, hasHiddenDetail as bi, holdPrompt as bj, isTaskRunning as bk, loadedSkillTokens as bl, markAction as bm, markRequestFor as bn, markdownHtml as bo, markdownTokens as bp, namedRepos as bq, newTaskFields as br, opsEditable as bs, outputLines as bt, outputPreview as bu, parseCount as bv, parseSwitch as bw, parseTaskOpsJson as bx, pastGateConfirmation as by, reconcileApprovals as bz, type ApprovalBlock as c, textHasTaskOps as c0, titledRefs as c1, titledTaskRef as c2, titledTaskRefOf as c3, toTaskLogEvent as c4, toolHeadline as c5, trimToolOutput as c6, truncateCheckpointSummary as c7, unsendAll as c8, unsendLatest as c9, type ApprovalStatus as d, type AssignmentIssue as e, type AssignmentScope as f, type AutoMergeTrigger as g, type CommandBlock as h, type CommandStatus as i, type CompactionBlock as j, type ConversationInput as k, type ConversationView as l, type DiffRowKind as m, type DiffStat as n, type DisplayBlock as o, EMPTY_HOLD as p, EMPTY_RUN as q, EMPTY_TASK_LOG as r, type EditCatalog as s, type ExecutionSummary as t, type GlobalRunSettings as u, type HeldEditMark as v, type IsolatedExecution as w, IsolationRunController as x, type LiveTailOptions as y, type LocalEntry 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 };