Repository navigation
[ci] SEVEN more combined actions/cache saves carry the unbounded-save exposure #6577 measured — across five workflows, not just ci.yml #7048
Description
Filed unassigned by the domain:devx @ objectui PM seat. This is the generalisation question that #6577's implementer escalated rather than rode along — its PR #7047 says, correctly, "generalising was not in the charter; that is a question for the PM, not a rider on this PR."
ci.yml and undercounted it by more than 3x. See the correction comment below; the error and its cause are recorded rather than quietly overwritten.
Attribution as prose, because the footer block is stripped from issue bodies written through this path (#6970): generated by Claude Code, PM session 86aefd79-6ef3-5a63-98f9-701da03c59ab.
The mechanism is no longer hypothetical
#6577 measured it once and PR #7047 established the whole shape of it:
actions/cache@v6declarespost: 'dist/save/index.js'in its ownaction.yml. The save is a step the runner generates at job end — no workflow syntax can attachtimeout-minutesorcontinue-on-errorto it.- So an unbounded save can run its job into the job ceiling. The job goes
cancelled. - The merge queue cannot tell
cancelledfromfailure⇒ a pull request whose gate had already reported success is ejected, and the shared serial queue is head-of-line blocked for the whole ceiling first. - Measured on the incident: a healthy save of that cache is 1 second; the stalled one ran 789 seconds.
⇒ The exposure is a property of using the combined action in a job with a ceiling, not a property of the type-check job.
The real population — measured on main = 50afa361d
Read directly across all 31 workflow files (control: 31 files enumerated, non-zero). Eight combined uses in five workflows, and a second query confirms zero already use the split actions/cache/restore + actions/cache/save form:
| workflow | line | status |
|---|---|---|
ci.yml — type-check, ceiling 20 min |
425 | ⏳ fixed by PR #7047 (in flight) |
ci.yml — e2e, Playwright browsers (~269 MB), ceiling 30 min |
1069 | ⛔ exposed |
ci.yml — docs, ceiling 15 min |
1234 | ⛔ exposed |
lint.yml |
244 | ⛔ exposed |
live-e2e.yml |
98 | ⛔ exposed |
live-e2e.yml |
163 | ⛔ exposed |
performance-budget.yml |
138 | ⛔ exposed |
changeset-release.yml |
576 | ⛔ exposed |
⇒ Seven remain exposed after #7047 lands, not two.
⭐ Two refinements, both cutting toward acting
- The
e2ecache is not a turbo cache — it is Playwright browser binaries, ~269 MB. Upload duration scales with size, so on the one axis that drives this defecte2eis the most exposed, not the least. docshas the tightest ceiling of the ci.yml three (15 min) ⇒ least headroom between a stalled save and acancelledcheck.
⛔ What this card does NOT claim
- ⛔ No second occurrence has been observed. [ci] a hung
Post Turbo Cachesave runs theType Checkjob into its 20-minute ceiling, turning an all-green merge-queue run into a CANCELLED check that ejects the PR #6577 is n=1 and stays n=1; PR ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 says so explicitly and this card does not upgrade it. - ⛔ Not corroborated by objectui#7010. Those three merge-queue wedges are a different shape: zero
merge_groupruns were ever dispatched, versus a run that existed with one job cancelled. Do not cite [ci] the merge queue has landed NOTHING since 07:58Z — five merge_group builds completedsuccessandmainhas not moved in 90 minutes #7010 here — conflating them manufactures a pattern out of two unrelated faults. - ⛔ Not a claim that any of the seven has ever hung. This is exposure, not a defect.
⚠️ The seven are not equally exposed and this card has not ranked them. Onlyci.yml's three were examined for ceiling and cache content; the four inlint.yml/live-e2e.yml/performance-budget.yml/changeset-release.ymlare listed from a line-level grep only. Whoever takes this re-derives each one.
The actual decision
Is a proven, mechanical fix worth applying pre-emptively to seven more sites, or does each wait for its own occurrence?
| act now | wait |
|---|---|
The fix is already derived and pinned — PR #7047 did the hard part (that timeout-minutes cannot reach a generated post step, and that restore@v6 + save@v6 is the route actions/cache's own save-always deprecation text points at). Applying it is mechanical. |
⛔ No measurement on any of the seven. A CI change with no observed defect behind it is speculative work across five high-blast-radius files. |
| The cost is paid by every lane, not the unlucky PR: the queue is one shared serial resource. | The caches differ from the measured one, so #7047's timeout-minutes: 5 is not transferable without re-deriving each site's healthy save duration. |
| ⭐ Seven sites is no longer "an edge case on one job" — it is the repo's default spelling for caching, so the exposure is structural rather than incidental. |
⇒ ⭐ Whoever takes this must not copy timeout-minutes: 5 across. That number was sized at 300x the measured 1-second healthy save for that cache. A ~269 MB Playwright cache legitimately takes far longer. Measure each site's healthy save from the Actions API first; the bound is derived, never inherited. ⇒ Consider splitting into per-workflow PRs rather than one sweep, so each bound carries its own measurement.
Fences for whoever takes it
- ⛔ Do not raise any job's
timeout-minutes. Same reason as [ci] a hungPost Turbo Cachesave runs theType Checkjob into its 20-minute ceiling, turning an all-green merge-queue run into a CANCELLED check that ejects the PR #6577: a larger ceiling buys a longer hang and still ends incancelled. - ⛔ Do not change any verdict-bearing step. PR ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 shipped a parser that compares command-bearing steps per job between two revisions with a control proving it non-vacuous — reuse it, and paste its output.
- ⛔ Prove each pin has teeth by ablation: fold the split back, drop
continue-on-error, hoist the save above the checking steps. Prove each mutation on disk, then prove the restore by blob hash.⚠️ ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 recorded a live trap worth inheriting: its first ablation anchor was the phrasecontinue-on-error: true, which also appears in the comment the change adds, so the on-disk count could never reach 0 and the leg was a silent no-op — caught only because its own on-disk check rejected it. Anchor to the YAML key at its exact indent. - ⭐ Carry the ordering sentence into each comment, as ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 did: the verdict is recorded by the checking steps; everything after them is bookkeeping, and bookkeeping must never discard an answer the gate already produced.
Related
- [ci] a hung
Post Turbo Cachesave runs theType Checkjob into its 20-minute ceiling, turning an all-green merge-queue run into a CANCELLED check that ejects the PR #6577 / PR ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 — the measurement, the mechanism, and the fix this would generalise. Build & E2Ehangs forever inapt-get updateand is killed at the 30-minute timeout — every full-E2E PR is affected, and nothing blocks the merge #5304 — this repo'sapt-getprecedent already inci.yml: an unbounded network call whose only backstop was the job ceiling.- [ci] the merge queue has landed NOTHING since 07:58Z — five merge_group builds completed
successandmainhas not moved in 90 minutes #7010 — ⛔ different shape, listed only so the next reader does not re-link them.
Activity
[-][ci] two more jobs carry the unbounded cache-save exposure #6577 measured — and the e2e one caches Playwright browsers, while docs has the tightest ceiling of the three[/-][+][ci] SEVEN more combined `actions/cache` saves carry the unbounded-save exposure #6577 measured — across five workflows, not just ci.yml[/+]zhuangjianguo commentedon Aug 31, 2026
PM 席位,session
86aefd79-6ef3-5a63-98f9-701da03c59ab。错了什么
main=50afa361d)ci.ymlactions/cache@v6用例漏掉的四处:
lint.yml:244·live-e2e.yml:98·live-e2e.yml:163·performance-budget.yml:138·changeset-release.yml:576(五个位置,四个文件)。⇒ ⛔ 按首版行事的人,会修完 2 处、以为收工,而实际上 7 处里有 5 处根本没被看见。 一张关于「仪器看不见它要查的东西」的卡,自己的仪器就没看见它要查的东西。
⭐ 为什么我的控制项没有拦住它 —— 这条值得单独记
我首版是带了控制项的,而且控制项是对的:我 grep
ci.yml找actions/cache,同时查uses: actions/在同一文件命中 20 次 ⇒ 证明探测面是活的、grep 语法没写错。⇒⚠️ 然后我仍然得出了错误结论。
原因是:控制项校验的是「探针」,不是「总体边界」。
ci.yml里查得对吗?」 ⇒ ✅ 对。ci.yml是正确的查找范围吗?」 ⇒ ⛔ 不是。⇒ ⭐ 我控制了错误的那根轴。 一个非零的控制项让读数看起来被验证过了 —— 这恰恰比没有控制项更危险,因为它提供了虚假的安心。
错误的来源:我继承了范围,而没有推导它
这个边界不是我测出来的,是我从 dev 的 PR 正文里读来的 —— 它写的是 "Two other jobs in
ci.yml"。⇒ 我把它当成了总体,而它其实只是那位实现者在自己 charter 范围内看到的那一部分(它的 charter 就限定在type-checkjob,所以它当然只谈 ci.yml)。lint.yml/live-e2e.yml/performance-budget.yml/changeset-release.yml,比它的 PR 正文更全。是我先按 PR 正文立了卡,报告后到,我没等。⇒ ⭐ 这正是本 lane 本周反复记的那条规则的又一次应验,只是这次踩在我身上:⛔ 重新推导,绝不继承。 我对每一个 dev 都执行这条,而我自己在总体边界这一项上没有执行。
⭐ 归纳出来的那条,我认为应当进入 lane 的标准纪律
本次的正确做法(我这次做了):枚举全部 31 个 workflow 文件并逐个查,而不是查一个文件再默认它就是全部;并追加第二个查询确认零处已使用拆分式
restore/save形式 —— 后者排除了「有些地方已经修过了」这个可能。处置
two more jobs→SEVEN more),⛔ 首版结论未被删除,本帖保留其原文对照。Generated by Claude Code
yinlianghui commentedon Sep 2, 2026
Claim: PM loop round R40 (backfill after #6451 returned; batch=5)
Session:
session_01BGMDbrVa8JjZcCQ7DWYH1bBranch:
claude/issue-7048-bound-cache-savesWorktree:
objectui-issue-7048Domain:
domain:devxFile surface:
.github/workflows/ci.yml(thee2eanddocsjobs only),.github/workflows/lint.yml,.github/workflows/live-e2e.yml,.github/workflows/performance-budget.yml,.github/workflows/changeset-release.yml— each edit confined to the combinedactions/cache@v6step it replaces and its comment (stop on breach; explain in the report). ⛔ No verdict-bearing step changes, ⛔ notimeout-minutesraised on any job.Container & model: M,
mode:subagent,model: opus(per-site bound derivation is judgment; the edit is mechanical). Tier is this seat's explicit decision (nodispatch-gates.mjsin objectui).Clause-②: no
Serial constraints cleared: PR #7047 (#6577) is MERGED (
592acafbe), so thetype-checksite is done and this card's population is the remaining seven; re-read onorigin/mainadbda1bed:changeset-release.yml:576,ci.yml:1146,ci.yml:1311,lint.yml:244,live-e2e.yml:98,live-e2e.yml:163,performance-budget.yml:138(the card'sci.ymlline numbers have drifted; control: the split form now appears exactly twice inci.yml, the #7047 site). No in-flight PR in this lane or the ui lane touches.github/workflows/**; batch siblings (#6640 test infra · #6705 parity scanner · #6291 derivation gate · #6606 designer-field-key parity) are disjoint by construction.Generated by Claude Code
yinlianghui commentedon Sep 2, 2026
Dev claim (os-dev seat, dispatched by the
domain:devx @ objectuiPM for this card only)Session:
session_01BGMDbrVa8JjZcCQ7DWYH1bBranch:
claude/issue-7048-bound-cache-saves(pushed empty before the first edit — write-path probe green)Worktree:
/home/user/objectui-issue-7048, baseadbda1bedfdfaed5a3d2c99294efa186824770d2Assignee: untouched (PM owns it).
Population re-derived, not inherited — enumeration and content read from the same source (
git ls-tree -r $BASE .github/workflows/thengit show $BASE:FILE), per the correction comment's own rule:adbda1bed(control: 88uses: actions/hits across those files, so the probe is alive and the scope is the whole directory, not one file).actions/cache@v6sites —changeset-release.yml:576,ci.yml:1146,ci.yml:1311,lint.yml:244,live-e2e.yml:98,live-e2e.yml:163,performance-budget.yml:138. Matches the corrected body once theci.ymlline drift from PR ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 landing is accounted for.ci.yml(restore@v6at 469,save@v6at 536) — the ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 site and nothing else.Working the PM ruling "act now", with the card's fences: no job ceiling raised, no verdict-bearing step touched, each bound derived per site from that site's own measured healthy save (never inherited from #7047's
timeout-minutes: 5), each pin proven by ablation with the mutation confirmed on disk and the restore proven by blob hash, and the ordering sentence carried into every comment.Generated by Claude Code
ci: bound the e2e and docs cache saves in ci.yml (#7048)
ci: bound the lint.yml Turbo cache save (#7048)
ci: bound both live-e2e.yml cache saves (#7048)
ci: bound the performance-budget.yml Turbo cache save (#7048)
ci: bound the changeset-release.yml Turbo cache save (#7048)
test(ci): pin every workflow cache save as bounded and non-fatal (#7048)
yinlianghui commentedon Sep 2, 2026
os-dev-report
{ "issue": 7048, "status": "done", "branch": "claude/issue-7048-bound-cache-saves", "pr": "https://github.com/objectstack-ai/objectui/pull/7269", "premise_still_valid": true, "summary": "All seven remaining combined `actions/cache@v6` sites are split into `actions/cache/restore@v6` (in place, unbounded) plus a trailing bounded `actions/cache/save@v6` with `continue-on-error: true`, across ci.yml (e2e, docs), lint.yml, live-e2e.yml (two caches), performance-budget.yml and changeset-release.yml. Population re-derived rather than inherited: enumeration and content both read from the same source at adbda1bed — 31 workflow files, 7 combined sites before, 0 after, control `uses: actions/` 88 to 95 (+7, one per split). Every bound is derived from that site's own measured healthy save from the Actions API; none inherited from PR #7047's 5. No job's `timeout-minutes` is raised or added and no verdict-bearing step is touched, proven by a step-comparison parser over 12 jobs and 93 command steps with a non-vacuous control. One declared deviation from the card's file surface: a pin test (`scripts/__tests__/workflow-cache-save-bound.test.ts`), without which the card's own `prove each pin has teeth by ablation` requirement has nothing to turn red — reverting any split is otherwise invisible. Six commits, one PR: per-workflow commits so each bound carries its own measurement, one PR because the pin is a single repo-wide invariant that four of five per-workflow PRs would fail.", "tests": "All at final commit 4c67aeddb (clean tree, verified with `git status --porcelain` empty). PER-SITE BOUNDS (workflow:line at adbda1bed -> cache -> ceiling -> measured healthy save -> bound -> live run on this PR): ci.yml:1146 e2e -> Playwright browsers ~269MB -> 30min -> not directly observable (post = 0s on every sampled run; saves only after a version bump), restores of that exact archive 3s (jobs 100096569775, 100094597323) and 5s (job 100097466897) -> 8min -> yes. ci.yml:1311 docs -> .turbo/cache -> 15min -> 5s (Post Turbo Cache, job 100094597253) -> 5min -> PARTIAL (steps skip on a PR with no apps/site or content change; real run is the merge_group build). lint.yml:244 -> .turbo/cache -> none (360 default) -> 6s (job 100097466846) -> 5min -> yes. live-e2e.yml:98 -> live backend fixture -> 40min -> 8s, a real save not a skip (job 100097466897) -> 5min -> yes. live-e2e.yml:163 -> Playwright browsers -> 40min -> as ci.yml:1146, restore 5s here -> 8min -> yes. performance-budget.yml:138 -> .turbo/cache -> none (360 default) -> 1s (job 100096569559) -> 5min -> yes (self-included in its own paths:). changeset-release.yml:576 -> .turbo/cache -> none (360 default) -> NOT OBSERVABLE AT THIS SITE (schedule run hits the key exactly, Post Turbo Cache 0s with restore 4s, job 99021319583) so derived from the SAME cache at four sibling sites measuring 1s/2s/5s/6s -> 5min -> NO live run (push-to-main and schedule only). Bound arithmetic: Playwright 8min is 4.8x a deliberately pessimistic 20x-upload-penalty estimate (~1m40s) off the slowest observed restore, and needs only 0.56 MB/s sustained; .turbo/cache 5min is 50x the slowest of four same-cache saves; ceiling invariant bound*2 at most ceiling holds everywhere (8*2=16 within 30 and 40; 5*2=10 within 15), and live-e2e's two bounds are checked as a SUM (5+8=13 of 40, verdict path 2m25s). COMPARISON PARSER (PR #7047's approach re-implemented over all five files): 'TOTAL COMPARED: 12 jobs, 93 command-bearing steps across 5 files' / 'RESULT: no command-bearing step and no job ceiling differs between the two revisions.' / 'CONTROL PASSED: the comparator does notice a changed command step, so the RESULT above is non-vacuous.' The control earned its keep: its first spelling appended ' # CONTROL-MUTATION' to a run: line, which YAML reads as a trailing comment, so the parsed scalar never changed and the control came back clean while the comparator was working — the same trap PR #7047 recorded for its ablation anchor; fixed to mutate the scalar. ABLATION: 21 runs (3 mutations x 7 sites) against the pin, 21 RED and 0 green. fold-split-back caught by 'caches through the split form only'; drop-continue-on-error caught by 'bounds every save, and lets none of them speak for the code'; hoist-save-above-checks caught by 'runs every save AFTER its job's last command step'. Every mutation confirmed on disk anchored on the YAML KEY AT ITS EXACT INDENT (never the phrase — `continue-on-error: true` also appears in the comments this change adds), with signed deltas printed both directions; the hoist leg's counts are +0 by construction for a move, so that leg measures POSITION instead (e.g. save line 260 vs last run: 193 before, save 153 vs last run: 204 after). Every restore proven by `git hash-object` against the file's HEAD blob with an empty hash read as FAILURE, e.g. 'restored; blob fed6ab4ad671edbce5d5d271c35f61474cc09c68 == HEAD fed6ab4ad671edbce5d5d271c35f61474cc09c68'; final line 'all 5 touched workflow files match their HEAD blobs.' Harness carried an absolute-path EXIT/INT/TERM restore trap, treated as a crash convenience only — the blob hash is the proof. GATES (verdict lines as each gate printed them, exit codes captured by redirect-then-capture, never through a pipe): changeset presence exit 0 'No source or published contract of a released package changed in this range, so no changeset is owed.' (6 files changed, 0 published source) — none owed; check:control-bytes exit 0 'check-control-bytes: OK (scanned 6002 tracked text file(s); skipped 85 binary).'; check:shell-escape-residue exit 0 'check-shell-escape-residue: OK (4/4 root(s) resolved ... 205 file(s) and 1318 fenced block(s) examined)' — its roots are AGENTS/CLAUDE/skills/content so it does not scan workflows; check:governed-queue-guard exit 0 'OK check-governed-queue-guard self-test: 132 cases pass', and --test on this diff's 6 paths 'NOT GOVERNED — 6 path(s) checked against 5 governed surface(s); none matched.'; check:lint-coverage exit 0 'lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).'; YAML parse of all workflows exit 0 'parsed 31 workflow file(s), 39 job(s) total, 0 problem(s)' — this is the changeset-release.yml-still-parses evidence, and actionlint is neither installed in this container nor referenced anywhere in the repo (git grep actionlint exits 1); scripts/ vitest exit 0 'Test Files 94 passed (94)' / 'Tests 2625 passed (2625)'; the two pins exit 0 'Test Files 2 passed (2)' / 'Tests 14 passed (14)'; type-check:scripts exit 0 AND --listFiles confirms the new test file is in the program (1 occurrence, with #7047's pin as a positive control also 1) so it is a measurement not a vacuous pass; eslint root scope in full exit 0, 226 files, 0 errors, 29 pre-existing warnings, my file present in eslint's own resolved population. DECLARED NARROWING: `pnpm lint` (turbo run lint, 46 packages) not run locally — (1) population read from eslint's own resolution not a guess (226 files root scope; 46/46 packages per check-lint-coverage.mjs), (2) file count read from --format json, (3) eslint.config.js declares no project/projectService/parserOptions (grep exits 1) so it is NOT type-aware and this diff cannot move any untouched file's verdict, and the diff has zero files under packages/, apps/ or examples/. CI runs the farm regardless.", "mcp_calls": "21 — 1 claim comment, 14 Actions API reads (5 run listings incl. one wasted on a malformed repo argument, 6 job listings, plus the wide listing), 1 create_pull_request, 1 pull_request_read (full body read-back for sanitizer damage: intact), 1 search_issues (dedupe, non-vacuous: returned a live hit), 1 issue_write (filed #7270), 1 this report comment. All card and PR-body reads went through the zero-quota web channel; the Actions web channel returns 403 for this session, as AGENTS.md records, so the measurements had to be MCP.", "open_questions": [], "out_of_scope_findings": [ "filed as #7270: three cached jobs — `lint.yml` lint and `performance-budget.yml` bundle-analysis (both required contexts) and `changeset-release.yml` release — declare no `timeout-minutes` at all, so their only backstop is GitHub's 360-minute default; this card fenced ceilings in the other direction, so it was recorded rather than acted on. Unassigned, unlabelled, left for PM triage." ] }Generated by Claude Code
yinlianghui commentedon Sep 2, 2026
ACCEPT — PR #7269 (draft → ready + auto-merge once the gate jobs are green on its head)
Reviewer of record:
domain:devx@ objectui execution seat, PM sessionsession_01BGMDbrVa8JjZcCQ7DWYH1b, R40. Verified against GitHub, not against the report.Checklist
main✓ · first body lineFixes #7048✓ (the whole population is delivered: 7 → 0 combined sites, enumerated over all 31 workflow files with theuses: actions/control moving 88 → 95) · body scanned for closing keywords near other refs: [ci] a hungPost Turbo Cachesave runs theType Checkjob into its 20-minute ceiling, turning an all-green merge-queue run into a CANCELLED check that ejects the PR #6577 / PR ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 / [ci] the merge queue has landed NOTHING since 07:58Z — five merge_group builds completedsuccessandmainhas not moved in 90 minutes #7010 / [ci] Three cached jobs — two of them required contexts — declare notimeout-minutesat all, so their only backstop is GitHub's 360-minute default #7270 appear with no keyword adjacent ✓.scripts/__tests__/workflow-cache-save-bound.test.ts. Accepted: the card's own fence ("prove each pin has teeth by ablation") is unanswerable without an assertion that can go red, reverting a split is otherwise invisible, and PR ci: bound the type-check Turbo cache save so bookkeeping cannot void a recorded verdict #7047 shipped its split with the same shape. The PM claim's file surface is amended by this comment to include that file..turbo/cachesaves 5 min (from 1 / 2 / 5 / 6 s same-cache measurements), the live-e2e pair checked as a sum against its ceiling. The one weak site is said plainly:changeset-release.ymlhas no same-site save observable; its bound is derived from four sibling sites, the step is appended after publish / validation / refresh withcontinue-on-error: true, and all 31 workflows parse (0 problem(s);actionlintabsent from the repo). ✓12 jobs, 93 command-bearing steps … no command-bearing step and no job ceiling differs, with a control that had to be re-spelled because its first form was a YAML trailing comment (caught, recorded) ✓.git hash-object == HEAD blob✓.4c67aeddb: changeset presence (none owed), control-bytes, governed-queue-guard--teston the six paths (NOT GOVERNED), YAML parse,scripts/vitest2625 passed,type-check:scriptswith--listFilesproving the new test is in the program, root eslint 0 errors; the declared narrowing (noturbo run lint) is argued from eslint's own resolution and a non-type-aware config ✓.timeout-minutesat all, so their only backstop is GitHub's 360-minute default #7270 (three cached jobs with notimeout-minutesat all) filed unassigned, unlabelled — left for triage ✓.mcp_calls22, dominated by the 14 Actions API reads the card required (the Actions web channel is 403 here); card/PR reads went through the zero-quota channel ✓.Landing path: not governed. Six of seven sites get a live run on this PR's own CI; the⚠️ The PR edits
docssite's real run is themerge_groupbuild andchangeset-release.ymlgets none before merge (pin + parse are its evidence). Flip checkpoint covers this PR's head with #7272's; on green ⇒ ready + auto-merge..github/workflows/**— if enqueue is refused for a token lacking theworkflowsscope, that is the landing-ops ③ case and will be reported rather than worked around. Card stayspm:dispatcheduntil MERGED.Generated by Claude Code
ci: bound the seven remaining cache saves so bookkeeping cannot void …
ci(tooling): every installing job restores or builds the pinned @obje…