tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
# Saved issue views

A **saved view** is a named, repository-owned query definition: a filter, an
optional text query, a sort key, a group key, and a board-visibility snapshot,
plus explicit scope metadata. It is data, not code — applying one runs the same
`StructuredFilter` and board-rebuild path the live filter uses, and never
introduces a second query grammar.

Saved views live in `tix.toml` under `[views.<view-id>]`, beside `[cycles]`,
`[teams]`, and `[kanban]`. There is **no database and no hosted view service**;
the definitions are plain Markdown-adjacent configuration you can review and
hand-edit.

```toml
[views.v-1f0c9a3d5b7e2411]
id = "v-1f0c9a3d5b7e2411"
name = "Platform active"

[views.v-1f0c9a3d5b7e2411.scope]
kind = "project"
project_id = "p-platform"

[views.v-1f0c9a3d5b7e2411.plan]
sort = "due"
group = "cycle"

[views.v-1f0c9a3d5b7e2411.plan.filter]
status = "in progress"
```

## Identity: ids are stable, names are labels

The `v-…` **id is the identity**. It is generated once, never changes, and is
never reused after a delete. The `name` is a display label: renaming a view
changes nothing else about it. Every command accepts either the stable id or the
display name, but a name is resolved only when it is unambiguous — see *Scope and
conflicts* below.

## The query plan

A view's plan uses the **existing** grammar only (C-002/C-004):

- `--filter` — the same token query `tix list --filter` accepts
  (`status:todo`, `label:urgent`, `category:done`, `parent:3`, …);
- `--sort` — `created`, `due`, `start`, `reviewer`, `cycle`, `milestone`,
  `status`, or `priority`;
- `--group` — `cycle` or `milestone`;
- `--hide-columns` and `--show-mismatch` — the board's own visibility settings.

Plan fields are normalized on the way in (text trimmed, empty values dropped,
column lists de-duplicated) and **validated before anything is written**. An
unsupported sort key, an unknown filter criterion, or a column the board does not
have is refused with a field diagnostic, and the refusal writes no `tix.toml`.

Omitting both visibility flags means the plan *inherits* the live board's
visibility; giving either makes visibility an absolute snapshot that replaces the
live board's hidden columns on apply.

Unknown keys inside `[plan]`, `[plan.filter]`, or `[plan.visibility]` written by
a newer `tix` are preserved verbatim and reported as `unknown_metadata`
diagnostics — never silently dropped (NFR-002).

## Scope and conflicts

`scope.kind` is one of three explicit families:

| kind | requires | meaning |
| --- | --- | --- |
| `repository` (default) | — | visible to the whole repository |
| `project` | a `project_id` | scoped to one project |
| `user_share` | `owner` and/or `shared_with` | shared-with metadata |

`user_share` scope is **metadata only**. It is not authentication, it grants no
permission, it creates no account, and it is never matched against an ambient
"current user". Host identity, permissions, and hosted sharing are escalation
boundaries, not features.

When a view is addressed by **name** and more than one view has that name, the
most specific explicit scope wins: `user_share` over `project` over
`repository`. A tie at the winning rank is an explicit `scope_conflict`
diagnostic — `tix` never guesses. Address the view by its stable id instead. A
project scope whose project is unknown is *missing*, not corrupt: the view loads,
selection is refused, and the write path refuses to persist it.

## CLI

```text
tix view list [--scope repository|project|user]   # tab-separated: id, name, scope, resolution
tix view show <id|name>                           # id, name, scope, normalized plan, diagnostics
tix view create --name "Active" [--filter Q] [--sort K] [--group K]
                [--hide-columns LIST] [--show-mismatch true|false]
                [--scope repository|project|user] [--project ID]
                [--owner IDENTITY] [--shared-with LIST]
tix view update <id|name> [--name NEW] [plan flags]   # merge: only named fields change
tix view duplicate <id|name> [--name COPY]
tix view delete <id|name> --confirm                   # definition only — see below
tix view apply <id|name>                              # read-only; prints plan, result, marker
tix view status [<id|name>] [plan flags]              # divergence against a live plan
tix view share <id|name> --scope ...                  # replace scope metadata only
```

`tix view create` prints `Created <id>` on the first line so a script can capture
the stable id. `tix view <issue>` (an issue selector) still prints the issue
file, so the saved-view subcommands and the issue printer share one command
without colliding.

`tix view apply` is **read-only**: it resolves the view, installs an in-memory
active marker, rebuilds the board through the shared filter path, and prints the
plan and the resulting issue numbers. It writes no issue, comment, relationship,
or configuration byte.

## TUI

- **v** opens the saved-views list; the active view is marked.
- **Enter** applies the highlighted view to the live board.
- **n** saves the live board's current plan as a new view.
- **e** renames the highlighted view; **u** updates it from the live plan.
- **d** duplicates it; **x** then **y** deletes the definition (with
  confirmation).
- **q / Esc** closes the list.

The applied plan overlays the live board's filter/sort/group/visibility without
writing anything, and the board's indicator row shows the active view and whether
the live board is still in sync with it.

## MCP

The view tools mirror the CLI over one normalized plan/result DTO:

| tool | purpose |
| --- | --- |
| `view_list` | every view in declaration order, with plan, extensions, diagnostics |
| `view_show` | one view's id, scope, normalized plan, unknown metadata, diagnostics |
| `view_create` / `view_update` / `view_duplicate` | CRUD; `view_update` merges named fields |
| `view_delete` | definition only, and only with `confirm: true` |
| `view_share` | replace scope metadata |
| `view_apply` | read-only apply; returns plan, active marker, issue set, board summary |
| `view_status` | structured divergence between a saved plan and a live plan |

`view_show`'s text output is `tix view show`'s stdout byte for byte, and
`view_apply` returns the same normalized plan and issue set the CLI does (NFR-003).

## Portable output

`tix export --target views` and the portable document (`tix.saved-views`,
version 1) project the registry into one loss-aware JSON form: stable ids, names,
scope metadata, the raw `[plan]` table and its normalized form, the active
marker, divergence, preserved extensions, and diagnostics. Unknown record, scope,
plan, and document keys round-trip verbatim; `document → JSON → document → JSON`
is byte-identical. This surface is **output-only**: `tix import` accepts
`--target gitlab|linear` and there is no `views` import path, so the JSON is for
reading and external tooling, not a second write route into the registry. Remote
export targets are kept in a **separate** namespace and are never read as view
data; keys that name executable or query-payload content are retained and
reported as uninterpreted — a view is data, not code.

## Divergence

An **active view marker** records which saved definition the live board was last
associated with (the stable id, the normalized plan, the surface, and an optional
timestamp). It is informational provenance: it grants nothing and never
authorizes anything.

**Divergence** compares the live normalized plan against the active view's saved
definition, field by field, and reports the differences in a fixed order with an
`in_sync` summary. It is **read-only**: a board that diverges is reported, never
silently reconciled. A missing marker or a deleted active view is an explicit
diagnostic, not an implicit "in sync".

## Deletion safety

`tix view delete <id|name> --confirm` removes **only the saved definition**. It
never deletes an issue, comment, relationship, or board datum; `--confirm` (and
`confirm: true` over MCP, and `y` in the TUI) is required so the removal is
deliberate. If the deleted view was the active one, the marker is cleared and the
outcome says so. A delete of an unknown reference is refused and writes nothing.

## Escalation boundaries

The following are **out of scope by contract**, and a request for one is an
escalation rather than something to approximate:

- a hosted view service, database, or remote view registry;
- host account identity, permissions, or real hosted sharing (user/share scope
  stays metadata-only);
- arbitrary executable query code or a new query language — the existing
  `StructuredFilter`/board grammar is authoritative;
- dropping unknown metadata on a rewrite, or destructive deletion that also
  removes issues or comments.