Decisions
The decisions currently in force, each with its reason and consequence.
Decisions currently in force. When one is superseded, this file describes the new state — it is not a history.
Scripts are the product, not one feature among several
The start screen shows the project’s scripts immediately — grouped and labelled, with no area selection in front of them. Health, updates, clean and security are reachable by hotkey (
H,U,C,S), never by navigating through a menu layer first.Reason: the shortest path to running
devis what the tool is used for every day. An earlier draft put a “Run / Quality / Maintenance” selection in front of the scripts; that makes the common case slower to serve the rare one.Consequence:
Enteron the highlighted entry runs it. Anything that adds a step between openingopiand starting a script needs to justify itself against this.
A long list pages by group rather than scrolling past every one
Past 15 entries or 5 groups the groups become a row of tabs and only the active one is listed. The first group is preselected with the cursor on its first entry, so
opi ↵runs the same script it did before.Reason: the flat screen was decided against 24 entries in 8 groups, which still read at a glance. At 27 entries in 7 groups — one real project’s root — a heading per group makes 34 lines, so the top scrolls away and the screen stops being a screen. The rejected submenu demanded a choice before anything runnable was visible; a tab row demands nothing and hides what is not needed.
Consequence: the threshold is a count, not the terminal’s height, which would have fitted more exactly. The height changes while the menu is open, and a list that rearranged itself mid-keystroke would move entries under a cursor already on its way to one.
opitherefore looks the same in every window, andopi | catlists every group as before — a pipe cannot press a key to reach the second tab.Consequence:
/keeps searching every group. Tabs answer “I know roughly where”; the filter answers “I know exactly what”, and a query matching four groups is precisely the case tabs are worst at.Consequence: every tab names an action, except one that names the workspace’s packages and is marked off by a divider. One tab per package was built first and measured wrong: 52 packages against 6 action groups is a row of 58, and the two kinds stood side by side as peers, so “where is the thing that deploys insights?” had two right answers. Grouping is unchanged — each package keeps its heading inside the shared tab.
The script name is not the user interface
Groups are derived from script name prefixes — the part before the first
:becomes the group, known base names (dev,check,test) map to meaningful labels. Group order is fixed and meaning-oriented, not alphabetical. Descriptions come fromopi.scripts.<name>.description, thenscripts-info; where neither exists, the column stays empty.Reason: a flat alphabetical list of raw script names is the problem
opiexists to solve.Consequence: the description column is never filled with the raw command. A command line shown as a “description” reintroduces exactly the technical surface being removed.
runemark is extended rather than paired with a second library
The interactive list lives in runemark, behind its
selectfeature, and so do its viewport, its width fitting and its filter.opitakes on no other presentation dependency — no ratatui, no inquire.Reason: one presentation layer across the CLI tools in this ecosystem, rather than two competing rendering models inside one binary.
Consequence: runemark’s design boundary now covers grouped selection, and its documentation says so. The API stays generic — it knows nothing about scripts,
package.jsonor package managers. Four rounds ofopi’s needs shaped it without a singleopiterm entering it. If that generic form ever turns out not to carry the interface, that is grounds to revisit this decision, not to widen the boundary further.
Unix only
opidoes not build on Windows, and says so at compile time.Reason: running a script by replacing the process with
exec, and driving termios for the interactive list, have no Windows equivalent. Supporting it would mean a second execution model with differentCtrl-Csemantics and no coverage.Consequence: runemark’s
selectfeature is Unix-only for the same reason, while the rest of runemark stays cross-platform for its other consumers.
The crate is named
opi, with a known collisionVerified against the crates.io API on 2026-09-18:
opiitself is unclaimed, but theopi-*namespace —opi-agent,opi-ai,opi-tui,opi-sandbox,opi-protocol,opi-coding-agent, all at0.8.1— belongs to OdradekAI/opi, an unrelated coding agent whose CLI is most likely also calledopi.Reason: the name states what the tool is, and it is still registrable.
Consequence: three things follow. Installing both tools puts two
opibinaries in~/.cargo/bin, where one overwrites the other. A registry search for “opi” surfaces the other project first. Andopi-core,opi-adaptersand similar are unavailable, so any later split into multiple crates needs differently named internal crates. The name is also not reserved — publishing early is what secures it.
A favourite leaves its group
A script marked
favoritemoves into a Favorites group at the top rather than sorting first inside the group its name implies.Reason: sorting first within a group barely shows. Measured on a real project, five of its ten groups held two entries each.
Consequence: a favourite in a workspace member is the exception — it stays with its member, because lifting it out would lose which package it belongs to and its id would no longer say.
More than one ecosystem, and none of them wins
package.jsonandCargo.tomlare both looked for, and a repository that is both shows both under one set of headings.Reason: measured across 231 directories here, 133 carry a
package.json, 51 aCargo.tomland twelve both. Choosing one would misrepresent those twelve, and refusing a repository that has only the second turned 39 of them away.Consequence:
Taskcarries how to start it — through the package manager, or by naming its own program. A third ecosystem needs no second list.
Built-in actions never shadow a project’s scripts
A project with a script named
health,clean,releaseorcommitis not merely plausible —cleanappeared in 41 of 133 measured projects. Where a name collides the script wins, and every built-in area is reached by a flag or a hotkey instead of a bare word.Reason:
opimust not break in exactly the kind of project it is meant to improve.
Reporting beats acting, where acting is a guess
opi --updatesseparates what is outdated by semver jump, and applies only the safe ones, only on request, and only by calling the package manager with a list of names. Majors never ride along. Clean removes only what it has measured and been told to;opi --hooksappends one line to a hook.Reason: the package manager rewrites
package.json, the lockfile and pnpm catalogs correctly — measured, including the 45 of 79 workspaces here that use a catalog. opi writing any of them itself would be the guess. (The rule used to be “updates are never applied”; see constraints.md for why it narrowed.)Consequence: the value of the screen is the separation — “two safe, one major” is a decision, a column of version numbers is homework.
A
Reportwhere the data is parsed, raw output where it is notThe dependency audit and the update list render as runemark
Reports. Health, the workflows and the secret scan print the tool’s own output instead.Reason: a
Findingis a message. Relayed output is not one — putting it in a single finding collapses its structure, and splitting it per line flattens the tool’s indentation and counts lines as findings. Both were tried against real output and both read worse than the tool itself.Consequence:
opiuses roughly half of runemark’s surface, and that is the intended answer rather than a gap. The half it does not use is for applications that own their finding types;opideliberately does not.
No health score
Health reports concrete results — counts, findings, file locations — and never a synthetic figure like “87/100”.
Reason: a composite score stops meaning anything within weeks; concrete results stay useful.
opi is the CI for repositories without one
opi --check pushruns every check and the build, andopi --hooksinstalls it as the pre-push hook. A failing run blocks the push;git push --no-verifyis the way past it.Reason: the private repositories here run no CI on push, and the replacement was a hand-written
verifyscript and husky hook per repository. opi already knew the tools; what was missing was the build, a gate on the upstream branch and a hook nobody has to maintain.Consequence: workflows are no longer only “a convenience before pushing”. The push workflow is meant to be binding, which is why it blocks rather than warns, and why its gates stay offline — a hook that waits on the network is a hook people disable.