Skip to main content

The issue detail

An issue has one screen. Whether you clicked a row on the board, followed a /issues/ENG-4 link from the dashboard, hit it from ⌘K, or opened it from the Inbox, you land on the same surface with the same controls. That was not true before. Clicking a row inside /issues opened an inline panel; the canonical link /issues/<identifier> — the one the dashboard, the palette, the inbox, activity, the crew tabs and every sub-issue link point at — opened a completely different page, with different chrome, a different rail and a different set of things you could change. Which screen you got depended on how you arrived.

The two routes

The page renders components/features/issues/issue-detail-surface.tsx; the peek reads the same issue and changes nothing.

The selection is in the URL

Opening an issue or a project writes it into the query string:
Three consequences, all of which were missing before:
  • You can share it. Paste the URL and the other person opens the same issue, under the same project filter.
  • A refresh keeps it. The selection is no longer component state.
  • Back closes the detail instead of leaving the page. Selections are written with history.pushState, so each one is a history entry.
?project= used to be read on arrival and never written, so ⌘K could send you to a project but selecting one in the page left the URL saying nothing. Both params are now read and written, and they do not clobber each other — opening an issue inside a project keeps the project.
Writes go through the History API rather than router.push. A same-path query change through the App Router re-evaluates the dashboard layout subtree and flashes the auth provider’s full-screen spinner. Same reason as hooks/use-shallow-search-param.ts; the only difference is push versus replace.

What you can do on it

Everything the two old screens could do, plus the things only one of them could. Two of these did not work before this landed, and now do:
  • Labels. The sidebar sent label_ids, which PATCH /api/v1/crews/{crewId}/issues/{identifier} has no field for — the request fell through to 400 No fields to update. The field is labels.
  • Clear estimate. estimate decoded into a *int, so a JSON null was indistinguishable from an omitted field and the clear arrived as an empty patch. The handler now reads it as raw JSON and tells null apart from absent. A non-number is a 400.

Who started the bound routine

The Routine card used to say one true and incomplete thing: starting this issue runs that routine. The same routine also runs on a clock and on rules, and an operator asking why it ran at 03:00 had nowhere on this page to look. The breakdown groups the routine’s recent runs by source — issue, manual, schedule, automation — counting repeats rather than listing every run, because the question is “does this thing run without me”, not “list the runs”.
automation and schedule are counted separately on purpose. Every deferred run is stored with triggered_via: "schedule", cron and rule alike, so folding them together would report that a schedule did something a rule did. An automation row names the rule.

Work, hand-offs and Lead review

The Work & control card is the issue’s work contract (#2448): at any moment the issue is either with an agent (work_mode: agent — Start may open runs) or with a person (work_mode: human — automatic work is paused). Every change to that state is a compare-and-set on work_revision, so two people acting on a stale card cannot both win. Take over puts the issue in your hands. It is the same operation the Inbox’s Take over button runs on a NEEDS_HUMAN card: parked routine runs are cancelled, live runs get a cooperative cancel and a hard stop, waiting assignments and open plan tasks are cancelled, pending @mention deliveries are superseded, and the issue goes back to TODO held by you. While the runs are still winding down the card reads Taking over · stopping agent runs (work_stopping: true) and hand-back/submit wait. Hand off…, Delegate…, Submit result and Request changes open dialogs. Each transfer carries the revision you saw and an operation id that stays the same if you retry the same request, so a double submit is one transfer; a 409 refreshes the page and keeps what you typed. The dialog’s footer shows the revision. Delegate… is a handoff to an agent with Start work now on by default; Request changes says which of the Lead’s two correction rounds it is. Hand off names the next worker and carries a note. To a person, the issue stays paused and they get a targeted Inbox message. To an agent, that agent becomes the delegate and the plan is replaced by one task carrying your note, ready for Start work — nothing runs until someone starts it, and the previous result stays in the conversation. Submit result is how a person finishes: the note becomes the result, the issue moves to REVIEW, and the brief revision it was written against is remembered — if the title or description change afterwards, Approve is refused until an updated result is submitted. Require client acceptance after Lead review is the review policy. It can only be changed while the issue is TODO/BACKLOG with nothing running. On, the Lead verifies the result and then a person approves completion; off, the Lead can mark verified work DONE on its own. Either way, uncertainty comes back to a person. Starting an issue with an agent opens an execution — one attempt of the work → Lead review → correction loop:
  1. Workers run (or the bound routine does). Stage working.
  2. When every worker has finished, the Lead gets a review task with the acceptance criteria and the results. Stage reviewing.
  3. The Lead answers approve, request_changes naming a worker, or needs_human. approve on verified work is accepted (issue to REVIEW, or DONE when acceptance is not required). request_changes opens a second attempt for that worker at once; after the second attempt, or on needs_human, or when the Lead approved despite a failed worker, the stage is needs_human and the issue waits in REVIEW with an inbox notification.
  4. Approve / Request changes on the identity card are your half. A request_changes returns the issue to TODO and marks the execution changes_requested; the next Start opens a fresh attempt.
A take-over, hand-off, brief change or Stop while an execution is working/reviewing marks it superseded — one execution per issue is live at a time, and the card always shows the latest one with its reviewer, attempt number, workers and note. Deliverables. A worker that ends its ---HANDOFF--- block with an artifacts: line naming files under /crew/shared/ (comma-, semicolon- or newline-separated; up to ten) has those files attached to the issue when its run completes — same size limit and digest de-duplication as a manual attachment, recorded as attachment_added by the agent. Paths elsewhere, including /crew/shared/.memory/, are ignored rather than read. Everything above is also reachable without the browser: crewship issue work, review-policy, review, result and stop — see CLI → issue and the API reference.

From the issue to its runs and its journal

The Runs card lists every run on the issue, newest first — not the latest one alone. Each row names the agent (a link to its page when the slug is known), the task, the status as a word, when it started, how long it took, and Open run, which opens that run on the Activity page. A row whose assignment never reached a run says so instead of pretending to link. The card’s footer links the issue’s journal (/journal?mission_id=ENG-4 — the identifier works, the server resolves it) and every run of the issue in Activity. The rail’s Related card gathers the same links in one place: the crew, the agent, the run in flight, the journal trace, and Activity. The crew and the assignee in the identity row and in Properties are links too. Together with the run page’s own Related row and the journal’s issue pill, this is the loop the audit found broken: issue → run → journal → issue, with every leg a link.

The board moves without a refresh

An open board or issue tab repaints live on five kinds of change: an issue being created, its status changing, a comment landing, an agent session’s state moving (pending → active → idle/error/ awaiting_input), and a run’s outcome being decided. Each is a WebSocket event on the workspace channel — issue.created, issue.status_changed, issue.updated, issue.session.state, and run.outcome — and every one of them has to be on the client’s realtime allowlist (hooks/use-realtime.tsx) or it is dropped silently before anything can react to it. Registering a type is not the same as never missing a frame, though: under load, the server’s WebSocket hub sends non-blocking and drops a frame outright when a client’s send buffer is full, with no client-visible error. The one realtime signal that carries a sequence number (issue.delivery.acked, the “your message was received” ack — see Issue mentions) is watched for exactly that: if the next seq seen for an issue isn’t one more than the last, the client has missed at least one frame, and it resyncs by calling GET /api/v1/crews/{crewId}/issues/{identifier}/events?after_seq= — the same ordered event log crewship issue events reads — for whatever it missed, instead of staying stale until someone reloads the tab.

Sub-issues must finish before their parent

An issue with a sub-issue or a plan task that is still live cannot move to DONE or REVIEW — the PATCH is refused with a 409 naming every open child. Add ?force=true to the request (crewship issue update <id> --status DONE --force; the check lives on the same PATCH every status change goes through) to close it anyway. Forcing past an open child is recorded like any other status change — one more entry on the issue’s own activity/event log, naming who forced it and which children were still open — not a separate approval or a new table. “Live” means not yet DONE/CANCELLED/ DUPLICATE for a sub-issue, and not yet COMPLETED/FAILED/CANCELLED/ SKIPPED for a plan task; a FAILED sub-issue still counts as live on purpose, since a failure is not the same thing as the work being finished.

The layout

/issues/<id> has the Issues panel on its left, as /issues does. On this page the panel takes you somewhere: a view, a project, a facet or a search opens /issues with it. The top bar has Back (to wherever you came from), Previous issue / Next issue (through the list you came from, in your grouping and order; the origin is kept), Copy link, a ⋯ menu and Back to list. The menu has Copy link, Move to project…, Cancel issue… (asks first) and Delete issue… (red confirmation; only Backlog and Cancelled issues can be deleted, as the server allows). On a phone the bar is Back, the ID and ⋯. The page is two columns from xl up: the main column (ID, status, project, title, session, brief, sub-issues, relations, pull requests, timeline) and a 320 px side column (Work & control, properties, files, related links, technical details). Below xl the side column follows the main one. The Timeline is one card with a segment: All (comments, activity and runs, newest first; a run is one line), Comments, Activity and Runs (every run with its result, a link to the run and the journal). Relations › Add and Pull requests › Attach PR URL open popovers at their field: Add relation has the four kinds (Blocks, Blocked by, Related, Duplicate of) and an Issue field that searches the workspace; Attach a pull request takes the URL and reads its state with your stored GitHub or GitLab credential. Owner (the accountable person) is a people picker next to Delegate; the change waits for Save like the other properties. There is no “nobody” owner: the API clears owner and delegate together when it is asked to unassign. Everything on the surface is a card, including Run activity — it used to render bare, a heading and a list of steps sitting on the page background in the middle of a stack of cards. It is now a card like the rest, with the step count as its subtitle and the Running / Waiting-for-approval indicator in the header. The timeline itself is shared with the routine run panel and the activity bar, which still get the bare rail; the card is opt-in. On wide viewports (xl and up) the right rail follows you as you scroll. A description longer than the rail used to leave an empty column beside it for the rest of the page. The rail is capped at the height of the viewport and scrolls internally if it is taller, so a long rail on a short window is still reachable. Below xl the columns stack and the rail scrolls with the page, exactly as before.

Finding issues: the panel

The left panel of /issues holds Views (My work, All issues), Projects and Saved views, and a Filter button that opens the facets inside the panel, under its toolbar: Status, Priority, Delegate, Owner, Who holds the work, Labels and Crew. Each facet folds and names its current choice; it lists at most six values and the rest under All N…. The facets combine — “Engineering crew, high priority, still in Backlog” is one selection — and each pick changes only its own facet. Filters live in the URL (?status=REVIEW,TODO&priority=high&crew_id=…&assignee_id=…&owner=…&holder=agent&label=docs), so a filtered list can be shared and a link from another page lands filtered. Changing a filter replaces the URL rather than adding a history entry.
  • Click an active value again to drop that facet.
  • A facet’s × resets that facet and leaves the others.
  • Clear all drops every facet, as does Esc outside a text field.
  • Saved views › + keeps the current filters and layout under a name, optionally shared with the workspace.
  • Keys 1–9 select Views, then saved views, then projects; C opens New issue, / focuses search, ⇧V opens Display options.
My work is the open issues you own or hold. Needs my reply, Agents working, Waiting for my review and Blocked are not offered yet: the list endpoint does not report session state or relations, and a filter over the loaded page would leave issues out.

The list and the board

A list row is one 38 px line: priority, ID, status, title, sub-issues, labels, due date, when it last changed, and the owner and delegate. Display options (⇧V) choose the grouping (status, project, delegate, priority or none), the order (manual, priority, updated, due date, created) and which of those fields a row shows; they are personal. Ticking rows opens a bar at the bottom for Status, Priority and Project; it asks before changing anything and says when the server changed only some of the selection. The board has Backlog, Todo, In progress, In review, Done and Failed columns; cancelled and duplicate issues stay in the list. Dragging a card to another column changes its status — moves to Done or In review ask first, and a move the workflow refuses leaves the card where it was with the reason.

Read-only

The card renders without any of the editors when the host does not pass them — the same layout, every value as text. That is what a reader without permission gets, and it is what the /issues-new design preview was before this shipped. That route is gone: keeping it would have been a second URL for the same screen minus the editing, which is the defect this replaced.

The project detail

/issues?project=<id> opens the project page: a hero (icon, dates, name, description, status, health, lead, crews) and the tabs Overview (done of total, issues by status, milestones, team), Tree (milestone → issue → sub-issue), Issues (the project’s rows; a row opens the peek), Files (project files with their revision and a download; Upload file adds a file, or the next revision of a file with the same name, up to 1 MB; a row’s ⋯ › Retire file… removes it for new runs, and runs that already started keep the revision they read; a change that raced another is refused with a reason rather than overwriting) and Settings (properties, Access with Share, and — for admins and owners — Delete, behind a red confirmation; the issues stay, without a project). The tab is in the URL as ?tab=, so a link can open Settings and a refresh keeps Files. A ?project= that names no project shows This project doesn’t exist with Back to issues, rather than quietly opening the list. Updates, budget and archive are not shown: the backend has none of them yet. Name, icon, colour, status, priority, health, lead and dates are all editable. Dates are new: the old rail drew a row reading “Set dates” that had nothing behind it, while PATCH /api/v1/projects has taken start_date and target_date all along.

Known limits in 1.0

  • Plain Stop is cooperative, not instant — a hard variant exists now. Stop cancels every step that has not started and marks the issue CANCELLED; by default, a step whose agent is already executing finishes that execution first and is then recorded as CANCELLED regardless of what it reports. Passing hard=true (crewship issue stop --hard) additionally ends that step’s own tmux session (tmux kill-session -t agent-{slug}) and, if it is still alive after a short grace period, sends KILL to the session’s process group — both as a new exec into its own container, never by host pid and never a container-level kill, so a sibling agent sharing the same crew container is unaffected. A run that never had a tmux session is reported NOT_FOUND rather than signalled blind. Stop also reaches a run an @mention dispatched on an issue that was never started (BACKLOG/TODO) — the run is stamped (and, with hard=true, signalled) the same way, but the issue’s own status is left unchanged rather than moved to CANCELLED. The full contract is in the issues API reference.

Where the code is