import * as _harness_engineering_types from '@harness-engineering/types'; import { AgentBudgetConfig, Issue, BudgetEnvelopeStatus, WorkflowExecutionPlan, StageRun, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, LeafContextSource, ScopeTier, EscalationConfig, IssueRoutingDecision, Result, WorkflowDefinition, BackendDef, RoutingConfig, PolicyMetadata, RoutingDecision, RoutingUseCase, ContainerConfig, SecretConfig, PolicyNetworkMode, AgentBackend, RoutingRequest, CapabilityTier, WorkflowStep, RetrievalMode, WorkspaceConfig, HarnessIdentity, HooksConfig, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, CheckScriptDefinition, OutputRetentionConfig, BackendCapabilityRegistry, RoutingPolicy, ComplexityVerdict, RoutingTelemetry, RoutingStatus, RoutingError, PrivacyClass, BackendCapabilities, AgentConfig, PolicySandboxMode, LocalModelStatus, CustomTaskDefinition, MaintenanceConfig, TokenScope, AuthToken, AuthTokenPublic, IndexedFileKind, SessionSearchResult, ReindexStats, SessionSummarizationConfig, SessionSummary, SessionSummaryMeta, SessionsConfig, RetrospectionConfig, GatewayEvent, NotificationEnvelope, NotificationDeliveryResult, NotificationSinkConfig, NotificationsConfig } from '@harness-engineering/types'; import { IssueTrackerClient, Issue as Issue$1, CacheMetricsRecorder, TrackerConfig, ProvenanceTrailerInput, eventSourcing, RoadmapStore, ArchiveHooks, SkillProposal, ProposalGateFinding, SkillKind } from '@harness-engineering/core'; import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation, AnalysisProvider, PrecedentLookup, ProbeConfig, ProbeInput, GraphScope, TriageVerdict, ForkGenerator, BrainstormOutcome, BrainstormInput, SpecDraft, GoNoGoCandidate } from '@harness-engineering/intelligence'; export { RankableCandidate, TriageVerdict, pilotScore, rankTriageCandidates } from '@harness-engineering/intelligence'; import { GraphStore } from '@harness-engineering/graph'; import { execFile } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { PoolStateProvider, SchedulerTimerHandle, DiscoverCandidatesOptions, DiscoverCandidatesResult, HarnessFitRunner, HarnessFitProbeTask, HarnessFitResult } from '@harness-engineering/local-models'; export { DEFAULT_POOL_STATE_PATH, PoolState, PoolStateProvider, PoolStateStore, RankProfile, discoverCandidates, poolStateToCandidates } from '@harness-engineering/local-models'; import { z } from 'zod'; /** Length of one accounting window, in ms. */ declare function resolvePeriodLengthMs(period: AgentBudgetConfig['period']): number; /** @deprecated Use `resolvePeriodLengthMs` instead. */ declare const periodLengthMs: typeof resolvePeriodLengthMs; /** * Mutable per-period spend accumulator. Anchored at the first spend of a window; * a window that has fully elapsed is rolled (reset) on the next spend or read. */ interface BudgetState { /** Epoch-ms anchor of the active window. */ periodStartMs: number; /** Total tokens spent in the active window. */ spentTokens: number; /** Per-fleet token spend in the active window (fleet key → tokens). */ perFleetSpent: Record; } /** Fresh, empty budget state anchored at `nowMs`. */ declare function createBudgetState(nowMs: number): BudgetState; /** Deep clone (the state machine clones the whole {@link OrchestratorState} per event). */ declare function cloneBudgetState(state: BudgetState): BudgetState; /** * Roll the window if it has elapsed, returning a FRESH state anchored at * `nowMs`; otherwise return `state` unchanged (referentially, so callers can * cheaply detect a roll). Read paths use this to report a rolled window's spend * as zero without mutating; the write path (`recordBudgetSpend`) rolls before * accruing so a new window starts clean. */ declare function rollBudgetPeriod(state: BudgetState, config: AgentBudgetConfig, nowMs: number): BudgetState; /** * Attribute an issue to a fleet for per-fleet accounting. Reads the first label * matching the configured prefix (default `fleet:`) and returns the suffix. * Returns `null` when no fleet label is present — the lane then counts only * against the global envelope. */ declare function fleetKeyForIssue(issue: Issue, config: AgentBudgetConfig): string | null; /** * Record `tokens` of spend against the global envelope and (when attributed) the * `fleetKey` sub-allocation. Rolls an elapsed window first, so spend always * accrues into the current period. Returns a NEW state (never mutates the * argument). Non-positive token counts are ignored. */ declare function recordBudgetSpend(state: BudgetState, config: AgentBudgetConfig, fleetKey: string | null, tokens: number, nowMs: number): BudgetState; /** * True once the GLOBAL envelope is spent. A breach here stops dispatch for * EVERY fleet at the next lane boundary — the clean, whole-run stop. */ declare function isGlobalEnvelopeExhausted(state: BudgetState, config: AgentBudgetConfig, nowMs: number): boolean; /** * True once `fleetKey`'s sub-allocation is spent while the global envelope may * still have room. A breach here skips only THIS fleet's lanes — sibling fleets * keep dispatching — so two fleets sharing an envelope respect their split under * contention. `null`/unconfigured fleet ⇒ never fleet-exhausted (bounded only by * the global envelope). */ declare function isFleetAllocationExhausted(state: BudgetState, config: AgentBudgetConfig, fleetKey: string | null, nowMs: number): boolean; /** * The governor's dispatch verdict: may a NEW lane for `fleetKey` be dispatched * without breaching the global envelope OR the fleet's sub-allocation? * * Read-only. Used where a single lane is redispatched (the retry path): a breach * at either level parks the lane. The tick dispatch loop instead calls the two * predicates above directly, because global vs. per-fleet exhaustion differ in * whether they stop the whole loop or skip one lane. */ declare function canAffordDispatch(state: BudgetState, config: AgentBudgetConfig, fleetKey: string | null, nowMs: number): boolean; /** * Operator-visible remaining-budget signal (#1525). Surfaced in the orchestrator * snapshot so an operator sees how much of the envelope is left. Reports an * elapsed window as a fresh (fully-remaining) period. */ declare function getBudgetStatus(state: BudgetState, config: AgentBudgetConfig, nowMs: number): BudgetEnvelopeStatus; /** * Run attempt lifecycle phases (internal to orchestrator). * Tracks the current phase of a single run attempt. */ type RunAttemptPhase = 'PreparingWorkspace' | 'BuildingPrompt' | 'LaunchingAgent' | 'InitializingSession' | 'StreamingTurn' | 'RateLimitSleeping' | 'Finishing' | 'Succeeded' | 'Failed' | 'TimedOut' | 'Stalled' | 'CanceledByReconciliation'; /** * Live session metadata tracked while an agent subprocess is running. */ interface LiveSession { sessionId: string; backendName: string; agentPid: number | null; startedAt: string; lastEvent: string | null; lastTimestamp: string | null; lastMessage: string | null; inputTokens: number; outputTokens: number; totalTokens: number; lastReportedInputTokens: number; lastReportedOutputTokens: number; lastReportedTotalTokens: number; turnCount: number; } /** * Entry in the running map: one active worker. */ interface RunningEntry { issueId: string; identifier: string; issue: Issue; attempt: number | null; workspacePath: string; startedAt: string; phase: RunAttemptPhase; session: LiveSession | null; /** * AMR Phase 4 (D10/SC16): the tier the AdaptiveRouter resolved this dispatch * at (`decision.tierRequired`), captured so a later quality outcome can feed * `AdaptiveRouter.recordOutcome(issue.id, lastRoutedTier, ok)`. Absent when * AMR is off (no policy) or the dispatch used an override. */ lastRoutedTier?: _harness_engineering_types.CapabilityTier; /** split-routing: the plan this unit is running staged, if any. Engine-owned. */ workflow?: WorkflowExecutionPlan; /** split-routing: the stage index currently executing. Engine-owned. */ currentStageIndex?: number; /** * split-routing: per-stage runs. The per-stage session/abort live HERE * (C1), never in the issue-level `session` field above. */ stageRuns?: StageRun[]; } /** * Entry in the retry queue. */ interface RetryEntry { issueId: string; identifier: string; attempt: number; dueAtMs: number; error: string | null; } /** * Token totals for observability. */ interface TokenTotals { inputTokens: number; outputTokens: number; totalTokens: number; cacheReadTokens: number; cacheCreationTokens: number; secondsRunning: number; } /** * Rate limit snapshot (populated by agent events). */ interface RateLimitSnapshot$1 { requestsRemaining: number | null; requestsLimit: number | null; tokensRemaining: number | null; tokensLimit: number | null; } /** * Single authoritative in-memory orchestrator state. */ interface OrchestratorState { pollIntervalMs: number; maxConcurrentAgents: number; globalCooldownUntilMs: number | null; recentRequestTimestamps: number[]; recentInputTokens: { timestamp: number; tokens: number; }[]; recentOutputTokens: { timestamp: number; tokens: number; }[]; globalCooldownMs: number; maxRequestsPerMinute: number; maxRequestsPerSecond: number; maxInputTokensPerMinute: number; maxOutputTokensPerMinute: number; running: Map; claimed: Set; retryAttempts: Map; /** * Tracks completed issue IDs mapped to the epoch-ms timestamp when the * completion was recorded. The timestamp enables a grace-period so that * issues manually re-activated in the roadmap can be re-dispatched, while * still guarding against duplicate dispatch on the tick immediately after * completion (when the tracker write-back may not have persisted yet). */ completed: Map; tokenTotals: TokenTotals; rateLimits: RateLimitSnapshot$1; /** Running count of claim rejections (another orchestrator won the race). */ claimRejections: number; /** * Unattended-dispatch spend envelope accumulator (#1525), or `null` when no * `agent.budget` is configured (governor off — unbounded dispatch). When * present, `canDispatch` refuses a NEW lane once the period envelope is spent. */ budget: BudgetState | null; } /** * A persisted record of a single intelligence pipeline analysis run. */ interface AnalysisRecord { /** Issue ID this analysis belongs to */ issueId: string; /** Issue identifier (human-readable) */ identifier: string; /** Enriched spec from SEL, or null if skipped */ spec: EnrichedSpec | null; /** Complexity score from CML, or null if skipped */ score: ComplexityScore | null; /** PESL simulation result, or null if not run */ simulation: SimulationResult | null; /** ISO timestamp when this analysis was recorded */ analyzedAt: string; /** External tracker ID (e.g., "github:owner/repo#42"), populated at analysis time. Null if no tracker configured. */ externalId: string | null; } /** * Persistent archive of intelligence pipeline analysis results. * Each analysis is stored as a separate JSON file keyed by issue ID. * Overwrites previous analysis for the same issue (latest result wins). */ declare class AnalysisArchive { private dir; constructor(dir: string); private safePath; /** * Write an analysis record to disk, overwriting any previous record for the same issue. */ save(record: AnalysisRecord): Promise; /** * Read the analysis record for a specific issue. */ get(issueId: string): Promise; /** * List all archived analysis records. */ list(): Promise; } interface StreamManifest { issueId: string; externalId: number | string | null; identifier: string; title?: string; attempts: AttemptRecord[]; pr: PRLink | null; retention: RetentionInfo; highlights: HighlightsInfo | null; } interface AttemptRecord { attempt: number; file: string; startedAt: string; endedAt: string | null; outcome: string | null; stats: AttemptStats; } interface AttemptStats { durationMs: number; inputTokens: number; outputTokens: number; turnCount: number; toolsCalled: string[]; filesTouched: string[]; } interface PRLink { number: number; linkedAt: string; status: string; } interface RetentionInfo { strategy: 'orphan' | 'pr-linked'; orphanExpiresAt: string | null; } interface Highlight { timestamp: string; summary: string; category: 'file_op' | 'test' | 'git' | 'error' | 'completion'; } interface HighlightsInfo { extractedAt: string; postedToPr: boolean; moments: Highlight[]; } interface RecorderLogger$1 { info(msg: string, meta?: Record): void; warn(msg: string, meta?: Record): void; error(msg: string, meta?: Record): void; } declare class StreamRecorder { private readonly streamsDir; private readonly logger; private readonly accumulators; constructor(streamsDir: string, logger: RecorderLogger$1); startRecording(issueId: string, externalId: number | string | null, identifier: string, backend: string, attempt: number, title?: string): void; recordEvent(issueId: string, attempt: number, event: AgentEvent): void; finishRecording(issueId: string, attempt: number, outcome: 'normal' | 'error', tokenStats: { inputTokens: number; outputTokens: number; turnCount: number; }): void; getManifest(issueId: string): StreamManifest | null; /** Lists all session manifests, sorted by most recent attempt start (newest first). */ listSessions(): StreamManifest[]; getStream(issueId: string, attempt?: number): string | null; linkPR(issueId: string, prNumber: number): void; updateHighlights(issueId: string, moments: Highlight[]): void; markHighlightsPosted(issueId: string): void; sweepExpired(openPrNumbers: number[]): void; private sweepIfExpired; private manifestPath; private streamPath; private readManifest; private writeManifest; private createManifest; private readStreamFile; } /** * Minimal logger interface for PR detection. * Accepts any structured logger that provides debug/info/warn. */ interface PRDetectorLogger { debug(message: string, meta?: Record): void; info(message: string, meta?: Record): void; warn(message: string, meta?: Record): void; } /** * Function signature compatible with Node's `child_process.execFile`. * Allows injection for testing. */ type ExecFileFn$1 = typeof execFile; /** * Detects whether GitHub issues or branches already have open pull requests. * * Uses the `gh` CLI under the hood. All checks are fail-open: if `gh` is not * installed, auth is missing, or the network is down, candidates pass through * rather than being incorrectly blocked. */ declare class PRDetector { private logger; private execFileFn; private projectRoot; constructor(opts: { logger: PRDetectorLogger; execFileFn?: ExecFileFn$1; projectRoot: string; }); /** * Parse a `github:owner/repo#N` externalId into its parts. * Returns null for invalid or non-GitHub formats. * * Delegates to the `@harness-engineering/core` authority rather than inlining * the regex, which is the rule `roadmap/external-id.ts` states about itself and * which this class was violating (#1857). The copy that lived here was the * verbatim PRE-#1843 pattern `/^github:([^/]+)\/([^#]+)#(\d+)$/`, so the * hardening landed by #1854 never reached this consumer and * `github:x/../../../user/emails?#1` still parsed to * `owner="x"`, `repo="../../../user/emails?"`. * * Kept as a thin instance method because it is part of this class's public * surface — `tests/orchestrator-pr-guard.test.ts` calls * `detector.parseExternalId(...)` directly. */ parseExternalId(externalId: string): { owner: string; repo: string; number: number; } | null; /** * Checks whether a remote branch has a pull request (any state) via `gh`. * Returns `{ found: true }` if a PR exists, `{ found: false }` if none, * or `{ found: false, error }` if the check itself failed (gh not installed, * network error, etc.). Callers should distinguish "no PR" from "check failed" * to avoid false escalations. */ branchHasPullRequest(branch: string): Promise<{ found: boolean; error?: string; }>; /** * Checks whether a GitHub issue (identified by externalId) has an open PR * linked to it via `closes #N` or similar keywords. Fail-open on API errors * or non-GitHub externalId formats. */ hasOpenPRForExternalId(externalId: string): Promise; /** * Checks whether an issue identifier has an open GitHub PR by searching * for a branch matching the `feat/` naming convention used * by dispatched agents. Fail-open on API errors. */ hasOpenPRForIdentifier(identifier: string): Promise; /** * GitHub closing keywords that link a PR to an issue it will close. * @see https://docs.github.com/articles/closing-issues-using-keywords */ private static readonly CLOSING_REF_RE; /** * Extracts the issue numbers a PR body declares it will close * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`). */ private parseClosingIssueNumbers; /** * Lists every open PR for a repo in a single `gh pr list` call and returns * the set of issue numbers those PRs close (parsed from their bodies). * * This is the batched replacement for per-issue `gh pr list --search * "closes #N"` queries. Every `gh pr list` form (search or plain list) is * served by GitHub's GraphQL API and consumes from the shared ~5000/hr * GraphQL budget, so issuing one query per candidate per tick exhausted the * limit on busy boards. A single `--state open` list returns every open PR * in one request regardless of candidate count, collapsing N calls into 1. * * Returns `null` if the check itself failed (gh missing, rate limited, * network error) so callers can fail open rather than block real work. */ fetchOpenPRClosures(owner: string, repo: string): Promise | null>; /** * Filters out candidates that already have an open GitHub PR. * * For GitHub-issue candidates the check is batched: one `gh pr list` per * distinct repo (not one search per issue), parsing closing references * locally. Candidates without a GitHub externalId fall back to a * `feat/` branch lookup, throttled to a few concurrent gh calls. * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work. */ filterCandidatesWithOpenPRs(candidates: Issue[]): Promise; } interface LogEntry { level: 'debug' | 'info' | 'warn' | 'error'; message: string; timestamp?: string; context?: Record; } declare class StructuredLogger { debug(message: string, context?: Record): void; info(message: string, context?: Record): void; warn(message: string, context?: Record): void; error(message: string, context?: Record): void; log(level: LogEntry['level'], message: string, context?: Record): void; private formatLog; private safeStringify; } /** * Shared context passed to extracted sub-services (IntelligencePipelineRunner, * CompletionHandler). Bundles the orchestrator's dependencies so sub-services * can read config, log, and access shared caches without importing the * Orchestrator class itself. */ interface OrchestratorContext { readonly config: WorkflowConfig; readonly projectRoot: string; readonly logger: StructuredLogger; readonly tracker: IssueTrackerClient; readonly recorder: StreamRecorder; readonly prDetector: PRDetector; readonly orchestratorIdPromise: Promise; readonly pipeline: IntelligencePipeline | null; readonly graphStore: GraphStore | null; readonly analysisArchive: AnalysisArchive; /** Mutable cache: enriched specs keyed by issue ID. */ readonly enrichedSpecsByIssue: Map; /** Mutable cache: epoch-ms when intelligence analysis last failed for an issue. */ readonly analysisFailureCache: Map; getState(): OrchestratorState; setState(state: OrchestratorState): void; emit(event: string, ...args: unknown[]): boolean; } /** * Discriminated union of events that drive the orchestrator state machine. * All events are data -- the caller constructs them from I/O results. */ type OrchestratorEvent = TickEvent | WorkerExitEvent | AgentUpdateEvent | RetryFiredEvent | StallDetectedEvent | ClaimRejectedEvent; interface TickEvent { type: 'tick'; candidates: Issue[]; runningStates: Map; /** Caller-supplied wall clock (ms since epoch). Keeps state machine pure. */ nowMs: number; /** Pre-computed concern signals from intelligence pipeline (issueId → signals) */ concernSignals?: Map; /** Pre-computed enriched specs from intelligence pipeline (issueId → spec) */ enrichedSpecs?: Map; /** Pre-computed complexity scores from intelligence pipeline (issueId → score) */ complexityScores?: Map; /** Pre-computed PESL simulation results from intelligence pipeline (issueId -> result) */ simulationResults?: Map; /** Pre-computed persona recommendations from specialization scorer (issueId -> recommendations) */ personaRecommendations?: Map; /** * SF5.2 (#1524) — pre-computed served-comprehension attribution per leaf * (issueId → the compact served units the orchestrator will pre-warm into the * leaf's prompt). Threaded into the LIVE per-leaf context-budget consult so a * leaf that would blow its budget on raw source is measured against the compact * served units it actually receives. Computed OUTSIDE the pure reducer (disk I/O) * exactly like `enrichedSpecs`/`complexityScores`. Absent ⇒ the consult falls * back to the floor-only estimate (byte-identical to the pre-SF5.2 behavior). */ prewarmSources?: Map; /** Identity of this orchestrator. Items assigned to a different value are * filtered out of dispatch by `selectCandidates`. Omit for back-compat * (preserves today's permissive behavior). */ selfAssignee?: string; } interface WorkerExitEvent { type: 'worker_exit'; issueId: string; reason: 'normal' | 'error'; error?: string | undefined; attempt: number | null; } interface AgentUpdateEvent { type: 'agent_update'; issueId: string; event: AgentEvent; } interface RetryFiredEvent { type: 'retry_fired'; issueId: string; candidates: Issue[]; /** Caller-supplied wall clock (ms since epoch). Keeps state machine pure. */ nowMs: number; /** Pre-computed concern signals from intelligence pipeline (issueId → signals) */ concernSignals?: Map; } interface StallDetectedEvent { type: 'stall_detected'; issueId: string; } interface ClaimRejectedEvent { type: 'claim_rejected'; issueId: string; } /** * Discriminated union of side effects returned by the state machine. * These are data describing what to do -- the orchestrator loop executes them. */ type SideEffect = DispatchEffect | StopEffect | ScheduleRetryEffect | ReleaseClaimEffect | CleanWorkspaceEffect | UpdateTokensEffect | EmitLogEffect | EscalateEffect | ClaimEffect; interface DispatchEffect { type: 'dispatch'; issue: Issue; attempt: number | null; /** Which backend to dispatch to. Defaults to 'primary' for backward compat. */ backend?: 'local' | 'primary'; } interface StopEffect { type: 'stop'; issueId: string; reason: string; } interface ScheduleRetryEffect { type: 'scheduleRetry'; issueId: string; identifier: string; attempt: number; delayMs: number; error: string | null; } interface ReleaseClaimEffect { type: 'releaseClaim'; issueId: string; } interface CleanWorkspaceEffect { type: 'cleanWorkspace'; issueId: string; identifier: string; } interface UpdateTokensEffect { type: 'updateTokens'; issueId: string; usage: TokenUsage; } interface EmitLogEffect { type: 'emitLog'; level: 'info' | 'warn' | 'error'; message: string; context?: Record; } interface EscalateEffect { type: 'escalate'; issueId: string; identifier: string; reasons: string[]; /** Issue title for context in the interaction queue */ issueTitle?: string; /** Issue description for context in the interaction queue */ issueDescription?: string | null; /** Enriched spec from intelligence pipeline, if available */ enrichedSpec?: EnrichedSpec; /** Complexity score from intelligence pipeline, if available */ complexityScore?: ComplexityScore; } interface ClaimEffect { type: 'claim'; issue: Issue; /** Which backend to dispatch to after a successful claim */ backend?: 'local' | 'primary'; /** Retry attempt number, if this is a retry dispatch */ attempt: number | null; /** Persona recommended by specialization scoring, if available */ suggestedPersona?: string; } /** * Calculate the retry delay in milliseconds, based on attempt number and retry type. * * Continuation retries: fixed 1000ms delay. * Failure retries: exponential backoff 10000 * 2^(attempt-1), capped at maxRetryBackoffMs. */ declare function calculateRetryDelayMs(attempt: number, type: 'continuation' | 'failure', maxRetryBackoffMs?: number): number; /** @deprecated Use `calculateRetryDelayMs` instead. */ declare const calculateRetryDelay: typeof calculateRetryDelayMs; /** * Sort candidates by dispatch priority (stable sort). * 1. priority ascending (1..4 preferred; null sorts last) * 2. createdAt oldest first (null sorts last) * 3. identifier lexicographic tie-breaker */ declare function sortCandidates(issues: readonly Issue[]): Issue[]; /** * Check if a single issue is dispatch-eligible. * State comparisons are case-insensitive. */ declare function isEligible(issue: Issue, state: OrchestratorState, activeStates: string[], terminalStates: string[], selfAssignee?: string | null): boolean; /** * Select and sort eligible candidates from a list of issues. */ declare function selectCandidates(issues: readonly Issue[], state: OrchestratorState, activeStates: string[], terminalStates: string[], selfAssignee?: string | null): Issue[]; /** * Budget-governor inputs threaded into {@link canDispatch} (#1525). Absent ⇒ the * governor is off and dispatch is bounded only by concurrency/rate limits (the * pre-#1525 behaviour). */ interface DispatchBudgetOptions { config: AgentBudgetConfig; /** Fleet the candidate lane belongs to, for per-fleet sub-allocation. */ fleetKey: string | null; nowMs: number; } /** * Get the number of available global concurrency slots. */ declare function getAvailableSlots(state: OrchestratorState): number; /** * Count running entries by normalized (lowercase) issue state. */ declare function getPerStateCount(running: ReadonlyMap): Map; /** * Check if dispatching an issue in the given state is allowed * by both global and per-state concurrency limits. */ declare function canDispatch(state: OrchestratorState, issueState: string, maxConcurrentAgentsByState: Record, budget?: DispatchBudgetOptions): boolean; /** * Estimate the in-memory context load (in tokens) a leaf will assemble for an * issue, deterministically and WITHOUT I/O. It sums the text the orchestrator * already holds for the issue — `title` + `description` — and converts to a token * floor at ~{@link CHARS_PER_TOKEN} chars/token. `spec` and `plans` are file * PATHS (not content), so they are not read here; the estimate is a deliberate * floor over the always-in-memory surface. A richer estimate (reading spec/plan * files or reusing an enriched-spec token count) is a follow-up slice. */ declare function estimateIssueContextTokens(issue: Issue): number; /** * The live budget consult. When `config.agent.contextBudget.maxTokens` is set, * estimate the issue-leaf's context load and delegate to the core * `assertLeafWithinBudget` primitive, which THROWS a `ContextBudgetExceededError` * (fail-loud) when the estimate exceeds the ceiling. When no budget is configured * (or `maxTokens` is not a positive number) this returns immediately — a no-op * that keeps dispatch byte-identical to the pre-budget behavior. * * The caller (the dispatch loop in `state-machine.ts`) catches the throw, emits a * loud error effect, and skips dispatching that leaf — so an over-budget leaf * fails visibly and never spends. */ declare function assertIssueWithinContextBudget(config: WorkflowConfig, issue: Issue, prewarm?: { sources: LeafContextSource[]; }): void; /** * Reconcile running issues against their current tracker states. * * For each running issue found in runningStates: * - Terminal state -> stop + cleanWorkspace + releaseClaim * - Neither active nor terminal -> stop + releaseClaim (no workspace cleanup) * - Still active -> no side-effect entries (keep running) * * Issues not found in runningStates are left running (state refresh may have * partially failed; retry next tick per spec). */ declare function reconcileRunningIssues(state: OrchestratorState, runningStates: ReadonlyMap, activeStates: string[], terminalStates: string[]): SideEffect[]; /** @deprecated Use `reconcileRunningIssues` instead. */ declare const reconcile: typeof reconcileRunningIssues; /** * Artifact presence metadata for scope tier detection. */ interface ArtifactPresence { hasSpec: boolean; hasPlans: boolean; } /** * Derive artifact presence from an Issue's spec and plans fields. */ declare function artifactPresenceFromIssue(issue: Issue): ArtifactPresence; /** * Detect the scope tier for an issue based on label overrides and artifact presence. * * Label override (e.g., `scope:quick-fix`) takes precedence. * Otherwise, infer from spec/plan presence: * - No spec, no plan -> full-exploration * - Spec or plan exists -> guided-change */ declare function detectScopeTier(issue: Issue, artifacts: ArtifactPresence): ScopeTier; /** * Pure routing function. Determines whether an issue should be dispatched * to the local backend, the primary backend, or escalated to needs-human. * * Routing rules (in order): * 1. If tier is in alwaysHuman -> needs-human * 2. If tier is in primaryExecute -> dispatch-primary * 3. If tier is in autoExecute -> dispatch-local * 4. If tier is in signalGated -> check concern signals * 5. Otherwise -> dispatch-local (safe default) */ declare function routeIssue(scopeTier: ScopeTier, concernSignals: ConcernSignal[], config: EscalationConfig): IssueRoutingDecision; /** * Candidate skills the orchestrator may dispatch to. Keep this set * small on purpose — the triage router is a coarse routing layer, not * a skill registry. */ type TriageSkill = 'code-review' | 'security-review' | 'planning' | 'debugging' | 'refactoring' | 'docs'; /** * Shape-of-work signals the triage router consumes. These are derived * from the Issue plus diff metadata by the caller. */ interface TriageSignals { /** Raw title/body prefix, e.g. "feat:", "fix:", "docs:", "security:" */ titlePrefix?: string; /** True if the diff touches auth, crypto, session, or similar paths */ touchesSecuritySensitivePaths?: boolean; /** True if the diff touches a migrations or schema directory */ touchesMigrationPaths?: boolean; /** Count of changed files in the diff */ changedFileCount?: number; /** Whether tests are failing on the issue branch */ hasFailingTests?: boolean; /** Whether the issue is marked or labeled as a rollback */ isRollback?: boolean; /** True if every changed file is `.md` */ isDocsOnly?: boolean; } /** * Structured decision the orchestrator persists and consumes. */ interface TriageDecision { /** Target skill for dispatch. */ skill: TriageSkill; /** Specific agent within the skill, optional. */ agent?: string; /** Router confidence in the decision. */ confidence: 'high' | 'medium' | 'low'; /** Human-readable justifications (one entry per rule that matched). */ reasons: string[]; } /** * Optional user/project overrides for path heuristics and rule thresholds. */ interface TriageConfig { /** Max changed files for a fix to still route to code-review (default 3). */ smallFixChangedFileMax?: number; } /** * Extract the conventional-commit prefix (lowercased) from a title. * Returns undefined if no prefix is present. */ declare function extractTitlePrefix(title: string | null | undefined): string | undefined; /** * A pending human interaction, typically from an escalation. */ interface PendingInteraction { /** Unique interaction ID */ id: string; /** ID of the related issue */ issueId: string; /** Interaction type */ type: 'needs-human'; /** Reasons for escalation */ reasons: string[]; /** Context for the human */ context: { issueTitle: string; issueDescription: string | null; specPath: string | null; planPath: string | null; relatedFiles: string[]; /** Enriched spec from intelligence pipeline, if available */ enrichedSpec?: { intent: string; summary: string; affectedSystems: unknown[]; unknowns: string[]; ambiguities: string[]; riskSignals: string[]; }; /** Complexity score from intelligence pipeline, if available */ complexityScore?: { overall: number; confidence: number; riskLevel: string; blastRadius: { services: number; modules: number; filesEstimated: number; testFilesAffected: number; }; dimensions: { structural: number; semantic: number; historical: number; }; reasoning: string[]; recommendedRoute: string; }; }; /** ISO timestamp of creation */ createdAt: string; /** Current status */ status: 'pending' | 'claimed' | 'resolved'; } /** * Persistent queue of pending human interactions. * Each interaction is stored as a separate JSON file in the configured directory. */ declare class InteractionQueue { private dir; private pushListeners; private emitter; /** * @param dir - Directory path for storing interaction JSON files * @param emitter - Optional event bus that receives `interaction.created` * and `interaction.resolved` events. When omitted, the queue behaves as * it did pre-Phase-2 (no emission). Phase 2 Task 8 wires the * orchestrator (itself an EventEmitter) in as the bus so the SSE * handler (`GET /api/v1/events`) can fan these out to clients. */ constructor(dir: string, emitter?: EventEmitter); /** * Register a listener that fires after each push. */ onPush(listener: (interaction: PendingInteraction) => void): void; /** * Push a new interaction to the queue. * If a pending interaction for the same issueId already exists, it is * replaced (upsert) so the directory never accumulates duplicates. */ push(interaction: PendingInteraction): Promise; /** * List all interactions (regardless of status). */ list(): Promise; /** * List only pending interactions. */ listPending(): Promise; /** * Update the status of an interaction. */ updateStatus(id: string, status: PendingInteraction['status']): Promise; } interface ApplyEventResult { nextState: OrchestratorState; effects: SideEffect[]; } declare function resolveEscalationConfig(config: WorkflowConfig): EscalationConfig; /** * Pure state machine transition function. * * Takes the current state, an event, and config. * Returns the next state and a list of side effects to execute. * No I/O is performed -- all side effects are returned as data. */ declare function applyEvent(state: OrchestratorState, event: OrchestratorEvent, config: WorkflowConfig): ApplyEventResult; /** * Create an empty OrchestratorState initialized from config. */ declare function createEmptyState(config: WorkflowConfig): OrchestratorState; /** * Computes the delay (in ms) needed before the next API request can be made, * based on a snapshot of recent request/token activity and rate-limit config. * * Returns 0 when no throttling is needed. */ interface RateLimitSnapshot { globalCooldownUntilMs: number | null; recentRequestTimestamps: number[]; recentInputTokens: { timestamp: number; tokens: number; }[]; recentOutputTokens: { timestamp: number; tokens: number; }[]; } interface RateLimitConfig { maxRequestsPerMinute: number; maxRequestsPerSecond: number; maxInputTokensPerMinute: number; maxOutputTokensPerMinute: number; } declare function computeRateLimitDelay(snapshot: RateLimitSnapshot, config: RateLimitConfig): number; /** * Renders an AnalysisRecord as a structured markdown comment. * Format: summary header + reasoning bullets + collapsible JSON with discriminator. * * Used by both the orchestrator auto-publish and the CLI publish-analyses command. */ declare function renderAnalysisComment(record: AnalysisRecord): string; interface PublishedIndex { [issueId: string]: string; } /** * Load the published analyses index from disk. * Returns an empty object if the file does not exist or is unparseable. */ declare function loadPublishedIndex(projectRoot: string): PublishedIndex; /** * Persist the published analyses index to disk. * Creates parent directories if they do not exist. */ declare function savePublishedIndex(projectRoot: string, index: PublishedIndex): void; /** * Resolves the orchestrator identity. Uses the explicit configId if * provided; otherwise reads or creates a persisted machine UUID at * ~/.harness/orchestrator-id and derives a stable hash. * * Format: `orchestrator-{first8charsOfSha256(uuid)}` * Example: `orchestrator-a7f3b2c1` */ declare function resolveOrchestratorId(configId?: string): Promise; /** Exposed for testing only -- returns the path to the identity file. */ declare const ORCHESTRATOR_IDENTITY_FILE: string; interface ClaimManagerConfig { /** Delay in ms between claim write and verification read. Default: 2000 */ verifyDelayMs?: number; /** State name used for claimed/in-progress issues. Default: 'in-progress' */ claimedState?: string; } /** * Manages claim coordination for multi-orchestrator dispatch. * * Uses the tracker as the shared state layer. Claims are optimistic: * write then verify after a delay to detect races. */ declare class ClaimManager { private tracker; private orchestratorId; private verifyDelayMs; private claimedState; constructor(tracker: IssueTrackerClient, orchestratorId: string, config?: ClaimManagerConfig); /** * Optimistically claims an issue then verifies ownership after a delay. * * Returns 'claimed' if the assignee matches this orchestrator after * the verify delay, 'rejected' if another orchestrator won the race. */ claimAndVerify(issueId: string): Promise>; /** * Releases a claimed issue back to the available pool. */ release(issueId: string): Promise>; /** * Refreshes claim timestamps for all running issues in parallel. * Failures are non-fatal — individual heartbeat failures are swallowed * so one failing claim does not block others. */ heartbeat(issueIds: string[]): Promise; /** * Checks whether an issue's claim is stale based on its updatedAt * timestamp and the configured TTL. * * Returns true if the claim should be considered expired (the * owning orchestrator may have crashed). */ isStale(issue: Issue$1, ttlMs: number): boolean; /** * Scans the tracker for "in-progress" issues assigned to this orchestrator * and releases any that are not currently running in memory. * * Called once during orchestrator startup to clean up orphaned claims * from a previous crash or unclean shutdown. * * @param runningIssueIds - Set of issue IDs currently in the running map * @returns List of issue IDs that were released */ reconcileOnStartup(runningIssueIds: ReadonlySet): Promise>; } /** * Orchestrator flight recorder ("black-box"). * * A first-class, always-on forensic record of a single orchestrator RUN (one * process lifetime). The per-issue {@link StreamRecorder} already captures what * each dispatch DID (attempts, tools, tokens, outcome); this ties a whole run * together with the two things a stream can't answer on its own: * * 1. PROVENANCE — exactly WHICH code/config produced the run (git HEAD, node, * harness version, the resolved backends + routing). Without it, a verdict * ("shipped" / "looped" / "needs-human") is unfalsifiable: you can't tell a * real convergence from one that ran stale code or an off-goal config. * 2. VERDICTS — the terminal disposition of each unit (shipped / needs-human / * gate-blocked) WITH the gate reason, which otherwise lives only in the * process's stdout log and in-memory retry state. * * Written durably to `/../black-box//run.json` so any past * run can be read back and analyzed later (see the `orchestrator black-box` CLI). * * DESIGN: every method is best-effort and NEVER throws — a recorder failure must * not break a dispatch. Reads are static so a CLI can inspect runs without * constructing an Orchestrator. */ interface ProvenanceBackend { name: string; type: string; endpoint?: string; model?: string; } interface RunProvenance { gitHead: string | null; gitSubject: string | null; branch: string | null; harnessVersion: string | null; node: string; backends: ProvenanceBackend[]; routing: { default?: string; modes?: Record; }; } type Verdict = 'shipped' | 'needs-human' | 'gate-blocked' | 'error' | 'in-progress'; interface UnitVerdict { issueId: string; identifier: string; verdict: Verdict; attempt: number | null; /** Truncated gate/verify reason when the disposition is a block or escalation. */ gateReason?: string; /** Number of times the enforced gate blocked this unit across the run. */ gateBlocks: number; pr?: number; backend?: string; updatedAt: string; } interface RunRecord { runId: string; orchestratorId: string | null; startedAt: string; endedAt: string | null; provenance: RunProvenance; /** Keyed by issueId; last-write-wins on verdict, gateBlocks accumulates. */ units: Record; } interface RecorderLogger { info(msg: string, meta?: Record): void; warn(msg: string, meta?: Record): void; } /** * Best-effort provenance from the resolved config + the git working tree. Git is * optional (adopters may run outside a repo) — every probe degrades to null. * `execFile` is injectable so tests never shell out. */ declare function gatherProvenance(config: { agent?: { backends?: Record; routing?: { default?: unknown; modes?: Record; }; }; }, opts?: { cwd?: string; harnessVersion?: string | null; execFile?: (cmd: string, args: string[], cwd: string) => string; }): RunProvenance; declare class FlightRecorder { private readonly runId; private readonly logger; private readonly runDir; private record; constructor(baseDir: string, runId: string, logger: RecorderLogger); /** Begin a run: pin provenance and write the initial record. Idempotent. */ startRun(orchestratorId: string | null, provenance: RunProvenance): void; /** * Upsert a unit's disposition. `verdict:'gate-blocked'` increments the block * counter (the retry loop calls this once per block); terminal verdicts * (shipped/needs-human) overwrite it while preserving the accumulated count. */ recordVerdict(v: { issueId: string; identifier: string; verdict: Verdict; attempt?: number | null; gateReason?: string; pr?: number; backend?: string; }): void; /** Stamp the run's end. Safe to call more than once. */ finishRun(): void; private flush; static getRun(baseDir: string, runId: string): RunRecord | null; /** All runs, newest-started first. */ static listRuns(baseDir: string): RunRecord[]; } /** * Scan JSONL content for high-signal events and return the top 5 diverse moments. */ declare function extractHighlights(jsonlContent: string): Highlight[]; /** * Render a PR comment with execution metrics table and collapsible key moments. */ declare function renderPRComment(stats: AttemptStats, highlights: Highlight[], orchestratorId: string): string; declare class WorkflowLoader { loadWorkflow(filePath: string): Promise>; } /** * Cross-field check: every value in `routing` must reference a key in * `backends`. Mirrors the Phase 1 standalone helper but returns a flat * array of issues for synchronous consumption inside * `validateWorkflowConfig` (which is hand-rolled, not a Zod parse). * * Exported for unit testing. Production callers should prefer * `validateWorkflowConfig` (which wraps this helper with the surrounding * legacy-vs-modern branching). */ declare function crossFieldRoutingIssues(backends: Record, routing: RoutingConfig): Array<{ path: string[]; message: string; }>; /** * Spec B Phase 2 / S3: produce non-blocking warnings for misconfigured * routing entries that are SYNTACTICALLY valid (the cross-field check * has passed) but SEMANTICALLY suspicious: * * - `routing.skills.` where `` is not in the local skill * catalog. Likely a typo or a skill that was renamed / removed. * * - `routing.modes.` where `` is not in the * STANDARD_COGNITIVE_MODES tuple. Since `CognitiveMode` allows the * `(string & {})` escape hatch, the type system accepts custom modes * — but operators are far more likely to typo a standard mode than * introduce a custom one, so we warn. * * Returns an empty array when `knownSkillNames` is empty (i.e., the * catalog could not be discovered — most likely because `agents/skills/` * is absent). Skipping is preferable to flooding the operator with * false positives when the catalog itself is missing. * * Warnings are advisory; the loader continues to return `Ok` and the * orchestrator starts normally. */ declare function routingWarnings(routing: RoutingConfig, knownSkillNames: readonly string[]): string[]; interface ValidateWorkflowConfigOptions { /** * Known skill names from the local catalog. When non-empty, used to * warn (S3) on `routing.skills.` references that are not in * the catalog. When empty, skill-name warnings are suppressed — the * caller is presumed to be running without a discoverable catalog * (e.g., tests, or orchestrator outside a harness project root). */ knownSkillNames?: readonly string[]; } interface ValidatedWorkflowConfig { config: WorkflowConfig; /** * Non-blocking warnings produced during validation. Currently * includes (Spec B Phase 2 / S3): * - `routing.skills.` not in the local catalog * - `routing.modes.` not in `STANDARD_COGNITIVE_MODES` */ warnings: readonly string[]; } declare function validateWorkflowConfig(config: unknown, options?: ValidateWorkflowConfigOptions): Result; declare function getDefaultConfig(): WorkflowConfig; /** * Spec B Phase 3: an entry in the local skill catalog. * * Carries the skill's catalog `name` AND optional `cognitive_mode` * declaration from `skill.yaml`. Consumed by the orchestrator dispatch * site to construct `{ kind: 'skill', skillName, cognitiveMode }` * RoutingUseCases so per-skill / per-mode routing fires at dispatch. */ interface SkillCatalogEntry { readonly name: string; readonly cognitiveMode?: string; } /** * Spec B Phase 3: read the local skill catalog at orchestrator startup, * returning each declared skill's `name` AND optional `cognitive_mode`. * * Reads from EVERY host subdirectory under `agents/skills/` (claude-code, * cursor, gemini, etc.). Names are deduplicated across hosts — first * occurrence wins (matches Phase 2 behavior for `discoverSkillCatalogNames`). * * Returns an empty array when `agents/skills/` is absent (orchestrator * running outside a harness project root). In that case dispatch-site * routing falls through to per-tier resolution, preserving today's * behavior (F11/N2). * * Errors reading individual skill.yaml files (malformed YAML, missing * `name` field, IO errors) are swallowed silently. The catalog is * advisory; a single broken skill.yaml should not block dispatch. */ declare function discoverSkillCatalog(projectRoot: string): SkillCatalogEntry[]; /** * Spec B Phase 2: read the local skill catalog at orchestrator startup * for warning-level routing validation (`routing.skills.` where * `` is not in the catalog). * * Spec B Phase 3: thin alias over {@link discoverSkillCatalog} — name * extraction preserved for the Phase 2 WorkflowLoader → validation * pipeline (no behavioral change for Phase 2 callers). */ declare function discoverSkillCatalogNames(projectRoot: string): string[]; /** * split-routing D5/D12/D13 — the result of the doubly-opt-in match. * * `workflowFor` is the SINGLE matching authority: it returns BOTH the execution * `plan` AND the matched decl's optional `stageDeadlineMs` (D12) so `dispatchIssue` * never re-runs the prefix+labels+`>= 2` predicate to recover the deadline. One * match, one source of truth — no drift between the predicate and a second matcher. */ interface WorkflowMatch { /** The execution plan for the matched >= 2-stage decl. */ plan: WorkflowExecutionPlan; /** The matched decl's per-stage deadline override (D12), if declared. */ stageDeadlineMs?: number; /** * staged-verify-gate-convergence D2 — the matched decl's optional acceptance * command, surfaced HERE so the settle gate reads it from the SAME match * authority `dispatchIssue` used (identical prefix+labels+`>= 2` semantics, no * drift from a second matcher). Absent ⇒ the settle gate uses `verifyRunner`. */ acceptance?: string; } /** * split-routing D5/D13 — the doubly-opt-in gate as a PURE matcher. * * Selects a `WorkflowMatch` for `issue` iff `config.workflows` declares a * matching decl with `stages.length >= 2`. Match grain (v1): an * `identifierPrefix` and/or `labels`; both, when declared, must hold. The FIRST * matching decl wins. The returned `WorkflowMatch` carries the matched decl's * `stageDeadlineMs` (D12) so the caller reads it from HERE rather than * re-matching — this function is the single match authority. * * D13: a matching 1-stage decl returns `undefined` — the unit takes the * single-agent path (a 1-stage "workflow" is not staged). A 0-stage decl never * reaches here (rejected at config validation). * * SC4 — this matcher has ZERO side effects: it never touches the workspace, * claims, routes, logs, or mutates state. When it returns `undefined` * `dispatchIssue` falls through to the byte-identical single-agent path. Keep it * cheap and pure so calling it on every dispatch cannot change non-workflow * behavior. */ declare function workflowFor(issue: Issue, config: WorkflowConfig): WorkflowMatch | undefined; /** * Governance record handed to a {@link ClaudeBackendOptions.policyAudit} sink at * spawn time. The orchestrator wires this to `.harness/audit.log`. NO payload, * NO env values — `strippedEnvKeys` carries names only. */ interface PolicyAuditRecord { sessionId: string; workspacePath?: string; policy: PolicyMetadata; strippedEnvKeys: string[]; enforced: boolean; } type PolicyAuditSink = (record: PolicyAuditRecord) => void; interface RoutingDecisionBusFilter { skillName?: string; mode?: string; backendName?: string; limit?: number; } interface RoutingDecisionBusOptions { /** Default 500. Bound on the in-memory ring buffer. */ capacity?: number; /** * Logger for the structured `routing-decision` line (O1) and for * one-off warn() when a subscriber throws (S6). When omitted, the * bus silently swallows subscriber errors (test-mode default). */ logger?: StructuredLogger; } /** * Spec B Phase 4 (D8): in-process bus + ring buffer for * {@link RoutingDecision} events. One emit() per * {@link BackendRouter.resolve} call; subscribers receive the * decision synchronously after the ring buffer is updated. * * Subscriber errors are isolated (caught + logged, never thrown * back to the emitter) so a misbehaving subscriber cannot block a * dispatch. (S6) * * Capacity-bound (default 500) via Array.shift() — acceptable for * v1 (see plan C4); switch to circular indexing if 24h dispatch * volume ever pushes 10K+ records/min. */ declare class RoutingDecisionBus { private readonly ringBuffer; private readonly listeners; private readonly capacity; private readonly logger; constructor(opts?: RoutingDecisionBusOptions); emit(decision: RoutingDecision): void; recent(filter?: RoutingDecisionBusFilter): RoutingDecision[]; subscribe(listener: (d: RoutingDecision) => void): () => void; /** * Spec B Phase 5 (review-S2 fix): release all subscriber references so * teardown can complete without anchoring closures. Called from * `Orchestrator.stop()` before nulling the bus reference. The bus * remains usable after clear — `subscribe()` works as normal. */ clearListeners(): void; } interface BackendRouterOptions { backends: Record; routing: RoutingConfig; /** * Spec B Phase 4 (D8): when present, every resolve() emits its * decision onto the bus. The bus owns the structured log line + ring * buffer; the router stays a pure resolution function. */ decisionBus?: RoutingDecisionBus; } /** * BackendRouter (Spec B Phase 1) * * Owns the lookup from a {@link RoutingUseCase} (a discriminated query * — tier, intelligence layer, maintenance, chat, isolation, **skill**, * **mode**) to a {@link RoutingDecision} naming a chosen backend and * the full resolution path that produced it. * * Resolution order (D2): invocation override -> per-skill -> per-mode * -> existing per-tier/intelligence/isolation/maintenance/chat -> * `routing.default`. Within each source, fallback chain entries are * tried in declared order; first existing backend wins. Unknown * entries are recorded with `outcome: 'unknown-backend'` and the walk * continues. * * Construction-time validation guarantees every name referenced by * `routing` is present in `backends` so the static-config case can * never produce a runtime exhaustion throw. The runtime throw at the * end of `resolve()` is a safety net for future dynamic-backends * scenarios where a chain entry can become unknown post-construction. */ declare class BackendRouter { private readonly backends; private readonly routing; private readonly decisionBus; constructor(opts: BackendRouterOptions); /** * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}. * * @param useCase the routing query * @param opts.invocationOverride if set and the named backend exists, * beats all other sources (D7 — the `--backend ` escape hatch) */ resolve(useCase: RoutingUseCase, opts?: { invocationOverride?: string; }): RoutingDecision; /** * True iff `useCase` is a BARE SKILL stage — `kind:'skill'` with NO * `cognitiveMode` and NO `routing.skills[skillName]` binding — so `resolve()` * (absent an `invocationOverride`) would fall through to step 5 `routing.default`. * * The AMR path uses this to decide whether `selectCheapestQualifying`'s * tier-pick may legitimately override the per-phase split. A bare skill * execution stage MUST land on `routing.default` (the per-phase execution * backend), so AMR must NOT hand it an `invocationOverride` (that would * short-circuit resolve step 1 and mis-route it to the tier-cheapest backend — * the #876 execution-stage mis-route). * * Deliberately SCOPED to skill use cases: a `kind:'tier'` / `'intelligence'` * useCase with no explicit routing binding ALSO falls to default in `resolve()`, * but for those AMR tier selection is the INTENDED override (Task 5's * cheapest-qualifying selection), so they return `false` here. A design stage * (`cognitiveMode` bound in `routing.modes`) or a per-skill-overridden stage also * returns `false`, so AMR/#876 per-mode routing stays intact. * * Read-only over the router's own routing config (no mutation, no resolution). * * Keys on the PRESENCE of a `routing.skills`/`routing.modes` binding, whereas * `resolve()` additionally requires the referenced backend to EXIST. This is * equivalent under the standing invariant that `validateReferences` has run * (bindings resolve to real backends) — the only way stage requests reach here. */ wouldFallToDefault(useCase: RoutingUseCase): boolean; /** * Returns the {@link BackendDef} reference for the resolved name. * Identity-equal to the entry in `backends` (no copy) so callers * relying on reference equality (SC21) continue to work. */ resolveDefinition(useCase: RoutingUseCase, opts?: { invocationOverride?: string; }): BackendDef; /** * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup * for callers that need both. Replaces the previous pattern of * `resolveDefinition(useCase) + resolve(useCase)` which produced two * RoutingDecision emissions per dispatch — doubling routing-decision * log volume now that Phase 4 emits. * * Identity-equal `BackendDef` (no copy) so callers relying on * reference equality (SC21) continue to work. */ resolveDecisionAndDef(useCase: RoutingUseCase, opts?: { invocationOverride?: string; }): { decision: RoutingDecision; def: BackendDef; }; /** * The pre-Spec-B resolution helper: returns the configured * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat * use cases (or `undefined` for skill/mode use cases, which are owned * by the per-skill / per-mode steps in {@link resolve}). Returning * `undefined` lets the caller fall through to `routing.default`. */ private resolveExistingUseCase; private validateReferences; } /** * Options for `OrchestratorBackendFactory`. * * `sandboxPolicy` and `container`/`secrets` mirror the orchestrator's own * agent-config fields. `getResolverModelFor` is a registration hook the * orchestrator calls to bind each `local`/`pi` backend to its * `LocalModelResolver` (so multi-resolver array-fallback works without * leaking resolver lifetimes into the factory). */ /** * Runtime-feedback callbacks a `local`/`pi` backend fires as turns complete. * See {@link OrchestratorBackendFactoryOptions.getModelUsageHooksFor}. */ interface LocalModelUsageHooks { /** Fired with the resolved model after a successful turn (LRU + breaker clear). */ onModelUsed?: (model: string) => void; /** Fired with the resolved model after a failed turn (breaker increment). */ onModelFailed?: (model: string) => void; } interface OrchestratorBackendFactoryOptions { backends: Record; routing: RoutingConfig; sandboxPolicy: 'none' | 'docker'; container?: ContainerConfig; secrets?: SecretConfig; /** * Hook for resolver injection. Invoked per `local`/`pi` backend at * `forUseCase()` time with the backend's name. When the hook returns a * function, the factory rebuilds the local/pi instance using that * function as `getModel` (overriding the head-of-array placeholder * baked into `createBackend`). Returning `undefined` means "no * resolver registered for this name" — the placeholder stays in place. * * This indirection keeps the factory ignorant of `LocalModelResolver`'s * existence and lifecycle while still letting it produce backends that * route through the resolver Map. */ getResolverModelFor?: (backendName: string, useCase: RoutingUseCase) => (() => string | null) | undefined; /** * Consumption Phase 3 (T11): per-`local`/`pi` backend hook returning the * runtime-feedback callbacks bound to that backend's resolver + pool. The * factory forwards them to the constructed `LocalBackend` so a completed turn * stamps `lastUsedAt` (LRU) + clears the circuit breaker, and a failed turn * feeds the breaker. Returning `undefined` means "no feedback wiring" (the * backend runs exactly as before). Kept parallel to `getResolverModelFor` so * the factory stays ignorant of resolver/pool lifecycles. */ getModelUsageHooksFor?: (backendName: string) => LocalModelUsageHooks | undefined; /** * Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends. * Other backends accept-but-ignore. Shared across dispatches so the * `/api/v1/telemetry/cache/stats` endpoint sees the full rolling window. */ cacheMetrics?: CacheMetricsRecorder; /** * Spec B Phase 4 (D8): forwarded to the underlying BackendRouter so * every resolve() during forUseCase / resolveName emits. */ decisionBus?: RoutingDecisionBus; /** * Orchestrator gateway policy envelope + subprocess air-gap. `policyAudit` is * the governance sink each subprocess-spawning backend calls once per spawn * (wired to `.harness/audit.log`). `sandboxMode` is DERIVED per dispatch from * `sandboxPolicy` (docker wrap ⇒ `docker`), so it is not accepted here. * `networkMode` + `subprocessEnvAllow` are forwarded verbatim. */ policyAudit?: PolicyAuditSink; networkMode?: PolicyNetworkMode; subprocessEnvAllow?: readonly string[]; } /** * High-level factory wrapping `BackendRouter` + `createBackend` plus * orchestrator-side concerns (sandbox wrapping, resolver binding). * * Spec 2 SC22-SC25: every `forUseCase(useCase)` call returns a fresh * `AgentBackend` whose class matches the routed `BackendDef.type`. * `local`/`pi` defs are bound to their per-name resolver before being * returned, and the result is wrapped in `ContainerBackend` when * sandboxPolicy is 'docker'. */ declare class OrchestratorBackendFactory { private readonly router; private readonly opts; constructor(opts: OrchestratorBackendFactoryOptions); /** * Resolve `useCase` to a backend name, materialize a fresh * `AgentBackend`, optionally rebind its model resolver, and apply * sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner * holds the per-dispatch reference and discards it when the run ends. */ /** * Resolve `useCase` to its routed backend name, exposing the * router lookup without materializing a backend. Used by callers * (e.g., the orchestrator's dispatch site) that need to label * telemetry with the routed name BEFORE constructing the backend. * * Spec 2 P2-I2: previously the orchestrator labelled `LiveSession` * + `StreamRecorder` with the legacy `agent.backend` field, which * is `undefined` for pure-modern configs. Threading the routed name * through dispatch eliminates that gap. */ resolveName(useCase: RoutingUseCase, opts?: { invocationOverride?: string; }): string; /** * Spec B Phase 1: expose the underlying router for callers that need * it directly (e.g., {@link buildIntelligencePipeline} for the * I1 SEL/PESL comparison fix). Read-only access; consumers must not * mutate router state. */ getRouter(): BackendRouter; /** * Assemble the per-dispatch {@link CreateBackendOptions}: the shared cache * recorder plus the orchestrator gateway policy-envelope / subprocess air-gap * wiring. `sandboxMode` is DERIVED from the dispatch's sandbox policy (a docker * wrap ⇒ the subprocess runs container-isolated); the rest is forwarded * verbatim so subprocess-spawning backends can stamp the audit trail + enforce * the env allowlist. Extracted from `forUseCase` to keep that method flat. */ private buildCreateOpts; forUseCase(useCase: RoutingUseCase, opts?: { invocationOverride?: string; }): AgentBackend; /** * Rebuild a `local`/`pi` backend with a resolver-bound `getModel`, * mirroring `createBackend`'s local/pi branches but substituting the * head-of-array placeholder with the orchestrator-owned resolver. */ private buildLocalLikeWithResolver; /** * Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret * backend per call so each dispatch sees a fresh container handle map * (ContainerBackend keeps its own per-instance Map). */ private wrapInContainer; } /** * Narrow surface the stage-execution engine needs. The Orchestrator will * implement this in Phase 4 when dispatchIssue wires the branch; the engine * must NOT import orchestrator.ts (layer cycle). Phase 1 tests inject a fake. */ interface WorkflowEngineContext { recorder: StreamRecorder; logger: StructuredLogger; /** issueId of the coherence unit — used for recorder keys + terminal transitions. */ issueId: string; identifier: string; externalId: string | null; workspacePath: string; /** * D12 (SC7): per-stage wall-clock deadline in ms. If a stage does not finish * within it, the engine fires the per-stage abort (running the runner's * `finally { stopSession }` via `gen.return()`) and treats the stage as * `passed:false` → feeds the D8 retry/terminal path (never an unbounded hang). * Absent ⇒ the engine default (`DEFAULT_STAGE_DEADLINE_MS`). Per-stage/config * override is Phase 4. */ stageDeadlineMs?: number; /** Build a runner for a stage's routed backend. Phase 1: single stubbed backend. */ makeRunner(backend: AgentBackend): { runSession: (issue: unknown, ws: string, prompt: string, systemPrompt?: string) => AsyncGenerator; }; /** Phase 1 stub: resolve the single backend for a stage (Phase 2 replaces with route()). */ resolveStageBackend(step: WorkflowExecutionPlan['stages'][number]): AgentBackend; /** * Per-phase routing (identity path): synthesize THIS stage's RoutingDecision when * it was resolved via the identity fallback (no adaptiveRouter). The adaptive path * attaches `run.decision` from the router; the identity path had none, leaving * `run.decision` unset. That silently broke every downstream consumer of the last * stage's decision — critically `settleWorkflowSuccess`'s `isLocal` gate, which * derives locality from `runs[last].decision?.backendName`: with no decision the * gate+ship block is skipped, so a fully-local (no-`routing.policy`, i.e. AMR-off, * i.e. the DEFAULT) staged unit completes but never ships and loops via * `in_review` re-dispatch. This fills the same slot from the resolved backend's * name + type. ABSENT (fake/legacy contexts) ⇒ decision stays unset (byte-identical). */ stageDecisionFor?(step: WorkflowExecutionPlan['stages'][number]): RoutingDecision | undefined; /** * split-routing 4b: render the REAL per-stage prompt (issue + stage role + * prior-stage outputs), replacing the Phase-1 bare skill-name stub. Bound by * `buildWorkflowContext` via a `PromptRenderer` (never imports orchestrator). * `priorOutputs` maps each prior stage's `produces` label → its captured output. * Optional so fake contexts that don't exercise prompt content fall back to the * skill name (byte-identical to the old stub). * * CONTRACT: must be non-blocking (CPU-bound). It runs BEFORE the per-stage * deadline timer is armed, so an I/O-bound renderer that hangs would stall the * stage with no wall-clock bound. The shipped renderer is synchronous LiquidJS. * * `isLocalBackend` (per-phase routing) is the routed backend's locality: when * `true` the renderer selects the LOCAL-indirection stage template * (`harness skill run --autonomous`); when `false` OR OMITTED it renders * the byte-identical default `STAGE_PROMPT_TEMPLATE` (SC3 — a fake/legacy * context that passes no locality behaves exactly as before). */ renderStagePrompt?(step: WorkflowExecutionPlan['stages'][number], index: number, priorOutputs: Record, isLocalBackend?: boolean): string | Promise; /** * Per-phase routing: resolve whether a routed `AgentBackend` is a local-endpoint * backend (`local`/`pi`/`ollama` via `isLocalEndpointBackend`). The routed path * only has a name-only `AgentBackend`, so it cannot call `isLocalEndpointBackend` * (that needs the `BackendDef`); this resolver maps the name → its def in the * real context. ABSENT (fake/legacy contexts) ⇒ treated as non-local ⇒ default * template (SC3, byte-identical). */ isLocalBackend?(backend: AgentBackend): boolean; /** * split-routing Phase 2: per-stage adaptive router. Present ⇒ each stage is * routed via `route(buildStageRequest(...))`; ABSENT ⇒ identity fallback via * `resolveStageBackend` (no `routing.policy`). Narrow surface only (route + * recordOutcome) so the engine stays off the orchestrator import cycle. * * `route()` returns `{ decision }` only — the engine derives the backend name * from `decision.backendName` (a `BackendDef` carries no `name`; the name is * the router map key), so `def` is deliberately not part of this surface. */ adaptiveRouter?: { route(req: RoutingRequest): Promise<{ decision: RoutingDecision; }>; recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void; }; /** Terminal success — exactly one running/claimed delete + one lane persist (D6). */ emitWorkflowSuccess(unit: string, runs: StageRun[]): Promise; /** Terminal failure/safety-net — exactly one running/claimed delete + one lane persist (D6/I1). */ finalizeWorkflowTerminal(unit: string, runs: StageRun[], failingStep?: WorkflowExecutionPlan['stages'][number], err?: unknown): Promise; /** * Resume-from-failed-stage checkpoint (survives gate-block re-dispatches, on the * orchestrator not this per-dispatch ctx). `loadStageCheckpoint` returns the map of * previously-completed `checkpoint: true` stages (stageIndex → its StageRun) for the * unit; `saveStageCheckpoint` persists one. Absent seams (fake/legacy ctx) ⇒ no reuse * (byte-identical prior behavior). Cleared by the orchestrator on a terminal. */ loadStageCheckpoint?(unit: string): ReadonlyMap | undefined; saveStageCheckpoint?(unit: string, stageIndex: number, run: StageRun): void; /** * Persist a passing DOCUMENT stage's output (spec/plan → `docs/changes//…`) to * disk in the workspace. The stage prompt tells the model to write the artifact, but a * local reasoner often reasons WITHOUT writing the file — so the design phase runs yet * no proposal/plan lands, hollowing the lifecycle. This seam writes the captured stage * output to its `documentPath` when the model didn't, guaranteeing the artifact exists. * Best-effort; a no-op for non-document stages, empty output, or an already-written doc. * Absent (fake/legacy ctx) ⇒ no persistence (byte-identical prior behavior). */ persistStageDocument?(step: WorkflowStep, run: StageRun): Promise; } /** * split-routing Phase 4 — the narrow adaptive-router surface the workflow engine * consumes (`route` + `recordOutcome`). Structurally identical to * `WorkflowEngineContext.adaptiveRouter` and to the real `AdaptiveRouter`'s public * methods; typed here so the dep bag never depends on the concrete router class * (and never on `orchestrator.ts`). */ interface WorkflowRouterDep { route(req: RoutingRequest): Promise<{ decision: RoutingDecision; }>; recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void; } /** * The dependency bag `buildWorkflowContext` composes the real * `WorkflowEngineContext` from. It is passed IN by `dispatchIssue` (Task 8) rather * than read off `this`, so this module imports only same-layer siblings * (`agent/runner`, `agent/orchestrator-backend-factory` types, `core/stream-recorder`) * and NEVER `orchestrator.ts` — the layer-cycle guard the context surface exists * for (concern #3). The two terminal seams are supplied as `settleSuccess` / * `settleTerminal` callbacks the orchestrator binds to its private methods (Task 7). */ interface BuildWorkflowContextDeps { recorder: StreamRecorder; logger: StructuredLogger; /** The dispatch unit; issueId/identifier/externalId flow from here. */ issue: Issue; /** The unit's ONE worktree (from the single `ensureWorkspace` in dispatchIssue). */ workspacePath: string; /** `this.config.agent.maxTurns` — the per-stage runner turn cap. */ maxTurns: number; /** * The orchestrator's backend factory. Used to materialize the real routed * backend for a stage: `forUseCase(useCase, { invocationOverride: name })` * mirrors the AMR dispatch swap (orchestrator.ts:2062-2064). `null` when * migration failed (legacy-only config) — then the identity fallback uses the * name-only `routingDefault` backend. */ backendFactory: OrchestratorBackendFactory | null; /** * The real `AdaptiveRouter` (narrowed) when `routing.policy` is set * (orchestrator.ts:698), else `null` ⇒ the engine takes the identity fallback * (D5). Only `route`/`recordOutcome` are consumed. */ adaptiveRouter: WorkflowRouterDep | null; /** `this.config.agent.routing?.default` — the identity-fallback backend name. */ routingDefault: string | undefined; /** * Per-phase routing: the config's backend-name → `BackendDef` map * (`this.config.agent.backends`). Used ONLY to resolve a routed backend's * locality (`isLocalEndpointBackend`) so a local-endpoint stage renders the * `harness skill run --autonomous` indirection template. ABSENT (fake/legacy * contexts) ⇒ every stage is treated as non-local ⇒ default template (SC3). */ backends?: Record; /** D12 override; absent ⇒ engine default DEFAULT_STAGE_DEADLINE_MS. */ stageDeadlineMs?: number; /** * `this.config.agent.retrievalMode` (#1524 deferred slice). Drives the * graph-scoped context-assembly directive in each dispatched-leaf stage prompt. * ABSENT ⇒ {@link DEFAULT_RETRIEVAL_MODE} (`'graph-scoped'`) — graph-scoped * retrieval is the default; `'raw'` is the explicit byte-identical opt-out. */ retrievalMode?: RetrievalMode; /** * On a staged RE-dispatch after a gate block, the prior attempt's gate/verify * reason (the `tsc`/lint/test failure). The single-agent path threads this into * its dispatch prompt (orchestrator.ts:2597), but the staged path renders fresh * per-stage prompts and would otherwise DROP it — leaving the executor blind to * why the last attempt was blocked, so it reproduces the same failure every * retry. Threaded into every stage prompt as a "fix this first" preamble so the * staged retry loop actually converges. Absent on the first attempt. */ priorGateFailure?: string; /** Terminal-success seam (Task 6/7): bound to `settleWorkflowSuccess`. */ settleSuccess: (unit: string, runs: StageRun[]) => Promise; /** Terminal-failure/safety-net seam (Task 6/7): bound to `settleWorkflowTerminal`. */ settleTerminal: (unit: string, runs: StageRun[], failingStep?: WorkflowExecutionPlan['stages'][number], err?: unknown) => Promise; /** Resume-from-failed-stage checkpoint seams (bound to the orchestrator's map). */ loadStageCheckpoint?: (unit: string) => ReadonlyMap | undefined; saveStageCheckpoint?: (unit: string, stageIndex: number, run: StageRun) => void; } /** * split-routing Phase 4 — compose the REAL `WorkflowEngineContext` (minus the two * terminal seams, which Task 6 completes) from orchestrator machinery. * * Seam → real code: * - `recorder`/`logger`/`issueId`/`identifier`/`externalId`/`workspacePath` → the deps. * - `stageDeadlineMs` → `deps.stageDeadlineMs` (conditional; engine defaults when absent). * - `makeRunner(backend)` → `new AgentRunner(realBackend, { maxTurns })`. The engine * passes a name-only backend for the routed path (execute-workflow.ts:320); we * re-materialize the real backend from that name via * `backendFactory.forUseCase(useCase, { invocationOverride: backend.name })` * (same swap as orchestrator.ts:2062-2064). Its `runSession` returns a * `TurnResult` (runner.ts:112) — the seam is already typed against it * (carry-forward #9), confirmed self-documenting by the explicit annotation below. * - `resolveStageBackend(step)` → identity fallback: `backendFactory.forUseCase(useCase)` * when the factory exists, else a name-only backend from `routingDefault`. * - `adaptiveRouter` → present iff `deps.adaptiveRouter !== null` (D5). * * The two TERMINAL seams (`emitWorkflowSuccess` / `finalizeWorkflowTerminal`, * SC5) are thin forwarders to the orchestrator's `settleSuccess` / `settleTerminal` * callbacks. They are NOT routed through `emitWorkerExit`/`handleWorkerExit` * (which fire the ISSUE-keyed `finishRecording(attempt)` + `recordAmrOutcome` — a * DOUBLE-fire, since the engine already ran PER-STAGE recorders + per-stage * `recordOutcome`). The reducer-reproduction (running.delete → completed.set → * claimed.delete → cleanWorkspace → persist → emit for success; and the * finalizeRoutingTerminal + needs-human + cleanWorkspace sequence for terminal) * lives in the orchestrator's private methods (Task 7) the callbacks bind to. */ declare function buildWorkflowContext(deps: BuildWorkflowContextDeps): WorkflowEngineContext; /** * Adapter for using a markdown roadmap file as an issue tracker. * * This adapter parses a standard Harness roadmap file, extracts features, * and maps them to the internal Issue model using deterministic hashing * for identifiers. */ declare class RoadmapTrackerAdapter implements IssueTrackerClient { private config; /** * Creates a new RoadmapTrackerAdapter. * * @param config - The tracker configuration including the file path */ constructor(config: TrackerConfig); /** * Fetches all issues that are in an "active" state according to the config. */ fetchCandidateIssues(): Promise>; /** * Fetches issues that match any of the given state names. * * @param stateNames - List of statuses to filter by */ fetchIssuesByStates(stateNames: string[]): Promise>; /** * Transitions a roadmap feature into the first configured terminal state * and rewrites the markdown file. Idempotent: if the feature is already * in a terminal state, this is a no-op. * * The orchestrator calls this after a successful agent exit so the feature * is no longer returned by `fetchCandidateIssues` on the next tick — and, * critically, after an orchestrator restart. */ markIssueComplete(issueId: string): Promise>; /** * Claims an issue by transitioning its status to "in-progress" and * writing the orchestratorId into the assignee field. Idempotent if * already claimed by the same orchestratorId. */ claimIssue(issueId: string, orchestratorId: string): Promise>; /** * Releases a claimed issue by transitioning back to the first active * state and clearing the assignee field. */ releaseIssue(issueId: string): Promise>; /** * Resolve the roadmap store anchored on the configured aggregate file path. * The shard backend is chosen when a sibling `roadmap.d/` exists next to the * file; otherwise the monolith file is read/written whole. Callers guard * `this.config.filePath` before invoking. */ private resolveStore; /** Load the roadmap via the store (sharded or monolith), returning a Result. */ private loadRoadmap; private findFeatureById; /** * Fetches full issue details for a list of identifiers. * * @param issueIds - List of issue IDs to fetch */ fetchIssueStatesByIds(issueIds: string[]): Promise, Error>>; /** * Maps a raw RoadmapFeature from the parser to the unified Issue model. */ private mapFeatureToIssue; /** * Generates a deterministic, URL-safe identifier for a feature name. */ private generateId; } /** * Interface for a Linear GraphQL tool extension — a thin authenticated client * over Linear's GraphQL API (https://linear.app/developers/graphql). */ interface LinearGraphQLExtension { /** * Execute a GraphQL operation. Resolves to `Ok(data)` (the `data` object of * the GraphQL envelope) on success, or `Err` on a transport failure, a non-2xx * HTTP status, or a GraphQL `errors` array. */ query(query: string, variables?: Record): Promise>; } interface LinearGraphQLClientOptions { /** * Linear authentication token. A personal API key is sent verbatim in the * `Authorization` header; an OAuth access token must be passed already prefixed * with `Bearer ` (Linear's two supported schemes). */ apiKey: string; /** Override the GraphQL endpoint (e.g. a proxy). Defaults to Linear's API. */ endpoint?: string; /** Injectable fetch for tests; defaults to the global `fetch`. */ fetchFn?: typeof fetch; } /** * Real Linear GraphQL client. Replaces {@link LinearGraphQLStub}, which only * logged the query and returned an empty object. POSTs the operation to Linear's * GraphQL endpoint with the API key, and normalizes the three failure modes * (transport throw, non-2xx HTTP, GraphQL `errors`) into a single `Err`. */ declare class LinearGraphQLClient implements LinearGraphQLExtension { private readonly apiKey; private readonly endpoint; private readonly fetchFn; constructor(opts: LinearGraphQLClientOptions); query(query: string, variables?: Record): Promise>; /** POST the operation to Linear, normalizing transport throws into an `Err`. */ private sendRequest; /** Build the error for a non-2xx HTTP response, including a truncated body. */ private httpError; /** Parse the JSON envelope, normalizing parse failures into an `Err`. */ private parseEnvelope; } /** * @deprecated Phase-4 placeholder retained for backward compatibility. It logs * the query and returns an empty object; use {@link LinearGraphQLClient} for a * real authenticated client. */ declare class LinearGraphQLStub implements LinearGraphQLExtension { query(query: string, _variables?: Record): Promise>; } /** * AMR 4c: a single-agent quality-verdict source (ADR 0069). The escalation * mechanism + seam are complete; this supplies the *sound* signal that was * missing — a BASELINE-RELATIVE scan of the diff the agent introduced. Only the * ADDED lines are scanned, so a pre-existing pattern never counts against the * agent. This is sound (not approximate) because every security rule is * single-line, so per-added-line matching yields exactly the introduced findings. */ /** One hunk of added lines from a unified diff, with its true start line. */ interface IntroducedHunk { /** Repo-relative path of the changed file. */ file: string; /** The added lines (leading `+` stripped), joined with `\n`. */ addedContent: string; /** New-file line number the first added line sits at (for accurate findings). */ startLine: number; } /** * Structured event emitted when {@link WorkspaceManager.resolveBaseRef} * falls back past `origin/HEAD` and `origin/main`/`origin/master` to a * local-only ref. Operators see this in the dashboard's maintenance * event stream when the remote is misconfigured or unreachable. */ interface BaseRefFallbackEvent { kind: 'baseref_fallback'; /** The ref that was selected — `'main'`, `'master'`, or `'HEAD'`. */ ref: string; /** Absolute path to the git repository root. */ repoRoot: string; } /** Optional dependencies injected into {@link WorkspaceManager}. */ interface WorkspaceManagerOptions { /** * Synchronous fire-and-forget callback invoked when {@link * WorkspaceManager.resolveBaseRef} falls back to a local-only ref. * When omitted, fallback emission is silently skipped. */ emitEvent?: (event: BaseRefFallbackEvent) => void; } declare class WorkspaceManager { private config; /** Absolute path to the git repository root (resolved lazily). */ private repoRoot; /** Phase 3 (D6): emit baseref_fallback when fallback chain selects a local-only ref. */ private emitEvent; constructor(config: WorkspaceConfig, options?: WorkspaceManagerOptions); /** Runs a git command and returns stdout. Extracted for testability. */ protected git(args: string[], cwd: string): Promise; /** Backoff between PR-create retries. Overridable so tests don't actually wait. */ protected sleep(ms: number): Promise; /** * Runs a `gh` CLI command and returns stdout. Extracted for testability * (mirrors {@link git}) so {@link shipWorkspace} can create a PR without * spawning the real `gh`. Array argv (execFile, no shell) so PR title/body * content can never break out into shell metacharacters. */ protected gh(args: string[], cwd: string): Promise; /** * Sanitizes an issue identifier to be safe for use as a directory name. */ sanitizeIdentifier(identifier: string): string; /** * Resolves the full path for an issue's workspace. */ resolvePath(identifier: string): string; /** Path to a worktree's identity record under the (gitignored) .harness dir. */ private worktreeIdentityPath; /** Path to the per-repo worktree completion-number counter. */ private worktreeCounterPath; /** * Best-effort: record an immutable ULID identity for a worktree task. * Never throws — identity is metadata and must not block worktree creation. */ private recordWorktreeIdentity; /** * AMR 4c: the lines the dispatched agent INTRODUCED in its worktree, as * per-hunk added-line blocks — the sound input for a baseline-relative quality * scan (pre-existing content is excluded by construction). Diffs the working * tree (so both committed AND uncommitted agent work is captured) against the * MERGE-BASE of the worktree HEAD and the base ref — merge-base, not the base * ref itself, so a base branch that advanced mid-dispatch never attributes * other merges to this agent. The seeded handoff overlay (`seedPaths`) is * excluded — those files are not the agent's work. */ getIntroducedDiff(identifier: string): Promise; /** * AMR 4c v2: the SAME introduced change as {@link getIntroducedDiff}, but as the * RAW unified diff text (default context, NOT `--unified=0`) — the input an LLM * spec-satisfaction eval needs to judge whether the diff satisfies the spec. * Merge-base relative for the same reason (a base branch that advanced * mid-dispatch never attributes other merges here), and the seeded handoff * overlay is excluded via git `:(exclude)` pathspecs so the judge never reads * pre-seeded proposal/roadmap content as the agent's work. */ getIntroducedDiffText(identifier: string): Promise; /** * Mark untracked (non-ignored) files in the worktree as intent-to-add so a * subsequent `git diff` INCLUDES their full content. Without this, `git diff` * silently omits untracked files entirely — so a brand-NEW file the agent * created (e.g. a new rule module, not a modification of an existing one) is * invisible to the introduced-diff, and any spec-vs-diff judge concludes the * work is missing even though it is present and passing. `--intent-to-add` * respects `.gitignore` (build output / node_modules stay out) and leaves file * contents untouched; the residual index entries are harmless (the ship stages * with `git add -A` regardless). Best-effort: a failure falls back to the * tracked-only diff rather than blocking the eval/scan. */ private markUntrackedIntentToAdd; /** * Physically remove agent scratch/backup cruft ({@link SCRATCH_GLOBS}) from the * worktree before a ship commit. Clears the index entry (in case it was marked * intent-to-add) THEN deletes the on-disk file, so: (a) the cruft never lands in * the PR, and (b) lint-staged's pre-commit stash can't fail on an intent-to-add * cruft file ("Entry … not uptodate. Cannot merge."). Glob pathspecs, tolerant * of no match; best-effort — a failure never blocks the ship. */ private removeScratchFiles; /** * Discovers the git repository root from the workspace root directory. */ private getRepoRoot; /** * Ensures the workspace exists as a git worktree so the agent has * access to the full project source. * * Reuse-on-retry contract: when `opts.preserve` is true AND a valid * worktree already exists at the target path, the existing worktree is * returned untouched (`reused: true`) — skipping remove, `worktree add`, * and seeding — so a within-run retry preserves the agent's uncommitted * partial progress. Otherwise (the default, and every fresh dispatch / * orchestrator restart) the worktree is removed and recreated from the * latest base ref (`reused: false`), preserving the anti-stale guarantee. * * @param identifier - The issue/unit identifier for the workspace. * @param opts.preserve - When true, reuse an existing valid worktree * instead of wiping it. Defaults false → current wipe-and-recreate flow. */ ensureWorkspace(identifier: string, opts?: { preserve?: boolean; }): Promise>; /** * Best-effort `git fetch origin` so subsequent ref resolution sees the * latest remote state. Failures (offline, no remote, auth errors) are * swallowed — dispatch should not be blocked by transient network issues. */ private tryFetch; /** * Default paths seeded into a fresh worktree when {@link WorkspaceConfig.seedPaths} * is unset — the artifacts produced by the brainstorm → orchestrator handoff. */ private static readonly DEFAULT_SEED_PATHS; /** * Process artifacts excluded from the spec-vs-diff EVAL diff (on top of the * seed overlay) — they are NOT the agent's implementation: * - `docs/changes` — the design stage's proposal/plan (the proposal IS the * spec, re-included redundantly; the plan is planning). * - `docs/roadmap.d` — roadmap shards written by the run. * - `.pnpm-store` — the local package store (binary noise). * Why it matters: left in, these DWARF the actual code change (e.g. a ~280-line * proposal/plan vs a ~90-line rule+test) and let the judge conflate "described * in the proposal" with "implemented", so it reports the new file "missing" * among the noise. Validated end-to-end: excluding these flips the local * outcome-eval from a false NOT_SATISFIED to SATISFIED on a correct diff. The * 4c hunk scan keeps the fuller diff; only the eval text is narrowed. */ private static readonly EVAL_DIFF_EXCLUDES; /** * Agent scratch/backup cruft an autonomous coder can leave behind (backup * copies, temp/scratch files, editor litter). Glob pathspecs, matched anywhere * in the tree. Excluded from the eval diff so they don't mislead the judge, and * kept OUT of the shipped commit so a PR never carries them. Deliberately narrow * — only unambiguous scratch conventions, never a real source extension. */ private static readonly SCRATCH_GLOBS; /** * Copies the configured seed paths from the root working tree into a * freshly-created worktree, overlaying the committed checkout. * * A new worktree is based on a committed remote ref, so it does not contain * uncommitted artifacts that exist only in the root working tree (a * just-written proposal under `.harness/proposals/`, a promoted row in * `docs/roadmap.md`). Seeding carries them over so a dispatched agent sees * the same state the orchestrator dispatched from. * * Best-effort by design: a missing source is skipped, and a copy failure is * swallowed — neither must ever block dispatch. */ private seedWorkspace; /** * Resolves the ref that new worktrees should be based on. * * Priority order: * 1. `config.baseRef` (explicit override). Throws if it doesn't resolve. * 2. Default branch via `git symbolic-ref --short refs/remotes/origin/HEAD`. * 3. Remote fallbacks: `origin/main`, `origin/master`. (No event.) * 4. Local-only fallbacks: `main`, `master`. (Emits `baseref_fallback`.) * 5. `HEAD` as ultimate fallback. (Emits `baseref_fallback`.) * * Phase 3 / spec D6 / R4: when the priority chain falls past `origin/*` * to a local-only ref, the optional `emitEvent` callback (if injected) * is invoked exactly once with `{ kind: 'baseref_fallback', ref, repoRoot }` * so operators are warned when the remote is misconfigured or unreachable. */ private resolveBaseRef; /** * Phase 3 (D6): emit a `baseref_fallback` event via the injected * callback (if any). Errors from the callback are swallowed so a * broken emitter does not block worktree dispatch. */ private emitFallback; /** Returns true iff `git rev-parse --verify` accepts the ref. */ private refExists; /** * Checks if a workspace exists. */ exists(identifier: string): Promise; /** Reads the recorded worktree identity, or null if absent/unreadable. */ getWorkspaceIdentity(identifier: string): Promise; /** * Best-effort: assign (idempotently) the worktree's completion number. * Returns the updated/existing identity, or null on failure/absence. */ assignWorkspaceNumber(identifier: string): Promise; /** * Checks whether a worktree has commits ahead of the base branch that have * been pushed to a remote branch. Returns the remote branch name if found, * or null if the worktree is on a detached HEAD with no pushed branch. */ findPushedBranch(identifier: string): Promise; /** * Checks whether a branch exists on the remote by querying `git ls-remote`. * Returns false if the branch is not found or the command fails. */ branchExistsOnRemote(branch: string): Promise; /** * staged-verify-gate-convergence D4 — deterministically SHIP a worktree's * accumulated work. A local staged unit's worktree is a DETACHED-HEAD git * worktree whose acceptance gate just passed, but the weak local model often * skips push+PR (LESSONS.md #874). This commits the uncommitted work, creates * a SLASH-prefixed branch `orchestrator/` (so {@link * findPushedBranch} recognizes it), pushes it, and opens a PR via `gh pr * create` — so the existing `cleanWorkspaceWithGuard` "branch pushed + PR * exists" path fires and the PR-merge auto-dones the roadmap row (the loop * stops). * * Fully GUARDED / fail-safe: every step is awaited inside one try/catch and any * failure returns `Err` (never throws out), so the caller can treat a ship * failure as a BLOCK (retry) rather than a silent success. * * IDEMPOTENT / RESUMABLE (IMPORTANT #1). A prior partial ship (push landed but * `gh pr create` failed — gh down/unauth/5xx) leaves a pushed remote branch AND * a local `orchestrator/` branch in the PR-preserved worktree. Calling ship * again MUST converge to exactly one PR and never error on the pre-existing * branch. So each mutating step is preceded by an existence probe: * - branch pushed + an OPEN PR already covers it ⇒ ALREADY DONE (return that * PR; no duplicate). * - branch exists locally ⇒ `git switch` (no `-c`) instead of `switch -c`. * - branch pushed but NO PR ⇒ skip the push, resume at `gh pr create`. * * Sequence (in the worktree): * 0. `branchExistsOnRemote` + `openPrForBranch` — if pushed AND an open PR * exists, return it (idempotent short-circuit; no git/gh mutation). * 1. `git add -A`; `git commit -m ` — ONLY when `status --porcelain` * reports changes (a no-op commit would exit non-zero and must not error * the flow — the accumulated work may already be committed). * 2. `git switch [-c] orchestrator/` — `-c` only when the branch * does not already exist locally. * 3. `git push -u origin orchestrator/` — skipped when already pushed. * 4. `gh pr create --head --base --title --body `. * * @returns the created (or pre-existing) branch name and the PR URL on success. */ shipWorkspace(identifier: string, opts: { title: string; body: string; workspacePath?: string; /** * Machine-readable provenance (#1531). When supplied, a `Harness-*` trailer * (see {@link appendProvenanceTrailer}) is appended to BOTH the autonomous * commit message and the PR body — so the autonomous tier is mechanically * countable and, because the repo squash-merges, the PR body carries the * record even when the branch commit's message is discarded by the squash. * Omitted for any caller that does not thread run context, leaving the * commit + PR byte-identical to today (interactive commits unaffected). */ provenance?: ProvenanceTrailerInput; }): Promise>; /** * IMPORTANT #1 helper — true iff a LOCAL branch `refs/heads/` exists. * Used by {@link shipWorkspace} to pick `switch` vs `switch -c` so a resumed * ship never errors on the branch its prior attempt already created. Reuses the * {@link refExists} `rev-parse --verify --quiet` probe (exit-1 ⇒ absent). Never * throws — an unexpected failure reports "absent" so the caller falls back to * the create path (which surfaces any real error at `switch -c` time, guarded). */ private localBranchPresent; /** * IMPORTANT #1 helper — the URL of an OPEN pull request whose head is `branch`, * or `null` if none. Used by {@link shipWorkspace} to treat a pushed-branch + * open-PR world as ALREADY SHIPPED (no duplicate PR). Queries `gh pr list --head * --state open --json url`. A non-empty JSON array ⇒ a PR exists; its * `url` (when parseable) is returned so the caller can surface it. Any failure * (gh down/unauth) returns `null` (⇒ "no known PR"): the caller then RESUMES at * `gh pr create`, which is itself guarded — so a transient gh error never strands. */ private openPrForBranch; /** * Deletes remote branches whose PRs are merged and that are older than * `maxAgeDays`. Only considers branches matching agent naming conventions * (feat/*, fix/*). Returns the list of deleted branch names. * * Requires a `checkPR` callback so this class doesn't depend on PRDetector * directly. The orchestrator wires this up at call time. */ sweepStaleBranches(opts: { maxAgeDays: number; checkPR: (branch: string) => Promise<{ found: boolean; error?: string; }>; }): Promise; /** * Removes a workspace directory and its git worktree registration. */ removeWorkspace(identifier: string): Promise>; } declare class WorkspaceHooks { private config; constructor(config: HooksConfig); /** * Executes a shell hook in the specified workspace directory. */ executeHook(hookName: keyof Omit, cwd: string): Promise>; afterCreate(cwd: string): Promise>; beforeRun(cwd: string): Promise>; afterRun(cwd: string): Promise>; beforeRemove(cwd: string): Promise>; } /** * Language-aware workspace ecosystem detector. * * The local enforced gate (the mechanical verify that BLOCKS a dispatch) and the * workspace bootstrap step were both JS/pnpm-baked: verify shelled out to * `pnpm -w run …` unconditionally. For any non-JS workspace (Python, Rust, Go, * Ruby, Java, …) that fails ENVIRONMENTALLY — pnpm is absent or there is no * package.json — so the gate blocks every dispatch for a reason that has nothing * to do with the change under test. * * This module is the single, language-agnostic source of truth: given a * workspace it detects the ecosystem from the lockfiles/manifests present and * returns BOTH the dependency-install command (what a matching `hooks.afterCreate` * should scaffold) AND the ordered verify command set (what the enforced gate * should run). It hardcodes no package manager in orchestrator control flow — the * commands live in one declarative table here and every choice remains overridable * via config. * * The core matcher is PURE: it takes the set of filenames present at the workspace * root and returns a descriptor. A thin filesystem wrapper does the single * directory read and delegates, so the matching logic is unit-testable without * touching disk. */ type EcosystemId = 'node-pnpm' | 'node-npm' | 'node-yarn' | 'python-uv' | 'python-poetry' | 'python-pipenv' | 'python-pip' | 'rust-cargo' | 'go' | 'ruby-bundler' | 'java-maven' | 'java-gradle'; interface Ecosystem { /** Stable identifier — language family + package manager / toolchain. */ id: EcosystemId; /** Human-readable language family (node / python / rust / go / ruby / java). */ language: string; /** Package manager or build tool (pnpm, npm, yarn, uv, poetry, cargo, go, …). */ packageManager: string; /** * Command that installs the workspace's dependencies from a clean checkout — * the value a matching `hooks.afterCreate` should scaffold so the agent's fresh * worktree is not missing its deps when the gate runs. */ installCommand: string; /** * Ordered mechanical verify steps (the ecosystem's build / typecheck / lint / * test analogues). The gate runs them in order and short-circuits on the first * failure. Each entry is a whole command line split on whitespace at execution * time — no shell is involved, so nothing here may rely on shell features. * Kept to each toolchain's ALWAYS-PRESENT commands (e.g. `go vet`, `cargo test`) * rather than optional add-ons (mypy, ruff, clippy) so a stock checkout does not * red the gate on a merely-missing linter; adopters add those via config. */ verifyCommands: readonly string[]; } /** * One detector rule: if ANY of `markers` is present at the workspace root, the * workspace IS `ecosystem`. Rules are evaluated in array order and the FIRST * match wins, so the array order encodes the detection priority. */ interface EcosystemRule { markers: readonly string[]; ecosystem: Ecosystem; } /** * Detection rules in deterministic priority order (first match wins). * * Node is checked before every other family: the harness's own default is JS and * a polyglot repo that carries a `package.json` for tooling should still bootstrap * as node unless it is unambiguously another ecosystem. Within a family the more * specific lockfile precedes the looser manifest (e.g. `uv.lock` before * `poetry.lock` before a bare `pyproject.toml`) so a lockfile always pins the * package manager it belongs to. Every entry stays overridable via config. */ declare const ECOSYSTEM_RULES: readonly EcosystemRule[]; /** * PURE core: pick the ecosystem for a workspace from the set of filenames present * at its root. Returns the FIRST rule (in {@link ECOSYSTEM_RULES} priority order) * for which any marker is present, or `null` when nothing is recognized. Never * touches the filesystem, so it is fully unit-testable. */ declare function detectEcosystemFromFiles(files: Iterable): Ecosystem | null; /** * Filesystem wrapper around {@link detectEcosystemFromFiles}: read the workspace * root's directory entries ONCE and delegate to the pure matcher. Returns `null` * when the directory cannot be read (a missing/uninitialized workspace has no * detectable ecosystem) so callers degrade gracefully rather than throwing. */ declare function detectEcosystem(workspacePath: string): Ecosystem | null; declare class MockBackend implements AgentBackend { readonly name = "mock"; startSession(params: SessionStartParams): Promise>; runTurn(session: AgentSession, _params: TurnParams): AsyncGenerator; stopSession(_session: AgentSession): Promise>; healthCheck(): Promise>; } declare class PromptRenderer { private engine; constructor(); render(template: string, context: Record): Promise; } /** * Internal types for the maintenance module. * Public config types (MaintenanceConfig, TaskOverride, CustomTaskDefinition, * CheckScriptDefinition, OutputRetentionConfig) live in @harness-engineering/types. */ /** * Classification of maintenance task execution strategy. * * - mechanical-ai: Run a check command first; dispatch AI agent only if fixable issues are found. * - pure-ai: Always dispatch an AI agent on schedule regardless of preconditions. * - report-only: Run a command and record metrics; never create branches or PRs. * - housekeeping: Run a mechanical command directly; no AI, no PR. */ type TaskType = 'mechanical-ai' | 'pure-ai' | 'report-only' | 'housekeeping'; /** * Hermes Phase 2 — Provenance tag identifying the trigger source of a run. * * Set by the entry point, never user-configurable: * - 'cron' — scheduled by MaintenanceScheduler * - 'cli' — `harness maintenance run ` * - { kind: 'api', tokenName } — Gateway API trigger (Phase 0) * - { kind: 'chain', upstreamTaskId } — fired by a downstream `contextFrom` * dependency (reserved; not yet wired) */ type RunOrigin = 'cron' | 'cli' | { kind: 'api'; tokenName: string; } | { kind: 'chain'; upstreamTaskId: string; }; /** * Run mode for a maintenance task (on-demand pipeline, D4). * * - 'fix' — current cron behavior: mechanical-ai dispatches on findings, * pure-ai always dispatches, PRs may be opened. Default. * - 'report' — read-only sweep: run the check step, record findings, and take * the no-dispatch branch — never dispatch a fix agent or open a PR. */ type RunMode = 'report' | 'fix'; /** * Per-task cost ceiling (Hermes Phase 5). * * When set, the orchestrator's `CostCeilingMonitor` tracks cumulative * agent spend for the task and aborts dispatch on exceed (D6 — abort * is advisory at the turn boundary). */ interface TaskCostCeiling { /** Hard cap in USD. Cumulative spend > maxUsd fires the abort path. */ maxUsd: number; /** Warn threshold expressed as a percentage of `maxUsd` (1–99). */ warnAtPct?: number; } /** * Definition of a maintenance task (built-in or Phase 2 custom). * * Custom-task-only fields (`checkScript`, `inlineSkills`, `inlineSkillsBudgetTokens`, * `contextFrom`, `contextFromMaxAgeMinutes`, `outputRetention`, `isCustom`) are * populated by the scheduler when merging `MaintenanceConfig.customTasks` into the * resolved task list. Built-ins leave them unset and the runner falls through to * the legacy execution paths unchanged. */ interface TaskDefinition { /** Unique identifier for this task (e.g., 'arch-violations') */ id: string; /** Execution strategy */ type: TaskType; /** Human-readable description */ description: string; /** Default cron expression (e.g., '0 2 * * *' for daily at 2am) */ schedule: string; /** Branch name for PRs, or null for report-only/housekeeping tasks */ branch: string | null; /** CLI command args for the mechanical check step (mechanical-ai and report-only) */ checkCommand?: string[]; /** Skill name to dispatch for AI fix (mechanical-ai and pure-ai) */ fixSkill?: string; /** * Per-task cost ceiling (Hermes Phase 5). When set, cumulative agent * spend across all turns dispatched for this task is tracked; the * orchestrator aborts dispatch on `maxUsd` exceedance with * `RunResult.error === 'cost_ceiling_exceeded'`. Default: unset = no cap. */ costCeiling?: TaskCostCeiling; /** * Hermes Phase 2 — Arbitrary-executable check (replaces `checkCommand`). * Mutually-exclusive with `checkCommand`; validator rejects both. */ checkScript?: CheckScriptDefinition; /** Hermes Phase 2 — Skill names whose markdown is inlined into the agent prompt. */ inlineSkills?: string[]; /** Hermes Phase 2 — Token-budget cap for inlined skills. Default: 8000. */ inlineSkillsBudgetTokens?: number; /** Hermes Phase 2 — Upstream task IDs whose latest output feeds prompt context. */ contextFrom?: string[]; /** Hermes Phase 2 — Max upstream-output age (minutes). Default: 1440. */ contextFromMaxAgeMinutes?: number; /** Hermes Phase 2 — Output retention overrides. */ outputRetention?: OutputRetentionConfig; /** Hermes Phase 2 — Marks tasks originating from `customTasks` config. */ isCustom?: boolean; /** * On-demand maintenance pipeline (D5). When `true`, the task is excluded * from the human "overdue" sweep computed by `selectTasks` — used for * git-mutating housekeeping (`main-sync`, `perf-baselines`, * `session-cleanup`) and one-shot backfills (`proposal-provenance-backfill`) * that are infra hygiene, not developer-facing health signals. * `undefined` (default) → sweep-eligible. */ excludeFromHumanSweep?: boolean; } /** * Result of a single maintenance task run. */ interface RunResult { /** ID of the task that was run */ taskId: string; /** ISO timestamp when the run started */ startedAt: string; /** ISO timestamp when the run completed */ completedAt: string; /** Outcome of the run */ status: 'success' | 'failure' | 'skipped' | 'no-issues'; /** Number of issues/findings detected */ findings: number; /** Number of issues fixed */ fixed: number; /** URL of the created/updated PR, or null if no PR was created */ prUrl: string | null; /** Whether an existing PR was updated (vs newly created) */ prUpdated: boolean; /** Error message if status is 'failure' */ error?: string; /** * Cumulative agent spend in USD for this run (Hermes Phase 5). * * Always present (defaults to 0). Populated by the * `CostCeilingMonitor` from per-turn `TokenUsage` × `ModelPricing`. * When the run aborted on cost-ceiling exceed, `status === 'failure'` * and `error === 'cost_ceiling_exceeded'`. */ costUsd?: number; /** * Hermes Phase 2 — Provenance tag set by the entry point. * Older orchestrators may emit this field absent; renderers should fall * back to `'—'` rather than crash. */ origin?: RunOrigin; } /** * Schedule entry for a single task, used in MaintenanceStatus. */ interface ScheduleEntry { /** Task identifier */ taskId: string; /** * Task type (mechanical-ai | pure-ai | report-only | housekeeping). * * Optional on the WIRE shape so that a newer dashboard rendering data from * an older orchestrator (which did not yet emit this field) does not * produce empty cells of unknown semantics. The current orchestrator * always populates this field via `MaintenanceScheduler.getStatus()`. * Newer dashboards must render `row.type ?? '—'` (or similar fallback). */ type?: TaskType; /** ISO timestamp of the next scheduled run */ nextRun: string; /** Result of the most recent run, or null if never run */ lastRun: RunResult | null; } /** * Overall maintenance module status, exposed via dashboard API. */ interface MaintenanceStatus { /** Whether this orchestrator instance is the maintenance leader */ isLeader: boolean; /** ISO timestamp of the last successful leader claim, or null */ lastLeaderClaim: string | null; /** Schedule state for all enabled tasks */ schedule: ScheduleEntry[]; /** Currently executing task, or null if idle */ activeRun: { taskId: string; startedAt: string; } | null; /** History of completed runs (most recent first) */ history: RunResult[]; } /** * D10 vertical escalation. Per-`coherenceUnit` quality-failure counter that * raises the unit's floor tier one step on the Nth consecutive QUALITY failure * (never transport — that is the shipped per-model breaker). Monotonic + * `strong`-capped ⇒ cannot loop or thrash. Naming mirrors * `LocalModelResolver.recordSuccess/recordFailure` (local-model-resolver.ts:460/474). */ declare class EscalationState { private readonly threshold; private readonly units; constructor(threshold?: number); /** * The current escalation floor for a unit — the minimum tier `route()` must * resolve at. Defaults to `'fast'` (no-op floor) for an unknown/absent unit, * so an un-escalated request derives its tier normally. */ floorFor(coherenceUnit?: string): CapabilityTier; /** D10 mechanism flag: has this unit ever climbed a tier? (Phase 6 reads this for Tier-A disqualification.) */ isEscalated(coherenceUnit?: string): boolean; /** * AMR observability: the coherence units that have climbed ABOVE the `fast` * floor, with their current floor tier. Operator status only (read-only) — an * empty list means nothing has escalated. Units still at `fast` are omitted. */ climbedUnits(): Array<{ coherenceUnit: string; floor: CapabilityTier; }>; /** * SC16: a QUALITY failure at `tier` increments this unit's counter; on the * Nth (threshold) consecutive failure the floor climbs one step (fast→standard * →strong), the count resets, and `escalated` latches true. `strong` is the * ceiling: a threshold-crossing failure already at `strong` returns * 'exhausted' (router emits routing:escalation-exhausted). `ok` clears the * in-progress count but leaves the raised floor (monotonic per D10). * * `_tier` (the tier the outcome occurred at) is ADVISORY/UNUSED today: the climb * is driven entirely off the unit's own `state.floorTier`, not off this argument. * It is kept in the signature so the call-site contract stays stable for a future * refinement that guards against out-of-order (stale-tier) reports. (Note: this is * NOT positionally mirroring `LocalModelResolver.recordFailure`, which keys on a * model identifier and takes no tier — the earlier comment claiming that shape was * misleading.) */ recordOutcome(coherenceUnit: string, _tier: CapabilityTier, ok: boolean): 'ok' | 'escalated' | 'exhausted'; } interface AdaptiveRouterDeps { router: BackendRouter; registry: BackendCapabilityRegistry; policy: RoutingPolicy; /** * Complexity classifier. Accepts a sync verdict-provider (Phase-3 conservative * stub) OR an async classifier (D4 live cascade); `route()` awaits either shape. * A rejection/throw is caught and degraded to `{moderate, low}` (never blocks * dispatch — Failure modes / D4). */ classify: (req: RoutingRequest) => ComplexityVerdict | Promise; /** name → provider type, derived from agent.backends (Task 6). REQUIRED when policy.allowedProviders is set. */ providerOf?: (name: string) => BackendDef['type'] | undefined; /** Injected spend snapshot (D8/S1-001). Defaults to 0-spend. */ budgetState?: () => { spentUsd: number; }; /** * D10 vertical escalation state. When present, `route()` uses * `escalation.floorFor(req.coherenceUnit)` as the tier floor (a climbed unit * resolves higher); when absent, the floor is the no-op `'fast'`. */ escalation?: EscalationState; /** * D10 steward-escalation seam. Invoked with the `coherenceUnit` when * `recordOutcome` returns `'exhausted'` (floor already `strong`, re-crossed the * threshold). The orchestrator binds this to `routingDecisionBus`/logger to emit * `routing:escalation-exhausted`. */ onExhausted?: (coherenceUnit: string) => void; /** * SC9: enriched-decision bus. The shipped `resolveDecisionAndDef` emits the * BASE decision inside the D2-frozen router (no `complexity`/`tierRequired`/ * `estCostUsd`). When `AdaptiveRouter` owns live dispatch it emits the * ENRICHED decision here so a subscriber actually receives the AMR telemetry. * Absent ⇒ no enriched emit (and when AMR is off the router isn't constructed, * so nothing changes on the bus — SC8/SC17 preserved). */ decisionBus?: RoutingDecisionBus; } /** * AMR Phase 3 (D2): wraps — never modifies — the shipped {@link BackendRouter}. * * `route()` classifies the request, derives the required {@link CapabilityTier} * via `deriveRequiredTier`, picks the cheapest qualifying backend * (`selectCheapestQualifying`), and delegates the actual resolution to * `router.resolveDecisionAndDef` — enriching the returned {@link RoutingDecision} * with `complexity`/`tierRequired`/`estCostUsd` (SC9). The shipped router's * identity/default chain still owns the final backend lookup, so when tier * selection abstains (`undefined`) dispatch falls through unchanged. */ declare class AdaptiveRouter { private readonly deps; /** * D8 live spend accumulator. Monotonic sum of `estCostUsd` over every decision * this router has made. `route()` reads it (via the `budgetState` default) * BEFORE deriving a tier, so `deriveRequiredTier`'s budget clamp actually fires * as spend accrues — previously `budgetState` was an un-wired `{ spentUsd: 0 }` * stub, so the clamp was dead. * * Semantics (deliberately modest — do NOT read this as a hard ceiling): * - **Soft, lagging cap.** The clamp reads spend accrued from PRIOR dispatches; * a burst of concurrent dispatches all read a stale-low total before any of * them accrues, so a burst can overshoot `capUsd` before the clamp engages. * This is a degrade *signal*, not an admission gate. * - **Single-step degrade.** `deriveRequiredTier` lowers the tier by exactly ONE * step under budget pressure and never below the D5 privacy veto floor — it * does not throttle progressively toward `fast`. * - **Monotonic** (NOT the bounded `projectTelemetry` ring-sum): a long run must * not evict early spend and un-clamp. Preserved across `setPolicy` (instance * persists) — note a *lowered* `capUsd` then clamps immediately and, since the * total never decreases, irreversibly for the rest of the run. * An injected `deps.budgetState` overrides this for the clamp (DI/tests). */ private spentUsd; constructor(deps: AdaptiveRouterDeps); /** * Construct an `AdaptiveRouter` from an orchestrator's `agent.backends` * (plus optional LMLM pool). Builds the capability registry via * `buildCapabilityRegistry` and — crucially — derives `providerOf` * UNCONDITIONALLY from `agent.backends` (name → `def.type`). * * Phase-1 finding (capability-registry.ts:60-66): passing an allowlist to * `selectCheapestQualifying` WITHOUT a `providerOf` fail-closes EVERY request * (silent DoS), because every candidate's provider reads back as `undefined`. * Deriving `providerOf` here means enabling the (Phase-5) allowlist branch can * never accidentally deny-all. */ static fromConfig(args: { router: BackendRouter; backends: Record; pool?: PoolStateProvider; policy: RoutingPolicy; classify: (req: RoutingRequest) => ComplexityVerdict | Promise; budgetState?: () => { spentUsd: number; }; /** D10: injected escalation state; defaults to a fresh one seeded from `policy.escalationThreshold`. */ escalation?: EscalationState; /** D10: steward-escalation seam bound by the orchestrator to the decision bus/logger. */ onExhausted?: (coherenceUnit: string) => void; /** SC9: enriched-decision bus (the same instance dispatch emits base decisions onto). */ decisionBus?: RoutingDecisionBus; }): AdaptiveRouter; /** * AMR Phase 5 (D1): hot-swap the routing policy in place. Every policy field * takes effect on the NEXT dispatch EXCEPT `escalationThreshold` (see note) — * the capability registry and `providerOf` are derived from `agent.backends`, * NOT from `policy`, so a policy edit leaves them untouched; `route()` / * `buildConstraints` / `deriveRequiredTier` all read `this.deps.policy` fresh, * so the swapped-in tier matrix / privacy floor / allowlist / budget all apply. * The live {@link EscalationState} is preserved by reference: a unit's climbed * floor survives the swap (a policy edit must not reset accumulated escalation). * The monotonic spend accumulator also persists — so a swap that LOWERS `capUsd` * clamps immediately and irreversibly for the rest of the run (see `spentUsd`). * * Threshold note: the `EscalationState` was seeded with the ORIGINAL policy's * `escalationThreshold` and is intentionally NOT re-seeded here — re-seeding * would conflate "raise the bar" with "reset progress". Preserving climbed * floors is the invariant (SC1); a live threshold change is out of scope (D1). */ setPolicy(policy: RoutingPolicy): void; /** * AMR Phase 5 (D2): project the ENRICHED routing decisions in the bus ring * buffer into the Shuttle telemetry wire shape ({@link RoutingTelemetry}). * * Non-destructive — this backs the idempotent `GET /api/v1/routing/telemetry`, * so it reads (never clears) the ring; the ring self-evicts FIFO at capacity * and Shuttle dedups by `decisionTs`. `spentUsd` is therefore a sum over the * RETAINED ring (telemetry, not billing-of-record). * * Filters to ENRICHED decisions only — those `AdaptiveRouter.route()` emitted, * carrying `estCostUsd`+`tierRequired`. The SAME bus also carries the BASE emit * from `BackendRouter.resolveDecisionAndDef` (neither field), so an unfiltered * projection would double-count rows and under-fill `spentUsd`. Returns an * empty payload when no bus is wired or no enriched decisions exist. */ projectTelemetry(): RoutingTelemetry; /** * D8: the EFFECTIVE spend total the budget clamp reads — the injected * `budgetState` when present, otherwise the router's own monotonic accumulator. * Returning the effective value (not blindly the internal tally) means an * operator reading this always sees the number that actually drove routing. */ getSpentUsd(): number; /** * AMR observability: the live operator status — budget spend-vs-cap (using the * monotonic accumulator that drives the clamp, NOT the telemetry ring sum), * the coherence units that have climbed their escalation floor, and the active * provider allowlist. Called only on a live router, so `active` is always true. */ getStatus(): RoutingStatus; route(req: RoutingRequest): Promise<{ decision: RoutingDecision; def: BackendDef; }>; /** * D4 fail-safe: await the (possibly async) classifier; if it rejects or throws, * degrade to a conservative `{ level:'moderate', confidence:'low' }` verdict. * Classification NEVER blocks dispatch — a classifier failure/timeout must not * propagate out of `route()`. */ private classifySafe; /** * D10 outcome feedback (mirrors LocalModelResolver.recordSuccess/recordFailure). * Only QUALITY failures are reported here; transport/inference errors go to the * shipped per-model breaker and must NOT reach this path (they never * double-count). A quality failure that re-crosses the threshold while the floor * is already `strong` returns `'exhausted'` from EscalationState, at which point * the injected `onExhausted` seam emits `routing:escalation-exhausted` for * steward escalation. No-op when no EscalationState is injected. */ recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void; private selectTarget; /** * Translate the request's declared constraints into {@link SelectConstraints}. * `policy.privacyFloor`, the provider allowlist (AMR Phase 5, D3), and the * per-request capability needs are threaded in. `providerOf` is derived * unconditionally in `fromConfig`, so the allowlist can never silently * fail-close every request (Phase-1 finding, capability-registry.ts:60-66). */ private buildConstraints; } /** * The lanes projection shape, re-exported so the orchestrator can hold a * read-back snapshot without importing the core `eventSourcing` namespace * directly — keeping this module the single orchestrator<->core lane coupling. */ type PersistedLanes = eventSourcing.LanesProjection; /** * Gate-failure feedback distillation. * * When a local staged unit is blocked by the enforced gate (typecheck / lint / * test), the failure output is threaded into the NEXT retry's prompt so the model * can fix it (per-phase retry feedback, #931). The naive approach — a head+tail * slice ({@link truncateGateOutput}) — is actively harmful for a modern test * runner: with many test files, the *head* is consumed by the passing-file tree * and the *tail* by the summary counts, so the one thing the model actually needs * — the failing tests' `Expected`/`Received` assertion diffs, which sit in the * MIDDLE — is exactly what gets dropped. Observed live (af6, #220): the model * reached 264/267 passing and could not close 3 specific failures across 7 gated * retries because it never saw *why* they failed, only their names. * * {@link distillGateFailure} instead extracts the failure-relevant regions — * tsc `error TS…` lines, per-test failure markers and their following assertion * diffs, lint `error` rows — plus the trailing summary, dropping the passing * noise. It is framework-general (tsc / eslint / vitest / jest) and degrades * gracefully: output with no recognizable failure markers falls back to the * head+tail behavior, and small output is returned verbatim. */ /** * Truncate captured gate output to a bounded size for the re-dispatch prompt * preamble (a full typecheck/test log can be enormous). Keeps the head + tail * so both the first error and the summary survive. Kept as the fallback for * output {@link distillGateFailure} cannot recognize. */ declare function truncateGateOutput(output: string, max?: number): string; /** * Derives the worktree seed paths from a workflow config when * {@link WorkspaceConfig.seedPaths} is not set explicitly. * * A new worktree is checked out from a committed remote ref and therefore lacks * the uncommitted artifacts of the brainstorm → orchestrator handoff. Seeding * carries them over: the proposal directory, plus the roadmap file at its * *configured* location — a roadmap tracker may point `filePath` somewhere * other than the default `docs/roadmap.md`, and the default seed list would * otherwise miss it. */ declare function deriveSeedPaths(config: WorkflowConfig): string[]; /** * Resolve a maintenance `checkCommand` (or housekeeping command) into a runnable * argv. Built-in maintenance task definitions store the command as harness * SUBCOMMAND argv (e.g. `['check-arch']`, `['graph','scan']`); `main-sync` * carries an explicit leading `'harness'` literal. Either way the command must * be executed through the `harness` binary, so: * - `['check-arch']` → `['harness', 'check-arch']` * - `['harness','sync-main']` → `['harness', 'sync-main']` (no double-prefix) * - `[]` → `[]` * * The cron daemon resolves `harness` from PATH (the pre-existing assumption that * `main-sync` already relied on). */ declare function normalizeHarnessCommand(command: string[]): string[]; /** * The production default verify runner for the local enforced gate. It is * language-aware: {@link detectEcosystem} classifies the workspace by the * lockfiles/manifests present, and the runner executes THAT ecosystem's verify * command set, short-circuiting on the first red gate. For a node workspace it * runs the project's own scripts (typecheck + lint + test) via `pnpm run`, scoped * to the changed packages; for a Python/Rust/Go/Ruby/Java workspace it runs that * toolchain's commands (e.g. `pytest`, `cargo test`, `go test ./...`) at the root * so verify no longer fails ENVIRONMENTALLY for non-JS adopters. Adopter-portable: * the node path only runs the scripts that exist, and an unrecognized workspace / * missing package.json → a passing gate (nothing to check). Fully self-contained; * tests inject a fake via the `verifyRunner` seam so this concrete detector is * never exercised in unit tests. */ declare function changedWorkspacePackages(porcelain: string): string[]; /** * Verify each changed package's own build→typecheck→lint→test, scoped to the * package and short-circuiting on the first failure. Extracted from * {@link defaultLocalVerifyRunner} so the build-first ordering + short-circuit are * unit-testable via an injected `run` (production passes an execFile-backed pnpm * runner; tests pass a fake that records the command sequence). * * BUILD runs BEFORE lint/test: a package whose lint/test consume its OWN compiled * output — e.g. an eslint-plugin whose flat config dogfoods its built `dist`, or an * integration test importing the package entry — otherwise fails to resolve a * just-added source module ("Cannot find module './src/rules/…'") on a freshly * `pnpm install`ed-but-unbuilt worktree. That would block CORRECT code on a stale * dist rather than a real quality defect. `${name}...` builds the package and its * workspace deps. CI builds before lint/test; the local gate must match. */ declare function verifyChangedPackages(changedPkgs: readonly string[], readPkg: (dir: string) => { name?: string; scripts: Record; }, run: (args: string[]) => Promise<{ ok: boolean; output: string; }>): Promise<{ ok: boolean; output: string; }>; declare function defaultLocalVerifyRunner(workspacePath: string): Promise<{ ok: boolean; output: string; }>; /** * local-backend-full-workflow (Blocker 2b): the production default diff runner * for the local enforced gate's empty-diff halt. Reports whether the agent * produced ANY change in the workspace by running `git status --porcelain` over * `workspacePath` and treating non-empty (trimmed) output as changes. * * Files neutralized via `git update-index --skip-worktree` (the workspace-scan * neutralization) correctly do NOT appear in `status --porcelain`, so they never * mask a truly-empty diff. Fully self-contained; tests inject a fake via the * `diffRunner` seam so this concrete detector is never exercised in unit tests. * On any git error it fail-OPEN (`hasChanges: true`) so a detection failure never * blocks a genuine change — the verify + outcome-eval stages remain the gate. */ declare function defaultLocalDiffRunner(workspacePath: string): Promise<{ hasChanges: boolean; }>; /** * Default doc-coverage runner: fail the gate when a newly-ADDED public source file in * the workspace is not referenced anywhere under `docs/` (see {@link findUndocumentedAdditions}). * Gathers added files via `git` and the docs corpus via a shallow read of `docs/**\/*.md`, * then defers the decision to the pure gate. Fail-OPEN on any IO error — a scan failure * must never block a genuine change (verify + outcome-eval remain the real gate). */ declare function defaultLocalDocCoverageRunner(workspacePath: string): Promise<{ ok: boolean; output: string; }>; /** * staged-verify-gate-convergence (S2): the wall-clock bound applied to the local * settle gate's mechanical step (the acceptance command AND the verify runner). An * un-bounded gate command would hang `settleWorkflowSuccess` — and thus the entire * tick — indefinitely. 10 minutes is generous for a scoped typecheck+lint+test or a * declared acceptance command while still guaranteeing forward progress; a timeout * is treated as a gate FAIL (block → retry/escalate), never a silent pass. */ declare const LOCAL_GATE_TIMEOUT_MS: number; /** * staged-verify-gate-convergence D2: the production default acceptance runner. Runs * an operator-declared shell `command` in `workspacePath` and reports pass/fail from * its exit code (0 ⇒ ok). The command is config-driven (`StagedWorkflowDecl.acceptance`) * so NOTHING project-specific is baked in here. Mirrors the `verifyRunner` seam so * the settle gate is decoupled from the concrete spawner; tests inject a fake via the * `acceptanceRunner` seam so this concrete spawner is never exercised in unit tests. * A non-zero exit (or spawn error) is a FAIL — the acceptance command IS the gate, so * an un-runnable command must block (never silently pass), mirroring the verify floor. */ declare function defaultLocalAcceptanceRunner(workspacePath: string, command: string, timeoutMs?: number): Promise<{ ok: boolean; output: string; }>; /** * staged-verify-gate-convergence D3: default bound on consecutive staged-settle * gate failures before a local unit escalates to the needs-human terminal. A module * constant for now (config wiring deferred — see the proposal's `maxLocalStageRetries` * follow-up); the settle path reads `config.agent.routing?.maxLocalStageRetries` when * present, else this default. */ declare const DEFAULT_MAX_LOCAL_STAGE_GATE_RETRIES = 5; /** * The central orchestrator that manages the lifecycle of coding agents. * * It polls an issue tracker for candidate tasks, manages ephemeral workspaces, * runs agents to resolve issues, and updates the tracker with progress. * * @fires Orchestrator#state_change Emitted when the internal state machine transitions * @fires Orchestrator#agent_event Emitted when an agent produces an output or thought */ declare class Orchestrator extends EventEmitter { #private; private state; private config; private tracker; private workspace; private hooks; /** * Spec 2 SC30 / Task 11: per-dispatch backend factory replaces the * Phase 1 `runner` / `localRunner` two-runner split. Each * `dispatchIssue()` call asks the factory for a `RoutingUseCase`-routed * `AgentBackend`, then wraps it in a fresh `AgentRunner`. * * `AgentRunner` is stateless (just `{ backend, options }`), so * per-dispatch construction is safe and avoids the cross-call state * the old two-runner split had to coordinate. * * Null only in the legacy fallback path: when `migrateAgentConfig` * throws (legacy configs missing supplemental fields, e.g. * `agent.backend='anthropic'` with no `agent.model`) AND no * `overrides.backend` is supplied, factory construction is skipped to * preserve the prior behavior of failing at dispatch time rather than * construction time. Eliminating this fallback is autopilot Phase 4+. */ private backendFactory; /** * AMR Phase 3 (D11): opt-in adaptive router. Constructed ONLY when * `agent.routing.policy` is present and non-empty; `null` otherwise so * dispatch stays byte-identical on the shipped `BackendRouter` * (SC8/SC17/SC19). Exposed for tests via {@link getAdaptiveRouter}. */ private adaptiveRouter; /** * Spec B Phase 4 (D8): per-orchestrator in-process bus for * `RoutingDecision` events. Constructed alongside backendFactory when * agent.backends synthesis succeeds; null when legacy single-backend * config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard) * subscribe via `getRoutingDecisionBus()`. */ private routingDecisionBus; /** * Test-only: when overrides.backend is provided, dispatch uses this * instance directly (bypassing the factory). Mirrors Phase 1 * `overrides.backend → this.runner.backend` behavior so existing * MockBackend-injection tests keep working without touching the * factory's routing path. */ private overrideBackend; private renderer; private promptTemplate; /** * Backend-aware local dispatch template (Phase 1). Set from * `overrides.localPromptTemplate` (production: threaded by the CLI from * WorkflowLoader). Undefined -> resolvePromptTemplate falls back to the * default template (SC5). */ private localPromptTemplate; /** * local-backend-full-workflow Phase 2 (Option C): the verify runner the * local-only enforced gate (`runLocalWorkflowGate`) invokes to run the * project's mechanical gate (typecheck + lint + test) over the workspace. * Injected in tests to force fail→pass sequences; in production it defaults * to `defaultLocalVerifyRunner` (a thin project-script probe). Kept as a * field seam — mirrors how `execFileFn` is injected — so the completion path * is decoupled from the concrete detector. */ private verifyRunner; /** * local-backend-full-workflow (Blocker 2b): the diff runner the local-only * enforced gate invokes BEFORE verify to detect an empty diff (the agent * completed without implementing anything). Injected in tests to force * has/has-no-changes; in production it defaults to `defaultLocalDiffRunner` * (a `git status --porcelain` probe). Mirrors the `verifyRunner` seam so the * completion path is decoupled from the concrete detector. */ private diffRunner; /** * doc-coverage gate: after verify passes, block a change that ADDS a new public * source file without referencing it under `docs/` — so the local gate matches a * real ship (the repo's doc-drift check) instead of stopping at typecheck+lint+test. * Injected in tests; defaults to {@link defaultLocalDocCoverageRunner}. Fail-OPEN. */ private docCoverageRunner; /** * staged-verify-gate-convergence D2: the acceptance runner the settle gate * invokes when a matched workflow decl declares an `acceptance` command. Runs * that operator-declared command in the workspace and gates on its exit code, in * place of `verifyRunner`. Injected in tests; defaults to * `defaultLocalAcceptanceRunner`. Mirrors the `verifyRunner`/`diffRunner` seams. */ private acceptanceRunner; /** * Phase 2: the most recent gate-failure reason per issue, threaded into the * next dispatch's rendered prompt as a failure preamble (the re-prompt). Set * when a local gate blocks; consumed + cleared at the next `dispatchIssue` * render for that issue. */ private priorGateFailureByIssue; /** * staged-verify-gate-convergence D3: per-unit count of consecutive staged-settle * gate failures. Incremented each time `settleWorkflowSuccess`'s local gate blocks * and re-dispatches; at `maxLocalStageGateRetries` the unit escalates to the * needs-human terminal (D3) instead of retrying again. Cleared when the unit * settles terminally or ships (a green gate), so a later re-pickup starts fresh. */ private localStageGateAttempts; /** * Resume-from-failed-stage checkpoint: per unit, the completed `checkpoint: true` * stage runs (stageIndex → StageRun) that survive gate-block re-dispatches so the * stable design (spec/plan) is reused instead of regenerated each retry. Cleared on * every terminal (ship or needs-human) alongside {@link localStageGateAttempts}, so a * fresh re-pickup regenerates the design. */ private stageCheckpoints; private server?; private interval?; /** * Shutdown latch for the self-rearming polling loop. `stop()` clears * {@link interval}, but the loop re-arms itself from INSIDE the tick's own * `.finally` (see `scheduleNextTick` in {@link start}) — so a `stop()` that lands * after the poll timer fired but before that tick settles was silently undone and * a NEW timer was armed on a stopped orchestrator (the loop kept polling, and the * refed timer kept the event loop alive, forever after shutdown). `stop()` raises * this latch and `scheduleNextTick` refuses to re-arm while it is up; `start()` * lowers it so a restart still polls. */ private stopRequested; private heartbeatInterval?; private logger; private interactionQueue; /** * Per-named-backend resolver map (Spec 2 SC37). Each `local`/`pi` entry * in `agent.backends` spawns one `LocalModelResolver`. Legacy * single-backend configs converge here via `migrateAgentConfig` (Task 9), * so this map is the single source of truth post-migration. */ private localResolvers; /** * Consumption Phase 1 (T2): bus listener that debounce-refreshes every local * resolver when a `local-models:pool` mutation fires, so a just-installed or * swapped model becomes usable within the refresh window instead of waiting up * to `probeIntervalMs` for the next poll. Held for removal in {@link stop}. */ private poolRefreshListener; /** Phase 4 (D5): pool-state port shared by all local/pi resolvers. Null when LMLM disabled. */ private poolStateProvider; private poolStateStore; /** * LMLM Phase 6: live model pool + its installer. Constructed only when * `localModels.enabled` and a real `PoolStateStore` exists (not a test * override). Exposed to the server via `getModelPool()`, which retires the * proposals-route 501 stub for `kind: 'model'` approve/reject. Null when LMLM * is disabled. `PoolManager` reads `store.snapshot()` lazily, so constructing * before `store.load()` (in initLocalModelAndPipeline) is safe. */ private modelPool; private modelInstaller; /** * S1 drain re-entrancy guard (P7-SUG-DRAIN-REENTRANCY). `drainDeferredEvictions` * is fired fire-and-forget from `emitWorkerExit` (and, since P7-SUG-DRAIN-LIVENESS, * piggybacked on each refresh tick). Two overlapping drains would both read the * same `listPendingEvictions()` snapshot, both re-check `isLocalModelInUse`, and * both `await pool.evict` the SAME name — double-calling the installer and * broadcasting a duplicate `evict_completed` frame. The single-threaded event * loop makes a plain boolean sufficient: a drain that arrives while one is * running returns early rather than double-processing. */ private draining; /** * LMLM Phase 6: single per-instance background refresh scheduler. Started in * `initLocalModelAndPipeline` when the pool exists; stopped in `stop()`. Null * when LMLM is disabled. Exposed to the server via `getRefreshScheduler()`. */ private refreshScheduler; /** * LMLM Phase 7: the hardware-aware recommender bound at scheduler start. Reused * by `GET /api/v1/local-models/recommendations`. Null when LMLM is disabled (no * pool → scheduler never armed). Seeded from the bundled frozen snapshot at * scheduler start and re-seeded with live HuggingFace candidates by * `refreshCandidatesLive` (startup + operator Refresh) — see `seedRecommender`. */ private modelRecommender; /** * The candidate set the current `modelRecommender` was seeded over. Held so the * harness-fit probe's `reRankWithBuildQuality` can re-run the SAME ranking path over * these candidates augmented with probed `buildQuality` (harness-fit-probe D5). */ private recommenderCandidates; /** Live HF candidate discovery (injectable for tests so startup makes no network calls). */ private readonly discoverCandidatesFn; /** Snapshot of the last candidate seeding, surfaced to the refresh route. */ private candidateSourceState; /** Test seam: injected timer/clock for the scheduler so no real 24h timer runs. */ private readonly schedulerTimerOverride; /** * Spec B Phase 3: skill catalog (name + cognitiveMode) read once at * construction from `projectRoot/agents/skills/`. Consulted by * `buildRoutingUseCase` at dispatch start to construct * `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases. * Empty when the orchestrator runs outside a harness project root * (then dispatch falls through to per-tier, preserving F11/N2). */ private readonly skillCatalog; /** * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5 * (SC39): each local/pi resolver gets its own listener emitting a * `NamedLocalModelStatus` event tagged with `backendName` + `endpoint`. * The previous single-resolver field (`localModelStatusUnsubscribe`) * is replaced by this list so multi-local configs can teardown all * listeners on `stop()` without a Map mutation. */ private localModelStatusUnsubscribes; private pipeline; /** * AMR live-classifier provider (final-review finding #2). The complexity * cascade may spend a fast-tier LLM tie-break; it borrows the SEL-layer * AnalysisProvider. Built lazily on first classify (the AdaptiveRouter is * constructed before start(), so this cannot be eager); `null` means "no * provider — cascade stays fully offline / static-only". `undefined` means * "not yet resolved". */ private complexityProvider; private analysisArchive; private graphStore; private claimManager; private prDetector; private maintenanceScheduler; private maintenanceReporter; private webhookFanoutOff?; private webhookQueue?; private webhookDeliveryWorker?; private waypointBridgeOff?; private cacheMetrics?; private otlpExporter?; private telemetryFanoutOff?; private notificationsRegistry?; private notificationFanoutOff?; private orchestratorIdPromise; private recorder; /** Black-box for this run (one process lifetime). Null until constructed; all calls best-effort. */ private flightRecorder; private readonly flightRunId; private intelligenceRunner; private completionHandler; /** Project root directory, derived from workspace root. */ private get projectRoot(); private enrichedSpecsByIssue; /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */ private analysisFailureCache; /** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work. * The PID is stored here because the running entry may be deleted by the state machine * before the stop effect executes (e.g., stall_detected removes the entry first). */ private abortControllers; /** Guards against overlapping ticks when a tick takes longer than the polling interval */ private tickInProgress; /** Phase 4 (DLane-5): lanes read back from the durable log on first tick, for * observability only — NOT fed into reconciliation. Empty until the first tick. */ private persistedLanes; /** Ensures the lane read-back diagnostic runs at most once (first tick). */ private laneReadbackDone; /** Timestamp of the last stale branch sweep (at most once per hour) */ private lastBranchSweepMs; /** Current tick-phase activity visible to the dashboard */ private tickActivity; /** * Creates a new Orchestrator instance. * * @param config - The workflow configuration * @param promptTemplate - The template used to generate agent instructions * @param overrides - Optional dependency overrides for testing or custom behavior */ constructor(config: WorkflowConfig, promptTemplate: string, overrides?: { tracker?: IssueTrackerClient; backend?: AgentBackend; execFileFn?: ExecFileFn$1; poolState?: PoolStateProvider; /** LMLM Phase 6 test seam: inject the RefreshScheduler timer/clock. */ schedulerTimer?: { setTimer?: (cb: () => void, delayMs: number) => SchedulerTimerHandle; clearTimer?: (handle: SchedulerTimerHandle) => void; now?: () => number; random?: () => number; }; /** Live-candidate-discovery seam: tests inject a fake so startup makes no HF calls. */ discoverCandidates?: (opts: DiscoverCandidatesOptions) => Promise; /** Phase 1: backend-aware local dispatch template. Undefined -> fallback. */ localPromptTemplate?: string; /** * Phase 2 (Option C) test seam: the verify runner the local enforced * gate invokes. Injected so tests can force fail→pass sequences without * spawning real typecheck/lint/test. Defaults to `defaultLocalVerifyRunner`. */ verifyRunner?: (workspacePath: string) => Promise<{ ok: boolean; output: string; }>; /** * Blocker 2b test seam: the diff runner the local enforced gate invokes * to detect an empty diff before verify. Injected so tests can force * has/has-no-changes without a real git tree. Defaults to * `defaultLocalDiffRunner`. */ diffRunner?: (workspacePath: string) => Promise<{ hasChanges: boolean; }>; /** Test seam for the doc-coverage gate; defaults to `defaultLocalDocCoverageRunner`. */ docCoverageRunner?: (workspacePath: string) => Promise<{ ok: boolean; output: string; }>; /** * staged-verify-gate-convergence D2 test seam: the acceptance runner the * settle gate invokes for a decl with an `acceptance` command. Injected so * tests can force pass/fail without spawning a real command. Defaults to * `defaultLocalAcceptanceRunner`. */ acceptanceRunner?: (workspacePath: string, command: string) => Promise<{ ok: boolean; output: string; }>; }); /** * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout. * Only fires when the operator configures `telemetry.export.otlp` in * harness.config.json. Extracted from the server-init block in the * constructor to keep that block's cyclomatic complexity under threshold. */ private setupTelemetryExport; /** * Deprecated alias for /api/v1/local-model/status (Spec 1 endpoint retained * as a compat shim per spec line 35; superseded by getLocalModelStatuses for * the multi-local UI). Returns the first-registered resolver's status. */ private getFirstLocalModelStatus; /** * SC38: build NamedLocalModelStatus[] from each registered resolver, tagged * with its backendName + endpoint from the config. */ private buildLocalModelStatuses; /** * S1 conservative in-use probe (ADR 0060). Returns `true` when ANY agent run * is live AND `ollamaName` is a currently-resolved (or last-detected) local * model — i.e. an agent could be routing inference to it right now. * * This signal is AGENT-RUN-COARSE, not per-request: `state.running` is keyed * by GitHub issue (spawned agent runs), NOT by inference call, and no * per-model request counter exists today. The probe therefore MAY OVER-DEFER * (a swap waits until the pool is idle). Over-deferral is exactly S1's * intended safe failure — never yank a model mid-request; occasionally wait * longer than strictly necessary. A fine-grained per-request signal is an * explicit deferred gap (ADR 0060). */ private isLocalModelInUse; /** * S1 drain (ADR 0060): complete any eviction that was DEFERRED because its * target was in use, now that the probe reports it idle. Best-effort — called * from the run-completion path; it never blocks dispatch and swallows * per-model errors (a failed or still-busy evict stays pending for the next * drain). `pendingEviction` is a transient overlay, so a missed drain simply * leaves the flag set until the next completion re-checks it. */ private drainDeferredEvictions; private createTracker; /** * Creates a TaskRunner for the maintenance scheduler. * CheckCommandRunner and CommandExecutor use real child_process execution. * AgentDispatcher remains stubbed (requires full skill dispatch integration). */ private createMaintenanceTaskRunner; /** * Initializes the maintenance subsystem: reporter, scheduler, and server route wiring. * Extracted from start() to keep function length under threshold. */ private initMaintenance; private createIntelligencePipeline; /** * AMR live-classifier provider resolution (final-review finding #2). The * complexity cascade's OPTIONAL fast-tier tie-break borrows the SEL-layer * AnalysisProvider (the same one intelligence enrichment uses). Resolved and * memoized on first classify because the AdaptiveRouter is constructed BEFORE * start(), so the provider cannot be resolved eagerly. * * Returns `undefined` when no provider is available (intelligence disabled, no * backendFactory, or the layer resolves to nothing) — the cascade then stays * fully offline and returns the static verdict (never throws). A build failure * degrades the same way: static-only, never blocks dispatch (D4). */ private resolveComplexityProvider; /** * Lazily initializes the ClaimManager if it hasn't been created yet. * Called from both start() and asyncTick() to avoid duplicating the init block. */ private ensureClaimManager; /** * SF5.2 (#1524) — resolve each leaf's served-comprehension attribution for the * live context-budget consult. Returns `undefined` (tick carries no * `prewarmSources`, byte-identical) unless a positive `agent.contextBudget` is * configured — the consult is a no-op without a budget, so no disk I/O is spent. * Each candidate resolves best-effort against the PROJECT ROOT's committed tree * (cheap-early-out when absent); a leaf with nothing fresh is omitted so the * reducer uses its floor-only estimate. */ private resolvePrewarmSources; asyncTick(): Promise; tick(): Promise; /** * Processes a side effect generated by the state machine. * * @param effect - The effect to handle */ private handleEffect; /** * Phase 4 (DLane-5): persist an orchestrator lane transition to the durable * core event log at the effect boundary. NEVER throws — `persistLane` returns * an `Err` Result on failure, which is logged and swallowed here so a * lane-persistence failure can never break orchestrator dispatch. */ private persistLaneSafe; /** * Phase 4 (DLane-5): read persisted task lanes back from the durable log and * log a one-line summary. Stores the projection on `this.persistedLanes` for * observability. Read-only — never feeds reconciliation, never throws. */ private readBackPersistedLanes; /** * Phase 4 (DLane-5): the task lanes most recently read back from the durable * log on startup, exposed for external observability. Read-only — a fresh * `{ tasks: {} }` until the first tick's read-back has run. */ getPersistedLanes(): PersistedLanes; /** * Guards workspace cleanup by checking whether the agent pushed a branch * that does not yet have a pull request. If so, the worktree is preserved * and an interaction is queued so a human can create the PR manually. */ private cleanWorkspaceWithGuard; /** Run the beforeRemove hook for a workspace. Failures are logged but non-fatal. */ private runBeforeRemoveHook; /** * Delegates to PRDetector.filterCandidatesWithOpenPRs, THEN drops any unit this * process has already deterministically shipped (IMPORTANT #2 durable guard). * * The open-PR filter alone is insufficient here: a just-shipped staged unit's PR * may not yet be visible to `gh` (propagation lag) and its roadmap row is still * `in-progress`, so absent the `#shippedThisRun` exclusion the tick would re-select * it once the transient `state.completed` grace window expires and RE-SHIP it. This * exclusion is the process-lifetime double-ship guard — analogous to how the * single-dispatch path's `markIssueComplete` durably removes the row from the * active-candidate set. * @see PRDetector#filterCandidatesWithOpenPRs */ private filterCandidatesWithOpenPRs; /** * Scans candidate issues for stale claims from other orchestrators. * An issue is considered stale if: * - It is in an "in-progress" state * - It has an assignee that is NOT this orchestrator * - Its updatedAt timestamp exceeds the heartbeat TTL * * Stale claims are released so the issue becomes available on subsequent ticks. */ private releaseStaleClaims; /** * Handles an escalation effect by writing to the interaction queue and logging. */ private handleEscalation; /** * Handles a claim effect by calling claimAndVerify on the ClaimManager. * If claimed, proceeds to dispatch. If rejected, emits a claim_rejected * event to clean up the state machine. */ private handleClaimEffect; /** * Posts a structured comment on the GitHub issue when the orchestrator claims it. * Fire-and-forget: failures are logged but never block dispatch. */ private postClaimComment; /** * Posts a lifecycle event comment to the GitHub issue. * Supports: claimed, completed, released. * Fire-and-forget: failures are logged but never block the caller. */ private postLifecycleComment; /** * Phase 1 (local-backend-full-workflow): pick the dispatch template for * the resolved backend. `pi`/`local` backends get the bash-shaped local * template when one was loaded; every other backend — and any local * backend with no local template loaded (SC5) — gets the default. Pure * over (backendName, config.agent.backends, localPromptTemplate, * promptTemplate); unit-tested by orchestrator.template-resolution.test.ts. * * NOTE: the local template file (`harness.orchestrator.local.md`) carries a * full YAML frontmatter block, but that frontmatter is **intentionally * ignored** at dispatch — only the markdown body is loaded (WorkflowLoader * strips the frontmatter) and rendered here as the prompt. The frontmatter * exists solely so the file is a valid, self-documenting scaffold that * `harness init` can drop in; the orchestrator's configuration is always * read from the loaded `WorkflowConfig`, never from this template file's * frontmatter. */ private resolvePromptTemplate; /** * Dispatches a new agent to work on an issue. * * Within-run retries reuse the worktree: the first dispatch of an * identifier in a given process provisions a fresh worktree, and any later * dispatch of the SAME identifier within that run preserves it (via * {@link WorkspaceManager.ensureWorkspace}'s `preserve` option) so the * agent's uncommitted partial progress survives the retry. A restart * (empty {@link #dispatchedThisRun}) wipes and recreates on first dispatch. * `afterCreate` runs only on the fresh-create dispatch (D3), never on reuse. * * @param issue - The issue to resolve * @param attempt - The retry attempt number */ private dispatchIssue; private processAgentEvent; private awaitRateLimitClearance; private runAgentInBackgroundTask; /** * local-backend-full-workflow Phase 2 (Option C): the normal-exit completion * seam, extracted so the enforced-gate loop is directly testable. Runs the * LOCAL-ONLY enforced gate BEFORE the exit is treated as terminal; a red gate * routes through the SHIPPED `emitWorkerExit('error', …)` retry branch — the * re-dispatch IS the re-prompt (the next render threads the failure preamble), * and `checkRetryBudget` exhaustion queues `needs-human`. On a green gate (or a * non-local backend, where the gate is a no-op `{ ok: true }`), the existing * Claude/AMR verdict feeders run and the run completes normally — composition, * not replacement. */ private finalizeNormalCompletion; /** * local-backend-full-workflow Phase 2 (Option C): the LOCAL-ONLY enforced * gate. For a `pi`/`local` dispatch it runs the mechanical gate (verify = * typecheck+lint+test via the injected `verifyRunner`) and, on green, the * outcome evaluation (Task 7) against the workspace branch; a red result returns a * blocking `{ ok: false, reason }`. The completion path routes that reason * through `emitWorkerExit('error', …)` so the shipped state-machine retry * branch re-dispatches (the re-prompt) rather than marking the run complete. * * NON-local backends (Claude/AMR) get an unconditional `{ ok: true }` — this * gate never touches their completion path (D2 scopes enforcement to the * local path only; the AMR verdict feeders keep their existing behavior). * * Fully guarded: any thrown error → a CONSERVATIVE block `{ ok: false, * reason: 'gate error: …' }`, mirroring the shipped fail-safe pattern — a * gate that cannot run is treated as red (re-dispatch), never as a silent * pass that could ship a bad build. */ private runLocalWorkflowGate; /** * AMR 4c (ADR 0069): the sound single-agent quality-verdict feeder. On a normal * exit, when AMR is active, run a BASELINE-RELATIVE security scan of the lines * the agent INTRODUCED (only added lines — a pre-existing pattern never counts); * a NEW error-severity finding → `'quality-fail'`, which climbs the coherence * unit's escalation floor. Success stays `neutral` (returns `undefined`) — a * premature `'quality-pass'` would clear accumulating failures (ADR 0069). * * Fully guarded: any error (git/scan/parse) → `undefined` → `neutral`, so this * NEVER breaks completion. No-op (zero cost) when AMR is off, keeping the * dispatch path byte-identical. Staged workflows use their own per-stage gate * feeder; this is the single-agent equivalent. */ private deriveSingleAgentQualityVerdict; /** * The DISCRIMINATED single-agent feeder (#2221 prerequisite 1): the same control flow * as the collapsed wrapper above, but each `return` says WHICH situation it is — a * judged security defect, a judged-clean scan, a judged acceptance-eval verdict, or an * unjudged dispatch (AMR off / nothing to scan / an internal error). Escalation is * unchanged: every verdict collapses through `toOutcomeClass` to exactly the value this * method's predecessor returned. */ private deriveSingleAgentQualityVerdictDetail; /** * AMR 4c v2: opt-in LLM spec-satisfaction verdict. Gated on * `routing.policy.acceptanceEval.enabled` (HEAVY — one model call + latency per * exit, so separate from the always-on cheap security scan), a present spec, and * an available analysis provider. Runs the shared `OutcomeEvaluator` over the * introduced diff vs the spec's judgment section, reusing the SEL-layer provider * the live classifier already builds; a BLOCKING verdict (high-confidence * NOT_SATISFIED) → `'quality-fail'`. Conservative + fully guarded: no * spec / no provider / empty diff / any error → `undefined` (neutral). The mapper * only ever emits the negative, so success can never become a premature * `'quality-pass'`. An absent GraphStore falls back to an ephemeral one — the * evaluator's `execution_outcome` persistence is best-effort and never blocks. */ private deriveAcceptanceEvalVerdictDetail; /** * local-backend-full-workflow Phase 2 (Option C, SC4): the SHARED outcome-eval * core, lifted out of `deriveAcceptanceEvalVerdict` so BOTH callers reuse the * same `OutcomeEvaluator` engine over the introduced diff vs the spec's * judgment section. It does NOT gate on `adaptiveRouter !== null` or * `acceptanceEval.enabled` — those guards live in the AMR caller above; the * LOCAL gate (D2) always evaluates when a spec is present. Maps a * high-confidence NOT_SATISFIED to `'quality-fail'`; conservative + fully * guarded: no spec / no provider / empty diff / any error → `undefined`. */ /** * local-backend-full-workflow Phase 3 (D5/SC6): resolve the AnalysisProvider * for the outcome-eval gate. Caller-gated: ONLY the LOCAL gate caller consults * `agent.routing.workflowGates`. The AMR caller ALWAYS uses local SEL * (`resolveComplexityProvider`) so the AMR acceptance-eval path is byte-identical * (SC-neutral). When `workflowGates === 'primary'` on the local caller, resolve * from the primary (routing.default) backend; any miss degrades to local SEL * (fail-open — an unreachable stronger provider must NOT wedge the local gate; * see the fail-open note at the runLocalWorkflowGate call site). */ private resolveOutcomeEvalProvider; /** * The reasoner `AnalysisProvider` from the startup-derived analysis env * ({@link applyAnalysisEnv}). Lets the local outcome-eval gate judge on-device * without the full intelligence pipeline. Undefined when no analysis endpoint is * configured (⇒ the gate degrades to advisory, exactly as before). */ private resolveLocalAnalysisEnvProvider; /** * Build an AnalysisProvider from the PRIMARY (routing.default) backend for the * Phase-3 `workflowGates:'primary'` seam. Reuses the shipped * `buildAnalysisProvider` translator but forces the router to the default * backend. Returns undefined (→ caller degrades to local SEL) when intelligence * is disabled, the factory is absent, or the default backend cannot produce a * provider — fully guarded, never throws. */ private resolvePrimaryOutcomeEvalProvider; /** * Resolve an {@link AnalysisProvider} bound to the REASONER (thinking) backend — * `routing.modes.thinking`, the model used for design/plan/review. Mirrors * {@link resolvePrimaryOutcomeEvalProvider} but targets the thinking backend and does * NOT require the intelligence pipeline to be enabled (a local ollama provider only * needs its model loaded). Returns the provider + its name, or undefined (→ no * advisory) when unconfigured/unavailable. Guarded; never throws. */ private resolveReasonerProvider; /** * Unload the coder (execution model) from Ollama so the reasoner unstick call has a * free GPU (see {@link resolvePeerUnloadTarget}). Best-effort and never throws — a * miss just leaves the coder resident and the reasoner call takes its chances. */ private unloadPeerModelBestEffort; /** * Reasoner unstick advisory: when the local executor has STALLED (repeated gate * failures with retry budget left), ask the thinking model to diagnose the failure * and prescribe a concrete fix, returning a preamble to prepend to the coder's next * retry feedback. Best-effort — any miss (no reasoner, provider down, bad response) * returns undefined so the normal retry proceeds unchanged. Never throws. */ private maybeReasonerUnstickAdvisory; private evaluateOutcomeCore; /** * The DISCRIMINATED outcome-eval core (#2221 prerequisite 1). Same control flow as the * collapsed `evaluateOutcomeCore` wrapper above; each `return` distinguishes an eval * that JUDGED (defect / clean) from one that never ran (`eval-declined`: no spec on * disk, no provider, an empty introduced diff) and from a guarded internal error. All * non-defect paths still collapse to `undefined`, so the local gate is unchanged. */ private evaluateOutcomeDetail; /** * Roadmap Auto-Triage Phase 4 (SC1–SC3, SC5, SC7): the post-diff routing * retrospective — a SIBLING quality-verdict source to the 4c feeder above, * extending (not replacing) the proven escalation path. On a normal exit, when AMR * is live AND auto-triage is enabled AND this unit carries a stored pre-dispatch * prediction, it classifies the ACTUAL introduced diff at full strength * (phase:'post-diff') and grades it against that prediction: * - MATCH → records the graded outcome + annotates the PR "AI autonomous — * verify" (stage-2 human-verify handling, SC4) → returns `undefined` * (neutral: a match is not an escalation). * - MISMATCH (diff exceeded prediction, or a missing/garbled prediction, or ANY * error) → records the outcome (when a shape is known) + returns * `'quality-fail'`, which climbs the coherence unit's escalation floor * via `recordAmrOutcome`; exhaustion queues `needs-human` (SC3/SC7). * * Fail-safe & guarded (SC7): an error never a silent pass — it takes the MISMATCH * (block+escalate) path. No-op (byte-identical) when AMR is off, auto-triage is * off, or the unit was not dispatched through triage (no stored prediction) — the * last means an ordinary non-triaged run is never graded. */ private deriveRoutingRetrospectiveVerdict; /** * The DISCRIMINATED retrospective (#2221 prerequisite 1). The shipped feeder returned * `'quality-fail'` for a JUDGED mispredict AND for three FAIL-SAFE blocks (unreadable * store on a spec-bearing unit, missing/garbled prediction, internal error), so an * operator whose unit escalated could not tell bad code from a store hiccup. Each * `return` now names its situation; the collapse (`fail-safe` ⇒ escalate, exactly as * before) keeps SC3/SC7 behavior identical. */ private deriveRoutingRetrospectiveVerdictDetail; /** * v1 stage-2 match handling (SC4): annotate the unit's PR/issue with a "verify this * autonomous change" note so a human reviews every autonomous PR. Best-effort — a * missing tracker config / token / a failed comment never breaks completion (the * grade already recorded above). Mirrors `postLifecycleComment`'s adapter wiring. */ private annotateRetrospectiveMatch; /** * Informs the state machine that an agent worker has exited. */ private emitWorkerExit; /** * AMR Phase 4 (D10/SC16): feed a dispatch outcome into vertical escalation. * No-op unless an AdaptiveRouter is live (policy present) AND this dispatch was * AMR-routed (a `lastRoutedTier` was captured). Transport outcomes never reach * `recordOutcome` — the shipped per-model breaker owns those, so the two signals * never double-count. The coherence unit is the issue id (D6 issue-grain pinning). */ private recordAmrOutcome; /** * AMR steward-escalation seam (D10, findings item 1 + 2). Queues a `needs-human` * interaction for a coherence unit whose routing hard-failed — either the vertical * escalation exhausted the `strong` ceiling (`escalation-exhausted`) or the * fail-closed selector left no compliant backend (`privacy-no-match`). Both ride * the SAME `needs-human` mechanism as every other escalation. The `RoutingError` * code disambiguates the two on the steward's channel. The coherence unit is the * issue id (D6 issue-grain pinning); title/description are recovered from running * state when still present. Fire-and-forget + `.catch` — a queue write must never * block or throw out of the dispatch/outcome path. */ private escalateRoutingToHuman; /** * AMR dispatch-boundary routing-failure handler (finding #3 + live-wiring * review blocker). When `AdaptiveRouter.route()` throws a fail-closed * `PrivacyNoMatch` (`RoutingError` code `'privacy-no-match'`) at dispatch, that * distinct signal MUST NOT be swallowed by the generic transport/dispatch-error * path (S4-001): it is not a runner/transport failure, so it must never be * recorded as one or feed the vertical escalation breaker. Instead it emits a * DISTINCT `routing:no-tier-match` steward escalation (needs-human, same * mechanism as `onExhausted`) carrying the coherence unit + reason. * * It is ALSO deterministic — the `privacyFloor`/allowlist that emptied the * candidate set is config-driven, so re-dispatch would throw the SAME * `PrivacyNoMatch`. Therefore this path is TERMINAL: it drives the unit to the * `canceled` lane and removes it from `running`/`claimed` directly, rather than * routing through `emitWorkerExit('error')` (whose state-machine error branch * enqueues a retry whenever the retry budget is not yet exhausted — which would * re-dispatch, re-fail closed, and re-escalate up to `maxRetries` times). No * retry is scheduled, no transport outcome is recorded, and exactly one * needs-human escalation is queued. Fail-closed is preserved — `route()` already * refused to pick a non-compliant backend, and returning `true` here stops the * caller from falling through to any further routing. * * Returns `true` when the boundary CLAIMED the error (`privacy-no-match` or the * hard-cap `budget-exhausted`), so the caller returns without ANY * `emitWorkerExit`. Returns `false` for any other error (including * `escalation-exhausted`, which the `onExhausted` seam owns) so the generic * dispatch-error path runs unchanged. */ private handleRoutingFailure; /** * AMR live-wiring review blocker: terminally retire a unit whose dispatch failed * closed (`privacy-no-match`). Mirrors the terminal side of a worker exit — * remove the unit from `running` and release its `claimed` slot — then persist * the terminal `canceled` lane (`abandon`), matching how retry-exhausted * escalations settle. Crucially it does NOT run the state-machine `worker_exit` * reducer, so no `scheduleRetry` effect is emitted. Best-effort lane persistence * (`persistLaneSafe` never throws). No transport/escalation outcome is recorded — * that stays the sole job of the single `routing:no-tier-match` escalation already * queued by `escalateRoutingToHuman`. * * bug-fleet orchestrator-runloop-B: `route()` throws AFTER `dispatchIssue` * already ran `ensureWorkspace` (step 1) — the worktree for this attempt exists * on disk by the time this fail-closed path fires. Every OTHER terminal * completion path cleans it up (`settleWorkflowTerminal` calls * `cleanWorkspaceWithGuard`; a normal `worker_exit` emits the `cleanWorkspace` * effect) — this one didn't, leaking a git worktree per deterministic * privacy-no-match/budget-exhausted terminal. `cleanWorkspaceWithGuard` is * itself safe to call here: no branch was ever pushed (the agent never * started), so it falls straight through to `removeWorkspace`. */ private finalizeRoutingTerminal; /** * split-routing Phase 4 (D6/SC5) — terminal SUCCESS settle for a workflow unit. * The real `WorkflowEngineContext.emitWorkflowSuccess` forwards here (bound via * the context's `settleSuccess` dep in `dispatchIssue`). It reproduces the * `worker_exit`/`reason==='normal'` reducer BY HAND (state-machine.ts:457,467-474): * `running.delete` → `completed.set(now)` → `claimed.delete` → `cleanWorkspace` * effect → then persists the terminal `success` lane and emits one state change. * * It deliberately does NOT route through `emitWorkerExit`/`handleWorkerExit` * (completion/handler.ts): that fires the ISSUE-keyed `finishRecording(issueId, * attempt)` + `recordAmrOutcome`, but the engine already ran PER-STAGE recorders * (`stageAttemptKey(index, attempt)`) and per-stage `recordOutcome`. Going through * the worker-exit path would (a) finish a recording never started at the * issue-attempt key and (b) double-feed the escalation state. This is the ONE * hand-reproduced reducer sequence in Phase 4; the `worker_exit` reducer itself * stays untouched (Task 12 pins it) so the two remain in sync. * * `runs` are the per-stage records (best-effort telemetry; the per-stage cost is * already attributable via the recorders). Never throws — a success settle must * complete the single terminal transition (D6). * * staged-verify-gate-convergence D1/D3/D4 — for a LOCAL last-stage unit this * "success" is NOT taken at face value: BEFORE the reducer sequence above, the * unit routes through the SAME `runLocalWorkflowGate` the single-dispatch path * uses (empty-diff → verify/acceptance → outcome-eval). Gate FAIL → preserve + * re-dispatch (handleStagedGateFailure, lane blocked), bounded to * `maxLocalStageRetries` → then `settleWorkflowTerminal` (needs-human). Gate PASS * → `shipWorkspace` (commit + branch + PR) THEN the reducer sequence above (so * `cleanWorkspaceWithGuard` finds the pushed branch + PR and preserves it; the * PR-merge auto-dones the row). Non-local/primary units skip all of this and take * the reducer sequence unchanged — see ADR 0079/0080. */ private settleWorkflowSuccess; /** * staged-verify-gate-convergence D2 — recover the acceptance command for a unit * from the workflow decl that matched it. Delegates to `workflowFor` — the SINGLE * match authority `dispatchIssue` used (identical prefix+labels+`>= 2`-stage * semantics, including the `< 2`-stage fallback) — and reads the matched decl's * `acceptance` off the returned `WorkflowMatch`. Returns `undefined` when no * `>= 2`-stage decl matches or the matched decl declares no `acceptance` (⇒ the * settle gate uses `verifyRunner` unchanged). Pure; no side effects. */ private acceptanceCommandForIssue; /** * staged-verify-gate-convergence D4 — derive the PR title + body for a * deterministic ship of a converged local staged unit. Title is the issue * title (falling back to its identifier); body is a short summary noting the * autonomous local dispatch and that the acceptance gate passed. A `Closes #N` * trailer is appended ONLY when the issue's `externalId` carries a numeric * GitHub issue number (e.g. `github:owner/repo#42`) — never fabricated: without * a real number the trailer is omitted so the PR does not close an unrelated * issue. Pure; no side effects. */ /** * #1531 — build the machine-readable provenance trailer input for an autonomous * ship. Sourced entirely from live run context: the harness version as the * skill version, the flight-recorder run id, the executing backend's model + name, * and the work unit as the lane. Every optional field degrades to omission when * its context is absent — {@link appendProvenanceTrailer} tolerates that — so this * never throws. The `skill` is `orchestrator` (the autonomous ship path identity), * distinguishing the autonomous tier from interactive assistance. */ private buildShipProvenance; private buildShipPr; /** * staged-verify-gate-convergence D3 — the gate-FAIL branch of the staged settle, * extracted so `settleWorkflowSuccess` stays reviewable. PRESERVE the workspace * (do NOT cleanWorkspaceWithGuard, do NOT persistLane('success')): stash the * failure reason so the next dispatch's prompt gets the failure preamble (the SAME * map the single-dispatch path uses), then drive the retry through the SAME * `emitWorkerExit('error')` seam. #890's ensureWorkspace reuse accumulates work * across the preserved-workspace attempts. Bounded to `maxLocalStageRetries` * consecutive failures → the existing needs-human terminal (which cleans + * escalates). Behavior is identical to the pre-extraction inline branch. */ /** * Record a unit's disposition to the flight recorder ("black-box"). Best-effort: * a recorder failure must never break a dispatch, so it swallows everything. */ private recordFlightVerdict; private handleStagedGateFailure; /** * split-routing Phase 4 (D6/I1/SC5) — terminal FAILURE/safety-net settle for a * workflow unit. The real context's `finalizeWorkflowTerminal` forwards here * (bound via `settleTerminal`). Composed from the `finalizeRoutingTerminal` * pattern (`running.delete` + `claimed.delete` + `persistLaneSafe('abandon')`, * orchestrator.ts:2388-2394) PLUS a single `needs-human` escalation * (escalateRoutingToHuman-style queue push, :2301-2316) PLUS `cleanWorkspace` * (S5). It must NEVER rethrow — the engine's `catch` calls it on the I1 safety * net, so a throw here would defeat the single-exit guarantee. * * It is NOT a verbatim `finalizeRoutingTerminal` call (that lacks the needs-human * + cleanWorkspace the Phase-3 terminal contract pinned). Exactly one needs-human * per terminal transition. */ private settleWorkflowTerminal; /** * Hermes Phase 3: wire in-process notification sinks against the * orchestrator's event bus (`this`). A misconfigured sink (unknown kind, * missing env var) logs + skips rather than breaking startup — the * hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe * to the same topics as `wireWebhookFanout`; a slow Slack call cannot * block webhook delivery because the two paths fan out independently. */ private setupNotifications; /** * Stops execution for a specific issue. * * @param issueId - The ID of the issue to stop */ private stopIssue; /** * Dispatch a work item immediately, bypassing the normal tick → roadmap cycle. * Used by the dashboard's "Dispatch Now" action. */ dispatchAdHoc(issue: Issue$1): Promise; /** * Initialize the LocalModelResolver and intelligence pipeline. * * Runs the initial probe (so resolver state reflects server availability) * before constructing the intelligence pipeline. Subscribes the dashboard * broadcast stub to status changes. Called exactly once from start(). */ /** * LMLM Phase 6: construct the live `PoolManager` (Ollama installer + the * loaded pool-state store) and stash it for `getModelPool()`. Defensive * config fallbacks: `ollamaEndpoint → http://localhost:11434`. The pool reads * `store.snapshot()` lazily, so this runs safely at construction time before * `store.load()`. */ private initModelPool; /** * Resume installs interrupted by a restart. A proposal left `installing` had * its background `ollama pull` cut short when the orchestrator went down; the * pull is idempotent (ollama resumes from cached blobs), so we re-drive it. * Fire-and-forget with its own error isolation — a resumed multi-GB download * must not block startup, and a re-drive failure only logs. */ private redriveInterruptedInstalls; /** * LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk * budget + org/family allowlist) from `localModels.pool` to the live pool. * Called after `PoolStateStore.load()` so config wins over persisted bounds * (D2, declarative precedence). No-op when the pool is null (LMLM disabled) * or no `pool` block is configured, so it is safe to call unconditionally * on the startup path. */ private applyConfiguredPoolBounds; /** * LMLM Phase 6: arm the single background refresh scheduler over the live * pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs * hardware→recommend→reconcile(D12 drift)→diff→emit→score-writeback. * * The recommender is seeded here from the bundled frozen snapshot (offline-safe, * deterministic) filtered to the operator's org/family allowlist. Live HF * candidate discovery then runs in the background on startup (and on the operator * Refresh button) via `refreshCandidatesLive`, which swaps in a fresh recommender * over the discovered set so the tick and `GET /recommendations` emit real, * up-to-date swap proposals. */ private startRefreshScheduler; /** * Build the harness-fit probe deps bundle (harness-fit-probe D5, Task 3) — the * composition-root wiring that makes the probe actually FIRE when enabled. Returns * `undefined` (⇒ no probe; byte-identical prior tick behaviour) UNLESS * `config.localModels.harnessFit.enabled` is true. * * When enabled it constructs the three injected seams the pure policy needs: * - `runner` — a {@link HarnessFitProbeRunner} (orchestrator-owned; local-models * keeps only the interface), pointed at the discovered local endpoint. * - `cache` + `getLastProbeAt`/`setLastProbeAt` — a persistent * {@link HarnessFitCacheFileStore} under `~/.harness/local-models/` (buildQuality * cache AND cadence timestamp), mirroring the pool state store. * - `reRankWithBuildQuality` — a {@link createBuildQualityReRanker} binding that * re-runs the SAME ranker over the held candidate set with probed buildQuality * threaded in (no ranker-math duplication). * * It also does the config→deps field translation the reviewer flagged: * `cadenceMs → intervalMs`, `taskIds → tasks`, plus `cacheTtlMs`/`topN` pass-through. */ private buildHarnessFitDeps; /** (Re)build the recommender over `candidates` and record the seeding source. */ private seedRecommender; /** * Refresh ranking candidates live from HuggingFace, merge the curated * `ollamaName`/`family` tags from the frozen snapshot (so results stay * installable — decision A), and re-seed the recommender. Fail-closed: on any * error or an empty installable result, the current candidates stand. Runs a * `forceRefresh` tick so recommendations + proposals reflect the fresh set. * Used by both the startup background refresh and the operator "Refresh" button. */ private refreshCandidatesLive; /** Resolve the hardware profile for a refresh tick (operator override wins). */ private detectLmlmHardware; /** Map the on-disk model-proposal queue to F7 dedup pairs (open→pending, rejected→rejected). */ private lmlmDedupSource; private initLocalModelAndPipeline; /** * Starts the polling loop and the internal HTTP server. * Runs startup reconciliation to release orphaned claims before the first tick. */ start(): Promise; /** * Stops the orchestrator, clearing the polling interval and stopping the server. */ stop(): Promise; /** Update tick activity and broadcast the change to connected clients. */ private setTickActivity; /** * Returns a point-in-time snapshot of the orchestrator's internal state. */ getSnapshot(): Record; /** * Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and * Phase 7 (dashboard WS broadcast). Returns null when the legacy * single-backend config bypassed agent.backends synthesis. */ getRoutingDecisionBus(): RoutingDecisionBus | null; /** * AMR Phase 3 (D11): the opt-in adaptive router, or `null` when no * `routing.policy` is configured (the default-off path). Exposed for the * SC8/SC17/SC19 default-off proof: `null` here means dispatch stays on the * shipped `BackendRouter`, byte-identical, with no classify()/telemetry. */ getAdaptiveRouter(): AdaptiveRouter | null; /** * AMR Phase 3 (D11) / Phase 5 (D1): construct the opt-in AdaptiveRouter for a * policy. Extracted from the constructor so runtime ingestion * (`ingestRoutingPolicy`) builds a router IDENTICAL to the constructor's — * same live classify seam, strong-cap escalation-exhaustion hard-fail-to-human * (D10), and enriched-decision bus (SC9). Precondition: the routing subsystem * exists (`backendFactory` + `agent.backends` present); callers guard. */ private buildAdaptiveRouter; /** * AMR Phase 5 (D1/D5): ingest a routing policy pushed at runtime by the * Shuttle control plane (`PUT /api/v1/routing/policy`). Hot-swaps the live * router: * - empty policy (`{}` / no activating fields) → `adaptiveRouter = null` * (default-off restored, D5 — byte-identical dispatch resumes); * - an existing router → `setPolicy` (preserves the accumulated * `EscalationState` climbed floors — a policy edit must not reset them); * - no router yet → construct one from the pushed policy. * * The field-swap is atomic between `await`s (single-threaded): a dispatch that * already captured the router finishes on it; the next dispatch sees the new * policy. No-op-safe when the routing subsystem is absent (`backendFactory` * null) — the caller (`PUT` handler) reports 503 in that case, so this path is * reached only when routing is available. */ ingestRoutingPolicy(policy: RoutingPolicy): void; /** * AMR Phase 5 (D2): project the live router's enriched decision ring into the * Shuttle telemetry wire shape (`GET /api/v1/routing/telemetry`). Returns an * empty payload when routing is off (no router) — a safe, idempotent read. */ getRoutingTelemetry(): RoutingTelemetry; /** * AMR observability: the live operator status (budget spend-vs-cap, escalated * units, allowlist), or an inactive payload when AMR is off. Backs * `GET /api/v1/routing/status`. */ getRoutingStatus(): RoutingStatus; /** * Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator * dispatch path uses the factory-owned router directly; observability * routes (config / decisions) reach it through this accessor. Returns * null when the legacy single-backend config bypassed agent.backends * synthesis (no backendFactory built). */ getBackendRouter(): BackendRouter | null; /** * Spec B Phase 5: snapshot of the active RoutingConfig for the config * route and the trace route's bus-less router construction. Returns * null when the operator's harness.config.json carries no * `agent.routing` block. */ getRoutingConfig(): _harness_engineering_types.RoutingConfig | null; /** * Spec B Phase 5: snapshot of `agent.backends` for the config route * (existence annotations) and the trace route (bus-less router * construction). Returns null when no synthesized backends map exists * (legacy single-backend configs). */ getBackends(): Record | null; /** Returns the maintenance scheduler status, or null if maintenance is not enabled. */ getMaintenanceStatus(): MaintenanceStatus | null; } /** * Launches the Ink TUI for the given Orchestrator instance. * Returns a function to wait for the TUI to exit. * * @deprecated The TUI is maintained as a fallback for headless/SSH environments. * The web dashboard (served at port 8080) is the primary monitoring interface. */ declare function launchTUI(orchestrator: Orchestrator): { waitUntilExit: () => Promise; }; /** Result of running the acceptance/convergence command in the workspace. */ interface AcceptanceOutcome { /** Process exit code; 0 ⇒ converged. */ exitCode: number; } /** A throwaway workspace: an absolute path plus its cleanup. */ interface ProbeWorkspace { path: string; cleanup: () => Promise; } /** * Injectable seams for {@link HarnessFitProbeRunner}. Every seam has a real * default; tests override them to run WITHOUT a live Ollama server or real * temp-dir pollution. */ interface HarnessFitProbeRunnerDeps { /** Build the candidate backend for `model`. Default: the native OllamaBackend. */ createBackend?: (model: string) => AgentBackend; /** Ollama endpoint for the DEFAULT backend factory. Default {@link DEFAULT_OLLAMA_ENDPOINT}. */ endpoint?: string; /** * Run the acceptance command in `cwd`; exit 0 ⇒ converged. Default: spawn it. The * optional `signal` aborts the spawn on the overall probe timeout — the default * runner kills the child process group so a hanging command cannot outlive the probe. */ runAcceptance?: (command: string, cwd: string, signal?: AbortSignal) => Promise; /** Create a throwaway workspace. Default: an OS temp dir removed on cleanup. */ makeWorkspace?: () => Promise; /** Seed `files` (workspace-relative) into `wsPath`. Default: mkdir + writeFile. */ seedFiles?: (wsPath: string, files: Record) => Promise; /** Bounded turn cap for the single dispatch. Default {@link DEFAULT_MAX_TURNS}. */ maxTurns?: number; /** * Overall per-probe wall-clock timeout in ms (dispatch + acceptance). Default * {@link DEFAULT_PROBE_TIMEOUT_MS}. On timeout the probe fails open (`error` result ⇒ * buildQuality undefined) so a hung model/acceptance can never stall the refresh tick. */ timeoutMs?: number; } /** * Concrete {@link HarnessFitRunner} (D3): one single dispatch of a candidate * model against a contained task in a throwaway workspace, judged by the task's * acceptance command, with act-vs-narrate metrics read from the recording stream. */ declare class HarnessFitProbeRunner implements HarnessFitRunner { private readonly createBackend; private readonly runAcceptance; private readonly makeWorkspace; private readonly seedFiles; private readonly maxTurns; private readonly timeoutMs; constructor(deps?: HarnessFitProbeRunnerDeps); /** * Run ONE contained probe of `model` against `task`. Fully guarded: any thrown * error — OR the overall wall-clock timeout — resolves to a `HarnessFitResult` * carrying `error` (fail-open, D6). The throwaway workspace is always cleaned up. * * The whole probe (dispatch + acceptance) races an `AbortSignal.timeout(timeoutMs)`: * `maxTurns` bounds the turn COUNT but not wall time, so without this a hung model or * a hanging `acceptanceCommand` would block the refresh tick indefinitely. On timeout * the acceptance spawn is aborted (its child process group is SIGKILLed by the default * runner) and the probe returns `error: 'probe timed out'`. */ runProbe(model: string, task: HarnessFitProbeTask): Promise; /** The actual dispatch + acceptance judgement (raced against the timeout). */ private dispatchAndJudge; /** Resolve to a fail-open `error` result when the probe's wall-clock timeout fires. */ private timeoutResult; /** * Drain the recording stream, counting tool calls (`tool_execution_start`) and * the DISTINCT files touched by mutating tools, plus token usage for provenance. */ private consumeStream; } /** * Build the pure `ProbeInput` from an `Issue`. Reuses `buildTaskText` for the pre-diff * text-only signals (title+description length, spec/acceptance hints, prompt) and * `extractEntities` for the candidate entity mentions the scope lever will try to resolve. * * Pure over the Issue — unit-testable without instantiating an Orchestrator. */ declare function buildProbeInput(issue: Issue): ProbeInput; /** * The concrete `GraphScope` seam over a loaded `GraphStore`. For each candidate it resolves * a node (name → path → file-id) and simulates the blast radius via `CascadeSimulator` * (`summary.totalAffected`). Returns `null` for unresolved candidates — the S3 contract: an * unresolved candidate does NOT count toward scope. Never throws out of `resolve` (the probe * also guards it, but we keep this defensive so one odd node can't sink the lever). */ declare function makeGraphScope(store: GraphStore): GraphScope; /** Injected dependencies for the wired triage over a single issue. */ interface TriageWiringDeps { /** The SEL analysis provider (from `buildAnalysisProviderForLayer('sel', …)`). */ provider?: AnalysisProvider | null; /** The loaded knowledge-graph store. Absent ⇒ scope degrades ⇒ everything holds. */ graphStore?: GraphStore | null; /** The precedent lever (Phase 4 wires the real one; absent ⇒ cold-start unknown). */ precedent?: PrecedentLookup; /** Gate seeds from `roadmap.autoTriage.thresholds`. */ config?: Partial; /** Optional model overrides threaded to the classifier tie-break. */ models?: { fast?: string; standard?: string; }; /** * Signals that a model is available but its levers were DEFERRED for this run (cheap-first). * Threaded to the probe so a deferred (not missing) provider is worded accurately. See * `ProbeDeps.modelDeferred`. */ modelDeferred?: boolean; } /** * Run the scoping probe over a single `Issue`: build the input, resolve the injected SEL * provider + graph seam, and invoke the pure probe. Returns the `TriageVerdict`. Never * throws — the probe itself is total; the only added surface here is graph-store adaptation, * which `makeGraphScope` keeps defensive. */ declare function triageIssue(issue: Issue, deps?: TriageWiringDeps): Promise; /** The human-only rubric — named triggers that MUST bias the model to low confidence. */ declare const BRAINSTORM_RUBRIC: string; interface SelForkGeneratorOptions { /** Samples per fork for the self-consistency check (default 3; min 2 to detect a flip). */ samples?: number; /** Optional model override threaded to the provider. */ model?: string; /** * Max tokens per sample (default 4096). Sized for reasoning models (Qwen3 et al.): they emit * a `` trace before the JSON fork step, so a tight cap truncates mid-reasoning → * `finish_reason: length` → a thrown provider error → the runner halts the fork as `error`. * `maxTokens` is a ceiling (a non-thinking model still stops early), so the headroom is free * on the fast path and only spent when reasoning occurs. */ maxTokens?: number; } /** * Build a real `ForkGenerator` over an `AnalysisProvider`. For each fork index the generator * samples the model N times; it accepts the fork (passing through the model's reported `high`) * only when EVERY sample agrees on the SAME fork identity (`forkId` + normalized `question`) * AND the SAME recommended option, AND that recommendation is non-empty AND is one of the * fork's offered `options`. Any drift, instability, or degenerate/off-menu recommendation * forces `confidence:'low'` (the runner then halts and a human owns the fork). The generator * NEVER throws for a well-formed provider; a provider error propagates and the pure runner maps * it to `halted{ reason:'error' }`. */ declare function makeSelForkGenerator(provider: AnalysisProvider, input: BrainstormInput, opts?: SelForkGeneratorOptions): ForkGenerator; interface BrainstormWiringDeps { /** * An explicit `ForkGenerator` to drive the brainstorm. INJECTED in tests (no live model). * When absent, `provider` is required and a real SEL generator is built. */ generator?: ForkGenerator; /** The SEL provider used to build the real generator when `generator` is absent. */ provider?: AnalysisProvider | null; /** Self-consistency / model options for the built generator. */ generatorOptions?: SelForkGeneratorOptions; /** * Docs root under which a completed spec is written (`/changes//proposal.md`). * Absent ⇒ no spec is written (dry run) but the outcome is still returned. */ docsRoot?: string; /** Re-score deps threaded to `triageIssue` (SC4). Absent ⇒ re-score is skipped. */ rescore?: { graphStore?: GraphStore | null; } & Pick; } /** The wired result: the pure outcome + (on completion) the written path + the re-score verdict. */ interface WiredBrainstormResult { /** The pure decision-core outcome (completed{spec} | halted{fork,reason}). */ outcome: BrainstormOutcome; /** Absolute path of the written spec, when a spec was completed AND `docsRoot` was set. */ specPath?: string; /** The re-score verdict over the enriched content (SC4), when the brainstorm completed. */ rescore?: TriageVerdict; } /** * Build a `BrainstormInput` from an `Issue` + its Phase-1 complexity level. Pure over the Issue. */ declare function brainstormInputFromIssue(issue: Issue, level: BrainstormInput['level']): BrainstormInput; /** * Run the autonomous brainstorm for one issue and, on a clean completion, write a * proposal-shaped spec + re-score the enriched item (SC4). The generator is injected (tests) * or built over the SEL provider (production). Never throws — the pure runner is total, and * the doc-write / re-score are guarded. */ declare function runBrainstormForIssue(issue: Issue, level: BrainstormInput['level'], deps: BrainstormWiringDeps): Promise; /** Slugify an item identity into a docs directory name. */ declare function slugFor(spec: SpecDraft): string; /** * Render a `SpecDraft` as a proposal-shaped markdown spec: a decision record where each * accepted fork becomes an "Approach" section (question → recommended option + rationale). */ declare function renderSpecMarkdown(spec: SpecDraft): string; /** * Produce an enriched `Issue` whose description carries the drafted spec's substance, so the * re-score (SC4) reads REAL content — the resolved decisions + rationale — not the original * one-line length proxy. Pure over the inputs. */ declare function enrichIssueWithSpec(issue: Issue, spec: SpecDraft): Issue; /** * One approved item to mark, bundling everything the two writes need: the go/no-go verdict * (must be `humanApproved`), the roadmap feature name (→ shard slug), the Phase-2 spec path * to attach, and the probe outputs for the prediction slice. */ interface TriageMarkItem { /** The go/no-go candidate. Only acted on when `candidate.humanApproved` is true (SC5). */ candidate: GoNoGoCandidate; /** The roadmap feature's display name — slugified to resolve its shard. */ featureName: string; /** Repo-relative path to the generated spec (Phase 2 output) to attach to the feature. */ specPath: string; /** Item labels (for the prediction's shapeKey bucket). */ labels: readonly string[]; /** The pre-diff complexity verdict from the probe (the prediction being made). */ verdict: ComplexityVerdict; /** Predicted blast radius (from the scope lever) — recorded for the retrospective. */ scopeEstimate: number; /** * The EVIDENCE-DERIVED autonomy stage this item's SHAPE earned (SC6), resolved * per-shape by the caller (`min(resolveStage(history), configuredCeiling, 2)`). * When present it is stamped onto the prediction INSTEAD of the uniform * `config.ratchetStage`, so the ratchet advances per-shape. Absent ⇒ the marker * falls back to `config.ratchetStage` (the Phase-3 uniform behavior). */ effectiveStage?: 1 | 2; } /** The feature-flag + ratchet surface the marker reads (default-off master switch). */ interface TriageMarkConfig { /** Master switch. `false` ⇒ complete no-op (SC7). */ enabled: boolean; /** Ratchet stage stamped onto the prediction (Phase 3 pins 1). */ ratchetStage: 1 | 2 | 3 | 4; } /** Injected prediction writer — the Phase-0 TriageRecord store's append. Fakeable in tests. */ type RecordPrediction = (payload: eventSourcing.TriagePredictedInput) => Promise<{ ok: boolean; error?: Error; }>; interface TriageMarkDeps { /** The roadmap store to patch (real via `resolveRoadmapStoreForFile`, fake in tests). */ store: RoadmapStore; /** Feature-flag + ratchet config. */ config: TriageMarkConfig; /** Prediction writer (Phase-0 store). */ recordPrediction: RecordPrediction; /** * The orchestrator's self-identity for the assignee gate. When provided, an item assigned * to anyone else is skipped (SC6). When omitted, the gate is inactive (call-sites without * identity — mirrors `candidate-selection.ts`'s `selfAssignee` optionality). */ selfAssignee?: string | null; } /** Why an item was skipped rather than marked (legible, closed set). */ type MarkSkipReason = /** The candidate was not human-approved (SC5) — should not reach the marker, guarded anyway. */ 'not-approved' /** The item is assigned to a non-self assignee (SC6) — never stolen. */ | 'assigned-elsewhere' /** The feature was not found in the roadmap (stale externalId / renamed). */ | 'feature-not-found'; interface MarkSkip { externalId: string; reason: MarkSkipReason; } /** The marker's outcome: which items were made eligible, and which were skipped + why. */ interface TriageMarkResult { /** externalIds of items successfully marked eligible (spec attached + prediction recorded). */ marked: string[]; /** Items intentionally not marked, each with the legible reason. */ skipped: MarkSkip[]; } /** * Mark human-approved items eligible for the existing orchestrator pickup, and record their * pre-dispatch predictions. Returns `Ok` with the mark/skip partition, or `Err` on the first * store write that fails (so a partial roadmap write surfaces rather than silently dropping). * * NOTE ON DISPATCH: nothing here dispatches. The orchestrator's own pickup loop selects the * now-spec-bearing, active-status item on its next tick and runs it through unchanged gating. */ declare function markApprovedForDispatch(items: readonly TriageMarkItem[], deps: TriageMarkDeps): Promise<{ ok: true; value: TriageMarkResult; } | { ok: false; error: Error; }>; /** The minimal stored-record shape the precedent aggregation reads (structural mirror). */ interface StoredOutcomeRecord { shapeKey: string; outcome?: { matched: boolean; }; } /** * The REAL `PrecedentLookup` (SC5) — closes the Phase-1 degrade-empty stub. Aggregates * the autonomous-success base-rate over recorded outcomes sharing a `shapeKey` via the * pure `aggregatePrecedent`. Bridges core's `StoredTriageRecord` (whose `outcome` slice * is structurally `{ matched }` for aggregation purposes) into the intelligence * aggregation. With no outcome-bearing records for a shape it returns `unknown` — the * exact Phase-1 cold-start behavior, now real once outcomes accrue. */ declare function precedentLookupFromStored(records: readonly StoredOutcomeRecord[]): PrecedentLookup; /** * Fail-closed signal: privacy floor / allowlist emptied the candidate set (S4-001). * Distinguishable from a tier/cost-only exclusion, which returns `undefined`. * * Finding #4 — ONE typed error family: `PrivacyNoMatch` stays orchestrator-local * (it is thrown deep in the selector, a layer below the exported types package) but * EXTENDS the exported `RoutingError`, carrying its canonical `'privacy-no-match'` * code. The dispatch boundary can therefore catch it as either `PrivacyNoMatch` * (ergonomic `instanceof`) or `RoutingError` (code-narrowed), and the sibling * `escalation-exhausted` code is emitted via `RoutingError` too — no divergent * surfaces, no dead exported error type. `code` narrows to the literal here. */ declare class PrivacyNoMatch extends RoutingError { readonly code: "privacy-no-match"; constructor(message: string); } interface SelectConstraints { privacyFloor?: PrivacyClass; /** Present ⇒ only these providers allowed. Absent ⇒ all allowed. Empty array ⇒ none. */ allowed?: BackendDef['type'][]; needsVision?: boolean; needsToolUse?: boolean; minContextTokens?: number; } /** * D1 core: filter the registry to backends with tier ≥ requiredTier, privacyClass * at least as strong as the floor, provider in the allowlist, and capabilities ⊇ * required (vision/toolUse/minContextTokens); sort by costPer1kTokens ascending; * return the cheapest. * * Fail-closed (S4-001): if a privacy-floor OR allowlist constraint empties the set, * throw PrivacyNoMatch (the item must surface to the steward — never fall through * to identity routing at a non-compliant backend). A tier/cost-only exclusion is * best-effort: return undefined so the caller can fall back to the shipped router's * identity/default chain. No `if (local)` anywhere. */ declare function selectCheapestQualifying(registry: BackendCapabilityRegistry, requiredTier: CapabilityTier, constraints: SelectConstraints, /** * Provider lookup by name, required to enforce a `constraints.allowed` allowlist. * If `allowed` is set but `providerOf` is absent (or returns `undefined` for an * entry), that entry cannot be admitted → excluded; an allowlist with no * `providerOf` therefore excludes ALL candidates and fails closed * (`PrivacyNoMatch`). Phase 3 (`AdaptiveRouter`) MUST derive this from * `agent.backends` whenever it sets an allowlist. Absent `allowed` ⇒ unused. */ providerOf?: (name: string) => BackendDef['type'] | undefined): { name: string; capabilities: BackendCapabilities; } | undefined; /** Default capability block derived for an LMLM pool candidate that carries no * explicit capabilities. On-device ⇒ strongest privacy, zero marginal cost. * Seed values (tunable in later phases); a candidate is thus visible to tier * selection (spec "Failure modes": a backend with NO capabilities is invisible, * but a pool candidate is always given a derived block so it can win on cost). */ declare function defaultPoolCapabilities(): BackendCapabilities; /** * Build the tier-selection registry (name → capabilities) from configured * `agent.backends` (their `capabilities` blocks, when present) merged with LMLM * pool candidates (each given a derived on-device/zero-cost block). A configured * backend WITHOUT a `capabilities` block is omitted — invisible to tier selection, * reachable only via identity routing (spec "Failure modes"). No LMLM code changes. */ declare function buildCapabilityRegistry(backends: Record, pool?: PoolStateProvider): BackendCapabilityRegistry; declare function estimateCost(def: BackendDef, _req: RoutingRequest): number; /** * Result of running `migrateAgentConfig`. * * `config` is the *effective* AgentConfig — either the input unchanged * (when `agent.backends` is already set or no migration is needed) or * the input augmented with synthesized `backends` and `routing` fields. * * `warnings` is a list of human-readable deprecation messages, one per * legacy field encountered. Each message names the deprecated field * (dotted path) and includes a pointer to the migration guide. The * orchestrator emits these as `warn`-level log entries at startup. */ interface MigrationResult { config: AgentConfig; warnings: string[]; } declare function migrateAgentConfig(agent: AgentConfig): MigrationResult; /** * Orchestrator-owned dependencies threaded into backend constructors. Today * only the prompt-cache recorder is plumbed (consumed by ClaudeBackend); other * backends accept the option without using it so a single recorder instance * can be shared across the dispatch tree. The recorder itself is instantiated * by the orchestrator at startup — Phase 5 Task 16. */ interface CreateBackendOptions { /** Optional prompt-cache recorder shared across Anthropic-capable backends. */ cacheMetrics?: CacheMetricsRecorder; /** * Available machine memory (GiB) the orchestrator prefills from * `detectHardware()` (VRAM / unified memory). When present and an ollama def * omits `maxContextTokens`, the factory derives the context cap from it via * {@link contextCapFromMemoryGb}. Keeping detection at the orchestrator * boundary lets `createBackend` stay pure/sync and the backend never import * `local-models`. */ hardwareMemoryGb?: number; /** * Policy-envelope + subprocess air-gap wiring, forwarded to subprocess-spawning * backends (currently `claude`). `sandboxMode`/`networkMode` populate the audit * record's policy; `policyAudit` is the governance sink; `subprocessEnvAllow` * extends the env allowlist. Other backends accept-but-ignore these. */ sandboxMode?: PolicySandboxMode; networkMode?: PolicyNetworkMode; policyAudit?: PolicyAuditSink; subprocessEnvAllow?: readonly string[]; } /** * Pure constructor: BackendDef -> concrete AgentBackend instance. * No side effects beyond the underlying class constructors. * Container wrapping (sandbox policy) is the orchestrator's job, not the factory's. * * `options.cacheMetrics`, when provided, is forwarded to backends that * record prompt-cache hits (currently `ClaudeBackend`). Other backends * accept-but-ignore the recorder. */ declare function createBackend(def: BackendDef, options?: CreateBackendOptions): AgentBackend; /** * Maintenance backend resolver: map a configured backend NAME to a live * {@link AgentBackend}, or `null` when the name is absent from the loaded * `agent.backends` map. `null` lets the real agent dispatcher no-op honestly * (graceful degradation on a plain checkout) instead of throwing. */ type BackendResolver = (backendName: string) => AgentBackend | null; /** * Build a {@link BackendResolver} from a synthesized `agent.backends` map. * * This is the ONE shared implementation of "resolve a backend name against a * backends map via `createBackend`, else null". It is consumed by BOTH: * - the cron orchestrator (`createMaintenanceTaskRunner`), passing * `this.getBackends()`, and * - the on-demand CLI (`harness maintenance run --fix` → * `makeResolveBackend`), passing the config it loaded from * `harness.orchestrator.md`. * * Keeping it in the orchestrator package (next to `createBackend`) avoids the * two call sites drifting. A `null`/`undefined`/empty map yields a resolver * that always returns `null` — the nothing-configured degradation case. */ declare function makeBackendResolver(backends: Record | null | undefined): BackendResolver; /** * Zod schema for `BackendDef` (Spec 2 — multi-backend routing). * * Discriminated union on `type`. Per-variant validation surfaces shape * mismatches (missing `model`, missing `endpoint`, etc.) at config-load * time rather than at orchestrator runtime. * * Used in Phase 3 by `validateWorkflowConfig`; in Phase 1 it is exported * for standalone unit testing. */ declare const BackendDefSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{ type: z.ZodLiteral<"mock">; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "mock"; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; }, { type: "mock"; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"claude">; command: z.ZodOptional; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "claude"; command?: string | undefined; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; }, { type: "claude"; command?: string | undefined; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"anthropic">; model: z.ZodString; apiKey: z.ZodOptional; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "anthropic"; model: string; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; }, { type: "anthropic"; model: string; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"openai">; model: z.ZodString; apiKey: z.ZodOptional; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "openai"; model: string; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; }, { type: "openai"; model: string; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"gemini">; model: z.ZodString; apiKey: z.ZodOptional; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "gemini"; model: string; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; }, { type: "gemini"; model: string; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"local">; endpoint: z.ZodString; model: z.ZodUnion<[z.ZodString, z.ZodArray]>; apiKey: z.ZodOptional; timeoutMs: z.ZodOptional; probeIntervalMs: z.ZodOptional; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "local"; endpoint: string; model: string | [string, ...string[]]; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; timeoutMs?: number | undefined; probeIntervalMs?: number | undefined; }, { type: "local"; endpoint: string; model: string | [string, ...string[]]; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; timeoutMs?: number | undefined; probeIntervalMs?: number | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"pi">; endpoint: z.ZodString; model: z.ZodUnion<[z.ZodString, z.ZodArray]>; apiKey: z.ZodOptional; timeoutMs: z.ZodOptional; probeIntervalMs: z.ZodOptional; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "pi"; endpoint: string; model: string | [string, ...string[]]; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; timeoutMs?: number | undefined; probeIntervalMs?: number | undefined; }, { type: "pi"; endpoint: string; model: string | [string, ...string[]]; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; timeoutMs?: number | undefined; probeIntervalMs?: number | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"ollama">; endpoint: z.ZodString; model: z.ZodUnion<[z.ZodString, z.ZodArray]>; apiKey: z.ZodOptional; timeoutMs: z.ZodOptional; maxTurnsPerRun: z.ZodOptional; disableReasoning: z.ZodOptional; numCtx: z.ZodOptional; maxContextTokens: z.ZodOptional; numPredict: z.ZodOptional; keepAlive: z.ZodOptional; temperature: z.ZodOptional; topP: z.ZodOptional; topK: z.ZodOptional; mcpServers: z.ZodOptional>; env: z.ZodOptional>; cwd: z.ZodOptional; tools: z.ZodOptional>; }, "strict", z.ZodTypeAny, { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }, { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }>, "many">>; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "ollama"; endpoint: string; model: string | [string, ...string[]]; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; timeoutMs?: number | undefined; maxTurnsPerRun?: number | undefined; disableReasoning?: boolean | undefined; numCtx?: number | undefined; maxContextTokens?: number | undefined; numPredict?: number | undefined; keepAlive?: string | undefined; temperature?: number | undefined; topP?: number | undefined; topK?: number | undefined; mcpServers?: { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }[] | undefined; }, { type: "ollama"; endpoint: string; model: string | [string, ...string[]]; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; apiKey?: string | undefined; timeoutMs?: number | undefined; maxTurnsPerRun?: number | undefined; disableReasoning?: boolean | undefined; numCtx?: number | undefined; maxContextTokens?: number | undefined; numPredict?: number | undefined; keepAlive?: string | undefined; temperature?: number | undefined; topP?: number | undefined; topK?: number | undefined; mcpServers?: { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }[] | undefined; }>, z.ZodObject<{ type: z.ZodLiteral<"codex">; model: z.ZodUnion<[z.ZodString, z.ZodArray]>; localProvider: z.ZodOptional>; command: z.ZodOptional; timeoutMs: z.ZodOptional; reasoningEffort: z.ZodOptional>; mcpServers: z.ZodOptional>; env: z.ZodOptional>; cwd: z.ZodOptional; tools: z.ZodOptional>; }, "strict", z.ZodTypeAny, { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }, { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }>, "many">>; capabilities: z.ZodOptional; costPer1kTokens: z.ZodNumber; privacyClass: z.ZodEnum<["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]>; contextWindow: z.ZodNumber; vision: z.ZodOptional; toolUse: z.ZodOptional; }, "strict", z.ZodTypeAny, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }, { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; }>>; }, "strict", z.ZodTypeAny, { type: "codex"; model: string | [string, ...string[]]; command?: string | undefined; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; timeoutMs?: number | undefined; mcpServers?: { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }[] | undefined; localProvider?: "ollama" | "lmstudio" | undefined; reasoningEffort?: "high" | "medium" | "low" | "none" | undefined; }, { type: "codex"; model: string | [string, ...string[]]; command?: string | undefined; capabilities?: { tier: "fast" | "standard" | "strong"; costPer1kTokens: number; privacyClass: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud"; contextWindow: number; vision?: boolean | undefined; toolUse?: boolean | undefined; } | undefined; timeoutMs?: number | undefined; mcpServers?: { name: string; command: string; args?: string[] | undefined; env?: Record | undefined; cwd?: string | undefined; tools?: string[] | undefined; }[] | undefined; localProvider?: "ollama" | "lmstudio" | undefined; reasoningEffort?: "high" | "medium" | "low" | "none" | undefined; }>]>; /** * Spec B Phase 0: a routing target is either a backend name (scalar * string) or a non-empty ordered fallback chain (string tuple). The * scalar form is byte-compatible with pre-Spec-B configs. */ declare const RoutingValueSchema: z.ZodUnion<[z.ZodString, z.ZodReadonly>]>; /** * Zod schema for `RoutingConfig`. `.strict()` rejects unknown keys at * every level (per Spec 2 D7: typos in routing keys are validation * errors, not silent default-fallthroughs). * * Spec B Phase 0: all scalar routing fields accept `RoutingValueSchema` * (scalar or non-empty chain). New optional `skills` and `modes` maps * accept the same. * * Spec B Phase 2: the `isolation` block (added to the TS interface in * Hermes Phase 5 but not previously in this Zod schema) is now included * here with each tier widened to `RoutingValueSchema`. This closes the * Phase 0 I2 review finding (TS-vs-Zod drift) and ensures isolation * chain entries are validated by the same cross-field check that * covers `skills` / `modes`. */ declare const RoutingConfigSchema: z.ZodObject<{ default: z.ZodUnion<[z.ZodString, z.ZodReadonly>]>; 'quick-fix': z.ZodOptional>]>>; 'guided-change': z.ZodOptional>]>>; 'full-exploration': z.ZodOptional>]>>; diagnostic: z.ZodOptional>]>>; intelligence: z.ZodOptional>]>>; pesl: z.ZodOptional>]>>; }, "strict", z.ZodTypeAny, { sel?: string | readonly [string, ...string[]] | undefined; pesl?: string | readonly [string, ...string[]] | undefined; }, { sel?: string | readonly [string, ...string[]] | undefined; pesl?: string | readonly [string, ...string[]] | undefined; }>>; isolation: z.ZodOptional>]>>; container: z.ZodOptional>]>>; 'remote-sandbox': z.ZodOptional>]>>; }, "strict", z.ZodTypeAny, { none?: string | readonly [string, ...string[]] | undefined; container?: string | readonly [string, ...string[]] | undefined; 'remote-sandbox'?: string | readonly [string, ...string[]] | undefined; }, { none?: string | readonly [string, ...string[]] | undefined; container?: string | readonly [string, ...string[]] | undefined; 'remote-sandbox'?: string | readonly [string, ...string[]] | undefined; }>>; skills: z.ZodOptional>]>>>; modes: z.ZodOptional>]>>>; policy: z.ZodOptional, z.ZodEnum<["fast", "standard", "strong"]>>>; skillTierOverrides: z.ZodOptional>>; privacyFloor: z.ZodOptional>; budget: z.ZodOptional; onBudgetExhausted: z.ZodEnum<["degrade", "pause", "human"]>; }, "strip", z.ZodTypeAny, { capUsd: number; onBudgetExhausted: "human" | "degrade" | "pause"; degradeAtPct?: number | undefined; }, { capUsd: number; onBudgetExhausted: "human" | "degrade" | "pause"; degradeAtPct?: number | undefined; }>>; sensitivePaths: z.ZodOptional>; escalationThreshold: z.ZodOptional; allowedProviders: z.ZodOptional>; acceptanceEval: z.ZodOptional; }, "strip", z.ZodTypeAny, { enabled: boolean; model?: string | undefined; }, { enabled: boolean; model?: string | undefined; }>>; }, "strip", z.ZodTypeAny, { complexityTierMatrix?: Partial> | undefined; skillTierOverrides?: Record | undefined; privacyFloor?: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud" | undefined; budget?: { capUsd: number; onBudgetExhausted: "human" | "degrade" | "pause"; degradeAtPct?: number | undefined; } | undefined; sensitivePaths?: string[] | undefined; escalationThreshold?: number | undefined; allowedProviders?: string[] | undefined; acceptanceEval?: { enabled: boolean; model?: string | undefined; } | undefined; }, { complexityTierMatrix?: Partial> | undefined; skillTierOverrides?: Record | undefined; privacyFloor?: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud" | undefined; budget?: { capUsd: number; onBudgetExhausted: "human" | "degrade" | "pause"; degradeAtPct?: number | undefined; } | undefined; sensitivePaths?: string[] | undefined; escalationThreshold?: number | undefined; allowedProviders?: string[] | undefined; acceptanceEval?: { enabled: boolean; model?: string | undefined; } | undefined; }>>; workflowGates: z.ZodOptional>; maxLocalStageRetries: z.ZodOptional; }, "strict", z.ZodTypeAny, { default: string | readonly [string, ...string[]]; 'quick-fix'?: string | readonly [string, ...string[]] | undefined; 'guided-change'?: string | readonly [string, ...string[]] | undefined; 'full-exploration'?: string | readonly [string, ...string[]] | undefined; diagnostic?: string | readonly [string, ...string[]] | undefined; modes?: Record | undefined; intelligence?: { sel?: string | readonly [string, ...string[]] | undefined; pesl?: string | readonly [string, ...string[]] | undefined; } | undefined; isolation?: { none?: string | readonly [string, ...string[]] | undefined; container?: string | readonly [string, ...string[]] | undefined; 'remote-sandbox'?: string | readonly [string, ...string[]] | undefined; } | undefined; skills?: Record | undefined; policy?: { complexityTierMatrix?: Partial> | undefined; skillTierOverrides?: Record | undefined; privacyFloor?: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud" | undefined; budget?: { capUsd: number; onBudgetExhausted: "human" | "degrade" | "pause"; degradeAtPct?: number | undefined; } | undefined; sensitivePaths?: string[] | undefined; escalationThreshold?: number | undefined; allowedProviders?: string[] | undefined; acceptanceEval?: { enabled: boolean; model?: string | undefined; } | undefined; } | undefined; workflowGates?: "local" | "primary" | undefined; maxLocalStageRetries?: number | undefined; }, { default: string | readonly [string, ...string[]]; 'quick-fix'?: string | readonly [string, ...string[]] | undefined; 'guided-change'?: string | readonly [string, ...string[]] | undefined; 'full-exploration'?: string | readonly [string, ...string[]] | undefined; diagnostic?: string | readonly [string, ...string[]] | undefined; modes?: Record | undefined; intelligence?: { sel?: string | readonly [string, ...string[]] | undefined; pesl?: string | readonly [string, ...string[]] | undefined; } | undefined; isolation?: { none?: string | readonly [string, ...string[]] | undefined; container?: string | readonly [string, ...string[]] | undefined; 'remote-sandbox'?: string | readonly [string, ...string[]] | undefined; } | undefined; skills?: Record | undefined; policy?: { complexityTierMatrix?: Partial> | undefined; skillTierOverrides?: Record | undefined; privacyFloor?: "on-device" | "pooled-isolated" | "byo-endpoint" | "shared-cloud" | undefined; budget?: { capUsd: number; onBudgetExhausted: "human" | "degrade" | "pause"; degradeAtPct?: number | undefined; } | undefined; sensitivePaths?: string[] | undefined; escalationThreshold?: number | undefined; allowedProviders?: string[] | undefined; acceptanceEval?: { enabled: boolean; model?: string | undefined; } | undefined; } | undefined; workflowGates?: "local" | "primary" | undefined; maxLocalStageRetries?: number | undefined; }>; interface ResolverLogger { info(message: string, context?: Record): void; warn(message: string, context?: Record): void; } interface LocalModelResolverOptions { endpoint: string; apiKey?: string; /** Normalized candidate list (already turned from string|string[] into string[]). */ configured: string[]; /** * Phase 4 (D5): optional read-only pool-state port. When provided, the * candidate list derives from pool entries (currentScore desc → ollamaName) * instead of `configured`. When absent (default), behavior is byte-identical * to the pre-Phase-4 resolver. */ poolState?: PoolStateProvider; /** Probe cadence in ms; default 30_000, minimum 1_000. */ probeIntervalMs?: number; /** Debounce window for event-driven {@link LocalModelResolver.refresh}; default 250ms. */ refreshDebounceMs?: number; /** * Consumption Phase 5 (T19/T20): called with the newly-resolved model name * whenever the resolver's composite selection changes (and is non-null), so * the orchestrator can warm it into VRAM before the next dispatch. Naturally * debounced — it fires only on an actual selection change, not every probe. * Best-effort: a throwing hook is swallowed. Injected in tests; wired to * {@link defaultWarmModel} in production. */ warmModel?: (ollamaName: string) => void; /** * Consumption Phase 3 (T10): consecutive inference failures before a model is * deprioritized by the circuit breaker. Default 3. */ breakerThreshold?: number; /** Cooldown (ms) after which a tripped model is eligible again. Default 60_000. */ breakerCooldownMs?: number; /** Injectable clock (ms since epoch) for the circuit-breaker cooldown; default `Date.now`. Test seam. */ now?: () => number; /** * Per-request timeout for the default fetch implementation, in ms. * Default: 5_000. Ignored when a custom `fetchModels` is provided * (custom impls own their own timeout policy). Spec §3.1 line 136 * enumerates timeout as a supported failure mode. */ timeoutMs?: number; /** * Injectable for tests. Default: GET `${endpoint}/models` with bearer apiKey. * Resolves to detected model IDs. Rejects on network/timeout/non-2xx/malformed. */ fetchModels?: (endpoint: string, apiKey?: string) => Promise; logger?: ResolverLogger; } declare function normalizeLocalModel(input: string | string[] | undefined): string[]; /** * Default `fetchModels` — GET `${endpoint}/models` with bearer apiKey. * Throws on network failure, non-2xx, malformed body, or timeout. * * `timeoutMs` defaults to 5_000. A timeout aborts the in-flight request and * surfaces as `Error('request timeout (Nms)')` so callers can distinguish it * from generic network errors. */ declare function defaultFetchModels(endpoint: string, apiKey?: string, timeoutMs?: number): Promise; declare class LocalModelResolver { private readonly endpoint; private readonly apiKey?; private readonly configured; private readonly poolState?; private readonly probeIntervalMs; private readonly fetchModels; private readonly warmModel?; private readonly logger; private timer; /** Debounce handle for event-driven {@link refresh}; coalesces a burst into one probe. */ private refreshTimer; /** Test seam for the refresh debounce delay (0 in tests → next-tick probe). */ private readonly refreshDebounceMs; /** Consumption Phase 3 (T10): circuit-breaker config + per-model failure/trip state. */ private readonly breakerThreshold; private readonly breakerCooldownMs; private readonly now; /** Consecutive inference failures per model since its last success. */ private consecutiveFailures; /** Epoch-ms at which a tripped model becomes eligible again (cooldown expiry). */ private trippedUntil; private listeners; /** * Tracks an in-flight probe so concurrent invocations (interval tick while a * slow probe is running, or a manual `probe()` call mid-flight) share the * existing promise instead of racing to mutate `detected/resolved/lastError/ * warnings` non-atomically across `await` points. Applies to both the timer * callback and direct `probe()` calls — any caller that arrives during an * in-flight probe gets the same promise back. Cleared in `finally` so the * next tick can start a fresh probe. */ private probeInFlight; private resolved; private detected; private lastProbeAt; private lastError; private warnings; private available; constructor(opts: LocalModelResolverOptions); /** * The model to dispatch to. With no `useCase`, returns the cached composite * resolution from the last probe (byte-identical to the pre-Phase-4 resolver). * With a `useCase`, orders the (pool-derived) candidates by that use-case's * task profile and picks the best loaded, breaker-healthy one — so a * coding-tagged dispatch prefers the coding specialist. A `general`-mapped * use-case returns the cached composite resolution unchanged. */ resolveModel(useCase?: RoutingUseCase): string | null; getStatus(): LocalModelStatus; /** * Effective candidate list. With a poolState port present the list derives * from pool entries (currentScore desc → ollamaName); otherwise the static * `configured` list is returned unchanged (byte-identical to pre-Phase-4). */ private candidates; onStatusChange(handler: (status: LocalModelStatus) => void): () => void; probe(): Promise; private runProbe; /** Best-effort warm-hook invocation — a throwing warm never breaks a probe. */ private warm; /** * Consumption Phase 3 (T10): pick the resolved model from the candidates that * are actually loaded, preferring ones whose circuit breaker is not tripped. * Candidate order (score-desc from the pool) is preserved within each tier, so * a tripped top pick sinks below a healthy lower pick but still resolves as a * last resort when every loaded candidate is tripped (better a flaky model than * none). Returns null when no candidate is loaded. */ private selectMatch; /** * Whether `model`'s circuit breaker is currently tripped. Lazily clears the * trip once its cooldown has elapsed so the model becomes eligible again on the * next resolution without needing a separate timer. */ private isTripped; /** * Record a successful inference on `model`: clears its failure count and any * active trip so a recovered model is immediately re-preferred. */ recordSuccess(model: string): void; /** * Record a failed inference on `model`. On the Nth consecutive failure the * breaker trips (deprioritizing the model for `breakerCooldownMs`) and a * re-probe is scheduled so the resolver rolls to a healthy alternative. */ recordFailure(model: string): void; /** * Event-driven refresh: schedule a debounced re-probe. Called when a * `local-models:pool` mutation fires so a just-installed/swapped model becomes * usable within a debounce window instead of waiting up to `probeIntervalMs`. * A burst of frames coalesces into one probe (the timer is only armed once). */ refresh(): void; start(): Promise; stop(): void; private snapshotForDiff; } /** * Function signature compatible with Node's `child_process.execFile`. * Allows injection for testing. */ type ExecFileFn = typeof execFile; /** Reasons `syncMain()` declines to fast-forward (none of which are failures). */ type SyncSkipReason = 'wrong-branch' | 'diverged' | 'dirty-conflict' | 'no-remote' | 'fetch-failed'; /** Total result type for `syncMain()`. Discriminated by `status`. */ type SyncMainResult = { status: 'updated'; from: string; to: string; defaultBranch: string; } | { status: 'no-op'; defaultBranch: string; } | { status: 'skipped'; reason: SyncSkipReason; detail: string; defaultBranch: string; } | { status: 'error'; message: string; }; /** Options accepted by `syncMain()`. */ interface SyncMainOptions { /** Override the `git` runner; defaults to `node:child_process.execFile`. */ execFileFn?: ExecFileFn; /** Per-git-call timeout in ms. Defaults to 60_000ms. */ timeoutMs?: number; } /** * Fast-forward the local default branch from `origin/` if it can be * done safely. This function is total: it never throws under expected * conditions. Even unexpected errors are caught and converted to * `{ status: 'error', message }` so the maintenance scheduler does not back * off on transient git issues. */ declare function syncMain(repoRoot: string, opts?: SyncMainOptions): Promise; /** * All 21 built-in maintenance task definitions with default schedules. * * Tasks are grouped by type: * - mechanical-ai (7): Run check first, dispatch AI only if fixable issues found * - pure-ai (4): Always dispatch AI agent on schedule * - report-only (7): Run command, record metrics, no PR * - housekeeping (3): Mechanical command, no AI, no PR */ declare const BUILT_IN_TASKS: readonly TaskDefinition[]; /** * Unified logger interface for all maintenance classes. * Matches StructuredLogger's shape. */ interface MaintenanceLogger { info(message: string, context?: Record): void; warn(message: string, context?: Record): void; error(message: string, context?: Record): void; debug?(message: string, context?: Record): void; } /** * Hermes Phase 2 — A single persisted run entry. * * Mirrors `RunResult` plus the captured stdout/stderr, the parsed structured * status envelope (if any), the resolved upstream context that was injected * into the prompt (if any), and the trigger origin. */ interface PersistedOutputEntry { taskId: string; startedAt: string; completedAt: string; status: RunResult['status']; findings: number; fixed: number; prUrl: string | null; prUpdated: boolean; error?: string; costUsd?: number; origin?: RunOrigin; /** Raw captured stdout from the check step (or housekeeping command). */ stdout?: string; /** Raw captured stderr from the check step. */ stderr?: string; /** Structured envelope when a JSON status line was parsed; null otherwise. */ structured?: Record | null; /** Resolved upstream-context block, if any. */ context?: string; } interface TaskOutputStoreOptions { /** Root directory under which `/outputs/` lives. Default: `.harness/maintenance`. */ rootDir: string; /** Default retention bounds applied when a task doesn't specify its own. */ retentionDefaults?: Required; logger?: MaintenanceLogger; } /** * Persists per-task run outputs to disk and applies retention. The store is * intentionally simple: one file per run keyed by completion timestamp, JSON * payload, no SQLite. The chain-context read path (`latest`) and the * dashboard list path (`list`) both consume the same on-disk format. * * Concurrency: `processQueue` already serializes runs of the same task ID, * so the store assumes exclusive write access per task. */ declare class TaskOutputStore { private rootDir; private retentionDefaults; private logger; constructor(options: TaskOutputStoreOptions); /** * Reject task IDs that don't match the validator's kebab-case pattern — * defends `dirFor()` against caller-supplied path-traversal segments * (`'../foo'`) when the store is invoked from CLI surfaces that don't * round-trip through `validateCustomTasks`. */ private ensureSafeTaskId; /** * Persist a single run entry. Retention is applied after the write so * the latest record is durable even if pruning fails. */ write(taskId: string, entry: PersistedOutputEntry, retention?: OutputRetentionConfig): Promise; /** * Return the most recent persisted entry for the task, or null if none. */ latest(taskId: string): Promise; /** * List entries newest-first with offset+limit pagination. */ list(taskId: string, limit: number, offset: number): Promise; /** * Lookup a specific run by its file name (without the `.json` suffix) or * by its raw completion timestamp. */ get(taskId: string, runId: string): Promise; /** * The on-disk root for a given task. Exposed for tooling that needs to walk * outputs from outside the store API. */ dirFor(taskId: string): string; private readEntry; private applyRetention; } /** * Hermes Phase 2 — Validation errors surfaced by `validateCustomTasks`. * * `path` always begins with `customTasks.` so the caller can render * it directly without re-prefixing. Multiple errors may be returned in a * single call; the validator does not short-circuit on the first failure. */ interface CustomTaskValidationError { path: string; message: string; } interface CustomTaskValidatorDeps { /** Returns true if a skill with this name exists in the project's registry. */ skillExists?: (name: string) => boolean; /** Returns true if the executable referenced by a checkScript.path exists. */ scriptExists?: (path: string) => boolean; } /** * Validates a `MaintenanceConfig.customTasks` map. * * Checks: * - kebab-case task IDs (matching the BUILT_IN_TASKS convention) * - no collision with built-in IDs * - per-type required fields (e.g., mechanical-ai must have `branch` + `fixSkill`) * - exactly one of `checkCommand` / `checkScript` for types that need a check step * - `contextFrom` cycle detection across the merged graph (built-ins + customs) * - `contextFrom` entries reference existing task IDs * - `inlineSkills` entries exist in the skill registry (when `skillExists` is provided) * - `checkScript.path` exists on disk (when `scriptExists` is provided) * * Returns `Ok(void)` when all custom tasks pass; otherwise an `Err` carrying * every distinct violation. The validator is pure: no I/O outside the * injected predicates. */ declare function validateCustomTasks(customTasks: Record | undefined, builtIns: readonly TaskDefinition[], deps?: CustomTaskValidatorDeps): Result; /** Selection filter for `selectTasks`. `now` is injected for determinism. */ interface TaskSelectionFilter { mode: 'overdue' | 'all' | 'ids'; /** Required when `mode === 'ids'`; ignored otherwise. */ ids?: string[]; /** Reference instant — never read from the wall clock internally. */ now: Date; } /** * Select the maintenance tasks to run for an on-demand sweep (D3/D5). * Operates only on sweep-eligible tasks (`excludeFromHumanSweep !== true`) * in every mode. Deterministic under the injected `filter.now`. * * - `overdue`: eligible tasks with no satisfying run since their previous fire. * - `all`: every eligible task. * - `ids`: the eligible subset named in `filter.ids` (a named excluded id * is dropped, honoring "excluded tasks never run in either path"). * `filter.ids` is treated as a SET membership test, not an ordered * request list: results preserve the input `tasks` array order, not * the order ids appear in `filter.ids`. Duplicate ids are collapsed * and a task is returned at most once. Callers that need request * order must reorder the result themselves. */ declare function selectTasks(tasks: TaskDefinition[], history: RunResult[], filter: TaskSelectionFilter): TaskDefinition[]; /** * Hermes Phase 2 — Result of running an arbitrary check script. * * Structurally compatible with `CheckCommandResult` (`task-runner.ts`) so * `TaskRunner` can consume it through the same interface, plus a parsed * structured envelope and the captured stderr stream. `structured` is * `null` when no JSON status line was found on stdout. The shape is * declared locally (not imported from `task-runner.ts`) to keep the * module dependency one-way: `task-runner.ts` depends on * `check-script-runner.ts`, never the reverse. */ interface CheckScriptResult { passed: boolean; findings: number; /** Raw stdout. Field name matches `CheckCommandResult.output` for compatibility. */ output: string; stderr: string; structured: CheckScriptStatusEnvelope | null; } interface CheckScriptStatusEnvelope { status: 'ok' | 'findings' | 'skip' | 'error'; findings?: number; wakeAgent?: boolean; message?: string; outputs?: Record; } /** * Spawns an arbitrary executable for the mechanical/report/housekeeping * check step. Honors the structured JSON status envelope (last non-empty * stdout line) per Hermes Phase 2 D6. Falls back to the legacy heuristic * regex when no JSON envelope is present. * * The runner is intentionally simple and shells out via `execFile` (no * shell, no `sh -c`) — `args` are passed verbatim to avoid argument * injection from operator-supplied config. */ declare class CheckScriptRunner { private cwd; constructor(cwd: string); run(spec: CheckScriptDefinition, cwd?: string): Promise; } /** * Hermes Phase 2 — Reads an inline-skill body by name. Wraps the host's * skill registry; absent skills resolve to `null` so the resolver can * warn-and-skip instead of throwing. */ interface InlineSkillReader { read(skillName: string): Promise; } interface ContextResolverOptions { outputStore: TaskOutputStore; skillReader?: InlineSkillReader; logger?: MaintenanceLogger; /** Per-upstream truncation bound. Default: 2000 chars. */ perUpstreamMaxChars?: number; } /** * Resolves Hermes Phase 2 prompt-context inputs for a maintenance task: * * - `resolveContextFrom(taskIds, opts)`: pulls each upstream's latest * output from the store, applies stale/missing markers, and emits a * `## Upstream context` markdown block. * * - `resolveInlineSkills(skillNames, budgetTokens)`: pulls each skill's * markdown body via the registry, applies a per-skill char-count * budget (4 chars ≈ 1 token), warns-then-truncates on overflow, and * emits a `## Reference skills` markdown block. * * Both methods return an empty string when their inputs are absent so * callers can `prompt = skills + upstream + base` without conditional * scaffolding. */ declare class ContextResolver { private outputStore; private skillReader; private logger; private perUpstreamMaxChars; constructor(options: ContextResolverOptions); resolveContextFrom(upstreamTaskIds: string[] | undefined, options?: { maxAgeMinutes?: number; }): Promise; resolveInlineSkills(skillNames: string[] | undefined, budgetTokens?: number): Promise; private formatUpstream; } /** * Interface for running CLI check commands in-process. * Each method returns a structured result with a findings count. */ interface CheckCommandRunner { /** * Runs a check command and returns its structured output. * @param command - CLI command args (e.g., ['check-arch']) * @param cwd - Working directory * @returns Object with findings count and whether the check passed */ run(command: string[], cwd: string): Promise; } interface CheckCommandResult { passed: boolean; findings: number; /** Raw output for logging/reporting */ output: string; /** * True when the check command could NOT be executed to a usable result — * the process failed to spawn (ENOENT), the subcommand was unknown, or it * crashed with no parseable findings count. This is distinct from a check * that ran fine and legitimately reported findings (`passed: false`, * `findings > 0`). The TaskRunner maps `executionFailed` onto * `status: 'failure'` so an un-runnable check is never masked as a * successful 1-finding run (ADR 0050 — exit-1-on-check-crash). Omitted / * `undefined` is treated as `false` for backward compatibility with runners * that predate this field. */ executionFailed?: boolean; /** * How `findings` was derived (#691). `'contract'` means the check emitted the * standard machine-readable findings envelope (`{ findings: N, ... }`) and the * runner consumed THAT — the robust, wording-independent path. `'regex'` means * the count was recovered by scanning free-text output — the legacy fallback, * kept for checks not yet migrated to the contract. Omitted for runners (e.g. * `CheckScriptRunner`) that don't distinguish the two. */ findingsSource?: 'contract' | 'regex'; } /** * Interface for dispatching an AI agent to fix issues. * Wraps AgentRunner.runSession() with maintenance-specific parameters. */ interface AgentDispatcher { /** * Dispatch an AI agent to fix issues on a branch. * @param skill - Skill name to dispatch (e.g., 'harness-arch-fix') * @param branch - Branch to work on * @param backendName - Backend name to use (e.g., 'local', 'claude') * @param cwd - Working directory (worktree path) * @param options - Hermes Phase 2: optional `promptContext` prepended to * the agent's system prompt (inlined skills + upstream * outputs). The dispatcher MAY ignore this field on * backends that don't support context injection; the * TaskRunner persists it independently for observability. * @returns Whether the agent produced any commits */ dispatch(skill: string, branch: string, backendName: string, cwd: string, options?: AgentDispatchOptions): Promise; } interface AgentDispatchOptions { /** Hermes Phase 2 — prompt context (inlined skills + upstream outputs). */ promptContext?: string; } interface AgentDispatchResult { producedCommits: boolean; fixed: number; } /** * Interface for running housekeeping commands directly. */ interface CommandExecutor { /** * Executes a command directly (no AI). Returns captured stdout so * housekeeping tasks emitting a JSON status line (e.g. `sync-main --json`) * can be parsed by the runner. * * @param command - Command args (e.g., ['cleanup-sessions']) * @param cwd - Working directory */ exec(command: string[], cwd: string): Promise; } interface CommandExecResult { /** Captured stdout. May be empty for legacy housekeeping commands. */ stdout: string; } /** * Interface for managing branches and PRs for maintenance tasks. * Matches PRManager's public API shape for DI. */ interface PRLifecycleManager { ensureBranch(branchName: string, baseBranch: string): Promise<{ created: boolean; recreated: boolean; }>; ensurePR(task: TaskDefinition, runSummary: string): Promise<{ prUrl: string; prUpdated: boolean; }>; } interface TaskRunnerOptions { config: MaintenanceConfig; checkRunner: CheckCommandRunner; agentDispatcher: AgentDispatcher; commandExecutor: CommandExecutor; /** Project root directory for running commands */ cwd: string; /** Optional PR lifecycle manager for branch/PR operations */ prManager?: PRLifecycleManager; /** Base branch for PR operations (defaults to 'main') */ baseBranch?: string; /** * Hermes Phase 2 — Optional check-script runner. When a task declares * `checkScript`, the runner consults this instead of `checkRunner`. * Required for any custom task that uses `checkScript`; tests that don't * exercise custom tasks may omit it. */ checkScriptRunner?: CheckScriptRunner; /** * Hermes Phase 2 — Optional context resolver providing `contextFrom` * upstream injection and `inlineSkills` payload assembly. */ contextResolver?: ContextResolver; /** * Hermes Phase 2 — Persists per-task outputs (stdout / structured / * resolved context) at the end of each run. */ outputStore?: TaskOutputStore; } /** * TaskRunner executes a single maintenance task based on its type. * * Execution paths: * - mechanical-ai: run check -> if findings, dispatch AI agent * - pure-ai: always dispatch AI agent * - report-only: run check, record findings, no AI * - housekeeping: run command directly, no AI */ declare class TaskRunner { private config; private checkRunner; private agentDispatcher; private commandExecutor; private cwd; private prManager; private baseBranch; private checkScriptRunner; private contextResolver; private outputStore; constructor(options: TaskRunnerOptions); /** * Run a maintenance task and return the result. * Dispatches to the appropriate execution path based on task type. * Never throws -- errors are captured in the RunResult. * * @param task - Resolved task definition. * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'` * when called from the scheduler path. */ run(task: TaskDefinition, origin?: RunOrigin, mode?: RunMode): Promise; private persistOutput; /** * Run the check step using whichever runner the task asks for. Custom * tasks that declare `checkScript` go through the Hermes Phase 2 * `CheckScriptRunner`; built-ins (and customs that use the legacy * `checkCommand` shape) go through the original heuristic runner. */ private runCheckStep; /** * Hermes Phase 2 — Compose the agent prompt-context block from inlined * skills + upstream task outputs. Returns an empty string when nothing * is configured (or when the resolver is absent), which is the safe * no-op default. */ private composePromptContext; /** * Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent * only if fixable findings exist; persist captured stdout/stderr/context * via the output store on the way out. */ private runMechanicalAI; /** * Pure-AI: always dispatch agent with configured skill. */ private runPureAI; /** * Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch. * * Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run` * and `harness compound scan-candidates` in `--non-interactive` mode): * {"status":"success"|"skipped"|"failure"|"no-issues", * "candidatesFound"?: number, "error"?: string, "reason"?: string} * * Legacy report-only tasks emit free-form output and fall through to 'success'. */ private runReportOnly; /** * Housekeeping: run command directly, no AI, no PR. * * Captures stdout and parses a trailing JSON status line if present. * Recognized contracts: * - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues * - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status * Legacy housekeeping commands that emit no JSON keep the prior behavior: * status: 'success', findings: 0. * * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping * tasks; the runner falls through to the same JSON-status parsing path. */ private runHousekeeping; /** * Resolve which AI backend name to use for a given task. * Priority: per-task override > global config > 'local' default. */ private resolveBackend; private failureResult; /** * A precondition-gated check (e.g. `predict` with <3 snapshots, or a * graph-backed check before `harness scan`). The command is correctly * configured; the repo just lacks the state it needs. Reported as `skipped` * — distinct from a hard `failure` — carrying the refusal line as the reason * (surfaced in the run-report summary column). ADR 0050. */ private skippedResult; } /** * Classification of a check that a `CheckCommandRunner` flagged * `executionFailed` (non-zero exit / spawn error with no parseable finding * count). The runners cannot tell these three cases apart with a single * boolean, so the TaskRunner re-classifies the captured output here — keeping * the logic in ONE shared place consumed by BOTH the cron orchestrator and the * on-demand CLI (they each own a thin `CheckCommandRunner` but share this * `TaskRunner`). See ADR 0050 (report-first / execution-honesty). * * - `unrunnable` → the subcommand could not execute at all (unknown command, * ENOENT, missing module, empty output). Maps to `failure`. * - `precondition`→ the subcommand ran but refused because required state is * absent in this repo (e.g. `predict` needs ≥3 snapshots; * graph-backed checks need `harness scan` first). This is NOT * a misconfiguration or a breakage — maps to `skipped` with a * reason so dashboards/cron don't cry wolf. * - `ran-no-count`→ the subcommand ran and exited non-zero to SIGNAL drift but * emitted no machine-parseable count (e.g. `cleanup`, * `check-docs`). A check that ran and flagged work is not an * execution failure — maps to a real (recovered or assumed) * finding count, never `failure`. */ type CheckFailureKind = 'unrunnable' | 'precondition' | 'ran-no-count'; interface CheckFailureClassification { kind: CheckFailureKind; /** Human-readable reason, populated for `precondition` (the refusal line). */ reason?: string; } /** * Parse an EXPLICIT finding count from output the primary `N keyword` parser in * the `CheckCommandRunner` missed — it only matches "45 issues", not the * "Entropy issues: 32264" (count-after-keyword) shape. Returns `null` when no * explicit count is present (vs `recoverFindingsCount`, which defaults to 1). * * A recoverable explicit count is the strongest possible signal that a check * actually RAN and produced findings, so the classifier trusts it over any * scary-looking words ("ENOENT", "not found") buried in the findings body. */ declare function explicitFindingsCount(output: string): number | null; /** * Recover a finding count from a `ran-no-count` output, falling back to 1 — the * check ran and flagged something, exact count unknown. */ declare function recoverFindingsCount(output: string): number; declare function classifyCheckExecutionFailure(output: string): CheckFailureClassification; /** * Dependencies for {@link createAgentDispatcher}. All side-effecting collaborators * are injected so the dispatcher is unit-testable with a MockBackend and a fake * git seam (no real agent process, no real repository). */ interface AgentDispatcherDeps { /** * Resolve a configured backend by name (e.g. `'local'`, `'claude'`). Returns * `null` when the name is unknown/unconfigured — the dispatcher then no-ops * rather than throwing, so a misconfigured maintenance task degrades to * "produced nothing" instead of crashing the scheduler. */ resolveBackend: (backendName: string) => AgentBackend | null; /** Run `git ` in `cwd`, returning trimmed stdout (throws on failure). */ git: (args: string[], cwd: string) => string; /** Max agent turns per dispatch. Defaults to 10. */ maxTurns?: number; logger: { info: (message: string, meta?: Record) => void; warn: (message: string, meta?: Record) => void; }; } /** * Wire the maintenance {@link AgentDispatcher} to a real agent session. * * Replaces the previous stub (which only logged and returned `producedCommits: * false`). Resolves the named backend, drives a multi-turn {@link AgentRunner} * session over the skill prompt in the worktree, then measures whether the agent * actually committed anything by diffing `HEAD` before/after. Commit count — not * the agent's self-report — is the source of truth for `fixed`. */ declare function createAgentDispatcher(deps: AgentDispatcherDeps): AgentDispatcher; /** * Options for the MaintenanceReporter. */ interface MaintenanceReporterOptions { /** Directory where history.json is persisted (default: '.harness/maintenance/') */ persistDir?: string; /** Logger for structured error/info output. Falls back to console if not provided. */ logger?: MaintenanceLogger; } declare class MaintenanceReporter { private persistDir; private logger; private history; constructor(options?: MaintenanceReporterOptions); /** * Load history from disk. Creates the persist directory if it does not exist. * Errors in persistence are logged to stderr, not thrown. */ load(): Promise; /** * Record a run result. Appends to in-memory history (most recent first), * caps at MAX_HISTORY, and writes to disk asynchronously. */ record(result: RunResult): Promise; /** * Returns a paginated slice of the run history (most recent first). */ getHistory(limit: number, offset: number): RunResult[]; /** * Write history to disk. Errors are logged, not thrown. */ private persist; } /** * Generous stdout/stderr capture ceiling for spawned maintenance checks. The * Node `execFile` default (1 MB) is far too small for verbose checks — e.g. * `harness cleanup` on a large repo emits ~8 MB — and an exceeded buffer * rejects with EMPTY stdout/stderr, which the runner would misread as a check * that produced no output (a false execution failure). 64 MB leaves ample * headroom while still bounding memory. */ declare const MAINTENANCE_CHECK_MAX_BUFFER: number; /** * Per-check wall-clock budget. Raised from the original 120 s because heavy * whole-repo checks legitimately need longer on large monorepos — `harness * cleanup` (all entropy types) takes ~165 s here — and a too-tight timeout * SIGTERMs the child, yielding empty output that the runner can only read as a * (false) execution failure. 300 s keeps a bound while letting real checks * finish. */ declare const MAINTENANCE_CHECK_TIMEOUT_MS = 300000; /** A resolved child-process invocation: the file to spawn and its argv. The two * callers build this differently (CLI: `process.execPath` + the CLI's own entry * script; cron: the `harness` binary on PATH) but the spawn/parse core is one. */ interface HarnessSpawn { file: string; args: string[]; } /** The shape of an `execFile` rejection the timeout/classification logic reads. */ interface ExecFileError { stdout?: string | Buffer; stderr?: string | Buffer; killed?: boolean; signal?: string | null; code?: string | number | null; } /** Injectable `execFile` (promisified) so tests can drive the spawn-error, * clean-run, findings, and timeout branches without real subprocesses. */ type ExecFileAsyncFn = (file: string, args: string[], options: { cwd: string; timeout: number; maxBuffer: number; }) => Promise<{ stdout: string | Buffer; stderr?: string | Buffer; }>; interface RunHarnessCheckOptions { /** Injected for tests; defaults to the real promisified `execFile`. */ execFileAsync?: ExecFileAsyncFn; /** Per-check wall-clock budget (default {@link MAINTENANCE_CHECK_TIMEOUT_MS}). */ timeoutMs?: number; /** stdout/stderr capture ceiling (default {@link MAINTENANCE_CHECK_MAX_BUFFER}). */ maxBuffer?: number; } /** Best-effort detection of a child killed by the `execFile` timeout (SIGTERM / * ETIMEDOUT / killed flag). */ declare function isCheckTimeoutError(e: ExecFileError): boolean; /** * Spawn a resolved harness check invocation and classify its result into a * {@link CheckCommandResult}. Behavior (shared by cron + CLI): * * - findings contract present (#691) → the machine-readable envelope * (`{ findings: N, ... }`) wins over everything below, on BOTH a clean and a * non-zero exit (`findingsSource: 'contract'`). A migrated check reports its * count as structured data, so no wording change can break it. * - clean exit, parseable count → `{ findings: N }` (or 0 on a parse-miss; a * check that ran and said nothing is clean, not "1 finding"). * - non-zero exit WITH a parseable count → real findings (`executionFailed: * false`) — e.g. `check-arch` exits 1 with "45 issues". * - non-zero exit / spawn error with NO parseable count → `executionFailed: * true`, findings 0 (a broken check is not "1 finding"; ADR 0050). * - TIMEOUT (SIGTERM/ETIMEDOUT) → `executionFailed: true`, findings 0, with a * distinct "check timed out after Nms" marker APPENDED to whatever the child * flushed. A timed-out check did not complete: even partial parseable output * ("5 issues") it flushed before SIGTERM is truncated and untrustworthy, so * the timeout is classified ahead of any finding count — never a "ran-no- * count" success. {@link import('./task-runner').classifyCheckExecutionFailure} * matches this marker before `explicitFindingsCount`. */ declare function runHarnessCheck(spawn: HarnessSpawn, cwd: string, opts?: RunHarnessCheckOptions): Promise; interface CreateTokenInput { name: string; scopes: TokenScope[]; bridgeKind?: AuthToken['bridgeKind']; tenantId?: string; expiresAt?: string; } interface CreateTokenResult { id: string; token: string; record: AuthToken; } declare class TokenStore { private readonly path; private cache; constructor(path: string); private load; private persist; create(input: CreateTokenInput): Promise; verify(raw: string): Promise; private touchLastUsed; list(): Promise; revoke(id: string): Promise; /** * Synthetic admin record for the legacy HARNESS_API_TOKEN escape hatch. * Returned only when `presented` matches `envValue` byte-for-byte (constant-time). */ legacyEnvToken(presented: string, envValue: string | undefined): AuthToken | null; } declare const RETRY_DELAYS_MS: readonly [1000, 4000, 16000, 64000, 256000]; declare const MAX_ATTEMPTS = 5; interface QueueInsertInput { id: string; subscriptionId: string; eventType: string; payload: string; } interface QueueRow { id: string; subscriptionId: string; eventType: string; payload: string; attempt: number; status: 'pending' | 'in_flight' | 'failed' | 'delivered' | 'dead'; nextAttemptAt: number | null; lastError: string | null; deliveredAt: number | null; } interface QueueStats { pending: number; inFlight: number; failed: number; dead: number; delivered: number; } declare class WebhookQueue { private readonly db; constructor(dbPath: string); insert(row: QueueInsertInput): void; /** * Atomically lease a batch of deliverable rows: select pending|failed rows * whose nextAttemptAt has elapsed, mark them in_flight in the same * transaction, and return the leased rows. Subsequent calls cannot re-claim * the same rows because they are no longer in pending|failed. * * Without this, two overlapping ticks (tick interval 500ms, HTTP timeout * 5s) would both select the same row and double-fire the webhook. */ claim(now: number, limit?: number): QueueRow[]; /** * Reset any rows stuck in in_flight (e.g. from a crashed or abruptly * stopped worker) back to failed so they can be re-claimed by the next * tick. At-most-once semantics within a single process; at-least-once * across restarts (a row whose HTTP POST completed but whose DB update was * lost will be re-delivered — bridges must be idempotent on delivery-id). */ recoverInFlight(): number; markDelivered(id: string, deliveredAt: number): void; markFailed(id: string, attempt: number, nextAttemptAt: number, lastError: string): void; retryDead(id: string): boolean; list(filter?: { status?: string; subscriptionId?: string; }): QueueRow[]; purge(opts?: { deadOnly?: boolean; olderThanMs?: number; all?: boolean; }): number; /** * Count the rows a purge() call with these same options would delete. * Used by the CLI to show a confirmation preview before destructive deletes. */ previewPurge(opts?: { deadOnly?: boolean; olderThanMs?: number; all?: boolean; }): number; stats(): QueueStats; close(): void; } interface IndexedDoc { sessionId: string; archived: boolean; fileKind: IndexedFileKind; /** Path relative to project root, posix-style. */ path: string; mtimeMs: number; body: string; } interface SearchOptions { limit?: number; archivedOnly?: boolean; fileKinds?: IndexedFileKind[]; /** Maximum bytes to retain per doc body (defaults 256 KiB; longer bodies are truncated with a marker). */ maxBytesPerBody?: number; } /** * Convert a user-typed query string into a safe FTS5 expression. * * If the caller's query already contains explicit FTS5 syntax markers (double * quotes, parens, asterisk, caret, plus, the literal words AND/OR/NOT or a * `column:` selector) it is passed through unchanged so power users keep the * full FTS5 grammar. * * Otherwise each whitespace-separated token is wrapped as an FTS5 phrase so * characters like `-`, `:` and `*` inside the token are treated as content * (not operators), and the tokens are implicitly AND-joined by FTS5. * * Without this, `idx.search('token-aleph')` is parsed by FTS5 as * `token NOT aleph` and fails with `no such column: aleph`. */ declare function normalizeFts5Query(query: string): string; /** * Filesystem path of the search-index sqlite file for a given project root. * Stable so consumers (cleanup tools, doctor, gitignore guards) can locate it. */ declare function searchIndexPath(projectPath: string): string; declare class SqliteSearchIndex { private readonly db; private readonly upsertStmt; private readonly removeSessionStmt; private readonly totalStmt; constructor(dbPath: string); upsertSessionDoc(doc: IndexedDoc): void; removeSession(sessionId: string): number; /** * Drop all `archived=1` rows. Used by `reindexFromArchive` before a full * re-walk. Live (archived=0) rows are preserved. */ resetArchived(): void; /** Total rows currently indexed (across both live and archived). */ totalIndexed(): number; /** * Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to * FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`) * is therefore the user-facing language. Errors from malformed queries * surface as thrown `SqliteError` so the CLI can catch + render them. */ search(query: string, opts?: SearchOptions): SessionSearchResult; close(): void; } /** Open (or create) the project's search index. Idempotent. */ declare function openSearchIndex(projectPath: string): SqliteSearchIndex; /** * Walk a session/archive directory and upsert one row per existing file_kind. * Used by both the archive hook (`indexArchivedSession`) and `reindexFromArchive`. * * Bodies larger than `maxBytesPerBody` are truncated with a marker so the index * does not bloat on pathological session files. */ declare function indexSessionDirectory(idx: SqliteSearchIndex, args: { sessionId: string; sessionDir: string; archived: boolean; projectPath: string; /** Subset of file_kinds to consider (defaults to all). */ fileKinds?: IndexedFileKind[]; maxBytesPerBody?: number; }): { docsWritten: number; }; /** * Drop and rebuild the `archived=1` portion of the index from * `.harness/archive/sessions//`. Idempotent. * * Each subdirectory is treated as one session whose id is the basename. */ declare function reindexFromArchive(projectPath: string, opts?: { fileKinds?: IndexedFileKind[]; maxBytesPerBody?: number; }): ReindexStats; interface SummarizeContext { /** Path to the archived session directory, e.g. .harness/archive/sessions/foo-2026-05-16. */ archiveDir: string; /** Resolved AnalysisProvider — caller skips this step when no provider is available. */ provider: AnalysisProvider; /** Optional session summary config, defaults applied when fields are missing. */ config?: SessionSummarizationConfig | undefined; /** Optional logger; falls back to console.warn for diagnostics. */ logger?: { warn?: (msg: string, meta?: Record) => void; }; /** When true, on provider error a stub `llm-summary.md` is still written. Default true. */ writeStubOnError?: boolean; } interface SummarizeResult { summary: SessionSummary; meta: SessionSummaryMeta; filePath: string; } /** Approximate token cap via char count; conservative because tokens average ~4 chars. */ declare function truncateForBudget(text: string, inputBudgetTokens: number): string; /** Render the structured summary as the `llm-summary.md` markdown payload. */ declare function renderLlmSummaryMarkdown(summary: SessionSummary, meta: SessionSummaryMeta): string; /** * Summarise a single archived session via the provided AnalysisProvider. * * On success: writes `llm-summary.md` into the archive directory and returns * the structured summary + metadata. * * On provider error: optionally writes a stub `llm-summary.md` so callers can * still detect that summarization was attempted, then returns `Err`. * * Empty / missing input corpus is returned as `Err` and never produces a file. */ declare function summarizeArchivedSession(ctx: SummarizeContext): Promise>; /** Resolve whether summarization should run for the given config. */ declare function isSummaryEnabled(config?: SessionSummarizationConfig): boolean; /** * Session archive hook bundle. * * `buildArchiveHooks()` returns an `ArchiveHooks` implementation that wires * `summarizeArchivedSession()` + `retrospectArchivedSession()` + * `indexSessionDirectory()` together so the core `archiveSession()` lifecycle * invokes all three after a successful move. The retrospection step * auto-triggers at this session terminus and emits applyable proposals into * `.harness/proposals/` (emission only — never auto-applies). * * Every step is individually wrapped in try/catch — failure of any does not * propagate up the call stack. Spec: §"Risks" treats summary + retrospection + * index failure as non-fatal. */ interface BuildArchiveHooksOptions { /** Absolute path to the project root (contains `.harness/`). */ projectPath: string; /** Optional AnalysisProvider — summarization is skipped when omitted. */ provider?: AnalysisProvider | undefined; /** Optional sessions config slice. */ config?: SessionsConfig | undefined; /** Optional logger; falls back to console.warn. */ logger?: HookLogger | undefined; } interface HookLogger { warn?: (msg: string, meta?: Record) => void; } /** * Construct the `ArchiveHooks` impl. Always returns a working hook bundle — * missing provider or disabled config simply skips that step. */ declare function buildArchiveHooks(opts: BuildArchiveHooksOptions): ArchiveHooks; /** * Auto-triggered retrospection → applyable proposals. * * Fires at the session terminus (session archive). Reads the archived session * corpus, asks an `AnalysisProvider` to conduct a grounded retrospective, and * persists each resulting *applyable* proposal into the existing proposals store * (`.harness/proposals/`) via `createProposal`. * * Emission only — nothing is auto-applied. Each proposal lands with * `status: 'open'`; approval runs the soundness gate and promotion writes the * skill into the catalog. That apply step stays a separate, human-gated action * (the proposals queue), exactly as for agent-emitted proposals. * * The proposal records are ordinary `SkillProposal`s — the same shape produced * by `emit_skill_proposal` — so they surface, gate, and promote through the * unchanged pipeline. No parallel proposal type is introduced. */ /** Provenance stamped onto every retrospection-emitted proposal. */ declare const RETROSPECTION_PROPOSED_BY = "retrospection:session-terminus"; interface RetrospectionContext { /** Absolute path of the archived session directory. */ archiveDir: string; /** Archived session id (used as proposal provenance). */ sessionId: string; /** Project root containing `.harness/`. */ projectPath: string; /** Resolved AnalysisProvider — caller skips this step when none is available. */ provider: AnalysisProvider; /** Optional retrospection config; defaults applied per-field. */ config?: RetrospectionConfig | undefined; /** Optional logger; diagnostics only. */ logger?: { warn?: (msg: string, meta?: Record) => void; } | undefined; } interface RetrospectionResult { /** Proposals successfully written to `.harness/proposals/`. */ written: SkillProposal[]; /** Drafts dropped because they failed validation or collided with an open proposal. */ skipped: number; } /** * Resolve whether auto-triggered retrospection should run for the given config. * Mirrors `isSummaryEnabled`: off when no config block is present, off when * `enabled === false`, on otherwise (provider availability is checked by the * caller). Opt-in and safe by default. */ declare function isRetrospectionEnabled(config?: RetrospectionConfig): boolean; /** * Run a retrospection over one archived session and persist the applyable * proposals it yields. Never throws for expected failure modes (missing corpus, * provider error, invalid/colliding drafts) — those resolve to `Err` or a * per-draft skip so the archive lifecycle is never disrupted. */ declare function retrospectArchivedSession(ctx: RetrospectionContext): Promise>; /** * Wrap a `GatewayEvent` into a platform-agnostic `NotificationEnvelope`. * Used when a sink has `wrap_response: true` in its config. Unknown event * types fall back to a generic title/summary so newly-emitted events do * not require a code change to be deliverable. */ declare function wrapAsEnvelope(event: GatewayEvent): NotificationEnvelope; /** * Payload passed to `NotificationSink.deliver`. `wrapped` discriminates * which member of the union `payload` is. Sinks branch on `wrapped` * rather than runtime-detecting the shape. */ interface NotificationSinkDeliverInput { payload: GatewayEvent | NotificationEnvelope; wrapped: boolean; } /** * Hermes Phase 3 sink contract. * * Sinks subscribe to the orchestrator event bus via `wireNotificationSinks` * and deliver each filtered event to a destination (chat channel, webhook * URL, etc.). Delivery is best-effort: no retry, no persistence. * * Sinks MUST be idempotent w.r.t. their own delivery semantics — the bus * may emit the same logical state transition more than once during testing * or recovery. Sinks should not assume one-shot semantics. */ interface NotificationSink { /** Stable id used in config + CLI; lowercase, kebab-case. */ readonly id: string; /** Sink kind literal (matches `NotificationSinkKind`). */ readonly kind: string; /** One-shot delivery. Returns Ok on 2xx, Err on any other outcome. */ deliver(input: NotificationSinkDeliverInput): Promise; /** Optional teardown hook called on orchestrator stop. */ dispose?(): Promise; } interface SlackSinkOptions { id: string; webhookUrl: string; fetchImpl?: typeof fetch; timeoutMs?: number; } /** * Slack sink shipped with Hermes Phase 3. Uses incoming-webhook URLs only; * OAuth + bot tokens are intentionally out of scope (spec D3). Sends one * HTTP POST per delivery and never retries — retries are the operator's * call via the Phase 0 webhook fanout if they need durable delivery. */ declare class SlackSink implements NotificationSink { readonly kind = "slack"; readonly id: string; private readonly webhookUrl; private readonly fetchImpl; private readonly timeoutMs; constructor(opts: SlackSinkOptions); deliver(input: NotificationSinkDeliverInput): Promise; private renderEnvelope; private renderRawEvent; } interface RegistryEntry { config: NotificationSinkConfig; adapter: NotificationSink; } interface FromConfigOptions { env: NodeJS.ProcessEnv; /** Optional per-sink fetch override (testing). */ fetchImpl?: typeof fetch; } /** * Surfaced when a sink config refers to an unknown kind, or its env-var * secret cannot be resolved. Carrying the sinkId helps the doctor + CLI * print operator-actionable messages. */ declare class SinkConfigError extends Error { readonly sinkId: string; constructor(sinkId: string, message: string); } /** * In-memory registry of configured notification sinks. Built once at * orchestrator startup from `harness.config.json` `notifications.sinks[]`. * Disposed on orchestrator stop. */ declare class SinkRegistry { private readonly entries; private constructor(); static fromConfig(config: NotificationsConfig, options: FromConfigOptions): SinkRegistry; list(): readonly RegistryEntry[]; get(id: string): RegistryEntry | null; ids(): string[]; dispose(): Promise; } interface WireParams { bus: EventEmitter; registry: SinkRegistry; } declare function wireNotificationSinks({ bus, registry }: WireParams): () => void; /** * Phase 4 gate (degraded mode, see spec D5). * * The full design calls for `harness skill run harness-soundness-review * --mode skill` against materialized proposal content. The skill-mode check * vocabulary is not yet designed; its design is the explicit follow-up spec * referenced in Phase 4's Non-goals. * * In v1 we run a small set of mechanical checks inline against the proposal * payload. They cover the obvious structural failures (unparseable YAML, * empty markdown, name/regex drift) without needing an LLM. The result * shape mirrors the eventual soundness-review output so the downstream * promote step (and dashboard panel) does not need to change when * skill-mode lands. */ declare class GateRunError extends Error { constructor(message: string); } interface GateResult { proposalId: string; status: SkillProposal['status']; findings: ProposalGateFinding[]; runAt: string; } /** * Synchronously run the gate against the given proposal. The proposal is * read from disk, checks are computed, and the proposal JSON is patched * with the gate result. Returns the post-update gate snapshot for the * caller to render. */ declare function runGate(projectPath: string, proposalId: string): Promise; declare class GateNotReadyError extends Error { constructor(message: string); } declare class PromotionError extends Error { constructor(message: string); } interface PromotionResult { proposalId: string; skillPath: string; /** Provenance field stamped onto the promoted skill. */ provenance: 'agent-proposed'; } /** * Promote a proposal to the skill catalog. Caller is responsible for * emitting `proposal.approved` after a successful return. */ declare function promote(projectPath: string, proposalId: string, decidedBy: string): Promise; /** * Phase 4 — thin wrappers around the orchestrator event bus that emit the * three `proposal.*` lifecycle events with a stable, validated payload * shape. Both the webhook fan-out (gateway/webhooks/events.ts) and the * in-process notification dispatcher (notifications/events.ts) subscribe * to these topics; their envelope derivers know the field names below. */ interface ProposalCreatedData { id: string; kind: SkillKind; name: string; targetSkill?: string; proposedBy: string; justification: string; } interface ProposalApprovedData { id: string; kind: SkillKind; name: string; targetSkill?: string; decidedBy: string; } interface ProposalRejectedData { id: string; kind: SkillKind; name: string; decidedBy: string; reason: string; } declare function emitProposalCreated(bus: EventEmitter, proposal: SkillProposal): void; declare function emitProposalApproved(bus: EventEmitter, proposal: SkillProposal): void; declare function emitProposalRejected(bus: EventEmitter, proposal: SkillProposal): void; export { type AcceptanceOutcome, AdaptiveRouter, type AdaptiveRouterDeps, type AgentDispatchResult, type AgentDispatcher, type AgentDispatcherDeps, type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BRAINSTORM_RUBRIC, BUILT_IN_TASKS, BackendDefSchema, type BackendResolver, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BrainstormWiringDeps, type BudgetState, type BuildArchiveHooksOptions, type BuildWorkflowContextDeps, type CheckCommandResult, type CheckCommandRunner, type CheckFailureClassification, type CheckFailureKind, CheckScriptRunner, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CommandExecResult, type CommandExecutor, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, DEFAULT_MAX_LOCAL_STAGE_GATE_RETRIES, type DispatchBudgetOptions, type DispatchEffect, ECOSYSTEM_RULES, type Ecosystem, type EcosystemId, type EmitLogEffect, type EscalateEffect, EscalationState, type ExecFileAsyncFn, type ExecFileError, type ExecFileFn$1 as ExecFileFn, FlightRecorder, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, HarnessFitProbeRunner, type HarnessFitProbeRunnerDeps, type HarnessSpawn, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, LOCAL_GATE_TIMEOUT_MS, LinearGraphQLClient, type LinearGraphQLClientOptions, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAINTENANCE_CHECK_MAX_BUFFER, MAINTENANCE_CHECK_TIMEOUT_MS, MAX_ATTEMPTS, MaintenanceReporter, type MaintenanceReporterOptions, type MarkSkip, type MarkSkipReason, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PRLifecycleManager, type PendingInteraction, type PersistedOutputEntry, PrivacyNoMatch, type ProbeWorkspace, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETROSPECTION_PROPOSED_BY, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RecordPrediction, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetrospectionContext, type RetrospectionResult, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunHarnessCheckOptions, type RunMode, type RunOrigin, type RunProvenance, type RunRecord, type RunResult, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SelForkGeneratorOptions, type SelectConstraints, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StoredOutcomeRecord, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, TaskRunner, type TaskRunnerOptions, type TaskSelectionFilter, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageMarkConfig, type TriageMarkDeps, type TriageMarkItem, type TriageMarkResult, type TriageSignals, type TriageSkill, type TriageWiringDeps, type UnitVerdict, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, type Verdict, WebhookQueue, type WiredBrainstormResult, type WorkerExitEvent, WorkflowLoader, type WorkflowRouterDep, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, assertIssueWithinContextBudget, brainstormInputFromIssue, buildArchiveHooks, buildCapabilityRegistry, buildProbeInput, buildWorkflowContext, calculateRetryDelay, calculateRetryDelayMs, canAffordDispatch, canDispatch, changedWorkspacePackages, classifyCheckExecutionFailure, cloneBudgetState, computeRateLimitDelay, createAgentDispatcher, createBackend, createBudgetState, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, defaultLocalAcceptanceRunner, defaultLocalDiffRunner, defaultLocalDocCoverageRunner, defaultLocalVerifyRunner, defaultPoolCapabilities, deriveSeedPaths, detectEcosystem, detectEcosystemFromFiles, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, enrichIssueWithSpec, estimateCost, estimateIssueContextTokens, explicitFindingsCount, extractHighlights, extractTitlePrefix, fleetKeyForIssue, gatherProvenance, getAvailableSlots, getBudgetStatus, getDefaultConfig, getPerStateCount, indexSessionDirectory, isCheckTimeoutError, isEligible, isFleetAllocationExhausted, isGlobalEnvelopeExhausted, isRetrospectionEnabled, isSummaryEnabled, launchTUI, loadPublishedIndex, makeBackendResolver, makeGraphScope, makeSelForkGenerator, markApprovedForDispatch, migrateAgentConfig, normalizeFts5Query, normalizeHarnessCommand, normalizeLocalModel, openSearchIndex, periodLengthMs, precedentLookupFromStored, promote, reconcile, reconcileRunningIssues, recordBudgetSpend, recoverFindingsCount, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, renderSpecMarkdown, resolveEscalationConfig, resolveOrchestratorId, resolvePeriodLengthMs, retrospectArchivedSession, rollBudgetPeriod, routeIssue, routingWarnings, runBrainstormForIssue, runGate, runHarnessCheck, savePublishedIndex, searchIndexPath, selectCandidates, selectCheapestQualifying, selectTasks, slugFor, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, truncateGateOutput, validateCustomTasks, validateWorkflowConfig, verifyChangedPackages, wireNotificationSinks, workflowFor, wrapAsEnvelope };