packages feed

claude-gate-1.0.0: src/Claude/Gate/StopGate.hs

-- | The Stop hook: the end-of-turn gate.
--
-- Three phases run in order over the per-turn state, mirroring the shell gate:
--
--   Phase 0 (dumbify). A cheap canary checks the changed code is understandable,
--   and the larger model simplifies it if not. See "Claude.Gate.Dumbify".
--   Phase 1 (critique). An adversarial critic tries to prove the work wrong with
--   tests and sources. See "Claude.Gate.Critique". (This replaced the old verification
--   nudge.)
--   Phase A (rule review). The diffs are checked against the rules corpus. See
--   "Claude.Gate.RuleReview".
--
-- Each phase may block the Stop (emit a reason and exit); later phases only run
-- on a Stop that no earlier phase blocked. The phases converge across the turn's
-- repeated Stops via per-phase flags on tmpfs. When every phase that ran cleared
-- without blocking, a non-blocking "gate clear" notice names them.
module Claude.Gate.StopGate
  ( runStopGate
  ) where

import Control.Monad (unless)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Data.Text qualified as Text
import Claude.Gate.Critique (runCritique)
import Claude.Gate.Dumbify (runDumbify)
import Claude.Gate.HookProtocol (HookEvent (sessionId, transcriptPath), emitSystemMessage, readHookEvent)
import Claude.Gate.RuleReview (runRuleReview)
import Claude.Gate.TurnState
  ( TurnPaths (critiqueApproved, dumbifyApproved, reviewApproved)
  , ensureStateDir
  , flagExists
  , turnPaths
  )

-- | Entry point for the @stop-gate@ subcommand.
runStopGate :: IO ()
runStopGate = do
  event <- readHookEvent
  paths <- turnPaths (sessionId event)
  ensureStateDir paths
  runDumbify (sessionId event) paths
  runCritique (sessionId event) (transcriptPath event) paths
  runRuleReview (sessionId event) paths
  emitGateClear paths

-- | Every phase that ran this turn concluded without blocking (a block would
-- have exited above). Emit one user-visible, non-blocking confirmation naming
-- the phases that ran and cleared, from their per-turn approved markers.
emitGateClear :: TurnPaths -> IO ()
emitGateClear paths = do
  cleared <- clearedPhases paths
  unless (null cleared) (emitSystemMessage ("gate clear:" <> Text.concat (map (" " <>) cleared)))

clearedPhases :: TurnPaths -> IO [Text]
clearedPhases paths =
  catMaybes
    <$> sequence
      [ namedIfPresent (dumbifyApproved paths) "dumbify"
      , namedIfPresent (critiqueApproved paths) "critique"
      , namedIfPresent (reviewApproved paths) "rules"
      ]

namedIfPresent :: FilePath -> Text -> IO (Maybe Text)
namedIfPresent path name = do
  present <- flagExists path
  pure (if present then Just name else Nothing)