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 were Primary Files in every recorded pack, five took ranks 8 to 12 of an impact top 12 at an identical score, and every hotspot for "sync local main branch" was an i18n key matchingmain. A folded locale keeps its catalog's role, so changing it is not read as drift. The extension of a file named in the request (event-service.ts) no longer scores, wheretsandtsxhad matched every TypeScript file. A path or basename named in the request is pinned above every unnamed candidate as a required owner (services/event-service.tswas 40th), and a named symbol pins the definition that is exported and imported elsewhere (utils/create-event.tsforcombineDateAndTime) above an unused duplicate that merely shares its name, while its other definitions get a bonus. Tests and suggested tests list only files a runner executes, not a README, suite summary or snapshot under__tests__/.contextEngineVersionandimpactEngineVersionare now 4 because an entry can stand for a catalog and a named file is an owner whatever its kind. New fixtureevals/fixtures/multi-locale-weband retrieval casemulti-locale-named-files-and-symbol: the 21 earlier cases are unchanged, accuracy moves from p@5 0.867, mrr 0.975 to 0.873, 0.976 on the added case alone (the previous engine fails it at p@5 0.6).- Header and verdict boxes are as wide as their borders. Content lines were padded to
width - 4and then framed with│ … │, six cells, so every content line came out two cells wider than the top and bottom border in every command. They are padded towidth - 6now, and a test checks that every line of a box has the same display width in all three glyph sets, at 60, 78 and 120 columns, with wrapped content. The two golden fixtures were regenerated on purpose; in the formatter fixture every changed line of the gate, review, context, impact, route and doctor output is a box content line, two cells narrower, with the same text. - The route-prompt hook no longer routes what the harness submits itself, and
route-outcomesleaves those rows out of the grade. Claude Code delivers a finished background task throughUserPromptSubmit, andisRoutablefiltered only empty prompts, slash commands, turn-taking and very short prompts, so the hook routed the notice like a request. On 2026-09-27, 22 hours after the decision log began, 79 of the 146 decisions in~/.otito/route-decisions.jsonlwere prompts the harness wrote, each matched by hash to the queue record in its session transcript: 75<task-notification>, 3<bash-input>(the record of a!shell command and its output) and 1<ci-monitor-event>. A notice thatyarn testhad finished was routed premium with risk pathsdata modelandauth/security. Each cost a hook run and put "otito routed this request" in front of a prompt nobody wrote. The hook now skips all three and logs nothing for them. A<system-reminder>the harness puts in front of a prompt is looked past rather than skipped, because all 22 logged prompts that opened with one had a request behind it; what is behind the reminder is judged as before, soyesafter a reminder is turn-taking again.<pasted_content>,<create-pr-command>and<cross-session-message>reached the hook too and still route, because each carries a request.<command-name>and<local-command-stdout>matched no decision and get no rule. The rows already logged stay in the log.route-outcomeshashes the harness prompts it finds in each session's transcript, in user records and in queue records (33 of the 79 arrived mid-turn and exist only in the queue and as an attachment), leaves a decision with one of those hashes out of the grade, and reports the count: "146 logged, 79 excluded as harness prompts (task notifications, CI monitor events, shell records), 54 joined to a transcript prompt (13 not found)", andexcludedunder--json. A harness prompt in the transcript is no longer a prompt or a follow-up either. On those 146 rows the grader had graded 78 decisions, 36 of them harness prompts, and published two rates that were partly notifications (deterministic cheap 1.8% corrected on 55, offline mid 0.0% on 58); now 35 are graded and every lane is under the minimum sample. A decision whose transcript is gone cannot be identified and counts as not found. docs/18 has the table. - Box drawing, arrows and
✓are measured as one cell.visualWidthcounted every code point from U+1100 upwards as two cells, so a padded header or verdict line holding─,→or✓came out one cell short per mark and its right border drifted. It now counts the East Asian wide and fullwidth blocks and emoji (\p{Emoji_Presentation}, the supplementary emoji blocks, and any character followed by U+FE0F) as two cells and everything else as one, still stripping ANSI escapes and skipping combining marks and zero-width joiners. - Long content wraps inside the header and verdict boxes.
padRightnever truncates, so ablocked byreason or a path wider than the box pushed the right border out of line at 60 columns.headerandverdictnow wrap content to the inner width, split an unbroken run such as a path by cell, indent the continuation lines to the content column and keep a painted subtitle's colour on every line. Content that fits renders byte-for-byte as before;tests/fixtures/render-fancy-golden.jsonholds the pre-change output in emoji and ascii modes and the renderer tests compare against it. - The eval headers print the flask, not its source text.
otito eval --accuracy,--harnessand--gate-effectivenesspassed the header glyph as a double-escaped string, so in emoji mode the header box read\u{1F9EA} ACCURACY EVAL. The three headers now print🧪, and a test scanssrc/for any string that spells a code point with a doubled backslash. - The generated pre-commit hook gates under
--policy standard.otito initwrote a hook that ranotito gate . --stagedand so inherited the repository's.otitorc.jsonpolicy. A repository that setscompanyorhigh-riskthere means it for merge time; on a commit that has not been pushed yet the company policy fails on review state alone, so the hook refused every local commit. The hook now passes--policy standard, and a test stages a change under acompanypolicy and checks that the inherited gate fails while the hook's gate does not. - The generated workflow pins the action majors otito's own CI runs on. The
otito inittemplate usedactions/checkout@v4,actions/setup-node@v4andactions/upload-artifact@v4while.github/workflows/otito-ci.ymlhad moved to@v7. The majors are now one constant insrc/lib/init.js, the template reads them, andtests/init.test.jsfails when they differ from the majors inotito-ci.yml. - A type check named
tsc:checkreaches the context pack, the PR review and the pre-commit hook. The harness chose validation commands from a fixed list of script names and told a type check byname.includes("type") || name === "tsc". On 2026-09-27 a context pack for a yarn web app whose type check is"tsc:check": "tsc --noEmit"listedyarn lint,yarn test,yarn test:e2eandyarn buildand no type check, so an agent following the pack skipped it.otito prhadtsc:checkon its list and dropped it for want of a reason, and the quality job and pre-commit hook written byotito initnever saw it. The three now sharesrc/lib/package-scripts.js. A script is a type check when its name, split on:,-,_and camelCase, holdstsc,typecheck,type-checkorcheck-typesas whole segments, astsc:check,check:tsc,tsc-checkandtscCheckdo;tsconfig:syncandprototypedo not. A package that has none of the type-check names the list already had now gets every type check it defines, in the list's type-check slot; one that has one of them gets the same list as before, so a monorepo with an aggregatetypecheckdoes not run tsc again for each part. A name that sayswatchorbuildis left out, as is a type check whose command passes--watch(or-wtotsc): a watch never finishes and a build writes files. - The end-to-end command is the headless one when the package has it. The same pack listed
yarn test:e2e, which in that repository isplaywright test --headedand opens browser windows an agent or a CI runner has no display for, whiletest:e2e:headlesssat beside it. The harness andotito prnow list the headless sibling of an end-to-end script (test:e2e:headless,test:e2e-headless) in its place, and list it when it is the only one. A package with no headless sibling keepstest:e2e.