26 KiB
katip logging Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace all print/putStrLn logging under src/ with katip structured logging, threading a LogEnv through the existing Bus-based runtime.
Architecture: Approach A — Bus gains a busLogEnv :: LogEnv field; defaultMain builds the LogEnv (one stdout scribe, bracketed closeScribes) and passes it to newBus. The HASSEff interpreters (dryRunHassEval, channelHassEval) gain the controller name as a 2nd arg and log via runKatipT. supervised gains a leading LogEnv. The AFRP layer and controller definitions are untouched.
Tech Stack: Haskell (GHC2024), cabal, nix devShell, katip 0.8.8.4 (in nixpkgs), hspec/hedgehog.
Global Constraints
- Build always via the nix devShell:
nix develop -c cabal build. - Tests always via the nix devShell:
nix develop -c cabal test. - After any edit to
home-assistant-controller.cabal, regeneratedefault.nixwithnix run nixpkgs#cabal2nix -- ./. > default.nix— never hand-editdefault.nix(AGENTS.md). secrets.yamlis sops-encrypted — never read, print, or commit its contents.- Code style: vertical modules, minimal comments describing contracts not implementation (AGENTS.md). No comments unless asked.
app/Main.hsis outsidesrc/and is NOT modified.- AFRP layer (
AFRP.hs) and controller definitions (Controller.hs,Controller/Bedroom.hs) remain logging-free. -Wallis on (cabalcommon warnings); all code must compile without warnings (no unused bindings).
File Structure
| File | Responsibility | Change |
|---|---|---|
home-assistant-controller.cabal |
build config | add katip to library + test build-depends; add Support to test other-modules |
default.nix |
generated cabal derivation | regenerated by cabal2nix (twice) |
src/HomeAssistant/Runtime/Bus.hs |
Bus, newBus, channelHassEval |
add busLogEnv; newBus :: LogEnv -> Int -> IO Bus; channelHassEval :: Bus -> Text -> ... with katip |
src/HomeAssistant/Runtime.hs |
defaultMain, runController, dryRunHassEval |
build LogEnv + bracket; thread controller name; dryRunHassEval :: Bus -> Text -> ... with katip |
src/HomeAssistant/Runtime/Supervisor.hs |
supervised |
supervised :: LogEnv -> Text -> Backoff -> IO Void -> IO Void with katip |
src/HomeAssistant/Runtime/Connection.hs |
reader/writer workers | two putStrLn → logMsg |
test/Support.hs |
test helpers (new) | silentLogEnv :: IO LogEnv |
test/BusSpec.hs |
bus tests | pass silentLogEnv, add name arg |
test/RuntimeSpec.hs |
runtime tests | pass silentLogEnv |
test/SupervisorSpec.hs |
supervisor tests | pass silentLogEnv, add LogEnv arg (3 sites) |
No new modules under src/. No changes to AFRP.hs, Controller.hs, Controller/Bedroom.hs, ConnectionSpec.hs, BackoffProp.hs, app/Main.hs.
Task 1: Add katip dependency
Files:
- Modify:
home-assistant-controller.cabal(librarybuild-depends~line 77-89, test-suitebuild-depends~line 149-160) - Regenerate:
default.nix
Interfaces:
-
Consumes: nothing
-
Produces:
katipavailable to library and test-suite;default.nixlistskatipinlibraryHaskellDependsandtestHaskellDepends -
Step 1: Add katip to library build-depends
In home-assistant-controller.cabal, in the library stanza's build-depends (after uuid), add , katip. The library block becomes:
build-depends: base ^>=4.20.2.0
, websockets
, aeson
, lens-aeson
, lens
, text
, network
, bytestring
, time
, stm
, async
, annotated-exception
, uuid
, katip
- Step 2: Add katip to test-suite build-depends
In the test-suite home-assistant-controller-test stanza's build-depends (after time), add , katip:
build-depends:
base ^>=4.20.2.0,
home-assistant-controller,
hspec,
stm,
aeson,
text,
async,
hedgehog,
hspec-hedgehog,
annotated-exception,
time,
katip
- Step 3: Regenerate default.nix
Run:
nix run nixpkgs#cabal2nix -- ./. > default.nix
Expected: default.nix now lists katip in libraryHaskellDepends and testHaskellDepends. Verify with:
grep katip default.nix
Expected output: two lines mentioning katip.
- Step 4: Build to verify the dependency resolves
Run:
nix develop -c cabal build
Expected: builds successfully (no code uses katip yet, so no compile errors). If nix needs to build katip first this may take a while on the first run.
- Step 5: Commit
git add home-assistant-controller.cabal default.nix
git commit -m "Add katip dependency"
Task 2: Add busLogEnv to Bus; newBus signature; silentLogEnv helper; update callers
This task adds the LogEnv field to Bus, changes newBus to take LogEnv, adds a silent test helper, and updates every newBus caller so the build compiles. No logging behavior changes yet (the interpreters still print; they switch in Task 3).
Files:
- Modify:
src/HomeAssistant/Runtime/Bus.hs(full file) - Modify:
src/HomeAssistant/Runtime.hs(defaultMain~line 62-72) - Modify:
home-assistant-controller.cabal(testother-modules~line 130) - Regenerate:
default.nix - Create:
test/Support.hs - Modify:
test/BusSpec.hs(~line 19) - Modify:
test/RuntimeSpec.hs(~line 17)
Interfaces:
-
Consumes:
katip(LogEnv,initLogEnv) from Task 1 -
Produces:
Bushas fieldbusLogEnv :: LogEnvnewBus :: LogEnv -> Int -> IO BusSupport.silentLogEnv :: IO LogEnv
-
Step 1: Add busLogEnv field and update newBus in Bus.hs
In src/HomeAssistant/Runtime/Bus.hs, add imports and the field. The full updated file:
{-# LANGUAGE LambdaCase #-}
module HomeAssistant.Runtime.Bus
( Bus(..)
, CallIdGen(..)
, mkCallIdGen
, newBus
, channelHassEval
) where
import Control.Concurrent.STM
( TChan
, TVar
, atomically
, newBroadcastTChanIO
, newTChanIO
, newTVarIO
, writeTChan
)
import Data.Aeson (Value)
import Data.IORef (atomicModifyIORef', newIORef)
import HomeAssistant.Controller (HASSEff (..), Service)
import Katip (LogEnv)
import Network.WebSockets (Connection)
-- | Shared runtime state: inbound is a broadcast channel (controllers
-- read from 'dupTChan' copies), outbound queues service calls for the
-- writer, conn holds the current websocket (Nothing before first connect).
data Bus = Bus
{ busInbound :: TChan Value
, busOutbound :: TChan Service
, busConn :: TVar (Maybe Connection)
, busGen :: CallIdGen
, busLogEnv :: LogEnv
}
newBus :: LogEnv -> Int -> IO Bus
newBus le start = Bus
<$> newBroadcastTChanIO
<*> newTChanIO
<*> newTVarIO Nothing
<*> mkCallIdGen start
<*> pure le
channelHassEval :: Bus -> HASSEff a -> IO a
channelHassEval bus = \case
CallService svc -> atomically $ writeTChan (busOutbound bus) svc
Debug x -> print x
Trace req x -> print (req, x)
newtype CallIdGen = CallIdGen { generateCallId :: IO Int }
mkCallIdGen :: Int -> IO CallIdGen
mkCallIdGen start = do
gen <- newIORef start
pure $ CallIdGen $ atomicModifyIORef' gen (\old -> let new = old + 1 in new `seq` (new, new))
Note: channelHassEval still uses print here — it is converted in Task 3. Keeping it temporarily so this task compiles. Data.Text (Text) is NOT imported yet (it would be unused here, triggering -Wall); Task 3 adds it when the signature gains the Text name parameter.
- Step 2: Update defaultMain to build a LogEnv and pass to newBus
In src/HomeAssistant/Runtime.hs, update imports and defaultMain. Add these imports near the top (with the other imports):
import Control.Exception (bracket)
import Katip
( ColorStrategy (ColorIfTerminal)
, Severity (DebugS)
, Verbosity (V2)
, closeScribes
, defaultScribeSettings
, initLogEnv
, mkHandleScribe
, permitItem
, registerScribe
)
import System.IO (stdout)
Then replace defaultMain with:
defaultMain :: IO ()
defaultMain = withSocketsDo $ do
scribe <- mkHandleScribe ColorIfTerminal stdout (permitItem DebugS) V2
le <- registerScribe "stdout" scribe defaultScribeSettings
=<< initLogEnv "home-assistant-controller" "production"
bracket (pure le) closeScribes $ \le' -> do
token <- getEnv "HA_TOKEN"
bus <- newBus le' 0
let workers =
[ ("reader", readerAction "last-resort-redux" 8123 token bus)
, ("writer", writerAction bus)
] ++ [ (name, runController bus c) | c@(Controller name _) <- controllers ]
as <- mapM (\(name, act) -> async (supervised name defaultBackoff act)) workers
(_, v) <- waitAny as
absurd v
Note: supervised still has its 3-arg signature here — it is updated in Task 4. The Controller name _ pattern (was _name) is required now so the workers list can use name; this is safe since Controller's first field is already T.Text. mkHandleScribe, ColorIfTerminal, permitItem, and V2 are all re-exported by the main Katip module, so no separate Katip.Scribes.Handle import is needed.
- Step 3: Create test/Support.hs with silentLogEnv
Create test/Support.hs:
module Support
( silentLogEnv
) where
import Katip (LogEnv, initLogEnv)
-- | A LogEnv with no scribes: all log calls are no-ops. For tests that
-- must construct a Bus or call supervised without spewing output.
silentLogEnv :: IO LogEnv
silentLogEnv = initLogEnv "home-assistant-controller" "test"
- Step 4: Add Support to test-suite other-modules in cabal
In home-assistant-controller.cabal, in the test-suite's other-modules (currently lists BusSpec, ConnectionSpec, RuntimeSpec, SupervisorSpec, BackoffProp), add , Support:
other-modules: BusSpec
, ConnectionSpec
, RuntimeSpec
, SupervisorSpec
, BackoffProp
, Support
- Step 5: Regenerate default.nix
Run:
nix run nixpkgs#cabal2nix -- ./. > default.nix
Expected: default.nix regenerated; katip still present, Support is not listed (test modules aren't in the derivation) — this is fine.
- Step 6: Update BusSpec to use silentLogEnv
In test/BusSpec.hs, add the import and update the two newBus 0 calls. Add to imports:
import Support (silentLogEnv)
Replace each bus <- newBus 0 with:
le <- silentLogEnv
bus <- newBus le 0
There are two occurrences (lines ~19 and ~30). The channelHassEval bus (CallService svc) call on ~line 32 is unchanged in this task (Task 3 adds the name arg).
- Step 7: Update RuntimeSpec to use silentLogEnv
In test/RuntimeSpec.hs, add the import:
import Support (silentLogEnv)
Replace bus <- newBus 0 (line ~17) with:
le <- silentLogEnv
bus <- newBus le 0
- Step 8: Build and run tests to verify green
Run:
nix develop -c cabal build && nix develop -c cabal test
Expected: builds with no warnings (the print in channelHassEval is still used, so no unused-binding warning); all tests pass.
- Step 9: Commit
git add src/HomeAssistant/Runtime/Bus.hs src/HomeAssistant/Runtime.hs test/Support.hs test/BusSpec.hs test/RuntimeSpec.hs home-assistant-controller.cabal default.nix
git commit -m "Add busLogEnv to Bus, silentLogEnv test helper"
Task 3: Convert HASSEff interpreters to katip; thread controller name
Both interpreters gain the controller name (Text) as the 2nd arg and log via runKatipT + logMsg/logF. runController threads the Controller name into the active interpreter call. Debug/Trace use the controller name as the namespace; Trace logs requestTraceId as a structured field.
Files:
- Modify:
src/HomeAssistant/Runtime/Bus.hs(channelHassEval) - Modify:
src/HomeAssistant/Runtime.hs(dryRunHassEval,runController, imports) - Modify:
test/BusSpec.hs(channelHassEvalcall)
Interfaces:
-
Consumes:
busLogEnv :: LogEnvfrom Task 2 -
Produces:
channelHassEval :: Bus -> Text -> HASSEff a -> IO adryRunHassEval :: Bus -> Text -> HASSEff a -> IO arunControllerpasses the controller name to the interpreter
-
Step 1: Convert channelHassEval to katip in Bus.hs
In src/HomeAssistant/Runtime/Bus.hs, update imports and channelHassEval. First add OverloadedStrings to the pragmas at the top of the file (needed so the "traceId" literal in sl is Text):
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
Replace the import Katip (LogEnv) line with:
import AFRP (Request (requestTraceId))
import Data.Text (Text)
import Data.UUID (toText)
import Katip
( LogEnv
, Namespace (Namespace)
, Severity (DebugS)
, logF
, logMsg
, runKatipT
, showLS
, sl
)
Replace the channelHassEval definition with:
channelHassEval :: Bus -> Text -> HASSEff a -> IO a
channelHassEval bus name = \case
CallService svc -> atomically $ writeTChan (busOutbound bus) svc
Debug x -> runKatipT (busLogEnv bus) $
logMsg (Namespace [name]) DebugS (showLS x)
Trace req x -> runKatipT (busLogEnv bus) $
logF (sl "traceId" (toText (requestTraceId req))) (Namespace [name]) DebugS (showLS x)
Trace req x binds req :: Request (from HASSEff's Trace constructor, imported via HASSEff (..)); requestTraceId is the record field brought into scope by import AFRP (Request (requestTraceId)). toText :: UUID -> Text (aeson's ToJSON Text makes sl happy). UUID's ToJSON would also work directly, but toText is used consistently with Runtime.hs per the spec.
- Step 2: Convert dryRunHassEval to katip in Runtime.hs
In src/HomeAssistant/Runtime.hs, update imports. Extend the Data.UUID import to include toText:
import Data.UUID (UUID, toText)
Add to the Katip import list (from Task 2) the logging functions. The full Katip import for Runtime.hs becomes:
import Katip
( ColorStrategy (ColorIfTerminal)
, Namespace (Namespace)
, Severity (DebugS)
, Verbosity (V2)
, closeScribes
, defaultScribeSettings
, initLogEnv
, logF
, logMsg
, mkHandleScribe
, permitItem
, registerScribe
, runKatipT
, showLS
, sl
)
Replace dryRunHassEval with:
dryRunHassEval :: Bus -> T.Text -> HASSEff a -> IO a
dryRunHassEval bus name = \case
CallService svc -> do
callId <- generateCallId (busGen bus)
runKatipT (busLogEnv bus) $
logF (sl "callId" callId) (Namespace [name]) DebugS (showLS svc)
Debug x -> runKatipT (busLogEnv bus) $
logMsg (Namespace [name]) DebugS (showLS x)
Trace req x -> runKatipT (busLogEnv bus) $
logF (sl "traceId" (toText (requestTraceId req))) (Namespace [name]) DebugS (showLS x)
Note: the callId is logged as a structured field (keeping generateCallId meaningful under -Wall — a small refinement over the spec's logMsg ... showLS svc, which would have left callId unused). requestTraceId is accessed via the Request record; Runtime.hs already imports AFRP (Event (..), Mealy (..), Request (..)) — confirm Request (..) brings requestTraceId into scope. The current import is import AFRP (Event (..), Mealy (..), Request (..)), so requestTraceId is in scope. Good.
- Step 3: Thread controller name in runController
In src/HomeAssistant/Runtime.hs, update runController to use the name and pass it to the interpreter. Replace runController with:
runController :: Bus -> Controller -> IO Void
runController bus (Controller name machine) = do
inbound <- atomically (dupTChan (busInbound bus))
go inbound machine
where
go inbound f = do
msg <- atomically (readTChan inbound)
uuid <- UUID.V4.nextRandom
-- (_, f') <- step (channelHassEval bus name) uuid f (Event msg)
(_, f') <- step (dryRunHassEval bus name) uuid f (Event msg)
go inbound f'
The commented channelHassEval line is updated to the new signature too, so enabling it later requires no further edits.
- Step 4: Update BusSpec channelHassEval call
In test/BusSpec.hs, the test "channelHassEval writes CallService to the outbound channel" calls channelHassEval bus (CallService svc). Update it to pass a name:
channelHassEval bus "test" (CallService svc)
- Step 5: Build and run tests to verify green
Run:
nix develop -c cabal build && nix develop -c cabal test
Expected: builds with no warnings; all tests pass. The print calls in channelHassEval/dryRunHassEval are now gone — confirm with:
rg -n "print" src/HomeAssistant/Runtime/Bus.hs src/HomeAssistant/Runtime.hs
Expected: no output (no print remains in those two files).
- Step 6: Commit
git add src/HomeAssistant/Runtime/Bus.hs src/HomeAssistant/Runtime.hs test/BusSpec.hs
git commit -m "Convert HASSEff interpreters to katip, thread controller name"
Task 4: Convert supervised to take LogEnv and use katip
supervised gains a leading LogEnv argument and replaces its two putStrLn calls with logMsg. The crash message is WarningS, the restart message is InfoS, both under namespace runtime.supervisor.<name>. defaultMain and SupervisorSpec are updated.
Files:
- Modify:
src/HomeAssistant/Runtime/Supervisor.hs(imports,supervised) - Modify:
src/HomeAssistant/Runtime.hs(defaultMainsupervised call) - Modify:
test/SupervisorSpec.hs(3supervisedcall sites + import)
Interfaces:
-
Consumes:
LogEnvfrom katip (Task 1),busLogEnv(Task 2) for thedefaultMaincall -
Produces:
supervised :: LogEnv -> Text -> Backoff -> IO Void -> IO Void -
Step 1: Update supervised in Supervisor.hs
In src/HomeAssistant/Runtime/Supervisor.hs, add OverloadedStrings to the pragmas at the top (needed so the message string literals are LogStr):
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
Add imports. Add after the existing imports:
import Katip
( LogEnv
, Namespace (Namespace)
, Severity (InfoS, WarningS)
, logMsg
, ls
, runKatipT
, showLS
)
Also remove the now-unused import qualified Data.Text as T line (the only T. usages were T.unpack/T.pack in the removed putStrLn calls; with -Wall the unused qualified import would warn). Keep import Data.Text (Text) — it is still used by Fatal :: Text and the supervised signature.
Update the export list (the type signature changes but supervised stays exported). Update supervised's type and body. Replace the supervised definition with:
supervised :: LogEnv -> Text -> Backoff -> IO Void -> IO Void
supervised le name backoff action = go 1
where
go attempt = do
start <- getCurrentTime
outcome <- (action >> pure (Nothing :: Maybe (Either Fatal SomeException))) `catches`
[ Handler $ \(f :: Fatal) -> pure (Just (Left f))
, Handler $ \(e :: SomeException) -> pure (Just (Right e))
]
end <- getCurrentTime
case outcome of
Nothing -> error "unreachable: supervised action returned"
Just (Left f) -> throw f
Just (Right e) -> do
runKatipT le $
logMsg (Namespace ["runtime", "supervisor", name]) WarningS
("attempt " <> showLS attempt <> " crashed: " <> ls (displayException e))
let delay = backoffDelay backoff attempt
runKatipT le $
logMsg (Namespace ["runtime", "supervisor", name]) InfoS
("restarting in " <> showLS delay)
threadDelay (round (realToFrac delay * 1000000 :: Double))
go (nextAttempt backoff (diffUTCTime end start) attempt)
Note: logMsg's message is a LogStr; (<>) concatenates LogStr (it has a Semigroup instance). showLS uses show, so it is right for attempt (Int) and delay (NominalDiffTime) but wrong for displayException e (a String) — showLS would double-quote/escape it. Use ls (which converts via StringConv to Text) for raw String values. displayException is already imported from Control.Exception.Annotated. Data.Text (Text) stays for Fatal/the signature; the qualified T import is dropped (see above).
- Step 2: Update defaultMain supervised call in Runtime.hs
In src/HomeAssistant/Runtime.hs, in defaultMain, the supervised call is currently async (supervised name defaultBackoff act). Update it to pass the LogEnv:
as <- mapM (\(name, act) -> async (supervised le' name defaultBackoff act)) workers
(le' is the bracketed LogEnv from Task 2.)
- Step 3: Update SupervisorSpec to pass silentLogEnv
In test/SupervisorSpec.hs, add imports:
import Support (silentLogEnv)
There are three async (supervised "test" tinyBackoff action) calls (lines ~28, ~41, ~56). Each must become:
le <- silentLogEnv
sup <- async (supervised le "test" tinyBackoff action)
Insert the le <- silentLogEnv line before each async call. The three sites are in the it blocks: "restarts a crashing action until it stays up", "rethrows Fatal instead of restarting", and "does not restart on async exceptions".
For "rethrows Fatal instead of restarting" the surrounding block becomes:
it "rethrows Fatal instead of restarting" $ do
counter <- newIORef (0 :: Int)
let action = do
_ <- atomicModifyIORef' counter (\c -> (c + 1, c + 1))
throw (Fatal "auth_invalid")
le <- silentLogEnv
sup <- async (supervised le "test" tinyBackoff action)
res <- waitCatch sup
Apply the same le <- silentLogEnv insertion to the other two sites.
- Step 4: Build and run tests to verify green
Run:
nix develop -c cabal build && nix develop -c cabal test
Expected: builds with no warnings; all tests pass. Confirm no putStrLn remains in Supervisor.hs:
rg -n "putStrLn" src/HomeAssistant/Runtime/Supervisor.hs
Expected: no output.
- Step 5: Commit
git add src/HomeAssistant/Runtime/Supervisor.hs src/HomeAssistant/Runtime.hs test/SupervisorSpec.hs
git commit -m "Convert supervised to katip logging"
Task 5: Convert Connection.hs reader/writer putStrLn to katip
The two putStrLn sites in Connection.hs (reader "connected" and "skipping undecodable message") become logMsg calls under namespace runtime.reader. "connected" is InfoS; "skipping undecodable" is WarningS.
Files:
- Modify:
src/HomeAssistant/Runtime/Connection.hs(imports, two log sites)
Interfaces:
-
Consumes:
busLogEnv :: LogEnvfrom Task 2 -
Produces: no signature changes (reader/writer already take
Bus) -
Step 1: Add katip imports to Connection.hs
In src/HomeAssistant/Runtime/Connection.hs, add after the existing imports:
import Katip
( Namespace (Namespace)
, Severity (InfoS, WarningS)
, logMsg
, ls
, runKatipT
)
(showLS is not needed here — both messages use string literals / ls for the raw String err.)
- Step 2: Replace the "connected" putStrLn
In readerAction, replace:
putStrLn "[reader] connected"
with:
runKatipT (busLogEnv bus) $
logMsg (Namespace ["runtime", "reader"]) InfoS "connected"
"connected" is a LogStr via IsString. The onException line that follows is unchanged.
- Step 3: Replace the "skipping undecodable" putStrLn
In receiveLoop, replace:
Left err -> putStrLn $ "[reader] skipping undecodable message: " <> err
with:
Left err -> runKatipT (busLogEnv bus) $
logMsg (Namespace ["runtime", "reader"]) WarningS
("skipping undecodable message: " <> ls err)
err is a String from eitherDecode; ls err converts it to a LogStr without show's quoting (use ls, not showLS, for raw Strings). (<> on LogStr.)
- Step 4: Build and run tests to verify green
Run:
nix develop -c cabal build && nix develop -c cabal test
Expected: builds with no warnings; all tests pass. Confirm no putStrLn remains in Connection.hs:
rg -n "putStrLn" src/HomeAssistant/Runtime/Connection.hs
Expected: no output.
- Step 5: Commit
git add src/HomeAssistant/Runtime/Connection.hs
git commit -m "Convert reader/writer logging to katip"
Task 6: Final verification — no manual printing remains under src/
Confirm the whole logging swap is complete: the build and tests are green, and no print/putStrLn/putStr remains under src/.
Files:
-
None modified (verification only)
-
Step 1: Confirm no manual printing remains under src/
Run:
rg -n "print|putStrLn|putStr|hPutStr" src/
Expected: no output. If any match remains, it is a missed site — fix it before committing (revisit the relevant task).
- Step 2: Full clean build and test
Run:
nix develop -c cabal build && nix develop -c cabal test
Expected: builds with no warnings (-Wall); all tests pass (BusSpec, ConnectionSpec, RuntimeSpec, SupervisorSpec, BackoffProp).
- Step 3: Confirm default.nix is in sync with the cabal file
Run:
nix run nixpkgs#cabal2nix -- ./. > default.nix
git diff --exit-code default.nix
Expected: git diff --exit-code exits 0 (no changes — default.nix was already regenerated in Tasks 1 and 2). If it does change, the cabal file was edited without regenerating; stage and amend the relevant commit.
- Step 4: Smoke-check the executable wiring (optional, manual)
If a Home Assistant instance and HA_TOKEN are available, run the executable and confirm katip-formatted lines appear on stdout (e.g. [home-assistant-controller.runtime.reader][Info][...] connected). This is a manual check, not automated.
- Step 5: Final commit if any fixes were needed
If Steps 1-3 required fixes, commit them:
git add -A
git commit -m "Finish katip logging swap"
Otherwise no commit is needed — the work is already committed across Tasks 1-5.