/* * IS THIS BOARD CELL'S REF A PER-AGENT ACTIVITY SIGNAL? * * One classifier, used by the CHECKER that reads the cell and the VERB that * writes it. A rule enforced at the read and not the write is a rule the write * defeats: `claim` writes these rows, so if a bad ref is refusable by * `stall_check` and writable by `claim`, the verb the fleet is being pushed * toward becomes the thing that manufactures the bad row. * * FOUR WAYS THIS ONE CELL HAS BEEN WRONG, all measured on this fleet: * * `docs/*` a PATH — refused today, correctly * `main` a SHARED ref — resolved, and reported the * whole fleet's activity as one agent's, so the * row could never stall * `worker-3/task13-…` LOCAL-ONLY — resolved for its author and for * nobody else, so the same board read * differently from two checkouts * `worker-1/task16-3-…` MERGED — resolves everywhere and is FROZEN by * construction, so it reports an ever-growing * stall on a lane that is finished * * THREE OF THOSE FOUR RESOLVED CLEANLY. So the test cannot be "does it * resolve" — that check passed on every bad case except the path. The property * is narrower and it is the one thing all four violate: * * THE CELL MUST NAME A REF WHOSE MOVEMENT IS THIS AGENT'S WORK. * * Which decomposes into three questions git can actually answer: * * scoped does the ref name begin with this agent's id? That is the ONLY * per-agent signal this repo carries. Author cannot serve: measured * on 60 commits of `origin/main`, every one is authored "David * Balzan", so an `--author` filter would match every agent on every * ref forever WHILE LOOKING SCOPED. * shared is it resolvable from ANY checkout, not just the one that happens * to have fetched it? That means the remote-tracking form. * live is it still ahead of the base? A merged branch's ref never moves * again, so measuring it produces a number that only grows. * * WHAT THIS DELIBERATELY DOES NOT DO: guess. Every rejection is reported with * the reason and the population it judged, and an UNPUSHED branch is called * unpushed rather than stalled — a freshly claimed lane has no shared evidence * yet, and saying "stalled" there would be the manufactured-narrowing error * pointed at a new lane. */ import { execFileSync } from "node:child_process"; export type BoardRefVerdict = | { kind: "measurable"; ref: string } | { kind: "empty"; why: string } /** * ⟨q-5d1c8e04⟩ — A DELIBERATE NO-BRANCH. The cell holds WORDS in the ref * position (`docs-direct · no per-agent branch`, `own clone …`): a human * statement that this lane carries no per-agent branch. Out of the stall * clock's population, never "unmeasurable" — five seats wrote this shape * because the grammar gave them no other way to say it. */ | { kind: "none"; why: string } /** A ref a human ABBREVIATED for display (`…/kit-worker-lane`) — unreadable by construction, and said so. */ | { kind: "elided"; why: string } | { kind: "path"; why: string } | { kind: "shared"; why: string } | { kind: "unscoped"; why: string } | { kind: "local-only"; why: string } | { kind: "unpushed"; why: string } | { kind: "merged"; why: string }; const git = (repo: string, args: string[]): string | null => { try { return execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch { return null; } }; const resolves = (repo: string, ref: string): boolean => !!git(repo, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]); // Task 5.3/22.1's confirmation half: `docs/*` — a real historical cell value // — is a GLOB, and `git check-ref-format` refuses it as a branch name (a `*` // is not legal in a ref). A plain path like `docs/QUEUE.md` is, frustratingly, // ALSO a syntactically valid ref name (slashes and dots are both legal), so // this catches the glob shape precisely and does not claim to catch every // path — an honest partial fix is the property this task's own title asks // for ("every classifier can say I cannot tell"), not a heuristic that // pretends to be exhaustive. // Pure syntax check — never needs to run INSIDE a repo, so no `cwd` (unlike // every other helper here, which resolves refs against `repo`'s history). const isSyntacticallyValidRef = (name: string): boolean => { try { execFileSync("git", ["check-ref-format", "--branch", name], { stdio: ["ignore", "pipe", "ignore"] }); return true; } catch { return false; } }; /* * ⟨q-5d1c8e04⟩ — THE CELL GRAMMAR, read by POSITION rather than by scanning. * * \`ref\` · the ref is the LEADING backticked token; what follows is a note * — (or blank) nothing declared — on a lane row this is "lacks a branch" * a deliberate statement: this row carries no per-agent branch * * `refInCell` used to take the FIRST backticked token ANYWHERE, so the aide's * `**own clone \`groundwork-kit-aide-write\`** (ADR-016, \`a7bd341\`)` yielded a * clone name that was then looked up as `origin/groundwork-kit-aide-write` and * reported "unpushed" — a ref manufactured from prose. Measured live: five * prose cells on one board, three of them reaching `unmeasurable` under three * different mis-parses. A position cannot be mis-parsed that way. */ export type CellKind = "ref" | "empty" | "prose"; export function cellKindOf(cell: string): CellKind { const s = String(cell ?? "").trim(); if (!s || s === "—" || s === "-" || s === "–") return "empty"; return /^`[^`]+`/.test(s) ? "ref" : "prose"; } /** The `\`ref\`` in a `Branch · Worktree` cell's REF POSITION (leading token), or "". */ export function refInCell(cell: string): string { return (String(cell ?? "").trim().match(/^`([^`]+)`/)?.[1] ?? "").trim(); } /** A display elision a human typed into a machine-read field. */ const ELIDED = /…|\.\.\./; /* * ⟨q-5d1c8e04⟩ — LANES vs ROLES. A ROLE row is a standing seat (coordinator, * aide) with nothing to score: no branch, no slice, no stall. It is written with * its own status glyph and a `—` branch cell, in the same table. The glyph is * a bus-level ROW KIND, not a work state: the seam's `workStateOf` reads it as * `unknown`, which is correct — a role is not work, and the seam is not asked * to invent a state for it. * * ⚠ A 🪑 ROW THAT CARRIES A REF IS A LANE IN DISGUISE and is scored as one — * relabelling work as a role must not exempt it from the clock. */ export const ROLE_GLYPH = "🪑"; const STATUS_DECORATION = /^[\s*⭐]+/u; export const isRoleStatus = (status: string): boolean => String(status ?? "").replace(STATUS_DECORATION, "").startsWith(ROLE_GLYPH); export type RowKind = "lane" | "role" | "role-with-ref"; export function rowKindOf(row: { status: string; branchWorktree: string }): RowKind { if (!isRoleStatus(row.status)) return "lane"; return cellKindOf(row.branchWorktree) === "ref" ? "role-with-ref" : "role"; } /** The remote-tracking form a board cell should name for `branch`. */ export const boardRefFor = (branch: string): string => (branch.startsWith("origin/") ? branch : `origin/${branch}`); /** * Classify one cell for one agent. * * `base` is the ref a merged branch would be contained in — `origin/main` by * convention, passed rather than assumed so a consumer fleet on another default * branch is not silently misjudged. */ export function classifyBoardRef( repo: string, agentId: string, cell: string, base = "origin/main", ): BoardRefVerdict { const raw = refInCell(cell); if (!raw) { // Words in the ref position are a DECLARATION, not an absence: the row // says it has no per-agent branch. Only a blank or `—` is "nothing said". if (cellKindOf(cell) === "prose") { return { kind: "none", why: `the Branch · Worktree cell declares in words that this row carries no per-agent branch: "${String(cell).trim().slice(0, 60)}"` }; } return { kind: "empty", why: "no ref in the Branch · Worktree cell, and nothing declared in its place" }; } if (ELIDED.test(raw)) { return { kind: "elided", why: `'${raw}' is a ref a human abbreviated for display — it contains an elision and cannot be looked up as written. ` + `Unreadable by construction, not "unpushed": the real ref may well exist. Write the full ref in the cell; the note can carry the shape.`, }; } const bare = raw.replace(/^origin\//, ""); // Task 5.3/22.1: CHECKED FIRST AND UNCONDITIONALLY — the original defect // (`docs/*`) was previously only caught in a repo with NO origin remote at // all (further down); confirmed live that in a repo WITH an origin — the // normal case for every worktree this fleet actually runs — the same // value fell through to the "unpushed" branch instead and was reported as // "the branch has not been pushed", which is a confidently WRONG diagnosis // for a path. `git check-ref-format` answers "could this even syntactically // be a branch name" without touching the repo's history at all, so a glob // is caught regardless of what else is true about this checkout. if (!isSyntacticallyValidRef(bare)) { return { kind: "path", why: `'${raw}' is not a syntactically valid branch name — it is a path or glob` }; } // A SHARED REF FIRST, because `main` resolves and would otherwise be measured. // Named explicitly rather than inferred from scoping, so the reason a reader // gets is the real one. const shared = new Set(["main", "master", "develop", "trunk", "HEAD"]); if (shared.has(bare)) { return { kind: "shared", why: `'${raw}' is a SHARED branch — every agent merges into it, so its movement is the fleet's activity and not '${agentId}'s. ` + `A row pointing here can never stall, which is a manufactured green rather than a measurement.`, }; } // SCOPED BY NAME — but only refusing a ref that belongs to a DIFFERENT agent, // never one that is merely unconventional. // // Authorship cannot carry ownership here (every commit in this repo is // authored by the same person, so `--author` would match every agent on every // ref while looking scoped), which leaves the branch-naming convention // `/`. But REQUIRING that convention would make a repo that // does not use it universally blind — and a narrowing that disables the check // everywhere is worse than the defect it fixes. So a plain `feature-x` is // allowed and `/x` is refused: the second is positively // someone else's work, the first is only unlabelled. const owner = bare.includes("/") ? bare.slice(0, bare.indexOf("/")) : null; if (owner && owner !== agentId && /^[\w.-]+-(worker|aide|qa|ci|coordinator)(-\d+)?$/.test(owner)) { return { kind: "unscoped", why: `'${raw}' is scoped to '${owner}', not '${agentId}' — its movement is that agent's work. ` + `A per-agent signal needs a ref only this agent writes, and the branch name is the only ownership marker this repo carries.`, }; } const remote = `origin/${bare}`; const remoteResolves = resolves(repo, remote); const localResolves = resolves(repo, bare); // NO SHARED FORM CAN EXIST, so the local ref is the best evidence there is. // Without this a repo with no `origin` remote would report every row // unmeasurable forever — the check disabled everywhere by a rule meant to // make it honest. Preferring the shared form is right; requiring one that // cannot exist is not. if (!remoteResolves && !resolves(repo, "origin/HEAD") && git(repo, ["remote", "get-url", "origin"]) === null) { if (!localResolves) { return { kind: "path", why: `'${raw}' does not resolve as a git ref in a repo with no remote — it is a path or glob` }; } return { kind: "measurable", ref: bare }; } if (!remoteResolves) { // Local-only vs never-pushed are different facts and only one is a defect // in the CELL. Both are unmeasurable, and neither is a stall. if (localResolves) { return { kind: "local-only", why: `'${raw}' resolves in THIS checkout and not on the remote, so the same board reads differently from another checkout — ` + `whether it is measurable depends on who last ran \`git fetch\`, which is a property of the reader rather than of the work.`, }; } // Task 5.3/22.2's fourth cause: "a pushed branch absent from this // checkout" is NOT the same fact as "never pushed", and this check has // no way to tell them apart without a network call it deliberately does // not make (every other verdict here is a local git op only — adding // one here would trade the offline property for one more cause). SAYING // SO is the fix Task 22's own title asks for: a classifier that names // an ABSENCE of evidence as a specific, confident cause is the exact // shape "it is a path or glob" was wrong in — just moved one branch // over. `'${remote}' does not exist` is the one fact this DOES know; // everything past it is now framed as what it cannot distinguish rather // than a guess dressed as a finding. return { kind: "unpushed", why: `'${remote}' does not resolve in THIS checkout — either the branch has never been pushed, or it WAS pushed and this ` + `checkout's remote-tracking ref is simply stale (nobody has run \`git fetch\` here since). Cannot distinguish the two ` + `without asking the remote, which this check deliberately does not do. Either way there is no LOCAL evidence of ` + `activity, so it is not scored as a stall.`, }; } // MERGED IS FROZEN. Its ref can never move again, so any age computed from it // only grows — an ever-worsening stall on a lane that is finished. // // `--is-ancestor` ALONE CANNOT SEE IT, and this fleet is the case that proves // it: we SQUASH-merge, so the branch's commits never become ancestors of the // base. Measured on a branch I knew was merged — `--is-ancestor` said NO // while the work was plainly on main. A merged-detector that cannot detect // this repo's own merge strategy is inert, which is worse than absent because // it reads as covered. // // `git cherry` compares PATCH IDS, so a squashed single-commit branch reports // `-` (already upstream). It is not complete either — squashing several // commits into one changes the patch id — so BOTH tests run and either one // answering yes is enough. Neither is asked to be sufficient alone. if (resolves(repo, base)) { const ancestor = git(repo, ["merge-base", "--is-ancestor", remote, base]) !== null; const cherry = git(repo, ["cherry", base, remote]); const allUpstream = cherry !== null && cherry.length > 0 && cherry.split("\n").every((l) => l.trim().startsWith("-")); if (ancestor || allUpstream) { return { kind: "merged", why: `'${remote}' has already landed in ${base} (${ancestor ? "an ancestor" : "every commit's patch is upstream — a squash merge"}) — ` + `so this ref is FROZEN and can never move again. ` + `Measuring it reports a stall that grows forever on a finished lane. The row needs the agent's current slice, or an idle marker.`, }; } } return { kind: "measurable", ref: remote }; }