/** * Hook registrar — wires every harness hook (pre-edit dupe probe, post-session sweep, * layer-context-inject) onto the platform. * * **Project-scoped**: hooks are gated by `/.omp/supipowers/harness/marker.json`. * The registrar runs at extension boot but each individual hook checks the marker on * every event so a repo without the harness installed sees no behavior change. * * Hook activation also depends on the per-hook `enabled` flag in * `.omp/supipowers/config.json`; we read that lazily inside each hook so config edits * take effect on the next event without restarting OMP. */ import type { Platform } from "../../platform/types.js"; import type { HarnessConfig, HarnessDocsConfig, HarnessHookConfig } from "../../types.js"; import { buildBackendAdapter } from "../anti_slop/backend-factory.js"; import { registerLayerContextInjectHook, } from "./layer-context-inject.js"; import { registerPostSessionSweepHook, } from "./post-session-sweep.js"; import { registerPreEditDupeProbeHook, } from "./pre-edit-dupe-probe.js"; export const DEFAULT_HARNESS_HOOK_CONFIG: HarnessHookConfig = { pre_edit_dupe_probe: { enabled: true, threshold: 0.85, min_token_count: 30 }, post_session_sweep: { enabled: true, block_on_new_dead_code: false }, layer_context_inject: { enabled: true, addendum_max_chars: 800 }, score_floor: { strict: 75, lenient: 90, release_blocking: false }, }; export const DEFAULT_HARNESS_DOCS_CONFIG: HarnessDocsConfig = { tier: "simple", max_per_doc_loc: 150, agent_context_loc: 30, max_index_loc: 50, max_units: 12, max_concurrent_subagents: null, drift_warning: { enabled: true }, regen_preview_threshold: 1, }; export const DEFAULT_HARNESS_CONFIG: HarnessConfig = { anti_slop: DEFAULT_HARNESS_HOOK_CONFIG, implement_in_session_threshold: 10, docs: DEFAULT_HARNESS_DOCS_CONFIG, }; export interface HarnessHookRegistration { /** Tear down every registered hook. Used by tests for clean isolation. */ dispose(): void; /** True if any hook actually subscribed (i.e. config + backend allow it). */ active: boolean; } export interface RegisterHooksOptions { /** Backend to use for the duplicate / dead-code adapters. Defaults to fallow. */ backend?: "fallow" | "desloppify" | "supi-native" | "hybrid"; /** Hook config snapshot. Defaults to DEFAULT_HARNESS_HOOK_CONFIG. */ hooks?: HarnessHookConfig; /** * Extracts the candidate file the agent is about to edit. Implementations consult * session state. Defaults to a no-op resolver (the layer-inject hook becomes a no-op * unless a real resolver is wired). */ resolveCandidateFile?: (event: unknown, ctx: unknown) => string | null; /** CWD whose repo-local marker controls registration. Defaults to process.cwd(). */ cwd?: string; } // Re-export so existing call sites keep working without an import path change. export { buildBackendAdapter }; /** * Register every harness hook. Hooks subscribe unconditionally at bootstrap time; each * hook checks the repo-local marker per event, so creating the marker after install * activates already-registered handlers without an OMP restart, and removing the marker * disables them. `dispose()` is idempotent. * * The `cwd` option is retained for tests that exercise the legacy marker check; it is * unused by the new registration path because per-event handlers resolve cwd from the * event payload. */ export function registerHarnessHooks( platform: Platform, options: RegisterHooksOptions = {}, ): HarnessHookRegistration { void options.cwd; // reserved for future per-repo gating const backend = options.backend ?? "fallow"; const hooks = options.hooks ?? DEFAULT_HARNESS_HOOK_CONFIG; const adapter = buildBackendAdapter(backend); const teardowns: Array<() => void> = []; let active = false; if (hooks.pre_edit_dupe_probe.enabled && adapter) { teardowns.push( registerPreEditDupeProbeHook(platform, { adapter, config: hooks.pre_edit_dupe_probe, }), ); active = true; } if (hooks.post_session_sweep.enabled && adapter) { teardowns.push( registerPostSessionSweepHook(platform, { adapter, config: hooks.post_session_sweep, }), ); active = true; } if (hooks.layer_context_inject.enabled) { teardowns.push( registerLayerContextInjectHook(platform, { config: hooks.layer_context_inject, resolveCandidateFile: options.resolveCandidateFile ?? (() => null), }), ); active = true; } return { active, dispose() { while (teardowns.length > 0) { const t = teardowns.pop(); try { t?.(); } catch { // best-effort } } }, }; }