feat: the terminal answers what the operator came to ask - #7
Merged
Conversation
Five measured complaints, all the same complaint: paddock knew the answer and printed something else. An agent herdr never named was labelled `w3:p1`. Correct, and useless — it identifies the pane and says nothing about the work, so a machine where nothing had been named rendered a dashboard of coordinates. The fallback is now basename(cwd), which reverses the letter of a rule this project has had from the start, so read what that rule protected: the failure was two rows rendering IDENTICALLY, and cwd was banned because alone it cannot promise otherwise. toAgents supplies the promise — `project`, then `project p1`, then `project w1:p1`, climbing only when the rung below is ambiguous — so the guarantee holds and the ban is no longer what enforces it. It decides across the whole list because uniqueness is not a property of one row, and recomputes every reconcile so a suffix appears and leaves with the agent that caused it. A label that mutates was the accepted cost: AgentChip renders the name ALONE, so the alternative was an idle section of five identical chips, five controls with no way to know which one you are about to tap. Decision 15. Hints named commands in three conventions at once — bare, 'quoted', `backticked` — which is the same as naming them in none, because no one of them was reliable enough to read as a delimiter. Backticks everywhere now, and colour only DECORATES: term.test.ts asserts that stripping every escape returns the plain line, so NO_COLOR, a pipe, a CI log and a screenshot in an issue all carry the same information. Colour is decided per stream, not per process, because `paddock status > out.txt` must not put escape bytes in the file while stderr keeps its own. Applied at the print boundary rather than threaded through portInUseMessage, herdrUnreachableMessage and shapeMessage, whose whole value is that they can be asserted without a tty. Boot printed four lines of bookkeeping and then the URL. Every one of those lines is true and worth logging; none is what someone starting a dashboard is looking for. The URL cannot simply move up — the port is not bound until later, and a URL printed before the listener exists is a lie every time the bind hits EADDRINUSE — so the diagnostics moved instead, collected during boot and emitted as one line above the banner. Nothing is dropped: after end() every call site logs individually exactly as before, which is what the no-swallowing rule depends on. A `broken` shape is deliberately never folded in; compressing the loudest failure this project has into a clause would put it in the quietest typography it owns. A new release reached the dashboard and never the terminal. latestKnown has ridden the heartbeat to HostHeader since the check existed, rendered dim on purpose because an available update is not an alarm — but paddock is STARTED in a terminal, and nothing printed there ever mentioned a new version, so finding out required already looking at the dashboard for another reason. Announced when the answer arrives rather than at banner time: the check is async and caches for a day, so printing only at banner time would have silently dropped whichever half of that race lost. And cloudflared's log fought the tunnel display for stdout. startTunnel logged every line for the life of the child while draw() repainted with ESC[H ESC[J once a second, so a line landed wherever the cursor was and the next repaint erased it. Measured with the line that matters most — graceful shutdown due to signal interrupt — flashing under a block still reading `tunnel up`, because ^C signals the whole process group and reaches cloudflared before paddock clears its timer. The lines are held, not silenced: this project does not swallow errors and cloudflared's stderr is the only place a tunnel failure explains itself, so the tail is printed on every failing path, where `cloudflared exited N — the URL is gone` used to be the entire explanation. Only on a tty; a piped run has no repaint to collide with and its log is the only record there will be. Teardown now clears the block, because a stale frame asserting the tunnel is up is worse than no frame at the moment it stops being true. 849 pass, 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tale Two fixes in the section a new reader actually reads. `paddock tunnel` was documented only under "from your phone", four screens below the install steps, so the fastest path to the thing paddock is FOR — seeing it on a phone — was invisible to anyone following install top to bottom. It now sits with `start`/`status`/`stop`, cross-linked rather than duplicated: the security posture stays in one place, because a quick tunnel's one gate being a pairing code is not a caveat to restate loosely. The herdr-version section still claimed protocol 19, herdr 0.8.0, and that paddock "refuses to start on a mismatch". All three went stale in v0.7.0, which made the check directional — a newer herdr is accepted and verified by the FIELDS paddock reads, and only an older one is refused. Left as it was, the document told operators that the exact upgrade v0.7.0 exists to permit would break their install. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two faults, one symptom: an operator a version behind did not know. The check fired ONCE, at index.ts:245, and latestKnown was frozen from that moment. `paddock start` and a dashboard left open on a phone are the two documented ways to use this thing, and both are exactly the long-lived case that could never learn about a release published afterwards — the dashboard's notice and the terminal's both faithfully displayed a value that had no way to change. A paddock started on Monday reported Monday's view of the world on Friday. scheduleUpdateChecks now re-asks hourly. The timer is NOT the rate limit: checkForUpdate's 24h on-disk cache is, unchanged, so GitHub is still contacted at most once a day and PADDOCK_NO_UPDATE_CHECK=1 still disables the lot. 23 of every 24 ticks are a small local file read. Deliberately not a 24h timer — a tick the same length as the cache window drifts against it, and landing one millisecond early means waiting nearly two days. It takes a factory rather than a CheckOpts for the same class of reason: a captured `now` would make every tick look like the same instant, so the cache would never appear to expire, which is the original bug wearing a timer. A rejected check is reported and the schedule SURVIVES it; a laptop offline for one tick must not stop checking for the rest of the process's life. Cleared on SIGINT/SIGTERM so a tick cannot land mid-teardown and print a notice under the shutdown report. The terminal now names a version discovered mid-run, not only one already in hand when the banner printed, and says it once per version rather than once per tick. The notice itself was one dim 10px line in HostHeader, among other dim metadata, on the recorded reasoning that `paddock update` is "something the operator runs when they feel like it, not an alarm". The reasoning was right and the placement was wrong: a line the colour of its neighbours is not read, it is skipped. It is now ReleaseBanner, and the old line is GONE rather than left beside it, because two copies of one fact is how they drift. Dismissable, which no other banner here is, because of an asymmetry unique to this one: you cannot act on it from the device you are reading it on. `paddock update` runs on the host, which is by definition not the phone. A notice you can neither act on nor silence is how a dashboard teaches you to ignore banners — and the next one is the connection banner, which does mean an agent's state may be wrong right now. Dismissal is keyed by VERSION, not a boolean: a boolean would make the first dismissal permanent and the feature would quietly stop existing. HostHeader loses `latestKnown` as a prop. Its doc argued the prop was required so that dropping it would be a type error, which was a good guard — but a required prop nothing renders is that same trap pointing the other way. The guarantee moves to App.tsx, the one caller that has the value. The two HostHeader tests that asserted the dim line moved to the banner rather than being deleted with it, and app-update-notice.test.tsx keeps its subject — a value off the wire reaching the screen — with a new destination. Decision 16. 861 pass, 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six measured complaints from running paddock on a second machine, all the same
complaint underneath: paddock knew the answer and printed something else.
What changed
An unnamed agent gets a familiar name. herdr does not require a name, and
the fallback was
pane_id— so a machine where nothing had been named rendereda dashboard of coordinates like
w3:p1. Correct, and useless.This reverses the letter of a rule the project has had from the start, so the
reasoning is worth checking. The failure that rule protected against was two
rows rendering identically, and
basename(cwd)was banned because on itsown it cannot promise otherwise.
toAgentssupplies the promise —project,then
project p1, thenproject w1:p1, climbing only when the rung below isambiguous — so the guarantee holds and the ban is no longer what enforces it.
Recorded as decision 15;
gotchas.mdamended rather than contradicted.A label that mutates as agents come and go was the accepted cost.
AgentChiprenders the name alone, so the alternative was an idle section of five
identical chips — five controls with no way to know which one you are about to
tap.
Backticks delimit what you can type. Hints named commands in three
conventions at once (bare,
'quoted',`backticked`), which is the same asnaming them in none. Colour only decorates:
term.test.tsasserts thatstripping every escape returns the plain line, so
NO_COLOR, a pipe, a CI logand a screenshot in an issue all carry the same information. Decided per
stream, so
paddock status > out.txtcannot put escape bytes in the filewhile stderr keeps its colour.
Boot leads with the URL. It printed four lines of bookkeeping first. The URL
cannot simply move up — the port is not bound until later, and a URL printed
before the listener exists is a lie every time the bind hits
EADDRINUSE— sothe diagnostics moved instead. Nothing is dropped: after
end()every call sitelogs individually exactly as before, which is what the no-swallowing rule
depends on. A
brokenshape is deliberately never folded in.A new release reaches the terminal.
latestKnownhas ridden the heartbeatto
HostHeadersince the check existed, dim on purpose. But paddock is startedin a terminal, and nothing there ever mentioned a new version. Announced when
the answer arrives rather than at banner time, since the check is async and
caches for a day.
cloudflared stops fighting the tunnel display for stdout.
startTunnellogged every line for the life of the child while
draw()repainted withESC[H ESC[Jonce a second, so lines flashed and were erased. The lines areheld, not silenced — cloudflared's stderr is the only place a tunnel failure
explains itself, so the tail prints on every failing path, where
cloudflared exited N — the URL is goneused to be the entire explanation. Teardown nowclears the block:
^Csignals the whole process group and reaches cloudflaredfirst, so the display was repainting
tunnel upover a tunnel already dying.README:
paddock tunnelmoved into the install flow, and the stale"protocol 19 / herdr 0.8.0 / refuses to start on a mismatch" claim corrected —
all three went stale in v0.7.0, which made the check directional.
Verification
make check,make check-clean, 849 pass / 0 fail.Verified live against herdr 0.8.2: the boot block collapses to one
herdr:lineabove the URL, and the previously unnamed agent in this repo now labels from its
directory instead of a pane id. Colour verified under a pty and absent under a
pipe.
Not verified
paddock tunnelhas not been run live — it publishes a public URL. The fournew tests cover buffering, the 50-line cap, the failure dump and the teardown
clear, but not the real
^C-reaches-the-process-group timing that produced theoriginal report. Being tested on a second machine before this merges.
Also flagged, deliberately not fixed here:
herdr: new agent detected, resubscribingfires after boot having already reported its pane count, whichsuggests a redundant supervisor round-trip. Unrelated to this branch.
🤖 Generated with Claude Code
Added after review: a release nobody hears about is not a release
Two faults behind one symptom — an operator a version behind did not know.
The check fired once, at boot.
latestKnownwas frozen from that moment, soa paddock started on Monday reported Monday's view of the world on Friday.
paddock startand a dashboard left open on a phone are the two documented waysto use this, and both are exactly the long-lived case that could never learn.
scheduleUpdateChecksre-asks hourly. The timer is not the rate limit —checkForUpdate's 24h on-disk cache is, unchanged — so GitHub is still contactedat most once a day and
PADDOCK_NO_UPDATE_CHECK=1still disables everything. 23of every 24 ticks are a small local file read. Deliberately not a 24h timer: a
tick the same length as the cache window drifts against it, and landing a
millisecond early means waiting nearly two days.
The notice was one dim 10px line in
HostHeader, among other dim metadata,on the recorded reasoning that
paddock updateis "not an alarm". The reasoningwas right and the placement was wrong — a line the colour of its neighbours is
skipped, not read. Now
ReleaseBanner, and the old line is gone rather thankept beside it.
Dismissable, which no other banner here is, because of an asymmetry unique to
this one: you cannot act on it from the device you are reading it on.
paddock updateruns on the host, not the phone. A notice you can neither act onnor silence is how a dashboard teaches you to ignore banners — and the next one is
the connection banner, which does mean an agent's state may be wrong right now.
Dismissal is keyed by version, not a boolean, or the first dismissal would be
permanent and the feature would quietly stop existing.
HostHeaderloseslatestKnownas a prop. Its doc argued the prop was requiredso dropping it would be a type error — a good guard, but a required prop nothing
renders is the same trap pointing the other way. The guarantee moves to
App.tsx, the one caller with the value.Decision 16. 861 pass / 0 fail.