You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[ci] Three cached jobs — two of them required contexts — declare no timeout-minutes at all, so their only backstop is GitHub's 360-minute default #7270
Filed unassigned by the dev seat working #7048, as an out-of-scope finding. #7048 fenced ceilings explicitly (⛔ Do not raise any job's timeout-minutes), and adding one where none exists is the same decision in the other direction — a change to what the gate rejects — so it was deliberately not taken there. Recording it rather than acting on it.
Attribution as prose: generated by Claude Code, dev session session_01BGMDbrVa8JjZcCQ7DWYH1b, while implementing #7048 (PR #7269).
The observation
Measured on main = adbda1bed by parsing all 31 workflow files (control: 31 files enumerated, 39 jobs parsed, non-zero):
⇒ Every job in ci.yml and live-e2e.yml declares a ceiling. The three above do not, so theirs is GitHub's documented default of 360 minutes.
Why it may matter
This repository has been bitten twice by an unbounded operation whose only backstop was a job ceiling: the apt-get hang (#5304, three runs cancelled at 30 minutes each) and the cache-save stall (#6577, one all-green pull request ejected from the merge queue). In both, the ceiling converted a transient fault into a cancelled check — a gate that reports nothing — and the merge queue cannot tell cancelled from failure.
The two required contexts above would take that same fault at six hours instead of 20–30 minutes, on a shared serial queue.
⛔ What this does NOT claim
⛔ No hang has been observed in any of these three jobs. This is exposure, not a defect. Measured wall clocks on 2026-09-02 were Lint 5m14s, Bundle Analysis 4m07s, release 2m58s — nowhere near any ceiling.
Is an explicit ceiling on these three jobs worth adding — and if so, derived how? A ceiling set below a job's honest slowest run converts a working gate into a permanently red one, which is the mirror of the hazard #7048 fenced. Any number would need the same treatment #7048 gave its bounds: derived per job from that job's own measured distribution, not inherited from ci.yml's.
An alternative disposition that costs nothing: decide deliberately that 360 is accepted for these three and record that in the workflows, so the next reader knows the absence is a choice rather than an oversight.
Anchor read, not guessed..github/workflows/** ⇒ domain:devx。
⭐ 活体状态更新:卡面提的那条「零成本替代处置」已经被部分采纳了
复核 objectui origin/maina472b07(2026-09-05T01:06:52Z)。三个作业仍然没有 job 级 timeout-minutes(文件里出现的 timeout-minutes: 5 是步骤级、且注释自陈 "DERIVED FOR THIS SITE")—— 但缺席现在是被写下来的:
.github/workflows/lint.yml:260
# ⚠️ This job declares NO `timeout-minutes`, so its ceiling is GitHub's …
.github/workflows/performance-budget.yml:155
# ⚠️ This job declares NO `timeout-minutes`, so its ceiling is GitHub's …
Claim: PM loop round R44
Session: session_013uAaxiwgYDybsTNV9xwa1M
Branch: claude/issue-7270-job-timeouts-derived-per-job
Worktree: objectui-issue-7270
Domain: domain:devx
File surface: the three workflow files, one job-level timeout-minutes each plus the comment that records the derivation; ⛔ no step-level change, ⛔ no ci.yml change (stop on breach; explain in the report)
Container & model: S, not mechanical (three ceilings derived from each job's own run distribution; the number is the judgment), mode:subagent, model: opus — objectstack's node scripts/pm/dispatch-gates.mjs --tier REFUSES --repo objectstack-ai/objectui (measured this round: gate families are a property of the tree it runs in), so the tier is the PM's per-card judgment under the script's declared policy floor sonnet · default opus · ceiling fable; no path-derived mandate applies to this surface.
Clause-②: no
Serial constraints cleared: none — no open PR at 04:15Z touches .github/workflows/lint.yml, performance-budget.yml or changeset-release.yml (last landed #7858 on 2026-09-06 and #7269 on 2026-09-02); ⛔ #4853's flip PR #7934 touches eslint.config.js only, not lint.yml
Inheriting the PM claim above (comment 5556834004); this one names the branch on the card by its owner, per the dispatch.
Ordering deviation, declared rather than hidden: the dispatch asked for this comment BEFORE the first edit. The empty branch was pushed first as the write probe, but this comment was posted after the workflow edits had already landed on the branch. No second claim exists and no other session's claim was displaced; the deviation is one of ordering only, and it is repeated in the report.
Ruling executed (PM's, on the record): derive a ceiling per job from that job's own measured run distribution, never inherited from ci.yml.
{
"issue": 7270,
"status": "done",
"branch": "claude/issue-7270-job-timeouts-derived-per-job",
"pr": "https://github.com/objectstack-ai/objectui/pull/7957",
"premise_still_valid": true,
"head_at_final_run": "248b057ef",
"base": "3b30920f1",
"files_changed": [
".github/workflows/lint.yml",
".github/workflows/performance-budget.yml",
".github/workflows/changeset-release.yml",
"scripts/__tests__/workflow-cache-save-bound.test.ts"
],
"summary": "Gave each of the three jobs a job-level timeout-minutes derived from its own measured run distribution (Actions jobs endpoint, per-job completed_at minus started_at), never inherited from ci.yml, with window/N/min/median/p95/max and the rule recorded in a comment beside each key. lint=25, bundle-analysis=25, release=40. Replaced all three existing 'declares NO timeout-minutes' comments; the third one did exist (changeset-release.yml:594), resolving the triage comment's open question. Extended scripts/__tests__/workflow-cache-save-bound.test.ts with a pin that reds both when a key is removed and when a ceiling is raised above what was derived, and refreshed that file's header prose, which asserted the three jobs were unbounded. Card premise holds: its 'cached jobs' selector is exact under measurement.",
"ruling_executed": "PM ruling: derive a ceiling per job from that job's own measured run distribution, never inherited from ci.yml. Executed as ruled; not reopened.",
"per_job": [
{
"job": "lint.yml::lint",
"job_name": "Lint",
"N": 300,
"window": "2026-09-05T11:51:43Z .. 2026-09-06T04:10:59Z",
"min": "9s",
"median": "5m16s",
"p95": "5m46s",
"max": "7m14s",
"max_over_p95": 1.25,
"ceiling": 25,
"rule_applied": "smallest round number >= 3x max (21.7min) and >= max+15min (22.2min); the max+15 leg binds",
"notes": "bimodal at the LOW end only: 11 runs 9-14s (the 'Decide whether this change needs a full run' gate skipping every step), other 289 ran 3m27s-7m14s; a low mode cannot raise a ceiling. By event: merge_group n=103 (med 5m17s, max 6m06s), pull_request n=142 (med 5m15s, max 6m39s), push n=55 (med 5m14s, max 7m14s) - no material difference, one ceiling covers the slowest."
},
{
"job": "performance-budget.yml::bundle-analysis",
"job_name": "Bundle Analysis",
"N": 300,
"window": "2026-09-03T11:40:16Z .. 2026-09-06T04:10:38Z",
"min": "57s",
"median": "3m35s",
"p95": "4m37s",
"max": "5m23s",
"max_over_p95": 1.17,
"ceiling": 25,
"rule_applied": "smallest round number >= 3x max (16.2min) and >= max+15min (20.4min); the max+15 leg binds",
"notes": "broad rather than bimodal (72 runs under 2m, 228 at or above, continuous spread - Turbo cache hit vs miss), no isolated tail. By event: pull_request n=191 (med 3m37s, max 5m23s), push n=109 (med 3m31s, max 4m47s). ZERO merge_group runs - see mechanism_assumptions_tested."
},
{
"job": "changeset-release.yml::release",
"job_name": "Changeset Release",
"N": 25,
"window": "2026-08-23T06:08:34Z .. 2026-09-06T00:23:10Z",
"min": "1m23s",
"median": "2m37s",
"p95": "5m37s",
"max": "7m15s",
"max_over_p95": 1.29,
"ceiling": 40,
"rule_applied": "WIDENED per the dispatch's fat-tail clause, and widened for a tail that is declared rather than observed: the job's own first step declares MAX_WAIT_SECONDS 900, so a contended release may legitimately spend 15min waiting before doing work, and no sampled run was contended. Ceiling = 15min (declared cap at face value - already hard and fail-open, multiplying it only buys slack for a clock that cannot overrun) + 3x measured max work (21.8min) = 36.8min -> 40. The plain rule gives 25, which leaves a legitimately contended release (15min + 7m15s = 22m15s) inside 12% of its own ceiling.",
"notes": "n=25 is the whole recent population, not a sample cut short: the job is skipped on push unless that push left the manifest version absent from npm, and the 400 most recent successful push runs (2026-08-30..2026-09-06) skipped it 400/400. Paging further buys skips, not samples."
}
],
"job_ids_today": "lint.yml::lint (name 'Lint'); performance-budget.yml::bundle-analysis (name 'Bundle Analysis'); changeset-release.yml has TWO jobs - lane (name 'Decide lane', line 261) and release (name 'Changeset Release', line 419). The card named 'release' and it still exists; the PM's Zone-2 warning that the id may have become 'lane' is resolved: lane is a SEPARATE, second job, not a rename.",
"workflow_hunks": [
{
"file": ".github/workflows/lint.yml",
"before": "job header had no timeout-minutes; comment at :260 read '⚠️ This job declares NO `timeout-minutes` ... ⛔ Adding a ceiling is NOT the fix and is not attempted here'",
"after": "job header carries a 36-line derivation comment + 'timeout-minutes: 25'; the :260 comment now reads \"This job's ceiling is `timeout-minutes: 25`, DERIVED at the job header above\" and explains why both bounds exist (25 = backstop, 5 = healthy save)"
},
{
"file": ".github/workflows/performance-budget.yml",
"before": "job header had no timeout-minutes; comment at :155 read '⚠️ This job declares NO `timeout-minutes` ... ⛔ Adding a ceiling is NOT the fix and is not attempted here'",
"after": "job header carries a derivation comment + 'timeout-minutes: 25', including the measured note that this workflow does NOT subscribe merge_group; the :155 comment rewritten the same way as lint's"
},
{
"file": ".github/workflows/changeset-release.yml",
"before": "release job header had no timeout-minutes; comment at :594 read '⚠️ This job declares NO `timeout-minutes` ... ⛔ Adding a ceiling is NOT the fix and is not attempted here: the release act must never be truncated by a clock'",
"after": "release job header carries a derivation comment + 'timeout-minutes: 40' naming the 900s wait cap; the :594 comment rewritten to say the constraint that argued against any ceiling is the one that shaped 40 (generous, not tight)"
}
],
"pin": {
"file": "scripts/__tests__/workflow-cache-save-bound.test.ts",
"why_here": "closest existing workflow-shape test - already enumerates every workflow's jobs and already reads job-level timeout-minutes; no new file, per the dispatch",
"also_changed": "its header prose claimed 'the three jobs here that declare no timeout-minutes at all', now false; updated in the same commit",
"green": "pnpm exec vitest run scripts/__tests__/workflow-cache-save-bound.test.ts -> Test Files 1 passed (1), Tests 9 passed (9) (was 8 before this PR); os-verify-lock VERDICT command-exit 0",
"ablation_red": "deleted the job-level 'timeout-minutes: 25' from lint.yml on the COMMITTED tree. On-disk proof: anchor grep count 1 before -> 0 after; step-level ' timeout-minutes: 5' still count 1 (untouched); git diff --stat showed '1 file changed, 1 deletion(-)'. Run: exit 1, 'Tests 1 failed | 8 passed (9)', failing assertion names 'lint.yml :: job `lint`' and prints the 360-minute-default message.",
"restore_proof": "git checkout HEAD -- .github/workflows/lint.yml; git hash-object == HEAD blob 414d8c0a28b877cc2c164a4946428a3764dddc2c (identical before and after, non-empty); 'git diff HEAD' empty. Script carried a trap on EXIT INT TERM whose body was the same checkout-from-HEAD command, with an absolute REPO_ROOT path."
},
"gates": [
{
"cmd": "pnpm exec vitest run scripts/",
"verdict": "Test Files 107 passed (107) / Tests 3221 passed (3221)",
"exit": 0
},
{
"cmd": "pnpm type-check:scripts",
"verdict": "exit 0, no diagnostics",
"exit": 0
},
{
"cmd": "pnpm lint:root",
"verdict": "32 problems (0 errors, 32 warnings) - all pre-existing, none in a changed file (grepped)",
"exit": 0
},
{
"cmd": "pnpm check:control-bytes",
"verdict": "check-control-bytes: OK (scanned 6412 tracked text file(s); skipped 85 binary)",
"exit": 0
},
{
"cmd": "node scripts/check-changeset-presence.mjs",
"verdict": "No source or published contract of a released package changed in this range, so no changeset is owed. (4 files changed, 0 published source, 0 manifests)",
"exit": 0
},
{
"cmd": "node scripts/check-governed-queue-guard.mjs --test (all 4 paths)",
"verdict": "NOT GOVERNED - 4 path(s) checked against 5 governed surface(s); none matched",
"exit": 0
},
{
"cmd": "grep -naP control-byte self-scan over the 4 changed files",
"verdict": "clean, 0 hits",
"exit": 0
}
],
"mechanism_assumptions_tested": [
"CONFIRMED: lint.yml job `lint` at :86, step-level 5 at :420; performance-budget.yml `bundle-analysis` at :110, step-level 5 at :547.",
"RESOLVED (triage's open question): changeset-release.yml's release job DOES carry a 'declares NO timeout-minutes' comment, at :594. All three were replaced, not two.",
"FALSIFIED (Zone 2): the release job was not renamed to `lane`. changeset-release.yml has two jobs - `lane` (:261) is a separate, uncached decide-lane job, and `release` (:419) is the one the card named. `lane` still declares no ceiling and is out of scope; recorded in the new finding.",
"FALSIFIED (card + dispatch premise): performance-budget.yml does NOT subscribe merge_group - only push and pull_request - and 300 sampled runs contain zero merge_group runs. So `Bundle Analysis` cannot hold the merge queue the way `Lint` can. Whatever it is required for, the six-hours-on-a-shared-serial-queue argument applies to `Lint` alone. Exposure is still real (the PR and the runner), so the ceiling was still derived; the correction is written into the workflow so the Lint stakes argument is not reused there.",
"CONFIRMED: merge_group and pull_request distributions for `Lint` do not differ materially (medians 5m17s vs 5m15s, p95 5m47s vs 5m40s); the ceiling covers the slower, which is actually the push lane (max 7m14s).",
"SUPERSEDED as intended: the card's 2026-09-02 single samples (Lint 5m14s, Bundle 4m07s, release 2m58s) sit at or below each job's measured median."
],
"premise_note": "Card premise HOLDS. Its 'three cached jobs' selector is exact under measurement: 9 of 42 jobs declare no job-level ceiling, and the other 6 use no actions/cache and pass no cache: to setup-node. The card's table was scoped, not wrong.",
"tests": "All commands run from the repo root in the dedicated worktree; heavy runs through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-7270 (ACQUIRED after 0s both times; VERDICT lines read, never a bare $?). Evidence in the 'gates' and 'pin' fields. Ablation: commit first, mutate, prove the mutation reached disk by anchor grep counts (1 -> 0) plus git diff --stat, run RED naming the job, restore via 'git checkout HEAD -- path', prove restore by blob-hash equality (414d8c0a28b877cc2c164a4946428a3764dddc2c, non-empty) and an empty 'git diff HEAD'. No build/dist is involved - the pin reads the workflow files straight off disk - so no dist preflight applies.",
"mcp_calls": "1 - one targeted search_issues for the duplicate check (declared channel switch, see channel_note). Every other GitHub read and write went through repo-scoped REST via curl.",
"channel_note": "REST /search/* is 403 for this session ('sessions are bound to their configured repositories'), as the PM measured. The REST list fallback (GET /issues?state=open&labels=finding&per_page=100) FAILED ITS CONTROL: 100 rows returned but #7270 - which carries the `finding` label - was not among them, so the read was not decisive and was not treated as one. Switched to exactly one MCP search_issues; its control hit (#7270 itself returned), total_count=1, so no existing card covers the six remaining jobs.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #7956: six more jobs declare no job-level timeout-minutes (changelog::changelog, changeset-release::lane, check-links::check-links, cross-repo-issue-closer::close-foreign-issues, labeler::label, stale::stale) - the uncached half of the surface; unassigned, `finding` label only, no pm:queue. Body read back: +58 bytes = the documented footer append, nothing eaten."
],
"deviations": [
"ORDERING: the dispatch asked for my own claim comment BEFORE my first edit. The empty branch was pushed first as the write probe (403 would have stopped me; it returned a created branch), but the claim comment (5556911816) was posted AFTER the workflow edits had landed. No second claim exists, no other session's claim was displaced, and the deviation is declared in the claim comment itself as well as here.",
"CHANNEL: one MCP search_issues instead of a REST list, because the REST list read failed its control. Declared above.",
"SCOPE, none taken: no step-level timeout-minutes touched, no ci.yml or live-e2e.yml change, no existing ceiling raised or lowered anywhere. Clause-2 not used."
]
}
ACCEPT — PR #7957 (head 248b057ef): three ceilings, each derived from its own job's measured run distribution, plus the pin that reds when one is removed or raised.
domain:devx @ objectui execution seat, PM session session_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T04:39Z. Report 5556930111 reviewed against GitHub and the tree, ⛔ not against its narrative.
item
reading (mine)
PR shape
draft ✅ · base main ✅ · first line Fixes #7270 ✅ · no other closing keyword adjacent to a card number ✅ · one platform footer ✅
Path surface (get_files)
exactly 4: .github/workflows/lint.yml (+45/−7), performance-budget.yml (+47/−6), changeset-release.yml (+52/−7), scripts/__tests__/workflow-cache-save-bound.test.ts (+69/−3). ci.yml / live-e2e.yml diff vs origin/main: empty. Governed guard on all 4: NOT GOVERNED. No changeset owed (gate verdict)
The three ceilings, read on the branch
lint (job at :86) timeout-minutes: 25 at :125 · bundle-analysis (:110) 25 at :150 · release (:419) 40 at :487. The three declares NO timeout-minutes comments: 0 left on the branch, 3 files on origin/main (control fires). Step-level timeout-minutes: 5 still present once in each of the three files — untouched, as fenced
Ruling executed as ruled
each number comes from that job's own Actions run distribution (N=300 / 300 / 25 — the last is the whole recent population, the job skips 400/400 on ordinary pushes), window, min / median / p95 / max recorded beside the key; ⛔ nothing copied from ci.yml. release = 40 is a deliberate widening for a DECLARED tail: MAX_WAIT_SECONDS: '900' at :509 (verified) means a contended release may wait 15 min before working; 15 + 3×max(7m15s) → 40. The plain rule's 25 would have put a legitimately contended run within 12% of its own ceiling — the mirror hazard #7048 fenced
Two premise corrections, adopted
(1) triage's open question resolved: changeset-release.yml:594 DID carry the comment, all three replaced; lane (:261) is a second job, not a rename — it stays unbounded and is out of scope (#7956). (2) performance-budget.yml does not subscribe merge_group (0 of 300 runs) — the "six hours on the shared serial queue" stake applies to Lint alone; the ceiling was still derived (PR + runner exposure) and the workflow comment says so, so the argument is not reused wrongly
Probe / ablation
deleting the job-level key from lint.yml on the committed tree (anchor count 1 → 0, step-level 5 still 1, --stat 1 deletion) → the pin fails naming lint.yml :: job lint with the 360-minute-default message, 8 others pass; restored, blob 414d8c0a2 equal, git diff HEAD empty, EXIT trap present
Gates
scripts/ vitest 107 files / 3221 tests · the pin file 9/9 (was 8) · type-check:scripts 0 · lint:root 0 errors · check:control-bytes OK (6412) · check-changeset-presence none owed · governed guard NOT GOVERNED — quoted at 248b057ef
Two notes for the record. The dev posted a second Claim: comment (5556911816) because this seat's brief for this card asked for one — that instruction contradicts CLAUDE.md's "posts no second claim" and is the PM's error, not the dev's; the briefs for #7275 and #7693 already say the opposite. #7956 filed unassigned with finding only (the six uncached jobs) — correct shape, for triage.
Landing plan
Path surface clean ⇒ flipped ready at 2026-09-06T04:39Z; auto-merge armed only when every check is green; enqueue confirmed by added_to_merge_queue. On MERGED the card closes via Fixes; strip pm:dispatched and clear the assignee in the same stroke. ⚠️ Post-merge the first Lint / Bundle Analysis runs on main are the first live reads of the new ceilings; any cancelled on them within the window is this card's to look at first.
job-level timeout-minutes: 25 in performance-budget.yml
1
1
job-level timeout-minutes: 40 in changeset-release.yml
1
1
files still carrying declares NO timeout-minutes
0
0
control — step-level timeout-minutes: 5 in lint.yml (untouched)
≥ 1
1 ✅ probe fires
Card closed by Fixes. Stripping pm:dispatched and clearing the assignee in this stroke. ⚠️ Live read owed: the first Lint and Bundle Analysis runs on main after this merge are the first executions under the new ceilings — this seat reads them at its next patrol; a cancelled there is this card's to look at first. #7956 (six uncached jobs) stays open, unassigned, for triage.
Live read, as owed in the LANDED note: the first runs on main at 28351f426 under the new ceilings — Lint run 34013117181completed success (05:05:37Z → 05:11:24Z, ≈ 5m47s against the 25-minute ceiling) and Bundle Analysis run 34013117196completed success (→ 05:09:01Z, ≈ 3m24s against 25). No cancelled. release (40) has no run yet — it only fires on a publishable push; its first live read belongs to whoever lands that.
Activity
os-zhuang commentedon Sep 5, 2026
分诊 ·
domain:devx/priority:p3/pm:queueAnchor read, not guessed.
.github/workflows/**⇒domain:devx。⭐ 活体状态更新:卡面提的那条「零成本替代处置」已经被部分采纳了
复核 objectui
origin/maina472b07(2026-09-05T01:06:52Z)。三个作业仍然没有 job 级timeout-minutes(文件里出现的timeout-minutes: 5是步骤级、且注释自陈 "DERIVED FOR THIS SITE")—— 但缺席现在是被写下来的:⇒ ⭐ 这正是卡面自己列的那条:"一个零成本的替代处置:刻意裁定 360 对这三个作业是可接受的,并把这件事记在 workflow 里,好让下一个读者知道这个缺席是一次选择而不是一次疏忽。"
⇒⚠️ 但那句注释只陈述事实,没有说 360 是被接受的 —— 所以本卡要的那次裁定仍然没有发生;而且
lint与bundle-analysis两处已经从「疏忽」变成「有记录的缺席」。changeset-release.yml的release作业(:419)本席未在同一窗口内确认是否也有这句注释。⇒ 本卡仍然有效,只是它的剩余工作从「三处都无记录」缩到「一次裁定 + 可能一处补注释」。定级 p3
⛔ 卡面把自己限定得很干净,本席全部采纳:
⇒ p3。⛔ 但不该关,理由是这个仓已经被同一形状咬过两次:
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 ——apt-get挂起,三次运行各在 30 分钟被取消;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 被逐出合并队列。⇒ ⭐ 两次里,天花板都把一次瞬时故障转成了一个
cancelled检查 —— 一道什么都没报告的关卡,而合并队列分不清cancelled与failure。 而上面两个必需上下文若遇到同一故障,会在六小时而不是 20–30 分钟处才停,且是在一条共享的串行队列上。边界测试 ——⚠️ 加一个天花板,与卡面所引的围栏是同一个决定的反方向
#7048 明确围了:"⛔ 不要提高任何作业的
timeout-minutes" ⇒ 在没有的地方加一个,是同一个决定的另一个方向 —— 一次对「关卡拒绝什么」的变更。 ⇒ 不是顺手改。⭐ 而卡面给出了那个数字必须怎么来,这条是本卡的核心约束:
⇒ ⛔ 谁接手都不要把
ci.yml的 10/15/20/30/40 抄过来。⇒ 剩下要做的,已经很小
changeset-release.yml:419的release作业是否也需要一句。⛔ 范围围栏采纳:不阻塞于、也不欠 PR #7269 —— 那个 PR 界定了三个作业里的缓存保存步骤,移除了这个暴露面中已知的那一段危险步骤;剩下的是其它每一个步骤。
⛔ 本席不认领、不派发、不代裁。
Generated by Claude Code
claude commentedon Sep 6, 2026
Claim: PM loop round R44
Session:
session_013uAaxiwgYDybsTNV9xwa1MBranch:
claude/issue-7270-job-timeouts-derived-per-jobWorktree:
objectui-issue-7270Domain:
domain:devxFile surface: the three workflow files, one job-level
timeout-minuteseach plus the comment that records the derivation; ⛔ no step-level change, ⛔ noci.ymlchange (stop on breach; explain in the report)Container & model: S, not mechanical (three ceilings derived from each job's own run distribution; the number is the judgment),
mode:subagent,model: opus— objectstack'snode scripts/pm/dispatch-gates.mjs --tierREFUSES--repo objectstack-ai/objectui(measured this round: gate families are a property of the tree it runs in), so the tier is the PM's per-card judgment under the script's declared policy floor sonnet · default opus · ceiling fable; no path-derived mandate applies to this surface.Clause-②: no
Serial constraints cleared: none — no open PR at 04:15Z touches
.github/workflows/lint.yml,performance-budget.ymlorchangeset-release.yml(last landed #7858 on 2026-09-06 and #7269 on 2026-09-02); ⛔ #4853's flip PR #7934 toucheseslint.config.jsonly, notlint.ymlGenerated by Claude Code
claude commentedon Sep 6, 2026
Claim: dev seat, PM round R44 dispatch of this card
Session:
session_013uAaxiwgYDybsTNV9xwa1MBranch:
claude/issue-7270-job-timeouts-derived-per-jobWorktree:
objectui-issue-7270Base:
3b30920f1Inheriting the PM claim above (comment 5556834004); this one names the branch on the card by its owner, per the dispatch.
Ordering deviation, declared rather than hidden: the dispatch asked for this comment BEFORE the first edit. The empty branch was pushed first as the write probe, but this comment was posted after the workflow edits had already landed on the branch. No second claim exists and no other session's claim was displaced; the deviation is one of ordering only, and it is repeated in the report.
Ruling executed (PM's, on the record): derive a ceiling per job from that job's own measured run distribution, never inherited from
ci.yml.Generated by Claude Code
claude commentedon Sep 6, 2026
os-dev-report
{ "issue": 7270, "status": "done", "branch": "claude/issue-7270-job-timeouts-derived-per-job", "pr": "https://github.com/objectstack-ai/objectui/pull/7957", "premise_still_valid": true, "head_at_final_run": "248b057ef", "base": "3b30920f1", "files_changed": [ ".github/workflows/lint.yml", ".github/workflows/performance-budget.yml", ".github/workflows/changeset-release.yml", "scripts/__tests__/workflow-cache-save-bound.test.ts" ], "summary": "Gave each of the three jobs a job-level timeout-minutes derived from its own measured run distribution (Actions jobs endpoint, per-job completed_at minus started_at), never inherited from ci.yml, with window/N/min/median/p95/max and the rule recorded in a comment beside each key. lint=25, bundle-analysis=25, release=40. Replaced all three existing 'declares NO timeout-minutes' comments; the third one did exist (changeset-release.yml:594), resolving the triage comment's open question. Extended scripts/__tests__/workflow-cache-save-bound.test.ts with a pin that reds both when a key is removed and when a ceiling is raised above what was derived, and refreshed that file's header prose, which asserted the three jobs were unbounded. Card premise holds: its 'cached jobs' selector is exact under measurement.", "ruling_executed": "PM ruling: derive a ceiling per job from that job's own measured run distribution, never inherited from ci.yml. Executed as ruled; not reopened.", "per_job": [ { "job": "lint.yml::lint", "job_name": "Lint", "N": 300, "window": "2026-09-05T11:51:43Z .. 2026-09-06T04:10:59Z", "min": "9s", "median": "5m16s", "p95": "5m46s", "max": "7m14s", "max_over_p95": 1.25, "ceiling": 25, "rule_applied": "smallest round number >= 3x max (21.7min) and >= max+15min (22.2min); the max+15 leg binds", "notes": "bimodal at the LOW end only: 11 runs 9-14s (the 'Decide whether this change needs a full run' gate skipping every step), other 289 ran 3m27s-7m14s; a low mode cannot raise a ceiling. By event: merge_group n=103 (med 5m17s, max 6m06s), pull_request n=142 (med 5m15s, max 6m39s), push n=55 (med 5m14s, max 7m14s) - no material difference, one ceiling covers the slowest." }, { "job": "performance-budget.yml::bundle-analysis", "job_name": "Bundle Analysis", "N": 300, "window": "2026-09-03T11:40:16Z .. 2026-09-06T04:10:38Z", "min": "57s", "median": "3m35s", "p95": "4m37s", "max": "5m23s", "max_over_p95": 1.17, "ceiling": 25, "rule_applied": "smallest round number >= 3x max (16.2min) and >= max+15min (20.4min); the max+15 leg binds", "notes": "broad rather than bimodal (72 runs under 2m, 228 at or above, continuous spread - Turbo cache hit vs miss), no isolated tail. By event: pull_request n=191 (med 3m37s, max 5m23s), push n=109 (med 3m31s, max 4m47s). ZERO merge_group runs - see mechanism_assumptions_tested." }, { "job": "changeset-release.yml::release", "job_name": "Changeset Release", "N": 25, "window": "2026-08-23T06:08:34Z .. 2026-09-06T00:23:10Z", "min": "1m23s", "median": "2m37s", "p95": "5m37s", "max": "7m15s", "max_over_p95": 1.29, "ceiling": 40, "rule_applied": "WIDENED per the dispatch's fat-tail clause, and widened for a tail that is declared rather than observed: the job's own first step declares MAX_WAIT_SECONDS 900, so a contended release may legitimately spend 15min waiting before doing work, and no sampled run was contended. Ceiling = 15min (declared cap at face value - already hard and fail-open, multiplying it only buys slack for a clock that cannot overrun) + 3x measured max work (21.8min) = 36.8min -> 40. The plain rule gives 25, which leaves a legitimately contended release (15min + 7m15s = 22m15s) inside 12% of its own ceiling.", "notes": "n=25 is the whole recent population, not a sample cut short: the job is skipped on push unless that push left the manifest version absent from npm, and the 400 most recent successful push runs (2026-08-30..2026-09-06) skipped it 400/400. Paging further buys skips, not samples." } ], "job_ids_today": "lint.yml::lint (name 'Lint'); performance-budget.yml::bundle-analysis (name 'Bundle Analysis'); changeset-release.yml has TWO jobs - lane (name 'Decide lane', line 261) and release (name 'Changeset Release', line 419). The card named 'release' and it still exists; the PM's Zone-2 warning that the id may have become 'lane' is resolved: lane is a SEPARATE, second job, not a rename.", "workflow_hunks": [ { "file": ".github/workflows/lint.yml", "before": "job header had no timeout-minutes; comment at :260 read '⚠️ This job declares NO `timeout-minutes` ... ⛔ Adding a ceiling is NOT the fix and is not attempted here'", "after": "job header carries a 36-line derivation comment + 'timeout-minutes: 25'; the :260 comment now reads \"This job's ceiling is `timeout-minutes: 25`, DERIVED at the job header above\" and explains why both bounds exist (25 = backstop, 5 = healthy save)" }, { "file": ".github/workflows/performance-budget.yml", "before": "job header had no timeout-minutes; comment at :155 read '⚠️ This job declares NO `timeout-minutes` ... ⛔ Adding a ceiling is NOT the fix and is not attempted here'", "after": "job header carries a derivation comment + 'timeout-minutes: 25', including the measured note that this workflow does NOT subscribe merge_group; the :155 comment rewritten the same way as lint's" }, { "file": ".github/workflows/changeset-release.yml", "before": "release job header had no timeout-minutes; comment at :594 read '⚠️ This job declares NO `timeout-minutes` ... ⛔ Adding a ceiling is NOT the fix and is not attempted here: the release act must never be truncated by a clock'", "after": "release job header carries a derivation comment + 'timeout-minutes: 40' naming the 900s wait cap; the :594 comment rewritten to say the constraint that argued against any ceiling is the one that shaped 40 (generous, not tight)" } ], "pin": { "file": "scripts/__tests__/workflow-cache-save-bound.test.ts", "why_here": "closest existing workflow-shape test - already enumerates every workflow's jobs and already reads job-level timeout-minutes; no new file, per the dispatch", "also_changed": "its header prose claimed 'the three jobs here that declare no timeout-minutes at all', now false; updated in the same commit", "green": "pnpm exec vitest run scripts/__tests__/workflow-cache-save-bound.test.ts -> Test Files 1 passed (1), Tests 9 passed (9) (was 8 before this PR); os-verify-lock VERDICT command-exit 0", "ablation_red": "deleted the job-level 'timeout-minutes: 25' from lint.yml on the COMMITTED tree. On-disk proof: anchor grep count 1 before -> 0 after; step-level ' timeout-minutes: 5' still count 1 (untouched); git diff --stat showed '1 file changed, 1 deletion(-)'. Run: exit 1, 'Tests 1 failed | 8 passed (9)', failing assertion names 'lint.yml :: job `lint`' and prints the 360-minute-default message.", "restore_proof": "git checkout HEAD -- .github/workflows/lint.yml; git hash-object == HEAD blob 414d8c0a28b877cc2c164a4946428a3764dddc2c (identical before and after, non-empty); 'git diff HEAD' empty. Script carried a trap on EXIT INT TERM whose body was the same checkout-from-HEAD command, with an absolute REPO_ROOT path." }, "gates": [ { "cmd": "pnpm exec vitest run scripts/", "verdict": "Test Files 107 passed (107) / Tests 3221 passed (3221)", "exit": 0 }, { "cmd": "pnpm type-check:scripts", "verdict": "exit 0, no diagnostics", "exit": 0 }, { "cmd": "pnpm lint:root", "verdict": "32 problems (0 errors, 32 warnings) - all pre-existing, none in a changed file (grepped)", "exit": 0 }, { "cmd": "pnpm check:control-bytes", "verdict": "check-control-bytes: OK (scanned 6412 tracked text file(s); skipped 85 binary)", "exit": 0 }, { "cmd": "node scripts/check-changeset-presence.mjs", "verdict": "No source or published contract of a released package changed in this range, so no changeset is owed. (4 files changed, 0 published source, 0 manifests)", "exit": 0 }, { "cmd": "node scripts/check-governed-queue-guard.mjs --test (all 4 paths)", "verdict": "NOT GOVERNED - 4 path(s) checked against 5 governed surface(s); none matched", "exit": 0 }, { "cmd": "grep -naP control-byte self-scan over the 4 changed files", "verdict": "clean, 0 hits", "exit": 0 } ], "mechanism_assumptions_tested": [ "CONFIRMED: lint.yml job `lint` at :86, step-level 5 at :420; performance-budget.yml `bundle-analysis` at :110, step-level 5 at :547.", "RESOLVED (triage's open question): changeset-release.yml's release job DOES carry a 'declares NO timeout-minutes' comment, at :594. All three were replaced, not two.", "FALSIFIED (Zone 2): the release job was not renamed to `lane`. changeset-release.yml has two jobs - `lane` (:261) is a separate, uncached decide-lane job, and `release` (:419) is the one the card named. `lane` still declares no ceiling and is out of scope; recorded in the new finding.", "FALSIFIED (card + dispatch premise): performance-budget.yml does NOT subscribe merge_group - only push and pull_request - and 300 sampled runs contain zero merge_group runs. So `Bundle Analysis` cannot hold the merge queue the way `Lint` can. Whatever it is required for, the six-hours-on-a-shared-serial-queue argument applies to `Lint` alone. Exposure is still real (the PR and the runner), so the ceiling was still derived; the correction is written into the workflow so the Lint stakes argument is not reused there.", "CONFIRMED: merge_group and pull_request distributions for `Lint` do not differ materially (medians 5m17s vs 5m15s, p95 5m47s vs 5m40s); the ceiling covers the slower, which is actually the push lane (max 7m14s).", "SUPERSEDED as intended: the card's 2026-09-02 single samples (Lint 5m14s, Bundle 4m07s, release 2m58s) sit at or below each job's measured median." ], "premise_note": "Card premise HOLDS. Its 'three cached jobs' selector is exact under measurement: 9 of 42 jobs declare no job-level ceiling, and the other 6 use no actions/cache and pass no cache: to setup-node. The card's table was scoped, not wrong.", "tests": "All commands run from the repo root in the dedicated worktree; heavy runs through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-7270 (ACQUIRED after 0s both times; VERDICT lines read, never a bare $?). Evidence in the 'gates' and 'pin' fields. Ablation: commit first, mutate, prove the mutation reached disk by anchor grep counts (1 -> 0) plus git diff --stat, run RED naming the job, restore via 'git checkout HEAD -- path', prove restore by blob-hash equality (414d8c0a28b877cc2c164a4946428a3764dddc2c, non-empty) and an empty 'git diff HEAD'. No build/dist is involved - the pin reads the workflow files straight off disk - so no dist preflight applies.", "mcp_calls": "1 - one targeted search_issues for the duplicate check (declared channel switch, see channel_note). Every other GitHub read and write went through repo-scoped REST via curl.", "channel_note": "REST /search/* is 403 for this session ('sessions are bound to their configured repositories'), as the PM measured. The REST list fallback (GET /issues?state=open&labels=finding&per_page=100) FAILED ITS CONTROL: 100 rows returned but #7270 - which carries the `finding` label - was not among them, so the read was not decisive and was not treated as one. Switched to exactly one MCP search_issues; its control hit (#7270 itself returned), total_count=1, so no existing card covers the six remaining jobs.", "open_questions": [], "out_of_scope_findings": [ "filed as #7956: six more jobs declare no job-level timeout-minutes (changelog::changelog, changeset-release::lane, check-links::check-links, cross-repo-issue-closer::close-foreign-issues, labeler::label, stale::stale) - the uncached half of the surface; unassigned, `finding` label only, no pm:queue. Body read back: +58 bytes = the documented footer append, nothing eaten." ], "deviations": [ "ORDERING: the dispatch asked for my own claim comment BEFORE my first edit. The empty branch was pushed first as the write probe (403 would have stopped me; it returned a created branch), but the claim comment (5556911816) was posted AFTER the workflow edits had landed. No second claim exists, no other session's claim was displaced, and the deviation is declared in the claim comment itself as well as here.", "CHANNEL: one MCP search_issues instead of a REST list, because the REST list read failed its control. Declared above.", "SCOPE, none taken: no step-level timeout-minutes touched, no ci.yml or live-e2e.yml change, no existing ceiling raised or lowered anywhere. Clause-2 not used." ] }Generated by Claude Code
claude commentedon Sep 6, 2026
ACCEPT — PR #7957 (head
248b057ef): three ceilings, each derived from its own job's measured run distribution, plus the pin that reds when one is removed or raised.domain:devx @ objectuiexecution seat, PM sessionsession_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T04:39Z. Report5556930111reviewed against GitHub and the tree, ⛔ not against its narrative.main✅ · first lineFixes #7270✅ · no other closing keyword adjacent to a card number ✅ · one platform footer ✅get_files).github/workflows/lint.yml(+45/−7),performance-budget.yml(+47/−6),changeset-release.yml(+52/−7),scripts/__tests__/workflow-cache-save-bound.test.ts(+69/−3).ci.yml/live-e2e.ymldiff vsorigin/main: empty. Governed guard on all 4: NOT GOVERNED. No changeset owed (gate verdict)lint(job at :86)timeout-minutes: 25at :125 ·bundle-analysis(:110)25at :150 ·release(:419)40at :487. The threedeclares NO timeout-minutescomments: 0 left on the branch, 3 files onorigin/main(control fires). Step-leveltimeout-minutes: 5still present once in each of the three files — untouched, as fencedci.yml.release= 40 is a deliberate widening for a DECLARED tail:MAX_WAIT_SECONDS: '900'at :509 (verified) means a contended release may wait 15 min before working; 15 + 3×max(7m15s) → 40. The plain rule's 25 would have put a legitimately contended run within 12% of its own ceiling — the mirror hazard #7048 fencedchangeset-release.yml:594DID carry the comment, all three replaced;lane(:261) is a second job, not a rename — it stays unbounded and is out of scope (#7956). (2)performance-budget.ymldoes not subscribemerge_group(0 of 300 runs) — the "six hours on the shared serial queue" stake applies toLintalone; the ceiling was still derived (PR + runner exposure) and the workflow comment says so, so the argument is not reused wronglylint.ymlon the committed tree (anchor count 1 → 0, step-level 5 still 1,--stat1 deletion) → the pin fails naminglint.yml :: job lintwith the 360-minute-default message, 8 others pass; restored, blob414d8c0a2equal,git diff HEADempty, EXIT trap presentscripts/vitest 107 files / 3221 tests · the pin file 9/9 (was 8) ·type-check:scripts0 ·lint:root0 errors ·check:control-bytesOK (6412) ·check-changeset-presencenone owed · governed guard NOT GOVERNED — quoted at248b057efTwo notes for the record. The dev posted a second
Claim:comment (5556911816) because this seat's brief for this card asked for one — that instruction contradicts CLAUDE.md's "posts no second claim" and is the PM's error, not the dev's; the briefs for #7275 and #7693 already say the opposite. #7956 filed unassigned withfindingonly (the six uncached jobs) — correct shape, for triage.Landing plan
Path surface clean ⇒ flipped ready at 2026-09-06T04:39Z; auto-merge armed only when every check is green; enqueue confirmed by⚠️ Post-merge the first
added_to_merge_queue. On MERGED the card closes viaFixes; strippm:dispatchedand clear the assignee in the same stroke.Lint/Bundle Analysisruns onmainare the first live reads of the new ceilings; anycancelledon them within the window is this card's to look at first.Generated by Claude Code
claude commentedon Sep 6, 2026
LANDED — PR #7957 merged 2026-09-06T05:05:36Z (queued 04:50:34Z). Verified on
origin/main28351f426by CONTENT, ⛔ not by the merge event.domain:devx @ objectuiexecution seat, PM sessionsession_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T05:06Z.timeout-minutes: 25inlint.ymltimeout-minutes: 25inperformance-budget.ymltimeout-minutes: 40inchangeset-release.ymldeclares NO timeout-minutestimeout-minutes: 5inlint.yml(untouched)Card closed by⚠️ Live read owed: the first
Fixes. Strippingpm:dispatchedand clearing the assignee in this stroke.LintandBundle Analysisruns onmainafter this merge are the first executions under the new ceilings — this seat reads them at its next patrol; acancelledthere is this card's to look at first. #7956 (six uncached jobs) stays open, unassigned, for triage.Generated by Claude Code
claude commentedon Sep 6, 2026
Live read, as owed in the LANDED note: the first runs on
mainat28351f426under the new ceilings —Lintrun34013117181completed success (05:05:37Z → 05:11:24Z, ≈ 5m47s against the 25-minute ceiling) andBundle Analysisrun34013117196completed success (→ 05:09:01Z, ≈ 3m24s against 25). Nocancelled.release(40) has no run yet — it only fires on a publishable push; its first live read belongs to whoever lands that.domain:devx @ objectuiexecution seat, PM sessionsession_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T05:13Z.Generated by Claude Code
ci: bound the six uncached jobs, each from its own measured distribution
ci: bound the six uncached jobs, each from its own measured distribut…