···163163 Log.warn (fun m -> m "Failed to get channel members: %s" (Printexc.to_string e));
164164 []
165165166166-let create_claude_client ~sw ~proc ~clock =
166166+let create_claude_client ~sw ~proc ~clock ?(allow_read=false) () =
167167 let options =
168168 Claude.Options.default
169169 |> Claude.Options.with_model `Opus_4_5
170170 |> Claude.Options.with_permission_mode Claude.Permissions.Mode.Bypass_permissions
171171- |> Claude.Options.with_allowed_tools []
171171+ |> Claude.Options.with_allowed_tools (if allow_read then ["Read"] else [])
172172 in
173173 Claude.Client.create ~options ~sw ~process_mgr:proc ~clock ()
174174175175-let ask_claude ~sw ~proc ~clock prompt =
176176- let client = create_claude_client ~sw ~proc ~clock in
175175+let ask_claude ~sw ~proc ~clock ?(allow_read=false) prompt =
176176+ let client = create_claude_client ~sw ~proc ~clock ~allow_read () in
177177 Claude.Client.query client prompt;
178178 let responses = Claude.Client.receive_all client in
179179 let text =
···295295 end;
296296 String.trim (Buffer.contents buf)
297297298298+(* Threshold for using staged file approach (50KB) *)
299299+let large_prompt_threshold = 50_000
300300+301301+(* Build the common instructions part of the prompt *)
302302+let build_instructions ~subprojects_text ~fork_context_text ~members_text ~has_forks =
303303+ Printf.sprintf
304304+{|Write a changelog as a JSON object with three sections:
305305+- "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
306306+- "functionality": array of changelog items for feature additions, bug fixes, enhancements, and other functional changes (NOT forks). Give these expanded descriptions (2-3 sentences).
307307+- "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.
308308+309309+Affected sub-projects: %s
310310+%s
311311+Channel members who can be @mentioned (use exact @**Name** format):
312312+313313+%s
314314+315315+Each changelog item has:
316316+- "project": the project/package name (string)
317317+- "description": description of the change, may include @**Name** mentions (string)
318318+- "change_type": one of "new feature", "bug fix", "enhancement", "refactoring" (string)
319319+320320+Example output:
321321+```json
322322+{
323323+ "fork_activity": [
324324+ {"source": "anil.recoil.org", "target": "gazagnaire.org", "items": [
325325+ {"project": "ocaml-mdns", "description": "Refactored DNS query pipeline to use Eio effects.", "change_type": "enhancement"}
326326+ ]}
327327+ ],
328328+ "functionality": [
329329+ {"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"}
330330+ ],
331331+ "metadata": ["ocaml-dns", "dune"]
332332+}
333333+```
334334+335335+Guidelines:
336336+1. One item per logical change (group related commits)
337337+2. Functionality items should have 2-3 sentence descriptions explaining the change and its purpose
338338+3. Use @**Name** mentions in the description when authors match channel members
339339+4. No emojis
340340+5. Put documentation, CI, formatting, version bumps, and opam metadata changes in "metadata" not "functionality"
341341+6. If a change is to a forked repo, put it in "fork_activity" grouped by source/target pair
342342+343343+Output ONLY the JSON object, no markdown code fences or other text.|}
344344+ (if has_forks then "Group items by (source, target) pair based on the fork relationships listed above."
345345+ else "Leave empty if no fork relationships exist.")
346346+ subprojects_text fork_context_text members_text
347347+298348let generate ~sw ~proc ~clock ~fs ~commits ~members ?(fork_context=[]) ?opamrepo_path () =
299349 if commits = [] then None
300350 else begin
···337387 in
338388339389 let has_forks = fork_context <> [] in
390390+ let instructions = build_instructions ~subprojects_text ~fork_context_text ~members_text ~has_forks in
340391341341- let prompt = Printf.sprintf
342342-{|You are writing a changelog update for a Zulip channel about a monorepo.
392392+ (* Check if we need to stage the commit data in a file *)
393393+ let commits_size = String.length commits_text in
394394+ let use_staged_file = commits_size > large_prompt_threshold in
343395344344-Git commits:
396396+ let response =
397397+ if use_staged_file then begin
398398+ (* Write commits to a temporary file and ask Claude to read it *)
399399+ Log.info (fun m -> m "Large commit data (%d bytes), staging in temporary file" commits_size);
400400+ let tmp_dir = Filename.get_temp_dir_name () in
401401+ let tmp_file = Filename.concat tmp_dir (Printf.sprintf "poe-commits-%d.txt" (Unix.getpid ())) in
402402+ (* Write the file using Eio *)
403403+ let tmp_path = Eio.Path.(fs / tmp_file) in
404404+ Eio.Path.save ~create:(`Or_truncate 0o644) tmp_path commits_text;
405405+ Log.info (fun m -> m "Staged commit data to %s" tmp_file);
345406346346-%s
407407+ let prompt = Printf.sprintf
408408+{|You are writing a changelog update for a Zulip channel about a monorepo.
347409348348-Affected sub-projects: %s
410410+The git commit data is too large to include inline. Please read it from this file:
349411%s
350350-Channel members who can be @mentioned (use exact @**Name** format):
351412352352-%s
413413+After reading the file, generate the changelog.
353414354354-Write a changelog as a JSON object with three sections:
355355-- "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
356356-- "functionality": array of changelog items for feature additions, bug fixes, enhancements, and other functional changes (NOT forks). Give these expanded descriptions (2-3 sentences).
357357-- "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.
415415+%s|} tmp_file instructions
416416+ in
358417359359-Each changelog item has:
360360-- "project": the project/package name (string)
361361-- "description": description of the change, may include @**Name** mentions (string)
362362-- "change_type": one of "new feature", "bug fix", "enhancement", "refactoring" (string)
418418+ let result = ask_claude ~sw ~proc ~clock ~allow_read:true prompt in
419419+ (* Clean up the temporary file *)
420420+ (try Eio.Path.unlink tmp_path with _ -> ());
421421+ result
422422+ end else begin
423423+ (* Small enough to include inline *)
424424+ let prompt = Printf.sprintf
425425+{|You are writing a changelog update for a Zulip channel about a monorepo.
363426364364-Example output:
365365-```json
366366-{
367367- "fork_activity": [
368368- {"source": "anil.recoil.org", "target": "gazagnaire.org", "items": [
369369- {"project": "ocaml-mdns", "description": "Refactored DNS query pipeline to use Eio effects.", "change_type": "enhancement"}
370370- ]}
371371- ],
372372- "functionality": [
373373- {"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"}
374374- ],
375375- "metadata": ["ocaml-dns", "dune"]
376376-}
377377-```
427427+Git commits:
378428379379-Guidelines:
380380-1. One item per logical change (group related commits)
381381-2. Functionality items should have 2-3 sentence descriptions explaining the change and its purpose
382382-3. Use @**Name** mentions in the description when authors match channel members
383383-4. No emojis
384384-5. Put documentation, CI, formatting, version bumps, and opam metadata changes in "metadata" not "functionality"
385385-6. If a change is to a forked repo, put it in "fork_activity" grouped by source/target pair
429429+%s
386430387387-Output ONLY the JSON object, no markdown code fences or other text.|} commits_text subprojects_text fork_context_text members_text
388388- (if has_forks then "Group items by (source, target) pair based on the fork relationships listed above."
389389- else "Leave empty if no fork relationships exist.")
431431+%s|} commits_text instructions
432432+ in
433433+ ask_claude ~sw ~proc ~clock prompt
434434+ end
390435 in
391436392392- let response = ask_claude ~sw ~proc ~clock prompt in
393437 Log.info (fun m -> m "Claude generated: %s" response);
394438395439 let last_date = most_recent_date commits in