A terminal coding agent composed on the dsh plugin runtime, whose interaction design deliberately aligns with the best of today's agent CLIs.
Launcher:
The codsh-cli npm package — a zero-dependency command that finds an existing
dsh, registers the Bundle into a profile, and boots it. The found dsh must
meet the harness floor published on the launcher (codsh.requiresDsh).
Avoid: wrapper, shim, cli package
Bundle:
The codsh-bundle npm package — the interactive surface and agent preset,
installed into dsh profiles, never globally.
Avoid: runtime package, plugin (alone)
Profile:
A dsh-owned installation root under $DSH_HOME/profiles/<name> holding the
Bundle and its resolved dependencies. The unit a machine installs codsh into.
Preset:
The code-cli agent composition the Bundle ships; what a session mounts to
decide the model-facing toolset.
Viewport: The alternate-screen area the session owns: its own scrollback, wrapping, scrolling, and frame painting. The terminal's native buffer is never touched.
Sticky turn header:
The real user prompt that owns the response currently crossing the top of the
Viewport. Its display-only copy stays pinned until the next real prompt pushes
it away, shrinking from at most three rows to one. Pinned, it is a panel: one
padding row of its fill above the prompt and one below, then the divider that
hands the screen back to the transcript. The fill is the person's band in
the active Theme; a Theme that paints no bands (terminal), and NO_COLOR,
leave the panel unfilled. A prompt longer than three visual rows is a Fold by
default. Clicking the pinned copy expands that floating panel in place;
the inline prompt and the reading position stay where they were. Ctrl+O
on the original still expands the transcript fold, which then does not pin.
Plugin context, tools, and other injected user-role messages never start a
turn, and the copied header is not transcript or clipboard content.
Avoid: sticky message, pinned response
Turn navigation:
The retained real-user Prompt descriptors exposed as numbered reading anchors.
Shift+Left/Right moves one anchor; /jump previews an anchor while its selector
moves, commits on Enter, and restores the exact prior Viewport on Escape.
Plugin-sourced user-role messages never enter this index — including the
template a Canned command expands into, whose echo is the Prompt that enters
in its place. That echo shares the ordinary Prompt's background and text inset,
including the fill on its text rows as well as its surrounding padding.
Prompt-top anchor: Display-only tail space that places a newly submitted real-user Prompt at the Viewport top while its response streams below. A Canned command's echo is placed the same way, for the same reason: it spends a turn, so a reply is about to fill the space. A command that only works the Chrome answers nothing and is written where it falls. It belongs to the live turn, not to a reading position: it scrolls with the transcript, so reading back and returning to the tail lands on the anchored frame again. It ends when the response fills the Viewport or the next Prompt takes it over, and is never part of Scrollback, selection, search, replay, folds, or redirected output.
Conversation timeline: The display-only one-column rail in the terminal's reserved rightmost column. Each visible tick maps to a retained Turn navigation anchor; the current tick uses the user colour, enabled arrows jump to the nearest Turn anchor above or below the Viewport top, and tick hover previews up to two real-user Prompt lines. Rendering and hit-testing share one frame geometry; modal surfaces hide the rail.
Content address:
A stable raw-content address derived from assistant message events: N names
the Nth non-empty assistant answer, and N:C names its Cth closed fenced code
block. /copy selects these newest-first; /copy N copies raw Markdown and
/copy N:C copies the fence-free source. Tools, images, rendering chrome,
Sticky turn headers, Rules, and ANSI styling never enter this index.
Avoid: screen row, rendered block number
Fullscreen viewer:
A transient reader over one Content address, opened by /view, /view N, or
/view N:C, and over unified-diff text, opened by /diff or by clicking a
Diff card whose body was capped. It replaces transcript and Chrome for the
lifetime of the modal, reflows raw Markdown, fence-free code, or diff text at
the current terminal size, and gives wheel, shifted arrows, Page, Home/End, and
Escape to reading. Diff text is coloured by what each line does to the file,
never by the language it is written in. Closing restores the exact prior
Viewport; the viewer never adds a Prompt, Fold, search hit, session event,
clipboard write, or pipe output. Off a TTY there is no modal: /diff writes
its lines and stays a line reader.
Avoid: pager process, transcript view
Reader hand-off: The raw text a Fold carries so a click opens the Fullscreen viewer instead of expanding in place. Only a Diff card that outgrew its 24-line body takes one — a short diff is already whole on screen. Ctrl+O is unaffected: expanding everything still expands this block inline, and the collapsed line names both, so the affordance never promises a gesture the block does not have. Avoid: pager payload, click target
Resume list:
What /resume offers. The workspace a person is standing in is the list;
every other folder is one row that opens the rest, because the session wanted
is almost always in the folder they are in. Rows are ordered by when the
session was last touched — not when it began — and each names its title, that
age, how many messages it holds, and, only for a session from elsewhere, the
folder it belongs to.
Avoid: session picker, history list
Region pointer:
A pointer press, release, or move on the rows below the transcript — the
Chrome, and the Overlay drawn just above it. A selection may refuse it
outright (keyboardOnly): an approval grants a tool for the rest of the
session and cannot be taken back, so no click may decide it and no Pointer
mark may suggest one could. Those rows belong to whatever
composed them, so a pointer there never reaches the Viewport: a row that
offers something acts on it, a chrome row with text selects the way the box
does, and a row that offers nothing does nothing rather than starting a
Viewport selection. A press commits only where it is released, so
sliding off before letting go takes it back. A gesture nonetheless belongs to
where it began, through release: a drag the Viewport anchored keeps reaching it
once the pointer has left, because sweeping past the last line and letting go
over the input box is how a person selects to the end of what they can see, and
a drag the box or a chrome row anchored keeps selecting the same way, clamped
into its text.
The Viewport keeps press-to-anchor, drag-to-extend, release-to-copy, and the
blank space under the last line anchors there too — a press with nowhere to
land is still where the pointer was resting, though only a press that landed on
a row can work that row's Fold. A press in the box that then moved is a Box
selection, not a Viewport one; a press on a chrome row that then moved is a
Chrome selection.
Avoid: click handler, hit area
List window:
Where an open list starts showing its rows. It follows the marked row, so the
❯ is always in view — except while the wheel has moved it, which is the one
state it holds independently. Any key brings it back to the mark: a list
scrolled away from what Enter would take answers a question nobody asked. Only
the wheel moves it, never the keyboard's own scroll, which is why a scroll
carries where it turned and one without a place is left to the transcript.
Avoid: scroll offset, viewport
Caret placement:
Where a click inside the box puts the cursor. Near misses clamp rather than
miss — a border row takes the nearest content row, a column outside the text
takes the nearest end of it — because the text inside a frame is a narrow
target and "just above the first line" is an ordinary intention. The inverse
reads the same wrapped rows and the same window the box drew, so the cursor
cannot land somewhere the box never showed, and a shell box's hidden ! is
given back. The press only has to land in the box: the release is the position
it means, because putting a cursor somewhere is not a thing to be undone. A
press that then moved is a Box selection, not this.
Avoid: click to focus, text hit test
Box selection: A mouse selection over the text inside the box. Press anchors, drag extends, release copies — the same gesture the Viewport gives the transcript, because mouse reporting has taken the terminal's own selection. A press that never moved is still Caret placement, and sliding off before any drag takes it back. The span stays marked until the next click or move; typing, paste, and delete replace it. Escape dismisses it before it means leave. Avoid: input highlight, textarea selection
Chrome selection: A mouse selection over a chrome row's painted text — the teaser, todos, subagents, working line, shortcuts overlay, empty-box placeholder, or status row. Press anchors, drag extends, release copies — the same gesture the Viewport gives the transcript and the box gives its typed text, because mouse reporting has taken the terminal's own selection. A press that never moved is still that row's click, if it has one, and copies nothing. The span stays marked until the next click or Escape; a rewrite of the facts clamps it into the new line. A selector, a completion, or an open panel row stays a click, because the mark is what Enter would take. Avoid: footer highlight, metabar selection, Status selection
Pointer mark:
The row a Region pointer rests on, shown as a dim · in the column ❯ marks
from. One column answering two questions that cannot be confused: ❯ is what
Enter takes, · is only where the pointer is. Kept apart on purpose — a
pointer often comes to rest somewhere nobody chose, and moving the mark would
change what Enter does as a side effect of where the mouse is. The completion
menu is why it is the marker column rather than an underline: the label
already underlines the fragment that was typed.
Avoid: highlight, focus
Plan progress:
How far a /ship run has got, read from the spec file's ## Plan checkboxes
rather than from the conversation — the spec file is the workflow's memory, and
its boxes are the only place the work is counted. The chrome re-reads that
file as tickets tick, and pins it as its own row (plan k/n · current ticket).
A Ctrl-C, idle stop, or later typed continue does not freeze that row: the
spec poll keeps following checkboxes until verified delivery retires chrome.
Verified delivery retires the phase chip, plan, Panorama teaser and overlay,
and the old Todo readout. Later session events cannot re-pin that completed
run or apply its Mission Contract to unrelated work. The final graph remains
available in the Web panorama, and its loopback URL stays on the chrome.
Interrupted, blocked, or failed delivery keeps its resumable progress. Explicit Todo and Subagents panels retain history.
The working line names a Workflow round
while one is in flight; it reports done/total and the first unticked ticket
only when no round is running, so the two rows never stack the same figure.
Avoid: todo list, task count
Workflow progress:
What a tool-workflow/* record becomes on screen. A run opens with its name,
each round prints one line as it settles, and the stop reason closes it. The
round still running is named in the working line instead, because the
transcript is append-only and cannot unprint a line when it ends. The line is
all a round gets: a workflow's children run in a worker thread, so their
sessions are never in this process and no click could enter one.
Avoid: workflow log, progress bar
Canned command:
A command whose body is a prompt template rather than a handler: /ship,
/init, and a person's own custom-commands entries. It spends a turn, so
what a person typed is echoed as a Prompt and the template itself never
reaches the transcript.
Avoid: macro, alias
Fold preference:
An ephemeral, per-Fold choice created by clicking a block or pressing Ctrl+O.
Explicit expanded and collapsed choices survive streaming completion, resize,
scrollback trimming, search, and later turns; moving on collapses only automatic
fresh-output states. Thoughts land folded, so moving on only folds a thought
the person opened without choosing that form by hand. Moving on is a turn
spent, a Prompt or a Canned command; an empty Enter, a command that only works
the Chrome, and a ! line are not turns and fold nothing. A clear, a session
replacement, or the return from a Child view discards preferences, and replay
creates capable but automatically collapsed Folds from durable events.
Avoid: session fold state, global expanded mode
Chrome: The bottom-pinned rows: input box, menus, the Queue and Todo readouts, hint row, status row. Never scrolls.
Context readout:
The status row's context used/window (N% left) segment. used is the
projected next-request pressure, falling back to the latest provider sample;
it is not cumulative session usage. The session projection supplies both
pressure and capacity, so replay restores them and compaction updates them.
Available figures stay visible at normal pressure; missing figures are ?,
and no segment appears before either is known. Remaining capacity is muted
above 25%, warning at 25% or below, and error at 10% or below. Narrow rows
omit shortcuts, workspace, and model before context or workflow state, then
shorten context to N% left if needed. /status retains the detailed counts.
Row:
One line of a frame, painted at a position of its own. A row is text and
nothing else: a control character inside one is not a character but a cursor
movement, and the width authority scores it zero columns — so a row carrying a
newline measures as a fit, paints its head where it belongs, and drops the rest
at column 1 of the row below. That row is usually one the frame diff considers
unchanged, so nothing paints over the spill and it outlives every later frame.
Text becomes a row by being cut to fit (Chrome, menus, cards) or wrapped to fit
(transcript); both flatten control characters first — the cut before it
measures, the wrap by breaking a row where a newline asked for one — and the
frame flattens again as it paints, for whatever composes a row next. A wrap
breaks between words: at a space, which the break takes, around CJK text
(never before closing punctuation or after opening punctuation), or after a
/; only a word wider than a whole row is cut where the row ends. A wrapped
line's later rows hang under its text — its indent, and a list marker with
the space after it — and that indent is chrome, as the Rule is: a copy leaves
it behind. Whatever is laid out to fill a transcript row (a table, a todo
readout) is laid out the Rule's two columns narrower than the content width.
Avoid: line (a transcript line may occupy several rows)
Fold:
A transcript block kept in both a summary and a full form, swappable in place
— a click anywhere in the one under the pointer works it, Ctrl+O works them
all: it opens whatever is folded, and folds everything once nothing is.
Thinking is a Fold that opens folded: a thinking… head stands while it
runs — ticking the same Braille frames as the working line, which names
the thought thinking — and becomes its clock (│ thought for 3.2s)
when it ends. Both heads occupy one row without vertical padding and without
a panel fill — a full-width thinking background on the clock is a black bar
between tool rows. The light-blue │ runs down the clock, the pads, and the
expanded deliberation. Only the expanded form keeps the fill and its
panel inset, behind a click or Ctrl+O. Every tool card is a Fold that opens
folded: one row naming the call, how much it produced (+n -m, N results,
· N lines), and whether it worked, with the body behind it and no panel
fill. The row is grey, as Grok CLI mutes a collapsed call: the call's name
takes the thought clock's dim, the success bullet is dim, and only the
trailing ✔ stays green, so the work recedes behind the answer it serves.
A failed row keeps the name in the terminal's own colour, names its reason,
and a non-zero terminal exit or a kill is a failure (✗), not a green pass. A finished answer is transcript:
it stays whole, a click does not work it, and the pointer resting on it
names nothing.
Avoid: collapse block, expandable section
Child view:
The nested Viewport of an in-process child's transcript. A Fold that names a
child Session is a view, and so is a row of the Subagents panel: a click or
Enter enters, Esc pops one level, and the child's thinking, text, and tool
cards stream the way they do on the parent. Entering covers the parent's
transcript rather than replaying over it — history, the scroll offset, and
open folds stay on a stack — and Esc uncovers that same buffer, so a person
can still read back. Parent events that land while looking keep writing into
the covered buffer. Its status row is the child's title — the roster's mark,
label, elapsed time, call count, and latest call, then Esc returns to the parent, which is cut last. From inside a view, another panel row swaps the
view rather than stacking it — every open level drops once the door is known
to open — so Esc still returns to the parent.
The view is read-only; typing flashes that Esc returns. Fork views skip the
inherited parent prefix. A runner-dispatched in-process child is a view
without a parent tool call — that Fold is not a subagent card, not a
parent-log line, and not in a Card run. A child that has finished and left
the store — every background child, the moment it idles — opens read-only
from its persisted log. Worker-thread Workflow children are not views —
their sessions are never in this process, so the round line never offers
click to enter. Off a TTY there is no Fold.
Avoid: catalog, inspector, pager, synthetic tool-call, Claim on the card
Subagents readout:
The chrome row counting the children the live session started, by state —
subagents 3 · 2 running · 1 done · Ctrl+H — for as long as the roster holds
any, except settled entries retired from the readout after verified ship
delivery. Those entries remain in the panel and /subagents report; running
children stay visible and retire when they settle, while new children appear
normally. The roster is surface state fed by subagent/start, each direct
child's own log (its subagent/descriptor names it; calls, turn starts,
turn ends), and subagent/end, never a query over the store, which forgets
a child that finished; grandchildren belong to the child that started them.
completed is done; error, max-tokens, refusal, and a blocked turn
(a refusal to the runtime) failed; aborted and interrupted stopped — an
unfinished child is never ✔. Its
clocks tick once a second while any child runs, on the roster's own timer.
A click on the row, or Ctrl+H, opens the Subagents panel in its place.
Dropped with the session on /clear and /resume.
Avoid: agent list, task pane
Subagents panel:
The roster opened in the readout's place: a header with the readout's counts
and Ctrl+H closes, then a numbered list the shape of the Queue panel, one
row per child — a mark (▶ and ✔ are the todo readout's, ✗ the failed
tool card's, ■ for a stopped child is this row's own), the label the
child's log gave it, its elapsed time, its calls, and the latest one; the
child on screen ends · viewing. ↑/↓, Tab, Home/End, and digits move the
mark; Enter or a click on a row enters that child's view; Esc or Ctrl+H
closes (inside a Ctrl+R search, Ctrl+H cancels the search instead). It
enters and nothing else — stopping a child is the model's own tool. One
open panel at a time with the Todo readout and the Queue panel. /subagents
prints the header without its key and the rows without the ❯ cursor.
Avoid: dashboard, inspector
Card run: Tool cards that follow one another share one stretch of transcript rather than each opening a panel of its own. Consecutive cards on a TTY stack flush — a failed row among them included — so a run reads as one segment, and the Block gap on either side is what sets it off; piped output still closes each card with a blank. A door into a child Session is the only extra row under a head, and a run of similar cards rebuilds as one fold over the rows already on screen. Any other block printed under a run ends it and opens one Block gap below it. A runner Child view at the tail is not in a run. Avoid: card group, merged cards, runner Child view as a tool card
Block gap:
The one blank row between any two blocks on a TTY. A block that does not
follow a blank — the first card of a Card run, a runner notice, an answer,
a compaction summary — opens with one under its own rule; an answer still
closes with its own. Thought clocks (and the thinking… head before one)
and tool cards are one stretch of work, as in Grok CLI: a card opens no
blank under a clock, and a clock none under a card or another clock, so
the stretch stacks flush and keeps one Block gap from the person's prompt
above it and from the answer below it. A step reads as its work, then its
text. The gap above a thought takes the muted Rule, not the thought's: a
coloured Rule marks only its own block's rows, evenly. Rows that continue a block take none
either: the later cards of a run, a card's click to enter door, a
workflow's round and stop lines under its head, an approval note under the
pending card it explains. A pipe closes each block with a blank instead.
Avoid: margin, padding, spacer
Rule:
The connecting │ drawn down every transcript row, including blank
separators that belong to a block. Colour — not a different character —
says what the row is: DeepSeek blue (the Theme's accent) for the person's
own message, its lighter tint for thinking, dim for a tool card or runner
notice, red for a failed one,
muted for assistant prose and system chrome. The person's blue frames their
message evenly: one padding row above the text and one below, in every
Theme, and comfortable density's extra blank between turns sits above that
panel under the rule of the row before it. Chrome, not content: it
repeats on wrapped rows and never reaches the clipboard.
Avoid: border, gutter, sidebar
Theme:
The palette every role, fill, and the terminal cursor are painted in, chosen
with /theme and modelled on Grok CLI's slot themes: deepseek (dark),
deepseek-light, terminal (the terminal's own sixteen colours, no bands —
the person's message is bold instead, between unfilled padding rows where
the DeepSeek themes have filled ones), and auto, the default, which follows
the OSC 11 background between the two DeepSeek themes. The brand is DeepSeek
blue #4D6BFE (accent: frame, the person's rule, markers, the mark, the
cursor via OSC 12) with its tint #7E96F5 (thinking, links, headings, paths);
every slot is RGB with explicit 256- and 16-colour overrides. The choice is
kept under theme in $DSH_HOME/code-cli-ui.json; CODSH_THEME overrides it
for one launch. Switching mutates the one theme record in place, so every
renderer holding a role paints the new palette from its next call.
Avoid: skin, colour scheme
Repaint:
Replaying the live session into a fresh transcript so rows already painted
take a new Theme — the chrome restyles at once, the history once nothing is
in flight: a running turn or an open Child view defers it to the turn's end or
the return to the parent. It rebuilds rendering only; approvals, the roster,
the queue, and clocks are untouched. Like a resume, it cannot show what never
reached the session log (command reports, ! output, notices), and expanded
folds come back folded.
Avoid: refresh, redraw
Scrollback notice: The row that says how far back the reader has gone and takes the click that ends it. Display-only, drawn over the Viewport's last row — under what is being read, never over it, and never a chrome row, which would move the input box while scrolling. A drag that starts on it is a drag, not a click.
Update check:
One cached read of the codsh-cli dist-tag, behind a two-second budget, that
can only ever add a dim line under the welcome naming the newer version. It is
never a chrome row (the chrome's height is what keeps the box still), never
blocks the boot, and never installs anything on its own. Asking is /update
inside a session or codsh update outside one; both run
npm install -g codsh-cli@<latest> in the open and then move the code
profile's runtime to match, so a profile that launches straight through dsh
never waits for a boot to catch up. The boot's registration remains the catch
for a runtime a bare npm install -g codsh-cli upgrade, or a failed move,
left behind. A leftover node_modules linked from another pnpm store (another
pnpm major, or a moved store-dir) is dropped and the registration retried,
because dsh plugin add is a thin pnpm add that otherwise refuses to run.
CODSH_UPDATE_CHECK=off silences the automatic check but neither
of those; CODSH_UPDATE_REGISTRY points every one of them at another
registry.
Flash: A short-lived notice that borrows the hint row and gives it back (e.g. the copy toast).
Hover readout: The chrome row naming the fold the pointer rests on — what it is, how many lines it withholds (the count its own row names; a door names none), whether a click opens, folds, or enters it — for as long as it rests there. Outranked by a flash, and it outranks the working indicator. It borrows that chrome row rather than adding one, so the box does not jump. Paired with a panel fill on every visible row of that block, the way opencode marks the block under the pointer; the readout is what still speaks when the head row is off the screen. Avoid: tooltip, status hint
Pasted image:
The clipboard image Ctrl+V attaches behind an [Image #N] token in the box —
one backspace removes the token whole, and a deleted token drops its image.
At submit an image-capable model gets it as a first-class attachment block. A
text-only model always gets the original saved under
$DSH_HOME/attachments/pasted/; an explicit CODSH_VISION_* sidecar adds a
verbatim description first, otherwise a deepseek-official text model borrows
deepseek-v4-flash-vision-exp for that description automatically. Failure
keeps the file-only path. The file context and any description ride the same
message so they survive --resume; the selected conversation model never
changes.
Avoid: upload, embed
Image preview card:
The card centered over the transcript while the cursor rests against an
[Image #N] token — it says what is attached, and shows it. A terminal with an
inline-graphics protocol is handed the image itself: Kitty graphics for
Ghostty, kitty, and WezTerm, OSC 1337 for iTerm2. Sending the protocol a
terminal does not implement fails silently, and a multiplexer forwards neither,
so both are read off the environment rather than assumed; whatever is left gets
a half-block mosaic, resampled in a child process so no native decoder is
loaded here. The payload never travels as row text — a base64 image measures as
thousands of columns and is cut mid-sequence by the width every row is fitted
to, which leaves the terminal eating the rest of the frame as string data — so
the rows reserve blank cells and the frame paints the picture over them at an
absolute position. Transcript around the card is dimmed so the picture is what
reads; the card itself stays undimmed. Ctrl+O and a click on the card open the original in the
platform viewer. Card and picture come down together: a Kitty placement is not
cell content, so clearing its rows would leave it on screen.
Avoid: thumbnail, attachment chip
Todo readout:
The chrome row that holds the agent's todo list — progress plus the item in
flight — for as long as a list is live, and the /ship plan when one is on
disk. A click anywhere in the readout, or Ctrl+T, opens the full list and
closes it again, the way a Fold works. Read from the todos projection and
the spec file, never remembered from the write.
Avoid: todo panel, task bar, progress bar
Queue:
The lines submitted while nothing was asking for one — a turn running, a
question open — held by the Prompt in the order they were typed, each a
Prompt, a ! line, or a / command with the images its tokens claimed.
Adjacent Prompts leave as ONE message, a blank line between them; a ! or
/ line is a boundary that keeps its place and leaves alone, so shell output
lands between the thoughts it separated. Shown as the ↳ queued: chrome row
(count, each line's first line, Ctrl+Q). Surface state, never the dsh inbox:
the inbox holds only Steers. An interrupt leaves it alone, and it goes as the
next message; Ctrl-C is always the interrupt. Escape dismisses overlays and
Child views and does not stop the turn.
Avoid: inbox, backlog, type-ahead buffer
Queue panel:
The Queue opened in its row's place — Ctrl+Q, or a click on the readout — as
a numbered list with the keyboard: Enter edits the marked line back into the
box (its images with it; refused while the box holds text), d deletes,
Shift+↑/↓ reorders, s Steers a Prompt while a turn runs, digits pick a row,
Escape or Ctrl+Q closes. One open panel at a time with the Todo readout. It
closes itself after an edit or a Steer and when the Queue empties; the box
receives no keys while it is open, and the pointer marks and clicks its rows.
Avoid: selector (which replaces the box and settles once), menu
Steer:
A Prompt handed to the RUNNING turn instead of the Queue — Ctrl+Enter from
the box on a kitty-protocol terminal, s in the Queue panel anywhere — via
the agent's steer, which delivers it at the next step boundary. Shown as the
↳ steering: chrome row until the agent claims it, when it renders as a
Prompt block like any other. A turn that ends without taking it, an interrupt,
or a session switch reclaims it to the head of the Queue, so nothing typed is
lost. A ! or / line cannot Steer; it joins the Queue.
Avoid: inject (dsh's model-facing context), interrupt, follow-up
Question batch:
One ask_user_question request, owned by TerminalQuestions. Each answer is
stored by its position in the request; ← revisits an earlier question and →
returns to the next visited one. Navigation outcomes are separate from answer
data, so literal write-ins such as back, next, and edit remain text.
The append-only Transcript receives one final summary per answered question
when the batch settles, never intermediate revisions. Each summary is drawn
with the tool │ so the left rule continues through the answers
instead of breaking where the person replied. Esc/dismiss or EOF
ends the remaining batch; cancellation never opens another card. Accepted
answers remain and unanswered questions return empty selections.
FrontierCard owns the compact ship · grill presentation; ordinary choices
use Selector. Both restore all submitted selections and write-ins on a
revisit, retain a write-in draft while moving between options, and use Space
to toggle multi-select choices and Enter to submit (the focused choice if
nothing is checked). ←/→ edits the caret within a write-in before navigating
at its boundary. Frontier uses ❯ for focus and [x] for checked choices,
including without color. It wraps question context, the focused option's
explanation, and navigation hints. Explicit write-in labels identify inline
fields; incidental words in descriptions cannot convert concrete options
into text inputs. A grill dismissal does not abort Ship or approve a gate.
Avoid: navigation strings encoded as custom answers, per-visit transcript writes
Ship gates:
The two approvals in the /ship workflow — the confirmed spec file (gate 1)
and the approved ticket breakdown (gate 2). The runner auto-Confirms both with
a transcript notice; interrupt still aborts. Gate 1 Confirm seals the Main Track
and acceptance criteria. After seal there is no Edit modal; a sealed-track
contradiction is a ## Blocker. Everything after gate 2 is autonomous. Wayfinder
precedes grill as a planning-only contract: destination, named decision map,
dependency-linked decision tickets, and a local-Markdown fallback when no
tracker is configured. Charting no longer drops the person: the same /ship
invocation continues after HITL. A named map is always charted; an empty inner
ring is valid. Decision-ticket HITL still wakes the parent one unblocked
non-research ticket at a time. Unresolved work remains wayfinding;
explicit confirmation of a clear route advances to grilling. A small clear
route records a confirmed no-map handoff. The ledger's ## Wayfinder section
links the canonical map and its decisions, not a duplicate implementation plan.
Each /ship turn injects the bundled contract for that phase. The model must
not look up, read, or invoke a wayfinder, grill-me, to-spec, to-tickets, or
tdd skill. Grill then runs from that injected contract (recon, design tree,
frontier rounds with recommended answers, exhaustion handshake); to-spec and
to-tickets then run from theirs (exhaustive stories, vertical tickets with a
DAG and per-ticket acceptance, .scratch/ plus tracker when configured)
without another interview. Landing follows the injected TDD contract: one red
test witnessed failing, then minimal green, then the suite. Each /ship turn
injects only the phase the spec's Status: names, avoiding injection of all
phase instructions at once.
Earlier conversation remains in the parent; fresh-context children keep
independent investigation and implementation output out of that history. interviewing still means to-spec for existing files; later
status meanings remain unchanged. The MetaBar chip follows that Status
(ship · wayfinder / grill / spec / tickets / land k/n). The runtime
binds one spec for phase, goal, UI, and completion; several unfinished specs
open a selector, and a pipe refuses the ambiguity. The spec file is the
workflow's memory, not the conversation: the user's wording lives in
## Original Requirement, distinct from the compact Main Track; approved
tickets live as checkboxes; the Status: line names the phase; a baseline run
is recorded before any code; each green ticket is committed; and a bare /ship
resumes unfinished work without blanking the original requirement. Adjacent
<spec>.ship.json is runner-managed persistence of the original requirement
plus sealed Main Track and acceptance criteria at gate 1 when present. Later
phases and resumed runs check that snapshot at phase boundaries; mismatch or
corruption is a stop, not an accepted rewrite. A first snapshot cannot verify
earlier history — limited protection, not a tamper-proof sandbox. Plan mode
writes no snapshots. Coverage is original requirement → Track-N → acceptance →
ticket → evidence. One module owns that memory for a session — Plan progress,
the MetaBar chip, the spec poll, occupancy, the sealed-track snapshot, the
Mission Contract, and the canned phase loop — so the runner only begins, notes a
write, or aborts. Occupancy and dirty-tree preflight still ask on a TTY.
Occupancy is a Selector, not a third gate. Chrome stays
the MetaBar chip, plan row, and Panorama teaser; there is no GoalBar.
Avoid: checkpoints, review steps, GoalBar, in-session landing, process-only snapshot
Original Requirement:
The user's wording, kept in the spec as its own section. Clarifications refine
the design; they never silently replace the original request. Distinct from
Main Track. A bare /ship may omit a new idea; it must not blank this section.
Avoid: live rewrite, idea slot as the only memory
Ship snapshot:
The adjacent <spec>.ship.json the runner owns. It records the original
requirement and, after gate 1 Confirm, the sealed Main Track and acceptance
criteria when those sections exist. The model must not edit, remove, or
regenerate it, and children must not be asked to. Commit it unchanged with
the spec so resumed checkouts retain the comparison baseline. Checks run at phase
boundaries and on resume; field values are compared independently of JSON key
order. A missing, changed, or corrupt snapshot stops the run. A first
snapshot has no earlier history to compare, so protection is limited — not a
security sandbox. Plan mode writes none. Identity, snapshot, and phase checks
plus review and real proofs are the guardrails; semantic zero drift is not
claimed. Distinct from the Mission Contract JSON under .scratch/<slug>/,
which compiles richer control-plane ids from the same seal.
Avoid: process-only snapshot, live reread, tamper-proof, security sandbox
Main Track:
The compact compass /ship writes into the spec: the one-sentence idea,
numbered Track-N grill decisions, and Out of Scope — not the full spec and
not the Original Requirement. Gate 1 Confirm freezes it together with
acceptance criteria. Later phase turns and resumed runs are bound to that
sealed content via the runner snapshot, so landing cannot rewrite the design
to match what it already built. A needed contradiction is a blocker, never a
silent spec edit. Progress (Status, checkboxes, proof logs) remains writable.
Avoid: live reread, silent rewrite, GoalBar
Decision ticket:
A wayfinder child of the named map — a question, not a build slice. Its graph
key is the tracker's native id: decision:github:owner/repo#n, or
decision:local:NN from .scratch/<slug>/wayfinder/NN-slug.md when no
tracker is configured. Existing decision-NN-slug.md names resolve to the same
integer identity; duplicate integers across both forms are errors. Status,
Type, and Blocked by accept plain lines or Markdown list labels; closed
and resolved both close a decision, and wayfinder: type prefixes are accepted.
Claim updates replace the existing Status and never reopen a closed decision.
Distinct from a Landing ticket; it does not become a Track-N.
Avoid: implementation ticket, graph uuid, plan checkbox
Landing ticket:
A Gate 2 tracer-bullet ticket. Its graph key is landing:N from the
Ticket N: prefix on the spec ## Plan checkbox. After Gate 2 Confirm, N
is never renumbered or reused; checkbox order may still follow the DAG. The
scratch file is .scratch/<slug>/issues/NN-slug.md with the same integer N;
a published tracker issue is a locator (Issue: owner/repo#n), not a second
node.
Avoid: plan index, Track-N, Decision ticket
Track anchor:
A sealed Main Track decision as a graph node (track:N). REQ-* is an alias,
not a second key. Landing tickets hang off one or more anchors (many-to-many).
Wayfinder tickets stay a separate inner ring; during wayfinding there are no
Track anchors yet.
Avoid: edge label, inner-ring node, Main Track hub
Ship graph:
The canonical graph of one bound spec. The TTY projection groups Decision tickets (inner ring),
Landing tickets (outer ring), and Track anchors. Rebuilt from those canonical
sources into adjacent <spec>.ship.graph.json; the cache is not identity.
Missing or corrupt cache is discarded and rebuilt; a join failure against
canonical sources stops the run. Ticket nodes carry a derived Claim token;
decision nodes may carry ticketType. Edges are blocked-by and
hangs-off only. Map, spec, Idea, worktree, child, Claim, Blocker,
ACC-*, Panorama overlay, Panorama teaser, and Web panorama are not
nodes. A named-map locator (Canonical map, [Map], wayfinder:map) is
not a Decision ticket; local .scratch/<slug>/wayfinder/NN-*.md children
still join when that pointer is present, and the named map file's GitHub
children join when listed there. A confirmed no-map route has an empty
inner ring, not a fourth node kind. NN in filenames is an integer
(01-foo.md → 1). The join re-runs as soon as a canonical source is
written, so the overlay, teaser, and Web panorama show a new ticket
without waiting for the next phase.
Avoid: sidecar as source of truth, title matching, freeze-sidecar stop, fourth node kind
Ship delegation:
Fresh-context subagent is the default for investigation, research, ticket
implementation, and independent review. Prefer subagent, not
subagent_fork: copying the parent conversation defeats isolation. The parent
keeps questions, gates, coordination, and independently re-runs final proofs.
Children return at most 20 lines naming the result plus evidence/log paths.
The working tree is shared: read-only work may run in parallel; writers and
git mutations stay serial. Landing is a Landing wave: it dispatches every
currently unblocked, unclaimed ticket in parallel worktrees, including
siblings that share a closed prerequisite; the parent
serial-merges Ready-set then proves. An in-process child the runner dispatched is a Child view
without a parent tool call. Whenever the parent Viewport is shown, one
door per live Session that names the ticket's graph key, at the tail, in
graph-key order. Head is Ticket N: <title> or the decision name, with
· conflict / · repair only when one node has two Sessions, then
click to enter. The runner keeps that Session until it releases the
child; a leftover worktree with no Session is panorama 已认领, not a dead
door. Claim does not store a Session id. Decision-ticket HITL still
wakes the parent one unblocked ticket at a time. An unresolved ## Blocker
stops automatic continuation until archived; there is no landing
turn-budget breaker. Cascading re-verification unticks only tickets whose
proofs failed, plus already-closed DAG dependents of those tickets — never
every later-N. /ship never calls Ralph; its tool remains available
outside the workflow for explicit requests. Missing delegation is stated
as a limitation, not claimed as a child that ran.
Avoid: in-session landing, fork history, semantic zero drift, subsequent-untick, parent-log door
Mission Contract:
The machine-checkable control-plane memory Gate 1 Confirm compiles from the
sealed Main Track, Out of Scope, and acceptance criteria — REQ / NEG / ACC
ids with Track-N aliases — written to .scratch/<slug>/mission.contract.json.
Distinct from <spec>.ship.json, which snapshots the original wording and
sealed Main Track / acceptance for resume comparison. The Markdown spec stays
the human projection; the runner owns the JSON and prepends a compact summary
on later phases. After seal, write tiers apply: Main Track / Out of Scope /
grill decisions / original requirement / acceptance criteria / contract JSON
are immutable (protected writes are refused; external drift stops the run
and remains on disk for inspection); Implementation Decisions are semi-mutable (blocker required); Status,
Plan, Baseline, and Verification are mutable world state. A land-phase HITL
wake prepends the bound spec and the in-flight / Ready-set set, not a single
Active Ticket line. An Alignment Gate refuses
writes that lack requirement mapping or hit immutable memory. Those denials,
and a Drift Detector flash, are runner notices drawn with the tool │
so the left rule continues through them instead of breaking on every
line. While a
Conflict-resolution child is live, that gate does not apply to its
writes — mapping, invented supports, and active-ticket Track checks
stay off so marker fills are not refused as unmapped landing work;
immutable control-plane paths are still refused. A Drift
Detector scores plan/action drift against the seal and treats a rewritten
Main Track as a blocker, not an accepted rewrite. An independent
Verifier matches acceptance criteria to recorded evidence and reconciles
premature plan ticks at final verification, not after each ticket write;
delivery and the ship goal do not complete without that evidence.
Avoid: hand-authored JSON, prompt-only freeze, GoalBar, wording-snapshot substitute
Conflict-resolution child:
A fresh-context child that resolves git-named conflicts in the merge-target
tree during serial landing or delivery Merge-back. Text hunks preserve the
surrounding content; lockfiles/generated artifacts are regenerated, and
modify/delete or binary conflicts use both versions and ticket intent.
Validation failures feed back to a fresh child, up to three attempts per
merge, without aborting between attempts. Already auto-merged files form a
baseline, not an out-of-scope write. Sealed requirements stay protected.
The runner owns staging, non-interactive commit, and rollback (squash uses
reset --merge, not merge --abort). Unchanged markerless conflicts require
explicit confirmation on retry, not a silent choice of ours. Related staged
additions may be reconciled when Git represents a rename as modify/delete.
Rollback failures are reported without creating a blocker commit in a pending
merge. Success resumes landing and proof.
Avoid: merge bot, TDD repair, conflict agent
Worktree branch:
The dedicated git ref for a ticket worktree: wt/<slug>/<directory>, where
the directory is the filesystem form of the graph key (landing-N,
decision-<github-number>, decision-<local-NN>). Distinct from the feature
branch ship/<slug> — git cannot nest ship/<slug>/landing-N under
ship/<slug>. Never create a parent ref wt/<slug>. Resume identity is this
ref, not a detached HEAD. After any merge commit into ship/<slug> the
directory and ref are removed; abort keeps both under the same names. Nested
checkouts are ignored in the parent via .scratch/<slug>/worktrees/.gitignore.
The land → prove → tick/Blocker protocol applies only to landing-N.
Avoid: ship/<slug>/landing-N, detached worktree HEAD, wt/<slug> as a ref
Worktree commit: The runner's one commit on a landing Worktree branch, subject `Ticket N:
<title>`, Track-N in the body. Children never commit. Author is the host `user.name` / `user.email`. Distinct from the `--no-ff` land merge onto `ship/` and from the Tick commit that follows a green proof. _Avoid_: child-authored commits, synthetic `codsh` authorTick commit:
The commit on ship/<slug> after a green parent proof of a serial-merged
ticket: ticks the plan checkbox, appends Verification, leaves Claim. Distinct
from the --no-ff merge that landed the worktree (kept even when proof is
red) and from a red-proof commit that only writes ## Blocker. Interrupt
writes no git commit and no ## Blocker. A proof sweep that both ticks and
records a Blocker is still one commit — not two.
Avoid: amending the merge, ticking inside the merge message, two commits per sweep
Merge snapshot:
A timestamped dump of a mid-merge abort — unmerged paths, git output, and
partial hunk fills — at
.scratch/<slug>/merge-snapshots/<directory>/<utc>/. Landing uses the
worktree directory name; Merge-back uses delivery. Interrupt and
Blocker-class abort share that layout; a later retry does not overwrite.
The directory is gitignored in the parent except that a Blocker-class dump
is force-added with the ## Blocker commit; an interrupt dump stays
untracked.
Avoid: a single overwritten dump, storing the dump inside the worktree
Claim: The panorama bucket of one Decision ticket or Landing ticket: 待认领, 已认领, or 已关闭. Distinct from Occupancy. 已认领 means the ticket is taken, not that a child is running. Canonical writes stay on the tracker or scratch file; the Ship graph cache only copies a derived token. Avoid: Occupancy, graph node, fourth bucket, live-child, second claim store
Landing wave:
The standing set of in-flight landing worktrees plus the serial merge queue
on ship/<slug>. Not a barrier that waits for every child to finish.
Dispatch takes every currently unblocked, unclaimed landing ticket. Siblings
of one closed prerequisite start together; a numbered Blocked by: 1, 2
next to Blocked by: 1 is that fan-out, not a second wait.
Avoid: batch barrier, Track-N order, one Active Ticket, numbered chain
Ready-set:
Finished 已认领 landing children whose DAG blockers are already 已关闭.
Serial-merge order is lowest landing:N in this set. A keep-commit that
stayed [ ] does not unblock dependents. Distinct from git conflict and
from ## Blocker.
Avoid: completion order, strict earlier-N wait, Track-N order
Last proof:
Scratch field Proof: green or Proof: red beside Claim: claimed. A
green Tick writes green; a failed parent proof writes red; unticking a
still-green dependent does not change it. Omitted until the first parent
proof. Distinct from spec ## Verification.
Avoid: sidecar, Blocker body as identity, parsing Verification prose
In-place repair:
A TDD child whose cwd is the parent ship/<slug> tree, used when that
landing ticket already has a land-merge commit and no worktree. Distinct
from a worktree TDD child. New worktrees wait until this child is idle.
Avoid: second land merge, overlapping drain writers
Occupancy:
Before the first /ship phase turn, if an unrelated current /goal exists,
the runner pauses it then asks a Selector titled ship · occupancy —
Replace or Abort. TTY Esc/cancel is Abort: resume the paused stranger and
stop /ship. Off a TTY, auto-Replace. A goal is ours when its id matches
the spec Goal-Id: or its objective starts with [ship]; ours is reused
without asking. Occupancy is not a ship gate and not a ticket row.
Avoid: occupancy gate, silent steal, GoalBar, Claim
Panorama overlay: The fullscreen TTY projection of one bound spec's two-ring graph: inner ring then outer ring, 待认领 / 已认领 / 已关闭 on the ticket row, Track-N as a suffix. The title row also carries the loopback Web panorama URL when one is bound. Binding a graph keeps the teaser; Ctrl+G or a click on that row opens the overlay, and Esc, Ctrl+G, or a click folds it back. Exclusive with Queue/Todo. Distinct from the Panorama teaser and from the Web panorama. Not a graph node. An empty inner ring is still this overlay. Avoid: Track-N grouping, hub row, Queue-style window, fourth bucket
Panorama teaser:
The one-line TTY chrome of the same graph — 待认领 n · 已认领 n · 已关闭 n,
plus in-flight when greater than zero, plus the loopback Web panorama URL
when one is bound — above the plan row. In-flight is the live Landing-wave
child count; a graph rebuild must not drop it to zero while a Child view
Fold is still on screen. Distinct from Occupancy, the MetaBar land chip
(closed/total), and the Web panorama itself.
Avoid: Occupancy, fourth bucket, land chip, plan row
Web panorama:
The loopback React Flow projection of one bound spec's Ship graph. Six
ordered layers: Wayfinder, Grill, Spec (Gate 1), Tickets (Gate 2), Landing,
and Done. Each phase is a parent sub-flow containing descriptive steps;
Decision tickets belong to Wayfinder, Track anchors to Spec, and Landing
tickets to Landing. Original requirement and Ship goal nodes precede Wayfinder;
question/answer nodes belong to the phase in which the user was asked. Context,
answer, phase, and step nodes are view-only, not new graph kinds or Claim buckets.
Graph metadata carries original wording, the overall objective, and recorded
answers without changing canonical ticket identities. The runner saves human
question/answer history in adjacent <spec>.ship.answers.json, independently of
the disposable graph cache; resume restores it. Explicit local decision Question
and User answer fields can supply historical answers. Research resolutions and
automatic gate approvals are not user answers. Missing responses stay explicit.
Web interface labels are English; source text remains verbatim, including its
original language. The page title and heading are the typed original requirement
after /ship, falling back to Ship Flow when none is recorded. Long text scrolls
within nodes and remains complete in details.
The optional graph status comes from the ledger; an absent status stays
unknown, and step descriptions never claim independently observed completion.
Node fills, borders, and labeled badges distinguish current (blue), passed
(green), upcoming (gray), and unrecorded (purple/dashed) phase states. Ticket
claims use gray/amber/green for unclaimed/claimed/closed; Track anchors use
teal, never a ticket claim. The navigation and status legend use the same
palette. Ticket claim colors appear only in the status legend, not as a
duplicate count list in the left sidebar. Descriptive steps remain neutral, without inferred completion.
Compact cards reserve explicit horizontal and vertical connection gaps; ticket
relations are orthogonal. Cross-phase, skipping, reverse, cyclic, and skipped-rank
edges use dedicated lanes outside the node columns, split left or right by the
source column so two prerequisites never share one vertical run. Concurrent exits
from one rank and concurrent entries into one rank use staggered horizontal
channels. Adjacent same-column tickets keep the prerequisite arrow in the ticket
lane; within-phase DAG edges that skip a rank also leave the columns so labels
do not sit on cards. Landing tickets use a ranked DAG with extra column
and rank gaps rather than a dense 3-wide wrap. Each decision ticket sits on the
left of its answers, which stack on the right of that cluster, so the
parent-to-answer edge is a short straight horizontal link at ticket mid-height
even when the answer card is taller. CJK source text sizes cards by display
width, not Latin character count. Unlinked answers stay a vertical sequence;
when they follow ticket columns they join from those columns rather than from
the last workflow step. Sibling dependents of one prerequisite share a rank and
fork sideways instead of stacking. Research tickets do not invent user-answer
cards. A local Wayfinder ticket's answer copy and a captured human response
share one card when they match uniquely: exact question/answer text, or the
same nonempty, non-generic answer when the question was translated or reworded.
The card keeps the ticket identity and the captured question, source, and detail.
Ambiguous matches, short confirmations, changed answers, and separate phases
remain separate; the canonical answer history is never rewritten by this view.
The current phase and its claimed tickets pulse; incoming phase sequence and
within-phase guide edges flow in the arrow direction. Incoming ticket relations
animate only for current-phase claimed targets and closed prerequisites (or Track
anchors). This represents ledger/claim activity, not observed worker or step execution.
Completed, upcoming, and unknown phases stay still; disconnected snapshots and
reduced-motion preferences suppress animation. Collapsing retains aggregate activity.
Flow arrows reverse the cache's relation direction: prerequisite → dependent,
Track → supported ticket. Layers expand/collapse without changing graph keys.
One 127.0.0.1 ephemeral port serves the HTML, bundled browser assets (no CDN),
and /graph.json for the TTY session. The page polls the live graph without
rebinding when /ship continues or a spec appears; zoom and selection survive
updates. Connection failures retain the last graph with an automatic retry notice.
The URL is pinned on the Panorama teaser and overlay title; /ship does not
open a browser. Off a TTY the URL is printed once. On narrow screens, node
details sit below the graph.
Distinct from the Panorama overlay and the Panorama teaser. Not a second store.
Avoid: dashboard, hub, site, graph UI, second port per /ship
Hybrid compass:
The spec stays durable memory; the harness /goal is a disarmed session
compass whose objective is [ship] plus the Main Track. The /ship runner
may auto-continue after HITL and inside landing; /goal stays disarmed, so
a generic goal-round cannot fight the current phase. During a run, /goal
shows that compass; it stays the ordinary human command, not a canned
/ship-style prompt. Missing or throwing goal service degrades: spec+prepend
still binds later phases.
Avoid: armed continuation, canned /goal, second scheduler
Reference Agent: One of the agent CLIs codsh aligns against. For the interactive TUI, Grok CLI is primary and wins ties; Claude Code decides where Grok has no equivalent, then opencode, with Codex CLI and gemini-cli as corroboration (ADR-0002). Outside the TUI, Claude Code still wins ties (ADR-0001).
Alignment Matrix:
The in-repo table (docs/alignment.md) of every interaction/feature gap and
its state. The pipeline's memory and the definition of done.
Behavioral Probing: Driving a real Reference Agent in a PTY with the VT emulator and diffing its observable behavior against codsh. Used only when knowledge and source reading leave a dispute.
Batch:
One user-initiated autonomous run of the pipeline: pick open matrix rows,
implement, pin with tests, verify in the real TUI (PTY e2e or
MOCK=… pnpm run dev), sync, changeset, report.