import type { ExtensionContext, ExtensionFactory, ToolCallEvent, ToolCallEventResult, ToolResultEvent } from "@earendil-works/pi-coding-agent"; import type { ModelRuntime } from "@earendil-works/pi-coding-agent"; /** * Return type for tool_result pi handlers — matches ToolResultEventResult from * @earendil-works/pi-coding-agent (not exported from the package root). * Structural equivalence is sufficient for TypeScript assignability. */ export interface ToolResultEventResult { content?: Array<{ type: "text"; text: string; } | { type: "image"; source: unknown; }>; details?: unknown; isError?: boolean; } /** * Policy for a single persona/phase combination. * * residentFields — names of task/sprint record fields that are always retained * in context (not trimmed by Mechanism A). * toolBudgets — per-tool soft token budget caps (keyed by tool name). * T04 reads these when deciding how aggressively to trim tool_result content. * steerThreshold — fraction of contextWindow at which Mechanism B fires a * budget-steer note (0–1; e.g. 0.80 = steer when 80% of window is used). */ export interface PhasePolicy { residentFields: string[]; toolBudgets: Record; steerThreshold: number; } /** * Lookup table keyed by `"${persona}/${phase}"` (e.g. `"architect/plan"`, * `"engineer/review"`) plus a `"default"` entry for unknown combinations. */ export type PhasePolicyTable = Record; /** * Governor interface wired into hook-dispatcher.ts. * T03: both methods return undefined/void (no-op). T04 supplies live curation * logic via createGovernor. * * MUST NOT throw — IL7. Any internal failure must return undefined silently. */ export interface ContextGovernor { /** * Called after the triage-error block in the tool_result handler. * Return a ToolResultEventResult to replace the event content, or undefined * to pass through unchanged. */ applyToolResult(event: ToolResultEvent, ctx: ExtensionContext): ToolResultEventResult | undefined; /** * Called at the tail of the tool_call handler (after all existing guards). * Return a ToolCallEventResult to block or modify the call, or undefined/void * to pass through unchanged. */ applyToolCall(event: ToolCallEvent, ctx: ExtensionContext): ToolCallEventResult | void; } /** * Returns a ContextGovernor whose methods are pure pass-throughs. * Used as the default in registerHookDispatcher so existing callers that do * not pass a governor see zero behavioural change. */ export declare function createNoOpGovernor(): ContextGovernor; /** * Create a governor backed by the given policy table and model registry. * * Implements Mechanism A curation rules (T04): * Rule 1 — Dedup/reference-ize * Rule 2 — Schema-trim (forge_store results) * Rule 3 — Span-clamp (bash/grep/find/read results) * * Implements Mechanism B (T05): * Budget meter: per-turn ctx.getContextUsage() → ctx.ui.setStatus("forge:ctx-budget", ...) * Steer: one-shot note at policy.steerThreshold, injected via steerFn * * Implements Mechanism C (T06): * Checkpoint-and-shed: forge_store results for summarized entities are evicted * and replaced with an eviction pointer; unsummarized material is retained. * Shed criterion: summarySentinel(phaseKey, entityId) returns true. * * @param table Phase-policy table (keyed by "persona/phase"). * @param _modelRegistry Model registry (fallback contextWindow resolution only). * @param steerFn Optional callback injected at construction by registerHookDispatcher. * Receives the steer message string; called at most once per governor * instance (single-fire invariant). Callers that omit this see no steer. * @param summarySentinel Optional read-only probe injected at construction (Mechanism C / T06). * Receives (phaseKey, entityId); returns true when a {PHASE}-SUMMARY.json * has been durably written for that entity. When true, the forge_store result * is replaced with an eviction pointer. Callers that omit this param see no * shedding (backwards-compatible; undefined default). * The sentinel MUST NOT write to .forge/store/ or the summary itself (Pack 07). * Errors inside the sentinel are silently caught and cause retain, not eviction (IL7). * * contextWindow resolution (provider-neutral): usage.contextWindow from * ctx.getContextUsage(), used directly when available; otherwise the budget * meter is cleared for the turn and no steer/compact decision is made. * @param compactFn opt callback injected at construction (Mechanism E / T09). * Called proactively once when fraction >= policy.steerThreshold, * via a single-fire `compactFired` flag distinct from `steerFired`. * Callers pass `compactFn = () => session.compact()`. Errors inside * compactFn are caught and written to stderr (IL7). Omitting this * param is backwards-compatible — no compact trigger fires. * @param phaseKey opt construction-time phase key override ("persona/role"). * Production paths MUST pass this (via buildGovernorFactory) — * pi never populates persona/phase on ExtensionContext, so the * ctx probe always resolves "default" at runtime. Omitting it * preserves the legacy ctx-probe behaviour (test harnesses). */ export declare function createGovernor(table: PhasePolicyTable, _modelRegistry: ModelRuntime, steerFn?: (message: string) => void, summarySentinel?: (phaseKey: string, entityId: string) => boolean, compactFn?: () => void, phaseKey?: string): ContextGovernor; /** * Load the built-in phase-policy table. * * Ships an entry for every governed run-task PHASE_PIPELINE key * (`${personaNoun}/${role}` — engineer/plan, supervisor/review-plan, * engineer/implement, supervisor/review-code, qa-engineer/validate, * architect/approve) plus "default" for any unlisted persona/phase. * writeback/commit intentionally stay on "default" — small phases whose * git/store output must not be clamped. * * `read` budgets are deliberately more generous than `bash` — clamping file * reads too tightly degrades implement/review quality (the agent cannot see * whole files), while bash output (store-cli reads, test logs, greps) is the * dominant context bloat observed in the CART-S02-T03 baseline. * * Legacy design-time keys ("architect/plan", "engineer/review") are retained * for existing test fixtures; the pipeline never produces them. * * Values are conservative design-time decisions; a future task can promote * specific fields to project config once per-project tuning evidence exists. */ export declare function loadDefaultPolicyTable(): PhasePolicyTable; /** Options for buildGovernorFactory. */ export interface GovernorFactoryOptions { /** * Pipeline phase key, `${personaNoun}/${role}` (e.g. "supervisor/review-code"). * Known to run-task.ts at dispatch time; injected here because pi never * populates persona/phase on ExtensionContext. */ phaseKey: string; /** Project cwd — root containing `.forge/store/` (sentinel reads only). */ cwd: string; } /** * Build an ExtensionFactory that registers a fully-wired context governor in a * subagent session. Constructed per-phase by run-task.ts (which knows the * persona/role) and passed via RunSubagentOptions.extensionFactories — * the same injection channel as buildForgeCompactionFactory (Mechanism E). * * This is the production wiring the original FORGE-S30-T07 integration missed: * registerHookDispatcher(pi, …, governor) in index.ts only governs the PARENT * session, while every phase runs in an isolated createAgentSession subagent * that the parent's hooks never see. The CART-S02-T03 benchmark confirmed the * result: zero curation markers across a full FORGE_CTX_GOVERNOR=1 phase. * * Wiring supplied here: * phaseKey — construction-time (Mechanism D policies finally reachable) * steerFn — pi.sendUserMessage(msg, { deliverAs: "steer" }) (Mechanism B) * summarySentinel — storeSummarySentinel against .forge/store/ (Mechanism C) * compactFn — ctx.compact() proactive trigger (Mechanism E) * * steer uses the session-scoped ExtensionAPI directly; compact rides the * ExtensionContext captured at the start of each handler invocation — it only * fires synchronously inside applyToolResult, so the captured ctx is always * the live one. All callbacks are guarded: failures fall through silently * (IL7); the factory never writes to .forge/store/ (Pack 07). */ export declare function buildGovernorFactory(opts: GovernorFactoryOptions): ExtensionFactory;