feat(manifest): U5 — booths that say who posted them and why
The index card showed a name, an item count and a countdown, and nothing
the poster chose. An agent with something to show therefore had no way to
make the booth say "look at this" and posted a URL to the link board
instead — which is why 145 of that board's 210 rows (69%) ended up
pointing at booths that had already been swept. The board was absorbing a
job it was never shaped for. This is the shape.
Each booth carries `.booth.json` — {handle, title, why, created} — written
by the CLI from $ALTHING_HANDLE, and the provenance line renders on both
index lanes and on the booth page header.
WHAT IS WHERE
- booth/manifest.py, stdlib-only and importing nothing from booth.* either:
scripts/booth imports it under the system python3 with no venv, and a
cross-import between two stdlib-only modules is a second way for that
invariant to break. It joins the shared test_stdlib_only list and keeps
a stricter copy of its own.
- The read is lenient and cannot raise. list_booths touches every booth on
every index load, so a manifest that cannot be parsed costs that booth's
provenance and nothing else. That is the v0.2.2 lesson applied before the
same mistake rather than after it.
- Absent and damaged render differently — `unannounced` and `unreadable`.
Folding "cannot be read" into "never said" would hide the one case
somebody has to go and fix.
- Re-announcing preserves `created`. A second `booth add` sharpening the
why is not a second appearance of the booth.
- The write is atomic (invariant 5); the temp file is itself a dotfile, so
no listing can see it mid-write.
THREE OPERATOR CALLS, 2026-09-22
Flags on the existing new/add verbs rather than a separate `announce` verb
(a second step is the step that gets forgotten, which is the rot's own
mechanism). Unannounced booths get a quiet marker rather than nothing — the
convention is only adoptable if the gap is visible. U5 adds provenance only
and does NOT add a second index ordering keyed on announcement time; that
is a different surface needing its own stated rule, parked for v1.1.
NO EXEMPTION LIST
A pickup booth and the standing link board are created by the service, so
they announce themselves with handle `booth`, which is true rather than
manufactured. One rule — a booth with no manifest is unannounced — instead
of a growing set of special cases.
ALSO
tests/test_booth.py's keep/release assertion was slicing the page on the
bare word `boothhead`, which has lived in the stylesheet far longer than
the assertion has; it was reading CSS and passing on luck, and went red the
first time a new rule landed above the old one. Same assertion, aimed at
the markup. A U5 test had the mirror-image bug: pytest derives tmp_path
from the test name and the index renders data_dir, so a test named
`test_an_unannounced_booth_says_so` put the needle in the haystack itself
and passed against a template that did not yet exist.
310 tests (304 before this unit's CLI half). Live service restarted, 26/26
booth pages verified 200, end-to-end smoke through the real CLI.
NOT TAGGED. The cold contract-review panel is still in flight and the
code-review and bug-hunt gates have not run. Tagging with a gate
outstanding is what made v0.2.0 premature.
This commit is contained in:
+75
-3
@@ -3,8 +3,10 @@
|
||||
# folder under $BOOTH_DATA_DIR; this is sugar over mkdir/cp so you get the URL
|
||||
# back.
|
||||
#
|
||||
# booth new <name> make an empty booth, print its URL
|
||||
# booth add <name> <file>... copy files into a booth (creates it), print URL
|
||||
# booth new <name> [--why W] [--title T]
|
||||
# make an empty booth, print its URL
|
||||
# booth add <name> <file>... [--why W] [--title T]
|
||||
# copy files into a booth (creates it), print URL
|
||||
# booth url <name> print a booth's URL
|
||||
# booth ls list booths (kept ones marked ★)
|
||||
# booth rm <name> wipe a booth now (TTL would eventually anyway)
|
||||
@@ -75,6 +77,18 @@
|
||||
# URLs that then drown in terminal scrollback. They go on a standing kept board
|
||||
# instead, with provenance, so they outlive the session that produced them.
|
||||
#
|
||||
# ANNOUNCE YOUR BOOTH. `--why` is one line saying what the operator is looking
|
||||
# at and why he should care; it lands on the index card and on the booth page
|
||||
# beside your handle, taken from $ALTHING_HANDLE. It is optional and nothing
|
||||
# breaks without it — but a booth that cannot say what it is has no way to ask
|
||||
# for attention except by posting its URL somewhere, which is exactly how the
|
||||
# link board came to be 69% dead rows. The booth is the place to say it.
|
||||
#
|
||||
# booth add r18-ab out/*.png --why "pick the denoiser, left column is v3"
|
||||
#
|
||||
# Re-announcing (a second `new` or `add` on the same booth) updates the why and
|
||||
# KEEPS the original creation stamp: the booth appeared once.
|
||||
#
|
||||
# On a host that is NOT nh3-dev, rsync into the data dir instead, e.g.:
|
||||
# rsync -a ./out/ nh3-dev:booth-data/my-run/
|
||||
set -euo pipefail
|
||||
@@ -85,23 +99,76 @@ KEEP=".forever" # must match KEEP_MARKER in b
|
||||
BLUR=".blurred" # one booth-relative item path per line; see `blur` below
|
||||
LINKS_BOARD="${BOOTH_LINKS_BOARD:-links}"
|
||||
|
||||
# `--why` / `--title` for `new` and `add`. Pulled out of "$@" wherever they
|
||||
# appear, so `booth add b *.png --why "..."` and `booth add b --why "..." *.png`
|
||||
# both work — a glob is usually last and a flag usually after it, but nothing
|
||||
# enforces that and a session should not have to care.
|
||||
WHY=""; TITLE=""; ARGS=()
|
||||
strip_announce_flags() {
|
||||
ARGS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--why) [ $# -ge 2 ] || usage; WHY="$2"; shift 2 ;;
|
||||
--title) [ $# -ge 2 ] || usage; TITLE="$2"; shift 2 ;;
|
||||
--why=*) WHY="${1#--why=}"; shift ;;
|
||||
--title=*) TITLE="${1#--title=}"; shift ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Announce a booth. Goes through booth/manifest.py rather than printf-ing JSON
|
||||
# from the shell, because a why containing a quote, a backslash or a newline is
|
||||
# not an edge case — it is a sentence somebody wrote.
|
||||
announce() {
|
||||
BOOTH_SRC="$(cd "$(dirname -- "$(readlink -f -- "$0")")/.." && pwd)" \
|
||||
BOOTH_ANN_DIR="$1" BOOTH_ANN_HANDLE="$2" \
|
||||
BOOTH_ANN_TITLE="${3:-}" BOOTH_ANN_WHY="${4:-}" python3 -c '
|
||||
import os, pathlib, sys
|
||||
sys.path.insert(0, os.environ["BOOTH_SRC"])
|
||||
try:
|
||||
from booth.manifest import write_manifest
|
||||
write_manifest(pathlib.Path(os.environ["BOOTH_ANN_DIR"]),
|
||||
os.environ["BOOTH_ANN_HANDLE"],
|
||||
title=os.environ["BOOTH_ANN_TITLE"],
|
||||
why=os.environ["BOOTH_ANN_WHY"])
|
||||
except Exception as exc:
|
||||
# A booth that could not announce itself is still a booth. Say so on stderr
|
||||
# and carry on: failing `booth add` over its metadata would lose the files
|
||||
# the session just copied, which is a far worse trade.
|
||||
print(f"booth: could not write the announcement: {exc}", file=sys.stderr)
|
||||
'
|
||||
}
|
||||
|
||||
# Who is posting. The same chain `link` uses for its rows, so provenance means
|
||||
# the same thing on the board and on the card.
|
||||
whoami_handle() {
|
||||
echo "${ALTHING_HANDLE:-${BOOTH_SOURCE:-$(hostname -s 2>/dev/null || echo unknown)}}"
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "usage: booth {new <name>|add <name> <file>...|url <name>|ls|rm <name>|keep <name>|unkeep <name>|blur <name> <file>...|unblur <name> <file>...|link <url> [description]|links|unlink <id|index>|ask <name> <id> <prompt> <option>... [--no-notes]|marks <name> [--wait [SECS]]|asks <name> (deprecated alias for marks)|answer <name> <id> [--wait [SECS]]|marks-import <name>}" >&2
|
||||
echo "usage: booth {new <name> [--why W] [--title T]|add <name> <file>... [--why W] [--title T]|url <name>|ls|rm <name>|keep <name>|unkeep <name>|blur <name> <file>...|unblur <name> <file>...|link <url> [description]|links|unlink <id|index>|ask <name> <id> <prompt> <option>... [--no-notes]|marks <name> [--wait [SECS]]|asks <name> (deprecated alias for marks)|answer <name> <id> [--wait [SECS]]|marks-import <name>}" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
new)
|
||||
strip_announce_flags "$@"
|
||||
set -- ${ARGS+"${ARGS[@]}"}
|
||||
[ $# -ge 1 ] || usage
|
||||
mkdir -p -- "$DATA/$1"
|
||||
announce "$DATA/$1" "$(whoami_handle)" "$TITLE" "$WHY"
|
||||
echo "$URL/b/$1/"
|
||||
;;
|
||||
add)
|
||||
strip_announce_flags "$@"
|
||||
set -- ${ARGS+"${ARGS[@]}"}
|
||||
[ $# -ge 2 ] || usage
|
||||
name="$1"; shift
|
||||
mkdir -p -- "$DATA/$name"
|
||||
cp -- "$@" "$DATA/$name/"
|
||||
announce "$DATA/$name" "$(whoami_handle)" "$TITLE" "$WHY"
|
||||
echo "$URL/b/$name/"
|
||||
;;
|
||||
url)
|
||||
@@ -177,6 +244,11 @@ case "$cmd" in
|
||||
board="$DATA/$LINKS_BOARD"
|
||||
mkdir -p -- "$board"
|
||||
: > "$board/$KEEP" # the board is durable by definition
|
||||
# The board announces itself as the SERVICE's, not as any one agent's:
|
||||
# seventeen handles post to it, so no handle owns it. Idempotent — a second
|
||||
# link keeps the original creation stamp.
|
||||
announce "$board" "booth" "$LINKS_BOARD" \
|
||||
"the standing link board — every agent session posts here"
|
||||
# Provenance, because a bare URL is unreadable three days later: who posted
|
||||
# it, from where, and when.
|
||||
who="${ALTHING_HANDLE:-${BOOTH_SOURCE:-$(hostname -s 2>/dev/null || echo unknown)}}"
|
||||
|
||||
Reference in New Issue
Block a user