From a25bc2ff153a62a4b1180ee1921bc903e4dbd84a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:28:25 +0800 Subject: [PATCH 01/16] fix: align mode home responsive layout --- src/components/ClinicalDashboard.tsx | 8 ++++---- src/components/mode-home-template.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 51a31a707..3271189b4 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3400,10 +3400,10 @@ export function ClinicalDashboard({ differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined } desktopHomeComposerSlotId={desktopHomeComposerSlotId} - // Only the answer home ("How can I help?") keeps the in-flow hero - // pill + privacy notice on phones; every other mode home docks the - // compact pill to the bottom edge below sm. - heroComposerBreakpoint={showAnswerHome ? "all" : "sm-up"} + // Mode homes keep the composer in the centred hero slot at every + // breakpoint so documents, therapy, and the other homes share the + // same phone/tablet structure instead of switching to a bottom dock. + heroComposerBreakpoint={showDesktopHomeComposer || showAnswerHome ? "all" : "sm-up"} // Answer view: the header overlays the scrolling
at every width // (main reserves matching top padding) so content frosts under the // glass bar, and it slides away/returns with scroll direction. Other diff --git a/src/components/mode-home-template.tsx b/src/components/mode-home-template.tsx index b0e77b08d..e25b946f8 100644 --- a/src/components/mode-home-template.tsx +++ b/src/components/mode-home-template.tsx @@ -261,7 +261,7 @@ export function ModeHomeTemplate({ {actions?.length ? (
{actions.map((action, index) => { const ActionIcon = action.icon; @@ -285,8 +285,8 @@ export function ModeHomeTemplate({ ); const actionClassName = cn( - "mode-home-action group grid min-h-[4.4rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[4.75rem] sm:grid-cols-[2.75rem_minmax(0,1fr)_1rem] sm:gap-3 sm:rounded-lg sm:border sm:border-[color:var(--border)] sm:px-4 sm:py-3.5 sm:shadow-[var(--shadow-card)] lg:min-h-[4.75rem] lg:px-5", - index > 0 && "border-t border-[color:var(--border)] sm:border-t-[color:var(--border)]", + "mode-home-action group grid min-h-[4.4rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[4.4rem] sm:grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] sm:px-4 sm:py-3 lg:min-h-[4.75rem] lg:grid-cols-[2.75rem_minmax(0,1fr)_1rem] lg:gap-3 lg:rounded-lg lg:border lg:border-[color:var(--border)] lg:px-5 lg:py-3.5 lg:shadow-[var(--shadow-card)]", + index > 0 && "border-t border-[color:var(--border)] lg:border-t-0", ); if (action.href) { From 1c1dd35e4886435c431dba2f83d36affb9be1d56 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:05:32 +0800 Subject: [PATCH 02/16] fix: idle phone reserve for hero-owned mode homes Hero-owned phone composers no longer reserve a blank dock band, mode-home card top borders stay intact at lg, and contracts/UI expectations match the shared phone hero layout. Co-authored-by: Cursor --- src/components/ClinicalDashboard.tsx | 6 +++- .../mobile-composer-reserve.ts | 8 +++++ src/components/mode-home-template.tsx | 4 ++- tests/mobile-composer-reserve.test.ts | 29 ++++++++++++++++++- ...herapy-compass-responsive-contract.test.ts | 2 +- tests/ui-smoke.spec.ts | 8 +++-- tests/ui-tools.spec.ts | 5 ++-- 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 3271189b4..b9fc72c22 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3114,6 +3114,9 @@ export function ClinicalDashboard({ ((searchMode === "services" || searchMode === "forms") && !modeSearchSubmitted && !query.trim() && !loading); const differentialsCompareAddonActive = searchMode === "differentials" && modeSearchSubmitted && Boolean(query.trim()); + // Mode homes keep the composer in the centred hero at every breakpoint; when + // that hero owns phones there is no fixed bottom dock to clear. + const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome; // Hidden dock pad must stay at 0.75rem — Safari toolbar safe-area recreates a blank band. const mobileComposerReserve = resolveMobileComposerReserve( bottomComposerHidden, @@ -3121,6 +3124,7 @@ export function ClinicalDashboard({ searchMode, hasAnswerFollowUps: answerFollowUpSuggestions.length > 0, differentialsCompareAddonActive, + heroOwnsPhoneComposer, }), ); const renderDegradedNotice = () => ( @@ -3403,7 +3407,7 @@ export function ClinicalDashboard({ // Mode homes keep the composer in the centred hero slot at every // breakpoint so documents, therapy, and the other homes share the // same phone/tablet structure instead of switching to a bottom dock. - heroComposerBreakpoint={showDesktopHomeComposer || showAnswerHome ? "all" : "sm-up"} + heroComposerBreakpoint={heroOwnsPhoneComposer ? "all" : "sm-up"} // Answer view: the header overlays the scrolling
at every width // (main reserves matching top padding) so content frosts under the // glass bar, and it slides away/returns with scroll direction. Other diff --git a/src/components/clinical-dashboard/mobile-composer-reserve.ts b/src/components/clinical-dashboard/mobile-composer-reserve.ts index e4a19c25a..26575b96a 100644 --- a/src/components/clinical-dashboard/mobile-composer-reserve.ts +++ b/src/components/clinical-dashboard/mobile-composer-reserve.ts @@ -56,7 +56,15 @@ export function resolveDashboardVisibleMobileComposerReserve(input: { searchMode: string; hasAnswerFollowUps: boolean; differentialsCompareAddonActive: boolean; + /** Hero owns the phone composer (no fixed bottom dock) — match shell idle pad. */ + heroOwnsPhoneComposer?: boolean; }): string { + // Mode homes / answer home keep the in-flow hero pill on phones, so there is + // no floating dock to clear — only the idle content pad (same as standalone + // shell mode homes). Using the dock reserve here opens a blank bottom band. + if (input.heroOwnsPhoneComposer) { + return mobileComposerIdleReserve; + } if (input.searchMode === "answer") { return input.hasAnswerFollowUps ? mobileComposerVisibleReserve.dashboardAnswerWithFollowUps diff --git a/src/components/mode-home-template.tsx b/src/components/mode-home-template.tsx index e25b946f8..76fa14b00 100644 --- a/src/components/mode-home-template.tsx +++ b/src/components/mode-home-template.tsx @@ -286,7 +286,9 @@ export function ModeHomeTemplate({ ); const actionClassName = cn( "mode-home-action group grid min-h-[4.4rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[4.4rem] sm:grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] sm:px-4 sm:py-3 lg:min-h-[4.75rem] lg:grid-cols-[2.75rem_minmax(0,1fr)_1rem] lg:gap-3 lg:rounded-lg lg:border lg:border-[color:var(--border)] lg:px-5 lg:py-3.5 lg:shadow-[var(--shadow-card)]", - index > 0 && "border-t border-[color:var(--border)] lg:border-t-0", + // Phone stack separator only — at lg each action is its own card + // with `lg:border`, so do not zero the card's top edge. + index > 0 && "max-lg:border-t max-lg:border-[color:var(--border)]", ); if (action.href) { diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index 92afcc602..467633689 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -53,7 +53,7 @@ describe("mobile composer reserve contract", () => { ).toBe(mobileComposerIdleReserve); }); - it("uses the compact dock reserve for every non-answer dashboard dock (mode homes included)", () => { + it("uses the compact dock reserve for non-answer dashboard docks when the hero does not own phones", () => { for (const searchMode of ["documents", "services", "forms", "tools", "favourites"]) { expect( resolveDashboardVisibleMobileComposerReserve({ @@ -65,6 +65,33 @@ describe("mobile composer reserve contract", () => { } }); + it("keeps only the idle content pad when the dashboard hero owns the phone composer", () => { + expect( + resolveDashboardVisibleMobileComposerReserve({ + searchMode: "documents", + hasAnswerFollowUps: false, + differentialsCompareAddonActive: false, + heroOwnsPhoneComposer: true, + }), + ).toBe(mobileComposerIdleReserve); + expect( + resolveDashboardVisibleMobileComposerReserve({ + searchMode: "tools", + hasAnswerFollowUps: false, + differentialsCompareAddonActive: false, + heroOwnsPhoneComposer: true, + }), + ).toBe(mobileComposerIdleReserve); + expect( + resolveDashboardVisibleMobileComposerReserve({ + searchMode: "answer", + hasAnswerFollowUps: false, + differentialsCompareAddonActive: false, + heroOwnsPhoneComposer: true, + }), + ).toBe(mobileComposerIdleReserve); + }); + it("keeps the answer dock reserve compact, growing only for the follow-up chip row", () => { expect( resolveDashboardVisibleMobileComposerReserve({ diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts index db14838d3..79216871b 100644 --- a/tests/therapy-compass-responsive-contract.test.ts +++ b/tests/therapy-compass-responsive-contract.test.ts @@ -61,7 +61,7 @@ describe("Therapy Compass responsive contract", () => { expect(responsiveStackCount(therapyCardSource)).toBeGreaterThanOrEqual(2); expect(homeSource).toContain("ModeHomeMain"); expect(homeSource).toContain("ModeHomeTemplate"); - expect(modeHomeTemplateSource).toContain("sm:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]"); + expect(modeHomeTemplateSource).toContain("lg:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]"); expect(modeHomeTemplateSource).toContain("sm:flex-wrap"); expect(homeSource).toContain("desktopComposerSlotId={modeHomeDesktopComposerSlotId}"); expect(homeSource).toContain("ModeHomeVerificationFooter"); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 22d1b6997..4953e13ce 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2962,9 +2962,11 @@ test.describe("Clinical KB UI smoke coverage", () => { expect(startHereBox).not.toBeNull(); expect(documentsHeadingBox).not.toBeNull(); expect((documentsHeadingBox?.y ?? 0) + (documentsHeadingBox?.height ?? 0)).toBeLessThan(searchInputBox?.y ?? 0); - // Phones dock the compact composer at the bottom edge, below the hero content. - expect(searchInputBox?.y ?? 0).toBeGreaterThan(startHereBox?.y ?? 0); - await expect(page.locator('form.answer-footer-search-dock[data-footer-variant="compact"]')).toHaveCount(1); + // Phones keep the compact composer in the mode-home hero (above Start here), + // matching every other mode home — no fixed bottom dock on the empty home. + expect(searchInputBox?.y ?? 0).toBeLessThan(startHereBox?.y ?? 0); + await expect(page.locator('form.answer-footer-search-dock[data-footer-variant="compact"]')).toHaveCount(0); + await expect(page.locator(".mode-home-composer-slot").getByTestId("global-search-input")).toHaveCount(1); const recentDocumentsButton = page.getByRole("button", { name: /Recent documents/i }).first(); const browseLibraryButton = page.getByRole("button", { name: /Browse library/i }).first(); const sourcePdfButton = page.getByRole("button", { name: /Open a source PDF/i }).first(); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index b80984204..1aaa277d9 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -386,8 +386,9 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByLabel("Mode Tools")).toBeVisible(); await expect(visibleGlobalSearchInput(page)).toHaveCount(1); if (viewport.name === "mobile") { - // Phones dock the compact shared search at the bottom edge. - await expect(page.locator("form.answer-footer-search-dock").getByTestId("global-search-input")).toBeVisible(); + // Phones keep the compact shared search in the tools-home hero slot. + await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toBeVisible(); + await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); } else { await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toBeVisible(); } From a8abbc045efa2e2f62575fc953495dbd96becc41 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:18:32 +0800 Subject: [PATCH 03/16] chore: keep dashboard within size budget --- src/components/ClinicalDashboard.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index b9fc72c22..5f90724a6 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3114,8 +3114,6 @@ export function ClinicalDashboard({ ((searchMode === "services" || searchMode === "forms") && !modeSearchSubmitted && !query.trim() && !loading); const differentialsCompareAddonActive = searchMode === "differentials" && modeSearchSubmitted && Boolean(query.trim()); - // Mode homes keep the composer in the centred hero at every breakpoint; when - // that hero owns phones there is no fixed bottom dock to clear. const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome; // Hidden dock pad must stay at 0.75rem — Safari toolbar safe-area recreates a blank band. const mobileComposerReserve = resolveMobileComposerReserve( @@ -3404,9 +3402,8 @@ export function ClinicalDashboard({ differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined } desktopHomeComposerSlotId={desktopHomeComposerSlotId} - // Mode homes keep the composer in the centred hero slot at every - // breakpoint so documents, therapy, and the other homes share the - // same phone/tablet structure instead of switching to a bottom dock. + // Mode homes keep the composer in the centred hero at every breakpoint, + // sharing the phone/tablet structure instead of switching to a bottom dock. heroComposerBreakpoint={heroOwnsPhoneComposer ? "all" : "sm-up"} // Answer view: the header overlays the scrolling
at every width // (main reserves matching top padding) so content frosts under the From 5bbe3e38859a79c5bbe14cc9443e3b8b2f953ce3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:19:10 +0000 Subject: [PATCH 04/16] fix: keep dock reserve when answer home has no hero slot Base heroOwnsPhoneComposer on the mounted desktopHomeComposerSlotId so setup/error answer-home states keep phone dock clearance instead of the idle pad that lets the fixed composer cover the error message. Co-authored-by: BigSimmo --- src/components/ClinicalDashboard.tsx | 2 +- tests/mobile-composer-reserve.test.ts | 29 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index ebad5c3a1..82fe1ced8 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3114,7 +3114,7 @@ export function ClinicalDashboard({ ((searchMode === "services" || searchMode === "forms") && !modeSearchSubmitted && !query.trim() && !loading); const differentialsCompareAddonActive = searchMode === "differentials" && modeSearchSubmitted && Boolean(query.trim()); - const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome; + const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId); // Hidden dock pad must stay at 0.75rem — Safari toolbar safe-area recreates a blank band. const mobileComposerReserve = resolveMobileComposerReserve( bottomComposerHidden, diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index 46bf1b021..ce37b176b 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -1,3 +1,6 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + import { describe, expect, it } from "vitest"; import { @@ -12,6 +15,10 @@ import { resolveShellVisibleMobileComposerReserve, } from "@/components/clinical-dashboard/mobile-composer-reserve"; +function source(relativePath: string): string { + return readFileSync(resolve(process.cwd(), relativePath), "utf8"); +} + describe("mobile composer reserve contract", () => { it("collapses to zero hidden pad without Safari toolbar safe-area", () => { expect(mobileComposerHiddenReserve).toBe("0rem"); @@ -93,6 +100,28 @@ describe("mobile composer reserve contract", () => { ).toBe(mobileComposerIdleReserve); }); + it("derives hero phone ownership from the mounted hero slot, not answer-home alone", () => { + // Answer-home + !canRunSearch keeps showAnswerHome true while the hero slot + // is unset (showDesktopHomeComposer requires !error). Ownership must follow + // the slot so the dock reserve stays and the fixed composer cannot cover the + // setup/error message. + const dashboard = source("src/components/ClinicalDashboard.tsx"); + const header = source("src/components/clinical-dashboard/master-search-header.tsx"); + expect(dashboard).toContain("const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId);"); + expect(dashboard).not.toContain("const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome;"); + expect(header).toContain( + 'const heroComposerOwnsPhones = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all";', + ); + expect( + resolveDashboardVisibleMobileComposerReserve({ + searchMode: "answer", + hasAnswerFollowUps: false, + differentialsCompareAddonActive: false, + heroOwnsPhoneComposer: false, + }), + ).toBe(mobileComposerVisibleReserve.dashboardAnswer); + }); + it("keeps the answer dock reserve compact, growing only for the follow-up chip row", () => { expect( resolveDashboardVisibleMobileComposerReserve({ From 9a53289284f90272e9805962645a927cd4e86f33 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:23:56 +0000 Subject: [PATCH 05/16] fix: keep guest favourites on dock reserve Co-authored-by: BigSimmo --- src/components/ClinicalDashboard.tsx | 2 +- tests/mobile-composer-reserve.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 82fe1ced8..9bf19aa0d 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3092,7 +3092,7 @@ export function ClinicalDashboard({ const showDesktopHomeComposer = !error && (activeModeResultKind === "tools" || - activeModeResultKind === "favourites" || + (activeModeResultKind === "favourites" && favouritesAccessible) || (!loading && (showAnswerHome || (searchMode === "documents" && diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index ce37b176b..5268f836e 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -107,6 +107,7 @@ describe("mobile composer reserve contract", () => { // setup/error message. const dashboard = source("src/components/ClinicalDashboard.tsx"); const header = source("src/components/clinical-dashboard/master-search-header.tsx"); + expect(dashboard).toContain('(activeModeResultKind === "favourites" && favouritesAccessible)'); expect(dashboard).toContain("const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId);"); expect(dashboard).not.toContain("const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome;"); expect(header).toContain( From e3dc87000c292847d1a27a64ffadc863fa3f1b9a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:26:07 +0000 Subject: [PATCH 06/16] chore: note review reply permission denial GraphQL review-thread reply returned 403; resolved the fixed thread directly after validation. Co-authored-by: BigSimmo From 769cbbc96418a33572bbf941c362cd151058301d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:27:27 +0000 Subject: [PATCH 07/16] docs: record PR babysit sweep Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 03d3fca65..f09f0d7e9 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -707,3 +707,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-21 | claude/clinical-kb-pwa-review-asi3wb (Option A wave verdict — no code change; #1040 merged as cde6c5c) | canary run 29827012719 (#61, main cde6c5c) vs banked #60 (29800029819) | OPTION A WAVE ADOPTED — FIRST FULLY-GREEN 44-CASE CANARY IN PROGRAM HISTORY (Blocking failures: None). (1) Option A payoff EXCEEDED: citation_failure_rate 0.0227→0; the neuroleptic-side-effect-escalation case flipped from wrong-doc→failed-generation→1-citation-fallback to **strong route, successful gpt-5.6-sol generation, passed in 15.4s with no fallback marker** — the rescued S3 retrieval fixed generation itself, not just the citation count; expected_source_hit 0.6364→0.6591. (2) Golden held exactly as the blast-radius analysis promised: 36/36 PASS, content_recall 1.0, mrr@10 0.8921 BYTE-IDENTICAL to the pre-wave baseline (zero ordering movement — no golden case fires the predicate), irrelevant@10 0.1083→0.0917 (slightly better). (3) Parity payoff PARTIAL: monitoring targeting 1/5→2/5 (olanzapine-lai flipped — previously called a retrieval-depth residual; quetiapine-dose also flipped on the dose side); lithium-range (232ch) + metabolic (73ch, byte-identical answer to #60) did NOT flip despite offline-proven fixes — their live chunk sets evidently contain no admissible schedule sentence even under the widened gate → reclassified as retrieval-depth/live-content residuals joining adhd; below the ≥3/5 target but strictly improved, no regression anywhere. Dose 2/5 vs 2/4: same passing count, applicable set grew (new quality-metformin-renal-dosing miss = eval-set churn, not regression). (4) No-worse EXCEEDED: relevance 0.5333→0.6 (the two-step watch-item slide FULLY REVERSED to the #58 level), targeting_rate 0.6667→0.6957, fail_closed 0.9 held, readability/artifact_leaks 1.0, route ceilings 0, grounded 1.0, unsupported_correct 1.0, numeric 0, p95 22.8s, red_result 3/3. Adoption per the measured-gain rule: primary goal achieved, three case flips, relevance recovered, zero regressions. Residual queue: monitoring retrieval-depth trio (lithium-range/metabolic/adhd), E-3d H2 discards, weekly ANSWER_CASE_LIMIT 8→44 raise now unblocked (gate would be green), comparison-class coverage. Wave spend +~$2-4 → Phase E + Option A total ~$12-20 of ≤$20. | Evidence: run #61 job log read (Threshold Status: None; Answer Metrics; neuroleptic diagnostics row; targeting metric_rates + 6-miss list; golden 36 PASS lines + summary). Revert drill NOT triggered. | | 2026-07-21 | claude/clinical-kb-pwa-review-asi3wb (PR: I9 weekly coverage raise) | see PR head | ADDENDUM 5 post-green item I9 (plan-authorized "after reds fixed"): weekly scheduled canary ANSWER_CASE_LIMIT default 8→44 — the Sunday 18:00 UTC cron now guards the FULL answer-quality case set instead of the first 8 (both #57 blocking reds historically lived OUTSIDE the first 8, leaving the weekly gate blind to them). Unblocked by run #61 proving the citation gate green on the full 44. Cost: est +$1-2/week (user-authorized in the plan). Contract test pin updated in lockstep (eval-canary-workflow.test.ts). Dispatch shapes unchanged (input override still wins); operational-risk diff, plain-revert rollback. | check:github-actions PASS; check:ci-scope PASS; check:gate-manifest PASS (20/20); eval-canary-workflow contract 4/4; prettier clean; no provider calls | | 2026-07-21 | claude/database-governance-audit-10b6ed (PR #1051: source-governance audit — safe subset) | cee396730 | Governance-metadata observability + UI display + provenance flow test; no ranking/retrieval/generation surface touched. | IMPLEMENTED + handed off (not a review of prior work). Resolved audit #1 (logger.warn on unrecognized enum values; return value unchanged), #2 (review_due_source added to frontendVisibleWarningCodes → answer-level badge; warning-severity, no refusal impact), #9 (source_metadata retained on safety-finding citations + governance pill in SafetyFindingsListContent), #13 (new tests/provenance-flow.test.ts: DB-normalize→governance→client payload sources+safety citations→render policy). Deferred #4/5/6/8/10 (RAG-protected ranking/selection/LLM-context/cache — need live eval-canary+approval), #11/#5 flag debt (D5/D4), #3 (is_public schema/RLS), #7 (conflict-detection scope), #12 (canary automation). Rebased onto origin/main (was 18 behind; conflict-free — none of the 18 commits touched the 8 files). PR-policy CI green (confirmed no ragRankingPatterns match). | verify:pr-local exit 0 (351 files/3129 tests, production build, client-bundle secret scan, offline RAG fixtures 36/36); typecheck + lint + prettier green. verify:ui NOT run locally: pre-existing globals.css Tailwind/Turbopack dev-compile error (git-clean, unrelated; prod build passed) — CI Production UI job covers it. check:production-readiness deferred (offline env/config validator; PR changes no env/secret/config inputs; secretless worktree). No provider calls. | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | e3dc87000 | Run PR sweep: CI fix + threads + drift | Before: PR required green, 1 unresolved guest-favourites dock thread, branch behind main. After: merged origin/main cleanly; gated favourites hero composer slot on favouritesAccessible; thread resolved via GraphQL; reply mutation 403 noted in commit e3dc8700. | node scripts/run-vitest.mjs run --reporter=dot tests/mobile-composer-reserve.test.ts PASS (9/9); git diff --check PASS; no Supabase/OpenAI/live eval gates run. | From 6562a0f6af2e4b0c3dff847aa74966313ed33d48 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:28:45 +0800 Subject: [PATCH 08/16] docs: record Run PR behind-main sweep for #1124 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1b36513e0..31c50b62a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -732,3 +732,4 @@ This file is append-only. Never rewrite or delete an existing review record; app - Outcome: No new P0/P1 search chrome defect found in the static review. Fixed one regression hazard: a stale ClinicalDashboard comment still instructed a 0.75rem hidden dock pad despite the implementation/tests requiring 0rem. Added durable search chrome behaviour rules in AGENTS.md and docs/search-chrome-behaviour.md, with a static guard tying the remembered rules to the hidden-reserve contract. - Checks: dependency shortcut section count; git diff --check; targeted rg for stale 0.75rem hidden-pad source wording (only negative test assertions remain); targeted Vitest command attempted but blocked by missing node_modules/vitest under Node 20.20.2 in this container. No provider-backed checks run. | 2026-07-24 | cursor/search-interactive-perf-af54 (PR #1138) | ff4b293d95f922e70ebf5ee9b0c156c41a8bff3b | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green, 0 threads. RAG impact: no retrieval behaviour change — PR is client deferred-search/UI only (no src/lib/rag/**). After: merged origin/main; conflict resolved in src/components/ui/sheet.tsx by keeping main restoreTimersRef/unmountingRef focus-restore fix; pushed ff4b293d9. Threads: none. | merge origin/main only; no provider-backed checks run | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | 2cb98b40e2fd36650e293b27a97996c069971db3 | Run PR sweep: CI fix + threads + drift | Before: behind main by 35. After: merged origin/main cleanly (no conflicts). Threads: non-P0/P1 left open. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | From e061ce29300cd80df5c8cd95d2ea8ecfeef1e2b0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:40:47 +0800 Subject: [PATCH 09/16] fix: drop prescribing hero slot once draft query is typed MedicationHome unmounts on a non-empty query before submit. Keep showDesktopHomeComposer false in that state so phones use the dock path and dock-sized mobileComposerReserve instead of covering results. --- src/components/ClinicalDashboard.tsx | 7 ++++++- tests/mobile-composer-reserve.test.ts | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index c37a17a63..7cbef5023 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3082,7 +3082,12 @@ export function ClinicalDashboard({ activeModeResultKind === "documents" && documentMatches.length === 0 && !modeSearchSubmitted) || - (searchMode === "prescribing" && activeModeResultKind === "documents" && !modeSearchSubmitted) || + // Prescribing home unmounts as soon as the query is non-empty, so keep + // the hero/phone-composer slot only while MedicationHome actually mounts. + (searchMode === "prescribing" && + activeModeResultKind === "documents" && + !modeSearchSubmitted && + !query.trim()) || (activeModeResultKind === "differentials" && !modeSearchSubmitted)))); const desktopHomeComposerSlotId = showDesktopHomeComposer ? modeHomeDesktopComposerSlotId : undefined; // Favourites and Tools are content-rich hubs: they share the centred hero but diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index 5268f836e..ef4ed7a89 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -110,6 +110,11 @@ describe("mobile composer reserve contract", () => { expect(dashboard).toContain('(activeModeResultKind === "favourites" && favouritesAccessible)'); expect(dashboard).toContain("const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId);"); expect(dashboard).not.toContain("const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome;"); + // Prescribing leaves MedicationHome as soon as the draft query is non-empty; + // keep the hero slot (and idle phone reserve) only while that home mounts. + expect(dashboard).toMatch( + /searchMode === "prescribing" &&\s*activeModeResultKind === "documents" &&\s*!modeSearchSubmitted &&\s*!query\.trim\(\)/, + ); expect(header).toContain( 'const heroComposerOwnsPhones = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all";', ); From a0335df80693b0dd749f09396d4b6c65f3bc9eb3 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:43:13 +0800 Subject: [PATCH 10/16] docs: ledger Run PR babysit for #1124 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 31c50b62a..7eee032da 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -733,3 +733,4 @@ This file is append-only. Never rewrite or delete an existing review record; app - Checks: dependency shortcut section count; git diff --check; targeted rg for stale 0.75rem hidden-pad source wording (only negative test assertions remain); targeted Vitest command attempted but blocked by missing node_modules/vitest under Node 20.20.2 in this container. No provider-backed checks run. | 2026-07-24 | cursor/search-interactive-perf-af54 (PR #1138) | ff4b293d95f922e70ebf5ee9b0c156c41a8bff3b | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green, 0 threads. RAG impact: no retrieval behaviour change — PR is client deferred-search/UI only (no src/lib/rag/**). After: merged origin/main; conflict resolved in src/components/ui/sheet.tsx by keeping main restoreTimersRef/unmountingRef focus-restore fix; pushed ff4b293d9. Threads: none. | merge origin/main only; no provider-backed checks run | | 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | 2cb98b40e2fd36650e293b27a97996c069971db3 | Run PR sweep: CI fix + threads + drift | Before: behind main by 35. After: merged origin/main cleanly (no conflicts). Threads: non-P0/P1 left open. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | e061ce29300cd80df5c8cd95d2ea8ecfeef1e2b0 | Run PR babysit: CI/threads/drift | Run PR babysit: Codex P2 prescribing typed-draft dock path fixed + thread resolved. Before: CI mostly green, 1 unresolved Codex P2 (3644919028). After: showDesktopHomeComposer for prescribing requires !query.trim(); documents half dispositioned as not holding (home slot stays pre-submit); reply+resolve PRRT_kwDOSh5Fis6Th-6B. Not behind main. | npx vitest run tests/mobile-composer-reserve.test.ts PASS (9/9). No provider-backed checks run. | From 8adaaf22984890b3fde000de874dc88082a3b936 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:45:39 +0800 Subject: [PATCH 11/16] docs: ledger post-main sync for #1124 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 29629d491..7bfe920a6 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -735,3 +735,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | 2cb98b40e2fd36650e293b27a97996c069971db3 | Run PR sweep: CI fix + threads + drift | Before: behind main by 35. After: merged origin/main cleanly (no conflicts). Threads: non-P0/P1 left open. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | e061ce29300cd80df5c8cd95d2ea8ecfeef1e2b0 | Run PR babysit: CI/threads/drift | Run PR babysit: Codex P2 prescribing typed-draft dock path fixed + thread resolved. Before: CI mostly green, 1 unresolved Codex P2 (3644919028). After: showDesktopHomeComposer for prescribing requires !query.trim(); documents half dispositioned as not holding (home slot stays pre-submit); reply+resolve PRRT_kwDOSh5Fis6Th-6B. Not behind main. | npx vitest run tests/mobile-composer-reserve.test.ts PASS (9/9). No provider-backed checks run. | | 2026-07-24 | codex/fix-next.js-startup-failure-and-verify-pages (PR #1149) | 8ddddbab2a29a94b3f993cbd114889f72c95f4f1 | Run PR sweep: CI fix + threads + drift | Before: behind main. After: merged origin/main cleanly (no conflicts). Unresolved review threads left as non-P0/P1. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | bbd5aafaadc7334107bfe531eef291615b26ed4b | Run PR babysit: CI/threads/drift | Post-fix merge origin/main (clean). Prescribing dock P2 fixed+resolved earlier; CI re-running. | merge origin/main; vitest mobile-composer-reserve 9/9 earlier; no provider-backed checks run. | From 9d48f67c150e71eede5b1ce4e6b669ab9fbc6887 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:56:49 +0800 Subject: [PATCH 12/16] docs: ledger re-sync sweep for PR #1124 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 912270b58..643cf758a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -742,3 +742,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | remediate-audit-system-issues (PR #1160) | 04201a87cc7ad7dd1477d17cd2b96544b7379789 | Run PR sweep: CI fix + threads + drift | re-merge origin/main after CONFLICTING relapse; resolved scripts/run-eval-safe.mjs (kept main taskkill /T /F + validPids); sitemap Prettier format() retained from fe0588b86; no unresolved threads; no conflict markers | merge + conflict resolve only; no provider-backed checks run | | 2026-07-24 | codex/fix-next.js-startup-failure-and-verify-pages (PR #1149) | 8ddddbab2a29a94b3f993cbd114889f72c95f4f1 | Run PR sweep: CI fix + threads + drift | Before: behind main. After: merged origin/main cleanly (no conflicts). Unresolved review threads left as non-P0/P1. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | remediate-audit-system-issues (PR #1160) | bdf530fc8c6faaa4491c510396b47872fc39bf25 | Run PR sweep: CI fix + threads + drift | second re-merge after main moved to 2e68888f3 during first push; clean ort merge (ledger + layout.tsx); taskkill /T retained; sitemap prettier retained | merge only; no provider-backed checks run | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | d0a96caa01b90dffa40e60770b7700f36e8ba43f | Run PR re-sync sweep | Before: CONFLICTING @8adaaf229. After: merged origin/main clean (ort). CI re-running. Threads: not re-triaged. | merge origin/main and/or conflict re-check only; no provider-backed checks run | From c59ae820db3034ef3cf74ae6d0030a0472a3a264 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 16:06:29 +0000 Subject: [PATCH 13/16] fix: dock reserve for stale differentials drafts and ledger dup Gate the Differentials hero composer slot so a typed draft with leftover evidence matches stays on the phone dock path, and drop the duplicated PR #1149 branch-review ledger row blocking Static PR checks. Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 - src/components/ClinicalDashboard.tsx | 6 +++++- tests/mobile-composer-reserve.test.ts | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index be093628d..24279b013 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -743,7 +743,6 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | remediate-audit-system-issues (PR #1160) | 8b2359589fe61c19c78fb02be50316c8f29d7e18 | Run PR sweep: CI fix + threads + drift | before: CONFLICTING; Static PR checks + Unit coverage + PR required FAIL (stale docs/site-map.md). after: merged origin/main cleanly (2e7b034d1); regenerated site-map (8b2359589fe61c19c78fb02be50316c8f29d7e18); no unresolved review threads; CI re-running expected green for static-pr/coverage/pr-required | vitest tests/site-map.test.ts pass (6); sitemap:check pass; no provider-backed checks run | | 2026-07-24 | remediate-audit-system-issues (PR #1160) | 992ebefa296d6894d5448c1381f1b0b95580e529 | Run PR sweep: CI fix + threads + drift | supersedes prior #1160 row: final HEAD after prettier site-map; merge origin/main clean; stale sitemap fixed; no threads | vitest site-map pass; sitemap:check pass; no provider-backed checks run | | 2026-07-24 | remediate-audit-system-issues (PR #1160) | 04201a87cc7ad7dd1477d17cd2b96544b7379789 | Run PR sweep: CI fix + threads + drift | re-merge origin/main after CONFLICTING relapse; resolved scripts/run-eval-safe.mjs (kept main taskkill /T /F + validPids); sitemap Prettier format() retained from fe0588b86; no unresolved threads; no conflict markers | merge + conflict resolve only; no provider-backed checks run | -| 2026-07-24 | codex/fix-next.js-startup-failure-and-verify-pages (PR #1149) | 8ddddbab2a29a94b3f993cbd114889f72c95f4f1 | Run PR sweep: CI fix + threads + drift | Before: behind main. After: merged origin/main cleanly (no conflicts). Unresolved review threads left as non-P0/P1. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | codex/query-ribbon-search-headings (PR #1166) | cbb493c2b51dd7120df39cfbcd5f19b73f882d43 | Run PR babysit: CI/threads/drift | Before: not behind at start, then main advanced (8 behind); required checks green except Production UI still pending on prior head; 0 unresolved review threads; no Bugbot findings. After: merged origin/main cleanly (docs/branch-review-ledger.md + src/app/layout.tsx auto-merge). No code fix needed. | merge origin/main only; no provider-backed checks run. | | 2026-07-24 | codex/query-ribbon-search-headings (PR #1166) | ad3d38c62a19f5fa2a7e8356021795937c5b0f66 | Run PR babysit: CI/threads/drift | Supersedes prior #1166 row in this push: post-merge+ledger HEAD after syncing origin/main (clean auto-merge). 0 threads; required CI re-running. | merge origin/main; ledger append; no provider-backed checks run. | | 2026-07-24 | `codex/query-ribbon-search-headings` (PR #1166) | `37cfa5553ccb784ee5e9f47ded1ad69914c053ed` + reviewed correction diff | Correction: universal Query Ribbon implementation and responsive search-heading review | SUPERSEDES the earlier row that named non-existent pre-amend SHA `16ce57d9615708528e7924b41837210a24414722`. This resolvable reviewed tip contains functional commit `0b67944b0d2973d612833422fb4074aeacdb6c8c`, current-main syncs, and the append-only ledger correction. The prior APPROVE outcome and residual-risk statement are unchanged; no P0-P2 finding remains. | Query Ribbon DOM 4/4 after each main sync; exact-head hosted policy, static checks, unit coverage, build, advisory UI, Production UI, safety/config, Semgrep, Gitleaks, GitGuardian, and `PR required` passed before the final docs-only correction. No OpenAI, Supabase, Railway, deployment, production-data, or clinical provider workflow ran. | diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 7cbef5023..bf96f38d7 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3088,7 +3088,11 @@ export function ClinicalDashboard({ activeModeResultKind === "documents" && !modeSearchSubmitted && !query.trim()) || - (activeModeResultKind === "differentials" && !modeSearchSubmitted)))); + // DifferentialsHome leaves ModeHomeTemplate when a draft query coincides + // with stale evidence matches — keep the hero slot only while home mounts. + (activeModeResultKind === "differentials" && + !modeSearchSubmitted && + !(query.trim() && documentMatches.length > 0))))); const desktopHomeComposerSlotId = showDesktopHomeComposer ? modeHomeDesktopComposerSlotId : undefined; // Favourites and Tools are content-rich hubs: they share the centred hero but // stay top-aligned so their lists start in a stable position. diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index ef4ed7a89..b7c6f5874 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -115,6 +115,11 @@ describe("mobile composer reserve contract", () => { expect(dashboard).toMatch( /searchMode === "prescribing" &&\s*activeModeResultKind === "documents" &&\s*!modeSearchSubmitted &&\s*!query\.trim\(\)/, ); + // DifferentialsHome shows results (no mode-home slot) when a draft query + // coincides with stale evidence matches after clearing a submitted search. + expect(dashboard).toMatch( + /activeModeResultKind === "differentials" &&\s*!modeSearchSubmitted &&\s*!\(query\.trim\(\) && documentMatches\.length > 0\)/, + ); expect(header).toContain( 'const heroComposerOwnsPhones = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all";', ); From 55a4a61d10884c71ace2910e24e142325a697ba6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 16:06:29 +0000 Subject: [PATCH 14/16] docs: ledger babysit outcome for PR #1124 Record the merge/CI/thread cleanup pass for the mode-home phone layout PR. Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 24279b013..7762c1222 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -752,3 +752,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | d0a96caa01b90dffa40e60770b7700f36e8ba43f | Run PR re-sync sweep | Before: CONFLICTING @8adaaf229. After: merged origin/main clean (ort). CI re-running. Threads: not re-triaged. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | codex/query-ribbon-search-headings (PR #1166) | c94e89f392f578c4b2c749195dd485b74959074c | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. CI re-running. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | cursor/comprehensive-repo-review-ledger-d9a1 (PR #1150) | 345c02cdbaefb13aeb951a14674aedfe4648a50e | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | c59ae820db3034ef3cf74ae6d0030a0472a3a264 | Run PR babysit: merge/CI/threads | Before: CONFLICTING + Static PR checks FAIL (duplicate #1149 ledger row) + 2 unresolved Codex P2s. After: merged origin/main; removed duplicate ledger row; Differentials hero slot gated on !(query.trim() && documentMatches.length); threads resolved. | vitest mobile-composer-reserve 9/9; check:branch-review-ledger pass; maintainability 4132/4140. No provider-backed checks run. | From 581febd20c3478763c7af63dd12b305d9054f2f3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 19:14:24 +0000 Subject: [PATCH 15/16] fix: auto-sync open PR branches and clear postcss audit Add a main-push workflow and local helper that update behind PR heads so GitHub stops falsely marking the queue CONFLICTING/DIRTY after each land. Bump postcss to a non-vulnerable release so Safety npm audit stays green. --- .claude/skills/run-pr/SKILL.md | 12 ++- .cursor/agents/pr-babysit.md | 2 +- .github/workflows/pr-branch-sync.yml | 124 +++++++++++++++++++++++++++ AGENTS.md | 28 ++++++ docs/branch-review-ledger.md | 1 + docs/codebase-index.md | 28 +++--- docs/process-hardening.md | 14 +++ package-lock.json | 16 ++-- package.json | 8 +- scripts/sync-open-pr-branches.mjs | 122 ++++++++++++++++++++++++++ tests/sync-open-pr-branches.test.ts | 37 ++++++++ 11 files changed, 364 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/pr-branch-sync.yml create mode 100644 scripts/sync-open-pr-branches.mjs create mode 100644 tests/sync-open-pr-branches.test.ts diff --git a/.claude/skills/run-pr/SKILL.md b/.claude/skills/run-pr/SKILL.md index 76c7deda9..5f485646b 100644 --- a/.claude/skills/run-pr/SKILL.md +++ b/.claude/skills/run-pr/SKILL.md @@ -71,11 +71,17 @@ required checks, unresolved-thread count, and behind/ahead relative to `main`. ### Step 2 — branch drift first (so CI fixes target the merged state) +- Hosted mitigation: `.github/workflows/pr-branch-sync.yml` updates open PR branches + after each push to `main`. Do not treat a fresh `DIRTY` state as a product bug until + you confirm the tip is still behind or `git merge-tree` reports real conflicts. - Behind `main` but cleanly mergeable, with no local checkout otherwise needed → - `mcp__github__update_pull_request_branch`, then re-fetch. + `mcp__github__update_pull_request_branch` (or `npm run sync:pr-branches:apply` / + `gh api .../update-branch`), then re-fetch. - Conflicting (`mergeable_state: dirty`), or the branch is being checked out anyway → - `git switch ` after fetch, then `git merge origin/main`. If the merge brings dependency - changes or touches `package-lock.json`, run `npm install` before verification. + classify with `git merge-tree --write-tree origin/main ` first. If clean, merge + `origin/main` (or update-branch) and push. If conflicted, `git switch ` after + fetch, then `git merge origin/main`. If the merge brings dependency changes or touches + `package-lock.json`, run `npm install` before verification. - Mechanically resolvable conflicts (adjacent hunks, import lists, lockfile → regenerate via `npm install`, generated files → re-run their generator, e.g. `sitemap:update`) → resolve, then run the narrowest gate covering the conflicted files. diff --git a/.cursor/agents/pr-babysit.md b/.cursor/agents/pr-babysit.md index bcbbd8044..aa93a47f8 100644 --- a/.cursor/agents/pr-babysit.md +++ b/.cursor/agents/pr-babysit.md @@ -13,7 +13,7 @@ When invoked: 1. Identify the target PR (number, URL, or current branch), resolve and fetch its exact head SHA, then check out or isolate that head and assert the checkout still matches before editing or pushing. Fetch mergeability, check rollup, and unresolved review threads only. 2. Prefer the smallest safe fix that unblocks merge. Do not rewrite unrelated code. -3. Run `git fetch origin --prune` and verify the latest `origin/main` immediately before evaluating drift. Merge it into the feature branch when the PR is behind or conflicts are trivial; abort and ask when intents conflict. Never rebase. +3. Run `git fetch origin --prune` and verify the latest `origin/main` immediately before evaluating drift. Treat GitHub `DIRTY`/`CONFLICTING` as stale-behind until `git merge-tree` proves a real content conflict. Prefer GitHub update-branch / the `pr-branch-sync` workflow when available; otherwise merge `origin/main` into the feature branch when behind or conflicts are trivial; abort and ask when intents conflict. Never rebase. 4. Fix CI failures caused by this PR's scope. Never weaken workflows or delete required checks to force green. Ignore advisory jobs (`ui-advisory`, `release-browser-matrix`). 5. Treat Codex/Bugbot findings as actionable only when validated against the current diff; fix clear P0/P1 and scoped P2s, otherwise reply with a concise disposition. Prefer the `pr-bugbot` agent when Bugbot threads dominate. 6. After fixing a review thread, reply first (never resolve silently), then use the authorized direct resolution tool. Delegated Bugbot threads follow the same path. Only the trusted Codex autofix identity may fall back to ``; otherwise leave the thread open and report the missing capability. diff --git a/.github/workflows/pr-branch-sync.yml b/.github/workflows/pr-branch-sync.yml new file mode 100644 index 000000000..904562817 --- /dev/null +++ b/.github/workflows/pr-branch-sync.yml @@ -0,0 +1,124 @@ +name: Sync open PR branches + +# When main moves, open PR heads go stale and GitHub often reports them as +# CONFLICTING/DIRTY even when a clean merge exists. That blocks squash +# auto-merge and creates a perpetual "merge conflict" churn across the queue. +# +# This workflow updates every open same-repo PR branch from main after each +# push to main (and on manual dispatch). It uses GitHub's update-branch API +# (merge commit into the head), never rebases, and never merges PRs into main. + +on: + push: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: sync-open-pr-branches + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + name: Update behind PR branches + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Update open PR branches from main + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const base = "main"; + const skipLabels = new Set(["hold", "do-not-merge", "skip-branch-sync"]); + + const pulls = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + base, + per_page: 100, + }); + + const results = { updated: [], skipped: [], failed: [] }; + + for (const pr of pulls) { + const labels = new Set((pr.labels || []).map((l) => String(l.name || "").toLowerCase())); + const title = String(pr.title || ""); + const headRepo = pr.head?.repo?.full_name; + const reasonSkip = + headRepo && headRepo !== `${owner}/${repo}` + ? "fork-head" + : [...skipLabels].find((l) => labels.has(l)) + ? `label:${[...skipLabels].find((l) => labels.has(l))}` + : /\b(WIP|do not merge)\b/i.test(title) + ? "wip-title" + : null; + if (reasonSkip) { + results.skipped.push(`#${pr.number} ${reasonSkip}`); + continue; + } + + // Cheap behind check — avoid update-branch noise when already current. + let behindBy = null; + try { + const comparison = await github.rest.repos.compareCommitsWithBasehead({ + owner, + repo, + basehead: `${base}...${pr.head.ref}`, + }); + behindBy = comparison.data.behind_by ?? 0; + } catch (error) { + results.failed.push(`#${pr.number} compare:${error.status || error.message}`); + continue; + } + if (behindBy === 0) { + results.skipped.push(`#${pr.number} already-current`); + continue; + } + + try { + await github.rest.pulls.updateBranch({ + owner, + repo, + pull_number: pr.number, + expected_head_sha: pr.head.sha, + }); + results.updated.push(`#${pr.number} behind=${behindBy}`); + core.info(`Updated #${pr.number} (${pr.head.ref}) from ${base} (was behind ${behindBy})`); + } catch (error) { + const status = error.status || "err"; + const msg = error.message || String(error); + // 422 = not behind / merge conflict / head moved — report, do not fail the job. + results.failed.push(`#${pr.number} ${status}:${msg.split("\n")[0]}`); + core.warning(`Could not update #${pr.number}: ${status} ${msg}`); + } + + // Small pacing to reduce secondary-rate-limit pressure on large queues. + await new Promise((r) => setTimeout(r, 500)); + } + + core.summary + .addHeading("Open PR branch sync") + .addRaw( + [ + `Updated: ${results.updated.length}`, + `Skipped: ${results.skipped.length}`, + `Failed/blocked: ${results.failed.length}`, + "", + results.updated.length ? `### Updated\n${results.updated.map((x) => `- ${x}`).join("\n")}` : "", + results.failed.length ? `### Failed/blocked\n${results.failed.map((x) => `- ${x}`).join("\n")}` : "", + ] + .filter(Boolean) + .join("\n"), + ) + .write(); + + // Soft-success: real conflicts need humans; stale behind heads are the happy path. + if (results.failed.some((line) => /5\d\d:/.test(line))) { + core.setFailed("One or more PR branch updates failed with a server error."); + } diff --git a/AGENTS.md b/AGENTS.md index 50a3bd579..3025ebffe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -395,6 +395,33 @@ After completing `upload`, summarize the current branch and worktree state, whet + + +## Open PR branch sync (anti-churn) + +Open PR heads go stale whenever `main` advances. GitHub frequently labels those +branches `CONFLICTING` / `DIRTY` even when `git merge-tree` is clean — that is +staleness, not an unresolvable content fight, and it blocks squash auto-merge. + +Durable mitigations in this repo: + +- `.github/workflows/pr-branch-sync.yml` runs on every push to `main` (and on + `workflow_dispatch`) and calls GitHub's update-branch API for every open + same-repo PR that is behind `main`. Opt out per PR with labels `hold`, + `do-not-merge`, or `skip-branch-sync`, or a `WIP` / `do not merge` title. +- Local/operator dry-run: `npm run sync:pr-branches`. Apply: `npm run sync:pr-branches:apply`. +- Prefer fewer long-lived open PRs; land or close queue items rather than + repeatedly re-merging `main` by hand. +- `docs/branch-review-ledger.md` stays append-only with the `union` merge driver; + do not rewrite existing rows during syncs. + +When diagnosing "merge conflicts on every PR", first compare `behind_by` and +`git merge-tree --write-tree origin/main `. If the tree merge is clean, +sync the branch (workflow, `update-branch`, or `git merge origin/main` + push) +instead of rewriting product code. + + + ## Run PR shortcut When the user types exactly `Run PR` (case-insensitive, entire task message after trimming surrounding whitespace), treat it as a shortcut for a one-shot open-PR maintenance sweep on `bigsimmo/database`. This is a chat shortcut, not an app feature, script, automation, or CI workflow. @@ -418,6 +445,7 @@ Hard guardrails (never, even during a sweep): - Respect the `skip-codex-review` label as a full per-PR opt-out. - Preserve unrelated staged, unstaged, and untracked work; never commit secrets. - Resolve branch drift with `git merge origin/main` only; skip and report non-trivial conflicts instead of guessing. +- Before treating GitHub `DIRTY`/`CONFLICTING` as a real conflict, confirm with `git merge-tree` (see "## Open PR branch sync (anti-churn)"). Prefer the hosted `pr-branch-sync` workflow / update-branch API when the token can write; otherwise merge `origin/main` in a worktree and push. Procedure: in Claude Code sessions, invoke the `run-pr` skill (`.claude/skills/run-pr/SKILL.md`) — it is the canonical detailed procedure. In sessions without GitHub MCP write tooling, degrade to read-only diagnosis and a per-PR report; do not attempt pushes or thread resolution through other means. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index d33df56f7..4dba35a17 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -765,3 +765,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-25 | codex/fix-merge-conflicts-and-ci-on-open-prs (PR #1170) | e979fc892f11a17b1a8f2ef1ab058ef40d629182 | Open-PR maintenance: CI fix + threads + drift | Before: Static PR checks failed because route-group moves made nine valid legacy `src/app/*` documentation references appear missing; 0 unresolved threads; branch already contained current main. After: docs-link resolution checks known App Router route groups and the focused failure is fixed. | `node scripts/check-docs-links.mjs` pass (1162 references); Prettier check pass; `git diff --check` pass; no provider-backed checks run. | | 2026-07-25 | codex/document-clinical-summary-20260725 (PR #1169) | 605a47b551a03774fab41416bf980dfbc9610221 | Open-PR maintenance: malformed persisted profile guard | Before: one actionable thread showed non-array or malformed persisted summary groups could throw during render. After: every priority group is normalized through an array/item guard and malformed values are ignored while valid items still render. | Focused Vitest 7/7 pass; Prettier and diff checks pass; no provider-backed checks run. | | 2026-07-25 | cursor/search-performance-review-4ee9 (PR #1134) | 692834a86e612cc8b311dc6895e007f182f5c5b8 | Open-PR maintenance: superseded docs-link thread and clean main sync | Before: branch was behind current main with one outdated docs-link thread; its product tree already matched main. After: merged current main cleanly and verified the route-group-aware docs-link fix now covers legacy route references. RAG impact: no retrieval behaviour change — history sync and docs tooling verification only. | `node scripts/check-docs-links.mjs` pass (1154 references); clean merge-tree; no live RAG canary or provider-backed check run. | +| 2026-07-24 | cursor/pr-queue-hygiene-72ec | pending-push | PR queue hygiene | Add pr-branch-sync workflow + sync:pr-branches helper; bump postcss to clear npm audit high; document anti-churn guidance in AGENTS/process-hardening/pr-babysit/run-pr. | check:github-actions PASS; docs:check-scripts/index PASS; vitest sync-open-pr-branches 3/3; npm audit high clean; no provider-backed checks run | diff --git a/docs/codebase-index.md b/docs/codebase-index.md index d2b9b597f..062074337 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -314,19 +314,21 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement: ## Key config files -| File | Role | -| ----------------------------------- | ------------------------------------------- | -| `package.json` | Scripts, deps, Node 24 / npm 11 | -| `.env.example` | Full env template | -| `next.config.ts` | CSP, security headers, build config | -| `tsconfig.json` | Strict TS; excludes `supabase/functions/**` | -| `eslint.config.mjs` | Lint scope | -| `AGENTS.md` | Agent rules, verification gates, shortcuts | -| `.github/workflows/ci.yml` | CI pipeline | -| `docs/process-hardening.md` | Verification pyramid | -| `docs/clinical-governance.md` | Clinical safety governance | -| `docs/reindex-runbook.md` | Reindex operations | -| `docs/retrieval-quality-runbook.md` | Retrieval tuning | +| File | Role | +| -------------------------------------- | ---------------------------------------------- | +| `package.json` | Scripts, deps, Node 24 / npm 11 | +| `.env.example` | Full env template | +| `next.config.ts` | CSP, security headers, build config | +| `tsconfig.json` | Strict TS; excludes `supabase/functions/**` | +| `eslint.config.mjs` | Lint scope | +| `AGENTS.md` | Agent rules, verification gates, shortcuts | +| `.github/workflows/ci.yml` | CI pipeline | +| `.github/workflows/pr-branch-sync.yml` | Auto-update open PR branches when `main` moves | +| `scripts/sync-open-pr-branches.mjs` | Local dry-run/apply helper for PR branch sync | +| `docs/process-hardening.md` | Verification pyramid | +| `docs/clinical-governance.md` | Clinical safety governance | +| `docs/reindex-runbook.md` | Reindex operations | +| `docs/retrieval-quality-runbook.md` | Retrieval tuning | --- diff --git a/docs/process-hardening.md b/docs/process-hardening.md index f65a224b8..2466a10d4 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -40,6 +40,20 @@ gates. Run the standalone manual/nightly workflow and attach a recent green evid artifact before release; see [`docs/staging-tenancy-release-evidence.md`](staging-tenancy-release-evidence.md). +## Open PR branch sync (active) + +- **Problem:** landing one PR advances `main` and leaves the rest of a large open + queue behind. GitHub then marks many heads `CONFLICTING`/`DIRTY` even when the + merge tree is clean, which stalls squash auto-merge and creates endless manual + re-sync churn. +- **Mitigation:** `.github/workflows/pr-branch-sync.yml` updates open same-repo PR + branches from `main` after every push to `main`. Local dry-run/apply helpers: + `npm run sync:pr-branches` / `npm run sync:pr-branches:apply`. Opt out with + `hold`, `do-not-merge`, or `skip-branch-sync`. +- **Operator rule:** prefer clearing the open queue (merge or close) over keeping + dozens of long-lived feature branches that all touch shared docs like the + branch-review ledger. + ## Phase 1 - Active now - `npm run verify:cheap` is the default broad local gate for source/config/test changes: `check:runtime`, `sitemap:check`, lint, typecheck, and unit tests. diff --git a/package-lock.json b/package-lock.json index 0db2b6389..e0beaad4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "pdf-parse": "^2.4.5", "pdfjs-dist": "^6.1.200", "pdfkit": "^0.19.1", - "postcss": "^8.5.15", + "postcss": "^8.5.18", "react": "19.2.7", "react-dom": "19.2.7", "server-only": "^0.0.1", @@ -9155,9 +9155,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -10117,9 +10117,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -10136,7 +10136,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index 4b74888f2..ce7229033 100644 --- a/package.json +++ b/package.json @@ -182,7 +182,9 @@ "check:skills": "node scripts/list-database-skills.mjs --check", "check:drift": "node scripts/run-tsx.mjs scripts/check-drift.ts", "check:migration-history": "node scripts/run-tsx.mjs scripts/check-migration-history-alignment.ts", - "drift:manifest": "node scripts/run-tsx.mjs scripts/generate-drift-manifest.ts" + "drift:manifest": "node scripts/run-tsx.mjs scripts/generate-drift-manifest.ts", + "sync:pr-branches": "node scripts/sync-open-pr-branches.mjs", + "sync:pr-branches:apply": "node scripts/sync-open-pr-branches.mjs --apply" }, "dependencies": { "@next/env": "16.2.11", @@ -197,14 +199,14 @@ "pdf-parse": "^2.4.5", "pdfjs-dist": "^6.1.200", "pdfkit": "^0.19.1", - "postcss": "^8.5.15", + "postcss": "^8.5.18", "react": "19.2.7", "react-dom": "19.2.7", "server-only": "^0.0.1", "zod": "^4.4.3" }, "overrides": { - "postcss": "^8.5.15", + "postcss": "^8.5.18", "esbuild": "0.28.1", "sharp": "0.35.3", "tmp": "^0.2.7", diff --git a/scripts/sync-open-pr-branches.mjs b/scripts/sync-open-pr-branches.mjs new file mode 100644 index 000000000..c0220f772 --- /dev/null +++ b/scripts/sync-open-pr-branches.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node +/** + * sync-open-pr-branches — local/operator helper that mirrors + * `.github/workflows/pr-branch-sync.yml`. + * + * Default is dry-run (report only). Pass `--apply` to call GitHub's + * update-branch API for every open same-repo PR that is behind `main`. + * + * Requires `gh` auth with pull-requests:write (or a token in GITHUB_TOKEN / + * GH_TOKEN). Never rebases, never merges into main, never force-pushes. + * + * Run: + * node scripts/sync-open-pr-branches.mjs + * node scripts/sync-open-pr-branches.mjs --apply + * npm run sync:pr-branches + * npm run sync:pr-branches -- --apply + */ +import { spawnSync } from "node:child_process"; +import { pathToFileURL } from "node:url"; + +const APPLY = process.argv.includes("--apply"); +const BASE = "main"; +const SKIP_LABELS = new Set(["hold", "do-not-merge", "skip-branch-sync"]); + +function ghJson(args) { + const result = spawnSync("gh", args, { encoding: "utf8" }); + if (result.status !== 0) { + throw new Error((result.stderr || result.stdout || `gh ${args.join(" ")} failed`).trim()); + } + return JSON.parse(result.stdout || "null"); +} + +function shouldSkip(pr) { + const labels = new Set((pr.labels || []).map((l) => String(l.name || "").toLowerCase())); + for (const label of SKIP_LABELS) { + if (labels.has(label)) return `label:${label}`; + } + if (/\b(WIP|do not merge)\b/i.test(String(pr.title || ""))) return "wip-title"; + if (pr.isCrossRepository) return "fork-head"; + return null; +} + +export function classifyPr(pr, behindBy) { + const skip = shouldSkip(pr); + if (skip) return { action: "skip", reason: skip }; + if ((behindBy ?? 0) <= 0) return { action: "skip", reason: "already-current" }; + return { action: "update", reason: `behind=${behindBy}` }; +} + +function main() { + const repo = ghJson(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]); + const prs = ghJson([ + "pr", + "list", + "--repo", + repo, + "--state", + "open", + "--base", + BASE, + "--limit", + "100", + "--json", + "number,title,headRefName,headRefOid,labels,isCrossRepository,isDraft,url", + ]); + + const plan = []; + for (const pr of prs) { + const cmp = ghJson(["api", `repos/${repo}/compare/${BASE}...${pr.headRefName}`]); + const behindBy = cmp.behind_by ?? 0; + const decision = classifyPr(pr, behindBy); + plan.push({ pr, behindBy, ...decision }); + } + + console.log(`Open PRs against ${BASE}: ${plan.length} (${APPLY ? "APPLY" : "dry-run"})`); + for (const row of plan) { + console.log( + `#${row.pr.number} draft=${row.pr.isDraft} behind=${row.behindBy} -> ${row.action} (${row.reason}) ${row.pr.headRefName}`, + ); + } + + if (!APPLY) { + console.log("\nDry-run only. Re-run with --apply to update behind branches."); + return; + } + + let updated = 0; + let failed = 0; + for (const row of plan) { + if (row.action !== "update") continue; + const result = spawnSync( + "gh", + [ + "api", + "-X", + "PUT", + `repos/${repo}/pulls/${row.pr.number}/update-branch`, + "-f", + `expected_head_sha=${row.pr.headRefOid}`, + ], + { encoding: "utf8" }, + ); + if (result.status === 0) { + updated += 1; + console.log(`UPDATED #${row.pr.number}`); + } else { + failed += 1; + console.error(`FAILED #${row.pr.number}: ${(result.stderr || result.stdout || "").trim()}`); + } + } + console.log(`Done. updated=${updated} failed=${failed}`); + if (failed > 0) process.exitCode = 1; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + } +} diff --git a/tests/sync-open-pr-branches.test.ts b/tests/sync-open-pr-branches.test.ts new file mode 100644 index 000000000..2700d73aa --- /dev/null +++ b/tests/sync-open-pr-branches.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { classifyPr } from "../scripts/sync-open-pr-branches.mjs"; + +describe("sync-open-pr-branches classifyPr", () => { + it("skips hold / do-not-merge / skip-branch-sync labels", () => { + expect(classifyPr({ title: "x", labels: [{ name: "hold" }] }, 3)).toEqual({ + action: "skip", + reason: "label:hold", + }); + expect(classifyPr({ title: "x", labels: [{ name: "skip-branch-sync" }] }, 3)).toEqual({ + action: "skip", + reason: "label:skip-branch-sync", + }); + }); + + it("skips WIP titles and fork heads", () => { + expect(classifyPr({ title: "WIP: not yet", labels: [] }, 2)).toEqual({ + action: "skip", + reason: "wip-title", + }); + expect(classifyPr({ title: "ready", labels: [], isCrossRepository: true }, 2)).toEqual({ + action: "skip", + reason: "fork-head", + }); + }); + + it("skips already-current branches and updates behind ones", () => { + expect(classifyPr({ title: "ready", labels: [] }, 0)).toEqual({ + action: "skip", + reason: "already-current", + }); + expect(classifyPr({ title: "ready", labels: [] }, 12)).toEqual({ + action: "update", + reason: "behind=12", + }); + }); +}); From 41d55824faea51d834ecc26aeb37443e49740566 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 19:34:45 +0000 Subject: [PATCH 16/16] fix(ledger): drop duplicate rows blocking #1124 Static PR Exact duplicate review records from union merges failed check:branch-review-ledger. --- docs/branch-review-ledger.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index fdb32566e..832a57445 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -779,10 +779,9 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-25 | codex/fix-merge-conflicts-and-ci-on-open-prs (PR #1170) | e979fc892f11a17b1a8f2ef1ab058ef40d629182 | Open-PR maintenance: CI fix + threads + drift | Before: Static PR checks failed because route-group moves made nine valid legacy `src/app/*` documentation references appear missing; 0 unresolved threads; branch already contained current main. After: docs-link resolution checks known App Router route groups and the focused failure is fixed. | `node scripts/check-docs-links.mjs` pass (1162 references); Prettier check pass; `git diff --check` pass; no provider-backed checks run. | | 2026-07-24 | open-PR conflict sweep (13 clean + 5 conflicted) | multi-head | Conflict fix sweep | Merged origin/main into #1124 #1140-1142 #1146-1148 #1153 #1156-1158 #1167 #1169 #1171 #1172 (clean). Resolved conflicts on #1131 #1134 #1153 #1167 #1162. All 18 open PRs mergeable after sweep. | merge-tree classify + per-PR merge; focused private-access tests on #1162; Bugbot on #1162; no provider-backed checks | | 2026-07-24 | execute-audit-code-remediation (PR #1162) | 3cb7c977 | Conflict fix + Bugbot + local review | Before: CONFLICTING (21 files). After: mergeable. Restored atomic upload RPC; aligned private-access tests (133/133). Bugbot 2 medium left open. | private-access-routes 133/133; no provider-backed checks | -| 2026-07-25 | cursor/pr-babysit-bugbot-agents-6c52 (PR #1167) | ee44812aae9dad1973d8302eba5bfca5000dffb6 | Open-PR maintenance: review-thread fixes | Before: 8 unresolved Codex/CodeRabbit threads; branch current with main. After: target-head pinning, fresh-main verification, exact `cursor[bot]` identity checks, explicit mutation/provider authorization, direct reply-then-resolve semantics, and no-op ledger bookkeeping are documented. | Prettier check on both agent files pass; `git diff --check` pass; GitHub author probe confirmed `cursor[bot]` account type `Bot`; no provider-backed checks run. | -| 2026-07-25 | codex/document-clinical-summary-20260725 (PR #1169) | 6bbce2b97477cb4497624abe2a37c74864e872c8 | Open-PR maintenance: review-thread verification | Before: 2 unresolved Codex threads; branch current with main and required CI running. After: both persisted-profile/placeholder-summary fixes confirmed on the exact head and ready for reply-then-resolve; no further code change required. | `node scripts/run-vitest.mjs run tests/document-clinical-summary.test.ts tests/document-clinical-summary.dom.test.tsx --reporter=dot` pass (5/5); `git diff --check` pass; no provider-backed checks run. | | 2026-07-25 | codex/document-clinical-summary-20260725 (PR #1169) | 605a47b551a03774fab41416bf980dfbc9610221 | Open-PR maintenance: malformed persisted profile guard | Before: one actionable thread showed non-array or malformed persisted summary groups could throw during render. After: every priority group is normalized through an array/item guard and malformed values are ignored while valid items still render. | Focused Vitest 7/7 pass; Prettier and diff checks pass; no provider-backed checks run. | | 2026-07-25 | cursor/search-performance-review-4ee9 (PR #1134) | 692834a86e612cc8b311dc6895e007f182f5c5b8 | Open-PR maintenance: superseded docs-link thread and clean main sync | Before: branch was behind current main with one outdated docs-link thread; its product tree already matched main. After: merged current main cleanly and verified the route-group-aware docs-link fix now covers legacy route references. RAG impact: no retrieval behaviour change — history sync and docs tooling verification only. | `node scripts/check-docs-links.mjs` pass (1154 references); clean merge-tree; no live RAG canary or provider-backed check run. | | 2026-07-24 | open-PR conflict sync (20 PRs) | multi-head | Conflict resolution pass | Before: 8 PRs behind/dirty (#1124 #1131 #1162 #1169 #1174-1177). After: merged origin/main into all; all 20 open PRs MERGEABLE behind=0 (BLOCKED only by CI/reviews). | merge origin/main per branch; no provider-backed checks run | | 2026-07-24 | open-PR conflict sync (22 PRs) | multi-head | Conflict resolution pass | Before: all 22 open PRs behind/dirty vs main (several CONFLICTING/DIRTY). After: merged origin/main into every open head; all pushes OK; merge-tree classified 22/22 clean. | merge origin/main per branch; check:branch-review-ledger on #1172; no provider-backed checks run | | 2026-07-24 | cursor/pr-queue-hygiene-72ec | pending-push | PR queue hygiene | Add pr-branch-sync workflow + sync:pr-branches helper; bump postcss to clear npm audit high; document anti-churn guidance in AGENTS/process-hardening/pr-babysit/run-pr. | check:github-actions PASS; docs:check-scripts/index PASS; vitest sync-open-pr-branches 3/3; npm audit high clean; no provider-backed checks run | +| 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | pending | Babysit: ledger dedupe + merge readiness | Before: Static PR failed on exact duplicate ledger rows after main sync. After: removed duplicate rows; squash auto-merge armed. | check:branch-review-ledger PASS; no provider-backed checks run |