Repository navigation
feat(pr-pipeline): add a limits leaf for the limit-stop restart values - #7918
Conversation
pr-pipeline.yaml gains an optional `limits` block: fallback-reset, reset-clamp-min and reset-clamp-max (hours), restarts-per-run, limit-stops-per-head and limit-stops-per-issue, each defaulting to the value the lanes hard-coded. resolve-config emits all six. report-outcome and the work-unblock sweep read them from resolve-config's output file, and pr-feedback-dispatch from the config it resolves; pr-unblock-dispatch resolves no config and keeps the defaults. The shared defaults and reset clamp sit beside report-outcome.mjs, which runs from a copy of its own directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pr-unblock-dispatch.yml installs resolve-config's dependencies the way pr-feedback-dispatch.yml does, and dispatch.mjs resolves the base config's limits before any dispatch, so limits.limit-stops-per-head and the reset clamp reach both dispatchers. A config the resolver refuses fails the run before anything is dispatched. The schema caps each leaf (hours at 720, stop caps at 10, restarts per run at 50) so a typo cannot let a forged limit stop restart a lane without bound, and the pr-run-activity-write.yml comment names the limits keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #7889 and #7912. label-unblocked.mjs keeps #7889's refs-hold import, hand-off and restartRefusal guard and threads the resolved limits through them; the hand-off comment now names limits.restarts-per-run. The limits CHANGELOG entry moves to 2.8.0 above main's 2.7.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
claude-security-review reviewed Security review: PR #7918 (head 713e81e)No security findings. I read all 20 changed files, including tests and docs. What I checked:
I did not read the instruction exception register, because nothing in this diff deletes or softens a standing instruction. |
|
claude-review reviewed No blocking findings on head 713e81e. Closing issue #7764. I checked each acceptance criterion against the diff:
Checks I ran.
Not run. I did not run the test suites. The 924-pass result is the author's claim, and I did not verify it. Residual risk, below the blocking bar. The six defaults now live in three places: the schema, |
Closes #7764
Summary
The limit-stop restart values from #7737 were hard-coded in three scripts. They are now an optional
limitsleaf ofpr-pipeline.yaml. Its schema defaults equal today's values, so behavior does not change. This repository'spr-pipeline.yamlsets nolimitsblock.Keys follow the defaulted decision on the issue:
fallback-reset(5),reset-clamp-min(1) andreset-clamp-max(168), all in whole hours, plusrestarts-per-run(5),limit-stops-per-head(3) andlimit-stops-per-issue(3).Fix
docs/conventions/pr-pipeline/pr-pipeline.schema.json): newlimitsobject,additionalProperties: false, integer leaves, each withminimum: 1and a default. Each leaf also has a maximum: 720 for the hour leaves, 10 for both stop caps, 50 forrestarts-per-run. The stop caps bound how often a forged limit stop can restart a lane, so a typo cannot lift that bound.resolve.mjsemits a resolvedlimitsobject next toloop. The defaults are inlined the wayloopdoes it, andresolve.mjsgains no import.resolveLimits(files, { configPath })runs the same file checks asresolve()and returns only the limits.resolve-config.mjsexportslimitsFromBase(env), which reads the config from the base checkout..github/scripts/work-resolve-issue/limits.mjs(new, no package imports):LIMIT_DEFAULTS,readLimits(path)andclampReset(resets, from, limits).readLimitsreads resolve-config's output file and falls back to the defaults.report-outcome.mjsbecause that script runs from a copy of its own directory alone (report-outcome.test.mjs:1404-1406).limitsfromRESOLVED_CONFIG(default$RUNNER_TEMP/resolved-config.json), the file it already reads for denied paths. It re-exportsclampReset, solabel-unblocked.mjskeeps its import.label-unblocked.mjs) reads the same file, whichwork-unblock.yml:119already writes.restarts-per-runcapsrestartLimited.restartRefusaluseslimit-stops-per-issueand the resolved clamp. fix(pr-pipeline): don't leave issues parked on agent-ready after their lane PR ends #7889's refs hold, hand-off andrestartRefusalguard are merged in, and the hand-off count also runs againstrestarts-per-run.limitResetsAt(run, limits)andanyHandles(runs, { now, limits }).pr-unblock-dispatch.ymlgains an "Install resolve-config's dependencies" step. It is the same command aspr-feedback-dispatch.yml:79, under the dispatch step's own kill-switch and trusted-trigger condition.dispatch.mjsresolves the base config's limits throughlimitsFromBasebefore any dispatch and passes them toanyHandles. A config the resolver refuses fails the run red before anything is dispatched, as feedback-dispatch already does. That includes the review-dependency dispatches in the same run.resolved.limits. The skip logic is the same, but a head that already has a check run now costs one resolve-config read before it is skipped.FALLBACK_RESET_MS,RESTARTS_PER_RUNandMAX_LIMIT_STOPSare gone, along with every source comment that pointed at this issue.MAX_LIMIT_STOPScomment inpr-run-activity-write.ymlnow names the two limits keys andreset-clamp-min.Verification
Tests were written first and run red before each change:
limits(1) andresolve-config/dispatch-limits.test.mjs(2). The second file runs dispatch.mjs'smainagainst a real base checkout:limit-stops-per-head: 1stops a re-dispatch that the defaults allow, andlimit-stops-per-head: 0fails the run with no dispatch.limits) passed before the change only because the root schema refusedlimitsas an unknown key. It now exercises the leaf schema.Results:
node --test .github/scripts/pr-unblock/*.test.mjs .github/scripts/work-unblock/*.test.mjs .github/scripts/work-resolve-issue/*.test.mjs .github/actions/resolve-config/*.test.mjs .github/actions/report-check-run/*.test.mjs: 924 pass, 0 fail. Existing expectations are unchanged; the dispatch.testmainhelper now injects the default limits.actionlintonpr-unblock-dispatch.ymlandpr-run-activity-write.yml: exit 0.zizmor --offlineon both: no findings (14 ignored, 6 suppressed, unchanged).docs/conventions/pr-pipeline.yamlandexamples/claude-code-plugins.yamlare valid, and so is the live config with all six defaults set.markdownlint-cli2on the changed docs: 0 issues.refs/heads/maincheckout in.base.pr-feedback-dispatch.yml:79: lockfile with exact pins,--ignore-scripts, no token in its env, and the same kill-switch and gate condition.resolved-config.jsoncannot drop the restart floor below one hour.report-outcome.mjsstill loads from a copy of its own directory.GITHUB_TOKEN. That adds no powerpr-feedback-dispatch.ymldoes not already grant, so it is accepted as is.Basis:
pr-unblock-dispatch.yml:18-21: the dispatcher triggers onpushandworkflow_dispatchonly. Its checkout isrefs/heads/maininto.basewithpersist-credentials: false, so the installed lockfile and the config are the default branch's.pr-feedback-dispatch.yml:79: the same install command, already in use.resolve.mjsloopblock: the inline-defaults pattern copied here.report-outcome.test.mjs:1404-1406: the own-directory copy constraint.work-unblock.yml:119: writesresolved-config.json.reset-clamp-min≤reset-clamp-max; that hardening is deferred. Basis: judgment.Related
label-unblocked.mjsimport and therestartLimitedsignature, plus the CHANGELOG version.🤖 Generated with Claude Code