v3.2.0
The gates gate what they are given. otito gate and otito review take the repository from the positional or --path, in local and PR mode alike, and every CLI gate, otito pass-pr included, reads policy and governance from the .otitorc.json of the repository it gates, as the MCP gate tools now do too. The GitHub PR gate reads whether a PR is open, merged or closed and reports it as pr.state and pr.mergedAt, new report fields that make this a minor release. A --pr or selector that names no PR is refused on the command line rather than gating the wrong thing; over MCP a blank pr reads as no PR, and pr_merge_readiness with no selector gates the checked-out branch's PR again. Repair hints name the @nugehs packages that exist, and otito help no longer promises the legacy MCP tool names go away at 3.0. No command, field or schema was removed.
Fixed
- The compliance-controls gate names the bouncer package that exists. When a repository has
bouncer.config.jsonand no bouncer binary resolves, the local gate told you to runnpm install --save-dev @bashbop/bouncer, which npm answers with a 404: bouncer is published as@nugehs/bouncer. The hint and the README link now name it. A repository that runs bouncer in CI instead, through a workflow under.github/workflowsthat references@nugehs/bouncer(such asnpx -y @nugehs/bouncer@latest check), now gets a warning that says so and names the workflow line, rather than one that reads as a broken install. It stays a warning: the gate reads the workflow file but never runs npx or installs anything, so it has no evidence that the controls pass for the change it is looking at. - The contract-drift gate names the tieline package that exists. When a repository has
tieline.config.jsonand no tieline binary resolves, the local gate told you to runnpm install --save-dev @bashbop/tieline, which npm answers with a 404: tieline is published as@nugehs/tieline. The hint and the README link now name it. The README's aiglare link had the same fault and now points at@nugehs/aiglare, where aiglare is published. - The GitHub PR gate knows whether a PR is open, merged or closed.
otito gate --pr,otito pass-pr,otito review --prandreview_gate { pr }never askedghfor the PR's state, so a merged or closed PR was gated as if it were still open. Once #214 merged, GitHub reported its mergeability asUNKNOWNand the gate answered "GitHub mergeability is not settled yet"; the audit ledger's post-merge records for #195, #196 and #197 carry the samePR state: WARN. A closed PR was judged on the mergeability GitHub last reported, so #188 read as pending and #153 as conflicted. The gate now requestsstateandmergedAtand reports both underpr. A merged PR's state passes, "PR is already merged; there is nothing left to gate.", and the terminal verdict no longer names a check blocking it or calls it ready to merge. Its review decision, CODEOWNERS, conversation, branch-protection and status checks still run, because they describe what merged and post-merge attestation records them, so a PR merged without CODEOWNERS approval still fails that check. A closed PR's state fails: it cannot merge unless it is reopened. - docs/03 and CONTRIBUTING.md said otito runs team governance; it runs solo. The Contributor Governance page called this repository a team repository and ended "Do not copy solo governance onto this repository", and CONTRIBUTING.md said code owners review changes under team governance, while the checked-in
.otitorc.jsonhas made solo the default for every otito command run here since #193. Both now say the repository has one maintainer and what solo means for its gate: a missing separate review or CODEOWNERS approval isWARNevidence, notFAIL, requested changes still fail, and an explicit--governance teamstill overrides the file. - The MCP gate tools honour the repository's
.otitorc.json, as the CLI does. On 2026-09-26otito pass-pr 214gated #214 under solo governance and warned on CODEOWNERS ("solo-maintainer mode requires an explicit owner/admin merge decision"), whilereview_gate { pr: "214" }failed the same PR under team governance ("CODEOWNERS approval is missing"). This repository checks in.otitorc.jsonwith"governance": "solo". The CLI fills an omitted--policyor--governancefrom that file and the user config before any gate runs; the MCP server passed its arguments straight through, so a missinggovernancenormalised toteam.review_gateandreview_verdict, and themerge_readiness,pr_merge_readinessandreview_praliases that route to them, now fill an omitted or blankpolicyandgovernancefrom the same config, found from the repository atpathrather than from wherever the MCP host started the server, and their schemas say so. An explicit argument still wins. Called from a server started outside the repository,review_gate { pr: "214", path }now reportsgovernance: solo, CODEOWNERSWARNand verdictWARN, asotito pass-pr 214does, andgovernance: "team"still fails it. The lookup also walks up from a relative path now:path.dirname(".")is., so a gate called withpath: "."from a subdirectory stopped where it began and never reached the repository's.otitorc.json. - The CLI gates read the config of the repository they gate, not of the directory they run in.
otito pass <repo>,otito review <repo>,otito gateandotito pass-pr <n> --path <repo>filled an omitted--policyor--governancefrom the.otitorc.jsonfound from the working directory. On 2026-09-26, run from outside this repository,otito pass-pr 214 --path <checkout>gated #214 under team governance and failed it ("CODEOWNERS approval is missing for one or more changed files."), whileotito pass-pr 214run inside it read the checked-in"governance": "solo"and warned; run from inside it,otito pass <repo>gated a repository with no config as solo. Each command now resolves both from the repository it gates, the positional forpassandreview,--pathforpass-prand either forgate, throughgatePolicyinsrc/lib/config.js, which the MCP gate tools now share: that repository's.otitorc.json, then the user config. An explicit flag still wins and a blank one (--governance=) counts as omitted, as it does for the MCP tools. From outside the checkout,otito pass-pr 214 --path <checkout>,otito gate --pr 214 --path <checkout>andotito review <checkout> --pr 214now reportgovernance: solo, CODEOWNERSWARNand verdictWARN, and--governance teamstill fails the PR.otito workspace-gategates every repository under one policy and one governance, which its parent receipt records once, so it now resolves each repository's own config and, when they disagree, refuses rather than gate one repository under another's setting: "workspace-gate runs every repository under one governance, and their configs disagree (…/web: team, …/api: solo); pass --governance to choose it". The flag settles it for all of them; repositories that agree, including through a shared.otitorc.jsonabove them, need none. Emoji, color and theme still come from the directory otito runs in. otito gategates the repository it is given, in either mode. The local gate read the repository only from the positional and the PR gate only from--path; each ignored the other without a word and gated the directory otito ran in. On 2026-09-26, run from this checkout,otito gate --path <other-repo> --base HEAD~1 --jsonreportedrepo.rootas this checkout, andotito gate <other-repo> --pr 214 --jsongated #214, the PR with that number in this repository, under this checkout's.otitorc.json. The docs name the repository with--path(otito gate --pr "$PR_NUMBER" --path .),otito helpwith the positional (otito gate <repo>), and theagent-toolscatalog with--pathin both modes (otito gate [--pr <selector>] --path <repo>), so each documented form was silently dropped in one mode. Both modes now take the repository from the positional or--path, and resolve policy and governance from that repository's.otitorc.json. Naming it both ways is refused unless both name the same directory, compared by real path so.and a symlinked spelling of the same checkout agree: "gate was given two repositories (. and --path ../api); pass only one". A--pathwith no value is refused too. Rerun from this checkout,otito gate --path <other-repo>gates that repository under its ownhigh-riskpolicy,otito gate <other-repo> --pr 214asksghabout that repository, which has no remote ("no git remotes found"), instead of gating #214, andotito gate --pr 214 --path .still gates #214.otito helplistsotito gate [repo | --path repo]for the local gate andotito gate --pr <selector> [repo | --path repo]for the PR gate.otito reviewreviews the repository named by--path.reviewread the repository only from its first positional and took the positionals after it as the request, so it ignored--path, which theagent-toolscatalog gives as the CLI form of thereview_verdictMCP tool (otito review --path <repo> --json). On 2026-09-26, run from this checkout,otito review --path <other-repo> --base HEAD~1 --jsonreportedrepo.rootas this checkout and gated it under this checkout's settings (the defaultstandardpolicy), not the other repository's.otitorc.json(high-risk);otito review --path <other-repo> "tweak a"took the request for the repository ("repo path does not exist: …/tweak a"); and, run from another repository,otito review --pr 219 --path <checkout>askedghabout the repository it ran in ("no git remotes found") instead of #219.reviewnow reads its arguments asimpactandaxdo: with--path, that names the repository and every positional is the request; without it, the first positional names the repository and the rest is the request. Policy and governance come from the.otitorc.jsonof the repository reviewed, and a--pathwith no value is refused: "review --path needs a repository, e.g.otito review --path .". Rerun, the first two review the other repository under itshigh-riskpolicy, the second with the request "tweak a", and the third reviews #219 under this checkout's solo governance (WARN).otito helplistsotito review [repo | --path repo] [request].otito helpno longer says the legacy MCP tool names stop working at 3.0, and links a mapping that exists. It saidpr_review,review_pr,merge_readiness,pr_merge_readiness,repo_catalog,repo_discoverand thefind_*tools "keep working via tools/call until 3.0. See docs/MIGRATION-2.0.md.", and the comment onLEGACY_TOOL_ALIASESinsrc/lib/mcp.jsmade the same promise. The deadline was Repoctx's, set when its 2.0 folded 18 tools into 11. The rebrand to otito (a0f4f98, 2026-07-29) deleteddocs/MIGRATION-2.0.md, anddocs/MIGRATION-3.0.mdwith it, and its renaming turned the comment's "until repoctx 3.0" into "until otito 3.0". otito 3.0.0 and 3.1.0 shipped with all ten names still dispatching, astests/mcp-dispatch.test.jschecks, and with the help unchanged, so it described a removal that never happened and linked a page that did not exist. The help and the comment now say the names still work in 3.x and that no release is named to remove them. Both point at a new "Legacy tool names" table under "MCP Tool Surface" in docs/02-mcp-agent-workflows/README.md, restored from the deleted guide, which maps each name to its canonical tool and says how its arguments are translated; the help links it on the docs site. The help test now fails if that section disappears, and a new test fails if the table andLEGACY_TOOL_ALIASESdisagree on a name or its target. No alias was removed.- The MCP gate tools read a blank
pras no PR, andpr_merge_readinesswith no selector gates the checked-out branch's PR again. Before 2.0,pr_merge_readinessgated "the current branch's PR" when itsselectorwas left out, as its schema said and asgh pr viewdoes. #66 folded it intoreview_gateand mapped a missing selector topr: "", whichreview_gatereads as no PR, so the alias has run the local gate ever since, while the comment inreview_gatesaid it did "exactly what the old pr_merge_readiness and merge_readiness did".review_verdictreadprdifferently again:""ran the local gate, but" "counted as a PR, andgh pr viewdrops a blank selector and answers with the checked-out branch's PR. On 2026-09-26, against a checkout offix/review-path-flag,pr_merge_readiness {}andreview_gate { pr: " " }each returned a local-gate report againstorigin/main, whilereview_verdict { pr: " " }gated #220, which it never named ("PR is already merged; there is nothing left to gate."). The test meant to pin the route, "empty pr selector still routes to the PR gate and returns a payload", checked only that some text came back, so it passed on the local gate.review_gateandreview_verdictnow read a blankpras omitted and run the local gate, as they already read a blankpolicyorgovernance: unlike--pr "$PR_NUMBER"on the command line, a JSON argument has no unset variable behind it. Aprthat names a PR still gates it.pr_merge_readinesswith no selector, or a blank one, gates the checked-out branch's PR, whichreview_gate's ownprcannot ask for; aselector, or apr, still names the PR. Both tools'prschema descriptions say so, and the tests run each call against a fakeghthat answerspr view 42and a barepr viewwith different PRs, and assert which one, if either, was gated. Rerun,pr_merge_readiness {}gates #220 andreview_verdict { pr: " " }runs the local gate. otito gate --prandotito review --prrefuse a--prthat names no PR, andotito pass-pra blank selector. The gate switched to the GitHub PR gate only when--prcarried a non-empty value. On 2026-09-26,otito gate --path . --base HEAD~1 --pr= --jsonand the same command with a bare--preach returned a local-gate report, verdictWARN, exit 0 and noprfield, as if--prhad never been given. The documented CI formotito gate --pr "$PR_NUMBER" --path .fared worse with the variable unset: the parser reads--pr ""as a bare--prand keeps the empty string as a positional, which the gate took for the repository, failing with "git rev-parse --show-toplevel: command failed" when run from the repository and "gate was given two repositories ( and --path …); pass only one" from anywhere else.otito review --pr=ran the local gate the same way, andotito review . --pr "$PR_NUMBER"with the variable unset askedghfor the PR of a branch namedtrue. A blank selector that reachedghwas dropped, sogh pr viewfell back to the checked-out branch's PR: against a checkout offix/cli-gate-config-from-gated-repo,otito pass-pr "" --path <checkout>andotito gate --pr " " --path <checkout>both gated #217, which neither named.gateandreviewnow refuse a--prthat is bare or blank before they read a repository, "gate --pr needs a PR number or URL, e.g.otito gate --pr 123 --path ." (reviewnamesotito review . --pr 123), andpass-prrefuses a blank selector: "pass-pr was given a blank PR selector; name the PR, e.g.otito pass-pr 123 --path ., or leave it out to gate the current branch's PR". Each exits 1, with the message aserrorunder--json. Leavingpass-pr's selector out still gates the current branch's PR, asgh pr viewdoes, andotito gate --pr 217 --path .still gates #217.