ORB-164: register git-guardrails on PowerShell and carry the external-interface and never-admin-merge rules in AGENTS.md - #442
Conversation
The only PreToolUse matcher named "Bash", so the identical git command issued
through the PowerShell tool fired no hook at all and git-guardrails.mjs, the
local branch protection this repository relies on, was silently absent there.
Verified live in this worktree, both refused after the change:
PreToolUse:PowerShell hook error: BLOCKED git command (Orbit git workflow):
git push origin main
PreToolUse:Bash hook error: BLOCKED git command (Orbit git workflow):
git push origin main
The refusal echoing the command back confirms the PowerShell tool populates
tool_input.command, the field the hook reads, so no payload change was needed.
Also removes the two em dashes the hook carried; tools/check-dashes.mjs skips
.claude/ so the ban was never enforced on them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bojq5cLFZ87iBD8zhnoSWH
…AGENTS.md Codex workers launched into an orbit-api worktree read this repository's AGENTS.md, so orbit-ui-mobile PR #659's "Never assume an external interface" rule and its review-rule counterpart never reached them. Added here, adapted to this repository's externals (dotnet, NuGet) and its review-rule numbering. Adds the never-admin-merge prohibition from the harness repair specification section J3a alongside the existing branch-protection guardrail: no gh pr merge --admin, no direct PUT on the pulls merge endpoint, and no GraphQL mergePullRequest mutation. Naming both raw API calls is the point; forbidding only the CLI flag leaves the API paths open. If a merge genuinely needs the admin override, the agent stops and asks Thomas to merge it himself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bojq5cLFZ87iBD8zhnoSWH
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c0175eae63
ℹ️ 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".
| - Never perform an admin merge, in any shape: no `gh pr merge --admin`, no direct | ||
| `PUT /repos/{owner}/{repo}/pulls/{number}/merge`, and no GraphQL `mergePullRequest` | ||
| mutation. Naming the two raw API calls is deliberate; forbidding only the CLI flag |
There was a problem hiding this comment.
Enforce the new admin-merge prohibition
When a worker invokes gh pr merge --admin or either raw API form, this prohibition remains advisory: the repository-wide search finds no enforcement, and git-guardrails.mjs immediately allows commands without the word git (a direct hook invocation with the gh command exits 0). Because an admin merge bypasses the protections that could reject it after execution, add a pre-execution gate covering all three listed forms rather than placing this rule among guardrails described as mechanically enforced.
AGENTS.md reference: AGENTS.md:L26-L29
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review: PR #442 (thomasluizon/orbit-api)
Scope: PR #442 — AGENTS.md, .claude/settings.json, .claude/hooks/git-guardrails.mjs (ORB-164)
Recommendation: APPROVE
Summary
Documentation/tooling-only PR. It registers the existing git-guardrails.mjs PreToolUse hook on the PowerShell matcher (previously Bash-only, so the identical git command issued through PowerShell fired no guard at all), and adds two AGENTS.md sections: a never-admin-merge prohibition and a "never assume an external interface" verification rule adapted from the sibling orbit-ui-mobile repo. No src/, DTO, controller, or packages/shared surface is touched, so backend-hard-rules, security-reviewer, and contract-aligner gates never fire. The PR body is unusually well evidenced (live hook-trip transcripts for both Bash and PowerShell, check-dashes/check-frontmatter/node --check output, a recorded branch-protection payload). No Critical/High findings survived review.
Findings
Critical / High / Medium
None.
Low / Info
.claude/hooks/git-guardrails.mjsalso removes the file's two pre-existing em dashes (file:74, a user-facing message;file:79, a comment).tools/check-dashes.mjsexplicitly skips the whole.claude/tree, so this was never CI-required — a voluntary fix in exactly the domain (git-workflow enforcement) that upholds the rule. No action needed.- The new "never assume an external interface" rule is dogfooded by this PR itself: the PowerShell-matcher claim is backed by a live transcript in the PR body showing the hook actually tripping through the PowerShell tool, not an assumption from
--help/memory.
Subagents
| Agent | Verdict |
|---|---|
| security-reviewer | N/A — gate did not fire (no src/ code changed) |
| contract-aligner | N/A — gate did not fire (no DTO/route/packages/shared touched) |
Validation
| Check | Result |
|---|---|
| Build (dotnet) | N/A — skipped per CI adaptation; no .cs changes in this diff anyway |
| Tests (dotnet) | N/A — same; PR body's own local proof (check-dashes, check-frontmatter, node --check, JSON parse, lefthook dashes/protect-main) is consistent with the diff |
Deferred — N/A dimensions & files not verdicted
- Dimensions 8, 9, 10 (DESIGN.md/AI-slop, web↔mobile parity, i18n) — N/A, frontend-only / no
apps/*touched. - Dimension 11 (Contract drift + backward-compat) — N/A, no DTO/route/shared-type change; no candidates.
- Dimension 13 (Backend hard rules) — N/A, no
src/Orbit.*touched. - Dimension 14 (FEATURES.md parity) — N/A, no user-facing feature surface changed.
- PR body claim that
orbit-ui-mobilePR #659 landed the sibling rule — not independently verifiable, mobile repo not checked out in this job. - Whether
main's branch protection hasenforce_adminsenabled — the new "never admin-merge" bullet sits underAGENTS.md's pre-existing framing that the guardrail list is "enforced by ... GitHub branch protection," but an admin-merge override bypasses branch protection by definition unlessenforce_adminsis specifically on. Not confirmable from this diff alone. Not raised as a ranked finding (no evidence either way), but worth a human check: ifenforce_adminsisfalse, either enable it or soften the bullet's implied technical backstop to "policy-only."
What's good
- Fixes a real, previously-silent guard gap (PowerShell-issued git commands bypassed branch protection entirely).
- Rigorous self-verification in the PR body, matching the standard its own new rule demands of future changes.
- New
AGENTS.mdcontent is adapted to this repo's externals, not pasted verbatim, and correctly cross-numbered. - Correctly scoped: explicitly excludes items still blocked on the unlanded
orbit-ui-mobilepeer PR rather than landing a partial lockstep pair.
Recommendation
Approve. Optional non-blocking follow-up: confirm enforce_admins: true on main's branch protection so the new never-admin-merge bullet has the technical backstop its section's framing implies.
Branch protection on main requires three code scanning contexts: "Analyze
(csharp)", "Analyze (javascript-typescript)" and "Analyze (actions)". They
were produced by CodeQL default setup, which does not run on Dependabot pull
requests (github/codeql-action issue 2858). The three contexts therefore never
reported on a Dependabot head, and every Dependabot pull request sat
permanently BLOCKED waiting on checks that could not arrive.
Default setup is now disabled on the repository, and this workflow takes over.
An advanced-setup workflow is an ordinary workflow, so it triggers on a
Dependabot head like every other job here, and its job-level permissions block
raises the read-only Dependabot GITHUB_TOKEN to security-events: write.
The matrix job name "Analyze (${{ matrix.language }})" interpolates to exactly
the three required context names, so branch protection needs no change. C# uses
build-mode manual with the same dotnet restore plus dotnet build --no-restore
that API Tests already proves, because this repository generates code at build
time; javascript-typescript and actions use build-mode none. github/codeql-action
is pinned by SHA to tag v4.37.4, matching the repository convention for
non-actions/* actions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bojq5cLFZ87iBD8zhnoSWH
There was a problem hiding this comment.
Code Review: PR #442
Scope: Diff since the last review (commit 988e04c1), which added .github/workflows/codeql.yml. Earlier commits (.claude/hooks/git-guardrails.mjs, .claude/settings.json, AGENTS.md) were already reviewed and approved in a prior review at commit c0175eae and are not re-flagged here.
Recommendation: APPROVE
Summary
This commit adds an 87-line CodeQL "advanced setup" workflow replacing GitHub's default CodeQL setup. The motivation — default setup doesn't run on Dependabot-authored PRs, permanently blocking the three required status-check contexts — is a real, known codeql-action limitation. The workflow reproduces the three required check names (Analyze (csharp), Analyze (javascript-typescript), Analyze (actions)) via matrix-job-name interpolation, uses least-privilege job-level permissions (security-events: write, actions: read, contents: read), SHA-pins github/codeql-action, and reuses this repo's proven dotnet restore && dotnet build --no-restore build steps from test.yml for the C# manual-build-mode leg. No src/ or product code is touched, so backend hard-rules, security-reviewer, and contract-aligner gates don't apply.
Findings
Critical / High
None.
Medium
None.
Low / Info
- The commit message asserts CodeQL default setup is now disabled on the repository — this is an external GitHub repo-setting change not visible in the diff, and not independently verifiable from this CI job (the
code-scanning/default-setupAPI check was blocked by sandbox permissions). If that toggle wasn't actually flipped, advanced-setup SARIF uploads would be rejected while default setup is still on. Not blocking — worth a quick human glance at Settings → Code security. actions/checkout@v7/actions/setup-dotnet@v6are tag-pinned rather than SHA-pinned in the new workflow, consistent with every other workflow in this repo.github/codeql-actionis correctly SHA-pinned, matching the repo's convention for third-party actions.- No stale references to CodeQL default setup remain elsewhere in the repo that would need updating.
What's good
- Correctly diagnoses and fixes a real, previously-blocking CI gap (Dependabot PRs stuck on default-setup contexts that never report).
- Job-name interpolation is explicitly called out as load-bearing for branch protection.
- Reuses this repo's own proven C# build steps rather than inventing new ones.
- Permissions scoped tightly at workflow and job level; SHA-pinning matches repo convention.
Validation
Build/Test: N/A — skipped per CI adaptation (separate required checks handle this); no .cs changes in this diff.
Deferred / not verifiable in CI
- Dimensions covering frontend/mobile parity, contract drift, and FEATURES.md: N/A — no product code, DTOs, or shared types touched.
- CodeQL default-setup-disabled claim: not independently verifiable from this job's
gh apiaccess; flagged as Info above.
"SonarCloud Code Analysis" is a required context on main, and the SonarQubeCloud app only posts it once an analysis is uploaded. The scanner job was skipped on Dependabot heads because SONAR_TOKEN did not exist in the repository's Dependabot secret store, so the required context never reported and every Dependabot pull request stayed BLOCKED. SONAR_TOKEN is now present in that store, so the guard has nothing left to protect and is removed. The job already reads the secret as secrets.SONAR_TOKEN, which is the reference a Dependabot-triggered run resolves against the Dependabot store. claude-review.yml identified Dependabot with github.actor, which is whoever triggered the run, not the pull request author. A human re-run of a Dependabot run would therefore let the Claude review step through with an empty CLAUDE_CODE_OAUTH_TOKEN, since that secret lives only in the Actions store. It now checks the author, matching dependabot-auto-merge.yml. That step stays skipped for Dependabot on purpose; "review" is not a required context. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bojq5cLFZ87iBD8zhnoSWH
|



Harness-Lockstep-Peer: 665
ORB-164, the orbit-api half of the harness repair specification carried by ORB-163. This pull request lands only the three items that do not depend on the paired orbit-ui-mobile pull request. The remaining items stay for a later pass, because each one is bound to an edit that has not landed in #665 yet: deleting
.github/workflows/claude-review.ymland the four twins that name it, thepr-review/rubric.mdtwin edit, the byte-exactsecond-opinion.mjscorrection, and thepr-review/SKILL.mdclause 6 and 7 corrections.What changed
1.
git-guardrails.mjsnow runs on the PowerShell tool as well as Bash..claude/settings.jsonregistered its onlyPreToolUsehook on the matcher"Bash", so the identical git command issued through the PowerShell tool fired no hook at all and this repository's local branch protection was silently absent there. There are now two matchers,BashandPowerShell, both invoking the same hook.2.
AGENTS.mdcarries the "Never assume an external interface" rule at line 48, with its review-rule counterpart as Code Review Rule 5 at line 97. orbit-ui-mobile PR #659 landed this rule in its ownAGENTS.md, but codex workers launched into an orbit-api worktree read THIS repository'sAGENTS.md, so the rule did not reach them. Adapted to this repository's externals (dotnet, NuGet) and its existing review-rule numbering rather than pasted.3.
AGENTS.mdcarries the never-admin-merge prohibition from specification section J3a, at line 33, alongside the existing branch-protection guardrail. It names all three shapes:gh pr merge --admin, a directPUT /repos/{owner}/{repo}/pulls/{number}/merge, and the GraphQLmergePullRequestmutation. Naming both raw API calls is load bearing, because forbidding only the CLI flag leaves both API paths open. The one legitimate exception is stated: if a merge genuinely needs the admin override, the agent stops and asks Thomas to merge it himself.The branch was also renamed from
thomasluizon/orb-164-harness-repair-api-halftofeature/orb-164-harness-repair-api-half, which is whatCLAUDE.mdrequires.Proof that the guard actually refuses
Observed live in this worktree against the final file state, through a headless session rooted at the worktree so this repository's
.claude/settings.jsonis the project settings in effect. Both attempts were refused before the command reached git.Through the PowerShell tool:
Through the Bash tool:
git-guardrails.mjs:24readsinput.tool_input.command. The refusal echoinggit push origin mainback verbatim is the confirmation that the PowerShell tool populates that exact field, so the hook needed no payload change. The hook exits 0 silently whencommandis not a string, so a missing field would have produced a silent pass rather than this block. Localmainandorigin/mainwere both at319c774cwhen the attempts ran, so a hook miss would have been a no-op push rather than a real one.Branch protection payload, recorded before the first edit
Specification section J2 requires this because no revert restores branch protection state. Read from
GET /repos/thomasluizon/orbit-api/branches/main/protectionon 2026-07-30,strict: true, 15 required contexts:This pull request changes no branch protection setting and moves no
guards.ymljob name.Harness Lockstep on this pull request, and why
The three files changed here are
AGENTS.md,.claude/settings.jsonand.claude/hooks/git-guardrails.mjs. None of them is one of the six lockstep twins thatorbit-ui-mobile/tools/check-lockstep.mjs:16-24fixes at exactly six entries, and none of them isguards.yml. TheDetect a lockstep changestep atguards.yml:77-87therefore evaluates tochanged=false, every following step is skipped, and the job reports success without resolving the peer at all. SoHarness Lockstephere is green because the twins were untouched, not because the pair is complete. It is not evidence that the twin edits are done, and the later pass that lands them will be the run that actually exercises the sibling checkout.The
Harness-Lockstep-Peer: 665directive is on the first line regardless, because orbit-ui-mobile#665's mirror job requires the exact reciprocal line naming this pull request's number in this body. #665 must carryHarness-Lockstep-Peer:with this pull request's number or its ownHarness Lockstepjob fails.Also fixed while in the file
git-guardrails.mjscarried two em dashes, one of them inside the message the hook prints to the user.tools/check-dashes.mjsskips.claude/entirely, so the repository wide dash ban never saw them. Both are gone.Testing
No product code and no new decision path in a tool, so no unit test.
node tools/check-dashes.mjs --files AGENTS.md .claude/settings.json .claude/hooks/git-guardrails.mjsexits 0,node tools/check-dashes.mjs --check-baselineexits 0,node tools/check-frontmatter.mjsreports 8 files parsing,node --check .claude/hooks/git-guardrails.mjsexits 0, and.claude/settings.jsonparses as JSON. The lefthookdashespre-commit step and theprotect-mainpre-push step both ran green.Linear: ORB-164
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bojq5cLFZ87iBD8zhnoSWH