tooling: answer "is this file claimed?" in one command (#3175)
Addresses the root cause of a pattern I have now measured four times. ## The finding **Every fleet worker pushes as the same GitHub account.** `gh pr list --author "@me"` returns **all 17 open PRs** — mine and teammates' are indistinguishable. So "is this file already being converted?" can only be answered by fetching every open PR's file list by hand: 25+ API calls that no worker makes before starting. I didn't either. ## The measured cost | PR | Outcome | Landed instead as | |---|---|---| | #3096 | shrank to a test | teammate's serialisation + union | | #3116 | shrank to a test | `preExecLiveColumns`, `starvedWaitingColumns`, … | | #3140 | shrank to a test | #3137 | | #3125 | **shrank to nothing — closed** | #3135 | Plus #3118, a teammate independently writing the same coverage I wrote for #3112. **In every case both implementations were correct and independently reached the same design** — #3137 chose payload-first-with-sync-fallback for the same reason I did. This is not carelessness; the fleet is doing correct work twice and discovering coverage gaps by accident, when rebases collide. ## What this adds ``` $ node scripts/check-file-claimed.mjs packages/engine/src/self-healing.ts CLAIMED packages/engine/src/self-healing.ts #3152 fix(self-healing): 18 recovery rebounds hardcoded `todo` … ``` **On its first run it reported `self-healing.ts` claimed by #3152 — which I had no way to know a moment earlier.** Exits non-zero when claimed, so it can gate work: `node scripts/check-file-claimed.mjs <path> && start-work`. ## Deliberate limits - **It cannot see unpushed work**, so it narrows the collision window rather than closing it. Two workers starting the same file minutes apart still collide. Closing that needs **distinguishable authorship** — a per-worker `Co-Authored-By` or a title prefix — which is a coordination decision, not a script. - **A `gh` failure exits 2 and says UNKNOWN, not unclaimed.** A claim check that fails open is worse than none, which is the same false-green shape this program keeps finding elsewhere. Also adds a short AGENTS.md pointer next to the other standing rules. ## Verification Run against a claimed and an unclaimed path; both answers correct, exit codes as documented. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a command-line check for open pull requests that may already be modifying specified files. * Reports matching pull request details and clearly indicates whether each file is claimed. * Returns distinct statuses for claimed files, unclaimed files, and unavailable GitHub checks. * **Documentation** * Added guidance for checking file ownership before beginning conversion work. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
30
AGENTS.md
30
AGENTS.md
@@ -143,6 +143,36 @@ pnpm verify:workspace # deep opt-in verification (lint -> test:full -> build);
|
||||
|
||||
`pnpm verify:fast` is the recommended **test-free verification** path: bootstrap missing/stale workspace dist artifacts, typecheck + build scoped to the changed packages (it reuses `pnpm test`'s changed-package resolution), an always-on `@runfusion/fusion` CLI build required by the source-checkout boot smoke, plus the boot smoke once, with **no test run**. It is deterministic and flake-free, suitable as a project `testCommand`/verification command when you want non-test verification; the full suite stays available and runs non-blocking. It is additive and does not change `pnpm test`, the gate, or CI. See `docs/testing.md`.
|
||||
|
||||
### Check whether a file is claimed before converting it
|
||||
|
||||
Every fleet worker pushes as the same GitHub account, so `gh pr list --author "@me"` returns EVERY open
|
||||
PR and cannot distinguish your work from a teammate's. Before starting a conversion, ask:
|
||||
|
||||
```bash
|
||||
node scripts/check-file-claimed.mjs packages/engine/src/self-healing.ts
|
||||
```
|
||||
|
||||
It lists the open PRs touching that path and exits non-zero if any do, so it can gate work directly.
|
||||
|
||||
It narrows the collision window rather than closing it — it cannot see unpushed work in progress.
|
||||
Measured cost of not having it: four PRs in one session were superseded by teammates landing the same
|
||||
conversion first, each time with both implementations correct and independently identical.
|
||||
|
||||
<!--
|
||||
FNXC:FleetClaims 2026-07-31-21:15: WHY THIS IS A RULE AND NOT A SUGGESTION.
|
||||
|
||||
Every worker ranks work from the same census output, so without a published claim they independently
|
||||
pick the same top file. In one fleet phase that produced three parallel conversions of
|
||||
`self-healing.ts` (two left unmergeable after the first landed), two workers marking the same two
|
||||
files, and two independent versions of the same `task:moved` emitter fix — five collisions, all with
|
||||
both sides correct.
|
||||
|
||||
The check is cheap because the claim is a pushed branch: `git ls-remote` is authoritative the moment
|
||||
work starts, whereas a claim announced anywhere else is invisible until the duplicate work exists.
|
||||
That asymmetry is the whole point — the first signal of a collision used to be a failed checkout or a
|
||||
conflicting PR, i.e. after the cost was already paid.
|
||||
-->
|
||||
|
||||
### Standing Rule: Flaky Tests Are Quarantined on Sight (Deletion Ratchet)
|
||||
|
||||
- A test observed failing without a corresponding real bug in the change is QUARANTINED ON SIGHT: add an entry to `scripts/lib/test-quarantine.json` (`file`, `reason` with a link to the failing run, `quarantinedAt`) AND a matching one-line `exclude` in that package's vitest config, in the same commit.
|
||||
|
||||
93
scripts/check-file-claimed.mjs
Normal file
93
scripts/check-file-claimed.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
FNXC:FleetCoordination 2026-07-31-06:10 (fleet):
|
||||
|
||||
WHY THIS EXISTS. Every fleet worker pushes as the SAME GitHub account, so `gh pr list --author "@me"`
|
||||
returns all 17 open PRs and no worker can tell their own from a teammate's. There is no way to ask "is
|
||||
this file already being converted?" short of fetching every open PR's file list by hand — 25+ API calls
|
||||
that a worker will not make before starting, and I did not make either.
|
||||
|
||||
MEASURED COST, not a hypothetical: four of my PRs were superseded by teammates landing the same work
|
||||
first (#3096, #3116, #3140 shrank to tests; #3125 to nothing and was closed). In every case both
|
||||
implementations were correct and independently reached the same design. The fleet is not making
|
||||
mistakes — it is doing correct work twice, and finding coverage gaps only by accident when the rebases
|
||||
collide.
|
||||
|
||||
WHAT THIS DOES. One command, one answer:
|
||||
|
||||
node scripts/check-file-claimed.mjs packages/engine/src/self-healing.ts
|
||||
|
||||
It prints the open PRs touching that path, so "claimed?" is answerable before the work starts rather
|
||||
than at rebase time.
|
||||
|
||||
WHAT IT DOES NOT DO, deliberately. It cannot see work that is in progress and unpushed, so it narrows
|
||||
the window rather than closing it. Closing it needs distinguishable authorship — a per-worker
|
||||
`Co-Authored-By` or a title prefix — which is a coordination decision, not a script. This is the part
|
||||
that can be fixed from inside the repo.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const targets = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
||||
if (targets.length === 0) {
|
||||
console.error("usage: node scripts/check-file-claimed.mjs <path> [<path>...]");
|
||||
console.error(" paths are matched as substrings of each PR's changed-file list");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function gh(args) {
|
||||
try {
|
||||
return execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
||||
} catch (err) {
|
||||
console.error(`claim-check: gh failed — ${err?.message ?? err}`);
|
||||
console.error("claim-check: cannot prove a file is unclaimed without it. Treat as UNKNOWN, not free.");
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:FleetClaims 2026-07-31-21:10 (#3175 review — coderabbitai, "detect or eliminate truncation"):
|
||||
A SILENT CAP TURNS THIS TOOL INTO THE BUG IT PREVENTS.
|
||||
|
||||
`--limit 100` returns at most 100 PRs. Past that, claims live in PRs this never sees and a claimed file
|
||||
reports UNCLAIMED — the one answer that must never be wrong here, because a worker acts on it by
|
||||
starting work someone else already started. This fleet ran 50+ open PRs at once, so the cap is not
|
||||
hypothetical.
|
||||
|
||||
Fails closed rather than warning: a warning printed above an `UNCLAIMED` line is read as noise next to
|
||||
a verdict. Same reasoning as `gh()` exiting rather than returning empty.
|
||||
*/
|
||||
const PR_LIMIT = 300;
|
||||
const open = JSON.parse(gh(["pr", "list", "--state", "open", "--limit", String(PR_LIMIT), "--json", "number,title"]));
|
||||
if (open.length >= PR_LIMIT) {
|
||||
console.error(`claim-check: ${open.length} open PRs hit the --limit ${PR_LIMIT} cap, so the list may be truncated.`);
|
||||
console.error("claim-check: cannot prove a file is unclaimed from a partial list. Treat as UNKNOWN, not free.");
|
||||
process.exit(2);
|
||||
}
|
||||
const hits = new Map(targets.map((t) => [t, []]));
|
||||
|
||||
for (const pr of open) {
|
||||
/*
|
||||
FNXC:FleetClaims 2026-07-31-21:10 (#3175 review — coderabbitai, "unreachable catch"): `gh()` exits on
|
||||
failure, so this try/catch could never fire. Removed rather than made reachable: skipping a PR whose
|
||||
diff failed is exactly how a claim goes unseen, and the fail-closed exit is already the right answer.
|
||||
*/
|
||||
const files = gh(["pr", "diff", String(pr.number), "--name-only"]).split("\n").filter(Boolean);
|
||||
for (const t of targets) {
|
||||
if (files.some((f) => f.includes(t))) hits.get(t).push(pr);
|
||||
}
|
||||
}
|
||||
|
||||
let claimed = false;
|
||||
for (const t of targets) {
|
||||
const prs = hits.get(t);
|
||||
if (prs.length === 0) {
|
||||
console.log(`UNCLAIMED ${t}`);
|
||||
continue;
|
||||
}
|
||||
claimed = true;
|
||||
console.log(`CLAIMED ${t}`);
|
||||
for (const pr of prs) console.log(` #${pr.number} ${pr.title}`);
|
||||
}
|
||||
|
||||
/* Exit 1 when anything is claimed, so a worker can gate on it: `... && start-work`. */
|
||||
process.exit(claimed ? 1 : 0);
|
||||
Reference in New Issue
Block a user