tk - System & Data Specification¶
Specification Lifecycle
Architecture changes, format updates, and new features follow a spec-driven development lifecycle.
1. System Goals & Philosophy¶
tk is a dependency-aware, Git-friendly task queue for humans and coding agents.
Scope¶
tk core is a minimal task queue that many agents and humans can share through plain files. It provides:
- Plain-text tickets (
.tickets/*.md) that version and merge through Git; Git itself is optional. - DAG dependencies with cycle detection, and a deterministic ready queue (
tk ready). - Atomic claiming (
tk start --claim) and per-ticket locks for concurrent agents. - Post-write hooks (
.tickets/.hooks/post-write.d/) as the single extension point for reacting to changes.
Everything else lives in optional plugins, discovered on $PATH as tk-<cmd> (see Section 5): the Web UI (tk webui), GitHub import (tk github), and agent dispatch (tk scion-taskforce, an event-driven bridge from ticket hooks to Scion, which runs the agents).
Core Principles¶
- Zero Database: State is stored locally in
.tickets/*.md. - Plain-Text Transparency: Every ticket is a Markdown file with YAML frontmatter.
- DAG-Driven: Native dependency tracking (
deps) enables topological sorting, cycle detection, and ready-queue computation. - Unix Portability: Core CLI is a single portable Bash script (Bash 3.2+, macOS and Linux) using standard tools (
awk,sed,grep,find,mkdir); no other runtime required. It is Bash, not POSIXsh. - Decoupled Extensions: Web UI and external integrations operate as modular plugins.
Non-goals¶
Core will not:
- Use a database, server, or background daemon. Hooks run once per write and exit.
- Access the network. Sync with GitHub or other services belongs to plugins.
- Orchestrate agents (spawning, scheduling, assigning, or supervising workers). Core supplies the shared queue, claims, and hooks; orchestration belongs to plugins and the tools they drive.
- Require a runtime beyond Bash and standard Unix tools.
jqis optional and only needed fortk query <filter>; Python is used only by plugins. - Require Git. Tickets are plain files; Git is the recommended way to share and review them.
- Render a UI. The terminal output is plain text; visual views belong to
tk webui.
2. Data Model & Ticket Schema¶
2.1 File Storage & Naming¶
- Directory:
.tickets/(located in the repository root or discovered upwards in parent directories). - File Naming:
<prefix>-<hash>.md(e.g.,tic-6mex2q.md,msa-y8zi0k.md). - Prefix defaults to 3 characters derived from the folder name or random alphanumeric. Only ASCII letters, digits and
.are kept; a folder name with none falls back totk. - Hash is a random lowercase alphanumeric string from
/dev/urandom(fallback:$RANDOM), 6 characters by default (TK_ID_HASH_LENGTH, 4-32). Lowercase keeps IDs distinct on case-insensitive filesystems. - Generation retries if
<id>.mdalready exists locally. Across branches, 6 characters (36^6 ≈ 2.2 × 10^9 values) keep the collision chance for 1,000 tickets near 0.02%, versus about 25% with 4 characters. - Older 4-character IDs (e.g.,
tic-6mex) remain valid; exact and partial ID resolution work for both lengths.
2.2 YAML Frontmatter Schema¶
Each ticket file begins with a YAML header delimited by ---:
---
id: string # Unique ticket identifier, e.g. tic-6mex2q (Required)
status: enum # open | in_progress | closed (Required)
deps: list[string] # Array of ticket IDs this ticket depends on (Required)
links: list[string] # Array of related ticket IDs (symmetric) (Optional)
created: ISO-8601 # Timestamp of ticket creation (e.g. 2026-09-25T10:01:06Z) (Required)
type: enum # bug | feature | task | epic | chore (Default: task)
priority: integer # 0 (Critical) to 4 (Trivial) (Default: 2)
assignee: string # Assignee name or git username (Optional)
external-ref: string # External issue/PR reference, e.g. gh-123 (Optional)
parent: string # Parent ticket ID if this ticket is a subtask (Optional)
tags: list[string] # Array of tag strings (Optional)
---
The external reference key is external-ref (hyphen). Readers should also accept the legacy external_ref spelling.
2.3 Markdown Body Structure¶
Following the frontmatter, the Markdown body contains (sections are omitted when empty):
# <Title>
<Description text...>
## Design
<Architecture or design thoughts...>
## Acceptance Criteria
<Criteria...>
## Notes
**2026-09-25T10:30:00Z**
<Note text appended by tk add-note>
tk add-note creates the ## Notes section on first use and appends each note as a bold UTC timestamp line, a blank line, then the note text.
2.4 Field Values¶
- Verbatim: values are stored exactly as given, including
/,&,\and|. - Quoting: a scalar is YAML-quoted (single quotes) only when plain YAML would read it differently: a leading indicator character (
[,],{,},#,&,*,!,|,>,',",%,@, backtick,,), a leading-,?or:that stands alone or is followed by a space, an embedded:or#, a trailing:, or leading/trailing spaces. Other values are written unchanged. - Single line: frontmatter values must not contain a newline or carriage return.
tkrejects them withError: <field> must be a single line (newlines are not allowed)and exit code1; the value is not written. The title, description, design, acceptance and notes live in the body and may span lines. - Write failures: if a ticket file cannot be written (read-only directory, invalid path),
tkprintsError: cannot write ticket file '<path>', exits1, prints no ID, and fires no hooks.
2.5 Repository Files¶
| Path | Purpose | In Git |
|---|---|---|
.tickets/*.md |
Tickets | Yes |
.tickets/.gitignore |
Written by tk init: .locks/ and .hooks/hooks.log (missing lines appended only) |
Yes |
.tickets/.locks/ |
Per-ticket lock directories (Section 4.2); contains a self-ignoring .gitignore |
No |
.tickets/.hooks/post-write.d/ |
Executable post-write hooks (Section 5.1) | Optional |
.tickets/.hooks/hooks.log |
Hook output | No |
~/.local/state/tk/projects.json |
Projects registered by tk init, used by the Web UI project list |
Outside the repo |
3. Dependency DAG Semantics¶
3.1 Directed Acyclic Graph (DAG) Rules¶
- A dependency
A -> B(deps: [B]) means Ticket A is blocked by Ticket B. - Ticket A cannot be resolved or considered "Ready" until Ticket B is in
closedstatus. - Symmetric links (
links: [B]) denote relational associations without blocking semantics. - Write-time cycle check:
tk dep A Bis rejected whenA == Bor whenAis already reachable fromBthroughdeps. The check walks all tickets, including closed ones (reopening a closed ticket would otherwise revive the cycle). It printsError: A -> B would create a cycle: A -> B -> ... -> A, exits1, writes nothing, and fires no hook. Re-adding an existing dependency remains a no-op.
3.2 State Queries¶
- Ready Queue: All non-closed tickets whose dependencies are either empty or completely satisfied by tickets in
status: closed. - Blocked Queue: All non-closed tickets where at least one dependency in
depsis instatus: openorstatus: in_progress. - Cycle Detection: Depth-First Search (DFS) traversal identifying circular references (
A -> B -> A) across open tickets.tk depprevents new cycles (Section 3.1);tk dep cyclereports cycles created by hand edits, merges, or older versions.
4. CLI Interface & Exit Codes¶
| Command | Description |
|---|---|
tk init [dir] |
Initializes .tickets/ with a closed setup ticket and .tickets/.gitignore; safe to re-run |
tk uninit [dir] |
Deletes the .tickets/ directory (no confirmation) |
tk create [title] [options] |
Creates ticket, prints generated ID to stdout. Options: -d/--description, --design, --acceptance, -t/--type, -p/--priority, -a/--assignee, --external-ref, --parent, --tags |
tk start <id> |
Sets status to in_progress |
tk start <id> --claim [--assignee X] |
Atomically sets status in_progress and assignee (default: git config user.name, else $USER); fails with exit code 3 if already in_progress for a different assignee |
tk close <id> |
Sets status to closed |
tk reopen <id> |
Sets status to open |
tk status <id> <status> |
Updates status directly (open, in_progress, closed) |
tk dep <id> <dep-id> |
Adds dependency relationship |
tk undep <id> <dep-id> |
Removes dependency relationship |
tk link <id> <id> [id...] |
Adds symmetric relational links between tickets |
tk unlink <id> <target-id> |
Removes symmetric relational link between tickets |
tk dep tree [--full] <id> |
Prints hierarchical ASCII dependency tree (--full disables deduplication) |
tk dep cycle |
Traverses graph and outputs detected cycles |
tk ready [-a X] [-T X] |
Lists unblocked, actionable tickets (filter by assignee or tag) |
tk blocked [-a X] [-T X] |
Lists blocked tickets with unresolved blockers |
tk closed [--limit=N] [-a X] [-T X] |
Lists recently closed tickets by file mtime (default 20) |
tk ls / tk list [options] |
Lists tickets (default: hides closed, limit 10). Options: -s/--status, -a/--assignee, -t/--type, -p/--priority, -T/--tag, -n/--limit, --full, --all |
tk edit <id> |
Opens ticket in $EDITOR |
tk query [jq-filter] |
Outputs tickets as JSON lines; the optional filter requires jq |
tk find <search-term> |
Case-insensitive text search across tickets |
tk show <id> |
Displays ticket metadata and full markdown body (paged via $TICKET_PAGER/$PAGER on a terminal) |
tk update <id> [options] |
Updates fields. Options: --title, -d/--description, -p/--priority, -t/--type, -a/--assignee, --tags, --design, --acceptance, --parent, --external-ref |
tk add-note <id> [text] |
Appends timestamped note (reads stdin when text is omitted) |
tk agent-skill [--install] [--path] |
Displays the agent skill, installs it to ~/.agents/skills/tk/ (or $AGENTS_SKILLS_DIR), or prints its installed path |
tk version / --version / -v |
Prints version, description, and license information |
tk help / --help / -h |
Displays available built-in commands and discovered plugins |
tk super <cmd> [args] |
Runs a built-in command, bypassing a plugin with the same name |
All commands accept full or partial ticket IDs.
4.1 Exit Codes¶
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Error (usage, ticket not found, invalid or multi-line value, ticket file not writable) |
3 |
Claim lost: tk start --claim found the ticket in_progress for another assignee |
4 |
Lock timeout: another tk process held the ticket lock for longer than TK_LOCK_TIMEOUT |
4.2 Ticket Locks & Claiming¶
Status writes (start, close, reopen, status) and claims run under a per-ticket lock, so concurrent agents cannot interleave a read-check-write on the same ticket.
- Mechanism:
mkdir $TICKETS_DIR/.locks/<id>.lock(atomic on POSIX filesystems; noflockdependency). The lock holds anownerfile:<pid> <host> <epoch>. It is removed on exit, including on error orSIGINT/SIGTERM. - Waiting: a busy lock is retried every 0.1s for up to
TK_LOCK_TIMEOUTseconds (default10), then the command exits4. - Stale locks: a lock is stale when its owner pid no longer runs on the same host, or it is older than
TK_LOCK_STALE_SECONDS(default60). A waiter renames a stale lock aside (only one waiter wins the rename) and deletes it. Locks from crashed processes on other hosts (shared network filesystems) expire by age only. To clear one manually:rm -rf .tickets/.locks/<id>.lock. - Claim rule:
--claimsucceeds if the ticket is notin_progress, has no assignee, or is already assigned to the claimant (re-claiming is idempotent). Agents sharing one identity must pass distinct--assigneevalues. - Scope: locks coordinate
tkprocesses sharing one.tickets/directory. Claims across separate Git clones are reconciled through Git, not by this lock. - Git: lock files never belong in Git.
tk initadds.locks/and.hooks/hooks.logto.tickets/.gitignore(appending only missing lines), and the first lock writes a self-ignoring.tickets/.locks/.gitignore(*), which also covers repositories initialized earlier. Re-runningtk initin an existing repository is safe. - Plain
tk start <id>is unchanged: it setsin_progresswithout checking or changing the assignee.
5. Plugin & Extension Protocol¶
- Any executable named
tk-<command>orticket-<command>in$PATHor located withinplugins/<command>/is automatically invoked when runningtk <command>. - When invoked, the core CLI exports:
TICKETS_DIR: Absolute path to the resolved.ticketsdirectory.TK_SCRIPT: Absolute path to the roottkexecutable.- Plugins must conform to PLUGIN_SPEC.md:
- Directory isolation:
plugins/<plugin-name>/containing executable and<PLUGIN-NAME>-SPEC.md. - Optionality: All plugins are optional; the core never requires one. The default
--fullinstall bundle includes the Web UI for convenience, and the other plugins are installed only on request (see Section 1). - Metadata discovery:
# tk-plugin: <description>comment in header or--tk-describeflag fortk helpdiscovery. - Version contract:
--versionflag for standardized version reporting.
5.1 Post-Write Hooks¶
After a successful write, tk runs every executable file in $TICKETS_DIR/.hooks/post-write.d/ (lexical order). Plugins use this to react to saves without polling; tk scion-taskforce init installs one.
- Events (
TK_EVENT):create,start,close,reopen,status,update,add-note,dep,undep,link,unlink,edit. Read-only commands,dep tree,dep cycle, and failed writes fire nothing. - Environment:
TK_EVENT,TK_TICKET_ID(full ID),TK_TICKET_FILE,TICKETS_DIR(absolute),TK_SCRIPT,TK_HOOK_DEPTH=1. - Execution: background (
nohup) by default; inline whenTK_HOOKS_SYNC=1. Output is appended to.tickets/.hooks/hooks.log. Hook exit codes never change thetkexit code. - Recursion guard: hooks are skipped when
TK_HOOK_DEPTHis set, so a hook callingtkdoes not re-trigger itself. SetTK_NO_HOOKS=1to disable hooks for one command. - The Web UI writes through the
tkCLI, so its saves fire the same hooks. Hand edits andgit pulldo not.