Skip to content

Tickets and issues

neosource carries two kinds of work item on one storage row, and which one you are looking at is decided by a single field: whether it belongs to a repo.

Ticket Issue
Addressed ENG-7 acme/api#1526
Scoped to a team a repo
Numbered by the team’s sequence the repo’s sequence
Filed by people on the team anyone who can see the repo
Command group neo ticket neo issue

A ticket is planning work. It lives on a team’s board, it is numbered KEY-N from that team’s own sequence, and it has no repo at all — which is the point, because the work it describes usually spans several, or none yet.

An issue is intake. It belongs to one repository, it is numbered from that repository’s sequence, and it is what an outside reporter, a crash handler or a bot files when something is wrong with that specific codebase.

Neither is a lesser form of the other, and neither converts into the other. A repo issue is a real report against a real repo; a ticket is the planning work someone decided to do about it. Collapsing them would either force every report to pick a team it has no way of knowing, or force every planning item to pretend it belongs to one repo.

The link between the two tracks is a tracks relationship, and it has a fixed direction: a ticket tracks an issue, never the reverse.

ENG-7 ──tracks──▶ acme/api#1526
◀─tracked_by─

neo ticket track writes it; the inverse tracked_by row is written by the server in the same transaction, so the relationship reads correctly from either end and is never half-present. You cannot create a tracked_by row directly, and you cannot point tracks from an issue at a ticket — the server refuses both.

Terminal window
neo ticket track ENG-7 acme/api#1526
neo ticket untrack ENG-7 acme/api#1526

One ticket may track many issues, which is the usual shape: three bug reports across two repos, one piece of planned work that resolves them.

A team’s triage is its intake queue: the open issues, in the repositories the team holds a grant on, that no ticket tracks yet. Only repositories you can read contribute rows, and repositories mirrored from GitHub are left out.

Terminal window
neo ticket triage --team ENG

Accepting an item files a ticket that tracks it. The ticket and the link are written in one transaction, and the issue stays open with its own number: accepting links the two, it does not convert one into the other.

Terminal window
neo ticket create --team ENG --tracks acme/api#1526 --status todo --title "Fix the login redirect"

An item that belongs to work already planned is linked with neo ticket track ENG-7 acme/api#1526 instead; one that will not be done is closed with neo issue close 1526 acme/api --reason cancelled.

Every neo ticket verb has a neo issue twin, and they run the same code — neo ticket is the team-scoped path with the team supplied by the reference itself rather than by a flag.

Ticket Issue
neo ticket list --team ENG neo issue list acme/api
neo ticket create --team ENG --title … neo issue create acme/api --title …
neo ticket view ENG-7 neo issue view 1526 acme/api
neo ticket claim ENG-7 neo issue claim 1526 acme/api

neo ticket’s numbered verbs take no team flag and no repo target, because ENG-7 already says which board it is on. Where the team lives in a non-personal workspace, qualify it: neo ticket view acme/ENG-7.

The numbered neo issue verbs accept both spellings. A bare number is scoped by the repo target or --team exactly as it always was, and a KEY-N reference carries its own team, so it combines with neither:

Terminal window
neo issue view 1526 acme/api # repo-scoped, by target
neo issue view 7 --team ENG # team-scoped, by flag
neo issue view ENG-7 # team-scoped, by reference

neo ticket list shows the team’s board, which is its tickets — every row renders KEY-N. A repo-bound issue never appears there even when it carries the same team; it lives on its repo’s list (neo issue list <owner>/<repo>, rendering owner/repo#N), and the way a team plans one is to file a ticket that tracks it.

create and edit set the same fields on both kinds: --title, --body (or --body-file, - for stdin), --priority and --status. Labels and assignees are named, never addressed by id:

Terminal window
neo ticket create --team ENG --title "Fix the login redirect" \
--label bug --label auth --assignee @me
neo ticket edit ENG-7 --status in_review --add-label ui --remove-label auth \
--add-assignee alice --remove-assignee @me
neo issue labels acme/api # what --label resolves against
neo ticket labels --team ENG

A label name resolves against the labels its repo or team offers (exact, else the one case-insensitive match), and a miss lists the available ones. An assignee is a handle, with or without the @; @me is you. Every name resolves before the first write, so a typo sends nothing.

Labels and assignees are separate writes after the create or the field update. If one fails partway, the error says which writes already landed and which were never attempted, so a rerun can be aimed. edit compares against the issue’s current state and sends only what differs; an edit that changes nothing sends nothing.

A status change is sent by transition, the way the web UI sends it, because closing is more than a status: moving to done, cancelled or duplicate closes the issue with that reason (stamping when it was closed, again if it was already closed); moving a closed issue to an open status reopens it, then sets the status if it is not todo; moving between open statuses is the plain field update. --status and a document’s status: behave the same.

--edit (-e) and --from-file hand create and edit one document format: a frontmatter of five keys between two --- lines, then the markdown body.

---
title: Fix the login redirect
status: todo
priority: high
labels: [bug, auth]
assignees: [alice, @me]
---
The redirect drops the `next` parameter when…
  • The keys are title, status, priority, labels and assignees, and nothing else: an unknown key, a misspelt status or a key set twice is an error naming its line. Lines starting with # are comments.
  • Lists are [a, b] (the brackets are optional). Quote a value with "…" (escapes \", \\, \n) or '…' when it has a comma, a bracket or surrounding spaces.
  • On edit, a key the document leaves out keeps its current value, and a --from-file document with nothing after its closing --- leaves the body as it is: clearing a body that way takes an explicit --body "". In --edit the body is prefilled, so deleting it and saving clears it. A list that is present is the whole set: labels: [] removes every label.
  • Flags given beside a document win over it: --title replaces the document’s title, --label its labels, and on edit --add-label / --remove-label apply on top of the document’s list.

--edit opens $VISUAL, else $EDITOR, else vi, on a scratch file in the system temp directory: a template on create, the current values on edit. Saving the template untouched with an empty title on create aborts with nothing sent. If anything fails after you save — a document that does not parse, a label that does not resolve, a refused request — the scratch file is kept and its path is in the error, so rerunning with --from-file <path> loses nothing. With --json, the editor runs on the terminal (/dev/tty) so that stdout stays one JSON document; without a terminal, --edit --json is refused.

--from-file <PATH> reads the same document without an editor; - reads stdin. That is the editor-plugin seam: a plugin pipes a buffer in.

view --document prints an issue in the same format, labels and assignees included, so a plugin’s round trip is three commands, and feeding the output straight back changes nothing:

Terminal window
neo ticket view ENG-7 --document > buffer.md # open the buffer
neo ticket edit ENG-7 --from-file - --json < buffer.md # on save
neo ticket create --team ENG --from-file - --json < buffer.md
Terminal window
neo ticket list --team ENG --search login --label bug --assignee @me --priority high
neo issue list acme/api --state closed --limit 20
neo issue mine # assigned to you, both kinds
neo ticket mine --created --state all

A team list hands every filter to the server and follows every page, up to --limit rows. A repo list is one server page (--search is applied by the server; the list is capped at 500 rows) with the label, assignee and priority filters applied to its rows. mine is the same list under both command groups: the issues and tickets assigned to you (or --created by you), across every repo and team, each under its own reference.

Every neo issue and neo ticket verb takes --json, which is a stable contract for scripts and UIs to pipe into jq:

  • stdout carries exactly one JSON document and nothing else; progress and errors go to stderr, with the exit codes in neo docs exit-codes.
  • A single issue (create, edit, close, reopen, claim, release) is the server’s IssueResponse verbatim. After create or edit touched labels or assignees, labels and assignees carry the state read back from the server afterwards; otherwise they are null (not read), never [] for “unknown”.
  • view is {issue, comments} (--document prints the issue document instead, and does not combine with --json); context is the context pack (always JSON).
  • list is {issues}, every fetched page merged; mine and triage are {items, next_cursor} with every page merged and next_cursor: null; labels is {labels}.
  • comment is the created comment; track the created link; untrack the ticket’s {links} as they stand after the removal.