{
  "markdown": "# opys\n\nFile-based inventory of typed markdown documents for human + AI codebases — one\nmarkdown file per document, verified in CI.\n\n`opys` manages a version-controlled inventory of *what a product does*: one\nmarkdown file per document, each with YAML frontmatter (stable ID, status,\ntags) and an optional body (spec prose, a test plan, manual-verification\nprocedures). The document **types** — their ID prefixes, statuses, fields,\nrequired sections, and validation rules — are configured in one\n`opys.toml`. The default config ships a permanent **feature** type\n(`FEAT-NNNN`) plus ephemeral **task/bug/chore** types (`TASK-`/`BUG-`/`CHORE-NNNN`)\nfor in-flight work, deleted on `close`. Writes go through the CLI so invariants\nhold at write time and parallel agents don't collide; reads are plain `grep` +\ntargeted file reads. A `verify` subcommand is the CI gate. It is deliberately\n*not* a task board — no sprints or assignees; priority exists only as an\nopt-in declared int field (`[types.X.fields.priority]`) that the web UI's\nboard orders and reorders by.\n\nNeed a different lifecycle — an `epic`, an `adr`, a `risk`? Add a `[types.<name>]`\nblock to `opys.toml` and the whole tool (create, verify, index) works for\nit. Durable knowledge → features; \"what I'm doing right now\" → a task/bug/chore.\n\nIt pairs with the `opys` skill (under `skills/`), which\ndocuments the format and the authoring/implementation workflows for coding\nagents.\n\n## Install\n\n```sh\ncargo install opys                 # the CLI (what agents use)\n```\n\nOr build from source:\n\n```sh\ncargo build --release -p opys        # target/release/opys\n```\n\n### Use from another flake\n\nThe flake exposes `opys` as a package, an app, and an overlay, so other flakes\ncan consume the CLI without going through crates.io:\n\n```nix\n{\n  inputs.opys.url = \"github:BohdanTkachenko/opys\";\n\n  outputs = { nixpkgs, opys, ... }:\n    let\n      system = \"x86_64-linux\";\n      # Either apply the overlay and use `pkgs.opys`…\n      pkgs = import nixpkgs {\n        inherit system;\n        overlays = [ opys.overlays.default ];\n      };\n    in {\n      devShells.${system}.default = pkgs.mkShell {\n        # …or reference the package directly: opys.packages.${system}.default\n        packages = [ pkgs.opys ];\n      };\n    };\n}\n```\n\nOr run it straight from the flake, no install:\n\n```sh\nnix run github:BohdanTkachenko/opys -- --help\n```\n\n`opys.toml` lives at the **project root** — opys finds it by searching upward\nfrom the current directory (like git or Cargo). It declares a `base` directory\n(default `opys/`, relative to the root) so the inventory stays out of the\nrepo root: the document files, flat at `opys/` by default (the path is rendered\nfrom a configurable `[layout]` template — see the spec). A document's type is its\nID prefix.\n\n## Quick start\n\n```sh\nopys init                                   # bootstrap opys.toml + opys/\n# edit opys.toml: types, statuses, fields, sections, rules\n\nopys new --title \"Tab title follows OSC 0/2\" --tags osc,tabs\nopys list --status planned\nopys list --tag area                        # exact tag, or any tag with key `area`\nopys set-status FEAT-0001 implemented       # rejected unless a test item is checked\nopys verify                                 # integrity check; nonzero exit on problems\nopys stats                                  # configurable [[stats]] sections (default: status/coverage/tags)\nopys tags                                   # distinct tags (--keys for just keys)\n\n# Ephemeral work, linked to a feature (default types: task/bug/chore):\nopys new --type bug --title \"Survive profile switch\" --features FEAT-0001\nopys close BUG-0002                         # deletes the file; reference struck through\n\n# Bulk: the mutating commands take a comma-separated id list, or `-` for stdin\nopys set-status FEAT-0001,FEAT-0002 wontfix --reason \"superseded\"\nopys list --type task --status done --format ids | opys close -\n```\n\nMutating commands (`new`, `set-status`, `tag`, `retire`, `block`, `close`,\n`cleanup`) reconcile cross-references, linkify prose, and relocate documents to\ntheir canonical layout path (e.g. an archived doc moves into `_archived/`)\nautomatically; pass `--no-sync` to skip, or run `opys sync` after editing files\nby hand.\n\n## Commands\n\n| Command | Purpose |\n|---|---|\n| `init` | bootstrap `opys.toml` + `opys/`, print a CLAUDE.md snippet |\n| `config <init\\|validate>` | generate / validate the universal `opys.toml` |\n| `new --type <T>` | allocate the next ID and write a skeleton document of type `T` (auto-syncs) |\n| `import --type <T>` | bulk-create documents of type `T` from a JSONL file (sequential IDs, one sync) |\n| `show` / `list` | retrieval (`--type`, `--tag`, `--status`, `--format table\\|ids\\|paths`) |\n| `set-status` | guarded transitions, enforced by the type's configured rules |\n| `tag` | add/remove tags (`--add a,b --remove c`) |\n| `retire` | delete document(s); each ID is logged and never reused |\n| `block` / `unblock` | record a directional blocker between documents |\n| `close` / `cleanup` | finish document(s) of a type with a terminal status; strip struck refs |\n| `verify` | full integrity check — wire into CI |\n| `sync` | reconcile references, linkify prose, relocate docs to their layout path (for hand edits) |\n| `stats` | render configured `[[stats]]` sections (each a SQL query over the corpus, shown as a table; default: status counts, coverage, tags) |\n| `query \"SELECT …\"` | run a SQL query over the inventory (`-` reads it from stdin; `--stdin` binds stdin to `$1` for escape-free values) and print the result table; `--write` allows INSERT/UPDATE/DELETE, applied only if the edit introduces no new `verify` problem (else nothing is written). The `blocks` table decomposes bodies into `##` sections — `UPDATE blocks SET text = …` edits a section in place |\n| `agent-rules --tool <editor>` | generate a rules-based editor's instruction file from the canonical rule |\n| `web <start\\|add\\|remove\\|list\\|scan\\|install\\|uninstall>` | the always-on node: serve the allowlisted projects over HTTP — see [below](#the-always-on-node-opys-web) |\n\nA feature file looks like (the `references` map is auto-maintained — a work\nitem links back, and a closed one leaves a struck-through tombstone):\n\n```markdown\n---\nid: FEAT-0421\nstatus: implemented\ntags: [osc, tabs]\nreferences:\n  TASK-0042: Make tab title survive profile switch\n---\n\n# Tab title follows OSC 0/2 sequence\n\n## Test plan\n- [x] OSC 2 with valid UTF-8 updates title — `tab::osc_title_updates`\n- [ ] Invalid UTF-8 in title payload — uncovered\n```\n\nSee `skills/opys/references/format.md` for the normative document format and the\n`opys.toml` config reference.\n\n## The always-on node (`opys web`)\n\nEvery `opys` command so far is one shot: load the inventory, write, exit. The\n**node** is that same engine kept warm — a long-lived local process that serves\nthe projects you allowlisted over HTTP, with a web dashboard, a typed API and a\nlive event stream. It is what you open when you want to see every project at\nonce instead of grepping one repo at a time. It ships inside the `opys` binary:\nif you installed the CLI, you already have it.\n\n### From nothing to a dashboard\n\n**1. Look at the allowlist.** On a machine that has never run the node it is\nempty, and an empty allowlist means the node would serve nothing at all:\n\n```\n$ opys web list\nallowlist: /home/dan/.config/opys/server.toml\nbind:      127.0.0.1:6797 (default)\n\nnothing allowlisted — add a project with: opys web add <path>\n```\n\n**2. Allowlist a project** — any directory holding an `opys.toml`:\n\n```\n$ opys web add ~/work/notes\nadded /home/dan/work/notes to /home/dan/.config/opys/server.toml\na running node picks this up within a minute\n```\n\nAll that did was write two lines to `~/.config/opys/server.toml`. Nothing was\nstarted, and nothing was contacted:\n\n```toml\n[[project]]\npath = \"~/work/notes\"\n```\n\n`opys web list` now prints the allowlist as written, and under it what those\nentries resolve to right now:\n\n```\n$ opys web list\nallowlist: /home/dan/.config/opys/server.toml\nbind:      127.0.0.1:6797 (default)\n\n  project  ~/work/notes  -> /home/dan/work/notes\n\nserving 1 corpus in 1 project:\n  notes  /home/dan/work/notes\n```\n\n**3. Start the node.** It runs in the foreground and `Ctrl-C` stops it; make it\na background service once you like it ([below](#run-it-as-a-service)):\n\n```\n$ opys web start\nopys-server: serving 1 corpus from /home/dan/.config/opys/server.toml\nopys-server listening on http://127.0.0.1:6797\n```\n\n**4. Open <http://127.0.0.1:6797>.** That is the dashboard.\n\n### Why allowlisting is a separate step\n\nThis is the part that surprises people: `opys web start` takes no project paths,\nand the node finds nothing by itself. It serves exactly the entries in\n`~/.config/opys/server.toml` — a file only you write. Approving a project and\nrunning the node are deliberately two different acts, because that file is the\nsecurity boundary. Two guarantees follow from it:\n\n- **The node serves only what you allowlisted.** `opys web add` edits that file\n  and never contacts a running node; the node re-reads the file on its own and\n  picks up the change within a minute, no restart. So allowlisting is something\n  you do at a terminal — never something a page open in your browser can do to\n  you. Discovery only ever *suggests*: `opys web scan` prints candidates and has\n  no way to add one.\n- **The API is typed; the node cannot execute arbitrary commands.** Every write\n  the dashboard makes is a named action with named arguments — `set-status`,\n  `tag`, `block`, `unblock`, `close` — run through the same engine, the same\n  inventory lock and the same write-time rules as the CLI. The request body is a\n  closed set: there is no shell endpoint, no \"run this opys command\" endpoint,\n  and no endpoint anywhere that accepts a filesystem path.\n\n### More than one project\n\n`opys web scan` walks your home directory (ten levels, skipping hidden, build,\nvendor and cache directories), lists every project it finds and marks the ones\nalready allowlisted. It suggests and nothing more — the command cannot add\nanything:\n\n```\n$ opys web scan\nscanning /home/dan (depth 10)…\n  /home/dan/Projects/opys\n  /home/dan/Projects/opys-feature\n  /home/dan/work/notes  (allowlisted)\n\nscan never adds anything — allowlist one with:\n  opys web add /home/dan/Projects/opys\n```\n\nAdd them one `opys web add` at a time, or allowlist a whole tree with\n`--prefix`, which covers everything ten levels below it — including projects you\ncreate there later, found by the node's hourly rescan:\n\n```\n$ opys web add --prefix ~/Projects\nadded /home/dan/Projects to /home/dan/.config/opys/server.toml\na running node picks this up within a minute\n```\n\nOne entry can serve several *corpora* — a corpus is one inventory: one\n`opys.toml` and the documents under it. Sibling **git worktrees come along with\nthe project they belong to**, so allowlisting a repo covers every worktree of\nit. Here two entries serve three corpora:\n\n```\n$ opys web list\nallowlist: /home/dan/.config/opys/server.toml\nbind:      127.0.0.1:6797 (default)\n\n  project  ~/work/notes           -> /home/dan/work/notes\n  prefix   ~/Projects (depth 10)  -> /home/dan/Projects\n\nserving 3 corpora in 2 projects:\n  opys   /home/dan/Projects/opys  main  (primary)\n  opys   /home/dan/Projects/opys-feature  feature/web\n  notes  /home/dan/work/notes\n```\n\n`opys web remove <path>` takes an entry back out. A project reached *through* a\nprefix has no entry of its own, so instead of pretending, the CLI names the\nentry that is responsible:\n\n```\n$ opys web remove ~/Projects/opys\nnot allowlisted directly — served by the prefix entry ~/Projects\nremove that entry instead: opys web remove ~/Projects\n```\n\nStart the node again (or leave it running and wait a minute) and it serves all\nthree:\n\n```\n$ opys web start\nopys-server: serving 3 corpora from /home/dan/.config/opys/server.toml\nopys-server listening on http://127.0.0.1:6797\n```\n\n### What the dashboard shows\n\nThe sidebar lists every project and the corpora inside it — labelled by git\nbranch, with the primary worktree marked, and a dot per corpus for its verify\nstate (clean, *N* problems, or not read yet). Pick one and you get:\n\n- **the board** — every document in that corpus, in a column per status, with\n  filters for type and tag, a text filter set from the omnibox, and drag and\n  drop: onto another column to change status, within a column to set priority\n  (an opt-in field; see ADR-0095). The keyboard drives it too — arrows move\n  between columns and cards, Enter opens, Home/End jump within a column,\n  PageUp/PageDown switch projects;\n- **a document** — its frontmatter and rendered body, both edited in place:\n  status, tags, blockers and custom fields on the panel, the markdown body by\n  clicking into it, and close behind a confirmation. Every write is a typed\n  action taking the same write path as the equivalent `opys` command, so a\n  write the CLI would refuse — a status change whose rule is unmet, say — is\n  refused here too, with the same message. Creating documents stays a CLI job.\n- **the query console** — the same SQL over the corpus that `opys query` runs,\n  read-only;\n- **the union view** — every worktree of one project side by side, so you can\n  see where two branches disagree about a document. It shows the drift and\n  nothing else: nothing here merges anything, because git is the merger.\n\n**Ctrl+P** (⌘P on a Mac) or `/` opens the omnibox from any view: a fuzzy\nfinder over the corpus's tickets — or every served corpus, from the home page\n— that opens a ticket on Enter or, from a board, applies the text as its\nfilter.\n\nEverything updates live: the node watches each inventory and pushes events over\na WebSocket, so an edit you make in your editor — or a write from `opys` in\nanother terminal — shows up in the browser without a reload.\n\nThe port is **6797**, and the node binds loopback only. There is no\nauthentication, so the bind address *is* the boundary; while it is on loopback\nthe node also refuses any request whose `Host` is not loopback and any\ncross-origin request, so a page you happen to be visiting cannot drive it.\nWiden it — `opys web start --bind 0.0.0.0:6797`, or a `bind = \"…\"` line at the\ntop of the allowlist file — only if you mean to, and put something in front of\nit that authenticates.\n\n### Run it as a service\n\n`opys web install` writes a systemd **user** unit and prints the two commands\nthat turn it on. It never runs them — enabling a service on your session is your\ndecision, not a side effect of an install:\n\n```\n$ opys web install\nwrote /home/dan/.config/systemd/user/opys-server.service\n\nenable it with:\n  systemctl --user daemon-reload && systemctl --user enable --now opys-server\n\nthe node will listen on http://127.0.0.1:6797\n```\n\nRun those two commands and the node comes up at login and restarts if it\ncrashes. A **user** service lives and dies with your session, so on a machine\nyou are not usually logged into — a headless box you reach over SSH — also run\n`loginctl enable-linger $USER`, or the node stops the moment you disconnect.\n\nThe unit is static — `ExecStart=…/opys web start --bind 127.0.0.1:6797`,\npointing at the binary you ran `install` from — so it never needs touching again\nwhen you allowlist another project. Two things *are* fixed at install time: the\naddress (resolved then from `--bind`, else the allowlist file's `bind`, else the\ndefault) and the `--config` path if you passed one. Change either afterwards and\nre-run `opys web install --force`; editing `bind` in the allowlist file alone\nwill not move a service whose unit already names an address. Installing over an\nexisting unit is refused unless you pass `--force`:\n\n```\n$ opys web install\nerror: /home/dan/.config/systemd/user/opys-server.service already exists — pass --force to overwrite it\n```\n\n`opys web uninstall` deletes the unit and prints the disable line first, because\nthat is the order you have to run it in — deleting a unit file does not stop the\nservice it started:\n\n```\n$ opys web uninstall\nstop it first — removing the unit does not stop a running service:\n  systemctl --user disable --now opys-server && systemctl --user daemon-reload\n\nremoved /home/dan/.config/systemd/user/opys-server.service\n```\n\nOn a machine with no systemd user manager — a Mac, a container, WSL1, a distro\nthat boots something else — `install` prints how to run the node by hand and\nexits 0. That is a fact about the machine, not an error, and nothing is written:\na unit file no service manager will ever read is worse than no unit at all.\n\n**On NixOS or with home-manager, do not run `opys web install`** — declare the\nservice instead, so it is reproducible and survives a rebuild rather than living\nas an untracked file in `~/.config`. It is the same unit either way, so all you\nare doing is writing it down where your configuration can see it.\n\nFirst make `pkgs.opys` exist by applying this flake's overlay in your\nconfiguration (`opys` here is this flake, taken as an input — see\n[Use from another flake](#use-from-another-flake)):\n\n```nix\nnixpkgs.overlays = [ opys.overlays.default ];\n```\n\nThen, in **home-manager**, where the attributes are the unit's own sections:\n\n```nix\nsystemd.user.services.opys-server = {\n  Unit.Description = \"opys always-on node\";\n  Service = {\n    ExecStart = \"${pkgs.opys}/bin/opys web start --bind 127.0.0.1:6797\";\n    Restart = \"on-failure\";\n  };\n  Install.WantedBy = [ \"default.target\" ];\n};\n```\n\nor, in a plain **NixOS** configuration, where `systemd.user.services` is a typed\nsubmodule rather than a freeform unit — same service, different spelling:\n\n```nix\nsystemd.user.services.opys-server = {\n  description = \"opys always-on node\";\n  wantedBy = [ \"default.target\" ];\n  serviceConfig = {\n    ExecStart = \"${pkgs.opys}/bin/opys web start --bind 127.0.0.1:6797\";\n    Restart = \"on-failure\";\n  };\n};\n```\n\nOn a headless box add `users.users.<you>.linger = true;` (NixOS) for the same\nreason `loginctl enable-linger` exists above.\n\nThe allowlist stays yours to edit either way: `opys web add` writes it, and the\nnode picks the change up without a restart. (One caveat if you hand-edit\n`~/.config/opys/server.toml`: `opys web add`/`remove` rewrite the file from its\nparsed form, which preserves keys and values but drops comments.)\n\n### The `web` subcommands\n\n| Command | Purpose |\n|---|---|\n| `web start [--bind ADDR] [--config PATH]` | run the node in the foreground |\n| `web add <PATH> [--prefix]` | allowlist a project, or a directory to search under |\n| `web remove <PATH>` | drop an entry from the allowlist |\n| `web list` | the allowlist, and the corpora it currently resolves to |\n| `web scan [--under PATH] [--depth N]` | suggest projects; adds nothing, ever |\n| `web install [--bind ADDR] [--force]` | write the systemd user unit; print how to enable it |\n| `web uninstall` | remove the unit; print how to disable it |\n\nEvery one of them except `uninstall` also takes `--config <PATH>`, to work on an\nallowlist file other than `~/.config/opys/server.toml`. `install` writes that\npath into the unit's `ExecStart`, so the service it installs serves the file you\nnamed rather than the default one.\n\n(`web scan` spells its scan root `--under` rather than `--root`, because `opys`\nalready has a global `--root` for the inventory root and clap propagates a\nglobal into every subcommand. `--root` and `--no-sync` mean nothing to `web`,\nwhich refuses them rather than ignoring them — a scan of the wrong tree looks\nexactly like a scan of the right one. The same surface is also available as\n`opys-server web …` — one implementation, mounted by both binaries.)\n\n## The `opys` skill\n\nThis repo doubles as a multi-agent plugin that drives `opys` (authoring\ninterviews, the implementation workflow, retrieval discipline). The skill lives,\nonce, in [`skills/opys/`](skills/opys/) and is\ntool-agnostic; the repo also ships per-agent manifests so most tools can install\nit natively. (The `opys` binary itself is a prerequisite — `cargo install opys`.)\n\n**Native plugin/extension install:**\n\n| Agent | Install |\n|---|---|\n| Claude Code | `/plugin marketplace add BohdanTkachenko/opys` then `/plugin install opys@opys` |\n| Codex | `codex plugin marketplace add BohdanTkachenko/opys`, then install via `/plugins` |\n| Gemini CLI | `gemini extensions install https://github.com/BohdanTkachenko/opys` |\n| pi | `pi install git:github.com/BohdanTkachenko/opys` |\n| opencode | add `\"instructions\": [\"…/agent-rule.md\"]` (see `opencode.json`) |\n\n**Copy the skill folder** (conditional, fullest content) for tools that read a\nskills directory:\n\n| Tool | Copy `skills/opys/` to |\n|---|---|\n| Claude Code | `.claude/skills/opys/` (or `~/.claude/skills/`) |\n| Cursor | `.cursor/skills/opys/` |\n| Google Antigravity | `.agents/skills/opys/` |\n\n```sh\ngit clone --depth 1 https://github.com/BohdanTkachenko/opys /tmp/opys\ncp -r /tmp/opys/skills/opys <your-project>/.claude/skills/   # or .cursor/skills/ , .agents/skills/\n```\n\n**Always-on rule file** (a short, self-gating pointer — activates only when the\nproject has a `opys/` inventory) for rules-based editors: `opys` *generates*\nit from one canonical rule (`skills/opys/agent-rule.md`), so there\nare no duplicate files to keep in sync. Run it in your project:\n\n```sh\nopys agent-rules --tool cursor     # or windsurf | cline | copilot | kiro | all\nopys agent-rules --tool copilot --stdout   # print instead of writing\n```\n\nIt writes the right file in the right place (`.cursor/rules/opys.mdc`,\n`.windsurf/rules/…`, `.clinerules/…`, `.github/instructions/…`,\n`.kiro/steering/…`) with any host-specific frontmatter.\n\nThe skill folder carries the normative spec (`references/format.md`), so one\nfolder brings everything.\n\nThe CLI itself is universal — any agent that can run a shell command can use\n`opys`. For tools that read project instructions instead of skills, the\ncross-tool standard is **AGENTS.md** (this repo ships one). The substance is the\nsame everywhere: `opys new --type/set-status/close/verify ...` for writes,\n`opys list`/`rg` for reads.\n\n## License\n\nApache-2.0 — everything here, including the always-on node and its web UI.\nPermanently, and for every crate in the workspace.\n",
  "bytes": 22174,
  "sha": "9f6dee76bcad250d76c714ad4a79b6f50e21c0fafcf33f950bc4350606ee39d3",
  "repo_slug": "bohdantkachenko/opys",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_bohdantkachenko_opys_ca7ef97c/readme"
}