What is built, and where the rest is tracked
Planned work lives in GitHub issues, not in this file. Anything new — a bug, an idea, a change of mind — goes there, so there is one list rather than two that disagree.
A milestone is named after the release that carries it, and is closed when
that release is tagged. The names drifted once — milestones called v1…v4
against versions 0.1.0…0.6.0, which read as major versions and were not — so
they were renamed to the versions they actually shipped in. An issue blocked on
something outside this repository sits on no milestone: a schedule it cannot
meet is how the drift started.
- 0.2.0 — read and write issues, queues, projects, goals, attachments; every distribution channel; the agent skill. Shipped across 0.1.0 and 0.2.0.
- 0.3.0 — worklogs, checklists, portfolios, administration.
- 0.5.0 — what the API offered and the tool did not: the dictionaries and people that make a write guessable rather than a guess, issue history, moving an issue between queues, editing what was already written, and writes for the project-management entities. Shipped across 0.4.0 and 0.5.0.
- 0.6.0 — the endpoints a survey of the API found unused, and the query language written down against a live Tracker rather than from memory.
- 0.7.0 — the rest
of the Tracker API: components, link types, queue access, and the writes a
sweep found missing; then a study of another Tracker CLI, which added bars,
a timer,
sprint get, transition by status and two ways to stop fighting the shell. Shipped across 0.7.0 and 1.0.0. - 1.0.1, from issues filed by people using it:
--helpandytcli cheatsheet morecarry the docs, source and bug-tracker URLs, because a binary installed throughuv,cargoor a tap arrives detached from its repository and--helpis then the whole project; and the skill says which markup a description is written in — Markdown, not the Yandex wiki markup whose#numbers a list. The skill also offers to file an issue whenever the tool surprises somebody, andreporting.mdsays what to strip out of a report before it becomes a public document. The README’s example of what a person sees was the machine rendering, fence tags and all — a person sees markdown behind a margin bar, and now that is what it shows. - What
markupTypedecides, settled live: the default iswf, which honours the old wiki spellings as well as Markdown, andmdis the stricter mode that drops them. So this sends neither, and#heads a section under both — which is the only part a caller has to know. - A fixture audit,
every_fixture_still_has_the_shape_tracker_answers_with. The fixtures were written from an upstream client’s documented shapes rather than recorded, so the mocked suite is only as true as they are; the audit asks the real API once per endpoint and fails on a key a fixture claims and Tracker no longer returns. It prints what it could not check rather than passing quietly: an organisation with no sprints leaves the sprint fixture unverified, and that is a fact about the run rather than a success.tests/fixtures/NOTICEnames the seven that remain only borrowed, and issue #78 is where closing that gap is tracked. - 1.0.0 — no new surface of its own. It is the version that says the surface is finished: every endpoint a survey of the API found is reachable, the output shape has been stable for several releases, and what was deliberately left out is written down below rather than pending. The number is a promise about breakage from here, not a claim that there is nothing left to build.
- 2.0.0 — the
Yandex Wiki, and signing in without registering anything. The Wiki is the
other half of an organisation’s writing, as a
ytcli wikigroup that covers the whole Wiki API: pages, descendants, search, comments, files, dynamic tables, access, clones, and the writes to each. ADR 7 records why it belongs behind this binary. Checked against a real Wiki: the live suite makes one of everything and deletes it, andplugin/skills/ytcli/wiki-markup.mdis the source of a page whose rendering was checked in a browser. 2.0.1 puts the sign-in application into the Python wheels as well: 2.0.0 compiled it into the release binaries only, sopipanduvxusers could not sign in through the browser. - 2.1.0 —
auth statussays what each token may do (access: read,writeorunknown), recorded at sign-in because Yandex cannot be asked a token’s scopes. Wiki refusals name the Wiki rather than Tracker, and a grant the Wiki answers withUSER_NOT_FOUNDsays what usually causes it. The plugin moved toplugin/, so a local install no longer copies the build directory, and a test keeps both plugin manifests at the crate’s version. - 2.1.1 — a Wiki 403 that comes with a reason (“no rights to create a page in
this section”: an editor who is not an author) is reported as that reason
and exits 5, instead of blaming the token for a
wiki:writeit has. A bare 403 keeps its guidance, worded to name both causes. Refused responses are logged at debug level with their body, so-vvshows what the API said. - 2.2.0 — device sign-in issues a new code on every attempt: each request
carries a
device_idof its own, without which Yandex answers a retry with the pending code it already holds and the page rejects it as expired. The poll gives up after five minutes where Yandex omitsexpires_in, rather than waiting for a confirmation that can no longer arrive, and--deviceagainst a build with no OAuth application fails before the wizard, the keychain or any request instead of after them. The skill and the README lead with the code sign-in, withdefaultproposed as the account name. Once a day, at the end of a command and only where both streams are terminals, a newer version is named along with the way to get it — the registry and the advice follow whatever installed the binary, so auvinstall is asked about at pypi.org and told to runuv tool upgrade.YTCLI_NO_UPDATE_CHECK=1turns it off. kind:question— open design questions, filed so they are not re-litigated from scratch.
Labels split the work by area: area:issues, area:entities,
area:attachments, area:output, area:testing, area:distribution,
area:agents.
Already built
-
Project scaffolding: toolchain, lints, hooks,
justtasks, CI on three platforms, tagged releases, docs site. -
Layered configuration and profile resolution, reporting where the choice came from (ADR 2).
-
Keychain-backed credentials, keyed by account (ADR 2).
-
HTTP client: auth and organisation headers, typed errors mapped to exit codes, retries limited to transport failures and backpressure (ADR 4).
-
Compact text renderer for issues and issue pages: fixed field order, links with their type, fenced untrusted text, pagination tally — pinned by snapshots (ADR 1, ADR 3).
-
The whole command tree, built rather than declared: exit code 64 exists for a command a future build adds, and nothing in this one returns it.
-
ytcli auth statusend to end. -
ytcli cheatsheet, compiled into the binary. -
issue changelog,issue move, editing comments and worklogs,worklog findacross issues,queue versionsandqueue tags, and create/update/delete for projects, portfolios and goals — the whole of milestone v3 apart from making the tool work inside Claude Cowork, which needs a session there to answer. -
ytcli dict listand theusergroup: the two things a write had to be guessed at without. Dictionaries print the stable key beside the localised name, because only one of the two can go in a script.user findfilters the directory here — Tracker has no user search endpoint — and says how many people it read rather than presenting a capped answer as a complete one. -
Milestone 0.6.0:
plugin/skills/ytcli/yql.md, every query on it sent to a real Tracker before it was written down — which is howStoryPointsturned out not to be a filter name while"Story Points"is.field getsays what a field accepts, naming the command that lists the values when they live elsewhere.issue remotelinksshows what an issue is attached to outside Tracker.queue automationreports macros, autoactions and triggers, and says which section it was refused rather than counting it zero.sprint listandqueue local-fieldsclose the two listings that had no way in. -
Milestone 0.7.0:
link typesprints the vocabulary a write takes beside the type ids it is not — a distinction that turned out to be encoded backwards in the parser, the fixture and a unit test at once.queue accessanswers who may do what in a queue, in two tables because Tracker gives two answers: the rule, roles and all, and the people that rule resolves to. Only the second can say whether the caller is one of them, and it says?rather thannowhen the token could not name its own user.--format jsoncarriesstatus_keyandpriority_keybeside the displayed status and priority, and the terminal colours a status by that key: a Russian organisation answersЗакрыт, and everything that matched onclosedmatched nothing and said so in no way at all. A bare issue number is completed from the profile’s default queue.attachment deletecloses the one gap in the sweep of the API that a user hits by accident, naming the file rather than the id it is about to lose.issue updateover several issues is one request rather than one each, which turned out to be the safer shape as well as the cheaper one: Tracker validates the whole list before it writes, so an unknown key refuses the change instead of leaving half of it applied. It answerschanged N of Mand names every issue that did not change;ytcli bulk statusreads the result back later.issue transitionandissue movetake a list on the same terms, verified live to be refused entire when one key or the target queue does not exist. A list of keys leaves no unambiguous place for a bare transition id, so with several issues the transition is named with--to. -
ytcli issue listas an alias ofissue find: every other group lists with that word, and the group used most was the one exception. -
Profile routing: a bare key goes to the profile that can see its queue, and every command says on stderr which profile and organisation answered.
ytcli auth useswitches the stored default without reading a token, andytcli auth editchanges an existing profile — its name, the organisation it points at, and the free-text note saying which organisation that is — on the same terms. The note rides along on that stderr line, because an organisation id is a number nobody recognises.ytcli auth removedeletes a profile, and is deliberately notauth logout: one drops an organisation from the config, the other drops a credential from the keychain, and each says what it left behind. -
Help written in markdown and rendered with termimad for a terminal; the source goes to a pipe, where an agent reads it natively and escape codes would be noise.
-
The agent surface (ADR 6):
plugin/skills/ytcli/, loaded as a plugin by Claude Code and Codex from one directory, and--helpwritten as documentation rather than as clap’s defaults. Both are checked against the binary by tests, since a stale example is acted on rather than noticed. -
Worklogs, checklists and link editing, with reads and writes under separate command prefixes so an allowlist cannot be stretched from one into the other.
-
queue get, andqueue create --like: a new queue copies its issue types, workflows, resolutions and defaults from one that already works, because workflow ids are organisation-specific strings nobody has memorised.--yesis required for one queue, since a key is claimed once. -
Organisation-wide field and template listings, read-only. The template paths are
issueTemplatesandcommentTemplates; there is no_templatescollection, and every plausible guess at one answers 400 or 404. -
Boards, read-only: listing, columns in board order, and sprints. A board that cannot have sprints is refused in Tracker’s own words rather than answered with an empty list.
-
Portfolios: listing, reading, what one contains, and moving a project or a portfolio in and out of one. Containment writes quote the version they read, so a concurrent change is refused rather than overwritten. Containment is a separate command rather than part of
get, because it is a second request and nothing should pay for an answer it did not ask for. -
Image attachments drawn in the terminals that can draw them, and a next step printed everywhere else.
-
Documentation that is executed: the README and cheatsheet examples run as
trycmdcases against the stub, and a command documented without a case has to be declared unrunnable with a reason. -
The fixtures are checked against the real API, and the seven that nothing could check are checkable now: a live test makes a queue version, a component, a local field, a board with a sprint, a template, a link out of Tracker and a required field, because an empty array agrees with every fixture ever written.
tests/fixtures/NOTICEsays which shapes are confirmed and which are still only borrowed. -
brew install ormeilu/tap/ytcli, from ormeilu/homebrew-tap. The formula is generated by the release workflow from the archives it just published and pushed with a deploy key scoped to that one repository. The step is skipped when the key is absent: a release must not fail over an optional channel.
Deliberately out of scope
Recorded here rather than as issues, so the decisions are not re-opened by someone reading the backlog:
- A second entry point named
yandex-tracker-cli. It would double the size of every release artifact to save typing. The PyPI package keeps that name; the command isytcli, and a shell alias covers the rest. - Any verb that both reads and writes. ADR 1 depends on the split being total: agent hosts allowlist read verbs permanently, and one mixed verb would silently break that for every user.
- Printing a stored token. A tool whose main consumer is an agent should not offer secret exfiltration as a feature.
- Creating versions, sprints, fields and boards. A sweep of the API for
verbs this tool does not have turned up five, and one of them — deleting an
attachment — was worth building, because
attachment uploadhad no undo and the mistake is one anybody makes. The other four are all the configuration of a queue rather than work in it:POST /v3/queues/{key}/versionsand the version writes,POST /v3/sprintsand the board writes,POST /v3/fieldsandPOST /v3/queues/{key}/localFields. They are done once, by somebody with the rights to do them, in an interface that shows what the choice affects — the same argument that keeps triggers read-only here. A board is a view of work and not the work, andboardandsprintstay read-only for that reason. - Box-drawn tables, and wrapping a cell instead of cutting it. Both come
from looking at another Tracker CLI to see what was worth taking. Borders cost
tokens on every row and stop the output being
cutandgreped; a wrapped cell breaks one-line-per-issue, which is the property that makes a listing greppable at all. What was worth taking from that comparison was colouring by the status key rather than the words, and completing a bare issue number from the default queue — both are in. - A JSON error envelope on stderr under
--format json. The exit code already carries the class of failure — 2, 3, 4, 5 mean distinct things and are the documented contract — and a second, parseable copy of the message would be one more shape to keep stable for no question it answers. - Deleting a queue. Tracker deletes a queue by hiding it, so the key stays
spent and no
--yesbuys it back.queue createalready asks for exactly that reason; a delete would be the same irreversible act with less to show for it.