Skip to content

Releases: BASHBOP/solumbe

v4.0.0

Choose a tag to compare

@github-actions github-actions released this 30 Sep 18:48
e891f2d

Ò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/solumbe
    CLI otito solumbe
    MCP Registry io.github.BASHBOP/otito, host key otito (mcp__otito__* tools) io.github.BASHBOP/solumbe, solumbe (mcp__solumbe__*)
    Config .otitorc.json, gate config otito.gate.json .solumberc.json, solumbe.gate.json
    Evidence .otito/, user state ~/.otito/ .solumbe/, ~/.solumbe/
    Environment OTITO_* SOLUMBE_*
    Skills otito-*, secret marker otito:allow-secret, PR comment marker <!-- otito-pr-review --> solumbe-*, solumbe:allow-secret, <!-- solumbe-pr-review -->
    Repository BASHBOP/otito, docs bashbop.github.io/otito BASHBOP/solumbe, bashbop.github.io/solumbe

    To upgrade, install @bashbop/solumbe; rename .otitorc.json, otito.gate.json and any otito:allow-secret markers; move .otito/ and ~/.otito/; rename OTITO_* 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's analytics/otito route with an otito_version field, 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 at io.github.BASHBOP/solumbe. It refuses to run until the new listing is live.

  • The Release workflow skips npm publish when 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 quality now runs rebrand: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

Choose a tag to compare

@github-actions github-actions released this 30 Sep 06:08
5e95b21

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=delegate puts 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|advisory grades one arm, and otito route shows the arm and the subagent model in the terminal and as enforce in JSON. Without the variable, routing stays advisory and nothing changes.

Fixed

  • On the claude-code host, otito route and the route prompt hook name the premium model claude-opus-5-5 instead of claude-opus-5. The Agent tool's opus alias runs Opus 5.5, so the old name was never the model that ran.

v3.4.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 19:01
f5251d8

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_pack and otito context read a repository's companions too: 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. Explicit paths still win; a companion that is not checked out is skipped.
  • otito route shows 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.confidences carries both.

Fixed

  • change_impact no 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 route and 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, and tier in JSON is unchanged (premium), so callers that read only tier behave as before.
  • version:check catches a stale docs site. docs/index.md opens with a **vX.Y.Z** is published banner, which replaced the **Status:** v line the release sync and drift check looked for, so neither saw it: 3.3.0 was released on 2026-09-27 with version:check green while bashbop.github.io/otito still announced v3.2.0 and had no 3.3.0 entry under What's New. syncPinnedDocVersion now rewrites the banner, findPinnedDocVersionDrift reports it, and a new findWhatsNewDrift fails version:check when What's New has no vX.Y.Z published entry for the package.json version (it is written by hand, so the sync cannot add it). The site gets its v3.3.0 banner and entry.
  • context_pack reads 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_impact no 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

Choose a tag to compare

@github-actions github-actions released this 27 Sep 17:04
fec3bd0

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() in src/lib/render/fancy.js and every formatter that built a line as `${renderer.glyphs.item} ${text}` by hand (the shared summary behind install/init/discover/index/catalog/search/repo in src/lib/output.js, context_pack's Commands section, change_impact's suggested tests and risk hotspots, and the Context evidence lines in pass/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.json and tests/fixtures/formatters-golden.json were regenerated on purpose; every changed line is a bullet or list-item marker gaining \x1b[2m...\x1b[0m, nothing else moved.
  • otito install asks a human at a TTY how they want to install, with a personalized one-line pitch first. No --global/--link/--yes/--json and 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 local git 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 output install already gave, unchanged. New promptChoice in src/cli.js mirrors the existing promptYesNo pattern from init; getWelcomeMessage lives in src/lib/install.js.
  • The renderer resolves one glyph set per run and offers a set of shared string-building primitives. createRenderer now picks emoji, ascii or unicode once and exposes the choice as renderer.glyphMode with every mark it prints in renderer.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 still emoji, and emoji: false, NO_EMOJI and CI still give ascii. unicode, which prints ✓ ! ✗, box drawing and arrows and nothing that matches \p{Extended_Pictographic}, is reached only through the explicit glyphs: "unicode" option; it becomes the default in a later release. New string builders sit beside header and verdict: 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, phase and close, plus paint and a palette with magenta and blue. visualWidth, padRight and wrap are 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, the minimal theme and, new in this release, TERM=dumb keep ASCII glyphs, so anything already reading otito's plain output sees no change. --emoji, emoji: true and OTITO_EMOJI=1 opt back into the emoji look exactly as it was. The high-contrast theme keeps its bright palette and stops forcing emoji. TERM=dumb is detected in the renderer beside CI and NO_EMOJI, not in config, so the sources otito config list shows stay honest.
  • Every command that prints for a person ends with one closing line. Verified., Tests pass., Runs without errors. or Not 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, plain eval, config list, config get, telemetry status, install without a flag) end with Runs without errors.. Writers (init, install --global|--link, config set, telemetry on|off|clear|share, dashboard, obsidian, attest, pr --comment) end with Verified. 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-gate ends the way a gate does: Tests pass. when --run-validation ran and passed, Verified. on PASS, otherwise the check that blocked or warned. --json, --markdown, --mermaid, the --out messages, config get <key>, --version, help and mcp carry 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, search and repo prints its facts as a table under "At a glance", file lists as a tree and every other section as a - list. otito config list is a key, value and source table, otito config get a key and value table, otito telemetry a table followed by the commands that change it, and otito deps a header, a table and path:line matches.
  • Markdown tables in reports are aligned. renderDocument pads 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 help documents --emoji, --no-emoji, --color, --no-color and --theme once, 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 across context, impact, pass, pass-pr, review, route, the shared summary and the section, rank, bar and detail helpers now read renderer.glyphs or ask renderer.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.emoji is no longer read outside the renderer, and a test keeps it that way. otito report builds a renderer from the same --emoji, --color and --theme preferences every other command honours. Nothing visible changes: tests/fixtures/formatters-golden.json records each formatter's output in emoji and ascii modes, with and without colour, before this change, and tests/formatters-golden.test.js compares byte for byte.
  • A version bump that reaches main is tagged and released without a hand-pushed tag. The Release workflow runs only on a pushed v* tag, and pushing it was a manual checklist step. On 2026-09-26 the 3.2.0 release merged to main as 0766fe7 at 20:33 UTC with otito 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, until v3.2.0 was pushed by hand at 21:12. A new Tag release workflow runs when otito CI passes on a main push, reads the version from package.json at that commit and, when vX.Y.Z does not exist yet, tags the commit with scripts/tag-release.sh and starts the Release workflow on the tag. A tag pushed with GITHUB_TOKEN triggers no workflow, so release.yml now also accepts workflow_dispatch, which keeps it the workflow npm Trusted Publishing trusts; dispatched on a branch, it refuses before publishing. A main push 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 that CHANGELOG.md has 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 run otito doctor to verify the install. getDoctorReport (src/lib/doctor.js) never checks the otito binary itself — it checks seven unrelated tools (node, git, gh, rg, npx, opensrc, code-structure), three of which its own output calls "optional accelerators". Whether otito landed on PATH is already verified inside installOtito/handleInstall via commandExists, 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().nextSteps in src/lib/install.js now starts at index --discover.
  • context_pack and change_impact no 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 in src/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 under siblings; before, two locales we...
Read more

v3.2.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 21:14
0766fe7

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.json and no bouncer binary resolves, the local gate told you to run npm 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/workflows that references @nugehs/bouncer (such as npx -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.json and no tieline binary resolves, the local gate told you to run npm 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 --pr and review_gate { pr } never asked gh for 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 as UNKNOWN and the gate answered "GitHub mergeability is not settled yet"; the audit ledger's post-merge records for #195, #196 and #197 carry the same PR 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 requests state and mergedAt and reports both under pr. 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.json has 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 is WARN evidence, not FAIL, requested changes still fail, and an explicit --governance team still overrides the file.
  • The MCP gate tools honour the repository's .otitorc.json, as the CLI does. On 2026-09-26 otito pass-pr 214 gated #214 under solo governance and warned on CODEOWNERS ("solo-maintainer mode requires an explicit owner/admin merge decision"), while review_gate { pr: "214" } failed the same PR under team governance ("CODEOWNERS approval is missing"). This repository checks in .otitorc.json with "governance": "solo". The CLI fills an omitted --policy or --governance from that file and the user config before any gate runs; the MCP server passed its arguments straight through, so a missing governance normalised to team. review_gate and review_verdict, and the merge_readiness, pr_merge_readiness and review_pr aliases that route to them, now fill an omitted or blank policy and governance from the same config, found from the repository at path rather 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 reports governance: solo, CODEOWNERS WARN and verdict WARN, as otito pass-pr 214 does, and governance: "team" still fails it. The lookup also walks up from a relative path now: path.dirname(".") is ., so a gate called with path: "." 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 gate and otito pass-pr <n> --path <repo> filled an omitted --policy or --governance from the .otitorc.json found 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."), while otito pass-pr 214 run 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 for pass and review, --path for pass-pr and either for gate, through gatePolicy in src/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> and otito review <checkout> --pr 214 now report governance: solo, CODEOWNERS WARN and verdict WARN, and --governance team still fails the PR. otito workspace-gate gates 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.json above them, need none. Emoji, color and theme still come from the directory otito runs in.
  • otito gate gates 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 --json reported repo.root as this checkout, and otito gate <other-repo> --pr 214 --json gated #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 help with the positional (otito gate <repo>), and the agent-tools catalog with --path in 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 --path with no value is refused too. Rerun from this checkout, otito gate --path <other-repo> gates that repository under its own high-risk policy, otito gate <other-repo> --pr 214 asks gh about that repository, which has no remote ("no git remotes found"), instead of gating #214, and otito gate --pr 214 --path . still gates #214. otito help lists otito gate [repo | --path repo] for the local gate and otito gate --pr <selector> [repo | --path repo] for the PR gate.
  • otito review reviews the repository named by --path. review read the repository only from its first positional and took the positionals after it as the request, so it ignored --path, which the agent-tools catalog gives as the CLI form of the review_verdict MCP tool (otito review --path <repo> --json). On 2026-09-26, run from this checkout, otito review --path <other-repo> --base HEAD~1 --json reported repo.root as this checkout and gated it under this checkout's settings (the default standard policy), 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> asked gh about the repository it ran in ("no git remotes found") instead of #219. review now reads its arguments as impact and ax do: with --path, that names the ...
Read more

v3.1.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 11:30
183598d

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 help lists must be a card on the page or a row in SUPPORTING_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 a summary in src/lib/mcp.js, one or two plain sentences that the page and otito agent-tools --json print; the MCP description is unchanged and summary is not sent over tools/list. The page stamps the package version (<meta name="otito:version">) and the version lifecycle script re-renders it, so a release that skipped npm run docs:diagram fails docs:diagram:check.
  • "The gate never consults a model" is a test, not a sentence. tests/gate-imports.test.js walks the static import graph from every verdict-producing module (pass-local, pass-pr, pr-review, converge, review, impact) and fails if any path reaches jev.js, model-route.js or context-read.js. The page's first guarantee is checked against the same test.
  • otito attest / otito attest --verify. The post-merge attestation moves out of audit-pilot/attest.mjs and into the CLI. otito attest <repo> --verdict <file> --merge <sha> [--prev <sha>] [--pr <n>] [--author <name>] [--committed <iso>] appends a hash-chained record to audit-pilot/ledger.jsonl under the repository (or the file --ledger names); --verify recomputes the chain and exits 1 if any record was altered. Both take --json. scripts/post-merge-attest.sh and scripts/reconcile-attestations.sh call the command and honour OTITO_LEDGER.
  • schemaVersion on the verdict JSON and on every ledger record. otito review --json now reports schemaVersion: 1 beside reviewEngineVersion, and each attestation carries schemaVersion: 1 and the verdictSchemaVersion it 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 when TYPESAFE_API_KEY is set (--offline keeps a run keyless). Outcomes are the line-overlap repaired join otito calibrate uses, now exported from calibrate.js as joinRepairs and readHistory so 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 into mid. 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 signals scoreDecision reads (containment, riskPaths beside ax and candidates) and the answers exactly as scored under inputs (Score expectation, confidence and level distribution; Noul probability), and rescoreRegret / --rescore applies the current arithmetic to a saved run with no checkout and no model call. The rescore carries its own receipt, names its source in method.rescoredFrom, and keeps the source's replayable: 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. regretEngineVersion 0.3.0.
  • otito regret grades 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 --rescore applies 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 to mid, 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. regretEngineVersion 0.4.0.
  • The repaired join 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 the Develop (#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_SUBJECT missed hot-fix(...), hot-fit, bug(...), patch, fixes, fixed and fixing, 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). calibrate and regret now share FIX_COMMIT_RULE, and --rescore drops the rows the wider rule reads as fixes, counts them on the corpus line, and says their own repairs need a replay to join. regretEngineVersion 0.4.1.
  • The route-prompt hook keeps its decision, and route-outcomes grades 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_LOG moves it, off disables 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 --rescore can re-tier a live corpus, and the subagent model. otito route --json reports deterministic: { tier, route } beside the scored tier; NEUTRAL_ANSWERS moves to model-route.js. `no...
Read more

v3.0.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 22:31
3329c93

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): intent is now { action, topics }, and intent.hints, patterns and agentPrompt are gone. Read the ranked files and hotspots directly and let the model decide what the request means.
  • Impact (otito impact --json, change_impact): implementationPlan is gone. Rank, risk flags and suggested tests are unchanged.
  • PR review (otito pr --json, review_context): reviewPrompts and nextSteps are gone. Use risk.flags and reviewTargets, which carried the same information.
  • Convergence: untracked files are no longer scored by default. Pass --include-untracked / includeUntracked: true for the 1.x behaviour. Receipts issued by engine 0.1.0 do not recompute under 0.2.0; re-issue them.

Added

  • otito converge --head <ref> / convergence_score { head }: score exactly base..head. Convergence could only diff base against 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 untracked dump.rdb reported 29 changed files for a 25-file commit and listed all four extras as drift. head diffs 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 new git-commit subject (baseSha, headSha, treeSha) the way --staged binds to the index tree. --head and --staged cannot 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 a Commit snapshot check with scope: "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 into drivers.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 as index must sit beside their file. On the commit above, the new PersonDialog.tsx, SendMessageCard.tsx and three other components beside the PeopleTable.tsx owner, 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 untracked with a recommendation; --include-untracked / includeUntracked: true restores the old behaviour. change_impact still counts untracked files.
  • Receipts issued by convergence engine 0.1.0 do not recompute under 0.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 the patterns and agentPrompt fields, and their Markdown and terminal sections. intent is now { action, topics }.
    • impact: the implementationPlan field and its section.
    • pr / review_context: the reviewPrompts and nextSteps fields and their sections. They restated risk.flags and reviewTargets, which are unchanged.
    • Kept: classifyImpactRoles, because converge and model_route measure against its requiredOwners.
  • Measured against main on 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; impact output is 31% smaller as JSON. See docs/EVALS.md.

Fixed

  • Post-merge attestation attested whatever main was when the run started, not the commit it resolved. The workflow passed its target to the scripts as GITHUB_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 at b3f795d recorded fc0a7b9. The target is now passed as OTITO_TARGET_SHA, and GITHUB_SHA is 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 main since 2026-09-20, and CI had no way to recover. The durable ledger on audit-ledger holds 97 attestations whose commits are no longer in main's history. So every post-merge run stopped in reconcile-attestations.sh, correctly, and named a reset that only worked from a local shell. The workflow's manual trigger now takes reset_ledger, the only way an Actions run can set OTITO_ATTEST_RESET_LEDGER=1; a run started by CI never can. The persist step also keeps the archived chain (audit-pilot/ledger-orphaned-*.jsonl) on audit-ledger and in the uploaded evidence. Before this, a reset in CI would have written the archive on the runner and discarded it.

v1.15.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 06:05
b055e34

Added

  • model_route, the router as an MCP tool. otito route was 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 the otito route --json payload. It calls TypeSafe only when TYPESAFE_API_KEY is in the server's environment and offline is not true, and declares openWorldHint: true because it may. The MCP surface goes from 13 to 14 tools. Host resolution is shared with the CLI through hostModelFor, 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. In route the read is reported under model.read and 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 under modelRead.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 and src/lib/mcp.js moved to first.
  • Every MCP host can appear on a local Realtime Canvas. Set OTITO_CANVAS_URL (loopback http only) and OTITO_HOST in 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 run otito config set telemetry true failed telemetry defaults to off locally while CI, which has no such file, stayed green. Every config test now pins XDG_CONFIG_HOME to 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_KEY included. The key is now removed from the child's environment.

v1.14.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 18:40
d9742ed

Added

  • scripts/hooks/route-prompt.mjs, a UserPromptSubmit hook 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, PreModelSwitch may only block a switch and PostModelSwitch is 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, but otito search --offline never 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 --offline is 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's errors, naming the refresh, rather than being silently searched or unilaterally rebuilt.
  • 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 — route on "fix a typo in the README" found no candidates at all, the no evidence fail-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. cacheVersion stays as the manual escape hatch for extraction changes the corpus cannot observe.
  • The code map never indexed Markdown, so no skill or docs page could be found. isSourceFilePath admitted a fixed list of code extensions plus a special case for CHANGELOG.md; every other .md file 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" returned integrations/herdr/runtime.mjs and src/lib/model-route.js at containment 18, and codex/skills/model-router/SKILL.md — the file the request names — appeared at no rank at all. The same blind spot hid every page under docs/. 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. skill covers SKILL.md and its companion pages under a skills/ directory — these are instructions the repository ships and asks contributors to edit, so they are implementation, not commentary. doc covers the rest, and keeps the existing docs/ 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.js still owns "fix the model route scoring bug" — a skill whose path matches every term must not displace the code. The same request now ranks codex/skills/model-router/SKILL.md first at containment 90.
  • 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.md classifies as auth/security on its "auth" and "token" path tokens, which fired the risk-path bump and forced premium for "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. signalsFrom now skips prose candidates when collecting risk evidence, using the code map's kind where the ranking supplied one and falling back to isDocPath. Scoped to the risk signal: classifyPath is 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 128 and reported green when it had done nothing. The audit ledger chains against repoctx's merge commits, and otito's main is a fresh history, so none of the 97 durable records are reachable from it. reconcile-attestations.sh ran its coverage-gap check first, walking git rev-list FIRST..LAST before anything established those commits were present, so every run that resolved a merged commit failed with fatal: 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 via OTITO_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_MTOK covers input tokens only, but askJev fell back to total_tokens — which also contains output — and generateRoute priced whatever landed there at the input rate, so a 900-total-token response and a 900-input-token response both printed $0.000038 and 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. priceRouteCall also 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. confidence was Math.min of both Score answers, and a value under the floor bumped a tier. That discarded the confident answer: "rename the variable running to score in 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 — while blast_radius put 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, because score is 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 evidence and risk 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

Choose a tag to compare

@github-actions github-actions released this 20 Sep 16:50
86cf5e3

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_HOME points 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:sync and npm run skills:check. The canonical skills in codex/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. askJev falls 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.