Your assigned JIRA tickets and the pull requests addressing them, on one page, in your terminal.
No install, no credentials, nothing to configure — this renders the screenshot above with sample data:
go run github.com/tptodorov/devdash@latest -demoOnce you have the four environment variables from Configure, the same command shows your real tickets:
go run github.com/tptodorov/devdash@latestgo run compiles and runs it in one step, so it is the quickest way to try devdash
without adding a binary to your PATH. If you decide to keep it, see
Install.
The state of your own work is spread across three places: the JIRA board says what is assigned to you, GitHub says which pull requests exist, and neither knows about the other. Reconciling them is a tab-switching exercise you repeat all day.
devdash puts them on one line each. It also closes three gaps that cost real time:
- A merged PR vanishes from
is:opensearches, so a ticket still in review looks like it has no PR at all. devdash fetches recently merged PRs too. - Approvals can be invisible. GitHub reports no review decision when the base branch requires none, so an approved PR looks unreviewed. devdash counts approving reviews as well as reading the decision.
- Changing a ticket's status meant leaving the terminal. Press
s.
- Go 1.25+ to install
- A JIRA API token — create one at id.atlassian.com
- A GitHub token with
reposcope
Every glyph devdash draws is plain Unicode, one column wide, so no special font is needed.
go install github.com/tptodorov/devdash@latestThat puts devdash in $(go env GOPATH)/bin. Make sure that is on your PATH,
or set GOBIN first:
GOBIN="$HOME/.local/bin" go install github.com/tptodorov/devdash@latestFour environment variables. Add them to your shell profile:
export JIRA_URL='https://your-org.atlassian.net'
export JIRA_USERNAME='you@your-org.com' # your Atlassian account email
export JIRA_API_TOKEN='...' # from id.atlassian.com
export GITHUB_TOKEN='...' # a token with repo scopeIf you already use the GitHub CLI, the token is one command away:
export GITHUB_TOKEN=$(gh auth token)Check what devdash can see:
devdash help # the REQUIREMENTS section reports each variable as set or missingcd ~/code/your-repo
devdash # or: go run github.com/tptodorov/devdash@latestRun from inside a repository and the pull requests are narrowed to it; the header says which one. Run from anywhere else and every repo you have PRs in is included.
devdash -demo # sample data, no credentials needed — see it before you set it up
devdash --help # everything below, plus every API call the tool makes
devdash -once # print one snapshot and exit, for piping or a cron job
devdash -refresh 30s # slow the auto-refresh down
devdash -all-repos # ignore the current repository, show everything| Key | Action |
|---|---|
↑/k, ↓/j |
move between rows |
←/h, →/l |
move between the columns of the selected row |
g / G |
jump to the first / last row |
enter |
open whatever the selected column points at |
o |
open the ticket, whichever column is selected |
p |
open the selected row's pull request |
c |
copy a shareable snippet: title, ticket link, every PR link |
s |
change the selected ticket's status |
S |
schedule the ticket for Symphony, or take it back unless an agent is running |
r |
refresh now |
a |
pause or resume the automatic refresh |
? |
keys, columns, and every indicator explained |
q, esc, ctrl+c |
quit |
Ticket keys and PR references are OSC 8 hyperlinks — ⌘-click them in iTerm2, Ghostty, WezTerm, Kitty or any terminal that supports links.
Left and right walk the columns of the selected row, which is highlighted in a contrasting colour, in the order the columns are drawn. Enter opens whatever the active one points at:
| Column | enter opens |
|---|---|
| Symphony | the Symphony dashboard |
| relation | a JIRA search for its sub-tickets |
| ticket | the ticket in JIRA |
| pull request | that pull request — each PR on the row is its own column |
↩ ♪ +4 PROJ-455 Add a shared database registry ● platform #1088 ✓ ✓2
│ │ │ │
│ │ │ └ pull request
│ │ └ ticket
│ └ relation
└ Symphony
Columns appear only when they do on screen, so left and right never land
somewhere that would do nothing: a ticket with no relation, no PR and no Symphony
session has a single column. o still opens the ticket from anywhere on the row.
A sub-task's ↳ is not a stop either — the ticket does not carry its parent's
key, so there is nothing to open.
The first pull request shares the ticket's line. Every further one gets a line of
its own, marked ↳ immediately left of the pull request cell, so a line carrying
no ticket still reads as belonging to the ticket above it — and every reference
lands in the same column, which is what lets the states be read straight down.
Each keeps its own attention marker, so a pull request wanting something under one
that does not still announces itself:
↩ ♪ +4 PROJ-455 Add a shared database registry ● platform #1088 ✓ ✓2
↩ ↳ ● platform #1091 ◷ ↩
↳ ● platform #1104 ✓
They are all one row: ↑/↓ steps over the group, ←/→ walks into each PR in
turn, selecting the ticket highlights every line, and c copies all three links.
✓ ↳ PROJ-475 Add account identifiers ● platform #1103 ✓ ✓2
│ │ │ │ │ │ │
│ │ │ │ │ │ └ checks, then review
│ │ │ │ │ └ repo and number, linked
│ │ │ │ └ state: ● open, ○ draft; colour says merged or closed
│ │ │ └ summary
│ │ └ ticket key, linked to JIRA
│ └ relation: +N sub-tickets, or ↳ a sub-task
└ attention
Every column is sized to its contents. The relation, key and pull request reference columns fit the widest value on screen, so they line up down the page and cost nothing they do not use, and any column with nothing in it disappears. The PR title is capped at 30 columns, since it only describes a pull request the row already names.
The summary is the column that flexes: it takes whatever the terminal leaves, up
to the longest summary loaded, so a wide terminal spends its room on the ticket
descriptions rather than on padding. Squeeze the window and the summary gives way
first, then the title collapses, then the reference. Anything longer than its
column is cut with an … — the ticket itself is one enter away.
The PR title drops the ticket key it conventionally opens with, since the row already carries it.
The leftmost column is the one to read first. It answers a single question —
is there something here for me — so the edge of the screen can be scanned on its
own. A ticket takes the most urgent state of any of its pull requests, and the
header counts them: 3 need you.
| Marker | Meaning |
|---|---|
✓ |
approved and green: merge it |
↩ |
changes requested: respond to the review |
! |
the build failed, or Symphony is blocked waiting on you |
| blank | this row wants nothing from you |
Relation is +N when a ticket has N sub-tickets, or ↳ when it is itself a
sub-task. The two cannot both apply — JIRA forbids sub-tasks of sub-tasks — so one
column carries both.
Pull requests are one glyph coloured by state, then two fixed slots for checks and review. Holding the slots in place is what lets them be read as columns down the page:
| Glyph | Meaning | |
|---|---|---|
| state | ● |
open |
● violet |
merged | |
● red |
closed without merging | |
○ |
a draft — hollow, because a draft is a flag on an open PR, not a state of its own | |
| checks | ✓ |
passing — drawn faintly, because passing is expected |
✗ |
failing | |
◷ |
still running | |
| review | ✓ |
approved; ✓3 means three approving reviews |
↩ |
changes requested |
Each slot is blank when there is nothing to say. A pull request merely awaiting review is behaving normally, so it draws nothing: only a deviation earns ink. The reference beside the glyph is coloured to match, so a merged PR is a run of violet rather than a single tinted dot.
Tickets are grouped by status, most urgent first, and each group is coloured.
Press s. The choices come from JIRA's transitions API for that specific issue,
so they are exactly what your workflow permits from its current state.
╭─ PROJ-482 ────────────────────────────────────────────╮
│ Mark the CI wait helper as manual-only │
│ now: Awaiting CR │
│ │
│ ▸ 1 To Do │
│ 2 In Progress │
│ 3 Awaiting Verification via "Done" │
│ 4 Closed +1 step │
│ 8 Awaiting CR (current) │
│ │
│ ↑↓ choose ⏎ select esc cancel o open │
╰───────────────────────────────────────────────────────╯
1–9 jump straight to a choice. Two details that matter:
- The destination status is shown, not the transition's name. They differ more
often than you would expect — a transition called Done can move an issue to
Awaiting Verification. Where they differ, the transition's own name appears as
via "Done". +1 stepmarks a transition that needs a value first, such as a resolution when closing. You get a second prompt listing the allowed values.
Auto-refresh pauses while the panel is open, so rows cannot move under you.
Press c to copy the selected row as text you can paste to whoever should look at
it:
PROJ-482 — Mark the CI wait helper as manual-only
https://your-org.atlassian.net/browse/PROJ-482
https://github.com/acme/platform/pull/1099
Bare URLs, so they unfurl in Slack. A ticket with several PRs contributes all of their links. Nothing that goes stale is included — no status, no CI state.
Tickets that a local Symphony has in hand, or has been given, are marked in a column at the left-hand edge, beside the attention marker. The note is the constant — it means Symphony has this ticket — and the colour says what Symphony is doing with it:
| Marker | Meaning |
|---|---|
♪ grey |
scheduled, waiting for Symphony to pick it up |
♪ cyan |
Symphony is working on the ticket |
♪ yellow |
waiting for the next retry window |
♪ red |
paused waiting for operator input or approval |
S toggles. On a ticket Symphony would pick up it strips the required labels
again, which is enough to release the ticket: Symphony re-reads the labels before
it retries or reconsiders a blocked issue, so stuck work can be taken back, fixed,
and scheduled afresh. Only a ticket an agent is actively running is refused, since
removing a label cannot interrupt a turn already in progress — stop that session in
Symphony instead. Unrelated labels and the ticket's status are left alone.
The instance is located from the server block of WORKFLOW.md's front matter,
searching upward from the working directory:
---
server:
port: 10000
---Discovery and the query are repeated on every refresh, since Symphony starts and stops independently. If it is not running the column disappears entirely and nothing is reported — that is the ordinary case.
| Flag | What it does | Environment | Default |
|---|---|---|---|
-demo |
show sample data instead of contacting JIRA or GitHub; needs no credentials | — | real data |
-once |
print one snapshot and exit instead of running the interactive UI | — | interactive |
-refresh |
auto-refresh interval, e.g. 30s or 2m; 0 disables it |
DEVDASH_REFRESH |
10s, minimum 2s |
-all-repos |
show pull requests from every repository, not just this directory's | — | scoped to this repo |
-jql |
JQL selecting which tickets to show | DEVDASH_JQL |
assigned to you, not Done |
-pr-query |
GitHub search selecting which pull requests to show; overrides repo scoping | DEVDASH_PR_QUERY |
open and recently merged, scoped to this repo |
-include-archived |
keep pull requests whose repository is archived | — | archived hidden |
-no-links |
render plain text instead of OSC 8 terminal hyperlinks | — | hyperlinks on |
There is also a help subcommand, which reports whether each required
environment variable is set and lists every API call the tool makes:
devdash helpScope it to a project or an organisation by overriding the queries:
devdash -jql 'assignee = currentUser() AND project = PROJ AND statusCategory != Done ORDER BY updated DESC'
devdash -pr-query 'author:@me is:pr is:open org:your-org'An explicit -pr-query is run exactly as written, with no repository scoping and
no state filtering — ask for closed PRs and you get them.
Correlation. A pull request attaches to a ticket when a JIRA key appears in its title, falling back to its branch name. PRs matching no active ticket are listed under their own heading rather than hidden, because they still need something from you.
Which PRs are fetched. Open ones, plus any merged in the last 30 days — so a ticket still in review keeps showing the PR that did the work. Closed without merging is excluded as abandoned. A merged PR matching no active ticket is dropped rather than listed; it is finished work, and there can be hundreds.
Repository scope. The owner and name are read from .git/config directly — no
git subprocess — then confirmed against the API. That second step matters: a
renamed repository keeps its old URL in the remote, and GitHub's search does not
follow renames, so searching the stale name silently matches nothing.
Archived repositories are skipped. GitHub keeps returning their PRs forever,
but they cannot be merged. -include-archived brings them back.
No external binaries are needed for data. JIRA and GitHub are called over
HTTPS. pbcopy/xclip and open/xdg-open are used only for the clipboard and
the browser, and only when you press c or enter.
Nothing is written anywhere except the clipboard, and JIRA when you press s.
"0 tickets" or an authentication error. Run devdash help and check the
REQUIREMENTS table — it reports each variable as set or missing. Note that JIRA
answers an unauthenticated search with 200 and an empty list, so devdash verifies
your identity separately rather than reporting an empty backlog; an expired token
shows as an explicit error.
An indicator is a blank box. Your terminal font is missing one of the plain Unicode glyphs devdash uses. Any font with reasonable symbol coverage will do; no patched or Nerd Font is required.
A ticket shows no PR when you know there is one. The key must appear in the
PR's title or branch name. Check the spelling, and note that scoping to the current
repository hides PRs in other repositories — try -all-repos.
No colour when piping. Expected: colour is dropped when the output is not a
terminal. -once -no-links gives clean text for scripts.
Issues and pull requests are welcome.
git clone https://github.com/tptodorov/devdash
cd devdash
go test ./...
go build -o devdash .
./devdash -demo # no credentials neededCI runs on every push and pull request: gofmt, go vet, go test -race -cover,
and a cross-build for macOS and Linux on amd64 and arm64. Running gofmt -l . and
go test ./... locally is enough to predict it.
The README screenshot is generated from demo data, so it never contains anyone's real tickets. To regenerate it after a change to the interface:
pip install Pillow
go build -o devdash .
python3 scripts/screenshot.py ./devdash docs/screenshot.pngTestDemoDataCoversEveryState keeps the sample data covering everything the
documentation claims to show.
To see what S would do to every one of your tickets without changing anything:
DEVDASH_DRYRUN_DIR=/path/to/repo go test -run TestDryRunSchedulePlans -vOne test suite talks to real JIRA and is skipped unless you point it at a ticket you do not mind moving. It transitions the ticket and moves it back:
DEVDASH_LIVE_TICKET=PROJ-482 go test -run TestLiveTransitionRoundTrip -v