@ordewell/core 0.5.5 → 0.6.0

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 (54) hide show
  1. package/dist/IFileSystem-CFYf4yR2.d.mts +76 -0
  2. package/dist/IFileSystem-D4dAl2r8.d.ts +76 -0
  3. package/dist/{ModeResolver-D-SUFRNF.d.mts → ModeResolver-DVYVRbJp.d.ts} +6 -2
  4. package/dist/{ModeResolver-D3XO0fT9.d.ts → ModeResolver-a4xJFlEp.d.mts} +6 -2
  5. package/dist/Task-CZJr6rmz.d.mts +2121 -0
  6. package/dist/Task-CZJr6rmz.d.ts +2121 -0
  7. package/dist/{chunk-KLN7ELXO.mjs → chunk-2SOVKO7P.mjs} +1561 -1269
  8. package/dist/chunk-2SOVKO7P.mjs.map +1 -0
  9. package/dist/{chunk-T2S5O36I.mjs → chunk-C44UWIAD.mjs} +6 -6
  10. package/dist/chunk-C44UWIAD.mjs.map +1 -0
  11. package/dist/{chunk-HD2FWPRV.mjs → chunk-EDGUFCIR.mjs} +6 -1
  12. package/dist/chunk-EDGUFCIR.mjs.map +1 -0
  13. package/dist/{chunk-UUBGVCGJ.mjs → chunk-OM4D274O.mjs} +20 -2
  14. package/dist/chunk-OM4D274O.mjs.map +1 -0
  15. package/dist/index.d.mts +1594 -803
  16. package/dist/index.d.ts +1594 -803
  17. package/dist/index.js +9540 -6040
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +8167 -4996
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/order-labels.d.mts +3 -1
  22. package/dist/order-labels.d.ts +3 -1
  23. package/dist/{parsing-BTP4bwkk.d.ts → parsing-Bd6-b644.d.ts} +2 -2
  24. package/dist/{parsing-CDtRSxBY.d.mts → parsing-DIaGZmiQ.d.mts} +2 -2
  25. package/dist/parsing.d.mts +5 -3
  26. package/dist/parsing.d.ts +5 -3
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +2 -2
  29. package/dist/{plan-utils-pE4TBwxl.d.mts → plan-utils-Ba-l8EZS.d.mts} +212 -43
  30. package/dist/{plan-utils-BFaPo-IT.d.ts → plan-utils-_T5S7Mnk.d.ts} +212 -43
  31. package/dist/plan-utils.d.mts +5 -4
  32. package/dist/plan-utils.d.ts +5 -4
  33. package/dist/plan-utils.js +352 -64
  34. package/dist/plan-utils.js.map +1 -1
  35. package/dist/plan-utils.mjs +11 -5
  36. package/dist/testing.d.mts +54 -4
  37. package/dist/testing.d.ts +54 -4
  38. package/dist/testing.js +93 -2
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +91 -2
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +2 -1
  43. package/skills/grilling/SKILL.md +6 -16
  44. package/skills/improve-codebase-architecture/SKILL.md +1 -1
  45. package/dist/ApprovalPolicy-BVhGdECT.d.mts +0 -79
  46. package/dist/ApprovalPolicy-BVhGdECT.d.ts +0 -79
  47. package/dist/ITerminalRunner-BV9Rd2o9.d.ts +0 -563
  48. package/dist/ITerminalRunner-C77ZNZS9.d.mts +0 -563
  49. package/dist/Task-Vl5Zq_D-.d.mts +0 -825
  50. package/dist/Task-Vl5Zq_D-.d.ts +0 -825
  51. package/dist/chunk-HD2FWPRV.mjs.map +0 -1
  52. package/dist/chunk-KLN7ELXO.mjs.map +0 -1
  53. package/dist/chunk-T2S5O36I.mjs.map +0 -1
  54. package/dist/chunk-UUBGVCGJ.mjs.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,18 +1,16 @@
1
- import { H as RunnerId, v as PlanStatus, L as LegacyPlanState, k as IsolationMergeResult, g as IsolationLandedTask, I as IWorktreeIsolation, y as RepoGroupLayout, a as DiscoveredModel, T as Task, a3 as UsageRecord, O as SubagentOutcome, B as ResearchProgress, z as ResearchLogEntry, C as ConversationMessage, Z as TaskSnapshot, d as IsolationHandoff, U as TaskIsolation, s as IsolationView, P as PlanIsolation, Q as QueuedMessage, A as ActiveTaskSession, G as ResearchToolType, u as PlanState, aa as Verdict } from './Task-Vl5Zq_D-.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 IsolationPruneResult, n as IsolationRepo, o as IsolationRun, p as IsolationTaskRecord, q as IsolationTaskRepo, r as IsolationTaskStatus, M as Message, t as PlanModificationWarnings, w as PlannerUsage, x as PreparedTask, R as RepairEvidence, E as ResearchStep, F as ResearchStepOutcome, S as StreamEvent, J as StreamStepEvent, K as StreamThinkingEvent, N as SubagentLogEntry, V as TaskIsolationState, W as TaskMode, X as TaskModelAssignment, Y as TaskOutputSummary, _ as TaskStatus, $ as TaskType, a0 as TaskWithParent, a1 as ThinkingBlock, a2 as UsageLine, a4 as UsageTotals, a5 as UserPromptEntry, a6 as UserStep, a7 as ValidationCheck, a8 as ValidationContext, a9 as ValidationResult, ab as VerificationCheck, ac as addPlannerUsage, ad as addTaskToPlan, ae as addUsage, af as createEmptyPlan, ag as createTask, ah as emptyWarnings, ai as flattenTasks, aj as flattenTasksWithParents, ak as isMeasured, al as keepExecutionState, am as migrateLegacyPlan, an as migratePlanState, ao as migrateTask, ap as partedPromptUsage, aq as plannerContextFill, ar as removeTaskFromPlan, as as renumberTasks, at as updateTaskInPlan, au as usageLine, av as validateModifiedPlan, aw as warningsText } from './Task-Vl5Zq_D-.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-BV9Rd2o9.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-BV9Rd2o9.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, n as SessionBroadcaster, p as SessionNotice } from './plan-utils-BFaPo-IT.js';
8
- export { A as Adr0013IsolationRun, a as Adr0013PlanIsolation, b as Adr0013TaskRecord, c as ApplyTaskOpsResult, d as ApprovalBlock, e as ApprovalStatus, C as CHECKPOINT_TRUNCATE_LENGTH, f as ConversationInput, g as ConversationView, D as DisplayBlock, E as EMPTY_CONVERSATION, h as EMPTY_HOLD, G as GatedConversation, L as LocalEntry, M as MessageBlock, i as MessageRole, N as NO_TURN, O as OutputPreview, P as PlanBlock, j as PlanMarkerStatus, k as PromptHold, S as SerializedPlan, l as SerializedTask, m as SerializedTaskStatus, o as SessionMessage, q as SubagentBlock, r as SubagentChild, s as SubagentStatus, T as TakenPrompt, 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 dependencyCandidates, V as dependentsOf, W as drainNext, X as executionSummary, Y as followTurn, Z as fromTranscript, _ as hasHiddenDetail, $ as holdPrompt, a0 as migratePlanIsolation, a1 as outputLines, a2 as outputPreview, a3 as parseTaskOpsJson, a4 as reduceConversation, a5 as serializePlan, a6 as serializeTask, a7 as serializeTaskStatus, a8 as stopTurn, a9 as summarizeToolCall, aa as taskStartedNotice, ab as textHasTaskOps, ac as toolHeadline, ad as truncateCheckpointSummary, ae as unsendAll, af as unsendLatest } from './plan-utils-BFaPo-IT.js';
9
- import { R as RunnerModeInfo } from './ModeResolver-D3XO0fT9.js';
10
- export { M as ManifestLookup, b as buildModeGuide, f as filteredBuildModes, r as resolveDefaultMode, a as resolveTaskMode, c as runnerModesFrom } from './ModeResolver-D3XO0fT9.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, b0 as TaskRunnerFlags } from './Task-CZJr6rmz.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, 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 approvalScopes, bn as buildShellInvocation, bo as collectProviderCredentials, bp as createEmptyPlan, bq as createTask, br as emptyWarnings, bs as enabledRunners, bt as fetchAllProviderModels, bu as flattenTasks, bv as flattenTasksWithParents, bw as isAwaitingReason, bx as isExecutableResolved, by as isGranted, bz as isMeasured, bA as isReservedRunnerName, bB as isRunnerApproval, bC as isRunnerTransport, bD as isStructuredSession, bE as keepExecutionState, bF as knownModelId, bG as migrateLegacyPlan, bH as migratePlanState, bI as migrateTask, bJ as partedPromptUsage, bK as planDirectLaunch, bL as planShellLaunch, bM as plannerContextFill, bN as posixShellQuote, bO as removeTaskFromPlan, bP as renumberTasks, bQ as resolveModelShortcut, bR as resolveProvider, bS as stripAnsi, bT as toApprovalDecision, bU as toOrchestratorOptions, bV as updateTaskInPlan, bW as usageLine, bX as validateModifiedPlan, bY as warningsText, bZ as windowsCommandLine, b_ as wrapWithPty } from './Task-CZJr6rmz.js';
3
+ import { t as TaskOp, l as SessionBroadcaster, A as ApplyTaskOpsResult, m as SessionMessage, n as SessionNotice, r as TaskLogEvent } from './plan-utils-_T5S7Mnk.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-_T5S7Mnk.js';
5
+ import { I as IFileSystem, R as ReadFileOpts, T as ToolOutcome, b as GrepOptions, a as GlobOptions, F as FindSymbolOptions } from './IFileSystem-D4dAl2r8.js';
6
+ export { G as GREP_DEFAULT_HEAD_LIMIT, c as GrepOutputMode, S as SEARCH_EXCLUSIONS } from './IFileSystem-D4dAl2r8.js';
7
+ import { R as RunnerModeInfo } from './ModeResolver-DVYVRbJp.js';
8
+ export { M as ManifestLookup, a as autonomyLevelLabel, b as buildModeGuide, f as filteredBuildModes, p as parseAutonomyLevel, r as resolveDefaultMode, c as resolveTaskMode, d as runnerModesFrom } from './ModeResolver-DVYVRbJp.js';
11
9
  import { ChildProcess } from 'child_process';
12
- import { EventEmitter } from 'events';
13
- import { b as PlanParseError } from './parsing-BTP4bwkk.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, o as opensWithJsonObject, p as parsePartialPlan, n as parsePlanJson, s as stripModelNoise, q as stripTrailingCommas, v as validatePlanModification } from './parsing-BTP4bwkk.js';
10
+ import { b as PlanParseError } from './parsing-Bd6-b644.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-Bd6-b644.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
@@ -260,16 +261,19 @@ declare abstract class BaseFileSystem implements IFileSystem {
260
261
  * Path confinement for `bash`: an `auto`-tier binary (`cat`, `find`, `rg`, …)
261
262
  * is only auto because *reading* is read-only — its arguments can still
262
263
  * name a path outside the workspace, which is the exact escape confinement
263
- * closes for `readFile`/`glob`/`grep`. Each escaping path needs its own
264
- * approval (scoped to its containing directory); approving one does not
265
- * approve another, so a single command touching two external dirs prompts
266
- * once per distinct scope rather than carrying the first grant to the rest.
264
+ * closes for `readFile`/`glob`/`grep`. Each escaping path is scoped to its
265
+ * containing directory, one entry per distinct scope.
267
266
  */
268
- private authorizeCommandPaths;
267
+ private outsidePaths;
269
268
  /**
270
269
  * Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
271
270
  * anything else asks once and is remembered, and writes are refused outright
272
271
  * because a planner that mutates the workspace has stopped being a planner.
272
+ *
273
+ * Everything a command needs — its own approval and every outside directory
274
+ * it touches — is one request, so one command is one prompt. Each scope is
275
+ * still granted separately: approving the pair does not approve another
276
+ * directory the next command names.
273
277
  */
274
278
  bash(command: string, signal?: AbortSignal): Promise<ToolOutcome>;
275
279
  }
@@ -323,7 +327,9 @@ declare function grantScopeFor(abs: string, kind: 'file' | 'directory'): string;
323
327
  * Timeouts are load-bearing rather than defensive: a planner turn that blocks
324
328
  * forever on an unanswered prompt would hang the whole research loop with no
325
329
  * visible cause. On expiry the request resolves to denied and the model gets a
326
- * normal, actionable tool result.
330
+ * normal, actionable tool result. A task runner's request is the exception
331
+ * (ADR-0018, A1): the runner is parked on it rather than a research loop, and
332
+ * it waits for a person — or the supervisor — however long that takes.
327
333
  */
328
334
  interface PendingApproval {
329
335
  id: string;
@@ -336,20 +342,70 @@ interface PendingApprovalsOptions {
336
342
  /** Announce a new request to the surfaces. */
337
343
  onRequest?: (pending: PendingApproval) => void;
338
344
  /** Announce that a request is no longer actionable (answered or expired). */
339
- onSettled?: (id: string, granted: boolean) => void;
345
+ onSettled?: (id: string, granted: boolean, settled: {
346
+ request: ApprovalRequest;
347
+ decision: ApprovalDecision;
348
+ }) => void;
349
+ }
350
+ interface AskOptions {
351
+ /**
352
+ * An id the caller already announced the request under. One still in use is
353
+ * refused — denied at once — rather than overwriting the request that has it.
354
+ */
355
+ id?: string;
356
+ /** Wait for an answer however long it takes. */
357
+ noTimeout?: boolean;
358
+ /**
359
+ * Given the answer as the request settles, before anything awaiting the
360
+ * promise runs: a caller about to tear down what asked (a runner being
361
+ * stopped) must deliver the answer before it goes.
362
+ */
363
+ onDecision?: (decision: ApprovalDecision) => void;
340
364
  }
341
365
  declare class PendingApprovals {
342
366
  private readonly opts;
343
367
  private readonly entries;
344
368
  constructor(opts?: PendingApprovalsOptions);
345
369
  /** 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;
370
+ ask(request: ApprovalRequest, options?: AskOptions): Promise<boolean>;
371
+ /** Park a request whose answer is more than yes or no. */
372
+ decide(request: ApprovalRequest, options?: AskOptions): Promise<ApprovalDecision>;
373
+ /**
374
+ * Answer one request. Returns false when the id is unknown or already
375
+ * settled. "Allow for this task" on a request that did not offer it is a
376
+ * plain allow: no grant is made that the requester never proposed.
377
+ */
378
+ resolve(id: string, answer: ApprovalAnswer): boolean;
349
379
  /** Everything still awaiting an answer — replayed to a surface that connects late. */
350
380
  outstanding(): PendingApproval[];
351
- /** Deny everything in flight. Called on abort and on session reset. */
352
- clear(): void;
381
+ /**
382
+ * Deny everything in flight, or only the requests `which` picks. Called on
383
+ * abort and on session reset.
384
+ */
385
+ clear(which?: (request: ApprovalRequest) => boolean, note?: string): void;
386
+ }
387
+
388
+ /**
389
+ * Carries a structured task's tool requests to the session's one approval
390
+ * seam (ADR-0018, A1), so a runner's request is answered through
391
+ * `resolveApproval` like any other — by a person on any surface, or by the
392
+ * supervisor (#28), with nothing here assuming which.
393
+ *
394
+ * Like the task log, it sits around the runner rather than inside the
395
+ * orchestrator: every way an attempt ends reaches the runner as a `stop`, and
396
+ * that is where a task's open requests are denied, before the session goes,
397
+ * so nothing is left waiting on a process that no longer exists.
398
+ */
399
+ declare class RunnerApprovals {
400
+ private readonly approvals;
401
+ /** The open request ids of each live structured session. */
402
+ private readonly open;
403
+ constructor(approvals: PendingApprovals);
404
+ wrap(runner: ITerminalRunner): ITerminalRunner;
405
+ /** How many of a task's runner requests wait for an answer — what "waiting for approval" is derived from. */
406
+ waiting(taskId: string): number;
407
+ private watch;
408
+ private denySession;
353
409
  }
354
410
 
355
411
  /**
@@ -587,6 +643,37 @@ interface WorktreeIsolationDeps {
587
643
  }
588
644
  declare function createWorktreeIsolation(deps: WorktreeIsolationDeps): IWorktreeIsolation;
589
645
 
646
+ /** A task record as ADR-0013 persisted it, for one repository. */
647
+ interface Adr0013TaskRecord {
648
+ taskId: string;
649
+ order: number;
650
+ title: string;
651
+ branch: string;
652
+ worktree: string;
653
+ status: IsolationTaskStatus;
654
+ linked: string[];
655
+ }
656
+ /** A run as ADR-0013 persisted it (0.4.23): one repository, its refs on the run itself. */
657
+ interface Adr0013IsolationRun {
658
+ id: string;
659
+ workspaceRoot: string;
660
+ baseRef: string;
661
+ baseBranch?: string;
662
+ integrationBranch: string;
663
+ tasks: Record<string, Adr0013TaskRecord>;
664
+ }
665
+ interface Adr0013PlanIsolation {
666
+ run: Adr0013IsolationRun;
667
+ resolvers: Record<string, string>;
668
+ }
669
+ /**
670
+ * A persisted plan isolation in today's shape. A run saved in the ADR-0013
671
+ * format — recognised by its own `integrationBranch` — becomes a group of one
672
+ * at `.`, keeping its branches, so a session saved by 0.4.23 resumes and hands
673
+ * off exactly as it would have.
674
+ */
675
+ declare function migratePlanIsolation(state: PlanIsolation | Adr0013PlanIsolation): PlanIsolation;
676
+
590
677
  interface ILogger {
591
678
  warn(scope: string, message: string, err?: unknown): void;
592
679
  }
@@ -613,6 +700,11 @@ interface UserSettings {
613
700
  * back to the environment's defaults; `[]` is a deliberate "none of them".
614
701
  */
615
702
  enabledRunners?: string[];
703
+ /**
704
+ * How tasks' runners are driven (ADR-0018). A run copies it onto its plan
705
+ * when it starts, so a change applies from the next run.
706
+ */
707
+ runnerTransport: RunnerTransport;
616
708
  }
617
709
  /**
618
710
  * Where the user's toggles live. `ORDEWELL_SETTINGS_PATH` overrides it so several
@@ -629,6 +721,12 @@ declare class SettingsService {
629
721
  private filePath;
630
722
  private cache;
631
723
  private cachedMtimeMs;
724
+ /**
725
+ * Whether the transport on disk (or just set) is the user's own choice.
726
+ * Writing the default back out would turn it into one, and a later change of
727
+ * default would then never reach this user.
728
+ */
729
+ private transportChosen;
632
730
  constructor(filePath?: string);
633
731
  getAll(): UserSettings;
634
732
  private fileMtimeMs;
@@ -636,6 +734,8 @@ declare class SettingsService {
636
734
  setTdd(enabled: boolean): void;
637
735
  getVerification(): boolean;
638
736
  setVerification(enabled: boolean): void;
737
+ getRunnerTransport(): RunnerTransport;
738
+ setRunnerTransport(transport: RunnerTransport): void;
639
739
  getModelAllowlist(runner: string): string[] | undefined;
640
740
  setModelAllowlist(runner: string, ids: string[] | undefined): void;
641
741
  getPlannerModel(provider: string): {
@@ -812,492 +912,7 @@ type LiveOutputLookup = (taskId: string, opts: LiveTailOptions) => LiveTail | nu
812
912
  * state on every query rather than cached. `liveOutput` backs the `output`
813
913
  * field; omitting it reads as "nothing captured".
814
914
  */
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
- /**
1133
- * A complete run of the assistant's reply. Concatenated in order to form the
1134
- * turn's text. When the same run already streamed as `assistant_text_delta`,
1135
- * this is the authoritative copy of it — it replaces the deltas, it is not
1136
- * appended after them.
1137
- */
1138
- {
1139
- type: 'assistant_text';
1140
- text: string;
1141
- }
1142
- /**
1143
- * An incremental piece of the assistant's reply, for agents that stream
1144
- * partial messages. The planner's own text only: a subagent's words never
1145
- * arrive here, so they can never become the reply.
1146
- */
1147
- | {
1148
- type: 'assistant_text_delta';
1149
- text: string;
1150
- }
1151
- /**
1152
- * Reasoning the agent chose to expose. Never contributes to the reply text.
1153
- * Like `assistant_text`, it supersedes deltas already streamed for it.
1154
- */
1155
- | {
1156
- type: 'thinking';
1157
- text: string;
1158
- subagentId?: string;
1159
- } | {
1160
- type: 'thinking_delta';
1161
- text: string;
1162
- subagentId?: string;
1163
- }
1164
- /** `subagentId` marks a call made inside a subagent rather than by the planner itself. */
1165
- | {
1166
- type: 'tool_call';
1167
- id: string;
1168
- name: string;
1169
- args: Record<string, unknown>;
1170
- subagentId?: string;
1171
- } | {
1172
- type: 'tool_result';
1173
- id: string;
1174
- name: string;
1175
- output: string;
1176
- success: boolean;
1177
- subagentId?: string;
1178
- }
1179
- /** One model call's usage, as the agent reported it — never estimated. */
1180
- | {
1181
- type: 'usage';
1182
- record: UsageRecord;
1183
- }
1184
- /** The agent delegated `brief` to a subagent, whose events carry `subagentId` until it finishes. */
1185
- | {
1186
- type: 'subagent_started';
1187
- subagentId: string;
1188
- brief: string;
1189
- model?: string;
1190
- } | {
1191
- type: 'subagent_finished';
1192
- subagentId: string;
1193
- outcome: SubagentOutcome;
1194
- digest: string;
1195
- }
1196
- /**
1197
- * The agent asked to do something its read-only mode does not cover. Always
1198
- * auto-denied (T1) — a planner that can mutate is not a planner. The adapter
1199
- * is responsible for answering the agent so the turn does not hang.
1200
- */
1201
- | {
1202
- type: 'permission_request';
1203
- id: string;
1204
- name: string;
1205
- detail: string;
1206
- }
1207
- /**
1208
- * The agent delegated work to a subagent it left running in the background,
1209
- * and may end its turn before that work reports. Ordewell's conversation is
1210
- * request/response: a turn that ends hands control back to the user, and
1211
- * anything the agent says afterwards arrives with no turn open and is lost.
1212
- * Naming the launch is what lets the service ask for the results in time.
1213
- */
1214
- | {
1215
- type: 'background_agent';
1216
- id: string;
1217
- }
1218
- /** The agent finished its turn and is waiting for the next user message. */
1219
- | {
1220
- type: 'turn_end';
1221
- }
1222
- /** The turn failed. Carries the agent's own words — never a Ordewell paraphrase. */
1223
- | {
1224
- type: 'error';
1225
- message: string;
1226
- };
1227
- interface AgentStartOptions {
1228
- /** Workspace root. The agent explores from here and, in read-only mode, cannot leave it. */
1229
- cwd: string;
1230
- /** The planner system prompt, in its harness variant. */
1231
- systemPrompt: string;
1232
- /** Model id from the runner's own discovery catalog. Omitted means the agent's default. */
1233
- model?: string;
1234
- /** Variant / reasoning effort id from that model's `variants` list. */
1235
- effort?: string;
1236
- /**
1237
- * The agent's own session id from a previous run. A hint only: Ordewell's
1238
- * transcript is the source of truth (T4), so a failed resume degrades to a
1239
- * fresh session seeded from the stored history rather than an error.
1240
- */
1241
- resumeSessionId?: string;
1242
- }
1243
- interface AgentAdapter {
1244
- /** The runner id this adapter drives — `claude-code`, `codex`, `opencode`. */
1245
- readonly agentId: string;
1246
- /** Spawn the agent in its read-only mode and get it ready to receive messages. */
1247
- start(opts: AgentStartOptions): Promise<void>;
1248
- /**
1249
- * Send one user message and stream the turn's events until it ends. Resolves
1250
- * when the agent yields the floor; rejects only when the transport itself
1251
- * failed in a way no `error` event could describe.
1252
- *
1253
- * `onActivity`, when given, fires on raw transport traffic — every stdio
1254
- * line or stream chunk the process produces — independent of whether that
1255
- * traffic becomes an `AgentEvent`. An adapter may legitimately emit nothing
1256
- * for long stretches (a subagent's filtered output, most often); a caller
1257
- * using presence-of-events as a liveness signal would read that silence as
1258
- * a hang. `onActivity` is the seam that keeps liveness detection from being
1259
- * coupled to what each adapter chooses to surface.
1260
- */
1261
- send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
1262
- /** The agent's native session id once it has announced one. Resumption hint only. */
1263
- nativeSessionId(): string | null;
1264
- /** Kill the process and release its resources. Idempotent. */
1265
- dispose(): void;
1266
- }
1267
- /**
1268
- * The single injected boundary between Ordewell and the operating system —
1269
- * the same pattern `HeadlessRunnerDeps` uses for task execution. Tests feed
1270
- * recorded agent output through `spawn` (and, for HTTP-transport agents,
1271
- * `fetch`) so one test exercises adapter parsing, event mapping, reply
1272
- * classification and the repair loop as a single observable behavior.
1273
- */
1274
- interface AgentProcessDeps {
1275
- spawn: SpawnFn;
1276
- fetch: typeof globalThis.fetch;
1277
- /** Resolves the PATH agents are spawned under. Defaults to the augmented PATH. */
1278
- resolvePath?: () => Promise<string>;
1279
- /** Host platform. Defaults to the real one; injected so OS-specific behavior is testable anywhere. */
1280
- platform?: NodeJS.Platform;
1281
- /** True when `workspace` names an existing directory. Defaults to a real filesystem check. */
1282
- isDirectory?: (workspace: string) => boolean;
1283
- /** True when `candidate` names an existing, spawnable file. Defaults to a real filesystem check. */
1284
- exists?: (candidate: string) => boolean;
1285
- /** The workspace's own variables for a cwd (ADR-0016). Defaults to {@link resolveWorkspaceEnv}. */
1286
- workspaceEnv?: (cwd: string) => Promise<Record<string, string>>;
1287
- }
1288
- /** Builds the adapter for one runner id, or null when that runner cannot plan. */
1289
- type AgentAdapterFactory = (runner: string, deps: AgentProcessDeps) => AgentAdapter | null;
1290
- /**
1291
- * Split a stream of chunks into complete lines. Every agent transport here is
1292
- * newline-delimited JSON of some shape, and a chunk boundary lands mid-object
1293
- * often enough that parsing per-chunk silently drops events.
1294
- */
1295
- declare class LineBuffer {
1296
- private buffer;
1297
- push(chunk: string, onLine: (line: string) => void): void;
1298
- /** Anything left unterminated when the stream closed. */
1299
- flush(): string;
1300
- }
915
+ declare function renderTaskQueryAnswer(query: TaskQuery, tasks: readonly Task[], catalog: TaskQueryCatalog, liveOutput?: LiveOutputLookup): string;
1301
916
 
1302
917
  interface CliAgentAiServiceDeps extends Partial<AgentProcessDeps> {
1303
918
  /** Overrides adapter construction. Tests supply a fake agent; production picks by runner id. */
@@ -1371,6 +986,11 @@ declare class CliAgentAiService implements IAiService {
1371
986
  * at once, which all three of these do routinely.
1372
987
  */
1373
988
  private runTurn;
989
+ /**
990
+ * The only way this service starts an agent, and it takes a planner start by
991
+ * type: the read-only boundary (ADR-0008/0009) cannot be crossed into task
992
+ * mode from here without changing this signature.
993
+ */
1374
994
  private startAdapter;
1375
995
  /**
1376
996
  * The live agent process, restarted from its own session id if it died
@@ -1651,6 +1271,7 @@ declare abstract class BaseAiService {
1651
1271
 
1652
1272
  declare class GeminiService extends BaseAiService implements IAiService {
1653
1273
  private genAI;
1274
+ private genAIKey;
1654
1275
  private model;
1655
1276
  constructor(config: IConfig);
1656
1277
  private init;
@@ -1680,7 +1301,14 @@ declare class GeminiService extends BaseAiService implements IAiService {
1680
1301
 
1681
1302
  declare class OpenAiService extends BaseAiService implements IAiService {
1682
1303
  private client;
1304
+ private clientCredentials;
1683
1305
  constructor(config: IConfig);
1306
+ /**
1307
+ * Rebuilt whenever the key or endpoint differs from the one the client was
1308
+ * made with. A Session outlives `/key` and endpoint edits, so a client cached
1309
+ * for good kept sending the previous key: the provider answered 401 for a
1310
+ * key the user had already replaced.
1311
+ */
1684
1312
  private getClient;
1685
1313
  ensureInit(): void;
1686
1314
  private requireModel;
@@ -1702,16 +1330,18 @@ declare class OpenAiService extends BaseAiService implements IAiService {
1702
1330
 
1703
1331
  /**
1704
1332
  * The deep module owning all plan-shaped state. Holds `planTasks` (the ordered
1705
- * tree the user edits), the flattened `allTasks` view, the `taskMap` index, and
1706
- * the `completedTasks`/`failedTasks` sets the scheduler reads. The orchestrator
1707
- * calls `markCompleted`/`markFailed`/`markInProgress`/`retry` to update task
1708
- * status — it never mutates task state directly.
1333
+ * tree the user edits), the flattened `allTasks` view and the `taskMap` index.
1334
+ * The orchestrator calls `markCompleted`/`markFailed`/`markInProgress`/`retry`
1335
+ * to update task status — it never mutates task state directly.
1336
+ *
1337
+ * Completion and failure are read from `task.status` and nothing else, so
1338
+ * `isCompleted`, `completedCount`, `isAllComplete` and the scheduler's
1339
+ * dependency checks cannot disagree. The store owns its task objects: `load`
1340
+ * copies what it is given, the getters hand out readonly views, and
1341
+ * `snapshot` is the copy a caller may keep or write to disk.
1709
1342
  *
1710
1343
  * `rebuild` is the internal seam that keeps the flat views in sync with the
1711
- * tree. Structural removals (remove/merge/split) additionally prune
1712
- * `completedTasks`/`failedTasks` for ids that no longer exist; `removeFromActive`
1713
- * deliberately does not, so a completed task that leaves the active list still
1714
- * satisfies its dependents' dependency checks.
1344
+ * tree.
1715
1345
  *
1716
1346
  * `planRunners` lives here because it's part of the plan's identity (the runner
1717
1347
  * set, carried on the plan). `validateAssignedRunners` is pure store logic.
@@ -1720,15 +1350,13 @@ declare class PlanStore {
1720
1350
  private _planTasks;
1721
1351
  private _allTasks;
1722
1352
  private _taskMap;
1723
- private _completedTasks;
1724
- private _failedTasks;
1725
1353
  private _planRunners;
1726
1354
  private _onMutate;
1727
1355
  private _executionLog;
1728
- /** Hook called after every structural mutation (add/remove/update/merge/split/load). */
1356
+ /** Hook called after every structural mutation (add/remove/update/merge/split/resetForRun). */
1729
1357
  set onMutate(cb: (() => void) | null);
1730
- get planTasks(): Task[];
1731
- get allTasks(): Task[];
1358
+ get planTasks(): ReadonlyArray<Readonly<Task>>;
1359
+ get allTasks(): ReadonlyArray<Readonly<Task>>;
1732
1360
  get planRunners(): RunnerId[];
1733
1361
  get completedCount(): number;
1734
1362
  get failedCount(): number;
@@ -1736,8 +1364,10 @@ declare class PlanStore {
1736
1364
  isAnyFailed(): boolean;
1737
1365
  isCompleted(id: string): boolean;
1738
1366
  isFailed(id: string): boolean;
1739
- get(taskId: string): Task | undefined;
1740
- getExecutionLog(): TaskSnapshot[];
1367
+ get(taskId: string): Readonly<Task> | undefined;
1368
+ /** A copy of the task tree, detached from the store: later status changes do not reach it. */
1369
+ snapshot(): Task[];
1370
+ getExecutionLog(): ReadonlyArray<TaskSnapshot>;
1741
1371
  appendToLog(snapshot: TaskSnapshot): void;
1742
1372
  /**
1743
1373
  * Drop a task's archived snapshot. Un-marking a completion has to erase the
@@ -1745,10 +1375,10 @@ declare class PlanStore {
1745
1375
  * left-behind snapshot would keep feeding them a result that no longer exists.
1746
1376
  */
1747
1377
  removeFromLog(taskId: string): void;
1748
- removeFromActive(taskId: string): void;
1749
1378
  clearLog(): void;
1750
1379
  private notifyMutate;
1751
- load(tasks: Task[], runners: RunnerId[]): void;
1380
+ private countStatus;
1381
+ load(tasks: ReadonlyArray<Readonly<Task>>, runners: readonly RunnerId[]): void;
1752
1382
  add(partial: Partial<Task>): Task;
1753
1383
  remove(taskId: string): void;
1754
1384
  update(taskId: string, changes: Partial<Task>): Task | undefined;
@@ -1768,13 +1398,16 @@ declare class PlanStore {
1768
1398
  markCompleted(id: string): void;
1769
1399
  markFailed(id: string): void;
1770
1400
  markInProgress(id: string): void;
1771
- markAwaitingUser(id: string): void;
1401
+ markAwaitingUser(id: string, reason?: AwaitingReason): void;
1772
1402
  markPending(id: string): void;
1773
1403
  retry(id: string): void;
1404
+ /** A reason outlives nothing: any status but `awaiting_user` drops it. */
1405
+ private setStatus;
1774
1406
  blockDependents(id: string): void;
1775
1407
  unblockDependents(id: string): void;
1776
1408
  setTaskVerdict(id: string, verdict: Task['verdict']): void;
1777
1409
  setTaskOutputSummary(id: string, summary: Task['outputSummary']): void;
1410
+ setTaskTransport(id: string, transport: Task['transport']): void;
1778
1411
  getPlanVisualization(): {
1779
1412
  tasks: {
1780
1413
  id: string;
@@ -1791,16 +1424,310 @@ declare class PlanStore {
1791
1424
  * plan's first one instead.
1792
1425
  */
1793
1426
  admitRunner(runner: RunnerId): void;
1794
- resolveTaskRunner(task: Task): RunnerId;
1427
+ resolveTaskRunner(task: Readonly<Task>): RunnerId;
1795
1428
  private rebuild;
1429
+ private validateAssignedRunners;
1430
+ }
1431
+
1432
+ type IsolationNoticeLevel = 'info' | 'warn' | 'error';
1433
+ /**
1434
+ * What the controller reports. It never schedules or emits on its own: the
1435
+ * orchestrator turns these into observer events and decides what runs next.
1436
+ */
1437
+ interface IsolationRunListener {
1438
+ /** The run record changed and should be persisted with the plan. */
1439
+ changed(): void;
1796
1440
  /**
1797
- * Drop completed/failed ids that no longer exist in the plan. Called by the
1798
- * structural removals (remove/merge/split) — but NOT by removeFromActive,
1799
- * where a completed task leaves the active list yet must still satisfy its
1800
- * dependents' dependency checks.
1441
+ * A run opened — the one moment shared by Execute, a manual task run and
1442
+ * a force start, so whatever a run pins for its whole length is read here.
1801
1443
  */
1802
- private pruneTerminalSets;
1803
- private validateAssignedRunners;
1444
+ opened(): void;
1445
+ /** A run did not start: `repos` of the group have tracked changes. */
1446
+ blocked(repos: string[]): void;
1447
+ /** An isolated run closed and handed its integration branches over. */
1448
+ handoff(handoff: IsolationHandoff): void;
1449
+ /** Already said through the notifications; also for a surface that shows no toasts. */
1450
+ notice(level: IsolationNoticeLevel, message: string): void;
1451
+ /**
1452
+ * These tasks' worktrees are about to be removed. An agent left running in
1453
+ * one would sit in a deleted directory, so it has to go first.
1454
+ */
1455
+ releasing(taskIds: string[]): void;
1456
+ }
1457
+ interface IsolationRunControllerDeps {
1458
+ isolation: IWorktreeIsolation;
1459
+ config: IConfig;
1460
+ notifications: INotification;
1461
+ workspaceRoot: () => string;
1462
+ listener: IsolationRunListener;
1463
+ }
1464
+ /**
1465
+ * The lifecycle of a plan's isolation run (ADR-0013, ADR-0014), between the
1466
+ * scheduler and the git layer: deciding at a run's start whether it executes
1467
+ * in worktrees, a blocked run and how it goes on, each attempt's working
1468
+ * directory, releasing worktrees, the handoff when the run closes, and what the
1469
+ * user does with it afterwards. The record it keeps outlives one run — a
1470
+ * resumed plan continues it.
1471
+ */
1472
+ declare class IsolationRunController {
1473
+ private readonly isolation;
1474
+ private readonly config;
1475
+ private readonly notifications;
1476
+ private readonly workspaceRoot;
1477
+ private readonly listener;
1478
+ private run;
1479
+ /** Copied paths already reported for the current run: every task gets the same copies. */
1480
+ private reportedCopies;
1481
+ /**
1482
+ * How the open run executes; null while no run is open. A run is one
1483
+ * Execute-Plan or one manual task run, from its start until it settles or
1484
+ * is stopped.
1485
+ */
1486
+ private mode;
1487
+ /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
1488
+ private resolvers;
1489
+ private opening;
1490
+ /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
1491
+ private blockedStart;
1492
+ /** The dirty repos behind {@link blockedStart}, for the stash notice. */
1493
+ private blockedRepos;
1494
+ constructor(deps: IsolationRunControllerDeps);
1495
+ /** The plan's run record, open or not; null when the plan has not isolated. */
1496
+ get current(): IsolationRun | null;
1497
+ get isOpen(): boolean;
1498
+ /** The open run executes in worktrees. */
1499
+ get isolating(): boolean;
1500
+ /** A run is waiting on the user to stash or to go on without isolation. */
1501
+ get blocked(): boolean;
1502
+ /** What the plan persists of isolated execution; null when no run ever isolated. */
1503
+ get planIsolation(): PlanIsolation | null;
1504
+ requireRun(): IsolationRun;
1505
+ /** A task's record in the open run, only while that run isolates. */
1506
+ openRecord(taskId: string): IsolationTaskRecord | undefined;
1507
+ /** Where a task's isolated work stands; null when the plan has no isolation run to speak of. */
1508
+ taskIsolation(taskId: string): TaskIsolation | null;
1509
+ view(): IsolationView | null;
1510
+ /**
1511
+ * Take over a plan's persisted isolation, or none for a plan that has not
1512
+ * isolated yet. Whatever a crashed process left behind for the run — a
1513
+ * worktree still marked active, a directory no record owns — is pruned,
1514
+ * while kept, failed and conflicted worktrees stay for the user.
1515
+ *
1516
+ * Deliberately not reported as a change: adopting is not a change to
1517
+ * persist, and a host that adopts without persisting (VS Code's restore)
1518
+ * would otherwise write a new session file on every reload.
1519
+ */
1520
+ adopt(state: PlanIsolation | null): Promise<void>;
1521
+ /**
1522
+ * Open a run if none is: decide once, at its start, whether it executes in
1523
+ * worktrees. `resume` is what a dirty tree parks until the user chooses how
1524
+ * to go on. Resolves false when the run did not start.
1525
+ */
1526
+ open(resume: () => Promise<void>): Promise<boolean>;
1527
+ /**
1528
+ * Whether the run in force, or else the next one, gives each task its own
1529
+ * worktrees, and of which repo group — what the planner is told, since it
1530
+ * decides whether tasks on the same file have to be ordered and which shared
1531
+ * paths two tasks must not edit at once. A tree that would block counts as
1532
+ * not isolating: the user may yet run without isolation, and ordering is
1533
+ * the safe rule then.
1534
+ */
1535
+ plannerLayout(): Promise<IsolatedExecution>;
1536
+ /**
1537
+ * Go on with the start a dirty tree turned away. `stash` puts the user's
1538
+ * tracked changes on the git stash first, so the run isolates; `shared` runs
1539
+ * this one run in the workspace root, knowingly. Returns the parked start for
1540
+ * the caller to replay; null when nothing was parked.
1541
+ */
1542
+ continueBlocked(how: 'stash' | 'shared'): Promise<(() => Promise<void>) | null>;
1543
+ /**
1544
+ * The one place an attempt's working directory is decided: the worktree
1545
+ * prepared for it in an isolated run, the kept one for a conflict repair,
1546
+ * else the workspace root. `worktree` says which. Does not itself report a
1547
+ * worktree it creates as a change — the caller does once the attempt is
1548
+ * committed as running, so a spawn abandoned or failed after this settles
1549
+ * is not misreported as a change that stuck.
1550
+ */
1551
+ attemptCwd(task: Task, opts: {
1552
+ repair: boolean;
1553
+ }): Promise<{
1554
+ cwd: string;
1555
+ worktree: boolean;
1556
+ }>;
1557
+ /**
1558
+ * Land a task's work on the run's integration branches. The record is
1559
+ * reported changed once the landing is set and before the first merge, so
1560
+ * a crash mid-landing leaves the tips to roll back to, and again once it
1561
+ * settles. A git error is `failed`, never a throw.
1562
+ */
1563
+ integrate(task: Task): Promise<IsolationOutcome>;
1564
+ /** What a conflict repair's work shows (ADR-0015); git that cannot tell counts against it. */
1565
+ verifyRepair(task: Task): Promise<RepairEvidence>;
1566
+ /**
1567
+ * Let go of a task's worktree. `keep` leaves it and its branch for
1568
+ * inspection; otherwise both go. A landing still in flight settles first, so
1569
+ * the worktree is never torn down underneath its merge.
1570
+ */
1571
+ release(taskId: string, opts: {
1572
+ keep: boolean;
1573
+ }, landing?: Promise<unknown> | null): Promise<void>;
1574
+ /** Close the open run. An isolated one hands its integration branch over for review. */
1575
+ close(): Promise<void>;
1576
+ /**
1577
+ * A stop or a plan load: the open run ends without a handoff and a parked
1578
+ * start is dropped. `keepOpen` spares a run the scheduler is still driving.
1579
+ */
1580
+ interrupt(opts?: {
1581
+ keepOpen?: boolean;
1582
+ }): void;
1583
+ /**
1584
+ * Mark which added task resolves which conflict. The resolver merges the
1585
+ * conflicted task's branch by hand in its own worktree; once that lands, the
1586
+ * conflicted task can land in turn.
1587
+ */
1588
+ linkResolver(resolverId: string, conflictedId: string): void;
1589
+ /** The conflicted task a landed resolver was added for, forgotten as it is returned. */
1590
+ takeResolver(resolverId: string): string | undefined;
1591
+ reviewDiff(): Promise<string>;
1592
+ /**
1593
+ * "Merge all": the run's integration branches into whatever the user has
1594
+ * checked out, in every repo or none. Once everything merged, the run has
1595
+ * nothing left to hand over, so it is cleared up and forgotten; a branch
1596
+ * the user's HEAD somehow does not contain stays for the next run's sweep.
1597
+ */
1598
+ merge(): Promise<IsolationMergeResult>;
1599
+ /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
1600
+ cleanup(): Promise<void>;
1601
+ /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
1602
+ discard(): Promise<void>;
1603
+ private clearMerged;
1604
+ private forget;
1605
+ private decide;
1606
+ private activate;
1607
+ private begin;
1608
+ /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
1609
+ private sweep;
1610
+ /**
1611
+ * The plan's run carries on while it still has any record: anything landed
1612
+ * means a resumed plan's dependents must start from a tip that holds their
1613
+ * predecessors' work, and anything held — a kept attempt, a conflict, a
1614
+ * repair — is work the user may still want, which a fresh run's mint would
1615
+ * delete. Only a run with no records at all is superseded.
1616
+ */
1617
+ private continuableRun;
1618
+ /**
1619
+ * A run with no records at all holds only superseded attempts, so it goes
1620
+ * whole. One that cannot be continued for another reason — it ran from a
1621
+ * different workspace path — keeps its integration branch in each repo that
1622
+ * has not merged it: only the user gives landed work up.
1623
+ */
1624
+ private mint;
1625
+ private reportCopies;
1626
+ private tell;
1627
+ }
1628
+
1629
+ /** Which repair of a task an attempt is, of the most `conflictRepairAttempts` allows. */
1630
+ interface RepairAttempt {
1631
+ n: number;
1632
+ limit: number;
1633
+ }
1634
+ /** One thing the user is told about a landing. */
1635
+ interface LandingMessage {
1636
+ level: 'info' | 'warn' | 'error';
1637
+ text: string;
1638
+ /** Also handed to a surface that shows no toasts: ADR-0015 logs every conflict repair as a notice. */
1639
+ repairLog?: true;
1640
+ }
1641
+ /**
1642
+ * How a landing settled, for the orchestrator to apply — it marks the task,
1643
+ * starts the repair and says the messages. By the time one is returned the
1644
+ * merge has happened or not, and the run record says so.
1645
+ *
1646
+ * - `landed`: the work is on the integration branch of every repo it changed.
1647
+ * - `nothing-to-land`: the attempt ran in the workspace root, or the task
1648
+ * holds no unlanded work; it is done as it stands.
1649
+ * - `repair-needed`: the landing conflicted and the task is owed `repair`
1650
+ * (ADR-0015), in the worktree kept for it.
1651
+ * - `awaiting_user`: the work did not land and waits on the user, worktree
1652
+ * kept — a conflict with no repair left (`conflict`), a merge git refused
1653
+ * (`landing-failed`), or a repair that did not land (`repair-failed`). None
1654
+ * of them halts the run.
1655
+ */
1656
+ type LandingOutcome = {
1657
+ kind: 'landed';
1658
+ messages: LandingMessage[];
1659
+ } | {
1660
+ kind: 'nothing-to-land';
1661
+ messages: LandingMessage[];
1662
+ } | {
1663
+ kind: 'repair-needed';
1664
+ repair: RepairAttempt;
1665
+ messages: LandingMessage[];
1666
+ } | {
1667
+ kind: 'awaiting_user';
1668
+ reason: 'conflict' | 'landing-failed' | 'repair-failed';
1669
+ messages: LandingMessage[];
1670
+ };
1671
+ interface LandingDeps {
1672
+ runs: IsolationRunController;
1673
+ config: Pick<IConfig, 'conflictRepairAttempts'>;
1674
+ /** Read-only: only the orchestrator changes a task. */
1675
+ tasks: Pick<PlanStore, 'get'>;
1676
+ }
1677
+ /**
1678
+ * Landing and Conflict repair (ADR-0013, ADR-0014, ADR-0015): a passed
1679
+ * attempt's work onto the run's integration branches through the
1680
+ * {@link IsolationRunController}, and what a landing that did not go through
1681
+ * leads to. It answers with a {@link LandingOutcome} rather than acting on it —
1682
+ * it never marks a task, starts an attempt or emits — so the scheduler stays
1683
+ * the one place that decides what runs. A repair is spent when it starts: the
1684
+ * controller's `reopen` counts it before the runner spawns, and nothing here
1685
+ * hands one back.
1686
+ */
1687
+ declare class Landing {
1688
+ private readonly runs;
1689
+ private readonly config;
1690
+ private readonly tasks;
1691
+ constructor(deps: LandingDeps);
1692
+ /**
1693
+ * Land a passed attempt's work. One whose worktree has no unlanded record
1694
+ * behind it any more — the run was discarded under it — cannot land.
1695
+ */
1696
+ landPassed(task: Task, attempt: {
1697
+ worktree: boolean;
1698
+ }): Promise<LandingOutcome>;
1699
+ /** Mark complete is a passed verdict the user vouches for: what the task holds unlanded lands the same way. */
1700
+ landVouched(task: Task): Promise<LandingOutcome>;
1701
+ /**
1702
+ * A repair's verdict decides only whether its work may try to land; the
1703
+ * task keeps the verdict its own work earned. Evidence comes before the
1704
+ * queue, and the landing after it is the one every task goes through, so a
1705
+ * repair the tip has moved past again is a fresh conflict, not a pass.
1706
+ */
1707
+ settleRepair(task: Task, verdict: Verdict): Promise<LandingOutcome>;
1708
+ /** A repair that ended without landing leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
1709
+ unrepaired(task: Task, why: string): Promise<Extract<LandingOutcome, {
1710
+ kind: 'awaiting_user';
1711
+ }>>;
1712
+ /**
1713
+ * Land the conflicted task a resolver was added for, now that the resolver
1714
+ * has landed. Its branch is on the integration branch by then, so it merges
1715
+ * clean — and one the resolver did not really bring along conflicts again
1716
+ * instead of being taken at its word. Null when there is nothing to land.
1717
+ */
1718
+ landResolved(resolverId: string): Promise<{
1719
+ task: Readonly<Task>;
1720
+ outcome: LandingOutcome;
1721
+ } | null>;
1722
+ /** 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. */
1723
+ nextRepair(taskId: string): RepairAttempt | null;
1724
+ /** What a repair is asked to do; read once `reopen` has recorded the tips it must bring in. */
1725
+ repairPrompt(task: Task): string;
1726
+ private hasUnlandedWork;
1727
+ private unmerged;
1728
+ private noRepairReason;
1729
+ private describeEvidence;
1730
+ private isGroup;
1804
1731
  }
1805
1732
 
1806
1733
  /**
@@ -1833,6 +1760,15 @@ interface WorkspaceEnv {
1833
1760
  interface OrchestratorObserver {
1834
1761
  /** Any task-shaped state changed (store mutation, checkpoint, retry, …). */
1835
1762
  onTaskChanged?(): void;
1763
+ /**
1764
+ * A run reached an outcome that stands until the user acts — completed,
1765
+ * failed, or awaiting the user — on its own, not as the answer to a call.
1766
+ * Emitted before the `onTaskChanged` that announces it, so a listener can
1767
+ * save the outcome before any surface is told.
1768
+ */
1769
+ onTaskSettled?(data: {
1770
+ taskId: string;
1771
+ }): void;
1836
1772
  onTick?(): void;
1837
1773
  onExecutionComplete?(): void;
1838
1774
  /** Queued user messages are ready to be processed by the planner. */
@@ -1868,6 +1804,14 @@ interface OrchestratorObserver {
1868
1804
  message: string;
1869
1805
  }): void;
1870
1806
  }
1807
+ /**
1808
+ * A message or interrupt a task cannot take: it is not running, it runs on the
1809
+ * terminal transport, or it is waiting on something a message does not answer.
1810
+ * The request is wrong, not the orchestrator, so a surface says why.
1811
+ */
1812
+ declare class TaskControlError extends Error {
1813
+ constructor(message: string);
1814
+ }
1871
1815
  type AttemptPhase = 'starting' | 'running' | 'integrating';
1872
1816
  /** Read-only view of a task's live attempt. */
1873
1817
  interface TaskAttemptSnapshot {
@@ -1879,6 +1823,50 @@ interface TaskAttemptSnapshot {
1879
1823
  cwd: string | null;
1880
1824
  startedAt: string;
1881
1825
  }
1826
+ /**
1827
+ * The wiring a {@link TaskOrchestrator} runs on, every collaborator resolved.
1828
+ * {@link createTaskOrchestrator} is the only production caller; tests build one
1829
+ * through that factory too. The constructor makes nothing itself, so there is
1830
+ * exactly one place defaults live.
1831
+ */
1832
+ interface TaskOrchestratorDeps {
1833
+ config: IConfig;
1834
+ notifications: INotification;
1835
+ terminalRunner: ITerminalRunner;
1836
+ store: PlanStore;
1837
+ output: TaskOutputSource;
1838
+ /** The plan's isolation run and the open run's lifecycle (ADR-0013). */
1839
+ runs: IsolationRunController;
1840
+ /** A passed attempt's work onto the integration branch, and any conflict repair. */
1841
+ landing: Landing;
1842
+ registry: RunnerRegistry | null;
1843
+ /** The workspace root, read at call time — VS Code can change it. */
1844
+ workspaceRoot: () => string;
1845
+ /** The workspace's own variables for a task's cwd (ADR-0016). */
1846
+ workspaceEnv: (cwd: string) => Promise<WorkspaceEnv>;
1847
+ /** Read live at each spawn, so a toggle takes effect on the next task. */
1848
+ tddEnabled: () => boolean;
1849
+ /** Read once as a run opens, never per spawn (ADR-0018, S1). */
1850
+ runnerTransport: () => RunnerTransport;
1851
+ }
1852
+ /**
1853
+ * What a caller supplies to {@link createTaskOrchestrator}; every optional one
1854
+ * gets its default there. This is what a host or test chooses, not the resolved
1855
+ * wiring the orchestrator itself runs on.
1856
+ */
1857
+ interface TaskOrchestratorOptions {
1858
+ config: IConfig;
1859
+ notifications: INotification;
1860
+ terminalRunner: ITerminalRunner;
1861
+ store?: PlanStore;
1862
+ output?: TaskOutputSource;
1863
+ isolation?: IWorktreeIsolation;
1864
+ registry?: RunnerRegistry | null;
1865
+ workspaceRoot?: () => string;
1866
+ workspaceEnv?: (cwd: string) => Promise<WorkspaceEnv>;
1867
+ tddEnabled?: () => boolean;
1868
+ runnerTransport?: () => RunnerTransport;
1869
+ }
1882
1870
  /**
1883
1871
  * The pure scheduler. Owns execution state (`running`, `planStatus`,
1884
1872
  * `reviewApproved`, the live task attempts, `messageQueue`) and the verifier.
@@ -1893,25 +1881,17 @@ declare class TaskOrchestrator {
1893
1881
  private config;
1894
1882
  private notifications;
1895
1883
  private terminalRunner;
1896
- private output;
1897
1884
  private store;
1885
+ private output;
1898
1886
  private attempts;
1899
1887
  private verifier;
1900
1888
  private running;
1901
1889
  private planStatus;
1902
1890
  private messageQueue;
1903
- private queueSeq;
1904
1891
  private reviewApproved;
1905
1892
  private retryCounts;
1906
1893
  private spawnCounts;
1907
- /**
1908
- * The terminal a verdict left open, by task. It stays so the user can read
1909
- * the agent's output or keep talking to it — but only while its worktree
1910
- * does: once that is removed the agent sits in a deleted directory, and a
1911
- * newer attempt in the same worktree would share it with a second agent.
1912
- * Without this, every task of every run left one agent process running
1913
- * until the daemon stopped.
1914
- */
1894
+ /** The terminal a verdict left open, by task; see {@link LingeringRunners}. */
1915
1895
  private lingering;
1916
1896
  /**
1917
1897
  * A full-plan run a failure paused. Retrying the failed task is the explicit
@@ -1930,53 +1910,69 @@ declare class TaskOrchestrator {
1930
1910
  * task whose spawn always throws.
1931
1911
  */
1932
1912
  private onHold;
1933
- private isolation;
1934
- /** The plan's isolation run (ADR-0013). Outlives one run: a resumed plan continues it. */
1935
- private isolationRun;
1936
- /** Copied paths already reported for the current run: every task gets the same copies. */
1937
- private reportedCopies;
1938
- /**
1939
- * How the open run executes; null while no run is open. A run is one
1940
- * Execute-Plan or one manual task run, from its start until it settles or
1941
- * is stopped.
1942
- */
1943
- private runMode;
1944
- /** Resolver task id → the conflicted task it resolves; see {@link linkConflictResolver}. */
1945
- private resolvers;
1946
- private opening;
1947
- /** The start a dirty tree turned away, replayed once the user chooses how to go on. */
1948
- private blockedStart;
1949
- /** The dirty repos behind {@link blockedStart}, for the stash notice. */
1950
- private blockedRepos;
1913
+ /** The plan's isolation run and the open run's lifecycle (ADR-0013); see {@link IsolationRunController}. */
1914
+ private runs;
1915
+ /** A passed attempt's work onto the integration branch, and the conflict repair a landing may need. */
1916
+ private landing;
1951
1917
  private registry;
1952
1918
  private workspaceRootFn;
1953
1919
  private observers;
1954
1920
  private tddEnabled;
1955
- constructor(config: IConfig, notifications: INotification, terminalRunner: ITerminalRunner, store?: PlanStore, output?: TaskOutputSource, isolation?: IWorktreeIsolation);
1921
+ private readRunnerTransport;
1922
+ /**
1923
+ * The setting as the latest run copied it when it opened. Every spawn of
1924
+ * that run uses this, so flipping the setting mid-run changes the next run
1925
+ * only — the plan, not a live setting, says what runs (ADR-0001).
1926
+ */
1927
+ private planTransport;
1928
+ constructor(deps: TaskOrchestratorDeps);
1929
+ /**
1930
+ * The composition root: resolves every optional collaborator and wires the
1931
+ * isolation listener that the constructor cannot, because it needs the
1932
+ * instance's own (private) emit and lingering seams. Nothing else in the
1933
+ * codebase constructs an orchestrator, so this is the one place defaults
1934
+ * live — a host or test supplies what it cares about and gets the rest.
1935
+ */
1936
+ static compose(options: TaskOrchestratorOptions): TaskOrchestrator;
1956
1937
  /** Advisory silence timestamp for a task's live runner, or null if not idle. */
1957
1938
  getIdleSince(taskId: string): string | null;
1958
1939
  /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
1959
1940
  getLiveOutput(taskId: string, opts: LiveTailOptions): LiveTail | null;
1960
1941
  get storeInstance(): PlanStore;
1961
- setWorkspaceRoot(fn: () => string): void;
1962
- setWorkspaceEnvResolver(resolve: (cwd: string) => Promise<WorkspaceEnv>): void;
1963
1942
  /**
1964
1943
  * The variables a task's agent gets from its workspace. What cannot be
1965
1944
  * applied is said once — silently starting without them is how agents ran
1966
1945
  * under the wrong account when an edited `.envrc` was left unallowed.
1967
1946
  */
1968
1947
  private envForTask;
1969
- setRegistry(registry: RunnerRegistry): void;
1970
- /**
1971
- * A getter rather than a value where the caller has one: every task gets its
1972
- * prompt composed at spawn time, but only a full-plan run passes through a
1973
- * point where a snapshot could be refreshed — so "Run task", force-start and
1974
- * retry would compose against whatever the last run happened to set.
1975
- */
1976
- setTddEnabled(enabled: boolean | (() => boolean)): void;
1977
1948
  approveCheckpoint(taskId: string): void;
1978
1949
  rejectCheckpoint(taskId: string, reason?: string): void;
1950
+ private atCheckpoint;
1951
+ /**
1952
+ * Send a structured task a user message (ADR-0018, M1). Mid-turn it queues
1953
+ * behind the turn; a task waiting for input takes it at once and is back in
1954
+ * progress. Returns the message's id, for {@link removeQueuedTaskMessage}.
1955
+ */
1956
+ sendTaskMessage(taskId: string, text: string): string;
1957
+ /** Take back a message still queued behind a turn; false once it was delivered. */
1958
+ removeQueuedTaskMessage(taskId: string, id: string): boolean;
1959
+ /** Stop a structured task's running turn; it then waits for input like any turn that ends without its marker. */
1960
+ interruptTask(taskId: string): Promise<void>;
1961
+ /** What is waiting for a structured task's turn to end; empty for any other task. */
1962
+ getQueuedTaskMessages(taskId: string): QueuedTaskMessage[];
1963
+ private structuredSession;
1964
+ /**
1965
+ * A structured turn that ended without the done marker (ADR-0018, W1). No
1966
+ * verdict is guessed: the task waits for input, unless a checkpoint in the
1967
+ * same turn already has it waiting, or a queued message went straight out —
1968
+ * the session never passes through idle then, so nothing flickers.
1969
+ */
1970
+ private onTurnEnd;
1979
1971
  subscribe(observer: OrchestratorObserver): () => void;
1972
+ /**
1973
+ * Isolated per observer: one that throws must not take down the scheduler
1974
+ * or stop the remaining observers from hearing the event.
1975
+ */
1980
1976
  private emit;
1981
1977
  get isRunning(): boolean;
1982
1978
  /**
@@ -2005,35 +2001,24 @@ declare class TaskOrchestrator {
2005
2001
  get awaitingIsolationChoice(): boolean;
2006
2002
  /**
2007
2003
  * Take over a plan's persisted isolation, or none for a plan that has not
2008
- * isolated yet. Whatever a crashed process left behind for the run — a
2009
- * worktree still marked active, a directory no record owns — is pruned,
2010
- * while kept, failed and conflicted worktrees stay for the user.
2011
- *
2012
- * Deliberately silent on the observer: adopting is not a change to persist,
2013
- * and a host that adopts without persisting (VS Code's restore) would
2014
- * otherwise write a new session file on every reload.
2004
+ * isolated yet, pruning what a crashed process left behind. Silent on the
2005
+ * observer: adopting is not a change to persist.
2015
2006
  */
2016
2007
  adoptIsolation(state: PlanIsolation | null): Promise<void>;
2017
2008
  reviewRunDiff(): Promise<string>;
2018
- /**
2019
- * "Merge all": the run's integration branches into whatever the user has
2020
- * checked out, in every repo or none. Once everything merged, the run has
2021
- * nothing left to hand over, so it is cleared up and forgotten; a branch
2022
- * the user's HEAD somehow does not contain stays for the next run's sweep.
2023
- */
2009
+ /** "Merge all"; a run that merged everything is cleared up and forgotten. */
2024
2010
  mergeRun(): Promise<IsolationMergeResult>;
2025
- private clearMergedRun;
2026
2011
  /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
2027
2012
  cleanupRun(): Promise<void>;
2028
2013
  /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
2029
2014
  discardRun(): Promise<void>;
2030
2015
  private watchBlockingPrompts;
2031
- private closeLingering;
2032
- private closeLingeringOf;
2033
- private forgetRun;
2034
- private requireRun;
2035
2016
  /** What the plan persists of isolated execution; null when no run ever isolated. */
2036
2017
  get isolationRecord(): PlanIsolation | null;
2018
+ /** The transport the plan's latest run copied from the setting; null before its first run. */
2019
+ get runnerTransport(): RunnerTransport | null;
2020
+ /** Take over a saved plan's copied transport. The next run copies the setting afresh. */
2021
+ adoptRunnerTransport(transport: RunnerTransport | null): void;
2037
2022
  queueMessage(text: string): void;
2038
2023
  getQueuedMessages(): QueuedMessage[];
2039
2024
  /** Take one unsent message back out of the queue; false when it was never there (or already drained). */
@@ -2041,7 +2026,7 @@ declare class TaskOrchestrator {
2041
2026
  setQueuedMessages(messages: QueuedMessage[]): void;
2042
2027
  clearQueuedMessages(): void;
2043
2028
  processNextQueuedMessage(): QueuedMessage | null;
2044
- loadPlan(tasks: Task[], planRunners?: RunnerId[]): void;
2029
+ loadPlan(tasks: readonly Task[], planRunners?: RunnerId[]): void;
2045
2030
  /**
2046
2031
  * Adopt an edited plan without letting go of the run in progress. Everything
2047
2032
  * the scheduler owns — live sessions, holds, retry counts, the verifier and
@@ -2059,42 +2044,29 @@ declare class TaskOrchestrator {
2059
2044
  stop(): void;
2060
2045
  onUserTaskComplete(taskId: string): Promise<void>;
2061
2046
  private onVerdict;
2062
- /** Whether a stopped runner's own tail says its account, not the task, ran out. */
2063
- private stoppedOnUsageLimit;
2064
2047
  private afterVerdict;
2065
2048
  /**
2066
- * A repair's verdict decides only whether its work may try to land. It never
2067
- * replaces the verdict the task's own work earned, and a repair that does not
2068
- * land leaves the task waiting on the user — never a failed task, so never a
2069
- * halted run.
2049
+ * A repair's verdict never replaces the verdict the task's own work earned,
2050
+ * and a repair that does not land leaves the task waiting on the user —
2051
+ * never a failed task, so never a halted run.
2070
2052
  */
2071
2053
  private settleRepair;
2072
2054
  /**
2073
- * Evidence before the queue: the repair's work is committed and checked, and
2074
- * only then merged, through the same serialized landing as any other task —
2075
- * so a repair the tip has moved past again is a fresh conflict, not a pass.
2055
+ * Hand an attempt's verdict to Landing. The attempt stays live while that
2056
+ * settles — it may wait on the module's merge queue — so a cancel, retry or
2057
+ * stop in that window still wins, and waits for the merge before tearing the
2058
+ * worktree down; nothing counts the task as done before its work is on the
2059
+ * integration branch.
2076
2060
  */
2077
- private landRepair;
2078
- private landedRepair;
2079
- /** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
2080
- private unrepaired;
2081
- private describeEvidence;
2061
+ private land;
2062
+ /** Settle a task by how its landing went; `onLanded` is what the path that landed it adds to completing it. */
2063
+ private applyLanding;
2082
2064
  /**
2083
- * Merge a passed attempt's worktree. The attempt stays live while it waits on
2084
- * the module's merge queue, so a cancel, retry or stop in that window still
2085
- * wins, and nothing counts the task as done before its work is on the
2086
- * integration branch.
2065
+ * Work that did not land waits on the user — it never fails the task or
2066
+ * halts the run — unless its conflict is owed a repair, which takes the slot
2067
+ * the ending attempt freed and never one more than the run allows.
2087
2068
  */
2088
- private integrate;
2089
- /** A worktree whose work is not on the integration branch yet. */
2090
- private hasUnlandedWork;
2091
- private integrateWork;
2092
- /** Settle a task whose work passed, by what its integration reported. */
2093
- private landPassed;
2094
- private landUnmerged;
2095
- /** 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. */
2096
- private nextRepair;
2097
- private noRepairReason;
2069
+ private settleUnlanded;
2098
2070
  /**
2099
2071
  * Mark which added task resolves which conflict. The resolver merges the
2100
2072
  * conflicted task's branch by hand in its own worktree; once that lands, the
@@ -2104,21 +2076,7 @@ declare class TaskOrchestrator {
2104
2076
  */
2105
2077
  linkConflictResolver(resolverId: string, conflictedId: string): void;
2106
2078
  private landResolved;
2107
- /**
2108
- * Let go of a task's worktree. `keep` leaves it and its branch for
2109
- * inspection; otherwise both go. A merge still in flight finishes first, so
2110
- * the worktree is never torn down underneath it.
2111
- */
2112
- private releaseWorktree;
2113
2079
  getReadyTasks(): Task[];
2114
- /**
2115
- * In an isolated run a dependency is met once its work is on the integration
2116
- * branch, not merely once it passed: the dependent's worktree is cut from
2117
- * that branch, so starting earlier would hand it a tree without the work it
2118
- * depends on.
2119
- */
2120
- private dependencyMet;
2121
- isBlocked(task: Task): boolean;
2122
2080
  /**
2123
2081
  * Cancel a running (or scheduled) task: kill its session and return it to
2124
2082
  * 'pending' — "not executed". The task is put on hold so the scheduler
@@ -2151,6 +2109,15 @@ declare class TaskOrchestrator {
2151
2109
  markTaskIncomplete(taskId: string): Promise<void>;
2152
2110
  private logAndArchive;
2153
2111
  retryTask(taskId: string): Promise<void>;
2112
+ /**
2113
+ * Continue a finished structured task in its saved runner session (ADR-0018,
2114
+ * K1): a retry whose first turn is the user's message, sent to the session
2115
+ * the task ended in rather than a fresh one given its prompt again. Like a
2116
+ * retry it is a new attempt in a fresh worktree from the integration tip —
2117
+ * the same path, which is where the runner keeps its sessions — verified and
2118
+ * landed like any other, and dependents are left alone.
2119
+ */
2120
+ continueTask(taskId: string, message: string): Promise<void>;
2154
2121
  /**
2155
2122
  * Manually start a single AI task right now, bypassing dependency/readiness
2156
2123
  * gating (the "force start" affordance on a task card). Reuses the scheduler's
@@ -2183,55 +2150,37 @@ declare class TaskOrchestrator {
2183
2150
  tick(): Promise<void>;
2184
2151
  private startTask;
2185
2152
  /**
2186
- * A spawn whose attempt ended while it was in flight: kill what it produced
2187
- * and take back the claim it made. Only the claim — whatever ended the
2188
- * attempt may have decided the task since (mark complete, cancel, retry).
2153
+ * The fallible half of starting a task: resolving its cwd/worktree and
2154
+ * spawning the runner. Resolves false when the attempt never committed —
2155
+ * abandoned because it was ended from under it, or a failed spawn already
2156
+ * unwound (both leave `startTask` with nothing further to announce).
2189
2157
  */
2190
- private abandonSpawn;
2191
- private repairPrompt;
2158
+ private spawnAttempt;
2192
2159
  /**
2193
- * The one place an attempt's working directory is decided. Async so a
2194
- * per-attempt workspace (worktree isolation, #12) can be prepared here.
2195
- */
2196
- private resolveAttemptCwd;
2197
- private tell;
2198
- private reportCopies;
2199
- /**
2200
- * Open a run if none is: decide once, at its start, whether it executes in
2201
- * worktrees. `resume` is what a dirty tree parks until the user chooses how
2202
- * to go on. Resolves false when the run did not start.
2160
+ * Say on the task how this attempt is driven, when its plan asked for the
2161
+ * structured transport; a terminal plan's tasks record nothing. A structured
2162
+ * request that came back a terminal session is a fallback, and says why —
2163
+ * a host without a router included — never a silent downgrade.
2203
2164
  */
2204
- private openRun;
2165
+ private recordTransport;
2205
2166
  /**
2206
- * Whether the run in force, or else the next one, gives each task its own
2207
- * worktrees, and of which repo group — what the planner is told, since it
2208
- * decides whether tasks on the same file have to be ordered and which shared
2209
- * paths two tasks must not edit at once. A tree that would block counts as
2210
- * not isolating: the user may yet run without isolation, and ordering is
2211
- * the safe rule then.
2167
+ * A continue whose runner never announced the session it was told to
2168
+ * resume. Nothing was continued, and nothing fresh was started in its place.
2212
2169
  */
2213
- plannerIsolation(): Promise<IsolatedExecution>;
2214
- private decideRun;
2215
- private activate;
2216
- /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
2217
- private sweep;
2170
+ private unresumed;
2218
2171
  /**
2219
- * The plan's run carries on while it still has any record: anything landed
2220
- * means a resumed plan's dependents must start from a tip that holds their
2221
- * predecessors' work, and anything held — a kept attempt, a conflict, a
2222
- * repair — is work the user may still want, which a fresh run's mint would
2223
- * delete. Only a run with no records at all is superseded.
2172
+ * A spawn whose attempt ended while it was in flight: kill what it produced
2173
+ * and take back the claim it made. Only the claim — whatever ended the
2174
+ * attempt may have decided the task since (mark complete, cancel, retry).
2224
2175
  */
2225
- private continuableRun;
2176
+ private abandonSpawn;
2177
+ private tell;
2178
+ private say;
2226
2179
  /**
2227
- * A run with no records at all holds only superseded attempts, so it goes
2228
- * whole. One that cannot be continued for another reason — it ran from a
2229
- * different workspace path — keeps its integration branch in each repo that
2230
- * has not merged it: only the user gives landed work up.
2180
+ * Whether the run in force, or else the next one, gives each task its own
2181
+ * worktrees, and of which repo group — what the planner is told.
2231
2182
  */
2232
- private mintRun;
2233
- /** Close the open run. An isolated one hands its integration branch over for review. */
2234
- private closeRun;
2183
+ plannerIsolation(): Promise<IsolatedExecution>;
2235
2184
  /**
2236
2185
  * Replay the start a dirty tree turned away. `stash` puts the user's tracked
2237
2186
  * changes on the git stash first, so the run isolates; `shared` runs this one
@@ -2248,12 +2197,20 @@ declare class TaskOrchestrator {
2248
2197
  * stop(), and if that exit reaches VerdictEngine under the still-valid
2249
2198
  * generation it delivers a verdict that marks the task 'completed' for one
2250
2199
  * tick — long enough for the scheduler to start a dependent task. A verdict
2251
- * leaves the runner up so its terminal stays readable, and stop/load reset
2252
- * the whole verifier themselves.
2200
+ * leaves a terminal runner up so its screen stays readable (a structured one
2201
+ * ends), and stop/load reset the whole verifier themselves.
2253
2202
  */
2254
2203
  private endAttempt;
2204
+ /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
2205
+ private saveNativeSession;
2255
2206
  private endAllAttempts;
2256
2207
  }
2208
+ /**
2209
+ * Build a {@link TaskOrchestrator}, filling in every collaborator a caller did
2210
+ * not inject. The one entry point for hosts, bench harnesses and tests alike;
2211
+ * {@link TaskOrchestrator} itself constructs nothing.
2212
+ */
2213
+ declare function createTaskOrchestrator(options: TaskOrchestratorOptions): TaskOrchestrator;
2257
2214
 
2258
2215
  /**
2259
2216
  * Everything needed to produce a plan from a goal. Carries the planning context
@@ -2570,6 +2527,11 @@ interface EnvAdmission {
2570
2527
  */
2571
2528
  declare function admitSettingsEnv(env: Record<string, unknown>): EnvAdmission;
2572
2529
 
2530
+ /** Whether a runner's tasks can run on the structured transport. */
2531
+ declare function supportsTaskMode(runner: string): boolean;
2532
+ /** The adapter that drives one task. Throws {@link TaskModeUnsupportedError} for a runner without a connector. */
2533
+ declare function createTaskAdapter(runner: string, deps: AgentProcessDeps): TaskModeAgentAdapter;
2534
+
2573
2535
  interface SpawnSpec {
2574
2536
  command: string;
2575
2537
  args: string[];
@@ -2615,12 +2577,16 @@ declare abstract class StdioAgentAdapter implements AgentAdapter {
2615
2577
  private betweenTurns;
2616
2578
  /** Set once a turn has ended: after that, out-of-turn events are stale, not startup. */
2617
2579
  private hadTurn;
2580
+ /** A task's session, which wants what the agent says on its own after a turn closed; a planner registers none and drops it. */
2581
+ private outOfTurnListener;
2618
2582
  private disposed;
2619
2583
  /** Resolves when the process ends, so a handshake can lose the race instead of waiting out its timeout. */
2620
2584
  protected processEnded: Promise<void>;
2621
2585
  /** The environment the agent was spawned under, for any side process a handshake needs. */
2622
2586
  protected spawnEnv: NodeJS.ProcessEnv;
2623
2587
  private markEnded;
2588
+ /** What this process is to the user — a planner or a task's runner — for the words a failure is reported in. */
2589
+ protected role: 'planner' | 'task';
2624
2590
  constructor(deps: AgentProcessDeps);
2625
2591
  /** The command line that starts this agent in its read-only mode. */
2626
2592
  protected abstract spawnSpec(opts: AgentStartOptions): SpawnSpec;
@@ -2635,7 +2601,9 @@ declare abstract class StdioAgentAdapter implements AgentAdapter {
2635
2601
  protected handshake(_opts: AgentStartOptions): Promise<void>;
2636
2602
  start(opts: AgentStartOptions): Promise<void>;
2637
2603
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2604
+ onOutOfTurn(listener: (event: AgentEvent) => void): void;
2638
2605
  nativeSessionId(): string | null;
2606
+ onProcessExit(listener: (code: number) => void): void;
2639
2607
  dispose(): void;
2640
2608
  /** Write one raw protocol line to the agent. */
2641
2609
  protected writeLine(payload: unknown): void;
@@ -2655,8 +2623,15 @@ declare abstract class StdioAgentAdapter implements AgentAdapter {
2655
2623
  * same way. It is the richest of the three streams — partial messages and
2656
2624
  * separate thinking blocks — which is why this agent went first.
2657
2625
  */
2658
- declare class ClaudeCodeAdapter extends StdioAgentAdapter {
2626
+ declare class ClaudeCodeAdapter extends StdioAgentAdapter implements TaskModeAgentAdapter {
2659
2627
  readonly agentId = "claude-code";
2628
+ private interruptCount;
2629
+ /** Interrupts sent and not yet acknowledged, by request id. */
2630
+ private readonly pendingControl;
2631
+ /** An interrupt was sent during the current turn, so an aborted result is that interrupt, not a failure. */
2632
+ private interruptRequested;
2633
+ /** A task's tool requests still waiting for an answer, by request id, with what the answer echoes back. */
2634
+ private readonly openPermissions;
2660
2635
  /** Whether this turn has already emitted reply text — see {@link handleLine}. */
2661
2636
  private turnHasText;
2662
2637
  /** The text block streaming now follows earlier reply text, so its first delta opens the paragraph. */
@@ -2676,9 +2651,63 @@ declare class ClaudeCodeAdapter extends StdioAgentAdapter {
2676
2651
  private readonly openSubagents;
2677
2652
  /** Subagent messages already counted. A message arrives as one line per content block, each repeating its usage. */
2678
2653
  private readonly countedSubagentMessages;
2654
+ /** Background tasks (shells, agents) the CLI reports as still running, as of its last `background_tasks_changed`. */
2655
+ private backgroundTaskCount;
2656
+ /**
2657
+ * A task's turn whose `result` arrived while background work was still open.
2658
+ * The CLI reports that result when the model stops talking, then opens a turn
2659
+ * of its own when the work finishes; a turn ended at the first result would
2660
+ * lose everything said after it, the completion marker included.
2661
+ */
2662
+ private resultHeld;
2663
+ private followOnTimer;
2664
+ /** The mode a task asked for, to hold the CLI to it once its `init` says what it started in. */
2665
+ private requestedMode;
2666
+ /** The session `--resume` asked for, until the CLI's `init` shows it was taken up. */
2667
+ private pendingResume;
2679
2668
  protected spawnSpec(opts: AgentStartOptions): SpawnSpec;
2669
+ /**
2670
+ * A task's run: the manifest decides what its mode and effort mean
2671
+ * (ADR-0001), and this adds only the protocol around them. No tool list and
2672
+ * no system prompt — the task's prompt is its first turn, as on the terminal
2673
+ * transport. `--permission-prompt-tool stdio` routes the questions the mode
2674
+ * leaves open to the control channel, where the adapter must answer them;
2675
+ * without it `-p` refuses them silently and nothing can ever surface one.
2676
+ */
2677
+ private taskSpawnSpec;
2678
+ /**
2679
+ * Claude Code's soft interrupt: the turn stops, the process and its session
2680
+ * stay. The CLI acknowledges on the control channel, then closes the turn
2681
+ * with an `error_during_execution` result, which {@link handleLine} reports
2682
+ * as an interrupted `turn_end`.
2683
+ */
2684
+ interrupt(timeoutMs: number): Promise<boolean>;
2685
+ /**
2686
+ * A `--resume` the CLI cannot find is answered at once with an error result
2687
+ * and no `init`, after which it waits on stdin for input it never reads — a
2688
+ * turn sent to it would hang. Closing stdin lets it exit. The id it echoes is
2689
+ * not recorded: no session was taken up, and a continue must not offer it again.
2690
+ */
2691
+ private refuseResume;
2692
+ /**
2693
+ * The answer to an open tool request, in the shape the CLI validates: an
2694
+ * allow echoes the call's own input (an absent one is warned about and
2695
+ * replaced), and "for this task" hands back Claude's own suggestions — never
2696
+ * a grant of Ordewell's making.
2697
+ */
2698
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
2699
+ dispose(): void;
2700
+ private clearFollowOnTimer;
2680
2701
  protected turnPayload(message: string): string;
2681
2702
  protected handleLine(line: string, emit: (event: AgentEvent) => void): void;
2703
+ /**
2704
+ * `--permission-mode auto` on a model or account without auto mode is not
2705
+ * refused: the CLI says nothing and starts in `default`, then asks about every
2706
+ * write — a request nobody is there to answer. The plan, not the CLI, decides
2707
+ * the mode (ADR-0001), so a different one fails the turn rather than running.
2708
+ */
2709
+ private refuseOtherMode;
2710
+ private awaitFollowOnTurn;
2682
2711
  private startSubagent;
2683
2712
  /**
2684
2713
  * A subagent's messages do not stream, so each line's usage is the snapshot
@@ -2711,8 +2740,10 @@ declare class ClaudeCodeAdapter extends StdioAgentAdapter {
2711
2740
  }
2712
2741
 
2713
2742
  /**
2714
- * Codex as a planner, over its `app-server` stdio JSON-RPC transport
2715
- * (ADR-0009).
2743
+ * Codex as a planner (ADR-0009) and as a task's runner (ADR-0018, #54), over
2744
+ * its `app-server` stdio JSON-RPC transport. The start switch decides the
2745
+ * thread's settings and who answers its requests; everything a turn reports
2746
+ * is read the same way for both.
2716
2747
  *
2717
2748
  * Ordewell already speaks a slice of this protocol — `ModelDiscovery` drives
2718
2749
  * `initialize` → `model/list` to build the Codex model catalog — so the
@@ -2724,7 +2755,7 @@ declare class ClaudeCodeAdapter extends StdioAgentAdapter {
2724
2755
  * version that renames them will surface as a visible dead turn rather than a
2725
2756
  * hang, because the base class watches the process as well as the protocol.
2726
2757
  */
2727
- declare class CodexAdapter extends StdioAgentAdapter {
2758
+ declare class CodexAdapter extends StdioAgentAdapter implements TaskModeAgentAdapter {
2728
2759
  readonly agentId = "codex";
2729
2760
  private threadId;
2730
2761
  /** The model Codex opened the thread with — usage records name it. */
@@ -2735,6 +2766,28 @@ declare class CodexAdapter extends StdioAgentAdapter {
2735
2766
  private startOpts;
2736
2767
  /** Whether this turn has already emitted prose — see the `agentMessage` case. */
2737
2768
  private turnHasText;
2769
+ /**
2770
+ * The turn in flight. Its id arrives with `turn/started` or the `turn/start`
2771
+ * response, whichever lands first; `turn/interrupt` cannot be sent without it.
2772
+ */
2773
+ private turn;
2774
+ /** The JSON-RPC id of the `turn/start` that opened {@link turn}. */
2775
+ private turnRequestId;
2776
+ /** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
2777
+ private endedTurnId;
2778
+ /** An interrupt was asked for during this turn, so however it ends, it was cut short. */
2779
+ private interruptRequested;
2780
+ /** An interrupt asked for before Codex named the turn, sent once it does. */
2781
+ private interruptOnStart;
2782
+ /** A task's approval requests still waiting for an answer, by the request id as a string. */
2783
+ private readonly openPermissions;
2784
+ /**
2785
+ * The files each in-flight `fileChange` item touches. Its approval request
2786
+ * names only the item, and a person deciding needs to see the files.
2787
+ */
2788
+ private readonly fileChangePaths;
2789
+ /** Requests this adapter sent and awaits an answer to, by JSON-RPC id. */
2790
+ private readonly pendingRequests;
2738
2791
  private resumeAttempted;
2739
2792
  private resumeFallbackSent;
2740
2793
  private sandbox;
@@ -2746,22 +2799,41 @@ declare class CodexAdapter extends StdioAgentAdapter {
2746
2799
  private readonly subagents;
2747
2800
  protected spawnSpec(opts: AgentStartOptions): SpawnSpec;
2748
2801
  /**
2749
- * `initialize`, then `thread/start`, both before the first user message. The
2750
- * thread is pinned to the read-only sandbox with approvals set to `never` —
2751
- * with nobody watching a planner's prompts, "ask" would mean "hang", which is
2752
- * ADR-0008's absent-is-denial invariant kept by construction.
2802
+ * `initialize`, then `thread/start`, both before the first user message. A
2803
+ * planner's thread is pinned to the read-only sandbox with approvals set to
2804
+ * `never` — with nobody watching a planner's prompts, "ask" would mean
2805
+ * "hang", which is ADR-0008's absent-is-denial invariant kept by
2806
+ * construction. A task's runs as its mode says (see {@link threadParams}).
2753
2807
  */
2754
2808
  protected handshake(opts: AgentStartOptions): Promise<void>;
2755
2809
  /**
2756
- * Open the thread this session plans in. A resume id means the previous
2810
+ * Open this session's thread. A resume id means the previous
2757
2811
  * process died mid-session: `thread/resume` puts the agent back in front of
2758
- * the context it already paid to read. A failed resume is not an error — the
2759
- * response handler falls back to a fresh thread, which is the same
2760
- * degradation `restoreChat` performs on every surface (T4).
2812
+ * the context it already paid to read. A planner's failed resume is not an
2813
+ * error — the response handler falls back to a fresh thread, which is the
2814
+ * same degradation `restoreChat` performs on every surface (T4). A task's
2815
+ * is: Continue (K1) offers the session that did the work, and a fresh thread
2816
+ * in its place would pretend it was resumed.
2761
2817
  */
2762
2818
  private startThread;
2819
+ private threadParams;
2820
+ private effort;
2821
+ /**
2822
+ * `turn/interrupt` names the turn as well as the thread, so an interrupt
2823
+ * asked for before Codex has named the turn waits for it. Codex acknowledges
2824
+ * with an empty result, then ends the turn as `interrupted`.
2825
+ */
2826
+ interrupt(timeoutMs: number): Promise<boolean>;
2827
+ /**
2828
+ * Answer an open approval in its own result schema. Codex's decline carries
2829
+ * no message, so a deny note reaches the agent as input steered into the
2830
+ * running turn.
2831
+ */
2832
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
2763
2833
  protected turnPayload(message: string): string;
2764
2834
  protected handleLine(line: string, emit: (event: AgentEvent) => void): void;
2835
+ private turnStarted;
2836
+ private closeTurn;
2765
2837
  /**
2766
2838
  * The child thread id when `threadId` names a subagent's thread, or undefined
2767
2839
  * for the planner's own. Codex replays both threads on one stream and only
@@ -2782,6 +2854,14 @@ declare class CodexAdapter extends StdioAgentAdapter {
2782
2854
  * not added a second time. Codex reports no price.
2783
2855
  */
2784
2856
  private emitUsage;
2857
+ /**
2858
+ * A task's server→client request. An approval stays open for someone to
2859
+ * answer (ADR-0018, A1); everything else is answered at once, because an
2860
+ * unanswered request stalls the turn forever.
2861
+ */
2862
+ private openTaskRequest;
2863
+ /** The request as a person sees it, led by the field that says what it is about. */
2864
+ private approvalInput;
2785
2865
  /**
2786
2866
  * Refuse one server→client request. Requests whose result schema can express
2787
2867
  * a refusal get that payload; everything else — a permission grant, a
@@ -2807,7 +2887,8 @@ declare class CodexAdapter extends StdioAgentAdapter {
2807
2887
  }
2808
2888
 
2809
2889
  /**
2810
- * OpenCode as a planner, over its headless HTTP server (ADR-0009).
2890
+ * OpenCode as a planner or a task runner, over its headless HTTP server
2891
+ * (ADR-0009, ADR-0018).
2811
2892
  *
2812
2893
  * The odd one out: `opencode serve` is a real server rather than a stdio
2813
2894
  * protocol, so this adapter owns both halves of the boundary — it spawns the
@@ -2815,30 +2896,80 @@ declare class CodexAdapter extends StdioAgentAdapter {
2815
2896
  * talks to it through the injected `fetch`. Both are part of the one seam the
2816
2897
  * tests drive.
2817
2898
  *
2818
- * The turn ends when the message POST resolves, and its response is the
2899
+ * A planner turn ends when the message POST resolves, and its response is the
2819
2900
  * authoritative copy of the reply's last message. Everything else — reply text
2820
2901
  * as it streams, earlier model calls' text and reasoning, per-call usage, the
2821
2902
  * subagents a `task` call runs — arrives only on the server's `/event` channel.
2822
2903
  * An event name that changes between OpenCode versions therefore costs that
2823
- * detail, never the final reply.
2904
+ * detail, never the final reply. A task turn is posted asynchronously and read
2905
+ * entirely from the stream — see {@link sendTask}.
2824
2906
  */
2825
- declare class OpenCodeAdapter implements AgentAdapter {
2907
+ declare class OpenCodeAdapter implements TaskModeAgentAdapter {
2826
2908
  private deps;
2827
2909
  readonly agentId = "opencode";
2828
2910
  private process;
2829
2911
  private baseUrl;
2912
+ /** Sent on every request to a task's server, which a per-task password secures. */
2913
+ private authorization;
2830
2914
  private sessionId;
2831
2915
  private stderrTail;
2832
2916
  private exited;
2917
+ private exitCode;
2833
2918
  private disposed;
2834
- private opts;
2919
+ private markEnded;
2920
+ private readonly processEnded;
2921
+ private planner;
2922
+ private task;
2835
2923
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
2836
2924
  private turnHasText;
2837
2925
  /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
2838
2926
  private lastAssistantId;
2927
+ private taskTurn;
2928
+ /** An abort was posted during the current task turn, so the idle that follows ends it as interrupted. */
2929
+ private interruptRequested;
2930
+ /** A task's requests waiting for an answer, by request id, with the session that asked. */
2931
+ private readonly openPermissions;
2839
2932
  constructor(deps: AgentProcessDeps);
2840
2933
  start(opts: AgentStartOptions): Promise<void>;
2841
2934
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2935
+ /**
2936
+ * One task turn. `prompt_async` only queues the work, so the turn is over
2937
+ * when the session goes idle — OpenCode's own `run` rules: an idle frame
2938
+ * counts once `/session/status` confirms it, because a late idle can belong
2939
+ * to the turn before, and {@link pollStatus} stands behind the stream,
2940
+ * which can miss a status frame. No request is held open for the turn, so
2941
+ * fetch's 300s header timeout, the planner's reason for
2942
+ * {@link recoverReply}, never applies.
2943
+ */
2944
+ private sendTask;
2945
+ /**
2946
+ * Whatever of the turn's own messages the stream missed. Each part is
2947
+ * emitted once, so a turn the stream saw whole adds nothing here; one that
2948
+ * lost its last text part still reaches the plain-text channel, where the
2949
+ * done marker is looked for.
2950
+ */
2951
+ private readBack;
2952
+ /**
2953
+ * The stream can drop a status frame while still delivering the rest, so a
2954
+ * task turn also reads the status itself. A busy session is this turn's own
2955
+ * work as surely as a frame is.
2956
+ */
2957
+ private pollStatus;
2958
+ /** An idle frame ends the turn only if the server agrees now. When it will not say, the frame is trusted, as upstream trusts it. */
2959
+ private confirmIdle;
2960
+ /** Whether the task's session is working, or null when the server will not say. A session absent from the map is idle. */
2961
+ private sessionBusy;
2962
+ /** What a frame of the task's own session says about the turn's end. */
2963
+ private followTaskTurn;
2964
+ /**
2965
+ * Abort the running turn, keeping the server and its session. OpenCode
2966
+ * acknowledges by going idle, which is also what ends the turn — as
2967
+ * interrupted, because the request is remembered.
2968
+ */
2969
+ interrupt(timeoutMs: number): Promise<boolean>;
2970
+ onProcessExit(listener: (code: number) => void): void;
2971
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
2972
+ private role;
2842
2973
  /**
2843
2974
  * Turn one settled assistant message into events. The settled response is
2844
2975
  * authoritative: it names the assistant message, so its parts are the ones
@@ -2907,6 +3038,27 @@ declare class OpenCodeAdapter implements AgentAdapter {
2907
3038
  * shows the planner reaching for something it may not have.
2908
3039
  */
2909
3040
  private denyPermission;
3041
+ /**
3042
+ * A task's request. Under a mode whose manifest sets `approvals: auto` —
3043
+ * `build`, which the terminal transport runs with `--auto` — it is answered
3044
+ * at once with what `run --auto` answers, so the same plan behaves the same
3045
+ * on both transports (ADR-0001), and announced already decided so the log
3046
+ * still shows it. Any other mode leaves it open for an approval card.
3047
+ */
3048
+ private askPermission;
3049
+ /**
3050
+ * `POST /permission/:id/reply` is the current answer. The per-session path
3051
+ * it replaced is tried only when a server too old to know the new one 404s.
3052
+ */
3053
+ private replyPermission;
3054
+ /**
3055
+ * Open `/event` for one turn and wait until it is connected. The stream
3056
+ * stopped being best-effort the moment permission answers moved onto it: a
3057
+ * request raised before we connect is one nobody answers, and the turn
3058
+ * hangs on it. Waiting is bounded so a server that never opens `/event`
3059
+ * still gets its turn.
3060
+ */
3061
+ private openStream;
2910
3062
  /**
2911
3063
  * Server-sent events from `/event`: the turn's live text, reasoning, tool
2912
3064
  * activity and usage, and the only channel permission requests arrive on —
@@ -2914,6 +3066,8 @@ declare class OpenCodeAdapter implements AgentAdapter {
2914
3066
  */
2915
3067
  private streamEvents;
2916
3068
  private json;
3069
+ private request;
3070
+ private headers;
2917
3071
  nativeSessionId(): string | null;
2918
3072
  dispose(): void;
2919
3073
  private exitMessage;
@@ -2937,6 +3091,52 @@ declare function mapAgentTool(name: string): MappedTool;
2937
3091
  */
2938
3092
  declare function normalizeAgentArgs(tool: ResearchToolType, args: Record<string, unknown>): Record<string, unknown>;
2939
3093
 
3094
+ /** The dialogue record a fork carries — what {@link PlannerConversation.clone} returns. */
3095
+ interface ForkedDialogue {
3096
+ conversationHistory: ConversationMessage[];
3097
+ researchLog: ResearchLogEntry[];
3098
+ }
3099
+
3100
+ /** Everything (re)opening a conversation needs besides the dialogue itself. */
3101
+ type ConversationOpening = Omit<ConversationRequest, 'goal' | 'onProgress' | 'signal' | 'priorHistory' | 'initialMessage'>;
3102
+ /**
3103
+ * What the conversation needs from the session that hosts it. The session keeps
3104
+ * plan state, persistence and scheduling; the conversation reaches them only
3105
+ * through here, so a fake host is enough to drive it.
3106
+ */
3107
+ interface PlannerConversationHost {
3108
+ /** The plan the transcript lives on (`conversationHistory`, `researchLog`). */
3109
+ plan(): LegacyPlanState | null;
3110
+ goal(): string;
3111
+ aiService(): IAiService;
3112
+ onProgress(progress: ResearchProgress): void;
3113
+ /** Fresh discovery, allowlist-filtered the way the system prompt shows it. */
3114
+ opening(runners: RunnerId[]): Promise<ConversationOpening>;
3115
+ /** What the per-turn catalog block and every read draw from, as of now. */
3116
+ catalog(): TaskQueryCatalog;
3117
+ tasks(): readonly Task[];
3118
+ /**
3119
+ * The orchestrator's live capture for a task's latest attempt, backing the
3120
+ * `output` field of a read. Injected the same way as the catalog so the
3121
+ * conversation never reaches into execution state directly.
3122
+ */
3123
+ liveOutput: LiveOutputLookup;
3124
+ hasLiveWork(): boolean;
3125
+ /** The session's single mutation ritual: op → persist → notify (default: the plan). */
3126
+ mutate(op: () => boolean, notify?: () => void): LegacyPlanState | null;
3127
+ broadcast: SessionBroadcaster;
3128
+ /** `turnId`: the planner turn whose commit this is, when one is. */
3129
+ broadcastPlan(turnId?: string): void;
3130
+ /** Validate a batch against live state. Pure — nothing is applied. */
3131
+ validateOps(ops: TaskOp[]): ApplyTaskOpsResult;
3132
+ /** Load planner-produced tasks: an edit keeps run state, a commit starts over. Returns how many landed. */
3133
+ adoptTasks(tasks: readonly Task[], how: 'edit' | 'commit'): number;
3134
+ capturePrd(text: string): void;
3135
+ /** Park a structural edit until the next batch boundary. Returns the queue length. */
3136
+ queueEdit(userMessage: string): number;
3137
+ /** Wake a scheduler an applied edit may have unblocked. */
3138
+ afterEdit(): Promise<void>;
3139
+ }
2940
3140
  /**
2941
3141
  * A conversation edit (rewind, fork) the conversation refused because the
2942
3142
  * request itself is wrong — no such message, nothing to fork. Transport-agnostic
@@ -2963,12 +3163,232 @@ interface RewindTarget {
2963
3163
  content: string;
2964
3164
  timestamp: string;
2965
3165
  }
3166
+ /** What a rewind forks from, and the message it lands just before. */
3167
+ interface RewoundDialogue {
3168
+ dialogue: ForkedDialogue;
3169
+ rewoundMessage: string;
3170
+ }
2966
3171
  /** What a compaction left behind. */
2967
3172
  interface ConversationCompaction {
2968
3173
  summary: string;
2969
3174
  /** Transcript entries kept verbatim after the summary entry. */
2970
3175
  keptMessages: number;
2971
3176
  }
3177
+ /** A point a failed turn returns the dialogue to. Opaque outside this module. */
3178
+ interface TranscriptSnapshot {
3179
+ readonly plan: LegacyPlanState;
3180
+ readonly history: ConversationMessage[] | undefined;
3181
+ readonly researchLog: ResearchLogEntry[] | undefined;
3182
+ readonly persisted: number;
3183
+ readonly savedInBackground: number;
3184
+ }
3185
+ interface ReplyOptions {
3186
+ signal?: AbortSignal;
3187
+ /** The user's words before skill expansion — what a mid-run queue replays later. */
3188
+ verbatim?: string;
3189
+ }
3190
+ /**
3191
+ * The planner conversation (ADR-0002), end to end: the persisted dialogue
3192
+ * record, the live model context behind it, and every turn from the user's
3193
+ * message to a settled, persisted outcome.
3194
+ *
3195
+ * It is the only writer of `conversationHistory` and of the planner's
3196
+ * `researchLog`. The writes land on the host's plan object — which is what the
3197
+ * session persists and broadcasts — but only ever inside the host's mutation
3198
+ * ritual or ahead of a turn that will either reach it or be rolled back.
3199
+ *
3200
+ * The live model context (a vendor service's message list, a harness planner's
3201
+ * process and native session id) is disposable: {@link reset} drops it and the
3202
+ * next turn is replayed from the transcript. That is what lets the transcript
3203
+ * be replaced by a summary, or copied into a fork, without the model's memory
3204
+ * drifting from it.
3205
+ */
3206
+ declare class PlannerConversation {
3207
+ private readonly host;
3208
+ /** Bumped on every persist, so a rollback can tell whether its writes already reached disk. */
3209
+ private persisted;
3210
+ /** Saves an execution event made while a turn may be in flight; see {@link restore}. */
3211
+ private savedInBackground;
3212
+ private turnsInFlight;
3213
+ private compacting;
3214
+ private openTurnId;
3215
+ constructor(host: PlannerConversationHost);
3216
+ get transcript(): readonly ConversationMessage[];
3217
+ /** Whether a user turn is between its transcript append and its settled outcome. */
3218
+ get isTurnInFlight(): boolean;
3219
+ /** The user turn being answered, for what the host raises during it — an approval the turn's research asks for. */
3220
+ get currentTurnId(): string | undefined;
3221
+ /** Whether the model still holds this conversation in memory. */
3222
+ get isActive(): boolean;
3223
+ append(role: ConversationMessage['role'], content: string, opts?: {
3224
+ timestamp?: string;
3225
+ kind?: ConversationMessage['kind'];
3226
+ }): void;
3227
+ /** Swap the whole transcript. The live context no longer matches it, so pair with {@link reset}. */
3228
+ replace(messages: readonly ConversationMessage[]): void;
3229
+ snapshot(): TranscriptSnapshot | null;
3230
+ /**
3231
+ * Put the dialogue back where {@link snapshot} found it — unless anything
3232
+ * was persisted since, because then memory already matches disk and every
3233
+ * surface, and undoing it would erase work the user has seen land. Also a
3234
+ * no-op once a different plan has been adopted.
3235
+ *
3236
+ * A background save (a task settling mid-turn) wrote the turn's writes to
3237
+ * disk without landing anything the user saw, so it does not stop the undo;
3238
+ * the undo is saved in turn, or a reload would bring the writes back.
3239
+ */
3240
+ restore(snapshot: TranscriptSnapshot): boolean;
3241
+ /**
3242
+ * The host calls this after every persist. `background` is a save no call
3243
+ * of the user's made — an execution event landing mid-turn.
3244
+ */
3245
+ markPersisted(opts?: {
3246
+ background?: boolean;
3247
+ }): void;
3248
+ /**
3249
+ * Drop the live model context. Idempotent; the harness backend holds an OS
3250
+ * process here, so callers never gate it on {@link isActive}.
3251
+ */
3252
+ reset(): void;
3253
+ /**
3254
+ * The user messages a rewind may land before. The opening message is the
3255
+ * goal: cutting it leaves a conversation about nothing, which is a new
3256
+ * session, not a rewind. After a compaction the summary entry, always
3257
+ * first, plays the goal's part: what it replaced is gone, so a rewind stops
3258
+ * at it.
3259
+ */
3260
+ rewindTargets(): RewindTarget[];
3261
+ /**
3262
+ * Replace the transcript with a summary of it, keeping the last two
3263
+ * exchanges as they were, and drop the live context so the next message
3264
+ * replays from the shorter record.
3265
+ *
3266
+ * The summary is one hidden turn through whichever planner is configured —
3267
+ * on the live context when it still matches, replayed from the transcript
3268
+ * when not — so the compaction is the same for a vendor API and a harness
3269
+ * agent. Whatever the turn emits besides the summary is discarded, ops
3270
+ * included: it condenses the conversation, never the plan. Nothing is
3271
+ * written until the summary is in hand, so a failed or stopped turn leaves
3272
+ * the transcript exactly as it was.
3273
+ */
3274
+ compact(signal?: AbortSignal): Promise<ConversationCompaction>;
3275
+ private summarize;
3276
+ /**
3277
+ * A copy of the dialogue record for a forked session to carry. Refused
3278
+ * mid-turn: the copy would hold the user's message without the reply to it.
3279
+ * The live context is not part of it — the fork replays from this record on
3280
+ * its first turn, like any adopted session.
3281
+ */
3282
+ clone(): ForkedDialogue;
3283
+ /**
3284
+ * A copy of the dialogue as it stood just before the user message at
3285
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
3286
+ * from — plus that message's full text, for a surface to offer back. This
3287
+ * conversation is not touched. The research trace is cut at the same point
3288
+ * by time: its entries carry no link to the message that caused them.
3289
+ */
3290
+ cloneBefore(index: number): RewoundDialogue;
3291
+ /**
3292
+ * Queued mid-run edits applied between batches. The user's message is
3293
+ * already in the transcript from when it was queued; this records that it
3294
+ * finally took effect, so a replay does not read the plan as never changed.
3295
+ * Call inside the host's mutation ritual.
3296
+ */
3297
+ recordQueuedEdits(messages: string[], taskCount: number): void;
3298
+ /**
3299
+ * Queued edits the between-batches drain could not apply. Recorded so the
3300
+ * transcript does not go on promising a change that never landed. Call
3301
+ * inside the host's mutation ritual.
3302
+ */
3303
+ recordQueuedEditsFailed(messages: string[], reason: string): void;
3304
+ /** Open the conversation on a fresh plan: the goal is its first message. */
3305
+ start(goal: string, opening: ConversationOpening, signal?: AbortSignal): Promise<LegacyPlanState>;
3306
+ /**
3307
+ * Every later user message, from the transcript append to a persisted
3308
+ * outcome. A turn that throws before anything was persisted takes its own
3309
+ * writes back out, so session memory never drifts from disk and the UI.
3310
+ */
3311
+ reply(message: string, options?: ReplyOptions): Promise<LegacyPlanState>;
3312
+ private replyTurn;
3313
+ /**
3314
+ * Whether a settled structural edit reaches work a runner is executing. Only
3315
+ * these are queued: a whole-plan commit replaces the plan and would reset the
3316
+ * run, and a task-ops batch that names an in-progress task changes it under
3317
+ * the runner. An add, or an edit to any other task, is reconciled into the
3318
+ * plan in place while the running batch keeps going.
3319
+ */
3320
+ private editTouchesLiveWork;
3321
+ private assertIdle;
3322
+ private inTurn;
3323
+ /**
3324
+ * Bracket one user turn with its start and end, under an id minted here:
3325
+ * the turn is where the stream a surface draws begins and ends, and only the
3326
+ * conversation sees all of it — every backend call, read and retry.
3327
+ */
3328
+ private userTurn;
3329
+ private requirePlan;
3330
+ private recordUser;
3331
+ private recordResearch;
3332
+ /**
3333
+ * Reopen the model context from the transcript — because the in-memory one
3334
+ * is gone (session reload, extension restart), or because it is stale
3335
+ * against the planner config now in effect. No LLM call happens for the
3336
+ * replayed turns; the first call is the one the user's message opens.
3337
+ */
3338
+ private resume;
3339
+ /**
3340
+ * Answer every read the planner emits until it says something else.
3341
+ *
3342
+ * The channel is a text envelope rather than a registered tool because the
3343
+ * protocol has to be identical on both planner backends (ADR-0009): Ordewell
3344
+ * owns a tool loop only in the API case, and a harness planner running as a
3345
+ * coding-agent subprocess can only be reached this way.
3346
+ *
3347
+ * Draining here — outside {@link repairLoop} — is what keeps reads free of
3348
+ * the repair budget. A planner that looks a task up and *then* fumbles its
3349
+ * ops JSON still gets its two corrective retries; charging it for the read
3350
+ * would cost it the chance to fix the edit.
3351
+ */
3352
+ private drainTaskQueries;
3353
+ /**
3354
+ * Render one read out of live state. Never persisted to the transcript: the
3355
+ * detail is context for the planner's next reply, and re-sending it on every
3356
+ * later turn is exactly the cost this channel exists to avoid.
3357
+ */
3358
+ private taskQueryAnswer;
3359
+ /**
3360
+ * Drive a planner turn to a persisted, broadcast outcome. Task edits apply
3361
+ * atomically; validation failures are fed back to the model for up to 2
3362
+ * silent retries, then surfaced as a message with the plan untouched. The
3363
+ * first turn and every later turn route through here — one path, not two.
3364
+ */
3365
+ private settle;
3366
+ /** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
3367
+ private applyTaskOps;
3368
+ /** Commit a settled (non-task_ops) turn through the host's mutation ritual. */
3369
+ private commit;
3370
+ /**
3371
+ * The catalog the planner may actually draw from — model ids and task-mode
3372
+ * ids, per runner in the plan — emitted on EVERY turn, before any plan exists
3373
+ * or after. The system prompt shows this once at conversation start; a long
3374
+ * clarifying conversation outlives that single showing and the planner
3375
+ * starts misquoting it, so this re-states it per turn instead.
3376
+ *
3377
+ * The host's catalog is allowlist-filtered, so a restricted allowlist stays a
3378
+ * hard bound on every turn, and reads the plan's runners live, so a runner
3379
+ * admitted mid-session by a retarget is shown like every other.
3380
+ */
3381
+ private catalogBlock;
3382
+ /**
3383
+ * The "you are here" block for post-plan chat: current tasks with stable
3384
+ * references, plus the read and edit protocols. Injected per turn (never
3385
+ * persisted) so the model always sees live statuses — including which tasks
3386
+ * are locked by a running execution.
3387
+ */
3388
+ private planContextBlock;
3389
+ /** One line per task with stable references — null until the plan has tasks. */
3390
+ private currentPlanLines;
3391
+ }
2972
3392
 
2973
3393
  declare const BUILTIN_SKILL_NAMES: readonly ["grilling", "to-spec", "improve-codebase-architecture"];
2974
3394
  interface SkillMetadata {
@@ -3003,6 +3423,126 @@ declare class SkillsService {
3003
3423
  }
3004
3424
  declare function createSkillsService(workspaceRoot?: string): SkillsService;
3005
3425
 
3426
+ /** The `planner_usage` member of the session broadcast union. */
3427
+ type PlannerUsageMessage = Extract<SessionMessage, {
3428
+ type: 'planner_usage';
3429
+ }>;
3430
+ /**
3431
+ * The planner's session usage ledger (#49), owned by Session. Consumes the
3432
+ * `usage` progress events a backend emits per model call, keeps the running
3433
+ * session and per-subagent totals, and renders the `planner_usage` message.
3434
+ * Session persists {@link snapshot} with the plan and restores it on load, so a
3435
+ * reopened session shows its totals again. The accumulation itself lives in
3436
+ * `models/Usage` — the one place the sum's currency-per-currency rule is
3437
+ * defined — this module just holds the state and the broadcast shape.
3438
+ */
3439
+ declare class PlannerUsageLedger {
3440
+ private usage;
3441
+ /** Fold one model call's record into the totals; returns the running ledger. */
3442
+ record(record: UsageRecord): PlannerUsage;
3443
+ /** Adopt a persisted ledger (a reopened session) or start from zero. */
3444
+ restore(usage: PlannerUsage | undefined): void;
3445
+ /** Start a fresh session's ledger from zero. */
3446
+ clear(): void;
3447
+ /** Whether anything has been recorded — a plan with no usage says nothing. */
3448
+ get hasUsage(): boolean;
3449
+ /** The value persisted onto the plan state. */
3450
+ snapshot(): PlannerUsage;
3451
+ /** The broadcast message for the totals as they stand now. */
3452
+ message(turnId?: string): PlannerUsageMessage;
3453
+ }
3454
+
3455
+ interface SessionEventRelayDeps {
3456
+ broadcast: SessionBroadcaster;
3457
+ /** Where isolation notices go, for a host whose notifications are not seen by the user. */
3458
+ onNotice?: (notice: SessionNotice) => void;
3459
+ store: Pick<PlanStore, 'allTasks' | 'snapshot'>;
3460
+ orchestrator: Pick<TaskOrchestrator, 'getIdleSince' | 'getTaskIsolation' | 'getQueuedTaskMessages'>;
3461
+ /** Shared with the Session, which snapshots, restores and clears it. */
3462
+ usage: PlannerUsageLedger;
3463
+ /** How many of a task's runner requests wait for an answer (ADR-0018, A1). */
3464
+ awaitingApproval?: (taskId: string) => number;
3465
+ }
3466
+ /** Every orchestrator event the relay announces; `onTaskSettled` is persistence only, so it is the Session's. */
3467
+ type RelayObserver = Required<Omit<OrchestratorObserver, 'onTaskSettled'>>;
3468
+ /**
3469
+ * Turns orchestrator and planner events into {@link SessionMessage}
3470
+ * broadcasts — the one place a surface's view of a Session is produced. It
3471
+ * never persists: where an event also owes a save, the Session does that
3472
+ * before handing the event on, so no surface sees state the disk lacks.
3473
+ */
3474
+ declare class SessionEventRelay {
3475
+ private readonly broadcast;
3476
+ private readonly onNotice?;
3477
+ private readonly store;
3478
+ private readonly orchestrator;
3479
+ private readonly usage;
3480
+ private readonly awaitingApproval;
3481
+ /**
3482
+ * The in-flight turn's subagent activity, grouped one run per subagent so a
3483
+ * replay nests each step under its own brief/result. Folded into the plan's
3484
+ * researchLog at persist — see {@link flushSubagentRuns}.
3485
+ */
3486
+ private pendingSubagents;
3487
+ private statusHeld;
3488
+ private statusOwed;
3489
+ constructor(deps: SessionEventRelayDeps);
3490
+ /** `plan` is read per event: a Session with no plan (or mid-reset) announces nothing about tasks. */
3491
+ observer(plan: () => LegacyPlanState | null): RelayObserver;
3492
+ status(plan: LegacyPlanState | null): void;
3493
+ /**
3494
+ * Run `op` with status broadcasts held back. Store ops signal the observer
3495
+ * as they go, which would put a status_update on the wire before the
3496
+ * persist the caller owes — surfaces must never see plan state the disk
3497
+ * does not have yet. {@link releaseStatus} sends the one held back.
3498
+ */
3499
+ holdStatus<T>(op: () => T): T;
3500
+ releaseStatus(plan: LegacyPlanState | null): void;
3501
+ executionComplete(plan: LegacyPlanState | null): void;
3502
+ /** `turnId`: the planner turn whose commit this is, when one is. */
3503
+ planGenerated(plan: LegacyPlanState | null, goal: string, turnId?: string): void;
3504
+ progress(progress: ResearchProgress): void;
3505
+ /** The run for `subagentId`, created on first sighting so a step arriving
3506
+ * before (or without) its started event still gets a home. */
3507
+ private subagentRun;
3508
+ /**
3509
+ * Fold the turn's subagent runs into the plan's researchLog as one contiguous
3510
+ * group per subagent — its entry then its steps, in the order they started —
3511
+ * so a replay nests each step under its own subagent however the live stream
3512
+ * interleaved. A harness planner already logs its child steps through the
3513
+ * turn's researchLog; they are pulled out of that position and re-grouped
3514
+ * rather than duplicated.
3515
+ */
3516
+ flushSubagentRuns(plan: LegacyPlanState): void;
3517
+ /** Forget a turn's subagent runs that will never be persisted. */
3518
+ dropSubagentRuns(): void;
3519
+ }
3520
+
3521
+ /** Which session's logs, in which workspace. */
3522
+ interface TaskLogLocation {
3523
+ baseDir: string;
3524
+ sessionId: string;
3525
+ }
3526
+ /** One attempt's file, open for appending. */
3527
+ interface TaskLogFile {
3528
+ readonly attempt: number;
3529
+ append(events: readonly TaskLogEvent[]): void;
3530
+ }
3531
+ /** The attempts a task has a log for, oldest first. */
3532
+ declare function listTaskLogAttempts(location: TaskLogLocation, taskId: string): number[];
3533
+ /**
3534
+ * Start the next attempt's file. Numbered from what is on disk rather than
3535
+ * from the orchestrator's count, which restarts at one whenever a plan is
3536
+ * loaded and would overwrite the attempts before it.
3537
+ */
3538
+ declare function openTaskLog(location: TaskLogLocation, taskId: string): TaskLogFile;
3539
+ /**
3540
+ * One attempt's events, in order; empty when it has no log. A line that does
3541
+ * not parse is skipped — the last one, most likely, cut off by a crash
3542
+ * mid-write — so what was saved before it still reads.
3543
+ */
3544
+ declare function readTaskLog(location: TaskLogLocation, taskId: string, attempt: number): TaskLogEvent[];
3545
+
3006
3546
  /**
3007
3547
  * A direct (non-planner) plan edit the session refused. Distinct from a plain
3008
3548
  * Error so a surface can tell "you asked for something invalid" from "something
@@ -3022,12 +3562,14 @@ interface GeneratePlanOptions {
3022
3562
  signal?: AbortSignal;
3023
3563
  }
3024
3564
  /** The slice of Planner the Session drives — the injection seam for tests. */
3025
- type SessionPlanner = Pick<Planner, 'generate' | 'modify' | 'modifyDuringExecution'>;
3565
+ type SessionPlanner = Pick<Planner, 'generate' | 'modifyDuringExecution'>;
3026
3566
  /** Runtime prefs read live — may toggle between operations. */
3027
3567
  interface SessionRuntimeSettings {
3028
3568
  tddEnabled: boolean;
3029
3569
  verificationEnabled?: boolean;
3030
3570
  modelAllowlist?: Record<string, string[]>;
3571
+ /** Read once as a run starts, never per spawn (ADR-0018, S1). Absent means terminal. */
3572
+ runnerTransport?: RunnerTransport;
3031
3573
  }
3032
3574
  /**
3033
3575
  * The whole of what a host reads off disk for a Session. Both hosts used to
@@ -3053,13 +3595,6 @@ declare function sessionRuntimeSettings(settings: UserSettings): SessionRuntimeS
3053
3595
  * occurrence — later repeats stay literal.
3054
3596
  */
3055
3597
  declare function resolveSkillInvocation(text: string, skillsService: Pick<SkillsService, 'findSkill' | 'listSkills'>): string;
3056
- /**
3057
- * Everything a delivery surface constructs to host a session. Structural config
3058
- * (enabledRunners, orchestratorModel, providerModelLists) is snapshotted inside
3059
- * `config` at construction and never re-read from the environment. Runtime
3060
- * settings (tdd, verification) are read live via the `settings` callback so a toggle
3061
- * between generate and execute takes effect.
3062
- */
3063
3598
  /** Where a fork landed: the new session's id, and what adopting it needs. */
3064
3599
  interface ConversationFork {
3065
3600
  sessionId: string;
@@ -3070,6 +3605,15 @@ interface ConversationFork {
3070
3605
  interface ConversationRewind extends ConversationFork {
3071
3606
  rewoundMessage: string;
3072
3607
  }
3608
+ /** Writes one plan to the saved-session store: {@link saveSession}'s shape, injected so tests never touch disk. */
3609
+ type SaveSession = (plan: LegacyPlanState, goal: string, workspace: string, sessionId: string) => void;
3610
+ /**
3611
+ * Everything a delivery surface constructs to host a session. Structural config
3612
+ * (enabledRunners, orchestratorModel, providerModelLists) is snapshotted inside
3613
+ * `config` at construction and never re-read from the environment. Runtime
3614
+ * settings (tdd, verification) are read live via the `settings` callback so a toggle
3615
+ * between generate and execute takes effect.
3616
+ */
3073
3617
  interface SessionDeps {
3074
3618
  config: IConfig;
3075
3619
  notifications: INotification;
@@ -3117,49 +3661,75 @@ interface SessionDeps {
3117
3661
  * `config.worktreeIsolation`; tests inject `FakeWorktreeIsolation`.
3118
3662
  */
3119
3663
  isolation?: IWorktreeIsolation;
3664
+ /** Persistence seam. Defaults to the saved-session store under the workspace. */
3665
+ saveSession?: SaveSession;
3666
+ /** Where a structured task's log is saved (ADR-0018, P1). Defaults to a file per attempt beside the session's. */
3667
+ openTaskLog?: (location: TaskLogLocation, taskId: string) => TaskLogFile;
3120
3668
  }
3121
3669
  /**
3122
- * The per-session execution stack — deepened from a wiring bag into the
3123
- * lifecycle owner. Owns plan generation, execution, mutation, persistence, and
3124
- * the orchestrator observer wiring. The orchestrator's observer is subscribed
3125
- * once for the session's lifetime (not per-operation), which kills the
3126
- * double-subscribe class of bug. Persistence is an internal seam: every plan
3127
- * mutation routes through `persist()`, so the obligation has a home instead of
3128
- * being scattered across 11 call sites.
3670
+ * The composition root: builds every collaborator a Session drives and wires
3671
+ * them to each other, so the Session only receives them. Hosts create a
3672
+ * Session here; the optional {@link SessionDeps} are the seams tests fill.
3673
+ */
3674
+ declare function createSession(deps: SessionDeps): Session;
3675
+ /** What {@link createSession} hands a Session: every collaborator, already built and wired. */
3676
+ interface SessionParts {
3677
+ config: IConfig;
3678
+ registry: RunnerRegistry;
3679
+ workspace: string;
3680
+ fsAdapter: IFileSystem;
3681
+ broadcast: SessionBroadcaster;
3682
+ onNotice?: (notice: SessionNotice) => void;
3683
+ modelResolver: ModelResolver;
3684
+ settings: () => SessionRuntimeSettings;
3685
+ hostSessionId?: string;
3686
+ aiService: () => IAiService;
3687
+ planner: SessionPlanner;
3688
+ store: PlanStore;
3689
+ orchestrator: TaskOrchestrator;
3690
+ events: SessionEventRelay;
3691
+ usage: PlannerUsageLedger;
3692
+ approvals: PendingApprovals;
3693
+ approvalPolicy: ApprovalPolicy;
3694
+ fetcher: IWebFetcher;
3695
+ skillsService: Pick<SkillsService, 'findSkill' | 'listSkills'>;
3696
+ saveSession: SaveSession;
3697
+ /** The conversation's host is the Session itself, so only the Session can make it. */
3698
+ conversation: (host: PlannerConversationHost) => PlannerConversation;
3699
+ }
3700
+ /**
3701
+ * The per-session execution stack — the lifecycle owner. Owns plan
3702
+ * generation, execution, mutation and persistence; built by
3703
+ * {@link createSession}, which wires its collaborators. The orchestrator's
3704
+ * observer is subscribed once for the session's lifetime (not per-operation),
3705
+ * which kills the double-subscribe class of bug. Persistence is an internal
3706
+ * seam: every plan mutation routes through `persist()`.
3129
3707
  *
3130
- * The broadcast seam carries {@link SessionMessage} — the 15 plan-lifecycle
3131
- * events. Catalog/config messages (setModels, setRunnerList, …) stay on the
3132
- * host; Session never emits them.
3708
+ * What surfaces see is produced by the {@link SessionEventRelay}: the
3709
+ * {@link SessionMessage} plan-lifecycle events. Catalog/config messages
3710
+ * (setModels, setRunnerList, …) stay on the host; Session never emits them.
3133
3711
  */
3134
3712
  declare class Session {
3135
- /** Injected by a test; when present it is the service, forever. */
3136
- private readonly pinnedAiService?;
3137
- private liveAiService;
3138
- private readonly usageLedger;
3139
- /**
3140
- * The in-flight turn's subagent activity, grouped one run per subagent so a
3141
- * replay nests each step under its own brief/result. Flushed into the plan's
3142
- * researchLog at persist — see {@link flushSubagentRuns}.
3143
- */
3144
- private pendingSubagents;
3145
- private liveAiProvider;
3146
- private readonly workspaceRootFn;
3147
- private planner;
3148
- private orchestrator;
3149
- private store;
3150
- private config;
3151
- private registry;
3713
+ private readonly aiService;
3714
+ private readonly usage;
3715
+ private readonly events;
3716
+ private readonly planner;
3717
+ private readonly orchestrator;
3718
+ private readonly store;
3719
+ private readonly config;
3720
+ private readonly registry;
3152
3721
  private plan;
3153
3722
  private goal;
3154
3723
  private workspace;
3155
- private broadcast;
3156
- private onNotice?;
3157
- private modelResolver;
3158
- private fsAdapter;
3159
- private approvals;
3160
- private approvalPolicy;
3161
- private fetcher;
3162
- private settingsFn;
3724
+ private readonly broadcast;
3725
+ private readonly onNotice?;
3726
+ private readonly modelResolver;
3727
+ private readonly fsAdapter;
3728
+ private readonly approvals;
3729
+ private readonly approvalPolicy;
3730
+ private readonly fetcher;
3731
+ private readonly settingsFn;
3732
+ private readonly save;
3163
3733
  /** Last discovered model catalog — lets sync plan commits clamp thinking efforts to real variants. */
3164
3734
  private modelsCache;
3165
3735
  private unsubObserver;
@@ -3167,55 +3737,47 @@ declare class Session {
3167
3737
  private currentSessionId;
3168
3738
  private readonly skillsService;
3169
3739
  private readonly conversation;
3170
- constructor(deps: SessionDeps);
3171
- /**
3172
- * The planner transport for the provider configured *right now* (ADR-0009).
3173
- *
3174
- * Resolved on every read rather than once in the constructor, because a
3175
- * Session outlives the choice: VS Code hosts exactly one for the whole
3176
- * window, and the webview pills and `/planner` switch backends underneath it.
3177
- * The model id was already read live, so a service captured at construction
3178
- * meant a switched planner kept the old backend and got handed the new one's
3179
- * model — an OpenCode model id spawned as `claude --model opencode/…`, which
3180
- * the agent rejects as nonexistent.
3181
- *
3182
- * Switching releases the outgoing service: a harness planner holds an OS
3183
- * process, so dropping the reference without `reset()` leaks an agent.
3184
- */
3185
- private get aiService();
3740
+ constructor(parts: SessionParts);
3186
3741
  /**
3187
3742
  * Answer an outstanding approval. Every surface funnels here — the web
3188
3743
  * server's HTTP route, the VS Code webview, the TUI prompt — so the decision
3189
3744
  * path is identical regardless of who is looking.
3190
3745
  */
3191
- resolveApproval(id: string, granted: boolean): boolean;
3746
+ resolveApproval(id: string, answer: ApprovalAnswer): boolean;
3192
3747
  /** Requests still waiting for an answer, replayed to a surface that connects mid-prompt. */
3193
3748
  outstandingApprovals(): PendingApproval[];
3194
3749
  /** Scopes the user granted this session — surfaced so a UI can show what is already allowed. */
3195
3750
  approvedScopes(): string[];
3196
3751
  /** The stable id this session persists under — matches the host's id when one was provided. */
3197
3752
  get sessionId(): string;
3198
- get executionLog(): TaskSnapshot[];
3753
+ /** Where this session's structured task logs are saved (ADR-0018, P1). */
3754
+ get taskLogLocation(): TaskLogLocation;
3755
+ /** The attempts of a task that have a saved log, oldest first. */
3756
+ taskLogAttempts(taskId: string): number[];
3757
+ /** One attempt's saved log, for `replayTaskLog` — what a reopened task view shows. */
3758
+ taskLog(taskId: string, attempt: number): TaskLogEvent[];
3759
+ get executionLog(): ReadonlyArray<TaskSnapshot>;
3199
3760
  /** Tasks always read from PlanStore — the single source of truth. */
3200
- get planTasks(): Task[];
3201
- private attachObserver;
3202
- private buildObserver;
3203
- private broadcastStatus;
3204
- private translateProgress;
3205
- /** The run for `subagentId`, created on first sighting so a step arriving
3206
- * before (or without) its started event still gets a home. */
3207
- private subagentRun;
3761
+ get planTasks(): ReadonlyArray<Readonly<Task>>;
3208
3762
  /**
3209
- * Fold the turn's subagent runs into the plan's researchLog as one contiguous
3210
- * group per subagent — its entry then its steps, in the order they started —
3211
- * so a replay nests each step under its own subagent however the live stream
3212
- * interleaved. A harness planner already logs its child steps through the
3213
- * turn's researchLog; they are pulled out of that position and re-grouped
3214
- * rather than duplicated.
3763
+ * The relay announces every orchestrator event; this adds the saves some of
3764
+ * them owe, each made before the announcement so no surface sees state the
3765
+ * disk lacks.
3766
+ */
3767
+ private observer;
3768
+ /**
3769
+ * Refresh the plan's task list from the store. LegacyPlanState.tasks is only
3770
+ * ever written from the store (here and by the relay before it announces),
3771
+ * and always as a detached copy: sharing the store's live
3772
+ * tree let a host that edits `plan.tasks` rewrite task state behind the
3773
+ * store's back.
3774
+ */
3775
+ private syncPlanTasks;
3776
+ /**
3777
+ * Persists PlanStore state to disk. PlanStore is the single authority.
3778
+ * `background`: an execution event's save, which may land in the middle of a
3779
+ * planner turn without settling it (see {@link PlannerConversation.restore}).
3215
3780
  */
3216
- private flushSubagentRuns;
3217
- /** Persists PlanStore state to disk. PlanStore is the single authority;
3218
- * LegacyPlanState.tasks is populated only here, at persist time. */
3219
3781
  private persist;
3220
3782
  /** A new plan on a long-lived Session gets its own persisted identity (unless the host fixed one). */
3221
3783
  private remintSessionId;
@@ -3269,7 +3831,6 @@ declare class Session {
3269
3831
  */
3270
3832
  get currentPlanState(): PlanState | null;
3271
3833
  get currentGoal(): string;
3272
- get isPlanning(): boolean;
3273
3834
  /**
3274
3835
  * A task is running right now. Deliberately live work, not the scheduler's
3275
3836
  * armed flag: a run paused on a user task, a hold or a cancellation has
@@ -3280,7 +3841,6 @@ declare class Session {
3280
3841
  /** See {@link TaskOrchestrator.hasLiveWork} — a spawned runner, not merely an armed scheduler. */
3281
3842
  get hasLiveWork(): boolean;
3282
3843
  get status(): 'approved' | 'running' | 'completed';
3283
- get sessionConfig(): IConfig;
3284
3844
  /**
3285
3845
  * Deny every parked approval as soon as a planning turn is aborted, for the
3286
3846
  * same reason `beginFreshPlan` does it: a prompt raised by a turn nobody is
@@ -3295,7 +3855,6 @@ declare class Session {
3295
3855
  * *next* turn's prompts the moment that stale signal aborted.
3296
3856
  */
3297
3857
  private denyApprovalsOnAbort;
3298
- startExecution(): Promise<void>;
3299
3858
  generatePlan(goal: string, runners: RunnerId[], options?: GeneratePlanOptions): Promise<LegacyPlanState>;
3300
3859
  /**
3301
3860
  * Kick off the planner conversation (ADR-0002): research + the first planner
@@ -3379,11 +3938,15 @@ declare class Session {
3379
3938
  /** Start whatever the scheduler can now fit — after the parallel limit was raised mid-run, say. */
3380
3939
  reschedule(): Promise<void>;
3381
3940
  retryTask(taskId: string): Promise<void>;
3941
+ /**
3942
+ * Continue a finished structured task in its saved runner session with the
3943
+ * user's message (ADR-0018, K1); throws `TaskControlError` for a task that
3944
+ * cannot be continued.
3945
+ */
3946
+ continueTask(taskId: string, message: string): Promise<void>;
3382
3947
  cancelTask(taskId: string): Promise<void>;
3383
- markAiTaskComplete(taskId: string): Promise<void>;
3384
3948
  markTaskComplete(taskId: string): Promise<void>;
3385
3949
  markTaskIncomplete(taskId: string): Promise<void>;
3386
- tick(): Promise<void>;
3387
3950
  processQueuedMessages(): Promise<void>;
3388
3951
  /**
3389
3952
  * Every finished task as the log records it: this run's own entries, plus
@@ -3393,15 +3956,18 @@ declare class Session {
3393
3956
  private finishedWork;
3394
3957
  approveCheckpoint(taskId: string): void;
3395
3958
  rejectCheckpoint(taskId: string, reason?: string): void;
3396
- queueMessage(text: string): void;
3959
+ /**
3960
+ * A user message to a structured task (ADR-0018, M1); throws
3961
+ * `TaskControlError` for one that cannot take it. A task waiting for input
3962
+ * is back in progress, and saved so before any surface is told.
3963
+ */
3964
+ sendTaskMessage(taskId: string, text: string): string;
3965
+ removeQueuedTaskMessage(taskId: string, id: string): boolean;
3966
+ interruptTask(taskId: string): Promise<void>;
3397
3967
  getQueuedMessages(): QueuedMessage[];
3398
3968
  /** Take back one unsent message; the plan's persisted queue follows so a reload cannot resurrect it. */
3399
3969
  removeQueuedMessage(id: string): boolean;
3400
- setQueuedMessages(msgs: ReturnType<TaskOrchestrator['getQueuedMessages']>): void;
3401
- processNextQueuedMessage(): QueuedMessage | null;
3402
- get queuedCount(): number;
3403
- getTask(taskId: string): Task | undefined;
3404
- get isReviewApproved(): boolean;
3970
+ setQueuedMessages(msgs: QueuedMessage[]): void;
3405
3971
  /** Replay a run a dirty tree blocked, after stashing the tracked changes. */
3406
3972
  continueWithStash(): Promise<void>;
3407
3973
  /** Replay a run a dirty tree blocked, in the workspace root, for this run only. */
@@ -3507,7 +4073,6 @@ declare class Session {
3507
4073
  * rule instead of slipping past it.
3508
4074
  */
3509
4075
  setTaskDependencies(taskId: string, dependencies: string[]): Promise<LegacyPlanState | null>;
3510
- completeTask(taskId: string): Promise<void>;
3511
4076
  /**
3512
4077
  * Delete one task. A running task is cancelled first: the plan can drop it
3513
4078
  * either way, but nothing can reach its runner afterwards — the tmux session
@@ -3530,9 +4095,6 @@ declare class Session {
3530
4095
  * moved on, not that the whole task should be refused.
3531
4096
  */
3532
4097
  addTask(draft: Partial<Task>): Promise<LegacyPlanState | null>;
3533
- mergeTasks(taskIdA: string, taskIdB: string): Promise<LegacyPlanState | null>;
3534
- mergeMultipleTasks(taskIds: string[]): Promise<LegacyPlanState | null>;
3535
- splitTask(taskId: string, newTasks: Partial<Task>[]): Promise<LegacyPlanState | null>;
3536
4098
  /**
3537
4099
  * Planner-driven merge: validate compatibility up front, then ask the planner
3538
4100
  * LLM to produce a single "merge" taskOps op combining the selected tasks.
@@ -3552,9 +4114,7 @@ declare class Session {
3552
4114
  sessionId?: string;
3553
4115
  persist?: boolean;
3554
4116
  }): void;
3555
- modifyPlan(userRequest: string): Promise<Task[]>;
3556
4117
  destroy(): void;
3557
- private broadcastPlan;
3558
4118
  get aiServiceInstance(): IAiService;
3559
4119
  }
3560
4120
 
@@ -3575,6 +4135,11 @@ declare function globalDataDir(): string;
3575
4135
  */
3576
4136
  declare function migrateOldConfigDir(): void;
3577
4137
 
4138
+ /** Create `dir` (and its parents) owner-only, leaving any existing directory at its own mode. */
4139
+ declare function ensurePrivateDir(dir: string): void;
4140
+ /** Write `content` to `filePath`, atomically, ending at 0600 on POSIX. */
4141
+ declare function writePrivateFile(filePath: string, content: string): void;
4142
+
3578
4143
  type VerdictListener = (taskId: string, verdict: Verdict) => void;
3579
4144
  type CheckpointListener = (taskId: string, summary: string) => void;
3580
4145
  /** Fires on every idleSince transition (null→timestamp on silence, timestamp→null on resume/teardown). */
@@ -3602,11 +4167,19 @@ declare class VerdictEngine {
3602
4167
  private lastGeneration;
3603
4168
  private idleTimers;
3604
4169
  private idleSince;
4170
+ /** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
4171
+ private idlePaused;
3605
4172
  onVerdict(listener: VerdictListener): void;
3606
4173
  onCheckpoint(listener: CheckpointListener): void;
3607
4174
  onIdleChange(listener: IdleListener): void;
3608
4175
  /** Advisory silence timestamp for a task, or null if it isn't idle. */
3609
4176
  getIdleSince(taskId: string): string | null;
4177
+ /**
4178
+ * Stop watching a task's silence while it waits on the user (ADR-0018, W1).
4179
+ * Watching resumes when its next turn starts, or when a checkpoint is answered.
4180
+ */
4181
+ pauseIdle(taskId: string): void;
4182
+ private resumeIdle;
3610
4183
  /** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
3611
4184
  private touchIdle;
3612
4185
  /** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
@@ -3720,7 +4293,7 @@ declare function filterModelsForPrompt(modelsByRunner: Partial<Record<RunnerId,
3720
4293
  declare function clampThinkingEffort(effort: string | undefined, variants: {
3721
4294
  id: string;
3722
4295
  }[]): string | undefined;
3723
- declare function coerceAssignments(tasks: Task[], perRunnerAllowlist: Partial<Record<RunnerId, string[]>>, allowedRunners?: RunnerId[], modelsByRunner?: Partial<Record<RunnerId, DiscoveredModel[]>>): Task[];
4296
+ declare function coerceAssignments(tasks: readonly Task[], perRunnerAllowlist: Partial<Record<RunnerId, string[]>>, allowedRunners?: RunnerId[], modelsByRunner?: Partial<Record<RunnerId, DiscoveredModel[]>>): Task[];
3724
4297
 
3725
4298
  /** What a runner offers a task: the models discovered for it and the modes its manifest declares. */
3726
4299
  interface RunnerCatalog {
@@ -3795,13 +4368,13 @@ declare function buildModifyDuringExecutionPrompt(executionLog: TaskSnapshot[],
3795
4368
  * task-ops protocol (with the "merge" op) is injected alongside it by
3796
4369
  * `planContextBlock`. The model emits a single taskOps merge op.
3797
4370
  */
3798
- declare function buildMergePrompt(taskIds: string[], tasks: Task[]): string;
4371
+ declare function buildMergePrompt(taskIds: string[], tasks: readonly Task[]): string;
3799
4372
  /**
3800
4373
  * User-message prompt for a planner-driven split: asks the model to decompose
3801
4374
  * one task into a sequence of smaller tasks. The model decides the breakdown —
3802
4375
  * the user does not hand-type the parts.
3803
4376
  */
3804
- declare function buildSplitPrompt(taskId: string, tasks: Task[]): string;
4377
+ declare function buildSplitPrompt(taskId: string, tasks: readonly Task[]): string;
3805
4378
  /**
3806
4379
  * Runner prompt for the opt-in task that resolves an integration conflict
3807
4380
  * (ADR-0013). The resolver's own worktree starts at the integration tip, so the
@@ -3977,16 +4550,16 @@ declare function summarizeOutput(reviewReason: string | undefined, output: strin
3977
4550
  logTail: string;
3978
4551
  capturedAt: string;
3979
4552
  };
3980
- declare function collectDirectDependencyOutputs(task: Task, allTasks: Task[]): PriorOutput[];
4553
+ declare function collectDirectDependencyOutputs(task: Task, allTasks: readonly Task[]): PriorOutput[];
3981
4554
  declare function renderPriorOutputs(outputs: PriorOutput[]): string;
3982
- declare function augmentPromptWithPriorOutputs(task: Task, allTasks: Task[]): string;
4555
+ declare function augmentPromptWithPriorOutputs(task: Task, allTasks: readonly Task[]): string;
3983
4556
  /**
3984
4557
  * Numbered task list for the model's context. `planTasks` is the nested task
3985
4558
  * tree; subtasks render indented under their parent with the same dotted
3986
4559
  * `taskOrderLabel` the other surfaces use, so an `N.M` the model echoes back
3987
4560
  * matches what `resolveTaskId` accepts.
3988
4561
  */
3989
- declare function renderPlanMap(planTasks: Task[], currentTaskId: string, opts?: {
4562
+ declare function renderPlanMap(planTasks: readonly Task[], currentTaskId: string, opts?: {
3990
4563
  maxEntries?: number;
3991
4564
  }): string;
3992
4565
  interface ComposeOptions {
@@ -3994,7 +4567,14 @@ interface ComposeOptions {
3994
4567
  planMapMaxEntries?: number;
3995
4568
  tddEnabled?: boolean;
3996
4569
  }
3997
- declare function composeAugmentedPrompt(task: Task, allTasks: Task[], opts?: ComposeOptions): string;
4570
+ declare function composeAugmentedPrompt(task: Task, allTasks: readonly Task[], opts?: ComposeOptions): string;
4571
+ /**
4572
+ * The first turn of a continue (ADR-0018, K1): the user's message, then a
4573
+ * short reminder of the protocol. The resumed session already holds the
4574
+ * original prompt, so it is not sent again — but it holds the old worktree
4575
+ * too, and this attempt's is fresh from the integration branch.
4576
+ */
4577
+ declare function composeContinuationPrompt(task: Task, message: string): string;
3998
4578
 
3999
4579
  type ExecFileFn = (command: string, args: string[]) => Promise<{
4000
4580
  stdout: string;
@@ -4129,6 +4709,197 @@ declare class TmuxRunner extends AbstractRunner<TmuxSession> {
4129
4709
  }): Promise<ITerminalSession>;
4130
4710
  }
4131
4711
 
4712
+ interface StructuredRunnerDeps {
4713
+ /** The OS boundary the adapters spawn through. `workspaceEnv` is always the spawn's own `env`. */
4714
+ process?: Partial<Omit<AgentProcessDeps, 'workspaceEnv'>>;
4715
+ /** Overrides adapter construction; production picks by runner id and refuses runners without a connector. */
4716
+ createAdapter?: (runner: string, deps: AgentProcessDeps) => TaskModeAgentAdapter;
4717
+ interruptGraceMs?: number;
4718
+ }
4719
+ interface SessionLaunch {
4720
+ runner: string;
4721
+ deps: AgentProcessDeps;
4722
+ createAdapter: (runner: string, deps: AgentProcessDeps) => TaskModeAgentAdapter;
4723
+ startOptions: TaskStartOptions;
4724
+ interruptGraceMs: number;
4725
+ }
4726
+ /**
4727
+ * One task driven over its runner's programmatic protocol (ADR-0018). It *is*
4728
+ * an `ITerminalSession`, so everything downstream of `onOutput` is unchanged;
4729
+ * what a terminal cannot do sits on {@link StructuredSessionCapability}.
4730
+ *
4731
+ * Ordewell owns the message queue (M1): a message sent mid-turn waits for the
4732
+ * turn to end rather than being typed into a runner that is busy.
4733
+ */
4734
+ declare class StructuredSession extends AbstractTerminalSession implements StructuredSessionCapability {
4735
+ private readonly launch;
4736
+ readonly transport: "structured";
4737
+ private adapter;
4738
+ private adapterStarted;
4739
+ /** Bumped whenever the adapter is replaced, so the old one's late events and exit are ignored. */
4740
+ private generation;
4741
+ private output;
4742
+ private readonly text;
4743
+ private state;
4744
+ private turn;
4745
+ private turnCount;
4746
+ private queue;
4747
+ private messageCount;
4748
+ private interrupting;
4749
+ private lastSessionId;
4750
+ private readonly structuredEmitter;
4751
+ private permissionCount;
4752
+ /**
4753
+ * Open tool requests by the id this session gave them, with the runner's own
4754
+ * id. The runner's ids are only unique to its process — Codex numbers its
4755
+ * requests — while an approval must be answerable by id across the session.
4756
+ */
4757
+ private readonly permissions;
4758
+ constructor(id: string, taskId: string, launch: SessionLaunch);
4759
+ /** Start the runner and send the task's prompt as its first turn. */
4760
+ start(prompt: string): Promise<void>;
4761
+ getOutput(): string;
4762
+ /** A reply typed at the task — a checkpoint answer, most often — is a user message. */
4763
+ write(text: string): void;
4764
+ kill(): void;
4765
+ turnState(): 'working' | 'idle';
4766
+ onTurnEnd(listener: (reason: StructuredTurnEnd) => void): void;
4767
+ onEvent(listener: (event: StructuredEvent) => void): void;
4768
+ sendMessage(text: string): string;
4769
+ removeQueued(id: string): boolean;
4770
+ queued(): QueuedTaskMessage[];
4771
+ nativeSessionId(): string | null;
4772
+ answerPermission(id: string, decision: ApprovalDecision): boolean;
4773
+ /** Every open request goes unanswered once the process that asked is gone. */
4774
+ private withdrawPermissions;
4775
+ interrupt(): Promise<void>;
4776
+ private interruptTurn;
4777
+ /**
4778
+ * The fallback when the runner ignores a soft interrupt: kill it and resume
4779
+ * its session in a fresh process, the way the planner restarts from its
4780
+ * session id after an abort.
4781
+ */
4782
+ private restartInterrupted;
4783
+ private startAdapter;
4784
+ private openTurn;
4785
+ private deliver;
4786
+ private route;
4787
+ /**
4788
+ * The runner spoke with no message of ours in flight. When it is working — a
4789
+ * turn it opened itself once background work finished — that is a turn of the
4790
+ * task like any other: the state says working, a queued message waits, and its
4791
+ * end settles it. A message with no text in `turn_start` marks it as not ours.
4792
+ */
4793
+ private handleOutOfTurn;
4794
+ private openPermission;
4795
+ private cancelPermission;
4796
+ private handleEvent;
4797
+ /**
4798
+ * A queued message goes out as the turn closes, and the state never passes
4799
+ * through `idle` on the way, so a listener told the turn ended can already
4800
+ * see the task is not waiting.
4801
+ */
4802
+ private endTurn;
4803
+ private emitEvent;
4804
+ }
4805
+ /**
4806
+ * The structured transport (ADR-0018): a task's runner as a plain child
4807
+ * process speaking its protocol — no tmux, no `script` (W2). Only runners
4808
+ * with a task-mode connector can be spawned here; routing the rest to the
4809
+ * terminal transport is the caller's decision, not a silent downgrade here.
4810
+ */
4811
+ declare class StructuredRunner extends AbstractRunner<StructuredSession> {
4812
+ private spawnCount;
4813
+ private readonly processDeps;
4814
+ private readonly createAdapter;
4815
+ private readonly interruptGraceMs;
4816
+ constructor(deps?: StructuredRunnerDeps);
4817
+ spawn(opts: RunnerSpawnOptions): Promise<ITerminalSession>;
4818
+ }
4819
+
4820
+ interface TransportRoute {
4821
+ transport: RunnerTransport;
4822
+ /** Why a structured request runs on the terminal instead, in words a surface shows as is. */
4823
+ fallback?: string;
4824
+ }
4825
+ /**
4826
+ * Where one task runs (ADR-0018, S3): structured only when the plan asks for
4827
+ * it and the task's runner has a task-mode connector. Anything else runs on
4828
+ * the terminal, and a structured request says why.
4829
+ */
4830
+ declare function routeTransport(requested: RunnerTransport | undefined, runner: string, registry?: RunnerRegistry | null): TransportRoute;
4831
+ /**
4832
+ * The runner a host hands the orchestrator: its own terminal runner (tmux,
4833
+ * headless, the VS Code terminal) and the structured one behind a single
4834
+ * `ITerminalRunner`, picking per spawn by {@link routeTransport}.
4835
+ *
4836
+ * `stopAll` and `activeCount` reach both inner runners, so a host that shares
4837
+ * one across plans (the daemon's tmux) keeps its per-plan wrapper above this.
4838
+ */
4839
+ declare class TransportRouter implements ITerminalRunner {
4840
+ private readonly runners;
4841
+ /**
4842
+ * Session ids the structured runner owns; every other id is the terminal's.
4843
+ * Kept past exit, so a stop that arrives late never reaches the terminal
4844
+ * runner with an id it does not know.
4845
+ */
4846
+ private readonly structuredIds;
4847
+ constructor(runners: {
4848
+ terminal: ITerminalRunner;
4849
+ structured: ITerminalRunner;
4850
+ });
4851
+ get activeCount(): number;
4852
+ spawn(opts: RunnerSpawnOptions): Promise<ITerminalSession>;
4853
+ stop(sessionId: string): void;
4854
+ stopAll(): void;
4855
+ }
4856
+
4857
+ /** Whether a task can be continued in its saved runner session (ADR-0018, K1), and in which. */
4858
+ type Continuability = {
4859
+ ok: true;
4860
+ sessionId: string;
4861
+ } | {
4862
+ ok: false;
4863
+ reason: string;
4864
+ };
4865
+ /**
4866
+ * The rule every surface offers Continue by and the orchestrator enforces: a
4867
+ * completed or failed task whose last attempt ran structured and left a
4868
+ * session id behind. A conflict is resolved by its repair, not a new turn, and
4869
+ * a terminal task has no session Ordewell can reach — Retry covers both.
4870
+ */
4871
+ declare function continuability(task: Task): Continuability;
4872
+ declare function canContinue(task: Task): boolean;
4873
+
4874
+ interface TaskLogRecorderDeps {
4875
+ broadcast: SessionBroadcaster;
4876
+ /** Where the session's logs go, read as each attempt starts: a session's id can change between plans. */
4877
+ location: () => TaskLogLocation;
4878
+ /** Opens an attempt's file; tests pass one that never touches the disk. */
4879
+ open?: (location: TaskLogLocation, taskId: string) => TaskLogFile;
4880
+ flushMs?: number;
4881
+ logger?: ILogger;
4882
+ }
4883
+ /**
4884
+ * Keeps each structured task's log (ADR-0018, P1): every event of an attempt
4885
+ * is appended to that attempt's file and broadcast as `task_log`, in the same
4886
+ * batches, so what a surface saw live and what it reloads are one sequence.
4887
+ *
4888
+ * It sits around the runner rather than inside the orchestrator, which stays
4889
+ * unaware of logs; a terminal-transport session has no event stream and is
4890
+ * passed through untouched.
4891
+ */
4892
+ declare class TaskLogRecorder {
4893
+ private readonly deps;
4894
+ private readonly open;
4895
+ private readonly flushMs;
4896
+ private readonly logger;
4897
+ constructor(deps: TaskLogRecorderDeps);
4898
+ wrap(runner: ITerminalRunner): ITerminalRunner;
4899
+ /** Start a new attempt's log for `session` and keep it until the session exits. */
4900
+ record(taskId: string, session: ITerminalSession & StructuredSessionCapability): void;
4901
+ }
4902
+
4132
4903
  /**
4133
4904
  * A workspace path that does not exist, or exists but is not a directory.
4134
4905
  * Checked at every surface that starts a planner or spawns an agent inside
@@ -4208,10 +4979,11 @@ declare function daemonTokenPath(port: number): string;
4208
4979
  /**
4209
4980
  * Mint this daemon's token and hand it off through the filesystem.
4210
4981
  *
4211
- * Unlink-then-create-exclusively rather than a plain write: `mode` is ignored
4212
- * when the file already exists, so writing over a pre-created file would leave
4213
- * the token at whatever permissions that file already had, and an existing
4214
- * symlink would carry the write somewhere else entirely.
4982
+ * The write goes through `writePrivateFile` rather than a plain write because
4983
+ * `mode` is ignored when the file already exists: writing over a pre-created
4984
+ * file would leave the token at whatever permissions that file already had, and
4985
+ * an existing symlink would carry the write somewhere else entirely. The helper
4986
+ * writes a fresh 0600 temp file and renames it into place, so neither applies.
4215
4987
  */
4216
4988
  declare function mintDaemonToken(port: number): {
4217
4989
  token: string;
@@ -4397,6 +5169,14 @@ declare function assertInstallablePluginUrl(url: unknown): URL;
4397
5169
  declare function classifyPluginSource(source: string): PluginSource;
4398
5170
 
4399
5171
  declare function resolveArgs(manifest: RunnerPluginManifest, ctx: ResolveContext): RunnerInvocation;
5172
+ /**
5173
+ * The manifest's meaning of a task's mode and effort, for a runner driven over
5174
+ * its programmatic protocol rather than a command line built from
5175
+ * `argsTemplate` (ADR-0018, C1). The effort stays a raw id behind the same
5176
+ * `{{if thinking}}` gate as the template path: how an effort reaches the
5177
+ * runner is protocol, which each adapter owns.
5178
+ */
5179
+ declare function resolveTaskRunnerFlags(manifest: RunnerPluginManifest, ctx: Pick<ResolveContext, 'mode' | 'model' | 'thinkingEffort'>): TaskRunnerFlags;
4400
5180
 
4401
5181
  declare const CLAUDE_CODE_MANIFEST: RunnerPluginManifest;
4402
5182
 
@@ -4422,6 +5202,17 @@ declare function loadState(baseDir?: string, logger?: ILogger): LegacyPlanState
4422
5202
  declare function clearState(baseDir?: string): void;
4423
5203
  declare function stateExists(baseDir?: string): boolean;
4424
5204
 
5205
+ /**
5206
+ * A path segment for an id that came from outside — a session id from a URL,
5207
+ * a task id a planner wrote. A plain id is used as is; anything that could
5208
+ * climb out of its directory, or is not portable as a file name, is hex-encoded.
5209
+ */
5210
+ declare function idSegment(id: string): string;
5211
+ /**
5212
+ * The directory beside a session's file for what it keeps outside that file —
5213
+ * its task logs (ADR-0018, P1). It goes when the session does.
5214
+ */
5215
+ declare function sessionDataDir(sessionId: string, baseDir?: string): string;
4425
5216
  declare function saveSession(plan: LegacyPlanState, goal: string, baseDir?: string, id?: string): SessionMeta;
4426
5217
  declare function listSessions(baseDir?: string, logger?: ILogger): SessionMeta[];
4427
5218
  declare function loadSession(sessionId: string, baseDir?: string, logger?: ILogger): {
@@ -4516,4 +5307,4 @@ declare const DEFAULT_MAX_PARALLEL = 3;
4516
5307
  */
4517
5308
  declare function parseMaxParallel(value: unknown): number | null;
4518
5309
 
4519
- 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, SubagentOutcome, 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, UsageRecord, 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 };
5310
+ 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, TaskRunnerFlags, 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 };