Skip to content

v3.3.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 17:04
· 35 commits to main since this release
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 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 matching main. 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, where ts and tsx had 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.ts was 40th), and a named symbol pins the definition that is exported and imported elsewhere (utils/create-event.ts for combineDateAndTime) 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__/. contextEngineVersion and impactEngineVersion are now 4 because an entry can stand for a catalog and a named file is an owner whatever its kind. New fixture evals/fixtures/multi-locale-web and retrieval case multi-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 - 4 and 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 to width - 6 now, 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-outcomes leaves those rows out of the grade. Claude Code delivers a finished background task through UserPromptSubmit, and isRoutable filtered 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.jsonl were 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 that yarn test had finished was routed premium with risk paths data model and auth/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, so yes after 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-outcomes hashes 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)", and excluded under --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. visualWidth counted 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. padRight never truncates, so a blocked by reason or a path wider than the box pushed the right border out of line at 60 columns. header and verdict now 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.json holds 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, --harness and --gate-effectiveness passed 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 scans src/ for any string that spells a code point with a doubled backslash.
  • The generated pre-commit hook gates under --policy standard. otito init wrote a hook that ran otito gate . --staged and so inherited the repository's .otitorc.json policy. A repository that sets company or high-risk there 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 a company policy 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 init template used actions/checkout@v4, actions/setup-node@v4 and actions/upload-artifact@v4 while .github/workflows/otito-ci.yml had moved to @v7. The majors are now one constant in src/lib/init.js, the template reads them, and tests/init.test.js fails when they differ from the majors in otito-ci.yml.
  • A type check named tsc:check reaches 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 by name.includes("type") || name === "tsc". On 2026-09-27 a context pack for a yarn web app whose type check is "tsc:check": "tsc --noEmit" listed yarn lint, yarn test, yarn test:e2e and yarn build and no type check, so an agent following the pack skipped it. otito pr had tsc:check on its list and dropped it for want of a reason, and the quality job and pre-commit hook written by otito init never saw it. The three now share src/lib/package-scripts.js. A script is a type check when its name, split on :, -, _ and camelCase, holds tsc, typecheck, type-check or check-types as whole segments, as tsc:check, check:tsc, tsc-check and tscCheck do; tsconfig:sync and prototype do 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 aggregate typecheck does not run tsc again for each part. A name that says watch or build is left out, as is a type check whose command passes --watch (or -w to tsc): 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 is playwright test --headed and opens browser windows an agent or a CI runner has no display for, while test:e2e:headless sat beside it. The harness and otito pr now 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 keeps test:e2e.