Skip to content

MCP tools/list serves short description heads, and get_tool_guide serves the reading guides, with a size ratchet on both editions (#3898) - #4048

Merged
erikdarlingdata merged 5 commits into
devfrom
feature/3898-tool-guide-seam
Sep 23, 2026
Merged

erikdarlingdata merged 5 commits into
devfrom
feature/3898-tool-guide-seam

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Phase 3's seam (D1 + D3) plus Phase 0's ratchet, on both SKUs in lockstep (D6), proven on one pilot family. The issue stays open for the content PRs.

Why

tools/list costs about 100k tokens of context before the first call. The long descriptions carry reading guidance agents need (#3541), so the fix is where that guidance lives, not whether it exists. This PR builds the seam so content PRs can move guidance off the wire family by family, and a ratchet so the catalog cannot regrow.

What changes

The marker and the split. The marker is <<GUIDE>>, written inside a tool's existing Description literal (McpToolGuide.Marker). It can't occur in prose, and grep -rn "<<GUIDE>>" lists every converted tool.

  • The split lives in ONE place for both SKUs: McpSchemaCompat.WithGeminiCompatibleTools in PerformanceMonitor.Common/Mcp/McpSchemaCompat.cs. Both hosts register every tool through it.
  • tools/list serves the head, plus one uniform pointer sentence: " Reading guide: get_tool_guide." A tool with no marker is served whole, exactly as before.
  • The tail is recorded in a per-host McpToolGuideCatalog, a DI singleton that the registration itself adds.
  • A second marker, or a marker with an empty side, throws at registration.

get_tool_guide(tools[], topics[]) is a new tool on both SKUs: DarlingMcpToolGuideTools, and Lite's McpToolGuideTools. The shared logic is in McpToolGuide.Render.

  • It returns the requested tails and the named topic guides.
  • Unknown names come back under unknown_tools / unknown_topics, with available_topics. They are never thrown.
  • The answer is bounded:
    • at most 24,000 characters of guide text; items that don't fit are listed under deferred, not cut off;
    • at most 50 names per array; the rest are counted in names_not_read.
  • Called with no arguments, it returns the index: the tools that have guides, and each topic with a summary.
  • It is read-only and reads no store data. There are no MCP resources (D1).
  • One topic so far: system_health_empty_windows (McpToolGuideTopics).

Pilot: the nine get_health_parser_* tools, both SKUs. They are a good fit: the same roughly 400-character empty-window sentence was repeated in all 18 descriptions. That sentence became the topic, and every tail includes the topic verbatim, so one call answers it.

  • The heads are identical on both SKUs, since both use one shared parser and one shared significance gate. Each head says:
    • what the tool reads;
    • the fixed event_time window ending at as_of, newest first;
    • the significance gate. This is a guardrail no description stated before; it lived only in the empty-answer message;
    • "An empty answer is not a clean bill: read status, source_observed and last_captured_at."
  • Each SKU's original prose moved to its own tail verbatim, with one correction. Lite's get_health_parser_memory_broker claimed a notification of (RESOURCE_MEMPHYSICAL_HIGH/LOW), but the gate only ever returns LOW. The tail now says (RESOURCE_MEMPHYSICAL_LOW; the gate returns no other).
  • Dropped sentences: none. Every removed head sentence is in the tail, or in the topic that every tail includes.
  • D4: the family had no issue references, anecdotes or WIRE CHANGE notices.

Phase 0 ratchet: McpToolsListBudgetTests on each SKU.

  • It measures the serialized ListToolsResult: descriptions and input schemas, using McpJsonUtilities.DefaultOptions, UTF-8 bytes. The tools are built through the same registration path, over the tool types the host source registers.
  • Pins, in McpToolsListBudget.txt next to each test:
    • one line per tool (the served description's length);
    • one line per advertised parameter (its description's length);
    • the total, as a const with a change log.
  • Every line is exact: growth fails ("put it after the marker"), and so does shrinkage ("bank it: lower the line"). The total allows 4 KB of slack so small edits don't all conflict on one line.
  • Converted tools must meet D2's caps: head at most 1,000 characters (the pilot's are 273-472), parameter descriptions at most 200, and each tail and topic small enough to fit one get_tool_guide answer.
  • Near-miss markers fail, and no marker can reach the wire.

Head pins (D3). The new McpToolGuideTests pin each pilot head's guardrail facts: the window, the empty-answer guardrail, the per-tool gate or floor, and the pointer. They also pin that no sentence was lost (each tail carries the topic, and the topic names every rung), plus cross-SKU identity of the heads and of get_tool_guide.

Existing pins, re-pointed deliberately:

  • McpZeroIsAMeasurementTests.DescriptionOf / ToolDescription (Darling, which reads both SKUs' source) now read the head. Everything they pin is a zero-is-a-measurement guardrail.
  • McpDescriptionTruthPinTests.Description (Lite) now reads the head. The edition claim, the operator cut and the create_statement caveat are all guardrails.
  • McpPayloadContractCensusTests (the window-floor clause): window_truncated and "not a page cut" must be in the head. The rest of the clause may span head and guide, because its "spelled truncated before Brains-review campaign: deferred structural residue (from #3538 / #3539 / #3540 / #3541) #3653" WIRE CHANGE notice goes to the guide tail per D4.

Inventory (D8: no renames, nothing consolidated or removed, +1 tool per SKU).

  • Darling goes from 158 to 159 tools, Lite from 88 to 89.
  • Updated deliberately to match: the Darling instructions census (159 total / 88 shared / 71 unique, same character count, so the instructions budget is untouched), README (both sites per SKU), llms.txt (89-159) and Lite's instructions count.
  • The instructions do not mention get_tool_guide. The pointer on each converted head and the tool's own description carry it. Darling had only 8 characters of headroom.

Also in this PR. get_tool_guide was added to both READMEs' tool lists: the root table, and a Darling README bullet.

Small in-lane fix. The schema censuses (McpUnknownArgumentGuardTests, Lite McpSchemaCompatTests) treated every non-primitive as a DI service. That silently dropped int? / long? / bool? parameters, and the new string[], from the schemas they check. Both now use the shared McpServedSchema.IsServiceParameter.

Numbers

Before After
Darling tools/list 329,870 B (158 tools) 328,331 B (159 tools), -1,539 B net
Lite tools/list 149,448 B (88 tools) 147,053 B (89 tools), -2,395 B net
Darling pilot descriptions (9 tools) 5,248 chars 2,864 chars (-45%)
Lite pilot descriptions (9 tools) 6,094 chars 2,864 chars (-53%)
get_tool_guide itself (each SKU) - 487-char description + 2 x 98-char params

All four totals are measured with the same harness. For "before", the dev versions of the two pilot files were restored and the get_tool_guide registrations removed; the ratchet then failed as it should, with 329,870 > 328,331 and 149,448 > 147,053, which also proves it red on the old shape. The net figures include get_tool_guide's own cost (about 930 B per SKU). The seam's saving is small by design, because the content PRs deliver the cut.

Test plan

  • Darling.Tests and Lite.Tests build with 0 warnings, 0 errors, after git merge origin/dev. A --no-incremental rebuild surfaced one CA1720 on my Pointer const, which is now GuidePointer.
  • Darling targeted classes: 153 passed, 6 skipped (live PG, no rig). Classes: McpToolsListBudgetTests, McpToolGuideTests, McpZeroIsAMeasurement*, McpPayloadContractCensus*, McpUnknownArgumentGuardTests, DarlingMcpHealthParserTools*, McpToolTypeRegistrationTests, DarlingMcpInstructionsBudgetTests, DarlingSignificantWaitsReadTests, EngineCapabilityMissTests, ConsumedTimestampFrameDisciplineTests.
  • Lite targeted classes: 112 passed, 0 failed. Classes: McpToolsListBudgetTests, McpToolGuideTests, McpSchemaCompatTests, CrossAppMcpToolInventoryPinTests, McpInstructionsBudgetTests, McpDescriptionTruthPinTests, McpZeroIsAMeasurementTests, SignificantWaitsToolTests, EngineCapabilityMissTests, McpUnknownArgumentGuardTests, McpMissMessageParityPinTests, CrossSkuSurfaceSourceTests.
  • FULL Darling suite after git merge origin/dev: 13,082 total, 0 failed, 617 skipped (live PostgreSQL classes, no rig). The first full run caught DarlingWebEndpointsTests.ReadEndpoints_AreExactlyTheReadToolCatalog_MinusTheDocumentedExclusions: get_tool_guide had no /api/read mirror. It is now in DarlingWebEndpoints.ExcludedToolNames with its reason (it reads no data; its catalog exists only in the MCP host), and ExcludedToolNames_AreTheNonReadSurfaceTools is updated to match.
  • FULL Lite suite after git merge origin/dev: 5,198 total, 0 failed.
  • Live PostgreSQL classes in touched files (McpZeroIsAMeasurementLivePostgresTests, and the live halves of the health-parser tests): not run, because there was no rig. Only descriptions changed in those tools; no tool body, SQL or payload did.
  • Live tools/list from a running host: not touched, per lane rules. The ratchet builds the list through the host's registration path, and pins that every unconverted tool is served byte-identical to its [Description].

D9: known traps for the misreading check (the coordinator runs it)

  1. Q: get_health_parser_severe_errors returned no rows: were there no severe errors?
    Correct: only severity 19+ errors off the benign connection-reset list are listed. With status empty and events_in_window > 0, events were captured and gated out.
    Prevents it: the head's "Gated: severity 19 or higher only, benign connection-reset error numbers excluded, so a lower-severity error is never listed."
  2. Q: A get_health_parser_* read answered unavailable with source_observed: false: is the server clean?
    Correct: no evidence either way. The session was never read.
    Prevents it: the head's "An empty answer is not a clean bill: read status, source_observed and last_captured_at", and topic rung 4.
  3. Q: get_health_parser_significant_waits is empty: were there no waits?
    Correct: only real-session, non-BACKUP waits of at least 500 ms on non-idle types are listed. get_wait_stats has the totals.
    Prevents it: the head's "Floors: ... at least 500 ms ...; shorter waits are never listed. get_wait_stats has the instance-wide totals."
  4. Q: memory_node_oom is empty with status empty and source_observed: true: is there a gap in the data?
    Correct: a healthy measurement. The read is ungated, and an OOM that never happened is simply absent.
    Prevents it: the head's "Ungated: every recorded OOM is returned.", and topic rung 3.
  5. Q: get_health_parser_cpu_tasks is empty: was there no CPU pressure?
    Correct: only WARNING results with at least 10 pending tasks are listed.
    Prevents it: the head's "Gated: WARNING-state results with at least 10 pending tasks only."
  6. Q: "What happened at 02:00 yesterday?" The model widens hours_back to 48.
    Correct: set as_of to the incident's end.
    Prevents it: the as_of parameter description's "For a past incident set as_of to its end; do not widen hours_back.", and the head's "window ending at as_of".
  7. Q: Lite get_health_parser_memory_broker: were there HIGH notifications?
    Correct: only LOW is ever returned.
    Prevents it: the head's "Gated: RESOURCE_MEMPHYSICAL_LOW notifications only." (the tail was corrected in this PR).
  8. Q: get_query_store_regressions shows a null percent: no change?
    Correct: undefined, because the baseline side is 0.
    Prevents it: the description's pinned "undefined_percents" and "null percent never sorts as 0".
  9. Q: The first point of a duration trend is 0: was that a quiet instant?
    Correct: it is unrated, not measured.
    Prevents it: the pinned "unrated_points" in the trend descriptions.
  10. Q: get_pvs_stats shows pct_of_database: null: is the PVS 0%?
    Correct: it was unmeasured, or has no denominator. A measured 0 MB is published as 0.
    Prevents it: the pinned "pvs_measured says whether the DMV reported a size".
  11. Q: window_truncated: true: were rows paged away?
    Correct: retention floored the window. It is not a page cut, and effective_start / effective_hours_back give the reach.
    Prevents it: the McpHelpers.WindowTruncatedDescription clause (now head-pinned).
  12. Q: audit_config's MAXDOP advice: is it tailored to Standard edition?
    Correct: the edition is reported, never consulted.
    Prevents it: the pinned "NO check branches on it".
  13. Q: get_cpu_utilization on SQL Server: are the samples every 15 seconds?
    Correct: one ring-buffer record per minute. The 15-second cadence is the Azure SQL DB source.
    Prevents it: the pinned "one RING_BUFFER_SCHEDULER_MONITOR record per minute" and "samples_in_bucket is the measured count".
  14. Q: A plan tool's create_statement: is that the fix?
    Correct: it corroborates a statement already measured slow and is never a diagnosis. Every row carries the fixed caveat, including the regression risk for other plans.
    Prevents it: the pinned "corroboration for a statement already measured slow, never a diagnosis", "every row carries the fixed caveat" and "regression risk for other plans".

D9 round (2026-09-23)

The coordinator's misreading check gave a fresh Haiku model only the served heads and asked 14 trap questions (the list above). Two defects followed from what it did with them, both now fixed on this branch.

Finding 1 - the identical empty-answer sentence hid the gate. All nine heads said "An empty answer is not a clean bill: read status, source_observed and last_captured_at.", regardless of whether the tool gates at all. On memory_node_oom (ungated), given an empty answer with status empty and source_observed: true, the model hedged "ambiguous, maybe a gap" - wrong: per McpToolGuideTopics.SystemHealthEmptyWindows rungs (1)-(3), status empty is a real result, and only rung (4) (status unavailable, source_observed false) is no evidence. On severe_errors and significant_waits, the model anchored on that sentence and never cited the gate/floors stated right beside it.

The fix - each head's empty-answer sentence now names its own outcome, verified field-by-field against the actual WitnessStatus call in EmptyAsync (every field named below is serialized on both products' empty answers, for all nine tools):

  • Gated (severe_errors, io_issues, scheduler_issues, memory_conditions, memory_broker, cpu_tasks): "An empty answer with status empty is a real result: nothing in this window passed the gate, and events_in_window counts what was captured and filtered out. status unavailable with source_observed false is no evidence either way."
  • significant_waits (says "floors", matching its own head's wording): same sentence with "floors" in place of "gate".
  • Ungated (system_health, memory_node_oom): "An empty answer with status empty is a real result: nothing of this kind was recorded in this window. status unavailable with source_observed false is no evidence either way."

Note: these sentences no longer name last_captured_at by itself (the old universal sentence did); it still rides on the data envelope and every rung of the empty answer, and the guide topic still explains it. Updated McpZeroIsAMeasurementTests's description check accordingly (it only pins source_observed now, the one fact all three variants still state).

Finding 2 (the prior lane's own flagged deferral) - Darling's tails were vaguer than Lite's. Fixed here rather than deferred further. I diffed every one of the nine tools' JSON projections line-by-line between DarlingMcpHealthParserTools.cs and Lite/Mcp/McpHealthParserTools.cs: the two products return byte-identical field-key sets on all nine tools (severe_errors' database_name is resolved through DarlingSystemHealthReader.ResolveDatabaseName on Darling vs. a direct DTO property on Lite, but the JSON key and meaning match). That let Lite's tail field lists carry over to Darling as-is for 8 of the 9; significant_waits' Darling tail already matched Lite's specificity and was left alone. While rewriting, also corrected a factual error: Darling's system_health tail credited "sp_HealthParser" (the Dashboard's server-side proc) for data that this PARSE-ON-READ tool never touches; Lite's "captured by sp_server_diagnostics" (the actual SQL Server internal component) is accurate for both and is now what Darling's tail says too.

Tail field-verification table (fields named in Darling's rewritten tail, confirmed present in Darling's own JSON projection):

Tool Fields verified
system_health spinlock_backoffs, sick_spinlock_type(_after_av), latch_warnings, dump requests (total/interval), non_yielding_tasks_reported, system/sql_cpu_utilization, bad_pages_detected/fixed
severe_errors error_number, severity, state, database_id, database_name, message
io_issues state, io_latch_timeouts, interval/total_long_ios, longest_pending_requests_duration_ms, longest_pending_requests_file_path
scheduler_issues scheduler_id, cpu_id, status, is_online/runnable/running, non_yielding_time_ms
memory_conditions last_notification, out_of_memory_exceptions, available physical/virtual/paging memory, working_set_gb, vm_reserved/committed_gb, pages_*, the physical/virtual memory-low flags
cpu_tasks max_workers, workers_created/idle, tasks_completed_within_interval, pending_tasks, oldest_pending_task_waiting_time, has_unresolvable_deadlock_occurred, has_deadlocked_schedulers_occurred, did_blocking_occur
memory_broker currently_predicated/allocated, previously_allocated, broker, notification
memory_node_oom node's physical/virtual/page-file memory, target/reserved/committed_kb, failure_type, the memory-low flags
significant_waits unchanged - already at Lite's specificity

Pins updated: McpToolGuideTests (both SKUs) - replaced the single universal-sentence assertion with a per-tool EmptyAnswerFacts table (mirroring the existing GateFacts pattern), and raised the head-length target from 600 to 620 (significant_waits' floors sentence is now 615 chars, still far under D2's 1,000 absolute cap). McpToolsListBudget.txt on both SKUs - all nine tool get_health_parser_* lines grew (311-472 before, to 360-615 after) and both products' TotalCeilingBytes were raised to the measured value (Darling 328,331 -> 329,494; Lite 147,053 -> 148,216), with the reason logged in each test file's change log. McpZeroIsAMeasurementTests.EveryHealthParserTool_PublishesTheSourceWitness_AndClimbsTheSharedLadder - dropped its last_captured_at-in-description assertion (see above).

Red-watch: reverted severe_errors' new sentence back to the old universal text, rebuilt, and confirmed both EveryPilotHead_ServesTheWindow_TheEmptyGuardrail_AndItsGate and PilotHeads_AndTheGuideTool_AreIdenticalOnBothSkus failed as expected; restored the text, rebuilt, and confirmed green again.

Test totals for this round:

  • Targeted classes on both SKUs (McpToolGuideTests, McpToolsListBudgetTests, DarlingMcpHealthParserTools{SurfaceAndSql,LivePostgres}Tests, McpZeroIsAMeasurementTests, McpZeroIsAMeasurementTests, McpDescriptionTruthPinTests, EngineCapabilityMissTests, SignificantWaitsToolTests): all green.
  • FULL Darling.Tests: 13,082 total, 0 failed, 617 skipped (live PostgreSQL, no rig).
  • FULL Lite.Tests: 5,198 total, 0 failed, 0 skipped.

For the coordinator to double-check

  • The Darling pilot tails keep Darling's older, vaguer prose... Resolved in the D9 round above - Darling's tails now match Lite's specificity, field-verified against Darling's own projections.
  • The pointer text (" Reading guide: get_tool_guide.") is appended by the seam, not hand-written. It is 31 characters per converted tool, about 4.9 KB if all ~159 convert. Check that you're happy with it versus a shorter pointer or none.
  • The total ceilings allow 4 KB of slack; the per-tool and per-parameter lines are exact. A lane that edits any description must now update its budget line. That is intentional friction, and the failure message says exactly which line to change.
  • Where Darling and Lite's MCP server instructions drop from ~86KB/52KB to under 8KB each (#3898) #4039's per-tool prose went: it moved into the tools' own descriptions (get_collection_health, get_ag_health, the blocking/deadlock dedup_key, get_sweep_reports, get_query_store_clutter). None of it concerns the pilot. The dedup_key display-name scoping spans three tools and is the next topic candidate, for the blocking family's content PR.
  • deprecated/Dashboard was not changed. It shares WithGeminiCompatibleTools, so it now registers an unused catalog; its descriptions have no markers and are served unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv

erikdarlingdata and others added 5 commits September 23, 2026 11:24
…g guides, with a budget ratchet on both SKUs (#3898)

Phase 0: McpToolsListBudgetTests (Darling + Lite) measure the serialized tools/list and pin the total, each served description and each parameter description as ceilings that only go down. Seam: one <<GUIDE>> marker split in McpSchemaCompat.WithGeminiCompatibleTools; get_tool_guide(tools[], topics[]) on both SKUs. Pilot: the nine get_health_parser_* tools converted on both SKUs, the shared empty-window ladder moved to the system_health_empty_windows topic.

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

Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ADMEs; GuidePointer rename clears CA1720 (#3898)

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

Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ore, and a converted one as head plus pointer (#3898)

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

Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
… Lite's specificity (#4048)

D9's misreading check found a fresh Haiku model, given only the served head, hedged an
ungated tool's empty answer as ambiguous, and never cited severe_errors'/significant_waits'
gate or floors beside the sentence that mattered. All nine pilot heads carried the same
"read status, source_observed and last_captured_at" sentence regardless of whether the tool
gates at all.

Each head's empty-answer sentence now names the tool's own outcome: the seven gated tools
(significant_waits says "floors") tie it to events_in_window and the gate/floors; the two
ungated tools (system_health, memory_node_oom) say a recorded absence is a real result with
no gate to speak of. Verified field-by-field against each tool's own JSON projection before
wording it.

Darling's nine tails also get Lite's specificity: the two products return byte-identical
field sets per tool (confirmed by diffing every projection), so Lite's field lists carry
over as-is; the one exception, significant_waits, already matched and was left alone.
Also corrects Darling's system_health tail, which credited "sp_HealthParser" (the
Dashboard's proc) for data its own PARSE ON READ tools never touch.

Updates the pins this touches: McpToolGuideTests' per-tool empty-answer assertions (both
SKUs), the 600-char head target raised to 620 (significant_waits' floors sentence is now
615), McpToolsListBudget.txt's nine tool lines and each product's total ceiling, and
McpZeroIsAMeasurementTests' description check, which pinned the exact "last_captured_at"
wording the new heads intentionally no longer spell out (the data envelope and the guide
topic still carry it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
@erikdarlingdata

Copy link
Copy Markdown
Owner Author

D9 misreading check, run by the coordinator.

  • Method: a fresh Haiku model was given only the SERVED tools/list text of 17 tools: the 9 pilot heads, plus the tools behind traps 6-14. The text was dumped from the branch's own registration, not copied from source. The model couldn't call get_tool_guide.

Round 1, at 26b212e: 10 of 14 clean.

  • Trap 4 was misread. For memory_node_oom, empty with status empty and source_observed: true, the model answered "ambiguous, maybe a gap". The heads' "An empty answer is not a clean bill" was wrong for ungated reads.
  • Traps 1 and 3 were weak. The model anchored on that sentence and never cited the severity gate or the 500 ms floor beside it.
  • Traps 6 and 13 were fine.
    • Trap 6 kept hours_back narrow.
    • For trap 13 the model said "not stated" rather than guessing. The per-minute cadence is in the output payload's note, not the description.

Fixed at 66a5949 (lane W2):

  • Gated heads now read "nothing in this window passed the gate / floors, and events_in_window counts what was filtered out".
  • Ungated heads read "nothing of this kind was recorded".
  • Both say "status unavailable with source_observed false is no evidence either way".

Round 2, at 66a5949: traps 1-5 all clean, each for the right reason, with no guide call needed:

  • trap 1 cites the severity-19 gate;
  • trap 2: unavailable is no evidence;
  • trap 3 cites the 500 ms floor;
  • trap 4: no OOM was recorded, a real result and not a gap;
  • trap 5 cites the WARNING/10-task gate.

D9 is satisfied for this family. Later content PRs each carry their own trap list, and the coordinator runs the same check.

@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 23, 2026 16:18
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 16:18
@erikdarlingdata
erikdarlingdata merged commit 099ff4b into dev Sep 23, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-tool-guide-seam branch September 23, 2026 16:25
erikdarlingdata added a commit that referenced this pull request Sep 23, 2026
…ntries in their sections (#4080)

Adds 42 entries and 42 link refs (#3992, #3995, #3996, #3998, #4001, #4002, #4003, #4007, #4010, #4011, #4013, #4015, #4020, #4022, #4025, #4029, #4030, #4031, #4036, #4038, #4039, #4040, #4044, #4047, #4048, #4049, #4050, #4051, #4055, #4061, #4063, #4064, #4065, #4066, #4067, #4068, #4069, #4070, #4071, #4073, #4074, #4078). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran.

#3989 left 26 entries under bare 'Changed' and 'Fixed' lines above '### Added'. They move into '### Changed' and '### Fixed', below the new entries, and one blank line stays under [Unreleased].


Claude-Session: https://claude.ai/code/session_01Ua31ugERL5DmhFVRtf6keQ

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant