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: "link_issue",
            description: "Create a TYPED relationship between two issues (`tix link --type`). **Breaking change (C-001): the untype symmetric form is gone and `kind` is REQUIRED** -- there is no default kind, so an untyped relationship is never created by accident. Both issues must exist; linking an issue to itself errors. `blocks` is the stored form of a blocking relationship: `blocked_by` is an accepted input spelling that normalizes into the equivalent `blocks` edge, and a reciprocal pair is rejected as a cycle.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "a": { "type": ["string", "integer"], "description": "Source issue: number, or a filename/title fragment." },
                    "b": { "type": ["string", "integer"], "description": "Target issue: number, or a filename/title fragment." },
                    "kind": {
                        "type": "string",
                        "enum": ["blocks", "blocked_by", "duplicates", "related"],
                        "description": "REQUIRED relation kind. The vocabulary is closed -- an unknown kind is rejected, never coerced. `blocks`: the source blocks the target (the stored form of a blocking relationship). `blocked_by`: input-only spelling, normalizes into the equivalent `blocks` edge with the endpoints swapped. `duplicates`: directional, not reordered. `related`: explicit non-directional association, stored once and never mirrored."
                    }
                },
                "required": ["a", "b", "kind"]
            }),
        },
        ToolDescriptor {
            name: "list_relationships",
            description: "List typed relationships for one issue, or for the whole board (`tix link list`). Ordered by (kind, target), so the output is byte-comparable across runs and surfaces. A relationship is stored once in its canonical source's frontmatter, so this reports every relationship *touching* an issue -- including ones stored on the other endpoint.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Restrict to relationships touching this issue. Omit for every issue on the board." },
                    "kind": {
                        "type": "string",
                        "enum": ["blocks", "blocked_by", "duplicates", "related"],
                        "description": "Only relationships of this kind. `blocked_by` filters on the CANONICAL blocking edge, i.e. edges whose stored form is `blocks` -- no stored edge ever carries `blocked_by`, so filtering on it literally would match nothing."
                    }
                }
            }),
        },
        ToolDescriptor {
            name: "edit_relationship",
            description: "Retarget and/or re-kind an existing relationship by its stable id (`tix link edit`). The id SURVIVES the edit, so a relationship referenced elsewhere does not change identity. An edit naming its own current target and kind is the identity and writes nothing.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "id": { "type": "string", "description": "Relationship id, as returned by list_relationships." },
                    "to": { "type": ["string", "integer"], "description": "New target issue. Omit to keep the current one." },
                    "kind": {
                        "type": "string",
                        "enum": ["blocks", "blocked_by", "duplicates", "related"],
                        "description": "New relation kind. Omit to keep the current one. At least one of `to` or `kind` is required."
                    }
                },
                "required": ["id"]
            }),
        },
        ToolDescriptor {
            name: "remove_relationship",
            description: "Remove one relationship by its stable id (`tix link remove`). An id that is not stored is an IDEMPOTENT NO-OP: no file is written and no History entry is appended, and the call still succeeds, so it is safe to retry.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "id": { "type": "string", "description": "Relationship id, as returned by list_relationships." }
                },
                "required": ["id"]
            }),
        },
        ToolDescriptor {
            name: "issue_chain",
            description: "The transitive dependency chain an issue holds up, walking OUTBOUND `blocks` edges (`tix chain`). Nodes carry their issue number and display name so an agent can address them, and the order is the service's, unmodified. `truncated` is set when a traversal bound cut the result.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Root issue: number, or a filename/title fragment." }
                },
                "required": ["issue"]
            }),
        },
        ToolDescriptor {
            name: "issue_blocked_state",
            description: "Whether an issue has open blockers, which ones, and why (`tix list --show relationships`). Derived on read from the relationship service and never stored. An inbound `blocks` edge counts only if the blocker exists, is a different issue, and its status is not the board's configured done_column. An issue that is merely `related` to a blocker is NOT blocked, and neither is one whose blocker has reached the completion column. `reason` is the exact text the move warning and the detail view print.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Issue: number, or a filename/title fragment." }
                },
                "required": ["issue"]
            }),
        },
        ToolDescriptor {
            name: "list_children",
            description: "List an issue's direct children, in the same order `tix parent children` prints them. Direct children only — never grandchildren. Returns the count, the ordered `(number, display)` pairs, and the exact text the CLI prints.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." }
                },
                "required": ["issue"]
            }),
        },
        ToolDescriptor {
            name: "get_parent",
            description: "Read an issue's parent. Returns the resolved parent's canonical id in `parent`, its number alias in `parent_number`, and the shared display. When the recorded parent no longer exists, `recorded_parent` and `recorded_parent_number` retain the reference and `unresolved.reason` carries the same text the CLI shows. An issue with no parent reports `parent: null` and no `unresolved` block.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": false,
                "properties": {
                    "issue": { "type": ["string", "integer"], "description": "Number, or a filename/title fragment." }
                },
                "required": ["issue"]
            }),
        },
    ]
}