Repository navigation
ci: add a report-only merged-branch reaper sweep - #13500
Merged
os-project-manager merged 2 commits intoAug 30, 2026
Merged
Conversation
Adds .github/workflows/merged-branch-reaper.yml: a weekly (plus workflow_dispatch) sweep that classifies every claude/* remote branch by the state of the pull request whose head ref it is, and reports the would-delete list. It deletes nothing. The criterion is PR state MERGED. The is-ancestor probe is not used anywhere, including as a secondary check: this repo squash-merges through a queue that rewrites commits, so a fully merged branch's tip is normally not an ancestor of main. Report-only is enforced by the token, not only by the code -- the job runs with contents: read, which cannot delete a ref. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
The reaper is paths-filtered on itself, and dispatch-gates requires every paths-filtered workflow to either discover a check:* family or declare why it has none. This one genuinely has none: its single step is an API sweep run through actions/github-script, not a named local verification. Uses the marker the gate itself provides rather than an exemption list, per that mechanism's own docblock. The pull_request filter is kept because it is load-bearing: it produced the first real dry-run list (run 33318728567, "111 of 335 ... would be deleted"), which is the human look the ruling requires before deletion is enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
os-project-manager
marked this pull request as ready for review
August 30, 2026 15:44
os-project-manager
enabled auto-merge
August 30, 2026 15:44
This was referenced Aug 30, 2026
os-tesla
pushed a commit
that referenced
this pull request
Oct 25, 2026
…er (objectui#13492) (#13500) <!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D1ujqNbMkVbXgPHgdDMe18r)_ Fixes #13492 Clause-②: no Metadata-protocol: none ## What changed An assistant answer now renders its text and its charts in the order the stream produced them. A chart the model drew before writing its analysis shows first, so an open-ended analysis in the console's assistant panel puts its chart on the first screen (the acceptance objectstack-ai/cloud#3072 sets). Before, `mapMessages` joined every text part into one `content` and lifted every `data-chart` part into one trailing `charts[]`, and `ChatbotEnhanced` rendered the whole text first, so the chart always landed below every paragraph. - **`mapMessages.ts`:** `extractCharts` now records, on each chart, where it arrived among the text parts: `textOffset` = the length of the text that `extractText` joined before it. Tool, step and reasoning parts add nothing. Only when `content` IS that join; a legacy string `content` is not built from the parts, so its charts carry no offset. - **`ChatbotEnhanced.tsx`:** a new optional `ChatChart.textOffset`. `layoutAnswer` splits `content` at the offsets into text runs and chart runs; each text run renders through its own `MessageResponse` (so long-answer folding and record links work in every run), and each chart run through the same chart markup as before, now in `AnswerCharts`. The streaming caret rides the last text run, where it sat before. Charts render in the same tree position whether or not prose has arrived, so a chart drawn before the first word keeps its mounted node when the text streams in. - **Backward-compatible:** `content` and `charts` keep their shape. A chart with no `textOffset` (a host-built message, the legacy string-content shape) or one past the end of the text trails all of it, which is exactly the old layout; a message with no charts renders the same DOM as before. `textOffset` is a placement key and never reaches the chart schema handed to `SchemaRenderer`. ## The card's acceptance, item by item | Acceptance | Pinned by | |---|---| | parts `data-chart`, `text`: the chart renders before the text | `data-chart then text: the chart renders before the analysis` | | parts `text`, `data-chart`, `text`: the three in that order | `text, data-chart, text: the three render in that order` | | a re-hydrated stored message keeps the order | `useObjectChat.chartOrderRehydrate-13492.test.tsx` (API mode, stored message with `parts` through the hook and into the bubble) and `re-mapping a restored message (same parts) yields the same placement` | | long-answer folding (objectui#13032) does not regress | `objectui#13032 does not regress: the long analysis after a chart still folds once the stream ends`, plus the unchanged `chatAnswerLayout-13032.test.tsx` | | record links (objectui#13037) do not regress | `objectui#13037 does not regress: a record link in the text after a chart opens through the host opener`, plus the unchanged `chatRecordLink-13037.test.tsx` | | staging re-test with 「帮我分析一下客户」 | **NOT MEASURED**: staging reaches this through cloud's objectui pin; re-run after the pin moves past this merge | New files: `mapMessages.chartOrder-13492.test.ts` (6 tests), `ChatbotEnhanced.chartOrder-13492.test.tsx` (7, one a CONTROL for the old layout), `useObjectChat.chartOrderRehydrate-13492.test.tsx` (2). `mapMessages.test.ts` gains the `textOffset` in the one test that pins a chart's exact shape. ## Verification (local, head `cb0af96`) All runs from the repo root under the shared verify lock. - `pnpm exec vitest run --maxWorkers=2 packages/plugin-chatbot/`: **Test Files 82 passed (82), Tests 787 passed (787)**. - The 54 test files in `packages/app-shell/src` and `apps/console/src` that import `@object-ui/plugin-chatbot` (resolved to its `src` by the root config's alias): **Test Files 54 passed (54), Tests 582 passed (582)**. - `pnpm turbo run type-check --filter=@object-ui/plugin-chatbot` (builds the dependency closure, then `tsc --noEmit && tsc -p tsconfig.test.json`): exit 0 after one fix to the new re-hydrate test (it now hands the hook's messages through `toRuntimeMessages`, as hosts do). - `eslint --no-inline-config` on the six touched source and test files: 0 errors; the 11 warnings are all on lines this PR does not touch. - `node scripts/check-changeset-presence.mjs` and `node scripts/check-changeset-no-major.mjs`: pass. `pnpm check:new-line-citations`: 0 new citations. `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:test-path-roots`, `check:control-bytes`, `check:ui-root-lookups`, `check:unreferenced-sources`: all exit 0. - **Reverse verification**, both through `ablation-replace.mjs` with the fix committed and restore proven (blob equals HEAD, `git diff HEAD` empty): - renderer: `layoutAnswer` ignores `textOffset` (every chart trails the text) ⇒ 6 of the 9 render and re-hydrate tests turn red; the record-link test, the old-layout CONTROL and the mapper tests stay green, as they should. - mapper: the `textOffset` spread deleted ⇒ 13 tests red across the four files, including the existing exact-shape test; the legacy no-offset test stays green. - NOT MEASURED locally, left to CI: the sharded full `pnpm test`, repo-wide `pnpm lint`, builds and the remaining `check:*` gates. ## Acceptance notes - **No reload path in this repository restores a chart today, so on reload the chart is missing, not misplaced.** The server keeps a conversation as Vercel `ModelMessage`s (`IAIConversationService.addMessage` in `@objectstack/spec`'s `contracts/ai-service.ts`), which have no data parts. On the way back, app-shell's `hydratedMessagesToChatMessages` builds `content` and tool invocations only, and `sanitizeChatMessagesForCache` writes text and tool parts only. Read from code, not measured on staging; outside this card's file surface (`packages/plugin-chatbot/src/**`). A stored message that does carry its `data-chart` parts keeps its order (pinned above). Whether cloud's `visualize_data` result carries enough to re-derive the chart on reload is a cloud-side reading this session cannot reach. - In an answer that interleaves text and charts, each text run folds on its own (a run taller than two viewport heights folds; a short intro above the chart does not). An answer with no chart, or with its charts after all the text, is one run and folds exactly as before. - Docs: neither `packages/plugin-chatbot/README.md` nor `content/docs/plugins/plugin-chatbot.mdx` describes where charts render, so nothing there went stale; `textOffset` is documented on the `ChatChart` type. - [x] Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer) Card thread session: `session_01VWbYRWtBm3f8J2T8HAt97o`, claimed by the `domain:ui` seat 4 (claim comment 6393183623). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VWbYRWtBm3f8J2T8HAt97o --- _Generated by [Claude Code](https://claude.ai/code/session_01VWbYRWtBm3f8J2T8HAt97o)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #12771
Adds
.github/workflows/merged-branch-reaper.yml— a report-only sweep that classifies everyclaude/*remote branch by the state of the PR whose head ref it is, and prints the would-delete list. It deletes nothing.Part of, notFixes, on purpose: this delivers the dry-run half the ruling asked for. Enabling deletion is a separate, reviewed diff and the maintainer's call, and the measurement below raises a question the card should stay open for.The two fences
is-ancestorprobe appears nowhere — not as the criterion, not as a secondary check. The only mentions of the phrase in the diff are prose forbidding it. Verified:grep -n "is-ancestor\|merge-base"returns 5 hits, all comments or the rendered report caption; reverse-checked with a term known present (merged_at, 10 hits).permissions: contents: read, which is structurally incapable of deleting a ref. A defect in the classification still cannot delete a branch.Measurements — these changed the design
All figures measured 2026-08-30 against the live API, not relayed.
The population, re-derived: 335
claude/*branchesAlso measured, and worth a separate look:
copilot/carries 678 branches — twice theclaude/accumulation. Out of scope here (the ruling namesclaude/*), reported so it is not mistaken for covered.⭐
delete_branch_on_mergeis already enabled — so the event-driven shape is the wrong onedelete_branch_on_mergeclaude/*PRs merged 2026-08-20T13:11Z .. 2026-08-30An
on: pull_requestreaper would fire ~140x/day to find nothing 99.93% of the time, racing the platform setting on the same ref. So this is a weekly scheduled sweep +workflow_dispatchinstead — one mechanism that covers both the legacy backlog and the rare leak. The 111 merged survivors are a one-time debt, not a flow: 62 merged in 2026-04, 33 in 2026-06, 8 in 2026-08 (newest 2026-08-20).⭐ H3 confirmed, and it dominates: half the population has no PR at all
The no-PR bucket is not a stale tail, it is the live growth: 145 of the 170 carry a tip commit dated 2026-08, 123 within the trailing 14 days. Branches pushed by a session that died or never opened a PR.
⇒ This workflow is correct as ruled and clears a real 111-branch debt, but it reaches ~a third of the population and close to none of the ongoing accumulation. Widening the criterion to cover abandoned no-PR branches is a new ruling (what proves such a branch is abandoned rather than in flight?), deliberately not taken here.
Option B — granting seats delete rights — is not touched here; it stays with the #12728 credential pass.
Proving the filter, not just running it
Every bucket above is a demonstrated exclusion on real data, and the report renders each with named examples:
merged_atkeeps the sweep from racingdelete_branch_on_merge. On today's data this bucket is 0 (newest survivor merged 2026-08-20), so the first real run should show 111 would-delete / 0 held.One trap found and guarded in the code:
commits/{sha}/pullslooks like it answers "does this branch have a PR" and does not — it returns PRs that merely contain the commit. Measured, it claimed a PR for 4 of 5 branches that had none of their own. The workflow therefore filters onpr.head.ref === branch.namerather than trusting the endpoint shape.The
pull_requesttrigger on this file means this PR's own CI run produces the first real would-delete list on a real runner — the human look the ruling asks for, rather than a claim about one.Verification
Gate union re-run at final commit
c014a236— 17 derived families, all exit 0, exit codes captured before any pipe:Gate list derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, not recalled. The workflow-scanning gates report 29 workflow files where 28 existed before, which is how I know they actually read the new file rather than passing vacuously.github-scriptbody extracted andnode --checkclean.check-test-completeness.mjsexits 3 / PREREQUISITE NOT MET by design — recorded NOT MEASURED, neither red nor green.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'clean..github/workflows/file and publishes nothing from any package, soskip-changesetapplies.Patch round —
Lint & Repo Gatesred atc014a236, green ate1c856b7dispatch-gates' own self-test refused this file:Reproduced locally at
c014a236(exit 1, same assertion, line 636) before changing anything.The assertion names two legitimate exits: discover a
check:*family, or declare why there is none. This workflow genuinely has none — its single step is an API sweep throughactions/github-script, not a named local verification — so it now carries the marker the gate itself provides:That is the mechanism's intended use, not a way of making the gate stop asking: its own docblock argues against a hardcoded exemption list in the script and for a marker the workflow carries, "read fresh every run". No assertion was weakened, skipped or special-cased.
The
pull_requestfilter was kept, and that was measured rather than assumed. The alternative fix — drop the paths filter so the gate skips the file — would have been smaller but wrong: that trigger is what produced this workflow's first real dry-run list, on a real runner, which is the one human look the ruling requires. Run33318728567completed success with the annotation:That is the deliverable, generated by the workflow itself rather than predicted: 111 would-delete matches the local classification exactly, and the no-PR share (168 live vs 170 measured ~40 min earlier) drifts because this repo merges ~140 PRs/day and branches acquire PRs continuously.
After the fix, at
e1c856b7:Both legs run: the self-test leg (
pnpm check:pm-dispatch-gates, exit 0) and the work leg (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, exit 0, tree clean). Derived family union re-run ate1c856b7— all 17 exit 0.check:pm-dispatch-gatesis not in the family list thatdispatch-gatesderives for a.github/workflows/**surface, even though its live assertion reads every workflow file in the tree. That is exactly why the local run passed and CI did not. Reported rather than patched — it is outside this PR's declared surface.Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Generated by Claude Code