e9605df6ff
My smoke test used a refused port on a live host and presented it as the check for the 2 s budget. Measured: it returns in 79 ms, because a refused port answers instantly with an RST. It verifies the glyph and nothing about the deadline. An address that black-holes — an unrouted LAN host — takes 2.065 s and is the one that exercises it. Both are now in the page with what each actually proves, because letting the fast case imply the slow one is how a status line that hangs the prompt ships with a green test beside it. Found by forseti in their own copy of the same probe and confirmed here rather than adopted. The smoke test also moves from expected-values-in-trailing-comments to printing `got [x] wanted [y]` on one line. The comment form is the shape that produced the false pass that caught two agents inside an hour, and the expected-value column is what caught it — so the page should use the thing it recommends.
378 lines
20 KiB
Markdown
378 lines
20 KiB
Markdown
# althing deploy — six surfaces
|
|
|
|
**`scripts/deploy-althing.sh`** does all four. `--check` reports drift without
|
|
touching anything. This page is the why.
|
|
|
|
Deploying althing means updating **four independent surfaces on nh3-dev**. Three
|
|
were known; the fourth had no step in any runbook and drifted for five days
|
|
before anyone noticed, on 2026-09-01.
|
|
|
|
| # | surface | what it is | how it drifts |
|
|
|---|---|---|---|
|
|
| 1 | **binaries** | `uv tool install --force .` — the 7 console scripts | silently, see below |
|
|
| 2 | **herald** | `systemctl --user restart althing-po-herald` | new guards live here; nothing takes effect until it restarts |
|
|
| 3 | **skill** | `scripts/sync_skill.sh` → `~/.agents/skills/althing/SKILL.md` | covered by its own `--check` |
|
|
| 4 | **plugin** | repo `plugin/` → marketplace dir → Claude Code cache | **two hops, neither was automated** |
|
|
| 5 | **per-seat route** | `althing-route declare` on each seat | a channel change needs a re-declare; the plugin's SessionStart hook does it for CC seats |
|
|
| 6 | **`~/.claude/settings.json`** | `crossSessionInbound: "accept"` | **outside the althing repo entirely — no version can carry it** |
|
|
|
|
## ⚠ `uv tool install .` without `--force` is a silent no-op
|
|
|
|
$ uv tool install .
|
|
`althing-core @ file:///home/lkraven/development/althing` is already installed
|
|
$ echo $?
|
|
0
|
|
|
|
uv matches on the source **spec**, not its **contents**. On a box that already
|
|
installed from that path it declines and reports success. An operator following
|
|
a runbook literally would restart the herald, see everything green, and wonder
|
|
why the new binary was missing — with every command exiting 0.
|
|
|
|
## ⚠ Surface 4 is the one that bites, and it ate a hook
|
|
|
|
The chain is:
|
|
|
|
repo plugin/ → ~/.local/share/althing-plugin/ → ~/.claude/plugins/cache/althing/althing/<ver>/
|
|
^^^ nothing synced this hop
|
|
|
|
On 2026-09-01 the marketplace directory was a frozen copy from **Aug 28**:
|
|
|
|
deployed 0.0.1 hooks: ['UserPromptSubmit']
|
|
repo 0.1.1 hooks: ['UserPromptSubmit', 'SessionStart', 'SessionEnd']
|
|
+ scripts/pane-route.sh (absent from the deployment entirely)
|
|
|
|
So **"CC seats re-declare their pane route automatically at next SessionStart"
|
|
was never true on this box.** The hook existed and worked upstream; it was never
|
|
deployed. That is why every seat — including infra-ops' own Claude Code seat —
|
|
had to be hand-declared with a pid someone measured by hand, and why the
|
|
`idle_cursor` pin from 3.2.1 would not have self-applied either.
|
|
|
|
### ⚠ Compare the HOOK LIST, not the version string
|
|
|
|
A version number cannot tell you what a stale plugin actually cost you. 0.0.1 and
|
|
0.1.1 differ by **two hooks and a script**:
|
|
|
|
```bash
|
|
for d in ~/.claude/plugins/cache/althing/althing/*/; do
|
|
printf " %-8s hooks: %s pane-route.sh: %s\n" "$(basename "$d")" \
|
|
"$(python3 -c "import json;print(list(json.load(open('$d/.claude-plugin/plugin.json'))['hooks'].keys()))")" \
|
|
"$(test -f "$d/scripts/pane-route.sh" && echo yes || echo NO)"
|
|
done
|
|
```
|
|
|
|
Expect `['UserPromptSubmit', 'SessionStart', 'SessionEnd']` and `pane-route.sh: yes`.
|
|
Anything less and pane routes are not being declared or released automatically.
|
|
|
|
### ⚠ WHY THIS HID FOR FIVE DAYS — the shape to recognise
|
|
|
|
**A missing deploy surface does not present as an error. It presents as
|
|
"the migration needs manual work"** — and we had a ready explanation for that,
|
|
because four of five seats were non-Claude and genuinely did need hand-holding.
|
|
|
|
The seat that falsified the story was **infra-ops' own**: a Claude Code seat that
|
|
should have self-declared and didn't, and it looked exactly like the other four.
|
|
Neither infra-ops nor forseti asked why the automatic path had not fired for the
|
|
one seat it was supposed to work on.
|
|
|
|
⚠ **When a migration needs manual intervention, check whether the automatic path
|
|
was ever deployed before concluding it does not apply to your case.**
|
|
|
|
**Use the supported CLI for the second hop.** `claude plugin update althing`
|
|
(also `install` / `uninstall` / `list` / `details` / `validate` / `marketplace`).
|
|
⚠ Do **not** hand-edit `~/.claude/plugins/installed_plugins.json` or fabricate a
|
|
cache directory — that is Claude Code's own bookkeeping, and a subtle mistake
|
|
there breaks the plugin in a way that looks like an upstream bug.
|
|
|
|
⚠ **A Claude Code restart is required** for new plugin hooks. They are read at
|
|
session start; a running session keeps the old ones.
|
|
|
|
## Verifying a seat is actually reachable
|
|
|
|
`postbox status` reports `mode: push`, but until 3.2.4 a **failed** push
|
|
declaration was silent — `_declare_push` devnulled both streams with
|
|
`check=False`, so a missing binary, a non-zero exit and a server-side rejection
|
|
rendered identically as nothing. A seat could have been silently pull-only since
|
|
3.1.2.
|
|
|
|
Audit for "looks armed but is not", cross-referencing local waiter locks against
|
|
what the post office believes:
|
|
|
|
```bash
|
|
for f in ~/.althing/wake-listener-*.lock; do
|
|
h=$(basename "$f" .lock); h=${h#wake-listener-}
|
|
pid=$(cat "$f" 2>/dev/null)
|
|
[ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ] || continue
|
|
# IDENTITY, not liveness: a lock left by a reaped listener names a pid the
|
|
# kernel is free to hand to anything, so `kill -0` alone reports a stranger
|
|
# as a live waiter. Match the same cmdline segment `althing-listen --stop`
|
|
# requires before it will signal anything.
|
|
ours=0
|
|
while IFS= read -r -d '' seg; do [ "$seg" = "--_route=$h" ] && ours=1; done \
|
|
< "/proc/$pid/cmdline"
|
|
[ "$ours" = 1 ] || { printf " %-24s pid %-8s STRANGER (recycled pid)\n" "$h" "$pid"; continue; }
|
|
printf " %-24s waiter %-8s mode: %s\n" "$h" "$pid" \
|
|
"$(postbox status --handle "$h" 2>/dev/null | grep -oP 'mode: \K\w+')"
|
|
done
|
|
```
|
|
|
|
A live waiter reporting `mode: pull` is a seat that will never be poked. From
|
|
3.2.4 onward `$ALTHING_ROOT/listen.log` records failed declarations directly.
|
|
|
|
⚠ **Liveness is not identity, and this loop is the place that gets it wrong.**
|
|
The earlier `kill -0` form would print a phantom waiter for any handle whose
|
|
stale lock happens to name a recycled pid — and a phantom waiter is exactly what
|
|
sends a false "you are unreachable" notice to a seat that is fine.
|
|
`session_listener.sh` refuses to SIGTERM on liveness alone for precisely this
|
|
reason; an audit that only *reads* has no excuse for a weaker standard than the
|
|
one that *kills*. (Adopted 2026-09-02 after the regin-smithy-dev
|
|
cross-reference, where the pid happened to be genuine and the weaker check
|
|
happened to be right.)
|
|
|
|
⚠ **A stale `wake-listener-*.lock` is NOT a fault.** The gate is `flock -n` on an
|
|
open fd, which the kernel releases when the holder dies, so a lock file left by a
|
|
reaped listener is inert and exit 3 only fires against a genuinely live holder.
|
|
The pid in the file is read by `--stop` alone. (Recorded because infra-ops
|
|
claimed the opposite, untested, on 2026-09-01; forseti measured it.)
|
|
|
|
## ⚠ The plugin cache does not take a content-only change
|
|
|
|
`claude plugin update althing` matches on the version in `plugin.json` and
|
|
declines when it is unchanged. A release that edits hook or script CONTENT
|
|
without bumping the plugin version therefore leaves the Claude Code cache
|
|
stale while **every version check reports success** — the marketplace dir gets
|
|
the new bytes, the cache does not, and `update` says "already at the latest
|
|
version".
|
|
|
|
Measured on the 3.3.0 deploy (2026-09-02): marketplace and live cache both read
|
|
`0.1.1`, and `pane-route.sh` + `README.md` differed. That delta was
|
|
documentation-only and harmless. The next one need not be.
|
|
|
|
`scripts/deploy-althing.sh` now diffs the marketplace tree against the live
|
|
cache dir on every run and on `--check`, so the drift is reported rather than
|
|
inferred. **The fix is a plugin version bump upstream, not a hand-edit** —
|
|
`~/.claude/plugins/installed_plugins.json` and the cache directory are Claude
|
|
Code's own bookkeeping.
|
|
|
|
The 2026-09-01 lesson was *compare the hook list, not the version string*. This
|
|
is the same lesson one turn deeper: here the hook list was identical too, and
|
|
only the file contents moved.
|
|
|
|
## A seat already running when the plugin lands never auto-declares — leave it
|
|
|
|
The SessionStart hook fires at session start and nowhere else, so any session
|
|
that was already running when a plugin deploy landed keeps its old channel
|
|
until it restarts. After the 3.3.1 deploy that was **eight of seventeen live
|
|
seats**, all still on `fifo`.
|
|
|
|
**Operator ruling 2026-09-02: leave them.** They are reachable now, the post
|
|
office holds mail regardless, and each self-corrects at its next session start.
|
|
The residual exposure is that a fifo waiter is what Claude Code 2.1.257 reaps —
|
|
one seat was reaped three times in a morning — and a reap converts a healthy
|
|
seat into a silent one until the next restart. That costs timeliness, never
|
|
mail.
|
|
|
|
⚠ **Do not mass-message the affected seats.** Standing operator directive: no
|
|
unsolicited fleet notification, ever, unless asked. Direct correspondence with
|
|
a counterparty about work in hand is fine; sweeping the handle list is not.
|
|
Audit it, report the count, and let him decide — as he did here.
|
|
|
|
The one-liner, for a seat that asks or an operator who wants one moved:
|
|
|
|
althing-route declare --discover-pid
|
|
|
|
## ⚠ The `cc` channel is an undocumented interface, taken deliberately
|
|
|
|
From 3.3.0 a Claude Code seat is poked over its own message socket
|
|
(`$XDG_RUNTIME_DIR/cc-socks/<pid>.sock`) rather than by typing into its pane.
|
|
No process to reap, nothing near the input line, and delivery lands at the
|
|
receiver's next turn boundary.
|
|
|
|
It is not a published interface and **is expected to break on some future
|
|
Claude Code release**. Accepted on the operator's reasoning: the FIFO poker it
|
|
replaces was also an unsanctioned hack of the background-watcher system, so
|
|
this is a better instance of a dependency class we already had, not a new one.
|
|
|
|
When it breaks the failure mode is a seat going **pull-only with a logged
|
|
reason** — not lost mail; the post office holds it either way. Recovery is one
|
|
command:
|
|
|
|
althing-route declare --pid <pid> # prefers cc, falls back to pane
|
|
|
|
**Why cc is worth an undocumented dependency, in one measurement.** A pane poke
|
|
is typed into the session and submitted, so Claude Code records it in the
|
|
operator's prompt history exactly as if he had written it — 8 herald-written
|
|
entries accumulated across three projects, permanently in up-arrow recall. A cc
|
|
poke enters none: 2 delivered, 0 entries. Verified from the herald log against
|
|
the history on 2026-09-02. Channel sequence on one seat that day, all three in
|
|
order:
|
|
|
|
09-01 21:44 poked via fifo the old way, later reaped
|
|
09-02 08:34 could NOT reach via pane guard 4 refusing: he was typing
|
|
09-02 09:07 poked via pane typed into his input line
|
|
09-02 09:14 poked via cc arrived as a peer message
|
|
09-02 09:31 poked via cc after crossSessionInbound accept
|
|
|
|
⚠ **Count herald pokes by the whole display string, not by grepping
|
|
`ALTHING-HERALD`.** The substring matches the operator's own messages when he
|
|
pastes a hold notice into one, so the count runs high and the excess reads as
|
|
new activity. See the identity-not-resemblance note in the audit section.
|
|
|
|
⚠ **Ordering constraint at any upgrade that changes the channel set:** a
|
|
pre-3.3.0 herald refuses `channel=cc` at parse, so a seat that declares the new
|
|
channel before the herald restarts goes silently pull-only. `uv tool install
|
|
--force` then the herald restart, and only then let anything re-declare.
|
|
`deploy-althing.sh` already runs them in that order and updates the plugin
|
|
(whose SessionStart hook declares the route) last, which is what keeps the
|
|
window to a couple of seconds.
|
|
|
|
## ⚠ Surface 6 — `crossSessionInbound`, and it is not in the althing repo
|
|
|
|
**A cc poke to a default-configured seat is HELD, not delivered.** Claude Code
|
|
auto-delivers an inbound cross-session message only when the sender's
|
|
permission-mode class matches the receiver's, and **a sender that asserts no
|
|
class is held**. The herald is a daemon and asserts none, deliberately. What
|
|
the operator sees instead of a delivery:
|
|
|
|
Held peer message — from an unidentified session [verified pid <herald>];
|
|
preview: «ALTHING-HERALD althing: you have mail ...» — not delivered to
|
|
Claude (1 held). The sender did not attest its permission mode and this
|
|
session bypasses prompts.
|
|
|
|
Claude Code verified the herald through `SO_PEERCRED` and then correctly
|
|
declined to let it speak, because it would not say what it was.
|
|
|
|
**The fix is one key in `~/.claude/settings.json`:**
|
|
|
|
"crossSessionInbound": "accept"
|
|
|
|
Set on nh3-dev 2026-09-02. The operator's reasoning is the part to keep: *the
|
|
herald can only reach local seats, so bypass is the correct authorization type;
|
|
when the guard was not there, that was our default posture, including the pane
|
|
poke.* A pane poke types into a session and presses Enter — bypass-level access
|
|
by any measure, and what we had been doing all along. **The socket channel is
|
|
strictly narrower than what it replaces**, so accepting here states the existing
|
|
trust boundary rather than widening it.
|
|
|
|
⚠ **There is no attestation the herald could send instead.** Four probes
|
|
established that Claude Code identifies a sender by verified pid against the
|
|
session registry and reads that session's *live runtime* permission mode; a
|
|
daemon has none and the registry has no field for one. A `from_mode` field on a
|
|
`type:"user"` frame is not consulted at all — it belongs to the control actions.
|
|
Adding it would be shipping a field nothing reads.
|
|
|
|
**Cost without the setting is smaller than it looks:** the correspondent record
|
|
is in-memory session state, so the hold is **first-contact, not per-message** —
|
|
one approval per seat per session lifetime, and a long-lived pane pays it once.
|
|
|
|
⚠ **Why this surface is the dangerous one.** A seat without it is declared,
|
|
reachable and green, and its pokes go to a human instead of to the session.
|
|
That is the same shape as the SessionStart hook that was never deployed:
|
|
everything reports success, nothing arrives. `deploy-althing.sh` reads the key
|
|
and reports it on every run and on `--check` — and **deliberately never sets
|
|
it.** That file is the operator's permission configuration; a deploy script
|
|
that edits it is a deploy script granting itself trust.
|
|
|
|
## The status-line segment belongs to althing, not to this repo
|
|
|
|
From althing **3.4.0** the segment is a console script, `althing-statusline`,
|
|
installed on PATH by the same `uv tool install --force` as the other seven
|
|
executables. `scripts/claude-statusline-command.sh` calls it and does not
|
|
reimplement it:
|
|
|
|
althing=$(printf '%s' "$input" | timeout 5 althing-statusline 2>/dev/null)
|
|
|
|
⚠ **Keep that outer timeout LOOSER than the program's own 2 s budget.** If the
|
|
outer one fires first the segment comes back empty, which renders as "not an
|
|
althing directory" — the outage conflation, reintroduced by the guard meant to
|
|
prevent a hang.
|
|
|
|
**Why a console script rather than the `sync_skill.sh` canonical-file pattern:**
|
|
a script, unlike a document, has somewhere to be *installed*. Installing it makes
|
|
drift impossible instead of merely visible.
|
|
|
|
**Why it needed solving at all.** On 2026-09-02 the same twelve lines existed in
|
|
three places — althing's `plugin/scripts/statusline.sh`, the operator's wired
|
|
`~/.claude/statusline-command.sh`, and this repo's copy — and one audit found a
|
|
**different defect in each**. All three were fixed separately, by hand, the same
|
|
day. Two of the defects were in this repo's copy and are worth knowing because
|
|
they are both silent:
|
|
|
|
- the handle was resolved as the most recent launch in the directory, so a
|
|
directory hosting both a claude and a codex seat (`~/development/Worldtree`)
|
|
reports the **codex** handle's unread count and reachability to a Claude
|
|
session the moment the codex pane relaunches last;
|
|
- the post-office address was hardcoded, so it survives exactly until the post
|
|
office moves and then reports an outage that is really a stale constant.
|
|
|
|
### Deploying it
|
|
|
|
`scripts/claude-statusline-command.sh` is the tracked copy of what Claude Code
|
|
actually runs at `~/.claude/statusline-command.sh` (`settings.json` →
|
|
`statusLine.command`). Copies, not symlinks — same rule as `stacks/`.
|
|
|
|
diff -u scripts/claude-statusline-command.sh ~/.claude/statusline-command.sh
|
|
cp scripts/claude-statusline-command.sh ~/.claude/statusline-command.sh
|
|
|
|
⚠ **Diff before you copy, in both directions.** On 2026-09-02 the live path was
|
|
edited directly and was *ahead* of the tracked copy; a reflexive `cp` from the
|
|
repo would have destroyed the better version. The tracked copy is intent, the
|
|
live path is reality, and reality is sometimes right.
|
|
|
|
Smoke test — **strip the althing env vars, or you will test your own session's
|
|
identity instead of the resolution logic.** A hand-run inherits the exported
|
|
`ALTHING_HANDLE` from the session you run it in, so the seat's own directory and
|
|
a directory with no althing identity render identically and both look right.
|
|
That false pass caught two agents within one hour on 2026-09-02.
|
|
|
|
⚠ **Open question — whether Claude Code's own invocation inherits the
|
|
environment is NOT established, and nothing here depends on it.** An earlier
|
|
version of this page asserted a "bare shell"; that claim was never verified and
|
|
the evidence points the other way. Measured 2026-09-02: this box's Claude Code
|
|
process carries `ALTHING_HANDLE` and `ALTHING_POST_OFFICE` in its own `environ`
|
|
(inherited from the `dev-launch` shell), and a spawned child would normally
|
|
inherit both — observed independently on two seats. Whether Claude Code scrubs
|
|
the environment before spawning the status-line command was not observed either
|
|
way. **Write code that does not depend on the environment being present**, which
|
|
`althing-statusline` already does: environment first, launch history second, and
|
|
a seat started outside `dev-launch` has no handle in either place. Test with the
|
|
variables stripped regardless, because that is the harder case:
|
|
|
|
probe() { # $1 = cwd, $2 = what you expect
|
|
got=$(printf '{"workspace":{"current_dir":"%s"}}' "$1" \
|
|
| env -u ALTHING_HANDLE -u ALTHING_POST_OFFICE \
|
|
bash scripts/claude-statusline-command.sh)
|
|
printf 'got [%s] wanted [%s]\n' "$got" "$2"
|
|
}
|
|
probe /tmp '' # no althing identity
|
|
probe /home/lkraven/development/eshpfi-management '🔔 …'
|
|
|
|
⚠ **Print what you got beside what you wanted, on the same line.** Expected
|
|
values in trailing comments are what produced the false pass that caught two
|
|
agents inside an hour on 2026-09-02: `/tmp` rendered `🔔` and both of us read it
|
|
as "fine" rather than "that directory has no identity and should be empty." A
|
|
plausible value in the right shape is the hardest kind of wrong to see, and the
|
|
only thing that reliably catches it is the wanted value sitting on the same line
|
|
as the got value.
|
|
|
|
### ⚠ The outage probe: a refused port does NOT test the timeout
|
|
|
|
`📵` has two causes and they take different paths. Measured 2026-09-02 on
|
|
nh3-dev:
|
|
|
|
| address | elapsed | what it proves |
|
|
|---|---|---|
|
|
| `http://10.100.50.40:9999` — refused port on a **live** host | **0.079 s** | the glyph, and nothing about the deadline |
|
|
| `http://10.100.50.199:8390` — **unrouted** host, black-holes | **2.065 s** | the 2 s budget actually fires |
|
|
|
|
A refused port answers instantly with an RST, so the fast probe returns `📵`
|
|
without ever approaching the timeout. Use **both**, and know which is which —
|
|
letting the fast case imply the slow one is how a status line that hangs the
|
|
prompt ships with a green test beside it.
|
|
|
|
## Rollback
|
|
|
|
uv tool install althing-core==3.1.2
|
|
|
|
Routes written by later versions stay parseable — the old reader ignores unknown
|
|
keys — so nothing is stranded.
|