tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
# Templates: reusable issue and form definitions

For the create form itself (fields, pickers, targets), see the user guide at
[`for-users/new-issue.md`](for-users/new-issue.md); the agent-facing surface is
in [`mcp.md`](mcp.md).

A **template** is a reusable definition — a title, a body, labels, defaults,
structured prompts, and typed references — that resolves into a **normal create
request**. It is data stored in `tix.toml`: never a database, a hosted service,
executable code, or a second issue writer. Selecting a template is a shortcut to
a `tix create` that is otherwise exactly the one you already use.

Two features share the word "template". They do not share a key space:

| | Stable-ID registry (this document) | Legacy pre-fill shorthand |
|---|---|---|
| Config key | `[issue_templates.<id>]` | `[templates.<name>]` |
| Identity | immutable stable ID | display name |
| Scope, prompts, references, provenance | yes | no |
| Surfaces | `tix template …`, `tix create --template`, TUI picker, MCP | TUI picker (pre-fill only) |

The stable-ID registry is what `tix template` and `tix create --template`
operate on. The ID is the handle; a display name is only a label, so the two
registries can never confuse a name in one for an ID in the other.

## The registry

Templates live in `tix.toml` under `[issue_templates.<id>]`, keyed by the stable
ID. The section is **absent** when no template is declared, so an existing
`tix.toml` round-trips byte-identically until you add one. Writes go through the
same atomic config save every other `tix` setting uses.

```toml
[issue_templates.bug]
name = "Bug report"                 # mutable display label
# scope = "repository"              # the default (writes no key); or "board:<name>" / "team:<label>"

[issue_templates.bug.defaults]      # normal create fields only
title = "Bug report"
status = "todo"
priority = "high"
labels = ["bug"]

[[issue_templates.bug.prompts]]     # ordered structured prompts
key = "estimate"
kind = "text"
required = true

[[issue_templates.bug.references]]  # typed, validated before any write
kind = "label"
value = "bug"
required = true
```

A template is validated when `tix.toml` loads and again before any save. A
template that names an unknown board or team, a blank name/default, a prompt key
that is not a create field, a duplicate prompt key, or an unknown reference kind
is a load error naming the exact key (`issue_templates.<id>.<field>`), not a
silently ignored record.

### Identity: a stable ID and a display name

- The **ID** is the table key. It is immutable: ASCII letters, digits, `-` or
  `_`. It is what every surface selects, and it survives a rename.
- The **`name`** is a mutable display label. Renaming changes only the label, so
  scope, defaults, prompts, references, and preserved metadata are untouched.
- `tix template list` prints `id<TAB>name<TAB>scope`, one row per template in
  declaration order. `tix template show` accepts the ID first, and a display name
  only when it names exactly one template — an ambiguous name is refused rather
  than guessed.

### Scope: repository, board, or team

`scope` is an **explicit** selection with no inheritance:

| Spelling | Meaning |
|---|---|
| `repository` (default) | applies repository-wide |
| `board:<name>` | applies to the named board configuration |
| `team:<label>` | applies to the named team |

A `board:` or `team:` scope is checked against the configured boards/teams at
load; one that names something absent is a field error, because a template
pointed at a board that does not exist could never be selected. A scope with no
qualifier is refused. There is no parent, global, or user-identity inheritance:
selecting a template always uses exactly the scope you name, and the selected
scope is visible in the CLI output, the TUI picker, and MCP results.

### Defaults: normal create fields only

`[issue_templates.<id>.defaults]` pre-fills the fields the shared create core
accepts — `title`, `body`, `status`, `priority`, `assignee`, `labels`, `files`,
`estimate`, `cycles`, `milestones`, `reviewer`, `start_date`, `due_date`,
`parent`, `related_issues`, and `type`. An unknown key is a load error, because a
default the create model does not understand would be a second issue schema. A
blank value is refused for the same reason: a template that sets an empty value
sets nothing.

### Prompts: structured data

Each `[[issue_templates.<id>.prompts]]` entry is a named input field:

| Field | Meaning |
|---|---|
| `key` (required) | a create field; the answer is merged into the normal request |
| `label` | display text |
| `kind` | `text` (default) \| `select` \| `multi-select` \| `date` |
| `required` | the field must resolve to a value before an issue is written |
| `default` | a typed value merged before user input |
| `options_ref` | for `select`/`multi-select`: `statuses`, `priorities`, `labels`, `assignees`, `cycles`, or `milestones` |

A prompt key must be a create field and must not repeat another prompt's key; an
unknown `kind` or `options_ref` is a load error. A prompt is data — it cannot
carry a command, a script, or any runtime behavior.

### References: typed and validated

Each `[[issue_templates.<id>.references]]` entry declares a typed reference the
template needs:

| `kind` | `value` | Validated against |
|---|---|---|
| `label` | a label name | presence in the merged `labels` when `required` |
| `assignee` | an identity | presence in the merged `assignee` when `required` |
| `file` | a path | presence in the merged `files` when `required` |
| `cycle` | a cycle name | the planning registry |
| `milestone` | a milestone name | the planning registry |
| `relationship` | a relation kind | the closed `blocks`/`blocked_by`/`duplicates`/`related` vocabulary |

References name **existing** registries and model vocabulary; a template never
defines a second relationship schema. A referenced cycle or milestone that does
not exist, or a `relationship` value that is not a relation kind, is a
pre-write validation error with an actionable message.

### Unknown metadata: preserved, never interpreted

Any key under `[issue_templates.<id>]` the schema does not know is retained
verbatim in an extension area and round-trips through every load/save — including
a rename or an update. It is shown by `tix template show` under `unknown.<key>`
and by MCP as `extensions`, and it is never interpreted as a default, a prompt, a
reference, a command, or an issue field.

Note the asymmetry: unknown keys at the **template level** are preserved, while an
unknown key **inside** `defaults`/`prompts`/`references` is refused (those structs
deny unknown fields), because a typo there would silently change what the template
does.

## Resolution: one normal create request

Selecting a template resolves it with a **pure function** —
`resolve(template_id, scope, user_input)` — which reads the registry and returns
either one normal create request or the complete list of diagnostics. It never
reads stdin, launches an editor, mutates the registry, or writes an issue.

For every create field the resolved value is the first of:

1. the **user's input**, even when blank (an explicit override wins);
2. the field's **prompt `default`**, when a prompt declares one;
3. the template's **`defaults`** value.

List-valued fields (`labels`, `files`, `cycles`, `milestones`, `related_issues`)
are replaced wholesale rather than appended to. `title` is required by the normal
create model, so a template that neither defaults nor prompts for it still needs
you to supply one — a template path never prompts, because it has no terminal to
prompt on.

The resolved request is destructured straight into the shared
`commands::create::create` core — the same one `tix create`, the TUI form, and
MCP's `create_issue` use. The template ID and scope are **provenance**; they do
not add an issue field or a second writer.

### Failure is complete and writes nothing

Required prompts, the required title, invalid values (for example a priority
outside `''`/`low`/`medium`/`high`), and referenced labels/assignees/files/
cycles/milestones/relation kinds are all checked **before** any issue file is
written. A failed resolution returns **every** diagnostic — not just the first —
and creates no partial issue; the template and the store are left byte-unchanged.

Each diagnostic has a stable category and a field, so a script can branch without
parsing prose:

| Category | When |
|---|---|
| `unknown-template` | no template with the selected ID (a display name is diagnosed with the ID to use) |
| `invalid-scope` | a malformed scope, or one naming an unknown board/team |
| `scope-mismatch` | a well-formed scope that is not the template's own |
| `missing-required` | a required prompt, the title, or a required reference has no value |
| `unknown-reference` | a cycle/milestone/relation kind that does not exist |
| `invalid-value` | a provided value the create model refuses |

The CLI renders each as `kind: field: message`, under a headline naming the
template and scope. A submission that is missing several things lists them all,
one per line:

```text
$ tix create --template bug --labels bug
Error: template `bug` at scope `repository` did not resolve:
  - missing-required: estimate: required prompt `estimate` has no value
```

The create core's own validation (status column, planning references, dates,
parent) also runs before the file is written, so a refusal there likewise leaves
no partial issue. Exactly one issue file appears on success.

## CLI

### Managing templates

```text
tix template list
tix template show <id-or-unique-name>
tix template create --id <id> [--name <n>] [--scope <scope>] \
    [--default FIELD=VALUE]… [--prompt key=FIELD[,kind=…][,required=…][,default=…][,label=…][,options_ref=…]]… \
    [--reference kind=KIND,value=VALUE[,required=…]]… \
tix template update <ref> [--name <n>] [--scope <scope>] [--default …]… [--prompt …]… [--reference …]…
tix template duplicate <ref> [--id <new-id>] [--name <n>]
tix template delete <ref> [--yes]
```

- `list` and `show` are read-only. `show` prints a structured `key: value` block
  (`id`, `name`, `scope`, `defaults.<field>`, `prompt[i].…`, `reference[i].…`, and
  `unknown.<key>` for preserved metadata).
- `create` requires a new stable ID. An omitted `--name` shows the ID in its
  place; an omitted `--scope` is the documented `repository` default.
- `update` replaces **only** the parts you supply. `tix template update bug
  --name "Incident report"` is a rename: the ID, scope, defaults, prompts,
  references, and preserved metadata are untouched. Supplying any
  `--default`/`--prompt`/`--reference` replaces that whole block. A same-content
  update prints `unchanged template \`bug\`` and writes nothing.
- `duplicate` copies everything but the ID; without `--id` it allocates
  `<source>-copy`, then `-copy-2`, `-copy-3`, … deterministically. The source is
  untouched.
- `delete` removes the **definition** and nothing else — issues created from the
  template live in the issue tree and are never named here. It confirms with
  `--yes` (or a piped stdin); a bare TTY invocation asks.
- Every accepted mutation is one atomic save. A refused or failed operation
  writes nothing at all, and an unknown reference is refused rather than
  silently ignored.

### Creating from a template

```bash
tix create --template bug --priority high --labels bug
tix create --template boarded --scope board:default --title "Board issue"
```

`--scope` is only valid with `--template`, and an omitted scope is the explicit
`repository` default. The selected ID and scope are echoed on **stderr**
(`using template \`bug\` at scope \`repository\``) so `Created <file>` on stdout
keeps the shape scripts already parse. `tix create` without `--template` behaves
exactly as it did before.

## TUI

The create-issue form (**F1**, or `tix new`) has a **Template** row. Opening it
lists the stable-ID registry first, each entry labelled `id — name [scope]`, then
the legacy `[templates.<name>]` pre-fill names. Selecting one seeds the existing
`FormState` fields and pickers — it adds no second form state and changes no
text-input or keybinding semantics.

Submitting a form with a stable template selected resolves it through the same
pure resolver and hands the request to the same shared create core as the CLI, so
the created issue is equivalent for the same template, scope, and input. A failed
resolution shows every diagnostic in the status line and leaves the form and the
store untouched — no partial issue. The template ID and scope stay visible: on
the Template row before submit, and in the result message after.

Template **management** lives on the CLI (`tix template …`) and MCP; the TUI's
surface is selection.

## MCP

Six tools mirror the management commands — `list_templates`, `get_template`,
`create_template`, `update_template`, `duplicate_template`, and
`delete_template` — and `create_issue` takes `template` (and `scope`, only with
`template`). See [`mcp.md`](mcp.md#templates-the-stable-id-registry) for the
schemas. Points that matter:

- `list_templates`/`get_template` are read-only; their `content` text is the
  CLI's own `tix template list`/`show` output, byte for byte.
- A mutation returns a structured outcome with `status` of `accepted`, `no-op`,
  `rejected`, or `failed`. `rejected` and `failed` are `isError: true` **and**
  carry the same structured outcome, so a client branches on `status` rather than
  parsing the sentence. `delete_template` requires `confirm: true`.
- `create_issue(template=…)` resolves with the same resolver and creates through
  the same shared core; a resolution failure carries one diagnostic per missing
  required prompt or unknown reference and writes no issue file. On success the
  created issue carries `template: { id, scope }` as provenance. Without
  `template`, `create_issue` is unchanged.

## What templates are not

- **Not a hosted service.** The registry is a `tix.toml` section.
- **Not executable.** Defaults, prompts, and references are data; no template
  can introduce a command or runtime behavior.
- **Not a second issue writer.** Every template-aware create path calls the
  shared create core; the resolver itself cannot write.
- **Not inheritance.** Scope is exactly what the template declares; there is no
  parent, global, or user-identity scope.
- **Not a second issue schema.** A template can only pre-fill the documented
  create fields; a field the create model does not understand is refused.