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
Both render
components/features/issues/issue-detail-surface.tsx. The route
only decides what sits above it.
The selection is in the URL
Opening an issue or a project writes it into the query string:- 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, whichPATCH /api/v1/crews/{crewId}/issues/{identifier}has no field for — the request fell through to400 No fields to update. The field islabels. - Clear estimate.
estimatedecoded into a*int, so a JSONnullwas indistinguishable from an omitted field and the clear arrived as an empty patch. The handler now reads it as raw JSON and tellsnullapart from absent. A non-number is a400.
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”.
Work, hand-offs and Lead review
The Current work 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 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:
- Workers run (or the bound routine does). Stage
working. - When every worker has finished, the Lead gets a review task with the
acceptance criteria and the results. Stage
reviewing. - The Lead answers
approve,request_changesnaming a worker, orneeds_human.approveon verified work isaccepted(issue toREVIEW, orDONEwhen acceptance is not required).request_changesopens a second attempt for that worker at once; after the second attempt, or onneeds_human, or when the Lead approved despite a failed worker, the stage isneeds_humanand the issue waits inREVIEWwith an inbox notification. - Approve / Request changes on the identity card are your half. A
request_changesreturns the issue toTODOand marks the executionchanges_requested; the next Start opens a fresh attempt.
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 toDONE 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
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: one filter, two places
/issues filters by crew, agent, priority and status, and the four
combine — “Engineering crew, high priority, still in Backlog” is one
selection, not three attempts. Each pick used to clear the others, so the
dropdown behaved like a set of switches; picking a crew left the priority
alone, picking an agent did not, and no combination survived.
The two are one piece of state, not two filters: picking Backlog in the
dropdown lights the Backlog chip, and clearing the chip clears it in the
dropdown. The chips are the quick, count-carrying affordance and disappear
when an issue detail takes the centre pane; the dropdown is always reachable,
which is why status belongs in it too.
Clearing:
- Click an active value again to drop that facet.
- All crews / All agents / Any priority / Any status resets one facet and leaves the others.
- Clear all in the dropdown header drops everything, as does
Escon the board.
hooks/use-filtered-issues.ts). It used to apply only crew and agent, so a
priority picked in the dropdown narrowed the board while the list beside it
went on showing the rows the filter had excluded.
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> renders the matching project card:
identity → figures → the project’s issues → breakdown → health → properties.
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 asCANCELLEDregardless of what it reports. Passinghard=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, sendsKILLto 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 reportedNOT_FOUNDrather than signalled blind. Stop also reaches a run an@mentiondispatched on an issue that was never started (BACKLOG/TODO) — the run is stamped (and, withhard=true, signalled) the same way, but the issue’s own status is left unchanged rather than moved toCANCELLED. The full contract is in the issues API reference.