A Zulip bot agent to sit in our Black Sun. Ever evolving

Merge monopam_changes into monopam library and update poe

Consolidate the separate monopam_changes library into the main monopam
library as submodules (Changes.Aggregated, Changes.Daily, Changes.Query).
This simplifies the dependency graph and provides a cleaner interface.

Update poe to use the new Monopam.Changes interface and add:
- Git pull before checking for changes in the polling loop
- Detailed logging of git pull results (up-to-date vs new changes)
- --requests-verbose flag to control HTTP request logging (off by default)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+77 -22
+31 -12
bin/main.ml
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - let setup_logging style_renderer level = 6 + (* Log source prefixes for requests library - disabled by default to reduce noise *) 7 + let requests_src_prefix = "requests" 8 + 9 + let setup_logging style_renderer level ~requests_verbose = 7 10 Fmt_tty.setup_std_outputs ?style_renderer (); 8 11 Logs.set_level level; 9 - Logs.set_reporter (Logs_fmt.reporter ()) 12 + Logs.set_reporter (Logs_fmt.reporter ()); 13 + (* Disable requests logging by default unless explicitly enabled *) 14 + if not requests_verbose then 15 + List.iter (fun src -> 16 + let name = Logs.Src.name src in 17 + if String.length name >= String.length requests_src_prefix && 18 + String.sub name 0 (String.length requests_src_prefix) = requests_src_prefix then 19 + Logs.Src.set_level src (Some Logs.Warning) 20 + ) (Logs.Src.list ()) 10 21 11 22 let run_bot bot_name config_file = 12 23 Eio_main.run @@ fun env -> ··· 51 62 Logs.info (fun m -> m "Starting Poe bot..."); 52 63 Zulip_bot.Bot.run ~sw ~env ~config:zulip_config ~handler 53 64 65 + let requests_verbose_arg = 66 + let open Cmdliner in 67 + Arg.( 68 + value 69 + & flag 70 + & info [ "requests-verbose" ] 71 + ~doc:"Enable verbose HTTP request logging (disabled by default).") 72 + 54 73 let run_cmd = 55 74 let open Cmdliner in 56 75 let bot_name = ··· 67 86 & info [ "c"; "config" ] ~docv:"FILE" 68 87 ~doc:"Path to poe.toml configuration file.") 69 88 in 70 - let run style_renderer level bot_name config_file = 71 - setup_logging style_renderer level; 89 + let run style_renderer level requests_verbose bot_name config_file = 90 + setup_logging style_renderer level ~requests_verbose; 72 91 run_bot bot_name config_file 73 92 in 74 93 let doc = "Run the Poe Zulip bot" in 75 94 let info = Cmd.info "run" ~doc in 76 95 Cmd.v info 77 96 Term.( 78 - const run $ Fmt_cli.style_renderer () $ Logs_cli.level () $ bot_name 79 - $ config_file) 97 + const run $ Fmt_cli.style_renderer () $ Logs_cli.level () 98 + $ requests_verbose_arg $ bot_name $ config_file) 80 99 81 100 let broadcast_cmd = 82 101 let open Cmdliner in ··· 94 113 & info [ "n"; "name" ] ~docv:"NAME" 95 114 ~doc:"Bot name for Zulip configuration lookup.") 96 115 in 97 - let broadcast style_renderer level config_file bot_name = 98 - setup_logging style_renderer level; 116 + let broadcast style_renderer level requests_verbose config_file bot_name = 117 + setup_logging style_renderer level ~requests_verbose; 99 118 Eio_main.run @@ fun env -> 100 119 Eio.Switch.run @@ fun sw -> 101 120 let fs = Eio.Stdenv.fs env in ··· 134 153 Cmd.v info 135 154 Term.( 136 155 const broadcast $ Fmt_cli.style_renderer () $ Logs_cli.level () 137 - $ config_file $ bot_name) 156 + $ requests_verbose_arg $ config_file $ bot_name) 138 157 139 158 let loop_cmd = 140 159 let open Cmdliner in ··· 159 178 & info [ "i"; "interval" ] ~docv:"SECONDS" 160 179 ~doc:"Interval in seconds between change checks (default: 3600).") 161 180 in 162 - let loop style_renderer level config_file bot_name interval = 163 - setup_logging style_renderer level; 181 + let loop style_renderer level requests_verbose config_file bot_name interval = 182 + setup_logging style_renderer level ~requests_verbose; 164 183 Eio_main.run @@ fun env -> 165 184 Eio.Switch.run @@ fun sw -> 166 185 let fs = Eio.Stdenv.fs env in ··· 189 208 Cmd.v info 190 209 Term.( 191 210 const loop $ Fmt_cli.style_renderer () $ Logs_cli.level () 192 - $ config_file $ bot_name $ interval) 211 + $ requests_verbose_arg $ config_file $ bot_name $ interval) 193 212 194 213 let main_cmd = 195 214 let open Cmdliner in
+3 -3
lib/broadcast.ml
··· 26 26 | Some t -> t 27 27 in 28 28 29 - match Monopam_changes.Query.changes_since ~fs ~changes_dir ~since with 29 + match Monopam.Changes.Query.changes_since ~fs ~changes_dir ~since with 30 30 | Error e -> 31 31 Log.warn (fun m -> m "Error loading changes: %s" e); 32 32 (* Fall back to reading the markdown file *) ··· 51 51 end 52 52 else begin 53 53 (* Format the changes for Zulip *) 54 - let content = Monopam_changes.Query.format_for_zulip 54 + let content = Monopam.Changes.Query.format_for_zulip 55 55 ~entries ~include_date:true ~date:None 56 56 in 57 57 ··· 61 61 Log.info (fun m -> m "Updated broadcast time to %s" (Ptime.to_rfc3339 now)); 62 62 63 63 (* Send as stream message *) 64 - let summary = Monopam_changes.Query.format_summary ~entries in 64 + let summary = Monopam.Changes.Query.format_summary ~entries in 65 65 Log.info (fun m -> m "Broadcasting: %s" summary); 66 66 67 67 (* Return a compound response: stream message + confirmation reply *)
+1 -1
lib/dune
··· 1 1 (library 2 2 (name poe) 3 3 (public_name poe) 4 - (libraries eio eio_main zulip zulip.bot claude tomlt tomlt.bytesrw xdge logs fpath ptime ptime.clock.os monopam-changes)) 4 + (libraries eio eio_main zulip zulip.bot claude tomlt tomlt.bytesrw xdge logs fpath ptime ptime.clock.os monopam))
+37 -2
lib/loop.ml
··· 17 17 | `Exited 0 -> Some (String.trim (Buffer.contents buf)) 18 18 | _ -> None 19 19 20 + let run_git_pull ~proc ~cwd = 21 + Log.info (fun m -> m "Pulling latest changes from remote"); 22 + Eio.Switch.run @@ fun sw -> 23 + let buf_stdout = Buffer.create 256 in 24 + let buf_stderr = Buffer.create 256 in 25 + let child = Eio.Process.spawn proc ~sw ~cwd 26 + ~stdout:(Eio.Flow.buffer_sink buf_stdout) 27 + ~stderr:(Eio.Flow.buffer_sink buf_stderr) 28 + ["git"; "pull"; "--ff-only"] 29 + in 30 + match Eio.Process.await child with 31 + | `Exited 0 -> 32 + let output = String.trim (Buffer.contents buf_stdout) in 33 + if output = "Already up to date." then 34 + Log.info (fun m -> m "Repository already up to date") 35 + else begin 36 + Log.info (fun m -> m "Pulled new changes from remote"); 37 + (* Log the output which shows what was updated *) 38 + String.split_on_char '\n' output 39 + |> List.iter (fun line -> 40 + let line = String.trim line in 41 + if line <> "" then Log.info (fun m -> m " %s" line)) 42 + end; 43 + true 44 + | `Exited code -> 45 + let stderr = String.trim (Buffer.contents buf_stderr) in 46 + Log.warn (fun m -> m "git pull exited with code %d: %s" code stderr); 47 + false 48 + | `Signaled sig_ -> 49 + Log.warn (fun m -> m "git pull killed by signal %d" sig_); 50 + false 51 + 20 52 let run_monopam_changes ~proc ~cwd = 21 53 Log.info (fun m -> m "Running monopam changes --daily --aggregate"); 22 54 Eio.Switch.run @@ fun sw -> ··· 35 67 false 36 68 37 69 let send_changes ~client ~stream ~topic ~entries = 38 - let content = Monopam_changes.Query.format_for_zulip 70 + let content = Monopam.Changes.Query.format_for_zulip 39 71 ~entries ~include_date:true ~date:None 40 72 in 41 73 let msg = Zulip.Message.create ~type_:`Channel ~to_:[stream] ~topic ~content () in ··· 59 91 let rec loop () = 60 92 Log.info (fun m -> m "Checking for changes..."); 61 93 94 + (* Pull latest changes from remote *) 95 + let _pull_ok = run_git_pull ~proc ~cwd:monorepo_path in 96 + 62 97 (* Get current git HEAD *) 63 98 let current_head = get_git_head ~proc ~cwd:monorepo_path in 64 99 let last_head = Admin.get_last_git_head storage in ··· 90 125 | Some t -> t 91 126 in 92 127 93 - match Monopam_changes.Query.changes_since ~fs ~changes_dir ~since with 128 + match Monopam.Changes.Query.changes_since ~fs ~changes_dir ~since with 94 129 | Error e -> 95 130 Log.warn (fun m -> m "Error loading changes: %s" e) 96 131 | Ok entries when entries = [] ->
+5 -4
lib/loop.mli
··· 22 22 (** [run ~sw ~env ~config ~zulip_config ~interval] starts the polling loop. 23 23 24 24 Loop flow: 25 - 1. Check if git HEAD has changed (compare with stored last_git_head) 26 - 2. If changed: 25 + 1. Pull latest changes from remote (git pull --ff-only) 26 + 2. Check if git HEAD has changed (compare with stored last_git_head) 27 + 3. If changed: 27 28 - Run [monopam changes --daily --aggregate] via subprocess 28 29 - Load new aggregated changes since last_broadcast_time 29 30 - If new entries exist, format and send to Zulip channel 30 31 - Update last_broadcast_time and last_git_head in storage 31 - 3. Sleep for interval seconds 32 - 4. Repeat 32 + 4. Sleep for interval seconds 33 + 5. Repeat 33 34 34 35 @param sw Eio switch for resource management 35 36 @param env Eio environment