@ordewell/core 0.5.4 → 0.5.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 (56) hide show
  1. package/dist/IFileSystem-BkPX7mLD.d.mts +76 -0
  2. package/dist/IFileSystem-C0l-4MGT.d.ts +76 -0
  3. package/dist/{ModeResolver-Dkig8ghQ.d.ts → ModeResolver--16lh7dS.d.mts} +1 -1
  4. package/dist/{ModeResolver-DVJ7HV3k.d.mts → ModeResolver-CjpG5Wli.d.ts} +1 -1
  5. package/dist/Task-Dyxp67s2.d.mts +2086 -0
  6. package/dist/Task-Dyxp67s2.d.ts +2086 -0
  7. package/dist/{chunk-T2S5O36I.mjs → chunk-C44UWIAD.mjs} +6 -6
  8. package/dist/chunk-C44UWIAD.mjs.map +1 -0
  9. package/dist/{chunk-JVMDEHRQ.mjs → chunk-EDGUFCIR.mjs} +77 -37
  10. package/dist/chunk-EDGUFCIR.mjs.map +1 -0
  11. package/dist/{chunk-XWOUIA6A.mjs → chunk-JBEFAJ2W.mjs} +2 -2
  12. package/dist/chunk-ROVYWEBI.mjs +2115 -0
  13. package/dist/chunk-ROVYWEBI.mjs.map +1 -0
  14. package/dist/index.d.mts +1647 -758
  15. package/dist/index.d.ts +1647 -758
  16. package/dist/index.js +9301 -4857
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +6222 -2968
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/order-labels.d.mts +3 -1
  21. package/dist/order-labels.d.ts +3 -1
  22. package/dist/{parsing-DRp4dPC0.d.mts → parsing-DPEpAszP.d.mts} +11 -3
  23. package/dist/{parsing-CF_grC29.d.ts → parsing-EcmCsF1y.d.ts} +11 -3
  24. package/dist/parsing.d.mts +5 -3
  25. package/dist/parsing.d.ts +5 -3
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +2 -2
  28. package/dist/plan-utils-BAvW3hvl.d.mts +877 -0
  29. package/dist/plan-utils-BMyEiDKv.d.ts +877 -0
  30. package/dist/plan-utils.d.mts +5 -4
  31. package/dist/plan-utils.d.ts +5 -4
  32. package/dist/plan-utils.js +956 -8
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +47 -7
  35. package/dist/testing.d.mts +57 -5
  36. package/dist/testing.d.ts +57 -5
  37. package/dist/testing.js +96 -2
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +94 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +2 -1
  42. package/skills/grilling/SKILL.md +6 -16
  43. package/skills/improve-codebase-architecture/SKILL.md +1 -1
  44. package/dist/ApprovalPolicy-BVhGdECT.d.mts +0 -79
  45. package/dist/ApprovalPolicy-BVhGdECT.d.ts +0 -79
  46. package/dist/ITerminalRunner-Bd-vAJnw.d.ts +0 -561
  47. package/dist/ITerminalRunner-ByeoLF57.d.mts +0 -561
  48. package/dist/Task-BxQkPlXO.d.mts +0 -664
  49. package/dist/Task-BxQkPlXO.d.ts +0 -664
  50. package/dist/chunk-GWPIYDQW.mjs +0 -945
  51. package/dist/chunk-GWPIYDQW.mjs.map +0 -1
  52. package/dist/chunk-JVMDEHRQ.mjs.map +0 -1
  53. package/dist/chunk-T2S5O36I.mjs.map +0 -1
  54. package/dist/plan-utils-CkNbqAmS.d.ts +0 -329
  55. package/dist/plan-utils-CtB3_Ovf.d.mts +0 -329
  56. /package/dist/{chunk-XWOUIA6A.mjs.map → chunk-JBEFAJ2W.mjs.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,18 +1,16 @@
1
- import { F as RunnerId, u as PlanStatus, L as LegacyPlanState, k as IsolationMergeResult, g as IsolationLandedTask, I as IWorktreeIsolation, w as RepoGroupLayout, a as DiscoveredModel, T as Task, y as ResearchProgress, x as ResearchLogEntry, C as ConversationMessage, V as TaskSnapshot, d as IsolationHandoff, J as TaskIsolation, r as IsolationView, P as PlanIsolation, Q as QueuedMessage, A as ActiveTaskSession, E as ResearchToolType, t as PlanState, a3 as Verdict } from './Task-BxQkPlXO.js';
2
- export { D as DiscoveredMode, b as IntegrationDisposal, c as IsolationAvailability, e as IsolationHandoffRepo, f as IsolationInactiveReason, h as IsolationLanding, i as IsolationMergeBlock, j as IsolationMergeBlockReason, l as IsolationOutcome, m as IsolationRepo, n as IsolationRun, o as IsolationTaskRecord, p as IsolationTaskRepo, q as IsolationTaskStatus, M as Message, s as PlanModificationWarnings, v as PreparedTask, R as RepairEvidence, z as ResearchStep, B as ResearchStepOutcome, S as StreamEvent, G as StreamStepEvent, H as StreamThinkingEvent, K as TaskIsolationState, N as TaskMode, O as TaskModelAssignment, U as TaskOutputSummary, W as TaskStatus, X as TaskType, Y as TaskWithParent, Z as ThinkingBlock, _ as UserPromptEntry, $ as UserStep, a0 as ValidationCheck, a1 as ValidationContext, a2 as ValidationResult, a4 as VerificationCheck, a5 as addTaskToPlan, a6 as createEmptyPlan, a7 as createTask, a8 as emptyWarnings, a9 as flattenTasks, aa as flattenTasksWithParents, ab as migrateLegacyPlan, ac as migratePlanState, ad as migrateTask, ae as removeTaskFromPlan, af as renumberTasks, ag as updateTaskInPlan, ah as validateModifiedPlan, ai as warningsText } from './Task-BxQkPlXO.js';
3
- import { f as IFileSystem, R as ReadFileOpts, T as ToolOutcome, d as GrepOptions, c as GlobOptions, b as FindSymbolOptions, I as IConfig, A as AiProvider, r as ProviderModelLists, i as ITerminalSession, h as ITerminalRunner, w as RunnerRegistry, k as OrchestratorOption, u as RunnerInvocation, g as IPluginStore, v as RunnerPluginManifest, t as ResolveContext } from './ITerminalRunner-Bd-vAJnw.js';
4
- export { a as AllProviderModels, B as BlockingPrompt, C as CatalogModel, D as DiscoveryCommand, F as FetchAllProviderModelsOptions, G as GREP_DEFAULT_HEAD_LIMIT, e as GrepOutputMode, M as ModelCatalog, j as ModelShortcut, O as ORCHESTRATOR_SHORTCUTS, P as PluginCloneFn, l as PluginEntry, m as PluginFeatures, n as PluginMode, o as PluginModelDiscovery, p as PluginRunnerDef, q as ProviderCredentialSource, s as ProviderModelsResult, S as SEARCH_EXCLUSIONS, x as collectProviderCredentials, y as enabledRunners, z as fetchAllProviderModels, E as isReservedRunnerName, H as knownModelId, J as resolveModelShortcut, K as resolveProvider, L as toOrchestratorOptions } from './ITerminalRunner-Bd-vAJnw.js';
5
- import { I as IApproval, d as ApprovalRequest, a as ApprovalMode } from './ApprovalPolicy-BVhGdECT.js';
6
- export { A as ApprovalKind, b as ApprovalPolicy, c as ApprovalPolicyOptions, e as ApprovalSource, D as DENY_ALL } from './ApprovalPolicy-BVhGdECT.js';
7
- import { T as TaskOp, f as SessionBroadcaster, h as SessionNotice } from './plan-utils-CkNbqAmS.js';
8
- export { A as Adr0013IsolationRun, a as Adr0013PlanIsolation, b as Adr0013TaskRecord, c as ApplyTaskOpsResult, C as CHECKPOINT_TRUNCATE_LENGTH, S as SerializedPlan, d as SerializedTask, e as SerializedTaskStatus, g as SessionMessage, i as TaskRef, j as applyTaskOps, k as canMergeTasks, l as canSetDependencies, m as canSplitTask, n as capConflictFiles, o as classifyOutcome, p as dependencyCandidates, q as dependentsOf, r as executionSummary, s as migratePlanIsolation, t as parseTaskOpsJson, u as serializePlan, v as serializeTask, w as serializeTaskStatus, x as summarizeToolCall, y as textHasTaskOps, z as truncateCheckpointSummary } from './plan-utils-CkNbqAmS.js';
9
- import { R as RunnerModeInfo } from './ModeResolver-Dkig8ghQ.js';
10
- export { M as ManifestLookup, b as buildModeGuide, f as filteredBuildModes, r as resolveDefaultMode, a as resolveTaskMode, c as runnerModesFrom } from './ModeResolver-Dkig8ghQ.js';
1
+ import { aG as RunnerId, ag as PlanStatus, a6 as LegacyPlanState, I as IApproval, k as ApprovalDecision, p as ApprovalRequest, j as ApprovalAnswer, L as ITerminalRunner, J as IConfig, h as AiProvider, as as ProviderModelLists, m as ApprovalMode, X as IsolationMergeResult, T as IsolationLandedTask, N as IWorktreeIsolation, a2 as IsolationTaskStatus, ad as PlanIsolation, aL as RunnerTransport, az as RepoGroupLayout, M as ITerminalSession, w as DiscoveredModel, aU as Task, f as AgentProcessDeps, c as AgentAdapter, aB as ResearchProgress, aA as ResearchLogEntry, u as ConversationMessage, b9 as UsageRecord, b1 as TaskSnapshot, r as AwaitingReason, Q as IsolationHandoff, $ as IsolationRun, a0 as IsolationTaskRecord, aV as TaskIsolation, a3 as IsolationView, Y as IsolationOutcome, ay as RepairEvidence, bg as Verdict, aJ as RunnerRegistry, ax as QueuedTaskMessage, aw as QueuedMessage, b as ActiveTaskSession, ac as OrchestratorOption, aY as TaskModeAgentAdapter, g as AgentStartOptions, e as AgentEvent, aE as ResearchToolType, ai as PlannerUsage, n as ApprovalPolicy, af as PlanState, aH as RunnerInvocation, A as AbstractRunner, a as AbstractTerminalSession, aQ as StructuredSessionCapability, b2 as TaskStartOptions, aR as StructuredTurnEnd, aP as StructuredEvent, aK as RunnerSpawnOptions, K as IPluginStore, aI as RunnerPluginManifest, aF as ResolveContext } from './Task-Dyxp67s2.js';
2
+ export { d as AgentAdapterFactory, i as AllProviderModels, l as ApprovalKind, o as ApprovalPolicyOptions, q as ApprovalSource, B as BlockingPrompt, C as CMD_EXE_MAX_COMMAND_LINE, s as CatalogModel, t as CommandLineTooLongError, D as DENY_ALL, v as DiscoveredMode, x as DiscoveryCommand, E as EmbeddedNewlineError, y as ExecutableNotFoundError, F as FetchAllProviderModelsOptions, H as HeadlessRunner, z as HeadlessRunnerDeps, G as HeadlessSession, O as IntegrationDisposal, P as IsolationAvailability, R as IsolationHandoffRepo, S as IsolationInactiveReason, U as IsolationLanding, V as IsolationMergeBlock, W as IsolationMergeBlockReason, Z as IsolationPruneResult, _ as IsolationRepo, a1 as IsolationTaskRepo, a4 as LaunchDeps, a5 as LaunchPlan, a7 as LineBuffer, a8 as Message, a9 as ModelCatalog, aa as ModelShortcut, ab as ORCHESTRATOR_SHORTCUTS, ae as PlanModificationWarnings, ah as PlannerStartOptions, aj as PluginCloneFn, ak as PluginEntry, al as PluginFeatures, am as PluginMode, an as PluginModelDiscovery, ao as PluginRunnerDef, ap as PreparedLaunch, aq as PreparedTask, ar as ProviderCredentialSource, at as ProviderModelsResult, au as PtySize, av as PtyWrapOptions, aC as ResearchStep, aD as ResearchStepOutcome, aM as StreamEvent, aN as StreamStepEvent, aO as StreamThinkingEvent, aS as SubagentLogEntry, aT as SubagentOutcome, aW as TaskIsolationState, aX as TaskMode, aZ as TaskModeUnsupportedError, a_ as TaskModelAssignment, a$ as TaskOutputSummary, b0 as TaskRunnerFlags, b3 as TaskStatus, b4 as TaskTransport, b5 as TaskType, b6 as TaskWithParent, b7 as ThinkingBlock, b8 as UsageLine, ba as UsageTotals, bb as UserPromptEntry, bc as UserStep, bd as ValidationCheck, be as ValidationContext, bf as ValidationResult, bh as VerificationCheck, bi as WINDOWS_MAX_COMMAND_LINE, bj as addPlannerUsage, bk as addTaskToPlan, bl as addUsage, bm as buildShellInvocation, bn as collectProviderCredentials, bo as createEmptyPlan, bp as createTask, bq as emptyWarnings, br as enabledRunners, bs as fetchAllProviderModels, bt as flattenTasks, bu as flattenTasksWithParents, bv as isAwaitingReason, bw as isExecutableResolved, bx as isGranted, by as isMeasured, bz as isReservedRunnerName, bA as isRunnerApproval, bB as isRunnerTransport, bC as isStructuredSession, bD as keepExecutionState, bE as knownModelId, bF as migrateLegacyPlan, bG as migratePlanState, bH as migrateTask, bI as partedPromptUsage, bJ as planDirectLaunch, bK as planShellLaunch, bL as plannerContextFill, bM as posixShellQuote, bN as removeTaskFromPlan, bO as renumberTasks, bP as resolveModelShortcut, bQ as resolveProvider, bR as stripAnsi, bS as toApprovalDecision, bT as toOrchestratorOptions, bU as updateTaskInPlan, bV as usageLine, bW as validateModifiedPlan, bX as warningsText, bY as windowsCommandLine, bZ as wrapWithPty } from './Task-Dyxp67s2.js';
3
+ import { t as TaskOp, l as SessionBroadcaster, A as ApplyTaskOpsResult, m as SessionMessage, n as SessionNotice, r as TaskLogEvent } from './plan-utils-BMyEiDKv.js';
4
+ export { a as ApprovalBlock, b as ApprovalStatus, C as CHECKPOINT_TRUNCATE_LENGTH, c as ConversationInput, d as ConversationView, D as DisplayBlock, E as EMPTY_CONVERSATION, e as EMPTY_HOLD, f as EMPTY_TASK_LOG, G as GatedConversation, L as LocalEntry, M as MessageBlock, g as MessageRole, N as NO_TURN, O as OutputPreview, P as PlanBlock, h as PlanMarkerStatus, i as PromptHold, S as SerializedPlan, j as SerializedTask, k as SerializedTaskStatus, o as SubagentBlock, p as SubagentChild, q as SubagentStatus, T as TakenPrompt, s as TaskLogView, u as TaskRef, v as ThinkingDisplayBlock, w as ToolBlock, x as ToolHeadline, y as ToolStatus, z as TurnGate, U as UsageBlock, B as aheadOfDraft, F as applyTaskOps, H as canMergeTasks, I as canSetDependencies, J as canSplitTask, K as capConflictFiles, Q as classifyOutcome, R as coalesceTaskLog, V as dependencyCandidates, W as dependentsOf, X as drainNext, Y as executionSummary, Z as followTurn, _ as fromTranscript, $ as hasHiddenDetail, a0 as holdPrompt, a1 as outputLines, a2 as outputPreview, a3 as parseTaskOpsJson, a4 as reduceConversation, a5 as reduceTaskLog, a6 as replayTaskLog, a7 as runnerToolSubject, a8 as serializePlan, a9 as serializeTask, aa as serializeTaskStatus, ab as stopTurn, ac as summarizeToolCall, ad as taskStartedNotice, ae as textHasTaskOps, af as toTaskLogEvent, ag as toolHeadline, ah as trimToolOutput, ai as truncateCheckpointSummary, aj as unsendAll, ak as unsendLatest } from './plan-utils-BMyEiDKv.js';
5
+ import { I as IFileSystem, R as ReadFileOpts, T as ToolOutcome, b as GrepOptions, a as GlobOptions, F as FindSymbolOptions } from './IFileSystem-C0l-4MGT.js';
6
+ export { G as GREP_DEFAULT_HEAD_LIMIT, c as GrepOutputMode, S as SEARCH_EXCLUSIONS } from './IFileSystem-C0l-4MGT.js';
7
+ import { R as RunnerModeInfo } from './ModeResolver-CjpG5Wli.js';
8
+ export { M as ManifestLookup, b as buildModeGuide, f as filteredBuildModes, r as resolveDefaultMode, a as resolveTaskMode, c as runnerModesFrom } from './ModeResolver-CjpG5Wli.js';
11
9
  import { ChildProcess } from 'child_process';
12
- import { EventEmitter } from 'events';
13
- import { b as PlanParseError } from './parsing-CF_grC29.js';
14
- export { P as PLAN_ENVELOPE_KEY, a as PartialPlanTask, T as TASK_OPS_ENVELOPE_KEY, c as TASK_QUERY_ENVELOPE_KEY, d as checkDepsResolve, e as checkImmutableLog, f as checkInProgress, g as checkNoCycles, h as checkUniqueIds, i as escapeControlCharsInStrings, j as extractJsonObject, k as extractObjectWithBalance, l as extractObjectsWithKey, m as looksLikePlanAttempt, p as parsePartialPlan, n as parsePlanJson, s as stripModelNoise, o as stripTrailingCommas, v as validatePlanModification } from './parsing-CF_grC29.js';
10
+ import { b as PlanParseError } from './parsing-EcmCsF1y.js';
11
+ export { P as PLAN_ENVELOPE_KEY, a as PartialPlanTask, T as TASK_OPS_ENVELOPE_KEY, c as TASK_QUERY_ENVELOPE_KEY, d as checkDepsResolve, e as checkImmutableLog, f as checkInProgress, g as checkNoCycles, h as checkUniqueIds, i as escapeControlCharsInStrings, j as extractJsonObject, k as extractObjectWithBalance, l as extractObjectsWithKey, m as looksLikePlanAttempt, o as opensWithJsonObject, p as parsePartialPlan, n as parsePlanJson, s as stripModelNoise, q as stripTrailingCommas, v as validatePlanModification } from './parsing-EcmCsF1y.js';
15
12
  export { resolveOrderLabel, taskOrderLabel } from './order-labels.js';
13
+ import 'events';
16
14
 
17
15
  interface SessionMeta {
18
16
  id: string;
@@ -128,7 +126,10 @@ declare function researchShellWarning(shell: ResearchShell): string | null;
128
126
  *
129
127
  * A segment is classified by the command that will actually execute, not by the
130
128
  * name at the front of it: wrappers are unwrapped first, recursively, so
131
- * `timeout 10 env nice rm -rf x` is an `rm`. See {@link WRAPPER_FAMILY}.
129
+ * `timeout 10 env nice rm -rf x` is an `rm`. See {@link WRAPPER_FAMILY}. A
130
+ * runner that feeds its command arguments nobody can see is unwrapped the same
131
+ * way but never runs unprompted ({@link XARGS_TARGETS}), and one that hands its
132
+ * command to a shell is refused ({@link REFUSED_RUNNERS}).
132
133
  *
133
134
  * A permitted binary is permitted with the flags it is known to be read-only
134
135
  * with, not with any flag at all: several of them will run a helper program or
@@ -323,7 +324,9 @@ declare function grantScopeFor(abs: string, kind: 'file' | 'directory'): string;
323
324
  * Timeouts are load-bearing rather than defensive: a planner turn that blocks
324
325
  * forever on an unanswered prompt would hang the whole research loop with no
325
326
  * visible cause. On expiry the request resolves to denied and the model gets a
326
- * normal, actionable tool result.
327
+ * normal, actionable tool result. A task runner's request is the exception
328
+ * (ADR-0018, A1): the runner is parked on it rather than a research loop, and
329
+ * it waits for a person — or the supervisor — however long that takes.
327
330
  */
328
331
  interface PendingApproval {
329
332
  id: string;
@@ -336,20 +339,70 @@ interface PendingApprovalsOptions {
336
339
  /** Announce a new request to the surfaces. */
337
340
  onRequest?: (pending: PendingApproval) => void;
338
341
  /** Announce that a request is no longer actionable (answered or expired). */
339
- onSettled?: (id: string, granted: boolean) => void;
342
+ onSettled?: (id: string, granted: boolean, settled: {
343
+ request: ApprovalRequest;
344
+ decision: ApprovalDecision;
345
+ }) => void;
346
+ }
347
+ interface AskOptions {
348
+ /**
349
+ * An id the caller already announced the request under. One still in use is
350
+ * refused — denied at once — rather than overwriting the request that has it.
351
+ */
352
+ id?: string;
353
+ /** Wait for an answer however long it takes. */
354
+ noTimeout?: boolean;
355
+ /**
356
+ * Given the answer as the request settles, before anything awaiting the
357
+ * promise runs: a caller about to tear down what asked (a runner being
358
+ * stopped) must deliver the answer before it goes.
359
+ */
360
+ onDecision?: (decision: ApprovalDecision) => void;
340
361
  }
341
362
  declare class PendingApprovals {
342
363
  private readonly opts;
343
364
  private readonly entries;
344
365
  constructor(opts?: PendingApprovalsOptions);
345
366
  /** Park a request and return the promise the approval policy awaits. */
346
- ask(request: ApprovalRequest): Promise<boolean>;
347
- /** Answer one request. Returns false when the id is unknown or already settled. */
348
- resolve(id: string, granted: boolean): boolean;
367
+ ask(request: ApprovalRequest, options?: AskOptions): Promise<boolean>;
368
+ /** Park a request whose answer is more than yes or no. */
369
+ decide(request: ApprovalRequest, options?: AskOptions): Promise<ApprovalDecision>;
370
+ /**
371
+ * Answer one request. Returns false when the id is unknown or already
372
+ * settled. "Allow for this task" on a request that did not offer it is a
373
+ * plain allow: no grant is made that the requester never proposed.
374
+ */
375
+ resolve(id: string, answer: ApprovalAnswer): boolean;
349
376
  /** Everything still awaiting an answer — replayed to a surface that connects late. */
350
377
  outstanding(): PendingApproval[];
351
- /** Deny everything in flight. Called on abort and on session reset. */
352
- clear(): void;
378
+ /**
379
+ * Deny everything in flight, or only the requests `which` picks. Called on
380
+ * abort and on session reset.
381
+ */
382
+ clear(which?: (request: ApprovalRequest) => boolean, note?: string): void;
383
+ }
384
+
385
+ /**
386
+ * Carries a structured task's tool requests to the session's one approval
387
+ * seam (ADR-0018, A1), so a runner's request is answered through
388
+ * `resolveApproval` like any other — by a person on any surface, or by the
389
+ * supervisor (#28), with nothing here assuming which.
390
+ *
391
+ * Like the task log, it sits around the runner rather than inside the
392
+ * orchestrator: every way an attempt ends reaches the runner as a `stop`, and
393
+ * that is where a task's open requests are denied, before the session goes,
394
+ * so nothing is left waiting on a process that no longer exists.
395
+ */
396
+ declare class RunnerApprovals {
397
+ private readonly approvals;
398
+ /** The open request ids of each live structured session. */
399
+ private readonly open;
400
+ constructor(approvals: PendingApprovals);
401
+ wrap(runner: ITerminalRunner): ITerminalRunner;
402
+ /** How many of a task's runner requests wait for an answer — what "waiting for approval" is derived from. */
403
+ waiting(taskId: string): number;
404
+ private watch;
405
+ private denySession;
353
406
  }
354
407
 
355
408
  /**
@@ -587,6 +640,37 @@ interface WorktreeIsolationDeps {
587
640
  }
588
641
  declare function createWorktreeIsolation(deps: WorktreeIsolationDeps): IWorktreeIsolation;
589
642
 
643
+ /** A task record as ADR-0013 persisted it, for one repository. */
644
+ interface Adr0013TaskRecord {
645
+ taskId: string;
646
+ order: number;
647
+ title: string;
648
+ branch: string;
649
+ worktree: string;
650
+ status: IsolationTaskStatus;
651
+ linked: string[];
652
+ }
653
+ /** A run as ADR-0013 persisted it (0.4.23): one repository, its refs on the run itself. */
654
+ interface Adr0013IsolationRun {
655
+ id: string;
656
+ workspaceRoot: string;
657
+ baseRef: string;
658
+ baseBranch?: string;
659
+ integrationBranch: string;
660
+ tasks: Record<string, Adr0013TaskRecord>;
661
+ }
662
+ interface Adr0013PlanIsolation {
663
+ run: Adr0013IsolationRun;
664
+ resolvers: Record<string, string>;
665
+ }
666
+ /**
667
+ * A persisted plan isolation in today's shape. A run saved in the ADR-0013
668
+ * format — recognised by its own `integrationBranch` — becomes a group of one
669
+ * at `.`, keeping its branches, so a session saved by 0.4.23 resumes and hands
670
+ * off exactly as it would have.
671
+ */
672
+ declare function migratePlanIsolation(state: PlanIsolation | Adr0013PlanIsolation): PlanIsolation;
673
+
590
674
  interface ILogger {
591
675
  warn(scope: string, message: string, err?: unknown): void;
592
676
  }
@@ -613,6 +697,11 @@ interface UserSettings {
613
697
  * back to the environment's defaults; `[]` is a deliberate "none of them".
614
698
  */
615
699
  enabledRunners?: string[];
700
+ /**
701
+ * How tasks' runners are driven (ADR-0018). Experimental; a run copies it
702
+ * onto its plan when it starts, so a change applies from the next run.
703
+ */
704
+ runnerTransport: RunnerTransport;
616
705
  }
617
706
  /**
618
707
  * Where the user's toggles live. `ORDEWELL_SETTINGS_PATH` overrides it so several
@@ -636,6 +725,8 @@ declare class SettingsService {
636
725
  setTdd(enabled: boolean): void;
637
726
  getVerification(): boolean;
638
727
  setVerification(enabled: boolean): void;
728
+ getRunnerTransport(): RunnerTransport;
729
+ setRunnerTransport(transport: RunnerTransport): void;
639
730
  getModelAllowlist(runner: string): string[] | undefined;
640
731
  setModelAllowlist(runner: string, ids: string[] | undefined): void;
641
732
  getPlannerModel(provider: string): {
@@ -812,449 +903,7 @@ type LiveOutputLookup = (taskId: string, opts: LiveTailOptions) => LiveTail | nu
812
903
  * state on every query rather than cached. `liveOutput` backs the `output`
813
904
  * field; omitting it reads as "nothing captured".
814
905
  */
815
- declare function renderTaskQueryAnswer(query: TaskQuery, tasks: Task[], catalog: TaskQueryCatalog, liveOutput?: LiveOutputLookup): string;
816
-
817
- declare abstract class AbstractTerminalSession implements ITerminalSession {
818
- id: string;
819
- taskId: string;
820
- protected exited: boolean;
821
- protected outputEmitter: EventEmitter<any>;
822
- protected exitEmitter: EventEmitter<any>;
823
- constructor(id: string, taskId: string);
824
- protected baseHandleExit(code: number): void;
825
- onOutput(callback: (text: string) => void): void;
826
- onExit(callback: (code: number) => void): void;
827
- abstract kill(): void;
828
- abstract getOutput(): string;
829
- abstract write(text: string): void;
830
- }
831
- declare abstract class AbstractRunner<S extends ITerminalSession> implements ITerminalRunner {
832
- protected sessions: Map<string, S>;
833
- get activeCount(): number;
834
- stop(sessionId: string): void;
835
- stopAll(): void;
836
- protected registerSession(id: string, session: S): void;
837
- abstract spawn(opts: {
838
- taskId: string;
839
- runner: string;
840
- prompt: string;
841
- modelId?: string;
842
- thinkingEffort?: string;
843
- mode?: string;
844
- headless?: boolean;
845
- cwd: string;
846
- registry?: RunnerRegistry;
847
- }): Promise<ITerminalSession>;
848
- }
849
-
850
- declare function stripAnsi(text: string): string;
851
- declare function posixShellQuote(s: string): string;
852
- /**
853
- * Wrap a command for execution inside a POSIX login shell, which is what
854
- * resolves runner binaries managed by nvm/volta/asdf.
855
- *
856
- * Deliberately POSIX-only. This used to take a `platform` and emit
857
- * `powershell.exe -Command "'claude' '-p' '…'"` for Windows, which PowerShell
858
- * cannot run at all: a quoted string in leading position is parsed in
859
- * expression mode, so the invocation died with a parse error before the runner
860
- * started — a task that failed instantly, every time, on that platform. Windows
861
- * has no login-shell equivalent to emulate (its PATH comes from the registry
862
- * and is already inherited), so {@link planShellLaunch} starts the runner
863
- * directly there instead of routing it through a shell. Platform choice belongs
864
- * to that function; this one only knows how to phrase the POSIX half.
865
- */
866
- declare function buildShellInvocation(command: string, args: string[]): {
867
- shellPath: string;
868
- shellArgs: string[];
869
- };
870
- /** A terminal size in cells. */
871
- interface PtySize {
872
- cols: number;
873
- rows: number;
874
- }
875
- /**
876
- * Options for {@link wrapWithPty}. `size` sets the PTY's window size before the
877
- * wrapped command starts. `controlChannel` makes the wrapper listen on its fd 3
878
- * for `"<cols> <rows>"` lines and resize the PTY live — the caller spawns the
879
- * child with an extra pipe there and writes resize requests into it.
880
- */
881
- interface PtyWrapOptions {
882
- size?: PtySize;
883
- controlChannel?: boolean;
884
- }
885
- /**
886
- * Wrap a command in `script` to allocate the PTY some runners require when
887
- * headless; `-e` propagates the child's exit code so verification still works.
888
- *
889
- * POSIX-only by nature — there is no `script` on Windows, which
890
- * `HeadlessRunner`'s `hasScriptCmd` probe already discovers, so this is never
891
- * reached there.
892
- *
893
- * `script` sizes its PTY off the terminal it is attached to; spawned off a pipe
894
- * (every transport here) it allocates 0x0, which a runner TUI renders as
895
- * garbage. `stty` fixes the size on the PTY slave before the command starts, so
896
- * the TUI reads its true dimensions via ioctl.
897
- *
898
- * The control channel can only live on a *separate* fd from the agent's stdin,
899
- * so the wrapper saves the PTY on fd 4 first: POSIX sends an asynchronous
900
- * command's stdin to `/dev/null`, so the watcher's own stdin cannot be the PTY.
901
- * A background job also inherits an fd 0 that is not the terminal; `stty` names
902
- * fd 4 explicitly for that reason.
903
- */
904
- declare function wrapWithPty(command: string, args: string[], opts?: PtyWrapOptions): {
905
- command: string;
906
- args: string[];
907
- };
908
-
909
- /**
910
- * cmd.exe's command-line buffer. A longer line is truncated rather than
911
- * rejected, which would corrupt a planner's system prompt or a task's prompt
912
- * mid-sentence and produce a confident answer to half a question — so the
913
- * batch route refuses instead. See {@link CommandLineTooLongError}.
914
- */
915
- declare const CMD_EXE_MAX_COMMAND_LINE = 8191;
916
- /**
917
- * CreateProcess's own ceiling, which the native and PowerShell routes are
918
- * bounded by instead. Windows truncates here too, so the same refusal applies —
919
- * it is simply four times further away.
920
- */
921
- declare const WINDOWS_MAX_COMMAND_LINE = 32767;
922
- interface LaunchPlan {
923
- /** The executable handed to `spawn()` or `vscode.window.createTerminal`. */
924
- file: string;
925
- /** Arguments for `file`. Pass verbatim when {@link verbatim} is set. */
926
- args: string[];
927
- /**
928
- * Windows batch route only: `args` is already a quoted command line and must
929
- * not be re-quoted. Maps to `windowsVerbatimArguments` for `spawn`, and to
930
- * the string form of `shellArgs` for a VS Code terminal.
931
- */
932
- verbatim?: boolean;
933
- }
934
- /** Test seam: every OS touchpoint is injectable, and production uses the defaults. */
935
- interface LaunchDeps {
936
- platform?: NodeJS.Platform;
937
- /** The PATH executables are looked up on. Defaults to the augmented PATH. */
938
- resolvePath?: () => Promise<string>;
939
- /** True when `candidate` names an existing file. */
940
- exists?: (candidate: string) => boolean;
941
- /** Absolute path to the Windows command interpreter. */
942
- comSpec?: () => string;
943
- /** Absolute path to Windows PowerShell. */
944
- powerShell?: () => string;
945
- /** PATHEXT, as the environment reports it. */
946
- pathExt?: () => string;
947
- }
948
- /**
949
- * Thrown when a command's arguments do not fit the buffer of the only
950
- * interpreter that can start it. Windows truncates rather than rejecting, and a
951
- * system prompt cut off mid-sentence makes the planner answer half a question
952
- * confidently — the silent success this repo refuses — so this is raised
953
- * instead. `TaskOrchestrator.startTask` catches it and holds the task, so the
954
- * message is what the user reads: it names the fix, because they cannot infer
955
- * it from a truncated prompt.
956
- */
957
- declare class CommandLineTooLongError extends Error {
958
- readonly command: string;
959
- readonly length: number;
960
- readonly limit: number;
961
- constructor(command: string, length: number, limit?: number);
962
- }
963
- /**
964
- * Thrown when only a batch shim resolved for a multi-line argument. cmd.exe
965
- * reads up to the first CR/LF and discards the rest with no error and exit code
966
- * 0 — quoting does not help — so the agent would get the first paragraph of its
967
- * prompt without the completion marker instruction, then exit looking successful.
968
- */
969
- declare class EmbeddedNewlineError extends Error {
970
- readonly command: string;
971
- constructor(command: string);
972
- }
973
- /**
974
- * Thrown when `command` cannot be resolved to a real file. Kept distinguishable
975
- * from `WorkspaceNotFoundError` (utils/workspace) even though both a missing
976
- * cwd and a missing binary surface as the same `spawn` ENOENT to Node — the two
977
- * are checked, and named, separately so the failure names the actual cause.
978
- */
979
- declare class ExecutableNotFoundError extends Error {
980
- readonly command: string;
981
- readonly searchedPath: string;
982
- constructor(command: string, searchedPath: string);
983
- }
984
- /**
985
- * Whether `command` actually resolves to a file, given the plan
986
- * {@link planDirectLaunch} produced for it and the PATH it was resolved
987
- * against.
988
- *
989
- * POSIX is deliberately identity in `planDirectLaunch` (execvp does its own
990
- * PATH search), so the search is repeated here instead. Windows already did
991
- * the search inside `planDirectLaunch` — signalled by the returned file
992
- * differing from the bare command name it was given; an unresolved command
993
- * comes back unchanged.
994
- */
995
- declare function isExecutableResolved(command: string, plan: LaunchPlan, PATH: string, deps?: LaunchDeps): boolean;
996
- /** The verbatim command line cmd.exe receives after `/d /s /c`, before wrapping. */
997
- declare function windowsCommandLine(file: string, args: string[]): string;
998
- /**
999
- * How to start `command` with `args` through `spawn()`, with no shell.
1000
- *
1001
- * POSIX returns its input unchanged — execvp already searches PATH, and adding
1002
- * a resolution step there would be a new way for a working setup to break.
1003
- *
1004
- * Windows resolves the command against PATH × PATHEXT, preferring a native
1005
- * executable (spawned directly) over a batch shim (through cmd.exe) over a
1006
- * PowerShell script shim (through `powershell.exe -File`). A command that
1007
- * resolves to nothing is returned unchanged, so the caller's existing ENOENT —
1008
- * which names what the user typed — is what surfaces rather than a second,
1009
- * vaguer error from here.
1010
- *
1011
- * The tiers are tried in preference order and the first that *fits* wins, with
1012
- * one deliberate exception: an overflowing batch shim does not fall through to
1013
- * PowerShell. Overflow means a very large prompt, which is precisely where
1014
- * `-File` argument fidelity is least worth betting on, and where a clear held
1015
- * task beats a plausibly-mangled one. So capacity does not reorder the tiers —
1016
- * a `.ps1` beside a too-long `.cmd` still raises.
1017
- *
1018
- * A line break does reorder them: cmd.exe cannot carry one at any length, so a
1019
- * `.ps1` beside a `.cmd` wins, and a lone `.cmd` raises.
1020
- *
1021
- * @throws {CommandLineTooLongError} when the selected route's buffer cannot
1022
- * carry the arguments.
1023
- * @throws {EmbeddedNewlineError} when the arguments span lines and only the
1024
- * batch route resolved.
1025
- */
1026
- declare function planDirectLaunch(command: string, args: string[], deps?: LaunchDeps): Promise<LaunchPlan>;
1027
- /**
1028
- * How to start `command` for a surface that hands an executable and arguments
1029
- * to a terminal — the VS Code runner today, a Windows TUI later.
1030
- *
1031
- * On POSIX this is the login shell, unchanged: `bash -lc` runs the user's
1032
- * profile, which is how nvm/volta/asdf-managed runner binaries resolve at all.
1033
- * Windows has no login-shell equivalent (its PATH comes from the registry and
1034
- * is already inherited), so it takes the direct route instead. That is not just
1035
- * a simplification: it means the runner's own exit code is the terminal's exit
1036
- * code, rather than a `$LASTEXITCODE` that PowerShell propagates unreliably —
1037
- * and the exit code is half of what {@link VerdictEngine} judges a task on.
1038
- */
1039
- declare function planShellLaunch(command: string, args: string[], deps?: LaunchDeps): Promise<LaunchPlan>;
1040
-
1041
- type SpawnFn = (command: string, args: string[], options: {
1042
- env: NodeJS.ProcessEnv;
1043
- stdio: Array<'pipe' | 'ignore'>;
1044
- cwd: string;
1045
- /** Set by the Windows batch route, where `args` is already a quoted command line. */
1046
- windowsVerbatimArguments?: boolean;
1047
- }) => ChildProcess;
1048
- /** Test seam: every OS touchpoint is injectable; production uses the defaults. */
1049
- interface HeadlessRunnerDeps {
1050
- spawnImpl?: SpawnFn;
1051
- hasScriptCmd?: () => boolean;
1052
- resolvePath?: () => Promise<string>;
1053
- /**
1054
- * Overrides for executable resolution ({@link planDirectLaunch}). Only the
1055
- * Windows branch consults them, so a POSIX test never needs to pass anything.
1056
- */
1057
- launchDeps?: LaunchDeps;
1058
- }
1059
- declare class HeadlessSession extends AbstractTerminalSession {
1060
- private spawnImpl;
1061
- readonly interactive: boolean;
1062
- private process;
1063
- private outputBuffer;
1064
- private controlStream;
1065
- constructor(id: string, taskId: string, spawnImpl: SpawnFn, interactive?: boolean);
1066
- get isStarted(): boolean;
1067
- start(launch: LaunchPlan, cwd: string, resolvedPath: string, env?: Record<string, string>, options?: {
1068
- controlChannel?: boolean;
1069
- }): void;
1070
- kill(): void;
1071
- getOutput(): string;
1072
- write(text: string): void;
1073
- /** PTY resize requests from a surface that owns the terminal rendering the wrapper. */
1074
- writeControl(text: string): void;
1075
- }
1076
- type RunnerSpawnOptions = Parameters<ITerminalRunner['spawn']>[0];
1077
- /** Everything needed to start one runner process, resolved before any child exists. */
1078
- interface PreparedLaunch {
1079
- launch: LaunchPlan;
1080
- resolvedPath: string;
1081
- env: Record<string, string>;
1082
- /** True when the invocation was wrapped in `script` to allocate a PTY. */
1083
- pty: boolean;
1084
- /** True when the started process needs an explicit Enter sent to submit its pre-filled prompt. */
1085
- submitPromptKey: boolean;
1086
- }
1087
- declare class HeadlessRunner extends AbstractRunner<HeadlessSession> {
1088
- private spawnImpl;
1089
- private hasScriptCmd;
1090
- private resolvePath;
1091
- private launchDeps;
1092
- private spawnCount;
1093
- /**
1094
- * Session shape: a piped subprocess is not a terminal, so runners get their
1095
- * non-interactive subcommand. The VS Code runner owns a pseudoterminal and
1096
- * overrides this to true. Autonomy is a separate axis (see `ResolveContext`)
1097
- * and stays on either way — no surface has a human answering permission
1098
- * prompts on the orchestrator's behalf.
1099
- */
1100
- protected readonly defaultInteractive: boolean;
1101
- constructor(deps?: HeadlessRunnerDeps);
1102
- /**
1103
- * Unique per spawn, not per task: a retry reuses its task id and ids often
1104
- * share a prefix, and a shared registry key let the old attempt's exit
1105
- * unregister the new one. Unlike TmuxRunner, nothing outside this process
1106
- * keys on the id, so a counter is enough to scope it to the attempt.
1107
- */
1108
- protected nextSessionId(taskId: string): string;
1109
- protected createSession(id: string, taskId: string): HeadlessSession;
1110
- /** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
1111
- protected prepareLaunch(opts: RunnerSpawnOptions, ptyOptions?: PtyWrapOptions): Promise<PreparedLaunch>;
1112
- spawn(opts: RunnerSpawnOptions): Promise<ITerminalSession>;
1113
- }
1114
-
1115
- /**
1116
- * The harness-planner transport contract (ADR-0009).
1117
- *
1118
- * One adapter per coding agent, each speaking that agent's own programmatic
1119
- * protocol and normalizing it to the event union below. Everything above this
1120
- * line — reply classification, the repair loop, plan validation, the four
1121
- * surfaces — is already provider-agnostic, so an adapter is the entire cost of
1122
- * teaching Ordewell to plan with another agent.
1123
- */
1124
- /**
1125
- * One normalized event from a running agent turn. Deliberately smaller than
1126
- * any single agent's native protocol: this is the intersection Ordewell can act
1127
- * on, not a lossless re-encoding. Event fidelity differs by agent — `thinking`
1128
- * is rich on Claude Code and absent elsewhere — so consumers must tolerate a
1129
- * turn that emits nothing but `assistant_text` and `turn_end`.
1130
- */
1131
- type AgentEvent =
1132
- /** A chunk of the assistant's reply. Concatenated in order to form the turn's text. */
1133
- {
1134
- type: 'assistant_text';
1135
- text: string;
1136
- }
1137
- /** Reasoning the agent chose to expose. Never contributes to the reply text. */
1138
- | {
1139
- type: 'thinking';
1140
- text: string;
1141
- } | {
1142
- type: 'tool_call';
1143
- id: string;
1144
- name: string;
1145
- args: Record<string, unknown>;
1146
- } | {
1147
- type: 'tool_result';
1148
- id: string;
1149
- name: string;
1150
- output: string;
1151
- success: boolean;
1152
- }
1153
- /**
1154
- * The agent asked to do something its read-only mode does not cover. Always
1155
- * auto-denied (T1) — a planner that can mutate is not a planner. The adapter
1156
- * is responsible for answering the agent so the turn does not hang.
1157
- */
1158
- | {
1159
- type: 'permission_request';
1160
- id: string;
1161
- name: string;
1162
- detail: string;
1163
- }
1164
- /**
1165
- * The agent delegated work to a subagent it left running in the background,
1166
- * and may end its turn before that work reports. Ordewell's conversation is
1167
- * request/response: a turn that ends hands control back to the user, and
1168
- * anything the agent says afterwards arrives with no turn open and is lost.
1169
- * Naming the launch is what lets the service ask for the results in time.
1170
- */
1171
- | {
1172
- type: 'background_agent';
1173
- id: string;
1174
- }
1175
- /** The agent finished its turn and is waiting for the next user message. */
1176
- | {
1177
- type: 'turn_end';
1178
- }
1179
- /** The turn failed. Carries the agent's own words — never a Ordewell paraphrase. */
1180
- | {
1181
- type: 'error';
1182
- message: string;
1183
- };
1184
- interface AgentStartOptions {
1185
- /** Workspace root. The agent explores from here and, in read-only mode, cannot leave it. */
1186
- cwd: string;
1187
- /** The planner system prompt, in its harness variant. */
1188
- systemPrompt: string;
1189
- /** Model id from the runner's own discovery catalog. Omitted means the agent's default. */
1190
- model?: string;
1191
- /** Variant / reasoning effort id from that model's `variants` list. */
1192
- effort?: string;
1193
- /**
1194
- * The agent's own session id from a previous run. A hint only: Ordewell's
1195
- * transcript is the source of truth (T4), so a failed resume degrades to a
1196
- * fresh session seeded from the stored history rather than an error.
1197
- */
1198
- resumeSessionId?: string;
1199
- }
1200
- interface AgentAdapter {
1201
- /** The runner id this adapter drives — `claude-code`, `codex`, `opencode`. */
1202
- readonly agentId: string;
1203
- /** Spawn the agent in its read-only mode and get it ready to receive messages. */
1204
- start(opts: AgentStartOptions): Promise<void>;
1205
- /**
1206
- * Send one user message and stream the turn's events until it ends. Resolves
1207
- * when the agent yields the floor; rejects only when the transport itself
1208
- * failed in a way no `error` event could describe.
1209
- *
1210
- * `onActivity`, when given, fires on raw transport traffic — every stdio
1211
- * line or stream chunk the process produces — independent of whether that
1212
- * traffic becomes an `AgentEvent`. An adapter may legitimately emit nothing
1213
- * for long stretches (a subagent's filtered output, most often); a caller
1214
- * using presence-of-events as a liveness signal would read that silence as
1215
- * a hang. `onActivity` is the seam that keeps liveness detection from being
1216
- * coupled to what each adapter chooses to surface.
1217
- */
1218
- send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
1219
- /** The agent's native session id once it has announced one. Resumption hint only. */
1220
- nativeSessionId(): string | null;
1221
- /** Kill the process and release its resources. Idempotent. */
1222
- dispose(): void;
1223
- }
1224
- /**
1225
- * The single injected boundary between Ordewell and the operating system —
1226
- * the same pattern `HeadlessRunnerDeps` uses for task execution. Tests feed
1227
- * recorded agent output through `spawn` (and, for HTTP-transport agents,
1228
- * `fetch`) so one test exercises adapter parsing, event mapping, reply
1229
- * classification and the repair loop as a single observable behavior.
1230
- */
1231
- interface AgentProcessDeps {
1232
- spawn: SpawnFn;
1233
- fetch: typeof globalThis.fetch;
1234
- /** Resolves the PATH agents are spawned under. Defaults to the augmented PATH. */
1235
- resolvePath?: () => Promise<string>;
1236
- /** Host platform. Defaults to the real one; injected so OS-specific behavior is testable anywhere. */
1237
- platform?: NodeJS.Platform;
1238
- /** True when `workspace` names an existing directory. Defaults to a real filesystem check. */
1239
- isDirectory?: (workspace: string) => boolean;
1240
- /** True when `candidate` names an existing, spawnable file. Defaults to a real filesystem check. */
1241
- exists?: (candidate: string) => boolean;
1242
- /** The workspace's own variables for a cwd (ADR-0016). Defaults to {@link resolveWorkspaceEnv}. */
1243
- workspaceEnv?: (cwd: string) => Promise<Record<string, string>>;
1244
- }
1245
- /** Builds the adapter for one runner id, or null when that runner cannot plan. */
1246
- type AgentAdapterFactory = (runner: string, deps: AgentProcessDeps) => AgentAdapter | null;
1247
- /**
1248
- * Split a stream of chunks into complete lines. Every agent transport here is
1249
- * newline-delimited JSON of some shape, and a chunk boundary lands mid-object
1250
- * often enough that parsing per-chunk silently drops events.
1251
- */
1252
- declare class LineBuffer {
1253
- private buffer;
1254
- push(chunk: string, onLine: (line: string) => void): void;
1255
- /** Anything left unterminated when the stream closed. */
1256
- flush(): string;
1257
- }
906
+ declare function renderTaskQueryAnswer(query: TaskQuery, tasks: readonly Task[], catalog: TaskQueryCatalog, liveOutput?: LiveOutputLookup): string;
1258
907
 
1259
908
  interface CliAgentAiServiceDeps extends Partial<AgentProcessDeps> {
1260
909
  /** Overrides adapter construction. Tests supply a fake agent; production picks by runner id. */
@@ -1269,8 +918,8 @@ interface CliAgentAiServiceDeps extends Partial<AgentProcessDeps> {
1269
918
  * `GeminiService`. It deliberately does **not** extend {@link BaseAiService}:
1270
919
  * that class's body is Ordewell executing research tools on a model's behalf,
1271
920
  * which is precisely the part a coding agent replaces. What it reuses instead
1272
- * is everything above the transport — `classifyPlannerReply`, the bounded
1273
- * corrective re-emit loop, `parsePlanJson`, the `ResearchProgress` events the
921
+ * is everything above the transport — `settleReply` (reply classification
922
+ * and the bounded corrective retries), `parsePlanJson`, the `ResearchProgress` events the
1274
923
  * four surfaces already render. That is why this backend reaches VS Code, the
1275
924
  * web UI, the CLI and the TUI without any of them learning a coding agent is
1276
925
  * on the other end.
@@ -1290,6 +939,12 @@ declare class CliAgentAiService implements IAiService {
1290
939
  private lastNativeSessionId;
1291
940
  private conversation;
1292
941
  private activeAbort;
942
+ /**
943
+ * Every subagent this conversation has reported starting, and finishing.
944
+ * Agents restate a subagent's state as it changes, and one can finish a turn
945
+ * or a process restart after it started; surfaces get each once, in order.
946
+ */
947
+ private readonly subagents;
1293
948
  constructor(config: IConfig, deps?: CliAgentAiServiceDeps);
1294
949
  hasActiveConversation(): boolean;
1295
950
  /**
@@ -1308,10 +963,10 @@ declare class CliAgentAiService implements IAiService {
1308
963
  continueConversation(userMessage: string, onProgress: (progress: ResearchProgress) => void, signal?: AbortSignal): Promise<ConversationTurn>;
1309
964
  private openingMessage;
1310
965
  /**
1311
- * Drive one user message to a settled planner turn. Same shape as the API
1312
- * backend's conversation loop minus the tool rounds — those belong to the
1313
- * agent now — and with the same two policies layered on top: the
1314
- * empty-reply nudge, and a bounded corrective re-emit for botched JSON.
966
+ * Drive one user message to a settled planner turn through
967
+ * {@link settleReply}, the loop the API backend settles through too. The
968
+ * tool rounds belong to the agent now, so one call is one agent turn —
969
+ * continued while it left subagents running in the background.
1315
970
  */
1316
971
  private runConversation;
1317
972
  /**
@@ -1322,6 +977,11 @@ declare class CliAgentAiService implements IAiService {
1322
977
  * at once, which all three of these do routinely.
1323
978
  */
1324
979
  private runTurn;
980
+ /**
981
+ * The only way this service starts an agent, and it takes a planner start by
982
+ * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
983
+ * mode from here without changing this signature.
984
+ */
1325
985
  private startAdapter;
1326
986
  /**
1327
987
  * The live agent process, restarted from its own session id if it died
@@ -1330,12 +990,13 @@ declare class CliAgentAiService implements IAiService {
1330
990
  * degradation `restoreChat` already performs on every surface.
1331
991
  */
1332
992
  private ensureAdapter;
1333
- private startAbortScope;
1334
993
  private plannerModel;
1335
994
  /**
1336
995
  * A single agent session that answers one prompt and exits. Used by every
1337
996
  * non-conversational entry point; the plan is parsed from the reply text by
1338
- * the same extractor the conversational path uses.
997
+ * the same extractor the conversational path uses. Its envelope streams to
998
+ * the plan display, as a vendor planner's one-shot does, and the prose
999
+ * around it is not streamed at all, since no turn is open to show it in.
1339
1000
  */
1340
1001
  private oneShot;
1341
1002
  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<{
@@ -1365,6 +1026,12 @@ interface ConversationRequest {
1365
1026
  runnerModes?: Record<RunnerId, RunnerModeInfo[]>;
1366
1027
  autonomousDefault?: boolean;
1367
1028
  verificationEnabled?: boolean;
1029
+ /**
1030
+ * The planner model's context window when the catalog knows it. Threaded here
1031
+ * so a usage record can carry it (and the UI can show context fill); absent
1032
+ * when unknown, never guessed (#49).
1033
+ */
1034
+ contextWindow?: number;
1368
1035
  /** Where tasks will run (ADR-0013, ADR-0014): in worktrees, the prompt drops file-overlap ordering and describes a repo group. */
1369
1036
  isolatedExecution?: IsolatedExecution;
1370
1037
  signal?: AbortSignal;
@@ -1533,7 +1200,7 @@ declare abstract class BaseAiService {
1533
1200
  abstract ensureInit(): void;
1534
1201
  hasActiveConversation(): boolean;
1535
1202
  pruneContext(): number;
1536
- protected startAbortScope(callerSignal?: AbortSignal): AbortSignal | undefined;
1203
+ protected startAbortScope(callerSignal?: AbortSignal): AbortSignal;
1537
1204
  protected stopAbortScope(): void;
1538
1205
  sendPlanningPrompt(prompt: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Promise<Task[]>;
1539
1206
  continueConversation(userMessage: string, onProgress: (progress: ResearchProgress) => void, signal?: AbortSignal): Promise<ConversationTurn>;
@@ -1542,9 +1209,10 @@ declare abstract class BaseAiService {
1542
1209
  * Build a fresh chat for one research subagent (own history, subagent system
1543
1210
  * prompt, cheap model). Null means the provider does not support subagents —
1544
1211
  * the spawn tool then degrades to a steering message. `onReasoning` streams
1545
- * live reasoning deltas on models that expose them, same as the top-level loop.
1212
+ * live reasoning deltas on models that expose them, same as the top-level loop;
1213
+ * `onUsage` takes each call's usage, as the planner's own chat reports it.
1546
1214
  */
1547
- protected createSubagentChat(_onReasoning?: (delta: string) => void): ResearchChat | null;
1215
+ protected createSubagentChat(_onReasoning?: (delta: string) => void, _onUsage?: (record: UsageRecord) => void): ResearchChat | null;
1548
1216
  /**
1549
1217
  * One spawn_research_agent tool call, executed at the service layer (not in
1550
1218
  * executeTool — that would cycle the imports). Every failure path returns a
@@ -1567,13 +1235,15 @@ declare abstract class BaseAiService {
1567
1235
  private static appendBudgetCountdown;
1568
1236
  /**
1569
1237
  * Run one planner conversation turn (ADR-0002): send the message, satisfy
1570
- * tool calls until the model answers in prose or JSON, then classify the
1571
- * result. The model decides transitions — there are no sentinels, no
1572
- * question tags, and no correction nags. A turn whose final text parses as
1573
- * a `{tasks:[...]}` object commits the plan; anything else is a message to
1574
- * the user.
1238
+ * tool calls until the model answers in prose or JSON, and settle the reply
1239
+ * through {@link settleReply}, which owns classification and every
1240
+ * corrective retry. The model decides transitions — there are no sentinels
1241
+ * and no question tags. A turn whose final text parses as a `{tasks:[...]}`
1242
+ * object commits the plan; anything else is a message to the user.
1575
1243
  */
1576
1244
  protected runConversationTurn(ctx: ConversationTurnContext, message: string, onProgress: (progress: ResearchProgress) => void, signal?: AbortSignal): Promise<ConversationTurn>;
1245
+ /** One model call of a conversation turn: the message, then tool rounds until the model replies without tools. */
1246
+ private runToolRounds;
1577
1247
  /**
1578
1248
  * Run the LLM tool-calling research loop for one-shot planning. Returns
1579
1249
  * parsed tasks if a plan was emitted mid-loop, or null if the loop
@@ -1592,6 +1262,7 @@ declare abstract class BaseAiService {
1592
1262
 
1593
1263
  declare class GeminiService extends BaseAiService implements IAiService {
1594
1264
  private genAI;
1265
+ private genAIKey;
1595
1266
  private model;
1596
1267
  constructor(config: IConfig);
1597
1268
  private init;
@@ -1621,14 +1292,21 @@ declare class GeminiService extends BaseAiService implements IAiService {
1621
1292
 
1622
1293
  declare class OpenAiService extends BaseAiService implements IAiService {
1623
1294
  private client;
1295
+ private clientCredentials;
1624
1296
  constructor(config: IConfig);
1297
+ /**
1298
+ * Rebuilt whenever the key or endpoint differs from the one the client was
1299
+ * made with. A Session outlives `/key` and endpoint edits, so a client cached
1300
+ * for good kept sending the previous key: the provider answered 401 for a
1301
+ * key the user had already replaced.
1302
+ */
1625
1303
  private getClient;
1626
1304
  ensureInit(): void;
1627
1305
  private requireModel;
1628
1306
  reset(): void;
1629
1307
  protected streamPlanText(prompt: string, repairHint: string | undefined, onToken: (token: string) => void, onReasoning?: (token: string) => void, signal?: AbortSignal): Promise<string>;
1630
1308
  /** A research subagent: fresh history, digest contract, cheap model, read-only tools. */
1631
- protected createSubagentChat(onReasoning?: (delta: string) => void): ResearchChat | null;
1309
+ protected createSubagentChat(onReasoning?: (delta: string) => void, onUsage?: (record: UsageRecord) => void): ResearchChat | null;
1632
1310
  startConversation(req: ConversationRequest): Promise<ConversationTurn>;
1633
1311
  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<{
1634
1312
  tasks: Task[];
@@ -1643,16 +1321,18 @@ declare class OpenAiService extends BaseAiService implements IAiService {
1643
1321
 
1644
1322
  /**
1645
1323
  * The deep module owning all plan-shaped state. Holds `planTasks` (the ordered
1646
- * tree the user edits), the flattened `allTasks` view, the `taskMap` index, and
1647
- * the `completedTasks`/`failedTasks` sets the scheduler reads. The orchestrator
1648
- * calls `markCompleted`/`markFailed`/`markInProgress`/`retry` to update task
1649
- * status — it never mutates task state directly.
1324
+ * tree the user edits), the flattened `allTasks` view and the `taskMap` index.
1325
+ * The orchestrator calls `markCompleted`/`markFailed`/`markInProgress`/`retry`
1326
+ * to update task status — it never mutates task state directly.
1327
+ *
1328
+ * Completion and failure are read from `task.status` and nothing else, so
1329
+ * `isCompleted`, `completedCount`, `isAllComplete` and the scheduler's
1330
+ * dependency checks cannot disagree. The store owns its task objects: `load`
1331
+ * copies what it is given, the getters hand out readonly views, and
1332
+ * `snapshot` is the copy a caller may keep or write to disk.
1650
1333
  *
1651
1334
  * `rebuild` is the internal seam that keeps the flat views in sync with the
1652
- * tree. Structural removals (remove/merge/split) additionally prune
1653
- * `completedTasks`/`failedTasks` for ids that no longer exist; `removeFromActive`
1654
- * deliberately does not, so a completed task that leaves the active list still
1655
- * satisfies its dependents' dependency checks.
1335
+ * tree.
1656
1336
  *
1657
1337
  * `planRunners` lives here because it's part of the plan's identity (the runner
1658
1338
  * set, carried on the plan). `validateAssignedRunners` is pure store logic.
@@ -1661,15 +1341,13 @@ declare class PlanStore {
1661
1341
  private _planTasks;
1662
1342
  private _allTasks;
1663
1343
  private _taskMap;
1664
- private _completedTasks;
1665
- private _failedTasks;
1666
1344
  private _planRunners;
1667
1345
  private _onMutate;
1668
1346
  private _executionLog;
1669
- /** Hook called after every structural mutation (add/remove/update/merge/split/load). */
1347
+ /** Hook called after every structural mutation (add/remove/update/merge/split/resetForRun). */
1670
1348
  set onMutate(cb: (() => void) | null);
1671
- get planTasks(): Task[];
1672
- get allTasks(): Task[];
1349
+ get planTasks(): ReadonlyArray<Readonly<Task>>;
1350
+ get allTasks(): ReadonlyArray<Readonly<Task>>;
1673
1351
  get planRunners(): RunnerId[];
1674
1352
  get completedCount(): number;
1675
1353
  get failedCount(): number;
@@ -1677,8 +1355,10 @@ declare class PlanStore {
1677
1355
  isAnyFailed(): boolean;
1678
1356
  isCompleted(id: string): boolean;
1679
1357
  isFailed(id: string): boolean;
1680
- get(taskId: string): Task | undefined;
1681
- getExecutionLog(): TaskSnapshot[];
1358
+ get(taskId: string): Readonly<Task> | undefined;
1359
+ /** A copy of the task tree, detached from the store: later status changes do not reach it. */
1360
+ snapshot(): Task[];
1361
+ getExecutionLog(): ReadonlyArray<TaskSnapshot>;
1682
1362
  appendToLog(snapshot: TaskSnapshot): void;
1683
1363
  /**
1684
1364
  * Drop a task's archived snapshot. Un-marking a completion has to erase the
@@ -1686,10 +1366,10 @@ declare class PlanStore {
1686
1366
  * left-behind snapshot would keep feeding them a result that no longer exists.
1687
1367
  */
1688
1368
  removeFromLog(taskId: string): void;
1689
- removeFromActive(taskId: string): void;
1690
1369
  clearLog(): void;
1691
1370
  private notifyMutate;
1692
- load(tasks: Task[], runners: RunnerId[]): void;
1371
+ private countStatus;
1372
+ load(tasks: ReadonlyArray<Readonly<Task>>, runners: readonly RunnerId[]): void;
1693
1373
  add(partial: Partial<Task>): Task;
1694
1374
  remove(taskId: string): void;
1695
1375
  update(taskId: string, changes: Partial<Task>): Task | undefined;
@@ -1709,13 +1389,16 @@ declare class PlanStore {
1709
1389
  markCompleted(id: string): void;
1710
1390
  markFailed(id: string): void;
1711
1391
  markInProgress(id: string): void;
1712
- markAwaitingUser(id: string): void;
1392
+ markAwaitingUser(id: string, reason?: AwaitingReason): void;
1713
1393
  markPending(id: string): void;
1714
1394
  retry(id: string): void;
1395
+ /** A reason outlives nothing: any status but `awaiting_user` drops it. */
1396
+ private setStatus;
1715
1397
  blockDependents(id: string): void;
1716
1398
  unblockDependents(id: string): void;
1717
1399
  setTaskVerdict(id: string, verdict: Task['verdict']): void;
1718
1400
  setTaskOutputSummary(id: string, summary: Task['outputSummary']): void;
1401
+ setTaskTransport(id: string, transport: Task['transport']): void;
1719
1402
  getPlanVisualization(): {
1720
1403
  tasks: {
1721
1404
  id: string;
@@ -1732,16 +1415,310 @@ declare class PlanStore {
1732
1415
  * plan's first one instead.
1733
1416
  */
1734
1417
  admitRunner(runner: RunnerId): void;
1735
- resolveTaskRunner(task: Task): RunnerId;
1418
+ resolveTaskRunner(task: Readonly<Task>): RunnerId;
1736
1419
  private rebuild;
1420
+ private validateAssignedRunners;
1421
+ }
1422
+
1423
+ type IsolationNoticeLevel = 'info' | 'warn' | 'error';
1424
+ /**
1425
+ * What the controller reports. It never schedules or emits on its own: the
1426
+ * orchestrator turns these into observer events and decides what runs next.
1427
+ */
1428
+ interface IsolationRunListener {
1429
+ /** The run record changed and should be persisted with the plan. */
1430
+ changed(): void;
1737
1431
  /**
1738
- * Drop completed/failed ids that no longer exist in the plan. Called by the
1739
- * structural removals (remove/merge/split) — but NOT by removeFromActive,
1740
- * where a completed task leaves the active list yet must still satisfy its
1741
- * dependents' dependency checks.
1432
+ * A run opened — the one moment shared by Execute, a manual task run and
1433
+ * a force start, so whatever a run pins for its whole length is read here.
1742
1434
  */
1743
- private pruneTerminalSets;
1744
- private validateAssignedRunners;
1435
+ opened(): void;
1436
+ /** A run did not start: `repos` of the group have tracked changes. */
1437
+ blocked(repos: string[]): void;
1438
+ /** An isolated run closed and handed its integration branches over. */
1439
+ handoff(handoff: IsolationHandoff): void;
1440
+ /** Already said through the notifications; also for a surface that shows no toasts. */
1441
+ notice(level: IsolationNoticeLevel, message: string): void;
1442
+ /**
1443
+ * These tasks' worktrees are about to be removed. An agent left running in
1444
+ * one would sit in a deleted directory, so it has to go first.
1445
+ */
1446
+ releasing(taskIds: string[]): void;
1447
+ }
1448
+ interface IsolationRunControllerDeps {
1449
+ isolation: IWorktreeIsolation;
1450
+ config: IConfig;
1451
+ notifications: INotification;
1452
+ workspaceRoot: () => string;
1453
+ listener: IsolationRunListener;
1454
+ }
1455
+ /**
1456
+ * The lifecycle of a plan's isolation run (ADR-0013, ADR-0014), between the
1457
+ * scheduler and the git layer: deciding at a run's start whether it executes
1458
+ * in worktrees, a blocked run and how it goes on, each attempt's working
1459
+ * directory, releasing worktrees, the handoff when the run closes, and what the
1460
+ * user does with it afterwards. The record it keeps outlives one run — a
1461
+ * resumed plan continues it.
1462
+ */
1463
+ declare class IsolationRunController {
1464
+ private readonly isolation;
1465
+ private readonly config;
1466
+ private readonly notifications;
1467
+ private readonly workspaceRoot;
1468
+ private readonly listener;
1469
+ private run;
1470
+ /** Copied paths already reported for the current run: every task gets the same copies. */
1471
+ private reportedCopies;
1472
+ /**
1473
+ * How the open run executes; null while no run is open. A run is one
1474
+ * Execute-Plan or one manual task run, from its start until it settles or
1475
+ * is stopped.
1476
+ */
1477
+ private mode;
1478
+ /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
1479
+ private resolvers;
1480
+ private opening;
1481
+ /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
1482
+ private blockedStart;
1483
+ /** The dirty repos behind {@link blockedStart}, for the stash notice. */
1484
+ private blockedRepos;
1485
+ constructor(deps: IsolationRunControllerDeps);
1486
+ /** The plan's run record, open or not; null when the plan has not isolated. */
1487
+ get current(): IsolationRun | null;
1488
+ get isOpen(): boolean;
1489
+ /** The open run executes in worktrees. */
1490
+ get isolating(): boolean;
1491
+ /** A run is waiting on the user to stash or to go on without isolation. */
1492
+ get blocked(): boolean;
1493
+ /** What the plan persists of isolated execution; null when no run ever isolated. */
1494
+ get planIsolation(): PlanIsolation | null;
1495
+ requireRun(): IsolationRun;
1496
+ /** A task's record in the open run, only while that run isolates. */
1497
+ openRecord(taskId: string): IsolationTaskRecord | undefined;
1498
+ /** Where a task's isolated work stands; null when the plan has no isolation run to speak of. */
1499
+ taskIsolation(taskId: string): TaskIsolation | null;
1500
+ view(): IsolationView | null;
1501
+ /**
1502
+ * Take over a plan's persisted isolation, or none for a plan that has not
1503
+ * isolated yet. Whatever a crashed process left behind for the run — a
1504
+ * worktree still marked active, a directory no record owns — is pruned,
1505
+ * while kept, failed and conflicted worktrees stay for the user.
1506
+ *
1507
+ * Deliberately not reported as a change: adopting is not a change to
1508
+ * persist, and a host that adopts without persisting (VS Code's restore)
1509
+ * would otherwise write a new session file on every reload.
1510
+ */
1511
+ adopt(state: PlanIsolation | null): Promise<void>;
1512
+ /**
1513
+ * Open a run if none is: decide once, at its start, whether it executes in
1514
+ * worktrees. `resume` is what a dirty tree parks until the user chooses how
1515
+ * to go on. Resolves false when the run did not start.
1516
+ */
1517
+ open(resume: () => Promise<void>): Promise<boolean>;
1518
+ /**
1519
+ * Whether the run in force, or else the next one, gives each task its own
1520
+ * worktrees, and of which repo group — what the planner is told, since it
1521
+ * decides whether tasks on the same file have to be ordered and which shared
1522
+ * paths two tasks must not edit at once. A tree that would block counts as
1523
+ * not isolating: the user may yet run without isolation, and ordering is
1524
+ * the safe rule then.
1525
+ */
1526
+ plannerLayout(): Promise<IsolatedExecution>;
1527
+ /**
1528
+ * Go on with the start a dirty tree turned away. `stash` puts the user's
1529
+ * tracked changes on the git stash first, so the run isolates; `shared` runs
1530
+ * this one run in the workspace root, knowingly. Returns the parked start for
1531
+ * the caller to replay; null when nothing was parked.
1532
+ */
1533
+ continueBlocked(how: 'stash' | 'shared'): Promise<(() => Promise<void>) | null>;
1534
+ /**
1535
+ * The one place an attempt's working directory is decided: the worktree
1536
+ * prepared for it in an isolated run, the kept one for a conflict repair,
1537
+ * else the workspace root. `worktree` says which. Does not itself report a
1538
+ * worktree it creates as a change — the caller does once the attempt is
1539
+ * committed as running, so a spawn abandoned or failed after this settles
1540
+ * is not misreported as a change that stuck.
1541
+ */
1542
+ attemptCwd(task: Task, opts: {
1543
+ repair: boolean;
1544
+ }): Promise<{
1545
+ cwd: string;
1546
+ worktree: boolean;
1547
+ }>;
1548
+ /**
1549
+ * Land a task's work on the run's integration branches. The record is
1550
+ * reported changed once the landing is set and before the first merge, so
1551
+ * a crash mid-landing leaves the tips to roll back to, and again once it
1552
+ * settles. A git error is `failed`, never a throw.
1553
+ */
1554
+ integrate(task: Task): Promise<IsolationOutcome>;
1555
+ /** What a conflict repair's work shows (ADR-0015); git that cannot tell counts against it. */
1556
+ verifyRepair(task: Task): Promise<RepairEvidence>;
1557
+ /**
1558
+ * Let go of a task's worktree. `keep` leaves it and its branch for
1559
+ * inspection; otherwise both go. A landing still in flight settles first, so
1560
+ * the worktree is never torn down underneath its merge.
1561
+ */
1562
+ release(taskId: string, opts: {
1563
+ keep: boolean;
1564
+ }, landing?: Promise<unknown> | null): Promise<void>;
1565
+ /** Close the open run. An isolated one hands its integration branch over for review. */
1566
+ close(): Promise<void>;
1567
+ /**
1568
+ * A stop or a plan load: the open run ends without a handoff and a parked
1569
+ * start is dropped. `keepOpen` spares a run the scheduler is still driving.
1570
+ */
1571
+ interrupt(opts?: {
1572
+ keepOpen?: boolean;
1573
+ }): void;
1574
+ /**
1575
+ * Mark which added task resolves which conflict. The resolver merges the
1576
+ * conflicted task's branch by hand in its own worktree; once that lands, the
1577
+ * conflicted task can land in turn.
1578
+ */
1579
+ linkResolver(resolverId: string, conflictedId: string): void;
1580
+ /** The conflicted task a landed resolver was added for, forgotten as it is returned. */
1581
+ takeResolver(resolverId: string): string | undefined;
1582
+ reviewDiff(): Promise<string>;
1583
+ /**
1584
+ * "Merge all": the run's integration branches into whatever the user has
1585
+ * checked out, in every repo or none. Once everything merged, the run has
1586
+ * nothing left to hand over, so it is cleared up and forgotten; a branch
1587
+ * the user's HEAD somehow does not contain stays for the next run's sweep.
1588
+ */
1589
+ merge(): Promise<IsolationMergeResult>;
1590
+ /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
1591
+ cleanup(): Promise<void>;
1592
+ /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
1593
+ discard(): Promise<void>;
1594
+ private clearMerged;
1595
+ private forget;
1596
+ private decide;
1597
+ private activate;
1598
+ private begin;
1599
+ /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
1600
+ private sweep;
1601
+ /**
1602
+ * The plan's run carries on while it still has any record: anything landed
1603
+ * means a resumed plan's dependents must start from a tip that holds their
1604
+ * predecessors' work, and anything held — a kept attempt, a conflict, a
1605
+ * repair — is work the user may still want, which a fresh run's mint would
1606
+ * delete. Only a run with no records at all is superseded.
1607
+ */
1608
+ private continuableRun;
1609
+ /**
1610
+ * A run with no records at all holds only superseded attempts, so it goes
1611
+ * whole. One that cannot be continued for another reason — it ran from a
1612
+ * different workspace path — keeps its integration branch in each repo that
1613
+ * has not merged it: only the user gives landed work up.
1614
+ */
1615
+ private mint;
1616
+ private reportCopies;
1617
+ private tell;
1618
+ }
1619
+
1620
+ /** Which repair of a task an attempt is, of the most `conflictRepairAttempts` allows. */
1621
+ interface RepairAttempt {
1622
+ n: number;
1623
+ limit: number;
1624
+ }
1625
+ /** One thing the user is told about a landing. */
1626
+ interface LandingMessage {
1627
+ level: 'info' | 'warn' | 'error';
1628
+ text: string;
1629
+ /** Also handed to a surface that shows no toasts: ADR-0015 logs every conflict repair as a notice. */
1630
+ repairLog?: true;
1631
+ }
1632
+ /**
1633
+ * How a landing settled, for the orchestrator to apply — it marks the task,
1634
+ * starts the repair and says the messages. By the time one is returned the
1635
+ * merge has happened or not, and the run record says so.
1636
+ *
1637
+ * - `landed`: the work is on the integration branch of every repo it changed.
1638
+ * - `nothing-to-land`: the attempt ran in the workspace root, or the task
1639
+ * holds no unlanded work; it is done as it stands.
1640
+ * - `repair-needed`: the landing conflicted and the task is owed `repair`
1641
+ * (ADR-0015), in the worktree kept for it.
1642
+ * - `awaiting_user`: the work did not land and waits on the user, worktree
1643
+ * kept — a conflict with no repair left (`conflict`), a merge git refused
1644
+ * (`landing-failed`), or a repair that did not land (`repair-failed`). None
1645
+ * of them halts the run.
1646
+ */
1647
+ type LandingOutcome = {
1648
+ kind: 'landed';
1649
+ messages: LandingMessage[];
1650
+ } | {
1651
+ kind: 'nothing-to-land';
1652
+ messages: LandingMessage[];
1653
+ } | {
1654
+ kind: 'repair-needed';
1655
+ repair: RepairAttempt;
1656
+ messages: LandingMessage[];
1657
+ } | {
1658
+ kind: 'awaiting_user';
1659
+ reason: 'conflict' | 'landing-failed' | 'repair-failed';
1660
+ messages: LandingMessage[];
1661
+ };
1662
+ interface LandingDeps {
1663
+ runs: IsolationRunController;
1664
+ config: Pick<IConfig, 'conflictRepairAttempts'>;
1665
+ /** Read-only: only the orchestrator changes a task. */
1666
+ tasks: Pick<PlanStore, 'get'>;
1667
+ }
1668
+ /**
1669
+ * Landing and Conflict repair (ADR-0013, ADR-0014, ADR-0015): a passed
1670
+ * attempt's work onto the run's integration branches through the
1671
+ * {@link IsolationRunController}, and what a landing that did not go through
1672
+ * leads to. It answers with a {@link LandingOutcome} rather than acting on it —
1673
+ * it never marks a task, starts an attempt or emits — so the scheduler stays
1674
+ * the one place that decides what runs. A repair is spent when it starts: the
1675
+ * controller's `reopen` counts it before the runner spawns, and nothing here
1676
+ * hands one back.
1677
+ */
1678
+ declare class Landing {
1679
+ private readonly runs;
1680
+ private readonly config;
1681
+ private readonly tasks;
1682
+ constructor(deps: LandingDeps);
1683
+ /**
1684
+ * Land a passed attempt's work. One whose worktree has no unlanded record
1685
+ * behind it any more — the run was discarded under it — cannot land.
1686
+ */
1687
+ landPassed(task: Task, attempt: {
1688
+ worktree: boolean;
1689
+ }): Promise<LandingOutcome>;
1690
+ /** Mark complete is a passed verdict the user vouches for: what the task holds unlanded lands the same way. */
1691
+ landVouched(task: Task): Promise<LandingOutcome>;
1692
+ /**
1693
+ * A repair's verdict decides only whether its work may try to land; the
1694
+ * task keeps the verdict its own work earned. Evidence comes before the
1695
+ * queue, and the landing after it is the one every task goes through, so a
1696
+ * repair the tip has moved past again is a fresh conflict, not a pass.
1697
+ */
1698
+ settleRepair(task: Task, verdict: Verdict): Promise<LandingOutcome>;
1699
+ /** A repair that ended without landing leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
1700
+ unrepaired(task: Task, why: string): Promise<Extract<LandingOutcome, {
1701
+ kind: 'awaiting_user';
1702
+ }>>;
1703
+ /**
1704
+ * Land the conflicted task a resolver was added for, now that the resolver
1705
+ * has landed. Its branch is on the integration branch by then, so it merges
1706
+ * clean — and one the resolver did not really bring along conflicts again
1707
+ * instead of being taken at its word. Null when there is nothing to land.
1708
+ */
1709
+ landResolved(resolverId: string): Promise<{
1710
+ task: Readonly<Task>;
1711
+ outcome: LandingOutcome;
1712
+ } | null>;
1713
+ /** The repair a conflicted task is owed next; null when repair is off, used up, or there is no isolated run to repair it in. */
1714
+ nextRepair(taskId: string): RepairAttempt | null;
1715
+ /** What a repair is asked to do; read once `reopen` has recorded the tips it must bring in. */
1716
+ repairPrompt(task: Task): string;
1717
+ private hasUnlandedWork;
1718
+ private unmerged;
1719
+ private noRepairReason;
1720
+ private describeEvidence;
1721
+ private isGroup;
1745
1722
  }
1746
1723
 
1747
1724
  /**
@@ -1774,6 +1751,15 @@ interface WorkspaceEnv {
1774
1751
  interface OrchestratorObserver {
1775
1752
  /** Any task-shaped state changed (store mutation, checkpoint, retry, …). */
1776
1753
  onTaskChanged?(): void;
1754
+ /**
1755
+ * A run reached an outcome that stands until the user acts — completed,
1756
+ * failed, or awaiting the user — on its own, not as the answer to a call.
1757
+ * Emitted before the `onTaskChanged` that announces it, so a listener can
1758
+ * save the outcome before any surface is told.
1759
+ */
1760
+ onTaskSettled?(data: {
1761
+ taskId: string;
1762
+ }): void;
1777
1763
  onTick?(): void;
1778
1764
  onExecutionComplete?(): void;
1779
1765
  /** Queued user messages are ready to be processed by the planner. */
@@ -1805,10 +1791,18 @@ interface OrchestratorObserver {
1805
1791
  * daemon may leave unwired, so a surface without toasts can still show it.
1806
1792
  */
1807
1793
  onIsolationNotice?(data: {
1808
- level: 'info' | 'warn';
1794
+ level: 'info' | 'warn' | 'error';
1809
1795
  message: string;
1810
1796
  }): void;
1811
1797
  }
1798
+ /**
1799
+ * A message or interrupt a task cannot take: it is not running, it runs on the
1800
+ * terminal transport, or it is waiting on something a message does not answer.
1801
+ * The request is wrong, not the orchestrator, so a surface says why.
1802
+ */
1803
+ declare class TaskControlError extends Error {
1804
+ constructor(message: string);
1805
+ }
1812
1806
  type AttemptPhase = 'starting' | 'running' | 'integrating';
1813
1807
  /** Read-only view of a task's live attempt. */
1814
1808
  interface TaskAttemptSnapshot {
@@ -1820,6 +1814,50 @@ interface TaskAttemptSnapshot {
1820
1814
  cwd: string | null;
1821
1815
  startedAt: string;
1822
1816
  }
1817
+ /**
1818
+ * The wiring a {@link TaskOrchestrator} runs on, every collaborator resolved.
1819
+ * {@link createTaskOrchestrator} is the only production caller; tests build one
1820
+ * through that factory too. The constructor makes nothing itself, so there is
1821
+ * exactly one place defaults live.
1822
+ */
1823
+ interface TaskOrchestratorDeps {
1824
+ config: IConfig;
1825
+ notifications: INotification;
1826
+ terminalRunner: ITerminalRunner;
1827
+ store: PlanStore;
1828
+ output: TaskOutputSource;
1829
+ /** The plan's isolation run and the open run's lifecycle (ADR-0013). */
1830
+ runs: IsolationRunController;
1831
+ /** A passed attempt's work onto the integration branch, and any conflict repair. */
1832
+ landing: Landing;
1833
+ registry: RunnerRegistry | null;
1834
+ /** The workspace root, read at call time — VS Code can change it. */
1835
+ workspaceRoot: () => string;
1836
+ /** The workspace's own variables for a task's cwd (ADR-0016). */
1837
+ workspaceEnv: (cwd: string) => Promise<WorkspaceEnv>;
1838
+ /** Read live at each spawn, so a toggle takes effect on the next task. */
1839
+ tddEnabled: () => boolean;
1840
+ /** Read once as a run opens, never per spawn (ADR-0018, S1). */
1841
+ runnerTransport: () => RunnerTransport;
1842
+ }
1843
+ /**
1844
+ * What a caller supplies to {@link createTaskOrchestrator}; every optional one
1845
+ * gets its default there. This is what a host or test chooses, not the resolved
1846
+ * wiring the orchestrator itself runs on.
1847
+ */
1848
+ interface TaskOrchestratorOptions {
1849
+ config: IConfig;
1850
+ notifications: INotification;
1851
+ terminalRunner: ITerminalRunner;
1852
+ store?: PlanStore;
1853
+ output?: TaskOutputSource;
1854
+ isolation?: IWorktreeIsolation;
1855
+ registry?: RunnerRegistry | null;
1856
+ workspaceRoot?: () => string;
1857
+ workspaceEnv?: (cwd: string) => Promise<WorkspaceEnv>;
1858
+ tddEnabled?: () => boolean;
1859
+ runnerTransport?: () => RunnerTransport;
1860
+ }
1823
1861
  /**
1824
1862
  * The pure scheduler. Owns execution state (`running`, `planStatus`,
1825
1863
  * `reviewApproved`, the live task attempts, `messageQueue`) and the verifier.
@@ -1834,8 +1872,8 @@ declare class TaskOrchestrator {
1834
1872
  private config;
1835
1873
  private notifications;
1836
1874
  private terminalRunner;
1837
- private output;
1838
1875
  private store;
1876
+ private output;
1839
1877
  private attempts;
1840
1878
  private verifier;
1841
1879
  private running;
@@ -1844,14 +1882,7 @@ declare class TaskOrchestrator {
1844
1882
  private reviewApproved;
1845
1883
  private retryCounts;
1846
1884
  private spawnCounts;
1847
- /**
1848
- * The terminal a verdict left open, by task. It stays so the user can read
1849
- * the agent's output or keep talking to it — but only while its worktree
1850
- * does: once that is removed the agent sits in a deleted directory, and a
1851
- * newer attempt in the same worktree would share it with a second agent.
1852
- * Without this, every task of every run left one agent process running
1853
- * until the daemon stopped.
1854
- */
1885
+ /** The terminal a verdict left open, by task; see {@link LingeringRunners}. */
1855
1886
  private lingering;
1856
1887
  /**
1857
1888
  * A full-plan run a failure paused. Retrying the failed task is the explicit
@@ -1870,53 +1901,69 @@ declare class TaskOrchestrator {
1870
1901
  * task whose spawn always throws.
1871
1902
  */
1872
1903
  private onHold;
1873
- private isolation;
1874
- /** The plan's isolation run (ADR-0013). Outlives one run: a resumed plan continues it. */
1875
- private isolationRun;
1876
- /** Copied paths already reported for the current run: every task gets the same copies. */
1877
- private reportedCopies;
1878
- /**
1879
- * How the open run executes; null while no run is open. A run is one
1880
- * Execute-Plan or one manual task run, from its start until it settles or
1881
- * is stopped.
1882
- */
1883
- private runMode;
1884
- /** Resolver task id → the conflicted task it resolves; see {@link linkConflictResolver}. */
1885
- private resolvers;
1886
- private opening;
1887
- /** The start a dirty tree turned away, replayed once the user chooses how to go on. */
1888
- private blockedStart;
1889
- /** The dirty repos behind {@link blockedStart}, for the stash notice. */
1890
- private blockedRepos;
1904
+ /** The plan's isolation run and the open run's lifecycle (ADR-0013); see {@link IsolationRunController}. */
1905
+ private runs;
1906
+ /** A passed attempt's work onto the integration branch, and the conflict repair a landing may need. */
1907
+ private landing;
1891
1908
  private registry;
1892
1909
  private workspaceRootFn;
1893
1910
  private observers;
1894
1911
  private tddEnabled;
1895
- constructor(config: IConfig, notifications: INotification, terminalRunner: ITerminalRunner, store?: PlanStore, output?: TaskOutputSource, isolation?: IWorktreeIsolation);
1912
+ private readRunnerTransport;
1913
+ /**
1914
+ * The setting as the latest run copied it when it opened. Every spawn of
1915
+ * that run uses this, so flipping the setting mid-run changes the next run
1916
+ * only — the plan, not a live setting, says what runs (ADR-0001).
1917
+ */
1918
+ private planTransport;
1919
+ constructor(deps: TaskOrchestratorDeps);
1920
+ /**
1921
+ * The composition root: resolves every optional collaborator and wires the
1922
+ * isolation listener that the constructor cannot, because it needs the
1923
+ * instance's own (private) emit and lingering seams. Nothing else in the
1924
+ * codebase constructs an orchestrator, so this is the one place defaults
1925
+ * live — a host or test supplies what it cares about and gets the rest.
1926
+ */
1927
+ static compose(options: TaskOrchestratorOptions): TaskOrchestrator;
1896
1928
  /** Advisory silence timestamp for a task's live runner, or null if not idle. */
1897
1929
  getIdleSince(taskId: string): string | null;
1898
1930
  /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
1899
1931
  getLiveOutput(taskId: string, opts: LiveTailOptions): LiveTail | null;
1900
1932
  get storeInstance(): PlanStore;
1901
- setWorkspaceRoot(fn: () => string): void;
1902
- setWorkspaceEnvResolver(resolve: (cwd: string) => Promise<WorkspaceEnv>): void;
1903
1933
  /**
1904
1934
  * The variables a task's agent gets from its workspace. What cannot be
1905
1935
  * applied is said once — silently starting without them is how agents ran
1906
1936
  * under the wrong account when an edited `.envrc` was left unallowed.
1907
1937
  */
1908
1938
  private envForTask;
1909
- setRegistry(registry: RunnerRegistry): void;
1910
- /**
1911
- * A getter rather than a value where the caller has one: every task gets its
1912
- * prompt composed at spawn time, but only a full-plan run passes through a
1913
- * point where a snapshot could be refreshed — so "Run task", force-start and
1914
- * retry would compose against whatever the last run happened to set.
1915
- */
1916
- setTddEnabled(enabled: boolean | (() => boolean)): void;
1917
1939
  approveCheckpoint(taskId: string): void;
1918
1940
  rejectCheckpoint(taskId: string, reason?: string): void;
1941
+ private atCheckpoint;
1942
+ /**
1943
+ * Send a structured task a user message (ADR-0018, M1). Mid-turn it queues
1944
+ * behind the turn; a task waiting for input takes it at once and is back in
1945
+ * progress. Returns the message's id, for {@link removeQueuedTaskMessage}.
1946
+ */
1947
+ sendTaskMessage(taskId: string, text: string): string;
1948
+ /** Take back a message still queued behind a turn; false once it was delivered. */
1949
+ removeQueuedTaskMessage(taskId: string, id: string): boolean;
1950
+ /** Stop a structured task's running turn; it then waits for input like any turn that ends without its marker. */
1951
+ interruptTask(taskId: string): Promise<void>;
1952
+ /** What is waiting for a structured task's turn to end; empty for any other task. */
1953
+ getQueuedTaskMessages(taskId: string): QueuedTaskMessage[];
1954
+ private structuredSession;
1955
+ /**
1956
+ * A structured turn that ended without the done marker (ADR-0018, W1). No
1957
+ * verdict is guessed: the task waits for input, unless a checkpoint in the
1958
+ * same turn already has it waiting, or a queued message went straight out —
1959
+ * the session never passes through idle then, so nothing flickers.
1960
+ */
1961
+ private onTurnEnd;
1919
1962
  subscribe(observer: OrchestratorObserver): () => void;
1963
+ /**
1964
+ * Isolated per observer: one that throws must not take down the scheduler
1965
+ * or stop the remaining observers from hearing the event.
1966
+ */
1920
1967
  private emit;
1921
1968
  get isRunning(): boolean;
1922
1969
  /**
@@ -1945,41 +1992,32 @@ declare class TaskOrchestrator {
1945
1992
  get awaitingIsolationChoice(): boolean;
1946
1993
  /**
1947
1994
  * Take over a plan's persisted isolation, or none for a plan that has not
1948
- * isolated yet. Whatever a crashed process left behind for the run — a
1949
- * worktree still marked active, a directory no record owns — is pruned,
1950
- * while kept, failed and conflicted worktrees stay for the user.
1951
- *
1952
- * Deliberately silent on the observer: adopting is not a change to persist,
1953
- * and a host that adopts without persisting (VS Code's restore) would
1954
- * otherwise write a new session file on every reload.
1995
+ * isolated yet, pruning what a crashed process left behind. Silent on the
1996
+ * observer: adopting is not a change to persist.
1955
1997
  */
1956
1998
  adoptIsolation(state: PlanIsolation | null): Promise<void>;
1957
1999
  reviewRunDiff(): Promise<string>;
1958
- /**
1959
- * "Merge all": the run's integration branches into whatever the user has
1960
- * checked out, in every repo or none. Once everything merged, the run has
1961
- * nothing left to hand over, so it is cleared up and forgotten; a branch
1962
- * the user's HEAD somehow does not contain stays for the next run's sweep.
1963
- */
2000
+ /** "Merge all"; a run that merged everything is cleared up and forgotten. */
1964
2001
  mergeRun(): Promise<IsolationMergeResult>;
1965
- private clearMergedRun;
1966
2002
  /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
1967
2003
  cleanupRun(): Promise<void>;
1968
2004
  /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
1969
2005
  discardRun(): Promise<void>;
1970
2006
  private watchBlockingPrompts;
1971
- private closeLingering;
1972
- private closeLingeringOf;
1973
- private forgetRun;
1974
- private requireRun;
1975
2007
  /** What the plan persists of isolated execution; null when no run ever isolated. */
1976
2008
  get isolationRecord(): PlanIsolation | null;
2009
+ /** The transport the plan's latest run copied from the setting; null before its first run. */
2010
+ get runnerTransport(): RunnerTransport | null;
2011
+ /** Take over a saved plan's copied transport. The next run copies the setting afresh. */
2012
+ adoptRunnerTransport(transport: RunnerTransport | null): void;
1977
2013
  queueMessage(text: string): void;
1978
2014
  getQueuedMessages(): QueuedMessage[];
2015
+ /** Take one unsent message back out of the queue; false when it was never there (or already drained). */
2016
+ removeQueuedMessage(id: string): boolean;
1979
2017
  setQueuedMessages(messages: QueuedMessage[]): void;
1980
2018
  clearQueuedMessages(): void;
1981
2019
  processNextQueuedMessage(): QueuedMessage | null;
1982
- loadPlan(tasks: Task[], planRunners?: RunnerId[]): void;
2020
+ loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
1983
2021
  /**
1984
2022
  * Adopt an edited plan without letting go of the run in progress. Everything
1985
2023
  * the scheduler owns — live sessions, holds, retry counts, the verifier and
@@ -1999,38 +2037,27 @@ declare class TaskOrchestrator {
1999
2037
  private onVerdict;
2000
2038
  private afterVerdict;
2001
2039
  /**
2002
- * A repair's verdict decides only whether its work may try to land. It never
2003
- * replaces the verdict the task's own work earned, and a repair that does not
2004
- * land leaves the task waiting on the user — never a failed task, so never a
2005
- * halted run.
2040
+ * A repair's verdict never replaces the verdict the task's own work earned,
2041
+ * and a repair that does not land leaves the task waiting on the user —
2042
+ * never a failed task, so never a halted run.
2006
2043
  */
2007
2044
  private settleRepair;
2008
2045
  /**
2009
- * Evidence before the queue: the repair's work is committed and checked, and
2010
- * only then merged, through the same serialized landing as any other task —
2011
- * so a repair the tip has moved past again is a fresh conflict, not a pass.
2046
+ * Hand an attempt's verdict to Landing. The attempt stays live while that
2047
+ * settles — it may wait on the module's merge queue — so a cancel, retry or
2048
+ * stop in that window still wins, and waits for the merge before tearing the
2049
+ * worktree down; nothing counts the task as done before its work is on the
2050
+ * integration branch.
2012
2051
  */
2013
- private landRepair;
2014
- private landedRepair;
2015
- /** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
2016
- private unrepaired;
2017
- private describeEvidence;
2052
+ private land;
2053
+ /** Settle a task by how its landing went; `onLanded` is what the path that landed it adds to completing it. */
2054
+ private applyLanding;
2018
2055
  /**
2019
- * Merge a passed attempt's worktree. The attempt stays live while it waits on
2020
- * the module's merge queue, so a cancel, retry or stop in that window still
2021
- * wins, and nothing counts the task as done before its work is on the
2022
- * integration branch.
2056
+ * Work that did not land waits on the user — it never fails the task or
2057
+ * halts the run — unless its conflict is owed a repair, which takes the slot
2058
+ * the ending attempt freed and never one more than the run allows.
2023
2059
  */
2024
- private integrate;
2025
- /** A worktree whose work is not on the integration branch yet. */
2026
- private hasUnlandedWork;
2027
- private integrateWork;
2028
- /** Settle a task whose work passed, by what its integration reported. */
2029
- private landPassed;
2030
- private landUnmerged;
2031
- /** The repair a conflicted task is owed next; null when repair is off, used up, or there is no isolated run to repair it in. */
2032
- private nextRepair;
2033
- private noRepairReason;
2060
+ private settleUnlanded;
2034
2061
  /**
2035
2062
  * Mark which added task resolves which conflict. The resolver merges the
2036
2063
  * conflicted task's branch by hand in its own worktree; once that lands, the
@@ -2040,30 +2067,22 @@ declare class TaskOrchestrator {
2040
2067
  */
2041
2068
  linkConflictResolver(resolverId: string, conflictedId: string): void;
2042
2069
  private landResolved;
2043
- /**
2044
- * Let go of a task's worktree. `keep` leaves it and its branch for
2045
- * inspection; otherwise both go. A merge still in flight finishes first, so
2046
- * the worktree is never torn down underneath it.
2047
- */
2048
- private releaseWorktree;
2049
2070
  getReadyTasks(): Task[];
2050
- /**
2051
- * In an isolated run a dependency is met once its work is on the integration
2052
- * branch, not merely once it passed: the dependent's worktree is cut from
2053
- * that branch, so starting earlier would hand it a tree without the work it
2054
- * depends on.
2055
- */
2056
- private dependencyMet;
2057
- isBlocked(task: Task): boolean;
2058
2071
  /**
2059
2072
  * Cancel a running (or scheduled) task: kill its session and return it to
2060
2073
  * 'pending' — "not executed". The task is put on hold so the scheduler
2061
2074
  * doesn't immediately restart it; Retry / Force Start release the hold.
2075
+ *
2076
+ * The attempt's worktree is kept, as a stopped or failed one is: a runner is
2077
+ * often cancelled because it looked stuck after doing the work, and Mark
2078
+ * complete can still land that work. The next attempt replaces it.
2062
2079
  */
2063
2080
  cancelTask(taskId: string): Promise<void>;
2081
+ private cancelAttempt;
2064
2082
  /**
2065
2083
  * Let go of a task that is leaving the plan. A live runner is cancelled
2066
- * through {@link cancelTask}; a spawn still in flight just loses its attempt,
2084
+ * as {@link cancelTask} does, but its worktree goes: no task is left to land
2085
+ * it into. A spawn still in flight just loses its attempt,
2067
2086
  * which is what makes {@link startTask} kill the session it is about to
2068
2087
  * receive. The id's cross-attempt bookkeeping goes too — a hold or retry
2069
2088
  * count kept for a task that no longer exists would be inherited by nothing.
@@ -2081,6 +2100,15 @@ declare class TaskOrchestrator {
2081
2100
  markTaskIncomplete(taskId: string): Promise<void>;
2082
2101
  private logAndArchive;
2083
2102
  retryTask(taskId: string): Promise<void>;
2103
+ /**
2104
+ * Continue a finished structured task in its saved runner session (ADR-0018,
2105
+ * K1): a retry whose first turn is the user's message, sent to the session
2106
+ * the task ended in rather than a fresh one given its prompt again. Like a
2107
+ * retry it is a new attempt in a fresh worktree from the integration tip —
2108
+ * the same path, which is where the runner keeps its sessions — verified and
2109
+ * landed like any other, and dependents are left alone.
2110
+ */
2111
+ continueTask(taskId: string, message: string): Promise<void>;
2084
2112
  /**
2085
2113
  * Manually start a single AI task right now, bypassing dependency/readiness
2086
2114
  * gating (the "force start" affordance on a task card). Reuses the scheduler's
@@ -2091,9 +2119,9 @@ declare class TaskOrchestrator {
2091
2119
  forceStartTask(taskId: string): Promise<void>;
2092
2120
  /**
2093
2121
  * Run exactly one task outside full-plan scheduling. The active/starting
2094
- * session still contributes to isRunning so every surface exposes Stop and
2095
- * disables Execute Plan, but onVerdict cannot auto-schedule other tasks
2096
- * because the plan scheduler's `running` flag remains false.
2122
+ * session still contributes to {@link hasLiveWork} so every surface exposes
2123
+ * Stop and disables Execute Plan, but onVerdict cannot auto-schedule other
2124
+ * tasks because the plan scheduler's `running` flag remains false.
2097
2125
  */
2098
2126
  runTask(taskId: string): Promise<void>;
2099
2127
  getCompletedCount(): number;
@@ -2113,53 +2141,37 @@ declare class TaskOrchestrator {
2113
2141
  tick(): Promise<void>;
2114
2142
  private startTask;
2115
2143
  /**
2116
- * A spawn whose attempt ended while it was in flight: kill what it produced
2117
- * and take back the claim it made. Only the claim — whatever ended the
2118
- * attempt may have decided the task since (mark complete, cancel, retry).
2144
+ * The fallible half of starting a task: resolving its cwd/worktree and
2145
+ * spawning the runner. Resolves false when the attempt never committed —
2146
+ * abandoned because it was ended from under it, or a failed spawn already
2147
+ * unwound (both leave `startTask` with nothing further to announce).
2119
2148
  */
2120
- private abandonSpawn;
2121
- private repairPrompt;
2149
+ private spawnAttempt;
2122
2150
  /**
2123
- * The one place an attempt's working directory is decided. Async so a
2124
- * per-attempt workspace (worktree isolation, #12) can be prepared here.
2151
+ * Say on the task how this attempt is driven, when its plan asked for the
2152
+ * structured transport; a terminal plan's tasks record nothing. A structured
2153
+ * request that came back a terminal session is a fallback, and says why —
2154
+ * a host without a router included — never a silent downgrade.
2125
2155
  */
2126
- private resolveAttemptCwd;
2127
- private tell;
2128
- private reportCopies;
2156
+ private recordTransport;
2129
2157
  /**
2130
- * Open a run if none is: decide once, at its start, whether it executes in
2131
- * worktrees. `resume` is what a dirty tree parks until the user chooses how
2132
- * to go on. Resolves false when the run did not start.
2133
- */
2134
- private openRun;
2135
- /**
2136
- * Whether the run in force, or else the next one, gives each task its own
2137
- * worktrees, and of which repo group — what the planner is told, since it
2138
- * decides whether tasks on the same file have to be ordered and which shared
2139
- * paths two tasks must not edit at once. A tree that would block counts as
2140
- * not isolating: the user may yet run without isolation, and ordering is
2141
- * the safe rule then.
2158
+ * A continue whose runner never announced the session it was told to
2159
+ * resume. Nothing was continued, and nothing fresh was started in its place.
2142
2160
  */
2143
- plannerIsolation(): Promise<IsolatedExecution>;
2144
- private decideRun;
2145
- private activate;
2146
- /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
2147
- private sweep;
2161
+ private unresumed;
2148
2162
  /**
2149
- * The plan's run carries on while anything has landed on its integration
2150
- * branch: a resumed plan's dependents must start from a tip that holds their
2151
- * predecessors' work, and a fresh branch from the checked-out commit does not.
2163
+ * A spawn whose attempt ended while it was in flight: kill what it produced
2164
+ * and take back the claim it made. Only the claim — whatever ended the
2165
+ * attempt may have decided the task since (mark complete, cancel, retry).
2152
2166
  */
2153
- private continuableRun;
2167
+ private abandonSpawn;
2168
+ private tell;
2169
+ private say;
2154
2170
  /**
2155
- * A run with nothing landed holds only superseded attempts, so it goes whole.
2156
- * One that cannot be continued for another reason — it ran from a different
2157
- * workspace path — keeps its integration branch in each repo that has not
2158
- * merged it: only the user gives landed work up.
2171
+ * Whether the run in force, or else the next one, gives each task its own
2172
+ * worktrees, and of which repo group — what the planner is told.
2159
2173
  */
2160
- private mintRun;
2161
- /** Close the open run. An isolated one hands its integration branch over for review. */
2162
- private closeRun;
2174
+ plannerIsolation(): Promise<IsolatedExecution>;
2163
2175
  /**
2164
2176
  * Replay the start a dirty tree turned away. `stash` puts the user's tracked
2165
2177
  * changes on the git stash first, so the run isolates; `shared` runs this one
@@ -2176,12 +2188,20 @@ declare class TaskOrchestrator {
2176
2188
  * stop(), and if that exit reaches VerdictEngine under the still-valid
2177
2189
  * generation it delivers a verdict that marks the task 'completed' for one
2178
2190
  * tick — long enough for the scheduler to start a dependent task. A verdict
2179
- * leaves the runner up so its terminal stays readable, and stop/load reset
2180
- * the whole verifier themselves.
2191
+ * leaves a terminal runner up so its screen stays readable (a structured one
2192
+ * ends), and stop/load reset the whole verifier themselves.
2181
2193
  */
2182
2194
  private endAttempt;
2195
+ /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
2196
+ private saveNativeSession;
2183
2197
  private endAllAttempts;
2184
2198
  }
2199
+ /**
2200
+ * Build a {@link TaskOrchestrator}, filling in every collaborator a caller did
2201
+ * not inject. The one entry point for hosts, bench harnesses and tests alike;
2202
+ * {@link TaskOrchestrator} itself constructs nothing.
2203
+ */
2204
+ declare function createTaskOrchestrator(options: TaskOrchestratorOptions): TaskOrchestrator;
2185
2205
 
2186
2206
  /**
2187
2207
  * Everything needed to produce a plan from a goal. Carries the planning context
@@ -2312,6 +2332,14 @@ declare class ModelResolver {
2312
2332
  */
2313
2333
  getCachedRunnerModels(runner: string): DiscoveredModel[];
2314
2334
  getCachedPickerOptions(): OrchestratorOption[];
2335
+ /**
2336
+ * The planner model's context window, from whatever catalog is already
2337
+ * cached (#49): a vendor model comes from the picker catalog, a harness
2338
+ * planner's model from the runner's own discovered models. Reads only —
2339
+ * never triggers a fetch or discovery of its own, so an unknown window stays
2340
+ * unknown and context fill is omitted rather than guessed.
2341
+ */
2342
+ contextWindowFor(modelId: string): number | undefined;
2315
2343
  invalidate(): void;
2316
2344
  }
2317
2345
 
@@ -2490,6 +2518,11 @@ interface EnvAdmission {
2490
2518
  */
2491
2519
  declare function admitSettingsEnv(env: Record<string, unknown>): EnvAdmission;
2492
2520
 
2521
+ /** Whether a runner's tasks can run on the structured transport. */
2522
+ declare function supportsTaskMode(runner: string): boolean;
2523
+ /** The adapter that drives one task. Throws {@link TaskModeUnsupportedError} for a runner without a connector. */
2524
+ declare function createTaskAdapter(runner: string, deps: AgentProcessDeps): TaskModeAgentAdapter;
2525
+
2493
2526
  interface SpawnSpec {
2494
2527
  command: string;
2495
2528
  args: string[];
@@ -2541,6 +2574,8 @@ declare abstract class StdioAgentAdapter implements AgentAdapter {
2541
2574
  /** The environment the agent was spawned under, for any side process a handshake needs. */
2542
2575
  protected spawnEnv: NodeJS.ProcessEnv;
2543
2576
  private markEnded;
2577
+ /** What this process is to the user — a planner or a task's runner — for the words a failure is reported in. */
2578
+ protected role: 'planner' | 'task';
2544
2579
  constructor(deps: AgentProcessDeps);
2545
2580
  /** The command line that starts this agent in its read-only mode. */
2546
2581
  protected abstract spawnSpec(opts: AgentStartOptions): SpawnSpec;
@@ -2556,6 +2591,7 @@ declare abstract class StdioAgentAdapter implements AgentAdapter {
2556
2591
  start(opts: AgentStartOptions): Promise<void>;
2557
2592
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2558
2593
  nativeSessionId(): string | null;
2594
+ onProcessExit(listener: (code: number) => void): void;
2559
2595
  dispose(): void;
2560
2596
  /** Write one raw protocol line to the agent. */
2561
2597
  protected writeLine(payload: unknown): void;
@@ -2575,13 +2611,98 @@ declare abstract class StdioAgentAdapter implements AgentAdapter {
2575
2611
  * same way. It is the richest of the three streams — partial messages and
2576
2612
  * separate thinking blocks — which is why this agent went first.
2577
2613
  */
2578
- declare class ClaudeCodeAdapter extends StdioAgentAdapter {
2614
+ declare class ClaudeCodeAdapter extends StdioAgentAdapter implements TaskModeAgentAdapter {
2579
2615
  readonly agentId = "claude-code";
2616
+ private interruptCount;
2617
+ /** Interrupts sent and not yet acknowledged, by request id. */
2618
+ private readonly pendingControl;
2619
+ /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
2620
+ private interruptRequested;
2621
+ /** A task's tool requests still waiting for an answer, by request id, with what the answer echoes back. */
2622
+ private readonly openPermissions;
2580
2623
  /** Whether this turn has already emitted reply text — see {@link handleLine}. */
2581
2624
  private turnHasText;
2625
+ /** The text block streaming now follows earlier reply text, so its first delta opens the paragraph. */
2626
+ private pendingBreak;
2627
+ /** The model answering the planner's current message, as its `message_start` named it. */
2628
+ private plannerModel;
2629
+ /**
2630
+ * The session's `total_cost_usd` as last reported. Undefined after a resume:
2631
+ * the CLI restores the resumed session's running total, and what Ordewell
2632
+ * already counted of it is not ours to know here.
2633
+ */
2634
+ private reportedCostUsd;
2635
+ /**
2636
+ * Subagents started and not yet finished, keyed by the `Agent` call's id.
2637
+ * Kept across turns: a backgrounded one reports after its turn has ended.
2638
+ */
2639
+ private readonly openSubagents;
2640
+ /** Subagent messages already counted. A message arrives as one line per content block, each repeating its usage. */
2641
+ private readonly countedSubagentMessages;
2642
+ /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
2643
+ private pendingResume;
2582
2644
  protected spawnSpec(opts: AgentStartOptions): SpawnSpec;
2645
+ /**
2646
+ * A task's run: the manifest decides what its mode and effort mean
2647
+ * (ADR-0001), and this adds only the protocol around them. No tool list and
2648
+ * no system prompt — the task's prompt is its first turn, as on the terminal
2649
+ * transport. `--permission-prompt-tool stdio` routes the questions the mode
2650
+ * leaves open to the control channel, where the adapter must answer them;
2651
+ * without it `-p` refuses them silently and nothing can ever surface one.
2652
+ */
2653
+ private taskSpawnSpec;
2654
+ /**
2655
+ * Claude Code's soft interrupt: the turn stops, the process and its session
2656
+ * stay. The CLI acknowledges on the control channel, then closes the turn
2657
+ * with an `error_during_execution` result, which {@link handleLine} reports
2658
+ * as an interrupted `turn_end`.
2659
+ */
2660
+ interrupt(timeoutMs: number): Promise<boolean>;
2661
+ /**
2662
+ * A `--resume` the CLI cannot find is answered at once with an error result
2663
+ * and no `init`, after which it waits on stdin for input it never reads — a
2664
+ * turn sent to it would hang. Closing stdin lets it exit. The id it echoes is
2665
+ * not recorded: no session was taken up, and a continue must not offer it again.
2666
+ */
2667
+ private refuseResume;
2668
+ /**
2669
+ * The answer to an open tool request, in the shape the CLI validates: an
2670
+ * allow echoes the call's own input (an absent one is warned about and
2671
+ * replaced), and "for this task" hands back Claude's own suggestions — never
2672
+ * a grant of Ordewell's making.
2673
+ */
2674
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
2583
2675
  protected turnPayload(message: string): string;
2584
2676
  protected handleLine(line: string, emit: (event: AgentEvent) => void): void;
2677
+ private startSubagent;
2678
+ /**
2679
+ * A subagent's messages do not stream, so each line's usage is the snapshot
2680
+ * taken before generation: the prompt is real, the output a placeholder.
2681
+ * Only the prompt side is reported — an absent output count reads as "not
2682
+ * reported", a placeholder would read as a measurement.
2683
+ */
2684
+ private handleSubagentMessage;
2685
+ /**
2686
+ * The `Agent` call returned, so the subagent is done. Its last call — the
2687
+ * report — never appears as a line of its own; the result carries its
2688
+ * complete usage instead.
2689
+ */
2690
+ private finishForegroundSubagent;
2691
+ /**
2692
+ * Partial output of the planner's own message. The complete `assistant` line
2693
+ * for each block follows its deltas and is authoritative for that block (see
2694
+ * {@link AgentEvent}), so nothing here has to reconcile with it.
2695
+ */
2696
+ private handleStreamEvent;
2697
+ /**
2698
+ * What only the result line knows: the cost, which covers every call the
2699
+ * session made — subagents included, since none of their lines carries one —
2700
+ * and the planner model's window. `total_cost_usd` is a running total, so a
2701
+ * turn reports its growth. The first turn after a resume only sets the
2702
+ * baseline: its total includes turns counted before, and a turn's own share
2703
+ * cannot be told apart from them.
2704
+ */
2705
+ private reportSessionCost;
2585
2706
  }
2586
2707
 
2587
2708
  /**
@@ -2601,6 +2722,8 @@ declare class ClaudeCodeAdapter extends StdioAgentAdapter {
2601
2722
  declare class CodexAdapter extends StdioAgentAdapter {
2602
2723
  readonly agentId = "codex";
2603
2724
  private threadId;
2725
+ /** The model Codex opened the thread with — usage records name it. */
2726
+ private threadModel;
2604
2727
  private nextRequestId;
2605
2728
  private settleHandshake;
2606
2729
  private handshakeError;
@@ -2610,6 +2733,14 @@ declare class CodexAdapter extends StdioAgentAdapter {
2610
2733
  private resumeAttempted;
2611
2734
  private resumeFallbackSent;
2612
2735
  private sandbox;
2736
+ /**
2737
+ * Subagent threads this session has spawned, keyed by the child thread id.
2738
+ * Codex runs a subagent in its own thread and replays both threads' events on
2739
+ * one stream; the thread id is what tells them apart (see {@link subagentOf}).
2740
+ */
2741
+ private readonly subagents;
2742
+ /** Codex only plans for now (#54): its tasks stay on the terminal transport. */
2743
+ start(opts: AgentStartOptions): Promise<void>;
2613
2744
  protected spawnSpec(opts: AgentStartOptions): SpawnSpec;
2614
2745
  /**
2615
2746
  * `initialize`, then `thread/start`, both before the first user message. The
@@ -2628,6 +2759,26 @@ declare class CodexAdapter extends StdioAgentAdapter {
2628
2759
  private startThread;
2629
2760
  protected turnPayload(message: string): string;
2630
2761
  protected handleLine(line: string, emit: (event: AgentEvent) => void): void;
2762
+ /**
2763
+ * The child thread id when `threadId` names a subagent's thread, or undefined
2764
+ * for the planner's own. Codex replays both threads on one stream and only
2765
+ * the thread id separates them; the first time a thread is seen it is
2766
+ * registered so its usage and steps can be tagged with it as a subagent id.
2767
+ */
2768
+ private subagentOf;
2769
+ /**
2770
+ * One usage record per model call, from `thread/tokenUsage/updated`.
2771
+ *
2772
+ * The notification carries two breakdowns: `total` is cumulative for the
2773
+ * thread and `last` is the model call that just finished (verified against
2774
+ * the installed binary — a two-call turn ends with `last` equal to the second
2775
+ * call and `total` equal to both summed). Emitting `total` on each update
2776
+ * would count every earlier call again, so `last` is the record. Mapping
2777
+ * `last.outputTokens` already includes `reasoningOutputTokens` — the thread
2778
+ * total is `inputTokens + outputTokens`, not a sum of three — so reasoning is
2779
+ * not added a second time. Codex reports no price.
2780
+ */
2781
+ private emitUsage;
2631
2782
  /**
2632
2783
  * Refuse one server→client request. Requests whose result schema can express
2633
2784
  * a refusal get that payload; everything else — a permission grant, a
@@ -2639,6 +2790,17 @@ declare class CodexAdapter extends StdioAgentAdapter {
2639
2790
  /** A tool item entering `inProgress` — announce the call so the timeline moves. */
2640
2791
  private emitItemStart;
2641
2792
  private emitItemDone;
2793
+ /**
2794
+ * A `collabAgentToolCall` — the planner spawning, waiting on or messaging a
2795
+ * subagent. The call itself is planner-level tool activity; the lifecycle it
2796
+ * carries becomes `subagent_started` / `subagent_finished`, restated as
2797
+ * often as Codex restates it — the service reports each once. Codex tags
2798
+ * every collab item with the parent thread, so this one never runs for a
2799
+ * subagent.
2800
+ */
2801
+ private emitCollabItem;
2802
+ /** The readable result of a collab call: the brief it sent, or what came back. */
2803
+ private collabSummary;
2642
2804
  }
2643
2805
 
2644
2806
  /**
@@ -2650,10 +2812,12 @@ declare class CodexAdapter extends StdioAgentAdapter {
2650
2812
  * talks to it through the injected `fetch`. Both are part of the one seam the
2651
2813
  * tests drive.
2652
2814
  *
2653
- * The turn ends when the message POST resolves. Live events stream from the
2654
- * server's `/event` channel, but the POST is what settles the turn: an event
2655
- * name that changes between OpenCode versions then costs liveness, not
2656
- * correctness.
2815
+ * The turn ends when the message POST resolves, and its response is the
2816
+ * authoritative copy of the reply's last message. Everything else — reply text
2817
+ * as it streams, earlier model calls' text and reasoning, per-call usage, the
2818
+ * subagents a `task` call runs — arrives only on the server's `/event` channel.
2819
+ * An event name that changes between OpenCode versions therefore costs that
2820
+ * detail, never the final reply.
2657
2821
  */
2658
2822
  declare class OpenCodeAdapter implements AgentAdapter {
2659
2823
  private deps;
@@ -2670,14 +2834,20 @@ declare class OpenCodeAdapter implements AgentAdapter {
2670
2834
  /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
2671
2835
  private lastAssistantId;
2672
2836
  constructor(deps: AgentProcessDeps);
2673
- start(opts: AgentStartOptions): Promise<void>;
2837
+ /** OpenCode only plans for now (#55): its tasks stay on the terminal transport. */
2838
+ start(options: AgentStartOptions): Promise<void>;
2674
2839
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2675
2840
  /**
2676
2841
  * Turn one settled assistant message into events. The settled response is
2677
2842
  * authoritative: it names the assistant message, so its parts are the ones
2678
- * that make up the reply. Tool parts already seen live are deduplicated by
2679
- * call id; anything the stream missed (including a stream that never
2680
- * connected) arrives here.
2843
+ * that make up the reply. Parts already completed live are deduplicated;
2844
+ * anything the stream missed (including a stream that never connected)
2845
+ * arrives here.
2846
+ *
2847
+ * It is only the turn's *last* message, though. OpenCode writes one
2848
+ * assistant message per model call, so the calls before the final one —
2849
+ * their text, reasoning and usage — reach Ordewell over the stream or not at
2850
+ * all.
2681
2851
  */
2682
2852
  private settle;
2683
2853
  /**
@@ -2694,11 +2864,39 @@ declare class OpenCodeAdapter implements AgentAdapter {
2694
2864
  private recoverReply;
2695
2865
  private newestAssistantId;
2696
2866
  /**
2697
- * Emit one message part, once. OpenCode reports a tool part repeatedly as it
2698
- * moves through pending → running → completed, so parts are keyed by id and
2699
- * only the terminal state produces a result.
2867
+ * Emit one complete message part, once. OpenCode reports a tool part
2868
+ * repeatedly as it moves through pending → running → completed, so parts are
2869
+ * keyed by id and only the terminal state produces a result. A subagent's
2870
+ * text is its report to the planner, not the reply, so it is dropped; the
2871
+ * `task` call's result carries it.
2700
2872
  */
2701
2873
  private emitPart;
2874
+ /**
2875
+ * A `task` call runs a subagent in a child session. The call's part names
2876
+ * that session once it exists, which is what ties the child's frames to the
2877
+ * call; the subagent ends when the call does. The part is restated at every
2878
+ * status change, and so is what it says here — the service reports each
2879
+ * start and finish once, and no finish for a call that never had a child.
2880
+ */
2881
+ private trackSubagent;
2882
+ /**
2883
+ * One message's usage, once, when it has completed. Every assistant message
2884
+ * is one model call; until it completes its counts are zeros.
2885
+ */
2886
+ private countUsage;
2887
+ /**
2888
+ * One `/event` frame. Only the planner's session and its children are
2889
+ * followed: the server's stream is global, and another client's session is
2890
+ * none of this turn's business.
2891
+ */
2892
+ private onFrame;
2893
+ /**
2894
+ * Stream one piece of a reply text part. The part's paragraph break goes out
2895
+ * with its first visible delta, so the deltas add up to exactly the text the
2896
+ * completed part then re-sends; a part that is only whitespace so far is
2897
+ * held back, for the reason {@link emitPart} drops one.
2898
+ */
2899
+ private onTextDelta;
2702
2900
  /**
2703
2901
  * Deny one permission request (T1). OpenCode blocks the turn until the
2704
2902
  * request is answered, so this must answer — `reject` rather than a silent
@@ -2708,8 +2906,8 @@ declare class OpenCodeAdapter implements AgentAdapter {
2708
2906
  */
2709
2907
  private denyPermission;
2710
2908
  /**
2711
- * Server-sent events from `/event`. Tool activity on it is liveness only —
2712
- * the settled POST repeats it — but permission requests arrive nowhere else,
2909
+ * Server-sent events from `/event`: the turn's live text, reasoning, tool
2910
+ * activity and usage, and the only channel permission requests arrive on —
2713
2911
  * so the stream is load-bearing for {@link denyPermission}.
2714
2912
  */
2715
2913
  private streamEvents;
@@ -2737,6 +2935,52 @@ declare function mapAgentTool(name: string): MappedTool;
2737
2935
  */
2738
2936
  declare function normalizeAgentArgs(tool: ResearchToolType, args: Record<string, unknown>): Record<string, unknown>;
2739
2937
 
2938
+ /** The dialogue record a fork carries — what {@link PlannerConversation.clone} returns. */
2939
+ interface ForkedDialogue {
2940
+ conversationHistory: ConversationMessage[];
2941
+ researchLog: ResearchLogEntry[];
2942
+ }
2943
+
2944
+ /** Everything (re)opening a conversation needs besides the dialogue itself. */
2945
+ type ConversationOpening = Omit<ConversationRequest, 'goal' | 'onProgress' | 'signal' | 'priorHistory' | 'initialMessage'>;
2946
+ /**
2947
+ * What the conversation needs from the session that hosts it. The session keeps
2948
+ * plan state, persistence and scheduling; the conversation reaches them only
2949
+ * through here, so a fake host is enough to drive it.
2950
+ */
2951
+ interface PlannerConversationHost {
2952
+ /** The plan the transcript lives on (`conversationHistory`, `researchLog`). */
2953
+ plan(): LegacyPlanState | null;
2954
+ goal(): string;
2955
+ aiService(): IAiService;
2956
+ onProgress(progress: ResearchProgress): void;
2957
+ /** Fresh discovery, allowlist-filtered the way the system prompt shows it. */
2958
+ opening(runners: RunnerId[]): Promise<ConversationOpening>;
2959
+ /** What the per-turn catalog block and every read draw from, as of now. */
2960
+ catalog(): TaskQueryCatalog;
2961
+ tasks(): readonly Task[];
2962
+ /**
2963
+ * The orchestrator's live capture for a task's latest attempt, backing the
2964
+ * `output` field of a read. Injected the same way as the catalog so the
2965
+ * conversation never reaches into execution state directly.
2966
+ */
2967
+ liveOutput: LiveOutputLookup;
2968
+ hasLiveWork(): boolean;
2969
+ /** The session's single mutation ritual: op → persist → notify (default: the plan). */
2970
+ mutate(op: () => boolean, notify?: () => void): LegacyPlanState | null;
2971
+ broadcast: SessionBroadcaster;
2972
+ /** `turnId`: the planner turn whose commit this is, when one is. */
2973
+ broadcastPlan(turnId?: string): void;
2974
+ /** Validate a batch against live state. Pure — nothing is applied. */
2975
+ validateOps(ops: TaskOp[]): ApplyTaskOpsResult;
2976
+ /** Load planner-produced tasks: an edit keeps run state, a commit starts over. Returns how many landed. */
2977
+ adoptTasks(tasks: readonly Task[], how: 'edit' | 'commit'): number;
2978
+ capturePrd(text: string): void;
2979
+ /** Park a structural edit until the next batch boundary. Returns the queue length. */
2980
+ queueEdit(userMessage: string): number;
2981
+ /** Wake a scheduler an applied edit may have unblocked. */
2982
+ afterEdit(): Promise<void>;
2983
+ }
2740
2984
  /**
2741
2985
  * A conversation edit (rewind, fork) the conversation refused because the
2742
2986
  * request itself is wrong — no such message, nothing to fork. Transport-agnostic
@@ -2763,12 +3007,232 @@ interface RewindTarget {
2763
3007
  content: string;
2764
3008
  timestamp: string;
2765
3009
  }
3010
+ /** What a rewind forks from, and the message it lands just before. */
3011
+ interface RewoundDialogue {
3012
+ dialogue: ForkedDialogue;
3013
+ rewoundMessage: string;
3014
+ }
2766
3015
  /** What a compaction left behind. */
2767
3016
  interface ConversationCompaction {
2768
3017
  summary: string;
2769
3018
  /** Transcript entries kept verbatim after the summary entry. */
2770
3019
  keptMessages: number;
2771
3020
  }
3021
+ /** A point a failed turn returns the dialogue to. Opaque outside this module. */
3022
+ interface TranscriptSnapshot {
3023
+ readonly plan: LegacyPlanState;
3024
+ readonly history: ConversationMessage[] | undefined;
3025
+ readonly researchLog: ResearchLogEntry[] | undefined;
3026
+ readonly persisted: number;
3027
+ readonly savedInBackground: number;
3028
+ }
3029
+ interface ReplyOptions {
3030
+ signal?: AbortSignal;
3031
+ /** The user's words before skill expansion — what a mid-run queue replays later. */
3032
+ verbatim?: string;
3033
+ }
3034
+ /**
3035
+ * The planner conversation (ADR-0002), end to end: the persisted dialogue
3036
+ * record, the live model context behind it, and every turn from the user's
3037
+ * message to a settled, persisted outcome.
3038
+ *
3039
+ * It is the only writer of `conversationHistory` and of the planner's
3040
+ * `researchLog`. The writes land on the host's plan object — which is what the
3041
+ * session persists and broadcasts — but only ever inside the host's mutation
3042
+ * ritual or ahead of a turn that will either reach it or be rolled back.
3043
+ *
3044
+ * The live model context (a vendor service's message list, a harness planner's
3045
+ * process and native session id) is disposable: {@link reset} drops it and the
3046
+ * next turn is replayed from the transcript. That is what lets the transcript
3047
+ * be replaced by a summary, or copied into a fork, without the model's memory
3048
+ * drifting from it.
3049
+ */
3050
+ declare class PlannerConversation {
3051
+ private readonly host;
3052
+ /** Bumped on every persist, so a rollback can tell whether its writes already reached disk. */
3053
+ private persisted;
3054
+ /** Saves an execution event made while a turn may be in flight; see {@link restore}. */
3055
+ private savedInBackground;
3056
+ private turnsInFlight;
3057
+ private compacting;
3058
+ private openTurnId;
3059
+ constructor(host: PlannerConversationHost);
3060
+ get transcript(): readonly ConversationMessage[];
3061
+ /** Whether a user turn is between its transcript append and its settled outcome. */
3062
+ get isTurnInFlight(): boolean;
3063
+ /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
3064
+ get currentTurnId(): string | undefined;
3065
+ /** Whether the model still holds this conversation in memory. */
3066
+ get isActive(): boolean;
3067
+ append(role: ConversationMessage['role'], content: string, opts?: {
3068
+ timestamp?: string;
3069
+ kind?: ConversationMessage['kind'];
3070
+ }): void;
3071
+ /** Swap the whole transcript. The live context no longer matches it, so pair with {@link reset}. */
3072
+ replace(messages: readonly ConversationMessage[]): void;
3073
+ snapshot(): TranscriptSnapshot | null;
3074
+ /**
3075
+ * Put the dialogue back where {@link snapshot} found it — unless anything
3076
+ * was persisted since, because then memory already matches disk and every
3077
+ * surface, and undoing it would erase work the user has seen land. Also a
3078
+ * no-op once a different plan has been adopted.
3079
+ *
3080
+ * A background save (a task settling mid-turn) wrote the turn's writes to
3081
+ * disk without landing anything the user saw, so it does not stop the undo;
3082
+ * the undo is saved in turn, or a reload would bring the writes back.
3083
+ */
3084
+ restore(snapshot: TranscriptSnapshot): boolean;
3085
+ /**
3086
+ * The host calls this after every persist. `background` is a save no call
3087
+ * of the user's made — an execution event landing mid-turn.
3088
+ */
3089
+ markPersisted(opts?: {
3090
+ background?: boolean;
3091
+ }): void;
3092
+ /**
3093
+ * Drop the live model context. Idempotent; the harness backend holds an OS
3094
+ * process here, so callers never gate it on {@link isActive}.
3095
+ */
3096
+ reset(): void;
3097
+ /**
3098
+ * The user messages a rewind may land before. The opening message is the
3099
+ * goal: cutting it leaves a conversation about nothing, which is a new
3100
+ * session, not a rewind. After a compaction the summary entry, always
3101
+ * first, plays the goal's part: what it replaced is gone, so a rewind stops
3102
+ * at it.
3103
+ */
3104
+ rewindTargets(): RewindTarget[];
3105
+ /**
3106
+ * Replace the transcript with a summary of it, keeping the last two
3107
+ * exchanges as they were, and drop the live context so the next message
3108
+ * replays from the shorter record.
3109
+ *
3110
+ * The summary is one hidden turn through whichever planner is configured —
3111
+ * on the live context when it still matches, replayed from the transcript
3112
+ * when not — so the compaction is the same for a vendor API and a harness
3113
+ * agent. Whatever the turn emits besides the summary is discarded, ops
3114
+ * included: it condenses the conversation, never the plan. Nothing is
3115
+ * written until the summary is in hand, so a failed or stopped turn leaves
3116
+ * the transcript exactly as it was.
3117
+ */
3118
+ compact(signal?: AbortSignal): Promise<ConversationCompaction>;
3119
+ private summarize;
3120
+ /**
3121
+ * A copy of the dialogue record for a forked session to carry. Refused
3122
+ * mid-turn: the copy would hold the user's message without the reply to it.
3123
+ * The live context is not part of it — the fork replays from this record on
3124
+ * its first turn, like any adopted session.
3125
+ */
3126
+ clone(): ForkedDialogue;
3127
+ /**
3128
+ * A copy of the dialogue as it stood just before the user message at
3129
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
3130
+ * from — plus that message's full text, for a surface to offer back. This
3131
+ * conversation is not touched. The research trace is cut at the same point
3132
+ * by time: its entries carry no link to the message that caused them.
3133
+ */
3134
+ cloneBefore(index: number): RewoundDialogue;
3135
+ /**
3136
+ * Queued mid-run edits applied between batches. The user's message is
3137
+ * already in the transcript from when it was queued; this records that it
3138
+ * finally took effect, so a replay does not read the plan as never changed.
3139
+ * Call inside the host's mutation ritual.
3140
+ */
3141
+ recordQueuedEdits(messages: string[], taskCount: number): void;
3142
+ /**
3143
+ * Queued edits the between-batches drain could not apply. Recorded so the
3144
+ * transcript does not go on promising a change that never landed. Call
3145
+ * inside the host's mutation ritual.
3146
+ */
3147
+ recordQueuedEditsFailed(messages: string[], reason: string): void;
3148
+ /** Open the conversation on a fresh plan: the goal is its first message. */
3149
+ start(goal: string, opening: ConversationOpening, signal?: AbortSignal): Promise<LegacyPlanState>;
3150
+ /**
3151
+ * Every later user message, from the transcript append to a persisted
3152
+ * outcome. A turn that throws before anything was persisted takes its own
3153
+ * writes back out, so session memory never drifts from disk and the UI.
3154
+ */
3155
+ reply(message: string, options?: ReplyOptions): Promise<LegacyPlanState>;
3156
+ private replyTurn;
3157
+ /**
3158
+ * Whether a settled structural edit reaches work a runner is executing. Only
3159
+ * these are queued: a whole-plan commit replaces the plan and would reset the
3160
+ * run, and a task-ops batch that names an in-progress task changes it under
3161
+ * the runner. An add, or an edit to any other task, is reconciled into the
3162
+ * plan in place while the running batch keeps going.
3163
+ */
3164
+ private editTouchesLiveWork;
3165
+ private assertIdle;
3166
+ private inTurn;
3167
+ /**
3168
+ * Bracket one user turn with its start and end, under an id minted here:
3169
+ * the turn is where the stream a surface draws begins and ends, and only the
3170
+ * conversation sees all of it — every backend call, read and retry.
3171
+ */
3172
+ private userTurn;
3173
+ private requirePlan;
3174
+ private recordUser;
3175
+ private recordResearch;
3176
+ /**
3177
+ * Reopen the model context from the transcript — because the in-memory one
3178
+ * is gone (session reload, extension restart), or because it is stale
3179
+ * against the planner config now in effect. No LLM call happens for the
3180
+ * replayed turns; the first call is the one the user's message opens.
3181
+ */
3182
+ private resume;
3183
+ /**
3184
+ * Answer every read the planner emits until it says something else.
3185
+ *
3186
+ * The channel is a text envelope rather than a registered tool because the
3187
+ * protocol has to be identical on both planner backends (ADR-0009): Ordewell
3188
+ * owns a tool loop only in the API case, and a harness planner running as a
3189
+ * coding-agent subprocess can only be reached this way.
3190
+ *
3191
+ * Draining here — outside {@link repairLoop} — is what keeps reads free of
3192
+ * the repair budget. A planner that looks a task up and *then* fumbles its
3193
+ * ops JSON still gets its two corrective retries; charging it for the read
3194
+ * would cost it the chance to fix the edit.
3195
+ */
3196
+ private drainTaskQueries;
3197
+ /**
3198
+ * Render one read out of live state. Never persisted to the transcript: the
3199
+ * detail is context for the planner's next reply, and re-sending it on every
3200
+ * later turn is exactly the cost this channel exists to avoid.
3201
+ */
3202
+ private taskQueryAnswer;
3203
+ /**
3204
+ * Drive a planner turn to a persisted, broadcast outcome. Task edits apply
3205
+ * atomically; validation failures are fed back to the model for up to 2
3206
+ * silent retries, then surfaced as a message with the plan untouched. The
3207
+ * first turn and every later turn route through here — one path, not two.
3208
+ */
3209
+ private settle;
3210
+ /** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
3211
+ private applyTaskOps;
3212
+ /** Commit a settled (non-task_ops) turn through the host's mutation ritual. */
3213
+ private commit;
3214
+ /**
3215
+ * The catalog the planner may actually draw from — model ids and task-mode
3216
+ * ids, per runner in the plan — emitted on EVERY turn, before any plan exists
3217
+ * or after. The system prompt shows this once at conversation start; a long
3218
+ * clarifying conversation outlives that single showing and the planner
3219
+ * starts misquoting it, so this re-states it per turn instead.
3220
+ *
3221
+ * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
3222
+ * hard bound on every turn, and reads the plan's runners live, so a runner
3223
+ * admitted mid-session by a retarget is shown like every other.
3224
+ */
3225
+ private catalogBlock;
3226
+ /**
3227
+ * The "you are here" block for post-plan chat: current tasks with stable
3228
+ * references, plus the read and edit protocols. Injected per turn (never
3229
+ * persisted) so the model always sees live statuses — including which tasks
3230
+ * are locked by a running execution.
3231
+ */
3232
+ private planContextBlock;
3233
+ /** One line per task with stable references — null until the plan has tasks. */
3234
+ private currentPlanLines;
3235
+ }
2772
3236
 
2773
3237
  declare const BUILTIN_SKILL_NAMES: readonly ["grilling", "to-spec", "improve-codebase-architecture"];
2774
3238
  interface SkillMetadata {
@@ -2803,6 +3267,126 @@ declare class SkillsService {
2803
3267
  }
2804
3268
  declare function createSkillsService(workspaceRoot?: string): SkillsService;
2805
3269
 
3270
+ /** The `planner_usage` member of the session broadcast union. */
3271
+ type PlannerUsageMessage = Extract<SessionMessage, {
3272
+ type: 'planner_usage';
3273
+ }>;
3274
+ /**
3275
+ * The planner's session usage ledger (#49), owned by Session. Consumes the
3276
+ * `usage` progress events a backend emits per model call, keeps the running
3277
+ * session and per-subagent totals, and renders the `planner_usage` message.
3278
+ * Session persists {@link snapshot} with the plan and restores it on load, so a
3279
+ * reopened session shows its totals again. The accumulation itself lives in
3280
+ * `models/Usage` — the one place the sum's currency-per-currency rule is
3281
+ * defined — this module just holds the state and the broadcast shape.
3282
+ */
3283
+ declare class PlannerUsageLedger {
3284
+ private usage;
3285
+ /** Fold one model call's record into the totals; returns the running ledger. */
3286
+ record(record: UsageRecord): PlannerUsage;
3287
+ /** Adopt a persisted ledger (a reopened session) or start from zero. */
3288
+ restore(usage: PlannerUsage | undefined): void;
3289
+ /** Start a fresh session's ledger from zero. */
3290
+ clear(): void;
3291
+ /** Whether anything has been recorded — a plan with no usage says nothing. */
3292
+ get hasUsage(): boolean;
3293
+ /** The value persisted onto the plan state. */
3294
+ snapshot(): PlannerUsage;
3295
+ /** The broadcast message for the totals as they stand now. */
3296
+ message(turnId?: string): PlannerUsageMessage;
3297
+ }
3298
+
3299
+ interface SessionEventRelayDeps {
3300
+ broadcast: SessionBroadcaster;
3301
+ /** Where isolation notices go, for a host whose notifications are not seen by the user. */
3302
+ onNotice?: (notice: SessionNotice) => void;
3303
+ store: Pick<PlanStore, 'allTasks' | 'snapshot'>;
3304
+ orchestrator: Pick<TaskOrchestrator, 'getIdleSince' | 'getTaskIsolation' | 'getQueuedTaskMessages'>;
3305
+ /** Shared with the Session, which snapshots, restores and clears it. */
3306
+ usage: PlannerUsageLedger;
3307
+ /** How many of a task's runner requests wait for an answer (ADR-0018, A1). */
3308
+ awaitingApproval?: (taskId: string) => number;
3309
+ }
3310
+ /** Every orchestrator event the relay announces; `onTaskSettled` is persistence only, so it is the Session's. */
3311
+ type RelayObserver = Required<Omit<OrchestratorObserver, 'onTaskSettled'>>;
3312
+ /**
3313
+ * Turns orchestrator and planner events into {@link SessionMessage}
3314
+ * broadcasts — the one place a surface's view of a Session is produced. It
3315
+ * never persists: where an event also owes a save, the Session does that
3316
+ * before handing the event on, so no surface sees state the disk lacks.
3317
+ */
3318
+ declare class SessionEventRelay {
3319
+ private readonly broadcast;
3320
+ private readonly onNotice?;
3321
+ private readonly store;
3322
+ private readonly orchestrator;
3323
+ private readonly usage;
3324
+ private readonly awaitingApproval;
3325
+ /**
3326
+ * The in-flight turn's subagent activity, grouped one run per subagent so a
3327
+ * replay nests each step under its own brief/result. Folded into the plan's
3328
+ * researchLog at persist — see {@link flushSubagentRuns}.
3329
+ */
3330
+ private pendingSubagents;
3331
+ private statusHeld;
3332
+ private statusOwed;
3333
+ constructor(deps: SessionEventRelayDeps);
3334
+ /** `plan` is read per event: a Session with no plan (or mid-reset) announces nothing about tasks. */
3335
+ observer(plan: () => LegacyPlanState | null): RelayObserver;
3336
+ status(plan: LegacyPlanState | null): void;
3337
+ /**
3338
+ * Run `op` with status broadcasts held back. Store ops signal the observer
3339
+ * as they go, which would put a status_update on the wire before the
3340
+ * persist the caller owes — surfaces must never see plan state the disk
3341
+ * does not have yet. {@link releaseStatus} sends the one held back.
3342
+ */
3343
+ holdStatus<T>(op: () => T): T;
3344
+ releaseStatus(plan: LegacyPlanState | null): void;
3345
+ executionComplete(plan: LegacyPlanState | null): void;
3346
+ /** `turnId`: the planner turn whose commit this is, when one is. */
3347
+ planGenerated(plan: LegacyPlanState | null, goal: string, turnId?: string): void;
3348
+ progress(progress: ResearchProgress): void;
3349
+ /** The run for `subagentId`, created on first sighting so a step arriving
3350
+ * before (or without) its started event still gets a home. */
3351
+ private subagentRun;
3352
+ /**
3353
+ * Fold the turn's subagent runs into the plan's researchLog as one contiguous
3354
+ * group per subagent — its entry then its steps, in the order they started —
3355
+ * so a replay nests each step under its own subagent however the live stream
3356
+ * interleaved. A harness planner already logs its child steps through the
3357
+ * turn's researchLog; they are pulled out of that position and re-grouped
3358
+ * rather than duplicated.
3359
+ */
3360
+ flushSubagentRuns(plan: LegacyPlanState): void;
3361
+ /** Forget a turn's subagent runs that will never be persisted. */
3362
+ dropSubagentRuns(): void;
3363
+ }
3364
+
3365
+ /** Which session's logs, in which workspace. */
3366
+ interface TaskLogLocation {
3367
+ baseDir: string;
3368
+ sessionId: string;
3369
+ }
3370
+ /** One attempt's file, open for appending. */
3371
+ interface TaskLogFile {
3372
+ readonly attempt: number;
3373
+ append(events: readonly TaskLogEvent[]): void;
3374
+ }
3375
+ /** The attempts a task has a log for, oldest first. */
3376
+ declare function listTaskLogAttempts(location: TaskLogLocation, taskId: string): number[];
3377
+ /**
3378
+ * Start the next attempt's file. Numbered from what is on disk rather than
3379
+ * from the orchestrator's count, which restarts at one whenever a plan is
3380
+ * loaded and would overwrite the attempts before it.
3381
+ */
3382
+ declare function openTaskLog(location: TaskLogLocation, taskId: string): TaskLogFile;
3383
+ /**
3384
+ * One attempt's events, in order; empty when it has no log. A line that does
3385
+ * not parse is skipped — the last one, most likely, cut off by a crash
3386
+ * mid-write — so what was saved before it still reads.
3387
+ */
3388
+ declare function readTaskLog(location: TaskLogLocation, taskId: string, attempt: number): TaskLogEvent[];
3389
+
2806
3390
  /**
2807
3391
  * A direct (non-planner) plan edit the session refused. Distinct from a plain
2808
3392
  * Error so a surface can tell "you asked for something invalid" from "something
@@ -2822,12 +3406,14 @@ interface GeneratePlanOptions {
2822
3406
  signal?: AbortSignal;
2823
3407
  }
2824
3408
  /** The slice of Planner the Session drives — the injection seam for tests. */
2825
- type SessionPlanner = Pick<Planner, 'generate' | 'modify' | 'modifyDuringExecution'>;
3409
+ type SessionPlanner = Pick<Planner, 'generate' | 'modifyDuringExecution'>;
2826
3410
  /** Runtime prefs read live — may toggle between operations. */
2827
3411
  interface SessionRuntimeSettings {
2828
3412
  tddEnabled: boolean;
2829
3413
  verificationEnabled?: boolean;
2830
3414
  modelAllowlist?: Record<string, string[]>;
3415
+ /** Read once as a run starts, never per spawn (ADR-0018, S1). Absent means terminal. */
3416
+ runnerTransport?: RunnerTransport;
2831
3417
  }
2832
3418
  /**
2833
3419
  * The whole of what a host reads off disk for a Session. Both hosts used to
@@ -2853,13 +3439,6 @@ declare function sessionRuntimeSettings(settings: UserSettings): SessionRuntimeS
2853
3439
  * occurrence — later repeats stay literal.
2854
3440
  */
2855
3441
  declare function resolveSkillInvocation(text: string, skillsService: Pick<SkillsService, 'findSkill' | 'listSkills'>): string;
2856
- /**
2857
- * Everything a delivery surface constructs to host a session. Structural config
2858
- * (enabledRunners, orchestratorModel, providerModelLists) is snapshotted inside
2859
- * `config` at construction and never re-read from the environment. Runtime
2860
- * settings (tdd, verification) are read live via the `settings` callback so a toggle
2861
- * between generate and execute takes effect.
2862
- */
2863
3442
  /** Where a fork landed: the new session's id, and what adopting it needs. */
2864
3443
  interface ConversationFork {
2865
3444
  sessionId: string;
@@ -2870,6 +3449,15 @@ interface ConversationFork {
2870
3449
  interface ConversationRewind extends ConversationFork {
2871
3450
  rewoundMessage: string;
2872
3451
  }
3452
+ /** Writes one plan to the saved-session store: {@link saveSession}'s shape, injected so tests never touch disk. */
3453
+ type SaveSession = (plan: LegacyPlanState, goal: string, workspace: string, sessionId: string) => void;
3454
+ /**
3455
+ * Everything a delivery surface constructs to host a session. Structural config
3456
+ * (enabledRunners, orchestratorModel, providerModelLists) is snapshotted inside
3457
+ * `config` at construction and never re-read from the environment. Runtime
3458
+ * settings (tdd, verification) are read live via the `settings` callback so a toggle
3459
+ * between generate and execute takes effect.
3460
+ */
2873
3461
  interface SessionDeps {
2874
3462
  config: IConfig;
2875
3463
  notifications: INotification;
@@ -2917,42 +3505,75 @@ interface SessionDeps {
2917
3505
  * `config.worktreeIsolation`; tests inject `FakeWorktreeIsolation`.
2918
3506
  */
2919
3507
  isolation?: IWorktreeIsolation;
3508
+ /** Persistence seam. Defaults to the saved-session store under the workspace. */
3509
+ saveSession?: SaveSession;
3510
+ /** Where a structured task's log is saved (ADR-0018, P1). Defaults to a file per attempt beside the session's. */
3511
+ openTaskLog?: (location: TaskLogLocation, taskId: string) => TaskLogFile;
2920
3512
  }
2921
3513
  /**
2922
- * The per-session execution stack — deepened from a wiring bag into the
2923
- * lifecycle owner. Owns plan generation, execution, mutation, persistence, and
2924
- * the orchestrator observer wiring. The orchestrator's observer is subscribed
2925
- * once for the session's lifetime (not per-operation), which kills the
2926
- * double-subscribe class of bug. Persistence is an internal seam: every plan
2927
- * mutation routes through `persist()`, so the obligation has a home instead of
2928
- * being scattered across 11 call sites.
3514
+ * The composition root: builds every collaborator a Session drives and wires
3515
+ * them to each other, so the Session only receives them. Hosts create a
3516
+ * Session here; the optional {@link SessionDeps} are the seams tests fill.
3517
+ */
3518
+ declare function createSession(deps: SessionDeps): Session;
3519
+ /** What {@link createSession} hands a Session: every collaborator, already built and wired. */
3520
+ interface SessionParts {
3521
+ config: IConfig;
3522
+ registry: RunnerRegistry;
3523
+ workspace: string;
3524
+ fsAdapter: IFileSystem;
3525
+ broadcast: SessionBroadcaster;
3526
+ onNotice?: (notice: SessionNotice) => void;
3527
+ modelResolver: ModelResolver;
3528
+ settings: () => SessionRuntimeSettings;
3529
+ hostSessionId?: string;
3530
+ aiService: () => IAiService;
3531
+ planner: SessionPlanner;
3532
+ store: PlanStore;
3533
+ orchestrator: TaskOrchestrator;
3534
+ events: SessionEventRelay;
3535
+ usage: PlannerUsageLedger;
3536
+ approvals: PendingApprovals;
3537
+ approvalPolicy: ApprovalPolicy;
3538
+ fetcher: IWebFetcher;
3539
+ skillsService: Pick<SkillsService, 'findSkill' | 'listSkills'>;
3540
+ saveSession: SaveSession;
3541
+ /** The conversation's host is the Session itself, so only the Session can make it. */
3542
+ conversation: (host: PlannerConversationHost) => PlannerConversation;
3543
+ }
3544
+ /**
3545
+ * The per-session execution stack — the lifecycle owner. Owns plan
3546
+ * generation, execution, mutation and persistence; built by
3547
+ * {@link createSession}, which wires its collaborators. The orchestrator's
3548
+ * observer is subscribed once for the session's lifetime (not per-operation),
3549
+ * which kills the double-subscribe class of bug. Persistence is an internal
3550
+ * seam: every plan mutation routes through `persist()`.
2929
3551
  *
2930
- * The broadcast seam carries {@link SessionMessage} — the 15 plan-lifecycle
2931
- * events. Catalog/config messages (setModels, setRunnerList, …) stay on the
2932
- * host; Session never emits them.
3552
+ * What surfaces see is produced by the {@link SessionEventRelay}: the
3553
+ * {@link SessionMessage} plan-lifecycle events. Catalog/config messages
3554
+ * (setModels, setRunnerList, …) stay on the host; Session never emits them.
2933
3555
  */
2934
3556
  declare class Session {
2935
- /** Injected by a test; when present it is the service, forever. */
2936
- private readonly pinnedAiService?;
2937
- private liveAiService;
2938
- private liveAiProvider;
2939
- private readonly workspaceRootFn;
2940
- private planner;
2941
- private orchestrator;
2942
- private store;
2943
- private config;
2944
- private registry;
3557
+ private readonly aiService;
3558
+ private readonly usage;
3559
+ private readonly events;
3560
+ private readonly planner;
3561
+ private readonly orchestrator;
3562
+ private readonly store;
3563
+ private readonly config;
3564
+ private readonly registry;
2945
3565
  private plan;
2946
3566
  private goal;
2947
3567
  private workspace;
2948
- private broadcast;
2949
- private onNotice?;
2950
- private modelResolver;
2951
- private fsAdapter;
2952
- private approvals;
2953
- private approvalPolicy;
2954
- private fetcher;
2955
- private settingsFn;
3568
+ private readonly broadcast;
3569
+ private readonly onNotice?;
3570
+ private readonly modelResolver;
3571
+ private readonly fsAdapter;
3572
+ private readonly approvals;
3573
+ private readonly approvalPolicy;
3574
+ private readonly fetcher;
3575
+ private readonly settingsFn;
3576
+ private readonly save;
2956
3577
  /** Last discovered model catalog — lets sync plan commits clamp thinking efforts to real variants. */
2957
3578
  private modelsCache;
2958
3579
  private unsubObserver;
@@ -2960,43 +3581,47 @@ declare class Session {
2960
3581
  private currentSessionId;
2961
3582
  private readonly skillsService;
2962
3583
  private readonly conversation;
2963
- constructor(deps: SessionDeps);
2964
- /**
2965
- * The planner transport for the provider configured *right now* (ADR-0009).
2966
- *
2967
- * Resolved on every read rather than once in the constructor, because a
2968
- * Session outlives the choice: VS Code hosts exactly one for the whole
2969
- * window, and the webview pills and `/planner` switch backends underneath it.
2970
- * The model id was already read live, so a service captured at construction
2971
- * meant a switched planner kept the old backend and got handed the new one's
2972
- * model — an OpenCode model id spawned as `claude --model opencode/…`, which
2973
- * the agent rejects as nonexistent.
2974
- *
2975
- * Switching releases the outgoing service: a harness planner holds an OS
2976
- * process, so dropping the reference without `reset()` leaks an agent.
2977
- */
2978
- private get aiService();
3584
+ constructor(parts: SessionParts);
2979
3585
  /**
2980
3586
  * Answer an outstanding approval. Every surface funnels here — the web
2981
3587
  * server's HTTP route, the VS Code webview, the TUI prompt — so the decision
2982
3588
  * path is identical regardless of who is looking.
2983
3589
  */
2984
- resolveApproval(id: string, granted: boolean): boolean;
3590
+ resolveApproval(id: string, answer: ApprovalAnswer): boolean;
2985
3591
  /** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
2986
3592
  outstandingApprovals(): PendingApproval[];
2987
3593
  /** Scopes the user granted this session — surfaced so a UI can show what is already allowed. */
2988
3594
  approvedScopes(): string[];
2989
3595
  /** The stable id this session persists under — matches the host's id when one was provided. */
2990
3596
  get sessionId(): string;
2991
- get executionLog(): TaskSnapshot[];
3597
+ /** Where this session's structured task logs are saved (ADR-0018, P1). */
3598
+ get taskLogLocation(): TaskLogLocation;
3599
+ /** The attempts of a task that have a saved log, oldest first. */
3600
+ taskLogAttempts(taskId: string): number[];
3601
+ /** One attempt's saved log, for `replayTaskLog` — what a reopened task view shows. */
3602
+ taskLog(taskId: string, attempt: number): TaskLogEvent[];
3603
+ get executionLog(): ReadonlyArray<TaskSnapshot>;
2992
3604
  /** Tasks always read from PlanStore — the single source of truth. */
2993
- get planTasks(): Task[];
2994
- private attachObserver;
2995
- private buildObserver;
2996
- private broadcastStatus;
2997
- private translateProgress;
2998
- /** Persists PlanStore state to disk. PlanStore is the single authority;
2999
- * LegacyPlanState.tasks is populated only here, at persist time. */
3605
+ get planTasks(): ReadonlyArray<Readonly<Task>>;
3606
+ /**
3607
+ * The relay announces every orchestrator event; this adds the saves some of
3608
+ * them owe, each made before the announcement so no surface sees state the
3609
+ * disk lacks.
3610
+ */
3611
+ private observer;
3612
+ /**
3613
+ * Refresh the plan's task list from the store. LegacyPlanState.tasks is only
3614
+ * ever written from the store (here and by the relay before it announces),
3615
+ * and always as a detached copy: sharing the store's live
3616
+ * tree let a host that edits `plan.tasks` rewrite task state behind the
3617
+ * store's back.
3618
+ */
3619
+ private syncPlanTasks;
3620
+ /**
3621
+ * Persists PlanStore state to disk. PlanStore is the single authority.
3622
+ * `background`: an execution event's save, which may land in the middle of a
3623
+ * planner turn without settling it (see {@link PlannerConversation.restore}).
3624
+ */
3000
3625
  private persist;
3001
3626
  /** A new plan on a long-lived Session gets its own persisted identity (unless the host fixed one). */
3002
3627
  private remintSessionId;
@@ -3050,12 +3675,16 @@ declare class Session {
3050
3675
  */
3051
3676
  get currentPlanState(): PlanState | null;
3052
3677
  get currentGoal(): string;
3053
- get isPlanning(): boolean;
3678
+ /**
3679
+ * A task is running right now. Deliberately live work, not the scheduler's
3680
+ * armed flag: a run paused on a user task, a hold or a cancellation has
3681
+ * nothing executing, and reporting it as executing is what left the plan
3682
+ * unstartable after its last live task was cancelled.
3683
+ */
3054
3684
  get isExecuting(): boolean;
3055
3685
  /** See {@link TaskOrchestrator.hasLiveWork} — a spawned runner, not merely an armed scheduler. */
3056
3686
  get hasLiveWork(): boolean;
3057
3687
  get status(): 'approved' | 'running' | 'completed';
3058
- get sessionConfig(): IConfig;
3059
3688
  /**
3060
3689
  * Deny every parked approval as soon as a planning turn is aborted, for the
3061
3690
  * same reason `beginFreshPlan` does it: a prompt raised by a turn nobody is
@@ -3070,7 +3699,6 @@ declare class Session {
3070
3699
  * *next* turn's prompts the moment that stale signal aborted.
3071
3700
  */
3072
3701
  private denyApprovalsOnAbort;
3073
- startExecution(): Promise<void>;
3074
3702
  generatePlan(goal: string, runners: RunnerId[], options?: GeneratePlanOptions): Promise<LegacyPlanState>;
3075
3703
  /**
3076
3704
  * Kick off the planner conversation (ADR-0002): research + the first planner
@@ -3130,10 +3758,16 @@ declare class Session {
3130
3758
  private conversationOpening;
3131
3759
  /**
3132
3760
  * Load planner-produced tasks, coerced to the allowlist read live — it may
3133
- * have changed since planning started. An edit on an armed scheduler is
3761
+ * have changed since planning started. Tasks adopted on an armed scheduler are
3134
3762
  * reconciled rather than reloaded: `loadPlan` clears the on-hold set and the
3135
3763
  * review approval, so a task the user cancelled would be re-armed and
3136
3764
  * re-spawned by the re-tick that follows.
3765
+ *
3766
+ * A whole-plan commit is the planner restating every task, so it is laid
3767
+ * over the plan's execution state rather than replacing it: a planner that
3768
+ * answers "add a task" with the full plan must not undo the work already
3769
+ * done. Task ops need no overlay — their applier refuses to touch settled
3770
+ * tasks, and `rearm`, the one op meant to change a status, must stand.
3137
3771
  */
3138
3772
  private adoptPlannerTasks;
3139
3773
  /**
@@ -3148,21 +3782,36 @@ declare class Session {
3148
3782
  /** Start whatever the scheduler can now fit — after the parallel limit was raised mid-run, say. */
3149
3783
  reschedule(): Promise<void>;
3150
3784
  retryTask(taskId: string): Promise<void>;
3785
+ /**
3786
+ * Continue a finished structured task in its saved runner session with the
3787
+ * user's message (ADR-0018, K1); throws `TaskControlError` for a task that
3788
+ * cannot be continued.
3789
+ */
3790
+ continueTask(taskId: string, message: string): Promise<void>;
3151
3791
  cancelTask(taskId: string): Promise<void>;
3152
- markAiTaskComplete(taskId: string): Promise<void>;
3153
3792
  markTaskComplete(taskId: string): Promise<void>;
3154
3793
  markTaskIncomplete(taskId: string): Promise<void>;
3155
- tick(): Promise<void>;
3156
3794
  processQueuedMessages(): Promise<void>;
3795
+ /**
3796
+ * Every finished task as the log records it: this run's own entries, plus
3797
+ * the tasks earlier runs completed, which the log dropped when this run
3798
+ * began but which dependents still count on.
3799
+ */
3800
+ private finishedWork;
3157
3801
  approveCheckpoint(taskId: string): void;
3158
3802
  rejectCheckpoint(taskId: string, reason?: string): void;
3159
- queueMessage(text: string): void;
3803
+ /**
3804
+ * A user message to a structured task (ADR-0018, M1); throws
3805
+ * `TaskControlError` for one that cannot take it. A task waiting for input
3806
+ * is back in progress, and saved so before any surface is told.
3807
+ */
3808
+ sendTaskMessage(taskId: string, text: string): string;
3809
+ removeQueuedTaskMessage(taskId: string, id: string): boolean;
3810
+ interruptTask(taskId: string): Promise<void>;
3160
3811
  getQueuedMessages(): QueuedMessage[];
3161
- setQueuedMessages(msgs: ReturnType<TaskOrchestrator['getQueuedMessages']>): void;
3162
- processNextQueuedMessage(): QueuedMessage | null;
3163
- get queuedCount(): number;
3164
- getTask(taskId: string): Task | undefined;
3165
- get isReviewApproved(): boolean;
3812
+ /** Take back one unsent message; the plan's persisted queue follows so a reload cannot resurrect it. */
3813
+ removeQueuedMessage(id: string): boolean;
3814
+ setQueuedMessages(msgs: QueuedMessage[]): void;
3166
3815
  /** Replay a run a dirty tree blocked, after stashing the tracked changes. */
3167
3816
  continueWithStash(): Promise<void>;
3168
3817
  /** Replay a run a dirty tree blocked, in the workspace root, for this run only. */
@@ -3268,7 +3917,6 @@ declare class Session {
3268
3917
  * rule instead of slipping past it.
3269
3918
  */
3270
3919
  setTaskDependencies(taskId: string, dependencies: string[]): Promise<LegacyPlanState | null>;
3271
- completeTask(taskId: string): Promise<void>;
3272
3920
  /**
3273
3921
  * Delete one task. A running task is cancelled first: the plan can drop it
3274
3922
  * either way, but nothing can reach its runner afterwards — the tmux session
@@ -3291,9 +3939,6 @@ declare class Session {
3291
3939
  * moved on, not that the whole task should be refused.
3292
3940
  */
3293
3941
  addTask(draft: Partial<Task>): Promise<LegacyPlanState | null>;
3294
- mergeTasks(taskIdA: string, taskIdB: string): Promise<LegacyPlanState | null>;
3295
- mergeMultipleTasks(taskIds: string[]): Promise<LegacyPlanState | null>;
3296
- splitTask(taskId: string, newTasks: Partial<Task>[]): Promise<LegacyPlanState | null>;
3297
3942
  /**
3298
3943
  * Planner-driven merge: validate compatibility up front, then ask the planner
3299
3944
  * LLM to produce a single "merge" taskOps op combining the selected tasks.
@@ -3313,9 +3958,7 @@ declare class Session {
3313
3958
  sessionId?: string;
3314
3959
  persist?: boolean;
3315
3960
  }): void;
3316
- modifyPlan(userRequest: string): Promise<Task[]>;
3317
3961
  destroy(): void;
3318
- private broadcastPlan;
3319
3962
  get aiServiceInstance(): IAiService;
3320
3963
  }
3321
3964
 
@@ -3336,6 +3979,11 @@ declare function globalDataDir(): string;
3336
3979
  */
3337
3980
  declare function migrateOldConfigDir(): void;
3338
3981
 
3982
+ /** Create `dir` (and its parents) owner-only, leaving any existing directory at its own mode. */
3983
+ declare function ensurePrivateDir(dir: string): void;
3984
+ /** Write `content` to `filePath`, atomically, ending at 0600 on POSIX. */
3985
+ declare function writePrivateFile(filePath: string, content: string): void;
3986
+
3339
3987
  type VerdictListener = (taskId: string, verdict: Verdict) => void;
3340
3988
  type CheckpointListener = (taskId: string, summary: string) => void;
3341
3989
  /** Fires on every idleSince transition (null→timestamp on silence, timestamp→null on resume/teardown). */
@@ -3363,11 +4011,19 @@ declare class VerdictEngine {
3363
4011
  private lastGeneration;
3364
4012
  private idleTimers;
3365
4013
  private idleSince;
4014
+ /** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
4015
+ private idlePaused;
3366
4016
  onVerdict(listener: VerdictListener): void;
3367
4017
  onCheckpoint(listener: CheckpointListener): void;
3368
4018
  onIdleChange(listener: IdleListener): void;
3369
4019
  /** Advisory silence timestamp for a task, or null if it isn't idle. */
3370
4020
  getIdleSince(taskId: string): string | null;
4021
+ /**
4022
+ * Stop watching a task's silence while it waits on the user (ADR-0018, W1).
4023
+ * Watching resumes when its next turn starts, or when a checkpoint is answered.
4024
+ */
4025
+ pauseIdle(taskId: string): void;
4026
+ private resumeIdle;
3371
4027
  /** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
3372
4028
  private touchIdle;
3373
4029
  /** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
@@ -3481,7 +4137,7 @@ declare function filterModelsForPrompt(modelsByRunner: Partial<Record<RunnerId,
3481
4137
  declare function clampThinkingEffort(effort: string | undefined, variants: {
3482
4138
  id: string;
3483
4139
  }[]): string | undefined;
3484
- declare function coerceAssignments(tasks: Task[], perRunnerAllowlist: Partial<Record<RunnerId, string[]>>, allowedRunners?: RunnerId[], modelsByRunner?: Partial<Record<RunnerId, DiscoveredModel[]>>): Task[];
4140
+ declare function coerceAssignments(tasks: readonly Task[], perRunnerAllowlist: Partial<Record<RunnerId, string[]>>, allowedRunners?: RunnerId[], modelsByRunner?: Partial<Record<RunnerId, DiscoveredModel[]>>): Task[];
3485
4141
 
3486
4142
  /** What a runner offers a task: the models discovered for it and the modes its manifest declares. */
3487
4143
  interface RunnerCatalog {
@@ -3556,13 +4212,13 @@ declare function buildModifyDuringExecutionPrompt(executionLog: TaskSnapshot[],
3556
4212
  * task-ops protocol (with the "merge" op) is injected alongside it by
3557
4213
  * `planContextBlock`. The model emits a single taskOps merge op.
3558
4214
  */
3559
- declare function buildMergePrompt(taskIds: string[], tasks: Task[]): string;
4215
+ declare function buildMergePrompt(taskIds: string[], tasks: readonly Task[]): string;
3560
4216
  /**
3561
4217
  * User-message prompt for a planner-driven split: asks the model to decompose
3562
4218
  * one task into a sequence of smaller tasks. The model decides the breakdown —
3563
4219
  * the user does not hand-type the parts.
3564
4220
  */
3565
- declare function buildSplitPrompt(taskId: string, tasks: Task[]): string;
4221
+ declare function buildSplitPrompt(taskId: string, tasks: readonly Task[]): string;
3566
4222
  /**
3567
4223
  * Runner prompt for the opt-in task that resolves an integration conflict
3568
4224
  * (ADR-0013). The resolver's own worktree starts at the integration tip, so the
@@ -3598,11 +4254,12 @@ declare function buildConflictRepairPrompt(task: Task, conflict: {
3598
4254
  *
3599
4255
  * - {@link repairLoop} is the bounded driver (first reply → interpret →
3600
4256
  * corrective re-send), used by plan generation ({@link generatePlanWithRepair}),
3601
- * the Session's task-ops settlement, and Planner.modifyDuringExecution.
4257
+ * the Session's task-ops settlement, Planner.modifyDuringExecution, and
4258
+ * `settleReply` — the one loop both planner backends settle a conversation
4259
+ * turn's reply through.
3602
4260
  * - {@link classifyPlannerReply} decides what a planner reply *is* — a plan,
3603
4261
  * targeted task edits, a botched attempt at either (worth a corrective
3604
- * retry), or prose. The conversation loop in BaseAiService keeps its own
3605
- * driver (it also runs the tool rounds) but delegates classification here.
4262
+ * retry), or prose.
3606
4263
  * - The corrective prompt texts live here, once.
3607
4264
  *
3608
4265
  * Policies stay at the call sites (ops validation, abort guards) —
@@ -3610,10 +4267,15 @@ declare function buildConflictRepairPrompt(task: Task, conflict: {
3610
4267
  */
3611
4268
  type RepairVerdict<T> = {
3612
4269
  done: T;
3613
- } | {
4270
+ }
4271
+ /**
4272
+ * A corrective that costs something to prepare (freeing context first) is
4273
+ * passed as a function: it runs only when a re-send will actually follow.
4274
+ */
4275
+ | {
3614
4276
  retry: {
3615
4277
  errors: string[];
3616
- corrective: string;
4278
+ corrective: string | (() => string);
3617
4279
  cause?: unknown;
3618
4280
  };
3619
4281
  };
@@ -3732,16 +4394,16 @@ declare function summarizeOutput(reviewReason: string | undefined, output: strin
3732
4394
  logTail: string;
3733
4395
  capturedAt: string;
3734
4396
  };
3735
- declare function collectDirectDependencyOutputs(task: Task, allTasks: Task[]): PriorOutput[];
4397
+ declare function collectDirectDependencyOutputs(task: Task, allTasks: readonly Task[]): PriorOutput[];
3736
4398
  declare function renderPriorOutputs(outputs: PriorOutput[]): string;
3737
- declare function augmentPromptWithPriorOutputs(task: Task, allTasks: Task[]): string;
4399
+ declare function augmentPromptWithPriorOutputs(task: Task, allTasks: readonly Task[]): string;
3738
4400
  /**
3739
4401
  * Numbered task list for the model's context. `planTasks` is the nested task
3740
4402
  * tree; subtasks render indented under their parent with the same dotted
3741
4403
  * `taskOrderLabel` the other surfaces use, so an `N.M` the model echoes back
3742
4404
  * matches what `resolveTaskId` accepts.
3743
4405
  */
3744
- declare function renderPlanMap(planTasks: Task[], currentTaskId: string, opts?: {
4406
+ declare function renderPlanMap(planTasks: readonly Task[], currentTaskId: string, opts?: {
3745
4407
  maxEntries?: number;
3746
4408
  }): string;
3747
4409
  interface ComposeOptions {
@@ -3749,7 +4411,14 @@ interface ComposeOptions {
3749
4411
  planMapMaxEntries?: number;
3750
4412
  tddEnabled?: boolean;
3751
4413
  }
3752
- declare function composeAugmentedPrompt(task: Task, allTasks: Task[], opts?: ComposeOptions): string;
4414
+ declare function composeAugmentedPrompt(task: Task, allTasks: readonly Task[], opts?: ComposeOptions): string;
4415
+ /**
4416
+ * The first turn of a continue (ADR-0018, K1): the user's message, then a
4417
+ * short reminder of the protocol. The resumed session already holds the
4418
+ * original prompt, so it is not sent again — but it holds the old worktree
4419
+ * too, and this attempt's is fresh from the integration branch.
4420
+ */
4421
+ declare function composeContinuationPrompt(task: Task, message: string): string;
3753
4422
 
3754
4423
  type ExecFileFn = (command: string, args: string[]) => Promise<{
3755
4424
  stdout: string;
@@ -3776,6 +4445,9 @@ declare class TmuxSession extends AbstractTerminalSession {
3776
4445
  private outputBuffer;
3777
4446
  private offset;
3778
4447
  private timer;
4448
+ private quietPolls;
4449
+ private misses;
4450
+ private looking;
3779
4451
  constructor(id: string, taskId: string, tmuxSession: string, windowName: string, execFileImpl: ExecFileFn, pollIntervalMs: number, logPath: string, socket: string);
3780
4452
  private get target();
3781
4453
  /** Every tmux call must name the daemon's own socket; an unprefixed one
@@ -3790,6 +4462,18 @@ declare class TmuxSession extends AbstractTerminalSession {
3790
4462
  */
3791
4463
  start(command: string, args: string[], cwd: string, env?: Record<string, string>): Promise<void>;
3792
4464
  private poll;
4465
+ /** Emit what the log gained since the last read; false when it gained nothing. */
4466
+ private readLog;
4467
+ /**
4468
+ * The sentinel is printed by the wrapper shell, so a window closed from
4469
+ * outside — killed by the user, or with the whole tmux server — never prints
4470
+ * it, and the session would count as running forever. A silent window is
4471
+ * therefore looked up by exact name (a `-t` target falls back to another
4472
+ * window once its own is gone), and once it is confirmed missing the session
4473
+ * ends as a kill does. Whatever the log still held is read first, so a
4474
+ * completion marker printed just before the close still counts.
4475
+ */
4476
+ private checkWindow;
3793
4477
  /**
3794
4478
  * A task finishing (or being killed) stops observation, but never the
3795
4479
  * window itself on the sentinel path — only `kill()` closes the window.
@@ -3869,6 +4553,188 @@ declare class TmuxRunner extends AbstractRunner<TmuxSession> {
3869
4553
  }): Promise<ITerminalSession>;
3870
4554
  }
3871
4555
 
4556
+ interface StructuredRunnerDeps {
4557
+ /** The OS boundary the adapters spawn through. `workspaceEnv` is always the spawn's own `env`. */
4558
+ process?: Partial<Omit<AgentProcessDeps, 'workspaceEnv'>>;
4559
+ /** Overrides adapter construction; production picks by runner id and refuses runners without a connector. */
4560
+ createAdapter?: (runner: string, deps: AgentProcessDeps) => TaskModeAgentAdapter;
4561
+ interruptGraceMs?: number;
4562
+ }
4563
+ interface SessionLaunch {
4564
+ runner: string;
4565
+ deps: AgentProcessDeps;
4566
+ createAdapter: (runner: string, deps: AgentProcessDeps) => TaskModeAgentAdapter;
4567
+ startOptions: TaskStartOptions;
4568
+ interruptGraceMs: number;
4569
+ }
4570
+ /**
4571
+ * One task driven over its runner's programmatic protocol (ADR-0018). It *is*
4572
+ * an `ITerminalSession`, so everything downstream of `onOutput` is unchanged;
4573
+ * what a terminal cannot do sits on {@link StructuredSessionCapability}.
4574
+ *
4575
+ * Ordewell owns the message queue (M1): a message sent mid-turn waits for the
4576
+ * turn to end rather than being typed into a runner that is busy.
4577
+ */
4578
+ declare class StructuredSession extends AbstractTerminalSession implements StructuredSessionCapability {
4579
+ private readonly launch;
4580
+ readonly transport: "structured";
4581
+ private adapter;
4582
+ private adapterStarted;
4583
+ /** Bumped whenever the adapter is replaced, so the old one's late events and exit are ignored. */
4584
+ private generation;
4585
+ private output;
4586
+ private readonly text;
4587
+ private state;
4588
+ private turn;
4589
+ private turnCount;
4590
+ private queue;
4591
+ private messageCount;
4592
+ private interrupting;
4593
+ private lastSessionId;
4594
+ private readonly structuredEmitter;
4595
+ private permissionCount;
4596
+ /**
4597
+ * Open tool requests by the id this session gave them, with the runner's own
4598
+ * id. The runner's ids are only unique to its process — Codex numbers its
4599
+ * requests — while an approval must be answerable by id across the session.
4600
+ */
4601
+ private readonly permissions;
4602
+ constructor(id: string, taskId: string, launch: SessionLaunch);
4603
+ /** Start the runner and send the task's prompt as its first turn. */
4604
+ start(prompt: string): Promise<void>;
4605
+ getOutput(): string;
4606
+ /** A reply typed at the task — a checkpoint answer, most often — is a user message. */
4607
+ write(text: string): void;
4608
+ kill(): void;
4609
+ turnState(): 'working' | 'idle';
4610
+ onTurnEnd(listener: (reason: StructuredTurnEnd) => void): void;
4611
+ onEvent(listener: (event: StructuredEvent) => void): void;
4612
+ sendMessage(text: string): string;
4613
+ removeQueued(id: string): boolean;
4614
+ queued(): QueuedTaskMessage[];
4615
+ nativeSessionId(): string | null;
4616
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
4617
+ /** Every open request goes unanswered once the process that asked is gone. */
4618
+ private withdrawPermissions;
4619
+ interrupt(): Promise<void>;
4620
+ private interruptTurn;
4621
+ /**
4622
+ * The fallback when the runner ignores a soft interrupt: kill it and resume
4623
+ * its session in a fresh process, the way the planner restarts from its
4624
+ * session id after an abort.
4625
+ */
4626
+ private restartInterrupted;
4627
+ private startAdapter;
4628
+ private deliver;
4629
+ private openPermission;
4630
+ private cancelPermission;
4631
+ private handleEvent;
4632
+ /**
4633
+ * A queued message goes out as the turn closes, and the state never passes
4634
+ * through `idle` on the way, so a listener told the turn ended can already
4635
+ * see the task is not waiting.
4636
+ */
4637
+ private endTurn;
4638
+ private emitEvent;
4639
+ }
4640
+ /**
4641
+ * The structured transport (ADR-0018): a task's runner as a plain child
4642
+ * process speaking its protocol — no tmux, no `script` (W2). Only runners
4643
+ * with a task-mode connector can be spawned here; routing the rest to the
4644
+ * terminal transport is the caller's decision, not a silent downgrade here.
4645
+ */
4646
+ declare class StructuredRunner extends AbstractRunner<StructuredSession> {
4647
+ private spawnCount;
4648
+ private readonly processDeps;
4649
+ private readonly createAdapter;
4650
+ private readonly interruptGraceMs;
4651
+ constructor(deps?: StructuredRunnerDeps);
4652
+ spawn(opts: RunnerSpawnOptions): Promise<ITerminalSession>;
4653
+ }
4654
+
4655
+ interface TransportRoute {
4656
+ transport: RunnerTransport;
4657
+ /** Why a structured request runs on the terminal instead, in words a surface shows as is. */
4658
+ fallback?: string;
4659
+ }
4660
+ /**
4661
+ * Where one task runs (ADR-0018, S3): structured only when the plan asks for
4662
+ * it and the task's runner has a task-mode connector. Anything else runs on
4663
+ * the terminal, and a structured request says why.
4664
+ */
4665
+ declare function routeTransport(requested: RunnerTransport | undefined, runner: string, registry?: RunnerRegistry | null): TransportRoute;
4666
+ /**
4667
+ * The runner a host hands the orchestrator: its own terminal runner (tmux,
4668
+ * headless, the VS Code terminal) and the structured one behind a single
4669
+ * `ITerminalRunner`, picking per spawn by {@link routeTransport}.
4670
+ *
4671
+ * `stopAll` and `activeCount` reach both inner runners, so a host that shares
4672
+ * one across plans (the daemon's tmux) keeps its per-plan wrapper above this.
4673
+ */
4674
+ declare class TransportRouter implements ITerminalRunner {
4675
+ private readonly runners;
4676
+ /**
4677
+ * Session ids the structured runner owns; every other id is the terminal's.
4678
+ * Kept past exit, so a stop that arrives late never reaches the terminal
4679
+ * runner with an id it does not know.
4680
+ */
4681
+ private readonly structuredIds;
4682
+ constructor(runners: {
4683
+ terminal: ITerminalRunner;
4684
+ structured: ITerminalRunner;
4685
+ });
4686
+ get activeCount(): number;
4687
+ spawn(opts: RunnerSpawnOptions): Promise<ITerminalSession>;
4688
+ stop(sessionId: string): void;
4689
+ stopAll(): void;
4690
+ }
4691
+
4692
+ /** Whether a task can be continued in its saved runner session (ADR-0018, K1), and in which. */
4693
+ type Continuability = {
4694
+ ok: true;
4695
+ sessionId: string;
4696
+ } | {
4697
+ ok: false;
4698
+ reason: string;
4699
+ };
4700
+ /**
4701
+ * The rule every surface offers Continue by and the orchestrator enforces: a
4702
+ * completed or failed task whose last attempt ran structured and left a
4703
+ * session id behind. A conflict is resolved by its repair, not a new turn, and
4704
+ * a terminal task has no session Ordewell can reach — Retry covers both.
4705
+ */
4706
+ declare function continuability(task: Task): Continuability;
4707
+ declare function canContinue(task: Task): boolean;
4708
+
4709
+ interface TaskLogRecorderDeps {
4710
+ broadcast: SessionBroadcaster;
4711
+ /** Where the session's logs go, read as each attempt starts: a session's id can change between plans. */
4712
+ location: () => TaskLogLocation;
4713
+ /** Opens an attempt's file; tests pass one that never touches the disk. */
4714
+ open?: (location: TaskLogLocation, taskId: string) => TaskLogFile;
4715
+ flushMs?: number;
4716
+ logger?: ILogger;
4717
+ }
4718
+ /**
4719
+ * Keeps each structured task's log (ADR-0018, P1): every event of an attempt
4720
+ * is appended to that attempt's file and broadcast as `task_log`, in the same
4721
+ * batches, so what a surface saw live and what it reloads are one sequence.
4722
+ *
4723
+ * It sits around the runner rather than inside the orchestrator, which stays
4724
+ * unaware of logs; a terminal-transport session has no event stream and is
4725
+ * passed through untouched.
4726
+ */
4727
+ declare class TaskLogRecorder {
4728
+ private readonly deps;
4729
+ private readonly open;
4730
+ private readonly flushMs;
4731
+ private readonly logger;
4732
+ constructor(deps: TaskLogRecorderDeps);
4733
+ wrap(runner: ITerminalRunner): ITerminalRunner;
4734
+ /** Start a new attempt's log for `session` and keep it until the session exits. */
4735
+ record(taskId: string, session: ITerminalSession & StructuredSessionCapability): void;
4736
+ }
4737
+
3872
4738
  /**
3873
4739
  * A workspace path that does not exist, or exists but is not a directory.
3874
4740
  * Checked at every surface that starts a planner or spawns an agent inside
@@ -3948,10 +4814,11 @@ declare function daemonTokenPath(port: number): string;
3948
4814
  /**
3949
4815
  * Mint this daemon's token and hand it off through the filesystem.
3950
4816
  *
3951
- * Unlink-then-create-exclusively rather than a plain write: `mode` is ignored
3952
- * when the file already exists, so writing over a pre-created file would leave
3953
- * the token at whatever permissions that file already had, and an existing
3954
- * symlink would carry the write somewhere else entirely.
4817
+ * The write goes through `writePrivateFile` rather than a plain write because
4818
+ * `mode` is ignored when the file already exists: writing over a pre-created
4819
+ * file would leave the token at whatever permissions that file already had, and
4820
+ * an existing symlink would carry the write somewhere else entirely. The helper
4821
+ * writes a fresh 0600 temp file and renames it into place, so neither applies.
3955
4822
  */
3956
4823
  declare function mintDaemonToken(port: number): {
3957
4824
  token: string;
@@ -4137,6 +5004,17 @@ declare function assertInstallablePluginUrl(url: unknown): URL;
4137
5004
  declare function classifyPluginSource(source: string): PluginSource;
4138
5005
 
4139
5006
  declare function resolveArgs(manifest: RunnerPluginManifest, ctx: ResolveContext): RunnerInvocation;
5007
+ /**
5008
+ * The manifest's meaning of a task's mode and effort, for a runner driven over
5009
+ * its programmatic protocol rather than a command line built from
5010
+ * `argsTemplate` (ADR-0018, C1). Same functions and the same `{{if thinking}}`
5011
+ * gate as the template path, so a structured task and a terminal task given
5012
+ * the same plan run under the same permission mode and effort.
5013
+ */
5014
+ declare function resolveTaskRunnerFlags(manifest: RunnerPluginManifest, ctx: Pick<ResolveContext, 'mode' | 'model' | 'thinkingEffort'>): {
5015
+ permissionMode: string;
5016
+ effortArgs: string[];
5017
+ };
4140
5018
 
4141
5019
  declare const CLAUDE_CODE_MANIFEST: RunnerPluginManifest;
4142
5020
 
@@ -4162,6 +5040,17 @@ declare function loadState(baseDir?: string, logger?: ILogger): LegacyPlanState
4162
5040
  declare function clearState(baseDir?: string): void;
4163
5041
  declare function stateExists(baseDir?: string): boolean;
4164
5042
 
5043
+ /**
5044
+ * A path segment for an id that came from outside — a session id from a URL,
5045
+ * a task id a planner wrote. A plain id is used as is; anything that could
5046
+ * climb out of its directory, or is not portable as a file name, is hex-encoded.
5047
+ */
5048
+ declare function idSegment(id: string): string;
5049
+ /**
5050
+ * The directory beside a session's file for what it keeps outside that file —
5051
+ * its task logs (ADR-0018, P1). It goes when the session does.
5052
+ */
5053
+ declare function sessionDataDir(sessionId: string, baseDir?: string): string;
4165
5054
  declare function saveSession(plan: LegacyPlanState, goal: string, baseDir?: string, id?: string): SessionMeta;
4166
5055
  declare function listSessions(baseDir?: string, logger?: ILogger): SessionMeta[];
4167
5056
  declare function loadSession(sessionId: string, baseDir?: string, logger?: ILogger): {
@@ -4256,4 +5145,4 @@ declare const DEFAULT_MAX_PARALLEL = 3;
4256
5145
  */
4257
5146
  declare function parseMaxParallel(value: unknown): number | null;
4258
5147
 
4259
- export { ALLOWED_PLUGIN_HOSTS, ALL_PROVIDERS, AUTO_COMMANDS, AbstractRunner, AbstractTerminalSession, ActiveTaskSession, type AgentAdapter, type AgentAdapterFactory, type AgentEvent, type AgentProcessDeps, type AgentStartOptions, AiProvider, ApprovalMode, ApprovalRequest, BUILTIN_SKILL_NAMES, BaseAiService, BaseConfig, BaseFileSystem, BufferedTaskOutputSource, CLAUDE_CODE_MANIFEST, CLI_PROVIDERS, CMD_EXE_MAX_COMMAND_LINE, CORE_PLANNER_PROMPT, type CappedRows, type CheckpointListener, ClaudeCodeAdapter, CliAgentAiService, type CliAgentAiServiceDeps, CodexAdapter, type CommandClassification, CommandLineTooLongError, type CommandTier, type ComposeOptions, ConsoleLogger, ContextCollector, ConversationBusyError, type ConversationCompaction, ConversationEditError, type ConversationFork, ConversationMessage, type ConversationRequest, type ConversationRewind, type ConversationTurn, type ConversationVariant, DAEMON_SUBPROTOCOL, DAEMON_TOKEN_SUBPROTOCOL_PREFIX, DEFAULT_MAX_PARALLEL, DiscoveredModel, EmbeddedNewlineError, type EnvAdmission, EnvConfig, type ExecFileFn, type ExecImpl, ExecutableNotFoundError, FindSymbolOptions, FsPluginStore, GIT_READONLY_SUBCOMMANDS, GeminiService, type GitExecFn, GlobOptions, type GrepInvocation, GrepOptions, type HasBinFn, HeadlessRunner, type HeadlessRunnerDeps, HeadlessSession, HomeTranscriptReader, type IAiService, IApproval, IConfig, IFileSystem, type ILogger, type INotification, IPluginStore, ITerminalRunner, ITerminalSession, IWorktreeIsolation, IsolationHandoff, IsolationLandedTask, IsolationMergeResult, IsolationView, JSON_REPAIR_INSTRUCTION, type KillTreeDeps, type LaunchDeps, type LaunchPlan, LegacyPlanState, LineBuffer, type LiveOutputLookup, type LiveTail, type LiveTailOptions, type MappedTool, ModelResolver, type ModelResolverDeps, type ModifyPlanRequest, type NotificationAction, OPENCODE_MANIFEST, ORDEWELL_SETTABLE_ENV, OUTPUT_LINES_DEFAULT, OUTPUT_LINES_MAX, OpenAiService, OpenCodeAdapter, type OrchestratorObserver, OrchestratorOption, PLUGIN_NAME_PATTERN, PROVIDER_CREDENTIAL_ENV, PROVIDER_DETECT_PRIORITY, PROVIDER_LABEL, PROVIDER_PRIORITY, PROVIDER_SHORT_LABEL, type PendingApproval, PendingApprovals, type PendingApprovalsOptions, PlanEditError, PlanIsolation, PlanParseError, type PlanRequest, PlanState, PlanStatus, PlanStore, Planner, type PlannerModelCandidate, type PlannerModelChoice, PlannerModelMemory, type PlannerModelRecall, type PlannerModelStore, type PlannerReplyClassification, type PlannerRuntimeToggles, type PluginSource, type PrdBlock, type PreparedLaunch, type PriorOutput, type ProbeFn, ProviderModelLists, type ProviderRegistration, type PtySize, type PtyWrapOptions, QueuedMessage, REFUSED_COMMANDS, ReadFileOpts, type RepairLoopOpts, type RepairVerdict, RepoGroupLayout, type ResearchChat, ResearchLogEntry, ResearchProgress, type ResearchShell, type ResearchShellDeps, ResearchToolType, type ResearchTurn, ResolveContext, type RewindTarget, type RunnerCatalog, RunnerId, RunnerInstallation, RunnerInvocation, type RunnerMode, RunnerModeInfo, RunnerPluginManifest, RunnerRegistry, type RunnerSpawnOptions, SETTINGS_ENV_ALLOWLIST, SETTINGS_ENV_REFUSED, STATE_DIR, STOPPED_TOOL_RESULT, SYMBOL_LANGUAGES, Session, SessionBroadcaster, type SessionData, type SessionDeps, type SessionMeta, SessionNotice, type SessionPlanner, type SessionRuntimeSettings, SettingsService, type ShellDialect, type SkillInfo, type SkillMetadata, SkillsService, type SpawnSpec, StdioAgentAdapter, TASK_QUERY_ANSWER_MAX_CHARS, TASK_QUERY_ANSWER_OR_OPS, TASK_QUERY_FIELDS, TASK_QUERY_PROTOCOL, TASK_QUERY_REMINDER, TRUNCATED_PLAN_REPAIR_INSTRUCTION, Task, type TaskAttemptSnapshot, TaskIsolation, TaskOp, TaskOrchestrator, type TaskOutputAttempt, type TaskOutputSource, type TaskQuery, type TaskQueryCatalog, type TaskQueryField, TaskSnapshot, TmuxRunner, type TmuxRunnerDeps, type TokenCarriers, type ToolCall, ToolOutcome, type ToolResult, type TranscriptQuery, type TranscriptReader, type UserSettings, Verdict, VerdictEngine, type VerdictListener, WINDOWS_MAX_COMMAND_LINE, type WorkspaceCheckDeps, WorkspaceNotAProjectError, WorkspaceNotFoundError, type WorkspaceProjectCheckDeps, type WorktreeIsolationDeps, admitSettingsEnv, applyHeadLimit, assertInstallablePluginUrl, assertPlainPluginName, assertWorkspaceExists, assertWorkspaceIsProject, augmentPromptWithPriorOutputs, augmentedPath, bearerHeaderValue, buildConflictRepairPrompt, buildConflictResolutionPrompt, buildConversationSystemPrompt, buildFallbackGrepArgs, buildGlobArgs, buildGrepArgs, buildMergePrompt, buildModifyDuringExecutionPrompt, buildModifyPlanPrompt, buildPlanWithResults, buildResearchPrompt, buildResearchToolsPrompt, buildRunnerInvocation, buildShellInvocation, buildSplitPrompt, buildSubagentSystemPrompt, clampThinkingEffort, classifyCommand, classifyPlannerReply, classifyPluginSource, clearAugmentedPathCache, clearDaemonToken, clearResearchShellCache, clearState, clipboardCopyCommand, coerceAssignments, collectDirectDependencyOutputs, composeAugmentedPrompt, configuredProviders, createAiService, createSkillsService, createWorktreeIsolation, daemonTokenPath, defaultLogger, definitionPattern, deleteSession, describeMergeResult, discoverGeminiModels, effectiveAllowlist, ensureDir, ensureStateDirIgnored, executionLogBlock, extractPrdBlock, extractPresentedToken, filterFallbackByAnchoredInclude, filterModelsForPrompt, formatSearchOutput, generatePlanWithRepair, getLatestSession, getProviderMeta, getSettingsPath, getStateDir, globalDataDir, grantScopeFor, hasTmux, includeGlobFor, isCliProvider, isExecutableResolved, isOpenAiProvider, isPlainPluginName, isValidManifest, killTree, languageForId, listSessions, loadSession, loadSessionPlanState, loadState, mapAgentTool, migrateOldConfigDir, mintDaemonToken, mintSessionId, modelContextBlock, modifyValidationFeedback, normalizeAgentArgs, normalizeGeminiModel, parseMaxParallel, parseTaskQueryJson, pendingEditRulesBlock, planDirectLaunch, planShellLaunch, posixShellQuote, prefixModelId, providerForRunner, reEmitPlanPrompt, reEmitTaskOpsPrompt, reEmitTaskQueryPrompt, readDaemonToken, referencePattern, renderPlanMap, renderPriorOutputs, renderTaskQueryAnswer, repairLoop, researchShellWarning, researchToolsPath, resolveArgs, resolvePluginInstallDir, resolveProviderFromPrefix, resolveResearchShell, resolveSkillInvocation, resolveWithin, retargetTaskRunner, runnerAssignment, runnerForProvider, sanitizeSlug, savePrdMarkdown, saveSession, saveState, sessionRuntimeSettings, stateExists, stripAnsi, stripModelPrefix, summarizeOutput, taskOpsRejectedPrompt, taskQuerySignature, textHasTaskQuery, tmuxSessionName, tmuxSocketName, tmuxWindowName, tokenSubprotocols, tokensMatch, truncatedPlanReEmitPrompt, wellKnownBinDirs, windowsCommandLine, withPath, wrapWithPty };
5148
+ export { ALLOWED_PLUGIN_HOSTS, ALL_PROVIDERS, AUTO_COMMANDS, AbstractRunner, AbstractTerminalSession, ActiveTaskSession, type Adr0013IsolationRun, type Adr0013PlanIsolation, type Adr0013TaskRecord, AgentAdapter, AgentEvent, AgentProcessDeps, AgentStartOptions, AiProvider, ApplyTaskOpsResult, ApprovalAnswer, ApprovalDecision, ApprovalMode, ApprovalPolicy, ApprovalRequest, type AskOptions, AwaitingReason, BUILTIN_SKILL_NAMES, BaseAiService, BaseConfig, BaseFileSystem, BufferedTaskOutputSource, CLAUDE_CODE_MANIFEST, CLI_PROVIDERS, CORE_PLANNER_PROMPT, type CappedRows, type CheckpointListener, ClaudeCodeAdapter, CliAgentAiService, type CliAgentAiServiceDeps, CodexAdapter, type CommandClassification, type CommandTier, type ComposeOptions, ConsoleLogger, ContextCollector, type Continuability, ConversationBusyError, type ConversationCompaction, ConversationEditError, type ConversationFork, ConversationMessage, type ConversationRequest, type ConversationRewind, type ConversationTurn, type ConversationVariant, DAEMON_SUBPROTOCOL, DAEMON_TOKEN_SUBPROTOCOL_PREFIX, DEFAULT_MAX_PARALLEL, DiscoveredModel, type EnvAdmission, EnvConfig, type ExecFileFn, type ExecImpl, FindSymbolOptions, FsPluginStore, GIT_READONLY_SUBCOMMANDS, GeminiService, type GitExecFn, GlobOptions, type GrepInvocation, GrepOptions, type HasBinFn, HomeTranscriptReader, type IAiService, IApproval, IConfig, IFileSystem, type ILogger, type INotification, IPluginStore, ITerminalRunner, ITerminalSession, IWorktreeIsolation, IsolationHandoff, IsolationLandedTask, IsolationMergeResult, IsolationOutcome, IsolationRun, IsolationTaskRecord, IsolationTaskStatus, IsolationView, JSON_REPAIR_INSTRUCTION, type KillTreeDeps, LegacyPlanState, type LiveOutputLookup, type LiveTail, type LiveTailOptions, type MappedTool, ModelResolver, type ModelResolverDeps, type ModifyPlanRequest, type NotificationAction, OPENCODE_MANIFEST, ORDEWELL_SETTABLE_ENV, OUTPUT_LINES_DEFAULT, OUTPUT_LINES_MAX, OpenAiService, OpenCodeAdapter, type OrchestratorObserver, OrchestratorOption, PLUGIN_NAME_PATTERN, PROVIDER_CREDENTIAL_ENV, PROVIDER_DETECT_PRIORITY, PROVIDER_LABEL, PROVIDER_PRIORITY, PROVIDER_SHORT_LABEL, type PendingApproval, PendingApprovals, type PendingApprovalsOptions, PlanEditError, PlanIsolation, PlanParseError, type PlanRequest, PlanState, PlanStatus, PlanStore, Planner, type PlannerModelCandidate, type PlannerModelChoice, PlannerModelMemory, type PlannerModelRecall, type PlannerModelStore, type PlannerReplyClassification, type PlannerRuntimeToggles, PlannerUsage, type PluginSource, type PrdBlock, type PriorOutput, type ProbeFn, ProviderModelLists, type ProviderRegistration, QueuedMessage, QueuedTaskMessage, REFUSED_COMMANDS, ReadFileOpts, RepairEvidence, type RepairLoopOpts, type RepairVerdict, RepoGroupLayout, type ResearchChat, ResearchLogEntry, ResearchProgress, type ResearchShell, type ResearchShellDeps, ResearchToolType, type ResearchTurn, ResolveContext, type RewindTarget, RunnerApprovals, type RunnerCatalog, RunnerId, RunnerInstallation, RunnerInvocation, type RunnerMode, RunnerModeInfo, RunnerPluginManifest, RunnerRegistry, RunnerSpawnOptions, RunnerTransport, SETTINGS_ENV_ALLOWLIST, SETTINGS_ENV_REFUSED, STATE_DIR, STOPPED_TOOL_RESULT, SYMBOL_LANGUAGES, type SaveSession, Session, SessionBroadcaster, type SessionData, type SessionDeps, SessionMessage, type SessionMeta, SessionNotice, type SessionPlanner, type SessionRuntimeSettings, SettingsService, type ShellDialect, type SkillInfo, type SkillMetadata, SkillsService, type SpawnSpec, StdioAgentAdapter, StructuredEvent, StructuredRunner, type StructuredRunnerDeps, StructuredSession, StructuredSessionCapability, StructuredTurnEnd, TASK_QUERY_ANSWER_MAX_CHARS, TASK_QUERY_ANSWER_OR_OPS, TASK_QUERY_FIELDS, TASK_QUERY_PROTOCOL, TASK_QUERY_REMINDER, TRUNCATED_PLAN_REPAIR_INSTRUCTION, Task, type TaskAttemptSnapshot, TaskControlError, TaskIsolation, TaskLogEvent, type TaskLogFile, type TaskLogLocation, TaskLogRecorder, type TaskLogRecorderDeps, TaskModeAgentAdapter, TaskOp, TaskOrchestrator, type TaskOrchestratorDeps, type TaskOrchestratorOptions, type TaskOutputAttempt, type TaskOutputSource, type TaskQuery, type TaskQueryCatalog, type TaskQueryField, TaskSnapshot, TaskStartOptions, TmuxRunner, type TmuxRunnerDeps, type TokenCarriers, type ToolCall, ToolOutcome, type ToolResult, type TranscriptQuery, type TranscriptReader, type TransportRoute, TransportRouter, UsageRecord, type UserSettings, Verdict, VerdictEngine, type VerdictListener, type WorkspaceCheckDeps, WorkspaceNotAProjectError, WorkspaceNotFoundError, type WorkspaceProjectCheckDeps, type WorktreeIsolationDeps, admitSettingsEnv, applyHeadLimit, assertInstallablePluginUrl, assertPlainPluginName, assertWorkspaceExists, assertWorkspaceIsProject, augmentPromptWithPriorOutputs, augmentedPath, bearerHeaderValue, buildConflictRepairPrompt, buildConflictResolutionPrompt, buildConversationSystemPrompt, buildFallbackGrepArgs, buildGlobArgs, buildGrepArgs, buildMergePrompt, buildModifyDuringExecutionPrompt, buildModifyPlanPrompt, buildPlanWithResults, buildResearchPrompt, buildResearchToolsPrompt, buildRunnerInvocation, buildSplitPrompt, buildSubagentSystemPrompt, canContinue, clampThinkingEffort, classifyCommand, classifyPlannerReply, classifyPluginSource, clearAugmentedPathCache, clearDaemonToken, clearResearchShellCache, clearState, clipboardCopyCommand, coerceAssignments, collectDirectDependencyOutputs, composeAugmentedPrompt, composeContinuationPrompt, configuredProviders, continuability, createAiService, createSession, createSkillsService, createTaskAdapter, createTaskOrchestrator, createWorktreeIsolation, daemonTokenPath, defaultLogger, definitionPattern, deleteSession, describeMergeResult, discoverGeminiModels, effectiveAllowlist, ensureDir, ensurePrivateDir, ensureStateDirIgnored, executionLogBlock, extractPrdBlock, extractPresentedToken, filterFallbackByAnchoredInclude, filterModelsForPrompt, formatSearchOutput, generatePlanWithRepair, getLatestSession, getProviderMeta, getSettingsPath, getStateDir, globalDataDir, grantScopeFor, hasTmux, idSegment, includeGlobFor, isCliProvider, isOpenAiProvider, isPlainPluginName, isValidManifest, killTree, languageForId, listSessions, listTaskLogAttempts, loadSession, loadSessionPlanState, loadState, mapAgentTool, migrateOldConfigDir, migratePlanIsolation, mintDaemonToken, mintSessionId, modelContextBlock, modifyValidationFeedback, normalizeAgentArgs, normalizeGeminiModel, openTaskLog, parseMaxParallel, parseTaskQueryJson, pendingEditRulesBlock, prefixModelId, providerForRunner, reEmitPlanPrompt, reEmitTaskOpsPrompt, reEmitTaskQueryPrompt, readDaemonToken, readTaskLog, referencePattern, renderPlanMap, renderPriorOutputs, renderTaskQueryAnswer, repairLoop, researchShellWarning, researchToolsPath, resolveArgs, resolvePluginInstallDir, resolveProviderFromPrefix, resolveResearchShell, resolveSkillInvocation, resolveTaskRunnerFlags, resolveWithin, retargetTaskRunner, routeTransport, runnerAssignment, runnerForProvider, sanitizeSlug, savePrdMarkdown, saveSession, saveState, sessionDataDir, sessionRuntimeSettings, stateExists, stripModelPrefix, summarizeOutput, supportsTaskMode, taskOpsRejectedPrompt, taskQuerySignature, textHasTaskQuery, tmuxSessionName, tmuxSocketName, tmuxWindowName, tokenSubprotocols, tokensMatch, truncatedPlanReEmitPrompt, wellKnownBinDirs, withPath, writePrivateFile };