/** * Tier-1 pane-spawn seam — injectable herdr CLI boundary. * * Owns ALL herdr CLI access for the pane-spawn path: * worktree create → agent start (in the worktree workspace's root pane) * → report-agent → release-agent → pane close → worktree remove. * * Every method is behind an injectable `HerdrInvoker` interface so unit tests * mock the invoker and never touch a live herdr server. * * Run-level pane only (not per subagent). One real pi process per run. * * ## Herdr 0.8.0 contract (protocol 19) * * Herdr 0.8.0 split layout from agent lifecycle. `agent start` no longer creates * layout — it adopts an EXISTING pane that is sitting at an interactive shell * prompt, and it requires `--kind`: * * herdr agent start --kind --pane [--timeout MS] [-- ] * * `herdr worktree create` already returns a workspace whose ROOT PANE is an idle * shell at the worktree checkout, so the run pane is that root pane — no split is * needed in the common case. A split is only required when the manager is rooted * at a repo SUBDIRECTORY and the run must start below the worktree root. * * Response shapes are the socket-API envelope `{ id, result: { … } }`, NOT bare * payloads. See `herdr api schema --json`. */ import type { ConductorRunStatus } from "./conductor-types.js"; /** * Result of `herdr worktree create`. * * `workspaceId` and `rootPaneId` come from the same create response, so the * caller never has to correlate a second lookup. `workspaceId` is what * `worktree remove --workspace ` needs; `rootPaneId` is the idle shell pane * `agent start --pane` adopts. * * `sourceWorkspaceId` is set when herdr had to OPEN a workspace for the parent * repo in order to create the worktree. That workspace is not a linked worktree, * so `worktree remove` refuses it (`not_linked_worktree`) — cleanup must use * `workspace close`. Undefined when the parent repo already had a workspace. */ export interface HerdrWorktree { cwd: string; branch: string; /** Workspace holding the linked worktree checkout (`worktree remove --workspace`). */ workspaceId?: string; /** Idle shell pane at `cwd`, created by herdr with the workspace. */ rootPaneId?: string; /** Parent-repo workspace herdr auto-opened for this create, if any. */ sourceWorkspaceId?: string; } /** * Injectable boundary for every herdr CLI call in the pane-spawn path. * All methods return void/Promise and never throw into the runtime. */ export interface HerdrInvoker { /** * `herdr worktree create --branch --cwd ` → * `{cwd, branch, workspaceId, rootPaneId, sourceWorkspaceId}`. * * Worktree commands are JSON-only; `--json` was dropped from help in 0.8.0 and * is not passed. The response envelope is `{id, result: {worktree, workspace, * root_pane, tab}}`. */ worktreeCreate(opts: { cwd: string; branch: string; }): Promise; /** * `herdr worktree remove --workspace ` — deletes a Herdr-managed worktree * through Herdr's own CLI so Herdr's internal workspace/group bookkeeping stays * consistent. 0.8.0 removed `--branch`/`--cwd`: the ONLY selector is the * workspace id. * * `workspaceId` is optional so callers holding only a legacy persisted record * (branch + repo root, no workspace id) still clean up: the invoker falls back * to `worktree list --cwd ` and matches on branch. */ worktreeRemove(opts: { workspaceId?: string; cwd?: string; branch?: string; }): Promise; /** * `herdr workspace close ` — closes a workspace that is not a linked * worktree (`worktree remove` rejects those with `not_linked_worktree`). Used * to clean up the parent-repo workspace herdr auto-opens during * `worktree create`. */ workspaceClose(workspaceId: string): Promise; /** * `herdr pane split --pane --direction down --cwd --no-focus` → * new pane id. Layout only — 0.8.0's `agent start` never creates layout. */ paneSplit(opts: { pane: string; cwd: string; direction?: "right" | "down"; }): Promise<{ paneId: string; }>; /** * `herdr agent start --kind --pane [-- ]` * * Adopts an EXISTING idle shell pane. `pane` is normally the worktree * workspace's root pane from {@link HerdrWorktree.rootPaneId}; when the run * must start in a subdirectory of the worktree, the caller splits first and * passes the new pane. * * Returns `{paneId: ""}` on any failure so callers can fail closed. */ agentStart(opts: { name: string; kind: string; pane: string; timeoutMs?: number; }, argv: string[]): Promise<{ paneId: string; }>; /** * `herdr pane report-agent --source --agent --state [--message ] [--seq ]` * Reports the live state of the agent running in the spawned pane. * `--ttl-ms` is NOT supported by report-agent (only report-metadata); do not add it. */ reportAgent(pane: string, opts: { source: string; agent: string; state: "idle" | "working" | "blocked"; message?: string; customStatus?: string; seq?: string; }): void; /** * `herdr pane report-metadata --source --seq [--title ] [--ttl-ms ]` * Layers a one-line custom status on the same pane. */ reportMetadata(pane: string, opts: { source: string; seq: string; customStatus?: string; ttlMs?: number; }): void; /** `herdr pane release-agent --source --agent ` — marks the agent done. */ releaseAgent(pane: string, opts: { source: string; agent: string; }): void; /** `herdr pane close ` — closes the spawned pane. */ paneClose(pane: string): void; /** * `herdr notification show --sound <done|request>` — raises a desktop * toast/sound so a kept-open pane's terminal state (needs-finalize, * needs-human, completed, failed) is surfaced even when the operator is not * watching the pane. Fire-and-forget; never throws. */ notify(pane: string, opts: { title: string; sound: "done" | "request"; }): void; } /** * Default HerdrInvoker that shells `herdr` via `spawn(...).unref()`. * Fire-and-forget: swallows all errors so a missing/broken herdr binary * never throws into the workflow runtime. */ export declare function createDefaultHerdrInvoker(): HerdrInvoker; /** * Pure mapping from a ConductorRunStatus to the herdr report-agent state. * Implements the docs §6 table exactly: * * | ConductorStatus | herdr state | custom status | release | closePane | notify | * |---------------------------|-------------|----------------------------|---------|-----------|---------| * | spawned | working • spawned | — | — | — | * | workflow-running | working ▶ <phase> (reason) | — | — | — | * | workflow-complete-pane-open | working ◐ complete (pane open) | — | — | — | * | needs-finalize | blocked ! needs finalize | — | — | request | * | finalizing | working ⟳ finalizing | — | — | — | * | completed | idle ✓ done | yes | yes | done | * | failed | blocked ✗ failed | — | — | request | * | needs-human | blocked ? needs human | — | — | request | */ export interface HerdrStateMapping { state: "idle" | "working" | "blocked"; customStatus: string; release?: boolean; closePane?: boolean; notify?: "done" | "request"; } /** * Map a ConductorRunStatus to the herdr cell state. * Pure function — unit-tested directly. */ export declare function conductorToHerdrState(status: ConductorRunStatus): HerdrStateMapping; /** Lease returned by `acquire()` — must be returned via `release()`. */ export interface SpawnLease { runId: string; /** Release this lease back to the pool. */ release: () => void; } /** * Enforces `herdrMaxPanes` concurrency cap. * `acquire(runId)` returns a lease when under the cap, or `null` when the * cap is exceeded — never throws. The caller fails closed when null. */ export declare class PaneSpawnCoordinator { private static _cache; private active; private cap; constructor(maxPanes?: number); /** * Get or create a shared coordinator for a given project directory. * Ensures all WorkflowManager instances in the same project share * the same concurrency cap. If a new maxPanes is TIGHTER (lower) than the * cached cap, lower the cached cap so a manager reload honors the reduced * setting immediately (raising it is ignored to avoid loosening a cap set * by a stricter instance). Lowering only affects future `acquire()` calls; * in-flight leases are unaffected. */ static get(projectCwd: string, maxPanes?: number): PaneSpawnCoordinator; /** Clear the shared cache (for testing). */ static reset(): void; /** * Acquire a concurrency slot for `runId`. * Returns a lease on success, `null` when the cap is exceeded. * Never throws. * * Idempotent for the same `runId`: a failed/paused pane-spawn run intentionally * keeps its lease while the pane stays open (see executeRun's finally), and * `resume()` builds a fresh `ManagedRun` for the same `runId`. Re-acquiring * would count the run's own retained pane against the cap and return null * under `herdrMaxPanes: 1`, blocking the run from reattaching to its own pane. * When `runId` is already active, return a lease without re-adding it (release is * a no-op idempotent delete) so resume reuses the retained slot. */ acquire(runId: string): SpawnLease | null; /** Current active count (for observability). */ get activeCount(): number; /** Configured concurrency cap (for observability / error messages). */ get maxPanes(): number; /** * Reconcile the in-memory `active` set with persisted pane-spawn runs after a * process restart. A persisted run with a live `paneId` kept its Herdr pane * open (failed/paused/attention states retain the pane), so it must still count * against the cap — otherwise a fresh manager permits another full cap of * pane-spawn runs and defeats the VM memory ceiling. Each `runId` is seeded via * the idempotent `acquire()` so existing membership is not double-counted. * Returns the runIds that were newly seeded (already-active ones are skipped). */ reconcile(persistedPaneRunIds: Iterable<string>): string[]; } /** * Handle for a spawned pane — update the pane status or close it. Returned by * {@link createPaneHandle} after the manager has run `worktreeCreate` + * `agentStart` on the invoker. There is no single `spawnRunPane` orchestrator: * the manager drives the worktree/agent-start steps directly so it can interleave * persistence and concurrency-lease acquisition between them. */ export interface RunPaneHandle { paneId: string; /** Push a new conductor status into the herdr cell. */ updateStatus(status: ConductorRunStatus): void; /** Close the pane and release the agent. */ close(): void; } /** * Create a pane handle from a paneId. The handle manages report-agent / release / close * through the injected invoker. */ export declare function createPaneHandle(invoker: HerdrInvoker, paneId: string): RunPaneHandle;