/** * Orchestrate Spawn Operations * * orchestrateSpawn, orchestrateSpawnExecute, orchestrateSpawnSelectProvider, * and private spawn helpers migrated from * packages/cleo/src/dispatch/engines/orchestrate-engine.ts. * * # T9545 — Spawn hang root cause + timeout supervisor + auto-cleanup * * **Root cause (file:line evidence):** * The spawn pipeline invokes git/`cp` subprocesses through helpers in * `packages/worktree/src/git.ts:23` (`gitSync`), `:40` (`gitSilent`), * `:57` (`gitAsync`) — none had an `execFileSync` / `execFile` `timeout` * option set. The same gap existed at * `packages/worktree/src/worktree-list.ts:110`, * `packages/worktree/src/compat.ts:195`, and * `packages/worktree/src/copy-on-write.ts:99,104`. * * When `.git/index.lock` contention persists, a wedged `git worktree add` * (called from `worktree-create.ts:193,209,214`) would block * {@link spawnWorktree} forever, silently wedging `orchestrateSpawn`. The * parent CLI exited without surfacing the hang. * * Additionally, the original T9545 supervisor (v2026.5.99) **preserved** * partially-provisioned worktrees on timeout, requiring callers to manually * run `cleo orchestrate worktree.prune` to recover. The Saga T10176 / D010 * verdict reversed that decision: orphan worktrees compound across parallel * agent waves, so spawn timeouts now auto-cleanup. * * **Fix (T9545 — Saga T10176):** * 1. Every subprocess call in the spawn pipeline now has an explicit * `timeout` option (`DEFAULT_GIT_TIMEOUT_MS = 60_000` in * `packages/worktree/src/git.ts`). * 2. `orchestrateSpawn` runs under an overall {@link AbortController} budget * (`SPAWN_BUDGET_MS = 60_000`). On budget exhaustion the function returns * an `E_TIMEOUT` envelope rather than hanging. * 3. Progress logs (`engine:orchestrate`) emit at each major step: * `validate-readiness`, `provision-worktree`, `compose-prompt`, * `persist-state` — giving the orchestrator observability. * 4. **(Saga T10176 / D010 — reversed)** On timeout the supervisor invokes * {@link destroyWorktree} with its own bounded budget * ({@link CLEANUP_BUDGET_MS}) so any partial worktree is automatically * unwound (unlock + remove + branch delete + audit log + sentinel index * eviction). Cleanup is idempotent — re-invocation against an absent * worktree succeeds silently. Cleanup failures are captured in * `error.details.cleanup` and never overwrite the original timeout cause. * * Pattern mirrors {@link runGitWithLockRetry} from * `packages/core/src/release/engine-ops.ts:86-182` (T9501). * * @task T1570 * @task T4478 * @task T5236 * @task T932 * @task T1238 * @task T1140 * @task T1253 * @task T9545 */ import type { AgentSpawnCapability } from '@cleocode/contracts'; import { type EngineResult } from '../engine-result.js'; import type { HarnessSpawnCapability } from '../harness/types.js'; import type { SpawnPayload } from '../orchestration/spawn.js'; import type { ConduitSubscriptionConfig } from '../orchestration/spawn-prompt.js'; export type { EngineResult }; /** * Overall budget for a single {@link orchestrateSpawn} invocation in * milliseconds. Enforced via an {@link AbortController} that races against * the spawn pipeline; on expiry an `E_TIMEOUT` envelope is returned. * * 180s mirrors `DEFAULT_GIT_TIMEOUT_MS` so the wrapper never fires before a * single bounded subprocess would have surfaced its own timeout error. The * prior 60s budget caused every spawn on large-repo monorepos (10k+ files) * to fail with `E_WORKTREE_PROVISION_FAILED` because `git worktree add` * routinely exceeded 60s during cold checkout (T9823). * * @task T9545 * @task T9823 */ export declare const SPAWN_BUDGET_MS = 180000; /** * Bounded budget for the timeout-cleanup pass (Saga T10176 / D010). Cleanup * itself must not be allowed to hang — if `destroyWorktree` fails to return * within this window the timeout envelope reports the cleanup error in * `error.details.cleanup` and the caller falls back to `cleo worktree prune`. * * 5s is intentionally aggressive: `destroyWorktree` only runs a handful of * fast `git` commands (`unlock`, `remove --force`, `branch -D`) plus a * filesystem rm fallback. * * @task T9545 */ export declare const CLEANUP_BUDGET_MS = 5000; /** * Step name emitted in spawn-pipeline progress logs. Used by the integration * test harness to assert each major step is announced. * * @task T9545 */ export type SpawnPipelineStep = 'validate-readiness' | 'lint-changesets' | 'provision-worktree' | 'compose-prompt' | 'persist-state' | 'budget-exceeded'; /** * Result shape returned by {@link runTimeoutCleanup}. Surfaced on the * `E_TIMEOUT` envelope under `error.details.cleanup` so callers can verify * the orphan-worktree state was actually unwound. * * @task T9545 */ export interface TimeoutCleanupResult { /** True when `destroyWorktree` completed (or there was nothing to clean). */ attempted: boolean; /** True when the worktree was successfully removed (or already absent). */ worktreeRemoved: boolean; /** True when the task branch was deleted (or already absent). */ branchDeleted: boolean; /** Captured error message when cleanup failed or exceeded its own budget. */ error?: string; /** Milliseconds the cleanup pass took. */ elapsedMs: number; } /** * Signals that indicate a worktree may be in a partial / wedged state. * * @task T10455 */ export interface PartialWorktreeSignals { /** True when `git status --porcelain` returns non-empty output. */ hasUncommittedChanges: boolean; /** True when `node_modules` is missing from the worktree root. */ nodeModulesMissing: boolean; /** True when `.git/index.lock` exists (wedged git index). */ indexLockPresent: boolean; /** True when the worktree directory itself exists but looks incomplete. */ worktreeExists: boolean; } /** * Detect whether a worktree is in a partial state after a timeout or * ETIMEDOUT error. Checks for: * 1. Uncommitted changes (`git status --porcelain`) * 2. Missing `node_modules` * 3. Stale `.git/index.lock` * * The result is logged as structured JSON to stderr so external monitors * (e.g. the worktree-recovery cron or the orchestrator dashboard) can * observe it without parsing free-form text. * * @param worktreePath - Absolute path to the worktree directory. * @param taskId - Task ID associated with the worktree. * @returns Detection signals. * @task T10455 */ export declare function detectPartialWorktree(worktreePath: string, taskId: string): PartialWorktreeSignals; /** * Best-effort, bounded cleanup of a partially-provisioned worktree after the * spawn supervisor fires. Idempotent: re-running against an absent worktree * returns `worktreeRemoved: true` without erroring. Wrapped in its own * `setTimeout` so cleanup itself can never wedge the timeout envelope. * * @param projectRoot - Absolute path to the project root. * @param taskId - Task ID whose worktree is being cleaned up. * @returns Structured cleanup outcome (never throws). * @task T9545 */ export declare function runTimeoutCleanup(projectRoot: string, taskId: string): Promise; /** * Run the lint-changesets hygiene gate. * * Executes `node scripts/lint-changesets.mjs` from the project root. * Returns `ok: true` when the linter exits 0 (or when the script is absent, * so the gate is non-blocking in non-monorepo contexts). * Returns `ok: false` with the captured stderr when the linter exits non-zero. * * @param projectRoot - Absolute path to the project root. * @returns Gate result with ok flag and optional error output. * @task T10448 */ export declare function runLintChangesets(projectRoot: string): { ok: boolean; error?: string; }; /** Structured payload for a conduit orchestration event message. */ export interface ConduitOrchestrationEvent { event: 'agent.spawned' | 'orchestrate.handoff'; taskId: string; [key: string]: unknown; } /** * Send a structured orchestration event to conduit.db via the active agent * credential. Failures are silently swallowed — conduit events MUST NOT block * or alter orchestration outcomes. * * @param cwd - Project root used to locate conduit.db and the agent registry. * @param to - Recipient agent ID (e.g. 'cleo-core'). * @param event - Structured event payload (LAFS-shaped JSON). */ export declare function sendConduitEvent(cwd: string, to: string, event: ConduitOrchestrationEvent): Promise; /** * Compose a spawn payload through {@link composeSpawnPayload} with a task-id * lookup shortcut. * * Centralizes the composer invocation used by both {@link orchestrateSpawn} * and {@link orchestrateSpawnExecute} so every spawn emit path in the engine * routes through the same canonical composer. * * @param taskId - Task to render the spawn for. * @param root - Absolute project root. * @param options - Composer overrides (tier, sessionId, role, …). * @returns Full {@link SpawnPayload} envelope with atomicity + meta surfaced. * @task T932 */ export declare function composeSpawnForTask(taskId: string, root: string, options?: { tier?: 0 | 1 | 2; sessionId?: string | null; role?: AgentSpawnCapability; protocol?: string; skipAtomicityCheck?: boolean; /** Pre-provisioned worktree path — emits Worktree Setup section (T1140). */ worktreePath?: string; /** Branch name for the worktree (T1140). */ worktreeBranch?: string; /** * CONDUIT A2A subscription configuration derived from the task's parent * epic. When present the spawn prompt gains a `## CONDUIT Subscription` * section (tier 1+). Omit for tier-0 or top-level tasks. * * @task T1253 */ conduitSubscription?: ConduitSubscriptionConfig; /** * Glob patterns excluded from the worktree (T9226). When set, a * `## Worktree Scope` section is injected into the spawn prompt. * * @task T9226 */ spawnCloneExclude?: readonly string[]; /** * T9214 / T10430 atomicity waiver. When `'orchestrator-defer'`, flows * through to {@link composeSpawnPayload}'s `scope` option so the worker * file-scope gate records an auditable `atomicity_waiver` rather than * rejecting the spawn with `E_ATOMICITY_NO_SCOPE`. Distinct from * `spawnCloneExclude` (worktree-tree filter) — this governs the * file-scope gate only. * * @task T9214 * @task T10430 */ atomicityScope?: 'orchestrator-defer'; }): Promise; /** * orchestrate.spawn.select - Select best provider for spawn based on required capabilities * * @param capabilities - Required harness capabilities to filter providers. * @param _projectRoot - Optional project root path (unused but kept for API parity). * @returns Engine result with provider selection data. * @task T5236 */ export declare function orchestrateSpawnSelectProvider(capabilities: HarnessSpawnCapability[], _projectRoot?: string): Promise; /** * Options accepted by {@link orchestrateSpawnExecute} that go beyond the * primitive positional args. Currently used by T9548 to thread the auto-merge * toggle through without breaking the existing 5-arg signature. * * @task T9548 */ export interface OrchestrateSpawnExecuteOpts { /** * When `true` (default), auto-invoke {@link completeWorktreeForTask} after * a successful worker spawn returns. Idempotent — re-running on an already * completed worktree is a no-op. Set to `false` to skip the auto-merge step * (e.g. when the orchestrator wants to inspect the worktree before * integration, or for `--no-auto-complete` flows). * * @default true */ autoComplete?: boolean; } /** * orchestrate.spawn.execute - Execute spawn for a task using adapter registry * * T9548 — after a successful spawn returns, the function auto-invokes * {@link completeWorktreeForTask} so the worker's worktree is merged * (`git merge --no-ff` per ADR-062) back into the project default branch * and pruned. The behaviour is idempotent and can be disabled by passing * `autoComplete: false` in {@link OrchestrateSpawnExecuteOpts}. * * @param taskId - Task to spawn. * @param adapterId - Optional adapter id to use. Auto-selects if not provided. * @param protocolType - Optional protocol type override. * @param projectRoot - Optional project root path. * @param tier - Optional spawn tier (0=worker, 1=lead, 2=orchestrator). * @param opts - Optional behaviour overrides (T9548 auto-complete toggle). * @returns Engine result with spawn execution data. * @task T5236 * @task T9548 */ export declare function orchestrateSpawnExecute(taskId: string, adapterId?: string, protocolType?: string, projectRoot?: string, tier?: 0 | 1 | 2, opts?: OrchestrateSpawnExecuteOpts): Promise; /** * orchestrate.spawn - Generate spawn prompt for a task. * * Every spawn emit in the engine routes through * {@link composeSpawnPayload} (T932) so atomicity, harness-hint dedup, and * traceability metadata are active on every orchestrate spawn. Legacy * `prepareSpawn` is no longer called directly from this path. * * The response envelope surfaces: * * - `atomicity` — the worker file-scope gate verdict * - `meta.composerVersion` — pinned to `'3.0.0'` for the T932 composer path * - `meta.dedupSavedChars` — characters saved via harness-hint dedup * - `meta.promptChars` — total rendered prompt length * - `meta.sourceTier` — registry tier the resolved agent was sourced from * * Atomicity violations return an `E_ATOMICITY_VIOLATION` error envelope with * the full verdict attached to `error.details.atomicity` so callers can * inspect the rejection reason before retrying. * * T9545 — every invocation runs under a {@link SPAWN_BUDGET_MS} overall * timeout enforced via an {@link AbortController}. When the budget expires * the function returns an `E_TIMEOUT` envelope without deleting any * partially-provisioned worktree (the lifecycle prune step handles orphan * cleanup). Progress is logged per step via the `engine:orchestrate` logger * so a wedged step is observable in real time. * * @param taskId - Task to generate spawn prompt for. * @param protocolType - Optional protocol type override. * @param projectRoot - Optional project root path. * @param tier - Optional spawn tier (0=worker, 1=lead, 2=orchestrator). * @param noWorktree - If true, skip worktree provisioning. * @param spawnScope - T9807 sparse-checkout cone path (governs the worktree's * checked-out tree). Distinct from `atomicityScope`. * @param atomicityScope - T9214 / T10430 atomicity waiver. When * `'orchestrator-defer'`, the composer routes the waiver into * {@link composeSpawnPayload} so the worker file-scope gate grants the * spawn and records `atomicity_waiver: 'orchestrator-scope-tier1-call'` * instead of returning `E_ATOMICITY_NO_SCOPE`. * @returns Engine result with spawn prompt data. * @task T4478 * @task T932 * @task T9545 * @task T9214 * @task T10430 */ export declare function orchestrateSpawn(taskId: string, protocolType?: string, projectRoot?: string, tier?: 0 | 1 | 2, noWorktree?: boolean, spawnScope?: string, atomicityScope?: 'orchestrator-defer', /** * T10078 — when true, skip worktree provisioning and use an existing locked * worktree at the canonical XDG path. If the worktree does not exist at * the expected path, returns E_RESUME_WORKTREE_MISSING. */ resume?: boolean): Promise; //# sourceMappingURL=spawn-ops.d.ts.map