Skip to content

ORB-170: Gate root files with a closed allowlist - #680

Merged
thomasluizon merged 2 commits into
mainfrom
feature/orb-170-root-allowlist
Aug 4, 2026
Merged

thomasluizon merged 2 commits into
mainfrom
feature/orb-170-root-allowlist

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Summary

  • Add a closed repository-root file allowlist backed by committed JSON data.
  • Reject ignored and untracked root files while accepting the linked-worktree .git pointer.
  • Run the gate in pre-commit and as the Root Allowlist CI context.

Scope

  • This PR intentionally covers orbit-ui-mobile only. The orbit-api and orbit-landing-page mirrors require their own tickets and PRs under the one-ticket-per-repo rule.
  • The ticket says 22 root files, but its enumerated list and the real post-suite filesystem both contain 21. The allowlist uses those observed 21 files and does not invent a missing entry.
  • Parity: no. This is repository tooling only.

Verification

  • node tools/check-root-allowlist.mjs
  • node tools/test-tools.mjs
  • node .claude/hooks/test-hooks.mjs
  • npm run lint
  • npm run type-check
  • npm run test

The harness covers .agent-scratch-extract.mjs, .tmp-extract.mjs, and the load-bearing plain filename extract-tmp.mjs. It also proves a .tmp-extract.mjs fixture is both gitignored and untracked before the gate rejects it.

Human follow-up

After this lands, Thomas must add Root Allowlist as a required status context on main. This PR does not change branch protection.

Linear: ORB-170

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Aug 4, 2026 11:02pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e6e487125

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lefthook.yml
pre-commit:
commands:
root-allowlist:
run: node tools/check-root-allowlist.mjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exempt local env files from the pre-commit gate

When a developer has a legitimate local root .env/.env.local file (patterns this repo still ignores in .gitignore), this new pre-commit command runs check-root-allowlist, whose readdirSync scan includes ignored and untracked root files, so every commit is blocked even when nothing root-related is staged. Because contributors are also told not to bypass git hooks, this breaks the local secret/env workflow; either skip known local-only ignored files in the hook or run the closed allowlist against tracked/CI contents only.

AGENTS.md reference: AGENTS.md:L78-L80

Useful? React with 👍 / 👎.

…iles

Two gaps in the file-only gate, both measured rather than guessed.

A local root .env blocked every commit. The scan sees ignored and untracked
files by design, and .gitignore blesses .env and .env.local at the root, so the
first developer to create one could not commit at all and was also told never to
pass --no-verify. Both names are declared now.

Gating files alone left the directory half of the root fully open, so the same
throwaway simply lands one level down: .artifacts/transcript.mjs walked past a
gate written to stop exactly that. The closed set now covers directories too,
seeded from the real root, including the generated node_modules and .turbo that
a checkout genuinely carries after an install.

Closes ORB-176.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 16edb00 into main Aug 4, 2026
26 checks passed
@thomasluizon
thomasluizon deleted the feature/orb-170-root-allowlist branch August 4, 2026 23:33
thomasluizon added a commit that referenced this pull request Aug 5, 2026
Wires the three tools from the parent PR into the contract. /orchestrate now
takes one ticket, several, a project, or --auto, and grows --sleep, --parallel
and --auto. One ticket with no flags behaves exactly as it did.

--sleep opens pull requests and never merges one. Every piece of signing,
provenance, ledger and merge-sweep machinery the OLD --sleep needed stays
deleted, because nothing acts on a receipt. What makes an unattended run
trustworthy now is verify-delivery.mjs, which reads only artifacts and never a
worker's self-report. That is what was missing the first time.

Stacking, because nothing merges overnight: a ticket whose blocker is in the
same queue branches from its blocker's branch and targets it, since main will
not contain the blocker's work until morning. --base is passed to
verify-delivery too, or the size caps read the parent's commits as this
ticket's. A stack lives in one repository, so cross-repo blockers stay separate
pull requests and the api one deploys first.

New step 12 clears the Codex reviewer's threads, which nothing has ever read:
8 threads opened across #676, #680 and #681, 8 still unresolved, all merged.
The verdict comes from the presence of a review, never the thread count, and
NO_REVIEW is never reported as clean. Every resolve replies first. One fix
round, then re-verify and post @codex review explicitly, because the bot
reviews on open and on request but NEVER on a push: #676's only review landed
at 17:23:33Z while commits ran to 17:35:14Z.

Two jump references changed MEANING, not just digits. "0 blocking -> step 12"
and "all CLOSED -> step 12" used to mean "hand over" and now mean "go to the
bot pass"; a mechanical renumber would have skipped the new step on the
cleanest and most common path. The OPEN branch reaches it too, report-only.

Preflight 0a now asserts the ACTIVE gh account owns the target remote, not
merely that some account is logged in. This run hit the exact failure it
guards: a push refused for thomas-luizon_iqpay after the work was done.

Preflight 0b runs once per distinct repo rather than once per ticket, so
concurrent fetch and merge --ff-only cannot race on .git/index.

The visual layer is deliberately absent. A visible-effect ticket runs, opens
its pull request, and is NOT moved to In Review: the run prints "visual check
owed" and Thomas looks with /dev-server. The screenshots existed to prove
correctness before a machine merged, and no machine merges.

Amends the §5.3 round cap to scope "round" to the frozen cross-vendor list, or
the bot fix round reads as a forbidden third round. Amends the merge
prohibition to say what it means: never merges UNASKED. Thomas asking for a
merge in a later turn is the intended path; --admin, the raw REST and GraphQL
paths, force pushes and pushes to main stay blocked in every case.

caps.parallelTickets is 3, not 8: each worktree is a full install, build and
test run plus its own model session.

node tools/test-tools.mjs           ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs   ORBIT HOOKS OK
node tools/check-dashes.mjs         clean

Refs ORB-179, ORB-180.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Aug 5, 2026
Wires the three tools from the parent PR into the contract. /orchestrate now
takes one ticket, several, a project, or --auto, and grows --sleep, --parallel
and --auto. One ticket with no flags behaves exactly as it did.

--sleep opens pull requests and never merges one. Every piece of signing,
provenance, ledger and merge-sweep machinery the OLD --sleep needed stays
deleted, because nothing acts on a receipt. What makes an unattended run
trustworthy now is verify-delivery.mjs, which reads only artifacts and never a
worker's self-report. That is what was missing the first time.

Stacking, because nothing merges overnight: a ticket whose blocker is in the
same queue branches from its blocker's branch and targets it, since main will
not contain the blocker's work until morning. --base is passed to
verify-delivery too, or the size caps read the parent's commits as this
ticket's. A stack lives in one repository, so cross-repo blockers stay separate
pull requests and the api one deploys first.

New step 12 clears the Codex reviewer's threads, which nothing has ever read:
8 threads opened across #676, #680 and #681, 8 still unresolved, all merged.
The verdict comes from the presence of a review, never the thread count, and
NO_REVIEW is never reported as clean. Every resolve replies first. One fix
round, then re-verify and post @codex review explicitly, because the bot
reviews on open and on request but NEVER on a push: #676's only review landed
at 17:23:33Z while commits ran to 17:35:14Z.

Two jump references changed MEANING, not just digits. "0 blocking -> step 12"
and "all CLOSED -> step 12" used to mean "hand over" and now mean "go to the
bot pass"; a mechanical renumber would have skipped the new step on the
cleanest and most common path. The OPEN branch reaches it too, report-only.

Preflight 0a now asserts the ACTIVE gh account owns the target remote, not
merely that some account is logged in. This run hit the exact failure it
guards: a push refused for thomas-luizon_iqpay after the work was done.

Preflight 0b runs once per distinct repo rather than once per ticket, so
concurrent fetch and merge --ff-only cannot race on .git/index.

The visual layer is deliberately absent. A visible-effect ticket runs, opens
its pull request, and is NOT moved to In Review: the run prints "visual check
owed" and Thomas looks with /dev-server. The screenshots existed to prove
correctness before a machine merged, and no machine merges.

Amends the §5.3 round cap to scope "round" to the frozen cross-vendor list, or
the bot fix round reads as a forbidden third round. Amends the merge
prohibition to say what it means: never merges UNASKED. Thomas asking for a
merge in a later turn is the intended path; --admin, the raw REST and GraphQL
paths, force pushes and pushes to main stay blocked in every case.

caps.parallelTickets is 3, not 8: each worktree is a full install, build and
test run plus its own model session.

node tools/test-tools.mjs           ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs   ORBIT HOOKS OK
node tools/check-dashes.mjs         clean

Refs ORB-179, ORB-180.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Aug 5, 2026
Wires the three tools from the parent PR into the contract. /orchestrate now
takes one ticket, several, a project, or --auto, and grows --sleep, --parallel
and --auto. One ticket with no flags behaves exactly as it did.

--sleep opens pull requests and never merges one. Every piece of signing,
provenance, ledger and merge-sweep machinery the OLD --sleep needed stays
deleted, because nothing acts on a receipt. What makes an unattended run
trustworthy now is verify-delivery.mjs, which reads only artifacts and never a
worker's self-report. That is what was missing the first time.

Stacking, because nothing merges overnight: a ticket whose blocker is in the
same queue branches from its blocker's branch and targets it, since main will
not contain the blocker's work until morning. --base is passed to
verify-delivery too, or the size caps read the parent's commits as this
ticket's. A stack lives in one repository, so cross-repo blockers stay separate
pull requests and the api one deploys first.

New step 12 clears the Codex reviewer's threads, which nothing has ever read:
8 threads opened across #676, #680 and #681, 8 still unresolved, all merged.
The verdict comes from the presence of a review, never the thread count, and
NO_REVIEW is never reported as clean. Every resolve replies first. One fix
round, then re-verify and post @codex review explicitly, because the bot
reviews on open and on request but NEVER on a push: #676's only review landed
at 17:23:33Z while commits ran to 17:35:14Z.

Two jump references changed MEANING, not just digits. "0 blocking -> step 12"
and "all CLOSED -> step 12" used to mean "hand over" and now mean "go to the
bot pass"; a mechanical renumber would have skipped the new step on the
cleanest and most common path. The OPEN branch reaches it too, report-only.

Preflight 0a now asserts the ACTIVE gh account owns the target remote, not
merely that some account is logged in. This run hit the exact failure it
guards: a push refused for thomas-luizon_iqpay after the work was done.

Preflight 0b runs once per distinct repo rather than once per ticket, so
concurrent fetch and merge --ff-only cannot race on .git/index.

The visual layer is deliberately absent. A visible-effect ticket runs, opens
its pull request, and is NOT moved to In Review: the run prints "visual check
owed" and Thomas looks with /dev-server. The screenshots existed to prove
correctness before a machine merged, and no machine merges.

Amends the §5.3 round cap to scope "round" to the frozen cross-vendor list, or
the bot fix round reads as a forbidden third round. Amends the merge
prohibition to say what it means: never merges UNASKED. Thomas asking for a
merge in a later turn is the intended path; --admin, the raw REST and GraphQL
paths, force pushes and pushes to main stay blocked in every case.

caps.parallelTickets is 3, not 8: each worktree is a full install, build and
test run plus its own model session.

node tools/test-tools.mjs           ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs   ORBIT HOOKS OK
node tools/check-dashes.mjs         clean

Refs ORB-179, ORB-180.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Aug 5, 2026
* feat(tools): plan a queue, read the Codex reviewer, close its threads

Three tools the overnight /orchestrate queue needs. All new files, nothing
wired yet: the SKILL.md contract change is the PR stacked on this one.

plan-queue.mjs resolves a scope (explicit tickets, a Linear project, or the
whole board) into one ordered execution plan. The rule that matters: a blocker
INSIDE the queue is a stack edge, because the branch can carry it, while an
open blocker OUTSIDE it defers the ticket, because its branch would have to
contain work that does not exist. Admission runs to a fixed point, since
dropping one ticket can strand another behind it. Cross-repo blockers order the
waves but never stack, because GitHub requires every branch in a stack to live
in one repository.

list-bot-threads.mjs makes "the second reviewer never ran" a verdict instead of
a silence read as approval. Measured on 2026-08-05 across PRs #676, #680 and
#681: the Codex bot opened eight inline threads, eight are still unresolved,
and all three are merged. Two shapes make a naive reading wrong, and both are
handled rather than documented: an empty thread list is ambiguous between
"clean" and "not yet", so the verdict comes from the presence of a bot review;
and a body-level CHANGES_REQUESTED opens no thread at all, so zero unresolved
threads is not proof of a clean PR. A draft attracts no review ever, so draft
is checked before the wait clock starts.

resolve-bot-thread.mjs replies first and resolves only once the reply is
confirmed. A thread closed with no reason is indistinguishable from one nobody
read, so the ordering is a gate rather than a convention: a failed or empty
reply exits non-zero having mutated nothing. A failed resolve after a landed
reply says so, so a retry cannot double-post.

Verified against live data, not fixtures alone: list-bot-threads reads PR #681
and reports its real unresolved P2 on tools/launch-worker.mjs, and plan-queue
orders ORB-179 before ORB-180 and stacks the second on the first. Both GraphQL
mutations were checked against the live schema; pullRequestReviewId is optional
and names a pending review, so it is omitted.

node tools/test-tools.mjs      ORBIT TOOLS GATE OK (21/20/12 new assertions)
node .claude/hooks/test-hooks.mjs   ORBIT HOOKS OK

Refs ORB-179, ORB-180.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): reject a bot review pinned to an older head, refuse forked stacks

Both defects were raised by the Codex reviewer on this pull request and both
were right. The cross-vendor review earned its keep on the very PR that adds
the tool for reading it.

P1, list-bot-threads: a review is evidence about the commit it was given on and
nothing else. The tool's own usage says the bot never reviews on a push, then it
accepted the newest bot review regardless of which commit that review saw. After
any fixup push it would report REVIEWED for code the bot never looked at, which
is precisely the defect the tool exists to remove. It now compares
review.commit.oid against headRefOid and keeps waiting otherwise, and NO_REVIEW
distinguishes "never reviewed" from "reviewed a dead head" by naming the stale
commit so the caller knows to post @codex review.

P2, plan-queue: a branch has ONE parent, so a ticket blocked by two INDEPENDENT
same-repo tickets cannot stack on both. It picked the lexicographically last and
said nothing, planning a branch that would not contain the other blocker's work
while the plan claimed both were satisfied. It now picks the deepest blocker in
topological order and asserts every other same-repo blocker is one of its
ancestors; a shape that cannot be expressed is refused by name rather than
emitted wrong. Blockers that do form a chain are unaffected, and a cross-repo
blocker alongside a same-repo one is not a fork.

Seven regression cases added, including the fork refusal, the chained pair, the
mixed cross-repo parent, a stale review, a fresh review following a stale one,
and a review carrying no commit at all.

node tools/test-tools.mjs   ORBIT TOOLS GATE OK  (list-bot-threads 24, plan-queue 24)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(tools): a re-review after a push is not reliable, so do not claim it never happens

#676 reviewed once and never again while commits kept landing. #682 reviewed the
old head, took a push, and reviewed the new head seven minutes later. The blanket
'never on a push' claim was written from #676 alone and #682 falsified it the same
day.

Nothing about the code changes: comparing review.commit.oid to headRefOid is what
makes the verdict correct under either behaviour, which is the point. Only the
claim is corrected, because a run that trusts 'never' would skip the freshness
check as redundant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(tools): a resolve-only retry, and the body of a review-level block

Two P2s from the Codex re-review of this pull request, both real.

resolve-bot-thread: the failed-resolve branch told the caller to retry the
resolve alone and the tool offered no way to do it, so the advice could not be
followed. --resolve-only closes that WITHOUT weakening the no-bare-resolve rule:
it asks GitHub whether a reply is actually on the thread and refuses when there
is none, so the invariant is enforced against the live thread rather than
trusted from a flag. An already-resolved thread is a no-op exit 0.

list-bot-threads: a body-level CHANGES_REQUESTED carries its whole complaint in
the review body and opens no thread, so the caller learned it was blocked and
nothing about why. The body now rides along for that verdict only; on a
COMMENTED review it is boilerplate and the threads hold the findings.

The third P2, splitting sibling blockers into separate stack chains, is answered
on the thread rather than implemented. Refusing is one of the two remedies the
reviewer itself proposed in the previous round, and chaining two tickets Linear
never ordered would invent a dependency.

node tools/test-tools.mjs   ORBIT TOOLS GATE OK  (list-bot-threads 26, resolve-bot-thread 16)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Aug 5, 2026
* feat(orchestrate): a queue of tickets, a stack of reviewed pull requests

Wires the three tools from the parent PR into the contract. /orchestrate now
takes one ticket, several, a project, or --auto, and grows --sleep, --parallel
and --auto. One ticket with no flags behaves exactly as it did.

--sleep opens pull requests and never merges one. Every piece of signing,
provenance, ledger and merge-sweep machinery the OLD --sleep needed stays
deleted, because nothing acts on a receipt. What makes an unattended run
trustworthy now is verify-delivery.mjs, which reads only artifacts and never a
worker's self-report. That is what was missing the first time.

Stacking, because nothing merges overnight: a ticket whose blocker is in the
same queue branches from its blocker's branch and targets it, since main will
not contain the blocker's work until morning. --base is passed to
verify-delivery too, or the size caps read the parent's commits as this
ticket's. A stack lives in one repository, so cross-repo blockers stay separate
pull requests and the api one deploys first.

New step 12 clears the Codex reviewer's threads, which nothing has ever read:
8 threads opened across #676, #680 and #681, 8 still unresolved, all merged.
The verdict comes from the presence of a review, never the thread count, and
NO_REVIEW is never reported as clean. Every resolve replies first. One fix
round, then re-verify and post @codex review explicitly, because the bot
reviews on open and on request but NEVER on a push: #676's only review landed
at 17:23:33Z while commits ran to 17:35:14Z.

Two jump references changed MEANING, not just digits. "0 blocking -> step 12"
and "all CLOSED -> step 12" used to mean "hand over" and now mean "go to the
bot pass"; a mechanical renumber would have skipped the new step on the
cleanest and most common path. The OPEN branch reaches it too, report-only.

Preflight 0a now asserts the ACTIVE gh account owns the target remote, not
merely that some account is logged in. This run hit the exact failure it
guards: a push refused for thomas-luizon_iqpay after the work was done.

Preflight 0b runs once per distinct repo rather than once per ticket, so
concurrent fetch and merge --ff-only cannot race on .git/index.

The visual layer is deliberately absent. A visible-effect ticket runs, opens
its pull request, and is NOT moved to In Review: the run prints "visual check
owed" and Thomas looks with /dev-server. The screenshots existed to prove
correctness before a machine merged, and no machine merges.

Amends the §5.3 round cap to scope "round" to the frozen cross-vendor list, or
the bot fix round reads as a forbidden third round. Amends the merge
prohibition to say what it means: never merges UNASKED. Thomas asking for a
merge in a later turn is the intended path; --admin, the raw REST and GraphQL
paths, force pushes and pushes to main stay blocked in every case.

caps.parallelTickets is 3, not 8: each worktree is a full install, build and
test run plus its own model session.

node tools/test-tools.mjs           ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs   ORBIT HOOKS OK
node tools/check-dashes.mjs         clean

Refs ORB-179, ORB-180.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(orchestrate): state both measured shapes of the bot re-review

The step 12 text claimed the bot never reviews on a push, written from #676
alone. #682 falsified it the same day: it reviewed the old head, took a push,
and reviewed the new head seven minutes later.

Both shapes are now named, and the conclusion is stronger for it. A re-review is
luck, never the mechanism, so the explicit @codex review request stays and
list-bot-threads comparing review.commit.oid to the head is what makes the
verdict correct under either behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant