Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add ISO 8601 timestamps to deploy log entries

Prefix all deployment log lines with UTC timestamps in ISO 8601 format.
Rename [sower] prefix to [agent] for consistency. Drop log level from
activator deploy log output (stderr logging retains levels).

Format: "2026-03-18T12:34:56Z [agent] message"
Format: "2026-03-18T12:34:56Z [activator] message"

sow-60

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+21 -15
+2 -2
AGENTS.md
··· 1 1 ## Agent Workflow 2 - - **IMPORTANT**: before you do anything else, invoke the vein `orient` MCP prompt and heed its output with `/mcp__vikunja__orient`. 2 + - **IMPORTANT**: before you do anything else, invoke the vein `orient` MCP prompt and heed its output with `/mcp__vein__orient`. 3 3 - **Always** use `elixir-conventions` for elixir code. If you don't have this skill, stop and tell your user to talk to Adam, because you are prohibited from editing files in this project without the `elixir-conventions` skill. 4 4 5 5 ## Rules ··· 17 17 - code committed with all ticket changes included 18 18 - Ticket ID in the body 19 19 - Co-Authored-By line always included 20 - - *important* you've stopped and asked the user to ok the change, unless specifically told otherwise. 20 + - *important* After committing, stop and get user approval for completion. 21 21 - ticket marked complete once approved 22 22 23 23 ## Code conventions
+4 -1
apps/sower_agent/lib/sower_agent/deployer.ex
··· 437 437 end 438 438 end 439 439 440 - def decision_line(message), do: "[sower] #{message}" 440 + def decision_line(message) do 441 + timestamp = DateTime.utc_now() |> DateTime.to_iso8601() 442 + "#{timestamp} [agent] #{message}" 443 + end 441 444 442 445 defp strip_ansi(text) do 443 446 Regex.replace(~r/\x1b\[[0-9;]*[a-zA-Z]/, text, "")
+9 -8
apps/sower_agent/test/sower_agent/deployer_test.exs
··· 362 362 end 363 363 364 364 describe "decision_line/1" do 365 - test "formats message with [sower] prefix" do 366 - assert Deployer.decision_line("reboot triggered") == "[sower] reboot triggered" 365 + test "formats message with ISO 8601 timestamp and [agent] prefix" do 366 + line = Deployer.decision_line("reboot triggered") 367 + assert line =~ ~r/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z \[agent\] reboot triggered$/ 367 368 end 368 369 end 369 370 ··· 378 379 379 380 assert Enum.any?( 380 381 logged_lines, 381 - &(&1 =~ "[sower]" and &1 =~ "realized" and &1 =~ "seed-seed_r1") 382 + &(&1 =~ "[agent]" and &1 =~ "realized" and &1 =~ "seed-seed_r1") 382 383 ) 383 384 end 384 385 ··· 393 394 realize_seed_fun: fn seed_deploy -> {:error, :failed_to_realize, seed_deploy} end 394 395 ) 395 396 396 - assert Enum.any?(logged_lines, &(&1 =~ "[sower]" and &1 =~ "realization failed")) 397 + assert Enum.any?(logged_lines, &(&1 =~ "[agent]" and &1 =~ "realization failed")) 397 398 end 398 399 399 400 test "includes activation mode decision line in log output" do ··· 409 410 end 410 411 ) 411 412 412 - assert Enum.any?(logged_lines, &(&1 =~ "[sower]" and &1 =~ "boot" and &1 =~ "seed-seed_m1")) 413 + assert Enum.any?(logged_lines, &(&1 =~ "[agent]" and &1 =~ "boot" and &1 =~ "seed-seed_m1")) 413 414 end 414 415 415 416 test "includes reboot decision in last seed log" do ··· 450 451 451 452 assert Enum.any?( 452 453 reboot_lines, 453 - &(&1 =~ "[sower]" and &1 =~ "reboot initiated: policy_always") 454 + &(&1 =~ "[agent]" and &1 =~ "reboot initiated: policy_always") 454 455 ) 455 456 end 456 457 ··· 483 484 assert_received {:seed_result, :failure, _activation_lines} 484 485 # Second call: reboot decision 485 486 assert_received {:seed_result, nil, reboot_lines} 486 - assert Enum.any?(reboot_lines, &(&1 =~ "[sower]" and &1 =~ "reboot skipped")) 487 + assert Enum.any?(reboot_lines, &(&1 =~ "[agent]" and &1 =~ "reboot skipped")) 487 488 end 488 489 489 490 test "includes default activation mode when none configured" do ··· 496 497 497 498 assert Enum.any?( 498 499 logged_lines, 499 - &(&1 =~ "[sower]" and &1 =~ "switch" and &1 =~ "seed-seed_md1") 500 + &(&1 =~ "[agent]" and &1 =~ "switch" and &1 =~ "seed-seed_md1") 500 501 ) 501 502 end 502 503 end
+3 -1
cmd/sower-activator/activate.go
··· 8 8 "os" 9 9 "os/exec" 10 10 "sync" 11 + "time" 11 12 ) 12 13 13 14 // Seed type constants ··· 167 168 if mirror != nil { 168 169 fmt.Fprintln(mirror, line) 169 170 } 170 - callback(line, isError) 171 + timestamped := time.Now().UTC().Format(time.RFC3339) + " " + line 172 + callback(timestamped, isError) 171 173 } 172 174 }
+3 -3
cmd/sower-activator/handler.go
··· 11 11 "path/filepath" 12 12 "slices" 13 13 "strings" 14 + "time" 14 15 ) 15 16 16 17 // ConnectionHandler handles a single client connection. ··· 149 150 150 151 func (h *callbackSlogHandler) Handle(ctx context.Context, r slog.Record) error { 151 152 var sb strings.Builder 152 - sb.WriteString("[activator] ") 153 - sb.WriteString(r.Level.String()) 154 - sb.WriteString(" ") 153 + sb.WriteString(r.Time.UTC().Format(time.RFC3339)) 154 + sb.WriteString(" [activator] ") 155 155 sb.WriteString(r.Message) 156 156 r.Attrs(func(a slog.Attr) bool { 157 157 sb.WriteString(" ")