Uh oh!
There was an error while loading. Please reload this page.
release-train: staging -> main - #186
Merged
Merged
Conversation
…ks got impossible advice (backend#1409) (#154) * fix(pii-gate): the override could not rescue an unset secret, and forks got impossible advice (backend#1409) Two defects, plus the first test this gate has ever had. 1 · THE DOCUMENTED ESCAPE HATCH DID NOT WORK The `pii-gate-override` label was checked AFTER the unset-secret refusal, so on a PR where PII_DENYLIST is missing the refusal exited first and the label could not rescue anything. Both this file's own header ("Override a false positive: add the 'pii-gate-override' label") and backend#1409's remediation note ("or apply the pii-gate-override label to an individual PR") advertise it as the per-PR way out of exactly that state. That state is not hypothetical: the secret still does not exist, and today's `.github` promotion (8aabe41, 13:53) armed the fail-closed path, so all seven public repos now carry a red check that the documented workaround cannot clear. An override is a statement that this PR should not be evaluated, so it now precedes every reason the evaluation might refuse. 2 · A FORK PR WAS TOLD TO GO SET A SECRET THAT CANNOT HELP IT GitHub does not pass secrets to `pull_request` runs from a forked repository, so DENYLIST is empty on any fork PR regardless of what the org secret contains. The gate then took the unset-secret path, whose message asks an org admin to run `gh secret set` — advice that cannot work, because this is a property of the event and not of the configuration. Forks now get their own branch with a true reason and a real remedy: a maintainer reads the PR text and applies the override label to record it. Still fails closed. Compared against exactly "true", so an absent head.repo (deleted fork) falls through to the paths below, which refuse on an empty denylist and refuse again on a Compare read they cannot complete — an unknown fork status cannot buy a pass. This also unblocks #1409 defect 2. Making `pii-gate / pii-check` required would have made every fork PR unmergeable; with the fork path explicit, the check can be required for same-repo PRs and deliberately not for forks. Measured today: 0 fork PRs across all 7 public repos, so nothing is affected retroactively. 3 · A SELFTEST, IN THE SHAPE caller-drift ALREADY USES scripts/tests/pii-gate-selftest.sh extracts the gate's `run:` block and executes it against a stubbed `gh`. 15 cases: both new behaviours, the unset-secret and fork refusals, title/body/commit matching, and every fail-closed path (comma-only denylist, truncated commit list, unreadable Compare, missing base SHA, glob-shaped term), plus a regression case for defect 3 — an early match in a ~150KB haystack, which is the shape that used to be discarded via SIGPIPE. Every case asserts the exit status AND a distinguishing phrase, because a fork PR and an unconfigured org both exit 1 and the whole point of this change is that they must not say the same thing. A status-only test would pass while the gate gave impossible advice. Proof the suite bites rather than merely being green: run against the currently live main copy it fails exactly the 4 new cases and passes the other 11, so nothing pre-existing regressed. The test depends on the `run:` block staying free of `${{ }}` interpolation. That is asserted, not assumed — extraction fails loudly if a future edit inlines an expression, rather than silently covering less. Not fixed here, and still open on backend#1409: the secret itself does not exist (a content decision), and the check is required on 0 of 7 public repos. Scanning the diff rather than only PR metadata is backend#1559. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(pii-gate): pad the regression haystack via commits, not a 150KB env var The defect-3 case exported a ~150KB PR_BODY. Linux caps a single exec argument or environment string at 128KB (MAX_ARG_STRLEN), so every exec after that export died with E2BIG -- grep, head, tr and even the trap rm. macOS has a larger limit, so it passed locally and failed on the runner: "/usr/bin/grep: Argument list too long". Also unrealistic. GitHub caps a PR body at 65,536 characters, so no real run could produce that env var. Padding now comes from 250 commit messages generated straight into the fixture file, which never travel through argv or the environment -- and that is the faithful shape anyway: backend#1409 describes the haystack as title + body + up to 250 commit messages, title first. The case now asserts the haystack size (80,390 bytes) exceeds the 64KB pipe buffer. Without that, the fixture could shrink and the case would stop reaching the defect while staying green -- a regression test that no longer reproduces its bug is the same class of false comfort #1409 is about. Proof it reaches the defect: reverting the matcher to the pre-#130 pipe form makes this case, and only this case, fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ep-open sync (backend#1597) (#163) stale-backlog is column-blind: a quiet North Stars epic auto-closes after 6+8 weeks and routes to Cancelled. The sweep already exempts keep-open - this keeps that label synced from board state nightly. One-way by design; write failures fail the step. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ly does (backend#1597) (#164) project-number, org and target-priority were declared and never read; the step hard-codes the 'priority' label per D5. Measured all 16 callers on develop and prod branches: byte-identical, secrets: inherit only, no with: blocks - so outright removal breaks nothing. trigger-label stays (used in the job if:). Header comment now describes the actual behavior: trigger label in, binary 'priority' issue label out, no board field. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…4) (#162) release-train#34 merged 2026-08-06 and listed the repo, so release_train can now be true. It could not be set in the same PR as the caller flip (.github#153): caller-drift.py:562 verifies this field against release-train/repos.yml, so flipping it early fails the guard. RFC-BACKEND-1552 D2 is the decision; it amends RFC-BACKEND-1405 D1, which had named this repo in its exempt list. Verified: caller-drift exits 0, 20/20 repos, no drift. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…d skips CLOSED issues (.github#166) (#167) * sec(ci): SHA-pin checkout+setup-python in the pii-gate selftest (backend#1491, D10) The new selftest workflow used floating @v4/@v5 while every other workflow here pins to a 40-char SHA; this repo runs no code-quality caller to catch the drift. * fix(reconcile): North Stars keep-open shield skips CLOSED issues (Bugbot .github#166) The shield filtered on status+type but not issue state, so closed North Stars issues got a sticky keep-open the classify step may then terminalize.
… has one (backend#1586) (#169) The WHY-THIS-FILE-EXISTS note closed with a present-tense claim - "there is no closure caller there to route it" - that the body of the same file now contradicts. e2e-test-agent's entry reads kanban-closure-router.yml: required # kanban-closure-routing.yml since the e2e-test-agent#13 remediation, and the caller is really there: tracebloc/e2e-test-agent@develop ships .github/workflows/kanban-closure-routing.yml resolving to tracebloc/.github/.github/workflows/kanban-closure-router.yml@main. Note the filename - `kanban-closure-routing.yml`, not `-caller.yml` - which is exactly the mismatch the MATCHING IS ON `uses:` CONTENT paragraph below warns about, and exactly why a reader checking the header's claim by filename would have concluded it was still true. This is a file whose entire purpose is being the single source of truth that caller-drift.yml enforces. A header that misstates the live state of a repo the body governs is the one defect it cannot afford, even in a comment: the guard checks the entries, nobody checks the prose, so the prose is where a stale claim survives longest. The fix keeps the motivating history and moves it into the past tense, because e2e-test-agent#1 really did close without routing and that is the evidence backend#1415 was built on. What changed is not the diagnosis, it is the repo: the motivating gap is closed, the detection gap the file exists to fix is not. Zero runtime impact - a comment. YAML parse re-verified (20 repos load). Found on the 2026-08-05 develop->staging hop (.github mirror #155, repo-inventory.yml:444), landed to staging under triage. Parent backend#1405. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…1603) (#171) * feat(ci): run the org's own code-quality suite on this repo (backend#1603) This repo publishes code-quality.yml -- and its action-pins job -- to all 19 other repos, and was the only repo that never ran it. The exemption was invisible because actionlint.yml looks like coverage: it type-checks every expression, validates needs/uses wiring, and shellchecks every run: block. What it does not do is look at action REFS. So the pinning rule this repo defines was the one rule this repo was not subject to. That is not theoretical. On 2026-08-06 .github#168 -- an otherwise correct fix -- reverted actions/checkout from its pinned commit SHA back to the mutable @v4 tag, in advance-deploy-env.yml, the most-consumed reusable workflow in the org and one that runs with PROJECTS_KANBAN_TOKEN in scope. action-pins exists to fail exactly that diff. It did not run. A human reading the diff caught it, which is the review path D10 was written to stop depending on. Armed from day one rather than soft-failed into a backlog, because the tree was measured first: shellcheck --severity=error clean over both shell scripts, house-rules.sh clean over the same, and 24 `uses:` refs across 23 workflows with zero pin violations. ruff and gitleaks could not be run locally and get their first honest look on this PR; whatever they surface is fixed or baselined here rather than the gate being softened -- the same bargain e2e-test-agent's caller records in its own comment. action-pins-soft-fail is set to false explicitly rather than left to inherit soft-fail. It is the check whose absence let #168 through, and every other repo consumes these workflows at @main and inherits whatever refs they pin, so this repo has a specific duty to enforce it on itself. actionlint.yml stays separate. The two gates have deliberately different postures -- actionlint blocks from day one against a tree cleaned in the same change; code-quality ships soft-fail: true for repos still clearing backlog -- and folding them together would force one posture onto both. No paths: filter, so the check can be made required without leaving workflow-free PRs waiting on a status that never reports. Refs backend#1603, backend#1491 (D10), backend#1276. * fix(ci): drop the action-pins inputs -- @main does not declare them yet First push of this branch produced a startup_failure on the reusable call (run 31086491251): every other check reported normally and `Code quality` never started. Cause: `action-pins` and `action-pins-soft-fail` were added in #159, which is on `develop`; `main` still carries the five-job version of code-quality.yml. Callers must reference @main (Q3), and passing an input the @main callee does not declare fails the entire call before any job runs. Same merge-order trap as the #160 audit red: a change becomes consumable by @main callers when it is PROMOTED, not when it is merged. The other five jobs still run armed, so most of backend#1603 closes now. The arming diff is written verbatim in the file so the follow-up is mechanical once #159 reaches main. Refs backend#1603.
… (backend#1600) (#168) * fix(kanban): advance the ISSUES each promoted PR closes, not just the PR (backend#1600) advance-deploy-env advanced only PRs; issues closed by a develop-merged PR were parked at On dev by the closure-router and never advanced when their code shipped, drifting permanently. For each PR in the push, resolve closingIssuesReferences (cross-repo aware) and advance each closing issue with the same monotonic guard, archived-skip and (new) dry-run behaviour. Adds a dry_run input; update_field short-circuits under it so both the PR and issue paths can be tested without writes. * fix(kanban): only advance CLOSED closing-issues (Bugbot .github#168 High) PRs merge to develop (not the default branch), so closingIssuesReferences lists still-OPEN issues; advancing those into deploy columns is wrong and reconcile bounces them to Backlog. Gate the advancement on issue.state == CLOSED. * sec(ci): restore the actions/checkout SHA pin (backend#1491 D10) This branch changed `actions/checkout@11d5960a…` to the mutable `@v4` tag in advance-deploy-env.yml — the most-consumed reusable workflow in the org, and one that runs with PROJECTS_KANBAN_TOKEN in scope. A floating major tag can be repointed by the upstream owner at any commit, which is exactly what D10 pinning removes. Nothing in CI would have caught it: tracebloc/.github runs only its own actionlint.yml, which does not inspect action refs, and it does not call the code-quality workflow whose action-pins job exists to fail this diff. That gap is now backend#1603 (.github#171, merged) — but action-pins itself cannot arm here until #159 reaches main, so for the moment this restore is the only thing standing between the unpin and every repo that consumes these workflows at @main. No functional change to the backend#1600 fix.
…oved to PR time (backend#1563 Layer 1) (#172) * feat(ci): version-bump gate — the train's prod-hop refusal, moved to PR time (backend#1563 L1) The release train already refuses to publish changed bytes under an already-released version (promote-repo.sh:version_preflight). Correct, and days late: backend#1561 spent a whole release leg discovering that py-package's pyproject.toml still said 0.17.0 while v0.17.0 was tagged and the delta touched tracebloc/*. This asks the same question on the PR that causes it. delta touches publish_paths AND v<version-on-this-PR> is already tagged -> red Not "the version file changed vs base", which #1563 words it as: that would demand a bump on every published-path PR, and one bump has always covered a whole release cycle. A check that is wrong on the common case gets removed. Parity is the point, so read_version and publishable_delta are copied from promote-repo.sh rule for rule — the per-extension anchors included, since those are what keep appVersion, a dependency pin and a leading comment out of the answer. Two deliberate divergences, both strictly tighter: `set -f` (a publish path is a glob, and promote-repo.sh only survives it by running where no publish path exists), and one paginated tag listing instead of a per-version git/ref probe that cannot tell 404 from 403. Fails closed on every unreadable input, and `soft-fail` does not reach any of them: it downgrades the verdict, never the machinery. Zero files scanned is a malfunction. No paths: filter and no job-level if:, so a required check can never go missing on a PR. 45-case decision table in scripts/tests/, run offline against a stubbed gh, covering every fail-closed path and all five repos' version file formats. Callers follow in one PR per repo, once this is on main. * sec(version-gate): a branch name is not a credential — tighten the promotion exemption Self-review before Bugbot. The release-train exemption keyed on the head ref alone, and a head ref is chosen by whoever opens the PR: `release-train/anything` was an exemption anybody could mint, from a fork included. It only skipped a version check the train re-runs at the prod hop, so nothing could actually publish through it — but "the gate is disarmed by naming your branch" is not a property to leave in a gate. Now all three must hold, and the extra two are facts about a real promotion rather than claims made by its author: head ref prefix, head in THIS repository (never a fork), base in staging/main/master. A promotion never targets develop, so a PR that borrows the name to reach develop is evaluated normally — with a warning naming the mismatch, because a bypass that does not apply should be visible rather than silently absent. Also: an EMPTY version-file read is retried on purpose (it is the exact symptom of the Contents API lag that left py-package untagged on 2026-07-29), so the message after three failed attempts now names both possibilities instead of saying only "could not read". 50 cases, 4 of them new and all four red before this change. * sec(version-gate): the fork test demands a confirmed false, not merely 'not true' head.repo is absent on a PR from a deleted fork, so IS_FORK arrives empty and 'not true' read that as 'same repo' — granting the promotion exemption on a field nobody could read. Exempting on an unread field is the shape of every fail-open in backend#1409. Three-way now: false exempts, true says fork, anything else says the head repository could not be confirmed. Evaluating a genuine promotion by mistake costs a re-run; exempting a fork by mistake costs the gate. * fix(version-bump-gate): head -1 the version parse; grep -oE prints per match Bugbot, .github#172. The yaml/toml/py branches take `head -1` on the LINE, then run `grep -oE "$semver"` on it -- and grep -oE prints one line per match, not per input line. A version line carrying a second semver in a trailing comment version = "1.2.3" # bumped from 1.0.0 therefore parses to the two-line value "1.2.3\n1.0.0". Measured, not theorised. Here that corrupts the regression guard: `sort -V | tail -1` answers 1.2.3 while `"$HEAD_VER"` is the whole two-line string, so the `!=` compare is true and the gate refuses a perfectly legitimate bump. The bare-file branch already guarded this with a trailing head -1; the other three did not. The json branch is safe -- jq emits a single value and the pattern is anchored with ^ -- so it is left alone rather than given a cosmetic head -1. This same defect is in promote-repo.sh:read_version, which this file copies rule for rule; it is fixed there in release-train#40. In the train the consequence is worse than a false refusal: the parsed value is interpolated into the tag-probe URL, a malformed URL 404s, and the caller reads that as "this version is not released" -- fail-OPEN on the one check that stops a re-publish under an existing tag.
…(backend#1602) (#170) * feat(standards): org-standards canon + CLAUDE.md sync guard (backend#1602) One canonical org-standards.md — the org's working rules — stamped into every active repo's CLAUDE.md between org-standards markers, so every Claude Code session in every repo loads the same rules. standards-sync.py audits the fleet develop-first (weekly report-only; a create-prs dispatch opens per-repo sync PRs against develop), with caller-drift's design rules: never all-clear from a failed read, absence never implicit (devex-bootstrap exempt with written reason), malformed markers reported and never auto-spliced. Offline selftest: 19 checks over the splice logic and every fail-closed path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(standards): install PyYAML in the selftest job; exact-ref branch existence (backend#1602) Both from Bugbot on .github#170. The selftest's good-path load_targets check parses YAML, and a clean setup-python has no PyYAML — the job died at the final check (mirrors caller-drift's selftest job now). And resolve_branch now uses the exact-match git/ref/heads/develop lookup, the same endpoint remediate() trusts for the base sha. Verified live: backend resolves develop, release-train falls back to main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(standards): a guard crash exits 2 (could not evaluate), never 1 (drift) (backend#1602) Bugbot on .github#170: Python exits 1 on an unhandled traceback, and 1 is reserved for confirmed drift — so a malformed API payload or filesystem error produced a drift report instead of the unknown-state failure. The entry point now maps any crash to exit 2 with an ::error:: line. Pinned by a selftest that runs the guard with gh stripped from PATH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Managed sync from tracebloc/.github/org-standards.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on a failed audit (#173) * feat(ci): required conformance gate — a contract change cannot merge on a failed audit caller-drift.yml has run on PRs touching repo-inventory.yml since backend#1415, and it correctly fails when the inventory disagrees with reality. But it is not a required status check: .github/develop requires only actionlint. So a PR that adds a repo to the train with drift went RED and could still be merged. The gate existed in trigger form and had no teeth. It cannot simply be marked required as-is. It carries a paths: filter, and a path-filtered required check never reports on PRs that miss the filter, leaving them pending forever - the trap actionlint.yml's own header documents. Dropping the filter would instead run a 20-repo API audit on every PR in this repo, which is the reason the filter is there. So this adds a job that ALWAYS runs and ALWAYS reports. On a PR that does not touch the contract it is green in seconds with zero audit API calls. On a PR that does, it requires caller-drift's verdict for that exact head sha. It keys on the WORKFLOW FILE, not the check name. Check-run names are not unique across workflows - caller-drift.yml and standards-sync.yml both expose a job called selftest - so polling by name would happily accept the wrong workflow's verdict. Querying actions/workflows/caller-drift.yml/runs?head_sha=... cannot be confused that way. Fail-closed on every non-success branch, each with a named reason: no run found within the poll budget, a non-success conclusion, an unreadable API, or a changed-file list that came back empty (unreadable, not "no files"). The --paginate on the files query matters: a PR over one page would otherwise look like it touches only its first 30 files, and a truncated read reporting "not guarded" is precisely the silent pass this file exists to prevent. The path match is exact (grep -qxF), verified offline against the cases that matter: docs/repo-inventory.yml and repo-inventory.yml.bak do NOT count as touching the contract. This PR edits repo-inventory.yml's header on purpose, so it exercises its own guarded path rather than shipping a gate whose interesting branch never ran. Refs backend#1608, backend#1415. * fix(inventory): .github's code-quality caller is required, not exempt Caught by the gate this PR adds, on its own first run. The audit reported: .github: code-quality.yml is marked `exempt` but a caller exists (code-quality-caller.yml). The exemption is stale. Correct, and the drift is an hour old: .github#171 landed the caller for backend#1603 and never flipped this entry. That is precisely the follow-up caller-drift.py's docstring tells you to do -- "land the caller first, flip the entry in a follow-up" -- because the inventory is read from the checkout while repo state is read from the audit branch, so doing both in one commit fails. So the sequence worked exactly as designed: the caller landed, the entry went stale, and the very next inventory-touching PR was refused until it was reconciled. That is the whole point of the gate, demonstrated on itself rather than asserted in a comment. The exemption text is replaced with the remediation history rather than deleted, matching how the pii-gate and e2e-test-agent entries record theirs -- the reason the exemption existed is evidence, not noise. * fix(gate): here-string, not a pipe — SIGPIPE under pipefail turned a hit into a miss Bugbot, .github#173. Reproduced before fixing: 20,000 filenames (~349 KB) with the guarded path FIRST printf '%s\n' "$FILES" | grep -qxF repo-inventory.yml -> pipeline rc 141, `if` takes the else branch, gate reports NOT GUARDED grep -q exits on its first match and closes the pipe. With enough left to write, printf takes SIGPIPE and exits 141; pipefail then makes the PIPELINE 141, so a real match reads as a miss. The gate would report "not guarded" and pass GREEN on precisely the PRs most likely to matter -- the failure gets MORE likely as the diff gets bigger, and it fails OPEN, which is the one direction this file exists to rule out. A here-string is a single command, so the status is grep's own. Verified: same input, same guarded path, correctly GUARDED. Swept the rest of the repo for the pattern. fr-gate.yml has two instances (`echo "$LABELS" | grep -q ...`), and both are safe on two independent counts: a PR's label JSON is orders of magnitude under the 64 KB pipe buffer, so echo finishes before grep can exit; and a false negative there means the gate does NOT skip, i.e. it fails CLOSED. Same pattern, opposite consequence -- left alone deliberately rather than changed for symmetry. The `| head -1` chains in version-bump-gate.yml and the kanban workflows are likewise safe: each is either terminated by `|| true` or fed by output far under the buffer. Refs backend#1608, backend#1409 (same defect class in the PII gate). * fix(gate): renames escaped the guard; poll budget was shorter than what it waits for Two Bugbot findings on .github#173. HIGH - a rename escaped the contract guard. The changed-file probe read only .filename, and a rename reports the NEW path there. So `git mv repo-inventory.yml elsewhere.yml` sailed through as "not guarded": the contract file moves out from under the audit and the gate waves it past - precisely the silent pass this workflow exists to close. Now reads BOTH .filename and .previous_filename, which is the same reason promote-repo.sh's publishable_delta matches both fields (a file renamed OUT of a published tree is still a publish-path change). I had read that comment while building the version-bump gate and failed to carry it across. Proven on a synthetic rename payload, since no open PR has one: old jq (.filename only) -> NOT GUARDED <- the hole new jq (+ .previous_filename) -> GUARDED and verified against a real PR that the `// empty` guard emits nothing extra when previous_filename is absent. MEDIUM - the poll budget was shorter than the thing it waits for. 40x30s = 20 minutes, while caller-drift's worst case is selftest (timeout 5m) + audit (timeout 30m) = 35m. A legitimately slow audit would have made the gate go red as though no verdict ever arrived, blocking a contract change until someone re-ran it by hand. Fail-closed is right; fail-closed on a budget that cannot cover the wait is just fail-annoying, and a gate people have to re-run by hand is a gate they campaign to remove. Budget is now 80x30s = 40m with the job timeout raised to 45m so the named error message surfaces rather than an opaque job timeout. Refs backend#1608.
… reports on this PR (backend#1608)
…backend#1603 step 2) (#178) The second half of backend#1603. When code-quality-caller.yml landed in #171 it could not arm action-pins: the job and its two inputs were added in #159 and existed on `develop` only, and a caller must reference @main (Q3). Passing an input the @main callee does not declare kills the entire reusable call with a startup_failure - not the one job, the whole call - measured on that branch's first push, run 31086491251. #159 has since promoted, and `main` now carries the six-job version with both inputs. Verified against the @main copy before flipping, rather than assuming the promotion carried what it looks like it carried. Hard-armed rather than left to inherit soft-fail. This is the check whose absence let .github#168 revert actions/checkout from a pinned commit SHA to the mutable @v4 tag with nothing in CI to object - in advance-deploy-env.yml, the most-consumed reusable workflow in the org, running with PROJECTS_KANBAN_TOKEN in scope. Every other repo consumes these workflows at @main and inherits whatever refs they pin, so this repo has a specific duty to enforce the rule it publishes. Re-measured the tree before arming: 24 `uses:` refs across 23 workflows, zero violations. Arming imports no backlog.
…-repo default reviewer @saqlainsyed007 raised both on the sync PRs (backend#1611, averaging-service#308), and both are source-side: the block must not be hand-edited in a consuming repo. 1. The block opened with an H1 while every consuming CLAUDE.md already has '# CLAUDE.md' as its sole title. Two H1s break the outline and trip markdownlint MD025. Every heading is demoted one level so the standards nest as a section under the host document's title. 2. 'Each repo's CLAUDE.md names its default reviewer' is false fleet-wide -- no repo names one -- and it contradicts the org's actual model, which is author-picks-reviewer with no automation assigning anyone. The line now describes what we really do, so the rule above it is actionable. Selftest: 20 checks, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…github#166)
The closing-issue advancement swallowed gh api failures into {} (a token/rate-limit/
permission error read as 'closes no issues' and silently skipped advancement, run
green) and capped closingIssuesReferences at 30 with no totalCount (a PR closing >30
silently truncated). Now: fail-closed (RUN_FAILED=1 + continue) on any lookup failure,
first:100 + totalCount surfacing on truncation, projectItems first:20.release-train: develop -> staging
Removes this repo's caller for the public PII gate. Lukas's decision, 2026-08-06: retire the gate for now rather than leave it in place undeployed. WHY. The gate has never had a denylist. PII_DENYLIST does not exist as an org Actions secret and never has, so the workflow correctly failed closed on every PR - "an inactive guard must be visible as red, not invisible as green", as its own header puts it. Applied to a secret that was never provisioned, that produced a permanent red across all 7 public repos which blocked nothing: 24 open PRs carried it, and the whole org - this author included - had learned to filter it out. A check that is red on every PR is not fail-closed; it is off with extra steps, and it would be indistinguishable from noise on the day it went red for a real reason. WHAT LAPSES, stated plainly. The gate grepped PR title + body + every commit message against a private list of customer/partner names, on public repos only. It never read code, diffs or filenames, and detected no PII patterns - secrets in code remain gitleaks' job. So what lapses is the mechanical backstop for CLAUDE.md's rule that internal work and customer mentions never go in a public repo. That rule still stands and is now enforced by review alone. The reusable workflow and its selftest stay in tracebloc/.github, dormant with no callers. Restoring is three steps: set the org secret, restore this file, flip the inventory entry back to required. Checklist and the denylist-content caveat are on backend#1409. Not required on any branch, so this removes no merge gate.
…lCount (Bugbot .github#181) The v1 fail-closed reddened every promotion carrying a PR_NUMBERS entry that isn't actually a PR (issue ref / bogus number). Capture output and skip QUIETLY on 'Could not resolve to a PullRequest'/Not Found, fail CLOSED only on a real infra failure. Add the projectItems totalCount check for parity with the closingIssuesReferences fix.
…d-reviewer-claim docs(standards): fix the block's H1 and the false default-reviewer claim (backend#1602)
Managed sync from tracebloc/.github/org-standards.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…409) (#183) Completes the retirement Lukas decided on 2026-08-06. The 7 callers were removed first (cli#466, client#638, docs#96, data-ingestors#462, model-zoo#130, start-training#44, .github#182, all merged); this drops the reusable from the inventory so the contract matches reality. ORDERING MATTERS AND WAS NOT INCIDENTAL. The audit reads the inventory from the checkout but every repo's state over the API, so callers and inventory cannot change in one commit - the docstring says as much for the ADD direction, and the remove direction has the same hazard inverted. Callers first, then this. In between, the audit would have reported "MISSING required caller" for 7 repos; that window is now closed. Verified before pushing by running caller-drift.py locally against the live fleet: "20 repos x 8 reusables + 2 copies. Audited 20 of 20. No drift." Removed: - public-pii-gate.yml from the `reusables` list - all 20 per-repo `callers` entries for it (7 required, 13 exempt) - the `private_repo_no_public_exposure` shared reason, now referenced by nothing - the stale claim in the MATCHING header that public-pii-gate uses the `-caller.yml` convention; it no longer has callers to have a convention The .github entry's long red-inert explanation is replaced by a short retirement note rather than deleted outright: WHY a guard was withdrawn is the part a future reader needs, and it is the only place that records that the denylist never existed. The reusable workflow and its selftest stay in this repo, dormant with no callers, so restoring is three steps rather than a rebuild.
…gbot .github#181) 'Not Found' is also what a scoped-down/expired token returns, so skipping it as benign re-opened the fail-open. Match only 'Could not resolve to a PullRequest'; any access/transport failure now falls through to RUN_FAILED.
…ub#181) Bare cis_out=$(...) under set -euo pipefail aborts the step on a non-zero substitution (High) -- restore the 'if ! cis_out=$(...)' form the per-issue lookup already uses. And printf|grep -q can return 141 via SIGPIPE under pipefail even on a match (Medium) -- use a here-string so grep's own exit status is what's tested.
release-train: develop -> staging
…(backend#1630) Caught by Bugbot on the model-zoo staging mirror (model-zoo#131, held): the same promotion that deletes public-pii-gate-caller.yml also carries a CLAUDE.md asserting "gitleaks and the PII gate will catch it". The gate was retired earlier today by Lukas's decision (backend#1409); this line was not updated with it. That is a violation of this file's own rule, which I wrote: "If your change makes a statement in any CLAUDE.md, BUGBOT.md, or runbook false, update that file in the same PR." I retired the gate and left the canon claiming it protects - and this canon syncs into every repo's CLAUDE.md, so the false claim was queued to propagate to 19 repos. The replacement text does not just delete the reference; it names the resulting gap. gitleaks still covers secrets in CODE. Nothing now scans PR titles, descriptions or commit messages, which is exactly what the retired gate did and exactly where a customer name is easiest to leak on a public repo. A reader who only saw "gitleaks catches it" would reasonably assume the prose is covered too. Refs backend#1630, backend#1409.
LukasWodka
commented
Aug 10, 2026
ContributorAuthor
bugbot run |
Uh oh!
There was an error while loading. Please reload this page.
…taging" (backend#1592 step 1) Step 1 of the three-step rename. Nothing changes behaviourally: this teaches all five reusables to prefer the NEW column name and accept the OLD one, so they work on both sides of the rename instant. Step 2 is the UI rename (Lukas), step 3 removes the fallback. WHY A FALLBACK RATHER THAN A SWAP. A rename cannot be done reads-before-writes. Adding a column can - teach everything to read the new name, then start writing it - but a rename makes the old name stop existing at a single instant, with no period where both are present. Every `select(.name=="FR on staging")` would resolve to null on the far side of that instant, and null resolution in these workflows means "abort" or, worse, "skip silently". So each resolver asks for the new name and falls back to the old, and each comparator accepts both. WHY THE UI, NOT THE API - and this is measured, not cautious. The ticket records that `updateProjectV2Field` replaces the whole option set and re-mints every ID: on 2026-08-05, adding ONE option preserved 0 of 11 option IDs and blanked 625 of 625 items, recoverable only because a name-keyed backup existed (#1576). The UI renames in place, preserving the option ID and every item's value. Step 2 is therefore deliberately a human action. Changed, by kind rather than by file: RESOLVERS (name -> option id), given a new->old fallback: advance-deploy-env.yml, kanban-closure-router.yml, kanban-reconcile.yml (via a new opt_either helper) COMPARATORS (status name read back from the board), now accepting both: advance-deploy-env.yml override map + rank(), fr-gate.yml rank(), kanban-reconcile.yml x3, kanban-closure-router.yml, fr-pass-comment.yml WRITE SITES, now naming the new column: kanban-closure-router.yml staging branch, advance-deploy-env.yml staging EXISTENCE PROBE: fr-pass-comment.yml resolves whichever staging-review column exists and keeps its loud failure for the case where NEITHER does - which is what that block was written for, and its comment already anticipated this rename. Both rank() functions score the two names identically, so the monotonic never-demote guard cannot see a rename as a backwards move. VERIFIED against live board data in both states, not just reasoned about: today (only the old name exists): opt_either -> d3caf1d4 after rename (name swapped): opt_either -> d3caf1d4 live id of "FR on staging": d3caf1d4 Same id either way, which is exactly what the in-place UI rename preserves. 18 literal references remain in comments and user-facing message strings. Those are correct as they stand today and become wrong only after step 2, so they are step 3's work rather than a partial edit now.
Removes the public PII gate in full, per Lukas (2026-08-10). The gate was
already inert — parked under backend#1409 (PII_DENYLIST was never provisioned,
so it failed closed on every public-repo PR while being a required check
nowhere) and its public callers were removed org-wide. This deletes what
remained:
- .github/workflows/public-pii-gate.yml (the reusable gate)
- .github/workflows/public-pii-gate-selftest.yml (its decision-table selftest)
- scripts/tests/pii-gate-selftest.sh (the selftest harness)
Also fixes the references that would otherwise dangle at deleted files:
- code-quality-caller.yml / code-quality.yml: drop pointers to the removed
gate + selftest
- version-bump-gate.yml: keep the override-ordering + grep-error lessons but
past-tense them ("the retired public-pii-gate")
- caller-drift.py: the visibility-mismatch finding no longer claims the pii
gate is required
Kept as history: the repo-inventory retirement tombstone and the
conformance-gate defect-class lesson (both reference backend#1409, not a live
file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>docs(claude): sync org-standards block (backend#1602)
chore(ci): retire the public PII gate everywhere
docs(standards): the PII gate no longer exists, so stop promising it (backend#1630)
…ed-totalcount fix(kanban): fail-closed closing-issue lookups + totalCount (Bugbot .github#166)
…-fallback feat(kanban): resolve "Staging (human review)", fall back to "FR on staging" (backend#1592 step 1)
…hipped (backend#1563 Layer 2) (#189) * feat(release): open a version bump PR when the declared version has shipped (backend#1563 Layer 2) The train reads version_file as the DECLARED NEXT RELEASE, cuts vX.Y.Z from it, and never writes it back. So the instant a release succeeds the file names the version that just shipped, and the NEXT prod hop hits version_preflight's "v$ver already exists and the delta touches published files" refusal. Every successful release arms the next one to fail (backend#1440). It stalled the tracebloc-py-package prod leg on 2026-08-05 and cost a release leg plus a hand diagnosis before anyone realised the train was right and the file was stale (backend#1561). Layer 1 (.github#172) cannot catch this case. It fails a PR whose author changed published files without bumping - but here nobody authored anything. The staleness is created BY the release. VERIFIED AGAINST LIVE STATE, and it found the condition already present: tracebloc-py-package develop declares 0.18.0 highest released final tag v0.18.0 -> already shipped; this would propose 0.18.0 -> 0.18.1 today So the pilot repo is sitting in exactly the state that stalls the next hop. WHAT IT DOES NOT DO. It never tags and never publishes. backend#1563 names "two competing publishers" as the failure to avoid, and the train owns the prod tag plus the release-managers token in the tag ruleset. This opens a PR; a human merges it. That is also why release-please was not adopted - we want its "open the bump PR" half and specifically not its "tag and publish" half. A PR rather than a push because develop is protected everywhere, and a push would need admin bypass, making this a second unreviewed writer to develop. Patch is a FLOOR, not a judgement: it unblocks the hop, and a human wanting a minor or major edits the PR. Computing the bump from commit types needs Conventional Commits, which #1563 sequences separately and no repo has adopted. Parsing is read_version's case table copied rule for rule, including the trailing `head -1` on every branch - `grep -oE` prints one line per MATCH, so a version line with a trailing comment otherwise yields a two-line value (release-train#41). A parser that disagreed with the train's would propose a bump the train then rejects. It refuses rather than guesses: a missing or unparseable version file, an unreadable tag list, an unreadable compare, a released version that is not X.Y.Z, or a write that does not read back as intended all exit non-zero with a named reason. The read-back check matters most - a sed that silently matched nothing would otherwise open a PR containing no change at all. Does nothing in the common cases: declared already ahead of every release, no publishable path touched since the release, or a bump PR already open. Caller for the py-package pilot follows once this reaches main - a caller pins @main, and passing inputs a @main callee does not declare kills the whole call with startup_failure. * fix: refuse at the compare file cap instead of reading it as "nothing published changed" Bugbot, .github#189. The compare endpoint caps `files` at 300 and does NOT paginate that array - `--paginate` walks commits, not files - and there is no truncation flag. So "no publish-path match" was not something this could assert, and asserting it exits 0 and SKIPS the bump PR, leaving exactly the stall this workflow exists to clear. A silent skip is the worst possible failure for this particular job. Layer 1 already refuses at that cap. The two were inconsistent, which is the thing I keep insisting on avoiding: a guard that disagrees with the gate it front-runs is worse than no guard, and here the two halves of the SAME ticket disagreed about what a truncated compare means. Three changes, all mirroring Layer 1 rather than inventing a second rule: - EMPTY file list is now a refusal. GitHub omits `files` entirely on very large compares, so empty cannot be told apart from "nothing changed", and zero files scanned is a malfunction rather than a clean answer. - ONE LINE PER FILE, tab-separated (filename + previous_filename), instead of one line per path. The cap check counts lines, and emitting two lines for a rename would make it refuse at fewer than 300 real files. Verified: three files including one rename counts as 3, not 4. - THE CAP ITSELF: 300 files with no match refuses. Note the asymmetry, which is why the check sits only on the no-match branch - a MATCH is sound on a truncated list, so a large compare that does match still gets a precise answer instead of a refusal. The rewrite also fixes a bug Bugbot did not mention: the old loop was `for f in $FILES`, which word-splits, so any path containing a space became two bogus entries. It now reads tab-separated fields, and matches the pre-rename path too - a file moved OUT of a published tree left the package, and the compare reports only its new path in the entry. Verified: actionlint clean, shellcheck clean at error level, `bash -n` clean on the extracted script, and the heredocs resolve to column 0 after YAML strips the block indentation. * fix: require a PAT for the bump PR — GITHUB_TOKEN-authored PRs get no checks Bugbot, .github#189, and it defeats the entire design rather than degrading it. Events authored with GITHUB_TOKEN do not start workflow runs. So a bump PR opened with the ambient token gets ZERO checks - and on a repo with required checks it is unmergeable forever. Measured on the pilot repo: tracebloc-py-package develop requires 6 checks: tests (3.11), tests (3.12), quality / gitleaks, quality / house-rules, quality / ruff, quality / shellcheck None of them would ever report. The PR would sit until someone merged it with admin bypass - which is exactly the thing this workflow opens a PR to avoid. It would have swapped "a push needing admin bypass" for "a PR needing admin bypass", while looking safer. The release train hit this first and its comment says why: "Bugbot only auto-reviews HUMAN-authored PRs, and settle/release now require a review". It uses RELEASE_TRAIN_PR_TOKEN. Same fix here, same reasoning, same secret available. - `pr-token` is a REQUIRED secret, deliberately not defaulted to github.token. A default would reintroduce the bug silently in any caller that forgot it. - checkout takes the PAT, so the credential the push uses belongs to it - what decides whether a push starts runs is the identity that performed it, not the commit author. - Only the two WRITE operations use it, named at their call sites the way promote-repo.sh does. Reads stay on the ambient token. - An empty token refuses UP FRONT, before the branch exists, rather than after - discovering it later leaves a pushed branch and no PR. Verified: actionlint clean, bash -n and shellcheck clean on the extracted script, and the workflow_call contract parses with pr-token required: true. * fix: an empty compare has two opposite causes — stop treating both as failure Bugbot, .github#189. I collapsed "nothing has landed since the release" and "GitHub omitted the file array" into one hard error. They are opposites: ahead_by == 0 $BASE holds nothing the tag does not. Right after a release that is the NORMAL state, and there is genuinely nothing to bump. files omitted dropped entirely on very large compares. Nothing was scanned, so no conclusion about published paths is available. Treating the first as a malfunction fails this job RED on an ordinary post-release day. Treating the second as clean skips a bump that is owed. The previous commit did the former, which is the noisier half of exactly the distinction I added the truncation guard to make. Fixed with `ahead_by` from the same response, confirmed by `files` being NULL (absent) rather than []. Verified on real compares: v0.18.0...v0.18.0 status=identical ahead_by=0 files=present(0) -> exits 0 quietly (previously: RED) v0.18.0...develop status=diverged ahead_by=3 files=present(9) -> proceeds, scans 9 files Note the ordering matters and the test found why: an `identical` compare returns files as a PRESENT but EMPTY array, so the absent-check would not have caught it. `ahead_by` has to be consulted first. The compare is now fetched ONCE and kept. Three questions are answered from it - ahead_by, files-present, and the file list itself - and re-fetching could observe three different states. Also handles ahead-by-real-commits-with-an-empty-files-array (empty or merge-only commits): nothing published can have changed, so that is a clean "nothing to do" rather than a refusal. * fix: refuse empty inputs — an empty publish-paths silently skips every bump Bugbot, .github#189. `required: true` on a workflow_call input still permits "". With an empty publish-paths, `for pat in $PUBLISH_PATHS` never iterates, MATCHED stays empty, and the job exits 0 as though nothing publishable changed - so a caller that forgot the input gets a workflow that runs, reports success, and never opens a bump. Demonstrated: PUBLISH_PATHS="" -> MATCHED=<empty> (exits 0, skips) PUBLISH_PATHS="tracebloc/* pyproject.toml" -> MATCHED=hit Layer 1 refuses the same case, in the same words: absent is never permission to skip the version guard (backend#1439). Mirrored here rather than invented. Guarded version-file and base-branch too, on the same reasoning. An empty version-file previously fell through to the `[ ! -f "" ]` check and refused with "does not exist", which is true but points the reader at the wrong problem. This is the FOURTH finding on this PR and the fourth of the same species: an empty or absent thing being read as a verified negative. Truncated compare read as clean. Empty compare read as failure. And now an empty input read as "nothing matched". The logic each was wrapped around was fine every time. * fix: read-only permissions — a write grant would hide the mistake pr-token guards Bugbot, .github#189. The permissions block granted contents:write and pull-requests:write to GITHUB_TOKEN, but nothing uses the ambient token to write: the branch push uses the credential checkout persisted from pr-token, and gh pr create names PR_TOKEN explicitly. Audited every call - the three ambient-token operations are matching-refs, compare and pr list, all reads. So the grant bought nothing and cost the fail-fast. A future edit that forgot to name PR_TOKEN would SUCCEED against the ambient token instead of taking a 403 - and that produces a PR authored by GITHUB_TOKEN, which starts no workflow runs, so its required checks never report and it can never merge. That is precisely the failure the pr-token guard was added to prevent two commits ago. A permissions block that lets it through quietly is worse than one that stops it loudly. It also contradicted the comment directly above it, which claims reads run on the ambient token for least privilege. The comment was right and the grant was not. Now contents:read + pull-requests:read, with the reasoning recorded at the block rather than in a commit message nobody will find. * fix: the idempotency probe refuses with a named reason like its siblings Bugbot, .github#189. The open-PR check used a bare assignment while matching-refs and compare both use `if ! var=$(...)` with an ::error::. Under set -e the bare form still fails the step, so this is about the OPERATOR rather than about safety: the run died at an unnamed line, breaking this file's own "every failed read exits with a named reason" contract. The reason matters here more than the consistency does. An unreadable PR list is specifically dangerous to guess at: assuming "none open" would open a SECOND bump PR alongside the existing one, and two PRs proposing different versions of the same file is worse than none. Audited the rest while in there - 3 guarded reads, 0 bare `gh` assignments left. * fix: measure the delta from the DECLARED version's tag, not the latest release Bugbot, .github#189. version_preflight refuses on "v$DECLARED already exists AND the delta touches published files" - it keys on the DECLARED version. I was measuring from v$LATEST, which asks a question the train never asks. The two are the same tag in the common case (declared == latest) and diverge exactly when the file has fallen BEHIND a release - reverted, or a stale branch. Measured on the pilot repo, which shows the size of the gap: DECLARED=0.18.0 (== latest) -> base v0.18.0, develop ahead_by=3 DECLARED=0.17.0 (behind) -> base v0.17.0, develop ahead_by=15 Five times the window. In the behind case the old code inspected 3 commits and could answer "nothing publishable changed" about a window the train is not looking at, while version_preflight tested 15 commits' worth of delta against v0.17.0 and stalled. Falls back to v$LATEST when the declared version is not itself tagged: it is <= LATEST and unreleased, so the newest thing that IS released is the honest baseline for "what changed since we last shipped". All eight compare messages now name the actual base rather than hardcoding v$LATEST, so the log says which tag the answer is about. * fix: derive the declared tag's existence from the guarded read, not a second probe Bugbot, .github#189 - and this one I introduced in the previous commit while fixing finding #7. That commit added `gh api git/ref/tags/v$DECLARED >/dev/null 2>&1` and treated ANY non-zero as "not tagged". A 404 means untagged; a rate limit or a 5xx means unknown. Collapsing them makes a transient failure silently select v$LATEST as the compare baseline - which is exactly the wrong-baseline fail-open that finding #7 was about. I closed a fail-open by adding a smaller one. The fix is not a better probe, it is no probe: $REFS from the matching-refs read above ALREADY lists every v* tag, and that read already refuses on failure. So the question is answered from data whose failure mode is handled, and there is no second failure mode to get wrong. Uses a here-string rather than `printf | grep -q`: under pipefail grep -q closes the pipe on its first match, the producer takes SIGPIPE, and a real hit returns 141 and reads as a miss. That is the .github#173 defect, and piping here would have reintroduced it in a place where a miss silently changes the baseline. Verified against real tag data: v0.18.0 and v0.17.0 resolve to themselves, v0.99.0 falls back. One matching-refs call total; the extra probe is gone.
…kanban card (#193) Bugbot .github#190 (Medium). The projectItems totalCount>20 guard only warned, so if an issue is on >20 projects and its engineering-kanban card sits beyond the first 20 we fetch, ITEM_ID comes back empty and the card is silently skipped as "not on project" while the run stays green — the exact silent-skip the guard was meant to close (the sibling closingIssuesReferences guard already fails closed). Move the truncation check into the not-found branch and set RUN_FAILED there: we only fail when the card was actually not found AND the list was truncated (pi_total>20), so a normal issue-on-<=20-projects still skips cleanly with no false failures. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
release-train: develop -> staging
LukasWodka
commented
Aug 10, 2026
ContributorAuthor
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d09d208. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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.

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed
release-train/to-mainbranch (a mirror ofstaging), so it never collides with a human PR. Merged only when the fr-gate is green.Note
High Risk
Wide blast radius on kanban deploy automation (token-backed GraphQL writes), removal of the public PII gate, and new fail-closed gates that can block merges or train promotions if misconfigured.
Overview
This promotion ships a large batch of tracebloc/.github automation and governance changes that affect every repo consuming these reusables.
Kanban / deploy advancement renames the staging review column to Staging (human review) (with fallbacks for FR on staging during the board rename) across
advance-deploy-env, closure routing, FR pass, reconcile, and related rank logic.advance-deploy-envgains adry_runinput and now advances issues closed by promoted PRs (including cross-repo), with stricter fail-closed handling for lookup errors and pagination limits.Release versioning (backend#1563) adds reusable
version-bump-gate(PR-time check aligned with release-trainversion_preflight) andversion-bump-pr(opens a bump PR when develop is stale after a release), plus a selftest workflow. README onboarding now tells train repos to wire the gate caller.Org contract enforcement adds
conformance-gate(required check that pollscaller-driftwhenrepo-inventory.ymlor drift tooling changes),standards-syncwithorg-standards.md/scripts/standards-sync.py, and a managed block in this repo’sCLAUDE.md.repo-inventory.ymldropspublic-pii-gateeverywhere, marks.githubas running code-quality, and setse2e-test-agentrelease_train: true.Other changes:
code-quality-caller.ymlso this repo runs its own quality gate (including hard action-pins);kanban-reconcileapplieskeep-opento open North Stars issues before stale sweep;customer-priority-bumpis simplified to label-only (no project field inputs);public-pii-gateworkflow and caller are removed (documented as retired; PR prose scanning is no longer automated).Reviewed by Cursor Bugbot for commit d09d208. Bugbot is set up for automated code reviews on this repo. Configure here.