Repository navigation
fix(guards): scope the Dash Ban PR-body check to human-authored PRs - #598
Conversation
The step fed the raw PR title and body to check-dashes.mjs with no author scoping. Dependabot copies the dependency's own changelog verbatim into the body, so any upstream release note containing an en or em dash fails the gate on a PR whose text Orbit neither wrote nor can edit, and which Dependabot regenerates on every rebase. Measured: orbit-api#425 (a 21-package NuGet security bump) is blocked right now by en dashes inside a Hangfire release note. The same latent defect exists in all three repos, identically. The file-level dash check is deliberately left running on bot PRs: the files a Dependabot PR changes ARE Orbit's, so new dashes in them are still a real violation. Only the prose check, whose whole premise is that the text is Orbit-authored, is scoped. Verified github.event.pull_request.user.type against live data: "Bot" for dependabot[bot], "User" for a human author. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M2vH4pDvNnxUPecWWvwBc
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Code Review: PR #598 — fix(guards): scope the Dash Ban PR-body check to human-authored PRs
Scope: 1 file: .github/workflows/guards.yml (+7/-1)
Recommendation: APPROVE
Summary
Adds && github.event.pull_request.user.type != 'Bot' to the "PR title and body carry no dashes" step's if: condition, plus a WHY comment explaining that Dependabot copies upstream changelog text verbatim into PR bodies and shouldn't be held to Orbit's own dash-ban prose rule. The file-level (changed-files) dash check and the baseline ratchet check are untouched and still run on bot PRs — matches the PR's stated intent. Small, correctly scoped, low blast radius.
Findings
Critical / High / Medium
None.
Low / Info
- [Info] The condition excludes any
user.type == 'Bot'author, whereas every other bot-scoping check in this repo (guards.yml'sparityjob,sonarcloud.yml,claude-review.yml) scopes to the literalgithub.actor != 'dependabot[bot]'. Checked whether this could exempt a non-Dependabot bot's LLM-authored prose from the dash ban — no evidence it does today: orchestrator workers run locally under the human's ownghauth, and no workflow auto-opens code PRs as a GitHub App bot.user.type != 'Bot'andactor != 'dependabot[bot]'are currently equivalent in this repo. Style/robustness note only, not a defect. - [Info] Top-of-file summary comment doesn't mention the new bot exclusion — cosmetic.
- [Info] The WHY comment cites
orbit-api#425as bare text rather than a URL, which is what thelocal/no-commentsWHY-comment allowance literally requires — but that rule doesn't lint.github/workflows/*.yml, so it's not gate-enforced here and not postable under the signal gate.
Not verifiable in this job
orbit-api#425(the cited Hangfire release-note dash block motivating this fix) could not be independently checked —orbit-apiis not checked out in this CI job. Supporting rationale only, doesn't affect the verdict.- Build / Unit Tests / SonarCloud run as separate required checks for this PR; skipped here per task scope.
What's good
- Correctly scopes only the prose check; deliberately leaves the file-level dash check running on bot PRs.
- WHY comment is thorough, explains the constraint and scope boundary, cites concrete motivating evidence.
- PR body states the
user.typefield was verified against live GitHub API data rather than assumed. - Minimal, single-purpose diff, easy to review.
Recommendation
Approve as-is. No Critical/High findings. The Info-level notes are optional polish, not blockers.
There was a problem hiding this comment.
Code Review: PR #598 — fix(guards): scope the Dash Ban PR-body check to human-authored PRs
Scope: PR #598 in thomasluizon/orbit-ui-mobile
Recommendation: APPROVE
Summary
Single-file, 8-line change to .github/workflows/guards.yml that gates the "PR title and
body carry no dashes" step to human-authored PRs (github.event.pull_request.user.type != 'Bot'), so Dependabot's verbatim-copied upstream changelog text no longer trips the Dash
Ban gate. The file-level dash check (changed files) is correctly left unconditional for
bot PRs, since files a Dependabot PR touches are still Orbit's own. This is a root-cause
fix (narrows the check to the surface it was meant to police) rather than a workaround,
and the PR body documents the live-data verification of user.type (Bot vs User).
Findings
Critical
None.
High
None.
Medium
None.
Low / Info
None.
Subagents
Diff touches only .github/workflows/guards.yml — no apps/web/**, apps/mobile/**,
orbit-api, packages/shared/src/types/*, endpoints.ts, or i18n JSON changed, so every
gated subagent sits below its trigger threshold.
| Agent | Verdict |
|---|---|
| parity-checker | N/A — no apps/web or apps/mobile file changed |
| i18n-syncer | N/A — no user-facing strings or i18n JSON changed |
| contract-aligner | N/A — no shared types / endpoints / orbit-api change (cross-repo dimension also not verifiable in CI, sibling repo not checked out) |
| security-reviewer | N/A — no orbit-api code changed |
| design-reviewer | N/A — no apps/*/orbit-landing-page UI file changed |
Validation
Skipped per CI adaptation: this PR already runs Build / Unit Tests / SonarCloud as
separate required checks.
| Check | Result |
|---|---|
| Lint | N/A (CI adaptation) |
| Type check | N/A (CI adaptation) |
| Tests | N/A (CI adaptation) |
| Build (api) | N/A — orbit-api not touched |
Deferred — N/A dimensions & files not verdicted
- Dimension 8 (DESIGN.md/AI-slop): N/A — no
apps/*UI files changed. - Dimension 9 (Parity): N/A — no
apps/web/**orapps/mobile/**file changed. - Dimension 10 (i18n): N/A — no user-facing strings changed.
- Dimension 11 (Contract drift + backward-compat): N/A — no
packages/shared/src/types/*
or DTO changed; the orbit-api side is not checked out in this CI job and would not be
verifiable here regardless. - Dimension 12 (Security): reviewed inline — no injection/auth/secrets surface; the
user.typegate only widens/narrows a lint-style CI check, not an access control. - Dimension 13 (Backend hard rules): N/A —
orbit-apinot touched. - Dimension 14 (FEATURES.md parity): N/A — CI/guards change, no user-facing feature
surface change. - Dimension 4 (Comment policy): the new step comment is in a workflow YAML file, which
local/no-comments(ESLint) never targets (root/apps/*eslint configs cover only
.ts/.tsx/.js) — out of the dimension's lint-scoped surface, and consistent with
the file's pre-existing header comment block (lines 3-11). - Changed files:
.github/workflows/guards.yml— fully reviewed, verdict above (clean).
What's good
- Root-cause fix: narrows the check to what it was actually meant to police (Orbit's own
prose), rather than adding a suppression or exemption list. - Correctly leaves the file-level dash check unconditional for bot PRs — the files a
Dependabot PR changes are still Orbit's, so that check still applies. - PR body states the
user.typefield was verified against live GitHub event data
(BotvsUser), not assumed. - Explanatory comment on the workflow step gives the WHY and a concrete measured
instance (orbit-api#425), matching the file's existing comment convention.
Recommendation
Approve as-is. No follow-up required in this repo; the PR body notes the identical fix is
being applied to orbit-api and orbit-landing-page separately (out of scope for this
review).
There was a problem hiding this comment.
Code Review: PR #598
Recommendation: APPROVE
Single-file change to .github/workflows/guards.yml: the Dash Ban job's "PR title and body carry no dashes" step now also requires github.event.pull_request.user.type != 'Bot', so Dependabot (and other bot-authored) PRs skip the prose check on machine-copied changelog text, while the file-level dash check still runs unconditionally on files a bot PR actually changes. Small, correctly scoped, root-cause fix (not a workaround), with a compliant WHY comment naming the upstream constraint and citing orbit-api#425.
Findings
No Critical or High findings.
Info: the bot-detection style (user.type != 'Bot', any bot) differs from the rest of the file's github.actor != 'dependabot[bot]' idiom (parity job, claude-review.yml, sonarcloud.yml). Not a defect — the PR's own premise (any bot copies its changelog verbatim into the body) generalizes correctly to "any bot," so the broader primitive is arguably the more correct one here. No action needed.
Scope not touched by this diff (N/A)
Parity, i18n, contract drift/backward-compat, DESIGN.md/AI-slop, backend hard rules, and FEATURES.md parity are all N/A — this diff touches only .github/workflows/guards.yml, no apps/*, packages/shared, or orbit-api source. Comment-policy (no-comments.cjs) doesn't gate YAML, so the added comment was reviewed by judgment instead of the linter and satisfies the WHY-note bar independently.
Deferred
- Live CI check status for this PR was not retrievable in this session (gh subcommand access beyond
pr view/pr diffwas gated); YAML well-formedness and Actions-expression syntax were verified by inspection instead. - The referenced
orbit-api#425(Hangfire release note blocked by en dashes) is in a sibling repo not checked out here — taken as author-asserted, not independently verified.
Automated review via /pr-review.
|
The headless Claude engine inherited every user-scope MCP server, because --permission-mode bypassPermissions removes the interactive approval and the PreToolUse hooks match Bash and PowerShell only. --strict-mcp-config with no --mcp-config beside it loads zero servers instead. --output-format stream-json keeps the launcher's log-growth progress signal live, so a worker that spends a long model turn without touching the tree is no longer killed as KILLED_NO_PROGRESS. The installed binary refuses stream-json without --verbose. The config reader validates config.workers[config.worker] only, so a typo in the fallback engine surfaced at the first real launch, after reserveWorkerLaunch had spent one of the two launches the branch cap allows. The gate now resolves every declared tier of every declared engine. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
core.md loads on every turn and read "Claude never edits code", full stop, so a session that hit an exhausted allowance read that first and stopped the night. That is the ending D104 exists to prevent. It now points at the fallback. Five corrections inside the section the tree contradicted: the launcher's pid is the wake source with the worker's pid beside it, a subagent does NOT end the night silently because the Stop hook blocks, the --dry-run is an argv check rather than a preflight, the calibration stamp goes stale for the duration of the switch and nobody reseeds it, and the worker edit is uncommitted with git checkout -- .claude/orchestrator.json as its revert. The staleness trap in assertNotStale is named beside them, because it fires on the config edit itself. The reviewer clause turns on the word quietly. On protected main no substitute may stand in for pullfrog-approval. On unprotected redesign/main no such check exists to forge, so a separate-agent review is acceptable when the pull request comment names the substitution and lists checkable evidence. The skill is calibrated, so its verdict is rewritten for the new text and the stamp reseeded: 1 entry renewed, 31 carried forward, worker still codex. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 3 on #598. The reviewer clause 5.4.1 gained in round 2 contradicted three other sections of the same file, so a night run reached a different merge answer depending on which section it read. - Step 8, Hard prohibitions and 5.4.1 now state one branch-dependent rule: absolute on protected main, a named separate-agent review admissible on unprotected redesign/main, and a green check alone never an approval. - 5.4.1 records that the clause came from a written instruction at entry, not from a contract this run granted itself. - Pin the claude engine's command and both model ids by name, because resolveWorkerInvocation never reads command and reads the model from the block the loop compares against. claude-opus-4-8 is a real id, so that typo would run a whole night in silence. - Name the tools-harness red beside the calibration red, by assertion, and say neither licenses editing a test. - Correct the assertNotStale headline, the stream-json rationale, the launch-cap comment, the launcher line reference and the missing --tier mechanical dry run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The claude engine assertion checked that `--strict-mcp-config` is present and never that `--mcp-config` is absent, so adding the flag left every user-scope MCP server loading into a worker at bypassPermissions with the test still green. Assert the absence. `record-readiness.mjs` supplies the `pullfrog-approval` axis itself when protection names no required check, which is the whole reason an unprotected base is not a free pass (#429). That rule now lives beside the reader it feeds as `reviewChecksFor`, so a test drives the real rule, and the receipt verdict it produces on `redesign/main` with no review is pinned as CI_STALE alone. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 3 reconciled the four prose sections and left the tool saying something else. `record-readiness.mjs` supplies `pullfrog-approval` itself when protection names no required check, so on `redesign/main` a named substitute reviewer still produces a blocked receipt, and step 9 said a blocked receipt never permits a merge. One run merged and one run stopped from one tree. Step 9 now carries the single exception and the reasoning for keeping the recorder strict: it reads checks and reviews, never a claim in a comment, and a recorder that accepts an unverifiable claim is worse than a written exception a human reads (#429). Step 7 and the queue name the same branch-conditional bar by reference instead of naming Pullfrog. Hard prohibitions and §5.4.1 now say what they decide: the case where Pullfrog produced neither the check nor an APPROVED review, so neither reads as banning the #440 fallback that step 8 and the tool both allow on `main`. Also: three progress signals rather than four, the live `worker` edit is named in the handoff inventory and the step 11 report, and core.md names the file its two pointers live in. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erdict The merge bar moved out of prose alone and into step 9, where `record-readiness.mjs` decides. The verdict says what that costs a reader: the recorder cannot verify a substitution written in a comment, so it blocks on the review axis by design, and the run has to tell that one expected verdict apart from a real blocker reported under the same name. One entry renewed, 31 carried forward. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`reviewChecksFor` moved the rule, so step 9 names its real home and the line the recorder calls it from. The exception paragraph now also says what happens after it: a merge admitted there closes the pull request exactly as a READY one does, and when it does not apply the run records the `blocker` string that the Stop hook already accepts as a legitimate BLOCKED ending. Reseeded after the edit: one entry renewed, 31 carried forward. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…at ending
`merged` accepted any non-empty string, so copying the skill's own template and
leaving `<merge commit sha once it is merged, or absent>` unfilled reported an
UNMERGED pull request as a finished night, printing nothing. Driven against
`checkSleepStop`: that literal and the bare word "yes" both returned null.
Both predicates now test `/^[0-9a-f]{7,40}$/`, in the Stop hook and in the
recorder, and a new assertion pins the two copies to the same literal so the
lenient direction cannot drift back. A merged-only completion returns a terminal
MERGED banner naming each pull request and its sha, and `require-wake-source.mjs`
writes it beside the BLOCKED one, so the sha reaches a reader.
Step 11 now asks for the disposition per pull request, the readiness-receipt
assertion compares against the literal `pullfrog-approval` rather than the
constant it imports from the module under test, and both SKILL.md and NEXT.md
name the load-sensitive `launch-worker.mjs` CPU test so a fifth FAIL is checked
against that name first.
Refs #598
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e sha The skill gained a shape-checked `merged` sha, a MERGED terminal banner, the disposition per pull request in step 11, and the note that its fifth FAIL may be the load-sensitive CPU test rather than a defect. The verdict now names the call that change asks for: which self-asserted fact stays honest is the one a reader can check. `worker` stays `codex`, so `workerMoved` is false and the other 31 verdicts carry forward untouched: `stamped 32 file(s) ... 1 verdict(s) renewed, 31 carried forward`. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This round did not reproduce `ORBIT TOOLS GATE FAILED (1)` on the CPU test: the full run passes it at 1824 assertions. The measurement belongs to the review at head `4d590110`, so both documents now say so rather than reading as this run's own observation. The mechanics stay confirmed from the tree. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tract (#1023) * chore: add a Claude headless worker engine and forbid subagent workers An exhausted Codex allowance stopped every code round today. It should not: the launcher already routes on the `worker` key, so a second engine is all that was missing. `.claude/orchestrator.json` now declares `claude` beside `codex`, running Claude Code headless through the same launcher, order file, worktree, timeouts and wake source. Flags were confirmed from `claude --help`, not assumed, and `--dry-run` resolves both tiers. `§5.4.1` of the orchestrate skill states the switch plainly and forbids substituting a Claude subagent for a worker. Measured today: five subagents ran as workers, registered no wake source so the Stop hook read the run as having no continuation, and each had to be told to commit before a handoff was possible, because a subagent dies with its session and leaves its edits uncommitted in a worktree with no record of what they were. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep codex as the committed default worker tools/test-tools.mjs pins the shipped default implementer to gpt-5.6-sol at high reasoning effort, and committing worker="claude" broke both of those assertions. The gate is right: the shipped default belongs in the file, and running on Claude is an operational switch a session makes locally while the Codex allowance is out, not a new default. D95 forbids editing the gate a run is judged by, so the assertion stands and the config yields. Harness went from 61 failures to 1 with this change. The remaining one is create-worktree.mjs hitting EPERM on a lock file under Temp in its concurrency case, which touches nothing here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: isolate the claude worker engine and cover every declared engine The headless Claude engine inherited every user-scope MCP server, because --permission-mode bypassPermissions removes the interactive approval and the PreToolUse hooks match Bash and PowerShell only. --strict-mcp-config with no --mcp-config beside it loads zero servers instead. --output-format stream-json keeps the launcher's log-growth progress signal live, so a worker that spends a long model turn without touching the tree is no longer killed as KILLED_NO_PROGRESS. The installed binary refuses stream-json without --verbose. The config reader validates config.workers[config.worker] only, so a typo in the fallback engine surfaced at the first real launch, after reserveWorkerLaunch had spent one of the two launches the branch cap allows. The gate now resolves every declared tier of every declared engine. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say what the claude worker engine means when an operator sets it core.md loads on every turn and read "Claude never edits code", full stop, so a session that hit an exhausted allowance read that first and stopped the night. That is the ending D104 exists to prevent. It now points at the fallback. Five corrections inside the section the tree contradicted: the launcher's pid is the wake source with the worker's pid beside it, a subagent does NOT end the night silently because the Stop hook blocks, the --dry-run is an argv check rather than a preflight, the calibration stamp goes stale for the duration of the switch and nobody reseeds it, and the worker edit is uncommitted with git checkout -- .claude/orchestrator.json as its revert. The staleness trap in assertNotStale is named beside them, because it fires on the config edit itself. The reviewer clause turns on the word quietly. On protected main no substitute may stand in for pullfrog-approval. On unprotected redesign/main no such check exists to forge, so a separate-agent review is acceptable when the pull request comment names the substitution and lists checkable evidence. The skill is calibrated, so its verdict is rewritten for the new text and the stamp reseeded: 1 entry renewed, 31 carried forward, worker still codex. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: make the merge bar say one thing in all four places Round 3 on #598. The reviewer clause 5.4.1 gained in round 2 contradicted three other sections of the same file, so a night run reached a different merge answer depending on which section it read. - Step 8, Hard prohibitions and 5.4.1 now state one branch-dependent rule: absolute on protected main, a named separate-agent review admissible on unprotected redesign/main, and a green check alone never an approval. - 5.4.1 records that the clause came from a written instruction at entry, not from a contract this run granted itself. - Pin the claude engine's command and both model ids by name, because resolveWorkerInvocation never reads command and reads the model from the block the loop compares against. claude-opus-4-8 is a real id, so that typo would run a whole night in silence. - Name the tools-harness red beside the calibration red, by assertion, and say neither licenses editing a test. - Correct the assertNotStale headline, the stream-json rationale, the launch-cap comment, the launcher line reference and the missing --tier mechanical dry run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(tools): pin the MCP absence and the unprotected review verdict The claude engine assertion checked that `--strict-mcp-config` is present and never that `--mcp-config` is absent, so adding the flag left every user-scope MCP server loading into a worker at bypassPermissions with the test still green. Assert the absence. `record-readiness.mjs` supplies the `pullfrog-approval` axis itself when protection names no required check, which is the whole reason an unprotected base is not a free pass (#429). That rule now lives beside the reader it feeds as `reviewChecksFor`, so a test drives the real rule, and the receipt verdict it produces on `redesign/main` with no review is pinned as CI_STALE alone. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: make the tool and the prose agree on the merge bar Round 3 reconciled the four prose sections and left the tool saying something else. `record-readiness.mjs` supplies `pullfrog-approval` itself when protection names no required check, so on `redesign/main` a named substitute reviewer still produces a blocked receipt, and step 9 said a blocked receipt never permits a merge. One run merged and one run stopped from one tree. Step 9 now carries the single exception and the reasoning for keeping the recorder strict: it reads checks and reviews, never a claim in a comment, and a recorder that accepts an unverifiable claim is worse than a written exception a human reads (#429). Step 7 and the queue name the same branch-conditional bar by reference instead of naming Pullfrog. Hard prohibitions and §5.4.1 now say what they decide: the case where Pullfrog produced neither the check nor an APPROVED review, so neither reads as banning the #440 fallback that step 8 and the tool both allow on `main`. Also: three progress signals rather than four, the live `worker` edit is named in the handoff inventory and the step 11 report, and core.md names the file its two pointers live in. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(calibration): reseed the orchestrate stamp and reconsider its verdict The merge bar moved out of prose alone and into step 9, where `record-readiness.mjs` decides. The verdict says what that costs a reader: the recorder cannot verify a substitution written in a comment, so it blocks on the review axis by design, and the run has to tell that one expected verdict apart from a real blocker reported under the same name. One entry renewed, 31 carried forward. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: correct the new citations and say how the night ends `reviewChecksFor` moved the rule, so step 9 names its real home and the line the recorder calls it from. The exception paragraph now also says what happens after it: a merge admitted there closes the pull request exactly as a READY one does, and when it does not apply the run records the `blocker` string that the Stop hook already accepts as a legitimate BLOCKED ending. Reseeded after the edit: one entry renewed, 31 carried forward. #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(orchestrate): let a merged pull request end an unattended night A pull request merged under the step 9 exception could never end the run that merged it. The Stop hook reads the receipt and never the pull request state, and that receipt stays CI_STALE forever because pullfrog-approval will not publish on an unprotected base. Driven against checkSleepStop: a merged row with no blocker returned block:true, the 2026-08-08 deadlock reached after a successful merge. Adds `merged` as the third ledger disposition beside READY and BLOCKED. The bar is the merge commit sha, a fact the run records and a reader can check on GitHub. It is sticky where a blocker is not, because a merge cannot be undone. Also corrects two things the tree contradicted. The expected-FAIL count for the worker switch is four, not two: the earlier reading used --only orchestrator-config, which cannot see the two launch-worker pins. And the MCP absence was asserted on workers.claude.args, while the worker's real argv is the engine array concatenated with the tier's, so --mcp-config in a tier's args passed the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(calibration): reseed the orchestrate stamp for the third disposition Sharpens the CI_STALE signature in step 9, then reseeds. The signature is delivery's ci.checks.pass true beside the receipt's ci.green false, and only a required check absent from the rollup entirely reads that way: one that ran and failed lands in rollup.failing, and a running one in rollup.pending, so either drops delivery's own pass. The verdict text is rewritten before the stamp, so the reseed is not re-blessing old reasoning about new prose. worker stays codex, so workerMoved is false and the other 31 dates carry forward untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(orchestrate): require a real sha for a merged row and announce that ending `merged` accepted any non-empty string, so copying the skill's own template and leaving `<merge commit sha once it is merged, or absent>` unfilled reported an UNMERGED pull request as a finished night, printing nothing. Driven against `checkSleepStop`: that literal and the bare word "yes" both returned null. Both predicates now test `/^[0-9a-f]{7,40}$/`, in the Stop hook and in the recorder, and a new assertion pins the two copies to the same literal so the lenient direction cannot drift back. A merged-only completion returns a terminal MERGED banner naming each pull request and its sha, and `require-wake-source.mjs` writes it beside the BLOCKED one, so the sha reaches a reader. Step 11 now asks for the disposition per pull request, the readiness-receipt assertion compares against the literal `pullfrog-approval` rather than the constant it imports from the module under test, and both SKILL.md and NEXT.md name the load-sensitive `launch-worker.mjs` CPU test so a fifth FAIL is checked against that name first. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(calibration): reseed the orchestrate stamp for the checked merge sha The skill gained a shape-checked `merged` sha, a MERGED terminal banner, the disposition per pull request in step 11, and the note that its fifth FAIL may be the load-sensitive CPU test rather than a defect. The verdict now names the call that change asks for: which self-asserted fact stays honest is the one a reader can check. `worker` stays `codex`, so `workerMoved` is false and the other 31 verdicts carry forward untouched: `stamped 32 file(s) ... 1 verdict(s) renewed, 31 carried forward`. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: attribute the load-sensitive FAIL to the head it was measured at This round did not reproduce `ORBIT TOOLS GATE FAILED (1)` on the CPU test: the full run passes it at 1824 assertions. The measurement belongs to the review at head `4d590110`, so both documents now say so rather than reading as this run's own observation. The mechanics stay confirmed from the tree. Refs #598 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>



The Dash Ban step fed the raw PR title and body to check-dashes.mjs with no author scoping. Dependabot copies the dependency's own changelog verbatim into the body, so any upstream release note containing an en or em dash fails the gate on text Orbit neither wrote nor can edit, and which Dependabot regenerates on every rebase.
Measured: orbit-api#425, a 21-package NuGet security bump, is blocked right now by en dashes inside a Hangfire release note. The identical defect exists in all three repos and is being fixed in all three.
The file-level dash check deliberately still runs on bot PRs: the files a Dependabot PR changes ARE Orbit's, so new dashes in them are still a real violation. Only the prose check, whose whole premise is that the text is Orbit-authored, is scoped.
Verified github.event.pull_request.user.type against live data: Bot for dependabot[bot], User for a human author.