···163 Log.warn (fun m -> m "Failed to get channel members: %s" (Printexc.to_string e));
164 []
165166-let create_claude_client ~sw ~proc ~clock =
167 let options =
168 Claude.Options.default
169 |> Claude.Options.with_model `Opus_4_5
170 |> Claude.Options.with_permission_mode Claude.Permissions.Mode.Bypass_permissions
171- |> Claude.Options.with_allowed_tools []
172 in
173 Claude.Client.create ~options ~sw ~process_mgr:proc ~clock ()
174175-let ask_claude ~sw ~proc ~clock prompt =
176- let client = create_claude_client ~sw ~proc ~clock in
177 Claude.Client.query client prompt;
178 let responses = Claude.Client.receive_all client in
179 let text =
···295 end;
296 String.trim (Buffer.contents buf)
29700000000000000000000000000000000000000000000000000298let generate ~sw ~proc ~clock ~fs ~commits ~members ?(fork_context=[]) ?opamrepo_path () =
299 if commits = [] then None
300 else begin
···337 in
338339 let has_forks = fork_context <> [] in
0340341- let prompt = Printf.sprintf
342-{|You are writing a changelog update for a Zulip channel about a monorepo.
0343344-Git commits:
000000000345346-%s
0347348-Affected sub-projects: %s
349%s
350-Channel members who can be @mentioned (use exact @**Name** format):
351352-%s
353354-Write a changelog as a JSON object with three sections:
355-- "fork_activity": array of objects with "source" (upstream owner handle), "target" (person who changed the fork), and "items" (array of changelog items for repos that are forks of another user). %s
356-- "functionality": array of changelog items for feature additions, bug fixes, enhancements, and other functional changes (NOT forks). Give these expanded descriptions (2-3 sentences).
357-- "metadata": array of project name strings for changes that are purely metadata (documentation, CI, formatting, version bumps, opam file updates, .ocamlformat changes, README updates). Just list the project names, no descriptions.
358359-Each changelog item has:
360-- "project": the project/package name (string)
361-- "description": description of the change, may include @**Name** mentions (string)
362-- "change_type": one of "new feature", "bug fix", "enhancement", "refactoring" (string)
0000363364-Example output:
365-```json
366-{
367- "fork_activity": [
368- {"source": "anil.recoil.org", "target": "gazagnaire.org", "items": [
369- {"project": "ocaml-mdns", "description": "Refactored DNS query pipeline to use Eio effects.", "change_type": "enhancement"}
370- ]}
371- ],
372- "functionality": [
373- {"project": "ocaml-claudeio", "description": "Added model types for Opus 4.5 and 4.1. This extends the client to support the latest Claude model lineup.", "change_type": "new feature"}
374- ],
375- "metadata": ["ocaml-dns", "dune"]
376-}
377-```
378379-Guidelines:
380-1. One item per logical change (group related commits)
381-2. Functionality items should have 2-3 sentence descriptions explaining the change and its purpose
382-3. Use @**Name** mentions in the description when authors match channel members
383-4. No emojis
384-5. Put documentation, CI, formatting, version bumps, and opam metadata changes in "metadata" not "functionality"
385-6. If a change is to a forked repo, put it in "fork_activity" grouped by source/target pair
386387-Output ONLY the JSON object, no markdown code fences or other text.|} commits_text subprojects_text fork_context_text members_text
388- (if has_forks then "Group items by (source, target) pair based on the fork relationships listed above."
389- else "Leave empty if no fork relationships exist.")
0390 in
391392- let response = ask_claude ~sw ~proc ~clock prompt in
393 Log.info (fun m -> m "Claude generated: %s" response);
394395 let last_date = most_recent_date commits in
···163 Log.warn (fun m -> m "Failed to get channel members: %s" (Printexc.to_string e));
164 []
165166+let create_claude_client ~sw ~proc ~clock ?(allow_read=false) () =
167 let options =
168 Claude.Options.default
169 |> Claude.Options.with_model `Opus_4_5
170 |> Claude.Options.with_permission_mode Claude.Permissions.Mode.Bypass_permissions
171+ |> Claude.Options.with_allowed_tools (if allow_read then ["Read"] else [])
172 in
173 Claude.Client.create ~options ~sw ~process_mgr:proc ~clock ()
174175+let ask_claude ~sw ~proc ~clock ?(allow_read=false) prompt =
176+ let client = create_claude_client ~sw ~proc ~clock ~allow_read () in
177 Claude.Client.query client prompt;
178 let responses = Claude.Client.receive_all client in
179 let text =
···295 end;
296 String.trim (Buffer.contents buf)
297298+(* Threshold for using staged file approach (50KB) *)
299+let large_prompt_threshold = 50_000
300+301+(* Build the common instructions part of the prompt *)
302+let build_instructions ~subprojects_text ~fork_context_text ~members_text ~has_forks =
303+ Printf.sprintf
304+{|Write a changelog as a JSON object with three sections:
305+- "fork_activity": array of objects with "source" (upstream owner handle), "target" (person who changed the fork), and "items" (array of changelog items for repos that are forks of another user). %s
306+- "functionality": array of changelog items for feature additions, bug fixes, enhancements, and other functional changes (NOT forks). Give these expanded descriptions (2-3 sentences).
307+- "metadata": array of project name strings for changes that are purely metadata (documentation, CI, formatting, version bumps, opam file updates, .ocamlformat changes, README updates). Just list the project names, no descriptions.
308+309+Affected sub-projects: %s
310+%s
311+Channel members who can be @mentioned (use exact @**Name** format):
312+313+%s
314+315+Each changelog item has:
316+- "project": the project/package name (string)
317+- "description": description of the change, may include @**Name** mentions (string)
318+- "change_type": one of "new feature", "bug fix", "enhancement", "refactoring" (string)
319+320+Example output:
321+```json
322+{
323+ "fork_activity": [
324+ {"source": "anil.recoil.org", "target": "gazagnaire.org", "items": [
325+ {"project": "ocaml-mdns", "description": "Refactored DNS query pipeline to use Eio effects.", "change_type": "enhancement"}
326+ ]}
327+ ],
328+ "functionality": [
329+ {"project": "ocaml-claudeio", "description": "Added model types for Opus 4.5 and 4.1. This extends the client to support the latest Claude model lineup.", "change_type": "new feature"}
330+ ],
331+ "metadata": ["ocaml-dns", "dune"]
332+}
333+```
334+335+Guidelines:
336+1. One item per logical change (group related commits)
337+2. Functionality items should have 2-3 sentence descriptions explaining the change and its purpose
338+3. Use @**Name** mentions in the description when authors match channel members
339+4. No emojis
340+5. Put documentation, CI, formatting, version bumps, and opam metadata changes in "metadata" not "functionality"
341+6. If a change is to a forked repo, put it in "fork_activity" grouped by source/target pair
342+343+Output ONLY the JSON object, no markdown code fences or other text.|}
344+ (if has_forks then "Group items by (source, target) pair based on the fork relationships listed above."
345+ else "Leave empty if no fork relationships exist.")
346+ subprojects_text fork_context_text members_text
347+348let generate ~sw ~proc ~clock ~fs ~commits ~members ?(fork_context=[]) ?opamrepo_path () =
349 if commits = [] then None
350 else begin
···387 in
388389 let has_forks = fork_context <> [] in
390+ let instructions = build_instructions ~subprojects_text ~fork_context_text ~members_text ~has_forks in
391392+ (* Check if we need to stage the commit data in a file *)
393+ let commits_size = String.length commits_text in
394+ let use_staged_file = commits_size > large_prompt_threshold in
395396+ let response =
397+ if use_staged_file then begin
398+ (* Write commits to a temporary file and ask Claude to read it *)
399+ Log.info (fun m -> m "Large commit data (%d bytes), staging in temporary file" commits_size);
400+ let tmp_dir = Filename.get_temp_dir_name () in
401+ let tmp_file = Filename.concat tmp_dir (Printf.sprintf "poe-commits-%d.txt" (Unix.getpid ())) in
402+ (* Write the file using Eio *)
403+ let tmp_path = Eio.Path.(fs / tmp_file) in
404+ Eio.Path.save ~create:(`Or_truncate 0o644) tmp_path commits_text;
405+ Log.info (fun m -> m "Staged commit data to %s" tmp_file);
406407+ let prompt = Printf.sprintf
408+{|You are writing a changelog update for a Zulip channel about a monorepo.
409410+The git commit data is too large to include inline. Please read it from this file:
411%s
0412413+After reading the file, generate the changelog.
414415+%s|} tmp_file instructions
416+ in
00417418+ let result = ask_claude ~sw ~proc ~clock ~allow_read:true prompt in
419+ (* Clean up the temporary file *)
420+ (try Eio.Path.unlink tmp_path with _ -> ());
421+ result
422+ end else begin
423+ (* Small enough to include inline *)
424+ let prompt = Printf.sprintf
425+{|You are writing a changelog update for a Zulip channel about a monorepo.
426427+Git commits:
0000000000000428429+%s
000000430431+%s|} commits_text instructions
432+ in
433+ ask_claude ~sw ~proc ~clock prompt
434+ end
435 in
4360437 Log.info (fun m -> m "Claude generated: %s" response);
438439 let last_date = most_recent_date commits in