Releases: BASHBOP/solumbe
Release list
v4.0.0
Òtítọ́ is now Solumbe: the package, binary, MCP server, config files, evidence folders and environment variables all take the new name, with no fallback to the old ones. Behaviour is unchanged from 3.5.0.
Changed
-
Òtítọ́ is now Solumbe. Every name moves in one cut, with no fallback to the old ones:
Was Now npm @bashbop/otito@bashbop/solumbeCLI otitosolumbeMCP Registry io.github.BASHBOP/otito, host keyotito(mcp__otito__*tools)io.github.BASHBOP/solumbe,solumbe(mcp__solumbe__*)Config .otitorc.json, gate configotito.gate.json.solumberc.json,solumbe.gate.jsonEvidence .otito/, user state~/.otito/.solumbe/,~/.solumbe/Environment OTITO_*SOLUMBE_*Skills otito-*, secret markerotito:allow-secret, PR comment marker<!-- otito-pr-review -->solumbe-*,solumbe:allow-secret,<!-- solumbe-pr-review -->Repository BASHBOP/otito, docsbashbop.github.io/otitoBASHBOP/solumbe,bashbop.github.io/solumbeTo upgrade, install
@bashbop/solumbe; rename.otitorc.json,otito.gate.jsonand anyotito:allow-secretmarkers; move.otito/and~/.otito/; renameOTITO_*variables; and point MCP host configs at the new package and server key. Two things keep the old name on purpose: telemetry sharing still posts to bashbop-api'sanalytics/otitoroute with anotito_versionfield, because both sides of that contract have to move together, and links to releases before 4.0.0 still point at@bashbop/otito, where those versions live. -
A manually run workflow,
retire-legacy-listing.yml, marks every version of the pre-rename MCP Registry listing deprecated and points it atio.github.BASHBOP/solumbe. It refuses to run until the new listing is live. -
The Release workflow skips
npm publishwhen npm already has the version, instead of failing and taking the GitHub Release and MCP Registry jobs down with it. A new package name's first publish has to be done by hand, because npm Trusted Publishing is configured per existing package; re-running a release after a later job failed also hits this. -
scripts/rebrand.mjs(plan,apply,check) made the rename from.rebrandrc.json: case-preserving, git-moved paths, idempotent.npm run qualitynow runsrebrand:check, which also flags accented spellings no alias covers, so the old name cannot creep back outside the CHANGELOG and the preserved links.
v3.5.0
Model routing can now make a share of requests follow the tier it picks instead of only recommending one, and on Claude Code the premium tier names the model that actually runs. No command, field or schema was removed.
Added
- Enforce mode for model routing.
OTITO_ROUTE_MODE=delegateputs a share of requests (OTITO_ROUTE_DELEGATE_SHARE, 0 to 1, default 0.5) in a delegate arm, where the Claude Code prompt hook tells the session to do the tool work in a subagent on the routed tier, the only model switch a session can make; the rest are the control. Arms are assigned by the prompt's hash, so the same prompt always lands in the same arm. Each logged decision records its arm,route-outcomes.mjs --arm delegate|control|advisorygrades one arm, andotito routeshows the arm and the subagent model in the terminal and asenforcein JSON. Without the variable, routing stays advisory and nothing changes.
Fixed
- On the
claude-codehost,otito routeand the route prompt hook name the premium modelclaude-opus-5-5instead ofclaude-opus-5. The Agent tool'sopusalias runs Opus 5.5, so the old name was never the model that ran.
v3.4.0
One context pack can now span a web app and its API, change risk stops counting files that are only loose leads, and otito route shows each question's own confidence and says plainly when it has no recommendation. No command, field or schema was removed.
Added
context_packandotito contextread a repository'scompanionstoo: list sibling repositories in its.otitorc.json("companions": ["../api"], resolved against that file) and one pack covers them all, so a web bug whose cause is in the API response no longer gets a web-only pack. Explicitpathsstill win; a companion that is not checked out is skipped.otito routeshows each Score question's own confidence (specificity, blast radius) next to its score, not only the weaker of the two. Display only: confidence still never moves the tier.scoring.confidencescarries both.
Fixed
change_impactno longer raises a risk from a domain that only an advisory lead touches. An RSVP gallery fix ranked the RSVP payment-success page as a loose lead and warned of a money-flow change; risks now come from required and supporting files, plus any changed file.otito routeand the route prompt hook say no recommendation when otito matched no files, instead of presenting a read of an empty set as a tier. The fail-safe tier is still named, andtierin JSON is unchanged (premium), so callers that read onlytierbehave as before.version:checkcatches a stale docs site.docs/index.mdopens with a**vX.Y.Z** is publishedbanner, which replaced the**Status:** vline the release sync and drift check looked for, so neither saw it: 3.3.0 was released on 2026-09-27 withversion:checkgreen while bashbop.github.io/otito still announced v3.2.0 and had no 3.3.0 entry under What's New.syncPinnedDocVersionnow rewrites the banner,findPinnedDocVersionDriftreports it, and a newfindWhatsNewDriftfailsversion:checkwhen What's New has novX.Y.Z publishedentry for thepackage.jsonversion (it is written by hand, so the sync cannot add it). The site gets its v3.3.0 banner and entry.context_packreads a request that says "bug", "broken", "crash" or "regression" as debugging instead of an ambiguous action, and in a multi-repo query that names one repository by a word from its folder name, that repository's files rank first.change_impactno longer drops a UI page (page.tsx,layout.tsx) from required owners because the request used no API wording; controllers, DTOs and API routes still need it. A query term that recurs across most of the repository counts for less, and utility-file owners compete on score with conventional owners instead of only being a last resort.
v3.3.0
The terminal output gets colour and shape it didn't have before. otito resolves one glyph set per run (plain Unicode by default, ASCII in CI, emoji opt-in) and builds tables, trees and coloured lists from shared string-building primitives instead of ad-hoc strings; every bullet and list item now dims its marker the way a box border is dimmed, matching the headers, boxes and closing line that already had colour. otito install prompts a human at a TTY for how to install, with a short personalized pitch first, while every non-interactive caller (--yes, --json, CI, agents) is unaffected. No command, field or schema was removed.
Added
- List and bullet items pick up the renderer's colour, not just headers, boxes and the closing line.
bullet()insrc/lib/render/fancy.jsand every formatter that built a line as`${renderer.glyphs.item} ${text}`by hand (the shared summary behindinstall/init/discover/index/catalog/search/repoinsrc/lib/output.js,context_pack's Commands section,change_impact's suggested tests and risk hotspots, and the Context evidence lines inpass/pass-pr) now dim the marker the same way a box border is dimmed, when colour is on. Colour off (piped output,NO_COLOR, CI) is unaffected.tests/fixtures/render-fancy-golden.jsonandtests/fixtures/formatters-golden.jsonwere regenerated on purpose; every changed line is a bullet or list-item marker gaining\x1b[2m...\x1b[0m, nothing else moved. otito installasks a human at a TTY how they want to install, with a personalized one-line pitch first. No--global/--link/--yes/--jsonand a real terminal (process.stdin.isTTY) prompts once for plan-only, global (npm install -g .) or link (npm link), preceded by a greeting built from localgit config user.name(no network, nothing sent anywhere) and three lines on why a grounded, deterministic local context layer is worth having. Any explicit mode flag,--yes,--json, CI, or an MCP/agent caller skips both the pitch and the prompt and gets the exact plan-driven outputinstallalready gave, unchanged. NewpromptChoiceinsrc/cli.jsmirrors the existingpromptYesNopattern frominit;getWelcomeMessagelives insrc/lib/install.js.- The renderer resolves one glyph set per run and offers a set of shared string-building primitives.
createRenderernow picksemoji,asciiorunicodeonce and exposes the choice asrenderer.glyphModewith every mark it prints inrenderer.glyphs: status and verdict marks, box drawing, tree branches, the flow arrow, bullets, the section and tip markers and the dashes. Nothing visible changes yet: the interactive default is stillemoji, andemoji: false,NO_EMOJIandCIstill giveascii.unicode, which prints✓ ! ✗, box drawing and arrows and nothing that matches\p{Extended_Pictographic}, is reached only through the explicitglyphs: "unicode"option; it becomes the default in a later release. New string builders sit besideheaderandverdict:table(borderless, left-aligned, measured in display cells, with a dim rule under the head row and a last column that wraps to the terminal width),tree,flow,code,ref,list,phaseandclose, pluspaintand a palette withmagentaandblue.visualWidth,padRightandwrapare exported.
Changed
- The default terminal look is plain Unicode, with no emoji. An interactive terminal now gets
✓ ! ✗, box drawing and arrows; the header boxes no longer carry a decorative emoji, and nothing in the default output matches\p{Extended_Pictographic}. CI logs,NO_EMOJI=1,--no-emoji,emoji: false, theminimaltheme and, new in this release,TERM=dumbkeep ASCII glyphs, so anything already reading otito's plain output sees no change.--emoji,emoji: trueandOTITO_EMOJI=1opt back into the emoji look exactly as it was. Thehigh-contrasttheme keeps its bright palette and stops forcing emoji.TERM=dumbis detected in the renderer besideCIandNO_EMOJI, not in config, so the sourcesotito config listshows stay honest. - Every command that prints for a person ends with one closing line.
Verified.,Tests pass.,Runs without errors.orNot verified — manual check needed: <what>.(-in ASCII). Advisory commands (repo,discover,index,catalog,search,ax,calibrate,converge,regret,map,matrix,pr,report,workspace,harness,data-access,structure,deps, plaineval,config list,config get,telemetry status,installwithout a flag) end withRuns without errors.. Writers (init,install --global|--link,config set,telemetry on|off|clear|share,dashboard,obsidian,attest,pr --comment) end withVerified.once what they wrote re-reads, and name the file or step when it does not. Health checks (attest --verify,eval --accuracy|--harness|--gate-effectiveness) are verified when the chain is intact or the thresholds hold, and name the record or check that failed.workspace-gateends the way a gate does:Tests pass.when--run-validationran and passed,Verified.on PASS, otherwise the check that blocked or warned.--json,--markdown,--mermaid, the--outmessages,config get <key>,--version,helpandmcpcarry no closing line. The gate, review, context, impact, route and doctor formatters get theirs in the next change. - Summaries, config and telemetry take otito's shared shape. The summary behind
init,install,discover,index,catalog,searchandrepoprints its facts as a table under "At a glance", file lists as a tree and every other section as a-list.otito config listis a key, value and source table,otito config geta key and value table,otito telemetrya table followed by the commands that change it, andotito depsa header, a table andpath:linematches. - Markdown tables in reports are aligned.
renderDocumentpads the cells of a table, and extends the dashes of its rule row, so the columns line up. It aligns a table only when every row splits into the same number of cells after honouring\|and code spans, and leaves it untouched otherwise; nothing is removed, so every source character still reaches the terminal. otito helpdocuments--emoji,--no-emoji,--color,--no-colorand--themeonce, as global flags, instead of on the few commands that happened to list them.- Every terminal formatter takes its marks from the renderer's glyph set. About fifty
renderer.emoji ? "…" : "…"ternaries acrosscontext,impact,pass,pass-pr,review,route, the shared summary and the section, rank, bar and detail helpers now readrenderer.glyphsor askrenderer.pick({ emoji, ascii, unicode })for a mark, so the box, status and verdict sets, the list markers and every decoration follow the glyph mode instead of one boolean.renderer.emojiis no longer read outside the renderer, and a test keeps it that way.otito reportbuilds a renderer from the same--emoji,--colorand--themepreferences every other command honours. Nothing visible changes:tests/fixtures/formatters-golden.jsonrecords each formatter's output in emoji and ascii modes, with and without colour, before this change, andtests/formatters-golden.test.jscompares byte for byte. - A version bump that reaches
mainis tagged and released without a hand-pushed tag. TheReleaseworkflow runs only on a pushedv*tag, and pushing it was a manual checklist step. On 2026-09-26 the 3.2.0 release merged tomainas 0766fe7 at 20:33 UTC withotito CI, the docs deploy and the post-merge attestation all green, and nobody pushed the tag, so npm, GitHub Releases and the MCP Registry stayed on 3.1.0, and the docs site with them, untilv3.2.0was pushed by hand at 21:12. A newTag releaseworkflow runs whenotito CIpasses on amainpush, reads the version frompackage.jsonat that commit and, whenvX.Y.Zdoes not exist yet, tags the commit withscripts/tag-release.shand starts theReleaseworkflow on the tag. A tag pushed withGITHUB_TOKENtriggers no workflow, sorelease.ymlnow also acceptsworkflow_dispatch, which keeps it the workflow npm Trusted Publishing trusts; dispatched on a branch, it refuses before publishing. Amainpush whose version is already tagged, which is every push that is not a release, tags nothing. A version that is not newer than the latest release tag, or thatCHANGELOG.mdhas no section for, fails the run instead of publishing a release nobody prepared. Pushing a tag by hand still works, and is how to release a commit CI did not run on.
Fixed
otito install's next steps no longer tell you to runotito doctorto verify the install.getDoctorReport(src/lib/doctor.js) never checks theotitobinary itself — it checks seven unrelated tools (node,git,gh,rg,npx,opensrc,code-structure), three of which its own output calls "optional accelerators". Whetherotitolanded on PATH is already verified insideinstallOtito/handleInstallviacommandExists, surfaced as the summary's installed/not-verified close line, so the doctor pointer added a step that checked nothing the install itself hadn't already checked.getInstallPlan().nextStepsinsrc/lib/install.jsnow starts atindex --discover.context_packandchange_impactno longer rank translation catalogs, file extensions and test notes ahead of the code a request names. Found reviewing otito's use on bashbop-event-web (2026-09-27), every rule shared by both engines insrc/lib/ranking-rules.js. A translation catalog is demoted (×0.3) unless the request is about copy, wording, i18n or translation, and the locales of one catalog (messages/en-GB.json,en-NG,en-US,fr,pcm-NG) share one entry that lists the rest undersiblings; before, two locales we...
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 ...
v3.1.0
The router gets graded, and the answer so far is that nothing can grade it yet. otito regret replays a repository's history and grades the tier each half of the router would have given against the same repaired outcome otito calibrate uses; --rescore grades a new arithmetic on frozen model answers. On three repositories no variant orders outcomes, and an offline audit of the join and of a same-session outcome says why, so the route stays advisory. The route-prompt hook now keeps every decision it makes, so the router can be graded once enough real requests exist. Attestation moves into the CLI as otito attest, with versioned records and a reusable workflow. No command, field or schema was removed.
Added
- The How It Works page is tied to every change in the CLI, the tool catalog and the release. Three new checks, each with a message that says where to make the fix. Every command
otito helplists must be a card on the page or a row inSUPPORTING_COMMANDS(scripts/how-it-works/content.js) with the reason it is not a stage of the loop, so a new command cannot ship without deciding where it belongs, and a command that leaves the CLI has to leave the table too. Each MCP tool now carries asummaryinsrc/lib/mcp.js, one or two plain sentences that the page andotito agent-tools --jsonprint; the MCP description is unchanged andsummaryis not sent overtools/list. The page stamps the package version (<meta name="otito:version">) and theversionlifecycle script re-renders it, so a release that skippednpm run docs:diagramfailsdocs:diagram:check. - "The gate never consults a model" is a test, not a sentence.
tests/gate-imports.test.jswalks the static import graph from every verdict-producing module (pass-local,pass-pr,pr-review,converge,review,impact) and fails if any path reachesjev.js,model-route.jsorcontext-read.js. The page's first guarantee is checked against the same test. otito attest/otito attest --verify. The post-merge attestation moves out ofaudit-pilot/attest.mjsand into the CLI.otito attest <repo> --verdict <file> --merge <sha> [--prev <sha>] [--pr <n>] [--author <name>] [--committed <iso>]appends a hash-chained record toaudit-pilot/ledger.jsonlunder the repository (or the file--ledgernames);--verifyrecomputes the chain and exits 1 if any record was altered. Both take--json.scripts/post-merge-attest.shandscripts/reconcile-attestations.shcall the command and honourOTITO_LEDGER.schemaVersionon the verdict JSON and on every ledger record.otito review --jsonnow reportsschemaVersion: 1besidereviewEngineVersion, and each attestation carriesschemaVersion: 1and theverdictSchemaVersionit was built from. Records written before these fields existed verify unchanged.- Jev calls identify Otito. Every call to TypeSafe goes out under
user-agent: otito/<version>, with the client name and version only. otito regret, the router graded against the repository's own history. For each non-fix commit it checks the first parent out into a temporary worktree, scores the commit subject as the request, and grades three tiers side by side: the deterministic half alone (AX, containment and bumps, every model term at zero), the shipped offline heuristic, and the Jev read whenTYPESAFE_API_KEYis set (--offlinekeeps a run keyless). Outcomes are the line-overlaprepairedjoinotito calibrateuses, now exported fromcalibrate.jsasjoinRepairsandreadHistoryso both grade the same thing. Per tier: n, repaired, rate, lift, with rates withheld below--min-sample; per variant: whether cheap < mid < premium orders outcomes, and the regret count, a commit routed cheap that was repaired within the window. Per question: mean, min, max and spread of the answers across the corpus, so a question that cannot separate its inputs shows as a flat line. Never a saving: otito does not know which model a host used. Offline the run is a pure function of repository state with a receipt; with a key the receipt says the model's answers are not replayable.--json,--out,--since,--max,--window,--quiet. Run on this repository (150 gradable commits, 47 younger ones censored, 30-day window,jev-1.13.0): no variant is shown to order outcomes, every interval overlaps every other, and no variant routes premium often enough to grade the top tier. The deterministic half sits at the 17% base rate in both tiers it uses; the keyless heuristic runs the wrong way within noise (cheap repaired 20.3% against 14.8% for mid); the Jev read runs the right way within noise (cheap 14.7%, mid 17.9%) and its answers now separate their inputs, but the arithmetic funnels three quarters of commits intomid. The table, intervals and receipts are in docs/18 under "Measured, otito, 2026-09-26".otito regret --rescore <run.json>, a change to the router's arithmetic graded on frozen model answers. Jev's answers are not replayable, so two arithmetics graded on two runs also differ by the model's drift between them. Each regret row now keeps the signalsscoreDecisionreads (containment,riskPathsbesideaxandcandidates) and the answers exactly as scored underinputs(Score expectation, confidence and level distribution; Noul probability), andrescoreRegret/--rescoreapplies the current arithmetic to a saved run with no checkout and no model call. The rescore carries its own receipt, names its source inmethod.rescoredFrom, and keeps the source'sreplayable: false. Runs saved by regret 0.2.0 are refused rather than rescored on rounded answers. docs/18 records the bar the next arithmetic has to clear, written before any candidate was scored, and the three frozen runs it is graded on: each repository's whole history,jev-1.13.0, 2,385 answered calls, $0.20, every saved tier reproduced by a rescore. Against that bar the shipped arithmetic fails in two places. On bashbop-api the keyless tier is inverted (cheap repaired 22.5% against 10.1% for mid, intervals apart) while the Jev tier is ordered (4.8%, 12.1%, 38.6%) and its escalations out of the cheap lane are right (13.5% repaired against 4.8% for the commits it left cheap). On bashbop-event-web the Jev read routes 8 of 1,016 commits cheap, too few to grade. The first candidate under the bar, which charges each Score term from a centre so a read at the easy end earns a share of AX back, passed the tuning runs at one centre only (0.2, with its neighbours failing inside the intervals) and failed the confirmation run on criterion 3; it does not ship, and docs/18 records the sweep, the confirmation, and that a cheap-ward push found almost nothing to lift in this backtest.regretEngineVersion0.3.0.otito regretgrades no release commits, and the bashbop-api findings above are withdrawn. A commit written by release tooling (chore(release): 2.26.5 [skip ci],chore: bump version to 1.4.0, a bare version, anything marked[skip ci]) is neither a request nor an outcome: no router saw it, and the join almost never reads one as repaired. They now leave the corpus the way fix commits do (RELEASE_SUBJECT), the corpus line prints how many, and--rescoreapplies the rule to runs saved before it, with a caveat naming how many rows left. On the frozen bashbop-api run they were 504 of 1,214 graded commits, 4 repaired, and 365 of the 474 in the deterministic cheap lane: that lane's 10.1% was the release commits, the keyless "inversion" was the heuristic moving them tomid, and the model's 186-commit cheap lane was 166 of them. Re-graded without them no variant orders outcomes on either bashbop repository, the human base rates are 41.7% (bashbop-api) and 50.5% (bashbop-event-web), and the centre candidate's tuning pass does not survive. docs/18 "Re-graded, 2026-09-26: without release commits" has the tables.regretEngineVersion0.4.0.- The
repairedjoin audited, and the fix rule widened. Before any further arithmetic, the question the re-grade left open was put to the frozen runs offline: 7 and 14 day windows, eight stricter joins (2 or 3 overlapping lines, fix commits capped by files or lines), the share of repairs owed to the ten largest fixes, the unlabelled fixes, and theDevelop (#N)and dependency-bump exclusions. The join reproduces every frozen row; on the bashbop repositories it is not a few sweeping fixes (204 and 284 distinct earliest fixes for 296 and 472 repairs); and no cut orders any graded variant. Commit history cannot grade the router on these repositories, and the next evidence has to come from live requests; docs/18 "Audited, 2026-09-26: the join" has the cuts and the record a live log should keep. One rule was wrong on its own terms:FIX_SUBJECTmissedhot-fix(...),hot-fit,bug(...),patch,fixes,fixedandfixing, so 79 bashbop-api and 40 bashbop-event-web fix commits were graded as requests and invisible as repairs (joined, they add 6 and 9 repairs and move no tier).calibrateandregretnow shareFIX_COMMIT_RULE, and--rescoredrops the rows the wider rule reads as fixes, counts them on the corpus line, and says their own repairs need a replay to join.regretEngineVersion0.4.1. - The route-prompt hook keeps its decision, and
route-outcomesgrades it against the session. A tier printed as context is gone when the turn ends, so every routed prompt now leaves one line in~/.otito/route-decisions.jsonl(OTITO_ROUTE_LOGmoves it,offdisables it): session id, timestamp, the prompt's hash and length (never the prompt), repository, branch and head at prompt time, the tier and route each half gave, the signals and the answers exactly as scored so--rescorecan re-tier a live corpus, and the subagent model.otito route --jsonreportsdeterministic: { tier, route }beside the scored tier;NEUTRAL_ANSWERSmoves tomodel-route.js. `no...
v3.0.0
The first major release since the Òtítọ́ cutover. It follows 1.15.0 directly: the v2.x tags belong to the Repoctx releases, so the version skips them.
Migrating from 1.x
- Context pack (
otito context --json,context_pack):intentis now{ action, topics }, andintent.hints,patternsandagentPromptare gone. Read the ranked files and hotspots directly and let the model decide what the request means. - Impact (
otito impact --json,change_impact):implementationPlanis gone. Rank, risk flags and suggested tests are unchanged. - PR review (
otito pr --json,review_context):reviewPromptsandnextStepsare gone. Userisk.flagsandreviewTargets, which carried the same information. - Convergence: untracked files are no longer scored by default. Pass
--include-untracked/includeUntracked: truefor the 1.x behaviour. Receipts issued by engine0.1.0do not recompute under0.2.0; re-issue them.
Added
otito converge --head <ref>/convergence_score { head }: score exactlybase..head. Convergence could only diffbaseagainst the working tree, so any dirty or untracked file was scope drift. Scoring a one-commit feature (--base HEAD~1) in a checkout with three edited.claude/*files and an untrackeddump.rdbreported 29 changed files for a 25-file commit and listed all four extras as drift.headdiffs the two trees directly (no merge base), builds the scoring map from the head commit's raw blobs, and binds a v2 receipt to a newgit-commitsubject (baseSha,headSha,treeSha) the way--stagedbinds to the index tree.--headand--stagedcannot be combined.otito gate --head <ref>/review_gate { head }. The local gate's changed-path, risk, secret, and convergence checks read the head commit's tree, reported as aCommit snapshotcheck withscope: "commit". A JSON receipt whose subject names a different mode or head than the gate measured now fails with the mode to rerun in (--head <sha>,--staged,--pr <n>) instead of a bare hash mismatch.
Changed
- Convergence counts a confirmed owner's own fan-out as in scope (engine
0.2.0). A file added beside a confirmed required owner (owner-sibling) and a test named after a confirmed file or inferred sibling (owner-test) move out of drift intodrivers.inferredRelated, each with its rule and anchor. Siblings must be mapped, non-secret, and carry no risk flag the owner lacks; generic test stems such asindexmust sit beside their file. On the commit above, the newPersonDialog.tsx,SendMessageCard.tsxand three other components beside thePeopleTable.tsxowner, and five tests of confirmed files, stopped counting as drift: 55/100 (Scope 21, Risk alignment 15) became 84/100 (Scope 64, Risk alignment 85) with--head HEAD. - Working-tree convergence no longer scores untracked files by default. They are listed under
untrackedwith a recommendation;--include-untracked/includeUntracked: truerestores the old behaviour.change_impactstill counts untracked files. - Receipts issued by convergence engine
0.1.0do not recompute under0.2.0; re-issue them.
Removed
- BREAKING: otito stops interpreting the request; the model it serves does that better. Several rankers and output fields tried to understand what a request meant using keyword rules. The agent reading the pack does that job better, so otito now returns the evidence and leaves both interpretation and ordering to the model. Removed:
- Context pack:
intent.hints, and the ranking boosts built on them (the MCP/CLI/tool/API/test hints, the signup-verification boost of +220, the RSVP-privacy boost of +120, the Handlebars template boost, and the CLI-entrypoint and agent-tool related-file boosts). Also thepatternsandagentPromptfields, and their Markdown and terminal sections.intentis now{ action, topics }. impact: theimplementationPlanfield and its section.pr/review_context: thereviewPromptsandnextStepsfields and their sections. They restatedrisk.flagsandreviewTargets, which are unchanged.- Kept:
classifyImpactRoles, becauseconvergeandmodel_routemeasure against itsrequiredOwners.
- Context pack:
- Measured against
mainon the 21 labelled retrieval cases. p@5 stays at 0.867 and r@5 at 1.0, and 21/21 cases still pass. MRR drops from 1.0 to 0.975. Only the three cases the heuristics were written for changed rank, each by one place, and every expected file is still in the top three. Context packs are 18% smaller as JSON and 31% smaller as Markdown;impactoutput is 31% smaller as JSON. See docs/EVALS.md.
Fixed
- Post-merge attestation attested whatever
mainwas when the run started, not the commit it resolved. The workflow passed its target to the scripts asGITHUB_SHA, which GitHub does not let a step override, so the scripts saw the runner's own value, the default-branch head. A run could attest a commit before that commit's CI had passed, and the ledger commit could name one commit while recording another: the reset meant to restart the chain atb3f795drecordedfc0a7b9. The target is now passed asOTITO_TARGET_SHA, andGITHUB_SHAis still honoured when the scripts run on their own. - The context pack reported a working tree that no longer existed. Its uncommitted-changes warning, and
repos[].git, came from the cached code map, which records git state once, when the repository is indexed. The index is only rebuilt when a file's size or mtime changes, and a commit changes neither, so a repository indexed with work in progress kept warning about "4 uncommitted git change(s)" and told agents to inspect a tree that was already clean. The pack now reads git state live on every call, which costs about 50 ms. Other reports already read it live, and the catalog keeps its snapshot from index time on purpose. - Nothing had been attested on
mainsince 2026-09-20, and CI had no way to recover. The durable ledger onaudit-ledgerholds 97 attestations whose commits are no longer inmain's history. So every post-merge run stopped inreconcile-attestations.sh, correctly, and named a reset that only worked from a local shell. The workflow's manual trigger now takesreset_ledger, the only way an Actions run can setOTITO_ATTEST_RESET_LEDGER=1; a run started by CI never can. The persist step also keeps the archived chain (audit-pilot/ledger-orphaned-*.jsonl) onaudit-ledgerand in the uploaded evidence. Before this, a reset in CI would have written the archive on the runner and discarded it.
v1.15.0
Added
model_route, the router as an MCP tool.otito routewas CLI-only, so every MCP host (Cursor, VS Code, Claude Desktop, Codex, Gemini) could score AX but not ask for a tier. The tool takes{ query, path?, host?, offline?, includeMarkdown? }and returns theotito route --jsonpayload. It calls TypeSafe only whenTYPESAFE_API_KEYis in the server's environment andofflineis not true, and declaresopenWorldHint: truebecause it may. The MCP surface goes from 13 to 14 tools. Host resolution is shared with the CLI throughhostModelFor, so an unknown host fails with the same remedy on both.- A request read, folded into the route call. Three more questions ride the same System One call as the route questions: what kind of work the request is (
read_intent), which otito tool answers it (read_capability), and, per ranked file, whether the work needs it (read_file_<n>). They cost input tokens, not a round trip. Inroutethe read is reported undermodel.readand in its own output section, and is never scored: a test holds that the tier is identical with and without it. The Claude Code prompt hook adds a line for any answer that cleared its floor. Measured: 2,056 input tokens, $0.000086, 345 ms for route and read together. otito context --online/context_pack { online: true }. Applies the read to a context pack in one call: an intent at confidence ≥ 0.5 replaces otito's action word and withdraws the ambiguity question; a file the model scores under 0.2 is demoted out of the ranked lists, with its hotspots, and kept undermodelRead.demoted; the rest are re-ranked by relevance with their original rank kept. A read that rejects every primary file is not applied to them. Off unless asked, so a pack stays a pure function of repository state by default; a failed or unkeyed call returns the pack unchanged with the reason. On a question whose offline pack listed an RSVP eval fixture as related to MCP integration, the fixture was demoted at 0.17 andsrc/lib/mcp.jsmoved to first.- Every MCP host can appear on a local Realtime Canvas. Set
OTITO_CANVAS_URL(loopbackhttponly) andOTITO_HOSTin a host's MCP config and each request-bearing tool call is forwarded to the canvas's/ingest: the request text, the tool name and the host label, never a result, a path argument or file contents. Fire and forget, and the tool yields once so the loopback send flushes before a CPU-bound dispatch blocks the loop — so a canvas that is down or slow cannot delay an answer; a test with a canvas that never replies holds the response under 900 ms. Off unless set. See MCP and Agent Workflows, which also adds Codex CLI and ChatGPT guidance.
Fixed
- The config tests read the developer's own config.
loadConfig({ env: {} })still falls back to~/.config/otito/config.json, so anyone who had runotito config set telemetry truefailedtelemetry defaults to offlocally while CI, which has no such file, stayed green. Every config test now pinsXDG_CONFIG_HOMEto its temp directory. - The route hook's end-to-end test made a billed call on a keyed machine. It spawned the hook with the developer's environment,
TYPESAFE_API_KEYincluded. The key is now removed from the child's environment.
v1.14.0
Added
scripts/hooks/route-prompt.mjs, aUserPromptSubmithook that routes every request. A skill only routes when the model remembers to invoke it, so the request most worth routing — a quick one — is the one least likely to trigger it. The hook scores the request before any work starts and returns the tier as context, every time. It is explicit about the ceiling, because the ceiling is low: no hook output carries a model,PreModelSwitchmay only block a switch andPostModelSwitchis read-only, and a session cannot re-price its own turns. So the hook advises the session and binds the subagent — the Task/Agent tool takes a model, so delegated work genuinely runs on the routed tier — and says as much in the context it injects, because claiming the host switched models when it only recommended a tier is an anti-pattern the skill names. It never eats a prompt: every failure path exits 0 and writes nothing, the routing call is killed at six seconds, turn-taking prompts and slash commands are skipped, and a subagent never routes so a routed agent cannot launch another. See model routing.
Fixed
-
An offline workspace search trusted every stored index without checking any of it. The capability signature above guards
getCachedCodeMap, butotito search --offlinenever went through it: it read each catalogued repository's index file, took whatever map was inside, and searched it. So the fix landed for a single repository and not for the sweep across all of them — the surface where a stale index is hardest to notice, because one repository quietly contributing no Markdown records looks exactly like a repository that has none.- The acceptance rule is now one predicate shared by both readers, split along its cost. The repository fingerprint stays out of the offline path on purpose: it walks and stats every tracked file, and
--offlineis documented as using stored indexes without refreshing fingerprints, so re-fingerprinting every catalogued repository on every search is the regression this flag exists to avoid. The other three checks — cache version, capability signature, and the root the map names — are pure functions of an envelope already being parsed, they cost nothing, and they are the ones that catch an indexer mismatch. A repository whose index fails them is skipped with its reason recorded in the result'serrors, naming the refresh, rather than being silently searched or unilaterally rebuilt.
- The acceptance rule is now one predicate shared by both readers, split along its cost. The repository fingerprint stays out of the offline path on purpose: it walks and stats every tracked file, and
-
A repository indexed before a capability change kept serving the old index. The Markdown fix above changed which files the indexer admits, but not
cacheVersion, the hand-maintained counter that decides whether an on-disk index is still trustworthy. The repository fingerprint could not catch it either: it summarises the files on disk, and those had not changed. So an untouched repository indexed before that release went on being served a map with no Markdown records —routeon "fix a typo in the README" found no candidates at all, theno evidencefail-safe fired on the empty set, and a one-line documentation edit was routed to the premium tier. The fail-safe was right; it was being handed a wrong answer.- The cache now also records a capability signature, derived by running the real eligibility and classification functions over a fixed corpus of representative paths and hashing the answers. Admitting a new extension, dropping one, or moving a path to a different kind each move the signature on their own, so an index written by an indexer that disagrees with today's is rebuilt without anyone remembering to bump anything.
cacheVersionstays as the manual escape hatch for extraction changes the corpus cannot observe.
- The cache now also records a capability signature, derived by running the real eligibility and classification functions over a fixed corpus of representative paths and hashing the answers. Admitting a new extension, dropping one, or moving a path to a different kind each move the signature on their own, so an index written by an indexer that disagrees with today's is rebuilt without anyone remembering to bump anything.
-
The code map never indexed Markdown, so no skill or docs page could be found.
isSourceFilePathadmitted a fixed list of code extensions plus a special case forCHANGELOG.md; every other.mdfile in the repository was invisible to impact analysis,route, and convergence. A request naming a skill therefore ranked whatever library files happened to share its vocabulary: "make the model-router skill layout-agnostic" returnedintegrations/herdr/runtime.mjsandsrc/lib/model-route.jsat containment 18, andcodex/skills/model-router/SKILL.md— the file the request names — appeared at no rank at all. The same blind spot hid every page underdocs/. This is the failure class the empty-match fix addressed one release earlier: a confident answer resting on the wrong evidence.- Markdown is now indexed with two kinds.
skillcoversSKILL.mdand its companion pages under askills/directory — these are instructions the repository ships and asks contributors to edit, so they are implementation, not commentary.doccovers the rest, and keeps the existingdocs/ranking demotion. Frontmatter keys and values, headings, and relative links become the file's symbols and imports; Markdown never reaches the TypeScript parser, and SQL quoted in a document is no longer mined as data access. - Both kinds own a change only when the request is about them. A skill can carry a coverage obligation for "change the model-router skill", but
src/lib/model-route.jsstill owns "fix the model route scoring bug" — a skill whose path matches every term must not displace the code. The same request now rankscodex/skills/model-router/SKILL.mdfirst at containment 90.
- Markdown is now indexed with two kinds.
-
A document about a risky area escalated the tier as if it were that code. #175 made Markdown indexable, so prose became a routing candidate and, with it, risk evidence:
docs/AUTH_TOKEN_VALIDATION.mdclassifies asauth/securityon its "auth" and "token" path tokens, which fired the risk-path bump and forcedpremiumfor "fix a typo in the README". The bump itself is sound — a change touching auth code should escalate — but a document about token validation does not validate tokens, and matching it on a path substring prices a doc edit as an auth change.signalsFromnow skips prose candidates when collecting risk evidence, using the code map'skindwhere the ranking supplied one and falling back toisDocPath. Scoped to the risk signal:classifyPathis unchanged, because impact uses it for relevance, and a doc about auth should still rank for an auth request. -
Post-merge attestation died on
exit 128and reported green when it had done nothing. The audit ledger chains against repoctx's merge commits, and otito'smainis a fresh history, so none of the 97 durable records are reachable from it.reconcile-attestations.shran its coverage-gap check first, walkinggit rev-list FIRST..LASTbefore anything established those commits were present, so every run that resolved a merged commit failed withfatal: Invalid revision range— the script already held the right message for this, in the ancestry check below, but control never reached it. The runs that looked green were not passing: they resolved no merged commit, skipped every step and reported success, so the workflow alternated green and red while never once reconciling. Reachability is now established before any range is walked, and a ledger that fails it is reported as what it is with the recovery named. Recovery is opt-in viaOTITO_ATTEST_RESET_LEDGER=1, never automatic, archives the superseded chain rather than deleting it, and starts the new chain at the tip instead of backfilling every ancestor, which would mint verdicts for changes the gate never ran on. -
The route call priced output tokens at the input rate.
JEV_PER_MTOKcovers input tokens only, butaskJevfell back tototal_tokens— which also contains output — andgenerateRoutepriced whatever landed there at the input rate, so a 900-total-token response and a 900-input-token response both printed$0.000038and the inflated figure was indistinguishable from the correct one. The count and the billable quantity now stay apart: the response's token count is reported along with which quantity it is, only an input count is priced, and the terminal line says when a count cannot be priced rather than printing a number the rate does not support.priceRouteCallalso separates a measured zero from an absent measurement, which the old truthiness check collapsed into null. -
The router escalated two thirds of requests, sending one-file changes to the premium tier.
confidencewasMath.minof both Score answers, and a value under the floor bumped a tier. That discarded the confident answer: "rename the variablerunningtoscorein model-route.js" scored specificity 1.00 at confidence 1.00 — Jev put the entire distribution on "the request names the exact file, symbol, flag or user-visible string to change", which is exactly what that sentence does — whileblast_radiusput 0.80 on "contained to a single file" at confidence 0.54. Both answers say trivial. The minimum kept 0.54, missed the floor by one hundredth, and routed a one-file rename to the premium tier. It also double-counted, becausescoreis the expectation over that question's own level distribution, so a spread answer already pays through its own term; bumping on the spread as well charged for it twice. Confidence is now reported for a reader and not read by the router. Measured on this repository over nine requests: escalation 67% → 0%, premium 6/9 → 1/9. The remaining bumps,no evidenceandrisk path, are otito's own deterministic repository signals, which is the half of this pairing entitled to overrule a model; a vendor's self-reported certainty is not. See model routing.
v1.13.1
Changed
- The documentation pack is written for a reader rather than for its author. Nine of eighteen pages opened by saying they mapped an external source onto otito, seven built their main table around a video's claims, and eight carried a private backlog. Seven of those pages made one argument, that verification has to be computed from the repository rather than asked of the model that wrote the change, and they are replaced by a single deterministic verification page that makes it once, for a reader who has watched nothing. Calibration and model routing are kept, because they carry original measurement rather than commentary. Priority tables are gone from public docs entirely.
- Site navigation is grouped by what a reader is trying to do rather than by an eighteen-slot numbering that implied an order it did not have. The model routing page was missing from the navigation entirely.
- The model router skill can offer a realtime canvas, at most once per session, only when
$OTITO_CANVAS_HOMEpoints at one, and only when that canvas is watching the repository being routed. A canvas is fixed to one repository at startup, so feeding it a request about a different codebase would score that request against a codebase that has never seen it.
Added
npm run skills:syncandnpm run skills:check. The canonical skills incodex/skills/had drifted from the copies installed under host directories, and the repo copy, the one calling itself canonical, was the stale one. Syncing is now one-directional, and the check is local by design: a repository's CI cannot police files in a home directory, so it exits 0 where those directories do not exist rather than reporting a green tick for something it never examined.
Fixed
- A test that only passed on a machine without
TYPESAFE_API_KEY.askJevfalls back to the environment, so the missing-key assertion was handing a real key to its own stub and asserting the wrong error. It now controls the variable instead of depending on it.