diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 7ea8a24ad..a1ac78851 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -764,6 +764,32 @@ jobs: - name: Run audit-section-33-migration-xrefs (--enforce) run: bun tools/hygiene/audit-section-33-migration-xrefs.ts --enforce + lint-backlog-id-uniqueness: + # Fail if any B-NNNN ID is claimed by more than one backlog row file. + # Cross-agent ID-allocation collisions (Otto-CLI vs Otto-Desktop on + # B-0444 2026-05-13; Lior vs Otto-CLI on B-0532+B-0533 2026-05-15) cost + # ~15 min coordination effort each at observed ~20% rate. The + # duplicate-ID detection logic was added to audit-backlog-items.ts + # 2026-05-14 (PR #3249, Copilot caught two files claiming B-0329 on + # PR #3247) but ran detect-only โ€” this job is the CI gate that turns + # codified-but-not-enforced into a control. + # + # B-0535 โ€” sibling of lint-section-33-migration-xrefs + lint-archive- + # header-section33: catch-once-then-lint pattern at ID-allocation scope. + name: lint (backlog ID uniqueness) + timeout-minutes: 2 + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install toolchain via three-way-parity script (GOVERNANCE ยง24) + run: ./tools/setup/install.sh + + - name: Run audit-backlog-items (--enforce-duplicate-ids) + run: bun tools/hygiene/audit-backlog-items.ts --enforce-duplicate-ids + lint-no-empty-dirs: # Fail if a committed directory has no files โ€” almost always a # forgotten artefact (an agent-created skill folder without a diff --git a/tools/hygiene/audit-backlog-items.ts b/tools/hygiene/audit-backlog-items.ts index 00b1d5faf..0c8f3a1b4 100644 --- a/tools/hygiene/audit-backlog-items.ts +++ b/tools/hygiene/audit-backlog-items.ts @@ -34,11 +34,14 @@ // PR #3247; PR #3249 added this audit class). // // Usage: -// bun tools/hygiene/audit-backlog-items.ts +// bun tools/hygiene/audit-backlog-items.ts # detect-only +// bun tools/hygiene/audit-backlog-items.ts --enforce-duplicate-ids +// # exit non-zero on duplicate-ID groups (B-0535 CI gate) // // Exit codes: -// 0 -- survey ran (findings reported in body) -// 1 -- fatal invocation error (e.g., backlog dir missing) +// 0 -- survey ran (findings reported in body); detect-only mode +// 1 -- fatal invocation error (e.g., backlog dir missing) OR +// duplicate-ID groups found AND --enforce-duplicate-ids set import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; @@ -601,6 +604,15 @@ async function main(): Promise { return 1; } + const argv = process.argv.slice(2); + const enforceDuplicateIds = argv.includes("--enforce-duplicate-ids"); + for (const arg of argv) { + if (arg !== "--enforce-duplicate-ids") { + process.stderr.write(`error: unknown argument: ${arg}\n`); + return 1; + } + } + const today = nowIso(); const nowEpoch = Math.floor(Date.now() / 1000); @@ -652,6 +664,13 @@ async function main(): Promise { ); console.log(" (typed-edge backlog graph)."); + if (enforceDuplicateIds && duplicateIdGroups > 0) { + process.stderr.write( + `\nerror: ${duplicateIdGroups} duplicate-ID group(s) found; --enforce-duplicate-ids set (B-0535 gate)\n`, + ); + return 1; + } + return 0; }