feat(tags): typed issue tag surface #145

Merged
Claude-Run merged 1 commit from feat/YT-90-issue-tags into main 2026-08-05 20:52:03 +02:00
Member

Tags were the one issue attribute with no typed code path. Both yt issue tag add and remove were thin wrappers over the command language (tag <name> / remove tag <name>), so a write took a name and never an id, a typo silently created a new instance-wide tag that the CLI has no way to delete, and there was no read path at all: inspect_issue never selected tags, so neither yt issue inspect nor the MCP get_issue could report what an issue carried, and nothing could enumerate the tags that exist.

youtrack_client::api now owns the surface: list_tags and list_issue_tags read /api/tags and /api/issues/<id>/tags through fetch_all_pages, create_tag posts /api/tags, and the write is split from its resolution the way project create and project fields add are: plan_add_issue_tag returns an AddTagPlan carrying the resolved tag id and the byte-exact body that add_issue_tag posts. A name matching several visible tags is refused naming every candidate id; a name matching nothing is refused with the near matches unless a create was authorized. remove_issue_tag sends DELETE /api/issues/<id>/tags/<tagId> and re-reads the issue's tags afterwards, so a delete the server accepted and then ignored is reported rather than confirmed; the docs page for that sub-resource 404s, so its inferred shape is recorded in a comment next to the function the way the vcsIntegrationSettings functions record theirs. inspect_issue selects tags(id,name), which is what makes every write verifiable.

The CLI gains yt tag list (instance-wide) and yt issue tag list <ISSUE_ID>, both with --json and both paging fully, and issue tag add|remove are re-pointed onto the typed functions with --dry-run printing the endpoint, the byte-exact payload, and a separate resolved block. The MCP mirrors it as list_tags / list_issue_tags / add_issue_tag / remove_issue_tag, one verb per tool, with the two writes gated by dry_run through dispatch and the two reads rejecting it.

Breaking change: yt issue tag add no longer creates a tag that does not exist. --create (MCP: create: true) is the opt-in, and add_issue_tag's tool description says so, so a model calling it does not repeat the footgun.

#YT-90

Tags were the one issue attribute with no typed code path. Both `yt issue tag add` and `remove` were thin wrappers over the command language (`tag <name>` / `remove tag <name>`), so a write took a name and never an id, a typo silently created a new instance-wide tag that the CLI has no way to delete, and there was no read path at all: `inspect_issue` never selected `tags`, so neither `yt issue inspect` nor the MCP `get_issue` could report what an issue carried, and nothing could enumerate the tags that exist. `youtrack_client::api` now owns the surface: `list_tags` and `list_issue_tags` read `/api/tags` and `/api/issues/<id>/tags` through `fetch_all_pages`, `create_tag` posts `/api/tags`, and the write is split from its resolution the way `project create` and `project fields add` are: `plan_add_issue_tag` returns an `AddTagPlan` carrying the resolved tag id and the byte-exact body that `add_issue_tag` posts. A name matching several visible tags is refused naming every candidate id; a name matching nothing is refused with the near matches unless a create was authorized. `remove_issue_tag` sends `DELETE /api/issues/<id>/tags/<tagId>` and re-reads the issue's tags afterwards, so a delete the server accepted and then ignored is reported rather than confirmed; the docs page for that sub-resource 404s, so its inferred shape is recorded in a comment next to the function the way the `vcsIntegrationSettings` functions record theirs. `inspect_issue` selects `tags(id,name)`, which is what makes every write verifiable. The CLI gains `yt tag list` (instance-wide) and `yt issue tag list <ISSUE_ID>`, both with `--json` and both paging fully, and `issue tag add|remove` are re-pointed onto the typed functions with `--dry-run` printing the endpoint, the byte-exact payload, and a separate `resolved` block. The MCP mirrors it as `list_tags` / `list_issue_tags` / `add_issue_tag` / `remove_issue_tag`, one verb per tool, with the two writes gated by `dry_run` through `dispatch` and the two reads rejecting it. Breaking change: `yt issue tag add` no longer creates a tag that does not exist. `--create` (MCP: `create: true`) is the opt-in, and `add_issue_tag`'s tool description says so, so a model calling it does not repeat the footgun. #YT-90
feat(tags): typed issue tag surface
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 27s
Create release / Create release from merged PR (pull_request) Has been skipped
77c98b6bf3
Tags were the one issue attribute with no typed code path. Both `yt issue tag add` and `remove` were thin wrappers over the command language (`tag <name>` / `remove tag <name>`), so a write took a name and never an id, a typo silently created a new instance-wide tag that the CLI has no way to delete, and there was no read path at all: `inspect_issue` never selected `tags`, so neither `yt issue inspect` nor the MCP `get_issue` could report what an issue carried, and nothing could enumerate the tags that exist.

`youtrack_client::api` now owns the surface: `list_tags` and `list_issue_tags` read `/api/tags` and `/api/issues/<id>/tags` through `fetch_all_pages`, `create_tag` posts `/api/tags`, and the write is split from its resolution the way `project create` and `project fields add` are: `plan_add_issue_tag` returns an `AddTagPlan` carrying the resolved tag id and the byte-exact body that `add_issue_tag` posts. A name matching several visible tags is refused naming every candidate id; a name matching nothing is refused with the near matches unless a create was authorized. `remove_issue_tag` sends `DELETE /api/issues/<id>/tags/<tagId>` and re-reads the issue's tags afterwards, so a delete the server accepted and then ignored is reported rather than confirmed; the docs page for that sub-resource 404s, so its inferred shape is recorded in a comment next to the function the way the `vcsIntegrationSettings` functions record theirs. `inspect_issue` selects `tags(id,name)`, which is what makes every write verifiable.

The CLI gains `yt tag list` (instance-wide) and `yt issue tag list <ISSUE_ID>`, both with `--json` and both paging fully, and `issue tag add|remove` are re-pointed onto the typed functions with `--dry-run` printing the endpoint, the byte-exact payload, and a separate `resolved` block. The MCP mirrors it as `list_tags` / `list_issue_tags` / `add_issue_tag` / `remove_issue_tag`, one verb per tool, with the two writes gated by `dry_run` through `dispatch` and the two reads rejecting it.

Breaking change: `yt issue tag add` no longer creates a tag that does not exist. `--create` (MCP: `create: true`) is the opt-in, and `add_issue_tag`'s tool description says so, so a model calling it does not repeat the footgun.

#YT-90
Claude-Run deleted branch feat/YT-90-issue-tags 2026-08-05 20:52:03 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
pandoras-box/youtrack-cli!145
No description provided.