/** * Tier-1 pane-spawn seam — injectable herdr CLI boundary. * * Owns ALL herdr CLI access for the pane-spawn path: * worktree create --json → agent start (workspace/tab/split nesting) → report-agent → release-agent → pane close. * * 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. */ import type { ConductorRunStatus } from "./conductor-types.js"; /** Result of `herdr worktree create --json`. */ export interface HerdrWorktree { cwd: string; branch: 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 --json` → {cwd, branch}. */ worktreeCreate(opts: { cwd: string; branch: string; }): Promise; /** * `herdr worktree remove --workspace ` (or `--cwd `/`--branch `) — * deletes a Herdr-managed worktree/checkout through Herdr's own CLI so Herdr's * internal workspace/group bookkeeping stays consistent. Used to clean up a * Herdr-created worktree when the pane spawn fails (e.g. agentStart returned no * pane id) instead of reaching behind Herdr's back with the local git * `removeWorktree()` helper. */ worktreeRemove(opts: { cwd: string; branch: string; }): Promise; /** * `herdr agent start --cwd [--workspace ] [--tab ] [--split ] -- ` * Starts a new herdr agent pane, nested under the caller when workspace/tab are set. */ agentStart(opts: { name: string; cwd: string; workspace?: string; tab?: string; split?: string; }, argv: string[]): Promise<{ paneId: string; }>; /** * `herdr pane report-agent --source --agent --state [--custom-status ] [--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 [--custom-status ] [--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; /** * Resolve workspace/tab/split nesting from the caller's herdr environment * so the spawned agent pane is nested under the caller pane, never an * orphaned top-level agent. * * When HERDR_WORKSPACE_ID + HERDR_TAB_ID are present (inside herdr), returns * `{workspace, tab, split: 'down'}` so the new pane splits below the caller. * When env is empty (not inside herdr), returns `{}` — no nesting. */ export declare function resolveNesting(env: NodeJS.ProcessEnv): { workspace?: string; tab?: string; split?: string; }; /** 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;