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

roots

Directories scanned recursively for .git repositories. A directory holding .git is a repository and the scan stops descending into it.

excludes

Absolute repository paths dropped from the discovered set.

out_dir

Output directory for the file contracts. Defaults to the XDG data dir ~/.local/share/postup.

model

Optional Claude model name for postup enrich. Unset uses the claude CLI’s own default model.

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

data.json

The full portfolio snapshot. Carries a schema_version; an unknown version is rejected loudly on read.

commits-digest.md

Per-repository commit digest for later enrichment.

data-prev.json

The previous data.json, rotated before the new one is written, so it always holds the prior run — it feeds the since-last diff.

history.jsonl

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 claude is on PATH the 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 model setting is passed to claude only when set; otherwise the claude CLI’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.json is the one hard failure: the command reports that you must run postup collect first.

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_dir renders 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 / CommandEnrich through the composition root (one action, one implementation), and report the CommandResult message. 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 foreign Host header 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 under out_dir before 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 gh CLI for forge data. Its absence degrades per repository into errors rather than failing the run.

  • The claude CLI on PATH for postup enrich only. Its absence makes enrichment a no-op warning — postup collect and every deterministic surface are unaffected.

  • No extra needed for postup collect and the text brief — they run on the core-only buvis-gems install with zero tool-specific dependencies. The postup extra (Textual) is required only for postup tui: uv tool install buvis-gems[postup]. The postup-web extra (fastapi / uvicorn / watchfiles) is required only for postup serve: uv tool install buvis-gems[postup-web].