tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

#[allow(clippy::too_many_lines)]
pub(super) fn descriptors() -> Vec<ToolDescriptor> {
    vec![
        ToolDescriptor {
            name: "create_issue",
            description: "Create a new issue. Returns the created issue (its number, filename, and fields) exactly as `tix create` would write it. With `template`, the stable template's defaults and prompts are merged with the fields by the shared resolver and the resolved request goes through the same create core (`tix create --template`), with the selected ID and scope returned as provenance.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "title": { "type": "string", "description": "Non-empty title (required unless `template` is set; no TTY prompt fallback)." },
                    "status": { "type": "string", "description": "Column; default the first configured column." },
                    "priority": { "type": "string", "enum": ["low", "medium", "high"], "description": "Default medium." },
                    "assignee": { "type": "string", "description": "Handle (`alice`/`@alice`/`@me`) or full identity; default git author." },
                    "labels": { "type": "string", "description": "Comma-separated labels." },
                    "files": { "type": "string", "description": "Comma-separated spec file paths." },
                    "related_issues": { "type": "string", "description": "Comma-separated related-issue references: a local number/selector, a relative or absolute path to another issue file, or a repository URL (`host/path#id`). Stored verbatim." },
                    "type": { "type": "string", "description": "Issue type (e.g. `bug`, `feature`); `tix lint` checks it against `custom_types`." },
                    "estimate": { "type": "string", "description": "Free text: `3`, `3d`, `1 day`, `0.5-1 day`, `3 weeks 2 hours`. Prose unit words fold to the board symbols; compound terms are converted through the board's declared units; fractions and ranges are pessimistic (an upper bound, rounded up); unreadable comma-separated annotations are ignored. A value with no readable segment is kept but contributes nothing to calculations." },
                    "cycles": { "type": "string", "description": "Comma-separated cycle names." },
                    "milestones": { "type": "string", "description": "Comma-separated milestone names." },
                    "projects": { "type": "string", "description": "Comma-separated project references to associate the new issue with, mirroring `tix create --projects`. A project is selected by its **stable id**; a name is a convenience that resolves to the same project (identity survives a rename). A value **sets the full association set** for the new issue; duplicates collapse to one association. Every reference is resolved through the project service **before** the issue is written, so an unknown reference refuses the whole create with `unknown_project` and nothing is written. Not accepted beside `template`, exactly as the CLI refuses the combination." },
                    "epics": { "type": "string", "description": "Comma-separated epic references to associate the new issue with, mirroring `tix create --epics`. An epic is selected by its **stable id**; a name is a convenience that resolves to the same epic (identity survives a rename). A value **sets the full reference set** for the new issue, and duplicates collapse to one reference. Every reference is resolved through the epic service **before** the issue is written, so an unknown reference refuses the whole create with `unknown_epic` and nothing is written. Referencing an epic never turns the issue into a task entity: it stays an ordinary issue (C-001). Not accepted beside `template`, exactly as the CLI refuses the combination." },
                    "reviewer": { "type": "string", "description": "Free-form reviewer name/identity." },
                    "start_date": { "type": "string", "description": "`YYYY-MM-DD`." },
                    "due_date": { "type": "string", "description": "`YYYY-MM-DD`; must be `>= start_date`." },
                    "parent": { "type": "string", "description": "Parent issue selector — number, filename, or unique title fragment (as everywhere a selector is taken). An empty string clears an existing parent; omitted leaves it alone." },
                    "body": { "type": "string", "description": "Markdown body." },
                    "template": { "type": "string", "description": "Create from a stable template ID (`list_templates`). The template's defaults and prompts are merged with the fields above by the pure resolver, then the resolved request goes through the same shared create core — there is no second writer. Resolution never prompts: a missing required prompt is a structured diagnostic, no issue is written, and the template is left untouched." },
                    "scope": { "type": "string", "description": "Template scope: `repository` (the default) | `board:<name>` | `team:<label>`. Only meaningful with `template` (a `scope` without one is refused). The selected template ID and scope come back as provenance on the created issue." }
                },
                "anyOf": [
                    { "required": ["title"] },
                    { "required": ["template"] }
                ]
            }),
        },
        ToolDescriptor {
            name: "list_issues",
            description: "List issues as the board table. Filter by column, or restrict to one project's members.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "status": { "type": "string", "description": "Column to filter by." },
                    "project": { "type": "string", "description": "Restrict the listing to one project's members, resolved by its **stable id** (a name resolves to the same project). The matching set is the project service's derived membership, the same one `project_membership` returns, so the filter and the membership read can never disagree." },
                    "epic": { "type": "string", "description": "Restrict the listing to one epic's members, resolved by its **stable id** (a name resolves to the same epic). The matching set is the epic service's derived membership restricted to the **active** set the listing reads; `epic_membership` returns that same service membership over **every** issue file, so the two agree except for archived members (which the listing does not read)." },
                    "filter": { "type": "string", "description": "Token query (`cycle:`, `milestone:`, `reviewer:`, `overdue`, etc.)." },
                    "sort": { "type": "string", "description": "Sort key (`created`, `due`, `start`, `reviewer`, `cycle`, `milestone`, `status`, `priority`)." },
                    "group": { "type": "string", "description": "Group key (`cycle` | `milestone`)." },
                    "all": { "type": "boolean", "description": "List every issue, ignoring the default scope — your issues (the git author identity), in the board's entry column. The CLI spelling is `tix list --all`, and the two render the same listing." }
                }
            }),
        },
        ToolDescriptor {
            name: "get_issue",
            description: "Return the full issue (frontmatter + body) for a selector.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." }
                },
                "required": ["issue"]
            }),
        },
        ToolDescriptor {
            name: "board_status",
            description: "Column counts plus the total (`tix status`). `tix status --json` prints the same document.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "filter": { "type": "string", "description": "Token query (`cycle:`, `milestone:`, `reviewer:`, `overdue`, etc.)." },
                    "sort": { "type": "string", "description": "Sort key (`created`, `due`, `start`, `reviewer`, `cycle`, `milestone`, `status`, `priority`)." },
                    "group": { "type": "string", "description": "Group key (`cycle` | `milestone`)." },
                    "health": { "type": "boolean", "description": "Add the health summary (`tix status --health`): issue counts per state, the policy version, and the diagnostic count." }
                }
            }),
        },
        ToolDescriptor {
            name: "update_issue",
            description: "Merge fields into an issue (CLI `tix edit` flag path only; `$EDITOR` is never opened). At least one optional field must be provided.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." },
                    "title": { "type": "string" },
                    "body": { "type": "string" },
                    "status": { "type": "string", "description": "Set the status. This is a MOVE, not a field write: it is validated by the same transition service as `tix move` and recorded as the same History decision, so it is not a way around the policy. `\"\"` clears the field (matching every other field here) and is a plain field write, not a transition, because there is no target column to judge." },
                    "priority": { "type": "string", "enum": ["low", "medium", "high"] },
                    "assignee": { "type": "string" },
                    "labels": { "type": "string", "description": "Comma-separated." },
                    "files": { "type": "string", "description": "Comma-separated spec file paths." },
                    "related_issues": { "type": "string", "description": "Comma-separated related-issue references: a local number/selector, a relative or absolute path to another issue file, or a repository URL (`host/path#id`). Stored verbatim." },
                    "type": { "type": "string", "description": "Issue type (e.g. `bug`, `feature`); `tix lint` checks it against `custom_types`." },
                    "estimate": { "type": "string", "description": "Free text: `3`, `3d`, `1 day`, `0.5-1 day`, `3 weeks 2 hours`. Prose unit words fold to the board symbols; compound terms are converted through the board's declared units; fractions and ranges are pessimistic (an upper bound, rounded up); unreadable comma-separated annotations are ignored. A value with no readable segment is kept but contributes nothing to calculations." },
                    "cycles": { "type": "string", "description": "Comma-separated cycle names (`\"\"` clears)." },
                    "milestones": { "type": "string", "description": "Comma-separated milestone names (`\"\"` clears)." },
                    "projects": { "type": "string", "description": "Project associations, with exactly the semantics of `tix edit --projects`: this argument **only clears**. A blank value (`\"\"`) drops every association; a non-blank value is refused, because setting associations has one spelling here (`add_project`, `remove_project`) so a typo cannot silently replace the set." },
                    "add_project": { "type": "array", "items": { "type": "string" }, "description": "Project references to **associate** the issue with (mirrors `tix edit --add-project`). Each entry is one CLI occurrence, taken **verbatim** — an entry is resolved by **stable id** first, with a name as a convenience that resolves to the same project; entries are neither trimmed nor comma-split (only `projects` is a comma-separated list, matching `--projects`). Idempotent: an association already present is a no-op that appends no History entry. Resolved through the project service before any write, so an unknown reference (including a padded or blank one — exactly as `--add-project \"\"` is refused by the CLI) refuses the whole update with `unknown_project` and leaves every file untouched; no file is written when nothing changed. An empty array contributes no reference." },
                    "remove_project": { "type": "array", "items": { "type": "string" }, "description": "Project references to **dissociate** from the issue (mirrors `tix edit --remove-project`), each entry taken **verbatim** as one CLI occurrence. Drops only the association — never the issue. An entry that resolves names that project; one that does not resolve must be an id the issue already carries (removing a stale reference is the repair path); an entry that resolves to nothing and is not stored is refused with `unknown_project`. Idempotent when the association is absent. An empty array contributes no reference." },
                    "epics": { "type": "string", "description": "Epic references, with exactly the semantics of `tix edit --epics`: this argument **only clears**. A blank value (`\"\"`) drops every reference; a non-blank value is refused, because setting references has one spelling here (`add_epic`, `remove_epic`) so a typo cannot silently replace the set." },
                    "add_epic": { "type": "array", "items": { "type": "string" }, "description": "Epic references to **add** to the issue (mirrors `tix edit --add-epic`). Each entry is one CLI occurrence, taken **verbatim** — an entry is resolved by **stable id** first, with a name as a convenience that resolves to the same epic; entries are neither trimmed nor comma-split (only `epics` is a comma-separated list, matching `--epics`). Idempotent: a reference already present is a no-op that appends no History entry. Resolved through the epic service before any write, so an unknown reference refuses the whole update with `unknown_epic` and nothing is written." },
                    "remove_epic": { "type": "array", "items": { "type": "string" }, "description": "Epic references to **remove** from the issue (mirrors `tix edit --remove-epic`), each entry taken **verbatim** as one CLI occurrence. Drops only the reference — never the issue. An entry that resolves names that epic; one that does not resolve must be an id the issue already carries (removing a stale reference is the repair path); an entry that resolves to nothing and is not stored is refused with `unknown_epic`. Idempotent when the reference is absent. An empty array contributes no reference." },
                    "reviewer": { "type": "string", "description": "Free-form reviewer name/identity (`\"\"` clears)." },
                    "start_date": { "type": "string", "description": "`YYYY-MM-DD` (`\"\"` clears)." },
                    "due_date": { "type": "string", "description": "`YYYY-MM-DD`; must be `>= start_date` (`\"\"` clears)." },
                    "parent": { "type": "string", "description": "Parent issue selector — number, filename, or unique title fragment (as everywhere a selector is taken). `\"\"` clears the parent, naming no issue; omitted leaves it unchanged." },
                    "commit": { "type": "string", "description": "Record a commit on the issue; `\"clear\"` removes it." },
                    "reason": { "type": "string", "description": "Why a transition policy is being overridden by `status`. Required to move an issue OUT of a terminal category (`completed` or `canceled`) on a board that declares a `category_map`; without it the update is refused with `invalid_transition`. A blank value is not a reason and is refused. The reason is recorded in the same History decision the CLI records. Mirrors the CLI's `tix edit --reason`." }
                },
                "required": ["issue"]
            }),
        },
        ToolDescriptor {
            name: "move_issue",
            description: "Move an issue to a configured column (CLI `tix move`). A commit is recorded only when passed; never prompts. Moving toward the board's done column with open blockers is refused unless `yes` is set, and records the acknowledgement in the issue History. Validated by the same transition service **and the same work-in-progress policy** as the CLI and the TUI: a move that would exceed a column's `block` limit is refused with the policy's own diagnostics unless `override` authors it with `reason` and `yes`, and a `warn` limit proceeds with the decision reported in `wip`. A refusal returns the same named diagnostic on every surface.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." },
                    "column": { "type": "string", "description": "A configured column." },
                    "commit": { "type": "string", "description": "Optional commit to record when the move reaches the board's completion column. `auto` resolves it from git history, as the CLI does." },
                    "reason": { "type": "string", "description": "Why a transition policy is being overridden. Required to move an issue OUT of a terminal category (`completed` or `canceled`) on a board that declares a `category_map`; without it the move is refused with `invalid_transition`. A blank value is not a reason and is refused. The reason is recorded in the issue's History. Mirrors the CLI's `tix move --reason`. A board with no `category_map` has no transition policy to override." },
                    "yes": { "type": "boolean", "description": "Acknowledge open blockers and proceed. Without it a move toward the done column with open blockers is refused. Mirrors the CLI's `tix move --yes`." },
                    "override": { "type": "boolean", "description": "Authorize overriding a **blocking work-in-progress limit**. Not an authorization on its own: the move proceeds only with `override: true` AND a `reason` AND `yes: true`, and the override is recorded as one History entry on the issue (actor, timestamp, reason and the limit it was authorized against). A `warn` limit never needs one; a move within every limit does not need one either." }
                },
                "required": ["issue", "column"]
            }),
        },
        ToolDescriptor {
            name: "delete_issue",
            description: "Delete an issue file. Irreversible; requires confirm: true.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." },
                    "confirm": { "type": "boolean", "description": "Must be true to delete." }
                },
                "required": ["issue", "confirm"]
            }),
        },
        ToolDescriptor {
            name: "search_issues",
            description: "Case-insensitive substring search over the full issue markdown (frontmatter + body + comments), same corpus and matcher as `tix search`.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "query": { "type": "string" }
                },
                "required": ["query"]
            }),
        },
        ToolDescriptor {
            name: "comment_on_issue",
            description: "Append a comment to an issue's `## Comments` section via the model writer. Whitespace-only text is a no-op.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." },
                    "author": { "type": "string" },
                    "text": { "type": "string" }
                },
                "required": ["issue", "author", "text"]
            }),
        },
        ToolDescriptor {
            name: "lint_issues",
            description: "Validate frontmatter values in every issue file (same checks as `tix lint`); never crashes on malformed files. Also lints the foreign repositories reachable from `related_issues` unless `foreign` is false.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "foreign": { "type": "boolean", "description": "Also lint related repositories and merge their findings. Default true; `false` mirrors `tix lint --no-foreign`." }
                }
            }),
        },
    ]
}