postup
POrtfolio STandUP — a portfolio standup gem. postup gathers
the state of every repository in your portfolio into typed, versioned file
contracts that the other postup interfaces consume. It runs fully without any
LLM.
This page documents PRD A (the gem scaffold and deterministic collector),
PRD B (optional LLM enrichment via the claude CLI), PRD F (the terminal
standup surfaces — the text brief and the Textual TUI), and PRD E (the
postup serve web UI).
Usage
postup collect # collect every configured repo, fetch first
postup collect --no-fetch # skip 'git fetch' (fast path, offline-ish)
postup collect --days 30 # narrow the commit window (default 60)
postup enrich # optional: add narrative/epics/todos via claude
postup # print the text standup (bare = brief)
postup brief # print the text standup (explicit)
postup tui # open the interactive Textual standup
postup collect discovers repositories, gathers their signals in parallel,
and writes four file contracts under the output directory. A repository whose
data cannot be gathered (gh unauthenticated, a network hiccup, an odd repo
state) degrades into that repository’s errors list and a console warning —
the run never crashes and always exits successfully as long as at least one
repository was discovered.
postup enrich is an optional second step that turns the collected data into
a model-authored epics.json. It is described under Enrichment below.
Configuration
Settings load from the buvis config stack, BUVIS_POSTUP_ environment
variables, and CLI flags (CLI > env > YAML > defaults).
# postup.yaml (in the buvis config stack, or passed via --config)
roots:
- ~/git/src # directories scanned for .git repositories
excludes:
- ~/git/src/github.com/me/scratch # repository paths to skip
out_dir: ~/.local/share/postup # where the contracts are written
model: null # optional Claude model for enrich
Setting |
Meaning |
|---|---|
|
Directories scanned recursively for |
|
Absolute repository paths dropped from the discovered set. |
|
Output directory for the file contracts. Defaults to the XDG data
dir |
|
Optional Claude model name for |
Repository discovery uses these roots directly — there is no dependency on
gita or any external registry.
File contracts
All four files are written atomically (tempfile + fsync +
os.replace) under out_dir:
File |
Contents |
|---|---|
|
The full portfolio snapshot. Carries a
|
|
Per-repository commit digest for later enrichment. |
|
The previous |
|
One appended summary line per run, feeding the trend view. |
Per-repository signals
For each repository postup collect gathers commits, releases / last tag /
unreleased-commit count, open issues and pull requests, CI runs, security
alerts, stray branches and worktrees, the PRD pipeline counts under
dev/local/prds, the CHANGELOG [Unreleased] state, brush-hygiene
recency, local working-tree state (branch / dirty / ahead-behind / stashes),
and portfolio-external PRs (review-requested and authored) via an authenticated
gh CLI plus local git.
Enrichment
postup enrich is a strictly optional step that shells out to the claude
CLI to add the model-authored portion of the brief — a manager-voice narrative
summary, per-repository epics grouping commits by theme, and a handful of
judgment todos — writing them to epics.json beside the collected contracts.
postup enrich # reads data.json + commits-digest.md, writes epics.json
Behaviour:
Availability is detected before any prompt is built. When
claudeis onPATHthe run announces which mode and model it will use; when it is absent the run warns that quality suffers, continues, and exits successfully — enrichment never blocks the deterministic brief.Model — the
modelsetting is passed toclaudeonly when set; otherwise theclaudeCLI’s own default model is used. postup never pins a model.Validation — the model’s JSON is validated against the epics schema, and every epic commit SHA is cross-checked against the collected commit set so a hallucinated reference is rejected, not written. On a parse or validation failure the command retries exactly once with the errors appended to the prompt; a second failure warns and continues without writing a partial file.
Stable ids — each judgment todo’s id is derived deterministically from its repository and action, so done-state tracking survives re-enrichment.
Stale-input guard — a missing
data.jsonis the one hard failure: the command reports that you must runpostup collectfirst.
epics.json carries its own schema_version, a summary string, a
repos map of per-repo epics (each with a title, summary, and exact
shas), and a todos list (id, repo, urgency, action,
why, plus optional importance and effort). It is written atomically
alongside the collector’s contracts. No cloud AI SDK and no extra Python
dependency are involved — the claude binary on PATH is the entire LLM
integration.
Terminal standup
postup renders a deterministic terminal standup on two surfaces — a plain
text brief and an interactive Textual TUI — both driven by one Python derive
layer (postup.domain.derive), so the CLI and TUI show identical facts for the
same data (the all-interface rule). The derive layer is pure and UI-free: it
imports neither Textual nor Click, reads only the file contracts, and never
raises on missing inputs.
Text brief (the default)
Bare postup (and the explicit postup brief) print the standup from the
latest data.json:
postup # bare postup == the text brief
postup brief # the same surface, explicit
The brief shows a ranked attention queue (failing CI, security alerts, open
PRs, long-dirty checkouts, idle WIP PRDs, and portfolio-external review
requests), mechanical todos derived deterministically from the signals (cut
a release, fix failing CI, prune merged branches), a per-repo summary row,
and the since-last diff against the previous run. Judgment todos and the
narrative summary appear only when epics.json exists (i.e. after
postup enrich); otherwise a one-line not enriched cue is shown. This path
imports no Textual and runs on the core-only install — enforced by an
import-isolation test, not convention. A missing data.json prints a friendly
run postup collect first message, never a traceback.
Textual TUI
postup tui opens the interactive standup — attention queue, todos, and repo
list — as a read-only view over the same derive output as the text brief:
postup tui # requires the 'postup' extra (Textual)
The TUI needs the postup extra (Textual). When it is absent the command
reports a standardized install hint (uv tool install buvis-gems[postup])
rather than a traceback. Its layout is gated by Textual snapshot tests that run
only on the canonical CI env (Linux + Python 3.12) and are auto-skipped
elsewhere; regenerate baselines via the update-snapshots GitHub workflow.
Web UI
postup serve starts a FastAPI web server that delivers the SvelteKit brief
UI over localhost — the only web delivery path for postup. It serves the
committed production build and the live payload data, and pushes refreshes over
Server-Sent Events so an open browser updates without a manual reload.
postup serve # bind 127.0.0.1:8000, open the browser
postup serve -p 9000 # a different port
postup serve -H 0.0.0.0 # bind a non-loopback interface (see below)
postup serve --no-browser # do not open the browser on start
Behaviour:
Serves the last collected data until refreshed — starting the server does not auto-run a collect (no startup latency, no surprise network calls). A never-collected
out_dirrenders the UI’s explicit empty portfolio state rather than erroring. Use the in-UI collect / enrich triggers, or run the CLI commands, to populate or refresh the data; the browser picks up the change over SSE.Triggers run the same command classes as the CLI — the UI’s collect and enrich buttons drive
CommandCollect/CommandEnrichthrough the composition root (one action, one implementation), and report theCommandResultmessage. A trigger sent while a run is already active is rejected with an already running status rather than starting a second run.Confinement (the 00042 posture) — the server binds localhost by default, installs
TrustedHostMiddleware(a foreignHostheader is rejected), guards the mutating trigger routes with a per-process auth token (injected into the page on loopback), and resolves every request-derived filesystem path underout_dirbefore reading it. Binding a non-loopback host (-H 0.0.0.0) widens the allowed hosts and prints the auth token to the console with a warning that any host reaching the port can read the data without it.
postup serve needs the postup-web extra (fastapi / uvicorn /
watchfiles). When it is absent the command reports a standardized install hint
(uv tool install buvis-gems[postup-web]) rather than a traceback.
An authenticated
ghCLI for forge data. Its absence degrades per repository intoerrorsrather than failing the run.The
claudeCLI onPATHforpostup enrichonly. Its absence makes enrichment a no-op warning —postup collectand every deterministic surface are unaffected.No extra needed for
postup collectand the text brief — they run on the core-onlybuvis-gemsinstall with zero tool-specific dependencies. Thepostupextra (Textual) is required only forpostup tui:uv tool install buvis-gems[postup]. Thepostup-webextra (fastapi / uvicorn / watchfiles) is required only forpostup serve:uv tool install buvis-gems[postup-web].